//! Hostile bytes. Every parser that reads what another machine sent (a //! bundle, a tree, a chunk list, a git pack, a delta, a commit) answers an //! error for anything malformed, never a panic: in the hosted service's Wasm //! a panic takes down every project in the isolate. Valid inputs are //! mutated (bytes flipped, inserted, cut, repeated) and random ones made, //! many thousands of each, from fixed seeds so a failure repeats. use vcs_core::bundle::{self, Frame}; use vcs_core::git; use vcs_core::object::{self, Kind, Object}; use vcs_core::tree::{self, EntryKind, TreeEntry}; use vcs_core::{Id, MemSource}; struct Rng(u64); impl Rng { fn next(&mut self) -> u64 { // xorshift64* self.0 ^= self.0 >> 12; self.0 ^= self.0 << 25; self.0 ^= self.0 >> 27; self.0.wrapping_mul(0x2545_F491_4F6C_DD1D) } fn below(&mut self, n: usize) -> usize { if n == 0 { 0 } else { (self.next() % n as u64) as usize } } fn bytes(&mut self, n: usize) -> Vec { (0..n).map(|_| self.next() as u8).collect() } } /// `seed` with a few random changes. fn mutate(rng: &mut Rng, seed: &[u8]) -> Vec { let mut out = seed.to_vec(); for _ in 0..1 + rng.below(6) { let at = rng.below(out.len() + 1); match rng.below(7) { 0 if !out.is_empty() => { let i = rng.below(out.len()); out[i] ^= 1 << rng.below(8); } 1 => out.insert(at, rng.next() as u8), 2 if at < out.len() => { let end = (at + 1 + rng.below(16)).min(out.len()); out.drain(at..end); } 3 => out.truncate(at), 4 if at < out.len() => { let end = (at + 1 + rng.below(32)).min(out.len()); let piece = out[at..end].to_vec(); let to = rng.below(out.len() + 1); out.splice(to..to, piece); } 5 if !out.is_empty() => { // Sizes and counts: the extremes. let i = rng.below(out.len()); out[i] = if rng.below(2) == 0 { 0xff } else { 0x00 }; } _ => { let n = rng.below(8); let extra = rng.bytes(n); out.splice(at..at, extra); } } } out } /// VCS_FUZZ_SCALE=20 runs twenty times as many, with other seeds. fn scale() -> u64 { std::env::var("VCS_FUZZ_SCALE").ok().and_then(|s| s.parse().ok()).unwrap_or(1).max(1) } fn each_input(seeds: &[Vec], rounds: usize, salt: u64, mut f: impl FnMut(&[u8])) { let rounds = rounds * scale() as usize; let mut rng = Rng(0x9E37_79B9_7F4A_7C15 ^ salt ^ scale().wrapping_mul(0x1000_0001)); for seed in seeds { f(seed); } for i in 0..rounds { let input = if i % 5 == 4 { let n = rng.below(256); rng.bytes(n) } else { mutate(&mut rng, &seeds[i % seeds.len()]) }; f(&input); } } fn tree_bytes() -> Vec { let e = |name: &str, kind, b: u8| TreeEntry { name: name.into(), kind, id: Id([b; 32]) }; tree::encode(&[e("README.md", EntryKind::File, 1), e("src", EntryKind::Tree, 2), e("run.sh", EntryKind::Exec, 3), e("link", EntryKind::Link, 4), e("lib", EntryKind::Gitlink, 5)]) } #[test] fn bundles() { let objects = [Object::blob(b"hello\n".to_vec()), Object::new(Kind::Tree, tree_bytes()), Object::blob(vec![])]; let mut seed = Vec::new(); bundle::write_header(&mut seed); for o in &objects { bundle::write_frame(&mut seed, &Frame::raw(o)); } bundle::write_end(&mut seed); assert_eq!(bundle::read(&seed).unwrap().len(), 3); each_input(&[seed], 40_000, 1, |input| { if let Ok(frames) = bundle::read(input) { for f in frames { let _ = bundle::verify(&f, &f.payload); } } }); } #[test] fn trees() { let seed = tree_bytes(); assert_eq!(tree::decode(&seed).unwrap().len(), 5); each_input(&[seed], 40_000, 2, |input| { if let Ok(entries) = tree::decode(input) { // Whatever decodes encodes again without trouble. let _ = tree::encode(&entries); } }); } #[test] fn trees_read_through_a_source() { // Merges, diffs and edits over a tree whose bytes are hostile. let mut rng = Rng(3); let seed = tree_bytes(); for _ in 0..5_000 { let mut src = MemSource::default(); let bad = src.put(Object::new(Kind::Tree, mutate(&mut rng, &seed))); let good = src.put(Object::new(Kind::Tree, seed.clone())); let _ = tree::flatten(&src, &bad); let _ = tree::diff_trees(&src, &good, &bad); let _ = tree::unsafe_path(&src, &good, &bad); let _ = tree::edit(&src, &bad, &[tree::Put { path: "src/new.txt".into(), kind: EntryKind::File, content: b"x".to_vec() }], &["README.md".into()]); let _ = vcs_core::merge::trees(&src, Some(&good), &bad, &good, vcs_core::merge::Mode::Land, ("a", "b")); } } #[test] fn chunk_lists() { let seed = object::encode_chunks(&[(Id([1; 32]), 65_536), (Id([2; 32]), 12)]); assert_eq!(object::decode_chunks(&seed).unwrap().len(), 2); each_input(&[seed], 40_000, 4, |input| { let _ = object::decode_chunks(input); }); } /// A small pack with whole objects and both kinds of delta, as git sends. fn pack_bytes() -> Vec { use sha1::{Digest, Sha1}; use std::io::Write; let deflate = |data: &[u8]| { let mut z = flate2::write::ZlibEncoder::new(Vec::new(), flate2::Compression::fast()); z.write_all(data).unwrap(); z.finish().unwrap() }; let header = |code: u8, size: usize| { let mut out = Vec::new(); let mut b = (code << 4) | (size & 15) as u8; let mut rest = size >> 4; while rest > 0 { out.push(b | 0x80); b = (rest & 0x7f) as u8; rest >>= 7; } out.push(b); out }; let base = b"line one\nline two\n".repeat(4); let mut delta = vec![base.len() as u8, (base.len() + 4) as u8, 0x90, base.len() as u8, 4]; delta.extend_from_slice(b"end\n"); let mut pack = b"PACK\0\0\0\x02\0\0\0\x03".to_vec(); let base_at = pack.len(); pack.extend(header(3, base.len())); pack.extend(deflate(&base)); let at = pack.len(); pack.extend(header(6, delta.len())); pack.push((at - base_at) as u8); pack.extend(deflate(&delta)); pack.extend(header(7, delta.len())); pack.extend(git::object_id(git::GitKind::Blob, &base)); pack.extend(deflate(&delta)); let sum = Sha1::digest(&pack); pack.extend_from_slice(&sum); pack } #[test] fn git_packs() { let seed = pack_bytes(); assert_eq!(git::read_pack(&seed, 1 << 20).unwrap().objects.len(), 2); each_input(&[seed], 20_000, 5, |input| { let _ = git::read_pack(input, 1 << 20); }); // With the checksum fixed after each change, so the parser itself is reached. let mut rng = Rng(6); let seed = pack_bytes(); for _ in 0..20_000 { use sha1::{Digest, Sha1}; let mut body = mutate(&mut rng, &seed[..seed.len() - 20]); if body.len() < 12 { continue; } body[..4].copy_from_slice(b"PACK"); let sum = Sha1::digest(&body); body.extend_from_slice(&sum); if let Ok(pack) = git::read_pack(&body, 1 << 20) { for (kind, data) in pack.objects.values() { if *kind == git::GitKind::Commit { let _ = git::Commit::parse(data); } } } } } #[test] fn git_deltas_commits_and_trees() { let base = b"0123456789abcdef".repeat(4); let mut delta = vec![base.len() as u8, 20, 0x91, 4, 16]; delta.extend_from_slice(&[4]); delta.extend_from_slice(b"tail"); assert_eq!(git::apply_delta(&base, &delta, 1 << 20).unwrap().len(), 20); each_input(&[delta], 40_000, 7, |input| { let _ = git::apply_delta(&base, input, 1 << 20); }); let commit = b"tree 0123456789012345678901234567890123456789\nparent 0123456789012345678901234567890123456789\nauthor A 1700000000 +0000\ncommitter A 1700000000 +0000\n\nTitle\n\nBody\n".to_vec(); assert!(git::Commit::parse(&commit).is_ok()); each_input(&[commit], 40_000, 8, |input| { let _ = git::Commit::parse(input); }); let mut gtree = b"100644 a.txt\0".to_vec(); gtree.extend_from_slice(&[1; 20]); gtree.extend_from_slice(b"40000 src\0"); gtree.extend_from_slice(&[2; 20]); assert_eq!(git::tree_entries(>ree).unwrap().len(), 2); each_input(&[gtree.clone()], 40_000, 9, |input| { let _ = git::tree_entries(input); }); // Converting a hostile git tree into a vcs tree. let mut rng = Rng(10); for _ in 0..10_000 { let data = mutate(&mut rng, >ree); let id = git::object_id(git::GitKind::Tree, &data); let mut pack = git::Pack::default(); pack.objects.insert(id, (git::GitKind::Tree, data)); let known = std::collections::HashMap::new(); let _ = git::ToVcs::new(&pack, &known).tree(id, true); } }