Skip to content

Commit f1848a4

Browse files
committed
Fix bug #78226: Don't call __set() on uninitialized typed properties
Assigning to an uninitialized typed property will no longer trigger a call to __set(). However, calls to __set() are still triggered if the property is explicitly unset(). This gives us both the behavior people generally expect, and still allows ORMs to do lazy initialization by unsetting properties. For PHP 8, we should fine a way to forbid unsetting of declared properties entirely, and provide a different way to achieve lazy initialization.
1 parent 4d8541d commit f1848a4

File tree

8 files changed

+89
-8
lines changed

8 files changed

+89
-8
lines changed

NEWS

+3
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,9 @@ PHP NEWS
22
|||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
33
?? ??? ????, PHP 7.4.0RC5
44

5+
- Core:
6+
. Fixed bug #78226 (Unexpected __set behavior with typed properties). (Nikita)
7+
58
- COM:
69
. Fixed bug #78694 (Appending to a variant array causes segfault). (cmb)
710

Original file line numberDiff line numberDiff line change
@@ -0,0 +1,54 @@
1+
--TEST--
2+
__set() should not be invoked when setting an uninitialized typed property
3+
--FILE--
4+
<?php
5+
6+
class Test {
7+
public int $foo;
8+
public function __set($name, $value) {
9+
echo "__set ", $name, " = ", $value, "\n";
10+
}
11+
}
12+
13+
$test = new Test;
14+
$test->foo = 42;
15+
var_dump($test->foo);
16+
17+
// __set will be called after unset()
18+
unset($test->foo);
19+
$test->foo = 42;
20+
21+
// __set will be called after unset() without prior initialization
22+
$test = new Test;
23+
unset($test->foo);
24+
$test->foo = 42;
25+
26+
class Test2 extends Test {
27+
}
28+
29+
// Check that inherited properties work correctly
30+
$test = new Test;
31+
$test->foo = 42;
32+
var_dump($test->foo);
33+
unset($test->foo);
34+
$test->foo = 42;
35+
36+
// Test that cloning works correctly
37+
$test = clone $test;
38+
$test->foo = 42;
39+
$test = clone new Test;
40+
$test->foo = 42;
41+
var_dump($test->foo);
42+
unset($test->foo);
43+
$test->foo = 42;
44+
45+
?>
46+
--EXPECT--
47+
int(42)
48+
__set foo = 42
49+
__set foo = 42
50+
int(42)
51+
__set foo = 42
52+
__set foo = 42
53+
int(42)
54+
__set foo = 42

Zend/zend_API.c

+6-3
Original file line numberDiff line numberDiff line change
@@ -1263,13 +1263,13 @@ static zend_always_inline void _object_properties_init(zend_object *object, zend
12631263

12641264
if (UNEXPECTED(class_type->type == ZEND_INTERNAL_CLASS)) {
12651265
do {
1266-
ZVAL_COPY_OR_DUP(dst, src);
1266+
ZVAL_COPY_OR_DUP_PROP(dst, src);
12671267
src++;
12681268
dst++;
12691269
} while (src != end);
12701270
} else {
12711271
do {
1272-
ZVAL_COPY(dst, src);
1272+
ZVAL_COPY_PROP(dst, src);
12731273
src++;
12741274
dst++;
12751275
} while (src != end);
@@ -3725,6 +3725,7 @@ ZEND_API int zend_declare_typed_property(zend_class_entry *ce, zend_string *name
37253725
}
37263726
}
37273727
} else {
3728+
zval *property_default_ptr;
37283729
if ((property_info_ptr = zend_hash_find_ptr(&ce->properties_info, name)) != NULL &&
37293730
(property_info_ptr->flags & ZEND_ACC_STATIC) == 0) {
37303731
property_info->offset = property_info_ptr->offset;
@@ -3745,7 +3746,9 @@ ZEND_API int zend_declare_typed_property(zend_class_entry *ce, zend_string *name
37453746
ce->properties_info_table[ce->default_properties_count - 1] = property_info;
37463747
}
37473748
}
3748-
ZVAL_COPY_VALUE(&ce->default_properties_table[OBJ_PROP_TO_NUM(property_info->offset)], property);
3749+
property_default_ptr = &ce->default_properties_table[OBJ_PROP_TO_NUM(property_info->offset)];
3750+
ZVAL_COPY_VALUE(property_default_ptr, property);
3751+
Z_PROP_FLAG_P(property_default_ptr) = Z_ISUNDEF_P(property) ? IS_PROP_UNINIT : 0;
37493752
}
37503753
if (ce->type & ZEND_INTERNAL_CLASS) {
37513754
switch(Z_TYPE_P(property)) {

Zend/zend_inheritance.c

+3-3
Original file line numberDiff line numberDiff line change
@@ -1167,7 +1167,7 @@ ZEND_API void zend_do_inheritance_ex(zend_class_entry *ce, zend_class_entry *par
11671167
do {
11681168
dst--;
11691169
src--;
1170-
ZVAL_COPY_VALUE(dst, src);
1170+
ZVAL_COPY_VALUE_PROP(dst, src);
11711171
} while (dst != end);
11721172
pefree(src, ce->type == ZEND_INTERNAL_CLASS);
11731173
end = ce->default_properties_table;
@@ -1182,7 +1182,7 @@ ZEND_API void zend_do_inheritance_ex(zend_class_entry *ce, zend_class_entry *par
11821182
do {
11831183
dst--;
11841184
src--;
1185-
ZVAL_COPY_OR_DUP(dst, src);
1185+
ZVAL_COPY_OR_DUP_PROP(dst, src);
11861186
if (Z_OPT_TYPE_P(dst) == IS_CONSTANT_AST) {
11871187
ce->ce_flags &= ~ZEND_ACC_CONSTANTS_UPDATED;
11881188
}
@@ -1192,7 +1192,7 @@ ZEND_API void zend_do_inheritance_ex(zend_class_entry *ce, zend_class_entry *par
11921192
do {
11931193
dst--;
11941194
src--;
1195-
ZVAL_COPY(dst, src);
1195+
ZVAL_COPY_PROP(dst, src);
11961196
if (Z_OPT_TYPE_P(dst) == IS_CONSTANT_AST) {
11971197
ce->ce_flags &= ~ZEND_ACC_CONSTANTS_UPDATED;
11981198
}

Zend/zend_object_handlers.c

+7
Original file line numberDiff line numberDiff line change
@@ -836,6 +836,11 @@ ZEND_API zval *zend_std_write_property(zval *object, zval *member, zval *value,
836836
zend_assign_to_variable(variable_ptr, value, IS_TMP_VAR, EG(current_execute_data) && ZEND_CALL_USES_STRICT_TYPES(EG(current_execute_data)));
837837
goto exit;
838838
}
839+
if (Z_PROP_FLAG_P(variable_ptr) == IS_PROP_UNINIT) {
840+
/* Writes to uninitializde typed properties bypass __set(). */
841+
Z_PROP_FLAG_P(variable_ptr) = 0;
842+
goto write_std_property;
843+
}
839844
} else if (EXPECTED(IS_DYNAMIC_PROPERTY_OFFSET(property_offset))) {
840845
if (EXPECTED(zobj->properties != NULL)) {
841846
if (UNEXPECTED(GC_REFCOUNT(zobj->properties) > 1)) {
@@ -1113,6 +1118,8 @@ ZEND_API void zend_std_unset_property(zval *object, zval *member, void **cache_s
11131118
}
11141119
goto exit;
11151120
}
1121+
/* Reset the IS_PROP_UNINIT flag, if it exists. */
1122+
Z_PROP_FLAG_P(slot) = 0;
11161123
} else if (EXPECTED(IS_DYNAMIC_PROPERTY_OFFSET(property_offset))
11171124
&& EXPECTED(zobj->properties != NULL)) {
11181125
if (UNEXPECTED(GC_REFCOUNT(zobj->properties) > 1)) {

Zend/zend_objects.c

+1-1
Original file line numberDiff line numberDiff line change
@@ -209,7 +209,7 @@ ZEND_API void ZEND_FASTCALL zend_objects_clone_members(zend_object *new_object,
209209

210210
do {
211211
i_zval_ptr_dtor(dst);
212-
ZVAL_COPY_VALUE(dst, src);
212+
ZVAL_COPY_VALUE_PROP(dst, src);
213213
zval_add_ref(dst);
214214
if (UNEXPECTED(Z_ISREF_P(dst)) &&
215215
(ZEND_DEBUG || ZEND_REF_HAS_TYPE_SOURCES(Z_REF_P(dst)))) {

Zend/zend_types.h

+14
Original file line numberDiff line numberDiff line change
@@ -1262,4 +1262,18 @@ static zend_always_inline uint32_t zval_delref_p(zval* pz) {
12621262
} \
12631263
} while (0)
12641264

1265+
/* Properties store a flag distinguishing unset and unintialized properties
1266+
* (both use IS_UNDEF type) in the Z_EXTRA space. As such we also need to copy
1267+
* the Z_EXTRA space when copying property default values etc. We define separate
1268+
* macros for this purpose, so this workaround is easier to remove in the future. */
1269+
#define IS_PROP_UNINIT 1
1270+
#define Z_PROP_FLAG_P(z) Z_EXTRA_P(z)
1271+
#define ZVAL_COPY_VALUE_PROP(z, v) \
1272+
do { *(z) = *(v); } while (0)
1273+
#define ZVAL_COPY_PROP(z, v) \
1274+
do { ZVAL_COPY(z, v); Z_PROP_FLAG_P(z) = Z_PROP_FLAG_P(v); } while (0)
1275+
#define ZVAL_COPY_OR_DUP_PROP(z, v) \
1276+
do { ZVAL_COPY_OR_DUP(z, v); Z_PROP_FLAG_P(z) = Z_PROP_FLAG_P(v); } while (0)
1277+
1278+
12651279
#endif /* ZEND_TYPES_H */

ext/opcache/zend_accelerator_util_funcs.c

+1-1
Original file line numberDiff line numberDiff line change
@@ -270,7 +270,7 @@ static void zend_class_copy_ctor(zend_class_entry **pce)
270270
end = src + ce->default_properties_count;
271271
ce->default_properties_table = dst;
272272
for (; src != end; src++, dst++) {
273-
ZVAL_COPY_VALUE(dst, src);
273+
ZVAL_COPY_VALUE_PROP(dst, src);
274274
}
275275
}
276276

0 commit comments

Comments
 (0)