feat: fuzz-harness (#20)
This commit is contained in:
parent
75aefa0c49
commit
b828ff0fae
5 changed files with 1486 additions and 1 deletions
4
.gitignore
vendored
4
.gitignore
vendored
|
|
@ -1,5 +1,7 @@
|
|||
**/target
|
||||
**/node_modules/
|
||||
.vscode/
|
||||
**/.venv
|
||||
.venv
|
||||
__pycache__
|
||||
fuzz/artifacts/**
|
||||
corpus
|
||||
|
|
|
|||
197
fuzz/Cargo.lock
generated
Normal file
197
fuzz/Cargo.lock
generated
Normal file
|
|
@ -0,0 +1,197 @@
|
|||
# This file is automatically @generated by Cargo.
|
||||
# It is not intended for manual editing.
|
||||
version = 4
|
||||
|
||||
[[package]]
|
||||
name = "arbitrary"
|
||||
version = "1.4.2"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "c3d036a3c4ab069c7b410a2ce876bd74808d2d0888a82667669f8e783a898bf1"
|
||||
dependencies = [
|
||||
"derive_arbitrary",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "cc"
|
||||
version = "1.2.55"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "47b26a0954ae34af09b50f0de26458fa95369a0d478d8236d3f93082b219bd29"
|
||||
dependencies = [
|
||||
"find-msvc-tools",
|
||||
"jobserver",
|
||||
"libc",
|
||||
"shlex",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "cfg-if"
|
||||
version = "1.0.4"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "9330f8b2ff13f34540b44e946ef35111825727b38d33286ef986142615121801"
|
||||
|
||||
[[package]]
|
||||
name = "crossbeam-queue"
|
||||
version = "0.3.12"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "0f58bbc28f91df819d0aa2a2c00cd19754769c2fad90579b3592b1c9ba7a3115"
|
||||
dependencies = [
|
||||
"crossbeam-utils",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "crossbeam-utils"
|
||||
version = "0.8.21"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "d0a5c400df2834b80a4c3327b3aad3a4c4cd4de0629063962b03235697506a28"
|
||||
|
||||
[[package]]
|
||||
name = "derive_arbitrary"
|
||||
version = "1.4.2"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "1e567bd82dcff979e4b03460c307b3cdc9e96fde3d73bed1496d2bc75d9dd62a"
|
||||
dependencies = [
|
||||
"proc-macro2",
|
||||
"quote",
|
||||
"syn",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "find-msvc-tools"
|
||||
version = "0.1.9"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "5baebc0774151f905a1a2cc41989300b1e6fbb29aff0ceffa1064fdd3088d582"
|
||||
|
||||
[[package]]
|
||||
name = "getrandom"
|
||||
version = "0.2.17"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "ff2abc00be7fca6ebc474524697ae276ad847ad0a6b3faa4bcb027e9a4614ad0"
|
||||
dependencies = [
|
||||
"cfg-if",
|
||||
"libc",
|
||||
"wasi",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "getrandom"
|
||||
version = "0.3.4"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "899def5c37c4fd7b2664648c28120ecec138e4d395b459e5ca34f9cce2dd77fd"
|
||||
dependencies = [
|
||||
"cfg-if",
|
||||
"libc",
|
||||
"r-efi",
|
||||
"wasip2",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "jobserver"
|
||||
version = "0.1.34"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "9afb3de4395d6b3e67a780b6de64b51c978ecf11cb9a462c66be7d4ca9039d33"
|
||||
dependencies = [
|
||||
"getrandom 0.3.4",
|
||||
"libc",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "libc"
|
||||
version = "0.2.180"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "bcc35a38544a891a5f7c865aca548a982ccb3b8650a5b06d0fd33a10283c56fc"
|
||||
|
||||
[[package]]
|
||||
name = "libfuzzer-sys"
|
||||
version = "0.4.10"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "5037190e1f70cbeef565bd267599242926f724d3b8a9f510fd7e0b540cfa4404"
|
||||
dependencies = [
|
||||
"arbitrary",
|
||||
"cc",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "proc-macro2"
|
||||
version = "1.0.106"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "8fd00f0bb2e90d81d1044c2b32617f68fcb9fa3bb7640c23e9c748e53fb30934"
|
||||
dependencies = [
|
||||
"unicode-ident",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "quote"
|
||||
version = "1.0.44"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "21b2ebcf727b7760c461f091f9f0f539b77b8e87f2fd88131e7f1b433b3cece4"
|
||||
dependencies = [
|
||||
"proc-macro2",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "r-efi"
|
||||
version = "5.3.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "69cdb34c158ceb288df11e18b4bd39de994f6657d83847bdffdbd7f346754b0f"
|
||||
|
||||
[[package]]
|
||||
name = "shlex"
|
||||
version = "1.3.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "0fda2ff0d084019ba4d7c6f371c95d8fd75ce3524c3cb8fb653a3023f6323e64"
|
||||
|
||||
[[package]]
|
||||
name = "swactor"
|
||||
version = "0.1.0"
|
||||
dependencies = [
|
||||
"crossbeam-queue",
|
||||
"crossbeam-utils",
|
||||
"getrandom 0.2.17",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "swactor-fuzz"
|
||||
version = "0.0.0"
|
||||
dependencies = [
|
||||
"arbitrary",
|
||||
"libfuzzer-sys",
|
||||
"swactor",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "syn"
|
||||
version = "2.0.114"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "d4d107df263a3013ef9b1879b0df87d706ff80f65a86ea879bd9c31f9b307c2a"
|
||||
dependencies = [
|
||||
"proc-macro2",
|
||||
"quote",
|
||||
"unicode-ident",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "unicode-ident"
|
||||
version = "1.0.22"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "9312f7c4f6ff9069b165498234ce8be658059c6728633667c526e27dc2cf1df5"
|
||||
|
||||
[[package]]
|
||||
name = "wasi"
|
||||
version = "0.11.1+wasi-snapshot-preview1"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "ccf3ec651a847eb01de73ccad15eb7d99f80485de043efb2f370cd654f4ea44b"
|
||||
|
||||
[[package]]
|
||||
name = "wasip2"
|
||||
version = "1.0.2+wasi-0.2.9"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "9517f9239f02c069db75e65f174b3da828fe5f5b945c4dd26bd25d89c03ebcf5"
|
||||
dependencies = [
|
||||
"wit-bindgen",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "wit-bindgen"
|
||||
version = "0.51.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "d7249219f66ced02969388cf2bb044a09756a083d0fab1e566056b04d9fbcaa5"
|
||||
27
fuzz/Cargo.toml
Normal file
27
fuzz/Cargo.toml
Normal file
|
|
@ -0,0 +1,27 @@
|
|||
[package]
|
||||
name = "swactor-fuzz"
|
||||
version = "0.0.0"
|
||||
publish = false
|
||||
edition = "2024"
|
||||
|
||||
[package.metadata]
|
||||
cargo-fuzz = true
|
||||
|
||||
[dependencies]
|
||||
libfuzzer-sys = { version = "0.4", features = ["arbitrary-derive"] }
|
||||
arbitrary = { version = "1", features = ["derive"] }
|
||||
swactor = { path = "..", default-features = true }
|
||||
|
||||
# Prevent this from interfering with workspaces
|
||||
[workspace]
|
||||
members = ["."]
|
||||
|
||||
[[bin]]
|
||||
name = "fuzz_runtime"
|
||||
path = "fuzz_targets/fuzz_runtime.rs"
|
||||
doc = false
|
||||
|
||||
[[bin]]
|
||||
name = "fuzz_mt"
|
||||
path = "fuzz_targets/fuzz_mt.rs"
|
||||
doc = false
|
||||
440
fuzz/fuzz_targets/fuzz_mt.rs
Normal file
440
fuzz/fuzz_targets/fuzz_mt.rs
Normal file
|
|
@ -0,0 +1,440 @@
|
|||
#![no_main]
|
||||
|
||||
use std::fmt::{self, Write as _};
|
||||
use std::sync::atomic::{AtomicU64, Ordering};
|
||||
use std::sync::OnceLock;
|
||||
use std::thread;
|
||||
use std::time::Duration;
|
||||
|
||||
use arbitrary::{Arbitrary, Unstructured};
|
||||
use libfuzzer_sys::fuzz_target;
|
||||
|
||||
use swactor::actor::{ActorAddress, ActorInterface};
|
||||
use swactor::config::RuntimeConfig;
|
||||
use swactor::runtime::{Ctx, Inbox, Runtime, RuntimeHandle};
|
||||
|
||||
// ─── Run Logging ────────────────────────────────────────────────────────────
|
||||
|
||||
static RUN_COUNTER: AtomicU64 = AtomicU64::new(0);
|
||||
|
||||
fn log_interval() -> u64 {
|
||||
static INTERVAL: OnceLock<u64> = OnceLock::new();
|
||||
*INTERVAL.get_or_init(|| {
|
||||
std::env::var("FUZZ_LOG")
|
||||
.ok()
|
||||
.and_then(|v| v.parse().ok())
|
||||
.unwrap_or(0)
|
||||
})
|
||||
}
|
||||
|
||||
// ─── Message Types ──────────────────────────────────────────────────────────
|
||||
|
||||
#[derive(Clone, Debug)]
|
||||
struct FuzzMsg {
|
||||
value: u64,
|
||||
}
|
||||
|
||||
impl<'a> Arbitrary<'a> for FuzzMsg {
|
||||
fn arbitrary(u: &mut Unstructured<'a>) -> arbitrary::Result<Self> {
|
||||
Ok(FuzzMsg {
|
||||
value: u.arbitrary()?,
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Clone, Debug)]
|
||||
struct WrongTypeMsg;
|
||||
|
||||
// ─── Actor Kinds ────────────────────────────────────────────────────────────
|
||||
|
||||
#[derive(Debug, Clone, Copy)]
|
||||
enum ActorKind {
|
||||
Echo,
|
||||
Counter,
|
||||
Noop,
|
||||
Bomber,
|
||||
WrongType,
|
||||
}
|
||||
|
||||
impl fmt::Display for ActorKind {
|
||||
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
|
||||
match self {
|
||||
ActorKind::Echo => write!(f, "Echo"),
|
||||
ActorKind::Counter => write!(f, "Counter"),
|
||||
ActorKind::Noop => write!(f, "Noop"),
|
||||
ActorKind::Bomber => write!(f, "Bomber"),
|
||||
ActorKind::WrongType => write!(f, "WrongType"),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
struct EchoActor;
|
||||
impl ActorInterface for EchoActor {
|
||||
type Incoming = FuzzMsg;
|
||||
type Response = ();
|
||||
fn handle(&mut self, _ctx: &Ctx, _msg: FuzzMsg) {}
|
||||
}
|
||||
|
||||
struct CounterActor {
|
||||
count: u64,
|
||||
}
|
||||
impl ActorInterface for CounterActor {
|
||||
type Incoming = FuzzMsg;
|
||||
type Response = ();
|
||||
fn handle(&mut self, _ctx: &Ctx, _msg: FuzzMsg) {
|
||||
self.count += 1;
|
||||
}
|
||||
}
|
||||
|
||||
struct NoopActor;
|
||||
impl ActorInterface for NoopActor {
|
||||
type Incoming = FuzzMsg;
|
||||
type Response = ();
|
||||
fn handle(&mut self, _ctx: &Ctx, _msg: FuzzMsg) {}
|
||||
}
|
||||
|
||||
const BOMBER_BUDGET: u32 = 256;
|
||||
|
||||
struct BomberActor {
|
||||
n: u8,
|
||||
remaining: u32,
|
||||
}
|
||||
|
||||
impl BomberActor {
|
||||
fn new(n: u8) -> Self {
|
||||
Self {
|
||||
n,
|
||||
remaining: BOMBER_BUDGET,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl ActorInterface for BomberActor {
|
||||
type Incoming = FuzzMsg;
|
||||
type Response = ();
|
||||
fn handle(&mut self, ctx: &Ctx, msg: FuzzMsg) {
|
||||
let to_send = (self.n as u32).min(self.remaining);
|
||||
self.remaining = self.remaining.saturating_sub(to_send);
|
||||
for _ in 0..to_send {
|
||||
let _ = ctx.send(ctx.self_addr(), FuzzMsg { value: msg.value });
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
struct WrongTypeActor;
|
||||
impl ActorInterface for WrongTypeActor {
|
||||
type Incoming = WrongTypeMsg;
|
||||
type Response = ();
|
||||
fn handle(&mut self, _ctx: &Ctx, _msg: WrongTypeMsg) {}
|
||||
}
|
||||
|
||||
// ─── Action Enum ────────────────────────────────────────────────────────────
|
||||
|
||||
#[derive(Debug, Arbitrary)]
|
||||
enum Action {
|
||||
// Spawning
|
||||
SpawnEcho,
|
||||
SpawnCounter,
|
||||
SpawnNoop,
|
||||
SpawnBomber { n: u8 },
|
||||
SpawnWrongType,
|
||||
|
||||
// Messaging
|
||||
Send { actor_idx: u8, msg: FuzzMsg },
|
||||
SendWrongType { actor_idx: u8 },
|
||||
BurstSend { actor_idx: u8, count: u8, value: u64 },
|
||||
|
||||
// Inboxes
|
||||
NewInbox,
|
||||
DrainInbox { inbox_idx: u8 },
|
||||
|
||||
// Timing — sleep between actions to create interleaving variety
|
||||
SleepMicros { us: u8 },
|
||||
|
||||
// Observation
|
||||
CheckStats,
|
||||
}
|
||||
|
||||
// ─── FuzzInput ──────────────────────────────────────────────────────────────
|
||||
|
||||
#[derive(Debug, Arbitrary)]
|
||||
struct FuzzInput {
|
||||
num_threads: u8,
|
||||
max_actors: u8,
|
||||
actions: Vec<Action>,
|
||||
}
|
||||
|
||||
// ─── FuzzState ──────────────────────────────────────────────────────────────
|
||||
|
||||
struct FuzzState {
|
||||
handle: RuntimeHandle,
|
||||
actors: Vec<(ActorAddress, ActorKind)>,
|
||||
inboxes: Vec<Inbox<FuzzMsg>>,
|
||||
total_spawned: usize,
|
||||
total_sent: usize,
|
||||
trace: Option<String>,
|
||||
}
|
||||
|
||||
impl FuzzState {
|
||||
// ── Logging helpers ─────────────────────────────────────────
|
||||
|
||||
fn log(&mut self, line: fmt::Arguments<'_>) {
|
||||
if let Some(ref mut t) = self.trace {
|
||||
let _ = writeln!(t, " {line}");
|
||||
}
|
||||
}
|
||||
|
||||
fn actor_label(&self, addr: ActorAddress) -> String {
|
||||
for (i, (a, kind)) in self.actors.iter().enumerate() {
|
||||
if *a == addr {
|
||||
return format!("actor#{i}({kind})");
|
||||
}
|
||||
}
|
||||
"actor#?".into()
|
||||
}
|
||||
|
||||
// ── Resolvers ───────────────────────────────────────────────
|
||||
|
||||
fn resolve_actor_addr(&self, idx: u8) -> Option<ActorAddress> {
|
||||
if self.actors.is_empty() {
|
||||
None
|
||||
} else {
|
||||
Some(self.actors[idx as usize % self.actors.len()].0)
|
||||
}
|
||||
}
|
||||
|
||||
fn resolve_inbox(&self, idx: u8) -> Option<usize> {
|
||||
if self.inboxes.is_empty() {
|
||||
None
|
||||
} else {
|
||||
Some(idx as usize % self.inboxes.len())
|
||||
}
|
||||
}
|
||||
|
||||
// ── Execution ───────────────────────────────────────────────
|
||||
|
||||
fn execute(&mut self, action: &Action) {
|
||||
let rt = &self.handle.runtime;
|
||||
match action {
|
||||
Action::SpawnEcho => {
|
||||
if let Ok(addr) = rt.spawn(EchoActor) {
|
||||
let id = self.actors.len();
|
||||
self.actors.push((addr, ActorKind::Echo));
|
||||
self.total_spawned += 1;
|
||||
self.log(format_args!("[SPAWN] Echo -> actor#{id}"));
|
||||
}
|
||||
}
|
||||
Action::SpawnCounter => {
|
||||
if let Ok(addr) = rt.spawn(CounterActor { count: 0 }) {
|
||||
let id = self.actors.len();
|
||||
self.actors.push((addr, ActorKind::Counter));
|
||||
self.total_spawned += 1;
|
||||
self.log(format_args!("[SPAWN] Counter -> actor#{id}"));
|
||||
}
|
||||
}
|
||||
Action::SpawnNoop => {
|
||||
if let Ok(addr) = rt.spawn(NoopActor) {
|
||||
let id = self.actors.len();
|
||||
self.actors.push((addr, ActorKind::Noop));
|
||||
self.total_spawned += 1;
|
||||
self.log(format_args!("[SPAWN] Noop -> actor#{id}"));
|
||||
}
|
||||
}
|
||||
Action::SpawnBomber { n } => {
|
||||
let clamped = (*n).max(1).min(16);
|
||||
if let Ok(addr) = rt.spawn(BomberActor::new(clamped)) {
|
||||
let id = self.actors.len();
|
||||
self.actors.push((addr, ActorKind::Bomber));
|
||||
self.total_spawned += 1;
|
||||
self.log(format_args!("[SPAWN] Bomber(n={clamped}) -> actor#{id}"));
|
||||
}
|
||||
}
|
||||
Action::SpawnWrongType => {
|
||||
if let Ok(addr) = rt.spawn(WrongTypeActor) {
|
||||
let id = self.actors.len();
|
||||
self.actors.push((addr, ActorKind::WrongType));
|
||||
self.total_spawned += 1;
|
||||
self.log(format_args!("[SPAWN] WrongType -> actor#{id}"));
|
||||
}
|
||||
}
|
||||
Action::Send { actor_idx, msg } => {
|
||||
if let Some(addr) = self.resolve_actor_addr(*actor_idx) {
|
||||
let label = self.actor_label(addr);
|
||||
let _ = rt.send_to(addr, msg.clone());
|
||||
self.total_sent += 1;
|
||||
self.log(format_args!("[SEND] {label} <- FuzzMsg(val={})", msg.value));
|
||||
}
|
||||
}
|
||||
Action::SendWrongType { actor_idx } => {
|
||||
if let Some(addr) = self.resolve_actor_addr(*actor_idx) {
|
||||
let label = self.actor_label(addr);
|
||||
let _ = rt.send_to(addr, WrongTypeMsg);
|
||||
self.total_sent += 1;
|
||||
self.log(format_args!("[SEND!] {label} <- WrongTypeMsg (mismatch)"));
|
||||
}
|
||||
}
|
||||
Action::BurstSend {
|
||||
actor_idx,
|
||||
count,
|
||||
value,
|
||||
} => {
|
||||
if let Some(addr) = self.resolve_actor_addr(*actor_idx) {
|
||||
let label = self.actor_label(addr);
|
||||
let n = (*count).max(1).min(32) as usize;
|
||||
for _ in 0..n {
|
||||
let _ = rt.send_to(addr, FuzzMsg { value: *value });
|
||||
}
|
||||
self.total_sent += n;
|
||||
self.log(format_args!("[BURST] {label} <- FuzzMsg x{n}"));
|
||||
}
|
||||
}
|
||||
Action::NewInbox => {
|
||||
if let Ok(inbox) = rt.new_inbox::<FuzzMsg>() {
|
||||
let id = self.inboxes.len();
|
||||
self.inboxes.push(inbox);
|
||||
self.log(format_args!("[INBOX] new inbox#{id}"));
|
||||
}
|
||||
}
|
||||
Action::DrainInbox { inbox_idx } => {
|
||||
if let Some(i) = self.resolve_inbox(*inbox_idx) {
|
||||
let mut count = 0usize;
|
||||
while self.inboxes[i].try_recv().is_some() {
|
||||
count += 1;
|
||||
}
|
||||
if count > 0 {
|
||||
self.log(format_args!("[DRAIN] inbox#{i} -> {count} msg(s)"));
|
||||
} else {
|
||||
self.log(format_args!("[DRAIN] inbox#{i} -> empty"));
|
||||
}
|
||||
}
|
||||
}
|
||||
Action::SleepMicros { us } => {
|
||||
let micros = (*us as u64).min(500);
|
||||
if micros > 0 {
|
||||
thread::sleep(Duration::from_micros(micros));
|
||||
self.log(format_args!("[SLEEP] {micros}us"));
|
||||
}
|
||||
}
|
||||
Action::CheckStats => {
|
||||
self.check_invariants();
|
||||
let stats = self.handle.runtime.stats();
|
||||
let depth: usize = stats.workers.iter().map(|w| w.mailbox_depth).sum();
|
||||
self.log(format_args!(
|
||||
"[CHECK] ok ({} actors, {depth} queued)",
|
||||
stats.actors.len()
|
||||
));
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
fn check_invariants(&self) {
|
||||
let stats = self.handle.runtime.stats();
|
||||
|
||||
assert!(
|
||||
stats.num_workers >= 1,
|
||||
"num_workers must be >= 1, got {}",
|
||||
stats.num_workers
|
||||
);
|
||||
|
||||
for (addr, wid) in &stats.actors {
|
||||
assert!(
|
||||
*wid < stats.num_workers,
|
||||
"actor {:?} on worker {} but only {} workers",
|
||||
addr,
|
||||
wid,
|
||||
stats.num_workers
|
||||
);
|
||||
}
|
||||
|
||||
for info in &stats.workers {
|
||||
assert!(info.id < stats.num_workers);
|
||||
}
|
||||
|
||||
assert_eq!(stats.workers.len(), stats.num_workers);
|
||||
}
|
||||
}
|
||||
|
||||
impl fmt::Debug for FuzzState {
|
||||
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
|
||||
f.debug_struct("FuzzState")
|
||||
.field("actors", &self.actors.len())
|
||||
.field("inboxes", &self.inboxes.len())
|
||||
.field("total_spawned", &self.total_spawned)
|
||||
.finish()
|
||||
}
|
||||
}
|
||||
|
||||
// ─── Fuzz Target ────────────────────────────────────────────────────────────
|
||||
|
||||
fuzz_target!(|input: FuzzInput| {
|
||||
let num_threads = (input.num_threads as usize).max(2).min(4);
|
||||
let max_actors = (input.max_actors as usize).max(1).min(200);
|
||||
|
||||
let interval = log_interval();
|
||||
let run = if interval > 0 {
|
||||
RUN_COUNTER.fetch_add(1, Ordering::Relaxed) + 1
|
||||
} else {
|
||||
0
|
||||
};
|
||||
let tracing = interval > 0 && run % interval == 0;
|
||||
|
||||
let config = RuntimeConfig {
|
||||
max_actors,
|
||||
num_threads,
|
||||
..Default::default()
|
||||
};
|
||||
let rt = Runtime::new(config);
|
||||
|
||||
// run() consumes the Runtime and spawns worker threads
|
||||
let handle = match rt.run() {
|
||||
Ok(h) => h,
|
||||
Err(_) => return,
|
||||
};
|
||||
|
||||
let mut state = FuzzState {
|
||||
handle,
|
||||
actors: Vec::new(),
|
||||
inboxes: Vec::new(),
|
||||
total_spawned: 0,
|
||||
total_sent: 0,
|
||||
trace: if tracing {
|
||||
Some(String::with_capacity(1024))
|
||||
} else {
|
||||
None
|
||||
},
|
||||
};
|
||||
|
||||
let action_limit = input.actions.len().min(256);
|
||||
let actions = &input.actions[..action_limit];
|
||||
for action in actions {
|
||||
state.execute(action);
|
||||
}
|
||||
|
||||
// Let workers process remaining messages
|
||||
thread::sleep(Duration::from_millis(10));
|
||||
|
||||
state.check_invariants();
|
||||
|
||||
// Print trace if this run was logged
|
||||
if let Some(trace) = &state.trace {
|
||||
let stats = state.handle.runtime.stats();
|
||||
let processed: u64 = stats.workers.iter().map(|w| w.messages_processed).sum();
|
||||
let depth: usize = stats.workers.iter().map(|w| w.mailbox_depth).sum();
|
||||
eprintln!(
|
||||
"\
|
||||
\n=== Run #{run} (MT) | threads={num_threads} cap={max_actors} | {total} actions ===
|
||||
{trace}\
|
||||
--- {spawned} spawned, {sent} sent, {alive} alive, {depth} queued, {processed} processed ---\n",
|
||||
total = actions.len(),
|
||||
spawned = state.total_spawned,
|
||||
sent = state.total_sent,
|
||||
alive = stats.actors.len(),
|
||||
);
|
||||
}
|
||||
|
||||
state.handle.shutdown();
|
||||
|
||||
// Destructure to move handle out for join() which consumes self
|
||||
let FuzzState { handle, .. } = state;
|
||||
handle.join();
|
||||
});
|
||||
819
fuzz/fuzz_targets/fuzz_runtime.rs
Normal file
819
fuzz/fuzz_targets/fuzz_runtime.rs
Normal file
|
|
@ -0,0 +1,819 @@
|
|||
#![no_main]
|
||||
|
||||
use std::fmt::{self, Write as _};
|
||||
use std::sync::atomic::{AtomicU64, Ordering};
|
||||
use std::sync::OnceLock;
|
||||
|
||||
use arbitrary::Arbitrary;
|
||||
use libfuzzer_sys::fuzz_target;
|
||||
|
||||
use swactor::actor::{ActorAddress, ActorInterface};
|
||||
use swactor::config::RuntimeConfig;
|
||||
use swactor::runtime::{Ctx, Inbox, Runtime};
|
||||
|
||||
// ─── Run Logging ────────────────────────────────────────────────────────────
|
||||
// FUZZ_LOG=1 → trace every run
|
||||
// FUZZ_LOG=100 → trace every 100th run
|
||||
// unset → silent
|
||||
|
||||
static RUN_COUNTER: AtomicU64 = AtomicU64::new(0);
|
||||
|
||||
fn log_interval() -> u64 {
|
||||
static INTERVAL: OnceLock<u64> = OnceLock::new();
|
||||
*INTERVAL.get_or_init(|| {
|
||||
std::env::var("FUZZ_LOG")
|
||||
.ok()
|
||||
.and_then(|v| v.parse().ok())
|
||||
.unwrap_or(0)
|
||||
})
|
||||
}
|
||||
|
||||
// ─── Message Types ──────────────────────────────────────────────────────────
|
||||
|
||||
#[derive(Clone, Debug, Arbitrary)]
|
||||
struct FuzzMsg {
|
||||
value: u64,
|
||||
reply_to_idx: Option<u8>,
|
||||
}
|
||||
|
||||
#[derive(Clone, Debug)]
|
||||
struct WrongTypeMsg;
|
||||
|
||||
// ─── Actor Kinds ────────────────────────────────────────────────────────────
|
||||
|
||||
#[derive(Debug, Clone, Copy)]
|
||||
enum ActorKind {
|
||||
Echo,
|
||||
Counter,
|
||||
Forwarder,
|
||||
Spawner,
|
||||
Bomber,
|
||||
Noop,
|
||||
WrongType,
|
||||
}
|
||||
|
||||
impl fmt::Display for ActorKind {
|
||||
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
|
||||
match self {
|
||||
ActorKind::Echo => write!(f, "Echo"),
|
||||
ActorKind::Counter => write!(f, "Counter"),
|
||||
ActorKind::Forwarder => write!(f, "Forwarder"),
|
||||
ActorKind::Spawner => write!(f, "Spawner"),
|
||||
ActorKind::Bomber => write!(f, "Bomber"),
|
||||
ActorKind::Noop => write!(f, "Noop"),
|
||||
ActorKind::WrongType => write!(f, "WrongType"),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
struct EchoActor;
|
||||
impl ActorInterface for EchoActor {
|
||||
type Incoming = FuzzMsg;
|
||||
type Response = ();
|
||||
fn handle(&mut self, ctx: &Ctx, msg: FuzzMsg) {
|
||||
if let Some(idx) = msg.reply_to_idx {
|
||||
let reply = FuzzMsg { value: msg.value, reply_to_idx: None };
|
||||
let _ = ctx.send(INBOX_ADDRS.lock_or_default().get(idx as usize), reply);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
struct CounterActor { count: u64 }
|
||||
impl ActorInterface for CounterActor {
|
||||
type Incoming = FuzzMsg;
|
||||
type Response = ();
|
||||
fn handle(&mut self, ctx: &Ctx, msg: FuzzMsg) {
|
||||
self.count += 1;
|
||||
if let Some(idx) = msg.reply_to_idx {
|
||||
let reply = FuzzMsg { value: self.count, reply_to_idx: None };
|
||||
let _ = ctx.send(INBOX_ADDRS.lock_or_default().get(idx as usize), reply);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
struct ForwarderActor { target: ActorAddress }
|
||||
impl ActorInterface for ForwarderActor {
|
||||
type Incoming = FuzzMsg;
|
||||
type Response = ();
|
||||
fn handle(&mut self, ctx: &Ctx, msg: FuzzMsg) {
|
||||
let _ = ctx.send(self.target, msg);
|
||||
}
|
||||
}
|
||||
|
||||
struct SpawnerActor;
|
||||
impl ActorInterface for SpawnerActor {
|
||||
type Incoming = FuzzMsg;
|
||||
type Response = ();
|
||||
fn handle(&mut self, ctx: &Ctx, msg: FuzzMsg) {
|
||||
let _ = ctx.spawn(NoopActor);
|
||||
if let Some(idx) = msg.reply_to_idx {
|
||||
let reply = FuzzMsg { value: msg.value, reply_to_idx: None };
|
||||
let _ = ctx.send(INBOX_ADDRS.lock_or_default().get(idx as usize), reply);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
const BOMBER_BUDGET: u32 = 256;
|
||||
struct BomberActor { n: u8, remaining: u32 }
|
||||
impl BomberActor {
|
||||
fn new(n: u8) -> Self { Self { n, remaining: BOMBER_BUDGET } }
|
||||
}
|
||||
impl ActorInterface for BomberActor {
|
||||
type Incoming = FuzzMsg;
|
||||
type Response = ();
|
||||
fn handle(&mut self, ctx: &Ctx, msg: FuzzMsg) {
|
||||
let to_send = (self.n as u32).min(self.remaining);
|
||||
self.remaining = self.remaining.saturating_sub(to_send);
|
||||
for _ in 0..to_send {
|
||||
let _ = ctx.send(ctx.self_addr(), FuzzMsg { value: msg.value, reply_to_idx: None });
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
struct NoopActor;
|
||||
impl ActorInterface for NoopActor {
|
||||
type Incoming = FuzzMsg;
|
||||
type Response = ();
|
||||
fn handle(&mut self, _ctx: &Ctx, _msg: FuzzMsg) {}
|
||||
}
|
||||
|
||||
struct WrongTypeActor;
|
||||
impl ActorInterface for WrongTypeActor {
|
||||
type Incoming = WrongTypeMsg;
|
||||
type Response = ();
|
||||
fn handle(&mut self, _ctx: &Ctx, _msg: WrongTypeMsg) {}
|
||||
}
|
||||
|
||||
// ─── Shared Inbox Address Table ─────────────────────────────────────────────
|
||||
|
||||
struct InboxAddrs(Vec<ActorAddress>);
|
||||
impl InboxAddrs {
|
||||
fn get(&self, idx: usize) -> ActorAddress {
|
||||
if self.0.is_empty() { ActorAddress::default() }
|
||||
else { self.0[idx % self.0.len()] }
|
||||
}
|
||||
}
|
||||
struct InboxAddrsCell(std::sync::Mutex<InboxAddrs>);
|
||||
impl InboxAddrsCell {
|
||||
const fn new() -> Self { Self(std::sync::Mutex::new(InboxAddrs(Vec::new()))) }
|
||||
fn lock_or_default(&self) -> std::sync::MutexGuard<'_, InboxAddrs> { self.0.lock().unwrap() }
|
||||
fn update(&self, addrs: Vec<ActorAddress>) { self.0.lock().unwrap().0 = addrs; }
|
||||
}
|
||||
static INBOX_ADDRS: InboxAddrsCell = InboxAddrsCell::new();
|
||||
|
||||
// ─── Scenarios ──────────────────────────────────────────────────────────────
|
||||
|
||||
#[derive(Debug, Arbitrary)]
|
||||
enum Scenario {
|
||||
EchoRoundTrip { value: u64 },
|
||||
CountToN { n: u8 },
|
||||
ForwardChain { len: u8, value: u64 },
|
||||
ForwardRing { len: u8, ticks: u8 },
|
||||
Fanout { n: u8, value: u64 },
|
||||
SpawnAndImmediateSend { value: u64 },
|
||||
SpawnSendTickSendTick { v1: u64, v2: u64 },
|
||||
InterleavedSpawnSend { count: u8 },
|
||||
BomberStress { bomber_n: u8, burst: u8 },
|
||||
BackpressureStepper { burst: u8 },
|
||||
TypeConfusionBarrage { num_fuzz: u8, num_wrong: u8, sends: u8 },
|
||||
WrongTypeToAll,
|
||||
DeadLetterFlood { count: u8 },
|
||||
InboxReuse { sends: u8, value: u64 },
|
||||
SpawnStorm { sends: u8 },
|
||||
PopulateAndObserve { kinds: Vec<ActorKindChoice> },
|
||||
RawActions { actions: Vec<RawAction> },
|
||||
TickFlush { n: u8 },
|
||||
CheckInvariants,
|
||||
}
|
||||
|
||||
#[derive(Debug, Arbitrary, Clone, Copy)]
|
||||
enum ActorKindChoice { Echo, Counter, Noop, Spawner, Bomber { n: u8 }, WrongType }
|
||||
|
||||
#[derive(Debug, Arbitrary)]
|
||||
enum RawAction {
|
||||
SpawnEcho,
|
||||
SpawnCounter,
|
||||
SpawnForwarder { target_idx: u8 },
|
||||
SpawnNoop,
|
||||
SpawnWrongType,
|
||||
Send { actor_idx: u8, msg: FuzzMsg },
|
||||
SendWrongType { actor_idx: u8 },
|
||||
SendToRandomAddr { seed: u16 },
|
||||
BurstSend { actor_idx: u8, count: u8, value: u64 },
|
||||
NewInbox,
|
||||
DrainInbox { inbox_idx: u8 },
|
||||
DrainAll,
|
||||
Tick,
|
||||
TickN { n: u8 },
|
||||
}
|
||||
|
||||
#[derive(Debug, Arbitrary)]
|
||||
struct FuzzInput {
|
||||
max_actors: u8,
|
||||
mailbox_waterlevel: u8,
|
||||
scenarios: Vec<Scenario>,
|
||||
}
|
||||
|
||||
// ─── FuzzState ──────────────────────────────────────────────────────────────
|
||||
|
||||
struct FuzzState {
|
||||
runtime: Runtime,
|
||||
actors: Vec<(ActorAddress, ActorKind)>,
|
||||
inboxes: Vec<Inbox<FuzzMsg>>,
|
||||
total_spawned: usize,
|
||||
total_sent: usize,
|
||||
total_inbox_received: usize,
|
||||
trace: Option<String>,
|
||||
}
|
||||
|
||||
impl FuzzState {
|
||||
fn new(runtime: Runtime, tracing: bool) -> Self {
|
||||
Self {
|
||||
runtime,
|
||||
actors: Vec::new(),
|
||||
inboxes: Vec::new(),
|
||||
total_spawned: 0,
|
||||
total_sent: 0,
|
||||
total_inbox_received: 0,
|
||||
trace: if tracing { Some(String::with_capacity(2048)) } else { None },
|
||||
}
|
||||
}
|
||||
|
||||
// ── Logging helpers ─────────────────────────────────────────
|
||||
|
||||
fn log(&mut self, line: fmt::Arguments<'_>) {
|
||||
if let Some(ref mut t) = self.trace {
|
||||
let _ = writeln!(t, " {line}");
|
||||
}
|
||||
}
|
||||
|
||||
fn log_header(&mut self, name: &str) {
|
||||
if let Some(ref mut t) = self.trace {
|
||||
let _ = writeln!(t, "--- {name} ---");
|
||||
}
|
||||
}
|
||||
|
||||
fn actor_label(&self, addr: ActorAddress) -> String {
|
||||
for (i, (a, kind)) in self.actors.iter().enumerate() {
|
||||
if *a == addr { return format!("actor#{i}({kind})"); }
|
||||
}
|
||||
"actor#?".into()
|
||||
}
|
||||
|
||||
fn msg_label(&self, msg: &FuzzMsg) -> String {
|
||||
match msg.reply_to_idx {
|
||||
Some(idx) => {
|
||||
let resolved = if self.inboxes.is_empty() { "none".into() }
|
||||
else { format!("inbox#{}", idx as usize % self.inboxes.len()) };
|
||||
format!("FuzzMsg(reply_to={resolved})")
|
||||
}
|
||||
None => "FuzzMsg".into(),
|
||||
}
|
||||
}
|
||||
|
||||
// ── Shared helpers ──────────────────────────────────────────
|
||||
|
||||
fn sync_inbox_addrs(&self) {
|
||||
let addrs: Vec<ActorAddress> = self.inboxes.iter().map(|i| *i.addr()).collect();
|
||||
INBOX_ADDRS.update(addrs);
|
||||
}
|
||||
|
||||
fn resolve_actor(&self, idx: u8) -> Option<ActorAddress> {
|
||||
if self.actors.is_empty() { None }
|
||||
else { Some(self.actors[idx as usize % self.actors.len()].0) }
|
||||
}
|
||||
|
||||
fn resolve_inbox_idx(&self, idx: u8) -> Option<usize> {
|
||||
if self.inboxes.is_empty() { None }
|
||||
else { Some(idx as usize % self.inboxes.len()) }
|
||||
}
|
||||
|
||||
fn msg(&self, value: u64, inbox_idx: Option<u8>) -> FuzzMsg {
|
||||
FuzzMsg { value, reply_to_idx: inbox_idx }
|
||||
}
|
||||
|
||||
// ── Primitive operations (with tracing) ─────────────────────
|
||||
|
||||
fn spawn_actor(&mut self, kind: ActorKind) -> Option<ActorAddress> {
|
||||
let result = match kind {
|
||||
ActorKind::Echo => self.runtime.spawn(EchoActor),
|
||||
ActorKind::Counter => self.runtime.spawn(CounterActor { count: 0 }),
|
||||
ActorKind::Noop => self.runtime.spawn(NoopActor),
|
||||
ActorKind::Spawner => self.runtime.spawn(SpawnerActor),
|
||||
ActorKind::WrongType => self.runtime.spawn(WrongTypeActor),
|
||||
_ => return None,
|
||||
};
|
||||
if let Ok(addr) = result {
|
||||
let id = self.actors.len();
|
||||
self.actors.push((addr, kind));
|
||||
self.total_spawned += 1;
|
||||
self.log(format_args!("[SPAWN] {kind:<20} -> actor#{id}"));
|
||||
Some(addr)
|
||||
} else {
|
||||
self.log(format_args!("[SPAWN] {kind:<20} -> FAILED (full)"));
|
||||
None
|
||||
}
|
||||
}
|
||||
|
||||
fn spawn_forwarder(&mut self, target: ActorAddress) -> Option<ActorAddress> {
|
||||
let target_label = self.actor_label(target);
|
||||
if let Ok(addr) = self.runtime.spawn(ForwarderActor { target }) {
|
||||
let id = self.actors.len();
|
||||
self.actors.push((addr, ActorKind::Forwarder));
|
||||
self.total_spawned += 1;
|
||||
self.log(format_args!("[SPAWN] Forwarder -> {target_label:<10} -> actor#{id}"));
|
||||
Some(addr)
|
||||
} else {
|
||||
self.log(format_args!("[SPAWN] Forwarder -> FAILED (full)"));
|
||||
None
|
||||
}
|
||||
}
|
||||
|
||||
fn spawn_bomber(&mut self, n: u8) -> Option<ActorAddress> {
|
||||
let clamped = n.max(1).min(16);
|
||||
if let Ok(addr) = self.runtime.spawn(BomberActor::new(clamped)) {
|
||||
let id = self.actors.len();
|
||||
self.actors.push((addr, ActorKind::Bomber));
|
||||
self.total_spawned += 1;
|
||||
self.log(format_args!("[SPAWN] Bomber(n={clamped}){:<13} -> actor#{id}", ""));
|
||||
Some(addr)
|
||||
} else {
|
||||
self.log(format_args!("[SPAWN] Bomber(n={clamped}) -> FAILED (full)"));
|
||||
None
|
||||
}
|
||||
}
|
||||
|
||||
fn send_msg(&mut self, addr: ActorAddress, msg: FuzzMsg) {
|
||||
let actor = self.actor_label(addr);
|
||||
let m = self.msg_label(&msg);
|
||||
let _ = self.runtime.send_to(addr, msg);
|
||||
self.total_sent += 1;
|
||||
self.log(format_args!("[SEND] {actor} <- {m}"));
|
||||
}
|
||||
|
||||
fn send_wrong_type(&mut self, addr: ActorAddress) {
|
||||
let actor = self.actor_label(addr);
|
||||
let _ = self.runtime.send_to(addr, WrongTypeMsg);
|
||||
self.total_sent += 1;
|
||||
self.log(format_args!("[SEND!] {actor} <- WrongTypeMsg (mismatch)"));
|
||||
}
|
||||
|
||||
fn send_dead_letter(&mut self, seed: u16) {
|
||||
let mut bytes = [0u8; 32];
|
||||
bytes[0..2].copy_from_slice(&seed.to_le_bytes());
|
||||
bytes[2] = 0xFF;
|
||||
bytes[3] = 0xFF;
|
||||
let _ = self.runtime.send_to(
|
||||
ActorAddress(bytes),
|
||||
FuzzMsg { value: seed as u64, reply_to_idx: None },
|
||||
);
|
||||
self.log(format_args!("[SEND?] <dead letter seed={seed}>"));
|
||||
}
|
||||
|
||||
fn burst_send(&mut self, addr: ActorAddress, count: usize, value: u64) {
|
||||
let actor = self.actor_label(addr);
|
||||
for _ in 0..count {
|
||||
let _ = self.runtime.send_to(addr, self.msg(value, None));
|
||||
self.total_sent += 1;
|
||||
}
|
||||
self.log(format_args!("[BURST] {actor} <- FuzzMsg x{count}"));
|
||||
}
|
||||
|
||||
fn new_inbox(&mut self) -> Option<usize> {
|
||||
if let Ok(inbox) = self.runtime.new_inbox::<FuzzMsg>() {
|
||||
let id = self.inboxes.len();
|
||||
self.inboxes.push(inbox);
|
||||
self.sync_inbox_addrs();
|
||||
self.log(format_args!("[INBOX] new inbox#{id}"));
|
||||
Some(id)
|
||||
} else { None }
|
||||
}
|
||||
|
||||
fn drain_inbox(&mut self, idx: usize) -> usize {
|
||||
let mut count = 0;
|
||||
while self.inboxes[idx].try_recv().is_some() {
|
||||
count += 1;
|
||||
self.total_inbox_received += 1;
|
||||
}
|
||||
if count > 0 {
|
||||
self.log(format_args!("[DRAIN] inbox#{idx} -> {count} msg(s)"));
|
||||
} else {
|
||||
self.log(format_args!("[DRAIN] inbox#{idx} -> empty"));
|
||||
}
|
||||
count
|
||||
}
|
||||
|
||||
fn drain_all_inboxes(&mut self) -> usize {
|
||||
let mut count = 0;
|
||||
for inbox in &self.inboxes {
|
||||
while inbox.try_recv().is_some() { count += 1; }
|
||||
}
|
||||
self.total_inbox_received += count;
|
||||
self.log(format_args!("[DRAIN] all {} inbox(es) -> {count} msg(s)", self.inboxes.len()));
|
||||
count
|
||||
}
|
||||
|
||||
fn tick(&mut self) {
|
||||
self.runtime.tick();
|
||||
self.log(format_args!("[TICK]"));
|
||||
}
|
||||
|
||||
fn tick_n(&mut self, n: usize) {
|
||||
for _ in 0..n { self.runtime.tick(); }
|
||||
if n > 1 {
|
||||
self.log(format_args!("[TICK] x{n}"));
|
||||
} else {
|
||||
self.log(format_args!("[TICK]"));
|
||||
}
|
||||
}
|
||||
|
||||
fn check_invariants(&self) {
|
||||
let stats = self.runtime.stats();
|
||||
assert!(stats.num_workers >= 1);
|
||||
for (addr, wid) in &stats.actors {
|
||||
assert!(*wid < stats.num_workers,
|
||||
"actor {:?} on worker {} but only {} workers", addr, wid, stats.num_workers);
|
||||
}
|
||||
for info in &stats.workers {
|
||||
assert!(info.id < stats.num_workers);
|
||||
}
|
||||
assert_eq!(stats.workers.len(), stats.num_workers);
|
||||
for inbox in &self.inboxes {
|
||||
if let Some(msg) = inbox.try_recv() { let _ = msg.value; }
|
||||
}
|
||||
}
|
||||
|
||||
fn log_check(&mut self) {
|
||||
self.check_invariants();
|
||||
let stats = self.runtime.stats();
|
||||
let depth: usize = stats.workers.iter().map(|w| w.mailbox_depth).sum();
|
||||
self.log(format_args!("[CHECK] ok ({} actors, {depth} queued)", stats.actors.len()));
|
||||
}
|
||||
|
||||
// ── Scenario execution ──────────────────────────────────────
|
||||
|
||||
fn run_scenario(&mut self, scenario: &Scenario) {
|
||||
match scenario {
|
||||
Scenario::EchoRoundTrip { value } => {
|
||||
self.log_header("Echo Round-Trip");
|
||||
let inbox_i = self.new_inbox();
|
||||
let inbox_reply = inbox_i.map(|i| i as u8);
|
||||
if let Some(addr) = self.spawn_actor(ActorKind::Echo) {
|
||||
self.tick();
|
||||
self.send_msg(addr, self.msg(*value, inbox_reply));
|
||||
self.tick_n(3);
|
||||
if let Some(i) = inbox_i { self.drain_inbox(i); }
|
||||
}
|
||||
}
|
||||
|
||||
Scenario::CountToN { n } => {
|
||||
let count = (*n).max(1).min(32) as usize;
|
||||
self.log_header(&format!("Count to {count}"));
|
||||
let inbox_i = self.new_inbox();
|
||||
let inbox_reply = inbox_i.map(|i| i as u8);
|
||||
if let Some(addr) = self.spawn_actor(ActorKind::Counter) {
|
||||
self.tick();
|
||||
let label = self.actor_label(addr);
|
||||
for _ in 0..count {
|
||||
let _ = self.runtime.send_to(addr, self.msg(0, inbox_reply));
|
||||
self.total_sent += 1;
|
||||
}
|
||||
self.log(format_args!("[BURST] {label} <- FuzzMsg x{count}"));
|
||||
self.tick_n(count + 2);
|
||||
if let Some(i) = inbox_i { self.drain_inbox(i); }
|
||||
}
|
||||
}
|
||||
|
||||
Scenario::ForwardChain { len, value } => {
|
||||
let chain_len = (*len).max(1).min(16) as usize;
|
||||
self.log_header(&format!("Forward Chain ({chain_len} hops)"));
|
||||
let inbox_i = self.new_inbox();
|
||||
let inbox_reply = inbox_i.map(|i| i as u8);
|
||||
let tail = self.spawn_actor(ActorKind::Echo);
|
||||
self.tick();
|
||||
let mut next = match tail { Some(a) => a, None => return };
|
||||
for _ in 0..chain_len {
|
||||
match self.spawn_forwarder(next) {
|
||||
Some(a) => { self.tick(); next = a; }
|
||||
None => return,
|
||||
}
|
||||
}
|
||||
self.send_msg(next, self.msg(*value, inbox_reply));
|
||||
self.tick_n(chain_len + 4);
|
||||
if let Some(i) = inbox_i { self.drain_inbox(i); }
|
||||
}
|
||||
|
||||
Scenario::ForwardRing { len, ticks } => {
|
||||
let ring_len = (*len).max(2).min(12) as usize;
|
||||
let tick_count = (*ticks).max(1).min(64) as usize;
|
||||
self.log_header(&format!("Forward Ring ({ring_len} nodes, {tick_count} ticks)"));
|
||||
let mut prev_addr = ActorAddress::default();
|
||||
let mut addrs = Vec::with_capacity(ring_len);
|
||||
for _ in 0..ring_len {
|
||||
match self.spawn_forwarder(prev_addr) {
|
||||
Some(a) => { self.tick(); addrs.push(a); prev_addr = a; }
|
||||
None => return,
|
||||
}
|
||||
}
|
||||
if let Some(&last) = addrs.last() {
|
||||
self.send_msg(last, self.msg(0, None));
|
||||
self.tick_n(tick_count);
|
||||
}
|
||||
}
|
||||
|
||||
Scenario::Fanout { n, value } => {
|
||||
let fan = (*n).max(1).min(20) as usize;
|
||||
self.log_header(&format!("Fan-Out ({fan} echoes)"));
|
||||
let inbox_i = self.new_inbox();
|
||||
let inbox_reply = inbox_i.map(|i| i as u8);
|
||||
let mut targets = Vec::with_capacity(fan);
|
||||
for _ in 0..fan {
|
||||
if let Some(addr) = self.spawn_actor(ActorKind::Echo) {
|
||||
targets.push(addr);
|
||||
}
|
||||
}
|
||||
self.tick();
|
||||
for addr in &targets {
|
||||
self.send_msg(*addr, self.msg(*value, inbox_reply));
|
||||
}
|
||||
self.tick_n(fan + 2);
|
||||
if let Some(i) = inbox_i { self.drain_inbox(i); }
|
||||
}
|
||||
|
||||
Scenario::SpawnAndImmediateSend { value } => {
|
||||
self.log_header("Spawn + Immediate Send (NO TICK)");
|
||||
if let Some(addr) = self.spawn_actor(ActorKind::Echo) {
|
||||
let inbox_i = self.new_inbox();
|
||||
let inbox_reply = inbox_i.map(|i| i as u8);
|
||||
self.log(format_args!(" (actor not ticked into pool yet)"));
|
||||
self.send_msg(addr, self.msg(*value, inbox_reply));
|
||||
self.tick_n(4);
|
||||
if let Some(i) = inbox_i { self.drain_inbox(i); }
|
||||
}
|
||||
}
|
||||
|
||||
Scenario::SpawnSendTickSendTick { v1, v2 } => {
|
||||
self.log_header("Two-Phase Delivery");
|
||||
let inbox_i = self.new_inbox();
|
||||
let inbox_reply = inbox_i.map(|i| i as u8);
|
||||
if let Some(addr) = self.spawn_actor(ActorKind::Counter) {
|
||||
self.send_msg(addr, self.msg(*v1, inbox_reply));
|
||||
self.tick_n(2);
|
||||
self.send_msg(addr, self.msg(*v2, inbox_reply));
|
||||
self.tick_n(2);
|
||||
if let Some(i) = inbox_i { self.drain_inbox(i); }
|
||||
}
|
||||
}
|
||||
|
||||
Scenario::InterleavedSpawnSend { count } => {
|
||||
let n = (*count).max(1).min(20) as usize;
|
||||
self.log_header(&format!("Interleaved Spawn+Send (x{n}, no ticks)"));
|
||||
for v in 0..n {
|
||||
self.spawn_actor(ActorKind::Noop);
|
||||
if let Some(addr) = self.resolve_actor(v as u8) {
|
||||
self.send_msg(addr, self.msg(v as u64, None));
|
||||
}
|
||||
}
|
||||
self.log(format_args!(" (now flushing)"));
|
||||
self.tick_n(n + 2);
|
||||
}
|
||||
|
||||
Scenario::BomberStress { bomber_n, burst } => {
|
||||
let n = (*bomber_n).max(1).min(16);
|
||||
let burst_count = (*burst).max(1).min(32) as usize;
|
||||
self.log_header(&format!("Bomber Stress (n={n}, burst={burst_count})"));
|
||||
if let Some(addr) = self.spawn_bomber(n) {
|
||||
self.tick();
|
||||
self.burst_send(addr, burst_count, 0);
|
||||
self.tick_n(16);
|
||||
self.log_check();
|
||||
}
|
||||
}
|
||||
|
||||
Scenario::BackpressureStepper { burst } => {
|
||||
let burst_count = (*burst).max(4).min(64) as usize;
|
||||
self.log_header(&format!("Backpressure Stepper (burst={burst_count})"));
|
||||
if let Some(addr) = self.spawn_actor(ActorKind::Noop) {
|
||||
self.tick();
|
||||
self.burst_send(addr, burst_count, 0);
|
||||
self.tick();
|
||||
self.log_check();
|
||||
self.tick();
|
||||
self.log_check();
|
||||
self.tick_n(burst_count);
|
||||
}
|
||||
}
|
||||
|
||||
Scenario::TypeConfusionBarrage { num_fuzz, num_wrong, sends } => {
|
||||
let nf = (*num_fuzz).max(1).min(10) as usize;
|
||||
let nw = (*num_wrong).max(1).min(10) as usize;
|
||||
let nsends = (*sends).max(1).min(32) as usize;
|
||||
self.log_header(&format!("Type Confusion ({nf} normal + {nw} wrong-type, {nsends} rounds)"));
|
||||
let start_idx = self.actors.len();
|
||||
for _ in 0..nf { self.spawn_actor(ActorKind::Echo); }
|
||||
for _ in 0..nw { self.spawn_actor(ActorKind::WrongType); }
|
||||
self.tick();
|
||||
let end_idx = self.actors.len();
|
||||
for _ in 0..nsends {
|
||||
for i in start_idx..end_idx {
|
||||
let addr = self.actors[i].0;
|
||||
let kind = self.actors[i].1;
|
||||
match kind {
|
||||
ActorKind::WrongType => {
|
||||
self.send_msg(addr, self.msg(0, None));
|
||||
}
|
||||
_ => {
|
||||
self.send_wrong_type(addr);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
self.tick_n(nsends + 2);
|
||||
}
|
||||
|
||||
Scenario::WrongTypeToAll => {
|
||||
let n = self.actors.len();
|
||||
self.log_header(&format!("Wrong Type Blast ({n} actors)"));
|
||||
for i in 0..n {
|
||||
let addr = self.actors[i].0;
|
||||
self.send_wrong_type(addr);
|
||||
}
|
||||
self.tick_n(4);
|
||||
}
|
||||
|
||||
Scenario::DeadLetterFlood { count } => {
|
||||
let n = (*count).max(1).min(64) as usize;
|
||||
self.log_header(&format!("Dead Letter Flood (x{n})"));
|
||||
for seed in 0..n {
|
||||
self.send_dead_letter(seed as u16);
|
||||
}
|
||||
}
|
||||
|
||||
Scenario::InboxReuse { sends, value } => {
|
||||
let n = (*sends).max(1).min(16) as usize;
|
||||
self.log_header(&format!("Inbox Reuse ({n} sends x2 rounds)"));
|
||||
if let Some(idx) = self.new_inbox() {
|
||||
let addr = *self.inboxes[idx].addr();
|
||||
for round in 0..2u64 {
|
||||
self.log(format_args!(" round {}", round + 1));
|
||||
for _ in 0..n {
|
||||
let _ = self.runtime.send_to(
|
||||
addr, self.msg(value.wrapping_add(round), None),
|
||||
);
|
||||
self.total_sent += 1;
|
||||
}
|
||||
self.log(format_args!("[SEND] inbox#{idx} <- FuzzMsg x{n}"));
|
||||
self.drain_inbox(idx);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
Scenario::SpawnStorm { sends } => {
|
||||
let n = (*sends).max(1).min(24) as usize;
|
||||
self.log_header(&format!("Spawn Storm ({n} child spawns)"));
|
||||
if let Some(addr) = self.spawn_actor(ActorKind::Spawner) {
|
||||
self.tick();
|
||||
self.burst_send(addr, n, 0);
|
||||
self.log(format_args!(" (each msg spawns a child Noop)"));
|
||||
self.tick_n(n + 4);
|
||||
self.log_check();
|
||||
}
|
||||
}
|
||||
|
||||
Scenario::PopulateAndObserve { kinds } => {
|
||||
let limit = kinds.len().min(30);
|
||||
self.log_header(&format!("Populate & Observe ({limit} actors)"));
|
||||
for k in &kinds[..limit] {
|
||||
match k {
|
||||
ActorKindChoice::Echo => { self.spawn_actor(ActorKind::Echo); }
|
||||
ActorKindChoice::Counter => { self.spawn_actor(ActorKind::Counter); }
|
||||
ActorKindChoice::Noop => { self.spawn_actor(ActorKind::Noop); }
|
||||
ActorKindChoice::Spawner => { self.spawn_actor(ActorKind::Spawner); }
|
||||
ActorKindChoice::Bomber { n } => { self.spawn_bomber(*n); }
|
||||
ActorKindChoice::WrongType => { self.spawn_actor(ActorKind::WrongType); }
|
||||
}
|
||||
}
|
||||
self.tick();
|
||||
self.log_check();
|
||||
}
|
||||
|
||||
Scenario::RawActions { actions } => {
|
||||
let limit = actions.len().min(64);
|
||||
self.log_header(&format!("Raw Actions ({limit} ops)"));
|
||||
for action in &actions[..limit] {
|
||||
self.run_raw(action);
|
||||
}
|
||||
}
|
||||
|
||||
Scenario::TickFlush { n } => {
|
||||
let t = (*n).max(1).min(64) as usize;
|
||||
self.log_header(&format!("Tick Flush (x{t})"));
|
||||
self.tick_n(t);
|
||||
}
|
||||
|
||||
Scenario::CheckInvariants => {
|
||||
self.log_header("Invariant Check");
|
||||
self.log_check();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
fn run_raw(&mut self, action: &RawAction) {
|
||||
match action {
|
||||
RawAction::SpawnEcho => { self.spawn_actor(ActorKind::Echo); }
|
||||
RawAction::SpawnCounter => { self.spawn_actor(ActorKind::Counter); }
|
||||
RawAction::SpawnForwarder { target_idx } => {
|
||||
let target = self.resolve_actor(*target_idx)
|
||||
.unwrap_or(ActorAddress::default());
|
||||
self.spawn_forwarder(target);
|
||||
}
|
||||
RawAction::SpawnNoop => { self.spawn_actor(ActorKind::Noop); }
|
||||
RawAction::SpawnWrongType => { self.spawn_actor(ActorKind::WrongType); }
|
||||
RawAction::Send { actor_idx, msg } => {
|
||||
if let Some(addr) = self.resolve_actor(*actor_idx) {
|
||||
self.send_msg(addr, msg.clone());
|
||||
}
|
||||
}
|
||||
RawAction::SendWrongType { actor_idx } => {
|
||||
if let Some(addr) = self.resolve_actor(*actor_idx) {
|
||||
self.send_wrong_type(addr);
|
||||
}
|
||||
}
|
||||
RawAction::SendToRandomAddr { seed } => { self.send_dead_letter(*seed); }
|
||||
RawAction::BurstSend { actor_idx, count, value } => {
|
||||
if let Some(addr) = self.resolve_actor(*actor_idx) {
|
||||
self.burst_send(addr, (*count).max(1).min(64) as usize, *value);
|
||||
}
|
||||
}
|
||||
RawAction::NewInbox => { self.new_inbox(); }
|
||||
RawAction::DrainInbox { inbox_idx } => {
|
||||
if let Some(i) = self.resolve_inbox_idx(*inbox_idx) {
|
||||
self.drain_inbox(i);
|
||||
}
|
||||
}
|
||||
RawAction::DrainAll => { self.drain_all_inboxes(); }
|
||||
RawAction::Tick => { self.tick(); }
|
||||
RawAction::TickN { n } => { self.tick_n((*n).max(1).min(64) as usize); }
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl fmt::Debug for FuzzState {
|
||||
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
|
||||
f.debug_struct("FuzzState")
|
||||
.field("actors", &self.actors.len())
|
||||
.field("inboxes", &self.inboxes.len())
|
||||
.field("total_spawned", &self.total_spawned)
|
||||
.field("total_sent", &self.total_sent)
|
||||
.finish()
|
||||
}
|
||||
}
|
||||
|
||||
// ─── Fuzz Target ────────────────────────────────────────────────────────────
|
||||
|
||||
fuzz_target!(|input: FuzzInput| {
|
||||
let max_actors = (input.max_actors as usize).max(1).min(200);
|
||||
let mailbox_waterlevel = (input.mailbox_waterlevel as usize).max(1).min(50);
|
||||
|
||||
let interval = log_interval();
|
||||
let run = if interval > 0 {
|
||||
RUN_COUNTER.fetch_add(1, Ordering::Relaxed) + 1
|
||||
} else { 0 };
|
||||
let tracing = interval > 0 && run % interval == 0;
|
||||
|
||||
let config = RuntimeConfig {
|
||||
max_actors,
|
||||
mailbox_waterlevel,
|
||||
num_threads: 1,
|
||||
..Default::default()
|
||||
};
|
||||
let rt = Runtime::new(config);
|
||||
let mut state = FuzzState::new(rt, tracing);
|
||||
|
||||
let scenario_limit = input.scenarios.len().min(64);
|
||||
let scenarios = &input.scenarios[..scenario_limit];
|
||||
for scenario in scenarios {
|
||||
state.run_scenario(scenario);
|
||||
}
|
||||
|
||||
// Final flush
|
||||
for _ in 0..64 { state.runtime.tick(); }
|
||||
state.check_invariants();
|
||||
|
||||
// Print trace if this run was logged
|
||||
if let Some(trace) = &state.trace {
|
||||
let stats = state.runtime.stats();
|
||||
let depth: usize = stats.workers.iter().map(|w| w.mailbox_depth).sum();
|
||||
let processed: u64 = stats.workers.iter().map(|w| w.messages_processed).sum();
|
||||
eprintln!("\
|
||||
\n=== Run #{run} | cap={max_actors} waterlevel={mailbox_waterlevel} ===
|
||||
{trace}\
|
||||
--- {spawned} spawned, {sent} sent, {recv} received, \
|
||||
{alive} alive, {depth} queued, {processed} processed ---\n",
|
||||
spawned = state.total_spawned,
|
||||
sent = state.total_sent,
|
||||
recv = state.total_inbox_received,
|
||||
alive = stats.actors.len(),
|
||||
);
|
||||
}
|
||||
});
|
||||
Loading…
Reference in a new issue