Add Fuzzing + fix found issues (#464)

* add fuzz tests

* fix CI

* fix found vuln

* fix deny

* add more targets

* fmt + clippy

* Add exclude for fuzz

* add defaults rather than exclude

* add fuzz to CI

* remove clippy run

* add readme

* review fixes

* Update Cargo.toml

Co-authored-by: hinto-janai <hinto.janai@protonmail.com>

---------

Co-authored-by: hinto-janai <hinto.janai@protonmail.com>
This commit is contained in:
Boog900
2025-05-28 20:29:06 +01:00
committed by GitHub
parent 118f02d52e
commit 1b28c3b728
21 changed files with 464 additions and 7 deletions

View File

@@ -118,21 +118,21 @@ jobs:
uses: lukka/get-cmake@v3.31.6 # Needed for `randomx-rs`
- name: Documentation
run: cargo doc --workspace --all-features --no-deps
run: cargo doc --all-features --no-deps
- name: Clippy (fail on warnings)
run: cargo clippy --workspace --all-features --all-targets -- -D warnings
run: cargo clippy --all-features --all-targets -- -D warnings
# HACK: how to test both DB backends that are feature-gated?
- name: Test
run: |
cargo test --all-features --workspace
cargo test --all-features
cargo test --package cuprate-blockchain --no-default-features --features redb
- name: Build
run: cargo build --all-features --all-targets --workspace
run: cargo build --all-features --all-targets
- name: Hack Check
run: |
cargo install cargo-hack --locked
cargo hack --workspace check --feature-powerset --no-dev-deps
cargo hack check --feature-powerset --no-dev-deps

57
.github/workflows/fuzz.yml vendored Normal file
View File

@@ -0,0 +1,57 @@
name: Smoke-Test Fuzz Targets
on:
push:
branches: [ "main" ]
pull_request:
branches: [ "main" ]
jobs:
fuzz:
runs-on: ubuntu-latest
env:
# The version of `cargo-fuzz` to install and use.
CARGO_FUZZ_VERSION: 0.12.0
# The number of seconds to run the fuzz target.
FUZZ_TIME: 60
# We need to turn lto off for fuzzing.
CARGO_PROFILE_RELEASE_LTO: false
strategy:
matrix:
include:
- fuzz_target: cryptonight
- fuzz_target: epee_encoding
- fuzz_target: epee_p2p_messages
- fuzz_target: levin_codec
- fuzz_target: oxide_block
- fuzz_target: oxide_tx
steps:
- uses: actions/checkout@v4
# Install the nightly Rust channel.
- run: rustup toolchain install nightly
- run: rustup default nightly
# Install and cache `cargo-fuzz`.
- uses: actions/cache@v4
with:
path: ${{ runner.tool_cache }}/cargo-fuzz
key: cargo-fuzz-bin-${{ env.CARGO_FUZZ_VERSION }}
- run: echo "${{ runner.tool_cache }}/cargo-fuzz/bin" >> $GITHUB_PATH
- run: cargo install --root "${{ runner.tool_cache }}/cargo-fuzz" --version ${{ env.CARGO_FUZZ_VERSION }} cargo-fuzz --locked
# Build and then run the fuzz target.
- run: cargo fuzz build ${{ matrix.fuzz_target }} -O
- run: cargo fuzz run ${{ matrix.fuzz_target }} -O -- -max_total_time=${{ env.FUZZ_TIME }}
# Upload fuzzing artifacts on failure for post-mortem debugging.
- uses: actions/upload-artifact@v4
if: failure()
with:
name: fuzzing-artifacts-${{ matrix.fuzz_target }}-${{ github.sha }}
path: fuzz/artifacts

2
.gitignore vendored
View File

@@ -4,3 +4,5 @@ monerod
books/*/book
fast_sync_hashes.bin
/books/user/Cuprated.toml
fuzz/corpus
fuzz/artifacts

58
Cargo.lock generated
View File

@@ -56,6 +56,15 @@ version = "1.0.98"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "e16d2d3311acee920a9eb8d33b8cbc1787ce4a264e85f964c2404b969bdcd487"
[[package]]
name = "arbitrary"
version = "1.4.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "dde20b3d026af13f561bdd0f15edf01fc734f0dafcedbaf42bba506a9517f223"
dependencies = [
"derive_arbitrary",
]
[[package]]
name = "arc-swap"
version = "1.7.1"
@@ -358,6 +367,8 @@ version = "1.2.22"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "32db95edf998450acc7881c932f94cd9b05c87b4b2599e8bab064753da4acfd1"
dependencies = [
"jobserver",
"libc",
"shlex",
]
@@ -815,6 +826,20 @@ dependencies = [
"thiserror",
]
[[package]]
name = "cuprate-fuzz"
version = "0.0.0"
dependencies = [
"bytes",
"cuprate-cryptonight",
"cuprate-epee-encoding",
"cuprate-levin",
"cuprate-wire",
"libfuzzer-sys",
"monero-serai",
"tokio-util",
]
[[package]]
name = "cuprate-helper"
version = "0.1.0"
@@ -856,6 +881,7 @@ dependencies = [
name = "cuprate-levin"
version = "0.1.0"
dependencies = [
"arbitrary",
"bitflags 2.9.0",
"bytes",
"cfg-if",
@@ -1060,6 +1086,7 @@ dependencies = [
name = "cuprate-wire"
version = "0.1.0"
dependencies = [
"arbitrary",
"bitflags 2.9.0",
"bytes",
"cuprate-epee-encoding",
@@ -1227,6 +1254,17 @@ dependencies = [
"powerfmt",
]
[[package]]
name = "derive_arbitrary"
version = "1.4.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "30542c1ad912e0e3d22a1935c290e12e8a29d704a420177a31faad4a601a0800"
dependencies = [
"proc-macro2",
"quote",
"syn 2.0.101",
]
[[package]]
name = "diff"
version = "0.1.13"
@@ -1880,6 +1918,16 @@ dependencies = [
"ppv-lite86",
]
[[package]]
name = "jobserver"
version = "0.1.33"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "38f262f097c174adebe41eb73d66ae9c06b2844fb0da69969647bbddd9b0538a"
dependencies = [
"getrandom 0.3.3",
"libc",
]
[[package]]
name = "js-sys"
version = "0.3.77"
@@ -1926,6 +1974,16 @@ version = "0.2.172"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "d750af042f7ef4f724306de029d18836c26c1765a54a6a3f094cbd23a7267ffa"
[[package]]
name = "libfuzzer-sys"
version = "0.4.9"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "cf78f52d400cf2d84a3a973a78a592b4adc535739e0a5597a0da6f0c357adc75"
dependencies = [
"arbitrary",
"cc",
]
[[package]]
name = "libm"
version = "0.2.15"

View File

@@ -49,6 +49,64 @@ members = [
"helper",
"pruning",
"test-utils",
# Fuzz
"fuzz"
]
# Windows is a pain, so instead of making the fuzz tests work on windows, we just don't build the fuzz target on windows.
# This is done by defining the `default-members` here to include everything but `fuzz`.
# You can still build everything, including `fuzz` by using `--workspace`.
#
# Remember to update this if you add any new crates!
default-members = [
# Binaries
"binaries/cuprated",
# Consensus
"consensus",
"consensus/context",
"consensus/fast-sync",
"consensus/rules",
# Net
"net/epee-encoding",
"net/levin",
"net/wire",
# P2P
"p2p/p2p",
"p2p/p2p-core",
"p2p/bucket",
"p2p/dandelion-tower",
"p2p/async-buffer",
"p2p/address-book",
# Storage
"storage/blockchain",
"storage/service",
"storage/txpool",
"storage/database",
# Types
"types/types",
"types/hex",
"types/fixed-bytes",
# RPC
"rpc/json-rpc",
"rpc/types",
"rpc/interface",
# ZMQ
"zmq/types",
# Misc
"constants",
"cryptonight",
"helper",
"pruning",
"test-utils",
]
[profile.release]
@@ -160,6 +218,7 @@ pretty_assertions = { version = "1" }
proptest = { version = "1" }
proptest-derive = { version = "0.5" }
tokio-test = { version = "0.4" }
arbitrary = { version = "1" }
## TODO:
## Potential dependencies.

View File

@@ -107,6 +107,7 @@ allow = [
"BSD-3-Clause", # https://tldrlegal.com/license/bsd-3-clause-license-(revised)
"ISC", # https://tldrlegal.com/license/isc-license
"MIT", # https://tldrlegal.com/license/mit-license
"NCSA", # https://opensource.org/license/uoi-ncsa-php
# Must include copyright notice/attribution/changes/etc
"Apache-2.0 WITH LLVM-exception", # https://spdx.org/licenses/LLVM-exception.html

63
fuzz/Cargo.toml Normal file
View File

@@ -0,0 +1,63 @@
[package]
name = "cuprate-fuzz"
version = "0.0.0"
publish = false
edition = "2021"
license = "MIT"
[package.metadata]
cargo-fuzz = true
[dependencies]
libfuzzer-sys = "0.4"
cuprate-epee-encoding = { workspace = true, features = ["std"]}
cuprate-cryptonight = { workspace = true }
cuprate-levin = { workspace = true, features = ["arbitrary"] }
cuprate-wire = { workspace = true, features = ["arbitrary"] }
monero-serai = { workspace = true }
bytes = { workspace = true }
tokio-util = { workspace = true }
[[bin]]
name = "epee_encoding"
path = "fuzz_targets/epee_encoding.rs"
test = false
doc = false
bench = false
[[bin]]
name = "epee_p2p_messages"
path = "fuzz_targets/epee_p2p_messages.rs"
test = false
doc = false
bench = false
[[bin]]
name = "cryptonight"
path = "fuzz_targets/cryptonight.rs"
test = false
doc = false
bench = false
[[bin]]
name = "levin_codec"
path = "fuzz_targets/levin_codec.rs"
test = false
doc = false
bench = false
[[bin]]
name = "oxide_block"
path = "fuzz_targets/oxide_block.rs"
test = false
doc = false
bench = false
[[bin]]
name = "oxide_tx"
path = "fuzz_targets/oxide_tx.rs"
test = false
doc = false
bench = false

43
fuzz/README.md Normal file
View File

@@ -0,0 +1,43 @@
# Fuzz Tests
This folder contains the fuzz tests for crates that make up `cuprated`. To run you will need Rust and `cargo-fuzz`
installed, the instructions for installing `cargo-fuzz` can be found here: https://rust-fuzz.github.io/book/cargo-fuzz/setup.html.
Once you have `cargo-fuzz` and have switched to the nightly compiler, you can list the possible targets with:
```
cargo fuzz list
```
Now you can pick a target to fuzz and fuzz it with:
```
CARGO_PROFILE_RELEASE_LTO=false cargo fuzz run $TARGET -O
```
for example to fuzz the `levin_codec` target you would do:
```
CARGO_PROFILE_RELEASE_LTO=false cargo fuzz run levin_codec -O
```
`CARGO_PROFILE_RELEASE_LTO=false` is needed to disable lto, which is not supported when fuzzing, `-O` enables optimisations.
You can use `-j X` to increase the number of concurrent jobs.
## Adding New Tests
To add new tests, create a new `.rs` file in `fuzz_targets`.
Then add an entry in `Cargo.toml`, for example:
```toml
[[bin]]
name = "oxide_tx"
path = "fuzz_targets/oxide_tx.rs"
test = false
doc = false
bench = false
```
Then update the CI file `fuzz` with the new fuzz_target.

View File

@@ -0,0 +1,33 @@
#![no_main]
use libfuzzer_sys::{fuzz_target, Corpus};
fuzz_target!(|data: &[u8]| -> Corpus {
if data.is_empty() {
return Corpus::Reject;
}
match data[0] % 4 {
0 => {
cuprate_cryptonight::cryptonight_hash_v0(&data[1..]);
}
1 => {
let _ = cuprate_cryptonight::cryptonight_hash_v1(&data[1..]);
}
2 => {
cuprate_cryptonight::cryptonight_hash_v2(&data[1..]);
}
_ => {
if data.len() < 9 {
return Corpus::Reject;
}
cuprate_cryptonight::cryptonight_hash_r(
&data[9..],
u64::from_le_bytes(data[1..9].try_into().unwrap()),
);
}
}
Corpus::Keep
});

View File

@@ -0,0 +1,76 @@
#![no_main]
use bytes::{Bytes, BytesMut};
use libfuzzer_sys::fuzz_target;
use cuprate_epee_encoding::{epee_object, from_bytes};
const HEADER: &[u8] = b"\x01\x11\x01\x01\x01\x01\x02\x01\x01";
struct T {
a: u8,
b: u16,
c: u32,
d: u64,
e: i8,
f: i16,
g: i32,
h: i64,
i: f64,
j: String,
k: bool,
l: Vec<u8>,
m: Vec<u16>,
n: Vec<u32>,
o: Vec<u64>,
p: Vec<i8>,
q: Vec<i16>,
r: Vec<i32>,
s: Vec<i64>,
t: Vec<f64>,
u: Vec<String>,
v: Vec<bool>,
w: Vec<T>,
x: Vec<[u8; 32]>,
y: Bytes,
z: BytesMut,
}
epee_object! (
T,
a: u8,
b: u16,
c: u32,
d: u64,
e: i8,
f: i16,
g: i32,
h: i64,
i: f64,
j: String,
k: bool,
l: Vec<u8>,
m: Vec<u16>,
n: Vec<u32>,
o: Vec<u64>,
p: Vec<i8>,
q: Vec<i16>,
r: Vec<i32>,
s: Vec<i64>,
t: Vec<f64>,
u: Vec<String>,
v: Vec<bool>,
w: Vec<T>,
x: Vec<[u8; 32]>,
y: Bytes,
z: BytesMut,
);
fuzz_target!(|data: &[u8]| {
let data = [HEADER, data].concat();
drop(from_bytes::<T, _>(&mut data.as_slice()));
});

View File

@@ -0,0 +1,18 @@
#![no_main]
use libfuzzer_sys::fuzz_target;
use cuprate_levin::{LevinBody, MessageType};
use cuprate_wire::{LevinCommand, Message};
const HEADER: &[u8] = b"\x01\x11\x01\x01\x01\x01\x02\x01\x01";
fuzz_target!(|data: (&[u8], MessageType, LevinCommand)| {
let bytes = [HEADER, data.0].concat();
drop(Message::decode_message(
&mut bytes.as_slice(),
data.1,
data.2,
));
});

View File

@@ -0,0 +1,21 @@
#![no_main]
use bytes::{BufMut, BytesMut};
use libfuzzer_sys::fuzz_target;
use tokio_util::codec::Decoder;
use cuprate_levin::BucketHead;
use cuprate_wire::{LevinCommand, MoneroWireCodec};
fuzz_target!(|data: Vec<(BucketHead<LevinCommand>, Vec<u8>)>| {
let mut codec = MoneroWireCodec::default();
for (bucket, body) in data {
let mut bytes = BytesMut::new();
bucket.write_bytes_into(&mut bytes);
bytes.put_slice(&body);
drop(codec.decode(&mut bytes));
}
});

View File

@@ -0,0 +1,9 @@
#![no_main]
use libfuzzer_sys::fuzz_target;
use monero_serai::block::Block;
fuzz_target!(|data: &[u8]| {
drop(Block::read(&mut &data[..]));
});

View File

@@ -0,0 +1,9 @@
#![no_main]
use libfuzzer_sys::fuzz_target;
use monero_serai::transaction::{NotPruned, Transaction};
fuzz_target!(|data: &[u8]| {
drop(Transaction::<NotPruned>::read(&mut &data[..]));
});

View File

@@ -7,7 +7,7 @@ authors = ["Boog900"]
readme = "README.md"
keywords = ["monero", "epee", "no-std"]
description = "Epee binary format library."
repository = "https://github.com/Boog900/epee-encoding"
repository = "https://github.com/Cuprate/cuprate/tree/main/net/epee-encoding"
[features]
default = ["std"]

View File

@@ -154,7 +154,7 @@ fn read_head_object<T: EpeeObject, B: Buf>(r: &mut B) -> Result<T> {
}
fn read_field_name_bytes<B: Buf>(r: &mut B) -> Result<Bytes> {
let len: usize = r.get_u8().into();
let len: usize = checked_read_primitive(r, Buf::get_u8)?.into();
checked_read(r, |b: &mut B| b.copy_to_bytes(len), len)
}

View File

@@ -22,6 +22,8 @@ tokio-util = { workspace = true, features = ["codec"]}
tracing = { workspace = true, features = ["std"], optional = true }
arbitrary = { workspace = true, features = ["derive"], optional = true }
[dev-dependencies]
proptest = { workspace = true }
rand = { workspace = true, features = ["std", "std_rng"] }

View File

@@ -26,6 +26,7 @@ pub const HEADER_SIZE: usize = 33;
/// Levin header flags
#[derive(Debug, Default, PartialEq, Eq, Clone, Copy)]
#[cfg_attr(feature = "arbitrary", derive(arbitrary::Arbitrary))]
pub struct Flags(u32);
bitflags! {
@@ -74,6 +75,7 @@ impl From<Flags> for u32 {
/// The Header of a Bucket. This contains
#[derive(Debug, PartialEq, Eq, Clone, Copy)]
#[cfg_attr(feature = "arbitrary", derive(arbitrary::Arbitrary))]
pub struct BucketHead<C> {
/// The network signature, should be `LEVIN_SIGNATURE` for Monero
pub signature: u64,

View File

@@ -129,6 +129,7 @@ pub struct Bucket<C> {
/// An enum representing if the message is a request, response or notification.
#[derive(Debug, Eq, PartialEq, Clone, Copy)]
#[cfg_attr(feature = "arbitrary", derive(arbitrary::Arbitrary))]
pub enum MessageType {
/// Request
Request,

View File

@@ -21,6 +21,8 @@ bitflags = { workspace = true, features = ["std"] }
bytes = { workspace = true, features = ["std"] }
thiserror = { workspace = true }
arbitrary = { workspace = true, features = ["derive"], optional = true }
[dev-dependencies]
hex = { workspace = true, features = ["std"]}

View File

@@ -34,6 +34,7 @@ pub use common::{BasicNodeData, CoreSyncData, PeerListEntryBase};
use protocol::*;
#[derive(Copy, Clone, Eq, PartialEq, Debug)]
#[cfg_attr(feature = "arbitrary", derive(arbitrary::Arbitrary))]
pub enum LevinCommand {
Handshake,
TimedSync,