Cross builds, packaging split, CI, fuzzing, supply-chain policy
build.sh builds the Rust targets with the Go-era names, refuses an OUT that is /, $HOME or the tree, checks static linking and names the Go targets Rust cannot build. packaging/package.sh builds and inspects packages without signing or publishing; repo.sh reuses it. Seven fuzz targets (one found a URL-parser panic, fixed here with a regression test), cargo-deny policy, CI for lint, tests on Linux/macOS/Windows, cross builds with qemu runs, supply chain and fuzzing. Compatibility specification and security notes. Co-Authored-By: Claude <noreply@anthropic.com>
This commit is contained in:
Generated
+1283
File diff suppressed because it is too large
Load Diff
@@ -0,0 +1,68 @@
|
||||
[package]
|
||||
name = "vault-fuzz"
|
||||
version = "0.0.0"
|
||||
publish = false
|
||||
edition = "2024"
|
||||
|
||||
[package.metadata]
|
||||
cargo-fuzz = true
|
||||
|
||||
[dependencies]
|
||||
libfuzzer-sys = "0.4"
|
||||
tar = { version = "0.4.46", default-features = false }
|
||||
vault = { path = ".." }
|
||||
|
||||
# Not part of the main package's workspace.
|
||||
[workspace]
|
||||
|
||||
[profile.release]
|
||||
debug = 1
|
||||
|
||||
[[bin]]
|
||||
name = "header"
|
||||
path = "fuzz_targets/header.rs"
|
||||
test = false
|
||||
doc = false
|
||||
bench = false
|
||||
|
||||
[[bin]]
|
||||
name = "stream"
|
||||
path = "fuzz_targets/stream.rs"
|
||||
test = false
|
||||
doc = false
|
||||
bench = false
|
||||
|
||||
[[bin]]
|
||||
name = "keyfile"
|
||||
path = "fuzz_targets/keyfile.rs"
|
||||
test = false
|
||||
doc = false
|
||||
bench = false
|
||||
|
||||
[[bin]]
|
||||
name = "record"
|
||||
path = "fuzz_targets/record.rs"
|
||||
test = false
|
||||
doc = false
|
||||
bench = false
|
||||
|
||||
[[bin]]
|
||||
name = "names"
|
||||
path = "fuzz_targets/names.rs"
|
||||
test = false
|
||||
doc = false
|
||||
bench = false
|
||||
|
||||
[[bin]]
|
||||
name = "html"
|
||||
path = "fuzz_targets/html.rs"
|
||||
test = false
|
||||
doc = false
|
||||
bench = false
|
||||
|
||||
[[bin]]
|
||||
name = "tar"
|
||||
path = "fuzz_targets/tar.rs"
|
||||
test = false
|
||||
doc = false
|
||||
bench = false
|
||||
@@ -0,0 +1,15 @@
|
||||
// SPDX-License-Identifier: GPL-3.0-or-later
|
||||
//! The Fury header parser never panics, and accepts only canonical headers:
|
||||
//! whatever it accepts re-serialises to exactly the bytes it consumed.
|
||||
#![no_main]
|
||||
use std::io::Read;
|
||||
use vault::crypto::header;
|
||||
|
||||
libfuzzer_sys::fuzz_target!(|data: &[u8]| {
|
||||
if let Ok((h, mut rest)) = header::parse(data) {
|
||||
let mut tail = Vec::new();
|
||||
rest.read_to_end(&mut tail).unwrap();
|
||||
let consumed = &data[..data.len() - tail.len()];
|
||||
assert_eq!(h.marshal(), consumed, "non-canonical header accepted");
|
||||
}
|
||||
});
|
||||
@@ -0,0 +1,15 @@
|
||||
// SPDX-License-Identifier: GPL-3.0-or-later
|
||||
//! The mirror's HTML and CSS rewriters never panic, and rewriting nothing
|
||||
//! returns the input byte for byte.
|
||||
#![no_main]
|
||||
use vault::net::html;
|
||||
|
||||
libfuzzer_sys::fuzz_target!(|data: &[u8]| {
|
||||
let _ = html::tokenize(data);
|
||||
assert_eq!(html::rewrite_html(data, &mut |_| None, &mut |_| None), data);
|
||||
assert_eq!(html::rewrite_css(data, false, &mut |_| None), data);
|
||||
let _ = html::rewrite_html(data, &mut |_| None, &mut |u| Some(format!("x/{}", u.len())));
|
||||
if let Ok(s) = std::str::from_utf8(data) {
|
||||
let _ = vault::net::Url::parse(s).map(|u| u.join("../a?b#c"));
|
||||
}
|
||||
});
|
||||
@@ -0,0 +1,11 @@
|
||||
// SPDX-License-Identifier: GPL-3.0-or-later
|
||||
//! PEM decoding never panics, and decoding what it encoded is the identity.
|
||||
#![no_main]
|
||||
use vault::keyfile::{decode_pem, encode_pem, parse_body};
|
||||
|
||||
libfuzzer_sys::fuzz_target!(|data: &[u8]| {
|
||||
if let Some(body) = decode_pem(data) {
|
||||
let _ = parse_body(&body);
|
||||
assert_eq!(decode_pem(encode_pem(&body).as_bytes()), Some(body));
|
||||
}
|
||||
});
|
||||
@@ -0,0 +1,18 @@
|
||||
// SPDX-License-Identifier: GPL-3.0-or-later
|
||||
//! Path validation: a valid vault name is clean, relative and confined; the
|
||||
//! tar name check only ever yields valid-shaped relative names.
|
||||
#![no_main]
|
||||
use vault::store::VaultName;
|
||||
|
||||
libfuzzer_sys::fuzz_target!(|data: &[u8]| {
|
||||
let Ok(s) = std::str::from_utf8(data) else { return };
|
||||
if let Some(n) = VaultName::new(s) {
|
||||
let n = n.as_str();
|
||||
assert_eq!(vault::util::clean(n), n);
|
||||
assert!(!n.starts_with('/') && !n.contains('\\') && !n.split('/').any(|c| c == ".." || c == "." || c.is_empty()));
|
||||
assert!(n.len() <= 1024);
|
||||
}
|
||||
if let Some(n) = vault::absorb::safe_name(s) {
|
||||
assert!(!n.is_empty() && !n.starts_with('/') && !n.split('/').any(|c| c == ".."));
|
||||
}
|
||||
});
|
||||
@@ -0,0 +1,15 @@
|
||||
// SPDX-License-Identifier: GPL-3.0-or-later
|
||||
//! Record JSON parsing never panics; an accepted record re-encodes (Go's
|
||||
//! JSON) to something that parses back to the same values.
|
||||
#![no_main]
|
||||
use vault::store::{Id, Record};
|
||||
|
||||
libfuzzer_sys::fuzz_target!(|data: &[u8]| {
|
||||
if let Ok(r) = Record::from_json(data, Id([0; 16])) {
|
||||
let Some(j) = r.to_json() else { return };
|
||||
let r2 = Record::from_json(&j, Id([0; 16])).expect("own encoding parses");
|
||||
assert_eq!(r.name, r2.name);
|
||||
assert_eq!((r.size, r.mode, r.modified, r.added), (r2.size, r2.mode, r2.modified, r2.added));
|
||||
assert!(r.seed.same_as(&r2.seed));
|
||||
}
|
||||
});
|
||||
@@ -0,0 +1,46 @@
|
||||
// SPDX-License-Identifier: GPL-3.0-or-later
|
||||
//! The streaming, in-memory and random-access STREAM readers agree on every
|
||||
//! input: the same plaintext or all reject. Inputs are mostly forgeries;
|
||||
//! the first byte selects a mode that starts from a valid stream instead.
|
||||
#![no_main]
|
||||
use std::io::{Read, Write};
|
||||
use vault::crypto::stream::{StreamReader, StreamReaderAt, StreamWriter, open_all};
|
||||
|
||||
const KEY: [u8; 32] = [7; 32];
|
||||
|
||||
libfuzzer_sys::fuzz_target!(|data: &[u8]| {
|
||||
let Some((&mode, rest)) = data.split_first() else { return };
|
||||
let input = if mode & 1 == 1 {
|
||||
// A valid stream of the given plaintext, then a mutation.
|
||||
let mut w = StreamWriter::new(&KEY, Vec::new());
|
||||
let n = rest.len().min(200_000);
|
||||
w.write_all(&rest[..n]).unwrap();
|
||||
let mut c = w.finish().unwrap();
|
||||
if mode & 2 == 2 && !c.is_empty() {
|
||||
let i = (mode as usize * 7919) % c.len();
|
||||
c[i] ^= mode | 1;
|
||||
}
|
||||
if mode & 4 == 4 {
|
||||
c.truncate(c.len().saturating_sub(mode as usize % 40));
|
||||
}
|
||||
c
|
||||
} else {
|
||||
rest.to_vec()
|
||||
};
|
||||
let mut a = Vec::new();
|
||||
let streaming = StreamReader::new(&KEY, &input[..]).read_to_end(&mut a).map(|_| a);
|
||||
let mut b = input.clone();
|
||||
let whole = open_all(&KEY, &mut b).map(|n| b[..n].to_vec());
|
||||
assert_eq!(streaming.is_ok(), whole.is_ok());
|
||||
if let (Ok(x), Ok(y)) = (&streaming, &whole) {
|
||||
assert_eq!(x, y);
|
||||
}
|
||||
if let Ok(r) = StreamReaderAt::new(&KEY, &input[..], input.len() as u64) {
|
||||
let mut out = vec![0u8; r.size() as usize];
|
||||
let got = r.read_at(&mut out, 0).map(|n| out[..n].to_vec());
|
||||
assert_eq!(got.is_ok(), whole.is_ok());
|
||||
if let (Ok(x), Ok(y)) = (got, whole) {
|
||||
assert_eq!(x, y);
|
||||
}
|
||||
}
|
||||
});
|
||||
@@ -0,0 +1,19 @@
|
||||
// SPDX-License-Identifier: GPL-3.0-or-later
|
||||
//! Walking arbitrary tar input as absorb does never panics, and every name
|
||||
//! it would store is confined.
|
||||
#![no_main]
|
||||
use std::io::Read;
|
||||
|
||||
libfuzzer_sys::fuzz_target!(|data: &[u8]| {
|
||||
let mut a = tar::Archive::new(data);
|
||||
let Ok(entries) = a.entries() else { return };
|
||||
for e in entries.take(64) {
|
||||
let Ok(mut e) = e else { return };
|
||||
let name = e.path_bytes().into_owned();
|
||||
if let Some(n) = std::str::from_utf8(&name).ok().and_then(vault::absorb::safe_name) {
|
||||
assert!(!n.split('/').any(|c| c == ".."));
|
||||
}
|
||||
let mut sink = Vec::new();
|
||||
let _ = e.by_ref().take(1 << 20).read_to_end(&mut sink);
|
||||
}
|
||||
});
|
||||
Reference in New Issue
Block a user