Skip to content

Commit f235a27

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 two returns inside the lazy block through exit so the deferred release runs on those paths too. Closes GH-21776
1 parent 9c6ec37 commit f235a27

2 files changed

Lines changed: 24 additions & 3 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: 8 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -742,6 +742,7 @@ ZEND_API zval *zend_std_read_property(zend_object *zobj, zend_string *name, int
742742
uintptr_t property_offset;
743743
const zend_property_info *prop_info = NULL;
744744
uint32_t *guard = NULL;
745+
bool release_zobj = false;
745746

746747
#if DEBUG_OBJECT_HANDLERS
747748
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
936937
if (zobj->ce->__get && !((*guard) & IN_GET)) {
937938
goto call_getter;
938939
}
939-
OBJ_RELEASE(zobj);
940+
release_zobj = true;
940941
} else if (zobj->ce->__get && !((*guard) & IN_GET)) {
941942
goto call_getter_addref;
942943
}
@@ -998,11 +999,12 @@ ZEND_API zval *zend_std_read_property(zend_object *zobj, zend_string *name, int
998999
(*guard) |= guard_type;
9991000
retval = zend_std_read_property(instance, name, type, cache_slot, rv);
10001001
(*guard) &= ~guard_type;
1001-
return retval;
1002+
goto exit;
10021003
}
10031004
}
10041005

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

10171019
exit:
1020+
if (release_zobj) {
1021+
OBJ_RELEASE(zobj);
1022+
}
10181023
return retval;
10191024
}
10201025
/* }}} */

0 commit comments

Comments
 (0)