Skip to content

Fix GH-21478: Forward read_property to real instance for initialized lazy proxies#21480

Open
iliaal wants to merge 1 commit intophp:masterfrom
iliaal:fix/gh-21478-proxy-read-property-guard
Open

Fix GH-21478: Forward read_property to real instance for initialized lazy proxies#21480
iliaal wants to merge 1 commit intophp:masterfrom
iliaal:fix/gh-21478-proxy-read-property-guard

Conversation

@iliaal
Copy link
Contributor

@iliaal iliaal commented Mar 21, 2026

Summary

zend_std_read_property() was calling __get/__isset on an initialized lazy proxy before forwarding to the real instance. This produced double magic method invocations when the real instance's __get accessed the proxy -- the proxy's own guard was clear, so __get fired on the proxy too.

For pure reads (BP_VAR_R, BP_VAR_IS), forward directly to the real instance before attempting magic methods on the proxy. Write contexts (BP_VAR_W/RW) are excluded since read_property with those types is a fallback from get_property_ptr_ptr for reference operations.

Also updates gh18038-004 and gh18038-007 test expectations to match -- these tests previously asserted the double-call behavior that GH-21478 reports as a bug.

Fixes #21478

@dstogov
Copy link
Member

dstogov commented Mar 23, 2026

@arnaud-lb this is your area.

@dstogov dstogov requested a review from arnaud-lb March 23, 2026 08:46
@arnaud-lb
Copy link
Member

arnaud-lb commented Mar 23, 2026

Thank you @iliaal!

Unfortunately this breaks the assumption that when interacting with a proxy, the proxy's code is executed instead of the real instance:

class Foo {
    private $_;

    public function __get($name) {
        echo __CLASS__, " ", $name, "\n";
        echo $this->{$name};
    }
}

class Bar extends Foo {
    public function __get($name) {
        echo __CLASS__, " ", $name, "\n";
        echo $this->{$name};
    }
}

$rc = new ReflectionClass(Bar::class);
$proxy = $rc->newLazyProxy(function () {
    return new Foo();
});
$rc->initializeLazyObject($proxy);

$proxy->x;

This should print "Bar x", but now this prints "Foo x".

…ed lazy proxies

For initialized lazy proxies, the proxy and real instance have separate
magic method guard slots. When the real instance's __get/__isset is
running and code inside it accesses the proxy, the proxy's guard is
clear, causing __get/__isset to fire on the proxy too (double invocation).

Check whether the real instance's guard is already set before invoking
magic methods on the proxy. If it is, forward to the real instance
(we're inside a recursive call). If not, the proxy was accessed directly
and its own magic methods should run as normal.
@iliaal iliaal force-pushed the fix/gh-21478-proxy-read-property-guard branch from 83b7358 to d68fcd8 Compare March 23, 2026 12:04
@iliaal
Copy link
Contributor Author

iliaal commented Mar 23, 2026

Good catch, the unconditional forward was too broad.

I've narrowed it: before calling __get/__isset on the proxy, I check whether the real instance's guard is already set for that property. If it is, we're in a recursive call from the real instance's magic method, so I forward to avoid the double invocation. If not, the proxy was accessed directly and its own magic methods fire as expected.

Your example prints "Bar x" now. Added it as a test (gh21478-proxy-get-override.phpt).

I also updated gh20875.phpt. The removed lines were the proxy's __get('b') firing while the real instance already had its __get('b') guard set, same underlying bug through a different path.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Projects

None yet

Development

Successfully merging this pull request may close these issues.

Property access on lazy proxy may invoke magic method despite real instance guards

3 participants