From f235a2739c82d40ced2bfd17aeff7496d0edb255 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 two returns inside the lazy block through exit so the deferred release runs on those paths too. Closes GH-21776 --- Zend/tests/gh21776.phpt | 16 ++++++++++++++++ Zend/zend_object_handlers.c | 11 ++++++++--- 2 files changed, 24 insertions(+), 3 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 1a2c70fac1e0..5c281caef75e 100644 --- a/Zend/zend_object_handlers.c +++ b/Zend/zend_object_handlers.c @@ -742,6 +742,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)); @@ -936,7 +937,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; } @@ -998,11 +999,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 exit; } } - return zend_std_read_property(instance, name, type, cache_slot, rv); + retval = zend_std_read_property(instance, name, type, cache_slot, rv); + goto exit; } } if (type != BP_VAR_IS) { @@ -1015,6 +1017,9 @@ ZEND_API zval *zend_std_read_property(zend_object *zobj, zend_string *name, int retval = &EG(uninitialized_zval); exit: + if (release_zobj) { + OBJ_RELEASE(zobj); + } return retval; } /* }}} */