- encrypted_blob, derived_key_json, and mnemonic_unlock each ran a full 600 s detached campaign (fork mode, exiting 0, none stopped early); crash/oom/timeout 0/0/0 across all three logs, so no regression tests, fixes, re-runs, or escalations were needed - Per-target stats recorded in docs/plans/fuzzing.md session notes: encrypted_blob 73,473 execs / 3,118 cov blocks; derived_key_json 2,090,033 execs / 2,175 cov blocks; mnemonic_unlock 7,950,541 execs / 1,934 cov blocks (path_parse baseline: 718) - Review findings closeout: the assert-message echo class is accepted as fuzz evidence with acceptance notes in the two shared modules, and the repeated-word pin wording is fixed with a fuzz/README.md reword - Flip the three target checkboxes and the detached-campaigns item; mark tasks/fuzz_campaigns_new_targets.md completed with Notes and Summary Verification: cargo test --all-features 112 passed, cargo test 105 passed, cargo clippy --all-features --all-targets clean, cargo test --manifest-path fuzz/shared/Cargo.toml 8/8 passed
alkvault fuzzing
libFuzzer targets for alkvault's untrusted-input surfaces. The scope
decisions and task tracking live in docs/plans/fuzzing.md; the
tooling/corpus/runner posture is inherited from alkcall's
docs/research/fuzzing.md as relayed by alktunnels' fuzz/README.md.
Layout
fuzz_targets/: one binary per target; thinfuzz_target!wrappers.shared/, the invariant logic, as a plain library so normalcargo 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.corpus/<target>/: committed hand-made seeds (regenerate withpython3 fuzz/gen_fuzz_seeds.pyfrom the repo root). Grown corpora and artifacts are gitignored.run-detached.sh, the mandatory runner for agent sessions: wrapscargo fuzz runinsetsid+nohup+ log redirection so an OOM in a target can never take down the agent host (fuzzing.md section 5).
Targets
| Target | Drives | Invariants |
|---|---|---|
path_parse |
parse_derivation_path, derive_path_from_seed over a fixed 64-byte seed, encryption_path_for_version and device_path over arbitrary indices (src/derivation.rs) |
no-panic on any bytes; only InvalidPath errors ever; deterministic parse and derive; hardened-index overflow rejected (regression pins: m/2147483648', m/4294967295'); PATHS constants parse to pinned index tables; version/device path helpers produce parseable paths inside their legal ranges and clean rejections outside; derived keys are 32+32 bytes with path echo and repeatable derivation |
encrypted_blob |
serde deserialization into EncryptedData from arbitrary bytes (raw path plus a structured field-dictionary path), then VaultServiceHandle::decrypt/rotate/encrypt over a service unlocked with the fixed fuzz mnemonic (src/encryption.rs, src/service.rs) |
no-panic on raw bytes, structured JSON, and decrypt/rotate of whatever deserializes; failures only as Encryption wrapping base64-decode/AEAD-decrypt messages or InvalidPath for out-of-range key_version (v0/v1 and the hardened-encoding overflow); malformed base64, wrong IV length, auth failures all clean errors (regression-pinned); deterministic unlock of the fixed mnemonic; round-trip (decrypt to re-encrypt to decrypt) asserted only for blobs the service produced; rotate to a legal target version yields key_version = target and decrypts back |
derived_key_json |
serde_json::from_slice::<DerivedKey> over arbitrary JSON (raw-bytes path plus a structured Value path built from byte prefixes with fields spliced/removed from any embedded JSON object), the custom Deserialize redacted-payload guard, and the Zeroize-on-drop path on deserialized keys (src/protocol.rs) |
no-panic on raw bytes and structured JSON; both redacted forms rejected, the byte-array guard message mentions the [REDACTED] marker and stays a fixed ASCII literal; successful deserializations re-serialize with a [REDACTED] private_key field carrying nothing beyond key_type/public_key, and the private bytes never appear as a decimal array (skipped only when the payload echoed them as public_key); Debug redacts; the redacted serialized form must not deserialize back (round-trip unsupported); deserialized keys drop cleanly including garbage private bytes (regression pins: valid 32-byte payload round-trip pins, all three KeyType variants, both redacted forms, empty key arrays) |
mnemonic_unlock |
Mnemonic::from_phrase over arbitrary bytes (the &str surface skips non-UTF-8 corpus) plus VaultServiceHandle::unlock/lock/derive_ed25519/encrypt/decrypt on a fresh handle per input, with a corpus-selected Some passphrase variant when the first byte is odd (src/mnemonic.rs, src/service.rs) |
no-panic on any bytes; parse failures only InvalidPhrase-shaped (through the service, VaultServiceError::Mnemonic, never Generation), deterministic with no echo of the phrase in the failure text (guarded scan: five-plus lowercase-alpha tokens); Debug of a parsed mnemonic shows [REDACTED] and leaks no word or word span, with one documented exemption (the word phrase is itself a BIP39 word and the Debug field name); parse/seed determinism, 64-byte seeds with and without a passphrase; on valid phrases the identity key is deterministic across re-unlock, the encrypt/decrypt round-trip holds only for service-produced blobs, lock leaves is_unlocked == false and only VaultLocked failures, and a whitespace-tokenized respace of a valid phrase derives the identical key (regression pins: the repo's 12-word vector, the canonical 24-word all-abandon vector, an even-first-byte no-passphrase phrase, checksum/word-count/case/unknown-word/control-character/empty/non-UTF-8 near misses; a valid phrase with a repeated word rides the committed corpus replay rather than the pins function) |
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.
# 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 path_parse
# corpus replay through the invariants (stable toolchain, no nightly)
cargo test --manifest-path fuzz/shared/Cargo.toml
# coverage
cargo fuzz coverage path_parse
The fuzz workspace is excluded from the main workspace and from the
published package (exclude in the root Cargo.toml); it pins its own
nightly toolchain via rust-toolchain.toml inside fuzz/ and does not
affect the crate's stable MSRV.