Fix SimpleXML integer offsets that cannot resolve aliasing a node - #23068
Fix SimpleXML integer offsets that cannot resolve aliasing a node#23068iliaal wants to merge 1 commit into
Conversation
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
b8b2e9b to
7bdf744
Compare
|
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 |
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
7bdf744 to
1a71661
Compare
|
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 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, |
| 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))) { |
There was a problem hiding this comment.
if (member && (Z_LVAL_P(member) < 0 || cnt < Z_LVAL_P(member)))
it is redundant is false only when SXE_ITER_NONE && member != 0
There was a problem hiding this comment.
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).
| if (value_str) { | ||
| zend_string_release(value_str); | ||
| } | ||
| return &EG(error_zval); |
There was a problem hiding this comment.
value = &EG(error_zval);
goto out;
There was a problem hiding this comment.
same when the node type is XML_ATTRIBUTE_NODE just above.
There was a problem hiding this comment.
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
1a71661 to
374bb20
Compare
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.