7.5 KiB
id, name, status, depends_on, scope, risk, impact, level, tags
| id | name | status | depends_on | scope | risk | impact | level | tags | |||
|---|---|---|---|---|---|---|---|---|---|---|---|
| fuzz-target-payload-codec | Fuzz target — `payload_codec` (core payload encode/decode invariants) | completed |
|
moderate | low | component | implementation |
|
Description
The §3.1 target: payload_codec, driving core's payload surface
(alkstore/src/payload.rs — encode_payload, payload_as::<T>) —
already-pub API, no exposure needed.
fuzz_targets/payload_codec.rs— the thinfuzz_target!wrapper (3-line pattern) delegating toalkstore_fuzz_shared::payload_codec::fuzz_payload_codec(data); the[[bin]]entry lands infuzz/Cargo.toml.shared/src/payload_codec.rs— the invariant logic, with the#[cfg(test)]corpus-replay module (the gate). Input: raw&[u8]plusarbitrary-derivedserde_json::Values (theJsonValuebounded-depth helper from the scaffold). Invariants (§3.1, verbatim):- no-panic on any byte sequence (the baseline);
- typed-failure partition: every decode failure is
Error::Codec— no other variant, no panic (the ADR-023 §1 fallibility pin, demonstrated across the input space); - deep-input loudness: serde_json's recursion limit surfaces as
typed
Codec, never a stack overflow; - Value round-trip:
encode_payload(v)→payload_as::<Value>yieldsvfor every arbitraryValue(canonicalization is serde_json's, pinned-not-regressed); - typed round-trip: for a representative typed-payload set,
encode(Value::from(t))decodes back tot(the ADR-020 §4 decode side).
- Seeds (§3.1's list, via
gen_fuzz_seeds.pygaining thepayload_codecfunction): valid JSON of every Value variant, truncations, deep nesting at and past the recursion limit, non-UTF-8 bytes, trailing garbage, oversized numbers, duplicate keys, theNull-decode shape (ADR-023 §1's empty-byte-store corruption note). Corpus committed underfuzz/corpus/payload_codec/; thefuzz/.gitignoregains the!corpus/payload_codecnegation. fuzz/README.mdtargets-table row.shared's manifest gainsalkstore(path) if the scaffold didn't already carry it, plusserde_json+arbitrary.
Campaign flags are the campaign task's concern (-max_len=65536 -dict=json.dict starting point, §7); this task lands target + seeds +
replay.
Acceptance Criteria
fuzz_targets/payload_codec.rs+shared/src/payload_codec.rsexist;cargo fuzz buildbuilds the target (nightly, ASAN)- All five §3.1 invariants implemented in
sharedand exercised by both the fuzz path and the#[cfg(test)]replay module - Seed corpus committed (
fuzz/corpus/payload_codec/), generated bypython3 fuzz/gen_fuzz_seeds.py(deterministic — re-running is a no-op diff); gitignore negation in place cargo test --manifest-path fuzz/shared/Cargo.tomlgreen (replays the committed corpus through the invariants)sharedclippy/fmt gates green (cargo clippy --manifest-path fuzz/shared/Cargo.toml --all-targets -- -D warnings,fmt --check)- Root workspace gates still green
References
- docs/research/fuzzing.md §3.1 (target spec), §4 (wave-1 scope), §7 (flags)
- docs/architecture/decisions/020-payload-encoding.md §4 (the encoding posture the round-trips pin)
- docs/architecture/decisions/023-fourth-review-round.md §1 (encode fallibility)
- /workspace/@alkdev/alksocks/fuzz/shared/src/datagram_codec.rs (the invariant-module + replay-test pattern)
Notes
- serde added to
shared's manifest withderive(beyond the spec's manifest list ofalkstore/serde_json/arbitrary): the typed round-trip needs a representative struct payload (TestJobPayload— string/int/vec/option/map fields, the consumer payload shape), and#[derive(Serialize, Deserialize)]requires the direct dep for the bounds.serde_jsonre-exports nothing derive-able; core already depends on serde transitively. - The typed-failure partition is exercised four ways: over
Value,TestJobPayload,String, andVec<Value>decodes of the raw input — each failure must beError::Codec(the ADR-023 §1 pin), each success is then driven back throughencode_payload→payload_as::<Value>for input-closure Value round-trip (canonicalization is serde_json's: duplicate-key last-write-wins etc., pinned-not-regressed, not re-derived). - Deep-input loudness probes: raw nested-array strings at 4x and
past-4096 depth decode →
Err(Codec)(bothValueand typed); a constructedValuenested one past the 128 limit encodes fine (the serializer's recursion is unbounded but shallow here) and its stored bytes decode →Err(Codec)— the stored-deep-form loudness the ADR-023 §1 posture wants. Encode is deliberately NOT probed at extreme depth (serializer recursion would overflow at absurd depths); 129 + shallow-8 form the pair. serde_json's 128 limit is pinned asRECURSION_LIMIT; a future serde_json bump failing these probes is the intended loud signal, not a fragility. arbitrarypath runsUnstructured::new(data)→ up to twoJsonValues from the same bytes (exhaustion → skip, the raw path already covers it): encode Ok, decode == v, plus a typed-stability check for arbitrary values that happen to decode asTestJobPayload.- Placeholder target removed per the scaffold's lifecycle note —
payload_codec's[[bin]]replaces it; the two remaining wave-1 target tasks still add their own bins. - Seeds came out at 97 (spec list expanded: per-prefix truncations of two docs, depth probes at 8/127/128/129/256/1024 for arrays and 8/200 for objects, plus the loud-fail shapes NaN/Infinity/+1/01/.5/ 1./--1/lone-key/missing-value). Replay-floor set at ≥ 64.
- One behavior surfaced by the corpus:
payload_as::<Value>of deep/sub-limit vs shallow shapes needed the round-trip battery's constructedValueto match the probe doc's leaf (0, notNull) — fixed during implementation; the final replay is green.
Summary
Landed the §3.1 payload_codec fuzz target: fuzz_targets/payload_codec.rs
(the thin fuzz_target! wrapper, replacing the scaffold placeholder's
[[bin]]) and fuzz/shared/src/payload_codec.rs implementing all five
§3.1 invariants — no-panic baseline via the four-way typed decode
partition (Value / TestJobPayload / String / Vec<Value>),
deep-input loudness at serde_json's recursion limit (raw-byte probes
and the stored-deep-form probe), empty-byte-store loudness (the ADR-023
§1 corruption note), the Value round-trip (fixed battery + raw-input
closure + arbitrary JsonValues from the bounded-depth helper), and
the typed round-trip over a representative payload set (derived struct
- std types). 97 deterministic seeds committed under
fuzz/corpus/payload_codec/(generated bygen_fuzz_seeds.py's newpayload_codec_seeds(), re-run is a no-op), with the.gitignorenegation and the README targets-table row.fuzz/shared/Cargo.tomlgainedserde(derive) — no other manifest changes;fuzz/Cargo.lockupdated.
Verified: cargo test --manifest-path fuzz/shared/Cargo.toml green
(corpus replay 97/97 + invariant unit probes); shared clippy
-D warnings + fmt --check green; cargo fuzz build green from
fuzz/ (nightly/ASAN binary at
target/x86_64-unknown-linux-gnu/release/payload_codec); root
workspace cargo build / cargo test (all suites pass) / clippy
-D warnings / fmt --check green. Campaign flags remain the
campaigns-and-gate task's concern.