Skip to content

Commit d37595a

Browse files
committed
Keep the object alive across jsonSerialize() in json_encode()
php_json_encode_serializable_object() holds a raw pointer to the object across the jsonSerialize() call, then reads its recursion guard and compares the returned value's identity against it. A user error handler triggered from jsonSerialize() can drop the last reference to the object, for example by nulling a reference that aliases the encoded array slot, freeing it before those reads and causing a use-after-free. Hold a reference on the object across the call. The array path already guards against this with a ZVAL_COPY; the JsonSerializable object path did not. Same use-after-free class as GH-21024 in var_dump().
1 parent f605d20 commit d37595a

2 files changed

Lines changed: 28 additions & 0 deletions

File tree

‎ext/json/json_encoder.c‎

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -581,6 +581,11 @@ static zend_result php_json_encode_serializable_object(smart_str *buf, zend_obje
581581

582582
ZEND_GUARD_PROTECT_RECURSION(guard, JSON);
583583

584+
/* jsonSerialize() may run a user error handler that drops the last
585+
* reference to the object; keep it alive so the recursion guard and the
586+
* identity check below stay valid. */
587+
GC_ADDREF(obj);
588+
584589
zend_function *json_serialize_method = zend_hash_str_find_ptr(&ce->function_table, ZEND_STRL("jsonserialize"));
585590
ZEND_ASSERT(json_serialize_method != NULL && "This should be guaranteed prior to calling this function");
586591
zend_call_known_function(json_serialize_method, obj, ce, &retval, 0, NULL, NULL);
@@ -590,6 +595,7 @@ static zend_result php_json_encode_serializable_object(smart_str *buf, zend_obje
590595
smart_str_appendl(buf, "null", 4);
591596
}
592597
ZEND_GUARD_UNPROTECT_RECURSION(guard, JSON);
598+
OBJ_RELEASE(obj);
593599
return FAILURE;
594600
}
595601

@@ -604,6 +610,7 @@ static zend_result php_json_encode_serializable_object(smart_str *buf, zend_obje
604610
}
605611

606612
zval_ptr_dtor(&retval);
613+
OBJ_RELEASE(obj);
607614

608615
return return_code;
609616
}

‎ext/json/tests/gh21024.phpt‎

Lines changed: 21 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,21 @@
1+
--TEST--
2+
GH-21024 (UAF in json_encode() when jsonSerialize()'s error handler frees the object)
3+
--EXTENSIONS--
4+
json
5+
--FILE--
6+
<?php
7+
class Bar implements JsonSerializable {
8+
public function jsonSerialize(): mixed {
9+
echo $undefined;
10+
return ['k' => 1];
11+
}
12+
}
13+
$arr = [new Bar];
14+
$ref = &$arr[0];
15+
set_error_handler(function () use (&$ref) { $ref = null; });
16+
var_dump(json_encode($arr));
17+
echo "survived\n";
18+
?>
19+
--EXPECT--
20+
string(9) "[{"k":1}]"
21+
survived

0 commit comments

Comments
 (0)