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