diff --git a/sapi/fuzzer/Makefile.frag b/sapi/fuzzer/Makefile.frag
index c1fa75896912..eeae1eba8809 100644
--- a/sapi/fuzzer/Makefile.frag
+++ b/sapi/fuzzer/Makefile.frag
@@ -31,3 +31,6 @@ $(SAPI_FUZZER_PATH)/php-fuzz-mbstring: $(PHP_GLOBAL_OBJS) $(PHP_BINARY_OBJS) $(P
$(SAPI_FUZZER_PATH)/php-fuzz-mbregex: $(PHP_GLOBAL_OBJS) $(PHP_BINARY_OBJS) $(PHP_FUZZER_MBREGEX_OBJS)
$(FUZZER_BUILD) $(PHP_FUZZER_MBREGEX_OBJS) -o $@
+
+$(SAPI_FUZZER_PATH)/php-fuzz-dom-html: $(PHP_GLOBAL_OBJS) $(PHP_BINARY_OBJS) $(PHP_FUZZER_DOM_HTML_OBJS)
+ $(FUZZER_BUILD) $(PHP_FUZZER_DOM_HTML_OBJS) -o $@
diff --git a/sapi/fuzzer/config.m4 b/sapi/fuzzer/config.m4
index ce045c8962d6..c4ef3b8f0653 100644
--- a/sapi/fuzzer/config.m4
+++ b/sapi/fuzzer/config.m4
@@ -70,5 +70,10 @@ if test "$PHP_FUZZER" != "no"; then
fi
fi
+ dnl HTML5 parsing of untrusted documents through the lexbor engine (ext/dom).
+ if test -n "$enable_dom" && test "$enable_dom" != "no"; then
+ PHP_FUZZER_TARGET([dom-html], [PHP_FUZZER_DOM_HTML_OBJS])
+ fi
+
PHP_SUBST([PHP_FUZZER_BINARIES])
fi
diff --git a/sapi/fuzzer/dict/dom-html b/sapi/fuzzer/dict/dom-html
new file mode 100644
index 000000000000..2bbb72733d98
--- /dev/null
+++ b/sapi/fuzzer/dict/dom-html
@@ -0,0 +1,58 @@
+# Tokens for HTML5 parsing (php-fuzz-dom-html).
+"
"
+"
"
+""
+". |
+ +----------------------------------------------------------------------+
+ | Authors: arthurscchan |
+ +----------------------------------------------------------------------+
+*/
+
+#include "fuzzer.h"
+
+#include "main/php.h"
+#include
+#include "main/php_main.h"
+
+#include
+#include
+#include
+
+#include "fuzzer-sapi.h"
+
+/* Large documents mostly slow down fuzzing without reaching new codepaths. */
+#define MAX_SIZE (1 * 1024 * 1024)
+
+/* createFromString()'s $options accept HTML_PARSE_* bits (LIBXML_NOERROR is
+ * 1<<5 = 32 in libxml2); 0 is the neutral default and HTML5 parsing is
+ * error-tolerant, so no suppression bit is needed. */
+#define FUZZER_HTML_OPTIONS 0
+
+int LLVMFuzzerTestOneInput(const uint8_t *Data, size_t Size) {
+ if (Size > MAX_SIZE || Size < 2) {
+ return 0;
+ }
+
+ if (fuzzer_request_startup() == FAILURE) {
+ return 0;
+ }
+
+ fuzzer_setup_dummy_frame();
+
+ zend_string *class_name = zend_string_init(ZEND_STRL("Dom\\HTMLDocument"), 0);
+ zend_class_entry *ce = zend_lookup_class(class_name);
+ zend_string_release(class_name);
+
+ if (ce) {
+ /* Method table entries are stored lowercased; lookups are case-sensitive. */
+ zend_function *create = zend_hash_str_find_ptr(&ce->function_table, ZEND_STRL("createfromstring"));
+ zend_function *save = zend_hash_str_find_ptr(&ce->function_table, ZEND_STRL("savehtml"));
+ ZEND_ASSERT(create != NULL);
+ ZEND_ASSERT(save != NULL);
+
+ zval retval, args[3];
+ ZVAL_STRINGL(&args[0], (const char *) Data, Size);
+ ZVAL_LONG(&args[1], FUZZER_HTML_OPTIONS);
+ ZVAL_NULL(&args[2]);
+
+ /* Parse untrusted bytes into an HTML5 document (lexbor engine). */
+ zend_call_known_function(create, NULL, ce, &retval, 3, args, NULL);
+
+ /* Round-trip direction: serialize the parsed document back to HTML. */
+ if (Z_TYPE(retval) == IS_OBJECT && save != NULL) {
+ zval retval2;
+ zend_call_known_instance_method_with_0_params(save, Z_OBJ_P(&retval), &retval2);
+ zval_ptr_dtor(&retval2);
+ }
+ zval_ptr_dtor(&retval);
+ zval_ptr_dtor(&args[0]);
+
+ if (EG(exception)) {
+ zend_clear_exception();
+ }
+ }
+
+ fuzzer_request_shutdown();
+ return 0;
+}
+
+int LLVMFuzzerInitialize(int *argc, char ***argv) {
+ fuzzer_init_php(NULL);
+ return 0;
+}
diff --git a/sapi/fuzzer/generate_all.php b/sapi/fuzzer/generate_all.php
index 5794bdacc6ba..45eb9bc8fa7b 100644
--- a/sapi/fuzzer/generate_all.php
+++ b/sapi/fuzzer/generate_all.php
@@ -1,6 +1,7 @@
$maxLen) { continue; }
+ file_put_contents(sprintf('%s/domhtml_%03d.html', $corpusDir, $n++), $content);
+ }
+ }
+
+ /* Hand-built edge cases not present as fixtures. */
+ $extra = [
+ 't'
+ . 'café & "quotes"
',
+ ''
+ . '| t |
',
+ ''
+ . 'unclosed bold both spans',
+ ''
+ . '',
+ "\xEF\xBB\xBF"
+ . '' . str_repeat('d
', 8) . '',
+ ];
+ foreach ($extra as $content) {
+ file_put_contents(sprintf('%s/domhtml_extra_%02d.html', $corpusDir, $n++), $content);
+ }
+}