Commit de6ffebd1a2 for php

commit de6ffebd1a2a0145be891263432f34d237c609e3
Author: Ilia Alshanetsky <ilia@ilia.ws>
Date:   Thu Sep 24 19:10:44 2026 -0400

    ext/json: Stop encoding after a key fails to encode

    A failure to escape an array or object key was ignored unless
    JSON_PARTIAL_OUTPUT_ON_ERROR was set, so encoding went on: later values
    still ran user code such as jsonSerialize(), and a later error replaced
    the one that was reported, e.g. a NAN value after an invalid UTF-8 key
    reported "Inf and NaN cannot be JSON encoded". Both key sites, the
    declared-property loop and the property table loop, now return FAILURE
    the same way a failed value does.

    Closes GH-24123

diff --git a/NEWS b/NEWS
index 529f07daccb..b73d012c4eb 100644
--- a/NEWS
+++ b/NEWS
@@ -120,6 +120,8 @@ PHP                                                                        NEWS
     (adapik)
   . Fixed json_encode() silently narrowing $depth values outside the int range,
     which wrapped to an unrelated depth. (cuishuang)
+  . Fixed json_encode() continuing to encode after an array or object key
+    failed to encode. (Ilia Alshanetsky)

 - LDAP:
   . Fix crash in ldap_exop_sync() when $response_data is omitted. (lacatoire)
diff --git a/ext/json/json_encoder.c b/ext/json/json_encoder.c
index a6814c00695..cfef043ddf6 100644
--- a/ext/json/json_encoder.c
+++ b/ext/json/json_encoder.c
@@ -187,11 +187,11 @@ static zend_result php_json_encode_array(smart_str *buf, zval *val, int options,

 			if (php_json_escape_string(buf, ZSTR_VAL(prop_info->name), ZSTR_LEN(prop_info->name),
 					options & ~PHP_JSON_NUMERIC_CHECK, encoder) == FAILURE) {
-				if (ZEND_HAS_OOM()) {
+				if (ZEND_HAS_OOM() || !(options & PHP_JSON_PARTIAL_OUTPUT_ON_ERROR)) {
 					PHP_JSON_HASH_UNPROTECT_RECURSION(obj);
 					return FAILURE;
 				}
-				if ((options & PHP_JSON_PARTIAL_OUTPUT_ON_ERROR) && buf->s) {
+				if (buf->s) {
 					ZEND_ASSERT(ZSTR_LEN(buf->s) >= 4);
 					ZSTR_LEN(buf->s) -= 4;
 					smart_str_appendl(buf, "\"\"", 2);
@@ -308,13 +308,13 @@ static zend_result php_json_encode_array(smart_str *buf, zval *val, int options,

 					if (php_json_escape_string(buf, ZSTR_VAL(key), ZSTR_LEN(key),
 								options & ~PHP_JSON_NUMERIC_CHECK, encoder) == FAILURE) {
-						if (ZEND_HAS_OOM()) {
+						if (ZEND_HAS_OOM() || !(options & PHP_JSON_PARTIAL_OUTPUT_ON_ERROR)) {
 							PHP_JSON_HASH_UNPROTECT_RECURSION(recursion_rc);
 							zend_release_properties(prop_ht);
 							zval_ptr_dtor(&tmp);
 							return FAILURE;
 						}
-						if ((options & PHP_JSON_PARTIAL_OUTPUT_ON_ERROR) && buf->s) {
+						if (buf->s) {
 							ZEND_ASSERT(ZSTR_LEN(buf->s) >= 4);
 							ZSTR_LEN(buf->s) -= 4;
 							smart_str_appendl(buf, "\"\"", 2);
diff --git a/ext/json/tests/json_encode_invalid_key_aborts.phpt b/ext/json/tests/json_encode_invalid_key_aborts.phpt
new file mode 100644
index 00000000000..76afc5be43f
--- /dev/null
+++ b/ext/json/tests/json_encode_invalid_key_aborts.phpt
@@ -0,0 +1,55 @@
+--TEST--
+json_encode() stops encoding after an object or array key fails to encode
+--FILE--
+<?php
+
+class Value implements JsonSerializable
+{
+    public function jsonSerialize(): mixed
+    {
+        echo "jsonSerialize() called\n";
+        return 1;
+    }
+}
+
+eval('class Declared { public $' . "\xff" . ' = 1; public $next; }');
+
+$dynamic = new stdClass;
+$dynamic->{"\xff"} = 1;
+$dynamic->next = new Value;
+
+$declared = new Declared;
+$declared->next = new Value;
+
+$cases = [
+    'array' => ["\xff" => 1, 'next' => new Value],
+    'array, later NAN' => ["\xff" => 1, 'next' => NAN],
+    'dynamic property' => $dynamic,
+    'declared property' => $declared,
+];
+
+foreach ($cases as $name => $value) {
+    echo "$name:\n";
+    var_dump(json_encode($value), json_last_error_msg());
+}
+
+echo "partial output:\n";
+var_dump(json_encode(["\xff" => 1, 'next' => 2], JSON_PARTIAL_OUTPUT_ON_ERROR), json_last_error_msg());
+
+?>
+--EXPECT--
+array:
+bool(false)
+string(56) "Malformed UTF-8 characters, possibly incorrectly encoded"
+array, later NAN:
+bool(false)
+string(56) "Malformed UTF-8 characters, possibly incorrectly encoded"
+dynamic property:
+bool(false)
+string(56) "Malformed UTF-8 characters, possibly incorrectly encoded"
+declared property:
+bool(false)
+string(56) "Malformed UTF-8 characters, possibly incorrectly encoded"
+partial output:
+string(15) "{"":1,"next":2}"
+string(56) "Malformed UTF-8 characters, possibly incorrectly encoded"