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"