Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
32 changes: 32 additions & 0 deletions fuzz/Cargo.toml
Original file line number Diff line number Diff line change
@@ -0,0 +1,32 @@
[package]
name = "postcard-fuzz"
version = "0.0.0"
publish = false
edition = "2021"

[package.metadata]
cargo-fuzz = true

[dependencies]
libfuzzer-sys = "0.4"
# The `use-std` feature exposes `to_stdvec`/`from_bytes` on the host target.
postcard = { path = "../source/postcard", features = ["use-std"] }
serde = { version = "1", features = ["derive"] }

# `fuzz/` is its own workspace so it does not collide with the
# postcard root workspace (`Cargo.toml` at the repo root).
[workspace]

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

[[bin]]
name = "postcard_decoder_fuzzer"
path = "fuzz_targets/postcard_decoder_fuzzer.rs"
test = false
doc = false
bench = false
24 changes: 24 additions & 0 deletions fuzz/fuzz_targets/common.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,24 @@
//! Shared recursive value type for the postcard fuzz targets.
//!
//! A self-describing recursive value that exercises every serde type path
//! postcard must serialize/deserialize (unit, bool, int, uint, float, str,
//! bytes, seq, map). Because it is recursive and there is no recursion limit
//! in postcard, a deeply nested input can also reach the stack overflow that
//! OSS-Fuzz is built to surface for Rust.

use std::collections::BTreeMap;

use serde::{Deserialize, Serialize};

#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
pub enum FuzzValue {
Null,
Bool(bool),
Int(i64),
UInt(u64),
Float(f64),
Str(String),
Bytes(Vec<u8>),
Seq(Vec<FuzzValue>),
Map(BTreeMap<String, FuzzValue>),
}
29 changes: 29 additions & 0 deletions fuzz/fuzz_targets/postcard_decoder_fuzzer.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,29 @@
//! postcard decoder fuzzer: pure deserialization coverage over arbitrary
//! (mostly malformed) bytes. Unlike the roundtrip fuzzer, this still reaches
//! deep into the decoder when the input never decodes cleanly, and it drives
//! several common real-world payload shapes for broader code-path coverage.
#![no_main]

use std::collections::HashMap;

use libfuzzer_sys::fuzz_target;

use crate::common::FuzzValue;

mod common;

fuzz_target!(|data: &[u8]| {
// Deep recursive value — catches stack overflow on nesting bombs.
let _ = postcard::from_bytes::<FuzzValue>(data);

// Common real-world payload shapes to broaden code-path coverage.
let _ = postcard::from_bytes::<Vec<(u32, String)>>(data);
let _ = postcard::from_bytes::<HashMap<u16, Vec<u8>>>(data);
let _ = postcard::from_bytes::<Option<(String, [u8; 8], i64)>>(data);
let _ = postcard::from_bytes::<Vec<Vec<Vec<u8>>>>(data);

// COBS-flavored framing (the crate's second decode path).
// NB: `from_bytes_cobs` consumes a mutable slice.
let mut cobs_buf = data.to_vec();
let _ = postcard::from_bytes_cobs::<FuzzValue>(&mut cobs_buf);
});
48 changes: 48 additions & 0 deletions fuzz/fuzz_targets/postcard_roundtrip_fuzzer.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,48 @@
//! postcard roundtrip fuzzer: decode -> re-encode -> re-decode -> assert equal.
//!
//! postcard is a binary, non-self-describing format. The deserializer parses
//! attacker-controlled varint length prefixes, string/map/sequence lengths and
//! COBS framing, so arbitrary input is the correct fuzz surface. A recursive
//! self-describing value (`FuzzValue`) drives every type path. There is no
//! recursion limit, so a deeply-nested input can also trigger a stack overflow
//! here — exactly the class of bug OSS-Fuzz is built to surface for Rust.
#![no_main]

use libfuzzer_sys::fuzz_target;

use crate::common::FuzzValue;

mod common;

/// Assert that `value` encodes and decodes back to the identical value.
fn assert_stable_roundtrip(value: &FuzzValue) {
// Heap path (varint framing).
if let Ok(bytes) = postcard::to_stdvec(value) {
let round = postcard::from_bytes::<FuzzValue>(&bytes);
assert_eq!(round.as_ref(), Ok(value), "varint roundtrip unstable");
}

// Fixed-buffer path (varint framing into a caller-provided slice).
let mut buf = [0u8; 4096];
if let Ok(written) = postcard::to_slice(value, &mut buf) {
let round = postcard::from_bytes::<FuzzValue>(written);
assert_eq!(round.as_ref(), Ok(value), "slice roundtrip unstable");
}

// COBS-framing roundtrip (the crate's second framing scheme).
let mut out = [0u8; 8192];
let mut in_buf = [0u8; 8192];
if let Ok(written) = postcard::to_slice_cobs(value, &mut out) {
in_buf[..written.len()].copy_from_slice(written);
let round = postcard::from_bytes_cobs::<FuzzValue>(&mut in_buf[..written.len()]);
assert_eq!(round.as_ref(), Ok(value), "COBS roundtrip unstable");
}
}

fuzz_target!(|data: &[u8]| {
// Deserialize arbitrary input into a recursive value, then prove the
// encoder and decoder are mutually stable across all three framing paths.
if let Ok(v) = postcard::from_bytes::<FuzzValue>(data) {
assert_stable_roundtrip(&v);
}
});