From 1a55764af6de09d8612bf0bf309efca566581506 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Bor=20Gro=C5=A1elj=20Simi=C4=87?= Date: Thu, 21 Oct 2021 21:04:39 +0200 Subject: [PATCH] limit recursion to max 25 asterisks The algorithm used is exponential in number of '*' characters. By limiting the recursion to 25 we can guarantee it will finish reasonably fast anyway. A testcase for this behavior is provided. This is a port of the fix for the same bug in NetBSD libc. --- fnmatch.go | 53 ++++++++++++++++++++++++++++++++----------------- fnmatch_test.go | 31 +++++++++++++++++++++++++++++ 2 files changed, 66 insertions(+), 18 deletions(-) diff --git a/fnmatch.go b/fnmatch.go index 07ac7b3..575e972 100644 --- a/fnmatch.go +++ b/fnmatch.go @@ -11,6 +11,7 @@ package fnmatch // a period must be matched explicitly, but a range will match it too import ( + "errors" "unicode" "unicode/utf8" ) @@ -33,10 +34,23 @@ func unpackRune(str *string) rune { return rune } +func Match(pattern, s string, flags int) bool { + found, err := match(pattern, s, flags, 25) + if err != nil || !found { + return false + } else { + return true + } +} + // Matches the pattern against the string, with the given flags, // and returns true if the match is successful. // This function should match fnmatch.3 as closely as possible. -func Match(pattern, s string, flags int) bool { +func match(pattern, s string, flags int, limit uint) (bool, error) { + if limit == 0 { + return false, errors.New("recursion limit exceeded") + } + // The implementation for this function was patterned after the BSD fnmatch.c // source found at http://src.gnu-darwin.org/src/contrib/csup/fnmatch.c.html noescape := (flags&FNM_NOESCAPE != 0) @@ -63,14 +77,14 @@ func Match(pattern, s string, flags int) bool { switch c { case '?': if len(s) == 0 { - return false + return false, nil } sc := unpackS() if pathname && sc == '/' { - return false + return false, nil } if period && sc == '.' && (sLastAtStart || (pathname && sLastSlash)) { - return false + return false, nil } case '*': // collapse multiple *'s @@ -79,20 +93,19 @@ func Match(pattern, s string, flags int) bool { pattern = pattern[1:] } if period && s[0] == '.' && (sAtStart || (pathname && sLastUnpacked == '/')) { - return false + return false, nil } // optimize for patterns with * at end or before / if len(pattern) == 0 { if pathname { - return leadingdir || (strchr(s, '/') == -1) + return leadingdir || (strchr(s, '/') == -1), nil } else { - return true + return true, nil } - return !(pathname && strchr(s, '/') >= 0) } else if pathname && pattern[0] == '/' { offset := strchr(s, '/') if offset == -1 { - return false + return false, nil } else { // we already know our pattern and string have a /, skip past it s = s[offset:] // use unpackS here to maintain our bookkeeping state @@ -105,23 +118,27 @@ func Match(pattern, s string, flags int) bool { for test := s; len(test) > 0; unpackRune(&test) { // I believe the (flags &^ FNM_PERIOD) is a bug when FNM_PATHNAME is specified // but this follows exactly from how fnmatch.c implements it - if Match(pattern, test, (flags &^ FNM_PERIOD)) { - return true + found, err := match(pattern, test, (flags &^ FNM_PERIOD), limit-1) + if err != nil { + return false, err + } + if found { + return true, nil } else if pathname && test[0] == '/' { break } } - return false + return false, nil case '[': if len(s) == 0 { - return false + return false, nil } if pathname && s[0] == '/' { - return false + return false, nil } sc := unpackS() if !rangematch(&pattern, sc, flags) { - return false + return false, nil } case '\\': if !noescape { @@ -132,18 +149,18 @@ func Match(pattern, s string, flags int) bool { fallthrough default: if len(s) == 0 { - return false + return false, nil } sc := unpackS() switch { case sc == c: case casefold && unicode.ToLower(sc) == unicode.ToLower(c): default: - return false + return false, nil } } } - return len(s) == 0 || (leadingdir && s[0] == '/') + return len(s) == 0 || (leadingdir && s[0] == '/'), nil } func rangematch(pattern *string, test rune, flags int) bool { diff --git a/fnmatch_test.go b/fnmatch_test.go index 8fc5000..cc96277 100644 --- a/fnmatch_test.go +++ b/fnmatch_test.go @@ -2,6 +2,7 @@ package fnmatch import ( "testing" + "time" ) // This is a set of tests ported from a set of tests for C fnmatch @@ -347,3 +348,33 @@ func TestFNMLeadingDir(t *testing.T) { } } } + +func TestRecursionLimit(t *testing.T) { + timeout := time.After(10 * time.Second) + success := make(chan bool) + go func() { + success <- Match("x*x*x*x*x*x*x*x*x*x*x*x*x*x*x*x*x*x*x*x*x*x*x*x*x*", "xxxxxxxxxxxxxxxxxxxxxxxx", 0) + }() + + select { + case <-timeout: + t.Errorf("Timed out\n"); + case found := <-success: + if found { + t.Errorf("Failed\n"); + } + } + + go func() { + success <- Match("x*x*x*x*x*x*x*x*x*x*x*x*x*x*x*x*x*x*x*x*x*x*x*x*x*x*x*x*x*x*x*x*x*x*x*x*x*x*x*x*x*x*x*x*x*x*x*x*x*x*x*x*x*x*x*x*x*x*x*x*x*x*x*x*x*x*x*x*x*x*x*x*x*x*x*x*x*x*x*x*x*x*x*x*x*x*x*x*x*x*x*x*x*x*x*x*x*x*x*x*", "xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx", 0) + }() + + select { + case <-timeout: + t.Errorf("Timed out\n"); + case found := <-success: + if found { + t.Errorf("Failed\n"); + } + } +} -- 2.45.2