Skip to content

Commit

Permalink
Merge pull request #30 from niklasf/fuzz
Browse files Browse the repository at this point in the history
fuzz test archive round trip and reading
  • Loading branch information
mdsteele authored Mar 26, 2024
2 parents fae9a49 + 7c46815 commit a531aa7
Show file tree
Hide file tree
Showing 8 changed files with 357 additions and 1 deletion.
3 changes: 2 additions & 1 deletion .github/workflows/tests.yml
Original file line number Diff line number Diff line change
Expand Up @@ -45,4 +45,5 @@ jobs:
toolchain: ${{ matrix.rust }}
- name: Test
run: cargo test --verbose

- name: Compile fuzz suite
run: cargo check --manifest-path fuzz/Cargo.toml
4 changes: 4 additions & 0 deletions fuzz/.gitignore
Original file line number Diff line number Diff line change
@@ -0,0 +1,4 @@
target
corpus
artifacts
coverage
113 changes: 113 additions & 0 deletions fuzz/Cargo.lock

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

27 changes: 27 additions & 0 deletions fuzz/Cargo.toml
Original file line number Diff line number Diff line change
@@ -0,0 +1,27 @@
[package]
name = "ar-fuzz"
publish = false
edition = "2021"

[package.metadata]
cargo-fuzz = true

[dependencies]
libfuzzer-sys = "0.4"
ar = { path = ".." }
arbitrary = { version = "1.3.2", features = ["derive"] }

[workspace]
members = ["."] # Let fuzz suite not interfere with workspaces

[[bin]]
name = "roundtrip"
path = "fuzz_targets/roundtrip.rs"

[[bin]]
name = "roundtrip_gnu"
path = "fuzz_targets/roundtrip_gnu.rs"

[[bin]]
name = "read"
path = "fuzz_targets/read.rs"
13 changes: 13 additions & 0 deletions fuzz/fuzz_targets/read.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,13 @@
#![no_main]

use libfuzzer_sys::fuzz_target;
use std::io::Read;

fuzz_target!(|data: &[u8]| {
let mut data = data;
let mut archive = ar::Archive::new(&mut data);
while let Some(Ok(mut entry)) = archive.next_entry() {
let mut discard = [0; 1024];
let _ = entry.read(&mut discard[..]);
}
});
47 changes: 47 additions & 0 deletions fuzz/fuzz_targets/roundtrip.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,47 @@
#![no_main]

use ar_fuzz::{Entry, Header, Model};
use arbitrary::{Arbitrary as _, Unstructured};
use libfuzzer_sys::fuzz_target;
use std::io::Read as _;

fuzz_target!(|data: &[u8]| {
let mut u = Unstructured::new(data);
let model = Model::arbitrary(&mut u).expect("make arbitrary model");

if model.entries.is_empty() {
return; // Builder does not do anything unless there is at least one entry
}

let mut buffer = Vec::new();

let mut builder = ar::Builder::new(&mut buffer);
for entry in &model.entries {
if let Err(err) = builder.append(&entry.header(), &mut &entry.data[..])
{
panic!("append entry: {err} with {model:?}"); // Or just return if invalid input
}
}

let mut rountripped = Model::default();
let mut reader = &buffer[..];
let mut archive = ar::Archive::new(&mut reader);
while let Some(entry) = archive.next_entry() {
let mut entry = match entry {
Ok(entry) => entry,
Err(err) => panic!("read entry: {err} with {model:?}"),
};
rountripped.entries.push(Entry {
header: Header::from_ar(entry.header()),
data: {
let mut data = Vec::new();
if let Err(err) = entry.read_to_end(&mut data) {
panic!("read entry data: {err} with {model:?}");
}
data
},
})
}

assert_eq!(model, rountripped);
});
47 changes: 47 additions & 0 deletions fuzz/fuzz_targets/roundtrip_gnu.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,47 @@
#![no_main]

use ar_fuzz::{Entry, Header, Model};
use arbitrary::{Arbitrary as _, Unstructured};
use libfuzzer_sys::fuzz_target;
use std::io::Read as _;

fuzz_target!(|data: &[u8]| {
let mut u = Unstructured::new(data);
let model = Model::arbitrary(&mut u).expect("make arbitrary model");

if model.entries.is_empty() {
return; // Builder does not do anything unless there is at least one entry
}

let mut buffer = Vec::new();

let mut builder = ar::GnuBuilder::new(&mut buffer, model.identifiers());
for entry in &model.entries {
if let Err(err) = builder.append(&entry.header(), &mut &entry.data[..])
{
panic!("append entry: {err} with {model:?}"); // Or just return if invalid input
}
}

let mut rountripped = Model::default();
let mut reader = &buffer[..];
let mut archive = ar::Archive::new(&mut reader);
while let Some(entry) = archive.next_entry() {
let mut entry = match entry {
Ok(entry) => entry,
Err(err) => panic!("read entry: {err} with {model:?}"),
};
rountripped.entries.push(Entry {
header: Header::from_ar(entry.header()),
data: {
let mut data = Vec::new();
if let Err(err) = entry.read_to_end(&mut data) {
panic!("read entry data: {err} with {model:?}");
}
data
},
})
}

assert_eq!(model, rountripped);
});
104 changes: 104 additions & 0 deletions fuzz/src/lib.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,104 @@
use arbitrary::{Arbitrary, Unstructured};

#[derive(Clone, Default, Debug, Eq, PartialEq, Arbitrary)]
pub struct Model {
pub entries: Vec<Entry>,
}

impl Model {
pub fn identifiers(&self) -> Vec<Vec<u8>> {
self.entries.iter().map(|e| e.header.identifier.0.clone()).collect()
}
}

#[derive(Clone, Debug, Eq, PartialEq, Arbitrary)]
pub struct Entry {
pub header: Header,
pub data: Vec<u8>,
}

impl Entry {
pub fn header(&self) -> ar::Header {
let mut header = ar::Header::new(
self.header.identifier.0.clone(),
self.data.len() as u64,
);
header.set_mtime(self.header.mtime.0);
header.set_uid(self.header.uid.0);
header.set_gid(self.header.gid.0);
header.set_mode(self.header.mode.0);
header
}
}

#[derive(Clone, Debug, Eq, PartialEq, Arbitrary)]
pub struct Header {
identifier: Identifier,
mtime: Timestamp,
uid: Uid,
gid: Uid,
mode: Mode,
}

impl Header {
pub fn from_ar(header: &ar::Header) -> Header {
Header {
identifier: Identifier(header.identifier().to_vec()),
mtime: Timestamp(header.mtime()),
uid: Uid(header.uid()),
gid: Uid(header.gid()),
mode: Mode(header.mode()),
}
}
}

#[derive(Clone, Debug, Eq, PartialEq)]
struct Identifier(Vec<u8>);

impl Arbitrary<'_> for Identifier {
fn arbitrary(u: &mut Unstructured<'_>) -> arbitrary::Result<Self> {
// TODO: Reject invalid identifiers at creation time:
// - Long identifiers
// - Invalid ASCII or invalid UTF-8?
// - Containing NUL
// - Rules for '/'?
// - Rules for spaces?
String::arbitrary(u).map(|mut v| {
v.retain(|ch| ch != '\0' && ch != '/' && ch != ' ');
while v.len() > 15 {
v.pop();
}
if v.is_empty() {
v.push('a');
}
Identifier(v.into_bytes())
})
}
}

#[derive(Copy, Clone, Debug, Eq, PartialEq)]
struct Mode(u32);

impl Arbitrary<'_> for Mode {
fn arbitrary(u: &mut Unstructured<'_>) -> arbitrary::Result<Self> {
u32::arbitrary(u).map(|v| Mode(v & 0o7777))
}
}

#[derive(Copy, Clone, Debug, Eq, PartialEq)]
struct Timestamp(u64);

impl Arbitrary<'_> for Timestamp {
fn arbitrary(u: &mut Unstructured<'_>) -> arbitrary::Result<Self> {
u.int_in_range(0..=999_999_999_999).map(Timestamp) // TODO: Deal with entire range
}
}

#[derive(Copy, Clone, Debug, Eq, PartialEq)]
struct Uid(u32);

impl Arbitrary<'_> for Uid {
fn arbitrary(u: &mut Unstructured<'_>) -> arbitrary::Result<Self> {
u.int_in_range(0..=999_999).map(Uid) // TODO: Deal with entire range (#29)
}
}

0 comments on commit a531aa7

Please sign in to comment.