From ad1285d6d85a709fd729bc075c2015b7176eac64 Mon Sep 17 00:00:00 2001 From: Yuma Kakei Date: Mon, 26 Oct 2026 16:26:26 +0900 Subject: [PATCH] Add a fuzzer for ext/uri The fuzzer parses its input with Uri\Rfc3986\Uri and Uri\WhatWg\Url, calls every getter, equals(), toString() and friends, and, if the input contains a newline, passes the rest to resolve() and the with*() methods. The input is also passed to parse_url(). --- sapi/fuzzer/Makefile.frag | 3 + sapi/fuzzer/README.md | 8 ++ sapi/fuzzer/config.m4 | 1 + sapi/fuzzer/corpus/uri/seed01 | 1 + sapi/fuzzer/corpus/uri/seed02 | 1 + sapi/fuzzer/corpus/uri/seed03 | 1 + sapi/fuzzer/corpus/uri/seed04 | 1 + sapi/fuzzer/corpus/uri/seed05 | 1 + sapi/fuzzer/corpus/uri/seed06 | 1 + sapi/fuzzer/corpus/uri/seed07 | 1 + sapi/fuzzer/corpus/uri/seed08 | 1 + sapi/fuzzer/corpus/uri/seed09 | 1 + sapi/fuzzer/corpus/uri/seed10 | 1 + sapi/fuzzer/corpus/uri/seed11 | 1 + sapi/fuzzer/corpus/uri/seed12 | 2 + sapi/fuzzer/corpus/uri/seed13 | 1 + sapi/fuzzer/corpus/uri/seed14 | 1 + sapi/fuzzer/corpus/uri/seed15 | 1 + sapi/fuzzer/corpus/uri/seed16 | 1 + sapi/fuzzer/corpus/uri/seed17 | 1 + sapi/fuzzer/corpus/uri/seed18 | 2 + sapi/fuzzer/corpus/uri/seed19 | 2 + sapi/fuzzer/corpus/uri/seed20 | 1 + sapi/fuzzer/corpus/uri/seed21 | 2 + sapi/fuzzer/fuzzer-uri.c | 150 ++++++++++++++++++++++++++++++++++ 25 files changed, 187 insertions(+) create mode 100644 sapi/fuzzer/corpus/uri/seed01 create mode 100644 sapi/fuzzer/corpus/uri/seed02 create mode 100644 sapi/fuzzer/corpus/uri/seed03 create mode 100644 sapi/fuzzer/corpus/uri/seed04 create mode 100644 sapi/fuzzer/corpus/uri/seed05 create mode 100644 sapi/fuzzer/corpus/uri/seed06 create mode 100644 sapi/fuzzer/corpus/uri/seed07 create mode 100644 sapi/fuzzer/corpus/uri/seed08 create mode 100644 sapi/fuzzer/corpus/uri/seed09 create mode 100644 sapi/fuzzer/corpus/uri/seed10 create mode 100644 sapi/fuzzer/corpus/uri/seed11 create mode 100644 sapi/fuzzer/corpus/uri/seed12 create mode 100644 sapi/fuzzer/corpus/uri/seed13 create mode 100644 sapi/fuzzer/corpus/uri/seed14 create mode 100644 sapi/fuzzer/corpus/uri/seed15 create mode 100644 sapi/fuzzer/corpus/uri/seed16 create mode 100644 sapi/fuzzer/corpus/uri/seed17 create mode 100644 sapi/fuzzer/corpus/uri/seed18 create mode 100644 sapi/fuzzer/corpus/uri/seed19 create mode 100644 sapi/fuzzer/corpus/uri/seed20 create mode 100644 sapi/fuzzer/corpus/uri/seed21 create mode 100644 sapi/fuzzer/fuzzer-uri.c diff --git a/sapi/fuzzer/Makefile.frag b/sapi/fuzzer/Makefile.frag index c1fa75896912..f10596fd265f 100644 --- a/sapi/fuzzer/Makefile.frag +++ b/sapi/fuzzer/Makefile.frag @@ -23,6 +23,9 @@ $(SAPI_FUZZER_PATH)/php-fuzz-unserializehash: $(PHP_GLOBAL_OBJS) $(PHP_BINARY_OB $(SAPI_FUZZER_PATH)/php-fuzz-json: $(PHP_GLOBAL_OBJS) $(PHP_BINARY_OBJS) $(PHP_FUZZER_JSON_OBJS) $(FUZZER_BUILD) $(PHP_FUZZER_JSON_OBJS) -o $@ +$(SAPI_FUZZER_PATH)/php-fuzz-uri: $(PHP_GLOBAL_OBJS) $(PHP_BINARY_OBJS) $(PHP_FUZZER_URI_OBJS) + $(FUZZER_BUILD) $(PHP_FUZZER_URI_OBJS) -o $@ + $(SAPI_FUZZER_PATH)/php-fuzz-exif: $(PHP_GLOBAL_OBJS) $(PHP_BINARY_OBJS) $(PHP_FUZZER_EXIF_OBJS) $(FUZZER_BUILD) $(PHP_FUZZER_EXIF_OBJS) -o $@ diff --git a/sapi/fuzzer/README.md b/sapi/fuzzer/README.md index c9ce6ebab52b..3c0d78cdbb21 100644 --- a/sapi/fuzzer/README.md +++ b/sapi/fuzzer/README.md @@ -27,6 +27,7 @@ When running `make` it creates these binaries in `sapi/fuzzer/`: * `php-fuzz-unserialize`: Fuzzing unserialize() function * `php-fuzz-unserializehash`: Fuzzing unserialize() for HashContext objects * `php-fuzz-json`: Fuzzing JSON parser +* `php-fuzz-uri`: Fuzzing the `Uri\Rfc3986\Uri` and `Uri\WhatWg\Url` classes and `parse_url()` * `php-fuzz-exif`: Fuzzing `exif_read_data()` function (requires --enable-exif) * `php-fuzz-mbstring`: Fuzzing `mb_convert_encoding()` (requires `--enable-mbstring`) * `php-fuzz-mbregex`: Fuzzing `mb_ereg[i]()` (requires --enable-mbstring) @@ -95,3 +96,10 @@ export ONIG_LIBS="-L$PWD/oniguruma/src/.libs -l:libonig.a" ``` This will link an instrumented libonig statically into the PHP binary. + +For the uri fuzzer, a seed corpus is available: + +```sh +cp -r sapi/fuzzer/corpus/uri ./my-uri-corpus +sapi/fuzzer/php-fuzz-uri ./my-uri-corpus +``` diff --git a/sapi/fuzzer/config.m4 b/sapi/fuzzer/config.m4 index 21a44cd6d886..39352d46b747 100644 --- a/sapi/fuzzer/config.m4 +++ b/sapi/fuzzer/config.m4 @@ -63,6 +63,7 @@ if test "$PHP_FUZZER" != "no"; then PHP_FUZZER_TARGET([unserialize], [PHP_FUZZER_UNSERIALIZE_OBJS]) PHP_FUZZER_TARGET([unserializehash], [PHP_FUZZER_UNSERIALIZEHASH_OBJS]) PHP_FUZZER_TARGET([json], [PHP_FUZZER_JSON_OBJS]) + PHP_FUZZER_TARGET([uri], [PHP_FUZZER_URI_OBJS]) if test -n "$enable_exif" && test "$enable_exif" != "no"; then PHP_FUZZER_TARGET([exif], [PHP_FUZZER_EXIF_OBJS]) diff --git a/sapi/fuzzer/corpus/uri/seed01 b/sapi/fuzzer/corpus/uri/seed01 new file mode 100644 index 000000000000..1f8666542d15 --- /dev/null +++ b/sapi/fuzzer/corpus/uri/seed01 @@ -0,0 +1 @@ +https://user:pass@example.com:8080/a/b/c?x=1&y=2#frag \ No newline at end of file diff --git a/sapi/fuzzer/corpus/uri/seed02 b/sapi/fuzzer/corpus/uri/seed02 new file mode 100644 index 000000000000..7d3403c77be5 --- /dev/null +++ b/sapi/fuzzer/corpus/uri/seed02 @@ -0,0 +1 @@ +http://[2001:db8::1]:80/path \ No newline at end of file diff --git a/sapi/fuzzer/corpus/uri/seed03 b/sapi/fuzzer/corpus/uri/seed03 new file mode 100644 index 000000000000..edae4d83ecd7 --- /dev/null +++ b/sapi/fuzzer/corpus/uri/seed03 @@ -0,0 +1 @@ +http://[v1.fe80::a+en1]/ \ No newline at end of file diff --git a/sapi/fuzzer/corpus/uri/seed04 b/sapi/fuzzer/corpus/uri/seed04 new file mode 100644 index 000000000000..26c277355b19 --- /dev/null +++ b/sapi/fuzzer/corpus/uri/seed04 @@ -0,0 +1 @@ +http://192.168.0.1./x \ No newline at end of file diff --git a/sapi/fuzzer/corpus/uri/seed05 b/sapi/fuzzer/corpus/uri/seed05 new file mode 100644 index 000000000000..656462a3a84c --- /dev/null +++ b/sapi/fuzzer/corpus/uri/seed05 @@ -0,0 +1 @@ +http://0x7f.1/ \ No newline at end of file diff --git a/sapi/fuzzer/corpus/uri/seed06 b/sapi/fuzzer/corpus/uri/seed06 new file mode 100644 index 000000000000..68eedd165fe0 --- /dev/null +++ b/sapi/fuzzer/corpus/uri/seed06 @@ -0,0 +1 @@ +file:///C:/Windows/system32 \ No newline at end of file diff --git a/sapi/fuzzer/corpus/uri/seed07 b/sapi/fuzzer/corpus/uri/seed07 new file mode 100644 index 000000000000..557ded7992dc --- /dev/null +++ b/sapi/fuzzer/corpus/uri/seed07 @@ -0,0 +1 @@ +file:///tmp/test.txt \ No newline at end of file diff --git a/sapi/fuzzer/corpus/uri/seed08 b/sapi/fuzzer/corpus/uri/seed08 new file mode 100644 index 000000000000..44d438efbc1f --- /dev/null +++ b/sapi/fuzzer/corpus/uri/seed08 @@ -0,0 +1 @@ +mailto:someone@example.org?subject=hi \ No newline at end of file diff --git a/sapi/fuzzer/corpus/uri/seed09 b/sapi/fuzzer/corpus/uri/seed09 new file mode 100644 index 000000000000..31d8a5633d7a --- /dev/null +++ b/sapi/fuzzer/corpus/uri/seed09 @@ -0,0 +1 @@ +urn:isbn:0451450523 \ No newline at end of file diff --git a/sapi/fuzzer/corpus/uri/seed10 b/sapi/fuzzer/corpus/uri/seed10 new file mode 100644 index 000000000000..715a44bd06a3 --- /dev/null +++ b/sapi/fuzzer/corpus/uri/seed10 @@ -0,0 +1 @@ +//example.com/network/path \ No newline at end of file diff --git a/sapi/fuzzer/corpus/uri/seed11 b/sapi/fuzzer/corpus/uri/seed11 new file mode 100644 index 000000000000..3e8883892562 --- /dev/null +++ b/sapi/fuzzer/corpus/uri/seed11 @@ -0,0 +1 @@ +../relative/./path/../x \ No newline at end of file diff --git a/sapi/fuzzer/corpus/uri/seed12 b/sapi/fuzzer/corpus/uri/seed12 new file mode 100644 index 000000000000..d8ebcb2f44c7 --- /dev/null +++ b/sapi/fuzzer/corpus/uri/seed12 @@ -0,0 +1,2 @@ +https://example.com/ +../other?q#f \ No newline at end of file diff --git a/sapi/fuzzer/corpus/uri/seed13 b/sapi/fuzzer/corpus/uri/seed13 new file mode 100644 index 000000000000..52f9f9031d9d --- /dev/null +++ b/sapi/fuzzer/corpus/uri/seed13 @@ -0,0 +1 @@ +http://xn--nxasmq6b.example/ \ No newline at end of file diff --git a/sapi/fuzzer/corpus/uri/seed14 b/sapi/fuzzer/corpus/uri/seed14 new file mode 100644 index 000000000000..53b37573dc11 --- /dev/null +++ b/sapi/fuzzer/corpus/uri/seed14 @@ -0,0 +1 @@ +http://日本.example/ \ No newline at end of file diff --git a/sapi/fuzzer/corpus/uri/seed15 b/sapi/fuzzer/corpus/uri/seed15 new file mode 100644 index 000000000000..cd7b2bb409f5 --- /dev/null +++ b/sapi/fuzzer/corpus/uri/seed15 @@ -0,0 +1 @@ +https://example.com/%7Euser/%E2%82%AC \ No newline at end of file diff --git a/sapi/fuzzer/corpus/uri/seed16 b/sapi/fuzzer/corpus/uri/seed16 new file mode 100644 index 000000000000..20c9fbd26e36 --- /dev/null +++ b/sapi/fuzzer/corpus/uri/seed16 @@ -0,0 +1 @@ +data:text/plain;base64,SGVsbG8= \ No newline at end of file diff --git a/sapi/fuzzer/corpus/uri/seed17 b/sapi/fuzzer/corpus/uri/seed17 new file mode 100644 index 000000000000..08551640b5c9 --- /dev/null +++ b/sapi/fuzzer/corpus/uri/seed17 @@ -0,0 +1 @@ +HTTP://EXAMPLE.COM:/A%2fB \ No newline at end of file diff --git a/sapi/fuzzer/corpus/uri/seed18 b/sapi/fuzzer/corpus/uri/seed18 new file mode 100644 index 000000000000..74f751784ab7 --- /dev/null +++ b/sapi/fuzzer/corpus/uri/seed18 @@ -0,0 +1,2 @@ +http://example.com +http://other.example:1/ \ No newline at end of file diff --git a/sapi/fuzzer/corpus/uri/seed19 b/sapi/fuzzer/corpus/uri/seed19 new file mode 100644 index 000000000000..05a9b6667616 --- /dev/null +++ b/sapi/fuzzer/corpus/uri/seed19 @@ -0,0 +1,2 @@ +ws://example.com/chat +?query \ No newline at end of file diff --git a/sapi/fuzzer/corpus/uri/seed20 b/sapi/fuzzer/corpus/uri/seed20 new file mode 100644 index 000000000000..0045b075b554 --- /dev/null +++ b/sapi/fuzzer/corpus/uri/seed20 @@ -0,0 +1 @@ +foo://bar@[::ffff:1.2.3.4]:65535/ \ No newline at end of file diff --git a/sapi/fuzzer/corpus/uri/seed21 b/sapi/fuzzer/corpus/uri/seed21 new file mode 100644 index 000000000000..17af2129946f --- /dev/null +++ b/sapi/fuzzer/corpus/uri/seed21 @@ -0,0 +1,2 @@ +http://a/b/c/d;p?q +g;x=1/../y \ No newline at end of file diff --git a/sapi/fuzzer/fuzzer-uri.c b/sapi/fuzzer/fuzzer-uri.c new file mode 100644 index 000000000000..8c27014df533 --- /dev/null +++ b/sapi/fuzzer/fuzzer-uri.c @@ -0,0 +1,150 @@ +/* + +----------------------------------------------------------------------+ + | Copyright © The PHP Group and Contributors. | + +----------------------------------------------------------------------+ + | This source file is subject to the Modified BSD License that is | + | bundled with this package in the file LICENSE, and is available | + | through the World Wide Web at . | + | | + | SPDX-License-Identifier: BSD-3-Clause | + +----------------------------------------------------------------------+ + | Author: Yuma Kakei | + +----------------------------------------------------------------------+ +*/ + +#include
+#include + +#include "fuzzer.h" +#include "fuzzer-sapi.h" + +#define MAX_SIZE (8 * 1024) + +static const char *rfc3986_getters[] = { + "getUriType", "getScheme", "getRawScheme", "getUserInfo", "getRawUserInfo", + "getUsername", "getRawUsername", "getPassword", "getRawPassword", "getHost", + "getRawHost", "getHostType", "getPort", "getPath", "getRawPath", "getQuery", + "getRawQuery", "getFragment", "getRawFragment", "toString", "toRawString", + "__serialize", "__debugInfo", NULL +}; + +static const char *rfc3986_withers[] = { + "withScheme", "withUserInfo", "withHost", "withPath", "withQuery", "withFragment", NULL +}; + +static const char *whatwg_getters[] = { + "getScheme", "isSpecialScheme", "getUsername", "getPassword", "getAsciiHost", + "getUnicodeHost", "getHostType", "getPort", "getPath", "getQuery", "getFragment", + "toAsciiString", "toUnicodeString", "__serialize", "__debugInfo", NULL +}; + +static const char *whatwg_withers[] = { + "withScheme", "withUsername", "withPassword", "withHost", "withPath", "withQuery", + "withFragment", NULL +}; + +/* Call a method, discarding its result and any exception it throws. If the result is an + * object, call to_string_method on it. */ +static void call_method( + zend_object *obj, const char *name, uint32_t argc, zval *argv, const char *to_string_method) +{ + zend_function *fn = zend_hash_str_find_ptr_lc(&obj->ce->function_table, name, strlen(name)); + ZEND_ASSERT(fn != NULL); + + zval retval; + ZVAL_UNDEF(&retval); + zend_call_known_instance_method(fn, obj, &retval, argc, argv); + zend_clear_exception(); + + if (to_string_method && Z_TYPE(retval) == IS_OBJECT) { + call_method(Z_OBJ(retval), to_string_method, 0, NULL, NULL); + } + zval_ptr_dtor(&retval); +} + +static void fuzz_uri_class( + const char *class_name, size_t class_name_len, zval *uri, zval *ref, + const char **getters, const char **withers, const char *to_string_method) +{ + zend_class_entry *ce = zend_hash_str_find_ptr(CG(class_table), class_name, class_name_len); + ZEND_ASSERT(ce != NULL); + zend_function *parse = zend_hash_str_find_ptr(&ce->function_table, ZEND_STRL("parse")); + ZEND_ASSERT(parse != NULL); + + /* Url::parse() reports errors through its third, by-reference argument. */ + zval args[3], retval; + ZVAL_COPY_VALUE(&args[0], uri); + ZVAL_NULL(&args[1]); + ZVAL_NEW_EMPTY_REF(&args[2]); + ZVAL_NULL(Z_REFVAL(args[2])); + ZVAL_UNDEF(&retval); + zend_call_known_function(parse, NULL, ce, &retval, parse->common.num_args, args, NULL); + zend_clear_exception(); + zval_ptr_dtor(&args[2]); + + if (Z_TYPE(retval) != IS_OBJECT) { + zval_ptr_dtor(&retval); + return; + } + + zend_object *obj = Z_OBJ(retval); + for (const char **getter = getters; *getter; getter++) { + call_method(obj, *getter, 0, NULL, NULL); + } + call_method(obj, "equals", 1, &retval, NULL); + + if (!Z_ISUNDEF_P(ref)) { + call_method(obj, "resolve", 1, ref, to_string_method); + for (const char **wither = withers; *wither; wither++) { + call_method(obj, *wither, 1, ref, to_string_method); + } + } + + zval_ptr_dtor(&retval); +} + +int LLVMFuzzerTestOneInput(const uint8_t *Data, size_t Size) { + if (Size > MAX_SIZE) { + /* Large inputs have a large impact on fuzzer performance, + * but are unlikely to be necessary to reach new codepaths. */ + return 0; + } + + if (fuzzer_request_startup() == FAILURE) { + return 0; + } + + fuzzer_setup_dummy_frame(); + + /* The input is a URI, optionally followed by a newline and a string that is passed to + * resolve() and the with*() methods. The WHATWG parser strips newlines from its input, so + * using them as separator loses little. */ + const uint8_t *newline = memchr(Data, '\n', Size); + size_t uri_len = newline ? (size_t) (newline - Data) : Size; + zval uri, ref; + ZVAL_STRINGL(&uri, (const char *) Data, uri_len); + if (newline) { + ZVAL_STRINGL(&ref, (const char *) newline + 1, Size - uri_len - 1); + } else { + ZVAL_UNDEF(&ref); + } + + fuzz_uri_class(ZEND_STRL("uri\\rfc3986\\uri"), &uri, &ref, + rfc3986_getters, rfc3986_withers, "toRawString"); + fuzz_uri_class(ZEND_STRL("uri\\whatwg\\url"), &uri, &ref, + whatwg_getters, whatwg_withers, "toAsciiString"); + fuzzer_call_php_func_zval("parse_url", 1, &uri); + + zval_ptr_dtor(&uri); + zval_ptr_dtor(&ref); + + fuzzer_request_shutdown(); + return 0; +} + +int LLVMFuzzerInitialize(int *argc, char ***argv) { + fuzzer_init_php(NULL); + + /* fuzzer_shutdown_php(); */ + return 0; +}