From e0a2b9e26f0e89bae5fc465279e7630c19a24f44 Mon Sep 17 00:00:00 2001 From: Vimal Kumar Yadav <174954713+vimalyad@users.noreply.github.com> Date: Wed, 30 Sep 2026 13:27:18 +0530 Subject: [PATCH] fix(loader): skip broken symlinks matched by .helmignore A dangling symlink in a chart directory made loading fail, even when a .helmignore rule excluded it. The walker returned an error from symwalk before the loader could apply its ignore rules, so the entry was never checked against .helmignore. Pass the symlink resolution error to the walk callback instead of returning it directly. In loadDir, if the entry is a symlink that cannot be resolved and a .helmignore rule matches it, skip it. A broken symlink that is not ignored still fails with "error evaluating symlink", as before. Apply the change to both the v2 and v3 chart loaders, and add a test covering the ignored and non-ignored cases. Signed-off-by: Vimal Kumar Yadav <174954713+vimalyad@users.noreply.github.com> --- internal/chart/v3/loader/directory.go | 5 +++++ internal/sympath/walk.go | 2 +- pkg/chart/v2/loader/directory.go | 5 +++++ pkg/chart/v2/loader/load_test.go | 30 +++++++++++++++++++++++++++ 4 files changed, 41 insertions(+), 1 deletion(-) diff --git a/internal/chart/v3/loader/directory.go b/internal/chart/v3/loader/directory.go index 535468dd1..bb8c44595 100644 --- a/internal/chart/v3/loader/directory.go +++ b/internal/chart/v3/loader/directory.go @@ -82,6 +82,11 @@ func loadDir(dir string, budget int64) (*chart.Chart, error) { n = filepath.ToSlash(n) if err != nil { + // A symlink that cannot be resolved is skipped only if a + // .helmignore rule matches it. + if fi != nil && sympath.IsSymlink(fi) && rules.Ignore(n, fi) { + return nil + } return err } if fi.IsDir() { diff --git a/internal/sympath/walk.go b/internal/sympath/walk.go index b54b97ad4..7f5dfe721 100644 --- a/internal/sympath/walk.go +++ b/internal/sympath/walk.go @@ -69,7 +69,7 @@ func symwalk(path string, info os.FileInfo, walkFn filepath.WalkFunc) error { if IsSymlink(info) { resolved, err := filepath.EvalSymlinks(path) if err != nil { - return fmt.Errorf("error evaluating symlink %s: %w", path, err) + return walkFn(path, info, fmt.Errorf("error evaluating symlink %s: %w", path, err)) } // This log message is to highlight a symlink that is being used within a chart, symlinks can be used for nefarious reasons. slog.Info("found symbolic link in path. Contents of linked file included and used", "path", path, "resolved", resolved) diff --git a/pkg/chart/v2/loader/directory.go b/pkg/chart/v2/loader/directory.go index bfc649f2a..92250d700 100644 --- a/pkg/chart/v2/loader/directory.go +++ b/pkg/chart/v2/loader/directory.go @@ -82,6 +82,11 @@ func loadDir(dir string, budget int64) (*chart.Chart, error) { n = filepath.ToSlash(n) if err != nil { + // A symlink that cannot be resolved is skipped only if a + // .helmignore rule matches it. + if fi != nil && sympath.IsSymlink(fi) && rules.Ignore(n, fi) { + return nil + } return err } if fi.IsDir() { diff --git a/pkg/chart/v2/loader/load_test.go b/pkg/chart/v2/loader/load_test.go index 5af88b341..eaad86f5c 100644 --- a/pkg/chart/v2/loader/load_test.go +++ b/pkg/chart/v2/loader/load_test.go @@ -90,6 +90,36 @@ func TestLoadDirWithSymlink(t *testing.T) { verifyDependenciesLock(t, c) } +func TestLoadDirWithBrokenSymlink(t *testing.T) { + tests := []struct { + name string + helmignore string + wantErr bool + }{ + {"not ignored", "", true}, + {"ignored by .helmignore", "bar\n", false}, + } + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + dir := t.TempDir() + require.NoError(t, os.WriteFile(filepath.Join(dir, "Chart.yaml"), []byte("apiVersion: v2\nname: test\nversion: 0.1.0\n"), 0o644)) + require.NoError(t, os.WriteFile(filepath.Join(dir, ".helmignore"), []byte(tt.helmignore), 0o644)) + require.NoError(t, os.Mkdir(filepath.Join(dir, "templates"), 0o755)) + require.NoError(t, os.Symlink("foo", filepath.Join(dir, "templates", "bar"))) + + c, err := LoadDir(dir) + if tt.wantErr { + require.Error(t, err) + assert.Contains(t, err.Error(), "error evaluating symlink") + return + } + require.NoError(t, err) + assert.Equal(t, "test", c.Name()) + assert.Empty(t, c.Templates) + }) + } +} + func TestBomTestData(t *testing.T) { testFiles := []string{"frobnitz_with_bom/.helmignore", "frobnitz_with_bom/templates/template.tpl", "frobnitz_with_bom/Chart.yaml"} for _, file := range testFiles {