//! Structured fuzz over the custom-pages CSS sanitizer. //! //! Row 2 of `astra-soak-overview`, and the other half of the same trust //! boundary as the `html` target: creator-authored input rendered on a public //! page. What CSS can do that HTML cannot is escape the user canvas and restyle //! platform chrome, so scoping is asserted here alongside the safety floor. //! //! ## The oracle lives in the crate, not here //! //! Everything asserted is `custom_pages::oracle::check_css`, the same function //! `tests/regressions.rs` replays on stable. A crash found here becomes a //! permanent test by copying one file into `fuzz/regressions/`, and neither //! side can drift into checking less than the other. //! //! It covers BOTH scoping entry points. The doors were counted before this was //! written, as the task instructs: nine call sites, all in MNW server, but two //! entry points -- `sanitize_css` for a profile or project page and //! `sanitize_item_css` for the item pages that wear the parent project's //! styling re-scoped to `.item-canvas#ic-`. Only the first had an oracle over //! it until this target was built. //! //! ## It parses the output rather than scanning it //! //! Worth knowing before reading a finding from this target. The oracle it //! replaced matched substrings over the whole stylesheet and had four false //! positives, the plainest of which was a two-step `@keyframes`: the step //! `100%` read as a selector that had escaped the canvas. Ordinary creator CSS, //! reported as a security finding. What ships reparses the printed sheet and //! walks the AST, so `@import` is forbidden as a rule rather than as a string //! and a `content: "@import"` is what it is -- text. #![no_main] use libfuzzer_sys::fuzz_target; use std::sync::LazyLock; /// The owner scope woven into the canvas selector, and the one /// `tests/regressions.rs` replays with, so a crash reproduces there unchanged. /// It must stay id-safe: the sanitizer refuses a scope that is not, and fuzzing /// that refusal would only measure `is_id_safe`. const OWNER_SCOPE: &str = "11111111-1111-1111-1111-111111111111"; static POLICY: LazyLock = LazyLock::new(|| { custom_pages::UrlPolicy::new( "https://u.makenot.work/alice/proj", [ "makenot.work".to_string(), "u.makenot.work".to_string(), "cdn.makenot.work".to_string(), ], ) .expect("the fixture policy is well-formed") }); /// The oracle runs on its own thread, with a large stack. /// /// Not a workaround for a defect in the crate: the SANITIZER is fine on a 2 MB /// stack, which is what tokio gives a worker, and that was measured against the /// input that prompted this (243 bytes in, 381 KB of flattened CSS out, survives /// at 2 MB). What needs the room is the ORACLE's own reparse of that output, /// which recurses per nesting level in a build where AddressSanitizer makes /// every frame several times its normal size. /// /// The alternative was to stop checking large outputs, which would have blinded /// the target on exactly the inputs most worth checking -- the ones that /// amplify. Stack is cheaper than coverage. fn checked(input: &str) { let owned = input.to_string(); let handle = std::thread::Builder::new() .stack_size(256 * 1024 * 1024) .spawn(move || custom_pages::oracle::check_css(&owned, OWNER_SCOPE, &POLICY)) .expect("spawning the oracle thread"); // Propagate a panic rather than swallowing it: the panic IS the finding, and // a target that joined and ignored the result would report clean forever. if let Err(payload) = handle.join() { std::panic::resume_unwind(payload); } } fuzz_target!(|input: &str| { checked(input); });