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
2 changes: 2 additions & 0 deletions NEWS
Original file line number Diff line number Diff line change
Expand Up @@ -36,6 +36,8 @@ PHP NEWS
(Marti2203).
. Fixed bug GH-24989 (Use-after-free in GC when an exception is thrown).
(David Carlier)
. Fixed use-after-free when assigning to a typed property releases the
object while the value is coerced. (Alexander Danilov)

- CLI:
. Fixed bug GH-24697 (PHP_HTTP_PARSER_STRICT build option doesn't work).
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,172 @@
--TEST--
Assigning to a typed property must not use the object after coercion user code released it
--FILE--
<?php

/* Unlike GH-10169, the assignments below are primed first with a harmless
* value, so that the releasing ones are executed through the cached fast path
* and not through zend_std_write_property(). */

class Test {
public string $s = "a";
public int $i = 0;

public function set($v) {
$this->s = $v;
return $this->s;
}
}

class Releaser {
public function __toString(): string {
unset($GLOBALS['a'][0]);
return "5";
}
}

function attempt(Closure $fn) {
try {
$fn();
echo "assigned\n";
} catch (Error $e) {
echo $e->getMessage(), "\n";
}
}

function assign_tmp(array &$a, bool $release) { $a[0]->s = $release ? new Releaser : "x"; }
function assign_cv(array &$a, $v) { $a[0]->s = $v; }
function assign_int(array &$a, $v) { $a[0]->i = $v; }

$a = [new Test];
assign_tmp($a, false);
assign_cv($a, "x");
assign_int($a, 1);

echo "--- __toString()\n";
for ($k = 0; $k < 3; $k++) {
$a = [new Test];
attempt(function () use (&$a) { assign_tmp($a, true); });
$a = [new Test];
$v = new Releaser;
attempt(function () use (&$a, $v) { assign_cv($a, $v); });
}

echo "--- error handler\n";
set_error_handler(function () {
unset($GLOBALS['a'][0]);
return true;
});
for ($k = 0; $k < 3; $k++) {
$a = [new Test];
attempt(function () use (&$a) { assign_int($a, "1.5"); });
}

echo "--- class type: the object is not coerced\n";
class Foo {}
class Holder {
public Foo $foo;
public string|Foo $u = "";
}
$a = [new Holder];
for ($k = 0; $k < 3; $k++) {
$a[0]->foo = new Foo;
var_dump($a[0]->foo instanceof Foo);
}

echo "--- union with string: the object is coerced\n";
function assign_union(array &$a, $v) { $a[0]->u = $v; }
$a = [new Holder];
assign_union($a, "x");
for ($k = 0; $k < 3; $k++) {
$a = [new Holder];
attempt(function () use (&$a) { assign_union($a, new Releaser); });
}

echo "--- \$this keeps the object alive\n";
for ($k = 0; $k < 3; $k++) {
$a = [new Test];
var_dump($a[0]->set(new Releaser));
}

echo "--- static property\n";
class Stringy {
public function __toString(): string { return "str"; }
}
class Sta {
public static string $s = "a";
public static int $i = 0;
public static ?Foo $foo = null;
}
for ($k = 0; $k < 3; $k++) {
Sta::$s = 12;
var_dump(Sta::$s);
Sta::$s = new Stringy;
var_dump(Sta::$s);
Sta::$i = "5";
var_dump(Sta::$i);
Sta::$i = "1.5";
var_dump(Sta::$i);
Sta::$foo = new Foo;
var_dump(Sta::$foo instanceof Foo);
attempt(function () { Sta::$i = "x"; });
attempt(function () { Sta::$foo = new stdClass; });
var_dump(Sta::$i);
}

echo "--- object stays alive\n";
$a = [new Test];
$a[0]->s = 12;
var_dump($a[0]->s);

?>
--EXPECT--
--- __toString()
Object was released while assigning to property Test::$s
Object was released while assigning to property Test::$s
Object was released while assigning to property Test::$s
Object was released while assigning to property Test::$s
Object was released while assigning to property Test::$s
Object was released while assigning to property Test::$s
--- error handler
Object was released while assigning to property Test::$i
Object was released while assigning to property Test::$i
Object was released while assigning to property Test::$i
--- class type: the object is not coerced
bool(true)
bool(true)
bool(true)
--- union with string: the object is coerced
Object was released while assigning to property Holder::$u
Object was released while assigning to property Holder::$u
Object was released while assigning to property Holder::$u
--- $this keeps the object alive
string(1) "5"
string(1) "5"
string(1) "5"
--- static property
string(2) "12"
string(3) "str"
int(5)
int(1)
bool(true)
Cannot assign string to property Sta::$i of type int
Cannot assign stdClass to property Sta::$foo of type ?Foo
int(1)
string(2) "12"
string(3) "str"
int(5)
int(1)
bool(true)
Cannot assign string to property Sta::$i of type int
Cannot assign stdClass to property Sta::$foo of type ?Foo
int(1)
string(2) "12"
string(3) "str"
int(5)
int(1)
bool(true)
Cannot assign string to property Sta::$i of type int
Cannot assign stdClass to property Sta::$foo of type ?Foo
int(1)
--- object stays alive
string(2) "12"
22 changes: 18 additions & 4 deletions Zend/zend_execute.c
Original file line number Diff line number Diff line change
Expand Up @@ -1057,7 +1057,7 @@ ZEND_API bool zend_never_inline zend_verify_property_type(const zend_property_in
return i_zend_verify_property_type(info, property, strict);
}

static zend_never_inline zval* zend_assign_to_typed_prop(zend_property_info *info, zval *property_val, zval *value, zend_refcounted **garbage_ptr EXECUTE_DATA_DC)
static zend_never_inline zval* zend_assign_to_typed_prop(zend_object *zobj, zend_property_info *info, zval *property_val, zval *value, zend_refcounted **garbage_ptr EXECUTE_DATA_DC)
{
zval tmp;

Expand All @@ -1075,9 +1075,23 @@ static zend_never_inline zval* zend_assign_to_typed_prop(zend_property_info *inf
ZVAL_DEREF(value);
ZVAL_COPY(&tmp, value);

if (UNEXPECTED(!i_zend_verify_property_type(info, &tmp, EX_USES_STRICT_TYPES()))) {
zval_ptr_dtor(&tmp);
return &EG(uninitialized_zval);
if (UNEXPECTED(!ZEND_TYPE_CONTAINS_CODE(info->type, Z_TYPE(tmp)))) {
bool protect_object = zobj
&& (Z_TYPE(tmp) != IS_OBJECT || (ZEND_TYPE_FULL_MASK(info->type) & MAY_BE_STRING));
if (protect_object) {
GC_ADDREF(zobj);
}
bool type_matched = i_zend_verify_property_type(info, &tmp, EX_USES_STRICT_TYPES());
if (protect_object && UNEXPECTED(GC_DELREF(zobj) == 0)) {
zend_object_released_while_assigning_to_property_error(info);
zend_objects_store_del(zobj);
zval_ptr_dtor(&tmp);
return &EG(uninitialized_zval);
}
if (UNEXPECTED(!type_matched)) {
zval_ptr_dtor(&tmp);
return &EG(uninitialized_zval);
}
}

Z_PROP_FLAG_P(property_val) &= ~IS_PROP_REINITABLE;
Expand Down
4 changes: 2 additions & 2 deletions Zend/zend_vm_def.h
Original file line number Diff line number Diff line change
Expand Up @@ -2461,7 +2461,7 @@ ZEND_VM_C_LABEL(assign_obj_simple):
property_val = OBJ_PROP(zobj, prop_offset);
if (Z_TYPE_P(property_val) != IS_UNDEF) {
if (prop_info != NULL) {
value = zend_assign_to_typed_prop(prop_info, property_val, value, &garbage EXECUTE_DATA_CC);
value = zend_assign_to_typed_prop(zobj, prop_info, property_val, value, &garbage EXECUTE_DATA_CC);
ZEND_VM_C_GOTO(free_and_exit_assign_obj);
} else {
ZEND_VM_C_LABEL(fast_assign_obj):
Expand Down Expand Up @@ -2595,7 +2595,7 @@ ZEND_VM_HANDLER(25, ZEND_ASSIGN_STATIC_PROP, ANY, ANY, CACHE_SLOT, SPEC(OP_DATA=
value = GET_OP_DATA_ZVAL_PTR(BP_VAR_R);

if (ZEND_TYPE_IS_SET(prop_info->type)) {
value = zend_assign_to_typed_prop(prop_info, prop, value, &garbage EXECUTE_DATA_CC);
value = zend_assign_to_typed_prop(NULL, prop_info, prop, value, &garbage EXECUTE_DATA_CC);
FREE_OP_DATA();
} else {
value = zend_assign_to_variable_ex(prop, value, OP_DATA_TYPE, EX_USES_STRICT_TYPES(), &garbage);
Expand Down
Loading
Loading