fuzz: wave 2 — stateful read_opseq + layout_build targets, seeds, one engine fix

Targets 3-4 of docs/plans/fuzzing.md, per the sibling layout:

- fuzz/shared/src/read_opseq.rs — SequentialReader op sequences
  (Next/NextBorrowed/Field/Reset/End, Arbitrary-derived) over hostile
  buffers under the fixed packed schema menu. Invariants: cursor
  discipline (failed read leaves position untouched, state replay
  deterministic), None sticky at plan end, plan-order full walks with
  a spin bound, read_field leaves a usable reader, ADR-007 reader
  independence (shared Arc, isolated cursors), and the plan §6-1
  record-count ≥4-verified-bytes bound encoded as an explicit End-op
  assertion.
- fuzz/shared/src/layout_build.rs — LayoutBuilder::build with
  adversarial var_sizes over a five-schema menu (string/bytes, nested
  struct, byte-disc union, record+array, fixed control). Invariants:
  Offset-class failures only, position disjointness + total-size
  bounds, variable fields record their 4-byte prefix, failed writes
  leave the buffer byte-identical, write→read pair round trip.
- derive_var_sizes discovers the synthetic keys ('p.__discriminator')
  the builder actually wants by parsing the quoted key from the
  Offset reason.
- 73 committed seeds (58 read_opseq + 15 layout_build) hand-encoded
  against the pinned arbitrary 1.4.2 derive layout (4-byte LE
  multiply-shift variant selectors, keep-going vec elements,
  take-rest last field) and pinned by decode_lands_on_the_intended_variants
  replay tests; gen_fuzz_seeds.py mirrors the encoders.
- Engine fix (finding W2-1): plan_read_array returned Ok for a
  fixed-stride array whose count*stride window extended past the
  buffer — the struct/union arms bounds-check, the array arm did not;
  a truncated array deferred the failure to the next field (wrong
  path) or masked it entirely as an Ok walk. Now an Access error
  naming the array, regression test in sequential_reader.rs.
- Packed-mode 'encoding: offset-indirect' pinned as the documented
  inline-length-prefix no-op (finding W2-2, bast-format.md Default
  strategy selection); open design question recorded as plan §6-7.

Verification: fuzz corpus replay 19/19; main crate 570 tests incl.
the new regression; clippy -D warnings clean (crate + shared); wasm
build clean; cargo fuzz build clean (nightly confined to fuzz/).
Smoke campaigns (10 min detached each): read_opseq 52.1k execs exit 0
empty artifacts, layout_build 42.4k execs exit 0 empty artifacts; no
crash/oom/timeout on any fork job.
This commit is contained in:
glm-5.3-flash committed 2026-09-30 06:26:05 +00:00
1 parent 752e36b526
commit 16b9023f60
83 files changed
+1574 -17

No files matched your search

+564
View File
@@ -0,0 +1,564 @@
//! Invariants for the packed `LayoutBuilder` (wave 2, target 4):
//! no-panic across adversarial `var_sizes` (empty, zero, huge,
//! mismatched with declarations), failed-write buffer-untouched /
//! documented-partial contracts, position disjointness + bounds for
//! the reported total size on a successful build, and the
//! write-side/read-side pair contract for offset-indirect fields
//! (`read_*_indirect` bounds partition holds for everything the write
//! side can emit).
use alktype::data_access;
use alktype::layout_builder::FieldPosition;
use alktype::schema::{AlkTypeKind, Endian};
use alktype::{LayoutMode, PackedLayout};
use alktype::{AlkTypeEngine, AlkTypeError};
use arbitrary::Arbitrary;
use std::collections::HashMap;
/// The schema menu: one packed struct per variable-width shape plus a
/// fixed-size control. The fuzzer picks the schema lane by byte and
/// the `var_sizes` / write values from the payload.
const MENUS: [&str; 5] = [
// 0: one string + one bytes + tail (the classic variable pair).
r##"{"$defs": {"S": {"kind": "struct", "endian": "little", "fields": [
{ "name": "s", "kind": "string" },
{ "name": "b", "kind": "bytes" },
{ "name": "tail", "kind": "uint8" }]}}}"##,
// 1: nested struct with a variable field inside.
r##"{"$defs": {"S": {"kind": "struct", "fields": [
{ "name": "hdr", "kind": {"kind": "struct", "fields": [
{ "name": "id", "kind": "uint8" },
{ "name": "name", "kind": "string" }]}},
{ "name": "tail", "kind": "uint32" }]}}}"##,
// 2: union with byte discriminator whose variant has a variable
// field (paths shift with the discriminator value).
r##"{"$defs": {"S": {"kind": "struct", "fields": [
{ "name": "p", "kind": {"$ref": "#/$defs/Packet"} }]},
"Packet": {"kind": "union",
"discriminator": {"kind": "byte", "offset": 0, "type": "uint8"},
"mapping": {"5": {"$ref": "#/$defs/Read"}}},
"Read": {"kind": "struct", "fields": [
{ "name": "handle", "kind": "uint32" },
{ "name": "path", "kind": "string" }]}}}"##,
// 3: record (variable) + array control.
r##"{"$defs": {"S": {"kind": "struct", "fields": [
{ "name": "counts", "kind": {"kind": "record", "values": "uint32" } },
{ "name": "vals", "kind": {"kind": "array", "element": "uint16", "count": 2 } }]}}}"##,
// 4: fixed-size only — var_sizes must be ignorable here.
r##"{"$defs": {"S": {"kind": "struct", "endian": "big", "fields": [
{ "name": "x", "kind": "uint32" },
{ "name": "y", "kind": "uint8" }]}}}"##,
];
#[derive(Debug, Arbitrary, Clone, Copy)]
pub enum SizeKind {
Zero,
One,
Small(u8),
Huge,
Overflowing,
FromInput(u8),
}
#[derive(Debug, Arbitrary)]
pub struct LayoutInput {
menu: u8,
size_kind: SizeKind,
drive_writes: bool,
roundtrip_probe: bool,
payload: Vec<u8>,
}
pub fn fuzz_layout_build(data: &[u8]) {
let Ok(input) = LayoutInput::arbitrary_take_rest(arbitrary::Unstructured::new(data)) else {
return;
};
let menu = &MENUS[(input.menu as usize) % MENUS.len()];
let Ok(doc) = serde_json::from_str::<serde_json::Value>(menu) else {
return;
};
let Ok(engine) = AlkTypeEngine::compile(&doc, "S", LayoutMode::Packed, None) else {
// The menu schemas are all valid; a meta-gate failure on one is
// a menu bug, but the harness treats compile as Result anyway.
return;
};
let Some(builder) = engine.layout_builder() else {
return;
};
let var_sizes = derive_var_sizes(builder, &input.size_kind, &input.payload);
let layout = builder.build(&var_sizes);
match layout {
Ok(l) => {
assert_position_partition(&l);
if input.drive_writes {
drive_writes(&l, &input.payload);
}
if input.roundtrip_probe {
write_then_read_roundtrip(&doc, &l, &input.payload);
}
}
Err(e) => {
// Packed-mode build failures are Offset-class by contract
// (missing var size, unknown discriminator, overflow).
match e {
AlkTypeError::Offset { field_path, .. } => {
assert!(
!field_path.is_empty(),
"Offset errors carry the field path"
);
}
other => panic!(
"packed build with adversarial var_sizes must fail with Offset, got {other:?}"
),
}
}
}
}
/// Build a `var_sizes` map keyed by the schema's actual
/// variable-length field paths (discovered from a reference build's
/// error paths — the builder names them on failure), with values from
/// the size_kind selector. Missing keys are the mismatched case.
fn derive_var_sizes(
builder: &alktype::LayoutBuilder,
kind: &SizeKind,
payload: &[u8],
) -> HashMap<String, usize> {
let value = match kind {
SizeKind::Zero => 0usize,
SizeKind::One => 1,
SizeKind::Small(b) => *b as usize,
SizeKind::Huge => 1 << 20,
SizeKind::Overflowing => usize::MAX - 2,
SizeKind::FromInput(b) => payload.iter().fold(0usize, |a, x| {
(a.wrapping_mul(31).wrapping_add(*x as usize))
.min(if *b & 0x80 != 0 { usize::MAX } else { *b as usize * 97 })
}),
};
// Discover every variable-length path by walking a probe build
// that fails on the first missing size, then supplying it and
// repeating until the build succeeds or the error stops asking.
// The error names the *field* (`p`) but the missing key is the
// synthetic `p.__discriminator` / `p.__variant` — quoted in the
// reason; parse it out, falling back to the documented suffix per
// failure class.
let mut map = HashMap::new();
for _ in 0..16 {
match builder.build(&map) {
Ok(_) => break,
Err(AlkTypeError::Offset { field_path, reason }) => {
let key = reason
.split('\'')
.nth(1)
.map(str::to_string)
.unwrap_or(field_path);
map.insert(key, value);
}
Err(_) => break,
}
}
map
}
/// A successful layout's positions are disjoint and in-bounds for the
/// reported total size. Discriminator entries (`__discriminator`) sit
/// inside the union region and may share bytes with the union start —
/// they are asserted in-bounds, and disjointness is asserted over the
/// non-synthetic leaf positions only (the synthetic disc entry
/// documents the discriminator word's home; variant fields start right
/// after it, so they are disjoint from it by construction).
fn assert_position_partition(l: &PackedLayout) {
let total = l.total_size();
let mut occupied: Vec<(usize, usize, String)> = Vec::new();
for (path, pos) in l.iter() {
assert!(
pos.offset.checked_add(pos.size).is_some()
&& pos.offset + pos.size <= total,
"field {path} window [{}, {}) exceeds total_size {total}",
pos.offset,
pos.offset + pos.size
);
match pos.kind {
// The length-prefix size 4 is contract for variable kinds.
AlkTypeKind::String | AlkTypeKind::Bytes | AlkTypeKind::Record => {
assert_eq!(pos.size, 4, "variable field {path} records its prefix size");
}
k => {
assert_eq!(
k.type_size(),
Some(pos.size),
"fixed field {path} size matches the kind's width"
);
}
}
if path.ends_with("__discriminator") {
continue;
}
for (o_start, o_end, o_path) in &occupied {
assert!(
pos.offset >= *o_end || pos.offset + pos.size <= *o_start,
"field {path} [{}, {}) overlaps {o_path} [{o_start}, {o_end})",
pos.offset,
pos.offset + pos.size
);
}
occupied.push((pos.offset, pos.offset + pos.size, path.clone()));
}
}
/// Write every field's declared value at its position (using the
/// engine-free data_access lane with the layout's own metadata). The
/// contract pinned: a failed fixed/length-prefixed write leaves the
/// buffer byte-equal to its pre-call snapshot; a failed
/// write_bytes_indirect leaves the declared pair region written
/// (documented partial) but never trips a panic, and every failed
/// write is an Access error naming the path.
fn drive_writes(l: &PackedLayout, payload: &[u8]) {
let endian = endian_of(l);
let mut buf = vec![0u8; l.total_size()];
for (path, pos) in l.iter() {
if path.ends_with("__discriminator") {
// Discriminator words are written by the value lane below.
continue;
}
let byte = payload.first().copied().unwrap_or(0x41);
match pos.kind {
AlkTypeKind::String => {
let value = "abcdefgh";
let before = buf.clone();
match data_access::write_string(&mut buf, pos.offset, value, path, endian) {
Ok(_) => {}
Err(e) => {
assert_eq!(
buf, before,
"failed write_string leaves the buffer untouched"
);
assert!(matches!(e, AlkTypeError::Access { .. }), "got {e:?}");
}
}
}
AlkTypeKind::Bytes | AlkTypeKind::Record => {
// Write the data inline after the prefix position: the
// packed variable contract. A payload-sized write into
// the recorded 4-byte window fails cleanly.
let before = buf.clone();
match data_access::write_bytes(&mut buf, pos.offset, &[byte; 8], path, endian) {
Ok(_) => {
// The prefix + data fit: reading back the
// declared length yields the declared bytes.
let back =
data_access::read_bytes(&buf, pos.offset, path, endian).expect(
"a successful write_bytes prefix always reads back",
);
assert_eq!(back, [byte; 8]);
}
Err(e) => {
assert_eq!(
buf, before,
"failed write_bytes leaves the buffer untouched"
);
assert!(matches!(e, AlkTypeError::Access { .. }), "got {e:?}");
}
}
}
k => {
// Fixed-width kind at a recorded offset: the value lane.
let before = buf.clone();
let res = write_fixed(&mut buf, pos, byte, path, endian);
let _ = k;
if res.is_err() {
assert_eq!(buf, before, "failed fixed write leaves the buffer untouched");
}
}
}
}
}
fn endian_of(_l: &PackedLayout) -> Endian {
// The packed layout itself does not store endianness; the seeds'
// menus declare it, the roundtrip probe compiles its own engine.
Endian::Little
}
fn write_fixed(
buf: &mut [u8],
pos: &FieldPosition,
byte: u8,
path: &str,
endian: Endian,
) -> Result<(), AlkTypeError> {
use alktype::data_access as da;
match pos.kind {
AlkTypeKind::Int8 => da::write_i8(buf, pos.offset, byte as i8, path),
AlkTypeKind::Uint8 => da::write_u8(buf, pos.offset, byte, path),
AlkTypeKind::Boolean => da::write_bool(buf, pos.offset, byte & 1 == 1, path),
AlkTypeKind::Int16 => da::write_i16(buf, pos.offset, byte as i16, path, endian),
AlkTypeKind::Uint16 => da::write_u16(buf, pos.offset, byte as u16, path, endian),
AlkTypeKind::Int32 => da::write_i32(buf, pos.offset, byte as i32, path, endian),
AlkTypeKind::Uint32 | AlkTypeKind::Enum => {
da::write_u32(buf, pos.offset, byte as u32, path, endian)
}
AlkTypeKind::Int64 => da::write_i64(buf, pos.offset, byte as i64, path, endian),
AlkTypeKind::Uint64 => da::write_u64(buf, pos.offset, byte as u64, path, endian),
AlkTypeKind::Float32 => da::write_f32(buf, pos.offset, byte as f32, path, endian),
AlkTypeKind::Float64 => da::write_f64(buf, pos.offset, byte as f64, path, endian),
other => panic!("write_fixed over a non-fixed kind {other:?}"),
}
}
/// The write-then-read pair fixture (top-level string only — the
/// sequential walk surfaces nested structs as `FieldValue::Struct`
/// descriptors and never descends, so a nested `hdr.name` can never be
/// round-tripped through `read_next`): build the layout, write a
/// length-prefixed value at the recorded position into a correctly
/// sized buffer, then read it back through the sequential walk — the
/// write side and read side of the format are one contract.
fn write_then_read_roundtrip(doc: &serde_json::Value, l: &PackedLayout, payload: &[u8]) {
let Some(s_pos) = l.get("s") else {
return;
};
let Ok(engine) = AlkTypeEngine::compile(doc, "S", LayoutMode::Packed, None) else {
return;
};
let value = payload
.iter()
.take(4)
.map(|b| std::char::from_u32(0x40 + u32::from(b % 26)).unwrap_or('a'))
.collect::<String>();
let mut buf = vec![0u8; l.total_size()];
data_access::write_string(&mut buf, s_pos.offset, &value, "s", Endian::Little)
.expect("roundtrip buffer is sized from the same layout");
let mut reader = engine.sequential_reader().expect("packed reader");
let mut steps = 0usize;
loop {
steps += 1;
if steps > buf.len() + 2 {
panic!("roundtrip walk exceeded its bound");
}
match reader.read_next(&buf) {
Ok(Some((name, alktype::FieldValue::String(s)))) if name == "s" => {
assert_eq!(s, value, "the written value reads back verbatim");
return;
}
Ok(Some(_)) => continue,
Ok(None) => panic!("the roundtrip walk ended before reaching the string"),
Err(e) => panic!("the roundtrip walk over a builder-written buffer failed: {e:?}"),
}
}
}
#[cfg(test)]
mod corpus_replay {
use super::*;
#[test]
fn committed_corpus_replays_through_the_invariants() {
let corpus =
std::path::PathBuf::from(env!("CARGO_MANIFEST_DIR")).join("../corpus/layout_build");
let mut count = 0usize;
for entry in std::fs::read_dir(&corpus).expect("corpus directory is committed") {
let path = entry.expect("corpus entry readable").path();
let data = std::fs::read(&path).expect("corpus entry readable");
fuzz_layout_build(&data);
count += 1;
}
assert!(
count >= 10,
"committed seed corpus is present, found {count}"
);
}
/// Byte encodings against the pinned arbitrary 1.4.2 derive layout:
/// field order { menu: u8, size_kind: SizeKind, drive_writes: bool,
/// roundtrip_probe: bool, payload: Vec<u8> } with payload
/// take-rest. SizeKind has 6 variants.
mod enc {
pub fn variant_u32(k: u64, count: u64) -> [u8; 4] {
let mut v = (k << 32).div_ceil(count);
while (v.wrapping_mul(count) >> 32) != k {
v += 1;
}
u32::try_from(v).expect("k < count always admits a < 2^32 v").to_le_bytes()
}
pub const SIZE_ZERO: u64 = 0;
pub const SIZE_SMALL: u64 = 2;
pub const SIZE_HUGE: u64 = 3;
pub const SIZE_OVERFLOW: u64 = 4;
pub const SIZE_FROM_INPUT: u64 = 5;
pub fn size_kind(tag: u64, payload_byte: u8) -> Vec<u8> {
let mut v = variant_u32(tag, 6).to_vec();
if tag == SIZE_SMALL || tag == SIZE_FROM_INPUT {
v.push(payload_byte);
}
v
}
pub fn layout_seed(
menu: u8,
size: Vec<u8>,
payload: &[u8],
drive: bool,
rt: bool,
) -> Vec<u8> {
let mut v = vec![menu];
v.extend_from_slice(&size);
v.push(u8::from(drive));
v.push(u8::from(rt));
for b in payload {
v.push(0x01);
v.push(*b);
}
v.push(0x00);
v
}
}
#[test]
fn decode_lands_on_the_intended_variants() {
fn decode(data: &[u8]) -> (usize, String, bool, bool, usize) {
let input =
LayoutInput::arbitrary_take_rest(arbitrary::Unstructured::new(data))
.expect("the encoding decodes");
let size = match input.size_kind {
SizeKind::Zero => "Zero".to_string(),
SizeKind::One => "One".to_string(),
SizeKind::Small(n) => format!("S{n}"),
SizeKind::Huge => "Huge".to_string(),
SizeKind::Overflowing => "Ovf".to_string(),
SizeKind::FromInput(n) => format!("I{n}"),
};
(
input.menu as usize,
size,
input.drive_writes,
input.roundtrip_probe,
input.payload.len(),
)
}
// bool is `byte & 1 == 1` — any odd byte is true, even false.
let seed =
enc::layout_seed(3, enc::size_kind(enc::SIZE_SMALL, 10), b"wxyz", true, false);
assert_eq!(decode(&seed), (3, "S10".to_string(), true, false, 4));
assert_eq!(
decode(&enc::layout_seed(
4,
enc::size_kind(enc::SIZE_ZERO, 0),
&[],
false,
true
)),
(4, "Zero".to_string(), false, true, 0)
);
assert_eq!(
decode(&enc::layout_seed(
2,
enc::size_kind(enc::SIZE_OVERFLOW, 0),
&[],
true,
true
)),
(2, "Ovf".to_string(), true, true, 0)
);
assert_eq!(
decode(&enc::layout_seed(
0,
enc::size_kind(enc::SIZE_FROM_INPUT, 0x80),
&[9],
true,
false
)),
(0, "I128".to_string(), true, false, 1)
);
}
#[test]
fn spec_example_u8_u32_string_total_19_holds() {
// The builder's own doc example, exercised through the fuzz
// entry's build path: s=10, b=0 → tail at 18, total 19.
let doc: serde_json::Value =
serde_json::from_str(MENUS[0]).expect("menu 0 parses");
let engine =
AlkTypeEngine::compile(&doc, "S", LayoutMode::Packed, None).expect("menu compiles");
let mut vs = HashMap::new();
vs.insert("s".to_string(), 10usize);
vs.insert("b".to_string(), 0usize);
let l = engine
.layout_builder()
.expect("packed builder")
.build(&vs)
.expect("builds");
assert_eq!(l.get("s").expect("s").offset, 0);
assert_eq!(l.get("b").expect("b").offset, 14);
assert_eq!(l.get("tail").expect("tail").offset, 18);
assert_eq!(l.total_size(), 19);
}
#[test]
fn failed_writes_untouched_and_roundtrip_holds() {
// The write contract menu through the fuzz entry: sizes that
// fit, sizes that overflow, and the roundtrip probe.
fuzz_layout_build(&enc::layout_seed(
0,
enc::size_kind(enc::SIZE_SMALL, 7),
b"x",
true,
true,
));
fuzz_layout_build(&enc::layout_seed(0, enc::size_kind(enc::SIZE_HUGE, 0), b"", true, true));
fuzz_layout_build(&enc::layout_seed(
2,
enc::size_kind(enc::SIZE_SMALL, 9),
b"y",
true,
true,
));
fuzz_layout_build(&enc::layout_seed(3, enc::size_kind(enc::SIZE_ZERO, 0), b"", true, true));
}
#[test]
fn union_and_record_overflow_failures_are_offset_class() {
// The union menu (2) with an overflowing size must fail with
// Offset naming the path (derive_var_sizes supplies
// __discriminator then hits overflow); record menu with huge.
fuzz_layout_build(&enc::layout_seed(
2,
enc::size_kind(enc::SIZE_OVERFLOW, 0),
b"",
false,
false,
));
fuzz_layout_build(&enc::layout_seed(
3,
enc::size_kind(enc::SIZE_HUGE, 0),
b"",
false,
false,
));
}
#[test]
fn union_menu_reaches_success_paths_with_the_discovered_key_form() {
// The discovery loop parses the synthetic key
// ('p.__discriminator') out of the reason quoting — with a
// small size the union layout builds and the position
// partition holds over the variant's fields.
fuzz_layout_build(&enc::layout_seed(
2,
enc::size_kind(enc::SIZE_SMALL, 8),
b"",
true,
false,
));
fuzz_layout_build(&enc::layout_seed(
2,
enc::size_kind(enc::SIZE_ZERO, 0),
b"",
true,
true,
));
}
}
+2
View File
@@ -6,3 +6,5 @@
pub mod bast_compile;
pub mod data_access;
pub mod layout_build;
pub mod read_opseq;
+603
View File
@@ -0,0 +1,603 @@
//! Invariants for the stateful `SequentialReader` (wave 2, target 3):
//! no-panic over any op interleaving and any buffer, cursor discipline
//! (a failed read leaves the cursor untouched; `reset` restores the
//! exact initial state; the cursor never exceeds the buffer), plan-order
//! reads with `None` exactly at plan end, `read_field` leaves a usable
//! reader on every outcome, the wire-controlled record-count spin bound
//! (≥ 4 verified bytes per inner iteration), and ADR-007's
//! reader-independence contract.
use alktype::{AlkTypeEngine, AlkTypeError, LayoutMode};
use arbitrary::Arbitrary;
use std::sync::Arc;
/// The fixed schema menu. Every shape a hostile op sequence can
/// interleave against: fixed-width leaves, a variable-length field
/// sandwiched between tails, nested structs, fixed-stride arrays,
/// records (the count-loop spin candidate), and a byte-discriminator
/// TUnion.
const DOC: &str = r##"{
"$defs": {
"S": {
"kind": "struct",
"endian": "little",
"fields": [
{ "name": "a", "kind": "uint8" },
{ "name": "n", "kind": "uint32" },
{ "name": "s", "kind": "string" },
{ "name": "tail", "kind": "uint8" },
{ "name": "nested", "kind": { "$ref": "#/$defs/Inner" } },
{ "name": "arr", "kind": { "kind": "array", "element": "uint16", "count": 2 } },
{ "name": "rec", "kind": { "kind": "record", "values": "uint32" } },
{ "name": "un", "kind": { "$ref": "#/$defs/U" } }
]
},
"Inner": { "kind": "struct", "fields": [
{ "name": "x", "kind": "int16" },
{ "name": "y", "kind": "bool" }
]},
"U": {
"kind": "union",
"discriminator": { "kind": "byte", "offset": 0, "type": "uint8" },
"mapping": {
"0": { "$ref": "#/$defs/Small" },
"1": { "$ref": "#/$defs/Wide" }
}
},
"Small": { "kind": "struct", "fields": [{ "name": "v", "kind": "uint8" }] },
"Wide": { "kind": "struct", "fields": [
{ "name": "w", "kind": "uint64" },
{ "name": "blob", "kind": "bytes" }
]}
}
}"##;
/// Field paths a `Field` op may target: every declared field (including
/// a dotted nested path) plus junk that must error with `Schema`,
/// never panic. The op's u8 payload picks `byte % 10`.
const FIELD_PATHS: [&str; 10] = [
"a", "n", "s", "tail", "nested", "arr", "rec", "un", "nested.x", "__nope__",
];
#[derive(Debug, Arbitrary)]
pub enum Op {
Next,
NextBorrowed,
Field(u8),
Reset,
End,
}
/// Post-decode transform of the hostile bytes.
#[derive(Debug, Arbitrary, Clone, Copy)]
pub enum Shape {
Raw,
Empty,
Truncate(u8),
Padded,
}
/// The typed fuzz input. `bytes` carries the hostile buffer body;
/// `shape` transforms it; `ops` (the take-rest field) drives the
/// stateful sequence.
#[derive(Debug, Arbitrary)]
pub struct OpSeq {
bytes: Vec<u8>,
shape: Shape,
ops: Vec<Op>,
}
pub fn fuzz_read_opseq(data: &[u8]) {
let Ok(seq) = OpSeq::arbitrary_take_rest(arbitrary::Unstructured::new(data)) else {
return;
};
let Ok(doc) = serde_json::from_str::<serde_json::Value>(DOC) else {
return;
};
let Ok(engine) = AlkTypeEngine::compile(&doc, "S", LayoutMode::Packed, None) else {
return;
};
let buffer = shape_buffer(&seq.shape, &seq.bytes);
run_ops(&engine, &buffer, &seq.ops);
reader_independence(&engine, &buffer);
}
fn shape_buffer(shape: &Shape, bytes: &[u8]) -> Vec<u8> {
match shape {
Shape::Raw => bytes.to_vec(),
Shape::Empty => Vec::new(),
Shape::Truncate(n) => bytes[..bytes.len().min(*n as usize)].to_vec(),
Shape::Padded => {
let mut v = bytes.to_vec();
v.extend_from_slice(&[0u8; 64]);
v
}
}
}
/// The spin bound for one full walk: every successful `read_next`
/// advances the field index, and every record/variable-length inner
/// iteration verifies ≥ 4 bytes, so a completed walk is bounded by
/// `field_count` outer steps regardless of buffer length. `buffer_len`
/// is added headroom so the bound also fires if a zero-cost loop
/// appears anywhere.
fn walk_bound(buffer_len: usize, field_count: usize) -> usize {
buffer_len.saturating_add(field_count).saturating_add(2)
}
fn run_ops(engine: &AlkTypeEngine, buffer: &[u8], ops: &[Op]) {
let Some(mut reader) = engine.sequential_reader() else {
return;
};
let field_count = reader.plan().fields().len();
for op in ops {
match op {
Op::Next => {
let before = reader.position();
match reader.read_next(buffer) {
Ok(Some(_)) => {
assert!(
reader.position() <= buffer.len(),
"cursor {} exceeds buffer len {}",
reader.position(),
buffer.len()
);
}
Ok(None) => {
assert!(
matches!(reader.read_next(buffer), Ok(None)),
"None must be sticky at plan end"
);
}
Err(e) => {
assert_eq!(
reader.position(),
before,
"a failed read_next leaves the position unchanged"
);
let msg = format!("{e:?}");
assert_clean(&e);
let e2 = reader.read_next(buffer).unwrap_err();
assert_eq!(
msg,
format!("{e2:?}"),
"the same cursor state + buffer must reproduce the error"
);
}
}
}
Op::NextBorrowed => {
match reader.read_next_borrowed(buffer) {
Ok(Some(_)) => {
assert!(reader.position() <= buffer.len());
}
Ok(None) => {
assert!(
matches!(reader.read_next_borrowed(buffer), Ok(None)),
"None must be sticky at plan end (borrowed)"
);
}
Err(e) => assert_clean(&e),
}
}
Op::Field(choice) => {
let path = FIELD_PATHS[(*choice as usize) % FIELD_PATHS.len()];
match reader.read_field(buffer, path) {
Ok(_) => {
assert!(reader.position() <= buffer.len());
}
Err(e) => match e {
AlkTypeError::Schema(_) => {}
other => assert_clean(&other),
},
}
reader.reset();
assert_eq!(reader.position(), 0);
match reader.read_next(buffer) {
Ok(Some(_)) | Ok(None) => {}
Err(e) => {
assert_clean(&e);
reader.reset();
}
}
}
Op::Reset => {
reader.reset();
assert_eq!(reader.position(), 0, "reset zeroes the position");
let first = reader.plan().fields().first().map(|f| f.name().to_string());
if let Ok(Some((name, _))) = reader.read_next(buffer) {
assert_eq!(Some(name), first, "reset re-reads field 0");
}
}
Op::End => {
let mut steps = 0usize;
let mut names = Vec::new();
let Some(mut probe) = engine.sequential_reader() else {
return;
};
loop {
steps += 1;
if steps > walk_bound(buffer.len(), field_count) {
panic!(
"walk exceeded the spin bound at step {steps} over a \
{len}-byte buffer with {fields} fields — a \
wire-controlled count is looping",
len = buffer.len(),
fields = field_count
);
}
match probe.read_next(buffer) {
Ok(Some((name, _))) => names.push(name),
Ok(None) => break,
Err(e) => {
assert_clean(&e);
break;
}
}
}
if walk_completes(engine, buffer, field_count) && names.len() == field_count {
let expected: Vec<String> =
probe.plan().fields().iter().map(|f| f.name().to_string()).collect();
assert_eq!(
names, expected,
"a completed walk reports fields exactly in plan order"
);
}
}
}
}
}
fn walk_completes(engine: &AlkTypeEngine, buffer: &[u8], field_count: usize) -> bool {
let Some(mut probe) = engine.sequential_reader() else {
return false;
};
let mut steps = 0usize;
loop {
steps += 1;
if steps > walk_bound(buffer.len(), field_count) {
panic!("walk_completes exceeded the spin bound");
}
match probe.read_next(buffer) {
Ok(Some(_)) => continue,
Ok(None) => return true,
Err(_) => return false,
}
}
}
/// Two engine-issued readers over the same plan and buffer never share
/// cursor state (ADR-007): identical initial state ⇒ identical first
/// read; interleaved reads and resets never move the other's cursor.
fn reader_independence(engine: &AlkTypeEngine, buffer: &[u8]) {
let Some(mut r1) = engine.sequential_reader() else {
return;
};
let Some(mut r2) = engine.sequential_reader() else {
return;
};
assert!(
Arc::ptr_eq(r1.plan(), r2.plan()),
"engine-issued readers share the compiled plan Arc"
);
let first1 =
r1.read_next_borrowed(buffer).map(|o| o.map(|(n, _)| n.to_string()));
let first2 =
r2.read_next_borrowed(buffer).map(|o| o.map(|(n, _)| n.to_string()));
if let (Ok(a), Ok(b)) = (&first1, &first2) {
assert_eq!(a, b, "independent readers in identical state agree");
}
let pos = r1.position();
let _ = r2.read_next(buffer);
let _ = r2.read_next(buffer);
assert_eq!(r1.position(), pos, "r2's reads never move r1's cursor");
r2.reset();
assert_eq!(r1.position(), pos, "r2's reset never moves r1's cursor");
let field_count = r1.plan().fields().len();
let mut steps = 0usize;
loop {
steps += 1;
if steps > walk_bound(buffer.len(), field_count) {
panic!("r1's interleaved walk exceeded the spin bound");
}
match r1.read_next(buffer) {
Ok(Some(_)) => continue,
Ok(None) | Err(_) => break,
}
}
}
fn assert_clean(e: &AlkTypeError) {
match e {
AlkTypeError::Access { field_path, .. } => {
assert!(!field_path.is_empty(), "Access errors carry the field path");
}
AlkTypeError::Schema(_) => {}
AlkTypeError::Offset { .. } | AlkTypeError::Validation(_) => {
panic!("sequential reads must never error with Offset/Validation, got {e:?}")
}
}
}
#[cfg(test)]
mod corpus_replay {
use super::*;
#[test]
fn committed_corpus_replays_through_the_invariants() {
let corpus =
std::path::PathBuf::from(env!("CARGO_MANIFEST_DIR")).join("../corpus/read_opseq");
let mut count = 0usize;
for entry in std::fs::read_dir(&corpus).expect("corpus directory is committed") {
let path = entry.expect("corpus entry readable").path();
let data = std::fs::read(&path).expect("corpus entry readable");
fuzz_read_opseq(&data);
count += 1;
}
assert!(
count >= 10,
"committed seed corpus is present, found {count}"
);
}
#[test]
fn the_fixed_menu_compiles() {
let doc: serde_json::Value = serde_json::from_str(DOC).expect("menu doc is valid JSON");
AlkTypeEngine::compile(&doc, "S", LayoutMode::Packed, None)
.expect("the fixed menu schema compiles");
}
/// Byte encodings against the pinned arbitrary 1.4.2 derive layout:
/// field order { bytes: Vec<u8>, shape: Shape, ops: Vec<Op> } with
/// ops the take-rest field. Enum variants consume a 4-byte LE u32
/// `v` and select variant `(v × count) >> 32`. Vec elements are
/// keep-going byte (odd = one more element) + element bytes. bools
/// are not involved. The Rust side mirrors this in `enc` below;
/// the Python seed generator mirrors both.
mod enc {
pub fn variant_u32(k: u64, count: u64) -> [u8; 4] {
let mut v = (k << 32).div_ceil(count);
while (v.wrapping_mul(count) >> 32) != k {
v += 1;
}
u32::try_from(v).expect("variant selector < 2^32 for k < count").to_le_bytes()
}
pub const SHAPE_RAW: u64 = 0;
pub const SHAPE_EMPTY: u64 = 1;
pub const SHAPE_TRUNCATE: u64 = 2;
pub const SHAPE_PADDED: u64 = 3;
pub const OP_NEXT: u64 = 0;
pub const OP_NEXT_BORROWED: u64 = 1;
pub const OP_FIELD: u64 = 2;
pub const OP_RESET: u64 = 3;
pub const OP_END: u64 = 4;
pub fn bytes_field(bytes: &[u8]) -> Vec<u8> {
let mut v = Vec::new();
for b in bytes {
v.push(0x01);
v.push(*b);
}
v.push(0x00);
v
}
pub fn op(tag: u64) -> Vec<u8> {
variant_u32(tag, 5).to_vec()
}
pub fn field_op(choice: u8) -> Vec<u8> {
let mut v = op(OP_FIELD);
v.push(choice);
v
}
/// shape_field with no variant payload; Truncate carries a u8.
pub fn shape_field(tag: u64) -> Vec<u8> {
variant_u32(tag, 4).to_vec()
}
pub fn shape_truncate(n: u8) -> Vec<u8> {
let mut v = variant_u32(SHAPE_TRUNCATE, 4).to_vec();
v.push(n);
v
}
pub fn opseq(bytes: &[u8], shape: Vec<u8>, ops: &[Vec<u8>]) -> Vec<u8> {
let mut v = bytes_field(bytes);
v.extend_from_slice(&shape);
for o in ops {
v.push(0x01);
v.extend_from_slice(o);
}
v.push(0x00);
v
}
}
/// A buffer covering every field of the menu schema: u8 | u32 |
/// string | u8 | Inner(i16, bool) | u16[2] | record<u32> ×2 |
/// union(disc=0 → Small { u8 }).
fn host_buffer() -> Vec<u8> {
let mut buf = Vec::new();
buf.push(0x11); // a
buf.extend_from_slice(&7u32.to_le_bytes()); // n
buf.extend_from_slice(&5u32.to_le_bytes()); // s len
buf.extend_from_slice(b"hello"); // s
buf.push(0x22); // tail
buf.extend_from_slice(&(-2i16).to_le_bytes()); // nested.x
buf.push(0x01); // nested.y
buf.extend_from_slice(&0xBEEFu16.to_le_bytes()); // arr[0]
buf.extend_from_slice(&0xCAFEu16.to_le_bytes()); // arr[1]
buf.extend_from_slice(&2u32.to_le_bytes()); // rec count
buf.extend_from_slice(&1u32.to_le_bytes()); // rec[0].key len
buf.push(b'k'); // rec[0].key
buf.extend_from_slice(&0x0102_0304u32.to_le_bytes()); // rec[0].value
buf.extend_from_slice(&1u32.to_le_bytes()); // rec[1].key len
buf.push(b'j'); // rec[1].key
buf.extend_from_slice(&0x0A0B_0C0Du32.to_le_bytes()); // rec[1].value
buf.push(0x00); // un disc → "0"
buf.push(0x33); // Small.v
buf
}
fn all_next_ops(n: usize) -> Vec<Vec<u8>> {
let mut ops = vec![enc::op(enc::OP_NEXT); n];
ops.push(enc::op(enc::OP_END));
ops
}
#[test]
fn decode_lands_on_the_intended_variants() {
// The seed-decode pin: every hand-encoding helper must decode
// (through the real Arbitrary derive) to exactly the variant it
// intends, so the committed seeds carry the menu they claim.
use arbitrary::Arbitrary;
fn decode(data: &[u8]) -> (String, String, usize) {
let seq = OpSeq::arbitrary_take_rest(arbitrary::Unstructured::new(data))
.expect("the encoding decodes");
let ops = seq
.ops
.iter()
.map(|o| match o {
Op::Next => "N".to_string(),
Op::NextBorrowed => "B".to_string(),
Op::Field(c) => format!("F{c}"),
Op::Reset => "R".to_string(),
Op::End => "E".to_string(),
})
.collect::<Vec<_>>()
.join("");
let shape = match seq.shape {
Shape::Raw => "Raw".to_string(),
Shape::Empty => "Empty".to_string(),
Shape::Truncate(n) => format!("T{n}"),
Shape::Padded => "P".to_string(),
};
(ops, shape, seq.bytes.len())
}
let full = enc::opseq(
&[0xAA, 0xBB],
enc::shape_field(enc::SHAPE_PADDED),
&[
enc::op(enc::OP_NEXT),
enc::op(enc::OP_NEXT_BORROWED),
enc::field_op(3),
enc::op(enc::OP_RESET),
enc::op(enc::OP_END),
],
);
assert_eq!(decode(&full), ("NB".to_string() + "F3" + "RE", "P".to_string(), 2));
assert_eq!(
decode(&enc::opseq(&[], enc::shape_field(enc::SHAPE_RAW), &[enc::field_op(9)]))
.0,
"F9"
);
assert_eq!(
decode(&enc::opseq(&[], enc::shape_truncate(42), &[])).1,
"T42"
);
assert_eq!(
decode(&enc::opseq(&[], enc::shape_field(enc::SHAPE_EMPTY), &[])).1,
"Empty"
);
// Exhausted input with a keep-going byte but no element bytes:
// the u32 selector zero-fills → variant 0 (Next). This is
// exactly the boundary the fuzzer explores; pin it so a
// derive-layout change surfaces here.
let mut cut = enc::opseq(&[], enc::shape_field(enc::SHAPE_RAW), &[enc::op(enc::OP_END)]);
cut.truncate(cut.len() - 5); // stop byte + 4-byte op selector
let (ops, shape, n) = decode(&cut);
assert_eq!((ops.as_str(), shape.as_str(), n), ("N", "Raw", 0));
}
#[test]
fn full_walk_over_a_complete_buffer() {
let buf = host_buffer();
fuzz_read_opseq(&enc::opseq(&buf, enc::shape_field(enc::SHAPE_RAW), &all_next_ops(10)));
// Padded shape: the same semantic walk over 64 zero tails.
fuzz_read_opseq(&enc::opseq(&buf, enc::shape_field(enc::SHAPE_PADDED), &all_next_ops(10)));
}
#[test]
fn reset_mid_walk_then_walk_again() {
let buf = host_buffer();
let ops = [
enc::op(enc::OP_NEXT),
enc::op(enc::OP_NEXT),
enc::op(enc::OP_NEXT),
enc::op(enc::OP_RESET),
enc::op(enc::OP_END),
];
fuzz_read_opseq(&enc::opseq(&buf, enc::shape_field(enc::SHAPE_RAW), &ops));
}
#[test]
fn failed_read_then_reset() {
// Truncated at 12 bytes: reads fail mid-walk; every failure
// must leave a usable reader (the op loop resets and replays).
let ops = [
enc::op(enc::OP_END),
enc::op(enc::OP_RESET),
enc::op(enc::OP_END),
enc::field_op(2),
enc::op(enc::OP_END),
];
fuzz_read_opseq(&enc::opseq(&host_buffer(), enc::shape_truncate(12), &ops));
// Empty buffer: nothing succeeds; the walk bound still holds.
fuzz_read_opseq(&enc::opseq(&host_buffer(), enc::shape_field(enc::SHAPE_EMPTY), &ops));
}
#[test]
fn junk_and_real_field_paths() {
let buf = host_buffer();
fuzz_read_opseq(&enc::opseq(&buf, enc::shape_field(enc::SHAPE_RAW), &[enc::field_op(9)]));
fuzz_read_opseq(&enc::opseq(&buf, enc::shape_field(enc::SHAPE_RAW), &[enc::field_op(8)]));
fuzz_read_opseq(&enc::opseq(&buf, enc::shape_field(enc::SHAPE_RAW), &[enc::field_op(6)]));
}
#[test]
fn hostile_record_count_fails_fast_with_access() {
// The record-count spin candidate (plan §6 item 1): the count
// far above any iteration budget. Every iteration must verify
// ≥ 4 bytes, so the walk fails with Access inside the bound —
// asserted per the plan, not just no-panic. A regression to a
// zero-width inner loop trips the End-op spin-bound panic
// and/or cargo's test timeout.
let rec_at = 1 + 4 + 4 + 5 + 1 + 2 + 1 + 4; // a, n, s(4+5), tail, x, y, arr
let mut hostile = host_buffer();
hostile[rec_at..rec_at + 4].copy_from_slice(&0xFFFF_FFFFu32.to_le_bytes());
hostile.truncate(rec_at + 4);
fuzz_read_opseq(&enc::opseq(&hostile, enc::shape_field(enc::SHAPE_RAW), &all_next_ops(10)));
// Zero-length keys with a large count: 8 verified bytes per
// iteration (key prefix + value) — still bounded.
let mut zero_keys = host_buffer();
zero_keys[rec_at..rec_at + 4].copy_from_slice(&0x0001_0000u32.to_le_bytes());
for b in &mut zero_keys[rec_at + 4..] {
*b = 0x00;
}
fuzz_read_opseq(&enc::opseq(&zero_keys, enc::shape_field(enc::SHAPE_RAW), &all_next_ops(10)));
}
#[test]
fn truncated_at_every_prefix_stays_clean() {
let buf = host_buffer();
for n in 1..buf.len() {
fuzz_read_opseq(&enc::opseq(
&buf,
enc::shape_truncate(u8::try_from(n).expect("fixture < 256")),
&all_next_ops(10),
));
}
}
}