//! The crate's contract, written as an executable assertion. //! //! A normal public module rather than something behind a `fuzzing` feature, //! because two callers need it and neither is the fuzzer: the committed //! regression replay in `tests/regressions.rs` runs it on stable, and the //! libFuzzer target in `fuzz/` runs it on nightly. A property asserted in one //! and not the other is a property that drifts. //! //! ## What is asserted //! //! 1. **Chunk equivalence.** A payload that arrives in one body and the same //! payload arriving as `m=1` chunks must produce the same bytes. This is the //! property with teeth: `accept_chunked` decodes base64 straight into the //! accumulator through `resize` plus `decode_slice`, which is size //! arithmetic over attacker-controlled lengths, and the host indexes the //! result as pixels. //! 2. **Retained state is bounded by the input.** See //! [`MAX_RETAINED_PER_INPUT_BYTE`], and absolutely, via //! [`MAX_RETAINED_BYTES`]. The second is the one that catches an //! accumulator growing with a transmission nobody finishes, which the //! first cannot see: it grew under 1:1. //! 3. **Every query is answered, and answered to the right client.** //! [`super::query_response`] must be a well-formed APC that carries the id //! the query carried, and must say OK only for a medium the host can //! actually satisfy. Silence, or an OK the terminal cannot honour, both cost //! the asking program its timeout or its picture. //! 4. **A decoded payload is no larger than its base64 could describe.** Cheap, //! and it is the assertion that would fail first if the accumulator's //! `resize` bound and its `truncate` ever disagreed. use base64::{Engine, engine::general_purpose::STANDARD as B64}; use super::{Command, Control, Medium, Parser, query_response}; /// Bytes the parser may hold per byte of input before the oracle calls it a /// finding. /// /// 4 is set from the worst case the shape allows: an accumulator caught just /// past a doubling holds twice its length, and base64 costs the writer four /// bytes for every three it lands, so the ratio cannot exceed 1.5. The bounds /// that hold it there are [`super::MAX_IN_FLIGHT_TRANSMISSIONS`] and /// [`super::MAX_IN_FLIGHT_BYTES`]. Anything that pushes the ratio back over 4 /// is either a new accumulator or a cap that stopped being enforced. pub const MAX_RETAINED_PER_INPUT_BYTE: usize = 4; /// Bytes the parser may hold after any input at all, however long. /// /// The ratio ceiling above cannot see an accumulator under a single `i=`: it /// retains under one byte per input byte, so no per-input-byte limit fires, /// while it grows for as long as the writer keeps sending. This is the absolute /// bound that catches that. /// /// Twice the budget rather than the budget, because the accumulators grow by /// doubling: the budget is checked against length, and capacity is what the /// process pays. pub const MAX_RETAINED_BYTES: usize = crate::MAX_IN_FLIGHT_BYTES * 2 + 64 * 1024; /// Slack for a parser holding a handful of small transmissions. pub const RETAINED_BASE_BYTES: usize = 4096; /// Panics if `control`'s query answer is not one a client can use. /// /// Returns whether the answer claimed OK, for the same reason [`check_body`] /// returns a bool: a body of pure assertions replaced by `()` is invisible to /// every test that passes, so the oracle has to hand back something. /// /// # Panics /// /// By design. It is an oracle, and a panic is how it reports. // The reply is tens of bytes long, so the bytecount crate would be a // dependency bought for nothing. #[allow(clippy::naive_bytecount)] pub fn check_query_response(control: &Control) -> bool { let reply = query_response(control); assert!( reply.starts_with(b"\x1b_G"), "query answer is not an APC: {reply:?}" ); assert!( reply.ends_with(b"\x1b\\"), "query answer has no string terminator: {reply:?}" ); // Exactly two ESCs, the introducer and the terminator. A third would end // the string early and leave the rest of the answer on the client's screen. assert_eq!( reply.iter().filter(|&&b| b == 0x1b).count(), 2, "query answer contains an embedded escape: {reply:?}" ); let text = std::str::from_utf8(&reply).expect("query answer is ASCII"); let id = control.id.unwrap_or(0); assert!( text.contains(&format!("i={id};")), "query answer is addressed to nobody: {text:?}" ); // Direct is the only medium the host can satisfy. An OK for any other // promises a picture that never arrives, and the client stops looking for // another way to send it. let claims_ok = text.contains(";OK"); let satisfiable = control.format.is_some() && matches!(control.medium, None | Some(Medium::Direct)); assert_eq!( claims_ok, satisfiable, "query answered {text:?} for format {:?} medium {:?}", control.format, control.medium ); claims_ok } /// Panics if the same payload sent as chunks does not arrive as the same bytes. /// /// Returns how many chunks it took, for the same reason as the rest of this /// module: an oracle that returns nothing cannot be observed to have run. /// /// # Panics /// /// By design. /// How wide a chunk to cut the encoded payload into: roughly a third of it, /// rounded to a whole number of base64 quanta. /// /// Chunk boundaries are multiples of four, which is what the protocol requires /// of a sender: each chunk is decoded on its own, so a boundary inside a quantum /// would not be decodable by any implementation. /// /// The floor of one quantum is load-bearing rather than defensive. The walk /// below consumes `chunk_len` bytes per turn, so a width of zero does not /// terminate, and the arithmetic here is the only thing that could produce one. /// Named and floored, it is a thing a test can state instead of a mutant that /// spends five minutes not answering. fn chunk_len_for(encoded_len: usize) -> usize { let quanta = encoded_len.div_ceil(4); (4 * quanta.div_ceil(3)).max(4) } fn check_chunk_equivalence(action: char, payload: &[u8]) -> usize { if payload.is_empty() { return 0; } let encoded = B64.encode(payload); let chunk_len = chunk_len_for(encoded.len()); let mut p = Parser::new(); let mut first = true; let mut rest = encoded.as_str(); let mut got = None; let mut chunks = 0; while !rest.is_empty() { chunks += 1; // At least one byte, whatever the width says. The walk's termination // is then a property of the loop rather than of the arithmetic above // it, so a width that comes back wrong is a failed assertion below // instead of a run that never ends. let take = chunk_len.clamp(1, rest.len()); let (head, tail) = rest.split_at(take); rest = tail; let more = if rest.is_empty() { "0" } else { "1" }; let body = if first { format!("Ga={action},i=4242,m={more};{head}") } else { format!("Gi=4242,m={more};{head}") }; first = false; if let Some(cmd) = p.feed(body.as_bytes()) { got = Some(cmd); } } let chunked = match got { Some(Command::Transmit { payload, .. } | Command::FrameAppend { payload, .. }) => payload, Some(other) => panic!("chunked {action} came back as {other:?}"), None => panic!( "chunked {action} of {} bytes never completed", payload.len() ), }; assert_eq!( chunked.len(), payload.len(), "chunked payload is {} bytes, single-shot was {}", chunked.len(), payload.len() ); assert!( chunked == payload, "chunked payload differs from the single-shot one" ); assert_eq!( p.pending_transmissions(), 0, "a completed transmission was left in flight" ); chunks } /// Feed one APC body to a fresh parser and hold what comes out to everything /// above. /// /// Returns whether a command was produced, for the same reason /// `git_command::oracle::check_line` returns a bool: without a return value /// nothing can observe this function running at all, and `cargo mutants` /// replacing the body with `()` would leave every test passing. /// /// # Panics /// /// By design, on any violation. /// The base64 characters in `body`: everything after the first `;`. /// /// Named because the bound it feeds is an upper one, and every mutation of this /// arithmetic makes it looser. A looser upper bound is still satisfied by a /// correct decode, so nothing in a passing run disagrees with a mutant here. fn b64_len_of(body: &[u8]) -> usize { body.iter() .position(|&b| b == b';') .map_or(0, |i| body.len() - i - 1) } /// The most a parser may retain after `input_len` bytes of input. /// /// Same reason as [`b64_len_of`]: it is a ceiling, so every mutation raises it /// and a well-behaved parser stays under both. fn retained_ceiling(input_len: usize) -> usize { RETAINED_BASE_BYTES + input_len.saturating_mul(MAX_RETAINED_PER_INPUT_BYTE) } pub fn check_body(body: &[u8]) -> bool { let mut p = Parser::new(); let Some(cmd) = p.feed(body) else { return false; }; // A body that produced a command left nothing behind, unless it opened a // chunked transmission, which returns None and never reaches here. assert_eq!( p.pending_transmissions(), 0, "a single body both completed and stayed in flight" ); match &cmd { Command::Query { control } => { check_query_response(control); } Command::Transmit { control, payload } | Command::FrameAppend { control, payload } => { // base64 carries three bytes in every four characters, so a decoded // payload longer than that is the accumulator's own arithmetic // disagreeing with itself. let b64_len = b64_len_of(body); assert!( payload.len() <= b64_len.div_ceil(4) * 3, "decoded {} bytes out of {b64_len} base64 characters", payload.len() ); let action = if matches!(cmd, Command::FrameAppend { .. }) { 'f' } else { control.action }; check_chunk_equivalence(action, payload); } Command::Place { .. } | Command::Delete { .. } | Command::FrameCompose { .. } => {} } true } /// Split `input` into APC bodies and run a whole session through one parser. /// /// The split is on ESC, and a leading `_` and trailing `\` are stripped, so a /// captured `\e_G…\e\` stream from a real client is a seed as it stands rather /// than in a format of this harness's invention. /// /// Returns how many bodies were fed. See [`check_body`] for why it returns /// anything at all. /// /// # Panics /// /// By design, on any violation. pub fn check_bodies(input: &[u8]) -> usize { let mut session = Parser::new(); let mut fed = 0; for piece in input.split(|&b| b == 0x1b) { let piece = piece.strip_prefix(b"_").unwrap_or(piece); let piece = piece.strip_suffix(b"\\").unwrap_or(piece); if piece.is_empty() { continue; } fed += 1; // The session parser is what carries chunk state between bodies, and // it is the one whose retained state is bounded below. if let Some(Command::Query { control }) = session.feed(piece) { check_query_response(&control); } // Every body is also checked on its own, where a completed command can // be held to the properties that need one. check_body(piece); // Per body, not only at the end. The cap is what keeps the map from // growing with the id space, and a parser that has stopped enforcing // it gets slower with every body it takes: checked once at the end, // the oracle spends the whole input finding that out. assert!( session.pending_transmissions() <= crate::MAX_IN_FLIGHT_TRANSMISSIONS, "{} transmissions in flight after {fed} bodies, over the cap", session.pending_transmissions() ); } let retained = session.pending_bytes(); let ceiling = retained_ceiling(input.len()); assert!( retained <= ceiling, "parser holds {retained} bytes after {} bytes of input, over the {ceiling} ceiling", input.len() ); assert!( retained <= MAX_RETAINED_BYTES, "parser holds {retained} bytes, over the {MAX_RETAINED_BYTES} absolute ceiling" ); assert!( session.pending_transmissions() <= fed.min(crate::MAX_IN_FLIGHT_TRANSMISSIONS), "{} transmissions in flight after {fed} bodies", session.pending_transmissions() ); fed } #[cfg(test)] mod tests { //! Unit tests for the oracle's own decisions. //! //! The oracle asserts things about the crate; nothing asserted anything //! about the oracle. Every function below is either a ceiling the crate is //! held to, or a piece of arithmetic that decides how much of the input the //! oracle looks at. Neither can be observed through a passing run: a looser //! ceiling is still satisfied by a correct parser, and a narrower walk still //! agrees with everything it did check. use super::{ MAX_RETAINED_BYTES, RETAINED_BASE_BYTES, b64_len_of, check_body, check_chunk_equivalence, check_query_response, chunk_len_for, retained_ceiling, }; use crate::{Control, Format, Medium}; use base64::{Engine, engine::general_purpose::STANDARD as B64}; #[test] fn the_absolute_retention_ceiling_is_two_budgets_and_a_slack() { // Written out, so a mutant rewriting the expression in the source has // nothing here to agree with. assert_eq!(MAX_RETAINED_BYTES, 134_283_264); assert_eq!(RETAINED_BASE_BYTES, 4096); } #[test] fn the_per_input_ceiling_is_the_base_plus_four_per_byte() { assert_eq!(retained_ceiling(1000), 8096); assert_eq!(retained_ceiling(0), RETAINED_BASE_BYTES); } #[test] fn the_chunk_width_is_a_third_of_the_payload_in_whole_quanta() { // 128 encoded characters is 32 quanta; a third of that, rounded up, is // 11 quanta of four bytes each. assert_eq!(chunk_len_for(128), 44); } #[test] fn the_chunk_width_never_falls_below_one_quantum() { // The walk consumes chunk_len bytes a turn, so a zero here does not // terminate. This is the floor that makes that unrepresentable. assert_eq!(chunk_len_for(0), 4); assert_eq!(chunk_len_for(4), 4); assert!(chunk_len_for(3) >= 4); } #[test] fn the_base64_length_is_what_follows_the_first_semicolon() { assert_eq!(b64_len_of(b"a=T,f=32;QUJD"), 4); assert_eq!( b64_len_of(b"a=T,f=32"), 0, "a body with no payload marker carries no base64" ); assert_eq!( b64_len_of(b"a=T;QUJD;RUZH"), 9, "the FIRST semicolon is the separator; later ones are payload" ); } #[test] fn chunk_equivalence_reports_how_many_chunks_it_took() { // 96 bytes encode to 128 characters, which is three chunks of 44. assert_eq!(check_chunk_equivalence('T', &[0x5A; 96]), 3); assert_eq!( check_chunk_equivalence('T', &[]), 0, "an empty payload is not chunked at all" ); } #[test] fn check_body_reports_whether_a_command_came_out() { let body = format!("Ga=T,f=32,s=2,v=2;{}", B64.encode([0x11u8; 16])); assert!(check_body(body.as_bytes()), "a valid transmit yields one"); assert!( !check_body(b"Gnot-a-command"), "a body that parses to nothing yields none" ); } #[test] fn the_query_answer_says_ok_only_for_a_medium_the_host_can_serve() { let direct = Control { action: 'q', format: Some(Format::Rgba), medium: Some(Medium::Direct), id: Some(4242), ..Control::default() }; assert!(check_query_response(&direct), "inline base64 is servable"); let from_file = Control { medium: Some(Medium::File), ..direct.clone() }; assert!( !check_query_response(&from_file), "a file transfer must not be answered OK" ); let no_format = Control { format: None, ..direct }; assert!( !check_query_response(&no_format), "a query naming no format is not something to say OK to" ); } }