diff --git a/pkg/storage/chunked.go b/pkg/storage/chunked.go index be411e66..2dcff5dc 100644 --- a/pkg/storage/chunked.go +++ b/pkg/storage/chunked.go @@ -427,6 +427,9 @@ func (c *chunkedSecrets) decodeRelease(ctx context.Context, indexSecret *corev1. } }() + // Close the read side when decoding returns so that the writer goroutine + // does not block forever if decoding stops before all chunk data is read. + defer pr.Close() gzr, err := gzip.NewReader(pr) if err != nil { return nil, fmt.Errorf("failed to create gzip reader: %w", err) diff --git a/pkg/storage/chunked_test.go b/pkg/storage/chunked_test.go index d8bd0c12..e99901c6 100644 --- a/pkg/storage/chunked_test.go +++ b/pkg/storage/chunked_test.go @@ -5,6 +5,7 @@ import ( "encoding/json" "fmt" "maps" + "runtime" . "github.com/onsi/ginkgo/v2" . "github.com/onsi/gomega" @@ -95,6 +96,27 @@ var _ = Describe("chunkedSecrets", func() { _, err := maxReadDriver.Get(releaseKey(rel)) Expect(err).To(MatchError(ContainSubstring("release too large"))) }) + It("should not leak goroutines if the release fails to decode", func() { + corruptIndex := &corev1.Secret{ + ObjectMeta: metav1.ObjectMeta{Name: "corrupt-release.v1"}, + Type: SecretTypeChunkedIndex, + Data: map[string][]byte{ + "extraChunks": []byte("[]"), + // Larger than the gzip reader's read buffer, so the + // reader stops before consuming all of the chunk data. + "chunk": make([]byte, 10*chunkSize), + }, + } + _, err := secretInterface.Create(context.Background(), corruptIndex, metav1.CreateOptions{}) + Expect(err).ToNot(HaveOccurred()) + + goroutinesBefore := runtime.NumGoroutine() + for range 5 { + _, err := chunkedDriver.Get(corruptIndex.Name) + Expect(err).To(MatchError(ContainSubstring("failed to create gzip reader"))) + } + Eventually(runtime.NumGoroutine).Should(BeNumerically("<=", goroutinesBefore)) + }) }) var _ = Describe("Update", func() {