test: coverage-guided fuzz targets and a nightly fuzz job
rust/fuzz (cargo-fuzz, nightly, its own workspace with the same IronRDP patches) has targets for the Telnet IAC parser, the IRC parser + state machine + mIRC formatter, VNC framebuffer updates and decoders, ~/.ssh/config parsing and the update manifest. They reach the private parsers through hidden entry points behind nexus-core's 'fuzzing' feature. A nightly workflow runs each for two minutes and uploads any crashing input. A first 45 s run of each locally found nothing.
This commit is contained in:
@@ -0,0 +1,32 @@
|
|||||||
|
name: Fuzz
|
||||||
|
|
||||||
|
on:
|
||||||
|
schedule:
|
||||||
|
- cron: "17 3 * * *"
|
||||||
|
workflow_dispatch:
|
||||||
|
|
||||||
|
permissions:
|
||||||
|
contents: read
|
||||||
|
|
||||||
|
jobs:
|
||||||
|
fuzz:
|
||||||
|
name: Fuzz (${{ matrix.target }})
|
||||||
|
runs-on: ubuntu-22.04
|
||||||
|
strategy:
|
||||||
|
fail-fast: false
|
||||||
|
matrix:
|
||||||
|
target: [telnet, irc, vnc, sshconfig, updater]
|
||||||
|
defaults:
|
||||||
|
run:
|
||||||
|
working-directory: rust/fuzz
|
||||||
|
steps:
|
||||||
|
- uses: actions/checkout@11d5960a326750d5838078e36cf38b85af677262 # v4.4.0
|
||||||
|
- run: rustup toolchain install nightly --profile minimal
|
||||||
|
- run: cargo +nightly install cargo-fuzz --locked
|
||||||
|
- run: cargo +nightly fuzz run ${{ matrix.target }} -- -max_total_time=120 -rss_limit_mb=2048
|
||||||
|
- name: Upload crashing inputs
|
||||||
|
if: failure()
|
||||||
|
uses: actions/upload-artifact@ea165f8d65b6e75b540449e92b4886f43607fa02 # v4.6.2
|
||||||
|
with:
|
||||||
|
name: fuzz-${{ matrix.target }}
|
||||||
|
path: rust/fuzz/artifacts
|
||||||
@@ -1,6 +1,7 @@
|
|||||||
[workspace]
|
[workspace]
|
||||||
resolver = "3"
|
resolver = "3"
|
||||||
members = ["core", "app"]
|
members = ["core", "app"]
|
||||||
|
exclude = ["fuzz"]
|
||||||
|
|
||||||
[workspace.package]
|
[workspace.package]
|
||||||
version = "0.1.0"
|
version = "0.1.0"
|
||||||
|
|||||||
@@ -3,6 +3,10 @@ name = "nexus-core"
|
|||||||
version.workspace = true
|
version.workspace = true
|
||||||
edition.workspace = true
|
edition.workspace = true
|
||||||
|
|
||||||
|
[features]
|
||||||
|
# Exposes parser entry points for rust/fuzz (cargo-fuzz).
|
||||||
|
fuzzing = []
|
||||||
|
|
||||||
[dependencies]
|
[dependencies]
|
||||||
serde = { version = "1", features = ["derive"] }
|
serde = { version = "1", features = ["derive"] }
|
||||||
serde_json = "1"
|
serde_json = "1"
|
||||||
|
|||||||
@@ -1046,6 +1046,25 @@ fn split_first_arg(input: &str) -> (&str, &str) {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// Fuzzing entry point: each line of `data` through the parser, the state machine (as a
|
||||||
|
/// client in #a) and the mIRC formatter.
|
||||||
|
#[cfg(feature = "fuzzing")]
|
||||||
|
#[doc(hidden)]
|
||||||
|
pub fn fuzz_lines(data: &[u8]) {
|
||||||
|
let client = Client::new(Config::default());
|
||||||
|
client.st().nick = "me".into();
|
||||||
|
if let Some(m) = parse_line(":me!u@h JOIN #a") {
|
||||||
|
client.handle_message(&m);
|
||||||
|
}
|
||||||
|
for line in String::from_utf8_lossy(data).lines() {
|
||||||
|
if let Some(m) = parse_line(line) {
|
||||||
|
client.handle_message(&m);
|
||||||
|
let e = Event { kind: "message".into(), target: m.param(0).into(), from: m.source.clone(), text: m.last().into(), ..Default::default() };
|
||||||
|
let _ = crate::ircformat::format_event(&e, "12:00");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
#[cfg(test)]
|
#[cfg(test)]
|
||||||
mod tests {
|
mod tests {
|
||||||
use super::*;
|
use super::*;
|
||||||
|
|||||||
@@ -314,6 +314,17 @@ fn encode_outgoing(data: &[u8], encoding: &str) -> Vec<u8> {
|
|||||||
if super::is_latin1(encoding) { super::utf8_to_latin1(data) } else { data.to_vec() }
|
if super::is_latin1(encoding) { super::utf8_to_latin1(data) } else { data.to_vec() }
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// Fuzzing entry point: `data` through the IAC negotiation parser, in uneven chunks.
|
||||||
|
#[cfg(feature = "fuzzing")]
|
||||||
|
#[doc(hidden)]
|
||||||
|
pub fn fuzz_negotiation(data: &[u8]) {
|
||||||
|
let c = TelnetClient::new(&crate::models::Profile::new(crate::models::Protocol::Telnet), Callbacks::noop(), None);
|
||||||
|
let mut pending = Vec::new();
|
||||||
|
for chunk in data.chunks(37) {
|
||||||
|
c.inner.process(&mut pending, chunk);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
#[cfg(test)]
|
#[cfg(test)]
|
||||||
mod tests {
|
mod tests {
|
||||||
use super::*;
|
use super::*;
|
||||||
|
|||||||
@@ -861,6 +861,19 @@ pub fn keysym_for(name: &str, ch: Option<char>) -> Option<u32> {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// Fuzzing entry point: `data` as a stream of FramebufferUpdate messages into a 64x64 desktop.
|
||||||
|
#[cfg(feature = "fuzzing")]
|
||||||
|
#[doc(hidden)]
|
||||||
|
pub fn fuzz_updates(data: &[u8]) {
|
||||||
|
let (mut fb, mut dec) = (Framebuffer::new(64, 64), Decoders::default());
|
||||||
|
let mut r = data;
|
||||||
|
while !r.is_empty() && read_update(&mut r, &mut fb, &mut dec).is_ok() {
|
||||||
|
if fb.width as usize * fb.height as usize > 1 << 20 {
|
||||||
|
fb = Framebuffer::new(64, 64);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
#[cfg(test)]
|
#[cfg(test)]
|
||||||
mod tests {
|
mod tests {
|
||||||
use super::*;
|
use super::*;
|
||||||
|
|||||||
@@ -171,6 +171,15 @@ fn is_trueish(v: &str) -> bool {
|
|||||||
matches!(v.to_lowercase().as_str(), "yes" | "true" | "on" | "1")
|
matches!(v.to_lowercase().as_str(), "yes" | "true" | "on" | "1")
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// Fuzzing entry point: `data` as an ~/.ssh/config file, then into profiles.
|
||||||
|
#[cfg(feature = "fuzzing")]
|
||||||
|
#[doc(hidden)]
|
||||||
|
pub fn fuzz_parse(data: &[u8]) {
|
||||||
|
if let Ok(blocks) = parse(&String::from_utf8_lossy(data)) {
|
||||||
|
let _ = to_profiles(&blocks, "f");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
#[cfg(test)]
|
#[cfg(test)]
|
||||||
mod tests {
|
mod tests {
|
||||||
use super::*;
|
use super::*;
|
||||||
|
|||||||
@@ -142,6 +142,13 @@ fn leading_int(s: Option<&&str>) -> i64 {
|
|||||||
}
|
}
|
||||||
|
|
||||||
/// Cuts at `max` bytes (backing off to a char boundary) and appends "…".
|
/// Cuts at `max` bytes (backing off to a char boundary) and appends "…".
|
||||||
|
/// Fuzzing entry point: `data` as an update manifest response.
|
||||||
|
#[cfg(feature = "fuzzing")]
|
||||||
|
#[doc(hidden)]
|
||||||
|
pub fn fuzz_manifest(data: &[u8]) {
|
||||||
|
let _ = parse_manifest(data, "1.2.3");
|
||||||
|
}
|
||||||
|
|
||||||
#[cfg(test)]
|
#[cfg(test)]
|
||||||
mod tests {
|
mod tests {
|
||||||
use super::*;
|
use super::*;
|
||||||
|
|||||||
@@ -0,0 +1,4 @@
|
|||||||
|
target/
|
||||||
|
corpus/
|
||||||
|
artifacts/
|
||||||
|
coverage/
|
||||||
Generated
+5115
File diff suppressed because it is too large
Load Diff
@@ -0,0 +1,60 @@
|
|||||||
|
[package]
|
||||||
|
name = "nexus-fuzz"
|
||||||
|
version = "0.0.0"
|
||||||
|
publish = false
|
||||||
|
edition = "2024"
|
||||||
|
|
||||||
|
[package.metadata]
|
||||||
|
cargo-fuzz = true
|
||||||
|
|
||||||
|
[dependencies]
|
||||||
|
libfuzzer-sys = "0.4"
|
||||||
|
nexus-core = { path = "../core", features = ["fuzzing"] }
|
||||||
|
|
||||||
|
# Its own workspace (cargo-fuzz needs nightly and its own profile), so the root's IronRDP
|
||||||
|
# patches are repeated here; see ../vendor/README.md.
|
||||||
|
[workspace]
|
||||||
|
members = ["."]
|
||||||
|
|
||||||
|
[patch.crates-io]
|
||||||
|
picky = { path = "../vendor/picky" }
|
||||||
|
sspi = { path = "../vendor/sspi" }
|
||||||
|
ironrdp-session = { path = "../vendor/ironrdp-session" }
|
||||||
|
|
||||||
|
[profile.release]
|
||||||
|
debug = 1
|
||||||
|
|
||||||
|
[[bin]]
|
||||||
|
name = "telnet"
|
||||||
|
path = "fuzz_targets/telnet.rs"
|
||||||
|
test = false
|
||||||
|
doc = false
|
||||||
|
bench = false
|
||||||
|
|
||||||
|
[[bin]]
|
||||||
|
name = "irc"
|
||||||
|
path = "fuzz_targets/irc.rs"
|
||||||
|
test = false
|
||||||
|
doc = false
|
||||||
|
bench = false
|
||||||
|
|
||||||
|
[[bin]]
|
||||||
|
name = "vnc"
|
||||||
|
path = "fuzz_targets/vnc.rs"
|
||||||
|
test = false
|
||||||
|
doc = false
|
||||||
|
bench = false
|
||||||
|
|
||||||
|
[[bin]]
|
||||||
|
name = "sshconfig"
|
||||||
|
path = "fuzz_targets/sshconfig.rs"
|
||||||
|
test = false
|
||||||
|
doc = false
|
||||||
|
bench = false
|
||||||
|
|
||||||
|
[[bin]]
|
||||||
|
name = "updater"
|
||||||
|
path = "fuzz_targets/updater.rs"
|
||||||
|
test = false
|
||||||
|
doc = false
|
||||||
|
bench = false
|
||||||
@@ -0,0 +1,3 @@
|
|||||||
|
#![no_main]
|
||||||
|
|
||||||
|
libfuzzer_sys::fuzz_target!(|data: &[u8]| nexus_core::clients::irc::fuzz_lines(data));
|
||||||
@@ -0,0 +1,3 @@
|
|||||||
|
#![no_main]
|
||||||
|
|
||||||
|
libfuzzer_sys::fuzz_target!(|data: &[u8]| nexus_core::sshconfig::fuzz_parse(data));
|
||||||
@@ -0,0 +1,3 @@
|
|||||||
|
#![no_main]
|
||||||
|
|
||||||
|
libfuzzer_sys::fuzz_target!(|data: &[u8]| nexus_core::clients::telnet::fuzz_negotiation(data));
|
||||||
@@ -0,0 +1,3 @@
|
|||||||
|
#![no_main]
|
||||||
|
|
||||||
|
libfuzzer_sys::fuzz_target!(|data: &[u8]| nexus_core::updater::fuzz_manifest(data));
|
||||||
@@ -0,0 +1,3 @@
|
|||||||
|
#![no_main]
|
||||||
|
|
||||||
|
libfuzzer_sys::fuzz_target!(|data: &[u8]| nexus_core::clients::vnc::fuzz_updates(data));
|
||||||
Reference in New Issue
Block a user