From 601445e88cf1fe66b2885c793d564030aad1d707 Mon Sep 17 00:00:00 2001 From: Dean Chen <862469039@qq.com> Date: Sat, 8 Aug 2026 11:46:06 +0500 Subject: [PATCH 1/2] fix(loader): do not drop values files ending at a 4096-byte boundary YAMLReader can return EOF without yielding the last line when that line has no trailing newline and its length is a multiple of bufio's default buffer. Read the file fully and ensure a trailing newline before parsing so compact JSON values files are not silently ignored. Fixes #32506 Signed-off-by: Dean Chen <862469039@qq.com> --- internal/chart/v3/loader/load.go | 14 +++++++++++++- internal/chart/v3/loader/load_test.go | 23 +++++++++++++++++++++++ pkg/chart/v2/loader/load.go | 14 +++++++++++++- pkg/chart/v2/loader/load_test.go | 23 +++++++++++++++++++++++ 4 files changed, 72 insertions(+), 2 deletions(-) diff --git a/internal/chart/v3/loader/load.go b/internal/chart/v3/loader/load.go index 48f346ccf..0ff040eae 100644 --- a/internal/chart/v3/loader/load.go +++ b/internal/chart/v3/loader/load.go @@ -183,8 +183,20 @@ func LoadFiles(files []*archive.BufferedFile) (*chart.Chart, error) { // The reader is expected to contain one or more YAML documents, the values of which are merged. // And the values can be either a chart's default values or user-supplied values. func LoadValues(data io.Reader) (map[string]any, error) { + // Read fully first. YAMLReader/LineReader can drop a final unterminated + // line when its length is an exact multiple of bufio.Reader's default + // buffer (4096). Appending a trailing newline avoids that case. + // See https://github.com/helm/helm/issues/32506 + b, err := io.ReadAll(data) + if err != nil { + return nil, err + } + if len(b) > 0 && b[len(b)-1] != '\n' { + b = append(b, '\n') + } + values := map[string]any{} - reader := utilyaml.NewYAMLReader(bufio.NewReader(data)) + reader := utilyaml.NewYAMLReader(bufio.NewReader(bytes.NewReader(b))) for { currentMap := map[string]any{} raw, err := reader.Read() diff --git a/internal/chart/v3/loader/load_test.go b/internal/chart/v3/loader/load_test.go index dd5fdc8db..f105b248f 100644 --- a/internal/chart/v3/loader/load_test.go +++ b/internal/chart/v3/loader/load_test.go @@ -419,6 +419,29 @@ foo: } } + +func TestLoadValuesEOFBoundary(t *testing.T) { + // Reproduces #32506: a single logical line whose length is a multiple of + // bufio's default buffer (4096) and has no trailing newline used to be + // dropped entirely by YAMLReader, yielding empty values. + prefix := []byte(`{"foo":"`) + suffix := []byte(`"}`) + pad := 4096 - len(prefix) - len(suffix) + data := make([]byte, 0, 4096) + data = append(data, prefix...) + data = append(data, bytes.Repeat([]byte("x"), pad)...) + data = append(data, suffix...) + if len(data) != 4096 { + t.Fatalf("test setup: want data length 4096, got %d", len(data)) + } + + values, err := LoadValues(bytes.NewReader(data)) + require.NoError(t, err) + assert.Equal(t, map[string]any{ + "foo": string(bytes.Repeat([]byte("x"), pad)), + }, values) +} + func TestMergeValuesV3(t *testing.T) { nestedMap := map[string]any{ "foo": "bar", diff --git a/pkg/chart/v2/loader/load.go b/pkg/chart/v2/loader/load.go index 28115d062..d7b125b9b 100644 --- a/pkg/chart/v2/loader/load.go +++ b/pkg/chart/v2/loader/load.go @@ -210,8 +210,20 @@ func LoadFiles(files []*archive.BufferedFile) (*chart.Chart, error) { // The reader is expected to contain one or more YAML documents, the values of which are merged. // And the values can be either a chart's default values or user-supplied values. func LoadValues(data io.Reader) (map[string]any, error) { + // Read fully first. YAMLReader/LineReader can drop a final unterminated + // line when its length is an exact multiple of bufio.Reader's default + // buffer (4096). Appending a trailing newline avoids that case. + // See https://github.com/helm/helm/issues/32506 + b, err := io.ReadAll(data) + if err != nil { + return nil, err + } + if len(b) > 0 && b[len(b)-1] != '\n' { + b = append(b, '\n') + } + values := map[string]any{} - reader := utilyaml.NewYAMLReader(bufio.NewReader(data)) + reader := utilyaml.NewYAMLReader(bufio.NewReader(bytes.NewReader(b))) for { currentMap := map[string]any{} raw, err := reader.Read() diff --git a/pkg/chart/v2/loader/load_test.go b/pkg/chart/v2/loader/load_test.go index 8bf254321..b4836ee95 100644 --- a/pkg/chart/v2/loader/load_test.go +++ b/pkg/chart/v2/loader/load_test.go @@ -463,6 +463,29 @@ foo: } } + +func TestLoadValuesEOFBoundary(t *testing.T) { + // Reproduces #32506: a single logical line whose length is a multiple of + // bufio's default buffer (4096) and has no trailing newline used to be + // dropped entirely by YAMLReader, yielding empty values. + prefix := []byte(`{"foo":"`) + suffix := []byte(`"}`) + pad := 4096 - len(prefix) - len(suffix) + data := make([]byte, 0, 4096) + data = append(data, prefix...) + data = append(data, bytes.Repeat([]byte("x"), pad)...) + data = append(data, suffix...) + if len(data) != 4096 { + t.Fatalf("test setup: want data length 4096, got %d", len(data)) + } + + values, err := LoadValues(bytes.NewReader(data)) + require.NoError(t, err) + assert.Equal(t, map[string]any{ + "foo": string(bytes.Repeat([]byte("x"), pad)), + }, values) +} + func TestMergeValuesV2(t *testing.T) { nestedMap := map[string]any{ "foo": "bar", From fd4ed4952b5f9f54344a0c014f7acd7885bd60c6 Mon Sep 17 00:00:00 2001 From: Dean Chen <862469039@qq.com> Date: Sat, 8 Aug 2026 19:45:24 +0500 Subject: [PATCH 2/2] test(loader): cover 8192-byte EOF boundary for LoadValues Also drop the extra blank line before the test. Addresses review on #32525. Signed-off-by: Dean Chen <862469039@qq.com> --- internal/chart/v3/loader/load_test.go | 37 +++++++++++++++------------ pkg/chart/v2/loader/load_test.go | 37 +++++++++++++++------------ 2 files changed, 42 insertions(+), 32 deletions(-) diff --git a/internal/chart/v3/loader/load_test.go b/internal/chart/v3/loader/load_test.go index f105b248f..e34364f39 100644 --- a/internal/chart/v3/loader/load_test.go +++ b/internal/chart/v3/loader/load_test.go @@ -21,6 +21,7 @@ import ( "bytes" "compress/gzip" "errors" + "fmt" "io" "log" "os" @@ -419,27 +420,31 @@ foo: } } - func TestLoadValuesEOFBoundary(t *testing.T) { // Reproduces #32506: a single logical line whose length is a multiple of // bufio's default buffer (4096) and has no trailing newline used to be // dropped entirely by YAMLReader, yielding empty values. - prefix := []byte(`{"foo":"`) - suffix := []byte(`"}`) - pad := 4096 - len(prefix) - len(suffix) - data := make([]byte, 0, 4096) - data = append(data, prefix...) - data = append(data, bytes.Repeat([]byte("x"), pad)...) - data = append(data, suffix...) - if len(data) != 4096 { - t.Fatalf("test setup: want data length 4096, got %d", len(data)) - } + // Also cover 8192 (2x buffer) so we do not only hit the single-buffer case. + for _, size := range []int{4096, 8192} { + t.Run(fmt.Sprintf("size_%d", size), func(t *testing.T) { + prefix := []byte(`{"foo":"`) + suffix := []byte(`"}`) + pad := size - len(prefix) - len(suffix) + data := make([]byte, 0, size) + data = append(data, prefix...) + data = append(data, bytes.Repeat([]byte("x"), pad)...) + data = append(data, suffix...) + if len(data) != size { + t.Fatalf("test setup: want data length %d, got %d", size, len(data)) + } - values, err := LoadValues(bytes.NewReader(data)) - require.NoError(t, err) - assert.Equal(t, map[string]any{ - "foo": string(bytes.Repeat([]byte("x"), pad)), - }, values) + values, err := LoadValues(bytes.NewReader(data)) + require.NoError(t, err) + assert.Equal(t, map[string]any{ + "foo": string(bytes.Repeat([]byte("x"), pad)), + }, values) + }) + } } func TestMergeValuesV3(t *testing.T) { diff --git a/pkg/chart/v2/loader/load_test.go b/pkg/chart/v2/loader/load_test.go index b4836ee95..ae1da1130 100644 --- a/pkg/chart/v2/loader/load_test.go +++ b/pkg/chart/v2/loader/load_test.go @@ -21,6 +21,7 @@ import ( "bytes" "compress/gzip" "errors" + "fmt" "io" "log" "os" @@ -463,27 +464,31 @@ foo: } } - func TestLoadValuesEOFBoundary(t *testing.T) { // Reproduces #32506: a single logical line whose length is a multiple of // bufio's default buffer (4096) and has no trailing newline used to be // dropped entirely by YAMLReader, yielding empty values. - prefix := []byte(`{"foo":"`) - suffix := []byte(`"}`) - pad := 4096 - len(prefix) - len(suffix) - data := make([]byte, 0, 4096) - data = append(data, prefix...) - data = append(data, bytes.Repeat([]byte("x"), pad)...) - data = append(data, suffix...) - if len(data) != 4096 { - t.Fatalf("test setup: want data length 4096, got %d", len(data)) - } + // Also cover 8192 (2x buffer) so we do not only hit the single-buffer case. + for _, size := range []int{4096, 8192} { + t.Run(fmt.Sprintf("size_%d", size), func(t *testing.T) { + prefix := []byte(`{"foo":"`) + suffix := []byte(`"}`) + pad := size - len(prefix) - len(suffix) + data := make([]byte, 0, size) + data = append(data, prefix...) + data = append(data, bytes.Repeat([]byte("x"), pad)...) + data = append(data, suffix...) + if len(data) != size { + t.Fatalf("test setup: want data length %d, got %d", size, len(data)) + } - values, err := LoadValues(bytes.NewReader(data)) - require.NoError(t, err) - assert.Equal(t, map[string]any{ - "foo": string(bytes.Repeat([]byte("x"), pad)), - }, values) + values, err := LoadValues(bytes.NewReader(data)) + require.NoError(t, err) + assert.Equal(t, map[string]any{ + "foo": string(bytes.Repeat([]byte("x"), pad)), + }, values) + }) + } } func TestMergeValuesV2(t *testing.T) {