Commit 83c17e31494 for php.net
commit 83c17e3149494f63d05f6cc140966cd196ab200c
Author: Ilia Alshanetsky <ilia@ilia.ws>
Date: Mon Sep 21 10:00:41 2026 -0400
Fix GH-21776: use-after-free in zend_std_read_property magic __isset (#21786)
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 GH-21776
diff --git a/Zend/tests/gh21776.phpt b/Zend/tests/gh21776.phpt
new file mode 100644
index 00000000000..4f6ae956a22
--- /dev/null
+++ b/Zend/tests/gh21776.phpt
@@ -0,0 +1,16 @@
+--TEST--
+GH-21776 (Heap use-after-free in zend_object_is_lazy via magic __isset)
+--FILE--
+<?php
+class C {
+ function __isset($x) {
+ $GLOBALS['o'] = 0;
+ return true;
+ }
+}
+$o = new C;
+$o->a ?? 0;
+echo "OK\n";
+?>
+--EXPECT--
+OK
diff --git a/Zend/zend_object_handlers.c b/Zend/zend_object_handlers.c
index 8ca6d212fd7..800f9111c3d 100644
--- a/Zend/zend_object_handlers.c
+++ b/Zend/zend_object_handlers.c
@@ -752,6 +752,7 @@ ZEND_API zval *zend_std_read_property(zend_object *zobj, zend_string *name, int
uintptr_t property_offset;
const zend_property_info *prop_info = NULL;
uint32_t *guard = NULL;
+ bool release_zobj = false;
#if DEBUG_OBJECT_HANDLERS
fprintf(stderr, "Read object #%d property: %s\n", zobj->handle, ZSTR_VAL(name));
@@ -970,7 +971,7 @@ ZEND_API zval *zend_std_read_property(zend_object *zobj, zend_string *name, int
if (zobj->ce->__get && !((*guard) & IN_GET)) {
goto call_getter;
}
- OBJ_RELEASE(zobj);
+ release_zobj = true;
} else if (zobj->ce->__get && !((*guard) & IN_GET)) {
goto call_getter_addref;
}
@@ -1019,7 +1020,7 @@ ZEND_API zval *zend_std_read_property(zend_object *zobj, zend_string *name, int
zend_object *instance = zend_lazy_object_init(zobj);
if (!instance) {
retval = &EG(uninitialized_zval);
- goto exit;
+ goto release_zobj_exit;
}
if (UNEXPECTED(guard && (instance->ce->ce_flags & ZEND_ACC_USE_GUARDS))) {
@@ -1032,11 +1033,12 @@ ZEND_API zval *zend_std_read_property(zend_object *zobj, zend_string *name, int
(*guard) |= guard_type;
retval = zend_std_read_property(instance, name, type, cache_slot, rv);
(*guard) &= ~guard_type;
- return retval;
+ goto release_zobj_exit;
}
}
- return zend_std_read_property(instance, name, type, cache_slot, rv);
+ retval = zend_std_read_property(instance, name, type, cache_slot, rv);
+ goto release_zobj_exit;
}
}
if (type != BP_VAR_IS) {
@@ -1048,6 +1050,10 @@ ZEND_API zval *zend_std_read_property(zend_object *zobj, zend_string *name, int
}
retval = &EG(uninitialized_zval);
+release_zobj_exit:
+ if (release_zobj) {
+ OBJ_RELEASE(zobj);
+ }
exit:
return retval;
}