[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.
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.