From 3abebf3e31bd0b02a82e4c4aaeb40463ab055272 Mon Sep 17 00:00:00 2001 From: Arnaud Le Blanc Date: Mon, 13 Oct 2025 12:57:16 +0200 Subject: [PATCH 1/8] Fix JIT TLS on MacOS The dynamic loader, starting around version 1284, patches the thunk emitted for thread local variables by the compiler, so that its format changes from struct Thunk { void *func; size_t module; size_t offset; } to struct Thunk_v2 { void *func; uint32_t module; uint32_t offset; // other fields } which has the same size, but not the same layout. This is mentionned in https://github.com/apple-oss-distributions/dyld/blob/9307719dd8dc9b385daa412b03cfceb897b2b398/libdyld/ThreadLocalVariables.h#L90 As a result, access to thread specific variables in JIT is broken. Fix by using the new layout when the new dynamic loader is in use. Closes GH-20121 --- NEWS | 2 ++ ext/opcache/jit/zend_jit.c | 4 ++++ ext/opcache/jit/zend_jit_x86.dasc | 40 +++++++++++++++++++++++++++---- 3 files changed, 42 insertions(+), 4 deletions(-) diff --git a/NEWS b/NEWS index 8412c6c2bf48..d8efbffb9c97 100644 --- a/NEWS +++ b/NEWS @@ -18,6 +18,8 @@ PHP NEWS - Opcache: . Fixed bug GH-20081 (access to uninitialized vars in preload_load()). (Arnaud) + . Fixed bug GH-20121 (JIT broken in ZTS builds on MacOS 15). + (Arnaud, Shivam Mathur) - Phar: . Fix memory leak of argument in webPhar. (nielsdos) diff --git a/ext/opcache/jit/zend_jit.c b/ext/opcache/jit/zend_jit.c index 147d0c0b112f..b58b515a6231 100644 --- a/ext/opcache/jit/zend_jit.c +++ b/ext/opcache/jit/zend_jit.c @@ -51,6 +51,10 @@ #include #endif +#if defined(__APPLE__) && defined(__x86_64__) +# include +#endif + #ifdef ZTS int jit_globals_id; #else diff --git a/ext/opcache/jit/zend_jit_x86.dasc b/ext/opcache/jit/zend_jit_x86.dasc index 7061f6b2b73a..358154cc4d1e 100644 --- a/ext/opcache/jit/zend_jit_x86.dasc +++ b/ext/opcache/jit/zend_jit_x86.dasc @@ -2826,6 +2826,25 @@ static zend_never_inline void zend_jit_set_sp_adj_vm(void) } #endif +#if defined(__APPLE__) && defined(__x86_64__) +/* Thunk format used since dydl 1284 (approx. MacOS 15) + * https://github.com/apple-oss-distributions/dyld/blob/9307719dd8dc9b385daa412b03cfceb897b2b398/libdyld/ThreadLocalVariables.h#L146 */ +struct TLV_Thunkv2 +{ + void* func; + uint32_t key; + uint32_t offset; +}; + +/* Thunk format used in earlier versions */ +struct TLV_Thunkv1 +{ + void* func; + size_t key; + size_t offset; +}; +#endif + static int zend_jit_setup(void) { if (!zend_cpu_supports_sse2()) { @@ -2889,12 +2908,25 @@ static int zend_jit_setup(void) # elif defined(__APPLE__) && defined(__x86_64__) tsrm_ls_cache_tcb_offset = tsrm_get_ls_cache_tcb_offset(); if (tsrm_ls_cache_tcb_offset == 0) { - size_t *ti; + struct TLV_Thunkv2 *thunk; __asm__( "leaq __tsrm_ls_cache(%%rip),%0" - : "=r" (ti)); - tsrm_tls_offset = ti[2]; - tsrm_tls_index = ti[1] * 8; + : "=r" (thunk)); + + /* Detect dyld 1284: With dyld 1284, thunk->func will be _tlv_get_addr. + * Unfortunately this symbol is private, but we can find it + * as _tlv_bootstrap+8: https://github.com/apple-oss-distributions/dyld/blob/9307719dd8dc9b385daa412b03cfceb897b2b398/libdyld/threadLocalHelpers.s#L54 + * In earlier versions, thunk->func will be tlv_get_addr, which is not + * _tlv_bootstrap+8. + */ + if (thunk->func == (void*)((char*)_tlv_bootstrap + 8)) { + tsrm_tls_offset = thunk->offset; + tsrm_tls_index = (size_t)thunk->key * 8; + } else { + struct TLV_Thunkv1 *thunkv1 = (struct TLV_Thunkv1*) thunk; + tsrm_tls_offset = thunkv1->offset; + tsrm_tls_index = thunkv1->key * 8; + } } # elif defined(__GNUC__) && defined(__x86_64__) tsrm_ls_cache_tcb_offset = tsrm_get_ls_cache_tcb_offset(); From 8e0504c38b8fecbeef4b552705b5645f5f461b81 Mon Sep 17 00:00:00 2001 From: Niels Dossche <7771979+nielsdos@users.noreply.github.com> Date: Sat, 11 Oct 2025 12:10:37 +0200 Subject: [PATCH 2/8] Partially fix GH-16317: DOM classes do not allow __debugInfo() overrides to work Closes GH-20132. --- NEWS | 4 ++++ ext/dom/php_dom.c | 6 ++++++ ext/dom/tests/gh16317.phpt | 20 ++++++++++++++++++++ 3 files changed, 30 insertions(+) create mode 100644 ext/dom/tests/gh16317.phpt diff --git a/NEWS b/NEWS index d8efbffb9c97..a1e87ca9c507 100644 --- a/NEWS +++ b/NEWS @@ -7,6 +7,10 @@ PHP NEWS . Fixed bug GH-20073 (Assertion failure in WeakMap offset operations on reference). (nielsdos) +- DOM: + . Partially fixed bug GH-16317 (DOM classes do not allow + __debugInfo() overrides to work). (nielsdos) + - FPM: . Fixed bug GH-19974 (fpm_status_export_to_zval segfault for parallel execution). (Jakub Zelenka, txuna) diff --git a/ext/dom/php_dom.c b/ext/dom/php_dom.c index 841dcd66186f..d097081b0bde 100644 --- a/ext/dom/php_dom.c +++ b/ext/dom/php_dom.c @@ -401,6 +401,7 @@ static int dom_property_exists(zend_object *object, zend_string *name, int check } /* }}} */ +/* This custom handler is necessary to avoid a recursive construction of the entire subtree. */ static HashTable* dom_get_debug_info_helper(zend_object *object, int *is_temp) /* {{{ */ { dom_object *obj = php_dom_obj_from_obj(object); @@ -411,6 +412,11 @@ static HashTable* dom_get_debug_info_helper(zend_object *object, int *is_temp) / dom_prop_handler *entry; zend_string *object_str; + /* As we have a custom implementation, we must manually check for overrides. */ + if (object->ce->__debugInfo) { + return zend_std_get_debug_info(object, is_temp); + } + *is_temp = 1; std_props = zend_std_get_properties(object); diff --git a/ext/dom/tests/gh16317.phpt b/ext/dom/tests/gh16317.phpt new file mode 100644 index 000000000000..870c337716f0 --- /dev/null +++ b/ext/dom/tests/gh16317.phpt @@ -0,0 +1,20 @@ +--TEST-- +GH-16317 (DOM classes do not allow __debugInfo() overrides to work) +--FILE-- + 'y']; + } +} + +var_dump(new Demo()); + +?> +--EXPECT-- +object(Demo)#1 (1) { + ["x"]=> + string(1) "y" +} From 390e24397a27639ec993316cedf9720506f53c62 Mon Sep 17 00:00:00 2001 From: Niels Dossche <7771979+nielsdos@users.noreply.github.com> Date: Mon, 13 Oct 2025 18:45:26 +0200 Subject: [PATCH 3/8] Add forgotten NEWS item --- NEWS | 1 + 1 file changed, 1 insertion(+) diff --git a/NEWS b/NEWS index 1ffcaa8b0617..e5b8670de5ce 100644 --- a/NEWS +++ b/NEWS @@ -31,6 +31,7 @@ PHP NEWS . Fix memory leak of argument in webPhar. (nielsdos) . Fix memory leak when setAlias() fails. (nielsdos) . Fix memory leak in phar_parse_zipfile() error handling. (nielsdos) + . Fix file descriptor/memory leak when opening central fp fails. (nielsdos) - Random: . Fix Randomizer::__serialize() w.r.t. INDIRECTs. (nielsdos) From 3b54fa4038bc19821e8200c8c9baf92729789e5a Mon Sep 17 00:00:00 2001 From: Niels Dossche <7771979+nielsdos@users.noreply.github.com> Date: Mon, 13 Oct 2025 18:47:11 +0200 Subject: [PATCH 4/8] Make entry argument nullable for phar_split_fname() to avoid extra allocations (#20146) --- ext/phar/dirstream.c | 14 ++++++-------- ext/phar/func_interceptors.c | 26 +++++++++----------------- ext/phar/phar.c | 22 ++++++++++++---------- ext/phar/phar_object.c | 18 +++++++----------- ext/phar/util.c | 11 ++++------- 5 files changed, 38 insertions(+), 53 deletions(-) diff --git a/ext/phar/dirstream.c b/ext/phar/dirstream.c index c5aa292200e8..f23907ae7623 100644 --- a/ext/phar/dirstream.c +++ b/ext/phar/dirstream.c @@ -349,12 +349,12 @@ int phar_wrapper_mkdir(php_stream_wrapper *wrapper, const char *url_from, int mo { phar_entry_info entry, *e; phar_archive_data *phar = NULL; - char *error, *arch, *entry2; - size_t arch_len, entry_len; + char *error, *arch; + size_t arch_len; php_url *resource = NULL; /* pre-readonly check, we need to know if this is a data phar */ - if (FAILURE == phar_split_fname(url_from, strlen(url_from), &arch, &arch_len, &entry2, &entry_len, 2, 2)) { + if (FAILURE == phar_split_fname(url_from, strlen(url_from), &arch, &arch_len, NULL, NULL, 2, 2)) { php_stream_wrapper_log_error(wrapper, options, "phar error: cannot create directory \"%s\", no phar archive specified", url_from); return 0; } @@ -364,7 +364,6 @@ int phar_wrapper_mkdir(php_stream_wrapper *wrapper, const char *url_from, int mo } efree(arch); - efree(entry2); if (PHAR_G(readonly) && (!phar || !phar->is_data)) { php_stream_wrapper_log_error(wrapper, options, "phar error: cannot create directory \"%s\", write operations disabled", url_from); @@ -477,12 +476,12 @@ int phar_wrapper_rmdir(php_stream_wrapper *wrapper, const char *url, int options { phar_entry_info *entry; phar_archive_data *phar = NULL; - char *error, *arch, *entry2; - size_t arch_len, entry_len; + char *error, *arch; + size_t arch_len; php_url *resource = NULL; /* pre-readonly check, we need to know if this is a data phar */ - if (FAILURE == phar_split_fname(url, strlen(url), &arch, &arch_len, &entry2, &entry_len, 2, 2)) { + if (FAILURE == phar_split_fname(url, strlen(url), &arch, &arch_len, NULL, NULL, 2, 2)) { php_stream_wrapper_log_error(wrapper, options, "phar error: cannot remove directory \"%s\", no phar archive specified, or phar archive does not exist", url); return 0; } @@ -492,7 +491,6 @@ int phar_wrapper_rmdir(php_stream_wrapper *wrapper, const char *url, int options } efree(arch); - efree(entry2); if (PHAR_G(readonly) && (!phar || !phar->is_data)) { php_stream_wrapper_log_error(wrapper, options, "phar error: cannot rmdir directory \"%s\", write operations disabled", url); diff --git a/ext/phar/func_interceptors.c b/ext/phar/func_interceptors.c index 976ce0b5e955..f7e553a45ce8 100644 --- a/ext/phar/func_interceptors.c +++ b/ext/phar/func_interceptors.c @@ -50,12 +50,11 @@ PHP_FUNCTION(phar_opendir) /* {{{ */ goto skip_phar; } - if (SUCCESS == phar_split_fname(ZSTR_VAL(fname), ZSTR_LEN(fname), &arch, &arch_len, &entry, &entry_len, 2, 0)) { + if (SUCCESS == phar_split_fname(ZSTR_VAL(fname), ZSTR_LEN(fname), &arch, &arch_len, NULL, NULL, 2, 0)) { php_stream_context *context = NULL; php_stream *stream; char *name; - efree(entry); entry = estrndup(filename, filename_len); /* fopen within phar, if :// is not in the url, then prepend phar:/// */ entry_len = filename_len; @@ -89,8 +88,8 @@ PHP_FUNCTION(phar_opendir) /* {{{ */ static zend_string* phar_get_name_for_relative_paths(zend_string *filename, bool using_include_path) { - char *arch, *entry; - size_t arch_len, entry_len; + char *arch; + size_t arch_len; zend_string *fname = zend_get_executed_filename_ex(); /* we are checking for existence of a file within the relative path. Chances are good that this is @@ -99,13 +98,10 @@ static zend_string* phar_get_name_for_relative_paths(zend_string *filename, bool return NULL; } - if (FAILURE == phar_split_fname(ZSTR_VAL(fname), ZSTR_LEN(fname), &arch, &arch_len, &entry, &entry_len, 2, 0)) { + if (FAILURE == phar_split_fname(ZSTR_VAL(fname), ZSTR_LEN(fname), &arch, &arch_len, NULL, NULL, 2, 0)) { return NULL; } - efree(entry); - entry = NULL; - entry_len = 0; /* fopen within phar, if :// is not in the url, then prepend phar:/// */ /* retrieving a file defaults to within the current directory, so use this if possible */ phar_archive_data *phar; @@ -122,8 +118,8 @@ static zend_string* phar_get_name_for_relative_paths(zend_string *filename, bool return NULL; } } else { - entry_len = ZSTR_LEN(filename); - entry = phar_fix_filepath(estrndup(ZSTR_VAL(filename), ZSTR_LEN(filename)), &entry_len, 1); + size_t entry_len = ZSTR_LEN(filename); + char *entry = phar_fix_filepath(estrndup(ZSTR_VAL(filename), ZSTR_LEN(filename)), &entry_len, 1); if (entry[0] == '/') { if (!zend_hash_str_exists(&(phar->manifest), entry + 1, entry_len - 1)) { /* this file is not in the phar, use the original path */ @@ -509,9 +505,7 @@ static void phar_file_stat(const char *filename, size_t filename_length, int typ phar = PHAR_G(last_phar); goto splitted; } - if (SUCCESS == phar_split_fname(ZSTR_VAL(fname), ZSTR_LEN(fname), &arch, &arch_len, &entry, &entry_len, 2, 0)) { - - efree(entry); + if (SUCCESS == phar_split_fname(ZSTR_VAL(fname), ZSTR_LEN(fname), &arch, &arch_len, NULL, NULL, 2, 0)) { entry = estrndup(filename, filename_length); /* fopen within phar, if :// is not in the url, then prepend phar:/// */ entry_len = filename_length; @@ -751,10 +745,9 @@ PHP_FUNCTION(phar_is_file) /* {{{ */ goto skip_phar; } - if (SUCCESS == phar_split_fname(ZSTR_VAL(fname), ZSTR_LEN(fname), &arch, &arch_len, &entry, &entry_len, 2, 0)) { + if (SUCCESS == phar_split_fname(ZSTR_VAL(fname), ZSTR_LEN(fname), &arch, &arch_len, NULL, NULL, 2, 0)) { phar_archive_data *phar; - efree(entry); entry = filename; /* fopen within phar, if :// is not in the url, then prepend phar:/// */ entry_len = filename_len; @@ -817,10 +810,9 @@ PHP_FUNCTION(phar_is_link) /* {{{ */ goto skip_phar; } - if (SUCCESS == phar_split_fname(ZSTR_VAL(fname), ZSTR_LEN(fname), &arch, &arch_len, &entry, &entry_len, 2, 0)) { + if (SUCCESS == phar_split_fname(ZSTR_VAL(fname), ZSTR_LEN(fname), &arch, &arch_len, NULL, NULL, 2, 0)) { phar_archive_data *phar; - efree(entry); entry = filename; /* fopen within phar, if :// is not in the url, then prepend phar:/// */ entry_len = filename_len; diff --git a/ext/phar/phar.c b/ext/phar/phar.c index 5a5012adbf18..d2c25dfbdb71 100644 --- a/ext/phar/phar.c +++ b/ext/phar/phar.c @@ -2298,16 +2298,18 @@ zend_result phar_split_fname(const char *filename, size_t filename_len, char **a *arch_len = ext_str - filename + ext_len; *arch = estrndup(filename, *arch_len); - if (ext_str[ext_len]) { - *entry_len = filename_len - *arch_len; - *entry = estrndup(ext_str+ext_len, *entry_len); -#ifdef PHP_WIN32 - phar_unixify_path_separators(*entry, *entry_len); -#endif - *entry = phar_fix_filepath(*entry, entry_len, 0); - } else { - *entry_len = 1; - *entry = estrndup("/", 1); + if (entry) { + if (ext_str[ext_len]) { + *entry_len = filename_len - *arch_len; + *entry = estrndup(ext_str+ext_len, *entry_len); + #ifdef PHP_WIN32 + phar_unixify_path_separators(*entry, *entry_len); + #endif + *entry = phar_fix_filepath(*entry, entry_len, 0); + } else { + *entry_len = 1; + *entry = estrndup("/", 1); + } } #ifdef PHP_WIN32 diff --git a/ext/phar/phar_object.c b/ext/phar/phar_object.c index de58c464cf3c..6d5d39604197 100644 --- a/ext/phar/phar_object.c +++ b/ext/phar/phar_object.c @@ -412,8 +412,8 @@ static void phar_postprocess_ru_web(char *fname, size_t fname_len, char **entry, PHP_METHOD(Phar, running) { zend_string *fname; - char *arch, *entry; - size_t arch_len, entry_len; + char *arch; + size_t arch_len; bool retphar = true; if (zend_parse_parameters(ZEND_NUM_ARGS(), "|b", &retphar) == FAILURE) { @@ -427,9 +427,8 @@ PHP_METHOD(Phar, running) if ( zend_string_starts_with_literal_ci(fname, "phar://") - && SUCCESS == phar_split_fname(ZSTR_VAL(fname), ZSTR_LEN(fname), &arch, &arch_len, &entry, &entry_len, 2, 0) + && SUCCESS == phar_split_fname(ZSTR_VAL(fname), ZSTR_LEN(fname), &arch, &arch_len, NULL, NULL, 2, 0) ) { - efree(entry); if (retphar) { RETVAL_STRINGL(ZSTR_VAL(fname), arch_len + 7); efree(arch); @@ -485,8 +484,7 @@ PHP_METHOD(Phar, mount) } #endif - if (fname_len > 7 && !memcmp(fname, "phar://", 7) && SUCCESS == phar_split_fname(fname, fname_len, &arch, &arch_len, &entry, &entry_len, 2, 0)) { - efree(entry); + if (fname_len > 7 && !memcmp(fname, "phar://", 7) && SUCCESS == phar_split_fname(fname, fname_len, &arch, &arch_len, NULL, NULL, 2, 0)) { entry = NULL; if (path_len > 7 && !memcmp(path, "phar://", 7)) { @@ -1300,9 +1298,9 @@ PHP_METHOD(Phar, getSupportedCompression) /* {{{ Completely remove a phar archive from memory and disk */ PHP_METHOD(Phar, unlinkArchive) { - char *fname, *error, *arch, *entry; + char *fname, *error, *arch; size_t fname_len; - size_t arch_len, entry_len; + size_t arch_len; phar_archive_data *phar; if (zend_parse_parameters(ZEND_NUM_ARGS(), "p", &fname, &fname_len) == FAILURE) { @@ -1329,16 +1327,14 @@ PHP_METHOD(Phar, unlinkArchive) if ( zend_file_name && zend_string_starts_with_literal_ci(zend_file_name, "phar://") - && SUCCESS == phar_split_fname(ZSTR_VAL(zend_file_name), ZSTR_LEN(zend_file_name), &arch, &arch_len, &entry, &entry_len, 2, 0) + && SUCCESS == phar_split_fname(ZSTR_VAL(zend_file_name), ZSTR_LEN(zend_file_name), &arch, &arch_len, NULL, NULL, 2, 0) ) { if (arch_len == fname_len && !memcmp(arch, fname, arch_len)) { zend_throw_exception_ex(phar_ce_PharException, 0, "phar archive \"%s\" cannot be unlinked from within itself", fname); efree(arch); - efree(entry); RETURN_THROWS(); } efree(arch); - efree(entry); } if (phar->is_persistent) { diff --git a/ext/phar/util.c b/ext/phar/util.c index aa30515eff6a..26a11d99a9c1 100644 --- a/ext/phar/util.c +++ b/ext/phar/util.c @@ -267,8 +267,8 @@ zend_result phar_mount_entry(phar_archive_data *phar, char *filename, size_t fil zend_string *phar_find_in_include_path(zend_string *filename, phar_archive_data **pphar) /* {{{ */ { zend_string *ret; - char *path, *arch, *entry, *test; - size_t arch_len, entry_len; + char *path, *arch, *test; + size_t arch_len; phar_archive_data *phar; if (pphar) { @@ -301,12 +301,10 @@ zend_string *phar_find_in_include_path(zend_string *filename, phar_archive_data goto splitted; } - if (!is_file_a_phar_wrapper || SUCCESS != phar_split_fname(ZSTR_VAL(fname), ZSTR_LEN(fname), &arch, &arch_len, &entry, &entry_len, 1, 0)) { + if (!is_file_a_phar_wrapper || SUCCESS != phar_split_fname(ZSTR_VAL(fname), ZSTR_LEN(fname), &arch, &arch_len, NULL, NULL, 1, 0)) { return NULL; } - efree(entry); - if (*ZSTR_VAL(filename) == '.') { size_t try_len; @@ -347,7 +345,7 @@ zend_string *phar_find_in_include_path(zend_string *filename, phar_archive_data if (ret && zend_string_starts_with_literal_ci(ret, "phar://")) { /* found phar:// */ - if (SUCCESS != phar_split_fname(ZSTR_VAL(ret), ZSTR_LEN(ret), &arch, &arch_len, &entry, &entry_len, 1, 0)) { + if (SUCCESS != phar_split_fname(ZSTR_VAL(ret), ZSTR_LEN(ret), &arch, &arch_len, NULL, NULL, 1, 0)) { return ret; } @@ -358,7 +356,6 @@ zend_string *phar_find_in_include_path(zend_string *filename, phar_archive_data } efree(arch); - efree(entry); } return ret; From b529c7709434383bc415661f141ecfe8abed2cf2 Mon Sep 17 00:00:00 2001 From: Niels Dossche <7771979+nielsdos@users.noreply.github.com> Date: Sun, 12 Oct 2025 23:59:19 +0200 Subject: [PATCH 5/8] phar: Fix more alias memory leaks Closes GH-20154. --- NEWS | 3 ++- ext/phar/zip.c | 4 +--- 2 files changed, 3 insertions(+), 4 deletions(-) diff --git a/NEWS b/NEWS index a1e87ca9c507..f322a7e08761 100644 --- a/NEWS +++ b/NEWS @@ -28,7 +28,8 @@ PHP NEWS - Phar: . Fix memory leak of argument in webPhar. (nielsdos) . Fix memory leak when setAlias() fails. (nielsdos) - . Fix memory leak in phar_parse_zipfile() error handling. (nielsdos) + . Fix a bunch of memory leaks in phar_parse_zipfile() error handling. + (nielsdos) . Fix file descriptor/memory leak when opening central fp fails. (nielsdos) - Random: diff --git a/ext/phar/zip.c b/ext/phar/zip.c index c188d6513abf..f1d0edd5bdff 100644 --- a/ext/phar/zip.c +++ b/ext/phar/zip.c @@ -314,6 +314,7 @@ int phar_parse_zipfile(php_stream *fp, char *fname, size_t fname_len, char *alia entry.fp_type = PHAR_FP; entry.is_persistent = mydata->is_persistent; #define PHAR_ZIP_FAIL(errmsg) \ + efree(actual_alias); \ zend_hash_destroy(&mydata->manifest); \ HT_INVALIDATE(&mydata->manifest); \ zend_hash_destroy(&mydata->mounted_dirs); \ @@ -619,7 +620,6 @@ int phar_parse_zipfile(php_stream *fp, char *fname, size_t fname_len, char *alia } if (!entry.uncompressed_filesize) { - efree(actual_alias); php_stream_filter_remove(filter, 1); pefree(entry.filename, entry.is_persistent); PHAR_ZIP_FAIL("unable to read in alias, truncated"); @@ -653,7 +653,6 @@ int phar_parse_zipfile(php_stream *fp, char *fname, size_t fname_len, char *alia } if (!entry.uncompressed_filesize) { - efree(actual_alias); php_stream_filter_remove(filter, 1); pefree(entry.filename, entry.is_persistent); PHAR_ZIP_FAIL("unable to read in alias, truncated"); @@ -677,7 +676,6 @@ int phar_parse_zipfile(php_stream *fp, char *fname, size_t fname_len, char *alia } if (!entry.uncompressed_filesize) { - efree(actual_alias); pefree(entry.filename, entry.is_persistent); PHAR_ZIP_FAIL("unable to read in alias, truncated"); } From 5a7c84f2748544946246932f59272c7a550b4aa0 Mon Sep 17 00:00:00 2001 From: Niels Dossche <7771979+nielsdos@users.noreply.github.com> Date: Mon, 13 Oct 2025 00:15:14 +0200 Subject: [PATCH 6/8] phar: Fix memleak+UAF when opening temp stream in buildFromDirectory() fails Obvious memleak, but can also cause a UAF depending on destruction ordering with lingering PCRE regex instances in the SPL objects. Closes GH-20157. --- NEWS | 2 ++ ext/phar/phar_object.c | 4 ++++ 2 files changed, 6 insertions(+) diff --git a/NEWS b/NEWS index f322a7e08761..b40e46298d9d 100644 --- a/NEWS +++ b/NEWS @@ -31,6 +31,8 @@ PHP NEWS . Fix a bunch of memory leaks in phar_parse_zipfile() error handling. (nielsdos) . Fix file descriptor/memory leak when opening central fp fails. (nielsdos) + . Fix memleak+UAF when opening temp stream in buildFromDirectory() fails. + (nielsdos) - Random: . Fix Randomizer::__serialize() w.r.t. INDIRECTs. (nielsdos) diff --git a/ext/phar/phar_object.c b/ext/phar/phar_object.c index b85777013595..c5bd8da398c0 100644 --- a/ext/phar/phar_object.c +++ b/ext/phar/phar_object.c @@ -1784,6 +1784,10 @@ PHP_METHOD(Phar, buildFromDirectory) pass.ret = return_value; pass.fp = php_stream_fopen_tmpfile(); if (pass.fp == NULL) { + zval_ptr_dtor(&iteriter); + if (apply_reg) { + zval_ptr_dtor(®exiter); + } zend_throw_exception_ex(phar_ce_PharException, 0, "phar \"%s\" unable to create temporary file", phar_obj->archive->fname); RETURN_THROWS(); } From aab5045a5b3c20d0ef99b3491868e40f1deb5e28 Mon Sep 17 00:00:00 2001 From: Niels Dossche <7771979+nielsdos@users.noreply.github.com> Date: Mon, 13 Oct 2025 21:18:13 +0200 Subject: [PATCH 7/8] Add extra checks to Phar::mungServer() (#20141) * Add extra checks to Phar::mungServer() * [ci skip] NEWS/UPGRADING --- NEWS | 5 +++++ UPGRADING | 7 +++++++ ext/phar/phar_object.c | 6 ++++-- .../tests/invalid_string_phar_mungserver.phpt | 15 +++++++++++++++ 4 files changed, 31 insertions(+), 2 deletions(-) create mode 100644 ext/phar/tests/invalid_string_phar_mungserver.phpt diff --git a/NEWS b/NEWS index 993bdda130cd..986783ce042d 100644 --- a/NEWS +++ b/NEWS @@ -20,6 +20,11 @@ PHP NEWS . Fixed bug GH-20051 (apache2 shutdowns when restart is requested during preloading). (Arnaud, welcomycozyhom) +- Phar: + . Support reference values in Phar::mungServer(). (nielsdos) + . Invalid values now throw in Phar::mungServer() instead of being silently + ignored. (nielsdos) + - Standard: . Fixed bug GH-19926 (reset internal pointer earlier while splicing array while COW violation flag is still set). (alexandre-daubois) diff --git a/UPGRADING b/UPGRADING index b9095380ed53..4b166bb5af46 100644 --- a/UPGRADING +++ b/UPGRADING @@ -19,6 +19,10 @@ PHP 8.6 UPGRADE NOTES 1. Backward Incompatible Changes ======================================== +- Phar: + . Invalid values now throw in Phar::mungServer() instead of being silently + ignored. + ======================================== 2. New Features ======================================== @@ -44,6 +48,9 @@ PHP 8.6 UPGRADE NOTES 5. Changed Functions ======================================== +- Phar: + . Phar::mungServer() now supports reference values. + ======================================== 6. New Functions ======================================== diff --git a/ext/phar/phar_object.c b/ext/phar/phar_object.c index a9c36f9fe59e..2ea701743f9f 100644 --- a/ext/phar/phar_object.c +++ b/ext/phar/phar_object.c @@ -903,7 +903,7 @@ PHP_METHOD(Phar, mungServer) phar_request_initialize(); ZEND_HASH_FOREACH_VAL(Z_ARRVAL_P(mungvalues), data) { - + ZVAL_DEREF(data); if (Z_TYPE_P(data) != IS_STRING) { zend_throw_exception_ex(phar_ce_PharException, 0, "Non-string value passed to Phar::mungServer(), expecting an array of any of these strings: PHP_SELF, REQUEST_URI, SCRIPT_FILENAME, SCRIPT_NAME"); RETURN_THROWS(); @@ -917,8 +917,10 @@ PHP_METHOD(Phar, mungServer) PHAR_G(phar_SERVER_mung_list) |= PHAR_MUNG_SCRIPT_NAME; } else if (zend_string_equals_literal(Z_STR_P(data), "SCRIPT_FILENAME")) { PHAR_G(phar_SERVER_mung_list) |= PHAR_MUNG_SCRIPT_FILENAME; + } else { + zend_throw_exception_ex(phar_ce_PharException, 0, "Invalid value passed to Phar::mungServer(), expecting an array of any of these strings: PHP_SELF, REQUEST_URI, SCRIPT_FILENAME, SCRIPT_NAME"); + RETURN_THROWS(); } - // TODO Warning for invalid value? } ZEND_HASH_FOREACH_END(); } /* }}} */ diff --git a/ext/phar/tests/invalid_string_phar_mungserver.phpt b/ext/phar/tests/invalid_string_phar_mungserver.phpt new file mode 100644 index 000000000000..46de113f6c08 --- /dev/null +++ b/ext/phar/tests/invalid_string_phar_mungserver.phpt @@ -0,0 +1,15 @@ +--TEST-- +Passing invalid string to Phar::mungServer() +--FILE-- +getMessage(), "\n"; +} + +?> +--EXPECT-- +Invalid value passed to Phar::mungServer(), expecting an array of any of these strings: PHP_SELF, REQUEST_URI, SCRIPT_FILENAME, SCRIPT_NAME From 88f8c5c0bb2a1917934abb1a2d4946cd35052da3 Mon Sep 17 00:00:00 2001 From: Niels Dossche <7771979+nielsdos@users.noreply.github.com> Date: Fri, 10 Oct 2025 23:22:54 +0200 Subject: [PATCH 8/8] Fix shm corruption with coercion in options of unserialize() Closes GH-20129. --- NEWS | 3 +++ .../shm_corruption_coercion_unserialize_options.phpt | 8 ++++++++ ext/standard/var.c | 7 ++++--- 3 files changed, 15 insertions(+), 3 deletions(-) create mode 100644 ext/standard/tests/serialize/shm_corruption_coercion_unserialize_options.phpt diff --git a/NEWS b/NEWS index b40e46298d9d..81bae73f4988 100644 --- a/NEWS +++ b/NEWS @@ -41,6 +41,9 @@ PHP NEWS . Partially fixed bug GH-16317 (SimpleXML does not allow __debugInfo() overrides to work). (nielsdos) +- Standard: + . Fix shm corruption with coercion in options of unserialize(). (nielsdos) + - XMLReader: . Fix arginfo/zpp violations when LIBXML_SCHEMAS_ENABLED is not available. (nielsdos) diff --git a/ext/standard/tests/serialize/shm_corruption_coercion_unserialize_options.phpt b/ext/standard/tests/serialize/shm_corruption_coercion_unserialize_options.phpt new file mode 100644 index 000000000000..530171b7bdea --- /dev/null +++ b/ext/standard/tests/serialize/shm_corruption_coercion_unserialize_options.phpt @@ -0,0 +1,8 @@ +--TEST-- +Shm corruption with coercion in options of unserialize() +--FILE-- + [0]]); +?> +--EXPECTF-- +Warning: unserialize(): Error at offset 0 of 2 bytes in %s on line %d diff --git a/ext/standard/var.c b/ext/standard/var.c index 795fb366c601..d1ae076992df 100644 --- a/ext/standard/var.c +++ b/ext/standard/var.c @@ -1366,13 +1366,14 @@ PHPAPI void php_unserialize_with_options(zval *return_value, const char *buf, co } if(class_hash && Z_TYPE_P(classes) == IS_ARRAY) { zval *entry; - zend_string *lcname; + zend_string *lcname, *tmp_str, *str; ZEND_HASH_FOREACH_VAL(Z_ARRVAL_P(classes), entry) { - convert_to_string(entry); - lcname = zend_string_tolower(Z_STR_P(entry)); + str = zval_get_tmp_string(entry, &tmp_str); + lcname = zend_string_tolower(str); zend_hash_add_empty_element(class_hash, lcname); zend_string_release_ex(lcname, 0); + zend_tmp_string_release(tmp_str); } ZEND_HASH_FOREACH_END(); /* Exception during string conversion. */