From 4a3fcfa090b08bd7724a1e1ba07b2dedcce86838 Mon Sep 17 00:00:00 2001 From: Dirk Hoffman Date: Sun, 11 Oct 2026 01:53:02 +0200 Subject: [PATCH] Fix GH-24250: Set visibility not enforced after write forwarded to __set() Since 94136cf9bf, the ZEND_ASSIGN_OBJ fast path skips the set visibility check when the cache slot is primed, relying on zend_std_write_property() to reset the slot when the check fails. When an unset property is written through __set(), the check is skipped altogether and the slot primed by zend_get_property_offset() stays valid, so the next write at the same call site to an initialized property bypasses protected(set)/private(set). Reset the cache slot in that case as well. --- Zend/tests/asymmetric_visibility/gh24250.phpt | 86 +++++++++++++++++++ Zend/zend_object_handlers.c | 9 ++ 2 files changed, 95 insertions(+) create mode 100644 Zend/tests/asymmetric_visibility/gh24250.phpt diff --git a/Zend/tests/asymmetric_visibility/gh24250.phpt b/Zend/tests/asymmetric_visibility/gh24250.phpt new file mode 100644 index 000000000000..2afc18c11bda --- /dev/null +++ b/Zend/tests/asymmetric_visibility/gh24250.phpt @@ -0,0 +1,86 @@ +--TEST-- +GH-24250: protected(set)/private(set) not enforced after a write at the same call site was routed to __set() +--FILE-- +x, $this->y); + } + } + public function __set($name, $value) { + echo "__set($name)\n"; + } +} + +function writeX(A $a, int $v) { $a->x = $v; } +function writeY(A $a, int $v) { $a->y = $v; } + +writeX(new A(true), 1); +writeY(new A(true), 1); + +$a = new A(false); +try { + writeX($a, 42); +} catch (Error $e) { + echo $e->getMessage(), "\n"; +} +try { + writeY($a, 42); +} catch (Error $e) { + echo $e->getMessage(), "\n"; +} +var_dump($a->x, $a->y); + +class P { + public private(set) int $x = 1; + public private(set) readonly int $r; + public function __construct(bool $unset) { + if ($unset) { + unset($this->x, $this->r); + } else { + $this->r = 1; + } + } + public function __set($name, $value) { + echo "__set($name)\n"; + } +} + +class C extends P { + public function writeX(int $v) { $this->x = $v; } + public function __clone() { $this->r = 42; } +} + +(new C(true))->writeX(1); +$c = new C(false); +try { + $c->writeX(42); +} catch (Error $e) { + echo $e->getMessage(), "\n"; +} +var_dump($c->x); + +clone new C(true); +try { + clone $c; +} catch (Error $e) { + echo $e->getMessage(), "\n"; +} + +?> +--EXPECT-- +__set(x) +__set(y) +Cannot modify protected(set) property A::$x from global scope +Cannot modify private(set) property A::$y from global scope +int(1) +int(1) +__set(x) +Cannot modify private(set) property P::$x from scope C +int(1) +__set(r) +Cannot modify private(set) property P::$r from scope C diff --git a/Zend/zend_object_handlers.c b/Zend/zend_object_handlers.c index 800f9111c3de..57fa88980af3 100644 --- a/Zend/zend_object_handlers.c +++ b/Zend/zend_object_handlers.c @@ -1124,6 +1124,15 @@ ZEND_API zval *zend_std_write_property(zend_object *zobj, zend_string *name, zva } else { guard = zend_get_property_guard(zobj, name); error = (*guard) & IN_SET; + if (!error + && cache_slot + && (prop_info->flags & ZEND_ACC_PPP_SET_MASK) + && !zend_asymmetric_property_has_set_access(prop_info)) { + /* The write is forwarded to __set() without checking set visibility. + * Reset cache slot to dodge fast path in next execution. */ + CACHE_POLYMORPHIC_PTR_EX(cache_slot, NULL, NULL); + CACHE_PTR_EX(cache_slot + 2, NULL); + } } if (error) { if ((prop_info->flags & ZEND_ACC_READONLY)