Commit c74b2587fef for php.net
commit c74b2587fefa5e088e8dc152d7fe2caeb010e767
Author: Ilia Alshanetsky <ilia@ilia.ws>
Date: Mon Jun 29 10:20:44 2026 -0400
Keep the object alive across jsonSerialize() in json_encode() (#22469)
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().
diff --git a/ext/json/json_encoder.c b/ext/json/json_encoder.c
index 424315eca7e..22af1c15a83 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 drop the last reference to the object, e.g. by
+ * nulling a reference that aliases the encoded array slot; 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 00000000000..612c577b2d8
--- /dev/null
+++ b/ext/json/tests/gh21024.phpt
@@ -0,0 +1,21 @@
+--TEST--
+GH-21024 (UAF in json_encode() when jsonSerialize() frees the object)
+--EXTENSIONS--
+json
+--FILE--
+<?php
+class Bar implements JsonSerializable {
+ public function jsonSerialize(): mixed {
+ global $ref;
+ $ref = null;
+ return ['k' => 1];
+ }
+}
+$arr = [new Bar];
+$ref = &$arr[0];
+var_dump(json_encode($arr));
+echo "survived\n";
+?>
+--EXPECT--
+string(9) "[{"k":1}]"
+survived