Description
The following code:
<?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:
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 94136cf (GH-22709). The ZEND_ASSIGN_OBJ fast path passes check_writable = false when the cache slot is primed (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). When the property is unset and the class has __set(), error is false (#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:
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
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)
Description
The following code:
Resulted in this output:
But I expected this output instead (as in PHP 8.5.11 and 8.4.26):
Same with
private(set), with and without OPcache. It also happens when a child class writes a parent'sprivate(set)property, and with apublic private(set) readonlyproperty written in a child class's__clone(). JIT-compiled code is not affected (opcache.jit=1205throws the expectedError).Found and verified with the help of AI tools; all outputs above were reproduced on the official
phpDocker images.PHP Version
Operating System
Debian (official
php:8.6.0RC3-cliDocker image)