From 3536787507517d9f1a8edec19f8de6c2b7cfa31c Mon Sep 17 00:00:00 2001 From: Yuma Kakei Date: Mon, 26 Oct 2026 16:11:59 +0900 Subject: [PATCH 1/2] Use the default depth in the JSON fuzzer and check json_validate() The fuzzer used a depth of 10 rather than json_decode()'s default of 512, so every input nesting deeper than that failed early. json_validate() uses its own parser methods and was not fuzzed at all. Check that it agrees with decoding to arrays, which, unlike decoding to objects, does not reject property names starting with a NUL byte. --- sapi/fuzzer/fuzzer-json.c | 14 ++++++++++++-- 1 file changed, 12 insertions(+), 2 deletions(-) diff --git a/sapi/fuzzer/fuzzer-json.c b/sapi/fuzzer/fuzzer-json.c index 0407a895835e..8a0a1c426368 100644 --- a/sapi/fuzzer/fuzzer-json.c +++ b/sapi/fuzzer/fuzzer-json.c @@ -55,10 +55,20 @@ int LLVMFuzzerTestOneInput(const uint8_t *Data, size_t Size) { for (int index = 0; index < 12; ++index) { zval result; php_json_parser parser; - php_json_parser_init(&parser, &result, data, Size, options[index], 10); - if (php_json_yyparse(&parser) == SUCCESS) { + php_json_parser_init(&parser, &result, data, Size, options[index], PHP_JSON_PARSER_DEFAULT_DEPTH); + bool decoded = php_json_yyparse(&parser) == SUCCESS; + if (decoded) { zval_ptr_dtor(&result); } + + /* json_validate() must agree with json_decode(). Only compare when decoding to arrays, + * as decoding to objects additionally rejects property names starting with a NUL byte. */ + if (options[index] == PHP_JSON_OBJECT_AS_ARRAY + || options[index] == (PHP_JSON_OBJECT_AS_ARRAY | PHP_JSON_INVALID_UTF8_IGNORE)) { + bool valid = php_json_validate_ex(data, Size, + options[index] & PHP_JSON_INVALID_UTF8_IGNORE, PHP_JSON_PARSER_DEFAULT_DEPTH); + ZEND_ASSERT(valid == decoded); + } } php_request_shutdown(NULL); From b48d573c57a9bc1d02fa46737b0afa2cb381099e Mon Sep 17 00:00:00 2001 From: Yuma Kakei Date: Tue, 27 Oct 2026 21:29:18 +0900 Subject: [PATCH 2/2] Keep the low depth in the JSON fuzzer --- sapi/fuzzer/fuzzer-json.c | 8 ++++++-- 1 file changed, 6 insertions(+), 2 deletions(-) diff --git a/sapi/fuzzer/fuzzer-json.c b/sapi/fuzzer/fuzzer-json.c index 8a0a1c426368..7ffa28df245a 100644 --- a/sapi/fuzzer/fuzzer-json.c +++ b/sapi/fuzzer/fuzzer-json.c @@ -27,6 +27,10 @@ #include "ext/json/php_json.h" #include "ext/json/php_json_parser.h" +/* Lower than the default depth of 512, so that deeply nested inputs fail quickly and the + * depth limit is reached with small inputs. */ +#define MAX_DEPTH 10 + int LLVMFuzzerTestOneInput(const uint8_t *Data, size_t Size) { if (fuzzer_request_startup() == FAILURE){ @@ -55,7 +59,7 @@ int LLVMFuzzerTestOneInput(const uint8_t *Data, size_t Size) { for (int index = 0; index < 12; ++index) { zval result; php_json_parser parser; - php_json_parser_init(&parser, &result, data, Size, options[index], PHP_JSON_PARSER_DEFAULT_DEPTH); + php_json_parser_init(&parser, &result, data, Size, options[index], MAX_DEPTH); bool decoded = php_json_yyparse(&parser) == SUCCESS; if (decoded) { zval_ptr_dtor(&result); @@ -66,7 +70,7 @@ int LLVMFuzzerTestOneInput(const uint8_t *Data, size_t Size) { if (options[index] == PHP_JSON_OBJECT_AS_ARRAY || options[index] == (PHP_JSON_OBJECT_AS_ARRAY | PHP_JSON_INVALID_UTF8_IGNORE)) { bool valid = php_json_validate_ex(data, Size, - options[index] & PHP_JSON_INVALID_UTF8_IGNORE, PHP_JSON_PARSER_DEFAULT_DEPTH); + options[index] & PHP_JSON_INVALID_UTF8_IGNORE, MAX_DEPTH); ZEND_ASSERT(valid == decoded); } }