Skip to content

Commit 0acc0d7

Browse files
committed
Merge branch 'master' into perf/yyjson
2 parents da816ab + 4288479 commit 0acc0d7

25 files changed

Lines changed: 665 additions & 44 deletions

.github/workflows/test-suite.yml

Lines changed: 0 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -573,10 +573,6 @@ jobs:
573573
php -r '$c = file_get_contents("src/Symfony/Component/HtmlSanitizer/Tests/HtmlSanitizerCustomTest.php"); $c = str_replace("public function testSanitizeDeepNestedString()", "#[\\PHPUnit\\Framework\\Attributes\\Group('"'"'skip'"'"')]\n public function testSanitizeDeepNestedString()", $c); file_put_contents("src/Symfony/Component/HtmlSanitizer/Tests/HtmlSanitizerCustomTest.php", $c);'
574574
# Buggy FFI test in Symfony, see https://github.com/symfony/symfony/issues/47668
575575
php -r '$c = file_get_contents("src/Symfony/Component/VarDumper/Tests/Caster/FFICasterTest.php"); $c = str_replace("public function testCastNonTrailingCharPointer()", "#[\\PHPUnit\\Framework\\Attributes\\Group('"'"'skip'"'"')]\n public function testCastNonTrailingCharPointer()", $c); file_put_contents("src/Symfony/Component/VarDumper/Tests/Caster/FFICasterTest.php", $c);'
576-
# Causes massive amounts of system calls with USE_ZEND_ALLOC=0, exceeding the timeout
577-
if [ -e 'src/Symfony/Component/Console/Tests/Helper/FileInputHelperTest.php' ]; then
578-
php -r '$c = file_get_contents("src/Symfony/Component/Console/Tests/Helper/FileInputHelperTest.php"); $c = str_replace("public function testReadWithPasteDetectionAbortsBeyondMaxBytes()", "#[\\PHPUnit\\Framework\\Attributes\\Group('"'"'skip'"'"')]\n public function testReadWithPasteDetectionAbortsBeyondMaxBytes()", $c); file_put_contents("src/Symfony/Component/Console/Tests/Helper/FileInputHelperTest.php", $c);'
579-
fi
580576
export SYMFONY_DEPRECATIONS_HELPER=max[total]=999
581577
X=0
582578
for component in $(find src/Symfony -mindepth 2 -type f -name phpunit.xml.dist -printf '%h\n'); do

NEWS

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -9,6 +9,8 @@ PHP NEWS
99
. Fixed bug GH-15672 and GH-15911 (Stack overflow when an internal function
1010
recurses through zend_call_function, such as a self-attached SPL
1111
iterator). (iliaal)
12+
. Lock unmodified readonly properties for modification after clone-with.
13+
(NickSdot)
1214

1315
- Calendar:
1416
. Fixed bug GH-22602 (gregoriantojd() and juliantojd() integer overflow with
@@ -41,6 +43,8 @@ PHP NEWS
4143
returning the first entity or notation. (Weilin Du)
4244
. Fixed bug GH-22623 (use after free with namespace nodes from
4345
XSLTProcessor::registerFunctions())/ (David Carlier)
46+
. Fixed bug GH-22554 (use-after-free with XPath callback returning a node
47+
from a foreign document). (David Carlier)
4448

4549
- Exif:
4650
. Fixed bug GH-11020 (exif_read_data() emits a spurious "Illegal IFD size"
@@ -95,6 +99,8 @@ PHP NEWS
9599
unsigned int timeout. (Weilin Du)
96100
. Fixed bug GH-22671 (assert.bail aborts the process when the assert callback
97101
throws an exception whose reporting re-throws). (iliaal)
102+
. Fixed bug GH-22678 (Use-after-free in array_multisort() when the comparator
103+
mutates the array being sorted). (azchin, iliaal)
98104

99105
- Streams:
100106
. Fixed bug GH-21468 (Segfault in file_get_contents w/ a https URL

UPGRADING.INTERNALS

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -173,6 +173,14 @@ PHP 8.6 INTERNALS UPGRADE NOTES
173173
. php_idate() now returns the result state, and moves the return value into an
174174
out parameter.
175175

176+
- ext/intl:
177+
. Added intl_icu_compat.h with helpers and feature macros for ICU
178+
version-specific API differences. Code in ext/intl should use the
179+
intl_icu_compat_* helpers and INTL_ICU_HAS_* macros instead of adding
180+
direct U_ICU_VERSION_* guards for supported ICU API variants.
181+
. The internal grapheme_get_break_iterator() helper no longer accepts a
182+
stack buffer argument; pass only the UErrorCode* status argument.
183+
176184
- ext/mbstring:
177185
. Added GB18030-2022 to default encoding list for zh-CN.
178186

Lines changed: 41 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,41 @@
1+
--TEST--
2+
Properties are still readonly after clone-with
3+
--FILE--
4+
<?php
5+
6+
readonly class Test {
7+
public public(set) int $a;
8+
public public(set) int $b;
9+
10+
public function __construct() {
11+
$this->a = 1;
12+
$this->b = 2;
13+
}
14+
}
15+
16+
$test = clone(new Test(), ['a' => 3]);
17+
var_dump($test);
18+
19+
try {
20+
$test->b = 4;
21+
} catch (Error $e) {
22+
echo $e::class, ": ", $e->getMessage(), PHP_EOL;
23+
}
24+
25+
var_dump($test);
26+
27+
?>
28+
--EXPECT--
29+
object(Test)#2 (2) {
30+
["a"]=>
31+
int(3)
32+
["b"]=>
33+
int(2)
34+
}
35+
Error: Cannot modify readonly property Test::$b
36+
object(Test)#2 (2) {
37+
["a"]=>
38+
int(3)
39+
["b"]=>
40+
int(2)
41+
}

Zend/zend_objects.c

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -321,6 +321,14 @@ ZEND_API zend_object *zend_objects_clone_obj_with(zend_object *old_object, const
321321
} ZEND_HASH_FOREACH_END();
322322

323323
EG(fake_scope) = old_scope;
324+
325+
/* Lock readonly properties once more. */
326+
if (ZEND_CLASS_HAS_READONLY_PROPS(new_object->ce)) {
327+
for (uint32_t i = 0; i < new_object->ce->default_properties_count; i++) {
328+
zval* prop = OBJ_PROP_NUM(new_object, i);
329+
Z_PROP_FLAG_P(prop) &= ~IS_PROP_REINITABLE;
330+
}
331+
}
324332
}
325333

326334
return new_object;

ext/dom/obj_map.c

Lines changed: 7 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -345,22 +345,20 @@ static void dom_map_get_by_class_name_item(dom_nnodemap_object *map, zend_long i
345345
if (nodep && index >= 0) {
346346
dom_node_idx_pair start_point = dom_obj_map_get_start_point(map, nodep, index);
347347
if (start_point.node) {
348-
if (start_point.index > 0) {
349-
/* Only start iteration at next point if we actually have an index to seek to. */
350-
itemnode = php_dom_next_in_tree_order(start_point.node, nodep);
351-
} else {
352-
itemnode = start_point.node;
353-
}
348+
itemnode = start_point.node;
354349
} else {
355350
itemnode = php_dom_first_child_of_container_node(nodep);
351+
while (itemnode != NULL && !dom_matches_class_name(map, itemnode)) {
352+
itemnode = php_dom_next_in_tree_order(itemnode, nodep);
353+
}
356354
}
357355

358-
do {
359-
--start_point.index;
356+
for (; start_point.index > 0 && itemnode != NULL; --start_point.index) {
357+
itemnode = php_dom_next_in_tree_order(itemnode, nodep);
360358
while (itemnode != NULL && !dom_matches_class_name(map, itemnode)) {
361359
itemnode = php_dom_next_in_tree_order(itemnode, nodep);
362360
}
363-
} while (start_point.index > 0 && itemnode);
361+
}
364362
}
365363
dom_ret_node_to_zobj(map, itemnode, return_value);
366364
if (itemnode) {

ext/dom/tests/gh22554.phpt

Lines changed: 38 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,38 @@
1+
--TEST--
2+
GH-22554 (Use-after-free when an XPath callback returns a node from a document created inside the callback)
3+
--CREDITS--
4+
waseem-cve
5+
--EXTENSIONS--
6+
dom
7+
--FILE--
8+
<?php
9+
10+
$doc = new DOMDocument;
11+
$doc->loadXML('<root/>');
12+
13+
$xp = new DOMXPath($doc);
14+
$xp->registerNamespace('my', 'my.ns');
15+
16+
$xp->registerPHPFunctionNS('my.ns', 'include', function () {
17+
$d = new DOMDocument;
18+
$d->loadXML('<r><uaf/></r>');
19+
20+
return $d->documentElement;
21+
});
22+
23+
$xp->registerPHPFunctionNS('my.ns', 'process', function ($arg) {
24+
echo "process arg: ", get_class($arg[0]), " ", $arg[0]->nodeName, "\n";
25+
return 'x';
26+
});
27+
28+
$result = $xp->query('my:process(my:include()/uaf)');
29+
var_dump($result->length);
30+
unset($xp);
31+
32+
echo "Done\n";
33+
34+
?>
35+
--EXPECT--
36+
process arg: DOMElement uaf
37+
int(0)
38+
Done
Lines changed: 84 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,84 @@
1+
--TEST--
2+
Dom\Element::getElementsByClassName() item() random access (cold and backwards)
3+
--EXTENSIONS--
4+
dom
5+
--FILE--
6+
<?php
7+
8+
/* Regression: the item() lookup must return the n-th match for random access,
9+
* not only for a strictly ascending / foreach walk. A fresh collection whose
10+
* first access is item(n) exercises the uncached path, and a decreasing index
11+
* on the same collection exercises the cache-discard path. Non-matching and
12+
* nested elements verify that iteration advances to the next matching element
13+
* in tree order, not merely to the next sibling. */
14+
15+
$dom = Dom\HTMLDocument::createFromString(<<<HTML
16+
<!DOCTYPE html>
17+
<body>
18+
<span class="x" id="E0"></span>
19+
<div class="y" id="skip1"><span class="x" id="E1"></span></div>
20+
<span class="x" id="E2"></span>
21+
<p class="z"><b class="x" id="E3"></b></p>
22+
<span class="x" id="E4"></span>
23+
</body>
24+
HTML);
25+
26+
$body = $dom->getElementsByTagName('body')->item(0);
27+
28+
function id(?Dom\Element $e): string {
29+
return $e === null ? 'NULL' : $e->id;
30+
}
31+
32+
echo "-- cold random access (fresh collection per call) --\n";
33+
foreach ([0, 1, 2, 3, 4, 5] as $i) {
34+
$collection = $body->getElementsByClassName('x');
35+
echo "item($i) = ", id($collection->item($i)), "\n";
36+
}
37+
38+
echo "-- backwards seek on one collection --\n";
39+
$collection = $body->getElementsByClassName('x');
40+
foreach ([4, 2, 0, 3, 1] as $i) {
41+
echo "item($i) = ", id($collection->item($i)), "\n";
42+
}
43+
44+
echo "-- item() seed then foreach --\n";
45+
$collection = $body->getElementsByClassName('x');
46+
$collection->item(3);
47+
$ids = [];
48+
foreach ($collection as $node) {
49+
$ids[] = $node->id;
50+
}
51+
echo implode(" ", $ids), "\n";
52+
53+
echo "-- last-element idiom --\n";
54+
$collection = $body->getElementsByClassName('x');
55+
echo "length = ", $collection->length, ", last = ", id($collection->item($collection->length - 1)), "\n";
56+
57+
echo "-- live collection after mutation --\n";
58+
$collection = $body->getElementsByClassName('x');
59+
echo "item(1) = ", id($collection->item(1)), "\n";
60+
$dom->getElementById('E1')->remove();
61+
echo "item(1) = ", id($collection->item(1)), ", length = ", $collection->length, "\n";
62+
63+
?>
64+
--EXPECT--
65+
-- cold random access (fresh collection per call) --
66+
item(0) = E0
67+
item(1) = E1
68+
item(2) = E2
69+
item(3) = E3
70+
item(4) = E4
71+
item(5) = NULL
72+
-- backwards seek on one collection --
73+
item(4) = E4
74+
item(2) = E2
75+
item(0) = E0
76+
item(3) = E3
77+
item(1) = E1
78+
-- item() seed then foreach --
79+
E0 E1 E2 E3 E4
80+
-- last-element idiom --
81+
length = 5, last = E4
82+
-- live collection after mutation --
83+
item(1) = E1
84+
item(1) = E2, length = 4

ext/dom/xpath.c

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -77,7 +77,8 @@ static void dom_xpath_proxy_factory(xmlNodePtr node, zval *child, dom_object *in
7777

7878
ZEND_ASSERT(node->type != XML_NAMESPACE_DECL);
7979

80-
php_dom_create_object(node, child, intern);
80+
dom_xpath_object *xobj = php_xpath_obj_from_obj(&intern->std);
81+
php_dom_create_object(node, child, dom_xpath_intern_for_doc(xobj, node->doc));
8182
}
8283

8384
static dom_xpath_object *dom_xpath_ext_fetch_intern(xmlXPathParserContextPtr ctxt)

0 commit comments

Comments
 (0)