From 3221a076f26da584b30ed90ce4a46fbb7343069b Mon Sep 17 00:00:00 2001 From: David Carlier Date: Thu, 25 Jun 2026 13:12:17 +0100 Subject: [PATCH 1/3] ext/dom: fix UAF when setting an attribute colliding by local name. Fix #22447 xmlHasProp() matches an attribute by local name only, ignoring its namespace, whereas xmlAddChild()/xmlAddPrevSibling() dedup an incoming no-namespace attribute via xmlHasNsProp(..., NULL), which matches only attributes with no namespace. When both a no-namespace and a namespaced attribute share a local name, the pre-insertion check unlinked the wrong (namespaced) attribute, leaving libxml to free the still-wrapped no-namespace duplicate and producing a use-after-free at request shutdown. Use xmlHasNsProp(..., NULL) so the pre-insertion check matches libxml's internal duplicate detection, as advised by @nwellnhof. Close GH-22452 --- NEWS | 3 +++ ext/dom/element.c | 2 ++ ext/dom/node.c | 6 +++--- ext/dom/tests/gh22447.phpt | 25 +++++++++++++++++++++++++ 4 files changed, 33 insertions(+), 3 deletions(-) create mode 100644 ext/dom/tests/gh22447.phpt diff --git a/NEWS b/NEWS index cfb82c4f0ca2..6b42bbf1cbdf 100644 --- a/NEWS +++ b/NEWS @@ -8,6 +8,9 @@ PHP NEWS - DOM: . Fixed bug GH-22825 (DOMElement::setAttribute() fails silently when the DTD declares a default value for the attribute). (iliaal) + . Fixed bug GH-22447 (UAF at dom_objects_free_storage when setting an + attribute node that collides by local name with a namespaced + attribute). (David Carlier) - Sockets: . Fixed various memory related issues in ext/sockets. (David Carlier) diff --git a/ext/dom/element.c b/ext/dom/element.c index b2564428db9c..3bce1bdac2a5 100644 --- a/ext/dom/element.c +++ b/ext/dom/element.c @@ -726,6 +726,8 @@ static void dom_element_set_attribute_node_common(INTERNAL_FUNCTION_PARAMETERS, nsp = attrp->ns; if (use_ns && nsp != NULL) { existattrp = xmlHasNsProp(nodep, attrp->name, nsp->href); + } else if (nsp == NULL) { + existattrp = xmlHasNsProp(nodep, attrp->name, NULL); } else { existattrp = xmlHasProp(nodep, attrp->name); } diff --git a/ext/dom/node.c b/ext/dom/node.c index df806bddfae7..81c80cb0c8ac 100644 --- a/ext/dom/node.c +++ b/ext/dom/node.c @@ -965,7 +965,7 @@ static void dom_node_insert_before_legacy(zval *return_value, zval *ref, dom_obj xmlAttrPtr lastattr; if (child->ns == NULL) - lastattr = xmlHasProp(refp->parent, child->name); + lastattr = xmlHasNsProp(refp->parent, child->name, NULL); else lastattr = xmlHasNsProp(refp->parent, child->name, child->ns->href); if (lastattr != NULL && lastattr->type != XML_ATTRIBUTE_DECL) { @@ -1012,7 +1012,7 @@ static void dom_node_insert_before_legacy(zval *return_value, zval *ref, dom_obj xmlAttrPtr lastattr; if (child->ns == NULL) - lastattr = xmlHasProp(parentp, child->name); + lastattr = xmlHasNsProp(parentp, child->name, NULL); else lastattr = xmlHasNsProp(parentp, child->name, child->ns->href); if (lastattr != NULL && lastattr->type != XML_ATTRIBUTE_DECL) { @@ -1374,7 +1374,7 @@ static void dom_node_append_child_legacy(zval *return_value, dom_object *intern, xmlAttrPtr lastattr; if (child->ns == NULL) - lastattr = xmlHasProp(nodep, child->name); + lastattr = xmlHasNsProp(nodep, child->name, NULL); else lastattr = xmlHasNsProp(nodep, child->name, child->ns->href); if (lastattr != NULL && lastattr->type != XML_ATTRIBUTE_DECL) { diff --git a/ext/dom/tests/gh22447.phpt b/ext/dom/tests/gh22447.phpt new file mode 100644 index 000000000000..396a0ff1ee1c --- /dev/null +++ b/ext/dom/tests/gh22447.phpt @@ -0,0 +1,25 @@ +--TEST-- +GH-22447 (UAF at dom_objects_free_storage when setAttributeNode collides with a namespaced attribute of the same local name) +--EXTENSIONS-- +dom +--FILE-- +createAttribute("my-attribute"); +$container = $dom->appendChild($dom->createElement("container")); +$attribute2 = $dom->createAttribute("my-attribute"); +$attribute4 = $dom->createAttributeNS("urn:a", "my-attribute"); + +$container->setAttributeNode($attribute1); +$container->setAttributeNode($attribute4); + +var_dump($container->setAttributeNode($attribute2) === $attribute1); +var_dump($container->setAttributeNode($attribute1) === $attribute2); + +echo $dom->saveXml($container), PHP_EOL; +?> +--EXPECT-- +bool(true) +bool(true) + From 853b89c092f0adb9c16f3fd597c4c4bff82c025f Mon Sep 17 00:00:00 2001 From: Arnaud Le Blanc <365207+arnaud-lb@users.noreply.github.com> Date: Wed, 22 Jul 2026 09:31:44 +0200 Subject: [PATCH 2/3] Clear ZREG_TYPE_ONLY flag for in-register vars in zend_jit_snapshot_handler() (#22764) Fixes GH-22763. The reproducer triggers an assertion failure in `zend_jit_use_reg()` because a var has `jit->ra[var].ref == IR_NULL` but `jit->ra[var].flags` doesn't contain `ZREG_LOAD`. This happens because of the following events: * An in-register variable is spilled to the VM stack frame by IR (IR_REG_SPILL_SPECIAL) * zend_jit_snapshot_handler() set the stack descriptor to .flags = ZREG_TYPE_ONLY, .reg = ZREG_NONE * During a subsequent call to zend_jit_snapshot_handler(), the reg is not IR_REG_SPILL_SPECIAL anymore, so .reg is set but .flags remains ZREG_TYPE_ONLY which is inconsistent * A side trace inherits from this stack descriptor * zend_jit_trace_deoptimization() doesn't set `jit->ra[var].ref` for this var because `.flags == ZREG_TYPE_ONLY`, which causes the assertion failure later Fix by removing irrelevant flags when setting reg in zend_jit_snapshot_handler(). --- ext/opcache/jit/zend_jit_ir.c | 4 ++- ext/opcache/jit/zend_jit_trace.c | 2 ++ ext/opcache/tests/jit/gh22763.phpt | 58 ++++++++++++++++++++++++++++++ 3 files changed, 63 insertions(+), 1 deletion(-) create mode 100644 ext/opcache/tests/jit/gh22763.phpt diff --git a/ext/opcache/jit/zend_jit_ir.c b/ext/opcache/jit/zend_jit_ir.c index f8503a131734..ae9088878b62 100644 --- a/ext/opcache/jit/zend_jit_ir.c +++ b/ext/opcache/jit/zend_jit_ir.c @@ -859,12 +859,14 @@ void *zend_jit_snapshot_handler(ir_ctx *ctx, ir_ref snapshot_ref, ir_insn *snaps t->stack_map[t->exit_info[exit_point].stack_offset + var].flags = ZREG_TYPE_ONLY; } else { if ((exit_flags & ZEND_JIT_EXIT_FIXED) - && t->stack_map[t->exit_info[exit_point].stack_offset + var].reg != IR_REG_NUM(reg)) { + && (t->stack_map[t->exit_info[exit_point].stack_offset + var].reg != IR_REG_NUM(reg) + || (t->stack_map[t->exit_info[exit_point].stack_offset + var].flags & ~(ZREG_LOAD|ZREG_STORE|ZREG_LAST_USE)))) { exit_point = zend_jit_duplicate_exit_point(ctx, t, exit_point, snapshot_ref); addr = (void*)zend_jit_trace_get_exit_addr(exit_point); exit_flags &= ~ZEND_JIT_EXIT_FIXED; } t->stack_map[t->exit_info[exit_point].stack_offset + var].reg = IR_REG_NUM(reg); + t->stack_map[t->exit_info[exit_point].stack_offset + var].flags &= (ZREG_LOAD|ZREG_STORE|ZREG_LAST_USE); } } else { if ((exit_flags & ZEND_JIT_EXIT_FIXED) diff --git a/ext/opcache/jit/zend_jit_trace.c b/ext/opcache/jit/zend_jit_trace.c index 5cc3dbd3aa0b..c8ef22c88069 100644 --- a/ext/opcache/jit/zend_jit_trace.c +++ b/ext/opcache/jit/zend_jit_trace.c @@ -3558,6 +3558,8 @@ static int zend_jit_trace_deoptimization( } } } else if (STACK_FLAGS(parent_stack, i) == ZREG_TYPE_ONLY) { + ZEND_ASSERT(reg == ZREG_NONE); + uint8_t type = STACK_TYPE(parent_stack, i); if (!zend_jit_store_type(jit, i, type)) { diff --git a/ext/opcache/tests/jit/gh22763.phpt b/ext/opcache/tests/jit/gh22763.phpt new file mode 100644 index 000000000000..67d7caa308ed --- /dev/null +++ b/ext/opcache/tests/jit/gh22763.phpt @@ -0,0 +1,58 @@ +--TEST-- +GH-22763: JIT fails to clear ZREG_TYPE_ONLY after setting reg +--FILE-- += -$d && $kf <= $d && $vf[$offset + $kf] + $vb[$offset + $k] >= $n) { + return [$n - $x, $m - $y, $n - $xs, $m - $ys]; + } + } + } + + return []; +} + +$from = [3,2,1]; +$to = [1,99,3]; +findMiddleSnake($from, 0, count($from), $to, 0, count($to)); +?> +--EXPECTF-- +int(0) + +Warning: Undefined array key 3 in %s on line %d From 740d1f57505f56b74b95255bc508e02c1d582493 Mon Sep 17 00:00:00 2001 From: Arnaud Le Blanc <365207+arnaud-lb@users.noreply.github.com> Date: Wed, 22 Jul 2026 09:44:57 +0200 Subject: [PATCH 3/3] Fix const expr support in preloading (#22783) Preloading will try to evaluate class constants during compilation but evaluated FCCs are objects, which can not be persisted. Apply the same fix as for enums: Fail evaluation when compiling. Preloading will also reset CG(map_ptr_last) after compilation, which results in collisions if map ptrs were allocated during compilation. Move the ZEND_MAP_PTR_NEW() to the persist phase, as a shared map ptr is not necessary before that. Fixes GH-22782. --- NEWS | 7 +++- .../constexpr/gh22782.inc | 18 ++++++++++ .../constexpr/gh22782.phpt | 35 +++++++++++++++++++ Zend/zend_ast.c | 6 ++++ Zend/zend_compile.c | 1 - ext/opcache/zend_file_cache.c | 6 ++-- ext/opcache/zend_persist.c | 3 ++ 7 files changed, 72 insertions(+), 4 deletions(-) create mode 100644 Zend/tests/first_class_callable/constexpr/gh22782.inc create mode 100644 Zend/tests/first_class_callable/constexpr/gh22782.phpt diff --git a/NEWS b/NEWS index 1f2b9540bfa4..0d4d5dfa1670 100644 --- a/NEWS +++ b/NEWS @@ -2,6 +2,9 @@ PHP NEWS ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||| ?? ??? ????, PHP 8.5.10 +- Core: + . Fixed bug GH-22782 (Const expr FCC crashes under preloading). (Arnaud) + - Date: . Fixed leak on double DatePeriod::__construct() call. (ilutov) @@ -12,6 +15,8 @@ PHP NEWS - Opcache: . Fixed GH-22693 (DT_TEXTREL in JIT-generated TLS access on x86_64). (David Carlier) + . Fixed bug GH-22763 (JIT fails to clear ZREG_TYPE_ONLY after setting reg). + (Arnaud) - Sockets: . Fixed socket_set_option() validation error messages for UDP_SEGMENT and @@ -126,7 +131,7 @@ PHP NEWS PHP 8.3 and PHP 8.5). (jorgsowa) - SPL: - . Fix class_parents for classes with leading slash in non-autoload mode. + . Fix class_parents for classes with leading slash in non-autoload mode. (jorgsowa) . Ignore leading back-slash in class_parents(), class_implements(), and class_uses(). (jorgsowa) diff --git a/Zend/tests/first_class_callable/constexpr/gh22782.inc b/Zend/tests/first_class_callable/constexpr/gh22782.inc new file mode 100644 index 000000000000..d67ba4eab14e --- /dev/null +++ b/Zend/tests/first_class_callable/constexpr/gh22782.inc @@ -0,0 +1,18 @@ + +--FILE-- +getAttributes(A::class)[0]->newInstance()->fn)('hello')); +echo "# Method attr\n"; +var_dump((new ReflectionMethod(C::class, 'f')->getAttributes(A::class)[0]->newInstance()->fn)('hello')); +echo "# Method default value\n"; +var_dump((new C()->f())('hello')); + +?> +--EXPECT-- +# Class const +int(5) +# Class attr +int(5) +# Method attr +int(5) +# Method default value +int(5) diff --git a/Zend/zend_ast.c b/Zend/zend_ast.c index bb405945849e..03ce073808ad 100644 --- a/Zend/zend_ast.c +++ b/Zend/zend_ast.c @@ -1054,6 +1054,12 @@ ZEND_API zend_result ZEND_FASTCALL zend_ast_evaluate_inner( case ZEND_AST_CALL: case ZEND_AST_STATIC_CALL: { + // Preloading will attempt to resolve constants but objects can't be stored in shm + // Aborting here to store the const AST instead + if (CG(in_compilation)) { + return FAILURE; + } + zend_function *fptr; zend_class_entry *called_scope = NULL; switch (ast->kind) { diff --git a/Zend/zend_compile.c b/Zend/zend_compile.c index 3bda0ffadc32..d8d61ea979e4 100644 --- a/Zend/zend_compile.c +++ b/Zend/zend_compile.c @@ -11513,7 +11513,6 @@ static void zend_compile_const_expr_fcc(zend_ast **ast_ptr) if ((*args_ast)->kind != ZEND_AST_CALLABLE_CONVERT) { zend_error_noreturn(E_COMPILE_ERROR, "Constant expression contains invalid operations"); } - ZEND_MAP_PTR_NEW(((zend_ast_fcc *)*args_ast)->fptr); switch ((*ast_ptr)->kind) { case ZEND_AST_CALL: { diff --git a/ext/opcache/zend_file_cache.c b/ext/opcache/zend_file_cache.c index d430f4833d3c..fef6a5a5f42b 100644 --- a/ext/opcache/zend_file_cache.c +++ b/ext/opcache/zend_file_cache.c @@ -1303,7 +1303,9 @@ static void zend_file_cache_unserialize_ast(zend_ast *ast, zend_ast_get_op_array(ast)->op_array = Z_PTR(z); } else if (ast->kind == ZEND_AST_CALLABLE_CONVERT) { zend_ast_fcc *fcc = (zend_ast_fcc*)ast; - ZEND_MAP_PTR_NEW(fcc->fptr); + if (!script->corrupted) { + ZEND_MAP_PTR_NEW(fcc->fptr); + } } else if (zend_ast_is_decl(ast)) { /* Not implemented. */ ZEND_UNREACHABLE(); @@ -2109,7 +2111,7 @@ void zend_file_cache_invalidate(zend_string *full_path) if (ZCG(accel_directives).file_cache_read_only) { return; } - + char *filename; filename = zend_file_cache_get_bin_file_path(full_path); diff --git a/ext/opcache/zend_persist.c b/ext/opcache/zend_persist.c index 29b6c2f7a6b2..a4aea9fae653 100644 --- a/ext/opcache/zend_persist.c +++ b/ext/opcache/zend_persist.c @@ -197,6 +197,9 @@ static zend_ast *zend_persist_ast(zend_ast *ast) node = (zend_ast *) copy; } else if (ast->kind == ZEND_AST_CALLABLE_CONVERT) { zend_ast_fcc *copy = zend_shared_memdup(ast, sizeof(zend_ast_fcc)); + if (!ZCG(current_persistent_script)->corrupted) { + ZEND_MAP_PTR_NEW(copy->fptr); + } node = (zend_ast *) copy; } else if (zend_ast_is_decl(ast)) { /* Not implemented. */