-
Notifications
You must be signed in to change notification settings - Fork 8.1k
Fix GH-12695: skip __get() when __isset() materialised the property #22181
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
base: master
Are you sure you want to change the base?
Changes from all commits
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,46 @@ | ||
| --TEST-- | ||
| GH-12695: __get() is not called under `??` when __isset() materialised the property | ||
| --FILE-- | ||
| <?php | ||
|
|
||
| #[AllowDynamicProperties] | ||
| class A { | ||
| public function __get($n) { | ||
| throw new Exception("__get must not be called when __isset materialised the property"); | ||
| } | ||
| public function __isset($n) { | ||
| echo " __isset($n)\n"; | ||
| $this->$n = 123; | ||
| return true; | ||
| } | ||
| } | ||
|
|
||
| echo "Dynamic property materialised in __isset, then `??`:\n"; | ||
| $a = new A; | ||
| var_dump($a->foo ?? 'fallback'); | ||
|
|
||
| echo "\nSame on a declared (unset) property:\n"; | ||
| class B { | ||
| public int $x = 99; | ||
| public function __get($n) { | ||
| throw new Exception("__get must not be called when __isset materialised the property"); | ||
| } | ||
| public function __isset($n) { | ||
| echo " __isset($n)\n"; | ||
| $this->$n = 7; | ||
| return true; | ||
| } | ||
| } | ||
| $b = new B; | ||
| unset($b->x); | ||
| var_dump($b->x ?? 'fallback'); | ||
|
|
||
| ?> | ||
| --EXPECT-- | ||
| Dynamic property materialised in __isset, then `??`: | ||
| __isset(foo) | ||
| int(123) | ||
|
|
||
| Same on a declared (unset) property: | ||
| __isset(x) | ||
| int(7) |
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,55 @@ | ||
| --TEST-- | ||
| GH-12695: empty() also benefits from the post-__isset() materialization re-check | ||
| --FILE-- | ||
| <?php | ||
|
|
||
| #[AllowDynamicProperties] | ||
| class A { | ||
| public function __get($n) { | ||
| throw new Exception("__get must not be called when __isset materialised the property"); | ||
| } | ||
| public function __isset($n) { | ||
| echo " __isset($n)\n"; | ||
| $this->$n = $GLOBALS['next_value']; | ||
| return true; | ||
| } | ||
| } | ||
|
|
||
| echo "1) empty() when __isset materialised a truthy value: __get is not called, empty=false:\n"; | ||
| $GLOBALS['next_value'] = 7; | ||
| $a = new A; | ||
| var_dump(empty($a->foo)); | ||
|
|
||
| echo "\n2) empty() when __isset materialised a falsy value: __get is not called, empty=true:\n"; | ||
| $GLOBALS['next_value'] = 0; | ||
| $a = new A; | ||
| var_dump(empty($a->bar)); | ||
|
|
||
| echo "\n3) empty() with no materialization: __get is still called (legacy path preserved):\n"; | ||
| class B { | ||
| public function __get($n) { | ||
| echo " __get($n)\n"; | ||
| return 'value'; | ||
| } | ||
| public function __isset($n) { | ||
| echo " __isset($n)\n"; | ||
| return true; | ||
| } | ||
| } | ||
| $b = new B; | ||
| var_dump(empty($b->any)); | ||
|
|
||
| ?> | ||
| --EXPECT-- | ||
| 1) empty() when __isset materialised a truthy value: __get is not called, empty=false: | ||
| __isset(foo) | ||
| bool(false) | ||
|
|
||
| 2) empty() when __isset materialised a falsy value: __get is not called, empty=true: | ||
| __isset(bar) | ||
| bool(true) | ||
|
|
||
| 3) empty() with no materialization: __get is still called (legacy path preserved): | ||
| __isset(any) | ||
| __get(any) | ||
| bool(false) |
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,26 @@ | ||
| --TEST-- | ||
| GH-12695: isset() itself is unchanged (still does not consult __get) | ||
| --FILE-- | ||
| <?php | ||
|
|
||
| /* The GH-12695 fix only affects `??` and `empty()` (which currently call | ||
| * __get after __isset). The isset() construct itself has never consulted | ||
| * __get and continues not to: it returns whatever __isset returned, cast | ||
| * to bool. */ | ||
|
|
||
| class C { | ||
| public function __get($n) { | ||
| throw new Exception("__get must not be called from a plain isset()"); | ||
| } | ||
| public function __isset($n) { | ||
| echo " __isset($n)\n"; | ||
| return true; | ||
| } | ||
| } | ||
| $c = new C; | ||
| var_dump(isset($c->any)); | ||
|
|
||
| ?> | ||
| --EXPECT-- | ||
| __isset(any) | ||
| bool(true) |
| Original file line number | Diff line number | Diff line change | ||||||||||||||||||||||||||||||||||||||||||||||||
|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|
| @@ -0,0 +1,66 @@ | ||||||||||||||||||||||||||||||||||||||||||||||||||
| --TEST-- | ||||||||||||||||||||||||||||||||||||||||||||||||||
| GH-12695: when __isset() does not materialise the property, __get() is still called | ||||||||||||||||||||||||||||||||||||||||||||||||||
| --FILE-- | ||||||||||||||||||||||||||||||||||||||||||||||||||
| <?php | ||||||||||||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||||||||||||
| /* The re-check after __isset() must not affect the legacy path when | ||||||||||||||||||||||||||||||||||||||||||||||||||
| * the property is genuinely magic-only: __get() is still called and | ||||||||||||||||||||||||||||||||||||||||||||||||||
| * its return value drives `??`'s null check. */ | ||||||||||||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||||||||||||
| class C { | ||||||||||||||||||||||||||||||||||||||||||||||||||
| public function __get($n) { | ||||||||||||||||||||||||||||||||||||||||||||||||||
| echo " __get($n)\n"; | ||||||||||||||||||||||||||||||||||||||||||||||||||
| return 'from-get'; | ||||||||||||||||||||||||||||||||||||||||||||||||||
| } | ||||||||||||||||||||||||||||||||||||||||||||||||||
| public function __isset($n) { | ||||||||||||||||||||||||||||||||||||||||||||||||||
| echo " __isset($n)\n"; | ||||||||||||||||||||||||||||||||||||||||||||||||||
| return true; | ||||||||||||||||||||||||||||||||||||||||||||||||||
| } | ||||||||||||||||||||||||||||||||||||||||||||||||||
| } | ||||||||||||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||||||||||||
| echo "1) `??` when __isset=true and __get returns a value: __get is called:\n"; | ||||||||||||||||||||||||||||||||||||||||||||||||||
|
Comment on lines
+10
to
+21
Member
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more.
Suggested change
Minor formatting nit: The other cases define the class after the echo. |
||||||||||||||||||||||||||||||||||||||||||||||||||
| $c = new C; | ||||||||||||||||||||||||||||||||||||||||||||||||||
| var_dump($c->any ?? 'fallback'); | ||||||||||||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||||||||||||
| echo "\n2) `??` when __isset=true and __get returns null: __get is called and fallback is used:\n"; | ||||||||||||||||||||||||||||||||||||||||||||||||||
| class D { | ||||||||||||||||||||||||||||||||||||||||||||||||||
| public function __get($n) { | ||||||||||||||||||||||||||||||||||||||||||||||||||
| echo " __get($n)\n"; | ||||||||||||||||||||||||||||||||||||||||||||||||||
| return null; | ||||||||||||||||||||||||||||||||||||||||||||||||||
| } | ||||||||||||||||||||||||||||||||||||||||||||||||||
| public function __isset($n) { | ||||||||||||||||||||||||||||||||||||||||||||||||||
| echo " __isset($n)\n"; | ||||||||||||||||||||||||||||||||||||||||||||||||||
| return true; | ||||||||||||||||||||||||||||||||||||||||||||||||||
| } | ||||||||||||||||||||||||||||||||||||||||||||||||||
| } | ||||||||||||||||||||||||||||||||||||||||||||||||||
| $d = new D; | ||||||||||||||||||||||||||||||||||||||||||||||||||
| var_dump($d->any ?? 'fallback'); | ||||||||||||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||||||||||||
| echo "\n3) `??` when __isset returns false: __get is not called:\n"; | ||||||||||||||||||||||||||||||||||||||||||||||||||
| class E { | ||||||||||||||||||||||||||||||||||||||||||||||||||
| public function __get($n) { | ||||||||||||||||||||||||||||||||||||||||||||||||||
| throw new Exception("__get must not be called when __isset returned false"); | ||||||||||||||||||||||||||||||||||||||||||||||||||
| } | ||||||||||||||||||||||||||||||||||||||||||||||||||
| public function __isset($n) { | ||||||||||||||||||||||||||||||||||||||||||||||||||
| echo " __isset($n)\n"; | ||||||||||||||||||||||||||||||||||||||||||||||||||
| return false; | ||||||||||||||||||||||||||||||||||||||||||||||||||
| } | ||||||||||||||||||||||||||||||||||||||||||||||||||
| } | ||||||||||||||||||||||||||||||||||||||||||||||||||
| $e = new E; | ||||||||||||||||||||||||||||||||||||||||||||||||||
| var_dump($e->any ?? 'fallback'); | ||||||||||||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||||||||||||
| ?> | ||||||||||||||||||||||||||||||||||||||||||||||||||
| --EXPECT-- | ||||||||||||||||||||||||||||||||||||||||||||||||||
| 1) `??` when __isset=true and __get returns a value: __get is called: | ||||||||||||||||||||||||||||||||||||||||||||||||||
| __isset(any) | ||||||||||||||||||||||||||||||||||||||||||||||||||
| __get(any) | ||||||||||||||||||||||||||||||||||||||||||||||||||
| string(8) "from-get" | ||||||||||||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||||||||||||
| 2) `??` when __isset=true and __get returns null: __get is called and fallback is used: | ||||||||||||||||||||||||||||||||||||||||||||||||||
| __isset(any) | ||||||||||||||||||||||||||||||||||||||||||||||||||
| __get(any) | ||||||||||||||||||||||||||||||||||||||||||||||||||
| string(8) "fallback" | ||||||||||||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||||||||||||
| 3) `??` when __isset returns false: __get is not called: | ||||||||||||||||||||||||||||||||||||||||||||||||||
| __isset(any) | ||||||||||||||||||||||||||||||||||||||||||||||||||
| string(8) "fallback" | ||||||||||||||||||||||||||||||||||||||||||||||||||
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,28 @@ | ||
| --TEST-- | ||
| GH-12695: object freed by __isset() during materialization | ||
| --FILE-- | ||
| <?php | ||
|
|
||
| /* The re-check after __isset() copies the materialised value into the | ||
| * caller's return-value buffer before releasing the object. This is | ||
| * required because __isset() may drop the last external reference to | ||
| * the object (here via $obj = null), so the property table is freed | ||
| * by OBJ_RELEASE() right after the re-check. */ | ||
|
|
||
| class C { | ||
| public $prop; | ||
| public function __isset($name) { | ||
| global $obj; | ||
| $obj = null; | ||
| $this->prop = 'materialised'; | ||
| return true; | ||
| } | ||
| } | ||
|
|
||
| $obj = new C(); | ||
| unset($obj->prop); | ||
| var_dump($obj->prop ?? 'fb'); | ||
|
|
||
| ?> | ||
| --EXPECT-- | ||
| string(12) "materialised" |
| Original file line number | Diff line number | Diff line change | ||||||||
|---|---|---|---|---|---|---|---|---|---|---|
|
|
@@ -934,6 +934,35 @@ ZEND_API zval *zend_std_read_property(zend_object *zobj, zend_string *name, int | |||||||||
| } | ||||||||||
|
|
||||||||||
| zval_ptr_dtor(&tmp_result); | ||||||||||
|
|
||||||||||
| /* __isset() may have materialised the property by writing into | ||||||||||
| * the property table. Re-check it before deferring to __get(), | ||||||||||
| * so the freshly-written value is returned directly without a | ||||||||||
| * redundant __get() call (GH-12695). The value is copied into | ||||||||||
| * `rv` because the property table can be freed by the OBJ_RELEASE | ||||||||||
| * below (e.g. when __isset() drops the last external reference | ||||||||||
| * to the object). */ | ||||||||||
| if (IS_VALID_PROPERTY_OFFSET(property_offset)) { | ||||||||||
| retval = OBJ_PROP(zobj, property_offset); | ||||||||||
| if (Z_TYPE_P(retval) != IS_UNDEF) { | ||||||||||
| ZVAL_COPY(rv, retval); | ||||||||||
| retval = rv; | ||||||||||
| OBJ_RELEASE(zobj); | ||||||||||
|
nicolas-grekas marked this conversation as resolved.
|
||||||||||
| goto exit; | ||||||||||
| } | ||||||||||
| } else if (IS_DYNAMIC_PROPERTY_OFFSET(property_offset)) { | ||||||||||
| if (zobj->properties != NULL) { | ||||||||||
| retval = zend_hash_find(zobj->properties, name); | ||||||||||
| if (retval) { | ||||||||||
| ZVAL_COPY(rv, retval); | ||||||||||
| retval = rv; | ||||||||||
| OBJ_RELEASE(zobj); | ||||||||||
| goto exit; | ||||||||||
| } | ||||||||||
| } | ||||||||||
| } | ||||||||||
|
Member
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Nit: for clarity I suggest to add a
Suggested change
Contributor
Author
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. I checked and |
||||||||||
| retval = &EG(uninitialized_zval); | ||||||||||
|
|
||||||||||
| if (zobj->ce->__get && !((*guard) & IN_GET)) { | ||||||||||
| goto call_getter; | ||||||||||
| } | ||||||||||
|
|
@@ -2487,7 +2516,20 @@ ZEND_API int zend_std_has_property(zend_object *zobj, zend_string *name, int has | |||||||||
| result = zend_is_true(&rv); | ||||||||||
| zval_ptr_dtor(&rv); | ||||||||||
| if (has_set_exists == ZEND_PROPERTY_NOT_EMPTY && result) { | ||||||||||
| if (EXPECTED(!EG(exception)) && zobj->ce->__get && !((*guard) & IN_GET)) { | ||||||||||
| /* GH-12695, see above. */ | ||||||||||
| zval *prop = NULL; | ||||||||||
| if (IS_VALID_PROPERTY_OFFSET(property_offset)) { | ||||||||||
| prop = OBJ_PROP(zobj, property_offset); | ||||||||||
| if (Z_TYPE_P(prop) == IS_UNDEF) { | ||||||||||
| prop = NULL; | ||||||||||
| } | ||||||||||
| } else if (IS_DYNAMIC_PROPERTY_OFFSET(property_offset) | ||||||||||
| && zobj->properties != NULL) { | ||||||||||
| prop = zend_hash_find(zobj->properties, name); | ||||||||||
| } | ||||||||||
| if (prop) { | ||||||||||
| result = i_zend_is_true(prop); | ||||||||||
| } else if (EXPECTED(!EG(exception)) && zobj->ce->__get && !((*guard) & IN_GET)) { | ||||||||||
| (*guard) |= IN_GET; | ||||||||||
| zend_std_call_getter(zobj, name, &rv); | ||||||||||
| (*guard) &= ~IN_GET; | ||||||||||
|
|
||||||||||
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Should this also test the case where
__isset()materializes tonullfor completeness?