Skip to content

Fix GH-21776: use-after-free in zend_std_read_property magic __isset#21786

Open
iliaal wants to merge 1 commit intophp:masterfrom
iliaal:fix/gh-21776-isset-uaf
Open

Fix GH-21776: use-after-free in zend_std_read_property magic __isset#21786
iliaal wants to merge 1 commit intophp:masterfrom
iliaal:fix/gh-21776-isset-uaf

Conversation

@iliaal
Copy link
Copy Markdown
Contributor

@iliaal iliaal commented Apr 17, 2026

Fixes #21776.

When __isset drops the last non-temp reference to $this (e.g.
$GLOBALS['o'] = 0), the OBJ_RELEASE(zobj) after the __isset
call freed the object before zend_std_read_property reached the
shared uninit_error check at zend_lazy_object_must_init(zobj).
ASAN reports a heap-use-after-free.

Defer the release via a local flag so zobj stays alive through the
lazy-init check and the recursive read on the initialized instance.
Route the two returns inside the lazy block through exit: so the
deferred release runs on those paths too.

Copy link
Copy Markdown
Member

@iluuu1994 iluuu1994 left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Looks correct, but given this is non-trivial I'd prefer to target master. This should not target 8.5 anyway, as it applies to 8.4 also.

exit:
if (release_zobj) {
OBJ_RELEASE(zobj);
}
Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

You could move this above exit with a new label and adjust the gotos in if (UNEXPECTED(zend_lazy_object_must_init(zobj))) { to go there instead. This check is not necessary the majority of the time.

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Fair enough, let me adjust, I just looked at bug report and it mention 8.5 so started from there

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Moved the release into a release_zobj_exit: label above exit: and routed the three lazy-init-block jumps there. The isset-false, getter, and wrong-offset paths still goto exit and skip the flag check. Retargeted to master.

When __isset drops the last non-temp reference to $this (e.g.
$GLOBALS['o'] = 0), the OBJ_RELEASE after the __isset call freed zobj
before zend_std_read_property reached the shared uninit_error check
at zend_lazy_object_must_init(zobj), a heap-use-after-free.

The GC_ADDREF/OBJ_RELEASE pair around __isset has been correct since
2018. The 2023 lazy-object support added a zobj read in the shared
fall-through path without extending the isset branch's ref coverage
to match. Defer the release via a local flag so zobj stays alive
through the lazy-init check and the recursive read on the initialized
instance. Route the lazy-init block's exits through a release_zobj_exit
label so the deferred release fires on those paths too, while the hot
paths that already released inline skip the flag check.

Closes phpGH-21776
@iliaal iliaal changed the base branch from PHP-8.5 to master April 17, 2026 15:34
@iliaal iliaal force-pushed the fix/gh-21776-isset-uaf branch from f235a27 to 4a843ef Compare April 17, 2026 15:34
@iliaal iliaal requested a review from iluuu1994 April 17, 2026 16:06
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.

Heap use-after-free in zend_object_is_lazy

2 participants