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 / src / expect / READMEs / sai-build-test-flow.png
Author[]Andy Green <andy@warmcat.com> 2026-10-04 20:21 UTC
Committer[]Andy Green <andy@warmcat.com> 2026-10-05 06:32 UTC
Treeaaed2432255247b98f95b57e9f42c8a56a31b568   Raw Patch
 
fuzz: the h1 parser as server and client, and the dechunker
fuzz: the h1 parser as server and client, and the dechunker

Three targets, each with an oracle, as the port plan's phase 1c asks:

 - h1-request, npro_h1::head as a server, and h1-response, as a client.
   The first byte chooses how the rest is split and, with its top bit, a
   256 byte table with token limits, so libFuzzer reaches the limits.  A
   head in one piece and in pieces must come to the same verdict and leave
   the same table; a refused head must stay refused; no value may hold a
   CR, LF or NUL; and a complete request's path from / must be normal, no
   //, /./ or /../ step and no /. or /.. at its end, which is what C's
   decoder is for.

 - chunked, npro_h1::chunked::Dechunk, against a second reading of RFC 9112
   7.1 with C's bounds, written apart from the decoder, over the whole
   body by index: the same data, ending at the same byte, or refused, in
   one piece and in pieces.

What these cannot see, a verdict wrong the same way whole and in pieces,
is what npro-test's h1_c test against C is for.

The seeds are C's fuzz/fuzz-h1/seeds, behind a control byte of 0 and
named as there, and some of npro-test's h1 corpus, a tight-* seed with
the top bit set.  regress-refused-mid-dot.http is the one finding of the
first run, in the oracle: it held a refused head's half-built path to
the rule for complete ones.  Five minutes of each since found nothing,
in 0.9M, 0.4M and 7.1M runs.

Planted bugs fail the smoke tests: the dechunker's bound one less (once
a seed sits on it, skip-at-bound.txt), and // no longer swallowed.

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 857bf23..9be86bd 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -15,6 +15,7 @@ name = "npro-fuzz" version = "0.0.2" dependencies = [ "npro-core", + "npro-h1", "npro-test", ] diff --git a/crates/npro-fuzz/Cargo.toml b/crates/npro-fuzz/Cargo.toml index c3450b1..776a308 100644 --- a/crates/npro-fuzz/Cargo.toml +++ b/crates/npro-fuzz/Cargo.toml @@ -11,6 +11,7 @@ repository.workspace = true [dependencies] npro-core = { path = "../npro-core" } +npro-h1 = { path = "../npro-h1" } npro-test = { path = "../npro-test" } [dev-dependencies] diff --git a/crates/npro-fuzz/src/h1.rs b/crates/npro-fuzz/src/h1.rs new file mode 100644 index 0000000..558c2e9 --- /dev/null +++ b/crates/npro-fuzz/src/h1.rs @@ -0,0 +1,378 @@ +//! The h1 targets: the head parser as a server and as a client, and the +//! dechunker. + +use core::num::NonZeroU16; + +use npro_h1::chunked::{Chunk, Dechunk, SKIP_MAX}; +use npro_h1::head::{Config, Head, Progress, Refused, Side, UnknownMethod}; +use npro_h1::table::{DEFAULT_CAPACITY, HeaderTable}; +use npro_h1::token::Token; + +use crate::targets::{Pieces, control, finding}; + +/// The configuration the input's control byte chooses: C's defaults, or a +/// small table with token limits and unknown methods falling back, so +/// limits are reached by inputs libFuzzer can grow to. +fn config(ctl: u8) -> (usize, Config) { + if ctl & 0x80 == 0 { + return (DEFAULT_CAPACITY, Config::new()); + } + let limit = |n| NonZeroU16::new(n).unwrap_or(NonZeroU16::MIN); + ( + 256, + Config::new() + .with_limit(Token::GetUri, limit(33)) + .with_limit(Token::UserAgent, limit(16)) + .with_limit(Token::Host, limit(24)) + .with_limit(Token::Cookie, limit(48)) + .with_unknown_method(UnknownMethod::Fallback), + ) +} + +/// Everything a parsed head is: the verdict, and the table it left. +#[derive(Debug, PartialEq, Eq)] +struct Parsed { + verdict: Result<Progress, Refused>, + used: usize, + tokens: Vec<(Token, Vec<Vec<u8>>)>, + unknown: Vec<(Vec<u8>, Vec<u8>)>, +} + +fn parsed(h: &Head<Vec<u8>>, verdict: Result<Progress, Refused>) -> Parsed { + let t = h.table(); + Parsed { + verdict, + used: t.used(), + tokens: Token::ALL + .into_iter() + .filter(|tok| t.is_present(*tok)) + .map(|tok| (tok, t.fragments(tok).map(<[u8]>::to_vec).collect())) + .collect(), + unknown: t + .unknown_headers() + .map(|(n, v)| (n.to_vec(), v.to_vec())) + .collect(), + } +} + +fn head(target: &str, side: Side, cap: usize, config: Config) -> Head<Vec<u8>> { + let mut table = HeaderTable::new(vec![0u8; cap]) + .unwrap_or_else(|e| finding(target, format_args!("a table of {cap}: {e}"))); + if side == Side::Client { + // a client's table holds its own request first + for (t, v) in [(Token::ClientUri, &b"/x"[..]), (Token::ClientHost, b"h")] { + if let Err(e) = table.create(t, v) { + finding(target, format_args!("client's own {t:?}: {e}")); + } + } + } + Head::with_table(table, side, config) +} + +/// The head given in one piece. +fn whole(target: &str, side: Side, ctl: u8, bytes: &[u8]) -> Parsed { + let (cap, config) = config(ctl); + let mut h = head(target, side, cap, config); + let verdict = if bytes.is_empty() { + Ok(Progress::More) + } else { + h.rx(bytes) + }; + // a refused head stays refused, saying the same + if let Err(r) = verdict { + if h.rx(b"\r\n\r\n") != Err(r) { + finding(target, format_args!("refused with {r}, then not")); + } + } + parsed(&h, verdict) +} + +/// The head given in pieces. +fn in_pieces(target: &str, side: Side, ctl: u8, bytes: &[u8]) -> Parsed { + let (cap, config) = config(ctl); + let mut h = head(target, side, cap, config); + let mut verdict = Ok(Progress::More); + let mut at = 0usize; + for piece in Pieces::new(ctl, bytes) { + verdict = h.rx(piece).map(|p| match p { + Progress::Complete { consumed } => Progress::Complete { + consumed: consumed.saturating_add(at), + }, + Progress::More | Progress::Fallback => p, + }); + if verdict != Ok(Progress::More) { + break; + } + at = at.saturating_add(piece.len()); + } + parsed(&h, verdict) +} + +/// What every parsed head must be, whatever the bytes were. +fn check(target: &str, p: &Parsed, len: usize, cap: usize) { + if let Ok(Progress::Complete { consumed }) = p.verdict { + if consumed == 0 || consumed > len { + finding(target, format_args!("consumed {consumed} of {len}")); + } + } + if p.used > cap { + finding(target, format_args!("used {} of {cap}", p.used)); + } + // the bytes that end a line, or every value, are never in one + let bad = |v: &[u8]| v.iter().any(|c| matches!(c, b'\r' | b'\n' | 0)); + for (t, frags) in &p.tokens { + if frags.iter().any(|f| bad(f)) { + finding(target, format_args!("{t:?} has a CR, LF or NUL: {frags:?}")); + } + } + for (n, v) in &p.unknown { + if bad(v) || n.contains(&0) { + finding(target, format_args!("unknown {n:?} = {v:?}")); + } + } + // a complete request's path is never above its root, nor has a step + // that is not one (a refused head's is wherever it stopped, and goes + // nowhere) + if !matches!(p.verdict, Ok(Progress::Complete { .. })) { + return; + } + for (t, frags) in &p.tokens { + if !t.is_method() { + continue; + } + for path in frags.iter().filter(|f| f.first() == Some(&b'/')) { + let steps = [&b"//"[..], b"/./", b"/../"]; + if steps.iter().any(|s| path.windows(s.len()).any(|w| w == *s)) + || path.ends_with(b"/.") + || path.ends_with(b"/..") + { + finding( + target, + format_args!("{t:?} path {} is not normal", path.escape_ascii()), + ); + } + } + } +} + +fn heads(target: &str, side: Side, data: &[u8]) { + let (ctl, bytes) = control(data); + let one = whole(target, side, ctl, bytes); + check(target, &one, bytes.len(), config(ctl).0); + let pieces = in_pieces(target, side, ctl, bytes); + if pieces != one { + finding( + target, + format_args!("in one piece:\n{one:?}\nin pieces:\n{pieces:?}"), + ); + } +} + +/// A request head as a server parses it. +/// +/// The first byte chooses how the rest is split, and with its top bit, a +/// small table with token limits. In one piece and in pieces, the head +/// must come to the same verdict and leave the same table; a refused head +/// must stay refused; no value may hold a CR, LF or NUL; and a request +/// path from the root must be normal, with no `//`, `/./` or `/../` step +/// and no `/.` or `/..` at its end. +pub fn h1_request(data: &[u8]) { + heads("h1-request", Side::Server, data); +} + +/// A response head as a client parses it: as [`h1_request`], from the +/// client's side. +pub fn h1_response(data: &[u8]) { + heads("h1-response", Side::Client, data); +} + +/// What a chunked body is: its data, and where it ended, or that it is not +/// over, or that it cannot be framed. +#[derive(Debug, PartialEq, Eq)] +enum Body { + End { consumed: usize, data: Vec<u8> }, + More { data: Vec<u8> }, + Refused, +} + +/// The oracle: RFC 9112 7.1's grammar with C's bounds, read off the whole +/// body at once, by index, written apart from the decoder. +struct Reference<'a> { + body: &'a [u8], + at: usize, + skipped: u16, + data: Vec<u8>, +} + +/// Why the reference stopped: the body ended, or it is refused. +enum Stop { + Short, + Refused, +} + +impl Reference<'_> { + fn peek(&self) -> Result<u8, Stop> { + self.body.get(self.at).copied().ok_or(Stop::Short) + } + + fn take(&mut self) -> Result<u8, Stop> { + let c = self.peek()?; + self.at = self.at.saturating_add(1); + Ok(c) + } + + fn expect(&mut self, want: u8) -> Result<(), Stop> { + if self.take()? == want { + Ok(()) + } else { + Err(Stop::Refused) + } + } + + const fn skip(&mut self) -> Result<(), Stop> { + self.skipped = self.skipped.saturating_add(1); + if self.skipped > SKIP_MAX { + return Err(Stop::Refused); + } + Ok(()) + } + + /// The bytes of an extension or a trailer line, to the CR its line + /// ends with, each counted. A LF before it is refused. + fn skip_line(&mut self) -> Result<(), Stop> { + loop { + match self.peek()? { + b'\r' => return Ok(()), + b'\n' => return Err(Stop::Refused), + _ => { + self.skip()?; + self.at = self.at.saturating_add(1); + } + } + } + } + + fn chunk_size(&mut self) -> Result<u32, Stop> { + let mut size = 0u32; + let mut digits = 0usize; + while let Some(d) = char::from(self.peek()?).to_digit(16) { + if size > (0x7fff_ffff - 15) / 16 { + return Err(Stop::Refused); + } + size = (size << 4) | d; + digits = digits.saturating_add(1); + self.at = self.at.saturating_add(1); + } + if digits == 0 { + return Err(Stop::Refused); + } + Ok(size) + } + + fn body(&mut self) -> Result<usize, Stop> { + loop { + let size = self.chunk_size()?; + match self.peek()? { + b'\r' => {} + b';' | b' ' | b'\t' => self.skip_line()?, + _ => return Err(Stop::Refused), + } + self.expect(b'\r')?; + self.expect(b'\n')?; + if size == 0 { + return self.trailers(); + } + let want = usize::try_from(size).unwrap_or(usize::MAX); + let rest = self.body.get(self.at..).unwrap_or_default(); + let got = rest.get(..want.min(rest.len())).unwrap_or_default(); + self.data.extend_from_slice(got); + self.at = self.at.saturating_add(got.len()); + if got.len() < want { + return Err(Stop::Short); + } + self.expect(b'\r')?; + self.expect(b'\n')?; + } + } + + fn trailers(&mut self) -> Result<usize, Stop> { + loop { + if self.peek()? == b'\r' { + self.at = self.at.saturating_add(1); + self.expect(b'\n')?; + return Ok(self.at); + } + self.skip_line()?; + // the CR ending a trailer line counts, its LF does not + self.skip()?; + self.at = self.at.saturating_add(1); + self.expect(b'\n')?; + } + } +} + +fn reference(body: &[u8]) -> Body { + let mut r = Reference { + body, + at: 0, + skipped: 0, + data: Vec::new(), + }; + match r.body() { + Ok(consumed) => Body::End { + consumed, + data: r.data, + }, + Err(Stop::Short) => Body::More { data: r.data }, + Err(Stop::Refused) => Body::Refused, + } +} + +/// The decoder, handed the body in the pieces `pieces` gives. +fn decoded<'a>(pieces: impl Iterator<Item = &'a [u8]>) -> Body { + let mut d = Dechunk::new(); + let (mut data, mut at) = (Vec::new(), 0usize); + for piece in pieces { + let mut rest = piece; + loop { + let Ok(step) = d.step(rest) else { + return Body::Refused; + }; + at = at.saturating_add(step.consumed); + rest = rest.get(step.consumed..).unwrap_or_default(); + match step.chunk { + Chunk::Data(b) => data.extend_from_slice(b), + Chunk::End => return Body::End { consumed: at, data }, + Chunk::More => break, + } + if rest.is_empty() { + break; + } + } + } + Body::More { data } +} + +/// A chunked body, as a server's request or a client's response has. +/// +/// The first byte chooses how the rest is split. Decoded in one piece and +/// in pieces, it must be what a second reading of RFC 9112 7.1 with C's +/// bounds makes of it: the same data, ending at the same byte, or refused. +pub fn chunked(data: &[u8]) { + let (ctl, body) = control(data); + let want = reference(body); + let one = decoded(core::iter::once(body)); + if one != want { + finding( + "chunked", + format_args!("in one piece {one:?}, the grammar says {want:?}"), + ); + } + let pieces = decoded(Pieces::new(ctl, body)); + if pieces != want { + finding( + "chunked", + format_args!("in pieces {pieces:?}, the grammar says {want:?}"), + ); + } +} diff --git a/crates/npro-fuzz/src/lib.rs b/crates/npro-fuzz/src/lib.rs index 82a53f6..bba862e 100644 --- a/crates/npro-fuzz/src/lib.rs +++ b/crates/npro-fuzz/src/lib.rs @@ -20,8 +20,10 @@ #![forbid(unsafe_code)] +mod h1; mod targets; +pub use h1::{chunked, h1_request, h1_response}; pub use targets::{base64, sha1, transcript, utf8}; /// A fuzz target: its name is the libFuzzer target's, the seed directory's @@ -45,11 +47,25 @@ pub enum Target { Base64, /// [`transcript`]: npro-test's transcript reader. Transcript, + /// [`h1_request`]: the h1 head parser, as a server. + H1Request, + /// [`h1_response`]: the h1 head parser, as a client. + H1Response, + /// [`chunked`]: the chunked transfer coding's decoder. + Chunked, } impl Target { /// Every target. - pub const ALL: [Self; 4] = [Self::Utf8, Self::Sha1, Self::Base64, Self::Transcript]; + pub const ALL: [Self; 7] = [ + Self::Utf8, + Self::Sha1, + Self::Base64, + Self::Transcript, + Self::H1Request, + Self::H1Response, + Self::Chunked, + ]; /// The target's name. #[must_use] @@ -59,6 +75,9 @@ impl Target { Self::Sha1 => "sha1", Self::Base64 => "base64", Self::Transcript => "transcript", + Self::H1Request => "h1-request", + Self::H1Response => "h1-response", + Self::Chunked => "chunked", } } @@ -74,6 +93,9 @@ impl Target { Self::Sha1 => sha1(data), Self::Base64 => base64(data), Self::Transcript => transcript(data), + Self::H1Request => h1_request(data), + Self::H1Response => h1_response(data), + Self::Chunked => chunked(data), } } } diff --git a/crates/npro-fuzz/src/targets.rs b/crates/npro-fuzz/src/targets.rs index 3f014ac..db10119 100644 --- a/crates/npro-fuzz/src/targets.rs +++ b/crates/npro-fuzz/src/targets.rs @@ -15,26 +15,26 @@ use npro_test::{MAX_BYTES, MAX_STEPS, Transcript}; reason = "a panic is how a fuzz target reports a finding to libFuzzer" )] #[cold] -fn finding(target: &str, what: fmt::Arguments<'_>) -> ! { +pub(crate) 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]) { +pub(crate) 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> { +pub(crate) struct Pieces<'a> { rest: &'a [u8], state: u32, } impl<'a> Pieces<'a> { - fn new(ctl: u8, bytes: &'a [u8]) -> Self { + pub(crate) fn new(ctl: u8, bytes: &'a [u8]) -> Self { Self { rest: bytes, state: u32::from(ctl), diff --git a/crates/npro-fuzz/tests/smoke.rs b/crates/npro-fuzz/tests/smoke.rs index a9fcbdd..230cf2e 100644 --- a/crates/npro-fuzz/tests/smoke.rs +++ b/crates/npro-fuzz/tests/smoke.rs @@ -195,9 +195,48 @@ fn transcript() { .unwrap(); } +/// A seed, sometimes changed, after a split byte which may also choose the +/// target's configuration. +fn seeded(r: &mut SeededRandom, seeds: &[Vec<u8>]) -> Vec<u8> { + let seed = seeds + .get(index_below(r, seeds.len())) + .map_or(&[][..], Vec::as_slice); + // past the seed's own control byte + let body = seed.get(1..).unwrap_or_default(); + let body = if below(r, 4) == 0 { + body.to_vec() + } else { + mutated(r, body) + }; + split_then(r, body) +} + +#[test] +fn h1_request() { + smoke(Target::H1Request, seeded).unwrap(); +} + +#[test] +fn h1_response() { + smoke(Target::H1Response, seeded).unwrap(); +} + +#[test] +fn chunked() { + smoke(Target::Chunked, seeded).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"]; + let tested = [ + "utf8", + "sha1", + "base64", + "transcript", + "h1-request", + "h1-response", + "chunked", + ]; assert_eq!(Target::ALL.map(Target::name), tested); } diff --git a/docs/fuzzing.md b/docs/fuzzing.md index 249397d..7d2e732 100644 --- a/docs/fuzzing.md +++ b/docs/fuzzing.md @@ -22,13 +22,18 @@ Fuzzing runs in three places: | `sha1` | `npro_core::sha1::Sha1`, for the ws accept | the digest of the message fed in pieces equals the digest of it in one go | | `base64` | `npro_core::base64::encode` | a strict decoder in the harness gets the input back; exactly `encoded_len` bytes are used and no more written; a buffer one byte short, or empty, is refused with nothing written | | `transcript` | npro-test's transcript reader | whatever it accepts keeps its limits, and step times never go backwards | +| `h1-request` | `npro_h1::head` as a server | the head in one piece and in pieces comes to the same verdict and leaves the same table; a refused head stays refused; no value holds a CR, LF or NUL; a complete request's path from `/` has no `//`, `/./` or `/../` step and no `/.` or `/..` at its end. The top bit of the first byte picks a 256 byte table with token limits, so limits are within reach | +| `h1-response` | `npro_h1::head` as a client | as `h1-request` | +| `chunked` | `npro_h1::chunked::Dechunk` | a second reading of RFC 9112 7.1 with C's bounds, written apart from the decoder: the same data, ending at the same byte, or refused, in one piece and in pieces | A target that splits its input to feed it in pieces takes the split from the input's first byte, so libFuzzer explores the split like the rest of the input. -The h1 parser's targets come next, in phase 1c of -[port-plan.md](port-plan.md), seeded from the C library's corpora. +What the h1 targets cannot see, a verdict that is wrong the same way +whole and in pieces, is what `crates/npro-test/tests/h1_c.rs` checks: +npro's parser against C's over thousands of heads +([its README](../crates/npro-test/h1/README.md)). ## Where things are diff --git a/fuzz/Cargo.lock b/fuzz/Cargo.lock index 7da7fcb..40c918f 100644 --- a/fuzz/Cargo.lock +++ b/fuzz/Cargo.lock @@ -78,6 +78,7 @@ name = "npro-fuzz" version = "0.0.2" dependencies = [ "npro-core", + "npro-h1", "npro-test", ] @@ -90,6 +91,10 @@ dependencies = [ ] [[package]] +name = "npro-h1" +version = "0.0.2" + +[[package]] name = "npro-test" version = "0.0.2" diff --git a/fuzz/Cargo.toml b/fuzz/Cargo.toml index c767bba..1757568 100644 --- a/fuzz/Cargo.toml +++ b/fuzz/Cargo.toml @@ -47,6 +47,27 @@ test = false doc = false bench = false +[[bin]] +name = "h1-request" +path = "fuzz_targets/h1_request.rs" +test = false +doc = false +bench = false + +[[bin]] +name = "h1-response" +path = "fuzz_targets/h1_response.rs" +test = false +doc = false +bench = false + +[[bin]] +name = "chunked" +path = "fuzz_targets/chunked.rs" +test = false +doc = false +bench = false + [lints.rust] unsafe_code = "forbid" unexpected_cfgs = "deny" diff --git a/fuzz/deny.toml b/fuzz/deny.toml index 03379ae..2d4afe2 100644 --- a/fuzz/deny.toml +++ b/fuzz/deny.toml @@ -35,6 +35,7 @@ allow = [ # the fuzz targets, and the npro crates they drive "npro-fuzz-targets", "npro-fuzz", + "npro-h1", "npro-core", "npro-test", diff --git a/fuzz/fuzz_targets/chunked.rs b/fuzz/fuzz_targets/chunked.rs new file mode 100644 index 0000000..8d114b9 --- /dev/null +++ b/fuzz/fuzz_targets/chunked.rs @@ -0,0 +1,5 @@ +//! libFuzzer target for [`npro_fuzz::Target::Chunked`]. + +#![no_main] + +libfuzzer_sys::fuzz_target!(|data: &[u8]| npro_fuzz::Target::Chunked.run(data)); diff --git a/fuzz/fuzz_targets/h1_request.rs b/fuzz/fuzz_targets/h1_request.rs new file mode 100644 index 0000000..65e45f3 --- /dev/null +++ b/fuzz/fuzz_targets/h1_request.rs @@ -0,0 +1,5 @@ +//! libFuzzer target for [`npro_fuzz::Target::H1Request`]. + +#![no_main] + +libfuzzer_sys::fuzz_target!(|data: &[u8]| npro_fuzz::Target::H1Request.run(data)); diff --git a/fuzz/fuzz_targets/h1_response.rs b/fuzz/fuzz_targets/h1_response.rs new file mode 100644 index 0000000..f7e4ff9 --- /dev/null +++ b/fuzz/fuzz_targets/h1_response.rs @@ -0,0 +1,5 @@ +//! libFuzzer target for [`npro_fuzz::Target::H1Response`]. + +#![no_main] + +libfuzzer_sys::fuzz_target!(|data: &[u8]| npro_fuzz::Target::H1Response.run(data)); diff --git a/fuzz/seeds/.gitattributes b/fuzz/seeds/.gitattributes new file mode 100644 index 0000000..8e4df57 --- /dev/null +++ b/fuzz/seeds/.gitattributes @@ -0,0 +1,5 @@ +# inputs as they go on the wire, CRLFs and all: no tool may change their +# line ends, and a patch carries them as bytes +h1-request/** binary +h1-response/** binary +chunked/** binary diff --git a/fuzz/seeds/README.md b/fuzz/seeds/README.md index 0604bac..2432fc5 100644 --- a/fuzz/seeds/README.md +++ b/fuzz/seeds/README.md @@ -14,6 +14,16 @@ The `transcript` target also starts from every transcript in 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 ([docs/fuzzing.md](../../docs/fuzzing.md)). 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. +kept between runs, not here ([docs/fuzzing.md](../../docs/fuzzing.md)). +The protocol crates' targets start from the C library's corpora +(`fuzz/fuzz-*/seeds` in the C tree), copied with where they came from: + +- `h1-request`: C's `fuzz/fuzz-h1/seeds`, each behind a control byte of + 0, named as there (`absuri.http`, `get.http`...), and some of + `crates/npro-test/h1/requests/`, named as there; +- `h1-response` and `chunked`: some of `crates/npro-test/h1/responses/` + and `chunked/`. + +A `tight-*` seed's control byte has its top bit set, choosing the small +table with token limits. A seed named `regress-*` is an input the fuzzer +once failed on, kept so it is tried every run, as in C. diff --git a/fuzz/seeds/chunked/ext-and-trailers.txt b/fuzz/seeds/chunked/ext-and-trailers.txt new file mode 100644 index 0000000..ab69d41 Binary files /dev/null and b/fuzz/seeds/chunked/ext-and-trailers.txt differ diff --git a/fuzz/seeds/chunked/ext.txt b/fuzz/seeds/chunked/ext.txt new file mode 100644 index 0000000..1dc4c12 Binary files /dev/null and b/fuzz/seeds/chunked/ext.txt differ diff --git a/fuzz/seeds/chunked/leading-zeros.txt b/fuzz/seeds/chunked/leading-zeros.txt new file mode 100644 index 0000000..6ea3757 Binary files /dev/null and b/fuzz/seeds/chunked/leading-zeros.txt differ diff --git a/fuzz/seeds/chunked/one.txt b/fuzz/seeds/chunked/one.txt new file mode 100644 index 0000000..cc8b17e Binary files /dev/null and b/fuzz/seeds/chunked/one.txt differ diff --git a/fuzz/seeds/chunked/size-at-limit.txt b/fuzz/seeds/chunked/size-at-limit.txt new file mode 100644 index 0000000..37792c7 Binary files /dev/null and b/fuzz/seeds/chunked/size-at-limit.txt differ diff --git a/fuzz/seeds/chunked/skip-at-bound.txt b/fuzz/seeds/chunked/skip-at-bound.txt new file mode 100644 index 0000000..a02d87f Binary files /dev/null and b/fuzz/seeds/chunked/skip-at-bound.txt differ diff --git a/fuzz/seeds/chunked/trailers.txt b/fuzz/seeds/chunked/trailers.txt new file mode 100644 index 0000000..408d789 Binary files /dev/null and b/fuzz/seeds/chunked/trailers.txt differ diff --git a/fuzz/seeds/chunked/two.txt b/fuzz/seeds/chunked/two.txt new file mode 100644 index 0000000..8aaab0f Binary files /dev/null and b/fuzz/seeds/chunked/two.txt differ diff --git a/fuzz/seeds/chunked/unfinished.txt b/fuzz/seeds/chunked/unfinished.txt new file mode 100644 index 0000000..65191e3 Binary files /dev/null and b/fuzz/seeds/chunked/unfinished.txt differ diff --git a/fuzz/seeds/h1-request/absuri.http b/fuzz/seeds/h1-request/absuri.http new file mode 100644 index 0000000..27d127c Binary files /dev/null and b/fuzz/seeds/h1-request/absuri.http differ diff --git a/fuzz/seeds/h1-request/chunked.http b/fuzz/seeds/h1-request/chunked.http new file mode 100644 index 0000000..0b6131c Binary files /dev/null and b/fuzz/seeds/h1-request/chunked.http differ diff --git a/fuzz/seeds/h1-request/duphdrs.http b/fuzz/seeds/h1-request/duphdrs.http new file mode 100644 index 0000000..070408d Binary files /dev/null and b/fuzz/seeds/h1-request/duphdrs.http differ diff --git a/fuzz/seeds/h1-request/get.http b/fuzz/seeds/h1-request/get.http new file mode 100644 index 0000000..a1042da Binary files /dev/null and b/fuzz/seeds/h1-request/get.http differ diff --git a/fuzz/seeds/h1-request/headers-bare-cr.http b/fuzz/seeds/h1-request/headers-bare-cr.http new file mode 100644 index 0000000..e2b7202 Binary files /dev/null and b/fuzz/seeds/h1-request/headers-bare-cr.http differ diff --git a/fuzz/seeds/h1-request/headers-many.http b/fuzz/seeds/h1-request/headers-many.http new file mode 100644 index 0000000..0216fdc Binary files /dev/null and b/fuzz/seeds/h1-request/headers-many.http differ diff --git a/fuzz/seeds/h1-request/headers-repeated.http b/fuzz/seeds/h1-request/headers-repeated.http new file mode 100644 index 0000000..2fd66a6 Binary files /dev/null and b/fuzz/seeds/h1-request/headers-repeated.http differ diff --git a/fuzz/seeds/h1-request/headers-unknown.http b/fuzz/seeds/h1-request/headers-unknown.http new file mode 100644 index 0000000..a6f8072 Binary files /dev/null and b/fuzz/seeds/h1-request/headers-unknown.http differ diff --git a/fuzz/seeds/h1-request/leading-empty-2.http b/fuzz/seeds/h1-request/leading-empty-2.http new file mode 100644 index 0000000..01256f6 Binary files /dev/null and b/fuzz/seeds/h1-request/leading-empty-2.http differ diff --git a/fuzz/seeds/h1-request/longvalue.http b/fuzz/seeds/h1-request/longvalue.http new file mode 100644 index 0000000..f812f10 Binary files /dev/null and b/fuzz/seeds/h1-request/longvalue.http differ diff --git a/fuzz/seeds/h1-request/method-unknown.http b/fuzz/seeds/h1-request/method-unknown.http new file mode 100644 index 0000000..847964c Binary files /dev/null and b/fuzz/seeds/h1-request/method-unknown.http differ diff --git a/fuzz/seeds/h1-request/post-cl.http b/fuzz/seeds/h1-request/post-cl.http new file mode 100644 index 0000000..588d91a Binary files /dev/null and b/fuzz/seeds/h1-request/post-cl.http differ diff --git a/fuzz/seeds/h1-request/regress-refused-mid-dot.http b/fuzz/seeds/h1-request/regress-refused-mid-dot.http new file mode 100644 index 0000000..4e7acd1 Binary files /dev/null and b/fuzz/seeds/h1-request/regress-refused-mid-dot.http differ diff --git a/fuzz/seeds/h1-request/tight-limits.http b/fuzz/seeds/h1-request/tight-limits.http new file mode 100644 index 0000000..5f39359 --- /dev/null +++ b/fuzz/seeds/h1-request/tight-limits.http @@ -0,0 +1,4 @@ +€GET / HTTP/1.1 +User-Agent: uuuuuuuuuuuuuuuuuuuuuuuuuuuuuuuuuuuuuuuu +X-Big: vvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvv + diff --git a/fuzz/seeds/h1-request/upgrade-h2c.http b/fuzz/seeds/h1-request/upgrade-h2c.http new file mode 100644 index 0000000..cbfd956 Binary files /dev/null and b/fuzz/seeds/h1-request/upgrade-h2c.http differ diff --git a/fuzz/seeds/h1-request/upgrade-ws-nokey.http b/fuzz/seeds/h1-request/upgrade-ws-nokey.http new file mode 100644 index 0000000..d5ad1e8 Binary files /dev/null and b/fuzz/seeds/h1-request/upgrade-ws-nokey.http differ diff --git a/fuzz/seeds/h1-request/upgrade-ws.http b/fuzz/seeds/h1-request/upgrade-ws.http new file mode 100644 index 0000000..d91a523 Binary files /dev/null and b/fuzz/seeds/h1-request/upgrade-ws.http differ diff --git a/fuzz/seeds/h1-request/uri-args-many.http b/fuzz/seeds/h1-request/uri-args-many.http new file mode 100644 index 0000000..b10b4b2 Binary files /dev/null and b/fuzz/seeds/h1-request/uri-args-many.http differ diff --git a/fuzz/seeds/h1-request/uri-dotdot-deep.http b/fuzz/seeds/h1-request/uri-dotdot-deep.http new file mode 100644 index 0000000..f332872 Binary files /dev/null and b/fuzz/seeds/h1-request/uri-dotdot-deep.http differ diff --git a/fuzz/seeds/h1-request/uri-escapes.http b/fuzz/seeds/h1-request/uri-escapes.http new file mode 100644 index 0000000..58c9e1b Binary files /dev/null and b/fuzz/seeds/h1-request/uri-escapes.http differ diff --git a/fuzz/seeds/h1-request/version-2.http b/fuzz/seeds/h1-request/version-2.http new file mode 100644 index 0000000..84eda8c Binary files /dev/null and b/fuzz/seeds/h1-request/version-2.http differ diff --git a/fuzz/seeds/h1-response/bare-lf.http b/fuzz/seeds/h1-response/bare-lf.http new file mode 100644 index 0000000..67b7a9a Binary files /dev/null and b/fuzz/seeds/h1-response/bare-lf.http differ diff --git a/fuzz/seeds/h1-response/chunked.http b/fuzz/seeds/h1-response/chunked.http new file mode 100644 index 0000000..305c52e Binary files /dev/null and b/fuzz/seeds/h1-response/chunked.http differ diff --git a/fuzz/seeds/h1-response/http10.http b/fuzz/seeds/h1-response/http10.http new file mode 100644 index 0000000..4aceee3 Binary files /dev/null and b/fuzz/seeds/h1-response/http10.http differ diff --git a/fuzz/seeds/h1-response/interim.http b/fuzz/seeds/h1-response/interim.http new file mode 100644 index 0000000..b31fe71 Binary files /dev/null and b/fuzz/seeds/h1-response/interim.http differ diff --git a/fuzz/seeds/h1-response/odd-names.http b/fuzz/seeds/h1-response/odd-names.http new file mode 100644 index 0000000..d910790 Binary files /dev/null and b/fuzz/seeds/h1-response/odd-names.http differ diff --git a/fuzz/seeds/h1-response/ok.http b/fuzz/seeds/h1-response/ok.http new file mode 100644 index 0000000..d3454d3 Binary files /dev/null and b/fuzz/seeds/h1-response/ok.http differ diff --git a/fuzz/seeds/h1-response/set-cookies.http b/fuzz/seeds/h1-response/set-cookies.http new file mode 100644 index 0000000..1525c79 Binary files /dev/null and b/fuzz/seeds/h1-response/set-cookies.http differ diff --git a/fuzz/seeds/h1-response/switching.http b/fuzz/seeds/h1-response/switching.http new file mode 100644 index 0000000..7e405f2 Binary files /dev/null and b/fuzz/seeds/h1-response/switching.http differ diff --git a/fuzz/seeds/h1-response/tight-long.http b/fuzz/seeds/h1-response/tight-long.http new file mode 100644 index 0000000..2ce408a --- /dev/null +++ b/fuzz/seeds/h1-response/tight-long.http @@ -0,0 +1,3 @@ +€HTTP/1.1 200 OK +X-Big: vvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvv + diff --git a/fuzz/seeds/h1-response/unknown-headers.http b/fuzz/seeds/h1-response/unknown-headers.http new file mode 100644 index 0000000..f3d8aa7 Binary files /dev/null and b/fuzz/seeds/h1-response/unknown-headers.http differ
Page fetched 0s ago, creation time: 3ms (vhost etag hits: 0%, cache hits: 0%)