From 4a843efbd80dffd27995f18838c59d51db482c88 Mon Sep 17 00:00:00 2001 From: Ilia Alshanetsky Date: Fri, 17 Apr 2026 07:44:34 -0400 Subject: [PATCH] Fix GH-21776: use-after-free in zend_std_read_property magic __isset 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 --- Zend/tests/gh21776.phpt | 16 ++++++++++++++++ Zend/zend_object_handlers.c | 14 ++++++++++---- 2 files changed, 26 insertions(+), 4 deletions(-) create mode 100644 Zend/tests/gh21776.phpt diff --git a/Zend/tests/gh21776.phpt b/Zend/tests/gh21776.phpt new file mode 100644 index 000000000000..4f6ae956a226 --- /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-- +a ?? 0; +echo "OK\n"; +?> +--EXPECT-- +OK diff --git a/Zend/zend_object_handlers.c b/Zend/zend_object_handlers.c index 571aa9e92abc..f9fa55d80924 100644 --- a/Zend/zend_object_handlers.c +++ b/Zend/zend_object_handlers.c @@ -743,6 +743,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)); @@ -937,7 +938,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; } @@ -986,7 +987,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))) { @@ -999,11 +1000,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) { @@ -1015,6 +1017,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; }