Skip to content

Commit

Permalink
test: Experiment with cargo fuzz (#1764)
Browse files Browse the repository at this point in the history
* test: Experiment with `cargo fuzz`

Far from being ready.

* Fix

* Fixes

* Fixes

* Make clippy happy

* Another clippy fix

* Fuzz `Frame::decode`

* Rework

* Fix clippy

* Try and disable on Windows

* Retry

* Retry

* Try again

* Review suggestions

* Clippy

* Update Cargo.toml

Co-authored-by: Martin Thomson <mt@lowentropy.net>
Signed-off-by: Lars Eggert <lars@eggert.org>

* Update neqo-transport/src/packet/mod.rs

Co-authored-by: Martin Thomson <mt@lowentropy.net>
Signed-off-by: Lars Eggert <lars@eggert.org>

* Rename directory to `fuzz`

* `lazy_static` -> `OnceLock`

* * Add ability to save items to fuzzing corpus via
  `build-fuzzing-corpus` feature.

* Disable RNG and encryption during fuzzing and corpus generation

* Misc. other fixes.

* Clippy

* `DefaultHasher` is not in `std::hash` yet in Rust 1.74

* Add two more fuzzers

* Tweaks

* Don't check coverage for fuzzing code

* Update neqo-common/src/fuzz.rs

Co-authored-by: Martin Thomson <mt@lowentropy.net>
Signed-off-by: Lars Eggert <lars@eggert.org>

* Update neqo-transport/src/connection/mod.rs

Co-authored-by: Martin Thomson <mt@lowentropy.net>
Signed-off-by: Lars Eggert <lars@eggert.org>

* Update neqo-transport/src/connection/mod.rs

Co-authored-by: Martin Thomson <mt@lowentropy.net>
Signed-off-by: Lars Eggert <lars@eggert.org>

* Update neqo-crypto/src/p11.rs

Co-authored-by: Martin Thomson <mt@lowentropy.net>
Signed-off-by: Lars Eggert <lars@eggert.org>

* Fixups

* More fixups

* Remove `fuzz/src/lib.rs` now that #1830 is merged

* Fix merge

* Create corpus directory if it does not exist

* Use a predictable sequence of bytes for `disable-random` instead of just zeros.

* Update neqo-common/src/lib.rs

Co-authored-by: Martin Thomson <mt@lowentropy.net>
Signed-off-by: Lars Eggert <lars@eggert.org>

* Use thread-local instead of atomic

* Only write generated packets, and only ones without test frames

* Build a corpus for `client_initial` and `server_initial` fuzz targets

* Cleanup

---------

Signed-off-by: Lars Eggert <lars@eggert.org>
Co-authored-by: Max Inden <mail@max-inden.de>
Co-authored-by: Martin Thomson <mt@lowentropy.net>
  • Loading branch information
3 people authored Apr 18, 2024
1 parent ccf0302 commit 3797225
Show file tree
Hide file tree
Showing 23 changed files with 391 additions and 21 deletions.
3 changes: 2 additions & 1 deletion .codecov.yml
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
# neqo has no test coverage for its example client and server
# neqo has no test coverage for its example client and server, and for the fuzzing code.
ignore:
- "fuzz"
- "neqo-bin"
- "test-fixture"

Expand Down
1 change: 1 addition & 0 deletions Cargo.toml
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
[workspace]
members = [
"fuzz",
"neqo-bin",
"neqo-common",
"neqo-crypto",
Expand Down
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
52 changes: 52 additions & 0 deletions fuzz/Cargo.toml
Original file line number Diff line number Diff line change
@@ -0,0 +1,52 @@
[package]
name = "fuzz"
authors.workspace = true
homepage.workspace = true
repository.workspace = true
version.workspace = true
edition.workspace = true
rust-version.workspace = true
license.workspace = true

[package.metadata]
cargo-fuzz = true

[dependencies]
neqo-common = { path = "../neqo-common" }
neqo-crypto = { path = "../neqo-crypto" }
neqo-transport = { path = "../neqo-transport" }
test-fixture = { path = "../test-fixture" }

[target.'cfg(not(windows))'.dependencies]
libfuzzer-sys = { version = "0.4" }

[lints]
workspace = true

[[bin]]
name = "packet"
path = "fuzz_targets/packet.rs"
test = false
doc = false
bench = false

[[bin]]
name = "frame"
path = "fuzz_targets/frame.rs"
test = false
doc = false
bench = false

[[bin]]
name = "client_initial"
path = "fuzz_targets/client_initial.rs"
test = false
doc = false
bench = false

[[bin]]
name = "server_initial"
path = "fuzz_targets/server_initial.rs"
test = false
doc = false
bench = false
73 changes: 73 additions & 0 deletions fuzz/fuzz_targets/client_initial.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,73 @@
#![cfg_attr(all(fuzzing, not(windows)), no_main)]

#[cfg(all(fuzzing, not(windows)))]
use libfuzzer_sys::fuzz_target;

#[cfg(all(fuzzing, not(windows)))]
fuzz_target!(|data: &[u8]| {
use neqo_common::{Datagram, Encoder, Role};
use neqo_transport::Version;
use test_fixture::{
default_client, default_server,
header_protection::{
apply_header_protection, decode_initial_header, initial_aead_and_hp,
remove_header_protection,
},
now,
};

let mut client = default_client();
let ci = client.process(None, now()).dgram().expect("a datagram");
let Some((header, d_cid, s_cid, payload)) = decode_initial_header(&ci, Role::Client) else {
return;
};
let (aead, hp) = initial_aead_and_hp(d_cid, Role::Client);
let (_, pn) = remove_header_protection(&hp, header, payload);

let mut payload_enc = Encoder::with_capacity(1200);
payload_enc.encode(data); // Add fuzzed data.

// Make a new header with a 1 byte packet number length.
let mut header_enc = Encoder::new();
header_enc
.encode_byte(0xc0) // Initial with 1 byte packet number.
.encode_uint(4, Version::default().wire_version())
.encode_vec(1, d_cid)
.encode_vec(1, s_cid)
.encode_vvec(&[])
.encode_varint(u64::try_from(payload_enc.len() + aead.expansion() + 1).unwrap())
.encode_byte(u8::try_from(pn).unwrap());

let mut ciphertext = header_enc.as_ref().to_vec();
ciphertext.resize(header_enc.len() + payload_enc.len() + aead.expansion(), 0);
let v = aead
.encrypt(
pn,
header_enc.as_ref(),
payload_enc.as_ref(),
&mut ciphertext[header_enc.len()..],
)
.unwrap();
assert_eq!(header_enc.len() + v.len(), ciphertext.len());
// Pad with zero to get up to 1200.
ciphertext.resize(1200, 0);

apply_header_protection(
&hp,
&mut ciphertext,
(header_enc.len() - 1)..header_enc.len(),
);
let fuzzed_ci = Datagram::new(
ci.source(),
ci.destination(),
ci.tos(),
ci.ttl(),
ciphertext,
);

let mut server = default_server();
let _response = server.process(Some(&fuzzed_ci), now());
});

#[cfg(any(not(fuzzing), windows))]
fn main() {}
17 changes: 17 additions & 0 deletions fuzz/fuzz_targets/frame.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,17 @@
#![cfg_attr(all(fuzzing, not(windows)), no_main)]

#[cfg(all(fuzzing, not(windows)))]
use libfuzzer_sys::fuzz_target;

#[cfg(all(fuzzing, not(windows)))]
fuzz_target!(|data: &[u8]| {
use neqo_common::Decoder;
use neqo_transport::frame::Frame;

// Run the fuzzer
let mut decoder = Decoder::new(data);
let _ = Frame::decode(&mut decoder);
});

#[cfg(any(not(fuzzing), windows))]
fn main() {}
21 changes: 21 additions & 0 deletions fuzz/fuzz_targets/packet.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,21 @@
#![cfg_attr(all(fuzzing, not(windows)), no_main)]

#[cfg(all(fuzzing, not(windows)))]
use libfuzzer_sys::fuzz_target;

#[cfg(all(fuzzing, not(windows)))]
fuzz_target!(|data: &[u8]| {
use std::sync::OnceLock;

use neqo_transport::{packet::PublicPacket, RandomConnectionIdGenerator};

static DECODER: OnceLock<RandomConnectionIdGenerator> = OnceLock::new();
let decoder = DECODER.get_or_init(|| RandomConnectionIdGenerator::new(20));
neqo_crypto::init().unwrap();

// Run the fuzzer
let _ = PublicPacket::decode(data, decoder);
});

#[cfg(any(not(fuzzing), windows))]
fn main() {}
77 changes: 77 additions & 0 deletions fuzz/fuzz_targets/server_initial.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,77 @@
#![cfg_attr(all(fuzzing, not(windows)), no_main)]

#[cfg(all(fuzzing, not(windows)))]
use libfuzzer_sys::fuzz_target;

#[cfg(all(fuzzing, not(windows)))]
fuzz_target!(|data: &[u8]| {
use neqo_common::{Datagram, Encoder, Role};
use neqo_transport::Version;
use test_fixture::{
default_client, default_server,
header_protection::{
apply_header_protection, decode_initial_header, initial_aead_and_hp,
remove_header_protection,
},
now,
};

let mut client = default_client();
let ci = client.process(None, now()).dgram().expect("a datagram");
let mut server = default_server();
let si = server
.process(Some(&ci), now())
.dgram()
.expect("a datagram");

let Some((header, d_cid, s_cid, payload)) = decode_initial_header(&si, Role::Server) else {
return;
};
let (aead, hp) = initial_aead_and_hp(d_cid, Role::Server);
let (_, pn) = remove_header_protection(&hp, header, payload);

let mut payload_enc = Encoder::with_capacity(1200);
payload_enc.encode(data); // Add fuzzed data.

// Make a new header with a 1 byte packet number length.
let mut header_enc = Encoder::new();
header_enc
.encode_byte(0xc0) // Initial with 1 byte packet number.
.encode_uint(4, Version::default().wire_version())
.encode_vec(1, d_cid)
.encode_vec(1, s_cid)
.encode_vvec(&[])
.encode_varint(u64::try_from(payload_enc.len() + aead.expansion() + 1).unwrap())
.encode_byte(u8::try_from(pn).unwrap());

let mut ciphertext = header_enc.as_ref().to_vec();
ciphertext.resize(header_enc.len() + payload_enc.len() + aead.expansion(), 0);
let v = aead
.encrypt(
pn,
header_enc.as_ref(),
payload_enc.as_ref(),
&mut ciphertext[header_enc.len()..],
)
.unwrap();
assert_eq!(header_enc.len() + v.len(), ciphertext.len());
// Pad with zero to get up to 1200.
ciphertext.resize(1200, 0);

apply_header_protection(
&hp,
&mut ciphertext,
(header_enc.len() - 1)..header_enc.len(),
);
let fuzzed_si = Datagram::new(
si.source(),
si.destination(),
si.tos(),
si.ttl(),
ciphertext,
);
let _response = client.process(Some(&fuzzed_si), now());
});

#[cfg(any(not(fuzzing), windows))]
fn main() {}
2 changes: 2 additions & 0 deletions neqo-common/Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -16,6 +16,7 @@ workspace = true
# Sync with https://searchfox.org/mozilla-central/source/Cargo.lock 2024-02-08
enum-map = { version = "2.7", default-features = false }
env_logger = { version = "0.10", default-features = false }
hex = { version = "0.4", default-features = false, features = ["alloc"], optional = true }
log = { workspace = true }
qlog = { workspace = true }
time = { version = "0.3", default-features = false, features = ["formatting"] }
Expand All @@ -26,6 +27,7 @@ test-fixture = { path = "../test-fixture" }

[features]
ci = []
build-fuzzing-corpus = ["hex"]

[target."cfg(windows)".dependencies.winapi]
version = "0.3"
Expand Down
43 changes: 43 additions & 0 deletions neqo-common/src/fuzz.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,43 @@
// Licensed under the Apache License, Version 2.0 <LICENSE-APACHE or
// http://www.apache.org/licenses/LICENSE-2.0> or the MIT license
// <LICENSE-MIT or http://opensource.org/licenses/MIT>, at your
// option. This file may not be copied, modified, or distributed
// except according to those terms.

use std::{
collections::hash_map::DefaultHasher,
fs::File,
hash::{Hash, Hasher},
io::Write,
path::Path,
};

/// Write a data item `data` for the fuzzing target `target` to the fuzzing corpus. The caller needs
/// to make sure that `target` is the correct fuzzing target name for the data written.
///
/// # Panics
///
/// Panics if the corpus directory does not exist or if the corpus item cannot be written.
pub fn write_item_to_fuzzing_corpus(target: &str, data: &[u8]) {
// This bakes in the assumption that we're executing in the root of the neqo workspace.
// Unfortunately, `cargo fuzz` doesn't provide a way to learn the location of the corpus
// directory.
let corpus = Path::new("../fuzz/corpus").join(target);
if !corpus.exists() {
std::fs::create_dir_all(&corpus).expect("failed to create corpus directory");
}

// Hash the data to get a unique name for the corpus item.
let mut hasher = DefaultHasher::new();
data.hash(&mut hasher);
let item_name = hex::encode(hasher.finish().to_be_bytes());
let item_path = corpus.join(item_name);
if item_path.exists() {
// Don't overwrite existing corpus items.
return;
}

// Write the data to the corpus item.
let mut file = File::create(item_path).expect("failed to create corpus item");
Write::write_all(&mut file, data).expect("failed to write to corpus item");
}
4 changes: 4 additions & 0 deletions neqo-common/src/lib.rs
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,8 @@
mod codec;
mod datagram;
pub mod event;
#[cfg(feature = "build-fuzzing-corpus")]
mod fuzz;
pub mod header;
pub mod hrtime;
mod incrdecoder;
Expand All @@ -21,6 +23,8 @@ use std::fmt::Write;

use enum_map::Enum;

#[cfg(feature = "build-fuzzing-corpus")]
pub use self::fuzz::write_item_to_fuzzing_corpus;
pub use self::{
codec::{Decoder, Encoder},
datagram::Datagram,
Expand Down
3 changes: 2 additions & 1 deletion neqo-crypto/Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -30,8 +30,9 @@ toml = { version = "0.5", default-features = false }
test-fixture = { path = "../test-fixture" }

[features]
gecko = ["mozbuild"]
disable-encryption = []
disable-random = []
gecko = ["mozbuild"]

[lib]
# See https://github.com/bheisler/criterion.rs/blob/master/book/src/faq.md#cargo-bench-gives-unrecognized-option-errors-for-valid-command-line-options
Expand Down
Loading

0 comments on commit 3797225

Please sign in to comment.