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 ||