[php-src] Issue #21548: Dom\XMLDocument::C14N() emits duplicate xmlns declarations after setAttributeNS() on element with default namespace
[email protected] (veewee)
| Newsgroups | php.bugs |
|---|---|
| Message-ID | <[email protected]> |
Issue: https://github.com/php/php-src/issues/21548
Author: veewee
### Description
`Dom\XMLDocument::C14N()` produces duplicate xmlns prefix declarations after calling `setAttributeNS()` to add a prefixed xmlns attribute on an element that already has a default xmlns and at least one non-xmlns attribute.
The `saveXML()` output is correct; only `C14N()` is affected.
Reproducer: https://3v4l.org/dDYik#v8.5.3
```php
<?php
echo "PHP " . PHP_VERSION . PHP_EOL;
echo "libxml " . LIBXML_DOTTED_VERSION . PHP_EOL . PHP_EOL;
$doc = Dom\XMLDocument::createFromString('<root xmlns="urn:a" attr="val"/>');
$doc->documentElement->setAttributeNS("http://www.w3.org/2000/xmlns/", "xmlns:ns1", "urn:a");
echo "saveXML: " . trim($doc->saveXML()) . PHP_EOL;
echo "C14N: " . $doc->C14N() . PHP_EOL;
// saveXML (correct):
// <root xmlns="urn:a" attr="val" xmlns:ns1="urn:a"/>
//
// C14N (BUGGY - xmlns:ns1 appears TWICE):
// <root xmlns="urn:a" xmlns:ns1="urn:a" attr="val" xmlns:ns1="urn:a"></root>
```
Confirmed on 3v4l.org with PHP 8.5.3 / libxml 2.9.14.
### Conditions
All three conditions must be met for the bug to trigger:
1. Element has a default xmlns (`xmlns="..."`)
2. Element has at least one non-xmlns attribute (e.g. `attr="val"`)
3. `setAttributeNS()` adds a prefixed xmlns pointing to the same URI as the default xmlns
Removing any of these conditions produces correct C14N output. `Element::rename()` is NOT required.
### Secondary issue: parsing the invalid C14N output
Attempting to parse the invalid C14N output with `createFromString()` correctly throws a `DOMException` on 3v4l.org ("Attribute xmlns:ns1 redefined"). However, this behavior appears to be very machine-dependent. In some environments (Docker `docphpro/php84`, GitHub Actions Ubuntu 24.04), parsing this same invalid output instead hangs indefinitely or crashes with memory corruption:
```
free(): double free detected in tcache 2
```
or:
```
munmap_chunk(): invalid pointer
```
This means the impact ranges from a catchable exception to a hard process crash depending on the environment.
### Real-world impact
The `comparable()` configurator in [veewee/xml](https://github.com/veewee/xml) calls `optimize_namespaces()` (which uses `setAttributeNS` + `Element::rename`) followed by `canonicalize()` (which calls `C14N()`). The invalid C14N output causes `createFromString()` to fail, breaking test suites that compare XML documents. On GitHub Actions (Ubuntu 24.04), this manifests as tests hanging until timeout.
Workaround: round-trip through `saveXML()` / `createFromString()` before calling `C14N()` to normalize namespace declarations. See https://github.com/veewee/xml/pull/101 for an example.
### Related
- https://github.com/php/php-src/issues/21544 (C14N drops namespace declarations on DOM-built documents)
- https://github.com/veewee/xml/pull/101 (workaround in veewee/xml)
### PHP Version
```plain
Reproduced on: PHP 8.4.19, PHP 8.5.3, PHP 8.5.4 (all with libxml 2.9.14)
Not reproducible on: PHP 8.4.13 with libxml 2.9.13 (macOS)
```
### Operating System
Ubuntu 24.04 (libxml 2.9.14), also confirmed on 3v4l.org. Not reproducible on macOS with libxml 2.9.13.