Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
59 changes: 59 additions & 0 deletions ext/standard/tests/general_functions/gh21024.phpt
Original file line number Diff line number Diff line change
@@ -0,0 +1,59 @@
--TEST--
GH-21024 (UAF in var_dump()/debug_zval_dump()/var_export() when a callback frees the dumped object)
--FILE--
<?php
class Bar {
public function __debugInfo(): array {
echo $undefined;
return ['ok' => 1];
}
}
class Hooked {
public $x { get { echo $undefined; return 1; } }
}

$a = [new Bar];
$ra = &$a[0];
set_error_handler(function () use (&$ra) { $ra = null; });
var_dump($a);
restore_error_handler();

$b = [new Bar];
$rb = &$b[0];
set_error_handler(function () use (&$rb) { $rb = null; });
debug_zval_dump($b);
restore_error_handler();

$c = [new Hooked];
$rc = &$c[0];
set_error_handler(function () use (&$rc) { $rc = null; });
var_export($c);
echo "\n";
restore_error_handler();

echo "survived\n";
?>
--EXPECTF--
array(1) {
[0]=>
&object(Bar)#%d (1) {
["ok"]=>
int(1)
}
}
array(1) packed refcount(%d){
[0]=>
reference refcount(%d) {
object(Bar)#%d (1) refcount(%d){
["ok"]=>
int(1)
}
}
}
array (
0 =>
\Hooked::__set_state(array(
'x' => 1,
)),
)
survived
41 changes: 30 additions & 11 deletions ext/standard/var.c
Original file line number Diff line number Diff line change
Expand Up @@ -179,15 +179,20 @@ PHPAPI void php_var_dump(zval *struc, int level) /* {{{ */
}
ZEND_GUARD_OR_GC_PROTECT_RECURSION(guard, DEBUG, zobj);

/* zend_get_properties_for() may invoke __debugInfo(), which can run
* a user error handler that drops the last reference to the object.
* Keep it alive and use the captured zobj rather than re-reading
* struc, whose zval the handler may also have overwritten. */
GC_ADDREF(zobj);
myht = zend_get_properties_for(struc, ZEND_PROP_PURPOSE_DEBUG);
if (ce->ce_flags & ZEND_ACC_ENUM) {
zval *case_name_zval = zend_enum_fetch_case_name(Z_OBJ_P(struc));
zval *case_name_zval = zend_enum_fetch_case_name(zobj);
php_printf("%senum(%s::%s) (%d) {\n", COMMON, ZSTR_VAL(ce->name), Z_STRVAL_P(case_name_zval), myht ? zend_array_count(myht) : 0);
} else {
class_name = Z_OBJ_HANDLER_P(struc, get_class_name)(Z_OBJ_P(struc));
const char *prefix = php_var_dump_object_prefix(Z_OBJ_P(struc));
class_name = zobj->handlers->get_class_name(zobj);
const char *prefix = php_var_dump_object_prefix(zobj);

php_printf("%s%sobject(%s)#%d (%d) {\n", COMMON, prefix, ZSTR_VAL(class_name), Z_OBJ_HANDLE_P(struc), myht ? zend_array_count(myht) : 0);
php_printf("%s%sobject(%s)#%d (%d) {\n", COMMON, prefix, ZSTR_VAL(class_name), zobj->handle, myht ? zend_array_count(myht) : 0);
zend_string_release_ex(class_name, 0);
}

Expand All @@ -202,7 +207,7 @@ PHPAPI void php_var_dump(zval *struc, int level) /* {{{ */
if (Z_TYPE_P(val) == IS_INDIRECT) {
val = Z_INDIRECT_P(val);
if (key) {
prop_info = zend_get_typed_property_info_for_slot(Z_OBJ_P(struc), val);
prop_info = zend_get_typed_property_info_for_slot(zobj, val);
}
}

Expand All @@ -217,6 +222,7 @@ PHPAPI void php_var_dump(zval *struc, int level) /* {{{ */
}
PUTS("}\n");
ZEND_GUARD_OR_GC_UNPROTECT_RECURSION(guard, DEBUG, zobj);
OBJ_RELEASE(zobj);
break;
}
case IS_RESOURCE: {
Expand Down Expand Up @@ -382,11 +388,16 @@ PHPAPI void php_debug_zval_dump(zval *struc, int level) /* {{{ */
}
ZEND_GUARD_OR_GC_PROTECT_RECURSION(guard, DEBUG, zobj);

/* zend_get_properties_for() may invoke __debugInfo(), which can run a
* user error handler that drops the last reference to the object. Keep
* it alive across the call and report the refcount excluding this
* temporary reference. */
GC_ADDREF(zobj);
myht = zend_get_properties_for(struc, ZEND_PROP_PURPOSE_DEBUG);
class_name = Z_OBJ_HANDLER_P(struc, get_class_name)(Z_OBJ_P(struc));
const char *prefix = php_var_dump_object_prefix(Z_OBJ_P(struc));
class_name = zobj->handlers->get_class_name(zobj);
const char *prefix = php_var_dump_object_prefix(zobj);

php_printf("%sobject(%s)#%d (%d) refcount(%u){\n", prefix, ZSTR_VAL(class_name), Z_OBJ_HANDLE_P(struc), myht ? zend_array_count(myht) : 0, Z_REFCOUNT_P(struc));
php_printf("%sobject(%s)#%d (%d) refcount(%u){\n", prefix, ZSTR_VAL(class_name), zobj->handle, myht ? zend_array_count(myht) : 0, GC_REFCOUNT(zobj) - 1);
zend_string_release_ex(class_name, 0);
if (myht) {
ZEND_HASH_FOREACH_KEY_VAL(myht, index, key, val) {
Expand All @@ -395,7 +406,7 @@ PHPAPI void php_debug_zval_dump(zval *struc, int level) /* {{{ */
if (Z_TYPE_P(val) == IS_INDIRECT) {
val = Z_INDIRECT_P(val);
if (key) {
prop_info = zend_get_typed_property_info_for_slot(Z_OBJ_P(struc), val);
prop_info = zend_get_typed_property_info_for_slot(zobj, val);
}
}

Expand All @@ -410,6 +421,7 @@ PHPAPI void php_debug_zval_dump(zval *struc, int level) /* {{{ */
}
PUTS("}\n");
ZEND_GUARD_OR_GC_UNPROTECT_RECURSION(guard, DEBUG, zobj);
OBJ_RELEASE(zobj);
break;
}
case IS_RESOURCE: {
Expand Down Expand Up @@ -608,13 +620,19 @@ PHPAPI zend_result php_var_export_ex(zval *struc, int level, smart_str *buf) /*
return SUCCESS;
}
ZEND_GUARD_OR_GC_PROTECT_RECURSION(guard, EXPORT, zobj);
/* zend_get_properties_for() and the property hooks read below may
* run userland (a get hook, a custom get_properties) that drops the
* last reference to the object. Keep it alive and use the captured
* zobj rather than re-reading struc, which the handler may also have
* overwritten. */
GC_ADDREF(zobj);
myht = zend_get_properties_for(struc, ZEND_PROP_PURPOSE_VAR_EXPORT);
if (level > 1) {
smart_str_appendc(buf, '\n');
buffer_append_spaces(buf, level - 1);
}

zend_class_entry *ce = Z_OBJCE_P(struc);
zend_class_entry *ce = zobj->ce;
bool is_enum = ce->ce_flags & ZEND_ACC_ENUM;

/* stdClass has no __set_state method, but can be casted to */
Expand All @@ -624,7 +642,6 @@ PHPAPI zend_result php_var_export_ex(zval *struc, int level, smart_str *buf) /*
smart_str_appendc(buf, '\\');
smart_str_append(buf, ce->name);
if (is_enum) {
zend_object *zobj = Z_OBJ_P(struc);
zval *case_name_zval = zend_enum_fetch_case_name(zobj);
smart_str_appendl(buf, "::", 2);
smart_str_append(buf, Z_STR_P(case_name_zval));
Expand All @@ -650,6 +667,7 @@ PHPAPI zend_result php_var_export_ex(zval *struc, int level, smart_str *buf) /*
if (EG(exception)) {
ZEND_GUARD_OR_GC_UNPROTECT_RECURSION(guard, EXPORT, zobj);
zend_release_properties(myht);
OBJ_RELEASE(zobj);
return FAILURE;
}
}
Expand All @@ -662,6 +680,7 @@ PHPAPI zend_result php_var_export_ex(zval *struc, int level, smart_str *buf) /*
zend_release_properties(myht);
}
ZEND_GUARD_OR_GC_UNPROTECT_RECURSION(guard, EXPORT, zobj);
OBJ_RELEASE(zobj);
if (level > 1 && !is_enum) {
buffer_append_spaces(buf, level - 1);
}
Expand Down
Loading