From 58fee8238e8caa0180d97dbae3b484326dd8fb74 Mon Sep 17 00:00:00 2001 From: Ilia Alshanetsky Date: Fri, 26 Jun 2026 07:08:18 -0400 Subject: [PATCH] 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(). --- ext/json/json_encoder.c | 7 +++++++ ext/json/tests/gh21024.phpt | 21 +++++++++++++++++++++ 2 files changed, 28 insertions(+) create mode 100644 ext/json/tests/gh21024.phpt diff --git a/ext/json/json_encoder.c b/ext/json/json_encoder.c index 424315eca7ec..1a443b9ed43a 100644 --- a/ext/json/json_encoder.c +++ b/ext/json/json_encoder.c @@ -577,6 +577,11 @@ static zend_result php_json_encode_serializable_object(smart_str *buf, zend_obje ZEND_GUARD_PROTECT_RECURSION(guard, JSON); + /* jsonSerialize() may run a user error handler that drops the last + * reference to the object; keep it alive so the recursion guard and the + * identity check below stay valid. */ + GC_ADDREF(obj); + zend_function *json_serialize_method = zend_hash_str_find_ptr(&ce->function_table, ZEND_STRL("jsonserialize")); ZEND_ASSERT(json_serialize_method != NULL && "This should be guaranteed prior to calling this function"); zend_call_known_function(json_serialize_method, obj, ce, &retval, 0, NULL, NULL); @@ -586,6 +591,7 @@ static zend_result php_json_encode_serializable_object(smart_str *buf, zend_obje smart_str_appendl(buf, "null", 4); } ZEND_GUARD_UNPROTECT_RECURSION(guard, JSON); + OBJ_RELEASE(obj); return FAILURE; } @@ -600,6 +606,7 @@ static zend_result php_json_encode_serializable_object(smart_str *buf, zend_obje } zval_ptr_dtor(&retval); + OBJ_RELEASE(obj); return return_code; } diff --git a/ext/json/tests/gh21024.phpt b/ext/json/tests/gh21024.phpt new file mode 100644 index 000000000000..c31359aaeeab --- /dev/null +++ b/ext/json/tests/gh21024.phpt @@ -0,0 +1,21 @@ +--TEST-- +GH-21024 (UAF in json_encode() when jsonSerialize()'s error handler frees the object) +--EXTENSIONS-- +json +--FILE-- + 1]; + } +} +$arr = [new Bar]; +$ref = &$arr[0]; +set_error_handler(function () use (&$ref) { $ref = null; }); +var_dump(json_encode($arr)); +echo "survived\n"; +?> +--EXPECT-- +string(9) "[{"k":1}]" +survived