[php-src] Issue #23365: ext/dom: `DOMNode::insertBefore($n, $n)` drops the node and leaves a self-referencing sibling list

[email protected] (alexandre-daubois)
Newsgroups php.bugs
Message-ID <1aU0EsJQUzCwZ1wEm07LBrJwb2dy05CWjHgdCLZ0QYc@main.internal.php.net>
Issue: https://github.com/php/php-src/issues/23365
Author: alexandre-daubois

### Description

The following code:

```php
<?php

$document = new DOMDocument();
$root = $document->appendChild($document->createElement('root'));
$text = $root->appendChild($document->createTextNode('self-insertion'));

// Use the same attached DOMText as both the child and insertion reference.
$root->insertBefore($text, $text);

// Reattach the corrupted node so document teardown traverses it.
$root->appendChild($text);

unset($text, $root, $document);
```

Produces a UAF on libxml2 2.9.14 and double free on libxml2. 2.9.13. `insertBefore($n, $n)` unlinks `$n` first, then builds the new position out of `$n`'s parent and sibling fields that the unlink just cleared, so `$n` ends up outside the document and pointing at itself as its own next sibling.

Asan trace:

```
==70244==ERROR: AddressSanitizer: attempting double-free on 0x602000002bd0 in thread T0:
    #0 0x000107055258 in free+0x7c (libclang_rt.asan_osx_dynamic.dylib:arm64e+0x41258)
    #1 0x000191105094 in xmlFreeNodeList+0x230 (libxml2.2.dylib:arm64e+0x11094)
    #2 0x000191104a50 in xmlFreeDoc+0xac (libxml2.2.dylib:arm64e+0x10a50)
    #3 0x000104b6da14 in php_libxml_decrement_doc_ref_directly libxml.c:1395
    #4 0x000104b6db28 in php_libxml_decrement_doc_ref libxml.c:1415
    #5 0x000104d5be0c in dom_objects_free_storage php_dom.c:1500
```

Note that the reproducer is kind of exotic with `$root->insertBefore($text, $text);`, and another reproducer is available, with less exotic code and its related Asan trace:

```php
<?php

$d = new DOMDocument();
$d->loadXML('<root><p>solo</p></root>');
$p = $d->getElementsByTagName('p')->item(0);
$t = $p->firstChild;
$p->insertBefore($p->firstChild, $p->lastChild);
$p->appendChild($t);

unset($t, $p, $d);
```

The issue is that `$p` only has one child, and `firstChild === lastChild` here

```
==70241==ERROR: AddressSanitizer: attempting double-free on 0x602000002c10 in thread T0:
    #0 0x000106995258 in free+0x7c (libclang_rt.asan_osx_dynamic.dylib:arm64e+0x41258)
    #1 0x000191105094 in xmlFreeNodeList+0x230 (libxml2.2.dylib:arm64e+0x11094)
    #2 0x000191104a50 in xmlFreeDoc+0xac (libxml2.2.dylib:arm64e+0x10a50)
    #3 0x0001043c5a14 in php_libxml_decrement_doc_ref_directly libxml.c:1395
    #4 0x0001043c5b28 in php_libxml_decrement_doc_ref libxml.c:1415
    #5 0x0001045b3e0c in dom_objects_free_storage php_dom.c:1500
    #6 0x0001051da578 in zend_objects_store_del zend_objects_API.c:193
    #7 0x0001050b1760 in ZEND_UNSET_CV_SPEC_CV_UNUSED_TAILCALL_HANDLER zend_vm_execute.h:102283
    #8 0x000104e857c8 in execute_ex zend_vm_execute.h:110551
    #9 0x000104e85f24 in zend_execute zend_vm_execute.h:115989
```

### PHP Version

```plain
All maintained versions
```

### Operating System

_No response_
lmpx.com only provides a reader for public news (NNTP) servers. It is not affiliated with the servers or forums shown here and is not responsible for the content of articles, which is written by their respective authors.