Skip to content

Commit ab665f5

Browse files
committed
ext/spl: Fix various bugs relating to property access
1 parent 196422e commit ab665f5

12 files changed

+285
-20
lines changed

ext/spl/spl_array.c

Lines changed: 131 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -460,6 +460,130 @@ static uint32_t spl_array_set_refcount(bool is_child, HashTable *ht, uint32_t re
460460
return old_refcount;
461461
} /* }}} */
462462

463+
/* For reasons ArrayObject must not go through the overloaded __set()/__get()/etc.
464+
* magic methods, so to emit a warning for dynamic props (or Error on classes
465+
* that do not support them, such as readonly classes) or an error for incompatible
466+
* typed properties, we need to reimplement the logic of zend_std_write_property()
467+
* somewhat. Thanks SPL. */
468+
static zval *zend_ignore_set_magic_method_write_property(
469+
HashTable *derived_properties_table,
470+
zend_object *zobj,
471+
zend_string *property_name,
472+
zval *value
473+
) {
474+
ZEND_ASSERT(!Z_ISREF_P(value));
475+
476+
zend_class_entry *ce = zobj->ce;
477+
zval *zv;
478+
if (UNEXPECTED(zend_hash_num_elements(&ce->properties_info) == 0)
479+
|| UNEXPECTED((zv = zend_hash_find(&ce->properties_info, property_name)) == NULL)) {
480+
/* Dynamic prop handling */
481+
dynamic_prop:
482+
/* TODO Modifying private/protected props?
483+
if (UNEXPECTED(ZSTR_VAL(member)[0] == '\0') && ZSTR_LEN(member) != 0) {
484+
zend_bad_property_name();
485+
return NULL;
486+
}
487+
*/
488+
// Dynamic props are not allowed on this class
489+
if (ce->ce_flags & ZEND_ACC_NO_DYNAMIC_PROPERTIES) {
490+
zend_throw_error(NULL, "Cannot create dynamic property %s::$%s", ZSTR_VAL(ce->name), ZSTR_VAL(property_name));
491+
return NULL;
492+
}
493+
if (UNEXPECTED(!(ce->ce_flags & ZEND_ACC_ALLOW_DYNAMIC_PROPERTIES))) {
494+
GC_ADDREF(zobj);
495+
zend_error(E_DEPRECATED, "Creation of dynamic property %s::$%s is deprecated", ZSTR_VAL(ce->name), ZSTR_VAL(property_name));
496+
if (UNEXPECTED(GC_DELREF(zobj) == 0)) {
497+
zend_objects_store_del(zobj);
498+
if (!EG(exception)) {
499+
/* We cannot continue execution and have to throw an exception */
500+
zend_throw_error(NULL, "Cannot create dynamic property %s::$%s", ZSTR_VAL(ce->name), ZSTR_VAL(property_name));
501+
}
502+
return NULL;
503+
}
504+
}
505+
/* Create/Update dynamic prop */
506+
zend_hash_update_ind(derived_properties_table, property_name, value);
507+
return value;
508+
}
509+
510+
const zend_property_info *property_info = (zend_property_info*)Z_PTR_P(zv);
511+
uint32_t prop_flags = property_info->flags;
512+
513+
if (prop_flags & (ZEND_ACC_CHANGED|ZEND_ACC_PROTECTED|ZEND_ACC_PRIVATE)) {
514+
zend_throw_error(NULL, "Cannot access %s property %s::$%s", zend_visibility_string(prop_flags), ZSTR_VAL(ce->name), ZSTR_VAL(property_name));
515+
return NULL;
516+
}
517+
if (UNEXPECTED(prop_flags & ZEND_ACC_STATIC)) {
518+
zend_error(E_NOTICE, "Accessing static property %s::$%s as non static", ZSTR_VAL(ce->name), ZSTR_VAL(property_name));
519+
/* Handle like dynamic prop */
520+
goto dynamic_prop;
521+
}
522+
523+
uintptr_t property_offset = property_info->offset;
524+
zval *variable_ptr = OBJ_PROP(zobj, property_offset);
525+
526+
// Property is already set, check readonly violation
527+
if (Z_TYPE_P(variable_ptr) != IS_UNDEF) {
528+
if (UNEXPECTED((prop_flags & ZEND_ACC_READONLY) && !(Z_PROP_FLAG_P(variable_ptr) & IS_PROP_REINITABLE))) {
529+
Z_TRY_DELREF_P(value);
530+
zend_readonly_property_modification_error(property_info);
531+
return NULL;
532+
}
533+
}
534+
535+
/* Remove fact that property can be reinitialized */
536+
Z_PROP_FLAG_P(variable_ptr) &= ~IS_PROP_REINITABLE;
537+
Z_TRY_ADDREF_P(value);
538+
/* Check value is of valid type if property has type */
539+
if (UNEXPECTED(ZEND_TYPE_IS_SET(property_info->type))) {
540+
zval tmp;
541+
ZVAL_COPY_VALUE(&tmp, value);
542+
// Increase refcount to prevent object from being released in __toString()
543+
GC_ADDREF(zobj);
544+
/* Engine never uses strict types */
545+
bool type_matched = zend_verify_property_type(property_info, &tmp, /* strict_types */ false);
546+
if (UNEXPECTED(GC_DELREF(zobj) == 0)) {
547+
zend_object_released_while_assigning_to_property_error(property_info);
548+
zend_objects_store_del(zobj);
549+
zval_ptr_dtor(&tmp);
550+
variable_ptr = &EG(error_zval);
551+
return NULL;
552+
}
553+
if (UNEXPECTED(!type_matched)) {
554+
zval_ptr_dtor(value);
555+
return NULL;
556+
}
557+
value = &tmp;
558+
Z_PROP_FLAG_P(variable_ptr) = 0;
559+
}
560+
561+
zend_refcounted *garbage = NULL;
562+
/* Engine never uses strict types */
563+
variable_ptr = zend_assign_to_variable_ex(variable_ptr, value, IS_TMP_VAR, /* strict_types */ false, &garbage);
564+
if (garbage) {
565+
if (GC_DELREF(garbage) == 0) {
566+
zend_execute_data *execute_data = EG(current_execute_data);
567+
// Assign to result variable before calling the destructor as it may release the object
568+
if (execute_data
569+
&& EX(func)
570+
&& ZEND_USER_CODE(EX(func)->common.type)
571+
&& EX(opline)
572+
&& EX(opline)->opcode == ZEND_ASSIGN_OBJ
573+
&& EX(opline)->result_type
574+
) {
575+
ZVAL_COPY_DEREF(EX_VAR(EX(opline)->result.var), variable_ptr);
576+
variable_ptr = NULL;
577+
}
578+
rc_dtor_func(garbage);
579+
} else {
580+
gc_check_possible_root_no_ref(garbage);
581+
}
582+
}
583+
//ZVAL_COPY_VALUE(variable_ptr, value);
584+
return variable_ptr;
585+
}
586+
463587
static void spl_array_write_dimension_ex(int check_inherited, zend_object *object, zval *offset, zval *value) /* {{{ */
464588
{
465589
spl_array_object *intern = spl_array_from_obj(object);
@@ -492,12 +616,6 @@ static void spl_array_write_dimension_ex(int check_inherited, zend_object *objec
492616
}
493617

494618
if (check_inherited && intern->fptr_offset_set) {
495-
zval tmp;
496-
497-
if (!offset) {
498-
ZVAL_NULL(&tmp);
499-
offset = &tmp;
500-
}
501619
zend_call_method_with_2_params(object, object->ce, &intern->fptr_offset_set, "offsetSet", NULL, offset, value);
502620
return;
503621
}
@@ -513,10 +631,15 @@ static void spl_array_write_dimension_ex(int check_inherited, zend_object *objec
513631
ht = spl_array_get_hash_table(intern);
514632
refcount = spl_array_set_refcount(intern->is_child, ht, 1);
515633
if (key.key) {
516-
if (spl_array_is_object(intern)) {
634+
if (spl_array_is_object(intern) && !(intern->ar_flags & SPL_ARRAY_IS_SELF)) {
517635
ZEND_ASSERT(Z_TYPE(intern->array) == IS_OBJECT);
518636
zend_object *obj = Z_OBJ(intern->array);
519-
obj->handlers->write_property(obj, key.key, value, NULL);
637+
/* For reasons ArrayObject must not go through the overloaded __set()/__get()/etc.
638+
* magic methods, so to emit a warning for dynamic props (or Error on classes
639+
* that do not support them, such as readonly classes) or an error for incompatible
640+
* typed properties, we need to reimplement the logic of zend_std_write_property()
641+
* somewhat. Thanks SPL. */
642+
zend_ignore_set_magic_method_write_property(ht, obj, key.key, value);
520643
} else {
521644
zend_hash_update_ind(ht, key.key, value);
522645
}
Lines changed: 38 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,38 @@
1+
--TEST--
2+
GH-9432: Dynamic property deprecation warning/readonly error not emitted when modifying through new ArrayObject($targetObj);
3+
--EXTENSIONS--
4+
spl
5+
--FILE--
6+
<?php
7+
class MyClass {}
8+
$c = new MyClass();
9+
$y = new ArrayObject($c);
10+
$y['foo1'] = 'bar';
11+
var_dump($c->foo1);
12+
$c->foo2 = 2;
13+
var_dump($c);
14+
15+
readonly class ReadOnly_ {}
16+
$r = new ReadOnly_();
17+
$z = new ArrayObject($r);
18+
try {
19+
$z['foo'] = 'bar';
20+
} catch (\Throwable $e) {
21+
echo $e::class, ': ', $e->getMessage(), PHP_EOL;
22+
}
23+
var_dump($r);
24+
?>
25+
--EXPECTF--
26+
Deprecated: Creation of dynamic property MyClass::$foo1 is deprecated in %s on line %d
27+
string(3) "bar"
28+
29+
Deprecated: Creation of dynamic property MyClass::$foo2 is deprecated in %s on line %d
30+
object(MyClass)#1 (2) {
31+
["foo1"]=>
32+
string(3) "bar"
33+
["foo2"]=>
34+
int(2)
35+
}
36+
Error: Cannot create dynamic property ReadOnly_::$foo
37+
object(ReadOnly_)#3 (0) {
38+
}
Lines changed: 25 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,25 @@
1+
--TEST--
2+
GH-9434: new ArrayObject($targetObject) allows setting typed properties to invalid types
3+
--EXTENSIONS--
4+
spl
5+
--FILE--
6+
<?php
7+
class C {
8+
public int $intProp;
9+
}
10+
$c = new C();
11+
$a = new ArrayObject($c);
12+
13+
try {
14+
$a['intProp'] = [];
15+
} catch (\Throwable $e) {
16+
echo $e::class, ': ', $e->getMessage(), PHP_EOL;
17+
}
18+
var_dump($c);
19+
?>
20+
--EXPECT--
21+
TypeError: Cannot assign array to property C::$intProp of type int
22+
object(C)#1 (0) {
23+
["intProp"]=>
24+
uninitialized(int)
25+
}
Lines changed: 63 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,63 @@
1+
--TEST--
2+
Cannot append to ArrayObject if backing value is an object
3+
--EXTENSIONS--
4+
spl
5+
--FILE--
6+
<?php
7+
8+
class MyClass {
9+
private $foo1;
10+
protected $foo2;
11+
12+
public function read() {
13+
var_dump($this->foo1, $this->foo2);
14+
}
15+
}
16+
$c = new MyClass();
17+
$ao = new ArrayObject($c);
18+
19+
try {
20+
$ao['foo1'] = 'bar1';
21+
} catch (\Throwable $e) {
22+
echo $e::class, ': ', $e->getMessage(), PHP_EOL;
23+
}
24+
try {
25+
$ao['foo2'] = 'bar2';
26+
} catch (\Throwable $e) {
27+
echo $e::class, ': ', $e->getMessage(), PHP_EOL;
28+
}
29+
30+
var_dump($c);
31+
$c->read();
32+
var_dump($ao['foo1']);
33+
var_dump($ao['foo2']);
34+
try {
35+
var_dump($c->foo1);
36+
} catch (\Throwable $e) {
37+
echo $e::class, ': ', $e->getMessage(), PHP_EOL;
38+
}
39+
try {
40+
var_dump($c->foo2);
41+
} catch (\Throwable $e) {
42+
echo $e::class, ': ', $e->getMessage(), PHP_EOL;
43+
}
44+
?>
45+
--EXPECTF--
46+
Error: Cannot access private property MyClass::$foo1
47+
Error: Cannot access protected property MyClass::$foo2
48+
object(MyClass)#1 (2) {
49+
["foo1":"MyClass":private]=>
50+
NULL
51+
["foo2":protected]=>
52+
NULL
53+
}
54+
NULL
55+
NULL
56+
57+
Warning: Undefined array key "foo1" in %s on line %d
58+
NULL
59+
60+
Warning: Undefined array key "foo2" in %s on line %d
61+
NULL
62+
Error: Cannot access private property MyClass::$foo1
63+
Error: Cannot access protected property MyClass::$foo2

ext/spl/tests/ArrayObject/using_null_as_key.phpt

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -14,7 +14,8 @@ var_dump($c);
1414
var_dump($c->{""});
1515
var_dump($ao[null]);
1616
?>
17-
--EXPECT--
17+
--EXPECTF--
18+
Deprecated: Creation of dynamic property MyClass::$ is deprecated in %s on line %d
1819
object(MyClass)#1 (1) {
1920
[""]=>
2021
string(8) "null key"

ext/spl/tests/ArrayObject/using_null_for_offset_methods.phpt

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -15,7 +15,8 @@ var_dump($ao->offsetGet(null));
1515
var_dump($c);
1616
var_dump($c->{""});
1717
?>
18-
--EXPECT--
18+
--EXPECTF--
19+
Deprecated: Creation of dynamic property MyClass::$ is deprecated in %s on line %d
1920
bool(true)
2021
string(8) "null key"
2122
object(MyClass)#1 (1) {

ext/spl/tests/ArrayObject_overloaded_SplFixedArray.phpt

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -9,7 +9,8 @@ $ao->exchangeArray($fixedArray);
99
$ao[0] = new stdClass();
1010
var_dump($ao);
1111
?>
12-
--EXPECT--
12+
--EXPECTF--
13+
Deprecated: Creation of dynamic property SplFixedArray::$0 is deprecated in %s on line %d
1314
object(ArrayObject)#1 (1) {
1415
["storage":"ArrayObject":private]=>
1516
object(SplFixedArray)#2 (2) {

ext/spl/tests/arrayObject_clone_basic3.phpt

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -23,7 +23,8 @@ $clonedOuterArrayObject['new.coAO'] = 'new element added to $clonedOuterArrayObj
2323

2424
var_dump($wrappedObject, $innerArrayObject, $outerArrayObject, $clonedOuterArrayObject);
2525
?>
26-
--EXPECT--
26+
--EXPECTF--
27+
Deprecated: Creation of dynamic property ArrayObject::$new.oAO is deprecated in %s on line %d
2728
object(C)#1 (5) {
2829
["p"]=>
2930
string(9) "C::p.orig"

ext/spl/tests/arrayObject_magicMethods1.phpt

Lines changed: 8 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -10,20 +10,16 @@ class UsesMagic {
1010
private $priv = 'secret';
1111

1212
function __get($name) {
13-
$args = func_get_args();
14-
echo "In " . __METHOD__ . "(" . implode($args, ',') . ")\n";
13+
echo "In " . __METHOD__ . "($name)\n";
1514
}
1615
function __set($name, $value) {
17-
$args = func_get_args();
18-
echo "In " . __METHOD__ . "(" . implode($args, ',') . ")\n";
16+
echo "In " . __METHOD__ . "($name, $value)\n";
1917
}
2018
function __isset($name) {
21-
$args = func_get_args();
22-
echo "In " . __METHOD__ . "(" . implode($args, ',') . ")\n";
19+
echo "In " . __METHOD__ . "($name)\n";
2320
}
2421
function __unset($name) {
25-
$args = func_get_args();
26-
echo "In " . __METHOD__ . "(" . implode($args, ',') . ")\n";
22+
echo "In " . __METHOD__ . "($name)\n";
2723
}
2824

2925
}
@@ -69,6 +65,10 @@ var_dump($ao);
6965
?>
7066
--EXPECTF--
7167
--> Write existent, non-existent and dynamic:
68+
69+
Deprecated: Creation of dynamic property UsesMagic::$dynamic is deprecated in %s on line %d
70+
71+
Deprecated: Creation of dynamic property UsesMagic::$dynamic is deprecated in %s on line %d
7272
Original wrapped object:
7373
object(UsesMagic)#1 (5) {
7474
["a"]=>

ext/spl/tests/arrayObject_magicMethods3.phpt

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -69,6 +69,10 @@ var_dump($ao);
6969
?>
7070
--EXPECTF--
7171
--> Write existent, non-existent and dynamic:
72+
73+
Deprecated: Creation of dynamic property UsesMagic::$dynamic is deprecated in %s on line %d
74+
75+
Deprecated: Creation of dynamic property UsesMagic::$dynamic is deprecated in %s on line %d
7276
Original wrapped object:
7377
object(UsesMagic)#1 (5) {
7478
["a"]=>

ext/spl/tests/arrayObject_magicMethods4.phpt

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -72,6 +72,10 @@ var_dump($ao);
7272
?>
7373
--EXPECTF--
7474
--> Write existent, non-existent and dynamic:
75+
76+
Deprecated: Creation of dynamic property C::$dynamic is deprecated in %s on line %d
77+
78+
Deprecated: Creation of dynamic property C::$dynamic is deprecated in %s on line %d
7579
Original wrapped object:
7680
object(C)#1 (5) {
7781
["a"]=>

ext/spl/tests/arrayObject_magicMethods6.phpt

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -72,6 +72,10 @@ var_dump($ao);
7272
?>
7373
--EXPECTF--
7474
--> Write existent, non-existent and dynamic:
75+
76+
Deprecated: Creation of dynamic property C::$dynamic is deprecated in %s on line %d
77+
78+
Deprecated: Creation of dynamic property C::$dynamic is deprecated in %s on line %d
7579
Original wrapped object:
7680
object(C)#1 (5) {
7781
["a"]=>

0 commit comments

Comments
 (0)