From 66671376cf35f49c6bad3ee6f122bb54482354fc Mon Sep 17 00:00:00 2001 From: Red Davies Date: Fri, 22 May 2026 00:02:20 -0400 Subject: [PATCH] Re-enable FuzzParseDoc The original FuzzParseDoc property was removed when crash-resistance fuzz tests were added (PR #32) because feeding arbitrary byte strings to Xml2Doc.parseDoc triggered cumulative state corruption inside libxml2 - a segfault or "free(): invalid pointer" abort after ~5-10 distinct failed parses. The crash was cumulative across inputs but did not reproduce when the same input was reparsed in a loop, suggesting state buildup in libxml2's internal error context across failed parses. PR #36 (errors-as-data refactor) routes parse failures through Xml2Error.from_last_error, which calls xmlResetError after reading the per-thread last-error. This appears to clear the state buildup that previously caused the corruption. Re-enabling the property with the default 100-sample budget. Five consecutive `make test` runs (500 distinct failed-parse attempts) complete without incident, well past the original ~5-10 crash threshold. The crash-resistance contract is back in force for the parse path. No release note: test-only change, follows PR #32 / #22 precedent of not generating CHANGELOG entries for test additions. --- libxml2/_test.pony | 1 + libxml2/_tests/fuzz_tests.pony | 46 ++++++++++++++++++++++++++-------- 2 files changed, 36 insertions(+), 11 deletions(-) diff --git a/libxml2/_test.pony b/libxml2/_test.pony index fee588f..8072406 100755 --- a/libxml2/_test.pony +++ b/libxml2/_test.pony @@ -78,6 +78,7 @@ actor \nodoc\ Main is TestList test(TestParseDocErrorRecovery) test(TestParseDocEntitiesNotSubstitutedByDefault) // Crash-resistance fuzz tests (PonyCheck Property1) + test(Property1UnitTest[String](recover iso FuzzParseDoc end)) test(Property1UnitTest[(String, String)]( recover iso FuzzCreateAndCreateElement end)) test(Property1UnitTest[(String, String)]( diff --git a/libxml2/_tests/fuzz_tests.pony b/libxml2/_tests/fuzz_tests.pony index 420b724..c513a8a 100644 --- a/libxml2/_tests/fuzz_tests.pony +++ b/libxml2/_tests/fuzz_tests.pony @@ -35,17 +35,41 @@ use "pony_check" // input into spurious errors: those are swallowed by the inner // `try ... end` by design. -// NOTE: a FuzzParseDoc property was investigated and removed. -// Feeding random byte strings, unicode_bmp strings, or even pure -// ascii_letters strings to Xml2Doc.parseDoc reliably segfaults or -// triggers a `free(): invalid pointer` abort after a handful of -// failed parses. The crash is cumulative across inputs (a single -// failed parse passes; ~5-10 distinct failed parses crash) but does -// not reproduce when the same input is reparsed in a loop. The -// underlying cause appears to be in libxml2's internal state -// handling for repeated parse errors. The other fuzz tests below -// avoid this by exercising APIs other than parseDoc, starting from -// a fixed valid document or a freshly created one. +class \nodoc\ iso FuzzParseDoc is Property1[String] + """ + Feed arbitrary byte strings (including invalid UTF-8 and embedded + NUL) to `Xml2Parser.parseDoc`. The call must either return a parsed + `Xml2Doc` or an `Xml2Error` — never crash the process. + + An earlier incarnation of this property was removed because + repeated parse failures triggered cumulative libxml2 state + corruption that segfaulted the test binary after ~5–10 distinct + failed parses. The new union-returning `Xml2Parser.parseDoc` reads + and resets libxml2's per-thread last-error on every failure path + (via `Xml2Error.from_last_error()?`'s embedded `xmlResetError`), + which appears to clear the state buildup. This property re-tests + that invariant. + """ + fun name(): String => "fuzz/parse-doc/property" + + fun gen(): Generator[String] => + // Arbitrary bytes 0..255 inclusive, lengths up to 256. Covers + // invalid UTF-8, embedded NUL, and partial XML fragments. + Generators.byte_string(Generators.u8(), 0, 256) + + fun ref property(arg1: String, h: PropertyHelper) => + match Xml2Parser.parseDoc(arg1) + | let doc: Xml2Doc => + // If parsing succeeded, exercise the doc briefly so any latent + // corruption from the parse path surfaces here. + try + let root = doc.getRootElement()? + let _ = root.name() + let _ = root.getNodePath() + end + | let _: Xml2Error => None + end + h.assert_true(true) class \nodoc\ iso FuzzCreateAndCreateElement is Property1[(String, String)] """