[php-src] Issue #24250: protected(set) / private(set) not enforced after a write at the same call site was routed to __set()

From: Date: Sun, 11 Oct 2026 00:04:03 +0000
Subject: [php-src] Issue #24250: protected(set) / private(set) not enforced after a write at the same call site was routed to __set()
Groups: php.bugs 
Request: Send a blank email to php-bugs+get-252967@lists.php.net to get a copy of this message
Issue: https://github.com/php/php-src/issues/24250 Author: DirkHoffman ### Description The following code: ```php <?php class A { public protected(set) int $x = 1; public function __construct(bool $unset) { if ($unset) unset($this->x); } public function __set($name, $value) {} } function write(A $a, int $v) { $a->x = $v; } write(new A(true), 1); // routed to __set() $a = new A(false); write($a, 42); var_dump($a->x); ``` Resulted in this output: ``` int(42) ``` But I expected this output instead (as in PHP 8.5.11 and 8.4.26): ``` Fatal error: Uncaught Error: Cannot modify protected(set) property A::$x from global scope in %s:10 ``` Same with private(set), with and without OPcache. It also happens when a child class writes a parent's private(set) property, and with a public private(set) readonly property written in a child class's __clone(). JIT-compiled code is not affected (opcache.jit=1205 throws the expected Error). > **LLM-generated analysis:** Regression from 94136cf9bfbab89e072e3f04992c1e16c3a981a1 > (GH-22709). The ZEND_ASSIGN_OBJ fast path passes > check_writable = false when the cache slot is primed > ([zend_vm_def.h#L2529](https://github.com/php/php-src/blob/fd128d501d9b85d4327530d4ab92eafca99ac6a7/Zend/zend_vm_def.h#L2529)), > relying on zend_std_write_property() resetting the slot when the > set-visibility check fails > ([zend_object_handlers.c#L1138](https://github.com/php/php-src/blob/fd128d501d9b85d4327530d4ab92eafca99ac6a7/Zend/zend_object_handlers.c#L1138)). > When the property is unset and the class has __set(), > error is false > ([#L1124](https://github.com/php/php-src/blob/fd128d501d9b85d4327530d4ab92eafca99ac6a7/Zend/zend_object_handlers.c#L1124)), > so the write is routed to __set() without a set-visibility check > while the slot primed by zend_get_property_offset() is kept. The > next execution of the same opline on an object of the same class with the property initialized then > takes the fast path. Resetting the slot on that path fixes it: > > ```diff > 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)) { > + CACHE_POLYMORPHIC_PTR_EX(cache_slot, NULL, NULL); > + CACHE_PTR_EX(cache_slot + 2, NULL); > + } > ``` > > With this change Zend/tests passes with and without OPcache on > a debug build of 8.6.0RC3. I have a regression test ready and can open a PR against PHP-8.6. Found and verified with the help of AI tools; all outputs above were reproduced on the official php Docker images. ### PHP Version ```plain PHP 8.6.0RC3 (cli) (built: Oct 9 2026 00:13:19) (ZTS) Copyright © The PHP Group and Contributors Built by https://github.com/docker-library/php Zend Engine v4.6.0RC3, Copyright © Zend by Perforce with Zend OPcache v8.6.0RC3, Copyright ©, by Zend by Perforce ``` ### Operating System Debian (official php:8.6.0RC3-cli Docker image)

« previous php.bugs (#252967) next »