From a6877aa28be697f225ff811e5186d7990160f286 Mon Sep 17 00:00:00 2001 From: Ilia Alshanetsky Date: Fri, 26 Jun 2026 08:31:23 -0400 Subject: [PATCH] Fix use-after-free in json_encode() of a JsonSerializable object php_json_encode_serializable_object() holds a raw pointer to the object and its recursion guard across the jsonSerialize() call. A user error handler invoked during that call can drop the object's last reference, leaving the post-call recursion-guard clear and the return-value identity check reading freed memory. Pin the object across the call. --- ext/json/json_encoder.c | 5 +++++ ext/json/tests/gh21024.phpt | 21 +++++++++++++++++++++ 2 files changed, 26 insertions(+) create mode 100644 ext/json/tests/gh21024.phpt diff --git a/ext/json/json_encoder.c b/ext/json/json_encoder.c index 4145bc2b6154..5f9f06ec0749 100644 --- a/ext/json/json_encoder.c +++ b/ext/json/json_encoder.c @@ -598,6 +598,8 @@ static zend_result php_json_encode_serializable_object(smart_str *buf, zval *val ZEND_GUARD_PROTECT_RECURSION(guard, JSON); + GC_ADDREF(obj); + ZVAL_STRING(&fname, "jsonSerialize"); if (FAILURE == call_user_function(NULL, val, &fname, &retval, 0, NULL) || Z_TYPE(retval) == IS_UNDEF) { @@ -610,6 +612,7 @@ static zend_result php_json_encode_serializable_object(smart_str *buf, zval *val smart_str_appendl(buf, "null", 4); } ZEND_GUARD_UNPROTECT_RECURSION(guard, JSON); + OBJ_RELEASE(obj); return FAILURE; } @@ -622,6 +625,7 @@ static zend_result php_json_encode_serializable_object(smart_str *buf, zval *val smart_str_appendl(buf, "null", 4); } ZEND_GUARD_UNPROTECT_RECURSION(guard, JSON); + OBJ_RELEASE(obj); return FAILURE; } @@ -638,6 +642,7 @@ static zend_result php_json_encode_serializable_object(smart_str *buf, zval *val zval_ptr_dtor(&retval); zval_ptr_dtor(&fname); + 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