Project homepage Mailing List  Warmcat.com  API Docs  Github Mirror 
    npro  
 Modern all-safe Rust Network Protocol library supporting h1, h2, h3, ws, wt sans-IO and with socket IO + tls
git clone https://npro.rs/repo/npro
 
root / crates / npro-test / h1 / requests / uri-dot-args.http
Author[]Andy Green <andy@warmcat.com> 2026-10-03 19:32 UTC
Committer[]Andy Green <andy@warmcat.com> 2026-10-05 06:32 UTC
Treeeaf0f59bc2022fbb0832ff9ed96b1f46cd118d9c   Raw Patch
 
npro-fuzz: fuzz harnesses with oracles, smoke-tested everywhere
npro-fuzz: fuzz harnesses with oracles, smoke-tested everywhere

A fuzz target is only as good as what it checks.  Add a publish = false
crate holding each target as a safe fn(&[u8]) that drives the code under
test and checks it against an oracle, reporting a disagreement, or any
panic in the code under test, as a panic:

 - utf8: Utf8Validator against core::str::from_utf8.  Fed a byte at a
   time it must refuse at exactly the first byte no well-formed text
   could have next; fed in pieces of 0 to 16 bytes, empty ones included,
   it must refuse in the piece holding that byte and go on refusing.
 - sha1: Sha1 fed in pieces against Sha1::digest of the whole.
 - base64: encode into a buffer with room to spare, which must use
   exactly encoded_len and decode back through a strict decoder; and
   into buffers one short and empty, which must be refused with nothing
   written.
 - transcript: npro-test's reader, whose results must keep its limits.

Where a target splits its input, the first byte chooses how, so a
fuzzer explores the split like any other part of the input.

The smoke tests run every target over its seeds in fuzz/seeds/ (and
the vendored transcripts) and over 4000 inputs drawn from SeededRandom,
in plain cargo test on every builder, so the harnesses stay working
without a fuzzer.  Under Miri they draw 24.

Checked by planting bugs: utf8 accepting surrogates, past U+10FFFF or
an overlong e0 form, or refusing a valid continuation byte, and base64
with a wrong symbol or padding, each fail the smoke tests.

npro-fuzz is admitted to deny.toml as a workspace crate.  Its path
dependencies on npro-core and npro-test have no version, which cargo
deny counts as wildcards; allow-wildcard-paths lets those through for
crates that are not published, and only for them.

The libFuzzer targets that drive these harnesses follow separately.

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_019kg5Eemy68ZaqDBcUJQG6J
diff --git a/Cargo.lock b/Cargo.lock index febbb52..3c2e2c6 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -11,5 +11,13 @@ name = "npro-core" version = "0.0.2" [[package]] +name = "npro-fuzz" +version = "0.0.2" +dependencies = [ + "npro-core", + "npro-test", +] + +[[package]] name = "npro-test" version = "0.0.2" diff --git a/crates/npro-fuzz/Cargo.toml b/crates/npro-fuzz/Cargo.toml new file mode 100644 index 0000000..c3450b1 --- /dev/null +++ b/crates/npro-fuzz/Cargo.toml @@ -0,0 +1,20 @@ +[package] +name = "npro-fuzz" +description = "Fuzz harnesses for npro: each target's input handling and its oracle, for libFuzzer and for the smoke tests" +publish = false +version.workspace = true +edition.workspace = true +rust-version.workspace = true +license.workspace = true +homepage.workspace = true +repository.workspace = true + +[dependencies] +npro-core = { path = "../npro-core" } +npro-test = { path = "../npro-test" } + +[dev-dependencies] +npro-core = { path = "../npro-core", features = ["replay"] } + +[lints] +workspace = true diff --git a/crates/npro-fuzz/src/lib.rs b/crates/npro-fuzz/src/lib.rs new file mode 100644 index 0000000..82a53f6 --- /dev/null +++ b/crates/npro-fuzz/src/lib.rs @@ -0,0 +1,79 @@ +//! Fuzz harnesses for npro, not published. +//! +//! Each target is a function taking the fuzzer's bytes. It drives the code +//! under test with them and checks the result against an oracle: another +//! way of getting the same answer, or a property the answer must have. A +//! disagreement is reported as a panic, which is how libFuzzer learns of a +//! finding; so is any panic in the code under test, since nothing reachable +//! from network data may panic. +//! +//! The same functions run in two places: +//! +//! - the libFuzzer targets in the separate `fuzz/` workspace, one per +//! [`Target`], for coverage-guided campaigns (`scripts/fuzz.sh`); +//! - this crate's smoke tests, part of every `cargo test`, which run each +//! target over its seeds and inputs from a fixed seed on every platform. +//! +//! Where a target needs to choose something beyond the bytes under test, +//! such as where to split them, the choice is taken from the input's first +//! byte, so libFuzzer explores it like any other part of the input. + +#![forbid(unsafe_code)] + +mod targets; + +pub use targets::{base64, sha1, transcript, utf8}; + +/// A fuzz target: its name is the libFuzzer target's, the seed directory's +/// under `fuzz/seeds/`, and the corpus's under `corpus-<name>`. +/// +/// ``` +/// use npro_fuzz::Target; +/// +/// for t in Target::ALL { +/// t.run(b""); +/// } +/// assert_eq!(Target::Utf8.name(), "utf8"); +/// ``` +#[derive(Clone, Copy, Debug, PartialEq, Eq)] +pub enum Target { + /// [`utf8`]: incremental UTF-8 validation against `core::str`. + Utf8, + /// [`sha1`]: SHA-1 fed in pieces against SHA-1 in one go. + Sha1, + /// [`base64`]: encoding against decoding, and buffers of every size. + Base64, + /// [`transcript`]: npro-test's transcript reader. + Transcript, +} + +impl Target { + /// Every target. + pub const ALL: [Self; 4] = [Self::Utf8, Self::Sha1, Self::Base64, Self::Transcript]; + + /// The target's name. + #[must_use] + pub const fn name(self) -> &'static str { + match self { + Self::Utf8 => "utf8", + Self::Sha1 => "sha1", + Self::Base64 => "base64", + Self::Transcript => "transcript", + } + } + + /// Runs the target over one input. + /// + /// # Panics + /// + /// On a finding: the code under test panicked, or disagreed with the + /// target's oracle. + pub fn run(self, data: &[u8]) { + match self { + Self::Utf8 => utf8(data), + Self::Sha1 => sha1(data), + Self::Base64 => base64(data), + Self::Transcript => transcript(data), + } + } +} diff --git a/crates/npro-fuzz/src/targets.rs b/crates/npro-fuzz/src/targets.rs new file mode 100644 index 0000000..4ea70d6 --- /dev/null +++ b/crates/npro-fuzz/src/targets.rs @@ -0,0 +1,313 @@ +//! The targets, and what each checks. + +use core::fmt; +use core::ops::Range; + +use npro_core::base64::{self as b64, encoded_len}; +use npro_core::sha1::Sha1; +use npro_core::utf8::Utf8Validator; +use npro_test::{MAX_BYTES, MAX_STEPS, Transcript}; + +/// Reports a finding to whatever is driving the target: libFuzzer, which +/// treats a panic as a crash and keeps the input, or a smoke test. +#[allow( + clippy::panic, + reason = "a panic is how a fuzz target reports a finding to libFuzzer" +)] +#[cold] +fn finding(target: &str, what: fmt::Arguments<'_>) -> ! { + panic!("{target}: {what}") +} + +/// The input's first byte, which chooses how a target splits the rest, +/// and the rest. +fn control(data: &[u8]) -> (u8, &[u8]) { + data.split_first().map_or((0, data), |(c, rest)| (*c, rest)) +} + +/// `bytes` split into pieces of 0 to 16 bytes, the sizes drawn from a +/// small generator seeded by `ctl`. Pieces are what arrives in one read, +/// so empty ones are included: a reader can be handed nothing. +struct Pieces<'a> { + rest: &'a [u8], + state: u32, +} + +impl<'a> Pieces<'a> { + fn new(ctl: u8, bytes: &'a [u8]) -> Self { + Self { + rest: bytes, + state: u32::from(ctl), + } + } +} + +impl<'a> Iterator for Pieces<'a> { + type Item = &'a [u8]; + + fn next(&mut self) -> Option<&'a [u8]> { + if self.rest.is_empty() { + return None; + } + // the LCG from C's rand(): period 2^32, so sizes of 0 do not + // repeat for long, and every split is reachable from some ctl + self.state = self.state.wrapping_mul(1_103_515_245).wrapping_add(12_345); + let want = usize::try_from((self.state >> 16) % 17).unwrap_or(1); + let (piece, rest) = self.rest.split_at(want.min(self.rest.len())); + self.rest = rest; + Some(piece) + } +} + +/// How text ends, as validation sees it. +#[derive(Clone, Copy, Debug, PartialEq, Eq)] +enum Utf8End { + /// Well-formed and between characters. + Complete, + /// Well-formed so far, but inside a character. + Partial, + /// The byte at this index cannot continue any well-formed text. + InvalidAt(usize), +} + +/// The oracle: how `text` ends according to `core::str`. +fn utf8_end_by_core(text: &[u8]) -> Utf8End { + let e = match core::str::from_utf8(text) { + Ok(_) => return Utf8End::Complete, + Err(e) => e, + }; + if e.error_len().is_none() { + return Utf8End::Partial; + } + // core reports where the last good character ends. The validator + // refuses at the first byte no well-formed text could have next, + // which is the first index whose prefix core calls broken rather + // than unfinished. + let from = e.valid_up_to(); + let refused = (from..text.len()).find(|&i| { + text.get(..=i) + .is_some_and(|p| core::str::from_utf8(p).is_err_and(|e| e.error_len().is_some())) + }); + Utf8End::InvalidAt(refused.unwrap_or(from)) +} + +/// Incremental UTF-8 validation, as ws text arrives, against `core::str` +/// over the whole text. +/// +/// The first byte chooses how the rest is split. It is validated a byte at +/// a time, which must refuse it at exactly the byte core's answer implies, +/// and in pieces, which must refuse it in the piece holding that byte, and +/// go on refusing every piece after, empty ones included. +pub fn utf8(data: &[u8]) { + let (ctl, text) = control(data); + let expect = utf8_end_by_core(text); + + let mut v = Utf8Validator::new(); + let mut bytewise = None; + for (i, b) in text.iter().enumerate() { + if v.feed(core::slice::from_ref(b)).is_err() { + bytewise = Some(Utf8End::InvalidAt(i)); + break; + } + } + let bytewise = bytewise.unwrap_or(if v.at_boundary() { + Utf8End::Complete + } else { + Utf8End::Partial + }); + if bytewise != expect { + finding( + "utf8", + format_args!("fed a byte at a time it ends {bytewise:?}, core says {expect:?}"), + ); + } + + let mut v = Utf8Validator::new(); + let mut refused: Option<Range<usize>> = None; + let mut at = 0usize; + for piece in Pieces::new(ctl, text) { + let span = at..at.saturating_add(piece.len()); + match (v.feed(piece), &refused) { + (Err(_), None) => refused = Some(span.clone()), + (Ok(()), Some(r)) => finding( + "utf8", + format_args!("accepted {span:?} after refusing {r:?}"), + ), + (Err(_), Some(_)) | (Ok(()), None) => {} + } + at = span.end; + } + let ok = match (expect, &refused) { + (Utf8End::InvalidAt(i), Some(r)) => r.contains(&i), + (Utf8End::Complete, None) => v.at_boundary(), + (Utf8End::Partial, None) => !v.at_boundary(), + (Utf8End::InvalidAt(_), None) | (Utf8End::Complete | Utf8End::Partial, Some(_)) => false, + }; + if !ok { + finding( + "utf8", + format_args!( + "fed in pieces it refused {refused:?} and ends at a boundary: {}, core says {expect:?}", + v.at_boundary() + ), + ); + } +} + +/// SHA-1 fed in pieces, as a ws key or a body arrives, against SHA-1 of +/// the whole message at once. The first byte chooses the split. +pub fn sha1(data: &[u8]) { + let (ctl, msg) = control(data); + let whole = Sha1::digest(msg); + + let mut h = Sha1::new(); + for piece in Pieces::new(ctl, msg) { + h.update(piece); + } + let pieces = h.finish(); + + if pieces != whole { + finding( + "sha1", + format_args!("in pieces {pieces:02x?}, in one go {whole:02x?}"), + ); + } +} + +/// What the encoder must leave alone: no base64 symbol is this byte. +const UNTOUCHED: u8 = 0xa5; + +/// The value of a base64 symbol, RFC 4648 4's alphabet. +fn sextet(c: u8) -> Option<u8> { + let (base, value) = match c { + b'A'..=b'Z' => (b'A', 0), + b'a'..=b'z' => (b'a', 26), + b'0'..=b'9' => (b'0', 52), + b'+' => return Some(62), + b'/' => return Some(63), + _ => return None, + }; + c.checked_sub(base)?.checked_add(value) +} + +/// The oracle: a strict RFC 4648 decoder, refusing anything the encoder +/// should never produce, so a wrong symbol or padding shows as `None`. +fn decode(text: &[u8]) -> Option<Vec<u8>> { + if text.len() % 4 != 0 { + return None; + } + let groups = text.len() / 4; + let mut out = Vec::with_capacity(groups.checked_mul(3)?); + for (n, g) in text.chunks_exact(4).enumerate() { + let last = n.checked_add(1)? == groups; + // padding symbols, bytes decoded, and the bits padding stands in for + let (pad, have, shift) = match *g { + [_, _, b'=', b'='] if last => (2, 1, 12), + [_, _, _, b'='] if last => (1, 2, 6), + _ => (0, 3, 0), + }; + let mut v = 0u32; + for &c in g.get(..4usize.checked_sub(pad)?)? { + v = (v << 6) | u32::from(sextet(c)?); + } + v <<= shift; + let bytes = v.to_be_bytes(); + let decoded = bytes.get(1..1usize.checked_add(have)?)?; + // the bits padding stands in for must be zero, as the encoder + // writes them + if bytes + .get(1usize.checked_add(have)?..)? + .iter() + .any(|&b| b != 0) + { + return None; + } + out.extend_from_slice(decoded); + } + Some(out) +} + +/// base64 encoding, as the ws accept and keys use it. +/// +/// The whole input is encoded into a buffer with room to spare, which must +/// use exactly `encoded_len` bytes and leave the rest alone, and decode back +/// to the input; and into buffers one byte short and empty, which must be +/// refused with nothing written. +pub fn base64(data: &[u8]) { + let Some(len) = encoded_len(data.len()) else { + finding( + "base64", + format_args!("no encoded length for {} bytes", data.len()), + ); + }; + let mut buf = vec![UNTOUCHED; len.saturating_add(4)]; + + for short in [Some(0), len.checked_sub(1)].into_iter().flatten() { + if short >= len { + continue; + } + buf.fill(UNTOUCHED); + let Some(dst) = buf.get_mut(..short) else { + continue; + }; + if b64::encode(data, dst).is_ok() { + finding( + "base64", + format_args!("{} bytes encoded into {short}", data.len()), + ); + } + if buf.iter().any(|&b| b != UNTOUCHED) { + finding( + "base64", + format_args!("refused {short} bytes but wrote to them"), + ); + } + } + + buf.fill(UNTOUCHED); + match b64::encode(data, &mut buf) { + Ok(n) if n == len => {} + Ok(n) => finding( + "base64", + format_args!("used {n} bytes, encoded_len says {len}"), + ), + Err(e) => finding( + "base64", + format_args!("refused a buffer of {}: {e}", buf.len()), + ), + } + let (text, spare) = buf.split_at(len); + if spare.iter().any(|&b| b != UNTOUCHED) { + finding("base64", format_args!("wrote past the {len} bytes it used")); + } + if decode(text).as_deref() != Some(data) { + finding( + "base64", + format_args!("{text:?} does not decode to the input"), + ); + } +} + +/// npro-test's transcript reader, which reads files that come from outside +/// the tree. Whatever it accepts must respect its limits and its promise +/// that step times never go backwards. +pub fn transcript(data: &[u8]) { + let Ok(t) = Transcript::parse(data) else { + return; + }; + if data.len() > MAX_BYTES { + finding("transcript", format_args!("accepted {} bytes", data.len())); + } + if t.steps.len() > MAX_STEPS { + finding( + "transcript", + format_args!("accepted {} steps", t.steps.len()), + ); + } + if t.steps + .windows(2) + .any(|w| matches!(w, [a, b] if b.t_us < a.t_us)) + { + finding("transcript", format_args!("accepted time going backwards")); + } +} diff --git a/crates/npro-fuzz/tests/smoke.rs b/crates/npro-fuzz/tests/smoke.rs new file mode 100644 index 0000000..0092347 --- /dev/null +++ b/crates/npro-fuzz/tests/smoke.rs @@ -0,0 +1,198 @@ +//! Every fuzz target, run over its seeds and over inputs drawn from a fixed +//! seed, so the harnesses and what they check stay working on every +//! platform without a fuzzer. This is a smoke test, not a campaign: that +//! is `scripts/fuzz.sh`. A failure here reproduces exactly, since the +//! inputs are the same every run. + +use std::fs; +use std::io; +use std::path::{Path, PathBuf}; + +use npro_core::random::SeededRandom; +use npro_fuzz::Target; + +/// Inputs drawn per target. Miri interprets the tests, thousands of times +/// slower, so it gets a taste of each. +const DRAWN: usize = if cfg!(miri) { 24 } else { 4000 }; + +/// The longest drawn input. +const MAX_LEN: u64 = 300; + +fn repo() -> PathBuf { + Path::new(env!("CARGO_MANIFEST_DIR")).join("../..") +} + +/// The directories `scripts/fuzz.sh` gives libFuzzer as seeds for `t`. +fn seed_dirs(t: Target) -> Vec<PathBuf> { + let mut dirs = vec![repo().join("fuzz/seeds").join(t.name())]; + if t == Target::Transcript { + dirs.push(repo().join("crates/npro-test/transcripts")); + } + dirs +} + +/// The seed files in `dir`, sorted, leaving out its README. +fn seed_files(dir: &Path) -> io::Result<Vec<PathBuf>> { + let mut paths = fs::read_dir(dir)? + .map(|e| e.map(|e| e.path())) + .collect::<io::Result<Vec<_>>>()?; + paths.retain(|p| p.is_file() && p.extension().is_none_or(|x| x != "md")); + paths.sort(); + Ok(paths) +} + +/// A number below `n`, or 0 if `n` is. +fn below(r: &mut SeededRandom, n: u64) -> u64 { + r.next_u64().checked_rem(n).unwrap_or(0) +} + +/// An index below `n`, or 0 if `n` is. +fn index_below(r: &mut SeededRandom, n: usize) -> usize { + let n = u64::try_from(n).unwrap_or(u64::MAX); + usize::try_from(below(r, n)).unwrap_or(0) +} + +fn byte(r: &mut SeededRandom) -> u8 { + let [b, ..] = r.next_u64().to_le_bytes(); + b +} + +fn bytes(r: &mut SeededRandom) -> Vec<u8> { + let n = below(r, MAX_LEN.saturating_add(1)); + (0..n).map(|_| byte(r)).collect() +} + +/// Text: random bytes are almost never UTF-8, so draw characters from +/// every length of encoding, then sometimes break it the ways a peer +/// might: a changed byte, a lost byte, or the end cut off. +fn text(r: &mut SeededRandom) -> Vec<u8> { + let mut t = String::new(); + for _ in 0..below(r, 60) { + // the first code point needing 1, 2, 3 and 4 bytes past the top + let top = match below(r, 4) { + 0 => 0x80, + 1 => 0x800, + 2 => 0x1_0000, + _ => 0x11_0000, + }; + if let Some(c) = u32::try_from(below(r, top)).ok().and_then(char::from_u32) { + t.push(c); + } + } + let mut t = t.into_bytes(); + let at = index_below(r, t.len()); + match below(r, 4) { + 0 => { + let b = byte(r); + if let Some(x) = t.get_mut(at) { + *x = b; + } + } + 1 if at < t.len() => { + t.remove(at); + } + 2 => t.truncate(at), + _ => {} + } + t +} + +/// A seed with a few bytes changed, inserted or removed. +fn mutated(r: &mut SeededRandom, seed: &[u8]) -> Vec<u8> { + let mut m = seed.to_vec(); + for _ in 0..=below(r, 4) { + let at = index_below(r, m.len().saturating_add(1)); + match below(r, 3) { + 0 => { + let b = byte(r); + if let Some(x) = m.get_mut(at) { + *x = b; + } + } + 1 => m.insert(at, byte(r)), + _ if at < m.len() => { + m.remove(at); + } + _ => {} + } + } + m +} + +/// Runs `t` over its seeds, then over `DRAWN` inputs from `draw`. +fn smoke( + t: Target, + mut draw: impl FnMut(&mut SeededRandom, &[Vec<u8>]) -> Vec<u8>, +) -> io::Result<()> { + let mut seeds = Vec::new(); + for dir in seed_dirs(t) { + let files = seed_files(&dir)?; + if files.is_empty() { + return Err(io::Error::other(format!("no seeds in {}", dir.display()))); + } + for f in files { + seeds.push(fs::read(&f)?); + } + } + for s in &seeds { + t.run(s); + } + let mut r = SeededRandom::new(1); + for _ in 0..DRAWN { + t.run(&draw(&mut r, &seeds)); + } + Ok(()) +} + +/// A drawn input: the first byte is the target's choice of split, then +/// the bytes under test. +fn split_then(r: &mut SeededRandom, body: Vec<u8>) -> Vec<u8> { + let mut v = vec![byte(r)]; + v.extend(body); + v +} + +#[test] +fn utf8() { + smoke(Target::Utf8, |r, _| { + let body = text(r); + split_then(r, body) + }) + .unwrap(); +} + +#[test] +fn sha1() { + smoke(Target::Sha1, |r, _| { + let body = bytes(r); + split_then(r, body) + }) + .unwrap(); +} + +#[test] +fn base64() { + smoke(Target::Base64, |r, _| bytes(r)).unwrap(); +} + +#[test] +#[cfg_attr( + miri, + ignore = "hundreds of kilobytes of transcripts: native runs keep it" +)] +fn transcript() { + smoke(Target::Transcript, |r, seeds| { + let seed = seeds + .get(index_below(r, seeds.len())) + .map_or(&[][..], Vec::as_slice); + mutated(r, seed) + }) + .unwrap(); +} + +#[test] +fn every_target_has_a_smoke_test() { + // the tests above, by name: a new target needs its own + let tested = ["utf8", "sha1", "base64", "transcript"]; + assert_eq!(Target::ALL.map(Target::name), tested); +} diff --git a/deny.toml b/deny.toml index c798b00..d5f0f59 100644 --- a/deny.toml +++ b/deny.toml @@ -23,6 +23,9 @@ confidence-threshold = 0.9 [bans] multiple-versions = "deny" wildcards = "deny" +# path dependencies between the workspace's own crates: cargo deny allows +# these only in crates that are not published, such as npro-fuzz +allow-wildcard-paths = true # The bouncers at the door. Any crate in the graph that is not named here # fails the gate, however deep in the tree it sits: a crate admitted for one @@ -35,6 +38,7 @@ allow = [ # the workspace "npro", "npro-core", + "npro-fuzz", "npro-test", # admitted dependencies, each with its entry in docs/dependencies.md: diff --git a/fuzz/seeds/README.md b/fuzz/seeds/README.md new file mode 100644 index 0000000..a7fdc31 --- /dev/null +++ b/fuzz/seeds/README.md @@ -0,0 +1,19 @@ +# Fuzz seeds + +One directory per fuzz target, named as the target is (`npro_fuzz::Target`). +Every file in it is one input libFuzzer starts its corpus from, and one the +smoke tests in `crates/npro-fuzz/tests/smoke.rs` run on every `cargo test`. +Files named `*.md` are left out of both. + +Where a target splits its input to feed it in pieces, as `utf8` and `sha1` +do, the **first byte chooses the split** and the bytes under test follow it. +A seed for those targets starts with that byte; any value will do. + +The `transcript` target also starts from every transcript in +`crates/npro-test/transcripts/`. + +Seeds are small and readable on purpose: each is a case worth starting from, +named for what it is. What the fuzzer finds goes in its corpus, which is +kept between runs, not here. When the protocol crates arrive, their +targets' seeds come from the C library's corpora (`fuzz/fuzz-*/seeds` in +the C tree), copied with where they came from. diff --git a/fuzz/seeds/base64/foobar b/fuzz/seeds/base64/foobar new file mode 100644 index 0000000..f6ea049 --- /dev/null +++ b/fuzz/seeds/base64/foobar @@ -0,0 +1 @@ +foobar \ No newline at end of file diff --git a/fuzz/seeds/base64/ws-key b/fuzz/seeds/base64/ws-key new file mode 100644 index 0000000..b66efb8 Binary files /dev/null and b/fuzz/seeds/base64/ws-key differ diff --git a/fuzz/seeds/sha1/abc b/fuzz/seeds/sha1/abc new file mode 100644 index 0000000..b8a9906 Binary files /dev/null and b/fuzz/seeds/sha1/abc differ diff --git a/fuzz/seeds/sha1/two-blocks b/fuzz/seeds/sha1/two-blocks new file mode 100644 index 0000000..eea2c06 --- /dev/null +++ b/fuzz/seeds/sha1/two-blocks @@ -0,0 +1 @@ +�abcdbcdecdefdefgefghfghighijhijkijkljklmklmnlmnomnopnopq \ No newline at end of file diff --git a/fuzz/seeds/sha1/ws-accept b/fuzz/seeds/sha1/ws-accept new file mode 100644 index 0000000..233eb9f --- /dev/null +++ b/fuzz/seeds/sha1/ws-accept @@ -0,0 +1 @@ +�dGhlIHNhbXBsZSBub25jZQ==258EAFA5-E914-47DA-95CA-C5AB0DC85B11 \ No newline at end of file diff --git a/fuzz/seeds/transcript/minimal b/fuzz/seeds/transcript/minimal new file mode 100644 index 0000000..1fe26f3 --- /dev/null +++ b/fuzz/seeds/transcript/minimal @@ -0,0 +1 @@ +{"format": "lws-transcript/1", "case": "x", "side": "client", "t0_us": 1000000000, "t0_wall": 1767225600, "seed": 0, "steps": [{"t": 1000, "tx": "4745"}, {"t": 2000, "close": ""}]} diff --git a/fuzz/seeds/utf8/ascii b/fuzz/seeds/utf8/ascii new file mode 100644 index 0000000..6c126ea Binary files /dev/null and b/fuzz/seeds/utf8/ascii differ diff --git a/fuzz/seeds/utf8/every-length b/fuzz/seeds/utf8/every-length new file mode 100644 index 0000000..0ffbae7 --- /dev/null +++ b/fuzz/seeds/utf8/every-length @@ -0,0 +1 @@ +�Aé€😀 中文 नमसà¥�ते \ No newline at end of file diff --git a/fuzz/seeds/utf8/limits b/fuzz/seeds/utf8/limits new file mode 100644 index 0000000..1634f2b --- /dev/null +++ b/fuzz/seeds/utf8/limits @@ -0,0 +1 @@ + €߿ࠀ퟿￿ð�€€ô�¿¿ \ No newline at end of file diff --git a/fuzz/seeds/utf8/overlong b/fuzz/seeds/utf8/overlong new file mode 100644 index 0000000..ffc0817 --- /dev/null +++ b/fuzz/seeds/utf8/overlong @@ -0,0 +1 @@ +�À¯à€¯ð€€¯ \ No newline at end of file diff --git a/fuzz/seeds/utf8/partial b/fuzz/seeds/utf8/partial new file mode 100644 index 0000000..1945141 --- /dev/null +++ b/fuzz/seeds/utf8/partial @@ -0,0 +1 @@ +�â‚ \ No newline at end of file diff --git a/fuzz/seeds/utf8/past-max b/fuzz/seeds/utf8/past-max new file mode 100644 index 0000000..f7446ed --- /dev/null +++ b/fuzz/seeds/utf8/past-max @@ -0,0 +1 @@ +�ô�€€ \ No newline at end of file diff --git a/fuzz/seeds/utf8/surrogate b/fuzz/seeds/utf8/surrogate new file mode 100644 index 0000000..a3d8b58 --- /dev/null +++ b/fuzz/seeds/utf8/surrogate @@ -0,0 +1 @@ +�í € \ No newline at end of file
Page fetched 0s ago, creation time: 3ms (vhost etag hits: 0%, cache hits: 0%)