[php-src] Issue #24250: protected(set) / private(set) not enforced after a write at the same call site was routed to __set()
| From: | DirkHoffman | 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)