From a135ac77366623e4320a40feda609b9908657f44 Mon Sep 17 00:00:00 2001 From: Arnaud Le Blanc Date: Fri, 20 Mar 2026 13:46:29 +0100 Subject: [PATCH 1/5] TAILLCALL VM: Do not return in ZEND_VM_ENTER_EX, ZEND_VM_LEAVE The TAILLCALL VM returns in ZEND_VM_ENTER_EX and ZEND_VM_LEAVE, but that's not necessary. Redefine ZEND_VM_ENTER_EX, ZEND_VM_LEAVE so that they tailcall instead. This makes the Symfony and bench.php benchmarks about 1% and 3.5% faster. Closes GH-21475 --- UPGRADING | 1 + Zend/zend_vm_execute.h | 4 ++++ Zend/zend_vm_gen.php | 4 ++++ 3 files changed, 9 insertions(+) diff --git a/UPGRADING b/UPGRADING index b7b160820bb2a..e55b9730f71a4 100644 --- a/UPGRADING +++ b/UPGRADING @@ -246,6 +246,7 @@ PHP 8.6 UPGRADE NOTES creation of intermediate Closures, the overhead of calling userland callbacks from internal functions and providing for better insight for the JIT. + . The performance of the TAILCALL VM has been improved. - DOM: . Made splitText() faster and consume less memory. diff --git a/Zend/zend_vm_execute.h b/Zend/zend_vm_execute.h index cbfae90802cfa..7dfedca98d3b9 100644 --- a/Zend/zend_vm_execute.h +++ b/Zend/zend_vm_execute.h @@ -53457,6 +53457,8 @@ static ZEND_OPCODE_HANDLER_RET ZEND_OPCODE_HANDLER_FUNC_CCONV ZEND_NULL_HANDLER( # undef ZEND_VM_RETURN # undef ZEND_VM_DISPATCH_TO_HELPER # undef ZEND_VM_INTERRUPT +# undef ZEND_VM_ENTER_EX +# undef ZEND_VM_LEAVE # define ZEND_VM_TAIL_CALL(call) ZEND_MUSTTAIL return call # define ZEND_VM_CONTINUE() ZEND_VM_TAIL_CALL(opline->handler(ZEND_OPCODE_HANDLER_ARGS_PASSTHRU)) @@ -53468,6 +53470,8 @@ static ZEND_OPCODE_HANDLER_RET ZEND_OPCODE_HANDLER_FUNC_CCONV ZEND_NULL_HANDLER( } while (0) # define ZEND_VM_DISPATCH_TO_LEAVE_HELPER(helper) opline = &call_leave_op; SAVE_OPLINE(); ZEND_VM_CONTINUE() # define ZEND_VM_INTERRUPT() ZEND_VM_TAIL_CALL(zend_interrupt_helper_SPEC_TAILCALL(ZEND_OPCODE_HANDLER_ARGS_PASSTHRU)) +# define ZEND_VM_ENTER_EX() ZEND_VM_INTERRUPT_CHECK(); ZEND_VM_CONTINUE() +# define ZEND_VM_LEAVE() ZEND_VM_CONTINUE() static ZEND_OPCODE_HANDLER_RET ZEND_OPCODE_HANDLER_CCONV zend_interrupt_helper_SPEC_TAILCALL(ZEND_OPCODE_HANDLER_ARGS); static ZEND_OPCODE_HANDLER_RET ZEND_OPCODE_HANDLER_CCONV ZEND_NULL_TAILCALL_HANDLER(ZEND_OPCODE_HANDLER_ARGS); diff --git a/Zend/zend_vm_gen.php b/Zend/zend_vm_gen.php index f8989b2336f47..ed0256832d589 100755 --- a/Zend/zend_vm_gen.php +++ b/Zend/zend_vm_gen.php @@ -2126,6 +2126,8 @@ function gen_executor($f, $skl, $spec, $kind, $executor_name, $initializer_name) out($f,"# undef ZEND_VM_RETURN\n"); out($f,"# undef ZEND_VM_DISPATCH_TO_HELPER\n"); out($f,"# undef ZEND_VM_INTERRUPT\n"); + out($f,"# undef ZEND_VM_ENTER_EX\n"); + out($f,"# undef ZEND_VM_LEAVE\n"); out($f,"\n"); out($f,"# define ZEND_VM_TAIL_CALL(call) ZEND_MUSTTAIL return call\n"); out($f,"# define ZEND_VM_CONTINUE() ZEND_VM_TAIL_CALL(opline->handler(ZEND_OPCODE_HANDLER_ARGS_PASSTHRU))\n"); @@ -2137,6 +2139,8 @@ function gen_executor($f, $skl, $spec, $kind, $executor_name, $initializer_name) out($f," } while (0)\n"); out($f,"# define ZEND_VM_DISPATCH_TO_LEAVE_HELPER(helper) opline = &call_leave_op; SAVE_OPLINE(); ZEND_VM_CONTINUE()\n"); out($f,"# define ZEND_VM_INTERRUPT() ZEND_VM_TAIL_CALL(zend_interrupt_helper".($spec?"_SPEC":"")."_TAILCALL(ZEND_OPCODE_HANDLER_ARGS_PASSTHRU))\n"); + out($f,"# define ZEND_VM_ENTER_EX() ZEND_VM_INTERRUPT_CHECK(); ZEND_VM_CONTINUE()\n"); + out($f,"# define ZEND_VM_LEAVE() ZEND_VM_CONTINUE()\n"); out($f,"\n"); out($f,"static ZEND_OPCODE_HANDLER_RET ZEND_OPCODE_HANDLER_CCONV zend_interrupt_helper".($spec?"_SPEC":"")."_TAILCALL(ZEND_OPCODE_HANDLER_ARGS);\n"); out($f,"static ZEND_OPCODE_HANDLER_RET ZEND_OPCODE_HANDLER_CCONV ZEND_NULL_TAILCALL_HANDLER(ZEND_OPCODE_HANDLER_ARGS);\n"); From 5375e97f4a3f9e967d33529994ce485bb64e3d7b Mon Sep 17 00:00:00 2001 From: Weilin Du <108666168+LamentXU123@users.noreply.github.com> Date: Thu, 26 Mar 2026 00:12:22 +0800 Subject: [PATCH 2/5] [skip ci] Fix typos in ext/dba/libinifile/inifile.c --- ext/dba/libinifile/inifile.c | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/ext/dba/libinifile/inifile.c b/ext/dba/libinifile/inifile.c index c5467396d4bfe..6221c7f7a6e71 100644 --- a/ext/dba/libinifile/inifile.c +++ b/ext/dba/libinifile/inifile.c @@ -249,7 +249,7 @@ val_type inifile_fetch(inifile *dba, const key_type *key, int skip) { ln.key.group = estrdup(dba->next.key.group); } else { /* specific instance or not same key -> restart search */ - /* the slow way: restart and seacrch */ + /* the slow way: restart and search */ php_stream_rewind(dba->fp); inifile_line_free(&dba->next); } @@ -471,7 +471,7 @@ static int inifile_delete_replace_append(inifile *dba, const key_type *key, cons * 8) Append temporary stream */ - assert(!append || (key->name && value)); /* missuse */ + assert(!append || (key->name && value)); /* misuse */ /* 1 - 3 */ inifile_find_group(dba, key, &pos_grp_start); From e5a723e83873d1cc6b72a19d163c55fbefc37a80 Mon Sep 17 00:00:00 2001 From: Ilija Tovilo Date: Wed, 25 Mar 2026 19:38:00 +0100 Subject: [PATCH 3/5] Avoid goto into zend_try block in do_cli() We must not jump into a zend_try block because that skips setting up of EG(bailout), which will deref null when trying to bailout. In practice, this bug is impossible to trigger, given php_execute_script() already guards against bailout. Fixes GH-21420 --- sapi/cli/php_cli.c | 2 ++ 1 file changed, 2 insertions(+) diff --git a/sapi/cli/php_cli.c b/sapi/cli/php_cli.c index d1781eab671c9..ca5cc91219b98 100644 --- a/sapi/cli/php_cli.c +++ b/sapi/cli/php_cli.c @@ -867,8 +867,10 @@ static int do_cli(int argc, char **argv) /* {{{ */ fprintf(stdout, "Executing for the first time...\n"); fflush(stdout); } + } zend_end_try(); do_repeat: + zend_try { /* only set script_file if not set already and not in direct mode and not at end of parameter list */ if (argc > php_optind && !script_file From d92c822fc47fb0e85ce976960c9be1245cdafad6 Mon Sep 17 00:00:00 2001 From: Ilija Tovilo Date: Wed, 25 Mar 2026 20:04:27 +0100 Subject: [PATCH 4/5] Revert "Avoid goto into zend_try block in do_cli()" This reverts commit e5a723e83873d1cc6b72a19d163c55fbefc37a80. This fix was wrong, because we don't want to run the second try block when the first block bails. Given this can't happen in practice, let's just keep it the way it is. --- sapi/cli/php_cli.c | 2 -- 1 file changed, 2 deletions(-) diff --git a/sapi/cli/php_cli.c b/sapi/cli/php_cli.c index ca5cc91219b98..d1781eab671c9 100644 --- a/sapi/cli/php_cli.c +++ b/sapi/cli/php_cli.c @@ -867,10 +867,8 @@ static int do_cli(int argc, char **argv) /* {{{ */ fprintf(stdout, "Executing for the first time...\n"); fflush(stdout); } - } zend_end_try(); do_repeat: - zend_try { /* only set script_file if not set already and not in direct mode and not at end of parameter list */ if (argc > php_optind && !script_file From 26ab037d7c2a07115801756ebec2ed8c0536ad4c Mon Sep 17 00:00:00 2001 From: David Carlier Date: Tue, 24 Mar 2026 12:23:25 +0000 Subject: [PATCH 5/5] Fix GH-21496: UAF in dom_objects_free_storage. Cloning a non-document DOM node creates a copy within the same xmlDoc. importStylesheet then passes that original document to xsltParseStylesheetDoc, which may strip and free nodes during processing, invalidating PHP objects still referencing them. Resolve the ownerDocument for non-document nodes and clone that instead. close GH-21500 --- NEWS | 2 ++ ext/xsl/tests/gh21496.phpt | 32 ++++++++++++++++++++++++++++++++ ext/xsl/xsltprocessor.c | 32 +++++++++++++++++++++++++++++++- 3 files changed, 65 insertions(+), 1 deletion(-) create mode 100644 ext/xsl/tests/gh21496.phpt diff --git a/NEWS b/NEWS index d31184822e66b..b7cc259571133 100644 --- a/NEWS +++ b/NEWS @@ -68,6 +68,8 @@ PHP NEWS - XSL: . Fix GH-21357 (XSLTProcessor works with DOMDocument, but fails with Dom\XMLDocument). (ndossche) + . Fixed bug GH-21496 (UAF in dom_objects_free_storage). + (David Carlier/ndossche) 12 Mar 2026, PHP 8.4.19 diff --git a/ext/xsl/tests/gh21496.phpt b/ext/xsl/tests/gh21496.phpt new file mode 100644 index 0000000000000..7a5cea37937d0 --- /dev/null +++ b/ext/xsl/tests/gh21496.phpt @@ -0,0 +1,32 @@ +--TEST-- +GH-21496 (UAF in dom_objects_free_storage when importing non-document node as stylesheet) +--EXTENSIONS-- +dom +xsl +--CREDITS-- +YuanchengJiang +--FILE-- +loadXML(<< +XML); +$doc->documentElement->appendChild($comment); +unset($doc); +$proc = new XSLTProcessor(); +var_dump($proc->importStylesheet($comment)); +$sxe = simplexml_load_string(''); +$proc = new XSLTProcessor(); +$proc->importStylesheet($sxe); +?> +--EXPECTF-- +Warning: XSLTProcessor::importStylesheet(): compilation error: file %s line 1 element container in %s on line %d + +Warning: XSLTProcessor::importStylesheet(): xsltParseStylesheetProcess : document is not a stylesheet in %s on line %d +bool(false) + +Warning: XSLTProcessor::importStylesheet(): compilation error: element container in %s on line %d + +Warning: XSLTProcessor::importStylesheet(): xsltParseStylesheetProcess : document is not a stylesheet in %s on line %d + diff --git a/ext/xsl/xsltprocessor.c b/ext/xsl/xsltprocessor.c index d066e2706510f..71971332a2517 100644 --- a/ext/xsl/xsltprocessor.c +++ b/ext/xsl/xsltprocessor.c @@ -167,7 +167,7 @@ PHP_METHOD(XSLTProcessor, importStylesheet) xsltStylesheetPtr sheetp; bool clone_docu = false; xmlNode *nodep = NULL; - zval *cloneDocu, rv, clone_zv; + zval *cloneDocu, rv, clone_zv, owner_zv; zend_string *member; id = ZEND_THIS; @@ -175,10 +175,40 @@ PHP_METHOD(XSLTProcessor, importStylesheet) RETURN_THROWS(); } + nodep = php_libxml_import_node(docp); + if (nodep == NULL) { + zend_argument_type_error(1, "must be a valid XML node"); + RETURN_THROWS(); + } + + if (Z_OBJ_HANDLER_P(docp, clone_obj) == NULL) { + zend_argument_type_error(1, "must be a cloneable node"); + RETURN_THROWS(); + } + + ZVAL_UNDEF(&owner_zv); + + /* For non-document nodes, resolve the ownerDocument and clone that + * instead as xsltParseStylesheetProcess may free nodes in the document. */ + if (nodep->type != XML_DOCUMENT_NODE && nodep->type != XML_HTML_DOCUMENT_NODE) { + if (nodep->doc == NULL) { + zend_argument_value_error(1, "must be part of a document"); + RETURN_THROWS(); + } + + /* See dom_import_simplexml_common */ + + dom_object *nodeobj = (dom_object *) ((char *) Z_OBJ_P(docp) - Z_OBJ_HT_P(docp)->offset); + + php_dom_create_object((xmlNodePtr) nodep->doc, &owner_zv, nodeobj); + docp = &owner_zv; + } + /* libxslt uses _private, so we must copy the imported * stylesheet document otherwise the node proxies will be a mess. * We will clone the object and detach the libxml internals later. */ zend_object *clone = Z_OBJ_HANDLER_P(docp, clone_obj)(Z_OBJ_P(docp)); + zval_ptr_dtor(&owner_zv); if (!clone) { RETURN_THROWS(); }