Skip to content

Commit 4a843ef

Browse files
committed
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
1 parent 6031497 commit 4a843ef

2 files changed

Lines changed: 26 additions & 4 deletions

File tree

Zend/tests/gh21776.phpt

Lines changed: 16 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,16 @@
1+
--TEST--
2+
GH-21776 (Heap use-after-free in zend_object_is_lazy via magic __isset)
3+
--FILE--
4+
<?php
5+
class C {
6+
function __isset($x) {
7+
$GLOBALS['o'] = 0;
8+
return true;
9+
}
10+
}
11+
$o = new C;
12+
$o->a ?? 0;
13+
echo "OK\n";
14+
?>
15+
--EXPECT--
16+
OK

Zend/zend_object_handlers.c

Lines changed: 10 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -743,6 +743,7 @@ ZEND_API zval *zend_std_read_property(zend_object *zobj, zend_string *name, int
743743
uintptr_t property_offset;
744744
const zend_property_info *prop_info = NULL;
745745
uint32_t *guard = NULL;
746+
bool release_zobj = false;
746747

747748
#if DEBUG_OBJECT_HANDLERS
748749
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
937938
if (zobj->ce->__get && !((*guard) & IN_GET)) {
938939
goto call_getter;
939940
}
940-
OBJ_RELEASE(zobj);
941+
release_zobj = true;
941942
} else if (zobj->ce->__get && !((*guard) & IN_GET)) {
942943
goto call_getter_addref;
943944
}
@@ -986,7 +987,7 @@ ZEND_API zval *zend_std_read_property(zend_object *zobj, zend_string *name, int
986987
zend_object *instance = zend_lazy_object_init(zobj);
987988
if (!instance) {
988989
retval = &EG(uninitialized_zval);
989-
goto exit;
990+
goto release_zobj_exit;
990991
}
991992

992993
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
9991000
(*guard) |= guard_type;
10001001
retval = zend_std_read_property(instance, name, type, cache_slot, rv);
10011002
(*guard) &= ~guard_type;
1002-
return retval;
1003+
goto release_zobj_exit;
10031004
}
10041005
}
10051006

1006-
return zend_std_read_property(instance, name, type, cache_slot, rv);
1007+
retval = zend_std_read_property(instance, name, type, cache_slot, rv);
1008+
goto release_zobj_exit;
10071009
}
10081010
}
10091011
if (type != BP_VAR_IS) {
@@ -1015,6 +1017,10 @@ ZEND_API zval *zend_std_read_property(zend_object *zobj, zend_string *name, int
10151017
}
10161018
retval = &EG(uninitialized_zval);
10171019

1020+
release_zobj_exit:
1021+
if (release_zobj) {
1022+
OBJ_RELEASE(zobj);
1023+
}
10181024
exit:
10191025
return retval;
10201026
}

0 commit comments

Comments
 (0)