Repository navigation
Fuzz harnesses for the command parser and the reply stream (#144) - #148
Merged
Merged
Conversation
protocol.h declares worker_list with no argument, supervisor_reset with at most one and mod-host's quit with none, but the daemon acted on any number of words after them. Each now answers ERR_INVALID_OPERATION to a word past its grammar, as mod-host refuses a command with too many arguments.
The end of a gesture the relay holds was written through a 1024-byte buffer, so a gesture whose line was longer reached the controller cut short, without its end word, and the control stayed held. The held line and the end word now go out as two writes under the relay's lock, one line of any length.
fuzz/fuzz_command.c is a libFuzzer harness over src/verbs.c: a run of controller messages, each in an allocation of exactly its length, with everything below the dispatcher (supervisor, pins, descriptors, the socket's send) replaced by the recorder of fuzz/recorder.c. The recorder answers with the reply each verb declares in protocol.h, or with the refusal the input chose, and keeps a digest of what accepted calls changed. It checks four properties, each an abort(): one NUL-terminated reply of a declared shape per message, passed on unchanged; a verb the daemon parses is acted on only with an argument count its declared syntax allows; a command's words joined by one space make the same calls; and a refused command changes nothing.
fuzz/fuzz_reply.c includes src/relay.c, as tests/gesture_alloc_test.c does, and answers its recv and send itself: the input chooses what each worker writes, where each read cuts it, when a worker closes or is removed, which instance goes, and how the controller's socket takes the lines. A reference splitter reads the same bytes. Its properties: the buffer of an unfinished line stays bounded; the controller reads every complete line once, in order, however the stream was cut; and every line the relay writes of its own is an event as written or the end of a gesture still held. The relay's line limit can now be set at build time, so the harness reaches the drop of a long line with inputs of a few kilobytes.
fuzz/gen_corpus.py writes the seed corpus from protocol/plugin-hostd.json and protocol/mod-host.json, every verb with sample arguments, with its optional words, with one word too many, refused by the supervisor and split by tabs, and from the send lines of README.md and docs/own-host.md. The reply seeds cut worker feedback at every byte and before every NUL, hold and end gestures, and put lines at and past the line limit.
make fuzz builds both harnesses into build/fuzz/ with clang, -fsanitize=fuzzer,address,undefined and -fno-sanitize-recover=all, nothing of the ordinary build touched. make fuzz-run fuzzes each from its corpus for FUZZ_TIME seconds and leaves a crashing input and its log in fuzz/findings/. make fuzz-corpus writes the corpus again, and fuzz-corpus-check fails when the committed one is not what it writes.
fuzz/mutants/<harness>-<property>.patch is a one-line bug for each property a harness checks. make fuzz-sabotage applies each, rebuilds, runs the harness over its seeds one at a time and is red unless a seed makes it abort in check_<property>; a crash in another property does not count. The source is restored with git checkout on every exit path.
make fuzz-coverage builds each harness for llvm-cov, runs it once over its corpus and prints the line coverage of the functions it drives and every line no seed reaches. fuzz/coverage-floor.txt holds the covered lines of each, 153 of 153 for the dispatcher and 189 of 210 for the relay, and the target is red below them, or when a property check of a harness is reached by no seed (fuzz/checks.awk, as libreac has it).
The fuzz workflow builds both harnesses in a fedora:44 container with the mod-host protocol headers of the FreeMixer channel, checks the corpus is current, fuzzes each for 60 s, finds every mutant and holds the coverage floor, and uploads a crashing input as fuzz-findings.
The tokenizer takes a tab for whitespace, but the end of a command was trimmed of newlines, carriage returns and spaces only. A command ending in a carriage return and a tab reached the worker with both, and its last word kept the carriage return, while the same command without the tab lost it. A trailing tab is now trimmed too.
With a tab at the end trimmed like the rest of the whitespace, a command's words joined by one space are read back as the same words whatever its last word ends in, so the round-trip property no longer leaves out a last word ending in a carriage return or a newline. The seed trailing-cr-tab is the command that was left out.
tests/gesture_alloc_test.c holds a 2000-byte gesture and ends it over a socket pair standing for the controller: what arrives is the held line, " end" and one NUL. Before the relay wrote the end in two parts this came out cut at 1024 bytes, with no end word, and the case is red.
make fuzz-sabotage stopped at the first seed that aborted in the mutant's own check_ function, so a mutant that also broke another property passed. It now runs every seed and is red unless at least one aborts and each that aborts does so in an abort() whose innermost check_ frame is the property the mutant names; a sanitizer report or another property's check makes the mutant red.
protocol/mod-host.json is generated from mod-host's headers and the build's check regenerates it; only the headers carrying param_info, remote_pages and track_info (mod-audio/mod-host#109) give it back unchanged. In the FreeMixer channel those are mod-host-protocol-devel 0.10.6.f14a230-11 (audinux's 11 and 12 carry them too) and libmod-host-protocol-dev 0.1.0-5: every earlier release there gives another mod-host.json, and those before 0.10.6.f14a230-8 do not compile tests/fake_host.c. The spec and debian/control declare these floors, and the fuzz workflow installs the headers at the spec's floor.
worker_list, supervisor_reset and quit refuse a word past their grammar with -902, a trailing tab is trimmed like a space, and a held gesture longer than 1024 bytes is ended whole. The build needs the mod-host protocol headers of release 11 (0.1.0-5 for the DEBs) or newer. The command parser and the reply stream have fuzz harnesses (#144).
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Two libFuzzer harnesses, built with clang under ASan and UBSan (
-fsanitize=fuzzer,address,undefined -fno-sanitize-recover=all), with a seed corpus, a coverage witness and one mutant per property, run by a newfuzzworkflow on every push and pull request. The chunking test ofci.ymlstays.fuzz_command drives
verbs_receiveand the dispatcher ofsrc/verbs.c, with the supervisor, pins, descriptors and socket send replaced by a recorder (fuzz/recorder.c) that answers each verb's declared reply or an input-chosen refusal. Properties: one NUL-terminated reply of a declared shape per message, passed on unchanged; a parsed verb is acted on only with an argument count its declared syntax allows; a command's words joined by one space make the same calls, reply and state; a refused command changes nothing.fuzz_reply includes
src/relay.cand answers itsrecvandsend. Properties: the buffer of an unfinished line stays bounded; the controller reads every complete line once and in order however the stream is cut, against a reference splitter; every line the relay writes of its own is an event as written or the end of a gesture still held.Mutants (
fuzz/mutants/, one per property):make fuzz-sabotageapplies each and requires a seed to abort in that property's owncheck_*function. All 7 are found.The harnesses found these, fixed here in their own commits:
worker_list,supervisor_resetandquitacted on words their grammar does not take.worker_list xansweredresp 0with the list,supervisor_reset all xreset, andquit nowansweredresp 0and stopped the daemon. Each now answersresp -902(ERR_INVALID_OPERATION) and does nothing, as mod-host refuses a command with too many arguments. A controller sending the bare verbs, orsupervisor_reset <worker|all>, sees no change. Daemon test:a_word_past_the_grammar_of_a_daemon_verb_is_refused.param_set 0 gain 6\r\tused to reach the worker with\r\tand a last word of6\r; it now reaches it asparam_set 0 gain 6. Daemon test:the_whitespace_that_ends_a_command_is_not_part_of_its_last_word; seedtrailing-cr-tab. The round-trip property no longer leaves anything out.make testby a case intests/gesture_alloc_test.c(red on the old relay) and by the seedgesture-long.Build floors:
mod-host-protocol-devel >= 0.10.6.f14a230-11in the spec, which the fuzz workflow reads, andlibmod-host-protocol-dev (>= 0.1.0-5)in debian/control. These are the first channel releases with the mod-audio/mod-host#109 headers, the only onescheck-generatedreproducesprotocol/mod-host.jsonfrom; I checked every release in both channels.make fuzz-sabotagepasses a mutant only when every seed that aborts does so in that mutant's owncheck_*function.Verified locally (clang 22, Fedora 44):
make fuzz,make fuzz-run FUZZ_TIME=30(no crash),make fuzz-sabotage(7 of 7, every aborting seed in its own property),make fuzz-coverage(command 153 of 153 lines, reply 189 of 210; the rest are allocation-failure paths, the relay thread and fd guards),make fuzz-corpus-check,make test.Closes #144