From c6ddeb2ff8861c09899485ba4718cfbadba29648 Mon Sep 17 00:00:00 2001 From: manon Date: Tue, 8 Sep 2026 16:23:04 +0900 Subject: [PATCH] fix(loader): accept a chart archive that exactly fills the size budget LoadArchiveFiles checks the aggregate decompression budget four times, and three of them are strict: hd.Size > remainingSize in this same loop, and both comparisons in BudgetedReader, which enforces the identical MaxDecompressedChartSize for directory loads. Only the post-copy check used remainingSize <= 0, so a chart whose contents exactly fill the budget drove remainingSize to zero and was rejected with "decompressed chart is larger than the maximum size" - a claim that is false, since it is equal. The same chart therefore loaded from a directory and failed from a .tgz. Note that after this change the clause is unreachable: io.LimitReader caps bytesWritten at remainingSize, so remainingSize can never go below zero. It is kept as a guard in case that changes. Signed-off-by: manon --- pkg/chart/loader/archive/archive.go | 2 +- pkg/chart/loader/archive/archive_test.go | 47 ++++++++++++++++++++++++ 2 files changed, 48 insertions(+), 1 deletion(-) diff --git a/pkg/chart/loader/archive/archive.go b/pkg/chart/loader/archive/archive.go index 633871bcc..e3c326b40 100644 --- a/pkg/chart/loader/archive/archive.go +++ b/pkg/chart/loader/archive/archive.go @@ -142,7 +142,7 @@ func LoadArchiveFiles(in io.Reader) ([]*BufferedFile, error) { // copying early. Here we report that error. This is important if the last file extracted // is the one that goes over the limit. It assumes the Size stored in the tar header // is correct, something many applications do. - if bytesWritten < hd.Size || remainingSize <= 0 { + if bytesWritten < hd.Size || remainingSize < 0 { return nil, fmt.Errorf("decompressed chart is larger than the maximum size %d", MaxDecompressedChartSize) } diff --git a/pkg/chart/loader/archive/archive_test.go b/pkg/chart/loader/archive/archive_test.go index 3c3d336c3..99dfa0b00 100644 --- a/pkg/chart/loader/archive/archive_test.go +++ b/pkg/chart/loader/archive/archive_test.go @@ -79,3 +79,50 @@ func TestLoadArchiveFiles(t *testing.T) { }) } } + +func TestLoadArchiveFilesAtMaxDecompressedSize(t *testing.T) { + const content = "apiVersion: v2\nname: mychart\nversion: 0.1.0\n" + + archiveOf := func(t *testing.T) *bytes.Buffer { + t.Helper() + buf := &bytes.Buffer{} + gzw := gzip.NewWriter(buf) + tw := tar.NewWriter(gzw) + require.NoError(t, tw.WriteHeader(&tar.Header{ + Typeflag: tar.TypeReg, + Name: "mychart/Chart.yaml", + Size: int64(len(content)), + })) + _, err := tw.Write([]byte(content)) + require.NoError(t, err) + require.NoError(t, tw.Close()) + require.NoError(t, gzw.Close()) + return buf + } + + orig := MaxDecompressedChartSize + t.Cleanup(func() { MaxDecompressedChartSize = orig }) + + for _, tc := range []struct { + name string + budget int64 + wantErr bool + }{ + {"one byte over the chart size", int64(len(content)) + 1, false}, + // A chart that exactly fills the budget is at the limit, not over it, + // and BudgetedReader accepts it for directory loads. + {"exactly the chart size", int64(len(content)), false}, + {"one byte under the chart size", int64(len(content)) - 1, true}, + } { + t.Run(tc.name, func(t *testing.T) { + MaxDecompressedChartSize = tc.budget + files, err := LoadArchiveFiles(archiveOf(t)) + if tc.wantErr { + require.Error(t, err) + return + } + require.NoError(t, err) + require.Len(t, files, 1) + }) + } +}