Commit 73e2bad976d for php
commit 73e2bad976d950438aacd9a4829732dcb0010fd1
Author: Nicolas Grekas <nicolas.grekas@gmail.com>
Date: Wed Oct 21 01:35:10 2026 +0800
Fix glob() not negating bracket expressions that start with "^" (#24422)
Since PHP 8.5, glob() uses the bundled implementation on all platforms.
It only recognizes a leading "!" as bracket negation, so patterns such as
"[^a]" match "^" or "a" instead of excluding "a", unlike glibc's glob().
Treat a leading "^" like "!", matching glibc and PHP's fnmatch() behavior,
including on Windows. POSIX leaves this syntax unspecified. Document the
intentional deviation from OpenBSD so it is preserved during future syncs.
Add regression coverage for negated characters and ranges, a leading "]",
and a literal "^" elsewhere in a bracket expression. This was found while
working on symfony/symfony#67133.
Closes #24422
diff --git a/NEWS b/NEWS
index 43496460ac6..b2ac87b48cc 100644
--- a/NEWS
+++ b/NEWS
@@ -106,6 +106,8 @@ PHP NEWS
(Nicolas Grekas)
. Fixed bug GH-24399 (usleep() and time_nanosleep() ignore queued APCs on
Windows). (Nicolas Grekas)
+ . Fixed glob() not negating bracket expressions that start with "^".
+ (Nicolas Grekas)
- Tidy:
. Fixed tidy_repair_string(), tidy_repair_file(), tidy::repairString() and
diff --git a/ext/standard/tests/file/glob_bracket_negation.phpt b/ext/standard/tests/file/glob_bracket_negation.phpt
new file mode 100644
index 00000000000..3f5ef513789
--- /dev/null
+++ b/ext/standard/tests/file/glob_bracket_negation.phpt
@@ -0,0 +1,30 @@
+--TEST--
+glob() negates a bracket expression that starts with "!" or "^"
+--FILE--
+<?php
+$dir = __DIR__ . '/glob_bracket_negation';
+@mkdir($dir);
+foreach (['a', 'b', 'c', '^', '!'] as $name) {
+ touch("$dir/$name");
+}
+
+foreach (['[!a]', '[^a]', '[!a-b]', '[^a-b]', '[!]a]', '[^]a]', '[a^]'] as $pattern) {
+ echo $pattern, ': ', implode(' ', array_map('basename', glob("$dir/$pattern"))), "\n";
+}
+?>
+--CLEAN--
+<?php
+$dir = __DIR__ . '/glob_bracket_negation';
+foreach (['a', 'b', 'c', '^', '!'] as $name) {
+ @unlink("$dir/$name");
+}
+@rmdir($dir);
+?>
+--EXPECT--
+[!a]: ! ^ b c
+[^a]: ! ^ b c
+[!a-b]: ! ^ c
+[^a-b]: ! ^ c
+[!]a]: ! ^ b c
+[^]a]: ! ^ b c
+[a^]: ^ a
diff --git a/main/php_glob.c b/main/php_glob.c
index 8edfb8326da..af087c4530a 100644
--- a/main/php_glob.c
+++ b/main/php_glob.c
@@ -107,6 +107,7 @@
#include "charclass.h"
+#define CIRCUMFLEX '^'
#define DOLLAR '$'
#define DOT '.'
#define EOS '\0'
@@ -566,6 +567,12 @@ static int glob0(const Char *pattern, php_glob_t *pglob, struct glob_lim *limitp
switch (c) {
case LBRACKET:
c = *qpatnext;
+ /*
+ * Not in OpenBSD's glob(), keep it when syncing: like fnmatch(),
+ * treat a leading '^' as '!'.
+ */
+ if (c == CIRCUMFLEX)
+ c = NOT;
if (c == NOT)
++qpatnext;
if (*qpatnext == EOS ||