From 01159a298b391f0b84f2707527197e764a6f4f13 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?PHP=20Compiler=20Ext=20=E2=80=94=20DOM=20=26=20XML?= Date: Fri, 28 Aug 2026 18:58:57 +0000 Subject: [PATCH] AOT DOM: fix DTD isId regression and setIdAttributeNode(null) LLVM verify (#34821, #33758) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Restore compile-time isIdBearingLiteral before the activeFunction guard so loadXML DTD ATTLIST ID attrs report isId() true again after #35627. Rework setIdNode*Argv to take only the Attr (Context bridge) — nested JIT mis-lowers two consecutive ObjectEntry params — and add VmDom::setIdAttributeNodeOnAttrOwner for the helper body. php-src: ext/dom/attr.c (dom_attr_is_id), ext/dom/element.c (setIdAttributeNode). Co-authored-by: Cursor --- ext/dom/DomSetIdAttributeJitHelper.php | 19 ++-- ext/dom/JitDomDocumentMethodKernel.php | 8 +- ext/dom/JitDomSetIdAttribute.php | 4 +- ext/dom/VmDom.php | 24 ++++++ lib/JIT/Builtin/DomSetIdAttributeRuntime.php | 4 +- lib/JIT/Call/DomAttrIsId.php | 9 +- lib/JIT/JitVmHelperLink.php | 91 ++++++++++++++++++++ 7 files changed, 134 insertions(+), 25 deletions(-) diff --git a/ext/dom/DomSetIdAttributeJitHelper.php b/ext/dom/DomSetIdAttributeJitHelper.php index 909e0c061cf..9da5f3b9b2d 100644 --- a/ext/dom/DomSetIdAttributeJitHelper.php +++ b/ext/dom/DomSetIdAttributeJitHelper.php @@ -4,6 +4,7 @@ namespace PHPCompiler\ext\dom; +use PHPCompiler\VM\Context; use PHPCompiler\VM\ObjectEntry; /** @@ -47,23 +48,13 @@ public static function setIdNsFalseArgv(ObjectEntry $element, string $namespace, VmDom::setIdAttributeNS($element, $ns, $localName, false); } - public static function setIdNodeTrueArgv(ObjectEntry $element, ObjectEntry $attr): void + public static function setIdNodeTrueArgv(Context $ctx, ObjectEntry $attr): void { - VmDom::syncDomRegistryParentChainFromProperties($element); - $qName = VmDom::resolveThinAotSetIdNodeQName($element, $attr); - if ('' !== $qName) { - VmDom::seedThinAotElementAttribute($element, $qName, VmDom::thinAttrValue($attr)); - } - VmDom::setIdAttributeNode($element, $attr, true); + VmDom::setIdAttributeNodeOnAttrOwner($attr, true); } - public static function setIdNodeFalseArgv(ObjectEntry $element, ObjectEntry $attr): void + public static function setIdNodeFalseArgv(Context $ctx, ObjectEntry $attr): void { - VmDom::syncDomRegistryParentChainFromProperties($element); - $qName = VmDom::resolveThinAotSetIdNodeQName($element, $attr); - if ('' !== $qName) { - VmDom::seedThinAotElementAttribute($element, $qName, VmDom::thinAttrValue($attr)); - } - VmDom::setIdAttributeNode($element, $attr, false); + VmDom::setIdAttributeNodeOnAttrOwner($attr, false); } } diff --git a/ext/dom/JitDomDocumentMethodKernel.php b/ext/dom/JitDomDocumentMethodKernel.php index 88321de04c2..f3f09658e1b 100644 --- a/ext/dom/JitDomDocumentMethodKernel.php +++ b/ext/dom/JitDomDocumentMethodKernel.php @@ -1153,11 +1153,11 @@ public static function ensureSetIdAttributeNsFalseBridge(Context $context): void public static function ensureSetIdAttributeNodeTrueBridge(Context $context): void { $objPtr = $context->getTypeFromString('__object__*'); - self::ensureBridge( + self::ensureContextBridge( $context, \PHPCompiler\JIT\Builtin\DomSetIdAttributeRuntime::ABI_NODE_TRUE, 'dom_element_set_id_attribute_node_true_user_script', - [$objPtr, $objPtr], + [$objPtr], $context->context->voidType(), 'PHPCompiler\\ext\\dom\\DomSetIdAttributeJitHelper::setIdNodeTrueArgv', '/ext/dom/DomSetIdAttributeJitHelper.php' @@ -1167,11 +1167,11 @@ public static function ensureSetIdAttributeNodeTrueBridge(Context $context): voi public static function ensureSetIdAttributeNodeFalseBridge(Context $context): void { $objPtr = $context->getTypeFromString('__object__*'); - self::ensureBridge( + self::ensureContextBridge( $context, \PHPCompiler\JIT\Builtin\DomSetIdAttributeRuntime::ABI_NODE_FALSE, 'dom_element_set_id_attribute_node_false_user_script', - [$objPtr, $objPtr], + [$objPtr], $context->context->voidType(), 'PHPCompiler\\ext\\dom\\DomSetIdAttributeJitHelper::setIdNodeFalseArgv', '/ext/dom/DomSetIdAttributeJitHelper.php' diff --git a/ext/dom/JitDomSetIdAttribute.php b/ext/dom/JitDomSetIdAttribute.php index 1fba380d162..f98b6390e1c 100644 --- a/ext/dom/JitDomSetIdAttribute.php +++ b/ext/dom/JitDomSetIdAttribute.php @@ -232,7 +232,6 @@ public static function invokeNode(Context $context, JITVariable ...$args): Value return self::invoke($context, $args[0], $nameVar, $args[2]); } - $element = self::loadObjectArg($context, $args[0]); $attr = $context->builder->call( $context->lookupFunction('__value__readObject'), JitValueBox::valuePtrFromVariable($context, $args[1]) @@ -247,17 +246,18 @@ public static function invokeNode(Context $context, JITVariable ...$args): Value } $context->builder->call( $context->lookupFunction($abi), - $element, $attr ); if (JitDomDocumentMethodKernel::shouldUse($context) && $isIdTrue) { BasicBlockHelper::ensureOpenInsertBlock($context, 'dom_set_id_attribute_node_post'); + $element = self::loadObjectArg($context, $args[0]); self::storeCacheFromRuntimeAttrValue($context, $element, $attr); $key = JitDomAttrRename::lastFetchedKey(); if (null !== $key) { DomUserScriptAttributeCacheLlvm::markIdBearingLiteral($key[0], $key[1], true); } } elseif (JitDomDocumentMethodKernel::shouldUse($context) && !$isIdTrue) { + $element = self::loadObjectArg($context, $args[0]); DomUserScriptElementCacheLlvm::invalidateIfElement($context, $element); } diff --git a/ext/dom/VmDom.php b/ext/dom/VmDom.php index 8b11be366cc..aa89b737add 100644 --- a/ext/dom/VmDom.php +++ b/ext/dom/VmDom.php @@ -2967,6 +2967,30 @@ private static function setIdAttributeNSInternal( self::applyIdAttributeRegistration($element, $qName, $isId, $syncIdMap); } + /** + * Thin-AOT helper entry: resolve owner from Attr and delegate (#33758). + */ + public static function setIdAttributeNodeOnAttrOwner(ObjectEntry $attr, bool $isId): void + { + if (!self::isAttr($attr)) { + throw new \TypeError('DOMElement::setIdAttributeNode(): Argument #1 ($attr) must be of type DOMAttr'); + } + if (!$attr->hasProperty(self::PROP_OWNER_ELEMENT)) { + return; + } + $ownerVar = $attr->getProperty(self::PROP_OWNER_ELEMENT)->resolveIndirect(); + if (Variable::TYPE_OBJECT !== $ownerVar->type) { + return; + } + $element = $ownerVar->toObject(); + self::syncDomRegistryParentChainFromProperties($element); + $qName = self::resolveThinAotSetIdNodeQName($element, $attr); + if ('' !== $qName) { + self::seedThinAotElementAttribute($element, $qName, self::thinAttrValue($attr)); + } + self::setIdAttributeNode($element, $attr, $isId); + } + /** * DOMElement::setIdAttributeNode() — mark an owned Attr as ID (php-src ext/dom/element.c; #20123). * Zend: NOT_FOUND_ERR when attr->parent != element. diff --git a/lib/JIT/Builtin/DomSetIdAttributeRuntime.php b/lib/JIT/Builtin/DomSetIdAttributeRuntime.php index 90495e64ea4..94c265d83d4 100644 --- a/lib/JIT/Builtin/DomSetIdAttributeRuntime.php +++ b/lib/JIT/Builtin/DomSetIdAttributeRuntime.php @@ -144,11 +144,11 @@ private static function linkNs(Context $context, string $abi, string $entry, str private static function linkNode(Context $context, string $abi, string $entry, string $helper): void { $objPtr = $context->getTypeFromString('__object__*'); - JitVmHelperLink::ensureBridge( + JitVmHelperLink::ensureContextBridge( $context, $abi, $entry, - [$objPtr, $objPtr], + [$objPtr], $context->context->voidType(), $helper, self::HELPER_PATH, diff --git a/lib/JIT/Call/DomAttrIsId.php b/lib/JIT/Call/DomAttrIsId.php index 9bb8f67944c..e0cc8230b61 100644 --- a/lib/JIT/Call/DomAttrIsId.php +++ b/lib/JIT/Call/DomAttrIsId.php @@ -35,15 +35,18 @@ public function call(Context $context, Variable ...$args): Value } $key = JitDomAttrRename::lastFetchedKey(); if (null !== $key) { + // loadXML DTD ATTLIST ID / xml:id / setIdAttribute* stamp compile-time flags (#34821). + // Must precede the user-function {@code id} global — activeFunction is the script + // name in {main}, not only nested closures (#23514). + if (DomUserScriptAttributeCacheLlvm::isIdBearingLiteral($key[0], $key[1])) { + return $context->getTypeFromString('int1')->constInt(1, false); + } $active = $context->activeFunction; if ('' !== $active && !str_starts_with($active, '__')) { // createElement id= toggles via module global — runtime stores from setIdAttribute (#29884). if ('' === $key[0] && 'id' === $key[1]) { return DomUserScriptAttributeCacheLlvm::loadIdBearingGlobal($context); } - if (DomUserScriptAttributeCacheLlvm::isIdBearingLiteral($key[0], $key[1])) { - return $context->getTypeFromString('int1')->constInt(1, false); - } } return DomAttrIsIdRuntime::invoke($context, $args[0]); diff --git a/lib/JIT/JitVmHelperLink.php b/lib/JIT/JitVmHelperLink.php index 28248bfe220..421ef39f6d3 100644 --- a/lib/JIT/JitVmHelperLink.php +++ b/lib/JIT/JitVmHelperLink.php @@ -312,6 +312,97 @@ public static function ensureBridge( } } + /** + * ABI bridge with implicit VmActiveContext as helper arg 0 (#33758 / peer DomAttrIsIdJitHelper). + * + * @param list $paramTypes ABI parameter types (helper receives Context + these) + */ + public static function ensureContextBridge( + Context $context, + string $abiName, + string $entryBlockName, + array $paramTypes, + Type $returnType, + string $helperLogical, + string $relativeHelperPath, + array $compiledHelpers, + string $issueTag, + bool $skipHelperRuntimeCache = false + ): void { + $probe = $context->module->getNamedFunction($abiName); + if (self::hasNamedBridgeEntry($probe, $entryBlockName)) { + $context->registerFunction($abiName, $probe); + + return; + } + + $savedBlock = null; + try { + $savedBlock = $context->builder->getInsertBlock(); + } catch (\Throwable) { + } + + self::ensureCompiled( + $context, + $relativeHelperPath, + $compiledHelpers, + $issueTag, + $skipHelperRuntimeCache + ); + + $helperFn = self::lookupCompiled($context, $helperLogical, $issueTag); + $ft = $context->context->functionType($returnType, false, ...$paramTypes); + $fn = null !== $probe + ? $probe + : $context->module->addFunction($abiName, $ft); + + $entry = self::bridgeEntryForEmit($fn, $entryBlockName); + $context->builder->positionAtEnd($entry); + $vmCtx = $context->builder->call(VmActiveContextLlvm::lookupAbi($context)); + $args = [ + JitNestedHelperCoerce::coerceArgForHelper( + $context, + $vmCtx, + $helperFn->getParam(0)->typeOf() + ), + ]; + for ($i = 0, $n = $fn->countParams(); $i < $n; ++$i) { + $args[] = JitNestedHelperCoerce::coerceArgForHelper( + $context, + $fn->getParam($i), + $helperFn->getParam($i + 1)->typeOf() + ); + } + $result = $context->builder->call($helperFn, ...$args); + if ('void' === $context->getStringFromType($returnType)) { + $context->builder->returnVoid(); + } else { + $ret = JitNestedHelperCoerce::coerceBridgeResult($context, $result, $returnType); + $context->builder->returnValue($ret); + } + $context->registerFunction($abiName, $fn); + + if (null !== $savedBlock) { + $context->builder->positionAtEnd($savedBlock); + } else { + $fallback = null; + if ('' !== $context->activeFunction && isset($context->functions[$context->activeFunction])) { + $active = $context->functions[$context->activeFunction]; + if ($active instanceof LlvmFunction) { + $fallback = $active; + } + } + if (null === $fallback && $context->main instanceof LlvmFunction) { + $fallback = $context->main; + } + if (null !== $fallback && $fallback->countBasicBlocks() > 0) { + $context->builder->positionAtEnd($fallback->getEntryBasicBlock()); + } else { + $context->builder->clearInsertionPosition(); + } + } + } + /** * Resolve helper source path: ext/* and lib/* live at repo root; /VM/* under lib/. */