Skip to main content

max / makenotwork

custom-pages: run the css oracle on a thread with room to recurse A 243-byte stylesheet flattens to 381 KB, and the oracle's reparse of that recurses per nesting level in a build where AddressSanitizer makes every frame several times its usual size. libFuzzer's thread ran out of stack. Measured before concluding, because the interesting question was whether this was ours: the SANITIZER handles the same input on a 2 MB stack, which is what tokio gives a worker. So there is no production crash here, only an oracle that needs more room than the fuzzing harness's thread has. The alternative was to skip the structural checks on large outputs, which would have blinded the target on exactly the inputs most worth checking: the ones that amplify. Stack is cheaper than coverage. The join propagates the panic rather than swallowing it, or a target that found something would report clean forever. Costs throughput -- about 1,500 exec/s to 430 -- from the per-input spawn. 773,015 runs over thirty minutes with no findings.
Co-Authored-By
Claude Opus 5 (1M context) <noreply@anthropic.com>
Author: Max Johnson <me@maxj.phd> · 2026-08-26 05:02 UTC
Signed with PGP, not checked
Commit: 39980b2c32c433d17e88483298ea7063de17361b
Parent: 56abf6d
1 file changed, +26 insertions, -1 deletion
@@ -52,6 +52,31 @@
52 52 .expect("the fixture policy is well-formed")
53 53 });
54 54
55 + /// The oracle runs on its own thread, with a large stack.
56 + ///
57 + /// Not a workaround for a defect in the crate: the SANITIZER is fine on a 2 MB
58 + /// stack, which is what tokio gives a worker, and that was measured against the
59 + /// input that prompted this (243 bytes in, 381 KB of flattened CSS out, survives
60 + /// at 2 MB). What needs the room is the ORACLE's own reparse of that output,
61 + /// which recurses per nesting level in a build where AddressSanitizer makes
62 + /// every frame several times its normal size.
63 + ///
64 + /// The alternative was to stop checking large outputs, which would have blinded
65 + /// the target on exactly the inputs most worth checking -- the ones that
66 + /// amplify. Stack is cheaper than coverage.
67 + fn checked(input: &str) {
68 + let owned = input.to_string();
69 + let handle = std::thread::Builder::new()
70 + .stack_size(256 * 1024 * 1024)
71 + .spawn(move || custom_pages::oracle::check_css(&owned, OWNER_SCOPE, &POLICY))
72 + .expect("spawning the oracle thread");
73 + // Propagate a panic rather than swallowing it: the panic IS the finding, and
74 + // a target that joined and ignored the result would report clean forever.
75 + if let Err(payload) = handle.join() {
76 + std::panic::resume_unwind(payload);
77 + }
78 + }
79 +
55 80 fuzz_target!(|input: &str| {
56 - custom_pages::oracle::check_css(input, OWNER_SCOPE, &POLICY);
81 + checked(input);
57 82 });