crates/veilvoice-crypto/tests/parser_fuzz.rs
veilvoice-crypto · 368 lines · read the source here · or on GitHub
Randomised robustness testing for the two parsers that read untrusted input.
What is being defended
container::Header::parse reads a file somebody sent you. lock::AppLock::parse reads the app-lock file, which is worse: it is parsed before anyone has authenticated anything, so it is the first bytes the program touches on a locked machine.
For a parser in a security tool the bar is not "usually returns the right answer". It is:
- Never panic. A panic on hostile input is a denial of service, and in a
panic = "abort"release profile it is the whole process. - Never hang. Every loop must be bounded by the input, not by a length field the input controls.
- Never report success for something it did not fully understand. An
Okmust come with offsets that are actually inside the buffer.
Why this and not cargo fuzz
cargo fuzz needs nightly and libFuzzer, which is a poor fit for a project that pins a stable toolchain and wants every check runnable by anyone who cloned it. This is a deterministic campaign instead: a seeded PRNG, so a failure is reproducible from its seed rather than being a story about a run nobody can repeat, and structure-aware mutation, so the bytes spend their time near the interesting boundaries rather than being rejected at the magic number.
It is not a substitute for a coverage-guided fuzzer and docs/AUDIT.md does not claim it is. It is the campaign that can actually be run on every commit, on every platform, by everybody.
Set VEILVOICE_FUZZ_ROUNDS to run it longer than the default.
In plain words
Throws malformed and deliberately hostile encrypted files at the code that reads them, in bulk.
Reading a file somebody else made is where most security problems live. Every one of these has to be refused with a reason: never accepted, and never able to bring the program down.
WHAT THIS FILE CONTAINS
368 lines defining 13 functions (0 public), 1 type and 0 constants. Everything below is read out of the source, so it cannot disagree with the code.
The types it owns.
struct Rngline 50 · xorshift32.
WHAT CALLS WHAT
The functions this file defines, and the calls between them. An edge means the callee's name appears, called, inside the caller's body. This is a syntactic reading, not a type-resolved one.
The same graph as Mermaid source
%%{init: {"theme":"base","themeVariables":{"background":"#1a1b26","primaryColor":"#1f2335","primaryTextColor":"#c0caf5","primaryBorderColor":"#7aa2f7","secondaryColor":"#16161e","tertiaryColor":"#16161e","lineColor":"#737aa2","textColor":"#c0caf5","mainBkg":"#1f2335","nodeBorder":"#7aa2f7","clusterBkg":"#16161e","clusterBorder":"#2f3549","fontFamily":"ui-monospace, SFMono-Regular, Consolas, monospace","fontSize":"14px"}}}%%
flowchart TD
n_new["Rng::new<br/>line 53"]
n_next_u32["Rng::next_u32<br/>line 56"]
n_below["Rng::below<br/>line 62"]
n_byte["Rng::byte<br/>line 69"]
n_rounds["rounds<br/>line 74"]
n_mutate["mutate<br/>line 86"]
n_weak["weak<br/>line 160"]
n_cheap["cheap<br/>line 178"]
n_the_container_header_parser_survives_hostile_input["the_container_header_parser_survives_<br/>hostile_input<br/>line 183"]
n_the_app_lock_parser_survives_hostile_input["the_app_lock_parser_survives_hostile_<br/>input<br/>line 234"]
n_both_parsers_survive_pure_noise["both_parsers_survive_pure_noise<br/>line 274"]
n_every_length_around_a_boundary_is_handled["every_length_around_a_boundary_is_<br/>handled<br/>line 294"]
n_the_header_the_coverage_guided_campaign_found_is_refused["the_header_the_coverage_guided_<br/>campaign_found_is_refused<br/>line 335"]
n_below --> n_next_u32
n_both_parsers_survive_pure_noise --> n_cheap
n_both_parsers_survive_pure_noise --> n_new
n_both_parsers_survive_pure_noise --> n_rounds
n_byte --> n_next_u32
n_every_length_around_a_boundary_is_handled --> n_weak
n_the_app_lock_parser_survives_hostile_input --> n_cheap
n_the_app_lock_parser_survives_hostile_input --> n_mutate
n_the_app_lock_parser_survives_hostile_input --> n_new
n_the_app_lock_parser_survives_hostile_input --> n_rounds
n_the_app_lock_parser_survives_hostile_input --> n_weak
n_the_container_header_parser_survives_hostile_input --> n_cheap
n_the_container_header_parser_survives_hostile_input --> n_mutate
n_the_container_header_parser_survives_hostile_input --> n_new
n_the_container_header_parser_survives_hostile_input --> n_rounds
n_the_container_header_parser_survives_hostile_input --> n_weak
click n_new href "https://github.com/tilas01/veilvoice/blob/main/crates/veilvoice-crypto/tests/parser_fuzz.rs#L53" "open the source"
click n_next_u32 href "https://github.com/tilas01/veilvoice/blob/main/crates/veilvoice-crypto/tests/parser_fuzz.rs#L56" "open the source"
click n_below href "https://github.com/tilas01/veilvoice/blob/main/crates/veilvoice-crypto/tests/parser_fuzz.rs#L62" "open the source"
click n_byte href "https://github.com/tilas01/veilvoice/blob/main/crates/veilvoice-crypto/tests/parser_fuzz.rs#L69" "open the source"
click n_rounds href "https://github.com/tilas01/veilvoice/blob/main/crates/veilvoice-crypto/tests/parser_fuzz.rs#L74" "open the source"
click n_mutate href "https://github.com/tilas01/veilvoice/blob/main/crates/veilvoice-crypto/tests/parser_fuzz.rs#L86" "open the source"
click n_weak href "https://github.com/tilas01/veilvoice/blob/main/crates/veilvoice-crypto/tests/parser_fuzz.rs#L160" "open the source"
click n_cheap href "https://github.com/tilas01/veilvoice/blob/main/crates/veilvoice-crypto/tests/parser_fuzz.rs#L178" "open the source"
click n_the_container_header_parser_survives_hostile_input href "https://github.com/tilas01/veilvoice/blob/main/crates/veilvoice-crypto/tests/parser_fuzz.rs#L183" "open the source"
click n_the_app_lock_parser_survives_hostile_input href "https://github.com/tilas01/veilvoice/blob/main/crates/veilvoice-crypto/tests/parser_fuzz.rs#L234" "open the source"
click n_both_parsers_survive_pure_noise href "https://github.com/tilas01/veilvoice/blob/main/crates/veilvoice-crypto/tests/parser_fuzz.rs#L274" "open the source"
click n_every_length_around_a_boundary_is_handled href "https://github.com/tilas01/veilvoice/blob/main/crates/veilvoice-crypto/tests/parser_fuzz.rs#L294" "open the source"
click n_the_header_the_coverage_guided_campaign_found_is_refused href "https://github.com/tilas01/veilvoice/blob/main/crates/veilvoice-crypto/tests/parser_fuzz.rs#L335" "open the source"
classDef helper fill:#1f2335,stroke:#bb9af7,color:#c0caf5
class n_new,n_next_u32,n_below,n_byte,n_rounds,n_mutate,n_weak,n_cheap,n_the_container_header_parser_survives_hostile_input,n_the_app_lock_parser_survives_hostile_input,n_both_parsers_survive_pure_noise,n_every_length_around_a_boundary_is_handled,n_the_header_the_coverage_guided_campaign_found_is_refused helper
This site loads no third-party script, so it cannot run Mermaid; the diagram above is the same nodes and edges drawn by the generator instead. GitHub renders the source below directly.
ITEMS
| Item | Line | Documentation |
|---|---|---|
Rng struct | 50 | xorshift32. |
Rng::new fn | 53 | |
Rng::next_u32 fn | 56 | |
Rng::below fn | 62 | |
Rng::byte fn | 69 | |
rounds fn | 74 | |
mutate fn | 86 | Mutate seed_bytes in one of the ways that break parsers in practice. |
weak fn | 160 | |
cheap fn | 178 | Whether it is worth actually running the KDF for these parameters. |
the_container_header_parser_survives_hostile_input fn | 183 | |
the_app_lock_parser_survives_hostile_input fn | 234 | |
both_parsers_survive_pure_noise fn | 274 | Pure noise, with no valid seed to start from. |
every_length_around_a_boundary_is_handled fn | 294 | The lengths a parser gets wrong are the ones either side of a boundary, and a random campaign hits them only by luck. |
the_header_the_coverage_guided_campaign_found_is_refused fn | 335 | F-82. |