Files
alkvault/fuzz
glm-5.3-flash 5b119eebcc Run detached campaigns for the three new fuzz targets
- 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
2026-11-04 08:20:07 +00:00
..
2026-11-04 07:36:58 +00:00
2026-11-04 07:22:53 +00:00
2026-11-04 07:36:58 +00:00

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; thin fuzz_target! wrappers.
  • 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.
  • corpus/<target>/: committed hand-made seeds (regenerate with python3 fuzz/gen_fuzz_seeds.py from the repo root). Grown corpora and artifacts are gitignored.
  • run-detached.sh, the 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 (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.