Skip to content

Fix SimpleXML integer offsets that cannot resolve aliasing a node - #23068

Closed
iliaal wants to merge 1 commit into
php:PHP-8.4from
iliaal:fix/sxe-negative-offset
Closed

Fix SimpleXML integer offsets that cannot resolve aliasing a node#23068
iliaal wants to merge 1 commit into
php:PHP-8.4from
iliaal:fix/sxe-negative-offset

Conversation

@iliaal

@iliaal iliaal commented Aug 5, 2026

Copy link
Copy Markdown
Contributor

sxe_get_element_by_offset scans with nodendx <= offset, which is already false on the first iteration for a negative offset, so it returns the node it was given, and the SXE_ITER_NONE branches of the read and write handlers alias the node for every offset other than 0. $xml->item[-1] reports isset() true and reads the first item, assigning to it overwrites $xml->item[0], and on a single-element view $n[5] = 'Y' warns that no such element exists and then overwrites the node anyway. An offset that resolves to no element now warns and leaves the document alone.

Attribute lists are unaffected, a negative attribute offset already misses and mutates nothing, only the count in its warning is wrong. $x->children() has a separate pre-existing problem where the write handler resolves the offset starting from the node itself instead of descending to its children, so $c[0] = 'Z' replaces the root's content; that is unchanged here.

@iliaal
iliaal requested a review from devnexen as a code owner August 5, 2026 13:27
iliaal added a commit to iliaal/php-src that referenced this pull request Aug 5, 2026
sxe_get_element_by_offset scanned with nodendx <= offset, so a negative
offset skipped the loop and returned the node it started from. Reads and
isset() reported the first element, and a write overwrote it. Negative
offsets now miss, and writing to one warns like an out-of-range positive
offset instead of creating a node.

Closes phpGH-23068
@iliaal
iliaal force-pushed the fix/sxe-negative-offset branch from b8b2e9b to 7bdf744 Compare August 5, 2026 13:28
@devnexen

devnexen commented Aug 5, 2026

Copy link
Copy Markdown
Member

is a bug indeed, however can the following test being added ?

--TEST--
Integer offsets that cannot resolve must never alias or mutate a node
--EXTENSIONS--
simplexml
--FILE--
<?php
function fresh(): SimpleXMLElement {
    return simplexml_load_string('<r a="1" b="2"><item>a</item><item>b</item><item>c</item></r>');
}

function state(SimpleXMLElement $x): string {
    return trim(strstr($x->asXML(), '<r'));
}

echo '== element list ==', PHP_EOL;
$x = fresh();
var_dump(isset($x->item[-1]));
var_dump($x->item[-1]);
$x->item[-1] = 'Z';
echo state($x), PHP_EOL;
unset($x->item[-1]);
echo state($x), PHP_EOL;

echo '== single element (SXE_ITER_NONE) ==', PHP_EOL;
$x = fresh();
$n = $x->item[0];
var_dump(isset($n[-1]));
var_dump($n[-1]);
$n[-1] = 'Z';
echo state($x), PHP_EOL;
$n[5] = 'Y';
echo state($x), PHP_EOL;
unset($n[-1]);
echo state($x), PHP_EOL;

echo '== nested write ==', PHP_EOL;
$x = fresh();
try {
    $x->item[-1]->kid = 'K';
} catch (Throwable $e) {
    echo get_class($e), ': ', $e->getMessage(), PHP_EOL;
}
echo state($x), PHP_EOL;

echo '== attributes ==', PHP_EOL;
$x = fresh();
$at = $x->attributes();
var_dump(isset($at[-1]));
var_dump($at[-1]);
$at[-1] = 'z';
echo state($x), PHP_EOL;
?>
--EXPECTF--
== element list ==
bool(false)
NULL

Warning: main(): Cannot add element item number -1 when only 3 such elements exist in %s on line %d
<r a="1" b="2"><item>a</item><item>b</item><item>c</item></r>
<r a="1" b="2"><item>a</item><item>b</item><item>c</item></r>
== single element (SXE_ITER_NONE) ==
bool(false)
NULL

Warning: main(): Cannot add element item number -1 when only 0 such %d
<r a="1" b="2"><item>a</item><item>b</item><item>c</item></r>

Warning: main(): Cannot add element item number 5 when only 0 such elements exist in %s on line %d
<r a="1" b="2"><item>a</item><item>b</item><item>c</item></r>
<r a="1" b="2"><item>a</item><item>b</item><item>c</item></r>
== nested write ==
ValueError: Cannot use a negative offset
<r a="1" b="2"><item>a</item><item>b</item><item>c</item></r>
== attributes ==
bool(false)
NULL

iliaal added a commit to iliaal/php-src that referenced this pull request Aug 9, 2026
sxe_get_element_by_offset scanned with nodendx <= offset, so a negative
offset skipped the loop and returned the node it started from, and the
SXE_ITER_NONE branches of the read and write handlers aliased the node
for every offset other than 0. Reads and isset() reported an existing
element and writes overwrote it. An offset that resolves to no element
now warns and leaves the document alone.

Closes phpGH-23068
@iliaal
iliaal force-pushed the fix/sxe-negative-offset branch from 7bdf744 to 1a71661 Compare August 9, 2026 13:13
@iliaal

iliaal commented Aug 9, 2026

Copy link
Copy Markdown
Contributor Author

Ah, the SXE_ITER_NONE block found a real gap. Those branches in the read and write handlers aliased the node for every offset other than 0, positive included, so on a single-element view $n[-1] = 'Z' overwrote it and $n[5] = 'Y' warned that no such element exists and then overwrote it anyway. Fixed in 1a71661, and I took your test with two changes to the expected output.

SimpleXML has no ValueError for a negative offset today, so the nested write gives the warning, then a notice, then an Error for assigning a property on null. The notice is the engine's: with no node to hand back, read_dimension returns a non-object in write mode and that always draws it, so a single clean diagnostic means trading the warning for a throw. I'd rather do that on master than 8.4. The attributes block also warns "Cannot change attribute number -1 when only 0 attributes exist"; the count is wrong because the attribute loop never advances for a negative offset, but nothing is mutated, so I left that branch alone.

@iliaal iliaal changed the title Fix negative SimpleXML offsets aliasing the first element Fix SimpleXML integer offsets that cannot resolve aliasing a node Aug 9, 2026
Comment thread ext/simplexml/simplexml.c Outdated
node_as_zval(sxe, node, rv, SXE_ITER_NONE, NULL, sxe->iter.nsprefix, sxe->iter.isprefix);
} else if (type == BP_VAR_W || type == BP_VAR_RW) {
if (member && cnt < Z_LVAL_P(member)) {
if (member && (!appendable || Z_LVAL_P(member) < 0 || cnt < Z_LVAL_P(member))) {

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

if (member && (Z_LVAL_P(member) < 0 || cnt < Z_LVAL_P(member)))

it is redundant is false only when SXE_ITER_NONE && member != 0

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Dropped. cnt is still 0 in that branch, so a negative member is caught by the first disjunct and a positive one by cnt < Z_LVAL_P(member).

Comment thread ext/simplexml/simplexml.c Outdated
if (value_str) {
zend_string_release(value_str);
}
return &EG(error_zval);

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

value = &EG(error_zval);
goto out;

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

same when the node type is XML_ATTRIBUTE_NODE just above.

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Both converted in 374bb20. The shared tail also runs *pnewnode = newnode on those two paths now, which the early returns skipped; the only caller passing a pnewnode checks for error_zval first and passes a string member, so it never reaches either branch.

sxe_get_element_by_offset scanned with nodendx <= offset, so a negative
offset skipped the loop and returned the node it started from, and the
SXE_ITER_NONE branches of the read and write handlers aliased the node
for every offset other than 0. Reads and isset() reported an existing
element and writes overwrote it. An offset that resolves to no element
now warns and leaves the document alone.

Closes phpGH-23068
@iliaal
iliaal force-pushed the fix/sxe-negative-offset branch from 1a71661 to 374bb20 Compare August 9, 2026 14:48
@iliaal iliaal closed this in 9e69641 Aug 9, 2026
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants