[PATCH v2 0/8] lib/glob: bug fixes, new features, and tests

0 views
Skip to first unread message

Josh Law

unread,
Mar 15, 2026, 4:18:40 PM (5 days ago) Mar 15
to Andrew Morton, Brendan Higgins, David Gow, Rae Moar, linux-k...@vger.kernel.org, kuni...@googlegroups.com, linux-...@vger.kernel.org, Josh Law
Bug fixes, extensions, and test coverage for lib/glob.c:

Patches 1-3 fix corner cases in the existing glob_match():
- Inverted character class ranges like [z-a] are now normalized
instead of silently failing to match.
- A trailing backslash is treated as a literal '\' rather than
reading past the end of the pattern string.
- [^...] is accepted as an alias for [!...] to match the
regex-style negation syntax documented in glob(7).

Patches 4-5 add two new utility functions:
- glob_match_nocase() for case-insensitive matching, useful for
subsystems like ATA that need case-folded denylist comparisons.
- glob_validate() for checking pattern syntax before use, so
callers can reject malformed patterns early with a clear error.

Patches 6-7 add kunit test coverage:
- 47 new test cases for glob_match covering escapes, inverted
ranges, caret negation, edge cases, and unclosed brackets.
- 11 test cases for glob_match_nocase.
- 17 test cases for glob_validate.

Patch 8 adds a real in-tree caller for glob_validate() in the kunit
executor, validating user-provided filter_glob patterns and returning
-EINVAL for malformed ones.

Changes since v1:
- Added patch 8 (kunit executor caller for glob_validate) so that
both new exported functions have in-tree users or documented
intended users.
- Updated glob_match_nocase() commit message to reference the ATA
denylist as the intended caller (follow-up patch).

Josh Law (8):
lib/glob: normalize inverted character class ranges
lib/glob: treat trailing backslash as literal character
lib/glob: accept [^...] as character class negation syntax
lib/glob: add case-insensitive glob_match_nocase()
lib/glob: add glob_validate() for pattern syntax checking
lib/tests: add glob test cases for escapes, edge cases, and new
features
lib/tests: add kunit tests for glob_match_nocase() and glob_validate()
kunit: validate glob filter patterns before use

include/linux/glob.h | 2 +
lib/glob.c | 122 +++++++++++++++++++++++++++++++++++++---
lib/kunit/executor.c | 13 +++++
lib/tests/glob_kunit.c | 124 +++++++++++++++++++++++++++++++++++++++++
4 files changed, 252 insertions(+), 9 deletions(-)

--
2.34.1

Josh Law

unread,
Mar 15, 2026, 4:18:41 PM (5 days ago) Mar 15
to Andrew Morton, Brendan Higgins, David Gow, Rae Moar, linux-k...@vger.kernel.org, kuni...@googlegroups.com, linux-...@vger.kernel.org, Josh Law
A pattern ending with a lone backslash (e.g., "path\\") reads the
NUL terminator as the escaped character, making the backslash
effectively match end-of-string rather than a literal '\' character.
This means glob_match("path\\", "path\\") would fail since the
pattern consumes the NUL early, while glob_match("path\\", "path")
would unexpectedly succeed.

Guard the escape so that a trailing backslash keeps d = '\\' and
falls through to literal matching, which is the intuitive behavior:
a trailing backslash matches a literal backslash character.

Signed-off-by: Josh Law <obje...@objecting.org>
---
lib/glob.c | 3 ++-
1 file changed, 2 insertions(+), 1 deletion(-)

diff --git a/lib/glob.c b/lib/glob.c
index cb45a9a47f28..df7f00619b1b 100644
--- a/lib/glob.c
+++ b/lib/glob.c
@@ -112,7 +112,8 @@ bool __pure glob_match(char const *pat, char const *str)
}
break;
case '\\':
- d = *pat++;
+ if (*pat != '\0')
+ d = *pat++;
fallthrough;
default: /* Literal character */
literal:
--
2.34.1

Josh Law

unread,
Mar 15, 2026, 4:18:43 PM (5 days ago) Mar 15
to Andrew Morton, Brendan Higgins, David Gow, Rae Moar, linux-k...@vger.kernel.org, kuni...@googlegroups.com, linux-...@vger.kernel.org, Josh Law
The glob kunit test suite had no coverage for backslash escape
sequences, and was missing edge cases around wildcards with empty
strings, inverted ranges, unclosed brackets, and consecutive
wildcards.

Add test cases covering:
- Backslash escapes: \*, \?, \[, \\, non-metachar after backslash,
and trailing backslash matching a literal '\' character.
- Wildcards matching special characters in the string (*, [, \, ?).
- Wildcards with empty strings: * matches empty, ** matches empty,
? does not match empty.
- Inverted (backwards) ranges: [z-a] and [9-0] now match thanks to
endpoint normalization.
- Single-character ranges: [a-a] matches 'a'.
- Caret negation: [^a] and [^a-z] as alias for [!...].
- Unclosed bracket: [ treated as literal.
- Consecutive wildcards: ***a, *?*, *?*?* patterns.

Signed-off-by: Josh Law <obje...@objecting.org>
---
lib/tests/glob_kunit.c | 47 ++++++++++++++++++++++++++++++++++++++++++
1 file changed, 47 insertions(+)

diff --git a/lib/tests/glob_kunit.c b/lib/tests/glob_kunit.c
index 362b1eda8e5b..9b53060e7c8e 100644
--- a/lib/tests/glob_kunit.c
+++ b/lib/tests/glob_kunit.c
@@ -80,6 +80,53 @@ static const struct glob_test_case glob_test_cases[] = {
{ .pat = "*bc", .str = "bc", .expected = true },
{ .pat = "*bc", .str = "bbc", .expected = true },
{ .pat = "*bc", .str = "bcbc", .expected = true },
+ /* Backslash escape sequences */
+ { .pat = "\\*", .str = "*", .expected = true },
+ { .pat = "\\*", .str = "a", .expected = false },
+ { .pat = "\\?", .str = "?", .expected = true },
+ { .pat = "\\?", .str = "a", .expected = false },
+ { .pat = "\\[", .str = "[", .expected = true },
+ { .pat = "\\[", .str = "a", .expected = false },
+ { .pat = "a\\bc", .str = "abc", .expected = true },
+ { .pat = "\\\\", .str = "\\", .expected = true },
+ { .pat = "\\\\", .str = "a", .expected = false },
+ /* Trailing backslash (matches literal backslash) */
+ { .pat = "a\\", .str = "a\\", .expected = true },
+ { .pat = "a\\", .str = "a", .expected = false },
+ /* Wildcards matching special characters in string */
+ { .pat = "?", .str = "*", .expected = true },
+ { .pat = "?", .str = "[", .expected = true },
+ { .pat = "?", .str = "\\", .expected = true },
+ { .pat = "?", .str = "?", .expected = true },
+ /* Wildcards with empty strings */
+ { .pat = "*", .str = "", .expected = true },
+ { .pat = "*", .str = "a", .expected = true },
+ { .pat = "**", .str = "", .expected = true },
+ { .pat = "**", .str = "abc", .expected = true },
+ { .pat = "?", .str = "", .expected = false },
+ /* Inverted (backwards) ranges */
+ { .pat = "[z-a]", .str = "m", .expected = true },
+ { .pat = "[z-a]", .str = "z", .expected = true },
+ { .pat = "[z-a]", .str = "a", .expected = true },
+ { .pat = "[9-0]", .str = "5", .expected = true },
+ { .pat = "[9-0]", .str = "a", .expected = false },
+ /* Single-character range */
+ { .pat = "[a-a]", .str = "a", .expected = true },
+ { .pat = "[a-a]", .str = "b", .expected = false },
+ /* Caret negation syntax */
+ { .pat = "[^a]", .str = "b", .expected = true },
+ { .pat = "[^a]", .str = "a", .expected = false },
+ { .pat = "[^a-z]", .str = "0", .expected = true },
+ { .pat = "[^a-z]", .str = "m", .expected = false },
+ /* Unclosed bracket (treated as literal) */
+ { .pat = "[", .str = "[", .expected = true },
+ { .pat = "[", .str = "a", .expected = false },
+ { .pat = "[abc", .str = "[abc", .expected = true },
+ /* Consecutive wildcards */
+ { .pat = "***a", .str = "a", .expected = true },
+ { .pat = "*?*", .str = "a", .expected = true },
+ { .pat = "*?*?*", .str = "ab", .expected = true },
+ { .pat = "*?*?*", .str = "a", .expected = false },
/* Multiple asterisks (complex backtracking) */
{ .pat = "*ac*", .str = "abacadaeafag", .expected = true },
{ .pat = "*ac*ae*ag*", .str = "abacadaeafag", .expected = true },
--
2.34.1

Josh Law

unread,
Mar 15, 2026, 4:18:44 PM (5 days ago) Mar 15
to Andrew Morton, Brendan Higgins, David Gow, Rae Moar, linux-k...@vger.kernel.org, kuni...@googlegroups.com, linux-...@vger.kernel.org, Josh Law
Add glob_validate() which checks whether a glob pattern is
syntactically well-formed before matching. It detects:

- Unclosed character classes: a '[' with no matching ']'
- Trailing backslash: a '\' at end of pattern with nothing to escape

glob_match() already handles these gracefully (unclosed brackets are
matched literally, a trailing backslash matches itself), but callers
like ftrace filters or sysfs attribute stores that accept patterns
from userspace may want to reject malformed input upfront with a
clear error rather than silently falling back to literal matching.

Signed-off-by: Josh Law <obje...@objecting.org>
---
include/linux/glob.h | 1 +
lib/glob.c | 44 ++++++++++++++++++++++++++++++++++++++++++++
2 files changed, 45 insertions(+)

diff --git a/include/linux/glob.h b/include/linux/glob.h
index 36527ae89730..deceaa2e4a74 100644
--- a/include/linux/glob.h
+++ b/include/linux/glob.h
@@ -7,5 +7,6 @@

bool __pure glob_match(char const *pat, char const *str);
bool __pure glob_match_nocase(char const *pat, char const *str);
+bool __pure glob_validate(char const *pat);

#endif /* _LINUX_GLOB_H */
diff --git a/lib/glob.c b/lib/glob.c
index 9c71ccc15abc..800163ed4dbf 100644
--- a/lib/glob.c
+++ b/lib/glob.c
@@ -186,3 +186,47 @@ bool __pure glob_match_nocase(char const *pat, char const *str)
return __glob_match(pat, str, true);
}
EXPORT_SYMBOL(glob_match_nocase);
+
+/**
+ * glob_validate - Check whether a glob pattern is well-formed
+ * @pat: Shell-style pattern to validate.
+ *
+ * Return: true if @pat is a syntactically valid glob pattern, false
+ * if it contains malformed constructs. The following are considered
+ * invalid:
+ *
+ * - An opening '[' with no matching ']' (unclosed character class).
+ * - A trailing '\' with no character following it.
+ *
+ * Note that glob_match() handles these gracefully (an unclosed bracket
+ * is matched literally, a trailing backslash matches itself), but
+ * callers that accept patterns from user input may wish to reject
+ * malformed patterns early with a clear error.
+ */
+bool __pure glob_validate(char const *pat)
+{
+ while (*pat) {
+ switch (*pat++) {
+ case '\\':
+ if (*pat == '\0')
+ return false;
+ pat++;
+ break;
+ case '[': {
+ if (*pat == '!' || *pat == '^')
+ pat++;
+ /* ] as first character is literal, not end of class */
+ if (*pat == ']')
+ pat++;
+ while (*pat && *pat != ']')
+ pat++;
+ if (*pat == '\0')
+ return false;
+ pat++; /* skip ']' */
+ break;
+ }
+ }
+ }
+ return true;
+}
+EXPORT_SYMBOL(glob_validate);
--
2.34.1

Josh Law

unread,
Mar 15, 2026, 4:18:51 PM (5 days ago) Mar 15
to Andrew Morton, Brendan Higgins, David Gow, Rae Moar, linux-k...@vger.kernel.org, kuni...@googlegroups.com, linux-...@vger.kernel.org, Josh Law
kunit_parse_glob_filter() accepts user-provided glob patterns via the
filter_glob module parameter but does not check whether they are
well-formed. A malformed pattern like "suite[.test" (unclosed bracket)
or "suite\" (trailing backslash) is silently passed to glob_match()
which handles it gracefully but not in the way the user likely intended
— the bracket is matched as a literal character rather than starting a
character class.

Use glob_validate() to reject malformed patterns early with -EINVAL,
so users get a clear error instead of silently wrong filter results.

Signed-off-by: Josh Law <obje...@objecting.org>
---
lib/kunit/executor.c | 13 +++++++++++++
1 file changed, 13 insertions(+)

diff --git a/lib/kunit/executor.c b/lib/kunit/executor.c
index 1fef217de11d..f0cc15e4b34d 100644
--- a/lib/kunit/executor.c
+++ b/lib/kunit/executor.c
@@ -91,6 +91,8 @@ static int kunit_parse_glob_filter(struct kunit_glob_filter *parsed,
const char *period = strchr(filter_glob, '.');

if (!period) {
+ if (!glob_validate(filter_glob))
+ return -EINVAL;
parsed->suite_glob = kstrdup(filter_glob, GFP_KERNEL);
if (!parsed->suite_glob)
return -ENOMEM;
@@ -102,12 +104,23 @@ static int kunit_parse_glob_filter(struct kunit_glob_filter *parsed,
if (!parsed->suite_glob)
return -ENOMEM;

+ if (!glob_validate(parsed->suite_glob)) {
+ kfree(parsed->suite_glob);
+ return -EINVAL;
+ }
+
parsed->test_glob = kstrdup(period + 1, GFP_KERNEL);
if (!parsed->test_glob) {
kfree(parsed->suite_glob);
return -ENOMEM;
}

+ if (!glob_validate(parsed->test_glob)) {
+ kfree(parsed->test_glob);
+ kfree(parsed->suite_glob);
+ return -EINVAL;
+ }
+
return 0;
}

--
2.34.1

Josh Law

unread,
Mar 15, 2026, 4:18:51 PM (5 days ago) Mar 15
to Andrew Morton, Brendan Higgins, David Gow, Rae Moar, linux-k...@vger.kernel.org, kuni...@googlegroups.com, linux-...@vger.kernel.org, Josh Law
glob(7) specifies [!...] for negated character classes, but many
users are more familiar with the regex-style [^...] syntax. Tools
like git, rsync, and bash all accept both forms, and the difference
is a common source of confusion when writing glob patterns.

Accept '^' as an alias for '!' at the start of a character class so
that both [!a-z] and [^a-z] work as expected.

Signed-off-by: Josh Law <obje...@objecting.org>
---
lib/glob.c | 8 ++++----
1 file changed, 4 insertions(+), 4 deletions(-)

diff --git a/lib/glob.c b/lib/glob.c
index df7f00619b1b..c5508be0d215 100644
--- a/lib/glob.c
+++ b/lib/glob.c
@@ -33,9 +33,9 @@ MODULE_LICENSE("Dual MIT/GPL");
* Like !fnmatch(@pat, @str, 0) and unlike the shell, this does NOT
* treat / or leading . specially; it isn't actually used for pathnames.
*
- * Note that according to glob(7) (and unlike bash), character classes
- * are complemented by a leading !; this does not support the regex-style
- * [^a-z] syntax.
+ * Note that according to glob(7), character classes are complemented by
+ * a leading !. The regex-style [^a-z] syntax is also accepted as an
+ * alias.
*
* An opening bracket without a matching close is matched literally.
*/
@@ -72,7 +72,7 @@ bool __pure glob_match(char const *pat, char const *str)
case '[': { /* Character class */
if (c == '\0') /* No possible match */
return false;
- bool match = false, inverted = (*pat == '!');
+ bool match = false, inverted = (*pat == '!' || *pat == '^');
char const *class = inverted ? pat + 1 : pat;
unsigned char a = *class++;

--
2.34.1

Josh Law

unread,
Mar 15, 2026, 4:18:51 PM (5 days ago) Mar 15
to Andrew Morton, Brendan Higgins, David Gow, Rae Moar, linux-k...@vger.kernel.org, kuni...@googlegroups.com, linux-...@vger.kernel.org, Josh Law
When a character class range has its endpoints reversed (e.g., [z-a]),
the comparison a <= c && c <= b can never be true, so the range
silently matches nothing. A pattern like "file[9-0]" intended to
match any digit would fail to match anything, with no indication
that the range is backwards.

Swap the endpoints when a > b so that inverted ranges behave the
same as their forward equivalents: [z-a] matches the same characters
as [a-z], and [9-0] matches the same as [0-9]. This is consistent
with how GNU fnmatch and other glob implementations handle reversed
ranges.

Signed-off-by: Josh Law <obje...@objecting.org>
---
lib/glob.c | 8 +++++++-
1 file changed, 7 insertions(+), 1 deletion(-)

diff --git a/lib/glob.c b/lib/glob.c
index 7aca76c25bcb..cb45a9a47f28 100644
--- a/lib/glob.c
+++ b/lib/glob.c
@@ -94,7 +94,13 @@ bool __pure glob_match(char const *pat, char const *str)
goto literal;

class += 2;
- /* Any special action if a > b? */
+ /* Normalize inverted ranges like [z-a] */
+ if (a > b) {
+ unsigned char tmp = a;
+
+ a = b;
+ b = tmp;
+ }
}
if (a <= c && c <= b)
match = true;
--
2.34.1

Josh Law

unread,
Mar 15, 2026, 4:18:51 PM (5 days ago) Mar 15
to Andrew Morton, Brendan Higgins, David Gow, Rae Moar, linux-k...@vger.kernel.org, kuni...@googlegroups.com, linux-...@vger.kernel.org, Josh Law
Add a case-insensitive variant of glob_match() for callers that need
to match patterns regardless of case.

The immediate motivation is the ATA device quirk denylist in
drivers/ata/libata-core.c, which uses glob_match() to match drive
model and firmware revision strings. ATA IDENTIFY DEVICE data is
space-padded ASCII and the specification does not mandate a particular
case, so a denylist entry could miss a drive that reports its model
string in unexpected casing. Switching the denylist to
glob_match_nocase() (in a follow-up ATA patch) would make it robust
against such variation.

Refactor the matching logic into a static __glob_match() helper that
takes a bool nocase parameter, and implement both glob_match() and
glob_match_nocase() as thin wrappers. When nocase is true, literal
characters and character class endpoints are folded through tolower()
before comparison.

Signed-off-by: Josh Law <obje...@objecting.org>
---
include/linux/glob.h | 1 +
lib/glob.c | 73 ++++++++++++++++++++++++++++++++++++++------
2 files changed, 64 insertions(+), 10 deletions(-)

diff --git a/include/linux/glob.h b/include/linux/glob.h
index 861327b33e41..36527ae89730 100644
--- a/include/linux/glob.h
+++ b/include/linux/glob.h
@@ -6,5 +6,6 @@
#include <linux/compiler.h> /* For __pure */

bool __pure glob_match(char const *pat, char const *str);
+bool __pure glob_match_nocase(char const *pat, char const *str);

#endif /* _LINUX_GLOB_H */
diff --git a/lib/glob.c b/lib/glob.c
index c5508be0d215..9c71ccc15abc 100644
--- a/lib/glob.c
+++ b/lib/glob.c
@@ -1,4 +1,5 @@
// SPDX-License-Identifier: (GPL-2.0 OR MIT)
+#include <linux/ctype.h>
#include <linux/module.h>
#include <linux/glob.h>
#include <linux/export.h>
@@ -11,8 +12,8 @@
MODULE_DESCRIPTION("glob(7) matching");
MODULE_LICENSE("Dual MIT/GPL");

-/**
- * glob_match - Shell-style pattern matching, like !fnmatch(pat, str, 0)
+/*
+ * __glob_match - Shell-style pattern matching, like !fnmatch(pat, str, 0)
* @pat: Shell-style pattern to match, e.g. "*.[ch]".
* @str: String to match. The pattern must match the entire string.
*
@@ -39,7 +40,7 @@ MODULE_LICENSE("Dual MIT/GPL");
*
* An opening bracket without a matching close is matched literally.
*/
-bool __pure glob_match(char const *pat, char const *str)
+static bool __pure __glob_match(char const *pat, char const *str, bool nocase)
{
/*
* Backtrack to previous * on mismatch and retry starting one
@@ -58,6 +59,11 @@ bool __pure glob_match(char const *pat, char const *str)
unsigned char c = *str++;
unsigned char d = *pat++;

+ if (nocase) {
+ c = tolower(c);
+ d = tolower(d);
+ }
+
switch (d) {
case '?': /* Wildcard: anything but nul */
if (c == '\0')
@@ -94,13 +100,17 @@ bool __pure glob_match(char const *pat, char const *str)
goto literal;

class += 2;
- /* Normalize inverted ranges like [z-a] */
- if (a > b) {
- unsigned char tmp = a;
+ }
+ if (nocase) {
+ a = tolower(a);
+ b = tolower(b);
+ }
+ /* Normalize inverted ranges like [z-a] */
+ if (a > b) {
+ unsigned char tmp = a;

- a = b;
- b = tmp;
- }
+ a = b;
+ b = tmp;
}
if (a <= c && c <= b)
match = true;
@@ -112,8 +122,11 @@ bool __pure glob_match(char const *pat, char const *str)
}
break;
case '\\':
- if (*pat != '\0')
+ if (*pat != '\0') {
d = *pat++;
+ if (nocase)
+ d = tolower(d);
+ }
fallthrough;
default: /* Literal character */
literal:
@@ -132,4 +145,44 @@ bool __pure glob_match(char const *pat, char const *str)
}
}
}
+
+/**
+ * glob_match - Shell-style pattern matching, like !fnmatch(pat, str, 0)
+ * @pat: Shell-style pattern to match, e.g. "*.[ch]".
+ * @str: String to match. The pattern must match the entire string.
+ *
+ * Perform shell-style glob matching, returning true (1) if the match
+ * succeeds, or false (0) if it fails. Equivalent to !fnmatch(@pat, @str, 0).
+ *
+ * Pattern metacharacters are ?, *, [ and \.
+ * (And, inside character classes, !, ^ - and ].)
+ *
+ * This is small and simple and non-recursive, with run-time at most
+ * quadratic: strlen(@str)*strlen(@pat). It does not preprocess the
+ * patterns. An opening bracket without a matching close is matched
+ * literally.
+ *
+ * Return: true if @str matches @pat, false otherwise.
+ */
+bool __pure glob_match(char const *pat, char const *str)
+{
+ return __glob_match(pat, str, false);
+}
EXPORT_SYMBOL(glob_match);
+
+/**
+ * glob_match_nocase - Case-insensitive shell-style pattern matching
+ * @pat: Shell-style pattern to match.
+ * @str: String to match. The pattern must match the entire string.
+ *
+ * Identical to glob_match(), but performs case-insensitive comparisons
+ * for literal characters and character class contents using tolower().
+ * Metacharacters (?, *, [, ]) are not affected by case folding.
+ *
+ * Return: true if @str matches @pat (case-insensitive), false otherwise.
+ */
+bool __pure glob_match_nocase(char const *pat, char const *str)
+{
+ return __glob_match(pat, str, true);
+}
+EXPORT_SYMBOL(glob_match_nocase);
--
2.34.1

Josh Law

unread,
Mar 15, 2026, 5:17:00 PM (5 days ago) Mar 15
to Andrew Morton, Brendan Higgins, David Gow, Rae Moar, linux-k...@vger.kernel.org, kuni...@googlegroups.com, linux-...@vger.kernel.org, Josh Law
Bug fixes, extensions, and test coverage for lib/glob.c:

Patches 1-3 fix corner cases in the existing glob_match():
- Inverted character class ranges like [z-a] are now normalized
instead of silently failing to match.
- A trailing backslash is treated as a literal '\' rather than
reading past the end of the pattern string.
- [^...] is accepted as an alias for [!...] to match the
regex-style negation syntax documented in glob(7).

Patches 4-5 add two new utility functions:
- glob_match_nocase() for case-insensitive matching, useful for
subsystems like ATA that need case-folded denylist comparisons.
- glob_validate() for checking pattern syntax before use, so
callers can reject malformed patterns early with a clear error.

Patches 6-7 add kunit test coverage:
- 47 new test cases for glob_match covering escapes, inverted
ranges, caret negation, edge cases, and unclosed brackets.
- 11 test cases for glob_match_nocase.
- 17 test cases for glob_validate.

Patch 8 adds a real in-tree caller for glob_validate() in the kunit
executor, validating user-provided filter_glob patterns and returning
-EINVAL for malformed ones.

Changes since v2:
- Fixed missing '^' in __glob_match metacharacter comment (patch 4).
- Dropped unnecessary braces in glob_validate case '[' (patch 5).
- Added real-world example to glob_validate() commit message (patch 5).

Changes since v1:
- Added patch 8 (kunit executor caller for glob_validate).
- Updated glob_match_nocase() commit message to reference the ATA
denylist as the intended caller (follow-up patch).

Josh Law (8):
lib/glob: normalize inverted character class ranges
lib/glob: treat trailing backslash as literal character
lib/glob: accept [^...] as character class negation syntax
lib/glob: add case-insensitive glob_match_nocase()
lib/glob: add glob_validate() for pattern syntax checking
lib/tests: add glob test cases for escapes, edge cases, and new
features
lib/tests: add kunit tests for glob_match_nocase() and glob_validate()
kunit: validate glob filter patterns before use

include/linux/glob.h | 2 +
lib/glob.c | 123 ++++++++++++++++++++++++++++++++++++----
lib/kunit/executor.c | 13 +++++
lib/tests/glob_kunit.c | 124 +++++++++++++++++++++++++++++++++++++++++
4 files changed, 252 insertions(+), 10 deletions(-)

--
2.34.1

Josh Law

unread,
Mar 15, 2026, 5:17:00 PM (5 days ago) Mar 15
to Andrew Morton, Brendan Higgins, David Gow, Rae Moar, linux-k...@vger.kernel.org, kuni...@googlegroups.com, linux-...@vger.kernel.org, Josh Law
Add glob_validate() which checks whether a glob pattern is
syntactically well-formed before matching. It detects:

- Unclosed character classes: a '[' with no matching ']'
- Trailing backslash: a '\' at end of pattern with nothing to escape

glob_match() already handles these gracefully (unclosed brackets are
matched literally, a trailing backslash matches itself), but callers
that accept patterns from userspace may want to reject malformed input
upfront with a clear error rather than silently falling back to
literal matching.

For example, the kunit executor accepts a filter_glob module parameter
to select which tests to run. A user who types "snd_*.[codec_test"
(forgetting the closing bracket) would currently see the '[' matched
literally instead of starting a character class, producing silently
wrong filter results. With glob_validate(), the executor can reject
the pattern early and report -EINVAL, as done in the following patch.

Signed-off-by: Josh Law <obje...@objecting.org>
---
include/linux/glob.h | 1 +
lib/glob.c | 43 +++++++++++++++++++++++++++++++++++++++++++
2 files changed, 44 insertions(+)

diff --git a/include/linux/glob.h b/include/linux/glob.h
index 36527ae89730..deceaa2e4a74 100644
--- a/include/linux/glob.h
+++ b/include/linux/glob.h
@@ -7,5 +7,6 @@

bool __pure glob_match(char const *pat, char const *str);
bool __pure glob_match_nocase(char const *pat, char const *str);
+bool __pure glob_validate(char const *pat);

#endif /* _LINUX_GLOB_H */
diff --git a/lib/glob.c b/lib/glob.c
index 172b8ba3cd8e..8ee539d19cc4 100644
--- a/lib/glob.c
+++ b/lib/glob.c
@@ -186,3 +186,46 @@ bool __pure glob_match_nocase(char const *pat, char const *str)
return __glob_match(pat, str, true);
}
EXPORT_SYMBOL(glob_match_nocase);
+
+/**
+ * glob_validate - Check whether a glob pattern is well-formed
+ * @pat: Shell-style pattern to validate.
+ *

Josh Law

unread,
Mar 15, 2026, 5:17:00 PM (5 days ago) Mar 15
to Andrew Morton, Brendan Higgins, David Gow, Rae Moar, linux-k...@vger.kernel.org, kuni...@googlegroups.com, linux-...@vger.kernel.org, Josh Law
Add a case-insensitive variant of glob_match() for callers that need
to match patterns regardless of case.

The immediate motivation is the ATA device quirk denylist in
drivers/ata/libata-core.c, which uses glob_match() to match drive
model and firmware revision strings. ATA IDENTIFY DEVICE data is
space-padded ASCII and the specification does not mandate a particular
case, so a denylist entry could miss a drive that reports its model
string in unexpected casing. Switching the denylist to
glob_match_nocase() (in a follow-up ATA patch) would make it robust
against such variation.

Refactor the matching logic into a static __glob_match() helper that
takes a bool nocase parameter, and implement both glob_match() and
glob_match_nocase() as thin wrappers. When nocase is true, literal
characters and character class endpoints are folded through tolower()
before comparison.

Signed-off-by: Josh Law <obje...@objecting.org>
---
include/linux/glob.h | 1 +
lib/glob.c | 75 +++++++++++++++++++++++++++++++++++++-------
2 files changed, 65 insertions(+), 11 deletions(-)

diff --git a/include/linux/glob.h b/include/linux/glob.h
index 861327b33e41..36527ae89730 100644
--- a/include/linux/glob.h
+++ b/include/linux/glob.h
@@ -6,5 +6,6 @@
#include <linux/compiler.h> /* For __pure */

bool __pure glob_match(char const *pat, char const *str);
+bool __pure glob_match_nocase(char const *pat, char const *str);

#endif /* _LINUX_GLOB_H */
diff --git a/lib/glob.c b/lib/glob.c
index c5508be0d215..172b8ba3cd8e 100644
--- a/lib/glob.c
+++ b/lib/glob.c
@@ -1,4 +1,5 @@
// SPDX-License-Identifier: (GPL-2.0 OR MIT)
+#include <linux/ctype.h>
#include <linux/module.h>
#include <linux/glob.h>
#include <linux/export.h>
@@ -11,8 +12,8 @@
MODULE_DESCRIPTION("glob(7) matching");
MODULE_LICENSE("Dual MIT/GPL");

-/**
- * glob_match - Shell-style pattern matching, like !fnmatch(pat, str, 0)
+/*
+ * __glob_match - Shell-style pattern matching, like !fnmatch(pat, str, 0)
* @pat: Shell-style pattern to match, e.g. "*.[ch]".
* @str: String to match. The pattern must match the entire string.
*
@@ -20,7 +21,7 @@ MODULE_LICENSE("Dual MIT/GPL");
* succeeds, or false (0) if it fails. Equivalent to !fnmatch(@pat, @str, 0).
*
* Pattern metacharacters are ?, *, [ and \.
- * (And, inside character classes, !, - and ].)
+ * (And, inside character classes, !, ^ - and ].)
*
* This is a small and simple implementation intended for device denylists
* where a string is matched against a number of patterns. Thus, it
+ *
+ * Return: true if @str matches @pat, false otherwise.
+ */
+bool __pure glob_match(char const *pat, char const *str)
+{
+ return __glob_match(pat, str, false);
+}
EXPORT_SYMBOL(glob_match);
+
+/**
+ * glob_match_nocase - Case-insensitive shell-style pattern matching
+ * @pat: Shell-style pattern to match.
+ * @str: String to match. The pattern must match the entire string.
+ *
+ * Identical to glob_match(), but performs case-insensitive comparisons
+ * for literal characters and character class contents using tolower().
+ * Metacharacters (?, *, [, ]) are not affected by case folding.
+ *

Josh Law

unread,
Mar 15, 2026, 5:17:01 PM (5 days ago) Mar 15
to Andrew Morton, Brendan Higgins, David Gow, Rae Moar, linux-k...@vger.kernel.org, kuni...@googlegroups.com, linux-...@vger.kernel.org, Josh Law
When a character class range has its endpoints reversed (e.g., [z-a]),
the comparison a <= c && c <= b can never be true, so the range
silently matches nothing. A pattern like "file[9-0]" intended to
match any digit would fail to match anything, with no indication
that the range is backwards.

Swap the endpoints when a > b so that inverted ranges behave the
same as their forward equivalents: [z-a] matches the same characters
as [a-z], and [9-0] matches the same as [0-9]. This is consistent
with how GNU fnmatch and other glob implementations handle reversed
ranges.

Signed-off-by: Josh Law <obje...@objecting.org>
---
lib/glob.c | 8 +++++++-
1 file changed, 7 insertions(+), 1 deletion(-)

diff --git a/lib/glob.c b/lib/glob.c
index 7aca76c25bcb..cb45a9a47f28 100644
--- a/lib/glob.c
+++ b/lib/glob.c
@@ -94,7 +94,13 @@ bool __pure glob_match(char const *pat, char const *str)
goto literal;

class += 2;
- /* Any special action if a > b? */
+ /* Normalize inverted ranges like [z-a] */
+ if (a > b) {
+ unsigned char tmp = a;
+
+ a = b;
+ b = tmp;
+ }
}
if (a <= c && c <= b)
match = true;
--
2.34.1

Josh Law

unread,
Mar 15, 2026, 5:17:01 PM (5 days ago) Mar 15
to Andrew Morton, Brendan Higgins, David Gow, Rae Moar, linux-k...@vger.kernel.org, kuni...@googlegroups.com, linux-...@vger.kernel.org, Josh Law
glob(7) specifies [!...] for negated character classes, but many
users are more familiar with the regex-style [^...] syntax. Tools
like git, rsync, and bash all accept both forms, and the difference
is a common source of confusion when writing glob patterns.

Accept '^' as an alias for '!' at the start of a character class so
that both [!a-z] and [^a-z] work as expected.

Signed-off-by: Josh Law <obje...@objecting.org>
---
lib/glob.c | 8 ++++----
1 file changed, 4 insertions(+), 4 deletions(-)

diff --git a/lib/glob.c b/lib/glob.c
index df7f00619b1b..c5508be0d215 100644
--- a/lib/glob.c
+++ b/lib/glob.c
@@ -33,9 +33,9 @@ MODULE_LICENSE("Dual MIT/GPL");
* Like !fnmatch(@pat, @str, 0) and unlike the shell, this does NOT
* treat / or leading . specially; it isn't actually used for pathnames.
*
- * Note that according to glob(7) (and unlike bash), character classes
- * are complemented by a leading !; this does not support the regex-style
- * [^a-z] syntax.
+ * Note that according to glob(7), character classes are complemented by
+ * a leading !. The regex-style [^a-z] syntax is also accepted as an
+ * alias.
*
* An opening bracket without a matching close is matched literally.
*/

Josh Law

unread,
Mar 15, 2026, 5:17:02 PM (5 days ago) Mar 15
to Andrew Morton, Brendan Higgins, David Gow, Rae Moar, linux-k...@vger.kernel.org, kuni...@googlegroups.com, linux-...@vger.kernel.org, Josh Law
The glob kunit test suite had no coverage for backslash escape
sequences, and was missing edge cases around wildcards with empty
strings, inverted ranges, unclosed brackets, and consecutive
wildcards.

Add test cases covering:
- Backslash escapes: \*, \?, \[, \\, non-metachar after backslash,
and trailing backslash matching a literal '\' character.
- Wildcards matching special characters in the string (*, [, \, ?).
- Wildcards with empty strings: * matches empty, ** matches empty,
? does not match empty.
- Inverted (backwards) ranges: [z-a] and [9-0] now match thanks to
endpoint normalization.
- Single-character ranges: [a-a] matches 'a'.
- Caret negation: [^a] and [^a-z] as alias for [!...].
- Unclosed bracket: [ treated as literal.
- Consecutive wildcards: ***a, *?*, *?*?* patterns.

Signed-off-by: Josh Law <obje...@objecting.org>
---

Josh Law

unread,
Mar 15, 2026, 5:17:03 PM (5 days ago) Mar 15
to Andrew Morton, Brendan Higgins, David Gow, Rae Moar, linux-k...@vger.kernel.org, kuni...@googlegroups.com, linux-...@vger.kernel.org, Josh Law
A pattern ending with a lone backslash (e.g., "path\\") reads the
NUL terminator as the escaped character, making the backslash
effectively match end-of-string rather than a literal '\' character.
This means glob_match("path\\", "path\\") would fail since the
pattern consumes the NUL early, while glob_match("path\\", "path")
would unexpectedly succeed.

Guard the escape so that a trailing backslash keeps d = '\\' and
falls through to literal matching, which is the intuitive behavior:
a trailing backslash matches a literal backslash character.

Signed-off-by: Josh Law <obje...@objecting.org>
---
lib/glob.c | 3 ++-
1 file changed, 2 insertions(+), 1 deletion(-)

diff --git a/lib/glob.c b/lib/glob.c
index cb45a9a47f28..df7f00619b1b 100644
--- a/lib/glob.c
+++ b/lib/glob.c
@@ -112,7 +112,8 @@ bool __pure glob_match(char const *pat, char const *str)
}
break;
case '\\':
- d = *pat++;
+ if (*pat != '\0')
+ d = *pat++;
fallthrough;
default: /* Literal character */
literal:
--
2.34.1

Josh Law

unread,
Mar 15, 2026, 5:17:10 PM (5 days ago) Mar 15
to Andrew Morton, Brendan Higgins, David Gow, Rae Moar, linux-k...@vger.kernel.org, kuni...@googlegroups.com, linux-...@vger.kernel.org, Josh Law
Add parameterized test cases for the new glob API functions:

glob_match_nocase() tests verify case-insensitive matching for
literal characters, wildcards, character class ranges, and
backslash escapes -- e.g., pattern "a*c" matches string "ABC",
and range [A-Z] matches lowercase 'm'.

glob_validate() tests verify detection of malformed patterns:
well-formed patterns (character classes, escapes, wildcards) return
true, while unclosed brackets ("[abc") and trailing backslashes
("abc\") return false.

Signed-off-by: Josh Law <obje...@objecting.org>
---
lib/tests/glob_kunit.c | 77 ++++++++++++++++++++++++++++++++++++++++++
1 file changed, 77 insertions(+)

diff --git a/lib/tests/glob_kunit.c b/lib/tests/glob_kunit.c
index 9b53060e7c8e..711cb44d1fc8 100644
--- a/lib/tests/glob_kunit.c
+++ b/lib/tests/glob_kunit.c
@@ -157,8 +157,85 @@ static void glob_test_match(struct kunit *test)
params->pat, params->str, params->expected);
}

+/* Case-insensitive matching tests */
+static const struct glob_test_case glob_nocase_test_cases[] = {
+ { .pat = "abc", .str = "ABC", .expected = true },
+ { .pat = "ABC", .str = "abc", .expected = true },
+ { .pat = "aBc", .str = "AbC", .expected = true },
+ { .pat = "a*c", .str = "ABC", .expected = true },
+ { .pat = "A?C", .str = "abc", .expected = true },
+ { .pat = "[A-Z]", .str = "m", .expected = true },
+ { .pat = "[a-z]", .str = "M", .expected = true },
+ { .pat = "abc", .str = "abd", .expected = false },
+ { .pat = "ABC", .str = "ABD", .expected = false },
+ { .pat = "a*z", .str = "ABZ", .expected = true },
+ { .pat = "\\A", .str = "a", .expected = true },
+};
+
+KUNIT_ARRAY_PARAM(glob_nocase, glob_nocase_test_cases, glob_case_to_desc);
+
+static void glob_test_match_nocase(struct kunit *test)
+{
+ const struct glob_test_case *params = test->param_value;
+
+ KUNIT_EXPECT_EQ_MSG(test,
+ glob_match_nocase(params->pat, params->str),
+ params->expected,
+ "nocase Pattern: \"%s\", String: \"%s\", Expected: %d",
+ params->pat, params->str, params->expected);
+}
+
+/* Pattern validation tests */
+struct glob_validate_case {
+ const char *pat;
+ bool expected;
+};
+
+static const struct glob_validate_case glob_validate_test_cases[] = {
+ { .pat = "abc", .expected = true },
+ { .pat = "*", .expected = true },
+ { .pat = "?", .expected = true },
+ { .pat = "[abc]", .expected = true },
+ { .pat = "[!abc]", .expected = true },
+ { .pat = "[^abc]", .expected = true },
+ { .pat = "[a-z]", .expected = true },
+ { .pat = "[]abc]", .expected = true },
+ { .pat = "\\*", .expected = true },
+ { .pat = "\\\\", .expected = true },
+ { .pat = "", .expected = true },
+ /* Invalid patterns */
+ { .pat = "[", .expected = false },
+ { .pat = "[abc", .expected = false },
+ { .pat = "[!abc", .expected = false },
+ { .pat = "abc\\", .expected = false },
+ { .pat = "\\", .expected = false },
+ { .pat = "abc[def", .expected = false },
+};
+
+static void glob_validate_case_to_desc(const struct glob_validate_case *t,
+ char *desc)
+{
+ snprintf(desc, KUNIT_PARAM_DESC_SIZE, "pat:\"%s\"", t->pat);
+}
+
+KUNIT_ARRAY_PARAM(glob_validate, glob_validate_test_cases,
+ glob_validate_case_to_desc);
+
+static void glob_test_validate(struct kunit *test)
+{
+ const struct glob_validate_case *params = test->param_value;
+
+ KUNIT_EXPECT_EQ_MSG(test,
+ glob_validate(params->pat),
+ params->expected,
+ "validate Pattern: \"%s\", Expected: %d",
+ params->pat, params->expected);
+}
+
static struct kunit_case glob_kunit_test_cases[] = {
KUNIT_CASE_PARAM(glob_test_match, glob_gen_params),
+ KUNIT_CASE_PARAM(glob_test_match_nocase, glob_nocase_gen_params),
+ KUNIT_CASE_PARAM(glob_test_validate, glob_validate_gen_params),
{}
};

--
2.34.1

Josh Law

unread,
Mar 15, 2026, 5:17:10 PM (5 days ago) Mar 15
to Andrew Morton, Brendan Higgins, David Gow, Rae Moar, linux-k...@vger.kernel.org, kuni...@googlegroups.com, linux-...@vger.kernel.org, Josh Law
Add glob_validate() which checks whether a glob pattern is
syntactically well-formed before matching. It detects:

- Unclosed character classes: a '[' with no matching ']'
- Trailing backslash: a '\' at end of pattern with nothing to escape

glob_match() already handles these gracefully (unclosed brackets are
matched literally, a trailing backslash matches itself), but callers
that accept patterns from userspace may want to reject malformed input
upfront with a clear error rather than silently falling back to
literal matching.

For example, the kunit executor accepts a filter_glob module parameter
to select which tests to run. A user who types "snd_*.[codec_test"
(forgetting the closing bracket) would currently see the '[' matched
literally instead of starting a character class, producing silently
wrong filter results. With glob_validate(), the executor can reject
the pattern early and report -EINVAL, as done in the following patch.

Signed-off-by: Josh Law <obje...@objecting.org>
---
include/linux/glob.h | 1 +
lib/glob.c | 43 +++++++++++++++++++++++++++++++++++++++++++
2 files changed, 44 insertions(+)

diff --git a/include/linux/glob.h b/include/linux/glob.h
index 36527ae89730..deceaa2e4a74 100644
--- a/include/linux/glob.h
+++ b/include/linux/glob.h
@@ -7,5 +7,6 @@

bool __pure glob_match(char const *pat, char const *str);
bool __pure glob_match_nocase(char const *pat, char const *str);
+bool __pure glob_validate(char const *pat);

#endif /* _LINUX_GLOB_H */
diff --git a/lib/glob.c b/lib/glob.c
index 172b8ba3cd8e..8ee539d19cc4 100644
--- a/lib/glob.c
+++ b/lib/glob.c
@@ -186,3 +186,46 @@ bool __pure glob_match_nocase(char const *pat, char const *str)
return __glob_match(pat, str, true);
}
EXPORT_SYMBOL(glob_match_nocase);
+
+/**
+ * glob_validate - Check whether a glob pattern is well-formed
+ * @pat: Shell-style pattern to validate.
+ *

Josh Law

unread,
Mar 15, 2026, 5:17:11 PM (5 days ago) Mar 15
to Andrew Morton, Brendan Higgins, David Gow, Rae Moar, linux-k...@vger.kernel.org, kuni...@googlegroups.com, linux-...@vger.kernel.org, Josh Law
The glob kunit test suite had no coverage for backslash escape
sequences, and was missing edge cases around wildcards with empty
strings, inverted ranges, unclosed brackets, and consecutive
wildcards.

Add test cases covering:
- Backslash escapes: \*, \?, \[, \\, non-metachar after backslash,
and trailing backslash matching a literal '\' character.
- Wildcards matching special characters in the string (*, [, \, ?).
- Wildcards with empty strings: * matches empty, ** matches empty,
? does not match empty.
- Inverted (backwards) ranges: [z-a] and [9-0] now match thanks to
endpoint normalization.
- Single-character ranges: [a-a] matches 'a'.
- Caret negation: [^a] and [^a-z] as alias for [!...].
- Unclosed bracket: [ treated as literal.
- Consecutive wildcards: ***a, *?*, *?*?* patterns.

Signed-off-by: Josh Law <obje...@objecting.org>
---
lib/tests/glob_kunit.c | 47 ++++++++++++++++++++++++++++++++++++++++++
1 file changed, 47 insertions(+)

diff --git a/lib/tests/glob_kunit.c b/lib/tests/glob_kunit.c
index 362b1eda8e5b..9b53060e7c8e 100644
--- a/lib/tests/glob_kunit.c
+++ b/lib/tests/glob_kunit.c
{ .pat = "*ac*ae*ag*", .str = "abacadaeafag", .expected = true },
--
2.34.1

Josh Law

unread,
Mar 15, 2026, 5:17:11 PM (5 days ago) Mar 15
to Andrew Morton, Brendan Higgins, David Gow, Rae Moar, linux-k...@vger.kernel.org, kuni...@googlegroups.com, linux-...@vger.kernel.org, Josh Law
kunit_parse_glob_filter() accepts user-provided glob patterns via the
filter_glob module parameter but does not check whether they are
well-formed. A malformed pattern like "suite[.test" (unclosed bracket)
or "suite\" (trailing backslash) is silently passed to glob_match()
which handles it gracefully but not in the way the user likely intended
— the bracket is matched as a literal character rather than starting a
character class.

Use glob_validate() to reject malformed patterns early with -EINVAL,
so users get a clear error instead of silently wrong filter results.

Signed-off-by: Josh Law <obje...@objecting.org>
---

Josh Law

unread,
Mar 15, 2026, 5:17:12 PM (5 days ago) Mar 15
to Andrew Morton, Brendan Higgins, David Gow, Rae Moar, linux-k...@vger.kernel.org, kuni...@googlegroups.com, linux-...@vger.kernel.org, Josh Law
Add parameterized test cases for the new glob API functions:

glob_match_nocase() tests verify case-insensitive matching for
literal characters, wildcards, character class ranges, and
backslash escapes -- e.g., pattern "a*c" matches string "ABC",
and range [A-Z] matches lowercase 'm'.

glob_validate() tests verify detection of malformed patterns:
well-formed patterns (character classes, escapes, wildcards) return
true, while unclosed brackets ("[abc") and trailing backslashes
("abc\") return false.

Signed-off-by: Josh Law <obje...@objecting.org>
---
lib/tests/glob_kunit.c | 77 ++++++++++++++++++++++++++++++++++++++++++
1 file changed, 77 insertions(+)

diff --git a/lib/tests/glob_kunit.c b/lib/tests/glob_kunit.c
index 9b53060e7c8e..711cb44d1fc8 100644
--- a/lib/tests/glob_kunit.c
+++ b/lib/tests/glob_kunit.c

Josh Law

unread,
Mar 15, 2026, 5:17:12 PM (5 days ago) Mar 15
to Andrew Morton, Brendan Higgins, David Gow, Rae Moar, linux-k...@vger.kernel.org, kuni...@googlegroups.com, linux-...@vger.kernel.org, Josh Law
Add a case-insensitive variant of glob_match() for callers that need
to match patterns regardless of case.

The immediate motivation is the ATA device quirk denylist in
drivers/ata/libata-core.c, which uses glob_match() to match drive
model and firmware revision strings. ATA IDENTIFY DEVICE data is
space-padded ASCII and the specification does not mandate a particular
case, so a denylist entry could miss a drive that reports its model
string in unexpected casing. Switching the denylist to
glob_match_nocase() (in a follow-up ATA patch) would make it robust
against such variation.

Refactor the matching logic into a static __glob_match() helper that
takes a bool nocase parameter, and implement both glob_match() and
glob_match_nocase() as thin wrappers. When nocase is true, literal
characters and character class endpoints are folded through tolower()
before comparison.

Signed-off-by: Josh Law <obje...@objecting.org>
---
include/linux/glob.h | 1 +
lib/glob.c | 75 +++++++++++++++++++++++++++++++++++++-------
2 files changed, 65 insertions(+), 11 deletions(-)

diff --git a/include/linux/glob.h b/include/linux/glob.h
index 861327b33e41..36527ae89730 100644
--- a/include/linux/glob.h
+++ b/include/linux/glob.h
@@ -6,5 +6,6 @@
#include <linux/compiler.h> /* For __pure */

bool __pure glob_match(char const *pat, char const *str);
+bool __pure glob_match_nocase(char const *pat, char const *str);

#endif /* _LINUX_GLOB_H */
diff --git a/lib/glob.c b/lib/glob.c
index c5508be0d215..172b8ba3cd8e 100644
--- a/lib/glob.c
+++ b/lib/glob.c
@@ -39,7 +40,7 @@ MODULE_LICENSE("Dual MIT/GPL");
*
* An opening bracket without a matching close is matched literally.
*/
-bool __pure glob_match(char const *pat, char const *str)
+static bool __pure __glob_match(char const *pat, char const *str, bool nocase)
{
/*
* Backtrack to previous * on mismatch and retry starting one
@@ -58,6 +59,11 @@ bool __pure glob_match(char const *pat, char const *str)
unsigned char c = *str++;
unsigned char d = *pat++;

+ if (nocase) {
+ c = tolower(c);
+ d = tolower(d);
+ }
+
switch (d) {
case '?': /* Wildcard: anything but nul */
if (c == '\0')
@@ -94,13 +100,17 @@ bool __pure glob_match(char const *pat, char const *str)
goto literal;

class += 2;
- /* Normalize inverted ranges like [z-a] */
- if (a > b) {
- unsigned char tmp = a;
+ }
+ if (nocase) {
+ a = tolower(a);
+ b = tolower(b);
+ }
+ /* Normalize inverted ranges like [z-a] */
+ if (a > b) {
+ unsigned char tmp = a;

- a = b;
- b = tmp;
- }
+ a = b;
+ b = tmp;
}
if (a <= c && c <= b)
match = true;
@@ -112,8 +122,11 @@ bool __pure glob_match(char const *pat, char const *str)
}
break;
case '\\':
- if (*pat != '\0')
+ if (*pat != '\0') {
d = *pat++;
+ if (nocase)
+ d = tolower(d);
+ }
fallthrough;
default: /* Literal character */
literal:
@@ -132,4 +145,44 @@ bool __pure glob_match(char const *pat, char const *str)
}
}
}
+
+/**
+ * glob_match - Shell-style pattern matching, like !fnmatch(pat, str, 0)
+ * @pat: Shell-style pattern to match, e.g. "*.[ch]".
+ * @str: String to match. The pattern must match the entire string.
+ *
+ * Perform shell-style glob matching, returning true (1) if the match
+ * succeeds, or false (0) if it fails. Equivalent to !fnmatch(@pat, @str, 0).
+ *
+ * Pattern metacharacters are ?, *, [ and \.
+ * (And, inside character classes, !, ^ - and ].)
+ *
+ * This is small and simple and non-recursive, with run-time at most
+ * quadratic: strlen(@str)*strlen(@pat). It does not preprocess the
+ * patterns. An opening bracket without a matching close is matched
+ * literally.
+ *
+ * Return: true if @str matches @pat, false otherwise.
+ */
+bool __pure glob_match(char const *pat, char const *str)
+{
+ return __glob_match(pat, str, false);
+}
EXPORT_SYMBOL(glob_match);
+
+/**
+ * glob_match_nocase - Case-insensitive shell-style pattern matching
+ * @pat: Shell-style pattern to match.
+ * @str: String to match. The pattern must match the entire string.
+ *
+ * Identical to glob_match(), but performs case-insensitive comparisons
+ * for literal characters and character class contents using tolower().
+ * Metacharacters (?, *, [, ]) are not affected by case folding.
+ *

Josh Law

unread,
Mar 15, 2026, 5:17:13 PM (5 days ago) Mar 15
to Andrew Morton, Brendan Higgins, David Gow, Rae Moar, linux-k...@vger.kernel.org, kuni...@googlegroups.com, linux-...@vger.kernel.org, Josh Law
kunit_parse_glob_filter() accepts user-provided glob patterns via the
filter_glob module parameter but does not check whether they are
well-formed. A malformed pattern like "suite[.test" (unclosed bracket)
or "suite\" (trailing backslash) is silently passed to glob_match()
which handles it gracefully but not in the way the user likely intended
— the bracket is matched as a literal character rather than starting a
character class.

Use glob_validate() to reject malformed patterns early with -EINVAL,
so users get a clear error instead of silently wrong filter results.

Signed-off-by: Josh Law <obje...@objecting.org>
---

Josh Law

unread,
Mar 15, 2026, 5:18:26 PM (5 days ago) Mar 15
to Andrew Morton, Brendan Higgins, David Gow, Rae Moar, linux-k...@vger.kernel.org, kuni...@googlegroups.com, linux-...@vger.kernel.org, Josh Law
Bug fixes, extensions, and test coverage for lib/glob.c:

Patches 1-3 fix corner cases in the existing glob_match():
- Inverted character class ranges like [z-a] are now normalized
instead of silently failing to match.
- A trailing backslash is treated as a literal '\' rather than
reading past the end of the pattern string.
- [^...] is accepted as an alias for [!...] to match the
regex-style negation syntax documented in glob(7).

Patches 4-5 add two new utility functions:
- glob_match_nocase() for case-insensitive matching, useful for
subsystems like ATA that need case-folded denylist comparisons.
- glob_validate() for checking pattern syntax before use, so
callers can reject malformed patterns early with a clear error.

Patches 6-7 add kunit test coverage:
- 47 new test cases for glob_match covering escapes, inverted
ranges, caret negation, edge cases, and unclosed brackets.
- 11 test cases for glob_match_nocase.
- 17 test cases for glob_validate.

Patch 8 adds a real in-tree caller for glob_validate() in the kunit
executor, validating user-provided filter_glob patterns and returning
-EINVAL for malformed ones.

Changes since v3:
- Resend: v3 accidentally included stale v2 patch files in the
same email thread. No code changes from v3.

Josh Law

unread,
Mar 15, 2026, 5:18:29 PM (5 days ago) Mar 15
to Andrew Morton, Brendan Higgins, David Gow, Rae Moar, linux-k...@vger.kernel.org, kuni...@googlegroups.com, linux-...@vger.kernel.org, Josh Law
When a character class range has its endpoints reversed (e.g., [z-a]),
the comparison a <= c && c <= b can never be true, so the range
silently matches nothing. A pattern like "file[9-0]" intended to
match any digit would fail to match anything, with no indication
that the range is backwards.

Swap the endpoints when a > b so that inverted ranges behave the
same as their forward equivalents: [z-a] matches the same characters
as [a-z], and [9-0] matches the same as [0-9]. This is consistent
with how GNU fnmatch and other glob implementations handle reversed
ranges.

Signed-off-by: Josh Law <obje...@objecting.org>
---
lib/glob.c | 8 +++++++-
1 file changed, 7 insertions(+), 1 deletion(-)

diff --git a/lib/glob.c b/lib/glob.c
index 7aca76c25bcb..cb45a9a47f28 100644
--- a/lib/glob.c
+++ b/lib/glob.c
@@ -94,7 +94,13 @@ bool __pure glob_match(char const *pat, char const *str)
goto literal;

class += 2;
- /* Any special action if a > b? */
+ /* Normalize inverted ranges like [z-a] */
+ if (a > b) {
+ unsigned char tmp = a;
+
+ a = b;
+ b = tmp;
+ }
}
if (a <= c && c <= b)
match = true;
--
2.34.1

Josh Law

unread,
Mar 15, 2026, 5:18:29 PM (5 days ago) Mar 15
to Andrew Morton, Brendan Higgins, David Gow, Rae Moar, linux-k...@vger.kernel.org, kuni...@googlegroups.com, linux-...@vger.kernel.org, Josh Law
A pattern ending with a lone backslash (e.g., "path\\") reads the
NUL terminator as the escaped character, making the backslash
effectively match end-of-string rather than a literal '\' character.
This means glob_match("path\\", "path\\") would fail since the
pattern consumes the NUL early, while glob_match("path\\", "path")
would unexpectedly succeed.

Guard the escape so that a trailing backslash keeps d = '\\' and
falls through to literal matching, which is the intuitive behavior:
a trailing backslash matches a literal backslash character.

Signed-off-by: Josh Law <obje...@objecting.org>
---
lib/glob.c | 3 ++-
1 file changed, 2 insertions(+), 1 deletion(-)

diff --git a/lib/glob.c b/lib/glob.c
index cb45a9a47f28..df7f00619b1b 100644
--- a/lib/glob.c
+++ b/lib/glob.c
@@ -112,7 +112,8 @@ bool __pure glob_match(char const *pat, char const *str)
}
break;
case '\\':
- d = *pat++;
+ if (*pat != '\0')
+ d = *pat++;
fallthrough;
default: /* Literal character */
literal:
--
2.34.1

Josh Law

unread,
Mar 15, 2026, 5:18:29 PM (5 days ago) Mar 15
to Andrew Morton, Brendan Higgins, David Gow, Rae Moar, linux-k...@vger.kernel.org, kuni...@googlegroups.com, linux-...@vger.kernel.org, Josh Law
glob(7) specifies [!...] for negated character classes, but many
users are more familiar with the regex-style [^...] syntax. Tools
like git, rsync, and bash all accept both forms, and the difference
is a common source of confusion when writing glob patterns.

Accept '^' as an alias for '!' at the start of a character class so
that both [!a-z] and [^a-z] work as expected.

Signed-off-by: Josh Law <obje...@objecting.org>
---
lib/glob.c | 8 ++++----
1 file changed, 4 insertions(+), 4 deletions(-)

diff --git a/lib/glob.c b/lib/glob.c
index df7f00619b1b..c5508be0d215 100644
--- a/lib/glob.c
+++ b/lib/glob.c
@@ -33,9 +33,9 @@ MODULE_LICENSE("Dual MIT/GPL");
* Like !fnmatch(@pat, @str, 0) and unlike the shell, this does NOT
* treat / or leading . specially; it isn't actually used for pathnames.
*
- * Note that according to glob(7) (and unlike bash), character classes
- * are complemented by a leading !; this does not support the regex-style
- * [^a-z] syntax.
+ * Note that according to glob(7), character classes are complemented by
+ * a leading !. The regex-style [^a-z] syntax is also accepted as an
+ * alias.
*
* An opening bracket without a matching close is matched literally.
*/

Josh Law

unread,
Mar 15, 2026, 5:18:30 PM (5 days ago) Mar 15
to Andrew Morton, Brendan Higgins, David Gow, Rae Moar, linux-k...@vger.kernel.org, kuni...@googlegroups.com, linux-...@vger.kernel.org, Josh Law
Add a case-insensitive variant of glob_match() for callers that need
to match patterns regardless of case.

The immediate motivation is the ATA device quirk denylist in
drivers/ata/libata-core.c, which uses glob_match() to match drive
model and firmware revision strings. ATA IDENTIFY DEVICE data is
space-padded ASCII and the specification does not mandate a particular
case, so a denylist entry could miss a drive that reports its model
string in unexpected casing. Switching the denylist to
glob_match_nocase() (in a follow-up ATA patch) would make it robust
against such variation.

Refactor the matching logic into a static __glob_match() helper that
takes a bool nocase parameter, and implement both glob_match() and
glob_match_nocase() as thin wrappers. When nocase is true, literal
characters and character class endpoints are folded through tolower()
before comparison.

Signed-off-by: Josh Law <obje...@objecting.org>
---
include/linux/glob.h | 1 +
lib/glob.c | 75 +++++++++++++++++++++++++++++++++++++-------
2 files changed, 65 insertions(+), 11 deletions(-)

diff --git a/include/linux/glob.h b/include/linux/glob.h
index 861327b33e41..36527ae89730 100644
--- a/include/linux/glob.h
+++ b/include/linux/glob.h
@@ -6,5 +6,6 @@
#include <linux/compiler.h> /* For __pure */

bool __pure glob_match(char const *pat, char const *str);
+bool __pure glob_match_nocase(char const *pat, char const *str);

#endif /* _LINUX_GLOB_H */
diff --git a/lib/glob.c b/lib/glob.c
index c5508be0d215..172b8ba3cd8e 100644
--- a/lib/glob.c
+++ b/lib/glob.c
@@ -39,7 +40,7 @@ MODULE_LICENSE("Dual MIT/GPL");
*
* An opening bracket without a matching close is matched literally.
*/
-bool __pure glob_match(char const *pat, char const *str)
+static bool __pure __glob_match(char const *pat, char const *str, bool nocase)
{
/*
* Backtrack to previous * on mismatch and retry starting one
@@ -58,6 +59,11 @@ bool __pure glob_match(char const *pat, char const *str)
unsigned char c = *str++;
unsigned char d = *pat++;

+ if (nocase) {
+ c = tolower(c);
+ d = tolower(d);
+ }
+
switch (d) {
case '?': /* Wildcard: anything but nul */
if (c == '\0')
@@ -94,13 +100,17 @@ bool __pure glob_match(char const *pat, char const *str)
goto literal;

class += 2;
- /* Normalize inverted ranges like [z-a] */
- if (a > b) {
- unsigned char tmp = a;
+ }
+ if (nocase) {
+ a = tolower(a);
+ b = tolower(b);
+ }
+ /* Normalize inverted ranges like [z-a] */
+ if (a > b) {
+ unsigned char tmp = a;

- a = b;
- b = tmp;
- }
+ a = b;
+ b = tmp;
}
if (a <= c && c <= b)
match = true;
@@ -112,8 +122,11 @@ bool __pure glob_match(char const *pat, char const *str)
}
break;
case '\\':
- if (*pat != '\0')
+ if (*pat != '\0') {
d = *pat++;
+ if (nocase)
+ d = tolower(d);
+ }
fallthrough;
default: /* Literal character */
literal:

Josh Law

unread,
Mar 15, 2026, 5:18:32 PM (5 days ago) Mar 15
to Andrew Morton, Brendan Higgins, David Gow, Rae Moar, linux-k...@vger.kernel.org, kuni...@googlegroups.com, linux-...@vger.kernel.org, Josh Law
The glob kunit test suite had no coverage for backslash escape
sequences, and was missing edge cases around wildcards with empty
strings, inverted ranges, unclosed brackets, and consecutive
wildcards.

Add test cases covering:
- Backslash escapes: \*, \?, \[, \\, non-metachar after backslash,
and trailing backslash matching a literal '\' character.
- Wildcards matching special characters in the string (*, [, \, ?).
- Wildcards with empty strings: * matches empty, ** matches empty,
? does not match empty.
- Inverted (backwards) ranges: [z-a] and [9-0] now match thanks to
endpoint normalization.
- Single-character ranges: [a-a] matches 'a'.
- Caret negation: [^a] and [^a-z] as alias for [!...].
- Unclosed bracket: [ treated as literal.
- Consecutive wildcards: ***a, *?*, *?*?* patterns.

Signed-off-by: Josh Law <obje...@objecting.org>
---
lib/tests/glob_kunit.c | 47 ++++++++++++++++++++++++++++++++++++++++++
1 file changed, 47 insertions(+)

diff --git a/lib/tests/glob_kunit.c b/lib/tests/glob_kunit.c
index 362b1eda8e5b..9b53060e7c8e 100644
--- a/lib/tests/glob_kunit.c
+++ b/lib/tests/glob_kunit.c
{ .pat = "*ac*ae*ag*", .str = "abacadaeafag", .expected = true },
--
2.34.1

Josh Law

unread,
Mar 15, 2026, 5:18:32 PM (5 days ago) Mar 15
to Andrew Morton, Brendan Higgins, David Gow, Rae Moar, linux-k...@vger.kernel.org, kuni...@googlegroups.com, linux-...@vger.kernel.org, Josh Law
Add parameterized test cases for the new glob API functions:

glob_match_nocase() tests verify case-insensitive matching for
literal characters, wildcards, character class ranges, and
backslash escapes -- e.g., pattern "a*c" matches string "ABC",
and range [A-Z] matches lowercase 'm'.

glob_validate() tests verify detection of malformed patterns:
well-formed patterns (character classes, escapes, wildcards) return
true, while unclosed brackets ("[abc") and trailing backslashes
("abc\") return false.

Signed-off-by: Josh Law <obje...@objecting.org>
---
lib/tests/glob_kunit.c | 77 ++++++++++++++++++++++++++++++++++++++++++
1 file changed, 77 insertions(+)

diff --git a/lib/tests/glob_kunit.c b/lib/tests/glob_kunit.c
index 9b53060e7c8e..711cb44d1fc8 100644
--- a/lib/tests/glob_kunit.c
+++ b/lib/tests/glob_kunit.c

Josh Law

unread,
Mar 15, 2026, 5:18:32 PM (5 days ago) Mar 15
to Andrew Morton, Brendan Higgins, David Gow, Rae Moar, linux-k...@vger.kernel.org, kuni...@googlegroups.com, linux-...@vger.kernel.org, Josh Law
Add glob_validate() which checks whether a glob pattern is
syntactically well-formed before matching. It detects:

- Unclosed character classes: a '[' with no matching ']'
- Trailing backslash: a '\' at end of pattern with nothing to escape

glob_match() already handles these gracefully (unclosed brackets are
matched literally, a trailing backslash matches itself), but callers
that accept patterns from userspace may want to reject malformed input
upfront with a clear error rather than silently falling back to
literal matching.

For example, the kunit executor accepts a filter_glob module parameter
to select which tests to run. A user who types "snd_*.[codec_test"
(forgetting the closing bracket) would currently see the '[' matched
literally instead of starting a character class, producing silently
wrong filter results. With glob_validate(), the executor can reject
the pattern early and report -EINVAL, as done in the following patch.

Signed-off-by: Josh Law <obje...@objecting.org>
---
include/linux/glob.h | 1 +
lib/glob.c | 43 +++++++++++++++++++++++++++++++++++++++++++
2 files changed, 44 insertions(+)

diff --git a/include/linux/glob.h b/include/linux/glob.h
index 36527ae89730..deceaa2e4a74 100644
--- a/include/linux/glob.h
+++ b/include/linux/glob.h
@@ -7,5 +7,6 @@

bool __pure glob_match(char const *pat, char const *str);
bool __pure glob_match_nocase(char const *pat, char const *str);
+bool __pure glob_validate(char const *pat);

#endif /* _LINUX_GLOB_H */
diff --git a/lib/glob.c b/lib/glob.c
index 172b8ba3cd8e..8ee539d19cc4 100644
--- a/lib/glob.c
+++ b/lib/glob.c
@@ -186,3 +186,46 @@ bool __pure glob_match_nocase(char const *pat, char const *str)
return __glob_match(pat, str, true);
}
EXPORT_SYMBOL(glob_match_nocase);
+
+/**
+ * glob_validate - Check whether a glob pattern is well-formed
+ * @pat: Shell-style pattern to validate.
+ *

Josh Law

unread,
Mar 15, 2026, 5:18:36 PM (5 days ago) Mar 15
to Andrew Morton, Brendan Higgins, David Gow, Rae Moar, linux-k...@vger.kernel.org, kuni...@googlegroups.com, linux-...@vger.kernel.org, Josh Law
kunit_parse_glob_filter() accepts user-provided glob patterns via the
filter_glob module parameter but does not check whether they are
well-formed. A malformed pattern like "suite[.test" (unclosed bracket)
or "suite\" (trailing backslash) is silently passed to glob_match()
which handles it gracefully but not in the way the user likely intended
— the bracket is matched as a literal character rather than starting a
character class.

Use glob_validate() to reject malformed patterns early with -EINVAL,
so users get a clear error instead of silently wrong filter results.

Signed-off-by: Josh Law <obje...@objecting.org>
---

David Gow

unread,
Mar 17, 2026, 3:19:59 AM (4 days ago) Mar 17
to Josh Law, Andrew Morton, Brendan Higgins, Rae Moar, linux-k...@vger.kernel.org, kuni...@googlegroups.com, linux-...@vger.kernel.org
Hi Josh,
Thanks: always good to have better test coverage (and those are some
nasty corner cases!)

Series is:

Acked-by: David Gow <da...@davidgow.net>
Tested-by: David Gow <da...@davidgow.net>

I'm making the assumption that this is going in via mm-nonmm, not the
kselftest/kunit tree, as it has dependencies in that branch.

Cheers,
-- David

Josh Law

unread,
Mar 17, 2026, 3:30:35 AM (4 days ago) Mar 17
to David Gow, Andrew Morton, Brendan Higgins, Rae Moar, linux-k...@vger.kernel.org, kuni...@googlegroups.com, linux-...@vger.kernel.org
Cheers David!

Have a great day!


V/R

Josh Law

Josh Law

unread,
Mar 17, 2026, 12:33:17 PM (3 days ago) Mar 17
to David Gow, Andrew Morton, Brendan Higgins, Rae Moar, linux-k...@vger.kernel.org, kuni...@googlegroups.com, linux-...@vger.kernel.org


On 17 March 2026 07:19:47 GMT, David Gow <da...@davidgow.net> wrote:
Also, David? You mean the *ENTIRE* series as in the code too (the new functions) Or just your kunit changes


V/R



Josh Law

David Gow

unread,
Mar 18, 2026, 3:20:03 AM (3 days ago) Mar 18
to Josh Law, Andrew Morton, Brendan Higgins, Rae Moar, linux-k...@vger.kernel.org, kuni...@googlegroups.com, linux-...@vger.kernel.org
I only really need to Ack the KUnit changes, but I did test the entire
series, so feel free to apply it to all of them.

Cheers,
-- David

Josh Law

unread,
Mar 18, 2026, 11:54:35 AM (3 days ago) Mar 18
to David Gow, Andrew Morton, Brendan Higgins, Rae Moar, linux-k...@vger.kernel.org, kuni...@googlegroups.com, linux-...@vger.kernel.org
Hello David, So the new functions are perfectly fine? I mean code review wise I think they are, and I have tested them with a stash driver..

So I guess also


Tested-by: Josh Law <obje...@objecting.org>

Josh Law

unread,
Mar 19, 2026, 6:24:20 PM (2 days ago) Mar 19
to David Gow, Andrew Morton, Brendan Higgins, Rae Moar, linux-k...@vger.kernel.org, kuni...@googlegroups.com, linux-...@vger.kernel.org
Thanks!


Andrew. This is ready to be merged


V/R

Josh Law
Reply all
Reply to author
Forward
0 new messages