From 84b0a73685d1218abe21764fdabb54d1a0b42b79 Mon Sep 17 00:00:00 2001 From: Nora Dossche <7771979+ndossche@users.noreply.github.com> Date: Tue, 3 Mar 2026 22:29:07 +0100 Subject: [PATCH 1/3] Fix GH-13204: glob() fails if square bracket is in current directory (#19647) The problem is not limited to square brackets, but to every meta character. The solution is to override the glob functions for handling paths with the VCWD ones in PHP. If that is not available, use the old but limited workaround. --- NEWS | 2 + ext/standard/dir.c | 56 ++++++++++++++++--- ext/standard/tests/file/gh13204.phpt | 12 ++++ .../tests/file/gh13204[brackets]/empty.txt | 0 4 files changed, 61 insertions(+), 9 deletions(-) create mode 100644 ext/standard/tests/file/gh13204.phpt create mode 100644 ext/standard/tests/file/gh13204[brackets]/empty.txt diff --git a/NEWS b/NEWS index cf36b1bad7c7f..75332b89a1ef2 100644 --- a/NEWS +++ b/NEWS @@ -123,6 +123,8 @@ PHP NEWS defaulted to 0. (Jorg Sowa) . Fixed bug GH-21058 (error_log() crashes with message_type 3 and null destination). (David Carlier) + . Fixed bug GH-13204 (glob() fails if square bracket is in current directory). + (ndossche) - Streams: . Added so_keepalive, tcp_keepidle, tcp_keepintvl and tcp_keepcnt stream diff --git a/ext/standard/dir.c b/ext/standard/dir.c index 7c1f8efe68875..730ef6154907e 100644 --- a/ext/standard/dir.c +++ b/ext/standard/dir.c @@ -400,13 +400,34 @@ PHP_FUNCTION(getcwd) /* }}} */ /* {{{ Find pathnames matching a pattern */ +#if defined(ZTS) && defined(PHP_GLOB_ALTDIRFUNC) +static void *php_glob_opendir_wrapper(const char *path) +{ + return VCWD_OPENDIR(path); +} + +static void php_glob_closedir_wrapper(void *dir) +{ + (void) closedir(dir); +} + +static int php_glob_lstat_wrapper(const char *buf, zend_stat_t *sb) +{ + return VCWD_LSTAT(buf, sb); +} + +static int php_glob_stat_wrapper(const char *buf, zend_stat_t *sb) +{ + return VCWD_STAT(buf, sb); +} +#endif + PHP_FUNCTION(glob) { size_t cwd_skip = 0; -#ifdef ZTS +#if defined(ZTS) && !defined(PHP_GLOB_ALTDIRFUNC) char cwd[MAXPATHLEN]; char work_pattern[MAXPATHLEN]; - char *result; #endif char *pattern = NULL; size_t pattern_len; @@ -433,28 +454,45 @@ PHP_FUNCTION(glob) RETURN_FALSE; } + memset(&globbuf, 0, sizeof(globbuf)); + + int passed_glob_flags = flags & PHP_GLOB_FLAGMASK; + #ifdef ZTS if (!IS_ABSOLUTE_PATH(pattern, pattern_len)) { - result = VCWD_GETCWD(cwd, MAXPATHLEN); + /* System glob uses the current work directory which is not thread safe. + * The first fix is to override the functions used to open/read/... paths + * with the VCWD ones used in PHP. + * If that functionality is unavailable for whatever reason, fall back + * to prepending the current working directory to the passed path. + * However, that comes with limitations regarding meta characters + * that is not solvable in general (GH-13204). */ +# ifdef PHP_GLOB_ALTDIRFUNC + globbuf.gl_opendir = php_glob_opendir_wrapper; + globbuf.gl_readdir = (struct dirent *(*)(void *)) readdir; + globbuf.gl_closedir = php_glob_closedir_wrapper; + globbuf.gl_lstat = php_glob_lstat_wrapper; + globbuf.gl_stat = php_glob_stat_wrapper; + passed_glob_flags |= PHP_GLOB_ALTDIRFUNC; +# else + char *result = VCWD_GETCWD(cwd, MAXPATHLEN); if (!result) { cwd[0] = '\0'; } -#ifdef PHP_WIN32 +# ifdef PHP_WIN32 if (IS_SLASH(*pattern)) { cwd[2] = '\0'; } -#endif +# endif cwd_skip = strlen(cwd)+1; snprintf(work_pattern, MAXPATHLEN, "%s%c%s", cwd, DEFAULT_SLASH, pattern); pattern = work_pattern; +# endif } #endif - - memset(&globbuf, 0, sizeof(globbuf)); - globbuf.gl_offs = 0; - if (0 != (ret = php_glob(pattern, flags & PHP_GLOB_FLAGMASK, NULL, &globbuf))) { + if (0 != (ret = php_glob(pattern, passed_glob_flags, NULL, &globbuf))) { #ifdef PHP_GLOB_NOMATCH if (PHP_GLOB_NOMATCH == ret) { /* Some glob implementation simply return no data if no matches diff --git a/ext/standard/tests/file/gh13204.phpt b/ext/standard/tests/file/gh13204.phpt new file mode 100644 index 0000000000000..1af66539195a4 --- /dev/null +++ b/ext/standard/tests/file/gh13204.phpt @@ -0,0 +1,12 @@ +--TEST-- +GH-13204 (glob() fails if square bracket is in current directory) +--FILE-- + +--EXPECT-- +array(1) { + [0]=> + string(11) "./empty.txt" +} diff --git a/ext/standard/tests/file/gh13204[brackets]/empty.txt b/ext/standard/tests/file/gh13204[brackets]/empty.txt new file mode 100644 index 0000000000000..e69de29bb2d1d From 7324cd7f210d255e86fef478a7fad89f0797cd02 Mon Sep 17 00:00:00 2001 From: Ilija Tovilo Date: Tue, 3 Mar 2026 16:53:42 +0100 Subject: [PATCH 2/3] Fix enabling of opcache in benchmark job in older branches 8.4 still needs a zend_extension=opcache.so in the ini file. Closes GH-21332 --- .github/matrix.php | 2 +- .github/workflows/test-suite.yml | 1 + 2 files changed, 2 insertions(+), 1 deletion(-) diff --git a/.github/matrix.php b/.github/matrix.php index 9164dd4b3c001..64bb60519de6b 100644 --- a/.github/matrix.php +++ b/.github/matrix.php @@ -73,7 +73,7 @@ function select_jobs($repository, $trigger, $nightly, $labels, $php_version, $re && ($all_jobs || !$no_jobs || $test_benchmarking) // push trigger is restricted to official repository. && ($repository === 'php/php-src' || $trigger === 'pull_request')) { - $jobs['BENCHMARKING'] = true; + $jobs['BENCHMARKING']['config']['integrated_opcache'] = version_compare($php_version, '8.5', '>='); } if ($all_jobs || $test_community) { $jobs['COMMUNITY']['matrix'] = version_compare($php_version, '8.4', '>=') diff --git a/.github/workflows/test-suite.yml b/.github/workflows/test-suite.yml index 0fd95f67f9c41..9f5496d1d69cb 100644 --- a/.github/workflows/test-suite.yml +++ b/.github/workflows/test-suite.yml @@ -1036,6 +1036,7 @@ jobs: sudo mkdir -p /etc/php.d sudo chmod 777 /etc/php.d echo mysqli.default_socket=/var/run/mysqld/mysqld.sock > /etc/php.d/mysqli.ini + ${{ !fromJson(inputs.branch).jobs.BENCHMARKING.config.integrated_opcache && 'echo zend_extension=opcache.so >> /etc/php.d/opcache.ini' || '' }} echo opcache.enable=1 >> /etc/php.d/opcache.ini echo opcache.enable_cli=1 >> /etc/php.d/opcache.ini - name: Setup From 7a1c2612c01d9bd4d0c2d128fb5b2bf8f0a2e671 Mon Sep 17 00:00:00 2001 From: tekimen Date: Wed, 4 Mar 2026 09:47:26 +0900 Subject: [PATCH 3/3] [RFC] Add grapheme_strrev function (#20949) * [RFC] Add grapheme_strrev function Add more tests Arabic for grapheme_strrev function. --- NEWS | 1 + UPGRADING | 4 ++ ext/intl/grapheme/grapheme_string.cpp | 59 ++++++++++++++++++++++++++ ext/intl/php_intl.stub.php | 2 + ext/intl/php_intl_arginfo.h | 8 +++- ext/intl/tests/grapheme_strrev.phpt | Bin 0 -> 866 bytes 6 files changed, 73 insertions(+), 1 deletion(-) create mode 100644 ext/intl/tests/grapheme_strrev.phpt diff --git a/NEWS b/NEWS index 75332b89a1ef2..4e1196fb09466 100644 --- a/NEWS +++ b/NEWS @@ -37,6 +37,7 @@ PHP NEWS (BogdanUngureanu) . Fixed bug GH-20426 (Spoofchecker::setRestrictionLevel() error message suggests missing constants). (DanielEScherzer) + . Added grapheme_strrev (Yuya Hamada) - JSON: . Enriched JSON last error / exception message with error location. diff --git a/UPGRADING b/UPGRADING index 7e47d0ba4817c..454a7b900f352 100644 --- a/UPGRADING +++ b/UPGRADING @@ -137,6 +137,10 @@ PHP 8.6 UPGRADE NOTES . Added ReflectionProperty::isReadable() and ReflectionProperty::isWritable(). RFC: https://wiki.php.net/rfc/isreadable-iswriteable +- Intl: + . `grapheme_strrev()` returns strrev for grapheme cluster unit. + RFC: https://wiki.php.net/rfc/grapheme_strrev + - Standard: . `clamp()` returns the given value if in range, else return the nearest bound. diff --git a/ext/intl/grapheme/grapheme_string.cpp b/ext/intl/grapheme/grapheme_string.cpp index 6dd5a002a65b8..36c0cc0f732c8 100644 --- a/ext/intl/grapheme/grapheme_string.cpp +++ b/ext/intl/grapheme/grapheme_string.cpp @@ -1135,4 +1135,63 @@ U_CFUNC PHP_FUNCTION(grapheme_levenshtein) efree(ustring1); } +U_CFUNC PHP_FUNCTION(grapheme_strrev) +{ + zend_string *string; + UText *ut = nullptr; + UErrorCode ustatus = U_ZERO_ERROR; + UBreakIterator *bi; + char *pstr, *end, *p; + zend_string *ret; + int32_t pos = 0, current = 0, end_len = 0; + unsigned char u_break_iterator_buffer[U_BRK_SAFECLONE_BUFFERSIZE]; + + ZEND_PARSE_PARAMETERS_START(1, 1) + Z_PARAM_STR(string) + ZEND_PARSE_PARAMETERS_END(); + + if (ZSTR_LEN(string) == 0) { + RETURN_EMPTY_STRING(); + } + + pstr = ZSTR_VAL(string); + ut = utext_openUTF8(ut, pstr, ZSTR_LEN(string), &ustatus); + + if (U_FAILURE(ustatus)) { + intl_error_set_code(nullptr, ustatus); + intl_error_set_custom_msg(nullptr, "Error opening UTF-8 text"); + + RETVAL_FALSE; + goto close; + } + + bi = nullptr; + ustatus = U_ZERO_ERROR; + + bi = grapheme_get_break_iterator((void*)u_break_iterator_buffer, &ustatus ); + ret = zend_string_alloc(ZSTR_LEN(string), 0); + p = ZSTR_VAL(ret); + + ubrk_setUText(bi, ut, &ustatus); + pos = ubrk_last(bi); + if (pos == UBRK_DONE) { + goto ubrk_end; + } + + current = ZSTR_LEN(string); + for (end = pstr; pos != UBRK_DONE; ) { + pos = ubrk_previous(bi); + end_len = current - pos; + for (int32_t j = 0; j < end_len; j++) { + *p++ = *(pstr + pos + j); + } + current = pos; + } +ubrk_end: + RETVAL_NEW_STR(ret); + ubrk_close(bi); +close: + utext_close(ut); +} + /* }}} */ diff --git a/ext/intl/php_intl.stub.php b/ext/intl/php_intl.stub.php index 9a8f036865cd5..4bcb8587f786b 100644 --- a/ext/intl/php_intl.stub.php +++ b/ext/intl/php_intl.stub.php @@ -445,6 +445,8 @@ function grapheme_str_split(string $string, int $length = 1): array|false {} function grapheme_levenshtein(string $string1, string $string2, int $insertion_cost = 1, int $replacement_cost = 1, int $deletion_cost = 1, string $locale = ""): int|false {} +function grapheme_strrev(string $string): string|false {} + /** @param int $next */ function grapheme_extract(string $haystack, int $size, int $type = GRAPHEME_EXTR_COUNT, int $offset = 0, &$next = null): string|false {} diff --git a/ext/intl/php_intl_arginfo.h b/ext/intl/php_intl_arginfo.h index e00e51420d46e..81160349980cd 100644 --- a/ext/intl/php_intl_arginfo.h +++ b/ext/intl/php_intl_arginfo.h @@ -1,5 +1,5 @@ /* This is a generated file, edit php_intl.stub.php instead. - * Stub hash: d9e331c3a1ae46f8eae07ef0d39cb9990e74a0d1 */ + * Stub hash: c52fd0def2530be628beedbbcdcfecdcb07449a8 */ ZEND_BEGIN_ARG_WITH_RETURN_OBJ_INFO_EX(arginfo_intlcal_create_instance, 0, 0, IntlCalendar, 1) ZEND_ARG_OBJ_TYPE_MASK(0, timezone, IntlTimeZone|DateTimeZone, MAY_BE_STRING|MAY_BE_NULL, "null") @@ -501,6 +501,10 @@ ZEND_BEGIN_ARG_WITH_RETURN_TYPE_MASK_EX(arginfo_grapheme_levenshtein, 0, 2, MAY_ ZEND_ARG_TYPE_INFO_WITH_DEFAULT_VALUE(0, locale, IS_STRING, 0, "\"\"") ZEND_END_ARG_INFO() +ZEND_BEGIN_ARG_WITH_RETURN_TYPE_MASK_EX(arginfo_grapheme_strrev, 0, 1, MAY_BE_STRING|MAY_BE_FALSE) + ZEND_ARG_TYPE_INFO(0, string, IS_STRING, 0) +ZEND_END_ARG_INFO() + ZEND_BEGIN_ARG_WITH_RETURN_TYPE_MASK_EX(arginfo_grapheme_extract, 0, 2, MAY_BE_STRING|MAY_BE_FALSE) ZEND_ARG_TYPE_INFO(0, haystack, IS_STRING, 0) ZEND_ARG_TYPE_INFO(0, size, IS_LONG, 0) @@ -922,6 +926,7 @@ ZEND_FUNCTION(grapheme_strstr); ZEND_FUNCTION(grapheme_stristr); ZEND_FUNCTION(grapheme_str_split); ZEND_FUNCTION(grapheme_levenshtein); +ZEND_FUNCTION(grapheme_strrev); ZEND_FUNCTION(grapheme_extract); ZEND_FUNCTION(idn_to_ascii); ZEND_FUNCTION(idn_to_utf8); @@ -1113,6 +1118,7 @@ static const zend_function_entry ext_functions[] = { ZEND_FE(grapheme_stristr, arginfo_grapheme_stristr) ZEND_FE(grapheme_str_split, arginfo_grapheme_str_split) ZEND_FE(grapheme_levenshtein, arginfo_grapheme_levenshtein) + ZEND_FE(grapheme_strrev, arginfo_grapheme_strrev) ZEND_FE(grapheme_extract, arginfo_grapheme_extract) ZEND_FE(idn_to_ascii, arginfo_idn_to_ascii) ZEND_FE(idn_to_utf8, arginfo_idn_to_utf8) diff --git a/ext/intl/tests/grapheme_strrev.phpt b/ext/intl/tests/grapheme_strrev.phpt new file mode 100644 index 0000000000000000000000000000000000000000..dff84fbba8e97cdee5af5c0c3ce346df018afd14 GIT binary patch literal 866 zcma))F>As=6vsQ?r+5Ux1dO<+)}f>z6kE`tP>Wo`*$6R)%cWFAkb-oOF5LvdF)kh4 zN&oh2N>~~ zhg4O+Y3Yq-{i@OA-93ldT)U`W>byFwxt5DEhtE2F+;e0FjS%psK#3<}p$f&Kh7v7~ zpL{;O3I~7vYP>*2{tVm3dszxmwbT&$-Mr|tXu_Y#|BK>_ZXqTBOv9Zf{Q8c!zfO>t=x@4(z1DF zqvtWZl$5Z%Gbm67sqLh$aqOjp`P9kCX=scPF|7e)Y&BFni?`xhKi}V