From e5d80c72c618a2edc945c9ac59aa4f7649b41997 Mon Sep 17 00:00:00 2001 From: Sebastiaan van Stijn Date: Mon, 23 Mar 2026 21:58:45 +0100 Subject: [PATCH] fix panic / nil pointer dereference on invalid patterns `Pattern.compile` was updating `Pattern.matchType` in-place. In situations where the resulting regex failed to compile, it would return early (with an error), but the `matchType` was already set. In that situation, `Pattern.match` would consider the `matchType` already set, skip the `p.matchType == unknownMatch` condition, and fall through to trying to use `p.regex`, which was nil, and resulted in a panic; ``` journalctl -u docker.service -f dockerd[423967]: panic: runtime error: invalid memory address or nil pointer dereference dockerd[423967]: [signal SIGSEGV: segmentation violation code=0x1 addr=0x90 pc=0x557e0f7ebf80] dockerd[423967]: goroutine 1241 [running]: dockerd[423967]: regexp.(*Regexp).doExecute(0x557e11b285a0?, {0x0?, 0x0?}, {0x0?, 0x557e11922650?, 0x557e11922650?}, {0xc0009d3db0?, 0xc000061778?}, 0x557e0f6d0d99?, 0x0, ...) dockerd[423967]: /usr/local/go/src/regexp/exec.go:527 +0x80 dockerd[423967]: regexp.(*Regexp).doMatch(...) dockerd[423967]: /usr/local/go/src/regexp/exec.go:514 dockerd[423967]: regexp.(*Regexp).MatchString(...) dockerd[423967]: /usr/local/go/src/regexp/regexp.go:527 dockerd[423967]: github.com/moby/patternmatcher.(*Pattern).match(0x557e11922650?, {0xc0009d3db0, 0x1}) dockerd[423967]: /root/build-deb/engine/vendor/github.com/moby/patternmatcher/patternmatcher.go:334 +0x26b dockerd[423967]: github.com/moby/patternmatcher.(*PatternMatcher).MatchesOrParentMatches(0xc000d761e0, {0xc0009d3db0, 0x1}) dockerd[423967]: /root/build-deb/engine/vendor/github.com/moby/patternmatcher/patternmatcher.go:142 +0xda dockerd[423967]: github.com/moby/buildkit/frontend/dockerfile/dockerfile2llb.validateCopySourcePath({0xc0009d3db0, 0x1}, 0xc0000621f8) dockerd[423967]: /root/build-deb/engine/vendor/github.com/moby/buildkit/frontend/dockerfile/dockerfile2llb/convert.go:2023 +0x55 dockerd[423967]: github.com/moby/buildkit/frontend/dockerfile/dockerfile2llb.dispatchCopy(_, {{{0xc0009d3dc0, 0x1}, {0xc0009b5d10, 0x1, 0x1}, {0x0, 0x0, 0x0}}, {0x0, ...}, ...}) dockerd[423967]: /root/build-deb/engine/vendor/github.com/moby/buildkit/frontend/dockerfile/dockerfile2llb/convert.go:1607 +0xd5c dockerd[423967]: github.com/moby/buildkit/frontend/dockerfile/dockerfile2llb.dispatch(_, {{_, _}, {_, _, _}, _}, {0xc000aab6a0, {0x557e1214c560, 0xc0007b79e0}, ...}) dockerd[423967]: /root/build-deb/engine/vendor/github.com/moby/buildkit/frontend/dockerfile/dockerfile2llb/convert.go:1004 +0xafb dockerd[423967]: github.com/moby/buildkit/frontend/dockerfile/dockerfile2llb.toDispatchState({_, _}, {_, _, _}, {{0xc000d5c8d0, {0x0, 0x0}, {0x0, 0x0}, ...}, ...}) dockerd[423967]: /root/build-deb/engine/vendor/github.com/moby/buildkit/frontend/dockerfile/dockerfile2llb/convert.go:731 +0x3926 dockerd[423967]: github.com/moby/buildkit/frontend/dockerfile/dockerfile2llb.Dockerfile2LLB({_, _}, {_, _, _}, {{0xc000d5c8d0, {0x0, 0x0}, {0x0, 0x0}, ...}, ...}) dockerd[423967]: /root/build-deb/engine/vendor/github.com/moby/buildkit/frontend/dockerfile/dockerfile2llb/convert.go:90 +0x65 dockerd[423967]: github.com/moby/buildkit/frontend/dockerfile/builder.Build.func6({0x557e121613e0, 0xc000cfd590}, 0x0, 0x557e0f64accb?) dockerd[423967]: /root/build-deb/engine/vendor/github.com/moby/buildkit/frontend/dockerfile/builder/build.go:136 +0xfe dockerd[423967]: github.com/moby/buildkit/frontend/dockerui.(*Client).Build.func1() dockerd[423967]: /root/build-deb/engine/vendor/github.com/moby/buildkit/frontend/dockerui/build.go:39 +0x71 dockerd[423967]: golang.org/x/sync/errgroup.(*Group).Go.func1() dockerd[423967]: /root/build-deb/engine/vendor/golang.org/x/sync/errgroup/errgroup.go:93 +0x50 dockerd[423967]: created by golang.org/x/sync/errgroup.(*Group).Go in goroutine 1136 dockerd[423967]: /root/build-deb/engine/vendor/golang.org/x/sync/errgroup/errgroup.go:78 +0x95 systemd[1]: docker.service: Main process exited, code=exited, status=2/INVALIDARGUMENT systemd[1]: docker.service: Failed with result 'exit-code'. ``` This patch: - updates `Pattern.compile` to use a local variable for the intermediate state, and only updates `Pattern.matchType` when completing successfully. - adds a nil-check in `Pattern.match` as defense-in-depth. Signed-off-by: Sebastiaan van Stijn --- patternmatcher.go | 47 ++++++++++++++++++++++-------------------- patternmatcher_test.go | 30 +++++++++++++++++++++++++++ 2 files changed, 55 insertions(+), 22 deletions(-) diff --git a/patternmatcher.go b/patternmatcher.go index 37a1a59..216dea6 100644 --- a/patternmatcher.go +++ b/patternmatcher.go @@ -331,14 +331,20 @@ func (p *Pattern) match(path string) (bool, error) { // **/foo matches "foo" return suffix[0] == os.PathSeparator && path == suffix[1:], nil case regexpMatch: + if p.regexp == nil { + return false, filepath.ErrBadPattern + } return p.regexp.MatchString(path), nil + case unknownMatch: + return false, filepath.ErrBadPattern + default: + return false, nil } - - return false, nil } func (p *Pattern) compile(sl string) error { regStr := "^" + detectedType := exactMatch // assume exact match pattern := p.cleanedPattern // Go through the pattern and convert it to a regexp. // We use a scanner so we can support utf-8 chars. @@ -350,7 +356,6 @@ func (p *Pattern) compile(sl string) error { escSL += `\` } - p.matchType = exactMatch for i := 0; scan.Peek() != scanner.EOF; i++ { ch := scan.Next() @@ -366,32 +371,32 @@ func (p *Pattern) compile(sl string) error { if scan.Peek() == scanner.EOF { // is "**EOF" - to align with .gitignore just accept all - if p.matchType == exactMatch { - p.matchType = prefixMatch + if detectedType == exactMatch { + detectedType = prefixMatch } else { regStr += ".*" - p.matchType = regexpMatch + detectedType = regexpMatch } } else { // is "**" // Note that this allows for any # of /'s (even 0) because // the .* will eat everything, even /'s regStr += "(.*" + escSL + ")?" - p.matchType = regexpMatch + detectedType = regexpMatch } if i == 0 { - p.matchType = suffixMatch + detectedType = suffixMatch } } else { // is "*" so map it to anything but "/" regStr += "[^" + escSL + "]*" - p.matchType = regexpMatch + detectedType = regexpMatch } } else if ch == '?' { // "?" is any char except "/" regStr += "[^" + escSL + "]" - p.matchType = regexpMatch + detectedType = regexpMatch } else if shouldEscape(ch) { // Escape some regexp special chars that have no meaning // in golang's filepath.Match @@ -408,31 +413,29 @@ func (p *Pattern) compile(sl string) error { } if scan.Peek() != scanner.EOF { regStr += `\` + string(scan.Next()) - p.matchType = regexpMatch + detectedType = regexpMatch } else { regStr += `\` } } else if ch == '[' || ch == ']' { regStr += string(ch) - p.matchType = regexpMatch + detectedType = regexpMatch } else { regStr += string(ch) } } - if p.matchType != regexpMatch { - return nil - } + if detectedType == regexpMatch { + regStr += "$" - regStr += "$" + re, err := regexp.Compile(regStr) + if err != nil { + return err + } - re, err := regexp.Compile(regStr) - if err != nil { - return err + p.regexp = re } - - p.regexp = re - p.matchType = regexpMatch + p.matchType = detectedType return nil } diff --git a/patternmatcher_test.go b/patternmatcher_test.go index ea4605a..5f02521 100644 --- a/patternmatcher_test.go +++ b/patternmatcher_test.go @@ -545,3 +545,33 @@ func testCompile(sl string) func(*testing.T) { } } } + +// regression test for https://github.com/moby/moby/issues/52203 +func TestMatchesOrParentMatchesMalformedPatternDoesNotPanicOnRepeatedCall(t *testing.T) { + pm, err := New([]string{"[Local-Only]/"}) + if err != nil { + t.Fatalf("expected pattern to pass initial validation, got %v", err) + } + + _, err = pm.MatchesOrParentMatches("x") + if err == nil { + t.Fatal("expected first match to fail with a bad pattern error") + } + if err != filepath.ErrBadPattern { + t.Fatalf("expected %v, got %v", filepath.ErrBadPattern, err) + } + + defer func() { + if r := recover(); r != nil { + t.Fatalf("second match panicked: %v", r) + } + }() + + _, err = pm.MatchesOrParentMatches("x") + if err == nil { + t.Fatal("expected second match to fail with a bad pattern error") + } + if err != filepath.ErrBadPattern { + t.Fatalf("expected %v on second call, got %v", filepath.ErrBadPattern, err) + } +}