fix(ignore): avoid mutating root patterns during matching

Concurrent calls to `Rules.Ignore` race when a `.helmignore` pattern starts with `/`: each match writes the stripped pattern back into a captured variable.

Strip the prefix while constructing the matcher, leaving evaluation read-only. A concurrent regression test checks that the root file matches and the same name in a subdirectory does not.

Signed-off-by: Alexandre Rodrigues <alexandre3ylf@gmail.com>
pull/32658/head
Alexandre Rodrigues 2 weeks ago
parent c3bb7ad28e
commit 3ff4bec535

@ -172,8 +172,8 @@ func (r *Rules) parseRule(rule string) error {
if after, ok := strings.CutPrefix(rule, "/"); ok { if after, ok := strings.CutPrefix(rule, "/"); ok {
// Require path matches the root path. // Require path matches the root path.
rule = after
p.match = func(n string, _ os.FileInfo) bool { p.match = func(n string, _ os.FileInfo) bool {
rule = after
ok, err := filepath.Match(rule, n) ok, err := filepath.Match(rule, n)
if err != nil { if err != nil {
slog.Error("failed to compile", slog.String("rule", rule), slog.Any("error", err)) slog.Error("failed to compile", slog.String("rule", rule), slog.Any("error", err))

@ -20,6 +20,7 @@ import (
"bytes" "bytes"
"os" "os"
"path/filepath" "path/filepath"
"sync"
"testing" "testing"
"github.com/stretchr/testify/assert" "github.com/stretchr/testify/assert"
@ -133,3 +134,18 @@ func parseString(str string) (*Rules, error) {
b := bytes.NewBuffer([]byte(str)) b := bytes.NewBuffer([]byte(str))
return Parse(b) return Parse(b)
} }
func TestIgnoreRootPatternConcurrent(t *testing.T) {
rules, err := parseString("/root.txt")
require.NoError(t, err)
var workers sync.WaitGroup
for range 8 {
workers.Go(func() {
for range 100 {
assert.True(t, rules.Ignore("root.txt", nil))
assert.False(t, rules.Ignore("nested/root.txt", nil))
}
})
}
workers.Wait()
}

Loading…
Cancel
Save