Commit a7bdb8b53d1 for php.net
commit a7bdb8b53d15393e1ff9ec693385ecd8d704f9a0
Author: David Carlier <devnexen@gmail.com>
Date: Tue Sep 15 21:26:40 2026 +0100
Fix GH-23365: DOMNode::insertBefore() drops the node used as its own reference
insertBefore($n, $n) unlinked the node, then rebuilt its position from the
pointers the unlink had just cleared, leaving it out of the document with a
self-referencing sibling list, freed twice at teardown. Retarget the reference
to the node's next sibling, as the modern DOM already does.
Close GH-23379
diff --git a/NEWS b/NEWS
index 7b552a67284..5f598989c66 100644
--- a/NEWS
+++ b/NEWS
@@ -6,6 +6,8 @@ PHP NEWS
. Fixed use-after-free when re-constructing a DOMXPath whose php:function
registrations are freed while still reachable from the cycle collector.
(Ilia Alshanetsky)
+ . Fixed bug GH-23365 (DOMNode::insertBefore($n, $n) drops the node and
+ leaves a self-referencing sibling list). (David Carlier)
- Intl:
. Fixed cloning IntlDateFormatter and MessageFormatter losing PHP-side state
diff --git a/ext/dom/node.c b/ext/dom/node.c
index a42dfedc32a..49e400ef530 100644
--- a/ext/dom/node.c
+++ b/ext/dom/node.c
@@ -931,6 +931,9 @@ static void dom_node_insert_before_legacy(zval *return_value, zval *ref, dom_obj
php_dom_throw_error(NOT_FOUND_ERR, stricterror);
RETURN_FALSE;
}
+ if (refp == child) {
+ refp = child->next;
+ }
}
if (child->doc == NULL && parentp->doc != NULL) {
@@ -940,7 +943,7 @@ static void dom_node_insert_before_legacy(zval *return_value, zval *ref, dom_obj
php_libxml_invalidate_node_list_cache(intern->document);
- if (ref != NULL) {
+ if (refp != NULL) {
if (child->parent != NULL) {
xmlUnlinkNode(child);
}
diff --git a/ext/dom/tests/gh23365.phpt b/ext/dom/tests/gh23365.phpt
new file mode 100644
index 00000000000..bc283b0659d
--- /dev/null
+++ b/ext/dom/tests/gh23365.phpt
@@ -0,0 +1,53 @@
+--TEST--
+GH-23365 (DOMNode::insertBefore($n, $n) drops the node and leaves a self-referencing sibling list)
+--CREDITS--
+Alexandre Daubois
+--EXTENSIONS--
+dom
+--FILE--
+<?php
+$doc = new DOMDocument();
+$doc->loadXML('<root>text<child/></root>');
+$root = $doc->documentElement;
+
+$text = $root->firstChild;
+var_dump($root->insertBefore($text, $text) === $text);
+var_dump($root->childNodes->length);
+var_dump($text->parentNode === $root, $text->nextSibling === $text, $text->previousSibling === $text);
+
+$child = $root->lastChild;
+var_dump($root->insertBefore($child, $child) === $child);
+var_dump($root->childNodes->length);
+
+echo $doc->saveXML($root), PHP_EOL;
+
+$doc2 = new DOMDocument();
+$doc2->loadXML('<root a="1" b="2"/>');
+$el = $doc2->documentElement;
+$attr = $el->getAttributeNode('a');
+var_dump($el->insertBefore($attr, $attr) === $attr);
+echo $doc2->saveXML($el), PHP_EOL;
+
+$doc3 = new DOMDocument();
+$root3 = $doc3->appendChild($doc3->createElement('root'));
+$root3->appendChild($doc3->createTextNode('A'));
+$t = $root3->appendChild($doc3->createTextNode('B'));
+$root3->insertBefore($t, $t);
+$root3->appendChild($t);
+echo $doc3->saveXML($root3), PHP_EOL;
+unset($t, $root3, $doc3);
+echo "done", PHP_EOL;
+?>
+--EXPECT--
+bool(true)
+int(2)
+bool(true)
+bool(false)
+bool(false)
+bool(true)
+int(2)
+<root>text<child/></root>
+bool(true)
+<root a="1" b="2"/>
+<root>AB</root>
+done