Commit c35253d364c for php.net
commit c35253d364c1c1c75ad9dbc4c8acbbdf239ffec1
Author: Ilia Alshanetsky <ilia@ilia.ws>
Date: Wed Jul 29 07:28:39 2026 -0400
Free the hooked property value when json_encode() sees an exception
php_json_encode_array() reads a hooked property into a local zval and
bails out when an exception is pending. The get hook can publish its
return value into that zval and then throw while its frame is freed,
for example from the destructor of one of its locals, so the bailout
leaks the value until request shutdown. Destroy it on the way out, the
way the other failure exit already does.
Closes GH-22919
diff --git a/ext/json/json_encoder.c b/ext/json/json_encoder.c
index 4145bc2b615..cefee25de90 100644
--- a/ext/json/json_encoder.c
+++ b/ext/json/json_encoder.c
@@ -287,6 +287,7 @@ static zend_result php_json_encode_array(smart_str *buf, zval *val, int options,
if (EG(exception)) {
PHP_JSON_HASH_UNPROTECT_RECURSION(recursion_rc);
zend_release_properties(prop_ht);
+ zval_ptr_dtor(&tmp);
return FAILURE;
}
}
diff --git a/ext/json/tests/json_encode_hooked_property_throwing_getter.phpt b/ext/json/tests/json_encode_hooked_property_throwing_getter.phpt
new file mode 100644
index 00000000000..63fc8cf42c0
--- /dev/null
+++ b/ext/json/tests/json_encode_hooked_property_throwing_getter.phpt
@@ -0,0 +1,44 @@
+--TEST--
+json_encode() releases the hooked property value when the get hook throws
+--FILE--
+<?php
+
+class Value
+{
+ public function __destruct()
+ {
+ echo "Value::__destruct\n";
+ }
+}
+
+class ThrowOnFree
+{
+ public function __destruct()
+ {
+ throw new Exception('thrown while freeing the get hook frame');
+ }
+}
+
+class Container
+{
+ public $hooked {
+ get {
+ $local = new ThrowOnFree();
+ return new Value();
+ }
+ }
+}
+
+try {
+ json_encode(new Container());
+} catch (Throwable $e) {
+ echo $e::class, ': ', $e->getMessage(), "\n";
+}
+
+echo "done\n";
+
+?>
+--EXPECT--
+Value::__destruct
+Exception: thrown while freeing the get hook frame
+done