diff --git a/gix-commitgraph/fuzz/.gitignore b/gix-commitgraph/fuzz/.gitignore new file mode 100644 index 00000000000..1a45eee7760 --- /dev/null +++ b/gix-commitgraph/fuzz/.gitignore @@ -0,0 +1,4 @@ +target +corpus +artifacts +coverage diff --git a/gix-commitgraph/fuzz/Cargo.toml b/gix-commitgraph/fuzz/Cargo.toml new file mode 100644 index 00000000000..b15f90419ea --- /dev/null +++ b/gix-commitgraph/fuzz/Cargo.toml @@ -0,0 +1,30 @@ +[package] +name = "gix-commitgraph-fuzz" +version = "0.0.0" +publish = false +edition = "2021" + +[package.metadata] +cargo-fuzz = true + +[dependencies] +anyhow = "1.0.76" +arbitrary = { version = "1.3.2", features = ["derive"] } +libfuzzer-sys = "0.4" +memmap2 = "0.9.0" + +[dependencies.gix-commitgraph] +path = ".." + +# Prevent this from interfering with workspaces +[workspace] +members = ["."] + +[profile.release] +debug = 1 + +[[bin]] +name = "fuzz_file" +path = "fuzz_targets/fuzz_file.rs" +test = false +doc = false diff --git a/gix-commitgraph/fuzz/fuzz_targets/fuzz_file.rs b/gix-commitgraph/fuzz/fuzz_targets/fuzz_file.rs new file mode 100644 index 00000000000..f6894f33bd0 --- /dev/null +++ b/gix-commitgraph/fuzz/fuzz_targets/fuzz_file.rs @@ -0,0 +1,29 @@ +#![no_main] + +use anyhow::Result; +use gix_commitgraph::File; +use libfuzzer_sys::fuzz_target; +use std::hint::black_box; + +fn fuzz(data: &[u8]) -> Result<()> { + let data = { + let mut d = memmap2::MmapMut::map_anon(data.len())?; + d.copy_from_slice(data); + d.make_read_only()? + }; + let file = File::new(data, "does not matter".into())?; + + _ = black_box(file.iter_base_graph_ids().count()); + _ = black_box(file.iter_commits().count()); + _ = black_box(file.iter_ids().count()); + + let _ = black_box(file.checksum()); + let _ = black_box(file.verify_checksum()); + let _ = black_box(file.object_hash()); + + Ok(()) +} + +fuzz_target!(|data: &[u8]| { + _ = black_box(fuzz(data)); +}); diff --git a/gix-commitgraph/src/file/init.rs b/gix-commitgraph/src/file/init.rs index 1d8f52e3f7b..df9ef369cda 100644 --- a/gix-commitgraph/src/file/init.rs +++ b/gix-commitgraph/src/file/init.rs @@ -1,3 +1,4 @@ +use std::path::PathBuf; use std::{ convert::{TryFrom, TryInto}, path::Path, @@ -62,24 +63,13 @@ impl File { pub fn at(path: impl AsRef) -> Result { Self::try_from(path.as_ref()) } -} -impl TryFrom<&Path> for File { - type Error = Error; - - fn try_from(path: &Path) -> Result { - let data = std::fs::File::open(path) - .and_then(|file| { - // SAFETY: we have to take the risk of somebody changing the file underneath. Git never writes into the same file. - #[allow(unsafe_code)] - unsafe { - Mmap::map(&file) - } - }) - .map_err(|e| Error::Io { - err: e, - path: path.to_owned(), - })?; + /// A lower-level constructor which constructs a new instance directly from the mapping in `data`, + /// assuming that it originated from `path`. + /// + /// Note that `path` is only used for verification of the hash its basename contains, but otherwise + /// is not of importance. + pub fn new(data: memmap2::Mmap, path: PathBuf) -> Result { let data_size = data.len(); if data_size < MIN_FILE_SIZE { return Err(Error::Corrupt( @@ -240,13 +230,33 @@ impl TryFrom<&Path> for File { extra_edges_list_range, fan, oid_lookup_offset, - path: path.to_owned(), + path, hash_len: object_hash.len_in_bytes(), object_hash, }) } } +impl TryFrom<&Path> for File { + type Error = Error; + + fn try_from(path: &Path) -> Result { + let data = std::fs::File::open(path) + .and_then(|file| { + // SAFETY: we have to take the risk of somebody changing the file underneath. Git never writes into the same file. + #[allow(unsafe_code)] + unsafe { + Mmap::map(&file) + } + }) + .map_err(|e| Error::Io { + err: e, + path: path.to_owned(), + })?; + Self::new(data, path.to_owned()) + } +} + // Copied from gix-odb/pack/index/init.rs fn read_fan(d: &[u8]) -> ([u32; FAN_LEN], usize) { let mut fan = [0; FAN_LEN];