From 5e87f7dad440470e0914b19eef12858aef556785 Mon Sep 17 00:00:00 2001 From: Max Freedom Pollard <272618364+MaxFreedomPollard@users.noreply.github.com> Date: Sat, 5 Sep 2026 21:48:26 -0400 Subject: [PATCH] fix(chart): format the v3 gzip extra field as an RFC 1952 subfield internal/chart/v3/util/save.go assigned gzip.Writer.Extra the 41-byte string "+aHR0cHM6Ly95b3V0dS5iZS96OVV6MWljandyTQo=", which is not a valid FEXTRA payload. RFC 1952 section 2.3.1.1 requires every subfield to be SI1, SI2, a 2-byte little-endian LEN, and then LEN bytes of data. Parsed that way those bytes give SI1='+', SI2='a' and LEN=21064 while only 37 bytes remain, so strict readers reject every archive the v3 writer produces. Go's compress/gzip does not parse subfields, so nothing in the tree noticed. pkg/chart/v2/util/save.go had the identical defect. It was fixed in #31884 for issue #31844, "helm package produces malformed .tgz", where the Bazel downloader failed with "Extra subfield lenght exceeds remaining bytes in extra: 21064 > 37". The v3 writer was never updated. Use the same header bytes the v2 writer now uses: SI1 and SI2 of 'r', LEN 0x0028, and the unchanged 40-byte payload. Both writers now emit byte-identical, well-formed extra fields. Adds the v2 regression test to the v3 package. Signed-off-by: Max Freedom Pollard <272618364+MaxFreedomPollard@users.noreply.github.com> --- internal/chart/v3/util/save.go | 6 ++++- internal/chart/v3/util/save_test.go | 36 +++++++++++++++++++++++++++++ 2 files changed, 41 insertions(+), 1 deletion(-) diff --git a/internal/chart/v3/util/save.go b/internal/chart/v3/util/save.go index 09235c1ea..3b91bcb7a 100644 --- a/internal/chart/v3/util/save.go +++ b/internal/chart/v3/util/save.go @@ -33,7 +33,11 @@ import ( "helm.sh/helm/v4/pkg/chart/common" ) -var headerBytes = []byte("+aHR0cHM6Ly95b3V0dS5iZS96OVV6MWljandyTQo=") +// RFC 1952 subfield header: +// +---+---+---+---+==================================+ +// |SI1|SI2| LEN |... LEN bytes of subfield data ...| +// +---+---+---+---+==================================+ +var headerBytes = []byte("rr\x28\x00aHR0cHM6Ly95b3V0dS5iZS96OVV6MWljandyTQo=") // SaveDir saves a chart as files in a directory. // diff --git a/internal/chart/v3/util/save_test.go b/internal/chart/v3/util/save_test.go index 00f2c5cf4..f0ff46077 100644 --- a/internal/chart/v3/util/save_test.go +++ b/internal/chart/v3/util/save_test.go @@ -21,6 +21,7 @@ import ( "bytes" "compress/gzip" "crypto/sha256" + "encoding/binary" "encoding/hex" "errors" "io" @@ -108,6 +109,41 @@ func TestSave(t *testing.T) { require.Error(t, err, "Expected error saving chart with invalid name") } +func TestSavedGzipExtraFieldIsValid(t *testing.T) { + tmp := t.TempDir() + c := &chart.Chart{ + Metadata: &chart.Metadata{ + APIVersion: chart.APIVersionV3, + Name: "ahab", + Version: "1.2.3", + }, + } + + where, err := Save(c, tmp) + require.NoError(t, err, "Failed to save") + + f, err := os.Open(where) + require.NoError(t, err, "Failed to open saved file") + defer f.Close() + + r, err := gzip.NewReader(f) + require.NoError(t, err, "Failed to create gzip reader") + defer r.Close() + + // RFC 1952 ยง2.3.1.1: + // Each subfield consists of SI1, SI2 (1 byte each), + // a 2-byte little-endian LEN, and LEN bytes of data. + // https://www.rfc-editor.org/rfc/rfc1952.html#page-8 + extra := r.Extra + + require.NotEmpty(t, extra) + require.GreaterOrEqual(t, len(extra), 4) + + dataLen := int(binary.LittleEndian.Uint16(extra[2:4])) + // Assume a single subfield. + require.Lenf(t, extra, 4+dataLen, "gzip extra field has malformed subfield: LEN=%d but %d data byte(s) follow the subfield header", dataLen, len(extra)-4) +} + // Creates a copy with a different schema; does not modify anything. func withSchema(chart chart.Chart, schema []byte) chart.Chart { chart.Schema = schema