From b55c619356c810144a68596f5b15c7d9f1354b73 Mon Sep 17 00:00:00 2001 From: David Carlier Date: Thu, 17 Sep 2026 20:03:18 +0100 Subject: [PATCH 1/9] ext/xsl: XSLTProcessor::importStylesheet() use-after-free during a transformation. Fix #23730 Importing a stylesheet from a php:function callback freed the stylesheet libxslt was still applying, and the transformation methods kept using the stale pointer to save the result. The transformation depth is now tracked on the object and importStylesheet() throws while it is non-zero. Close GH-23737 --- NEWS | 4 ++ ext/xsl/php_xsl.h | 1 + ext/xsl/tests/gh23730.phpt | 81 ++++++++++++++++++++++++++++++++++++++ ext/xsl/xsltprocessor.c | 12 +++++- 4 files changed, 96 insertions(+), 2 deletions(-) create mode 100644 ext/xsl/tests/gh23730.phpt diff --git a/NEWS b/NEWS index 42534ef4c376..c38d108d840a 100644 --- a/NEWS +++ b/NEWS @@ -40,6 +40,10 @@ PHP NEWS . Fixed a crash when SQLite3::close() is called from a userland callback. (Ilia Alshanetsky) +- XSL: + . Fixed bug GH-23730 (use-after-free when XSLTProcessor::importStylesheet() + is called during a transformation). (David Carlier) + - Zip: . Fixed ZipArchive::extractTo() ignoring files given in a non-list array. (David Carlier) diff --git a/ext/xsl/php_xsl.h b/ext/xsl/php_xsl.h index 36bd9cc72844..ac4ac5fc2fba 100644 --- a/ext/xsl/php_xsl.h +++ b/ext/xsl/php_xsl.h @@ -55,6 +55,7 @@ extern zend_module_entry xsl_module_entry; typedef struct xsl_object { void *ptr; HashTable *parameter; + uint32_t transform_depth; bool hasKeys; php_libxml_ref_obj *sheet_ref_obj; zend_long securityPrefs; diff --git a/ext/xsl/tests/gh23730.phpt b/ext/xsl/tests/gh23730.phpt new file mode 100644 index 000000000000..893f6277734b --- /dev/null +++ b/ext/xsl/tests/gh23730.phpt @@ -0,0 +1,81 @@ +--TEST-- +GH-23730 (Use-after-free when a stylesheet is imported during a transformation) +--EXTENSIONS-- +dom +xsl +--CREDITS-- +djarfluka +--FILE-- +importStylesheet($GLOBALS['other']); + echo $from, ': no error', PHP_EOL; + } catch (Error $e) { + echo $from, ': ', $e::class, ': ', $e->getMessage(), PHP_EOL; + } +} + +function callback($nodes) { + import_other('callback'); + return $nodes[0]; +} + +$xml = new DOMDocument(); +$xml->registerNodeClass(DOMElement::class, MyElement::class); +$xml->loadXML('a'); + +$xsl = new DOMDocument(); +$xsl->loadXML(<< + + + +XML); + +$other = new DOMDocument(); +$other->loadXML('REPLACED'); + +$proc = new XSLTProcessor(); +$proc->registerPHPFunctions(); +$proc->importStylesheet($xsl); + +$GLOBALS['proc'] = $proc; +$GLOBALS['other'] = $other; + +var_dump($proc->transformToXml($xml)); +var_dump($proc->transformToDoc($xml)->textContent); + +$uri = tempnam(sys_get_temp_dir(), 'gh23730'); +var_dump($proc->transformToUri($xml, $uri) > 0); +@unlink($uri); + +/* Importing outside of a transformation is still allowed. */ +var_dump($proc->importStylesheet($other)); +var_dump($proc->transformToXml($xml)); + +?> +--EXPECT-- +callback: Error: Cannot call XSLTProcessor::importStylesheet() while a transformation is in progress +destructor: Error: Cannot call XSLTProcessor::importStylesheet() while a transformation is in progress +string(24) " +a +" +callback: Error: Cannot call XSLTProcessor::importStylesheet() while a transformation is in progress +destructor: Error: Cannot call XSLTProcessor::importStylesheet() while a transformation is in progress +string(1) "a" +callback: Error: Cannot call XSLTProcessor::importStylesheet() while a transformation is in progress +destructor: Error: Cannot call XSLTProcessor::importStylesheet() while a transformation is in progress +bool(true) +bool(true) +string(31) " +REPLACED +" diff --git a/ext/xsl/xsltprocessor.c b/ext/xsl/xsltprocessor.c index cf5a941d95ca..fa6b2d41e601 100644 --- a/ext/xsl/xsltprocessor.c +++ b/ext/xsl/xsltprocessor.c @@ -175,6 +175,12 @@ PHP_METHOD(XSLTProcessor, importStylesheet) RETURN_THROWS(); } + xsl_object *intern = Z_XSL_P(id); + if (UNEXPECTED(intern->transform_depth > 0)) { + zend_throw_error(NULL, "Cannot call XSLTProcessor::importStylesheet() while a transformation is in progress"); + RETURN_THROWS(); + } + nodep = php_libxml_import_node(docp); if (nodep == NULL) { zend_argument_type_error(1, "must be a valid XML node"); @@ -251,8 +257,6 @@ PHP_METHOD(XSLTProcessor, importStylesheet) RETURN_FALSE; } - xsl_object *intern = Z_XSL_P(id); - /* Detach object */ clone_lxml_obj->document->ptr = NULL; /* The namespace mappings need to be kept alive. @@ -333,6 +337,8 @@ static xmlDocPtr php_xsl_apply_stylesheet(zval *id, xsl_object *intern, xsltStyl return NULL; } + intern->transform_depth++; + if (intern->profiling) { if (php_check_open_basedir(ZSTR_VAL(intern->profiling))) { f = NULL; @@ -438,6 +444,8 @@ static xmlDocPtr php_xsl_apply_stylesheet(zval *id, xsl_object *intern, xsltStyl efree(intern->doc); intern->doc = NULL; + intern->transform_depth--; + return newdocp; } From 132403de39604a7e7a3fb04276dde54d6326c9aa Mon Sep 17 00:00:00 2001 From: David Carlier Date: Fri, 18 Sep 2026 05:04:30 +0100 Subject: [PATCH 2/9] ext/zip: ZipArchive::close() use-after-free from a progress or cancel callback. Fix #23747 ZipArchive::close() called from a progress or cancel callback ran a nested zip_close() that failed, then zip_discard() freed the archive while the outer zip_close() from close() or open() was still using it. Track the close in progress and throw an Error from close() and open() meanwhile. Close GH-23749 --- NEWS | 2 + ext/zip/php_zip.c | 17 ++++++- ext/zip/php_zip.h | 1 + ext/zip/tests/gh23747.phpt | 102 +++++++++++++++++++++++++++++++++++++ 4 files changed, 121 insertions(+), 1 deletion(-) create mode 100644 ext/zip/tests/gh23747.phpt diff --git a/NEWS b/NEWS index c38d108d840a..c8eb609cc2a6 100644 --- a/NEWS +++ b/NEWS @@ -47,6 +47,8 @@ PHP NEWS - Zip: . Fixed ZipArchive::extractTo() ignoring files given in a non-list array. (David Carlier) + . Fixed bug GH-23747 (ZipArchive::close() use-after-free from a progress or + cancel callback). (David Carlier) 24 Sep 2026, PHP 8.4.26 diff --git a/ext/zip/php_zip.c b/ext/zip/php_zip.c index 682736d4fe76..42a5a527cfc7 100644 --- a/ext/zip/php_zip.c +++ b/ext/zip/php_zip.c @@ -1582,8 +1582,16 @@ PHP_METHOD(ZipArchive, open) if (ze_obj->archive) { /* we already have an opened zip, free it */ + if (ze_obj->archive->close) { + efree(resolved_path); + zend_throw_error(NULL, "Already being closed"); + RETURN_THROWS(); + } intern = ze_obj->archive->za; - if (zip_close(intern) != 0) { + ze_obj->archive->close = true; + err = zip_close(intern); + ze_obj->archive->close = false; + if (err != 0) { php_error_docref(NULL, E_WARNING, "Empty string as source"); efree(resolved_path); RETURN_FALSE; @@ -1668,7 +1676,14 @@ PHP_METHOD(ZipArchive, close) ze_obj = Z_ZIP_P(self); + if (ze_obj->archive->close) { + zend_throw_error(NULL, "Already being closed"); + RETURN_THROWS(); + } + + ze_obj->archive->close = true; err = zip_close(intern); + ze_obj->archive->close = false; if (err) { php_error_docref(NULL, E_WARNING, "%s", zip_strerror(intern)); /* Save error for property reader */ diff --git a/ext/zip/php_zip.h b/ext/zip/php_zip.h index 8385674a1cf0..e5930c87aada 100644 --- a/ext/zip/php_zip.h +++ b/ext/zip/php_zip.h @@ -73,6 +73,7 @@ typedef struct _php_zip_archive { /* libzip reads buffers until the archive is closed, can outlive the object. */ char **buffers; int buffers_cnt; + bool close; #ifdef HAVE_PROGRESS_CALLBACK zval progress_callback; #endif diff --git a/ext/zip/tests/gh23747.phpt b/ext/zip/tests/gh23747.phpt new file mode 100644 index 000000000000..24e977410e17 --- /dev/null +++ b/ext/zip/tests/gh23747.phpt @@ -0,0 +1,102 @@ +--TEST-- +GH-23747 (ZipArchive::close() from inside a progress or cancel callback causes segv) +--CREDITS-- +djarfluka +--EXTENSIONS-- +zip +--SKIPIF-- + +--FILE-- +open($filename, ZipArchive::CREATE | ZipArchive::OVERWRITE); + for ($i = 0; $i < 64; $i++) { + $zip->addFromString("f$i.txt", str_repeat('x', 2000)); + } +} + +$filename = __DIR__ . '/gh23747.zip'; + +$zip = new ZipArchive(); +populate($zip, $filename); +$zip->registerProgressCallback(0.0, function ($rate) use ($zip) { + static $done = false; + if (!$done) { + $done = true; + try { + $zip->close(); + } catch (Error $e) { + echo $e::class, ': ', $e->getMessage(), PHP_EOL; + } + } +}); +var_dump($zip->close()); + +$zip = new ZipArchive(); +populate($zip, $filename); +$zip->registerCancelCallback(function () use ($zip) { + static $done = false; + if (!$done) { + $done = true; + try { + $zip->close(); + } catch (Error $e) { + echo $e::class, ': ', $e->getMessage(), PHP_EOL; + } + } + return 0; +}); +var_dump($zip->close()); + +$zip = new ZipArchive(); +populate($zip, $filename); +$zip->registerProgressCallback(0.0, function ($rate) use ($zip) { + static $done = false; + if (!$done) { + $done = true; + try { + $zip->close(); + } catch (Error $e) { + echo $e::class, ': ', $e->getMessage(), PHP_EOL; + } + } +}); +var_dump($zip->open($filename)); +var_dump($zip->count()); + +$zip = new ZipArchive(); +populate($zip, $filename); +$zip->registerProgressCallback(0.0, function ($rate) use ($zip, $filename) { + static $done = false; + if (!$done) { + $done = true; + try { + $zip->open($filename); + } catch (Error $e) { + echo $e::class, ': ', $e->getMessage(), PHP_EOL; + } + } +}); +var_dump($zip->close()); +?> +--CLEAN-- + +--EXPECT-- +Error: Already being closed +bool(true) +Error: Already being closed +bool(true) +Error: Already being closed +bool(true) +int(64) +Error: Already being closed +bool(true) From a74f826ff2979a4d46d7c69b78fbb86c0f173373 Mon Sep 17 00:00:00 2001 From: David Carlier Date: Thu, 17 Sep 2026 19:40:13 +0100 Subject: [PATCH 3/9] Fix GH-23729: DOMXPath::__construct() use-after-free during an evaluation Reconstructing the object from a php:function callback freed the context libxml2 was still evaluating, and php_xpath_eval() wrote back into it once the evaluation returned. The evaluation depth is now tracked on the object and __construct() throws while it is non-zero. Close GH-23735 --- NEWS | 2 ++ ext/dom/php_dom.h | 1 + ext/dom/tests/gh23729.phpt | 73 ++++++++++++++++++++++++++++++++++++++ ext/dom/xpath.c | 10 +++++- 4 files changed, 85 insertions(+), 1 deletion(-) create mode 100644 ext/dom/tests/gh23729.phpt diff --git a/NEWS b/NEWS index c8eb609cc2a6..46824ac7d6ae 100644 --- a/NEWS +++ b/NEWS @@ -13,6 +13,8 @@ PHP NEWS . Fixed Dom\HTMLDocument giving attributes the namespace of their element when a fragment is parsed with an xlink, xml or xmlns context element. (Ilia Alshanetsky) + . Fixed bug GH-23729 (DOMXPath::__construct() use-after-free during an + evaluation). (David Carlier) - Intl: . Fixed cloning IntlDateFormatter and MessageFormatter losing PHP-side state diff --git a/ext/dom/php_dom.h b/ext/dom/php_dom.h index 13f49879bb38..d399d745084b 100644 --- a/ext/dom/php_dom.h +++ b/ext/dom/php_dom.h @@ -66,6 +66,7 @@ extern zend_module_entry dom_module_entry; typedef struct dom_xpath_object { php_dom_xpath_callbacks xpath_callbacks; + uint32_t evaluation_depth; bool register_node_ns; dom_object dom; } dom_xpath_object; diff --git a/ext/dom/tests/gh23729.phpt b/ext/dom/tests/gh23729.phpt new file mode 100644 index 000000000000..93ad67fde005 --- /dev/null +++ b/ext/dom/tests/gh23729.phpt @@ -0,0 +1,73 @@ +--TEST-- +GH-23729 (Use-after-free when DOMXPath is reconstructed during an evaluation) +--CREDITS-- +djarfluka +--EXTENSIONS-- +dom +--FILE-- +__construct($GLOBALS['other']); + } catch (Error $e) { + echo $e::class, ': ', $e->getMessage(), PHP_EOL; + } + return 'r'; +} + +function nested() { + echo 'nested: ', $GLOBALS['xpath']->evaluate('string(/root/b)'), PHP_EOL; + return 'n'; +} + +function test(string $class, object $doc, object $other) { + $xpath = new $class($doc); + $xpath->registerNamespace('php', 'http://php.net/xpath'); + $xpath->registerPhpFunctions(); + + $GLOBALS['xpath'] = $xpath; + $GLOBALS['other'] = $other; + + var_dump($xpath->evaluate('string(php:function("reconstruct"))')); + /* The evaluation the callback tried to destroy must still be usable. */ + var_dump($xpath->evaluate('string(/root/a)')); + /* A nested evaluation must not lift the guard of the outer one. */ + var_dump($xpath->evaluate('concat(php:function("nested"), php:function("reconstruct"))')); + var_dump($xpath->query('//b[php:function("reconstruct")]')->length); + + /* Reconstructing outside of an evaluation is still allowed. */ + $xpath->__construct($other); + var_dump($xpath->document->documentElement->nodeName); +} + +$doc = new DOMDocument(); +$doc->loadXML('12'); +$other = new DOMDocument(); +$other->loadXML(''); +test(DOMXPath::class, $doc, $other); + +$doc = Dom\XMLDocument::createFromString('12'); +$other = Dom\XMLDocument::createFromString(''); +test(Dom\XPath::class, $doc, $other); + +?> +--EXPECT-- +Error: Cannot call DOMXPath::__construct() while an XPath evaluation is in progress +string(1) "r" +string(1) "1" +nested: 2 +Error: Cannot call DOMXPath::__construct() while an XPath evaluation is in progress +string(2) "nr" +Error: Cannot call DOMXPath::__construct() while an XPath evaluation is in progress +int(1) +string(5) "other" +Error: Cannot call Dom\XPath::__construct() while an XPath evaluation is in progress +string(1) "r" +string(1) "1" +nested: 2 +Error: Cannot call Dom\XPath::__construct() while an XPath evaluation is in progress +string(2) "nr" +Error: Cannot call Dom\XPath::__construct() while an XPath evaluation is in progress +int(1) +string(5) "other" diff --git a/ext/dom/xpath.c b/ext/dom/xpath.c index 4f1b3b52714e..d4367c9aaa60 100644 --- a/ext/dom/xpath.c +++ b/ext/dom/xpath.c @@ -126,6 +126,13 @@ static void dom_xpath_construct(INTERNAL_FUNCTION_PARAMETERS, zend_class_entry * RETURN_THROWS(); } + dom_xpath_object *intern = Z_XPATHOBJ_P(ZEND_THIS); + if (UNEXPECTED(intern->evaluation_depth > 0)) { + zend_throw_error(NULL, "Cannot call %s::__construct() while an XPath evaluation is in progress", + ZSTR_VAL(Z_OBJCE_P(ZEND_THIS)->name)); + RETURN_THROWS(); + } + DOM_GET_OBJ(docp, doc, xmlDocPtr, docobj); xmlXPathContextPtr ctx = xmlXPathNewContext(docp); @@ -134,7 +141,6 @@ static void dom_xpath_construct(INTERNAL_FUNCTION_PARAMETERS, zend_class_entry * RETURN_THROWS(); } - dom_xpath_object *intern = Z_XPATHOBJ_P(ZEND_THIS); xmlXPathContextPtr oldctx = intern->dom.ptr; if (oldctx != NULL) { php_libxml_decrement_doc_ref((php_libxml_node_object *) &intern->dom); @@ -301,7 +307,9 @@ static void php_xpath_eval(INTERNAL_FUNCTION_PARAMETERS, int type, bool modern) ctxp->nsNr = in_scope_ns.count; } + intern->evaluation_depth++; xmlXPathObjectPtr xpathobjp = xmlXPathEvalExpression(BAD_CAST expr, ctxp); + intern->evaluation_depth--; ctxp->node = NULL; if (register_node_ns && nodep != NULL) { From 4e64a2655e9444afaf12970c41d45fbbc9abb2c7 Mon Sep 17 00:00:00 2001 From: tianfenghan Date: Fri, 3 Jul 2026 11:50:34 +0800 Subject: [PATCH 4/9] Fix GH-22567 (Windows ZTS CLI SAPI should refresh its TSRMLS cache during request activation) Closes GH-22568 --- NEWS | 4 ++++ sapi/cli/php_cli.c | 11 ++++++++++- 2 files changed, 14 insertions(+), 1 deletion(-) diff --git a/NEWS b/NEWS index 46824ac7d6ae..0382330fa8c1 100644 --- a/NEWS +++ b/NEWS @@ -2,6 +2,10 @@ PHP NEWS ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||| ?? ??? ????, PHP 8.4.27 +- CLI + . Fix GH-22567 (Windows ZTS CLI SAPI should refresh its TSRMLS cache during + request activation). (matyhtf) + - DOM: . Fixed use-after-free when re-constructing a DOMXPath whose php:function registrations are freed while still reachable from the cycle collector. diff --git a/sapi/cli/php_cli.c b/sapi/cli/php_cli.c index 4b19cf0604c6..6894f57c12c0 100644 --- a/sapi/cli/php_cli.c +++ b/sapi/cli/php_cli.c @@ -356,6 +356,15 @@ static void sapi_cli_log_message(const char *message, int syslog_type_int) /* {{ } /* }}} */ +static int sapi_cli_activate(void) /* {{{ */ +{ +#if defined(PHP_WIN32) && defined(ZTS) + ZEND_TSRMLS_CACHE_UPDATE(); +#endif + return SUCCESS; +} +/* }}} */ + static int sapi_cli_deactivate(void) /* {{{ */ { fflush(stdout); @@ -420,7 +429,7 @@ static sapi_module_struct cli_sapi_module = { php_cli_startup, /* startup */ php_module_shutdown_wrapper, /* shutdown */ - NULL, /* activate */ + sapi_cli_activate, /* activate */ sapi_cli_deactivate, /* deactivate */ sapi_cli_ub_write, /* unbuffered write */ From c2c01aea6c4ef9bf6fee6b8e460ac749e72f105f Mon Sep 17 00:00:00 2001 From: Ilia Alshanetsky Date: Fri, 21 Aug 2026 14:11:26 -0400 Subject: [PATCH 5/9] Fix heap over-read in cli_get_prompt() for empty cli.prompt The prompt parser ran as a do-while, so an empty cli.prompt executed the body on the terminator and scanned past it. Use a while loop and smart_str_extract(), which returns the interned empty string when nothing was appended. No regression test: extra unicode warnings from the over-read depend on heap contents, so a .phpt cannot pin the bug red-before. Closes GH-23415 --- NEWS | 4 ++++ ext/readline/readline_cli.c | 8 ++++---- 2 files changed, 8 insertions(+), 4 deletions(-) diff --git a/NEWS b/NEWS index 0382330fa8c1..d81d3170a6f0 100644 --- a/NEWS +++ b/NEWS @@ -38,6 +38,10 @@ PHP NEWS . Fixed PDOStatement::getColumnMeta() reading out of bounds for an invalid column index. (Ilia Alshanetsky) +- Readline: + . Fixed a heap over-read in the interactive shell prompt when cli.prompt is + set to an empty string. (Ilia Alshanetsky) + - Sockets: . Fixed socket_select() silently truncating sets larger than FD_SETSIZE on Windows. (David Carlier) diff --git a/ext/readline/readline_cli.c b/ext/readline/readline_cli.c index ff5caee9eb7d..8319b3b43641 100644 --- a/ext/readline/readline_cli.c +++ b/ext/readline/readline_cli.c @@ -130,7 +130,7 @@ static zend_string *cli_get_prompt(char *block, char prompt) /* {{{ */ char *prompt_spec = CLIR_G(prompt) ? CLIR_G(prompt) : DEFAULT_PROMPT; bool unicode_warned = false; - do { + while (*prompt_spec) { if (*prompt_spec == '\\') { switch (prompt_spec[1]) { case '\\': @@ -198,9 +198,9 @@ static zend_string *cli_get_prompt(char *block, char prompt) /* {{{ */ smart_str_appendc(&retval, '?'); } } - } while (++prompt_spec && *prompt_spec); - smart_str_0(&retval); - return retval.s; + ++prompt_spec; + } + return smart_str_extract(&retval); } /* }}} */ From 8d0d6307aaca39223d5dd9461dc8dea90286a3bb Mon Sep 17 00:00:00 2001 From: Ilia Alshanetsky Date: Fri, 21 Aug 2026 13:13:21 -0400 Subject: [PATCH 6/9] Fix three Win32-only defects in proc_open descriptor handling init_process_info() memset the pointer parameter instead of the PROCESS_INFORMATION structure it points at, leaving the struct uninitialized before CreateProcessW(). Zero it through the pointer. find_comspec_nt() dereferences *comspec in its cleanup while the caller only assigns it on success, so a failed SearchPathW() read an indeterminate value. Initialize the caller's variable to NULL. set_proc_descriptor_to_blackhole() tested CreateFileA() against NULL, but CreateFileA() signals failure with INVALID_HANDLE_VALUE, so a failed open went undetected and an invalid handle was inherited by the child. Test against INVALID_HANDLE_VALUE. Closes GH-23412 --- NEWS | 5 +++++ ext/standard/proc_open.c | 6 +++--- 2 files changed, 8 insertions(+), 3 deletions(-) diff --git a/NEWS b/NEWS index d81d3170a6f0..51580b6c7781 100644 --- a/NEWS +++ b/NEWS @@ -50,6 +50,11 @@ PHP NEWS . Fixed a crash when SQLite3::close() is called from a userland callback. (Ilia Alshanetsky) +- Standard: + . Fixed three Windows-only proc_open() defects: an uninitialized + PROCESS_INFORMATION, an indeterminate comspec pointer after a failed + lookup, and an unchecked CreateFileA() failure. (Ilia Alshanetsky) + - XSL: . Fixed bug GH-23730 (use-after-free when XSLTProcessor::importStylesheet() is called during a transformation). (David Carlier) diff --git a/ext/standard/proc_open.c b/ext/standard/proc_open.c index d2d51de5a856..bd4cf7a0a0ee 100644 --- a/ext/standard/proc_open.c +++ b/ext/standard/proc_open.c @@ -695,7 +695,7 @@ static void init_startup_info(STARTUPINFOW *si, descriptorspec_item *descriptors static void init_process_info(PROCESS_INFORMATION *pi) { - memset(&pi, 0, sizeof(pi)); + memset(pi, 0, sizeof(*pi)); } /* on success, returns length of *comspec, which then needs to be efree'd by caller */ @@ -746,7 +746,7 @@ static size_t find_comspec_nt(wchar_t **comspec) static zend_result convert_command_to_use_shell(wchar_t **cmdw, size_t cmdw_len) { - wchar_t *comspec; + wchar_t *comspec = NULL; size_t len = find_comspec_nt(&comspec); if (len == 0) { php_error_docref(NULL, E_WARNING, "Command conversion failed"); @@ -829,7 +829,7 @@ static zend_result set_proc_descriptor_to_blackhole(descriptorspec_item *desc) #ifdef PHP_WIN32 desc->childend = CreateFileA("nul", GENERIC_READ | GENERIC_WRITE, FILE_SHARE_READ | FILE_SHARE_WRITE, NULL, OPEN_EXISTING, 0, NULL); - if (desc->childend == NULL) { + if (desc->childend == INVALID_HANDLE_VALUE) { php_error_docref(NULL, E_WARNING, "Failed to open nul"); return FAILURE; } From 3213c3faa641265868f0d6aec3c17462382d5809 Mon Sep 17 00:00:00 2001 From: ndossche <7771979+ndossche@users.noreply.github.com> Date: Sat, 19 Sep 2026 20:04:37 +0200 Subject: [PATCH 7/9] Fix OSS-Fuzz #546798343: Heap-buffer-overflow in zend_delete_call_instructions with callable conversion Call level counter was broken: ZEND_CALLABLE_CONVERT was forgotten. Closes GH-23777. --- NEWS | 4 ++++ Zend/Optimizer/optimize_func_calls.c | 1 + ext/opcache/tests/opt/oss_fuzz_546798343.phpt | 19 +++++++++++++++++++ 3 files changed, 24 insertions(+) create mode 100644 ext/opcache/tests/opt/oss_fuzz_546798343.phpt diff --git a/NEWS b/NEWS index 51580b6c7781..2eef981bcf60 100644 --- a/NEWS +++ b/NEWS @@ -34,6 +34,10 @@ PHP NEWS . Fixed bug GH-23106 (mb_strpos() reads past the end of a haystack ending in a truncated UTF-8 sequence). (Lazizbek Ergashev) +- Opcache: + . Fixed OSS-Fuzz #546798343 (Heap-buffer-overflow in optimizer with + FCCs and inlining). (ndossche) + - PDO: . Fixed PDOStatement::getColumnMeta() reading out of bounds for an invalid column index. (Ilia Alshanetsky) diff --git a/Zend/Optimizer/optimize_func_calls.c b/Zend/Optimizer/optimize_func_calls.c index ce6c43afaedb..5449535c560a 100644 --- a/Zend/Optimizer/optimize_func_calls.c +++ b/Zend/Optimizer/optimize_func_calls.c @@ -63,6 +63,7 @@ static void zend_delete_call_instructions(zend_op_array *op_array, zend_op *opli case ZEND_DO_ICALL: case ZEND_DO_UCALL: case ZEND_DO_FCALL_BY_NAME: + case ZEND_CALLABLE_CONVERT: call++; break; case ZEND_SEND_VAL: diff --git a/ext/opcache/tests/opt/oss_fuzz_546798343.phpt b/ext/opcache/tests/opt/oss_fuzz_546798343.phpt new file mode 100644 index 000000000000..f7f058a11652 --- /dev/null +++ b/ext/opcache/tests/opt/oss_fuzz_546798343.phpt @@ -0,0 +1,19 @@ +--TEST-- +OSS-Fuzz #546798343 (Heap-buffer-overflow in zend_delete_call_instructions with callable conversion) +--EXTENSIONS-- +opcache +--INI-- +opcache.enable=1 +opcache.enable_cli=1 +--FILE-- + +--EXPECT-- +Done From 9c2acc343e55dc59bd92cd5acfdadee4787772d7 Mon Sep 17 00:00:00 2001 From: Ilia Alshanetsky Date: Wed, 16 Sep 2026 08:35:00 -0400 Subject: [PATCH 8/9] Fix GH-23693: JIT guard branches on stale flags across basic blocks The x86 matcher folds v = BINOP(a, b); c = CMP(v, 0); GUARD(c) into IR_GUARD_JCC_INT, emitting the BINOP, dropping the CMP and branching on the flags the BINOP left, but it only required the BINOP to precede the CMP in the IR. Once GCM hoists a loop-invariant BINOP into a dominating block, the jcc reads flags the intervening code has clobbered and the guard fires on whatever is in EFLAGS. Require the BINOP to sit in the guard's block and allow only snapshots between the comparison and the guard, the way ir_match_fuse_load() pairs ir_in_same_block() with ir_match_has_mem_deps(). The sibling MEM_BINOP fold already checks the block, the IF side folds are pinned by full ref adjacency, and ir_aarch64.dasc has no guard fold. Mirrors the upstream fix dstogov/ir@51107a3. Fixes GH-23693 Closes GH-23711 --- NEWS | 2 ++ ext/opcache/jit/ir/ir_x86.dasc | 19 +++++++++++++- ext/opcache/tests/jit/gh23693.phpt | 41 ++++++++++++++++++++++++++++++ 3 files changed, 61 insertions(+), 1 deletion(-) create mode 100644 ext/opcache/tests/jit/gh23693.phpt diff --git a/NEWS b/NEWS index 2eef981bcf60..982945e5ebfc 100644 --- a/NEWS +++ b/NEWS @@ -37,6 +37,8 @@ PHP NEWS - Opcache: . Fixed OSS-Fuzz #546798343 (Heap-buffer-overflow in optimizer with FCCs and inlining). (ndossche) + . Fixed bug GH-23693 (Tracing JIT produces wrong results for a guard on a + loop-invariant addition). (Ilia Alshanetsky) - PDO: . Fixed PDOStatement::getColumnMeta() reading out of bounds for an invalid diff --git a/ext/opcache/jit/ir/ir_x86.dasc b/ext/opcache/jit/ir/ir_x86.dasc index ca42001a8816..f5efb66698da 100644 --- a/ext/opcache/jit/ir/ir_x86.dasc +++ b/ext/opcache/jit/ir/ir_x86.dasc @@ -1923,6 +1923,21 @@ static bool ir_match_has_mem_deps(ir_ctx *ctx, ir_ref ref, ir_ref root) return 0; } +/* A naive check if anything that emits code, and so clobbers the flags, is + * scheduled between the flags setting instruction and the fusion root */ +static bool ir_match_has_flags_deps(ir_ctx *ctx, ir_ref ref, ir_ref root) +{ + ir_ref pos = ctx->prev_ref[root]; + + while (pos > ref) { + if (ctx->ir_base[pos].op != IR_SNAPSHOT) { + return 1; + } + pos = ctx->prev_ref[pos]; + } + return pos != ref; +} + static void ir_match_fuse_load(ir_ctx *ctx, ir_ref ref, ir_ref root) { if (ir_in_same_block(ctx, ref) && @@ -3089,7 +3104,9 @@ store_int: if (IR_IS_CONST_REF(op2_insn->op2) && !IR_IS_SYM_CONST(ctx->ir_base[op2_insn->op2].op) && ctx->ir_base[op2_insn->op2].val.i64 == 0) { - if (op2_insn->op1 == insn->op2 - 1) { /* previous instruction */ + if (op2_insn->op1 == insn->op2 - 1 /* previous instruction */ + && ir_in_same_block(ctx, op2_insn->op1) + && !ir_match_has_flags_deps(ctx, insn->op2, ref)) { ir_insn *op1_insn = &ctx->ir_base[op2_insn->op1]; if ((op1_insn->op == IR_OR || op1_insn->op == IR_AND || op1_insn->op == IR_XOR) || diff --git a/ext/opcache/tests/jit/gh23693.phpt b/ext/opcache/tests/jit/gh23693.phpt new file mode 100644 index 000000000000..1540f0c1368f --- /dev/null +++ b/ext/opcache/tests/jit/gh23693.phpt @@ -0,0 +1,41 @@ +--TEST-- +GH-23693: Tracing JIT reads stale flags for a guard on a hoisted addition +--INI-- +opcache.enable=1 +opcache.enable_cli=1 +opcache.jit_buffer_size=64M +opcache.jit=tracing +opcache.jit_hot_func=1 +--EXTENSIONS-- +opcache +--FILE-- +> 2; + $s = 0; + for ($dy = -1; $dy <= 1; ++$dy) { + for ($i = 0; $i < $n; ++$i) { + $ny = $y + $dy; + if ($ny < 0) { + continue; + } + if ($ny >= 4) { + continue; + } + $s += ($ny << 2) | ($i & 3); + } + } + return $s; +} +for ($k = 0; $k < 30; ++$k) { + f(16, 200); +} +var_dump(f(16, 200)); +var_dump(f(0, 200)); +?> +--EXPECT-- +int(2700) +int(1400) From 70bc729f736639502d2cbfe836f8c60408b6edde Mon Sep 17 00:00:00 2001 From: Ilia Alshanetsky Date: Mon, 24 Aug 2026 16:10:30 -0400 Subject: [PATCH 9/9] ext/standard: Bound-check RR parsing in the dns_get_mx() answer loop The answer loop read type, class, ttl, rdlength and weight with raw GETSHORT() after an unvalidated dn_skipname() advance, so a reply whose last record name ends at the end of the received data read up to about 12 bytes past the 64K querybuf stack union, with the weight reaching userland through the $weights array. Guard the fixed-size header fields the way php_parserr() already does; rdata skips stay bounded by the cp < end loop condition. --- NEWS | 2 ++ ext/standard/dns.c | 8 ++++++++ 2 files changed, 10 insertions(+) diff --git a/NEWS b/NEWS index 982945e5ebfc..6fcc446e5ee0 100644 --- a/NEWS +++ b/NEWS @@ -60,6 +60,8 @@ PHP NEWS . Fixed three Windows-only proc_open() defects: an uninitialized PROCESS_INFORMATION, an indeterminate comspec pointer after a failed lookup, and an unchecked CreateFileA() failure. (Ilia Alshanetsky) + . Fixed out-of-bounds read when parsing a malformed MX answer in + dns_get_mx(). (Ilia Alshanetsky) - XSL: . Fixed bug GH-23730 (use-after-free when XSLTProcessor::importStylesheet() diff --git a/ext/standard/dns.c b/ext/standard/dns.c index 64301ed04161..61319565da7b 100644 --- a/ext/standard/dns.c +++ b/ext/standard/dns.c @@ -1120,6 +1120,10 @@ PHP_FUNCTION(dns_get_mx) RETURN_FALSE; } cp += i; + if (cp + 10 > end) { + php_dns_free_handle(handle); + RETURN_FALSE; + } GETSHORT(type, cp); cp += INT16SZ + INT32SZ; GETSHORT(i, cp); @@ -1127,6 +1131,10 @@ PHP_FUNCTION(dns_get_mx) cp += i; continue; } + if (cp + 2 > end) { + php_dns_free_handle(handle); + RETURN_FALSE; + } GETSHORT(weight, cp); if ((i = dn_expand(answer.qb2, end, cp, buf, sizeof(buf)-1)) < 0) { php_dns_free_handle(handle);