wave 7: fuzz sidecar scaffold (fuzz/ workspace, shared crate, runner, dict, seeds skeleton)

This commit is contained in:
glm-5.3-flash committed 2026-10-10 21:02:06 +00:00
1 parent 67d1d7e34f
commit 62ff18d1ed
14 files changed
+614 -15

No files matched your search

+1
View File
@@ -7,6 +7,7 @@ members = [
"alkstore-contract-suite",
]
resolver = "3"
exclude = ["fuzz"]
[workspace.package]
version = "0.1.0"
+4
View File
@@ -0,0 +1,4 @@
target
corpus/*
artifacts
coverage
+238
View File
@@ -0,0 +1,238 @@
# This file is automatically @generated by Cargo.
# It is not intended for manual editing.
version = 4
[[package]]
name = "alkstore"
version = "0.1.0"
dependencies = [
"serde",
"serde_json",
"thiserror",
]
[[package]]
name = "alkstore-fuzz"
version = "0.0.0"
dependencies = [
"alkstore",
"alkstore-fuzz-shared",
"libfuzzer-sys",
]
[[package]]
name = "alkstore-fuzz-shared"
version = "0.0.0"
dependencies = [
"alkstore",
"arbitrary",
"serde_json",
]
[[package]]
name = "arbitrary"
version = "1.5.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "3bc62ac97cc33321f50863d514c3bc38a453947a8f9e781137e47c7401020aed"
dependencies = [
"derive_arbitrary",
]
[[package]]
name = "cc"
version = "1.7.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "50a649af8a827553c29fb0cb4bd4a6f1a0dd695bd3232b9bc98bd9c8a3ffbb8b"
dependencies = [
"find-msvc-tools",
"jobserver",
"libc",
"shlex",
]
[[package]]
name = "cfg-if"
version = "1.0.5"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "4e7648175b45a9a48536d676f68d918270699102aa8dab5496df06904c914600"
[[package]]
name = "derive_arbitrary"
version = "1.5.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "1b034bd7d5f032402a2479444dcc6f74e36a03f31854d41680fb240ef682a1ac"
dependencies = [
"proc-macro2",
"quote",
"syn",
]
[[package]]
name = "find-msvc-tools"
version = "0.1.14"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "aedcfb3409746eddb02b9e19ebda1c3394f759a152e48ee875a0844d1b955484"
[[package]]
name = "getrandom"
version = "0.4.3"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "300e883d756b2e4ec94e02791f39b04b522276138852cfc41d9fb7e904106099"
dependencies = [
"cfg-if",
"libc",
"r-efi",
]
[[package]]
name = "itoa"
version = "1.0.18"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "8f42a60cbdf9a97f5d2305f08a87dc4e09308d1276d28c869c684d7777685682"
[[package]]
name = "jobserver"
version = "0.1.35"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "1c00acbd29eabad4a2392fa0e921c874934dbbf4194312ad20f04a0ed67a3cb3"
dependencies = [
"getrandom",
"libc",
]
[[package]]
name = "libc"
version = "0.2.190"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "ce5d3ddc6d3fa000eb1536d85e147bfe31aacaba692ed6a876f95cb7c855be78"
[[package]]
name = "libfuzzer-sys"
version = "0.4.13"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "a9fd2f41a1cba099f79a0b6b6c35656cf7c03351a7bae8ff0f28f25270f929d2"
dependencies = [
"arbitrary",
"cc",
]
[[package]]
name = "memchr"
version = "2.8.3"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "cf8baf1c55e62ffcace7a9f06f4bd9cd3f0c4beb022d3b367256b91b87513d98"
[[package]]
name = "proc-macro2"
version = "1.0.107"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "985e7ec9bb745e6ce6535b544d84d6cd6f7ad8bd711c398938ae983b91a766d9"
dependencies = [
"unicode-ident",
]
[[package]]
name = "quote"
version = "1.0.47"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "1fbf4db142a473a8d80c26bbf18454ed458bf8d26c8219c331daecfdbd079001"
dependencies = [
"proc-macro2",
]
[[package]]
name = "r-efi"
version = "6.0.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "f8dcc9c7d52a811697d2151c701e0d08956f92b0e24136cf4cf27b57a6a0d9bf"
[[package]]
name = "serde"
version = "1.0.229"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "4148590afebada386688f18773da617792bf2ef03ffc1e4cbd2b1d45b023e0ba"
dependencies = [
"serde_core",
]
[[package]]
name = "serde_core"
version = "1.0.229"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "67dca2c9c51e58a4791a4b1ed58308b39c64224d349a935ab5039aa360942a48"
dependencies = [
"serde_derive",
]
[[package]]
name = "serde_derive"
version = "1.0.229"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "e7a5d71263a5a7d47b41f6b3f06ba276f10cc18b0931f1799f710578e2309348"
dependencies = [
"proc-macro2",
"quote",
"syn",
]
[[package]]
name = "serde_json"
version = "1.0.152"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "1741ab7a6cc54a03a89b5d563ed60075c277d9e3cfa73ad0c1f23f23974703c6"
dependencies = [
"itoa",
"memchr",
"serde",
"serde_core",
"zmij",
]
[[package]]
name = "shlex"
version = "2.0.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "f8fadd59c855ef2080decdef8ff161eb6661b86933c9d82e5ba29dc602a55aba"
[[package]]
name = "syn"
version = "3.0.7"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "d62a2e0561533f2ca2561d0cf27fd9fedb640a1bf2616ff5d5c80d99017faadc"
dependencies = [
"proc-macro2",
"quote",
"unicode-ident",
]
[[package]]
name = "thiserror"
version = "2.0.21"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "09e52cb86a36cede5cb101bf8908837b3e4c6e5e59fe7fd85c23fb56200d189e"
dependencies = [
"thiserror-impl",
]
[[package]]
name = "thiserror-impl"
version = "2.0.21"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "fe5197923287db20a58125f0bc85c062f7f2c892de97b18c356f9efb14b28524"
dependencies = [
"proc-macro2",
"quote",
"syn",
]
[[package]]
name = "unicode-ident"
version = "1.0.26"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "d245f478577f809a851594d02313b640fb437e0bb33866753cff937863096954"
[[package]]
name = "zmij"
version = "1.0.23"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "29666d0abbfad1e3dc4dcf6144730dd3a3ab225bbbdac83319345b1b44ccfc1b"
+24
View File
@@ -0,0 +1,24 @@
[package]
name = "alkstore-fuzz"
version = "0.0.0"
publish = false
edition = "2021"
[package.metadata]
cargo-fuzz = true
[dependencies]
libfuzzer-sys = "0.4"
alkstore-fuzz-shared = { path = "shared" }
[dependencies.alkstore]
path = "../alkstore"
[[bin]]
name = "placeholder"
path = "fuzz_targets/placeholder.rs"
test = false
doc = false
bench = false
[workspace]
+57
View File
@@ -0,0 +1,57 @@
# alkstore fuzzing
libFuzzer targets for alkstore's parse/codec surfaces (see
`docs/research/fuzzing.md` for the full rationale and campaign plan).
## Layout
- `fuzz_targets/` — one binary per target; thin `fuzz_target!` wrappers
(land with the wave-7 target tasks).
- `shared/` — the invariant logic, as a plain library so normal
`cargo test` (stable toolchain) can replay the committed corpora
through the same invariants (`fuzz/shared/src/*.rs` `#[cfg(test)]`
modules; quinn's CI pattern). The fuzz binaries are nightly-only;
the shared crate is stable-clean. It consumes alkstore's public API
only and must never become a dependency of any workspace crate.
- `corpus/<target>/` — committed hand-made seeds (regenerate with
`python3 fuzz/gen_fuzz_seeds.py` from the repo root). Grown corpora
and artifacts are gitignored; each target task adds its `!`-negation
line when its corpus lands.
- `run-detached.sh` — mandatory runner for agent sessions: wraps
`cargo fuzz run` in `setsid` + `nohup` + log redirection so an OOM
in a target can never take down the agent host (research doc §5).
- `json.dict` — JSON token dictionary for the structured targets.
- `gen_fuzz_seeds.py` — deterministic seed generator (quiche pattern).
## Targets
| Target | Drives | Invariants |
|---|---|---|
| `payload_codec` | `encode_payload` / `payload_as::<T>` (`alkstore/src/payload.rs`) | filled by its target task |
| `every_parse` | the three `parse_every_interval` implementations (differential) | filled by its target task |
| `name_validation` | `validate_name` / `validate_shared_name` / `validate_local_name` (`alkstore/src/validation.rs`) | filled by its target task |
## Commands
`cargo fuzz build` and `cargo fuzz run` must be executed with `fuzz/`
(or deeper) as the working directory so rustup selects the pinned
nightly toolchain — the detached runner handles that itself.
```sh
# build (nightly, pinned by rust-toolchain.toml inside fuzz/; run from fuzz/)
cargo fuzz build
# agent sessions: detached campaign (never foreground; CWD-independent)
FUZZ_RUNTIME_SECS=600 fuzz/run-detached.sh <target> [extra libfuzzer args...]
# corpus replay through the invariants (stable toolchain, no nightly)
cargo test --manifest-path fuzz/shared/Cargo.toml
# coverage
cargo fuzz coverage <target>
```
The fuzz workspace is excluded from the main workspace (`exclude` in
the root `Cargo.toml`); it pins its own nightly toolchain via
`rust-toolchain.toml` and does not affect the root workspace's stable
toolchain or MSRV.
+11
View File
@@ -0,0 +1,11 @@
//! Placeholder target: the scaffold carries no real targets yet. The
//! wave-1 target tasks (payload_codec, every_parse, name_validation)
//! replace this with their fuzz_targets entries; it exists only so the
//! bins-only sidecar manifest parses and `cargo fuzz build` exercises
//! the nightly + ASAN toolchain check from wave 7 onward.
#![no_main]
use libfuzzer_sys::fuzz_target;
fuzz_target!(|_data: &[u8]| {});
+32
View File
@@ -0,0 +1,32 @@
#!/usr/bin/env python3
"""Regenerate the committed seed corpora for alkstore's fuzz targets.
Writes into fuzz/corpus/<target>/. Deterministic: fixed inputs only, no
randomness. Run from the repo root:
python3 fuzz/gen_fuzz_seeds.py
Target seed functions and the main() registry land here with the
target tasks; the shared seed_name()/write_seed() plumbing is stable.
"""
import os
def seed_name(target, i):
return os.path.join("fuzz", "corpus", target, f"seed-{i:03d}")
def write_seed(target, i, data):
path = seed_name(target, i)
os.makedirs(os.path.dirname(path), exist_ok=True)
with open(path, "wb") as f:
f.write(data)
def main():
pass
if __name__ == "__main__":
main()
+23
View File
@@ -0,0 +1,23 @@
# JSON tokens for the envelope framing targets (ADR-014 wire format).
"call.requested"
"call.responded"
"call.completed"
"call.aborted"
"call.error"
"call.published"
"type"
"id"
"payload"
"operationId"
"input"
"output"
"code"
"message"
"retryable"
"details"
"NOT_FOUND"
"INVALID_INPUT"
"INTERNAL"
"TIMEOUT"
"FORBIDDEN"
"CONNECTION_CLOSED"
+29
View File
@@ -0,0 +1,29 @@
#!/usr/bin/env bash
# Detached fuzzing runner for agent sessions: the fuzz campaign never
# runs as a foreground child of the session (OOM in a target must not
# take down the agent host), and survives the session ending.
#
# Usage: fuzz/run-detached.sh <target> [extra libfuzzer args...]
# (works from the repo root or from fuzz/; CWD-independent)
# FUZZ_RUNTIME_SECS overrides the per-campaign budget (default 600 s).
#
# Poll instead of waiting:
# tail -n 50 fuzz/artifacts/<target>-*.log
# ls fuzz/artifacts/<target>/ (crash-* / oom-* / timeout-* files)
# pgrep -f "cargo fuzz run <target>"
set -euo pipefail
target="${1:?usage: run-detached.sh <target> [extra libfuzzer args...]}"
shift
root="$(git rev-parse --show-toplevel)"
fuzz_dir="$root/fuzz"
mkdir -p "$fuzz_dir/artifacts"
runtime="${FUZZ_RUNTIME_SECS:-600}"
log="$fuzz_dir/artifacts/${target}-$(date -u +%Y%m%d-%H%M%S).log"
cd "$fuzz_dir"
setsid nohup cargo fuzz run "$target" -- \
-fork=1 -rss_limit_mb=2048 -malloc_limit_mb=2048 -timeout=25 \
-max_total_time="$runtime" "$@" \
>"$log" 2>&1 < /dev/null &
echo "pid=$! log=$log"
+3
View File
@@ -0,0 +1,3 @@
[toolchain]
channel = "nightly"
components = ["llvm-tools-preview"]
+13
View File
@@ -0,0 +1,13 @@
[package]
name = "alkstore-fuzz-shared"
version = "0.0.0"
publish = false
edition = "2021"
[dependencies]
alkstore = { path = "../../alkstore" }
serde_json = "1"
[dependencies.arbitrary]
version = "1"
features = ["derive"]
+108
View File
@@ -0,0 +1,108 @@
//! `Arbitrary` support for `serde_json::Value` — a bounded-depth
//! recursive generator shared by the semantic targets. Depth and size
//! bounds keep exec/s high (serde_json's own 128-depth recursion limit
//! is the decode-side guard; this generator explores structure).
use arbitrary::{Arbitrary, Unstructured};
use serde_json::{Map, Number, Value};
#[derive(Debug, arbitrary::Arbitrary, Clone)]
#[allow(dead_code)]
enum JsonShape {
Null,
Bool(bool),
Number {
mantissa: u32,
negative: bool,
},
String {
len: u8,
byte: u8,
},
Array {
len: u8,
items: Vec<JsonShape>,
},
Object {
len: u8,
keys: Vec<(u8, u8)>,
values: Vec<JsonShape>,
},
}
/// Generate a `Value` with bounded depth: arrays/objects nest at most
/// 6 levels, each holding at most 6 items. The `u8`-bounded shapes
/// keep the generator cheap; the semantic target's value space is
/// structure, not size.
impl<'a> Arbitrary<'a> for JsonValue {
fn arbitrary(u: &mut Unstructured<'a>) -> arbitrary::Result<Self> {
let shape = JsonShape::arbitrary(u)?;
Ok(JsonValue(shape_to_value(shape, 6)))
}
}
fn shape_to_value(shape: JsonShape, depth: u32) -> Value {
if depth == 0 {
return Value::Null;
}
match shape {
JsonShape::Null => Value::Null,
JsonShape::Bool(b) => Value::Bool(b),
JsonShape::Number { mantissa, negative } => {
let n = i64::from(mantissa % 1_000_000);
Number::from(if negative { -n } else { n }).into()
}
JsonShape::String { len, byte } => {
// Repeat a printable-ish char; embedded control bytes and
// invalid UTF-8 come from the fuzzer's byte mutations of
// the raw input in the payload targets, not here.
let n = (len % 64) as usize;
std::iter::repeat_n((0x41u8 + (byte % 26)) as char, n)
.collect::<String>()
.into()
}
JsonShape::Array { len, items } => {
let items: Vec<Value> = items
.into_iter()
.take(6)
.map(|s| shape_to_value(s, depth - 1))
.collect();
let _ = len;
Value::Array(items)
}
JsonShape::Object { keys, values, .. } => {
let mut map = Map::new();
for (i, (k_len, k_byte)) in keys.into_iter().take(6).enumerate() {
let key: String = std::iter::repeat_n(
(0x61u8 + (k_byte % 26)) as char,
(k_len % 16) as usize + 1,
)
.collect();
let value = values
.get(i)
.map(|s| shape_to_value(s.clone(), depth - 1))
.unwrap_or(Value::Null);
map.insert(key, value);
}
Value::Object(map)
}
}
}
/// Newtype so `impl Arbitrary` applies (serde_json re-exports `Value`
/// as `serde_json::Value`; a foreign-type impl is not allowed).
#[derive(Debug, Clone, PartialEq)]
pub struct JsonValue(pub Value);
impl std::ops::Deref for JsonValue {
type Target = Value;
fn deref(&self) -> &Value {
&self.0
}
}
impl From<JsonValue> for Value {
fn from(value: JsonValue) -> Value {
value.0
}
}
+6
View File
@@ -0,0 +1,6 @@
//! Invariant logic shared by alkstore's fuzz targets. Kept out of the
//! fuzz-target binaries so the corpus replay unit tests can exercise the
//! same invariant checks against every committed corpus entry (the quinn
//! CI pattern) without a nightly toolchain.
pub mod arbitrary_value;
+65 -15
View File
@@ -1,7 +1,7 @@
---
id: fuzz-scaffold
name: Fuzz sidecar scaffold — `fuzz/` workspace, runner, dict, seed generator, shared-crate skeleton
status: pending
status: completed
depends_on: []
scope: moderate
risk: low
@@ -67,26 +67,31 @@ Toolchain prerequisites verified present in the dev environment
## Acceptance Criteria
- [ ] `fuzz/` exists with the §2 layout: sidecar `Cargo.toml` (own
- [x] `fuzz/` exists with the §2 layout: sidecar `Cargo.toml` (own
`[workspace]`, `publish = false`), `rust-toolchain.toml`
(nightly + llvm-tools-preview), `shared/` crate, `run-detached.sh`
(executable), `json.dict`, `gen_fuzz_seeds.py`, `README.md`,
`fuzz/.gitignore`
- [ ] `fuzz/shared` compiles and tests green on **stable** via
- [x] `fuzz/shared` compiles and tests green on **stable** via
`cargo test --manifest-path fuzz/shared/Cargo.toml` (the gate
command works from the wave's first task onward)
- [ ] `cargo fuzz build` succeeds from `fuzz/` (nightly selected by the
pin; zero targets yet is acceptable if cargo-fuzz tolerates it —
otherwise a placeholder target documents the check; record which)
- [ ] Root workspace unaffected: `cargo build` / `cargo test` /
- [x] `cargo fuzz build` succeeds from `fuzz/` (nightly selected by the
pin; **cargo-fuzz does NOT tolerate zero targets** here — a
bins-only package with no `[[bin]]` fails manifest parse, which
also blocks the shared gate's workspace discovery; a placeholder
target `fuzz_targets/placeholder.rs` documents the check and is
replaced by the wave-1 target tasks — recorded, as this AC asked)
- [x] Root workspace unaffected: `cargo build` / `cargo test` /
`clippy -D warnings` / `fmt --check` green from the root, and the
root toolchain is still stable (the pin did not leak upward)
- [ ] `exclude = ["fuzz"]` present on the root `[workspace]` table
- [ ] `run-detached.sh` is byte-identical to alksocks' modulo the
usage-line naming (no behavior drift)
- [ ] `shared` is not referenced by any workspace crate manifest
(wasm posture intact: `cargo check --target
wasm32-unknown-unknown -p alkstore-mem` still clean)
- [x] `exclude = ["fuzz"]` present on the root `[workspace]` table
- [x] `run-detached.sh` is byte-identical to alksocks' modulo the
usage-line naming (`cmp` clean — the usage line is already
repo-agnostic, so no diff at all)
- [x] `shared` is not referenced by any workspace crate manifest
(only the root's `exclude = ["fuzz"]` mentions fuzz at all;
`cargo check --target wasm32-unknown-unknown -p alkstore-mem`
clean)
## References
@@ -99,8 +104,53 @@ Toolchain prerequisites verified present in the dev environment
## Notes
> To be filled by implementation agent
- **Zero targets are NOT tolerated by cargo-fuzz / cargo here** — the
AC's "if tolerated" branch failed: a bins-only package with no
`[[bin]]` entries cannot manifest-parse ("no targets specified"),
and that parse failure also breaks the shared gate's workspace
discovery (`cargo test --manifest-path fuzz/shared/Cargo.toml`
resolves the workspace root at `fuzz/`). Placeholder route taken
(as the AC's otherwise-branch prescribed): `fuzz_targets/placeholder.rs`
— a minimal `fuzz_target!` stub with a doc comment saying the wave-1
target tasks replace it — plus its `[[bin]]` entry in `fuzz/Cargo.toml`.
- **Repo-layout substitution beyond naming:** alksocks has its package
at the repo root (`alksocks = { path = "../.." }`); alkstore is the
ADR-001 workspace split, so the core crate sits in the `alkstore/`
subdir — shared's dep is `alkstore = { path = "../../alkstore" }`
and the fuzz package's is `path = "../alkstore"`.
- **`json.dict`** is byte-identical across alkcall / alktunnels /
alksocks (md5-verified), so it was copied verbatim (alkcall-envelope
tokens). Whether any alkstore-specific tokens should be added is a
target-task concern, not scaffold scope.
- `fuzz/Cargo.lock` is committed (alksocks pattern); generated
fresh — alksocks' lock is not portable across dep sets.
- `arbitrary_value.rs` copied verbatim except one intra-comment stale
reference: alksocks' `envelope_frame` → "the payload targets".
- `fuzz/.gitignore` has no `!corpus/<target>` negation lines yet —
each target task adds its own when its corpus lands (task spec).
- `gen_fuzz_seeds.py` is the plumbing-only skeleton (`seed_name` /
`write_seed`, empty `main()` registry); runs clean as a no-op.
## Summary
> To be filled on completion
Landed the wave-7 scaffold: `fuzz/` sidecar workspace
(`alkstore-fuzz`, `publish = false`, own `[workspace]`, libfuzzer-sys +
path-dep shared crate) with `rust-toolchain.toml` pinning
nightly + llvm-tools-preview inside `fuzz/` only; stable-toolchain
`fuzz/shared` (`alkstore-fuzz-shared`) with the quinn-CI-pattern crate
doc and the alksocks `arbitrary_value.rs` `JsonValue` bounded-depth
helper; `run-detached.sh` (byte-identical to alksocks', executable);
`json.dict`; `gen_fuzz_seeds.py` skeleton; `.gitignore` (no corpus
negations yet); `README.md` skeleton; `exclude = ["fuzz"]` on the root
workspace; and the placeholder fuzz target so the toolchain check and
the shared gate work before the real targets land.
Verified: `cargo test --manifest-path fuzz/shared/Cargo.toml` green on
stable from the root; `cargo fuzz build` green from `fuzz/` (nightly
pin selected; placeholder + shared + alkstore compile under ASAN);
shared clippy `-D warnings` + `fmt --check` green; root `cargo build` /
`cargo test` (all suites pass) / clippy `-D warnings` / `fmt --check`
green with the stable toolchain still active at the root (rustc 1.99.0
stable; no upward pin leak); `cargo check -p alkstore-mem --target
wasm32-unknown-unknown` clean; no workspace manifest references
`alkstore-fuzz-shared` (only the root `exclude` mentions `fuzz`).