Category
Language · php-src-strict · magic property visibility (regression vs #3298)
Problem
When a property is declared private/protected but not visible from the calling scope, Zend still routes isset / empty / unset through __isset / __unset. The VM treats the declared slot as present and skips magic (and for unset, silently no-ops).
Probed 2026-07-31 on host PHP 8.2 vs php bin/vm.php (same tree):
| Repro |
Zend 8.2 |
VM |
isset($a->x) on private $x + __isset |
prints ISSET_x, then true |
true only (no __isset) |
unset($a->x) on private $x + __unset |
prints UNSET_x |
no __unset |
same class, isset from subclass method on protected $x |
uses real property (true) |
same |
outside isset on protected $x + __isset returning false |
ISSET_x / false |
true (no magic) |
Note: __get on the same private property does fire (parity OK) — only the has/unset paths are wrong.
php-src reference
PHP implementation target
lib/VM/ObjectEntry.php / lib/VM.php — TYPE_ISSET / TYPE_UNSET / empty on object dims: if property exists but is not visible from current scope, dispatch __isset/__unset instead of reading the private slot or no-op
- Keep PHP-in-PHP; no new
runtime/*.c logic
- Compliance:
test/compliance/cases/language/magic_isset_inaccessible.phpt (and unset twin)
Repro
./script/docker-exec.sh -- bash -lc 'cat > /tmp/isset_priv.php <<'"'"'PHP'"'"'
<?php
class A {
private $x = 1;
public function __isset($n) { echo "ISSET_$n\n"; return true; }
public function __unset($n) { echo "UNSET_$n\n"; }
}
$a = new A();
var_export(isset($a->x)); echo "\n";
unset($a->x);
PHP
php bin/vm.php /tmp/isset_priv.php'
Done when
Category
Language· php-src-strict · magic property visibility (regression vs #3298)Problem
When a property is declared private/protected but not visible from the calling scope, Zend still routes
isset/empty/unsetthrough__isset/__unset. The VM treats the declared slot as present and skips magic (and forunset, silently no-ops).Probed 2026-07-31 on host PHP 8.2 vs
php bin/vm.php(same tree):isset($a->x)onprivate $x+__issetISSET_x, thentruetrueonly (no__isset)unset($a->x)onprivate $x+__unsetUNSET_x__unsetissetfrom subclass method onprotected $xtrue)issetonprotected $x+__issetreturning falseISSET_x/falsetrue(no magic)Note:
__geton the same private property does fire (parity OK) — only the has/unset paths are wrong.php-src reference
Zend/zend_object_handlers.c—zend_std_has_property,zend_std_unset_property(visibility failure → magic)__isset/__unset; this is the inaccessible declared property pathPHP implementation target
lib/VM/ObjectEntry.php/lib/VM.php—TYPE_ISSET/TYPE_UNSET/emptyon object dims: if property exists but is not visible from current scope, dispatch__isset/__unsetinstead of reading the private slot or no-opruntime/*.clogictest/compliance/cases/language/magic_isset_inaccessible.phpt(and unset twin)Repro
Done when
isset/emptyon private/protected declared props call__issetwith Zend truth tableunsetcalls__unset(no silent no-op; no private-slot leak)