fuzz/src/lib.rs
//! **Gunnar's fuzz entry points.** One function per target in `fuzz_targets/`, each taking
//! the bytes a stranger chose and handing them to the parser Gunnar runs on them. Each
//! returns whether the input was ACCEPTED (it parsed, opened or verified), which
//! `tests/seeds.rs` uses to prove the committed `ok-*` seeds still reach the deep end of
//! their parser. A panic, an overflow or a hang here is the bug a fuzzer is looking for.
//!
//! The fixed keys below are throwaway test keys derived from constant seeds: nothing secret.
use gix_hash::Kind;
/// gunnar-server's pack-end scanner, compiled in from its own source file so the fuzz
/// build does not link the whole server (the scanner needs only `anyhow` and `flate2`).
mod storage;
/// A pushed pack's framing (`read_pushed_pack_spooling`'s scanner). The first byte picks
/// the chunk size (1..=256) so splits inside every header are explored.
pub fn pack_scan(data: &[u8]) -> bool {
let Some((&step, pack)) = data.split_first() else {
return false;
};
storage::scan(pack, 20, step as usize + 1)
}
/// Loose-object payloads: a commit, a tag (and its target) and a tree, as gunnar-store
/// decodes them, in both hash widths.
pub fn object_decode(data: &[u8]) -> bool {
let mut accepted = false;
for kind in [Kind::Sha1, Kind::Sha256] {
accepted |= gunnar_store::object::commit(data, kind).is_ok();
accepted |= gunnar_store::object::tag(data, kind).is_ok();
let _ = gunnar_store::object::tag_target(data, kind);
let mut entries = 0usize;
let mut iter = gunnar_store::object::TreeIter::from_bytes(data, kind);
let mut clean = true;
for entry in &mut iter {
match entry {
Ok(_) => entries += 1,
Err(_) => {
clean = false;
break;
}
}
}
accepted |= clean && entries > 0;
}
accepted
}
/// The throwaway keyring every envelope seed was sealed with.
pub fn fuzz_ring() -> gunnar_crypt::keyring::Keyring {
use gunnar_crypt::keyring::{DataKey, IdKey, Keyring, ID_KEY_LEN, KEY_LEN};
Keyring::new(
IdKey::from_bytes([7u8; ID_KEY_LEN]),
DataKey::from_bytes(1, [9u8; KEY_LEN]),
)
}
/// An encrypted repository's blob envelope (v1 and v2), opened whole and streamed.
pub fn envelope_open(data: &[u8]) -> bool {
let ring = fuzz_ring();
let whole = gunnar_crypt::envelope::open(&ring, data);
let mut out = Vec::new();
let streamed = gunnar_crypt::envelope::open_stream(&ring, &mut &data[..], &mut out);
if let (Ok(plain), Ok(n)) = (&whole, &streamed) {
assert_eq!(
*n as usize,
plain.len(),
"whole and streamed open disagree on the length"
);
assert_eq!(
&out, plain,
"whole and streamed open disagree on the plaintext"
);
}
whole.is_ok()
}
/// An encrypted repository's text files (the keyring, the member list, a quantum-safe
/// identity) and the crypt door's exec line and answer.
pub fn crypt_files(data: &[u8]) -> bool {
let mut accepted = false;
if let Some(r) = gunnar_crypt::door::parse_request(data) {
accepted |= r.is_ok();
}
if let Ok(text) = std::str::from_utf8(data) {
accepted |= gunnar_crypt::keyring::Keyring::parse(text).is_ok();
accepted |= gunnar_crypt::members::Members::parse(text).is_ok();
accepted |= gunnar_crypt::pq::parse_identity_file(text).is_ok();
accepted |= gunnar_crypt::door::Answer::parse(text).is_some();
}
accepted
}
/// The throwaway ed25519 key every signature seed was made with.
pub fn fuzz_key() -> ssh_key::PrivateKey {
let pair = ssh_key::private::Ed25519Keypair::from_seed(&[3u8; 32]);
ssh_key::PrivateKey::from(pair)
}
/// A commit's SSH signature (`gpgsig` header), checked as the policy gate checks a push.
pub fn commit_signature(data: &[u8]) -> bool {
let key = fuzz_key();
let allowed = [key.public_key().clone()];
let _ = gunnar_policy::signature::split_signature(data);
gunnar_policy::signature::check_commit_bytes(
gix_hash::ObjectId::null(Kind::Sha1),
data,
&allowed,
"git",
)
.is_verified()
}
/// The bytes a signed forge event's signature covers in the seeds.
pub const EVENT_BYTES: &[u8] = b"gunnar-fuzz signed event";
/// A forge event's `sshsig` (PEM) and an authorized-key line, as gunnar-core reads them.
pub fn sshsig_event(data: &[u8]) -> bool {
let Ok(text) = std::str::from_utf8(data) else {
return false;
};
for line in text.lines().take(8) {
let _ = gunnar_core::auth::parse_public_key(line);
}
let key = fuzz_key();
let public = key.public_key();
let fingerprint = gunnar_core::sshsig::fingerprint(public);
gunnar_core::sshsig::verify(
public,
gunnar_core::forge::FORGE_NAMESPACE,
EVENT_BYTES,
&fingerprint,
text,
)
.is_ok()
}
/// receive-pack's opening: the pushing client's ref commands and capabilities (pkt-lines).
pub fn receive_preamble(data: &[u8]) -> bool {
let mut input: &[u8] = data;
matches!(
gunnar_wire::receive_pack::ReceivePack::read_preamble(Kind::Sha1, &mut input),
Ok(Some(_))
)
}
/// upload-pack v2: the argument sections of `command=fetch` and `command=ls-refs`.
pub fn upload_request(data: &[u8]) -> bool {
let lines: Vec<Vec<u8>> = data.split(|&b| b == b'\n').map(<[u8]>::to_vec).collect();
let fetch = gunnar_wire::upload_pack::FetchRequest::parse(
&lines,
Kind::Sha1,
gunnar_wire::caps::Offered::default(),
)
.is_ok();
let ls_refs = gunnar_wire::upload_pack::LsRefsRequest::parse(&lines).is_ok();
fetch || ls_refs
}