[php-src] master: Keep the object alive across jsonSerialize() in json_encode() (#22469)

Ilia Alshanetsky via GitHub <[email protected]>
Newsgroups gmane.comp.php.cvs.general
Message-ID <[email protected]>
Author: Ilia Alshanetsky (iliaal)
Committer: GitHub (web-flow)
Pusher: iliaal
Date: 2026-06-29T10:20:44-04:00

Commit: https://github.com/php/php-src/commit/c74b2587fefa5e088e8dc152d7fe2caeb010e767
Raw diff: https://github.com/php/php-src/commit/c74b2587fefa5e088e8dc152d7fe2caeb010e767.diff

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().

Changed paths:
  A  ext/json/tests/gh21024.phpt
  M  ext/json/json_encoder.c


Diff:

diff --git a/ext/json/json_encoder.c b/ext/json/json_encoder.c
index 424315eca7ec..22af1c15a833 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 000000000000..612c577b2d88
--- /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
lmpx.com only provides a reader for public news (NNTP) servers. It is not affiliated with the servers or forums shown here and is not responsible for the content of articles, which is written by their respective authors.