[php-src] PR #23337: Fix GH-23331: UAF when an attribute child keeps a live wrapper
| From: | iliaal | Date: | Mon, 17 Aug 2026 19:24:07 +0000 |
| Subject: | [php-src] PR #23337: Fix GH-23331: UAF when an attribute child keeps a live wrapper | ||
| Groups: | php.git-pulls | ||
| Request: | Send a blank email to git-pulls+get-37952@lists.php.net to get a copy of this message | ||
Pull Request: https://github.com/php/php-src/pull/23337
Author: iliaal
node_list_unlink() abandoned an attribute's remaining children at two exits: xmlUnlinkNode()
clears node->next, so the loop stopped at the first child that had a wrapper, and the
XML_ENTITY_REF_NODE case broke out of the loop instead of only skipping the borrowed entity
children. xmlSetProp(), xmlSetNsProp() and xmlFreeProp() then freed everything past either exit
while a live wrapper still pointed at it.
The #37456 recursion guard is kept: entity-reference children are still never walked. Every caller
passes an attribute's or an element's child list rather than a DTD list, so continuing the
sibling walk stays inside a finite content list.
setAttribute(), setAttributeNS(), removeAttribute(), removeAttributeNode() and removeAttributeNS()
all reach it, on both the legacy and the modern classes. The remove methods reach it only when the
attribute itself carries no wrapper, where dom_remove_attribute() falls through to xmlFreeProp();
that case segfaults rather than reading freed memory. dom_remove_all_children() is unaffected
because php_libxml_node_free_list() already caches the sibling before unlinking. The wrapper
positions decide which exit fires rather than the caller, so gh23331.phpt pins the first and
gh23331_2.phpt the second.