diff --git a/.github/workflows/fuzz.yml b/.github/workflows/fuzz.yml new file mode 100644 index 0000000..2e4ea59 --- /dev/null +++ b/.github/workflows/fuzz.yml @@ -0,0 +1,71 @@ +name: fuzz + +# The libFuzzer harnesses of fuzz/ (fuzz/README.md), on every push and pull request: each built with clang under +# AddressSanitizer and UndefinedBehaviorSanitizer and fuzzed for 60 s from its seed corpus, every mutant of +# fuzz/mutants/ found by its own property, the coverage witness held to its floor, and the corpus current. An input +# that crashes a harness is uploaded as the fuzz-findings artifact. The command socket's chunking test stays in ci.yml. + +on: + pull_request: + push: + branches: [main] + +permissions: + contents: read + +jobs: + fuzz: + name: fuzz the command parser and the reply stream + runs-on: ubuntu-latest + # a backstop: fuzz-run spends 60 s on each harness and fuzz-sabotage runs seeds only, so the job ends well inside it + timeout-minutes: 30 + container: + image: fedora:44 + steps: + - name: Install clang, libFuzzer and llvm-cov + run: | + dnf install -y --setopt=install_weak_deps=False git dnf5-plugins clang compiler-rt llvm make gawk diffutils \ + python3 pkgconf-pkg-config + + - uses: actions/checkout@v4 + + # the harnesses read mod-host's protocol headers: the FreeMixer channel's mod-host-protocol-devel, at or above the + # floor the spec declares for the build + - name: Install the mod-host protocol headers from the FreeMixer channel + run: | + set -euo pipefail + dnf config-manager addrepo --setopt=retries=10 --from-repofile=https://freemixer.github.io/rpm/freemixer.repo + floor=$(sed -n 's/^BuildRequires:[[:space:]]*mod-host-protocol-devel[[:space:]]*>=[[:space:]]*\([^[:space:]]*\).*/\1/p' packaging/plugin-hostd.spec | head -n 1) + test -n "$floor" + dnf install -y --setopt=retries=10 "mod-host-protocol-devel >= $floor" + + # fuzz-sabotage applies each mutant with git and restores it with git checkout + - name: Let git work in the checkout + run: git config --global --add safe.directory "$GITHUB_WORKSPACE" + + - name: Build the harnesses (clang, libFuzzer, ASan, UBSan) + run: make fuzz + + - name: The seed corpus is what the declared protocol gives + run: make fuzz-corpus-check + + - name: Fuzz each harness for 60 s + run: make fuzz-run FUZZ_TIME=60 + + # a property no mutant can break checks nothing: each one-line mutant must make its harness abort on a seed, in + # that property's own check + - name: Find every mutant, each by its own property + run: make fuzz-sabotage + + # the seed corpus must keep reaching the lines it reaches today (fuzz/coverage-floor.txt) and every property + # check of every harness + - name: Hold the coverage floor + run: make fuzz-coverage + + - name: Upload the crashing inputs + if: failure() + uses: actions/upload-artifact@v4 + with: + name: fuzz-findings + path: fuzz/findings/ + if-no-files-found: ignore diff --git a/.gitignore b/.gitignore index 80d0bab..14fbe1f 100644 --- a/.gitignore +++ b/.gitignore @@ -20,3 +20,5 @@ tests/gesture-alloc-test tests/lv2-alloc-test tests/lv2-alloc-lv2.o tests/transforms-test +/build/ +fuzz/findings/ diff --git a/Makefile b/Makefile index b2c888c..b01c66d 100644 --- a/Makefile +++ b/Makefile @@ -158,9 +158,75 @@ private-refs: tools/private-refs-check.sh tools/private-refs-check.sh --self-test +# libFuzzer harnesses (fuzz/README.md): the command parser and dispatcher, src/verbs.c with the mod-host side replaced by +# a recorder, and the reply stream, src/relay.c with its sockets answered by the harness; clang with libFuzzer, +# AddressSanitizer and UndefinedBehaviorSanitizer, into build/fuzz/. Nothing of the ordinary build is touched. +# make fuzz build every harness +# make fuzz-run FUZZ_TIME=60 fuzz each one from its seed corpus; red on any crash +# make fuzz-sabotage each mutant of fuzz/mutants/ must make its harness abort on a seed, in its own property +# make fuzz-coverage the coverage witness: llvm-cov over the corpus, held to fuzz/coverage-floor.txt +# make fuzz-corpus write fuzz/corpus/ again from the declared protocol and the README examples +FUZZ_CC ?= clang +FUZZ_TIME ?= 60 +FUZZ_BUILD := build/fuzz +FUZZ_HARNESSES := command reply +FUZZ_SAN := -fsanitize=address,undefined -fno-sanitize-recover=all +FUZZ_CFLAGS := -O1 -g -std=c11 -D_GNU_SOURCE -Wall -Wextra -Werror -fno-omit-frame-pointer +FUZZ_INCS := -Iinclude -Isrc -Ifuzz $(PROTOCOL_CFLAGS) +# what each harness is built from beside its own file: the command harness links the parser and the recorder, the +# reply harness includes src/relay.c itself +FUZZ_SRC_command := src/verbs.c fuzz/recorder.c +FUZZ_SRC_reply := +FUZZ_DEPS := fuzz/fuzz.h fuzz/syntax.h fuzz/recorder.h src/relay.c src/verbs.h include/plugin-hostd/protocol.h + +$(FUZZ_BUILD)/fuzz_%: fuzz/fuzz_%.c $(FUZZ_DEPS) $(FUZZ_SRC_command) + @mkdir -p $(dir $@) + $(FUZZ_CC) $(FUZZ_CFLAGS) $(FUZZ_SAN) -fsanitize=fuzzer $(FUZZ_INCS) $< $(FUZZ_SRC_$*) -pthread -o $@ + +fuzz: $(FUZZ_HARNESSES:%=$(FUZZ_BUILD)/fuzz_%) + +fuzz-run: fuzz + FUZZ_BUILD=$(FUZZ_BUILD) FUZZ_TIME=$(FUZZ_TIME) fuzz/run.sh $(FUZZ_HARNESSES) + +fuzz-sabotage: + FUZZ_BUILD=$(FUZZ_BUILD) MAKE="$(MAKE)" fuzz/sabotage.sh + +# the coverage witness: the same harnesses without sanitizers, instrumented for llvm-cov, each run once over its seed +# corpus (-runs=0 executes the corpus and nothing else); the llvm-cov and llvm-profdata of FUZZ_CC's own LLVM +FUZZ_COV_BUILD := $(FUZZ_BUILD)/cov +FUZZ_COV_FLAGS := -O0 -g -std=c11 -D_GNU_SOURCE -fprofile-instr-generate -fcoverage-mapping +LLVM_COV ?= llvm-cov +LLVM_PROFDATA ?= llvm-profdata + +$(FUZZ_COV_BUILD)/fuzz_%: fuzz/fuzz_%.c $(FUZZ_DEPS) $(FUZZ_SRC_command) + @mkdir -p $(dir $@) + $(FUZZ_CC) $(FUZZ_COV_FLAGS) -fsanitize=fuzzer $(FUZZ_INCS) $< $(FUZZ_SRC_$*) -pthread -o $@ + +fuzz-coverage: $(FUZZ_HARNESSES:%=$(FUZZ_COV_BUILD)/fuzz_%) + @set -e; for h in $(FUZZ_HARNESSES); do \ + rm -f $(FUZZ_COV_BUILD)/$$h.profraw; \ + LLVM_PROFILE_FILE=$(FUZZ_COV_BUILD)/$$h.profraw $(FUZZ_COV_BUILD)/fuzz_$$h -runs=0 fuzz/corpus/$$h/ \ + >$(FUZZ_COV_BUILD)/$$h.log 2>&1 || \ + { cat $(FUZZ_COV_BUILD)/$$h.log; echo "fuzz-coverage: fuzz_$$h failed on its own corpus"; exit 1; }; \ + $(LLVM_PROFDATA) merge -sparse $(FUZZ_COV_BUILD)/$$h.profraw -o $(FUZZ_COV_BUILD)/$$h.profdata; \ + done + @LLVM_COV=$(LLVM_COV) fuzz/coverage.sh $(FUZZ_COV_BUILD) $(FUZZ_HARNESSES) + +# the seed corpus is written from protocol/plugin-hostd.json, protocol/mod-host.json and the send lines of README.md and +# docs/own-host.md, never from a network, and committed; fuzz-corpus-check fails when it is not what they give +fuzz-corpus: + python3 fuzz/gen_corpus.py fuzz/corpus + +fuzz-corpus-check: + @set -e; tmp=$$(mktemp -d); trap 'rm -rf $$tmp' EXIT; \ + python3 fuzz/gen_corpus.py $$tmp; diff -r $$tmp fuzz/corpus; echo "the seed corpus is current" + +.PHONY: fuzz fuzz-run fuzz-sabotage fuzz-coverage fuzz-corpus fuzz-corpus-check + # clean rule clean: @rm -f src/*.o $(PROG) $(PROBE) tests/probe-child tests/lv2-probe-child tests/rt_interpose.so $(FAKE) $(PIN_TEST) $(RELAY_TEST) tests/relay-test-relay.o $(FORMAT_TEST) $(GESTURE_ALLOC_TEST) $(LV2_ALLOC_TEST) tests/lv2-alloc-lv2.o $(TRANSFORMS_TEST) $(GEN) tests/stress.clap tests/jack_levels tests/host_scenarios $(LV2_FIXTURE) + @rm -rf build # the daemon against workers that are not plugin hosts at all (tests/fake-host speaks the same protocol and can be # made to die on cue): placement, forwarding, ledger, replay, attribution, the storm bound; no jack, no plugin diff --git a/debian/changelog b/debian/changelog index e3903ae..7582f5a 100644 --- a/debian/changelog +++ b/debian/changelog @@ -1,3 +1,22 @@ +plugin-hostd (0.9.2) unstable; urgency=medium + + * worker_list, supervisor_reset and quit answer -902 to a word past + their grammar instead of acting on it: worker_list x no longer lists, + supervisor_reset all x no longer resets, and quit now no longer stops + the daemon. + * A tab at the end of a command is trimmed like a space: a command + ending in a carriage return and a tab no longer reaches the worker with + both. + * The end of a held gesture longer than 1024 bytes reaches the controller + whole, with its end word; before, it was cut short and the control stayed + held. + * The build needs mod-host's protocol headers of mod-host-protocol-devel + 0.10.6.f14a230-11 or newer (libmod-host-protocol-dev 0.1.0-5 or newer), the + first with param_info, remote_pages and track_info. + * Fuzz harnesses for the command parser and the reply stream (#144). + + -- Pau Aliagas Sat, 24 Oct 2026 22:40:03 +0200 + plugin-hostd (0.9.1) unstable; urgency=medium * The preset schema, protocol/preset.schema.json, is installed in diff --git a/debian/control b/debian/control index 0f31858..8272a97 100644 --- a/debian/control +++ b/debian/control @@ -3,7 +3,7 @@ Section: sound Priority: optional Maintainer: Pau Aliagas Build-Depends: debhelper-compat (= 13), - libmod-host-protocol-dev, + libmod-host-protocol-dev (>= 0.1.0-5), libomx-contract-dev (>= 3.0.0~), omx-clap-host-tests (>= 0.9.0~) , pkgconf, diff --git a/doc/plugin-hostd-probe.1 b/doc/plugin-hostd-probe.1 index ac8e62e..2a96b9f 100644 --- a/doc/plugin-hostd-probe.1 +++ b/doc/plugin-hostd-probe.1 @@ -1,4 +1,4 @@ -.TH PLUGIN\-HOSTD\-PROBE 1 "October 2026" "plugin-hostd 0.9.1" "User Commands" +.TH PLUGIN\-HOSTD\-PROBE 1 "October 2026" "plugin-hostd 0.9.2" "User Commands" .SH NAME plugin\-hostd\-probe \- run a plugin prober under limits and write what happened .SH SYNOPSIS diff --git a/doc/plugin-hostd.1 b/doc/plugin-hostd.1 index 934b4da..2a6a959 100644 --- a/doc/plugin-hostd.1 +++ b/doc/plugin-hostd.1 @@ -1,4 +1,4 @@ -.TH PLUGIN\-HOSTD 1 "October 2026" "plugin-hostd 0.9.1" "User Commands" +.TH PLUGIN\-HOSTD 1 "October 2026" "plugin-hostd 0.9.2" "User Commands" .SH NAME plugin\-hostd \- supervisor of plugin host processes behind one mod\-host protocol socket .SH SYNOPSIS diff --git a/fuzz/README.md b/fuzz/README.md new file mode 100644 index 0000000..70b94c0 --- /dev/null +++ b/fuzz/README.md @@ -0,0 +1,108 @@ +Fuzzing plugin-hostd +==================== + +The daemon reads two streams it does not control: the commands of any local client, and what each worker writes on its +feedback socket. Each has a libFuzzer harness here, built with clang under AddressSanitizer and +UndefinedBehaviorSanitizer and run in CI on every push and pull request (`.github/workflows/fuzz.yml`). + +Each harness is one C file defining `LLVMFuzzerTestOneInput`. It reads its input as a small program, a sequence of +messages or operations, until the input ends; every input means something. No harness opens a socket, starts a +process or a thread, reads a clock, the environment or a file, or calls `dlopen`. Properties are checked with +`if (!cond) abort();`, never `assert()`; every outcome an input can choose returns 0, nothing calls `exit()`, and an +input of 64 MiB or more is refused. The header comment of each harness gives its input format and its properties. + +The harnesses +------------- + +| Harness | Code it drives | Input | +| --- | --- | --- | +| `fuzz_command` | `verbs_receive` and the dispatcher of `src/verbs.c`, with everything below it replaced by the recorder of `fuzz/recorder.c` | up to 32 messages, each a flags byte (accept, or refuse with a declared code) and the message's bytes | +| `fuzz_reply` | the feedback reader of `src/relay.c`: `drain`, the gesture table, `relay_remove`, `relay_instance_gone`, `relay_event` | up to 512 operations: a worker writes, is read, closes, is removed; an instance goes; the controller takes short sends or goes away | + +The recorder stands in for the supervisor, the pins, the descriptors and the socket's send. It answers every call with +the reply its verb declares in `include/plugin-hostd/protocol.h`, or with the refusal the input chose, and folds what an +accepted call changes into a digest of the daemon's state. `fuzz_reply` includes `src/relay.c` and answers its `recv` +and `send` itself, and builds it with a line limit of 6000 bytes so that small inputs reach the drop of a long line. + +What each harness proves, beyond no sanitizer report: + +- **command / reply**: every message gets one reply, a NUL-terminated string of a declared shape, the supervisor's + answer unchanged. +- **command / arity**: a verb the daemon parses itself is acted on only with an argument count its declared syntax + allows. +- **command / round-trip**: a command's words joined by one space, as the declaration writes them, make the same calls, + the same reply and the same state. +- **command / refusal**: a refused command leaves the daemon's state as it was. +- **reply / bounded**: an unfinished line held for a worker never passes the line limit, and its buffer stays bounded. +- **reply / no-loss**: the controller reads every complete line a worker wrote once, in order, however the stream was + cut and wherever its NULs fell, as a reference splitter over the same bytes reads them. +- **reply / shape**: every line the relay writes of its own is an event as written or the end of a gesture still held, + once each. + +The harnesses found what is fixed with them: `worker_list`, `supervisor_reset` and `quit` took words their grammar +does not, a tab at the end of a command was not trimmed as a space is, and the end of a held gesture longer than 1024 +bytes went out cut short. + +The seed corpus +--------------- + +`fuzz/corpus//` is written by `fuzz/gen_corpus.py` (`make fuzz-corpus`) and committed. The command seeds are +every verb of `protocol/plugin-hostd.json` and `protocol/mod-host.json` with sample arguments, each with its optional +words, with one word too many, refused by the supervisor and split by tabs, the placements of `add`, and 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, end and abandon gestures, and put lines at and past the line limit. Nothing is fetched; `make fuzz-corpus-check` +fails when the committed corpus is not what the generator writes. + +The coverage witness +-------------------- + +`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, then every line no seed reaches. `fuzz/coverage-floor.txt` holds the covered lines of each +harness, and the target is red below it, or when a property check of a harness file is executed by no seed +(`fuzz/checks.awk`). + +| Harness | Lines | Covered | Lines no seed reaches | +| --- | ---: | ---: | --- | +| command | 153 | 100.00% | | +| reply | 210 | 90.00% | the paths taken when `realloc` fails (`tests/relay_test.c` and `tests/gesture_alloc_test.c` drive them), the relay thread's join and the listening socket in `relay_finish`, and the `fd < 0` guards of `relay_add` and `relay_remove` | + +The mutants +----------- + +`fuzz/mutants/-.patch` is a one-line bug for each property. `make fuzz-sabotage` applies each, +rebuilds the harness, runs it over every seed one at a time, restores the source with `git checkout --`, and is red +unless at least one seed makes the harness abort and every seed that does aborts in `check_`: a mutant that +also breaks another property does not pass. + +| Mutant | The bug | Found by | +| --- | --- | --- | +| `command-reply` | the reply is sent without its NUL | `add-default` | +| `command-arity` | `pool_config` takes three words or more | `plugin-hostd-pool_config-extra` | +| `command-round-trip` | the first word is split at spaces only | `mod-host-add-tabs` | +| `command-refusal` | a refused `add` still starts a descriptor scan | `add-instance-past-the-table` | +| `reply-bounded` | the overflow check compares with the capacity, not the line limit | `line-past-the-limit-in-parts` | +| `reply-no-loss` | a line of exactly the line limit is dropped | `line-at-the-limit` | +| `reply-shape` | a held gesture is ended with `begin` | `controller-takes-a-few-bytes-a-send` | + +Running +------- + +``` +make fuzz # build both harnesses into build/fuzz/ +make fuzz-run FUZZ_TIME=60 # fuzz each for 60 s; red on any crash +make fuzz-sabotage # every mutant found by its own property +make fuzz-coverage # the coverage witness +make fuzz-corpus # write fuzz/corpus/ again +``` + +They need clang with libFuzzer (`compiler-rt` on Fedora) and, for `fuzz-coverage`, `llvm-cov` and `llvm-profdata`, and +mod-host's protocol headers, found as for the ordinary build. One harness by hand: + +``` +mkdir -p /tmp/new && build/fuzz/fuzz_reply -max_total_time=60 /tmp/new fuzz/corpus/reply +``` + +`make fuzz-run` writes an input that crashed a harness to `fuzz/findings/-crash-`, with the log beside +it; CI uploads the directory as the `fuzz-findings` artifact. `build/fuzz/fuzz_ ` replays it. Once +the bug is fixed, copy the input into `fuzz/corpus//` under a name that says what it is, and add it to +`fuzz/gen_corpus.py`, so every later run replays it first. diff --git a/fuzz/checks.awk b/fuzz/checks.awk new file mode 100644 index 0000000..839dbd6 --- /dev/null +++ b/fuzz/checks.awk @@ -0,0 +1,60 @@ +# SPDX-License-Identifier: GPL-3.0-or-later +# Copyright (C) 2026 Pau Aliagas +# +# THE HARNESS FLOOR (fuzz/coverage.sh). Reads `llvm-cov show` of one harness source +# (`LINE|COUNT|source`) and prints, for every property check in it, one line +# +# :::: +# +# A property check is an abort() and the statement that guards it: the `if` whose +# condition ends in the abort, or, for a bare abort() in a block (an `else` arm, a +# switch's `default:`), the statement that opens that block. is how often the +# seed corpus executed that statement's first line; 0 is a property no seed tests. +# Comments are stripped before anything is matched, so an abort() named in prose is +# not a site. +BEGIN { FS = "|" } +{ + ln = $1 + 0 + cnt = $2 + gsub(/[ \t]/, "", cnt) + src = $0 + sub(/^[^|]*\|[^|]*\|/, "", src) + code = src + if (incomment) { + if (code ~ /\*\//) { sub(/^.*\*\//, "", code); incomment = 0 } + else code = "" + } + while (match(code, /\/\*([^*]|\*+[^*\/])*\*+\//)) + code = substr(code, 1, RSTART - 1) substr(code, RSTART + RLENGTH) + if (code ~ /\/\*/) { sub(/\/\*.*$/, "", code); incomment = 1 } + sub(/\/\/.*$/, "", code) + gsub(/^[ \t]+|[ \t]+$/, "", code) + C[ln] = code; N[ln] = cnt; S[ln] = src + if (code ~ /(^|[^A-Za-z0-9_])abort\(\);/) site[++ns] = ln +} + +# The first line of the statement that ends on line l. +function stmt_start(l) { + while (l > 1 && C[l - 1] != "" && C[l - 1] !~ /[;{}:]$/) l-- + return l +} + +END { + for (i = 1; i <= ns; i++) { + l = site[i] + s = stmt_start(l) + if (s == l && C[l] ~ /^abort\(\);/) { + d = 0 + for (s = l - 1; s >= 1 && d >= 0; s--) + for (k = length(C[s]); k >= 1; k--) { + c = substr(C[s], k, 1) + if (c == "}") d++ + else if (c == "{" && --d < 0) break + } + s = stmt_start(s + 1) + } + t = S[s] + gsub(/^[ \t]+/, "", t) + printf "%s:%d:%d:%s:%s\n", file, s, l, N[s], t + } +} diff --git a/fuzz/corpus/command/add-default b/fuzz/corpus/command/add-default new file mode 100644 index 0000000..31d107c Binary files /dev/null and b/fuzz/corpus/command/add-default differ diff --git a/fuzz/corpus/command/add-fuzz_client b/fuzz/corpus/command/add-fuzz_client new file mode 100644 index 0000000..2a8640f Binary files /dev/null and b/fuzz/corpus/command/add-fuzz_client differ diff --git a/fuzz/corpus/command/add-instance-past-the-table b/fuzz/corpus/command/add-instance-past-the-table new file mode 100644 index 0000000..4a35f00 Binary files /dev/null and b/fuzz/corpus/command/add-instance-past-the-table differ diff --git a/fuzz/corpus/command/add-own b/fuzz/corpus/command/add-own new file mode 100644 index 0000000..73671e8 Binary files /dev/null and b/fuzz/corpus/command/add-own differ diff --git a/fuzz/corpus/command/add-own-fuzz_client-extra b/fuzz/corpus/command/add-own-fuzz_client-extra new file mode 100644 index 0000000..76d08fe Binary files /dev/null and b/fuzz/corpus/command/add-own-fuzz_client-extra differ diff --git a/fuzz/corpus/command/add-pool-p b/fuzz/corpus/command/add-pool-p new file mode 100644 index 0000000..c44869b Binary files /dev/null and b/fuzz/corpus/command/add-pool-p differ diff --git a/fuzz/corpus/command/add-pool-p-fuzz_client b/fuzz/corpus/command/add-pool-p-fuzz_client new file mode 100644 index 0000000..85d9642 Binary files /dev/null and b/fuzz/corpus/command/add-pool-p-fuzz_client differ diff --git a/fuzz/corpus/command/add-too-few b/fuzz/corpus/command/add-too-few new file mode 100644 index 0000000..aef8602 Binary files /dev/null and b/fuzz/corpus/command/add-too-few differ diff --git a/fuzz/corpus/command/chain-id-not-a-number b/fuzz/corpus/command/chain-id-not-a-number new file mode 100644 index 0000000..7a7e998 Binary files /dev/null and b/fuzz/corpus/command/chain-id-not-a-number differ diff --git a/fuzz/corpus/command/chain-not-a-number b/fuzz/corpus/command/chain-not-a-number new file mode 100644 index 0000000..f8fea20 Binary files /dev/null and b/fuzz/corpus/command/chain-not-a-number differ diff --git a/fuzz/corpus/command/chain-of-many b/fuzz/corpus/command/chain-of-many new file mode 100644 index 0000000..18e8d04 Binary files /dev/null and b/fuzz/corpus/command/chain-of-many differ diff --git a/fuzz/corpus/command/empty b/fuzz/corpus/command/empty new file mode 100644 index 0000000..4227ca4 Binary files /dev/null and b/fuzz/corpus/command/empty differ diff --git a/fuzz/corpus/command/leading-space b/fuzz/corpus/command/leading-space new file mode 100644 index 0000000..e75d046 Binary files /dev/null and b/fuzz/corpus/command/leading-space differ diff --git a/fuzz/corpus/command/many-words b/fuzz/corpus/command/many-words new file mode 100644 index 0000000..50b4265 Binary files /dev/null and b/fuzz/corpus/command/many-words differ diff --git a/fuzz/corpus/command/mod-host-add b/fuzz/corpus/command/mod-host-add new file mode 100644 index 0000000..2873e12 Binary files /dev/null and b/fuzz/corpus/command/mod-host-add differ diff --git a/fuzz/corpus/command/mod-host-add-extra b/fuzz/corpus/command/mod-host-add-extra new file mode 100644 index 0000000..19324f3 Binary files /dev/null and b/fuzz/corpus/command/mod-host-add-extra differ diff --git a/fuzz/corpus/command/mod-host-add-full b/fuzz/corpus/command/mod-host-add-full new file mode 100644 index 0000000..c8add98 Binary files /dev/null and b/fuzz/corpus/command/mod-host-add-full differ diff --git a/fuzz/corpus/command/mod-host-add-refused b/fuzz/corpus/command/mod-host-add-refused new file mode 100644 index 0000000..4d5e1ea Binary files /dev/null and b/fuzz/corpus/command/mod-host-add-refused differ diff --git a/fuzz/corpus/command/mod-host-add-tabs b/fuzz/corpus/command/mod-host-add-tabs new file mode 100644 index 0000000..7424bfb Binary files /dev/null and b/fuzz/corpus/command/mod-host-add-tabs differ diff --git a/fuzz/corpus/command/mod-host-bundle_add b/fuzz/corpus/command/mod-host-bundle_add new file mode 100644 index 0000000..ed61b61 Binary files /dev/null and b/fuzz/corpus/command/mod-host-bundle_add differ diff --git a/fuzz/corpus/command/mod-host-bundle_add-extra b/fuzz/corpus/command/mod-host-bundle_add-extra new file mode 100644 index 0000000..bb9db9e Binary files /dev/null and b/fuzz/corpus/command/mod-host-bundle_add-extra differ diff --git a/fuzz/corpus/command/mod-host-bundle_add-refused b/fuzz/corpus/command/mod-host-bundle_add-refused new file mode 100644 index 0000000..a5ff2fc Binary files /dev/null and b/fuzz/corpus/command/mod-host-bundle_add-refused differ diff --git a/fuzz/corpus/command/mod-host-bundle_add-tabs b/fuzz/corpus/command/mod-host-bundle_add-tabs new file mode 100644 index 0000000..3c4fc21 Binary files /dev/null and b/fuzz/corpus/command/mod-host-bundle_add-tabs differ diff --git a/fuzz/corpus/command/mod-host-bundle_remove b/fuzz/corpus/command/mod-host-bundle_remove new file mode 100644 index 0000000..ee1aa11 Binary files /dev/null and b/fuzz/corpus/command/mod-host-bundle_remove differ diff --git a/fuzz/corpus/command/mod-host-bundle_remove-extra b/fuzz/corpus/command/mod-host-bundle_remove-extra new file mode 100644 index 0000000..c06ac6e Binary files /dev/null and b/fuzz/corpus/command/mod-host-bundle_remove-extra differ diff --git a/fuzz/corpus/command/mod-host-bundle_remove-refused b/fuzz/corpus/command/mod-host-bundle_remove-refused new file mode 100644 index 0000000..4a90609 Binary files /dev/null and b/fuzz/corpus/command/mod-host-bundle_remove-refused differ diff --git a/fuzz/corpus/command/mod-host-bundle_remove-tabs b/fuzz/corpus/command/mod-host-bundle_remove-tabs new file mode 100644 index 0000000..051e2be Binary files /dev/null and b/fuzz/corpus/command/mod-host-bundle_remove-tabs differ diff --git a/fuzz/corpus/command/mod-host-bypass b/fuzz/corpus/command/mod-host-bypass new file mode 100644 index 0000000..b90d3c2 Binary files /dev/null and b/fuzz/corpus/command/mod-host-bypass differ diff --git a/fuzz/corpus/command/mod-host-bypass-extra b/fuzz/corpus/command/mod-host-bypass-extra new file mode 100644 index 0000000..e174afa Binary files /dev/null and b/fuzz/corpus/command/mod-host-bypass-extra differ diff --git a/fuzz/corpus/command/mod-host-bypass-refused b/fuzz/corpus/command/mod-host-bypass-refused new file mode 100644 index 0000000..04377dc Binary files /dev/null and b/fuzz/corpus/command/mod-host-bypass-refused differ diff --git a/fuzz/corpus/command/mod-host-bypass-tabs b/fuzz/corpus/command/mod-host-bypass-tabs new file mode 100644 index 0000000..1c004e8 Binary files /dev/null and b/fuzz/corpus/command/mod-host-bypass-tabs differ diff --git a/fuzz/corpus/command/mod-host-cc_map b/fuzz/corpus/command/mod-host-cc_map new file mode 100644 index 0000000..fe83640 Binary files /dev/null and b/fuzz/corpus/command/mod-host-cc_map differ diff --git a/fuzz/corpus/command/mod-host-cc_map-extra b/fuzz/corpus/command/mod-host-cc_map-extra new file mode 100644 index 0000000..9d30e8e Binary files /dev/null and b/fuzz/corpus/command/mod-host-cc_map-extra differ diff --git a/fuzz/corpus/command/mod-host-cc_map-full b/fuzz/corpus/command/mod-host-cc_map-full new file mode 100644 index 0000000..fea591f Binary files /dev/null and b/fuzz/corpus/command/mod-host-cc_map-full differ diff --git a/fuzz/corpus/command/mod-host-cc_map-refused b/fuzz/corpus/command/mod-host-cc_map-refused new file mode 100644 index 0000000..97d63e5 Binary files /dev/null and b/fuzz/corpus/command/mod-host-cc_map-refused differ diff --git a/fuzz/corpus/command/mod-host-cc_map-tabs b/fuzz/corpus/command/mod-host-cc_map-tabs new file mode 100644 index 0000000..bb703c0 Binary files /dev/null and b/fuzz/corpus/command/mod-host-cc_map-tabs differ diff --git a/fuzz/corpus/command/mod-host-cc_unmap b/fuzz/corpus/command/mod-host-cc_unmap new file mode 100644 index 0000000..a9ff831 Binary files /dev/null and b/fuzz/corpus/command/mod-host-cc_unmap differ diff --git a/fuzz/corpus/command/mod-host-cc_unmap-extra b/fuzz/corpus/command/mod-host-cc_unmap-extra new file mode 100644 index 0000000..5bfbeb7 Binary files /dev/null and b/fuzz/corpus/command/mod-host-cc_unmap-extra differ diff --git a/fuzz/corpus/command/mod-host-cc_unmap-refused b/fuzz/corpus/command/mod-host-cc_unmap-refused new file mode 100644 index 0000000..de09c29 Binary files /dev/null and b/fuzz/corpus/command/mod-host-cc_unmap-refused differ diff --git a/fuzz/corpus/command/mod-host-cc_unmap-tabs b/fuzz/corpus/command/mod-host-cc_unmap-tabs new file mode 100644 index 0000000..b5f451b Binary files /dev/null and b/fuzz/corpus/command/mod-host-cc_unmap-tabs differ diff --git a/fuzz/corpus/command/mod-host-cc_value_set b/fuzz/corpus/command/mod-host-cc_value_set new file mode 100644 index 0000000..c01c30a Binary files /dev/null and b/fuzz/corpus/command/mod-host-cc_value_set differ diff --git a/fuzz/corpus/command/mod-host-cc_value_set-extra b/fuzz/corpus/command/mod-host-cc_value_set-extra new file mode 100644 index 0000000..67d0c87 Binary files /dev/null and b/fuzz/corpus/command/mod-host-cc_value_set-extra differ diff --git a/fuzz/corpus/command/mod-host-cc_value_set-refused b/fuzz/corpus/command/mod-host-cc_value_set-refused new file mode 100644 index 0000000..7612b29 Binary files /dev/null and b/fuzz/corpus/command/mod-host-cc_value_set-refused differ diff --git a/fuzz/corpus/command/mod-host-cc_value_set-tabs b/fuzz/corpus/command/mod-host-cc_value_set-tabs new file mode 100644 index 0000000..2f3ff61 Binary files /dev/null and b/fuzz/corpus/command/mod-host-cc_value_set-tabs differ diff --git a/fuzz/corpus/command/mod-host-connect b/fuzz/corpus/command/mod-host-connect new file mode 100644 index 0000000..6258080 Binary files /dev/null and b/fuzz/corpus/command/mod-host-connect differ diff --git a/fuzz/corpus/command/mod-host-connect-extra b/fuzz/corpus/command/mod-host-connect-extra new file mode 100644 index 0000000..b7a4b9a Binary files /dev/null and b/fuzz/corpus/command/mod-host-connect-extra differ diff --git a/fuzz/corpus/command/mod-host-connect-refused b/fuzz/corpus/command/mod-host-connect-refused new file mode 100644 index 0000000..9f26057 Binary files /dev/null and b/fuzz/corpus/command/mod-host-connect-refused differ diff --git a/fuzz/corpus/command/mod-host-connect-tabs b/fuzz/corpus/command/mod-host-connect-tabs new file mode 100644 index 0000000..5055841 Binary files /dev/null and b/fuzz/corpus/command/mod-host-connect-tabs differ diff --git a/fuzz/corpus/command/mod-host-cpu_load b/fuzz/corpus/command/mod-host-cpu_load new file mode 100644 index 0000000..84d3e41 Binary files /dev/null and b/fuzz/corpus/command/mod-host-cpu_load differ diff --git a/fuzz/corpus/command/mod-host-cpu_load-extra b/fuzz/corpus/command/mod-host-cpu_load-extra new file mode 100644 index 0000000..a513175 Binary files /dev/null and b/fuzz/corpus/command/mod-host-cpu_load-extra differ diff --git a/fuzz/corpus/command/mod-host-cpu_load-refused b/fuzz/corpus/command/mod-host-cpu_load-refused new file mode 100644 index 0000000..2b2c062 Binary files /dev/null and b/fuzz/corpus/command/mod-host-cpu_load-refused differ diff --git a/fuzz/corpus/command/mod-host-cpu_load-tabs b/fuzz/corpus/command/mod-host-cpu_load-tabs new file mode 100644 index 0000000..b4a4740 Binary files /dev/null and b/fuzz/corpus/command/mod-host-cpu_load-tabs differ diff --git a/fuzz/corpus/command/mod-host-cv_map b/fuzz/corpus/command/mod-host-cv_map new file mode 100644 index 0000000..5d1f296 Binary files /dev/null and b/fuzz/corpus/command/mod-host-cv_map differ diff --git a/fuzz/corpus/command/mod-host-cv_map-extra b/fuzz/corpus/command/mod-host-cv_map-extra new file mode 100644 index 0000000..da8f208 Binary files /dev/null and b/fuzz/corpus/command/mod-host-cv_map-extra differ diff --git a/fuzz/corpus/command/mod-host-cv_map-refused b/fuzz/corpus/command/mod-host-cv_map-refused new file mode 100644 index 0000000..4bba36b Binary files /dev/null and b/fuzz/corpus/command/mod-host-cv_map-refused differ diff --git a/fuzz/corpus/command/mod-host-cv_map-tabs b/fuzz/corpus/command/mod-host-cv_map-tabs new file mode 100644 index 0000000..f93f324 Binary files /dev/null and b/fuzz/corpus/command/mod-host-cv_map-tabs differ diff --git a/fuzz/corpus/command/mod-host-cv_unmap b/fuzz/corpus/command/mod-host-cv_unmap new file mode 100644 index 0000000..4f2d2c2 Binary files /dev/null and b/fuzz/corpus/command/mod-host-cv_unmap differ diff --git a/fuzz/corpus/command/mod-host-cv_unmap-extra b/fuzz/corpus/command/mod-host-cv_unmap-extra new file mode 100644 index 0000000..a66c864 Binary files /dev/null and b/fuzz/corpus/command/mod-host-cv_unmap-extra differ diff --git a/fuzz/corpus/command/mod-host-cv_unmap-refused b/fuzz/corpus/command/mod-host-cv_unmap-refused new file mode 100644 index 0000000..72bee48 Binary files /dev/null and b/fuzz/corpus/command/mod-host-cv_unmap-refused differ diff --git a/fuzz/corpus/command/mod-host-cv_unmap-tabs b/fuzz/corpus/command/mod-host-cv_unmap-tabs new file mode 100644 index 0000000..17b24e4 Binary files /dev/null and b/fuzz/corpus/command/mod-host-cv_unmap-tabs differ diff --git a/fuzz/corpus/command/mod-host-disconnect b/fuzz/corpus/command/mod-host-disconnect new file mode 100644 index 0000000..05455d4 Binary files /dev/null and b/fuzz/corpus/command/mod-host-disconnect differ diff --git a/fuzz/corpus/command/mod-host-disconnect-extra b/fuzz/corpus/command/mod-host-disconnect-extra new file mode 100644 index 0000000..4378489 Binary files /dev/null and b/fuzz/corpus/command/mod-host-disconnect-extra differ diff --git a/fuzz/corpus/command/mod-host-disconnect-refused b/fuzz/corpus/command/mod-host-disconnect-refused new file mode 100644 index 0000000..72d36c5 Binary files /dev/null and b/fuzz/corpus/command/mod-host-disconnect-refused differ diff --git a/fuzz/corpus/command/mod-host-disconnect-tabs b/fuzz/corpus/command/mod-host-disconnect-tabs new file mode 100644 index 0000000..c5176e7 Binary files /dev/null and b/fuzz/corpus/command/mod-host-disconnect-tabs differ diff --git a/fuzz/corpus/command/mod-host-feature_enable b/fuzz/corpus/command/mod-host-feature_enable new file mode 100644 index 0000000..95f1564 Binary files /dev/null and b/fuzz/corpus/command/mod-host-feature_enable differ diff --git a/fuzz/corpus/command/mod-host-feature_enable-extra b/fuzz/corpus/command/mod-host-feature_enable-extra new file mode 100644 index 0000000..f7ca23b Binary files /dev/null and b/fuzz/corpus/command/mod-host-feature_enable-extra differ diff --git a/fuzz/corpus/command/mod-host-feature_enable-refused b/fuzz/corpus/command/mod-host-feature_enable-refused new file mode 100644 index 0000000..2d0240b Binary files /dev/null and b/fuzz/corpus/command/mod-host-feature_enable-refused differ diff --git a/fuzz/corpus/command/mod-host-feature_enable-tabs b/fuzz/corpus/command/mod-host-feature_enable-tabs new file mode 100644 index 0000000..d145fa9 Binary files /dev/null and b/fuzz/corpus/command/mod-host-feature_enable-tabs differ diff --git a/fuzz/corpus/command/mod-host-help b/fuzz/corpus/command/mod-host-help new file mode 100644 index 0000000..0b0734a Binary files /dev/null and b/fuzz/corpus/command/mod-host-help differ diff --git a/fuzz/corpus/command/mod-host-help-extra b/fuzz/corpus/command/mod-host-help-extra new file mode 100644 index 0000000..878b519 Binary files /dev/null and b/fuzz/corpus/command/mod-host-help-extra differ diff --git a/fuzz/corpus/command/mod-host-help-refused b/fuzz/corpus/command/mod-host-help-refused new file mode 100644 index 0000000..9f83fec Binary files /dev/null and b/fuzz/corpus/command/mod-host-help-refused differ diff --git a/fuzz/corpus/command/mod-host-help-tabs b/fuzz/corpus/command/mod-host-help-tabs new file mode 100644 index 0000000..1c5d718 Binary files /dev/null and b/fuzz/corpus/command/mod-host-help-tabs differ diff --git a/fuzz/corpus/command/mod-host-hmi_map b/fuzz/corpus/command/mod-host-hmi_map new file mode 100644 index 0000000..2f0b45b Binary files /dev/null and b/fuzz/corpus/command/mod-host-hmi_map differ diff --git a/fuzz/corpus/command/mod-host-hmi_map-extra b/fuzz/corpus/command/mod-host-hmi_map-extra new file mode 100644 index 0000000..0376a60 Binary files /dev/null and b/fuzz/corpus/command/mod-host-hmi_map-extra differ diff --git a/fuzz/corpus/command/mod-host-hmi_map-refused b/fuzz/corpus/command/mod-host-hmi_map-refused new file mode 100644 index 0000000..f3b3db4 Binary files /dev/null and b/fuzz/corpus/command/mod-host-hmi_map-refused differ diff --git a/fuzz/corpus/command/mod-host-hmi_map-tabs b/fuzz/corpus/command/mod-host-hmi_map-tabs new file mode 100644 index 0000000..31da106 Binary files /dev/null and b/fuzz/corpus/command/mod-host-hmi_map-tabs differ diff --git a/fuzz/corpus/command/mod-host-hmi_unmap b/fuzz/corpus/command/mod-host-hmi_unmap new file mode 100644 index 0000000..aacc7eb Binary files /dev/null and b/fuzz/corpus/command/mod-host-hmi_unmap differ diff --git a/fuzz/corpus/command/mod-host-hmi_unmap-extra b/fuzz/corpus/command/mod-host-hmi_unmap-extra new file mode 100644 index 0000000..6e2ff6d Binary files /dev/null and b/fuzz/corpus/command/mod-host-hmi_unmap-extra differ diff --git a/fuzz/corpus/command/mod-host-hmi_unmap-refused b/fuzz/corpus/command/mod-host-hmi_unmap-refused new file mode 100644 index 0000000..2155497 Binary files /dev/null and b/fuzz/corpus/command/mod-host-hmi_unmap-refused differ diff --git a/fuzz/corpus/command/mod-host-hmi_unmap-tabs b/fuzz/corpus/command/mod-host-hmi_unmap-tabs new file mode 100644 index 0000000..9a31c9a Binary files /dev/null and b/fuzz/corpus/command/mod-host-hmi_unmap-tabs differ diff --git a/fuzz/corpus/command/mod-host-licensee b/fuzz/corpus/command/mod-host-licensee new file mode 100644 index 0000000..9e8e9a1 Binary files /dev/null and b/fuzz/corpus/command/mod-host-licensee differ diff --git a/fuzz/corpus/command/mod-host-licensee-extra b/fuzz/corpus/command/mod-host-licensee-extra new file mode 100644 index 0000000..f4db126 Binary files /dev/null and b/fuzz/corpus/command/mod-host-licensee-extra differ diff --git a/fuzz/corpus/command/mod-host-licensee-refused b/fuzz/corpus/command/mod-host-licensee-refused new file mode 100644 index 0000000..595b3f5 Binary files /dev/null and b/fuzz/corpus/command/mod-host-licensee-refused differ diff --git a/fuzz/corpus/command/mod-host-licensee-tabs b/fuzz/corpus/command/mod-host-licensee-tabs new file mode 100644 index 0000000..718cb95 Binary files /dev/null and b/fuzz/corpus/command/mod-host-licensee-tabs differ diff --git a/fuzz/corpus/command/mod-host-load b/fuzz/corpus/command/mod-host-load new file mode 100644 index 0000000..c3118f9 Binary files /dev/null and b/fuzz/corpus/command/mod-host-load differ diff --git a/fuzz/corpus/command/mod-host-load-extra b/fuzz/corpus/command/mod-host-load-extra new file mode 100644 index 0000000..fda6da1 Binary files /dev/null and b/fuzz/corpus/command/mod-host-load-extra differ diff --git a/fuzz/corpus/command/mod-host-load-refused b/fuzz/corpus/command/mod-host-load-refused new file mode 100644 index 0000000..e4455e4 Binary files /dev/null and b/fuzz/corpus/command/mod-host-load-refused differ diff --git a/fuzz/corpus/command/mod-host-load-tabs b/fuzz/corpus/command/mod-host-load-tabs new file mode 100644 index 0000000..707126e Binary files /dev/null and b/fuzz/corpus/command/mod-host-load-tabs differ diff --git a/fuzz/corpus/command/mod-host-midi_learn b/fuzz/corpus/command/mod-host-midi_learn new file mode 100644 index 0000000..e600645 Binary files /dev/null and b/fuzz/corpus/command/mod-host-midi_learn differ diff --git a/fuzz/corpus/command/mod-host-midi_learn-extra b/fuzz/corpus/command/mod-host-midi_learn-extra new file mode 100644 index 0000000..48bf2bf Binary files /dev/null and b/fuzz/corpus/command/mod-host-midi_learn-extra differ diff --git a/fuzz/corpus/command/mod-host-midi_learn-refused b/fuzz/corpus/command/mod-host-midi_learn-refused new file mode 100644 index 0000000..f236301 Binary files /dev/null and b/fuzz/corpus/command/mod-host-midi_learn-refused differ diff --git a/fuzz/corpus/command/mod-host-midi_learn-tabs b/fuzz/corpus/command/mod-host-midi_learn-tabs new file mode 100644 index 0000000..946aee7 Binary files /dev/null and b/fuzz/corpus/command/mod-host-midi_learn-tabs differ diff --git a/fuzz/corpus/command/mod-host-midi_map b/fuzz/corpus/command/mod-host-midi_map new file mode 100644 index 0000000..826c19f Binary files /dev/null and b/fuzz/corpus/command/mod-host-midi_map differ diff --git a/fuzz/corpus/command/mod-host-midi_map-extra b/fuzz/corpus/command/mod-host-midi_map-extra new file mode 100644 index 0000000..6b9f4f2 Binary files /dev/null and b/fuzz/corpus/command/mod-host-midi_map-extra differ diff --git a/fuzz/corpus/command/mod-host-midi_map-refused b/fuzz/corpus/command/mod-host-midi_map-refused new file mode 100644 index 0000000..958f2f8 Binary files /dev/null and b/fuzz/corpus/command/mod-host-midi_map-refused differ diff --git a/fuzz/corpus/command/mod-host-midi_map-tabs b/fuzz/corpus/command/mod-host-midi_map-tabs new file mode 100644 index 0000000..841c683 Binary files /dev/null and b/fuzz/corpus/command/mod-host-midi_map-tabs differ diff --git a/fuzz/corpus/command/mod-host-midi_unmap b/fuzz/corpus/command/mod-host-midi_unmap new file mode 100644 index 0000000..8fbde03 Binary files /dev/null and b/fuzz/corpus/command/mod-host-midi_unmap differ diff --git a/fuzz/corpus/command/mod-host-midi_unmap-extra b/fuzz/corpus/command/mod-host-midi_unmap-extra new file mode 100644 index 0000000..0d02c7e Binary files /dev/null and b/fuzz/corpus/command/mod-host-midi_unmap-extra differ diff --git a/fuzz/corpus/command/mod-host-midi_unmap-refused b/fuzz/corpus/command/mod-host-midi_unmap-refused new file mode 100644 index 0000000..0d2cf1e Binary files /dev/null and b/fuzz/corpus/command/mod-host-midi_unmap-refused differ diff --git a/fuzz/corpus/command/mod-host-midi_unmap-tabs b/fuzz/corpus/command/mod-host-midi_unmap-tabs new file mode 100644 index 0000000..9d5a6f7 Binary files /dev/null and b/fuzz/corpus/command/mod-host-midi_unmap-tabs differ diff --git a/fuzz/corpus/command/mod-host-monitor b/fuzz/corpus/command/mod-host-monitor new file mode 100644 index 0000000..2698ef1 Binary files /dev/null and b/fuzz/corpus/command/mod-host-monitor differ diff --git a/fuzz/corpus/command/mod-host-monitor-extra b/fuzz/corpus/command/mod-host-monitor-extra new file mode 100644 index 0000000..94e5ad0 Binary files /dev/null and b/fuzz/corpus/command/mod-host-monitor-extra differ diff --git a/fuzz/corpus/command/mod-host-monitor-refused b/fuzz/corpus/command/mod-host-monitor-refused new file mode 100644 index 0000000..b8826de Binary files /dev/null and b/fuzz/corpus/command/mod-host-monitor-refused differ diff --git a/fuzz/corpus/command/mod-host-monitor-tabs b/fuzz/corpus/command/mod-host-monitor-tabs new file mode 100644 index 0000000..37cb074 Binary files /dev/null and b/fuzz/corpus/command/mod-host-monitor-tabs differ diff --git a/fuzz/corpus/command/mod-host-monitor_midi_program b/fuzz/corpus/command/mod-host-monitor_midi_program new file mode 100644 index 0000000..a86fbcb Binary files /dev/null and b/fuzz/corpus/command/mod-host-monitor_midi_program differ diff --git a/fuzz/corpus/command/mod-host-monitor_midi_program-extra b/fuzz/corpus/command/mod-host-monitor_midi_program-extra new file mode 100644 index 0000000..e6e456a Binary files /dev/null and b/fuzz/corpus/command/mod-host-monitor_midi_program-extra differ diff --git a/fuzz/corpus/command/mod-host-monitor_midi_program-refused b/fuzz/corpus/command/mod-host-monitor_midi_program-refused new file mode 100644 index 0000000..17afae2 Binary files /dev/null and b/fuzz/corpus/command/mod-host-monitor_midi_program-refused differ diff --git a/fuzz/corpus/command/mod-host-monitor_midi_program-tabs b/fuzz/corpus/command/mod-host-monitor_midi_program-tabs new file mode 100644 index 0000000..82ee8bf Binary files /dev/null and b/fuzz/corpus/command/mod-host-monitor_midi_program-tabs differ diff --git a/fuzz/corpus/command/mod-host-monitor_output b/fuzz/corpus/command/mod-host-monitor_output new file mode 100644 index 0000000..a995f4d Binary files /dev/null and b/fuzz/corpus/command/mod-host-monitor_output differ diff --git a/fuzz/corpus/command/mod-host-monitor_output-extra b/fuzz/corpus/command/mod-host-monitor_output-extra new file mode 100644 index 0000000..1833fa1 Binary files /dev/null and b/fuzz/corpus/command/mod-host-monitor_output-extra differ diff --git a/fuzz/corpus/command/mod-host-monitor_output-refused b/fuzz/corpus/command/mod-host-monitor_output-refused new file mode 100644 index 0000000..06dea30 Binary files /dev/null and b/fuzz/corpus/command/mod-host-monitor_output-refused differ diff --git a/fuzz/corpus/command/mod-host-monitor_output-tabs b/fuzz/corpus/command/mod-host-monitor_output-tabs new file mode 100644 index 0000000..cbddf7a Binary files /dev/null and b/fuzz/corpus/command/mod-host-monitor_output-tabs differ diff --git a/fuzz/corpus/command/mod-host-output_data_ready b/fuzz/corpus/command/mod-host-output_data_ready new file mode 100644 index 0000000..55777ba Binary files /dev/null and b/fuzz/corpus/command/mod-host-output_data_ready differ diff --git a/fuzz/corpus/command/mod-host-output_data_ready-extra b/fuzz/corpus/command/mod-host-output_data_ready-extra new file mode 100644 index 0000000..4fbcb13 Binary files /dev/null and b/fuzz/corpus/command/mod-host-output_data_ready-extra differ diff --git a/fuzz/corpus/command/mod-host-output_data_ready-refused b/fuzz/corpus/command/mod-host-output_data_ready-refused new file mode 100644 index 0000000..df1fe23 Binary files /dev/null and b/fuzz/corpus/command/mod-host-output_data_ready-refused differ diff --git a/fuzz/corpus/command/mod-host-output_data_ready-tabs b/fuzz/corpus/command/mod-host-output_data_ready-tabs new file mode 100644 index 0000000..63ed77e Binary files /dev/null and b/fuzz/corpus/command/mod-host-output_data_ready-tabs differ diff --git a/fuzz/corpus/command/mod-host-param_get b/fuzz/corpus/command/mod-host-param_get new file mode 100644 index 0000000..2ae4908 Binary files /dev/null and b/fuzz/corpus/command/mod-host-param_get differ diff --git a/fuzz/corpus/command/mod-host-param_get-extra b/fuzz/corpus/command/mod-host-param_get-extra new file mode 100644 index 0000000..34d35fd Binary files /dev/null and b/fuzz/corpus/command/mod-host-param_get-extra differ diff --git a/fuzz/corpus/command/mod-host-param_get-refused b/fuzz/corpus/command/mod-host-param_get-refused new file mode 100644 index 0000000..6573351 Binary files /dev/null and b/fuzz/corpus/command/mod-host-param_get-refused differ diff --git a/fuzz/corpus/command/mod-host-param_get-tabs b/fuzz/corpus/command/mod-host-param_get-tabs new file mode 100644 index 0000000..af215fa Binary files /dev/null and b/fuzz/corpus/command/mod-host-param_get-tabs differ diff --git a/fuzz/corpus/command/mod-host-param_info b/fuzz/corpus/command/mod-host-param_info new file mode 100644 index 0000000..4b53b94 Binary files /dev/null and b/fuzz/corpus/command/mod-host-param_info differ diff --git a/fuzz/corpus/command/mod-host-param_info-extra b/fuzz/corpus/command/mod-host-param_info-extra new file mode 100644 index 0000000..d214efe Binary files /dev/null and b/fuzz/corpus/command/mod-host-param_info-extra differ diff --git a/fuzz/corpus/command/mod-host-param_info-refused b/fuzz/corpus/command/mod-host-param_info-refused new file mode 100644 index 0000000..74067d8 Binary files /dev/null and b/fuzz/corpus/command/mod-host-param_info-refused differ diff --git a/fuzz/corpus/command/mod-host-param_info-tabs b/fuzz/corpus/command/mod-host-param_info-tabs new file mode 100644 index 0000000..9126d0d Binary files /dev/null and b/fuzz/corpus/command/mod-host-param_info-tabs differ diff --git a/fuzz/corpus/command/mod-host-param_monitor b/fuzz/corpus/command/mod-host-param_monitor new file mode 100644 index 0000000..1867c52 Binary files /dev/null and b/fuzz/corpus/command/mod-host-param_monitor differ diff --git a/fuzz/corpus/command/mod-host-param_monitor-extra b/fuzz/corpus/command/mod-host-param_monitor-extra new file mode 100644 index 0000000..5ea7d22 Binary files /dev/null and b/fuzz/corpus/command/mod-host-param_monitor-extra differ diff --git a/fuzz/corpus/command/mod-host-param_monitor-refused b/fuzz/corpus/command/mod-host-param_monitor-refused new file mode 100644 index 0000000..6b92955 Binary files /dev/null and b/fuzz/corpus/command/mod-host-param_monitor-refused differ diff --git a/fuzz/corpus/command/mod-host-param_monitor-tabs b/fuzz/corpus/command/mod-host-param_monitor-tabs new file mode 100644 index 0000000..c870eb5 Binary files /dev/null and b/fuzz/corpus/command/mod-host-param_monitor-tabs differ diff --git a/fuzz/corpus/command/mod-host-param_set b/fuzz/corpus/command/mod-host-param_set new file mode 100644 index 0000000..5e4e11e Binary files /dev/null and b/fuzz/corpus/command/mod-host-param_set differ diff --git a/fuzz/corpus/command/mod-host-param_set-extra b/fuzz/corpus/command/mod-host-param_set-extra new file mode 100644 index 0000000..6c59676 Binary files /dev/null and b/fuzz/corpus/command/mod-host-param_set-extra differ diff --git a/fuzz/corpus/command/mod-host-param_set-refused b/fuzz/corpus/command/mod-host-param_set-refused new file mode 100644 index 0000000..c328736 Binary files /dev/null and b/fuzz/corpus/command/mod-host-param_set-refused differ diff --git a/fuzz/corpus/command/mod-host-param_set-tabs b/fuzz/corpus/command/mod-host-param_set-tabs new file mode 100644 index 0000000..601cbd2 Binary files /dev/null and b/fuzz/corpus/command/mod-host-param_set-tabs differ diff --git a/fuzz/corpus/command/mod-host-patch_get b/fuzz/corpus/command/mod-host-patch_get new file mode 100644 index 0000000..033afde Binary files /dev/null and b/fuzz/corpus/command/mod-host-patch_get differ diff --git a/fuzz/corpus/command/mod-host-patch_get-extra b/fuzz/corpus/command/mod-host-patch_get-extra new file mode 100644 index 0000000..0dca8d5 Binary files /dev/null and b/fuzz/corpus/command/mod-host-patch_get-extra differ diff --git a/fuzz/corpus/command/mod-host-patch_get-refused b/fuzz/corpus/command/mod-host-patch_get-refused new file mode 100644 index 0000000..d5fddff Binary files /dev/null and b/fuzz/corpus/command/mod-host-patch_get-refused differ diff --git a/fuzz/corpus/command/mod-host-patch_get-tabs b/fuzz/corpus/command/mod-host-patch_get-tabs new file mode 100644 index 0000000..3931cdb Binary files /dev/null and b/fuzz/corpus/command/mod-host-patch_get-tabs differ diff --git a/fuzz/corpus/command/mod-host-patch_set b/fuzz/corpus/command/mod-host-patch_set new file mode 100644 index 0000000..f9d67a6 Binary files /dev/null and b/fuzz/corpus/command/mod-host-patch_set differ diff --git a/fuzz/corpus/command/mod-host-patch_set-extra b/fuzz/corpus/command/mod-host-patch_set-extra new file mode 100644 index 0000000..bffefd8 Binary files /dev/null and b/fuzz/corpus/command/mod-host-patch_set-extra differ diff --git a/fuzz/corpus/command/mod-host-patch_set-refused b/fuzz/corpus/command/mod-host-patch_set-refused new file mode 100644 index 0000000..fb4c40b Binary files /dev/null and b/fuzz/corpus/command/mod-host-patch_set-refused differ diff --git a/fuzz/corpus/command/mod-host-patch_set-tabs b/fuzz/corpus/command/mod-host-patch_set-tabs new file mode 100644 index 0000000..c924744 Binary files /dev/null and b/fuzz/corpus/command/mod-host-patch_set-tabs differ diff --git a/fuzz/corpus/command/mod-host-preset_load b/fuzz/corpus/command/mod-host-preset_load new file mode 100644 index 0000000..d1af9ef Binary files /dev/null and b/fuzz/corpus/command/mod-host-preset_load differ diff --git a/fuzz/corpus/command/mod-host-preset_load-extra b/fuzz/corpus/command/mod-host-preset_load-extra new file mode 100644 index 0000000..b9f0f3f Binary files /dev/null and b/fuzz/corpus/command/mod-host-preset_load-extra differ diff --git a/fuzz/corpus/command/mod-host-preset_load-refused b/fuzz/corpus/command/mod-host-preset_load-refused new file mode 100644 index 0000000..9816494 Binary files /dev/null and b/fuzz/corpus/command/mod-host-preset_load-refused differ diff --git a/fuzz/corpus/command/mod-host-preset_load-tabs b/fuzz/corpus/command/mod-host-preset_load-tabs new file mode 100644 index 0000000..7256bba Binary files /dev/null and b/fuzz/corpus/command/mod-host-preset_load-tabs differ diff --git a/fuzz/corpus/command/mod-host-preset_save b/fuzz/corpus/command/mod-host-preset_save new file mode 100644 index 0000000..eeef1ec Binary files /dev/null and b/fuzz/corpus/command/mod-host-preset_save differ diff --git a/fuzz/corpus/command/mod-host-preset_save-extra b/fuzz/corpus/command/mod-host-preset_save-extra new file mode 100644 index 0000000..a4e66f1 Binary files /dev/null and b/fuzz/corpus/command/mod-host-preset_save-extra differ diff --git a/fuzz/corpus/command/mod-host-preset_save-refused b/fuzz/corpus/command/mod-host-preset_save-refused new file mode 100644 index 0000000..f0fbce8 Binary files /dev/null and b/fuzz/corpus/command/mod-host-preset_save-refused differ diff --git a/fuzz/corpus/command/mod-host-preset_save-tabs b/fuzz/corpus/command/mod-host-preset_save-tabs new file mode 100644 index 0000000..26dc4a2 Binary files /dev/null and b/fuzz/corpus/command/mod-host-preset_save-tabs differ diff --git a/fuzz/corpus/command/mod-host-preset_show b/fuzz/corpus/command/mod-host-preset_show new file mode 100644 index 0000000..cfcb30e Binary files /dev/null and b/fuzz/corpus/command/mod-host-preset_show differ diff --git a/fuzz/corpus/command/mod-host-preset_show-extra b/fuzz/corpus/command/mod-host-preset_show-extra new file mode 100644 index 0000000..748b22d Binary files /dev/null and b/fuzz/corpus/command/mod-host-preset_show-extra differ diff --git a/fuzz/corpus/command/mod-host-preset_show-refused b/fuzz/corpus/command/mod-host-preset_show-refused new file mode 100644 index 0000000..770b400 Binary files /dev/null and b/fuzz/corpus/command/mod-host-preset_show-refused differ diff --git a/fuzz/corpus/command/mod-host-preset_show-tabs b/fuzz/corpus/command/mod-host-preset_show-tabs new file mode 100644 index 0000000..f10dbee Binary files /dev/null and b/fuzz/corpus/command/mod-host-preset_show-tabs differ diff --git a/fuzz/corpus/command/mod-host-quit b/fuzz/corpus/command/mod-host-quit new file mode 100644 index 0000000..debcfb3 Binary files /dev/null and b/fuzz/corpus/command/mod-host-quit differ diff --git a/fuzz/corpus/command/mod-host-quit-extra b/fuzz/corpus/command/mod-host-quit-extra new file mode 100644 index 0000000..ad73a74 Binary files /dev/null and b/fuzz/corpus/command/mod-host-quit-extra differ diff --git a/fuzz/corpus/command/mod-host-quit-refused b/fuzz/corpus/command/mod-host-quit-refused new file mode 100644 index 0000000..15f1106 Binary files /dev/null and b/fuzz/corpus/command/mod-host-quit-refused differ diff --git a/fuzz/corpus/command/mod-host-quit-tabs b/fuzz/corpus/command/mod-host-quit-tabs new file mode 100644 index 0000000..1552c3e Binary files /dev/null and b/fuzz/corpus/command/mod-host-quit-tabs differ diff --git a/fuzz/corpus/command/mod-host-remote_page_get b/fuzz/corpus/command/mod-host-remote_page_get new file mode 100644 index 0000000..ee14c38 Binary files /dev/null and b/fuzz/corpus/command/mod-host-remote_page_get differ diff --git a/fuzz/corpus/command/mod-host-remote_page_get-extra b/fuzz/corpus/command/mod-host-remote_page_get-extra new file mode 100644 index 0000000..6b190ee Binary files /dev/null and b/fuzz/corpus/command/mod-host-remote_page_get-extra differ diff --git a/fuzz/corpus/command/mod-host-remote_page_get-refused b/fuzz/corpus/command/mod-host-remote_page_get-refused new file mode 100644 index 0000000..3f65454 Binary files /dev/null and b/fuzz/corpus/command/mod-host-remote_page_get-refused differ diff --git a/fuzz/corpus/command/mod-host-remote_page_get-tabs b/fuzz/corpus/command/mod-host-remote_page_get-tabs new file mode 100644 index 0000000..a794cd3 Binary files /dev/null and b/fuzz/corpus/command/mod-host-remote_page_get-tabs differ diff --git a/fuzz/corpus/command/mod-host-remote_pages b/fuzz/corpus/command/mod-host-remote_pages new file mode 100644 index 0000000..c56f3e0 Binary files /dev/null and b/fuzz/corpus/command/mod-host-remote_pages differ diff --git a/fuzz/corpus/command/mod-host-remote_pages-extra b/fuzz/corpus/command/mod-host-remote_pages-extra new file mode 100644 index 0000000..8c42455 Binary files /dev/null and b/fuzz/corpus/command/mod-host-remote_pages-extra differ diff --git a/fuzz/corpus/command/mod-host-remote_pages-refused b/fuzz/corpus/command/mod-host-remote_pages-refused new file mode 100644 index 0000000..39a8f15 Binary files /dev/null and b/fuzz/corpus/command/mod-host-remote_pages-refused differ diff --git a/fuzz/corpus/command/mod-host-remote_pages-tabs b/fuzz/corpus/command/mod-host-remote_pages-tabs new file mode 100644 index 0000000..3e39489 Binary files /dev/null and b/fuzz/corpus/command/mod-host-remote_pages-tabs differ diff --git a/fuzz/corpus/command/mod-host-remove b/fuzz/corpus/command/mod-host-remove new file mode 100644 index 0000000..abea890 Binary files /dev/null and b/fuzz/corpus/command/mod-host-remove differ diff --git a/fuzz/corpus/command/mod-host-remove-extra b/fuzz/corpus/command/mod-host-remove-extra new file mode 100644 index 0000000..f93ee71 Binary files /dev/null and b/fuzz/corpus/command/mod-host-remove-extra differ diff --git a/fuzz/corpus/command/mod-host-remove-refused b/fuzz/corpus/command/mod-host-remove-refused new file mode 100644 index 0000000..9b5839c Binary files /dev/null and b/fuzz/corpus/command/mod-host-remove-refused differ diff --git a/fuzz/corpus/command/mod-host-remove-tabs b/fuzz/corpus/command/mod-host-remove-tabs new file mode 100644 index 0000000..675337f Binary files /dev/null and b/fuzz/corpus/command/mod-host-remove-tabs differ diff --git a/fuzz/corpus/command/mod-host-save b/fuzz/corpus/command/mod-host-save new file mode 100644 index 0000000..aad4535 Binary files /dev/null and b/fuzz/corpus/command/mod-host-save differ diff --git a/fuzz/corpus/command/mod-host-save-extra b/fuzz/corpus/command/mod-host-save-extra new file mode 100644 index 0000000..ca63e26 Binary files /dev/null and b/fuzz/corpus/command/mod-host-save-extra differ diff --git a/fuzz/corpus/command/mod-host-save-refused b/fuzz/corpus/command/mod-host-save-refused new file mode 100644 index 0000000..a1a8d1d Binary files /dev/null and b/fuzz/corpus/command/mod-host-save-refused differ diff --git a/fuzz/corpus/command/mod-host-save-tabs b/fuzz/corpus/command/mod-host-save-tabs new file mode 100644 index 0000000..9a39e8f Binary files /dev/null and b/fuzz/corpus/command/mod-host-save-tabs differ diff --git a/fuzz/corpus/command/mod-host-set_bpb b/fuzz/corpus/command/mod-host-set_bpb new file mode 100644 index 0000000..70ae451 Binary files /dev/null and b/fuzz/corpus/command/mod-host-set_bpb differ diff --git a/fuzz/corpus/command/mod-host-set_bpb-extra b/fuzz/corpus/command/mod-host-set_bpb-extra new file mode 100644 index 0000000..decca35 Binary files /dev/null and b/fuzz/corpus/command/mod-host-set_bpb-extra differ diff --git a/fuzz/corpus/command/mod-host-set_bpb-refused b/fuzz/corpus/command/mod-host-set_bpb-refused new file mode 100644 index 0000000..1358dfd Binary files /dev/null and b/fuzz/corpus/command/mod-host-set_bpb-refused differ diff --git a/fuzz/corpus/command/mod-host-set_bpb-tabs b/fuzz/corpus/command/mod-host-set_bpb-tabs new file mode 100644 index 0000000..99b9924 Binary files /dev/null and b/fuzz/corpus/command/mod-host-set_bpb-tabs differ diff --git a/fuzz/corpus/command/mod-host-set_bpm b/fuzz/corpus/command/mod-host-set_bpm new file mode 100644 index 0000000..47e9a99 Binary files /dev/null and b/fuzz/corpus/command/mod-host-set_bpm differ diff --git a/fuzz/corpus/command/mod-host-set_bpm-extra b/fuzz/corpus/command/mod-host-set_bpm-extra new file mode 100644 index 0000000..aabbba3 Binary files /dev/null and b/fuzz/corpus/command/mod-host-set_bpm-extra differ diff --git a/fuzz/corpus/command/mod-host-set_bpm-refused b/fuzz/corpus/command/mod-host-set_bpm-refused new file mode 100644 index 0000000..bd642aa Binary files /dev/null and b/fuzz/corpus/command/mod-host-set_bpm-refused differ diff --git a/fuzz/corpus/command/mod-host-set_bpm-tabs b/fuzz/corpus/command/mod-host-set_bpm-tabs new file mode 100644 index 0000000..d44d470 Binary files /dev/null and b/fuzz/corpus/command/mod-host-set_bpm-tabs differ diff --git a/fuzz/corpus/command/mod-host-show_external_ui b/fuzz/corpus/command/mod-host-show_external_ui new file mode 100644 index 0000000..60cba3d Binary files /dev/null and b/fuzz/corpus/command/mod-host-show_external_ui differ diff --git a/fuzz/corpus/command/mod-host-show_external_ui-extra b/fuzz/corpus/command/mod-host-show_external_ui-extra new file mode 100644 index 0000000..436ef55 Binary files /dev/null and b/fuzz/corpus/command/mod-host-show_external_ui-extra differ diff --git a/fuzz/corpus/command/mod-host-show_external_ui-refused b/fuzz/corpus/command/mod-host-show_external_ui-refused new file mode 100644 index 0000000..fee24b8 Binary files /dev/null and b/fuzz/corpus/command/mod-host-show_external_ui-refused differ diff --git a/fuzz/corpus/command/mod-host-show_external_ui-tabs b/fuzz/corpus/command/mod-host-show_external_ui-tabs new file mode 100644 index 0000000..6575047 Binary files /dev/null and b/fuzz/corpus/command/mod-host-show_external_ui-tabs differ diff --git a/fuzz/corpus/command/mod-host-state_load b/fuzz/corpus/command/mod-host-state_load new file mode 100644 index 0000000..2303711 Binary files /dev/null and b/fuzz/corpus/command/mod-host-state_load differ diff --git a/fuzz/corpus/command/mod-host-state_load-extra b/fuzz/corpus/command/mod-host-state_load-extra new file mode 100644 index 0000000..4d52ed0 Binary files /dev/null and b/fuzz/corpus/command/mod-host-state_load-extra differ diff --git a/fuzz/corpus/command/mod-host-state_load-refused b/fuzz/corpus/command/mod-host-state_load-refused new file mode 100644 index 0000000..a6a8010 Binary files /dev/null and b/fuzz/corpus/command/mod-host-state_load-refused differ diff --git a/fuzz/corpus/command/mod-host-state_load-tabs b/fuzz/corpus/command/mod-host-state_load-tabs new file mode 100644 index 0000000..5c1f535 Binary files /dev/null and b/fuzz/corpus/command/mod-host-state_load-tabs differ diff --git a/fuzz/corpus/command/mod-host-state_save b/fuzz/corpus/command/mod-host-state_save new file mode 100644 index 0000000..1eacbf7 Binary files /dev/null and b/fuzz/corpus/command/mod-host-state_save differ diff --git a/fuzz/corpus/command/mod-host-state_save-extra b/fuzz/corpus/command/mod-host-state_save-extra new file mode 100644 index 0000000..2e4a633 Binary files /dev/null and b/fuzz/corpus/command/mod-host-state_save-extra differ diff --git a/fuzz/corpus/command/mod-host-state_save-refused b/fuzz/corpus/command/mod-host-state_save-refused new file mode 100644 index 0000000..e89fdbc Binary files /dev/null and b/fuzz/corpus/command/mod-host-state_save-refused differ diff --git a/fuzz/corpus/command/mod-host-state_save-tabs b/fuzz/corpus/command/mod-host-state_save-tabs new file mode 100644 index 0000000..448ff62 Binary files /dev/null and b/fuzz/corpus/command/mod-host-state_save-tabs differ diff --git a/fuzz/corpus/command/mod-host-state_tmpdir b/fuzz/corpus/command/mod-host-state_tmpdir new file mode 100644 index 0000000..6a5c448 Binary files /dev/null and b/fuzz/corpus/command/mod-host-state_tmpdir differ diff --git a/fuzz/corpus/command/mod-host-state_tmpdir-extra b/fuzz/corpus/command/mod-host-state_tmpdir-extra new file mode 100644 index 0000000..138e99f Binary files /dev/null and b/fuzz/corpus/command/mod-host-state_tmpdir-extra differ diff --git a/fuzz/corpus/command/mod-host-state_tmpdir-refused b/fuzz/corpus/command/mod-host-state_tmpdir-refused new file mode 100644 index 0000000..9a2648a Binary files /dev/null and b/fuzz/corpus/command/mod-host-state_tmpdir-refused differ diff --git a/fuzz/corpus/command/mod-host-state_tmpdir-tabs b/fuzz/corpus/command/mod-host-state_tmpdir-tabs new file mode 100644 index 0000000..828ddff Binary files /dev/null and b/fuzz/corpus/command/mod-host-state_tmpdir-tabs differ diff --git a/fuzz/corpus/command/mod-host-track_info b/fuzz/corpus/command/mod-host-track_info new file mode 100644 index 0000000..76954f2 Binary files /dev/null and b/fuzz/corpus/command/mod-host-track_info differ diff --git a/fuzz/corpus/command/mod-host-track_info-extra b/fuzz/corpus/command/mod-host-track_info-extra new file mode 100644 index 0000000..f995768 Binary files /dev/null and b/fuzz/corpus/command/mod-host-track_info-extra differ diff --git a/fuzz/corpus/command/mod-host-track_info-full b/fuzz/corpus/command/mod-host-track_info-full new file mode 100644 index 0000000..600a611 Binary files /dev/null and b/fuzz/corpus/command/mod-host-track_info-full differ diff --git a/fuzz/corpus/command/mod-host-track_info-refused b/fuzz/corpus/command/mod-host-track_info-refused new file mode 100644 index 0000000..783061f Binary files /dev/null and b/fuzz/corpus/command/mod-host-track_info-refused differ diff --git a/fuzz/corpus/command/mod-host-track_info-tabs b/fuzz/corpus/command/mod-host-track_info-tabs new file mode 100644 index 0000000..c73546c Binary files /dev/null and b/fuzz/corpus/command/mod-host-track_info-tabs differ diff --git a/fuzz/corpus/command/mod-host-transport b/fuzz/corpus/command/mod-host-transport new file mode 100644 index 0000000..94a1577 Binary files /dev/null and b/fuzz/corpus/command/mod-host-transport differ diff --git a/fuzz/corpus/command/mod-host-transport-extra b/fuzz/corpus/command/mod-host-transport-extra new file mode 100644 index 0000000..0d5f6fe Binary files /dev/null and b/fuzz/corpus/command/mod-host-transport-extra differ diff --git a/fuzz/corpus/command/mod-host-transport-refused b/fuzz/corpus/command/mod-host-transport-refused new file mode 100644 index 0000000..4b6f3aa Binary files /dev/null and b/fuzz/corpus/command/mod-host-transport-refused differ diff --git a/fuzz/corpus/command/mod-host-transport-tabs b/fuzz/corpus/command/mod-host-transport-tabs new file mode 100644 index 0000000..abca506 Binary files /dev/null and b/fuzz/corpus/command/mod-host-transport-tabs differ diff --git a/fuzz/corpus/command/mod-host-transport_sync b/fuzz/corpus/command/mod-host-transport_sync new file mode 100644 index 0000000..e023e0d Binary files /dev/null and b/fuzz/corpus/command/mod-host-transport_sync differ diff --git a/fuzz/corpus/command/mod-host-transport_sync-extra b/fuzz/corpus/command/mod-host-transport_sync-extra new file mode 100644 index 0000000..5058da4 Binary files /dev/null and b/fuzz/corpus/command/mod-host-transport_sync-extra differ diff --git a/fuzz/corpus/command/mod-host-transport_sync-refused b/fuzz/corpus/command/mod-host-transport_sync-refused new file mode 100644 index 0000000..25b862a Binary files /dev/null and b/fuzz/corpus/command/mod-host-transport_sync-refused differ diff --git a/fuzz/corpus/command/mod-host-transport_sync-tabs b/fuzz/corpus/command/mod-host-transport_sync-tabs new file mode 100644 index 0000000..99fd080 Binary files /dev/null and b/fuzz/corpus/command/mod-host-transport_sync-tabs differ diff --git a/fuzz/corpus/command/nul-inside b/fuzz/corpus/command/nul-inside new file mode 100644 index 0000000..156a3af Binary files /dev/null and b/fuzz/corpus/command/nul-inside differ diff --git a/fuzz/corpus/command/only-spaces b/fuzz/corpus/command/only-spaces new file mode 100644 index 0000000..80ad503 Binary files /dev/null and b/fuzz/corpus/command/only-spaces differ diff --git a/fuzz/corpus/command/own-host-1 b/fuzz/corpus/command/own-host-1 new file mode 100644 index 0000000..8f1ab43 Binary files /dev/null and b/fuzz/corpus/command/own-host-1 differ diff --git a/fuzz/corpus/command/own-host-2 b/fuzz/corpus/command/own-host-2 new file mode 100644 index 0000000..e7e0345 Binary files /dev/null and b/fuzz/corpus/command/own-host-2 differ diff --git a/fuzz/corpus/command/own-host-3 b/fuzz/corpus/command/own-host-3 new file mode 100644 index 0000000..d648fce Binary files /dev/null and b/fuzz/corpus/command/own-host-3 differ diff --git a/fuzz/corpus/command/own-host-4 b/fuzz/corpus/command/own-host-4 new file mode 100644 index 0000000..debcfb3 Binary files /dev/null and b/fuzz/corpus/command/own-host-4 differ diff --git a/fuzz/corpus/command/own-host-5 b/fuzz/corpus/command/own-host-5 new file mode 100644 index 0000000..17e6f81 Binary files /dev/null and b/fuzz/corpus/command/own-host-5 differ diff --git a/fuzz/corpus/command/own-host-6 b/fuzz/corpus/command/own-host-6 new file mode 100644 index 0000000..db58cd5 Binary files /dev/null and b/fuzz/corpus/command/own-host-6 differ diff --git a/fuzz/corpus/command/own-host-7 b/fuzz/corpus/command/own-host-7 new file mode 100644 index 0000000..3bbba10 Binary files /dev/null and b/fuzz/corpus/command/own-host-7 differ diff --git a/fuzz/corpus/command/own-host-8 b/fuzz/corpus/command/own-host-8 new file mode 100644 index 0000000..cdfc187 Binary files /dev/null and b/fuzz/corpus/command/own-host-8 differ diff --git a/fuzz/corpus/command/own-host-session b/fuzz/corpus/command/own-host-session new file mode 100644 index 0000000..5c2e799 Binary files /dev/null and b/fuzz/corpus/command/own-host-session differ diff --git a/fuzz/corpus/command/pin-clear-refused b/fuzz/corpus/command/pin-clear-refused new file mode 100644 index 0000000..059c8aa Binary files /dev/null and b/fuzz/corpus/command/pin-clear-refused differ diff --git a/fuzz/corpus/command/pin-set-bad-verdict-key b/fuzz/corpus/command/pin-set-bad-verdict-key new file mode 100644 index 0000000..bb3c83a Binary files /dev/null and b/fuzz/corpus/command/pin-set-bad-verdict-key differ diff --git a/fuzz/corpus/command/pin-set-layout-unknown b/fuzz/corpus/command/pin-set-layout-unknown new file mode 100644 index 0000000..9c9ebb8 Binary files /dev/null and b/fuzz/corpus/command/pin-set-layout-unknown differ diff --git a/fuzz/corpus/command/pin-set-long b/fuzz/corpus/command/pin-set-long new file mode 100644 index 0000000..f7ba4ac Binary files /dev/null and b/fuzz/corpus/command/pin-set-long differ diff --git a/fuzz/corpus/command/plugin-hostd-add b/fuzz/corpus/command/plugin-hostd-add new file mode 100644 index 0000000..ee944b4 Binary files /dev/null and b/fuzz/corpus/command/plugin-hostd-add differ diff --git a/fuzz/corpus/command/plugin-hostd-add-extra b/fuzz/corpus/command/plugin-hostd-add-extra new file mode 100644 index 0000000..d9ba76b Binary files /dev/null and b/fuzz/corpus/command/plugin-hostd-add-extra differ diff --git a/fuzz/corpus/command/plugin-hostd-add-full b/fuzz/corpus/command/plugin-hostd-add-full new file mode 100644 index 0000000..da6f35c Binary files /dev/null and b/fuzz/corpus/command/plugin-hostd-add-full differ diff --git a/fuzz/corpus/command/plugin-hostd-add-refused b/fuzz/corpus/command/plugin-hostd-add-refused new file mode 100644 index 0000000..b5edc13 Binary files /dev/null and b/fuzz/corpus/command/plugin-hostd-add-refused differ diff --git a/fuzz/corpus/command/plugin-hostd-add-tabs b/fuzz/corpus/command/plugin-hostd-add-tabs new file mode 100644 index 0000000..2aa5fcf Binary files /dev/null and b/fuzz/corpus/command/plugin-hostd-add-tabs differ diff --git a/fuzz/corpus/command/plugin-hostd-all_notes_off b/fuzz/corpus/command/plugin-hostd-all_notes_off new file mode 100644 index 0000000..f1b96cc Binary files /dev/null and b/fuzz/corpus/command/plugin-hostd-all_notes_off differ diff --git a/fuzz/corpus/command/plugin-hostd-all_notes_off-extra b/fuzz/corpus/command/plugin-hostd-all_notes_off-extra new file mode 100644 index 0000000..201ac3e Binary files /dev/null and b/fuzz/corpus/command/plugin-hostd-all_notes_off-extra differ diff --git a/fuzz/corpus/command/plugin-hostd-all_notes_off-refused b/fuzz/corpus/command/plugin-hostd-all_notes_off-refused new file mode 100644 index 0000000..627ab2c Binary files /dev/null and b/fuzz/corpus/command/plugin-hostd-all_notes_off-refused differ diff --git a/fuzz/corpus/command/plugin-hostd-all_notes_off-tabs b/fuzz/corpus/command/plugin-hostd-all_notes_off-tabs new file mode 100644 index 0000000..6402ca3 Binary files /dev/null and b/fuzz/corpus/command/plugin-hostd-all_notes_off-tabs differ diff --git a/fuzz/corpus/command/plugin-hostd-chain b/fuzz/corpus/command/plugin-hostd-chain new file mode 100644 index 0000000..5b20603 Binary files /dev/null and b/fuzz/corpus/command/plugin-hostd-chain differ diff --git a/fuzz/corpus/command/plugin-hostd-chain-extra b/fuzz/corpus/command/plugin-hostd-chain-extra new file mode 100644 index 0000000..6abf8ca Binary files /dev/null and b/fuzz/corpus/command/plugin-hostd-chain-extra differ diff --git a/fuzz/corpus/command/plugin-hostd-chain-full b/fuzz/corpus/command/plugin-hostd-chain-full new file mode 100644 index 0000000..0f3db2a Binary files /dev/null and b/fuzz/corpus/command/plugin-hostd-chain-full differ diff --git a/fuzz/corpus/command/plugin-hostd-chain-refused b/fuzz/corpus/command/plugin-hostd-chain-refused new file mode 100644 index 0000000..7d8e80a Binary files /dev/null and b/fuzz/corpus/command/plugin-hostd-chain-refused differ diff --git a/fuzz/corpus/command/plugin-hostd-chain-tabs b/fuzz/corpus/command/plugin-hostd-chain-tabs new file mode 100644 index 0000000..a74ceee Binary files /dev/null and b/fuzz/corpus/command/plugin-hostd-chain-tabs differ diff --git a/fuzz/corpus/command/plugin-hostd-chain_clear b/fuzz/corpus/command/plugin-hostd-chain_clear new file mode 100644 index 0000000..09ad8ef Binary files /dev/null and b/fuzz/corpus/command/plugin-hostd-chain_clear differ diff --git a/fuzz/corpus/command/plugin-hostd-chain_clear-extra b/fuzz/corpus/command/plugin-hostd-chain_clear-extra new file mode 100644 index 0000000..add8be5 Binary files /dev/null and b/fuzz/corpus/command/plugin-hostd-chain_clear-extra differ diff --git a/fuzz/corpus/command/plugin-hostd-chain_clear-refused b/fuzz/corpus/command/plugin-hostd-chain_clear-refused new file mode 100644 index 0000000..357378b Binary files /dev/null and b/fuzz/corpus/command/plugin-hostd-chain_clear-refused differ diff --git a/fuzz/corpus/command/plugin-hostd-chain_clear-tabs b/fuzz/corpus/command/plugin-hostd-chain_clear-tabs new file mode 100644 index 0000000..92c1a17 Binary files /dev/null and b/fuzz/corpus/command/plugin-hostd-chain_clear-tabs differ diff --git a/fuzz/corpus/command/plugin-hostd-descriptor_get b/fuzz/corpus/command/plugin-hostd-descriptor_get new file mode 100644 index 0000000..2c91b24 Binary files /dev/null and b/fuzz/corpus/command/plugin-hostd-descriptor_get differ diff --git a/fuzz/corpus/command/plugin-hostd-descriptor_get-extra b/fuzz/corpus/command/plugin-hostd-descriptor_get-extra new file mode 100644 index 0000000..8815824 Binary files /dev/null and b/fuzz/corpus/command/plugin-hostd-descriptor_get-extra differ diff --git a/fuzz/corpus/command/plugin-hostd-descriptor_get-refused b/fuzz/corpus/command/plugin-hostd-descriptor_get-refused new file mode 100644 index 0000000..55654ef Binary files /dev/null and b/fuzz/corpus/command/plugin-hostd-descriptor_get-refused differ diff --git a/fuzz/corpus/command/plugin-hostd-descriptor_get-tabs b/fuzz/corpus/command/plugin-hostd-descriptor_get-tabs new file mode 100644 index 0000000..a6d6a66 Binary files /dev/null and b/fuzz/corpus/command/plugin-hostd-descriptor_get-tabs differ diff --git a/fuzz/corpus/command/plugin-hostd-instance_info b/fuzz/corpus/command/plugin-hostd-instance_info new file mode 100644 index 0000000..84450b2 Binary files /dev/null and b/fuzz/corpus/command/plugin-hostd-instance_info differ diff --git a/fuzz/corpus/command/plugin-hostd-instance_info-extra b/fuzz/corpus/command/plugin-hostd-instance_info-extra new file mode 100644 index 0000000..ab76d0b Binary files /dev/null and b/fuzz/corpus/command/plugin-hostd-instance_info-extra differ diff --git a/fuzz/corpus/command/plugin-hostd-instance_info-refused b/fuzz/corpus/command/plugin-hostd-instance_info-refused new file mode 100644 index 0000000..0c0e589 Binary files /dev/null and b/fuzz/corpus/command/plugin-hostd-instance_info-refused differ diff --git a/fuzz/corpus/command/plugin-hostd-instance_info-tabs b/fuzz/corpus/command/plugin-hostd-instance_info-tabs new file mode 100644 index 0000000..6062d3a Binary files /dev/null and b/fuzz/corpus/command/plugin-hostd-instance_info-tabs differ diff --git a/fuzz/corpus/command/plugin-hostd-lv2_worker_set b/fuzz/corpus/command/plugin-hostd-lv2_worker_set new file mode 100644 index 0000000..843d010 Binary files /dev/null and b/fuzz/corpus/command/plugin-hostd-lv2_worker_set differ diff --git a/fuzz/corpus/command/plugin-hostd-lv2_worker_set-extra b/fuzz/corpus/command/plugin-hostd-lv2_worker_set-extra new file mode 100644 index 0000000..0beb181 Binary files /dev/null and b/fuzz/corpus/command/plugin-hostd-lv2_worker_set-extra differ diff --git a/fuzz/corpus/command/plugin-hostd-lv2_worker_set-refused b/fuzz/corpus/command/plugin-hostd-lv2_worker_set-refused new file mode 100644 index 0000000..0486616 Binary files /dev/null and b/fuzz/corpus/command/plugin-hostd-lv2_worker_set-refused differ diff --git a/fuzz/corpus/command/plugin-hostd-lv2_worker_set-tabs b/fuzz/corpus/command/plugin-hostd-lv2_worker_set-tabs new file mode 100644 index 0000000..23b5dcb Binary files /dev/null and b/fuzz/corpus/command/plugin-hostd-lv2_worker_set-tabs differ diff --git a/fuzz/corpus/command/plugin-hostd-note_input b/fuzz/corpus/command/plugin-hostd-note_input new file mode 100644 index 0000000..a964161 Binary files /dev/null and b/fuzz/corpus/command/plugin-hostd-note_input differ diff --git a/fuzz/corpus/command/plugin-hostd-note_input-extra b/fuzz/corpus/command/plugin-hostd-note_input-extra new file mode 100644 index 0000000..daa10d3 Binary files /dev/null and b/fuzz/corpus/command/plugin-hostd-note_input-extra differ diff --git a/fuzz/corpus/command/plugin-hostd-note_input-refused b/fuzz/corpus/command/plugin-hostd-note_input-refused new file mode 100644 index 0000000..3ffb5ee Binary files /dev/null and b/fuzz/corpus/command/plugin-hostd-note_input-refused differ diff --git a/fuzz/corpus/command/plugin-hostd-note_input-tabs b/fuzz/corpus/command/plugin-hostd-note_input-tabs new file mode 100644 index 0000000..2408722 Binary files /dev/null and b/fuzz/corpus/command/plugin-hostd-note_input-tabs differ diff --git a/fuzz/corpus/command/plugin-hostd-note_off b/fuzz/corpus/command/plugin-hostd-note_off new file mode 100644 index 0000000..7e84b10 Binary files /dev/null and b/fuzz/corpus/command/plugin-hostd-note_off differ diff --git a/fuzz/corpus/command/plugin-hostd-note_off-extra b/fuzz/corpus/command/plugin-hostd-note_off-extra new file mode 100644 index 0000000..f2b1a19 Binary files /dev/null and b/fuzz/corpus/command/plugin-hostd-note_off-extra differ diff --git a/fuzz/corpus/command/plugin-hostd-note_off-refused b/fuzz/corpus/command/plugin-hostd-note_off-refused new file mode 100644 index 0000000..dc8bb70 Binary files /dev/null and b/fuzz/corpus/command/plugin-hostd-note_off-refused differ diff --git a/fuzz/corpus/command/plugin-hostd-note_off-tabs b/fuzz/corpus/command/plugin-hostd-note_off-tabs new file mode 100644 index 0000000..ee9afff Binary files /dev/null and b/fuzz/corpus/command/plugin-hostd-note_off-tabs differ diff --git a/fuzz/corpus/command/plugin-hostd-note_on b/fuzz/corpus/command/plugin-hostd-note_on new file mode 100644 index 0000000..0915a29 Binary files /dev/null and b/fuzz/corpus/command/plugin-hostd-note_on differ diff --git a/fuzz/corpus/command/plugin-hostd-note_on-extra b/fuzz/corpus/command/plugin-hostd-note_on-extra new file mode 100644 index 0000000..459b84b Binary files /dev/null and b/fuzz/corpus/command/plugin-hostd-note_on-extra differ diff --git a/fuzz/corpus/command/plugin-hostd-note_on-refused b/fuzz/corpus/command/plugin-hostd-note_on-refused new file mode 100644 index 0000000..3ec0826 Binary files /dev/null and b/fuzz/corpus/command/plugin-hostd-note_on-refused differ diff --git a/fuzz/corpus/command/plugin-hostd-note_on-tabs b/fuzz/corpus/command/plugin-hostd-note_on-tabs new file mode 100644 index 0000000..af6609c Binary files /dev/null and b/fuzz/corpus/command/plugin-hostd-note_on-tabs differ diff --git a/fuzz/corpus/command/plugin-hostd-param_info b/fuzz/corpus/command/plugin-hostd-param_info new file mode 100644 index 0000000..08f24a1 Binary files /dev/null and b/fuzz/corpus/command/plugin-hostd-param_info differ diff --git a/fuzz/corpus/command/plugin-hostd-param_info-extra b/fuzz/corpus/command/plugin-hostd-param_info-extra new file mode 100644 index 0000000..cabb99a Binary files /dev/null and b/fuzz/corpus/command/plugin-hostd-param_info-extra differ diff --git a/fuzz/corpus/command/plugin-hostd-param_info-refused b/fuzz/corpus/command/plugin-hostd-param_info-refused new file mode 100644 index 0000000..90c0234 Binary files /dev/null and b/fuzz/corpus/command/plugin-hostd-param_info-refused differ diff --git a/fuzz/corpus/command/plugin-hostd-param_info-tabs b/fuzz/corpus/command/plugin-hostd-param_info-tabs new file mode 100644 index 0000000..2560cbb Binary files /dev/null and b/fuzz/corpus/command/plugin-hostd-param_info-tabs differ diff --git a/fuzz/corpus/command/plugin-hostd-pin_accept b/fuzz/corpus/command/plugin-hostd-pin_accept new file mode 100644 index 0000000..5bb3b5b Binary files /dev/null and b/fuzz/corpus/command/plugin-hostd-pin_accept differ diff --git a/fuzz/corpus/command/plugin-hostd-pin_accept-extra b/fuzz/corpus/command/plugin-hostd-pin_accept-extra new file mode 100644 index 0000000..f5e187e Binary files /dev/null and b/fuzz/corpus/command/plugin-hostd-pin_accept-extra differ diff --git a/fuzz/corpus/command/plugin-hostd-pin_accept-refused b/fuzz/corpus/command/plugin-hostd-pin_accept-refused new file mode 100644 index 0000000..98d339e Binary files /dev/null and b/fuzz/corpus/command/plugin-hostd-pin_accept-refused differ diff --git a/fuzz/corpus/command/plugin-hostd-pin_accept-tabs b/fuzz/corpus/command/plugin-hostd-pin_accept-tabs new file mode 100644 index 0000000..5a3b4cf Binary files /dev/null and b/fuzz/corpus/command/plugin-hostd-pin_accept-tabs differ diff --git a/fuzz/corpus/command/plugin-hostd-pin_clear b/fuzz/corpus/command/plugin-hostd-pin_clear new file mode 100644 index 0000000..fd712f1 Binary files /dev/null and b/fuzz/corpus/command/plugin-hostd-pin_clear differ diff --git a/fuzz/corpus/command/plugin-hostd-pin_clear-extra b/fuzz/corpus/command/plugin-hostd-pin_clear-extra new file mode 100644 index 0000000..2c22daa Binary files /dev/null and b/fuzz/corpus/command/plugin-hostd-pin_clear-extra differ diff --git a/fuzz/corpus/command/plugin-hostd-pin_clear-refused b/fuzz/corpus/command/plugin-hostd-pin_clear-refused new file mode 100644 index 0000000..2f48325 Binary files /dev/null and b/fuzz/corpus/command/plugin-hostd-pin_clear-refused differ diff --git a/fuzz/corpus/command/plugin-hostd-pin_clear-tabs b/fuzz/corpus/command/plugin-hostd-pin_clear-tabs new file mode 100644 index 0000000..96501a0 Binary files /dev/null and b/fuzz/corpus/command/plugin-hostd-pin_clear-tabs differ diff --git a/fuzz/corpus/command/plugin-hostd-pin_expect b/fuzz/corpus/command/plugin-hostd-pin_expect new file mode 100644 index 0000000..c42aaf8 Binary files /dev/null and b/fuzz/corpus/command/plugin-hostd-pin_expect differ diff --git a/fuzz/corpus/command/plugin-hostd-pin_expect-extra b/fuzz/corpus/command/plugin-hostd-pin_expect-extra new file mode 100644 index 0000000..f18fa37 Binary files /dev/null and b/fuzz/corpus/command/plugin-hostd-pin_expect-extra differ diff --git a/fuzz/corpus/command/plugin-hostd-pin_expect-refused b/fuzz/corpus/command/plugin-hostd-pin_expect-refused new file mode 100644 index 0000000..d1a254c Binary files /dev/null and b/fuzz/corpus/command/plugin-hostd-pin_expect-refused differ diff --git a/fuzz/corpus/command/plugin-hostd-pin_expect-tabs b/fuzz/corpus/command/plugin-hostd-pin_expect-tabs new file mode 100644 index 0000000..4e70720 Binary files /dev/null and b/fuzz/corpus/command/plugin-hostd-pin_expect-tabs differ diff --git a/fuzz/corpus/command/plugin-hostd-pin_layout b/fuzz/corpus/command/plugin-hostd-pin_layout new file mode 100644 index 0000000..6c679d2 Binary files /dev/null and b/fuzz/corpus/command/plugin-hostd-pin_layout differ diff --git a/fuzz/corpus/command/plugin-hostd-pin_layout-extra b/fuzz/corpus/command/plugin-hostd-pin_layout-extra new file mode 100644 index 0000000..7d6c90e Binary files /dev/null and b/fuzz/corpus/command/plugin-hostd-pin_layout-extra differ diff --git a/fuzz/corpus/command/plugin-hostd-pin_layout-refused b/fuzz/corpus/command/plugin-hostd-pin_layout-refused new file mode 100644 index 0000000..1885556 Binary files /dev/null and b/fuzz/corpus/command/plugin-hostd-pin_layout-refused differ diff --git a/fuzz/corpus/command/plugin-hostd-pin_layout-tabs b/fuzz/corpus/command/plugin-hostd-pin_layout-tabs new file mode 100644 index 0000000..429ffa4 Binary files /dev/null and b/fuzz/corpus/command/plugin-hostd-pin_layout-tabs differ diff --git a/fuzz/corpus/command/plugin-hostd-pin_set b/fuzz/corpus/command/plugin-hostd-pin_set new file mode 100644 index 0000000..63b4391 Binary files /dev/null and b/fuzz/corpus/command/plugin-hostd-pin_set differ diff --git a/fuzz/corpus/command/plugin-hostd-pin_set-extra b/fuzz/corpus/command/plugin-hostd-pin_set-extra new file mode 100644 index 0000000..e5bf9ee Binary files /dev/null and b/fuzz/corpus/command/plugin-hostd-pin_set-extra differ diff --git a/fuzz/corpus/command/plugin-hostd-pin_set-full b/fuzz/corpus/command/plugin-hostd-pin_set-full new file mode 100644 index 0000000..6f12709 Binary files /dev/null and b/fuzz/corpus/command/plugin-hostd-pin_set-full differ diff --git a/fuzz/corpus/command/plugin-hostd-pin_set-refused b/fuzz/corpus/command/plugin-hostd-pin_set-refused new file mode 100644 index 0000000..6370ec7 Binary files /dev/null and b/fuzz/corpus/command/plugin-hostd-pin_set-refused differ diff --git a/fuzz/corpus/command/plugin-hostd-pin_set-tabs b/fuzz/corpus/command/plugin-hostd-pin_set-tabs new file mode 100644 index 0000000..591da41 Binary files /dev/null and b/fuzz/corpus/command/plugin-hostd-pin_set-tabs differ diff --git a/fuzz/corpus/command/plugin-hostd-policy_set b/fuzz/corpus/command/plugin-hostd-policy_set new file mode 100644 index 0000000..2ee8d89 Binary files /dev/null and b/fuzz/corpus/command/plugin-hostd-policy_set differ diff --git a/fuzz/corpus/command/plugin-hostd-policy_set-extra b/fuzz/corpus/command/plugin-hostd-policy_set-extra new file mode 100644 index 0000000..1488b90 Binary files /dev/null and b/fuzz/corpus/command/plugin-hostd-policy_set-extra differ diff --git a/fuzz/corpus/command/plugin-hostd-policy_set-refused b/fuzz/corpus/command/plugin-hostd-policy_set-refused new file mode 100644 index 0000000..5864ad9 Binary files /dev/null and b/fuzz/corpus/command/plugin-hostd-policy_set-refused differ diff --git a/fuzz/corpus/command/plugin-hostd-policy_set-tabs b/fuzz/corpus/command/plugin-hostd-policy_set-tabs new file mode 100644 index 0000000..3a5bc15 Binary files /dev/null and b/fuzz/corpus/command/plugin-hostd-policy_set-tabs differ diff --git a/fuzz/corpus/command/plugin-hostd-pool_config b/fuzz/corpus/command/plugin-hostd-pool_config new file mode 100644 index 0000000..6563d51 Binary files /dev/null and b/fuzz/corpus/command/plugin-hostd-pool_config differ diff --git a/fuzz/corpus/command/plugin-hostd-pool_config-extra b/fuzz/corpus/command/plugin-hostd-pool_config-extra new file mode 100644 index 0000000..ca95eba Binary files /dev/null and b/fuzz/corpus/command/plugin-hostd-pool_config-extra differ diff --git a/fuzz/corpus/command/plugin-hostd-pool_config-refused b/fuzz/corpus/command/plugin-hostd-pool_config-refused new file mode 100644 index 0000000..b2f48bb Binary files /dev/null and b/fuzz/corpus/command/plugin-hostd-pool_config-refused differ diff --git a/fuzz/corpus/command/plugin-hostd-pool_config-tabs b/fuzz/corpus/command/plugin-hostd-pool_config-tabs new file mode 100644 index 0000000..96a05d2 Binary files /dev/null and b/fuzz/corpus/command/plugin-hostd-pool_config-tabs differ diff --git a/fuzz/corpus/command/plugin-hostd-preset_get b/fuzz/corpus/command/plugin-hostd-preset_get new file mode 100644 index 0000000..bd34db1 Binary files /dev/null and b/fuzz/corpus/command/plugin-hostd-preset_get differ diff --git a/fuzz/corpus/command/plugin-hostd-preset_get-extra b/fuzz/corpus/command/plugin-hostd-preset_get-extra new file mode 100644 index 0000000..51f315b Binary files /dev/null and b/fuzz/corpus/command/plugin-hostd-preset_get-extra differ diff --git a/fuzz/corpus/command/plugin-hostd-preset_get-refused b/fuzz/corpus/command/plugin-hostd-preset_get-refused new file mode 100644 index 0000000..6180b84 Binary files /dev/null and b/fuzz/corpus/command/plugin-hostd-preset_get-refused differ diff --git a/fuzz/corpus/command/plugin-hostd-preset_get-tabs b/fuzz/corpus/command/plugin-hostd-preset_get-tabs new file mode 100644 index 0000000..4dc13ac Binary files /dev/null and b/fuzz/corpus/command/plugin-hostd-preset_get-tabs differ diff --git a/fuzz/corpus/command/plugin-hostd-preset_list b/fuzz/corpus/command/plugin-hostd-preset_list new file mode 100644 index 0000000..b9b8dbe Binary files /dev/null and b/fuzz/corpus/command/plugin-hostd-preset_list differ diff --git a/fuzz/corpus/command/plugin-hostd-preset_list-extra b/fuzz/corpus/command/plugin-hostd-preset_list-extra new file mode 100644 index 0000000..23afc22 Binary files /dev/null and b/fuzz/corpus/command/plugin-hostd-preset_list-extra differ diff --git a/fuzz/corpus/command/plugin-hostd-preset_list-refused b/fuzz/corpus/command/plugin-hostd-preset_list-refused new file mode 100644 index 0000000..cfa0708 Binary files /dev/null and b/fuzz/corpus/command/plugin-hostd-preset_list-refused differ diff --git a/fuzz/corpus/command/plugin-hostd-preset_list-tabs b/fuzz/corpus/command/plugin-hostd-preset_list-tabs new file mode 100644 index 0000000..cc8add9 Binary files /dev/null and b/fuzz/corpus/command/plugin-hostd-preset_list-tabs differ diff --git a/fuzz/corpus/command/plugin-hostd-quarantine_clear b/fuzz/corpus/command/plugin-hostd-quarantine_clear new file mode 100644 index 0000000..3de270f Binary files /dev/null and b/fuzz/corpus/command/plugin-hostd-quarantine_clear differ diff --git a/fuzz/corpus/command/plugin-hostd-quarantine_clear-extra b/fuzz/corpus/command/plugin-hostd-quarantine_clear-extra new file mode 100644 index 0000000..6cad409 Binary files /dev/null and b/fuzz/corpus/command/plugin-hostd-quarantine_clear-extra differ diff --git a/fuzz/corpus/command/plugin-hostd-quarantine_clear-refused b/fuzz/corpus/command/plugin-hostd-quarantine_clear-refused new file mode 100644 index 0000000..cbae4f9 Binary files /dev/null and b/fuzz/corpus/command/plugin-hostd-quarantine_clear-refused differ diff --git a/fuzz/corpus/command/plugin-hostd-quarantine_clear-tabs b/fuzz/corpus/command/plugin-hostd-quarantine_clear-tabs new file mode 100644 index 0000000..e5e6a56 Binary files /dev/null and b/fuzz/corpus/command/plugin-hostd-quarantine_clear-tabs differ diff --git a/fuzz/corpus/command/plugin-hostd-remote_page_get b/fuzz/corpus/command/plugin-hostd-remote_page_get new file mode 100644 index 0000000..ee14c38 Binary files /dev/null and b/fuzz/corpus/command/plugin-hostd-remote_page_get differ diff --git a/fuzz/corpus/command/plugin-hostd-remote_page_get-extra b/fuzz/corpus/command/plugin-hostd-remote_page_get-extra new file mode 100644 index 0000000..6b190ee Binary files /dev/null and b/fuzz/corpus/command/plugin-hostd-remote_page_get-extra differ diff --git a/fuzz/corpus/command/plugin-hostd-remote_page_get-refused b/fuzz/corpus/command/plugin-hostd-remote_page_get-refused new file mode 100644 index 0000000..3f65454 Binary files /dev/null and b/fuzz/corpus/command/plugin-hostd-remote_page_get-refused differ diff --git a/fuzz/corpus/command/plugin-hostd-remote_page_get-tabs b/fuzz/corpus/command/plugin-hostd-remote_page_get-tabs new file mode 100644 index 0000000..a794cd3 Binary files /dev/null and b/fuzz/corpus/command/plugin-hostd-remote_page_get-tabs differ diff --git a/fuzz/corpus/command/plugin-hostd-remote_pages b/fuzz/corpus/command/plugin-hostd-remote_pages new file mode 100644 index 0000000..c56f3e0 Binary files /dev/null and b/fuzz/corpus/command/plugin-hostd-remote_pages differ diff --git a/fuzz/corpus/command/plugin-hostd-remote_pages-extra b/fuzz/corpus/command/plugin-hostd-remote_pages-extra new file mode 100644 index 0000000..8c42455 Binary files /dev/null and b/fuzz/corpus/command/plugin-hostd-remote_pages-extra differ diff --git a/fuzz/corpus/command/plugin-hostd-remote_pages-refused b/fuzz/corpus/command/plugin-hostd-remote_pages-refused new file mode 100644 index 0000000..39a8f15 Binary files /dev/null and b/fuzz/corpus/command/plugin-hostd-remote_pages-refused differ diff --git a/fuzz/corpus/command/plugin-hostd-remote_pages-tabs b/fuzz/corpus/command/plugin-hostd-remote_pages-tabs new file mode 100644 index 0000000..3e39489 Binary files /dev/null and b/fuzz/corpus/command/plugin-hostd-remote_pages-tabs differ diff --git a/fuzz/corpus/command/plugin-hostd-supervisor_reset b/fuzz/corpus/command/plugin-hostd-supervisor_reset new file mode 100644 index 0000000..5847a07 Binary files /dev/null and b/fuzz/corpus/command/plugin-hostd-supervisor_reset differ diff --git a/fuzz/corpus/command/plugin-hostd-supervisor_reset-extra b/fuzz/corpus/command/plugin-hostd-supervisor_reset-extra new file mode 100644 index 0000000..51b6f1d Binary files /dev/null and b/fuzz/corpus/command/plugin-hostd-supervisor_reset-extra differ diff --git a/fuzz/corpus/command/plugin-hostd-supervisor_reset-full b/fuzz/corpus/command/plugin-hostd-supervisor_reset-full new file mode 100644 index 0000000..86c6d14 Binary files /dev/null and b/fuzz/corpus/command/plugin-hostd-supervisor_reset-full differ diff --git a/fuzz/corpus/command/plugin-hostd-supervisor_reset-refused b/fuzz/corpus/command/plugin-hostd-supervisor_reset-refused new file mode 100644 index 0000000..ef6faa5 Binary files /dev/null and b/fuzz/corpus/command/plugin-hostd-supervisor_reset-refused differ diff --git a/fuzz/corpus/command/plugin-hostd-supervisor_reset-tabs b/fuzz/corpus/command/plugin-hostd-supervisor_reset-tabs new file mode 100644 index 0000000..4373668 Binary files /dev/null and b/fuzz/corpus/command/plugin-hostd-supervisor_reset-tabs differ diff --git a/fuzz/corpus/command/plugin-hostd-track_info b/fuzz/corpus/command/plugin-hostd-track_info new file mode 100644 index 0000000..b34a4d5 Binary files /dev/null and b/fuzz/corpus/command/plugin-hostd-track_info differ diff --git a/fuzz/corpus/command/plugin-hostd-track_info-extra b/fuzz/corpus/command/plugin-hostd-track_info-extra new file mode 100644 index 0000000..0bc6ec8 Binary files /dev/null and b/fuzz/corpus/command/plugin-hostd-track_info-extra differ diff --git a/fuzz/corpus/command/plugin-hostd-track_info-full b/fuzz/corpus/command/plugin-hostd-track_info-full new file mode 100644 index 0000000..93e8a46 Binary files /dev/null and b/fuzz/corpus/command/plugin-hostd-track_info-full differ diff --git a/fuzz/corpus/command/plugin-hostd-track_info-refused b/fuzz/corpus/command/plugin-hostd-track_info-refused new file mode 100644 index 0000000..af13608 Binary files /dev/null and b/fuzz/corpus/command/plugin-hostd-track_info-refused differ diff --git a/fuzz/corpus/command/plugin-hostd-track_info-tabs b/fuzz/corpus/command/plugin-hostd-track_info-tabs new file mode 100644 index 0000000..298ee2a Binary files /dev/null and b/fuzz/corpus/command/plugin-hostd-track_info-tabs differ diff --git a/fuzz/corpus/command/plugin-hostd-worker_env b/fuzz/corpus/command/plugin-hostd-worker_env new file mode 100644 index 0000000..44a0486 Binary files /dev/null and b/fuzz/corpus/command/plugin-hostd-worker_env differ diff --git a/fuzz/corpus/command/plugin-hostd-worker_env-extra b/fuzz/corpus/command/plugin-hostd-worker_env-extra new file mode 100644 index 0000000..240fd6d Binary files /dev/null and b/fuzz/corpus/command/plugin-hostd-worker_env-extra differ diff --git a/fuzz/corpus/command/plugin-hostd-worker_env-refused b/fuzz/corpus/command/plugin-hostd-worker_env-refused new file mode 100644 index 0000000..3d9d69b Binary files /dev/null and b/fuzz/corpus/command/plugin-hostd-worker_env-refused differ diff --git a/fuzz/corpus/command/plugin-hostd-worker_env-tabs b/fuzz/corpus/command/plugin-hostd-worker_env-tabs new file mode 100644 index 0000000..da00526 Binary files /dev/null and b/fuzz/corpus/command/plugin-hostd-worker_env-tabs differ diff --git a/fuzz/corpus/command/plugin-hostd-worker_list b/fuzz/corpus/command/plugin-hostd-worker_list new file mode 100644 index 0000000..cdfc187 Binary files /dev/null and b/fuzz/corpus/command/plugin-hostd-worker_list differ diff --git a/fuzz/corpus/command/plugin-hostd-worker_list-extra b/fuzz/corpus/command/plugin-hostd-worker_list-extra new file mode 100644 index 0000000..64877e8 Binary files /dev/null and b/fuzz/corpus/command/plugin-hostd-worker_list-extra differ diff --git a/fuzz/corpus/command/plugin-hostd-worker_list-refused b/fuzz/corpus/command/plugin-hostd-worker_list-refused new file mode 100644 index 0000000..92c48a6 Binary files /dev/null and b/fuzz/corpus/command/plugin-hostd-worker_list-refused differ diff --git a/fuzz/corpus/command/plugin-hostd-worker_list-tabs b/fuzz/corpus/command/plugin-hostd-worker_list-tabs new file mode 100644 index 0000000..66b668a Binary files /dev/null and b/fuzz/corpus/command/plugin-hostd-worker_list-tabs differ diff --git a/fuzz/corpus/command/quit-refused-then-quit b/fuzz/corpus/command/quit-refused-then-quit new file mode 100644 index 0000000..8844c52 Binary files /dev/null and b/fuzz/corpus/command/quit-refused-then-quit differ diff --git a/fuzz/corpus/command/readme-1 b/fuzz/corpus/command/readme-1 new file mode 100644 index 0000000..7e0ce40 Binary files /dev/null and b/fuzz/corpus/command/readme-1 differ diff --git a/fuzz/corpus/command/readme-2 b/fuzz/corpus/command/readme-2 new file mode 100644 index 0000000..bc3365b Binary files /dev/null and b/fuzz/corpus/command/readme-2 differ diff --git a/fuzz/corpus/command/readme-3 b/fuzz/corpus/command/readme-3 new file mode 100644 index 0000000..c5aca6a Binary files /dev/null and b/fuzz/corpus/command/readme-3 differ diff --git a/fuzz/corpus/command/readme-4 b/fuzz/corpus/command/readme-4 new file mode 100644 index 0000000..cdfc187 Binary files /dev/null and b/fuzz/corpus/command/readme-4 differ diff --git a/fuzz/corpus/command/readme-session b/fuzz/corpus/command/readme-session new file mode 100644 index 0000000..91424a1 Binary files /dev/null and b/fuzz/corpus/command/readme-session differ diff --git a/fuzz/corpus/command/remove-all b/fuzz/corpus/command/remove-all new file mode 100644 index 0000000..19071ed Binary files /dev/null and b/fuzz/corpus/command/remove-all differ diff --git a/fuzz/corpus/command/trailing-cr-tab b/fuzz/corpus/command/trailing-cr-tab new file mode 100644 index 0000000..42c2027 Binary files /dev/null and b/fuzz/corpus/command/trailing-cr-tab differ diff --git a/fuzz/corpus/reply/buffer-grows b/fuzz/corpus/reply/buffer-grows new file mode 100644 index 0000000..583d335 Binary files /dev/null and b/fuzz/corpus/reply/buffer-grows differ diff --git a/fuzz/corpus/reply/controller-closes b/fuzz/corpus/reply/controller-closes new file mode 100644 index 0000000..7a1ca66 Binary files /dev/null and b/fuzz/corpus/reply/controller-closes differ diff --git a/fuzz/corpus/reply/controller-connection-reset b/fuzz/corpus/reply/controller-connection-reset new file mode 100644 index 0000000..bf8cdf9 Binary files /dev/null and b/fuzz/corpus/reply/controller-connection-reset differ diff --git a/fuzz/corpus/reply/controller-takes-a-few-bytes-a-send b/fuzz/corpus/reply/controller-takes-a-few-bytes-a-send new file mode 100644 index 0000000..77aef86 Binary files /dev/null and b/fuzz/corpus/reply/controller-takes-a-few-bytes-a-send differ diff --git a/fuzz/corpus/reply/event b/fuzz/corpus/reply/event new file mode 100644 index 0000000..cf6b8e1 Binary files /dev/null and b/fuzz/corpus/reply/event differ diff --git a/fuzz/corpus/reply/event-cut-at-nul b/fuzz/corpus/reply/event-cut-at-nul new file mode 100644 index 0000000..8ccdf31 Binary files /dev/null and b/fuzz/corpus/reply/event-cut-at-nul differ diff --git a/fuzz/corpus/reply/gesture-begun-twice b/fuzz/corpus/reply/gesture-begun-twice new file mode 100644 index 0000000..8e8c4cb Binary files /dev/null and b/fuzz/corpus/reply/gesture-begun-twice differ diff --git a/fuzz/corpus/reply/gesture-ended-by-its-worker b/fuzz/corpus/reply/gesture-ended-by-its-worker new file mode 100644 index 0000000..94bdbf3 Binary files /dev/null and b/fuzz/corpus/reply/gesture-ended-by-its-worker differ diff --git a/fuzz/corpus/reply/gesture-held-at-the-end b/fuzz/corpus/reply/gesture-held-at-the-end new file mode 100644 index 0000000..ad19993 Binary files /dev/null and b/fuzz/corpus/reply/gesture-held-at-the-end differ diff --git a/fuzz/corpus/reply/gesture-held-then-removed b/fuzz/corpus/reply/gesture-held-then-removed new file mode 100644 index 0000000..1903498 Binary files /dev/null and b/fuzz/corpus/reply/gesture-held-then-removed differ diff --git a/fuzz/corpus/reply/gesture-long b/fuzz/corpus/reply/gesture-long new file mode 100644 index 0000000..563feeb Binary files /dev/null and b/fuzz/corpus/reply/gesture-long differ diff --git a/fuzz/corpus/reply/gesture-not-a-gesture b/fuzz/corpus/reply/gesture-not-a-gesture new file mode 100644 index 0000000..55289f1 Binary files /dev/null and b/fuzz/corpus/reply/gesture-not-a-gesture differ diff --git a/fuzz/corpus/reply/gesture-of-a-removed-instance b/fuzz/corpus/reply/gesture-of-a-removed-instance new file mode 100644 index 0000000..6921d64 Binary files /dev/null and b/fuzz/corpus/reply/gesture-of-a-removed-instance differ diff --git a/fuzz/corpus/reply/interrupted b/fuzz/corpus/reply/interrupted new file mode 100644 index 0000000..147cb5f Binary files /dev/null and b/fuzz/corpus/reply/interrupted differ diff --git a/fuzz/corpus/reply/line-at-the-limit b/fuzz/corpus/reply/line-at-the-limit new file mode 100644 index 0000000..b988193 Binary files /dev/null and b/fuzz/corpus/reply/line-at-the-limit differ diff --git a/fuzz/corpus/reply/line-past-the-limit b/fuzz/corpus/reply/line-past-the-limit new file mode 100644 index 0000000..aa32684 Binary files /dev/null and b/fuzz/corpus/reply/line-past-the-limit differ diff --git a/fuzz/corpus/reply/line-past-the-limit-in-parts b/fuzz/corpus/reply/line-past-the-limit-in-parts new file mode 100644 index 0000000..8b81d57 Binary files /dev/null and b/fuzz/corpus/reply/line-past-the-limit-in-parts differ diff --git a/fuzz/corpus/reply/lines b/fuzz/corpus/reply/lines new file mode 100644 index 0000000..f6877c9 Binary files /dev/null and b/fuzz/corpus/reply/lines differ diff --git a/fuzz/corpus/reply/split-at-every-byte b/fuzz/corpus/reply/split-at-every-byte new file mode 100644 index 0000000..e065575 Binary files /dev/null and b/fuzz/corpus/reply/split-at-every-byte differ diff --git a/fuzz/corpus/reply/split-before-every-nul b/fuzz/corpus/reply/split-before-every-nul new file mode 100644 index 0000000..b06ae8c Binary files /dev/null and b/fuzz/corpus/reply/split-before-every-nul differ diff --git a/fuzz/corpus/reply/truncated-tail b/fuzz/corpus/reply/truncated-tail new file mode 100644 index 0000000..6fef45c Binary files /dev/null and b/fuzz/corpus/reply/truncated-tail differ diff --git a/fuzz/corpus/reply/two-workers-interleaved b/fuzz/corpus/reply/two-workers-interleaved new file mode 100644 index 0000000..2d9e4c7 Binary files /dev/null and b/fuzz/corpus/reply/two-workers-interleaved differ diff --git a/fuzz/corpus/reply/worker-closed b/fuzz/corpus/reply/worker-closed new file mode 100644 index 0000000..79eea09 Binary files /dev/null and b/fuzz/corpus/reply/worker-closed differ diff --git a/fuzz/corpus/reply/workers-past-the-table b/fuzz/corpus/reply/workers-past-the-table new file mode 100644 index 0000000..eab7368 Binary files /dev/null and b/fuzz/corpus/reply/workers-past-the-table differ diff --git a/fuzz/coverage-floor.txt b/fuzz/coverage-floor.txt new file mode 100644 index 0000000..3447b90 --- /dev/null +++ b/fuzz/coverage-floor.txt @@ -0,0 +1,4 @@ +# Covered lines each harness's seed corpus reaches (make fuzz-coverage, fuzz/coverage.sh). +# A harness below its floor turns fuzz-coverage red. harness covered-lines +command 153 +reply 189 diff --git a/fuzz/coverage.sh b/fuzz/coverage.sh new file mode 100755 index 0000000..ebfaf8b --- /dev/null +++ b/fuzz/coverage.sh @@ -0,0 +1,96 @@ +#!/bin/sh +# SPDX-License-Identifier: GPL-3.0-or-later +# Copyright (C) 2026 Pau Aliagas +# +# THE COVERAGE WITNESS (make fuzz-coverage). Each harness, built for llvm-cov without sanitizers, has run once over its +# seed corpus; this prints the line coverage of the functions the harness exists to drive, and every line of them the +# corpus never reached. The relay's thread, its listening socket and its wake-up pipe are not the reader and are not +# counted. +# +# THE FLOOR. fuzz/coverage-floor.txt holds, per harness, the number of covered lines the committed corpus reaches. A +# harness below its floor turns this red: a seed lost, a harness that stopped driving a function, or code that grew +# lines its corpus never reaches. Raise a floor when coverage rises; lower one only with the change that removed the +# lines. +# +# THE HARNESS FLOOR. Every property check of the harness itself, each abort() in fuzz/fuzz_.c and the +# statement that guards it (fuzz/checks.awk), must be executed by at least one seed: a check no seed reaches is a +# property nobody tests. This names the file and line of every such check and turns red. +# +# Usage: coverage.sh ... (LLVM_COV names llvm-cov) +set -eu +B=$1 +shift +LLVM_COV=${LLVM_COV:-llvm-cov} +FLOOR=fuzz/coverage-floor.txt +[ -f "$FLOOR" ] || { echo "coverage.sh: no $FLOOR" >&2; exit 2; } +low= +dead= + +# the functions each harness covers; static functions are matched by their bare name +funcs() { + case $1 in + command) echo src/verbs.c :: is_instance_verb is_placement pin_set_words number chain_words reply handle \ + verbs_receive ;; + reply) echo src/relay.c :: send_line gesture_seen gesture_end gestures_end gestures_end_instance drain \ + relay_finish relay_event relay_add relay_instance_gone relay_remove ;; + *) echo "coverage.sh: no function list for harness '$1'" >&2; exit 2 ;; + esac +} + +for h in "$@"; do + set -- $(funcs "$h") + files= + while [ "$1" != "::" ]; do files="$files $1"; shift; done + shift + bin=$B/fuzz_$h prof=$B/$h.profdata + echo "== fuzz_$h: $(ls fuzz/corpus/$h | wc -l) seed inputs" + # Name Regions Miss Cover Lines Miss Cover ...: keep the listed functions + tot=$B/$h.covered + "$LLVM_COV" report -show-functions "$bin" -instr-profile="$prof" $files | + awk -v want="$*" -v tot="$tot" ' + BEGIN { n = split(want, w, " "); for (i = 1; i <= n; i++) keep[w[i]] = 1 } + { name = $1; sub(/^.*:/, "", name) } + (name in keep) && NF >= 10 { + printf " %-26s lines %4d missed %3d %s\n", name, $5, $6, $7 + lines += $5; missed += $6; seen[name] = 1 + } + END { + for (k in keep) if (!(k in seen)) printf " %-26s NOT FOUND in the report\n", k + printf " %-26s lines %4d missed %3d %.2f%%\n", "TOTAL", lines, missed, + lines ? 100 * (lines - missed) / lines : 0 + print lines - missed > tot + }' + for f in "$@"; do + "$LLVM_COV" show "$bin" -instr-profile="$prof" -name-regex="^(.*:)?$f\$" $files 2>/dev/null | + awk -v f="$f" -F'|' '$2 ~ /^ *0$/ { sub(/^ */, "", $1); printf " unreached %s:%s %s\n", f, $1, $3 }' + done + src=fuzz/fuzz_$h.c + "$LLVM_COV" show "$bin" -instr-profile="$prof" "$src" | awk -v file="$src" -f fuzz/checks.awk >"$B/$h.checks" + nchk=$(wc -l <"$B/$h.checks") + if [ "$nchk" -eq 0 ]; then + echo " CHECKS: no property check found in $src" + dead="$dead $h" + elif awk -F: '$4 == "0" { n++ } END { exit !n }' "$B/$h.checks"; then + awk -F: '$4 == "0" { t = $0; sub(/^([^:]*:){4}/, "", t) + printf " CHECK: %s:%d no seed reaches (abort at line %d): %s\n", $1, $2, $3, t }' "$B/$h.checks" + dead="$dead $h" + else + echo " checks: all $nchk property checks of $src reached" + fi + covered=$(cat "$tot") + floor=$(awk -v h="$h" '$1 == h { print $2 }' "$FLOOR") + if [ -z "$floor" ]; then + echo " FLOOR: fuzz_$h has no line in $FLOOR" + low="$low $h" + elif [ "$covered" -lt "$floor" ]; then + echo " FLOOR: fuzz_$h covers $covered lines, below its floor of $floor" + low="$low $h" + else + echo " floor $floor: held ($covered covered)" + fi +done + +[ -z "$low" ] || echo "make fuzz-coverage: RED -- below the coverage floor:$low" +[ -z "$dead" ] || echo "make fuzz-coverage: RED -- harness checks no seed reaches:$dead" +[ -z "$low$dead" ] || exit 1 +echo "make fuzz-coverage: every harness at or above its coverage floor, every check reached" diff --git a/fuzz/fuzz.h b/fuzz/fuzz.h new file mode 100644 index 0000000..f114e48 --- /dev/null +++ b/fuzz/fuzz.h @@ -0,0 +1,92 @@ +/* SPDX-License-Identifier: GPL-3.0-or-later */ +/* + * This file is part of plugin-hostd. + * + * plugin-hostd is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * plugin-hostd is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with plugin-hostd. If not, see . + * + * Copyright (C) 2026 Pau Aliagas + */ + +/* What the two libFuzzer harnesses share: a cursor over the input, the input-size refusal, and the exact-size copy + * that lets the sanitizer see a read one byte past a message. + * + * The cursor is total. Reading past the end of the input yields zeros and an empty slice, never a failure, so every + * input decodes to some sequence of calls and the end of the input ends it. Nothing here reads a clock, the + * environment, a file or the network. */ + +#ifndef PLUGIN_HOSTD_FUZZ_H +#define PLUGIN_HOSTD_FUZZ_H + +#include +#include +#include +#include + +/* the OSS Verify driver refuses an input this large; the harnesses refuse it too, so the domain they are proven over + * is the domain they are run over */ +#define FUZZ_INPUT_MAX ((size_t)64 << 20) + +struct fuzz_in { + const uint8_t *p; + size_t n; +}; + +static inline struct fuzz_in fuzz_in(const uint8_t *data, size_t size) +{ + struct fuzz_in in = { data, size }; + + return in; +} + +static inline uint8_t fuzz_u8(struct fuzz_in *in) +{ + if (in->n == 0) + return 0; + in->n--; + return *in->p++; +} + +/* little-endian, like every multi-byte field the harnesses invent */ +static inline uint16_t fuzz_u16(struct fuzz_in *in) +{ + uint16_t lo = fuzz_u8(in); + + return (uint16_t)(lo | (uint16_t)fuzz_u8(in) << 8); +} + +/* up to `want` bytes from the cursor; returns how many there were */ +static inline size_t fuzz_take(struct fuzz_in *in, size_t want, const uint8_t **out) +{ + size_t n = want < in->n ? want : in->n; + + *out = in->p; + in->p += n; + in->n -= n; + return n; +} + +/* a heap copy of exactly `n` bytes, with no terminator: a message handed to the daemon lives in its own allocation, so + * a read past its end lands in a redzone the sanitizer watches */ +static inline char *fuzz_dup(const uint8_t *p, size_t n) +{ + char *b = malloc(n ? n : 1); + + if (b == NULL) + abort(); + if (n) + memcpy(b, p, n); + return b; +} + +#endif diff --git a/fuzz/fuzz_command.c b/fuzz/fuzz_command.c new file mode 100644 index 0000000..f57a5f2 --- /dev/null +++ b/fuzz/fuzz_command.c @@ -0,0 +1,311 @@ +/* SPDX-License-Identifier: GPL-3.0-or-later */ +/* + * This file is part of plugin-hostd. + * + * plugin-hostd is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * plugin-hostd is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with plugin-hostd. If not, see . + * + * Copyright (C) 2026 Pau Aliagas + */ + +/* libFuzzer harness: THE COMMAND PARSER AND DISPATCHER (src/verbs.c, verbs_receive and everything it calls in that + * file), with the mod-host side replaced by the recorder of fuzz/recorder.c. A controller's message is whatever bytes + * it sends; the daemon reads its words, refuses what is outside its grammar, and hands the rest to the supervisor. + * + * Input: up to MESSAGES_MAX messages, each + * [flags] [u16 LE length] [that many bytes, at most MESSAGE_MAX] + * flags bit 0: the recorder refuses every call of this message that can refuse, with the declared code + * rec_codes[(flags >> 1) % rec_ncodes]; clear, it accepts with the reply each verb declares. Each message is + * copied into an allocation of exactly its length, with no terminator, as the socket hands it over. + * + * The words of a message, below, are what the daemon reads: its bytes up to the first NUL, the newlines, carriage + * returns, spaces and tabs at its end removed, split at spaces and tabs. Its verb is the first word. + * + * Properties, each an abort(): + * - reply: every message gets exactly one reply, one NUL-terminated string with no NUL before its end, which is + * the text the supervisor answered, unchanged; a refusal is "resp " of a declared code, and anything else + * has the reply shape its verb declares (PHD_VERBS, PHD_INSTANCE_VERBS) or mod-host's "resp 0". + * Mutant: fuzz/mutants/command-reply.patch, the reply sent without its NUL. + * - arity: a verb the daemon parses itself (PHD_VERBS, the rows tools/protocol-gen.c writes into + * protocol/plugin-hostd.json, and mod-host's remove, connect, disconnect and quit) is acted on only with an + * argument count its declared syntax allows; a word past PHD_LINE_WORDS_MAX is not read. + * Mutant: fuzz/mutants/command-arity.patch, pool_config taking three words or more. + * - round-trip: the words of a command the daemon acted on, joined by one space as the declaration writes them, + * make the same calls with the same arguments, the same reply and the same state. + * Mutant: fuzz/mutants/command-round-trip.patch, words split at spaces only. + * - refusal: a refused command (its reply's code is negative) leaves the daemon's state as it was: no call the + * supervisor took and none of the daemon's own follow-ups (a descriptor scan, the LV2 refusals asked again, quit). + * The host settings are not part of it: PHD_HOST_SETTINGS keeps them as sent, whatever the workers answer. + * Mutant: fuzz/mutants/command-refusal.patch, a refused add still scanned. + */ + +#include +#include +#include +#include +#include + +#include +#include + +#include + +#include "supervisor.h" +#include "verbs.h" + +#include "fuzz.h" +#include "recorder.h" +#include "syntax.h" + +#define MESSAGES_MAX 32 +#define MESSAGE_MAX (2 * PHD_LINE_MAX) + +/* the verbs the daemon parses itself, with their declared argument syntax */ +static const struct { + const char *name; + const char *syntax; + int format; /* 1: syntax is a mod-host command format, " %i ..." */ +} g_parsed[] = { +#define X(id, name, arguments, reply, meaning) { name, arguments, 0 }, + PHD_VERBS(X) +#undef X + { "remove", EFFECT_REMOVE, 1 }, + { "connect", EFFECT_CONNECT, 1 }, + { "disconnect", EFFECT_DISCONNECT, 1 }, + { "quit", QUIT, 1 }, +}; + +/* the words of a message as the daemon reads them */ +struct words { + char *text; /* the message up to its first NUL, its end trimmed; the words point into a copy */ + char *copy; + char **w; + int n; +}; + +static void words_of(const char *data, size_t size, struct words *out) +{ + size_t len = strnlen(data, size), cap = len / 2 + 2; + char *t; + + out->text = malloc(len + 1); + out->copy = malloc(len + 1); + out->w = malloc(cap * sizeof(char *)); + if (!out->text || !out->copy || !out->w) + abort(); + memcpy(out->text, data, len); + out->text[len] = '\0'; + while (len > 0 && (out->text[len - 1] == '\n' || out->text[len - 1] == '\r' || out->text[len - 1] == ' ' || + out->text[len - 1] == '\t')) + out->text[--len] = '\0'; + memcpy(out->copy, out->text, len + 1); + out->n = 0; + for (t = out->copy; *t;) + { + t += strspn(t, " \t"); + if (!*t) + break; + out->w[out->n++] = t; + t += strcspn(t, " \t"); + if (*t) + *t++ = '\0'; + } +} + +static void words_free(struct words *w) +{ + free(w->text); + free(w->copy); + free(w->w); +} + +/* the message's words joined by one space */ +static char *joined(const struct words *w, size_t *len) +{ + size_t n = 0; + char *s; + int k; + + for (k = 0; k < w->n; k++) + n += strlen(w->w[k]) + 1; + s = malloc(n + 1); + if (!s) + abort(); + s[0] = '\0'; + for (k = 0, n = 0; k < w->n; k++) + { + size_t l = strlen(w->w[k]); + + if (k) + s[n++] = ' '; + memcpy(s + n, w->w[k], l); + n += l; + } + s[n] = '\0'; + *len = n; + return s; +} + +/* one message to the daemon, in an allocation of exactly its length */ +static void send_message(const uint8_t *bytes, size_t len) +{ + msg_t msg; + + msg.sender_id = 7; + msg.data = fuzz_dup(bytes, len); + msg.data_size = len; + g_quitting = 0; + rec_clear(); + verbs_receive(&msg); + free(msg.data); +} + +/* whether the daemon acted on the message: it called the supervisor, or it is quitting */ +static int acted(void) +{ + return g_quitting || g_rec.ncalls > 0; +} + +/* the reply property */ +__attribute__((noinline)) static void check_reply(const struct words *w) +{ + char refusal[32]; + const char *shape; + int code; + + /* one reply, and it is a string that ends where the message the socket sends ends */ + if (g_rec.nreplies != 1 || g_rec.reply_size == 0) + abort(); + if (g_rec.reply[g_rec.reply_size - 1] != '\0' || memchr(g_rec.reply, '\0', g_rec.reply_size - 1)) + abort(); + /* the supervisor's answer, as it answered it */ + if (g_rec.answer == NULL || strcmp(g_rec.reply, g_rec.answer)) + abort(); + code = sup_reply_code(g_rec.reply); + if (code < 0) + { + snprintf(refusal, sizeof(refusal), "resp %d", code); + if (!rec_code_declared(code) || strcmp(g_rec.reply, refusal)) + abort(); + return; + } + shape = w->n ? rec_shape_of(w->w[0], strlen(w->w[0])) : REC_MOD_HOST_SHAPE; + if (!syntax_reply_matches(shape, g_rec.reply) && !syntax_reply_matches(REC_MOD_HOST_SHAPE, g_rec.reply)) + abort(); +} + +/* the arity property */ +__attribute__((noinline)) static void check_arity(const struct words *w) +{ + size_t n; + int read, min, max; + + if (w->n == 0 || !acted() || (g_rec.ncalls > 0 && g_rec.calls[0].fn == REC_BROADCAST)) + return; + read = w->n < PHD_LINE_WORDS_MAX ? w->n : PHD_LINE_WORDS_MAX; + for (n = 0; n < sizeof(g_parsed) / sizeof(g_parsed[0]); n++) + { + if (strcmp(w->w[0], g_parsed[n].name)) + continue; + if (g_parsed[n].format) + syntax_format_arity(g_parsed[n].syntax, &min, &max); + else + syntax_arity(g_parsed[n].syntax, &min, &max); + /* acted on with an argument count the declaration allows */ + if (read - 1 < min || read - 1 > max) + abort(); + return; + } +} + +/* the refusal property */ +__attribute__((noinline)) static void check_refusal(uint64_t before) +{ + if (g_rec.reply == NULL || sup_reply_code(g_rec.reply) >= 0) + return; + /* nothing changed: no accepted call, no follow-up, no quit */ + if (g_rec.state != before || g_quitting) + abort(); +} + +/* the round-trip property: the message has been sent, and the daemon acted on it */ +__attribute__((noinline)) static void check_round_trip(const struct words *w, uint64_t state_before, + uint64_t host_before) +{ + struct rec_call first[REC_CALLS_MAX]; + int nfirst, quit_first; + uint64_t state_first, host_first; + char *reply_first, *line; + size_t len; + + if (w->n == 0 || !acted()) + return; + nfirst = rec_calls_copy(first); + quit_first = g_quitting; + state_first = g_rec.state; + host_first = g_rec.host_settings; + reply_first = strdup(g_rec.reply); + if (!reply_first) + abort(); + + /* the same command again, from the state it was sent in, as its words joined by one space */ + g_rec.state = state_before; + g_rec.host_settings = host_before; + line = joined(w, &len); + send_message((const uint8_t *)line, len); + free(line); + + if (!rec_calls_equal(first, nfirst, g_rec.calls, g_rec.ncalls)) + abort(); + if (g_rec.reply == NULL || strcmp(g_rec.reply, reply_first)) + abort(); + if (g_quitting != quit_first || g_rec.state != state_first || g_rec.host_settings != host_first) + abort(); + rec_calls_free(first, nfirst); + free(reply_first); +} + +int LLVMFuzzerTestOneInput(const uint8_t *data, size_t size) +{ + struct fuzz_in in; + int m; + + if (size >= FUZZ_INPUT_MAX) + return 0; + in = fuzz_in(data, size); + rec_clear(); + g_rec.state = 0; + g_rec.host_settings = 0; + for (m = 0; m < MESSAGES_MAX && in.n; m++) + { + uint8_t flags = fuzz_u8(&in); + uint16_t want = fuzz_u16(&in); + const uint8_t *bytes; + size_t len = fuzz_take(&in, want < MESSAGE_MAX ? want : MESSAGE_MAX, &bytes); + uint64_t state_before = g_rec.state, host_before = g_rec.host_settings; + struct words w; + + g_rec.refuse = flags & 1; + g_rec.code = rec_codes[(flags >> 1) % rec_ncodes]; + send_message(bytes, len); + words_of((const char *)bytes, len, &w); + check_reply(&w); + check_arity(&w); + check_refusal(state_before); + check_round_trip(&w, state_before, host_before); + words_free(&w); + } + rec_clear(); + g_quitting = 0; + return 0; +} diff --git a/fuzz/fuzz_reply.c b/fuzz/fuzz_reply.c new file mode 100644 index 0000000..114e05f --- /dev/null +++ b/fuzz/fuzz_reply.c @@ -0,0 +1,682 @@ +/* SPDX-License-Identifier: GPL-3.0-or-later */ +/* + * This file is part of plugin-hostd. + * + * plugin-hostd is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * plugin-hostd is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with plugin-hostd. If not, see . + * + * Copyright (C) 2026 Pau Aliagas + */ + +/* libFuzzer harness: THE REPLY STREAM (src/relay.c), which reads what each worker writes on its feedback socket, cuts + * it into NUL-terminated lines and writes them to the controller, ending the gestures a worker or an instance left + * held. The relay is included whole, as tests/gesture_alloc_test.c does, with its socket calls answered here: recv + * hands it the bytes the input chose, cut where the input chose, and send collects what the controller would read. + * No socket is opened and no thread is started: the harness calls drain(), relay_remove(), relay_instance_gone() and + * relay_event() where the relay's thread and the supervisor would. + * + * The relay is built with LINE_LIMIT at 6000 bytes, so an input of a few kilobytes reaches the drop of a long line, + * and the buffer's growth past its first 8192 bytes. + * + * Input: a source (worker feedback socket) is open from the start; then up to OPS_MAX operations, each [op] and its + * arguments, op % 10: + * 0 a worker more (at most SOURCES_MAX at once) + * 1 s n ... [s] [u16 LE n] n bytes: what source s writes next, handed to one recv or more, never two writes + * to one recv + * 2 s the relay reads source s (its thread's drain on POLLIN) + * 3 s source s's worker closes its end: recv answers 0 once its bytes are read + * 4 s the supervisor removes source s (relay_remove): the worker died or was stopped + * 5 id instance id is removed (relay_instance_gone) + * 6 s the next recv of source s is interrupted (EINTR) + * 7 n ... [u16 LE n] n bytes: one event of the daemon's own (relay_event), up to its first NUL + * 8 k the controller's socket takes at most k bytes a send from now on (0: all of them), and its next + * send is interrupted (EINTR) + * 9 m the controller goes away for the rest of the input: m & 1, it closes its connection + * (relay_controller_gone); else its next send fails, as on a connection it reset + * s is taken modulo the open sources. + * + * The reference: the controller should read, for each source, every complete line (its bytes up to a NUL) the worker + * wrote that is LINE_LIMIT bytes or shorter, once each, in order (PHD_RELAY VERBATIM and ORDER); for each gesture a + * relayed "param_gesture begin" opened and no relayed end closed, one + * "param_gesture end" when its source is removed or its instance is (PHD_EVENTS param_gesture). + * Once the controller is gone it reads nothing more, and the relay still reads every worker (PHD_RELAY NO_CONTROLLER). + * + * Properties, each an abort(): + * - bounded: after every operation each source's buffer holds at most LINE_LIMIT bytes of an unfinished line, and + * its capacity stays under 2 * (LINE_LIMIT + 4096). + * Mutant: fuzz/mutants/reply-bounded.patch, the overflow check against the capacity instead of LINE_LIMIT. + * - no-loss: after every read the lines relayed for a source are exactly the reference's complete lines, none lost, + * none twice, none out of order, however the stream was cut and wherever the NULs fell. + * Mutant: fuzz/mutants/reply-no-loss.patch, a line of exactly LINE_LIMIT bytes dropped. + * - shape: every line the relay writes of its own is a declared feedback line: an event as the daemon wrote it, or + * the end of a gesture still held, once for each such gesture and for no other; and the controller's stream + * always ends on a whole line. + * Mutant: fuzz/mutants/reply-shape.patch, a held gesture ended with begin. + */ + +#include +#include +#include +#include +#include +#include +#include +#include + +#include +#include +#include +#include +#include +#include +#include + +#include "fuzz.h" + +static ssize_t fuzz_recv(int fd, void *buf, size_t len, int flags); +static ssize_t fuzz_send(int fd, const void *buf, size_t len, int flags); +static int fuzz_shutdown(int fd, int how); +static int fuzz_close(int fd); +static int fuzz_fcntl(int fd, int cmd, ...); + +#define recv fuzz_recv +#define send fuzz_send +#define shutdown fuzz_shutdown +#define close fuzz_close +#define fcntl fuzz_fcntl +#define LINE_LIMIT 6000 +#include "../src/relay.c" +#undef recv +#undef send +#undef shutdown +#undef close +#undef fcntl + +#define SOURCES_MAX 4 +#define OPS_MAX 512 +#define CLIENT_FD 3 +#define FIRST_FD 100 + +/* a line the controller should read, or a gesture held: a copy of its bytes */ +struct text { + char *s; + size_t n; +}; + +struct list { + struct text *v; + int n, cap; +}; + +static void list_push(struct list *l, const char *s, size_t n) +{ + if (l->n == l->cap) + { + int cap = l->cap ? l->cap * 2 : 8; + struct text *v = realloc(l->v, (size_t)cap * sizeof(*v)); + + if (!v) + abort(); + l->v = v; + l->cap = cap; + } + l->v[l->n].s = malloc(n + 1); + if (!l->v[l->n].s) + abort(); + memcpy(l->v[l->n].s, s, n); + l->v[l->n].s[n] = '\0'; + l->v[l->n].n = n; + l->n++; +} + +static void list_drop(struct list *l, int i) +{ + free(l->v[i].s); + memmove(&l->v[i], &l->v[i + 1], (size_t)(l->n - i - 1) * sizeof(l->v[0])); + l->n--; +} + +static void list_free(struct list *l) +{ + while (l->n) + list_drop(l, l->n - 1); + free(l->v); + l->v = NULL; + l->cap = 0; +} + +static int list_find(const struct list *l, const char *s, size_t n) +{ + int i; + + for (i = 0; i < l->n; i++) + if (l->v[i].n == n && !memcmp(l->v[i].s, s, n)) + return i; + return -1; +} + +/* one worker's feedback socket, as the harness writes it and as the reference reads it */ +struct source { + int fd; + int open; /* in the relay's table */ + int closed; /* the worker closed its end */ + int eintr; /* the next recv is interrupted */ + struct list pending; /* what the worker wrote and recv has not handed over, one entry per write */ + size_t pending_off; /* how much of the first entry recv has handed over */ + /* the reference splitter over the bytes recv handed over */ + char *partial; + size_t npartial; + int overlong; + struct list expected; /* complete lines the controller should read and has not */ + struct list held; /* gestures the relayed lines opened and did not end, each its line up to the last word */ +}; + +static struct source g_s[SOURCES_MAX]; +static int g_nfd; + +/* what the controller read during one operation */ +static char *g_out; +static size_t g_nout, g_capout; +/* how the controller's socket takes a send: at most g_send_max bytes (0: all), the next one interrupted, the next one + * failed; and whether the controller is gone */ +static size_t g_send_max; +static int g_send_eintr, g_send_dead, g_gone; + +static struct source *source_of(int fd) +{ + int i; + + for (i = 0; i < SOURCES_MAX; i++) + if (g_s[i].open && g_s[i].fd == fd) + return &g_s[i]; + return NULL; +} + +/* the reference splitter: a line ends at a NUL, and one longer than LINE_LIMIT is not a line of the protocol */ +static void reference_feed(struct source *s, const char *p, size_t n) +{ + size_t i; + + for (i = 0; i < n; i++) + { + if (p[i] == '\0') + { + if (!s->overlong) + list_push(&s->expected, s->partial, s->npartial); + s->npartial = 0; + s->overlong = 0; + } + else if (!s->overlong) + { + if (s->npartial == LINE_LIMIT) + s->overlong = 1; + else + s->partial[s->npartial++] = p[i]; + } + } +} + +static ssize_t fuzz_recv(int fd, void *buf, size_t len, int flags) +{ + struct source *s = source_of(fd); + size_t n; + + (void)flags; + if (s == NULL) + { + errno = EBADF; + return -1; + } + if (s->eintr) + { + s->eintr = 0; + errno = EINTR; + return -1; + } + if (s->pending.n == 0) + { + if (s->closed) + return 0; + errno = EAGAIN; + return -1; + } + n = s->pending.v[0].n - s->pending_off; + if (n > len) + n = len; + memcpy(buf, s->pending.v[0].s + s->pending_off, n); + reference_feed(s, s->pending.v[0].s + s->pending_off, n); + s->pending_off += n; + if (s->pending_off == s->pending.v[0].n) + { + list_drop(&s->pending, 0); + s->pending_off = 0; + } + return (ssize_t)n; +} + +static ssize_t fuzz_send(int fd, const void *buf, size_t len, int flags) +{ + (void)flags; + if (fd != CLIENT_FD) + abort(); + if (g_send_eintr) + { + g_send_eintr = 0; + errno = EINTR; + return -1; + } + if (g_send_dead) + { + errno = EPIPE; + return -1; + } + if (g_send_max && len > g_send_max) + len = g_send_max; + if (g_nout + len > g_capout) + { + size_t cap = (g_nout + len) * 2; + char *out = realloc(g_out, cap); + + if (!out) + abort(); + g_out = out; + g_capout = cap; + } + memcpy(g_out + g_nout, buf, len); + g_nout += len; + return (ssize_t)len; +} + +static int fuzz_shutdown(int fd, int how) +{ + (void)fd; + (void)how; + return 0; +} + +static int fuzz_close(int fd) +{ + (void)fd; + return 0; +} + +static int fuzz_fcntl(int fd, int cmd, ...) +{ + (void)fd; + (void)cmd; + return 0; +} + +/* ---------------------------------------------------------------- the properties */ + +/* the bounded property, over every source in the relay's table */ +__attribute__((noinline)) static void check_bounded(void) +{ + int i; + + for (i = 0; i < g_nsrc; i++) + { + /* an unfinished line the relay keeps is never past LINE_LIMIT, and what holds it does not grow without end */ + if (g_src[i].used > LINE_LIMIT || g_src[i].used > g_src[i].cap) + abort(); + if (g_src[i].cap >= 2 * ((size_t)LINE_LIMIT + 4096)) + abort(); + } +} + +/* the controller's stream of one operation, cut into lines: *at is where the next one starts */ +static int next_line(size_t *at, const char **line, size_t *len) +{ + const char *nul; + + if (*at == g_nout) + return 0; + nul = memchr(g_out + *at, '\0', g_nout - *at); + /* the shape property: the stream ends on a whole line */ + if (nul == NULL) + abort(); + *line = g_out + *at; + *len = (size_t)(nul - *line); + *at += *len + 1; + return 1; +} + +/* a relayed line that opens or ends a gesture, read as PHD_EVENTS declares param_gesture */ +static void gesture_model(struct source *s, const char *line, size_t len) +{ + size_t verb = strlen(PHD_EVENT_PARAM_GESTURE), last; + const char *word; + + if (len <= verb || memcmp(line, PHD_EVENT_PARAM_GESTURE, verb) || line[verb] != ' ') + return; + for (last = len; last > 0 && line[last - 1] != ' '; last--) + ; + if (last == 0 || last - 1 == verb) + return; + word = line + last; + if (len - last == strlen(PHD_GESTURE_BEGIN) && !memcmp(word, PHD_GESTURE_BEGIN, len - last)) + { + if (list_find(&s->held, line, last - 1) < 0) + list_push(&s->held, line, last - 1); + } + else if (len - last == strlen(PHD_GESTURE_END) && !memcmp(word, PHD_GESTURE_END, len - last)) + { + int i = list_find(&s->held, line, last - 1); + + if (i >= 0) + list_drop(&s->held, i); + } +} + +/* the no-loss property for one source: the next `n` lines of the stream are its expected lines, in order, and then + * none is expected any more */ +__attribute__((noinline)) static void check_no_loss(struct source *s, size_t *at) +{ + const char *line; + size_t len; + + while (s->expected.n > 0) + { + if (g_gone) + { + /* read and dropped: its gestures are still followed */ + gesture_model(s, s->expected.v[0].s, s->expected.v[0].n); + list_drop(&s->expected, 0); + continue; + } + /* every complete line it wrote is read by the controller, as it wrote it */ + if (!next_line(at, &line, &len)) + abort(); + if (len != s->expected.v[0].n || memcmp(line, s->expected.v[0].s, len)) + abort(); + gesture_model(s, line, len); + list_drop(&s->expected, 0); + } +} + +static int has_prefix(const char *s, size_t n, const char *prefix) +{ + size_t p = strlen(prefix); + + return n >= p && !memcmp(s, prefix, p); +} + +/* the shape property for the gesture ends the relay writes for a source: one " end" for each held gesture + * `prefix` selects, and no other line */ +__attribute__((noinline)) static void check_shape_ends(struct source *s, const char *prefix, size_t *at) +{ + const char *line; + size_t len, end = strlen(" " PHD_GESTURE_END); + int want = 0, i; + + for (i = 0; i < s->held.n; i++) + if (has_prefix(s->held.v[i].s, s->held.v[i].n, prefix)) + want++; + if (g_gone) + { + /* ended where no controller reads them */ + for (i = s->held.n - 1; i >= 0; i--) + if (has_prefix(s->held.v[i].s, s->held.v[i].n, prefix)) + list_drop(&s->held, i); + return; + } + while (want-- > 0) + { + /* the end of a gesture still held, and of each one only once */ + if (!next_line(at, &line, &len)) + abort(); + if (len < end || memcmp(line + len - end, " " PHD_GESTURE_END, end)) + abort(); + i = list_find(&s->held, line, len - end); + if (i < 0 || !has_prefix(line, len, prefix)) + abort(); + list_drop(&s->held, i); + } +} + +/* the whole stream of an operation was read: nothing is left over */ +__attribute__((noinline)) static void check_shape_done(size_t at) +{ + /* no line the reference does not account for */ + if (at != g_nout) + abort(); +} + +/* the shape property for an event of the daemon's own: the controller reads it as it was written */ +__attribute__((noinline)) static void check_shape_event(const char *event) +{ + size_t at = 0, len; + const char *line; + + if (g_gone) + { + check_shape_done(at); + return; + } + if (!next_line(&at, &line, &len)) + abort(); + if (len != strlen(event) || memcmp(line, event, len)) + abort(); + check_shape_done(at); +} + +/* ---------------------------------------------------------------- the operations */ + +static void source_reset(struct source *s) +{ + list_free(&s->pending); + list_free(&s->expected); + list_free(&s->held); + free(s->partial); + memset(s, 0, sizeof(*s)); +} + +static int open_count(void) +{ + int i, n = 0; + + for (i = 0; i < SOURCES_MAX; i++) + n += g_s[i].open; + return n; +} + +/* the k-th open source, k taken modulo the open ones */ +static struct source *pick(uint8_t k) +{ + int n = open_count(), i; + + if (n == 0) + return NULL; + k %= n; + for (i = 0; i < SOURCES_MAX; i++) + if (g_s[i].open && k-- == 0) + return &g_s[i]; + return NULL; +} + +static source_t *relay_source(int fd) +{ + int i; + + for (i = 0; i < g_nsrc; i++) + if (g_src[i].fd == fd) + return &g_src[i]; + return NULL; +} + +static void op_add(void) +{ + int i; + + for (i = 0; i < SOURCES_MAX; i++) + if (!g_s[i].open) + { + source_reset(&g_s[i]); + g_s[i].fd = FIRST_FD + g_nfd++; + g_s[i].partial = malloc(LINE_LIMIT); + if (!g_s[i].partial) + abort(); + g_s[i].open = 1; + if (relay_add(g_s[i].fd) != 0) + abort(); + return; + } +} + +static void op_drain(struct source *s) +{ + source_t *r = relay_source(s->fd); + size_t at = 0; + + if (r == NULL || r->eof) + return; + pthread_mutex_lock(&g_lock); + drain(r); + pthread_mutex_unlock(&g_lock); + check_no_loss(s, &at); + check_shape_done(at); +} + +static void op_remove(struct source *s) +{ + size_t at = 0; + + relay_remove(s->fd); + check_no_loss(s, &at); + check_shape_ends(s, "", &at); + check_shape_done(at); + source_reset(s); +} + +static void op_gone(int id) +{ + char prefix[48]; + size_t at = 0; + int i, k; + + snprintf(prefix, sizeof(prefix), "%s %d ", PHD_EVENT_PARAM_GESTURE, id); + relay_instance_gone(id); + /* the relay walks its table in order: each source's lines, then the ends of its gestures of that instance */ + for (i = 0; i < g_nsrc; i++) + for (k = 0; k < SOURCES_MAX; k++) + if (g_s[k].open && g_s[k].fd == g_src[i].fd) + { + check_no_loss(&g_s[k], &at); + check_shape_ends(&g_s[k], prefix, &at); + } + check_shape_done(at); +} + +static void op_event(const uint8_t *p, size_t n) +{ + char *event = malloc(n + 1); + + if (!event) + abort(); + memcpy(event, p, n); + event[n] = '\0'; + relay_event(event); + check_shape_event(event); + free(event); +} + +int LLVMFuzzerTestOneInput(const uint8_t *data, size_t size) +{ + struct fuzz_in in; + int ops, i; + + if (size >= FUZZ_INPUT_MAX) + return 0; + in = fuzz_in(data, size); + g_client = CLIENT_FD; + g_client_broken = 0; + g_nfd = 0; + g_send_max = 0; + g_send_eintr = g_send_dead = g_gone = 0; + op_add(); + for (ops = 0; ops < OPS_MAX && in.n; ops++) + { + uint8_t op = fuzz_u8(&in) % 10; + struct source *s = NULL; + const uint8_t *p; + size_t n; + + g_nout = 0; + if (op == 1 || op == 2 || op == 3 || op == 4 || op == 6) + { + s = pick(fuzz_u8(&in)); + if (s == NULL && op != 1) + continue; + } + switch (op) + { + case 0: + op_add(); + break; + case 1: + n = fuzz_take(&in, fuzz_u16(&in), &p); + if (s && n) + list_push(&s->pending, (const char *)p, n); + break; + case 2: + op_drain(s); + break; + case 3: + s->closed = 1; + break; + case 4: + op_remove(s); + break; + case 5: + op_gone(fuzz_u8(&in)); + break; + case 6: + s->eintr = 1; + break; + case 7: + n = fuzz_take(&in, fuzz_u16(&in), &p); + op_event(p, strnlen((const char *)p, n)); + break; + case 8: + g_send_max = fuzz_u8(&in); + g_send_eintr = 1; + break; + default: + if (fuzz_u8(&in) & 1) + relay_controller_gone(); + else + g_send_dead = 1; + g_gone = 1; + break; + } + check_bounded(); + } + /* the daemon stops: every source left is closed and every gesture still held is ended, in the relay's table order */ + { + int order[SOURCES_MAX], norder = 0, k; + size_t at = 0; + + for (i = 0; i < g_nsrc && norder < SOURCES_MAX; i++) + order[norder++] = g_src[i].fd; + g_nout = 0; + relay_finish(); + for (i = 0; i < norder; i++) + for (k = 0; k < SOURCES_MAX; k++) + if (g_s[k].open && g_s[k].fd == order[i]) + check_shape_ends(&g_s[k], "", &at); + check_shape_done(at); + } + for (i = 0; i < SOURCES_MAX; i++) + source_reset(&g_s[i]); + return 0; +} diff --git a/fuzz/gen_corpus.py b/fuzz/gen_corpus.py new file mode 100644 index 0000000..c82ef1d --- /dev/null +++ b/fuzz/gen_corpus.py @@ -0,0 +1,235 @@ +#!/usr/bin/env python3 +# SPDX-License-Identifier: GPL-3.0-or-later +# Copyright (C) 2026 Pau Aliagas +# +# The seed corpus of the two fuzz harnesses (make fuzz-corpus), written from what the repository declares: +# protocol/plugin-hostd.json and protocol/mod-host.json for the command harness, every verb with sample arguments, and +# the send lines of README.md and docs/own-host.md; and, for the reply harness, worker feedback cut every way the +# reader has a branch for. Nothing is fetched and nothing is random: the same tree writes the same files. +# +# Usage: gen_corpus.py (writes /command/ and /reply/) +import json, os, re, shutil, sys + +HERE = os.path.dirname(os.path.abspath(__file__)) +ROOT = os.path.dirname(HERE) + +# ---------------------------------------------------------------- the command harness: [flags] [u16 LE len] [bytes] + +SAMPLE = { + "uri": "urn:fuzz:plugin", "instance": "0", "chain": "1", "name": "p", "max_instances": "4", "format": "clap", + "cpu-list": "0-1", "nice": "-", "path": "lib/fuzz.so", "sha256": "0" * 64, "scheme": "omx-layout/1", + "verdict": "isolated", "worker": "w1", "page": "0", "symbol": "gain", "index": "0", "channel": "0", "key": "60", + "velocity": "100", "#RRGGBB": "#336699", "client_name": "fuzz_client", +} +MOD_HOST_SAMPLE = {"%i": "0", "%s": "x", "%f": "0.5"} + + +def items(syntax): + """the items of a declared syntax: runs with no space outside <...> and [...]""" + out, cur, depth = [], "", 0 + for c in syntax: + if c == " " and depth == 0: + if cur: + out.append(cur) + cur = "" + continue + if c in "<[": + depth += 1 + elif c in ">]" and depth: + depth -= 1 + cur += c + if cur: + out.append(cur) + return out + + +def placeholder(m): + """a sample for ; of the sample for a, or a itself when it is a word of the protocol""" + alternatives = [a.strip() for a in m.group(1).split("|")] + if alternatives[0] in SAMPLE: + return SAMPLE[alternatives[0]] + return alternatives[0] if len(alternatives) > 1 else "x" + + +def word(item, full): + """one sample word for an item of a syntax""" + item = re.sub(r"\[([^\[\]]*)\]", (lambda m: m.group(1).replace("...", "")) if full else "", item) + return re.sub(r"<([^<>]*)>", placeholder, item) + + +def sample(syntax, full): + """sample arguments for a declared syntax: every required word, and with `full` every optional one""" + words, its = [], items(syntax) + i = 0 + while i < len(its): + it = its[i] + i += 1 + if it == "|": + i += 1 + continue + if it.startswith("[") and it.endswith("]"): + if full: + inner = items(it[1:-1]) + words.append(word(inner[0], full)) + continue + words.append(word(it, full)) + return words + + +def message(text, flags=0): + b = text.encode() if isinstance(text, str) else text + return bytes([flags]) + len(b).to_bytes(2, "little") + b + + +def command_corpus(out): + seeds = {} + phd = json.load(open(os.path.join(ROOT, "protocol", "plugin-hostd.json"))) + mod = json.load(open(os.path.join(ROOT, "protocol", "mod-host.json"))) + lines = [] + for v in phd["verbs"] + phd["instance_verbs"]: + lines.append(("plugin-hostd-" + v["name"], [v["name"]] + sample(v["arguments"], False), + [v["name"]] + sample(v["arguments"], True))) + for c in mod["commands"]: + args = [MOD_HOST_SAMPLE.get(a, "x") for a in c["args"]] + lines.append(("mod-host-" + c["verb"], [c["verb"]] + args, [c["verb"]] + args + (["tail"] if c["optional_tail"] else []))) + for name, short, full in lines: + seeds[name] = message(" ".join(short)) + if full != short: + seeds[name + "-full"] = message(" ".join(full)) + # one word too many, the supervisor refusing, and the words split by tabs + seeds[name + "-extra"] = message(" ".join(full + ["extra"])) + seeds[name + "-refused"] = message(" ".join(short), flags=1 | (2 << 1)) + seeds[name + "-tabs"] = message("\t".join(short) + "\r\n") + # the placements of add, one by one + for p in ("own", "default", "pool:p", "pool:p fuzz_client", "fuzz_client", "own fuzz_client extra"): + seeds["add-" + re.sub(r"[^a-z_]", "-", p)] = message("add urn:fuzz:plugin 0 " + p) + seeds["add-instance-past-the-table"] = message("add urn:fuzz:plugin 10000") + seeds["remove-all"] = message("remove -1") + seeds["chain-of-many"] = message("chain 1 " + " ".join(str(i) for i in range(70))) + seeds["chain-not-a-number"] = message("chain 1 0 x") + seeds["chain-id-not-a-number"] = message("chain x 0") + seeds["add-too-few"] = message("add urn:fuzz:plugin") + seeds["pin-set-long"] = message("pin_set urn:fuzz:plugin lib/fuzz.so=%s %s:%s%s" % + ("0" * 64, "omx-layout/1", "0" * 64, " " * 4100)) + seeds["pin-set-layout-unknown"] = message("pin_set urn:fuzz:plugin lib/fuzz.so=%s -" % ("0" * 64)) + seeds["pin-set-bad-verdict-key"] = message("pin_set urn:fuzz:plugin lib/fuzz.so=%s omx-layout/1:%s isolated" % + ("0" * 64, "0" * 64)) + seeds["pin-clear-refused"] = message("pin_clear all", flags=1) + seeds["empty"] = message("") + seeds["only-spaces"] = message(" \r\n") + seeds["nul-inside"] = message(b"remove 0\0remove 1") + seeds["trailing-cr-tab"] = message("param_set 0 gain 6\r\t") + seeds["leading-space"] = message(" param_set 0 gain 6") + seeds["many-words"] = message("param_set 0 " + " ".join("w%d" % i for i in range(80))) + seeds["quit-refused-then-quit"] = message("quit now") + message("quit") + # the send lines of the README and of the own-host page, each alone and as one session + for doc, tag in (("README.md", "readme"), (os.path.join("docs", "own-host.md"), "own-host")): + sends = re.findall(r'^\s*send "([^"]*)"', open(os.path.join(ROOT, doc)).read(), re.M) + for n, line in enumerate(sends): + seeds["%s-%d" % (tag, n + 1)] = message(line) + seeds[tag + "-session"] = b"".join(message(line) for line in sends) + write(out, seeds) + + +# ---------------------------------------------------------------- the reply harness: operations, fuzz/fuzz_reply.c + +def line_limit(): + src = open(os.path.join(HERE, "fuzz_reply.c")).read() + return int(re.search(r"^#define LINE_LIMIT (\d+)$", src, re.M).group(1)) + + +def add(): + return b"\x00" + + +def write_(s, data): + return b"\x01" + bytes([s]) + len(data).to_bytes(2, "little") + data + + +def drain(s): + return b"\x02" + bytes([s]) + + +def closed(s): + return b"\x03" + bytes([s]) + + +def remove(s): + return b"\x04" + bytes([s]) + + +def gone(i): + return b"\x05" + bytes([i]) + + +def eintr(s): + return b"\x06" + bytes([s]) + + +def event(text): + return b"\x07" + len(text).to_bytes(2, "little") + text + + +def short_sends(k): + return b"\x08" + bytes([k]) + + +def controller_gone(closes): + return b"\x09" + bytes([1 if closes else 0]) + + +def reply_corpus(out): + limit = line_limit() + feedback = b"param_set 0 gain 0.5000\0output_set 0 out 0.2500\0data_finish\0" + begin = b"param_gesture 1 gain begin\0" + seeds = { + "lines": write_(0, feedback) + drain(0), + "split-at-every-byte": b"".join(write_(0, feedback[i:i + 1]) for i in range(len(feedback))) + drain(0), + "split-before-every-nul": b"".join(write_(0, part + b"\0") for part in feedback.split(b"\0")[:-1]) + drain(0), + "truncated-tail": write_(0, feedback + b"param_set 0 ga") + drain(0) + closed(0) + drain(0) + remove(0), + "interrupted": eintr(0) + write_(0, feedback) + drain(0), + "gesture-held-then-removed": write_(0, begin) + drain(0) + remove(0), + "gesture-ended-by-its-worker": write_(0, begin + b"param_gesture 1 gain end\0") + drain(0) + remove(0), + "gesture-begun-twice": write_(0, begin + begin) + drain(0) + remove(0), + "gesture-not-a-gesture": write_(0, b"param_gesture begin\0param_gesture_x 1 a begin\0param_gesture 1 a b\0") + + drain(0) + remove(0), + "gesture-of-a-removed-instance": add() + write_(0, begin + b"param_gesture 2 gain begin\0") + drain(0) + + write_(1, b"param_gesture 1 freq begin\0") + drain(1) + gone(1) + remove(0), + "gesture-held-at-the-end": write_(0, begin) + drain(0), + "gesture-long": write_(0, b"param_gesture 1 " + b"s" * 2000 + b" begin\0") + drain(0) + remove(0), + "line-at-the-limit": write_(0, b"x" * limit + b"\0") + drain(0), + "line-past-the-limit": write_(0, b"x" * (limit + 1) + b"\0" + feedback) + drain(0), + "line-past-the-limit-in-parts": write_(0, b"x" * (limit + 1000)) + drain(0) + write_(0, b"y" * 10 + b"\0") + + write_(0, feedback) + drain(0), + "buffer-grows": write_(0, b"x" * 5000) + drain(0) + write_(0, b"x" * 500 + b"\0" + feedback) + drain(0), + "worker-closed": write_(0, feedback) + closed(0) + drain(0) + remove(0), + "two-workers-interleaved": add() + write_(0, feedback[:10]) + write_(1, feedback) + drain(1) + + write_(0, feedback[10:]) + drain(0) + remove(1) + remove(0), + "workers-past-the-table": add() * 6 + write_(3, feedback) + drain(3), + "event": event(b"worker_died w1 1234 signal:9 0"), + "event-cut-at-nul": event(b"instance_restored 0 w1\0junk"), + "controller-takes-a-few-bytes-a-send": short_sends(3) + write_(0, feedback + begin) + drain(0) + remove(0), + "controller-closes": write_(0, begin) + drain(0) + controller_gone(True) + write_(0, feedback) + drain(0) + + event(b"worker_died w1 1 exit:0 0") + remove(0), + "controller-connection-reset": controller_gone(False) + write_(0, feedback + begin) + drain(0) + gone(1), + } + write(out, seeds) + + +def write(out, seeds): + if os.path.isdir(out): + shutil.rmtree(out) + os.makedirs(out) + for name, data in sorted(seeds.items()): + with open(os.path.join(out, name), "wb") as f: + f.write(data) + + +def main(): + if len(sys.argv) != 2: + sys.exit("usage: gen_corpus.py ") + command_corpus(os.path.join(sys.argv[1], "command")) + reply_corpus(os.path.join(sys.argv[1], "reply")) + + +main() diff --git a/fuzz/mutants/command-arity.patch b/fuzz/mutants/command-arity.patch new file mode 100644 index 0000000..fb2c79f --- /dev/null +++ b/fuzz/mutants/command-arity.patch @@ -0,0 +1,11 @@ +diff --git a/src/verbs.c b/src/verbs.c +index 78daab7..e940605 100644 +--- a/src/verbs.c ++++ b/src/verbs.c +@@ -171,5 +171,5 @@ static char *handle(char *line, char **tok, int ntok) + return ntok == 3 ? sup_lv2_worker_set(tok[1], tok[2]) : sup_resp(ERR_INVALID_OPERATION); + if (!strcmp(verb, PHD_VERB_POOL_CONFIG)) +- return ntok == 3 ? sup_pool_config(tok[1], atoi(tok[2])) : sup_resp(ERR_INVALID_OPERATION); ++ return ntok >= 3 ? sup_pool_config(tok[1], atoi(tok[2])) : sup_resp(ERR_INVALID_OPERATION); + if (!strcmp(verb, PHD_VERB_WORKER_ENV)) + return ntok == 4 ? sup_worker_env(tok[1], tok[2], tok[3]) : sup_resp(ERR_INVALID_OPERATION); diff --git a/fuzz/mutants/command-refusal.patch b/fuzz/mutants/command-refusal.patch new file mode 100644 index 0000000..11f7c3c --- /dev/null +++ b/fuzz/mutants/command-refusal.patch @@ -0,0 +1,11 @@ +diff --git a/src/verbs.c b/src/verbs.c +index 78daab7..066bcf8 100644 +--- a/src/verbs.c ++++ b/src/verbs.c +@@ -142,5 +142,5 @@ static char *handle(char *line, char **tok, int ntok) + reply_text = sup_add(tok[1], atoi(tok[2]), placement, client); + /* the plugin is running: its files are scanned after the reply, never before it */ +- if (reply_text && sup_reply_code(reply_text) >= 0) ++ if (reply_text) + descriptors_add(tok[1]); + return reply_text; diff --git a/fuzz/mutants/command-reply.patch b/fuzz/mutants/command-reply.patch new file mode 100644 index 0000000..e8d6825 --- /dev/null +++ b/fuzz/mutants/command-reply.patch @@ -0,0 +1,11 @@ +diff --git a/src/verbs.c b/src/verbs.c +index 78daab7..6b8af56 100644 +--- a/src/verbs.c ++++ b/src/verbs.c +@@ -118,5 +118,5 @@ static char *chain_words(char **tok, int ntok) + static void reply(int fd, char *text) + { +- socket_send(fd, text, strlen(text) + 1); ++ socket_send(fd, text, strlen(text)); + free(text); + } diff --git a/fuzz/mutants/command-round-trip.patch b/fuzz/mutants/command-round-trip.patch new file mode 100644 index 0000000..85ff376 --- /dev/null +++ b/fuzz/mutants/command-round-trip.patch @@ -0,0 +1,11 @@ +diff --git a/src/verbs.c b/src/verbs.c +index 78daab7..bf45b47 100644 +--- a/src/verbs.c ++++ b/src/verbs.c +@@ -222,5 +222,5 @@ void verbs_receive(msg_t *msg) + *--end = '\0'; + copy = strdup(line); +- for (t = strtok_r(copy, " \t", &save); t && ntok < MAX_TOKENS; t = strtok_r(NULL, " \t", &save)) ++ for (t = strtok_r(copy, " ", &save); t && ntok < MAX_TOKENS; t = strtok_r(NULL, " \t", &save)) + tok[ntok++] = t; + if (ntok == 0) diff --git a/fuzz/mutants/reply-bounded.patch b/fuzz/mutants/reply-bounded.patch new file mode 100644 index 0000000..ebf5804 --- /dev/null +++ b/fuzz/mutants/reply-bounded.patch @@ -0,0 +1,11 @@ +diff --git a/src/relay.c b/src/relay.c +index ff852de..a6be5b0 100644 +--- a/src/relay.c ++++ b/src/relay.c +@@ -216,5 +216,5 @@ static void drain(source_t *s) + s->used -= start - s->buf; + memmove(s->buf, start, s->used); +- if (s->used > LINE_LIMIT) ++ if (s->used > s->cap) + { + s->used = 0; diff --git a/fuzz/mutants/reply-no-loss.patch b/fuzz/mutants/reply-no-loss.patch new file mode 100644 index 0000000..95dea0b --- /dev/null +++ b/fuzz/mutants/reply-no-loss.patch @@ -0,0 +1,11 @@ +diff --git a/src/relay.c b/src/relay.c +index ff852de..3bde252 100644 +--- a/src/relay.c ++++ b/src/relay.c +@@ -205,5 +205,5 @@ static void drain(source_t *s) + { + /* a line too long to relay is dropped whole: its end after a dropped start, or all of it at once */ +- if (s->skip || (size_t)(nul - start) > LINE_LIMIT) ++ if (s->skip || (size_t)(nul - start) >= LINE_LIMIT) + s->skip = 0; + else diff --git a/fuzz/mutants/reply-shape.patch b/fuzz/mutants/reply-shape.patch new file mode 100644 index 0000000..c7054b3 --- /dev/null +++ b/fuzz/mutants/reply-shape.patch @@ -0,0 +1,11 @@ +diff --git a/src/relay.c b/src/relay.c +index ff852de..1b4a79e 100644 +--- a/src/relay.c ++++ b/src/relay.c +@@ -126,5 +126,5 @@ static void gesture_end(const char *held) + { + send_line(held, strlen(held)); +- send_line(" " PHD_GESTURE_END, sizeof(" " PHD_GESTURE_END)); ++ send_line(" " PHD_GESTURE_BEGIN, sizeof(" " PHD_GESTURE_BEGIN)); + } + diff --git a/fuzz/recorder.c b/fuzz/recorder.c new file mode 100644 index 0000000..ef9a034 --- /dev/null +++ b/fuzz/recorder.c @@ -0,0 +1,564 @@ +/* SPDX-License-Identifier: GPL-3.0-or-later */ +/* + * This file is part of plugin-hostd. + * + * plugin-hostd is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * plugin-hostd is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with plugin-hostd. If not, see . + * + * Copyright (C) 2026 Pau Aliagas + */ + +/* The recorder that stands in for everything below src/verbs.c (fuzz/recorder.h). */ + +#include +#include +#include + +#include +#include +#include + +#include + +#include "descriptors.h" +#include "formats.h" +#include "pins.h" +#include "supervisor.h" + +#include "recorder.h" +#include "syntax.h" + +struct recorder g_rec; + +const int rec_codes[] = { + ERR_INSTANCE_INVALID, ERR_INSTANCE_ALREADY_EXISTS, ERR_INSTANCE_NON_EXISTS, ERR_INSTANCE_UNLICENSED, + ERR_HOST_INVALID_URI, ERR_HOST_INSTANTIATION, ERR_HOST_INVALID_PARAM_SYMBOL, ERR_HOST_INVALID_PRESET_URI, + ERR_HOST_CANT_LOAD_STATE, ERR_JACK_CLIENT_CREATION, ERR_JACK_CLIENT_ACTIVATION, ERR_JACK_CLIENT_DEACTIVATION, + ERR_JACK_PORT_REGISTER, ERR_JACK_PORT_CONNECTION, ERR_JACK_PORT_DISCONNECTION, ERR_JACK_VALUE_OUT_OF_RANGE, + ERR_ASSIGNMENT_ALREADY_EXISTS, ERR_ASSIGNMENT_INVALID_OP, ERR_ASSIGNMENT_LIST_FULL, ERR_ASSIGNMENT_FAILED, + ERR_ASSIGNMENT_UNUSED, ERR_CONTROL_CHAIN_UNAVAILABLE, ERR_ABLETON_LINK_UNAVAILABLE, ERR_HMI_UNAVAILABLE, + ERR_EXTERNAL_UI_UNAVAILABLE, ERR_MEMORY_ALLOCATION, ERR_INVALID_OPERATION, +#define X(id, meaning) PHD_ERR_##id, + PHD_ERRORS(X) +#undef X +}; +const int rec_ncodes = (int)(sizeof(rec_codes) / sizeof(rec_codes[0])); + +int rec_code_declared(int code) +{ + int n; + + for (n = 0; n < rec_ncodes; n++) + if (rec_codes[n] == code) + return 1; + return 0; +} + +/* the reply shapes the daemon's verbs declare (PHD_VERBS, PHD_INSTANCE_VERBS) */ +static const struct { const char *name, *reply; } g_shapes[] = { +#define X(id, name, arguments, reply, meaning) { name, reply }, + PHD_VERBS(X) +#undef X +#define X(id, name, arguments, reply, ledger, meaning) { name, reply }, + PHD_INSTANCE_VERBS(X) +#undef X +}; + +const char *rec_shape_of(const char *verb, size_t len) +{ + size_t n; + + for (n = 0; n < sizeof(g_shapes) / sizeof(g_shapes[0]); n++) + if (strlen(g_shapes[n].name) == len && !memcmp(g_shapes[n].name, verb, len)) + return g_shapes[n].reply; + return REC_MOD_HOST_SHAPE; +} + +static void *xmalloc(size_t n) +{ + void *p = malloc(n ? n : 1); + + if (p == NULL) + abort(); + return p; +} + +static char *xstrdup(const char *s) +{ + size_t n = strlen(s) + 1; + + return memcpy(xmalloc(n), s, n); +} + +char *rec_canonical(const char *text, size_t len) +{ + char *out = xmalloc(len + 1); + size_t i = 0, o = 0; + + while (i < len) + { + while (i < len && (text[i] == ' ' || text[i] == '\t')) + i++; + if (i == len) + break; + if (o) + out[o++] = ' '; + while (i < len && text[i] != ' ' && text[i] != '\t') + out[o++] = text[i++]; + } + out[o] = '\0'; + return out; +} + +void rec_clear(void) +{ + rec_calls_free(g_rec.calls, g_rec.ncalls); + g_rec.ncalls = 0; + free(g_rec.answer); + g_rec.answer = NULL; + free(g_rec.reply); + g_rec.reply = NULL; + g_rec.reply_size = 0; + g_rec.nreplies = 0; +} + +void rec_calls_free(struct rec_call *calls, int n) +{ + int i, a; + + for (i = 0; i < n; i++) + { + for (a = 0; a < calls[i].nargs; a++) + free(calls[i].args[a]); + calls[i].nargs = 0; + } +} + +int rec_calls_copy(struct rec_call *dst) +{ + int i, a; + + for (i = 0; i < g_rec.ncalls; i++) + { + dst[i].fn = g_rec.calls[i].fn; + dst[i].nargs = g_rec.calls[i].nargs; + for (a = 0; a < dst[i].nargs; a++) + dst[i].args[a] = xstrdup(g_rec.calls[i].args[a]); + } + return g_rec.ncalls; +} + +int rec_calls_equal(const struct rec_call *a, int na, const struct rec_call *b, int nb) +{ + int i, k; + + if (na != nb) + return 0; + for (i = 0; i < na; i++) + { + if (a[i].fn != b[i].fn || a[i].nargs != b[i].nargs) + return 0; + for (k = 0; k < a[i].nargs; k++) + if (strcmp(a[i].args[k], b[i].args[k])) + return 0; + } + return 1; +} + +/* FNV-1a, 64 bits */ +static uint64_t mix(uint64_t h, const void *p, size_t n) +{ + const unsigned char *b = p; + + if (h == 0) + h = 0xcbf29ce484222325ull; + while (n--) + { + h ^= *b++; + h *= 0x100000001b3ull; + } + return h; +} + +static uint64_t mix_call(uint64_t h, const struct rec_call *c) +{ + int a; + + h = mix(h, &c->fn, sizeof(c->fn)); + for (a = 0; a < c->nargs; a++) + h = mix(h, c->args[a], strlen(c->args[a]) + 1); + return h; +} + +/* a call, recorded with its arguments; returns it for the caller to fold into the state */ +static struct rec_call *record(int fn) +{ + static struct rec_call spill; + struct rec_call *c; + + if (g_rec.ncalls == REC_CALLS_MAX) + { + /* past the table: kept apart, so a runaway message still changes the state it would change */ + rec_calls_free(&spill, 1); + c = &spill; + } + else + c = &g_rec.calls[g_rec.ncalls++]; + c->fn = fn; + c->nargs = 0; + return c; +} + +static void arg_s(struct rec_call *c, const char *s) +{ + if (c->nargs < REC_ARGS_MAX) + c->args[c->nargs++] = xstrdup(s ? s : "(null)"); +} + +static void arg_line(struct rec_call *c, const char *line) +{ + if (c->nargs < REC_ARGS_MAX) + c->args[c->nargs++] = rec_canonical(line, strlen(line)); +} + +static void arg_i(struct rec_call *c, int v) +{ + char b[16]; + + snprintf(b, sizeof(b), "%d", v); + arg_s(c, b); +} + +static char *hand_back(const char *text) +{ + free(g_rec.answer); + g_rec.answer = xstrdup(text); + return xstrdup(text); +} + +/* the answer to a call that can refuse: the input's refusal, or the verb's declared reply, which changes the state + * when the call changes anything */ +static char *answer(struct rec_call *c, const char *shape, int changes) +{ + char buf[1024]; + + if (g_rec.refuse) + { + snprintf(buf, sizeof(buf), "resp %d", g_rec.code); + return hand_back(buf); + } + if (changes) + g_rec.state = mix_call(g_rec.state, c); + if (syntax_instantiate(shape, buf, sizeof(buf)) < 0) + abort(); + return hand_back(buf); +} + +static const char *daemon_shape(const char *verb) +{ + return rec_shape_of(verb, strlen(verb)); +} + +/* ---------------------------------------------------------------- the supervisor (src/supervisor.h) */ + +char *sup_resp(int code) +{ + char buf[32]; + + snprintf(buf, sizeof(buf), "resp %d", code); + return hand_back(buf); +} + +int sup_reply_code(const char *reply) +{ + return strncmp(reply, "resp ", 5) == 0 ? atoi(reply + 5) : -999; +} + +char *sup_add(const char *uri, int id, const char *placement, const char *client) +{ + struct rec_call *c = record(REC_ADD); + char buf[32]; + + arg_s(c, uri); + arg_i(c, id); + arg_s(c, placement ? placement : "-"); + arg_s(c, client ? client : "-"); + /* the supervisor takes an instance number of its table only, and answers it back (PHD_VERBS add) */ + if (id < 0 || id >= MAX_INSTANCE) + { + snprintf(buf, sizeof(buf), "resp %d", ERR_INSTANCE_INVALID); + return hand_back(buf); + } + if (g_rec.refuse) + return answer(c, NULL, 0); + g_rec.state = mix_call(g_rec.state, c); + snprintf(buf, sizeof(buf), "resp %d", id); + return hand_back(buf); +} + +char *sup_call(int id, const char *line) +{ + struct rec_call *c = record(REC_CALL); + + arg_i(c, id); + arg_line(c, line); + return answer(c, REC_MOD_HOST_SHAPE, 1); +} + +char *sup_remove(int id) +{ + struct rec_call *c = record(REC_REMOVE); + + arg_i(c, id); + return answer(c, REC_MOD_HOST_SHAPE, 1); +} + +char *sup_remove_all(void) +{ + return answer(record(REC_REMOVE_ALL), REC_MOD_HOST_SHAPE, 1); +} + +char *sup_connect(const char *verb, const char *line, const char *port_a, const char *port_b) +{ + struct rec_call *c = record(REC_CONNECT); + + arg_s(c, verb); + arg_line(c, line); + arg_s(c, port_a); + arg_s(c, port_b); + return answer(c, REC_MOD_HOST_SHAPE, 1); +} + +char *sup_broadcast(const char *line) +{ + struct rec_call *c = record(REC_BROADCAST); + + arg_line(c, line); + return answer(c, REC_MOD_HOST_SHAPE, 1); +} + +void sup_host_setting(const char *line, char **tok, int ntok) +{ + struct rec_call *c = record(REC_HOST_SETTING); + size_t len = 0; + char *words; + int n; + + arg_line(c, line); + arg_i(c, ntok); + for (n = 0; n < ntok; n++) + len += strlen(tok[n]) + 1; + words = xmalloc(len + 1); + words[0] = '\0'; + for (n = 0; n < ntok; n++) + { + if (n) + strcat(words, " "); + strcat(words, tok[n]); + } + arg_s(c, words); + free(words); + g_rec.host_settings = mix_call(g_rec.host_settings, c); +} + +char *sup_instance_verb(const char *line, int ledger) +{ + struct rec_call *c = record(REC_INSTANCE_VERB); + + arg_line(c, line); + arg_i(c, ledger); + /* the verb is the first word of the line, past any space before it */ + line += strspn(line, " \t"); + return answer(c, rec_shape_of(line, strcspn(line, " \t")), 1); +} + +char *sup_worker_list(void) +{ + return answer(record(REC_WORKER_LIST), daemon_shape(PHD_VERB_WORKER_LIST), 0); +} + +char *sup_instance_info(int id) +{ + struct rec_call *c = record(REC_INSTANCE_INFO); + + arg_i(c, id); + return answer(c, daemon_shape(PHD_VERB_INSTANCE_INFO), 0); +} + +char *sup_reset(const char *which) +{ + struct rec_call *c = record(REC_RESET); + + arg_s(c, which); + return answer(c, daemon_shape(PHD_VERB_SUPERVISOR_RESET), 1); +} + +char *sup_quarantine_clear(const char *which) +{ + struct rec_call *c = record(REC_QUARANTINE_CLEAR); + + arg_s(c, which); + return answer(c, daemon_shape(PHD_VERB_QUARANTINE_CLEAR), 1); +} + +char *sup_policy_set(const char *format, const char *place) +{ + struct rec_call *c = record(REC_POLICY_SET); + + arg_s(c, format); + arg_s(c, place); + return answer(c, daemon_shape(PHD_VERB_POLICY_SET), 1); +} + +char *sup_pin_accept(const char *uri) +{ + struct rec_call *c = record(REC_PIN_ACCEPT); + + arg_s(c, uri); + return answer(c, daemon_shape(PHD_VERB_PIN_ACCEPT), 1); +} + +char *sup_pool_config(const char *name, int max) +{ + struct rec_call *c = record(REC_POOL_CONFIG); + + arg_s(c, name); + arg_i(c, max); + return answer(c, daemon_shape(PHD_VERB_POOL_CONFIG), 1); +} + +char *sup_lv2_worker_set(const char *uri, const char *worker) +{ + struct rec_call *c = record(REC_LV2_WORKER_SET); + + arg_s(c, uri); + arg_s(c, worker); + return answer(c, daemon_shape(PHD_VERB_LV2_WORKER_SET), 1); +} + +char *sup_worker_env(const char *format, const char *cpus, const char *nice) +{ + struct rec_call *c = record(REC_WORKER_ENV); + + arg_s(c, format); + arg_s(c, cpus); + arg_s(c, nice); + return answer(c, daemon_shape(PHD_VERB_WORKER_ENV), 1); +} + +char *sup_chain(int id, const int *ids, int n) +{ + struct rec_call *c = record(REC_CHAIN); + char *list = xmalloc((size_t)n * 12 + 1); + size_t o = 0; + int k; + + arg_i(c, id); + arg_i(c, n); + list[0] = '\0'; + for (k = 0; k < n; k++) + o += (size_t)sprintf(list + o, k ? " %d" : "%d", ids[k]); + arg_s(c, list); + free(list); + return answer(c, daemon_shape(PHD_VERB_CHAIN), 1); +} + +char *sup_chain_clear(int id) +{ + struct rec_call *c = record(REC_CHAIN_CLEAR); + + arg_i(c, id); + return answer(c, daemon_shape(PHD_VERB_CHAIN_CLEAR), 1); +} + +/* a checkpoint of what a preset or patch changed: it writes down state, it changes none */ +void sup_after_reply(int id, const char *verb) +{ + struct rec_call *c = record(REC_AFTER_REPLY); + + arg_i(c, id); + arg_s(c, verb); +} + +/* ---------------------------------------------------------------- the pins (src/pins.h) */ + +int pins_set(const char *uri, const char *files, const char *layout, const char *verdict) +{ + struct rec_call *c = record(REC_PINS_SET); + + arg_s(c, uri); + arg_s(c, files); + arg_s(c, layout); + arg_s(c, verdict ? verdict : "-"); + if (g_rec.refuse) + return g_rec.code; + g_rec.state = mix_call(g_rec.state, c); + return SUCCESS; +} + +int pins_clear(const char *uri) +{ + struct rec_call *c = record(REC_PINS_CLEAR); + + arg_s(c, uri); + if (g_rec.refuse) + return g_rec.code; + g_rec.state = mix_call(g_rec.state, c); + return SUCCESS; +} + +/* ---------------------------------------------------------------- the descriptors and the LV2 refusals */ + +/* a scan of the plugin's files starts: a change whatever the add was answered */ +void descriptors_add(const char *uri) +{ + struct rec_call *c = record(REC_DESCRIPTORS_ADD); + + arg_s(c, uri); + g_rec.state = mix_call(g_rec.state, c); +} + +char *descriptors_get(const char *uri) +{ + struct rec_call *c = record(REC_DESCRIPTORS_GET); + + arg_s(c, uri); + return answer(c, daemon_shape(PHD_VERB_DESCRIPTOR_GET), 0); +} + +/* the adapter is asked again about what it refused: a change */ +void lv2_refusals_forget(const char *uri) +{ + struct rec_call *c = record(REC_LV2_REFUSALS_FORGET); + + arg_s(c, uri ? uri : "*"); + g_rec.state = mix_call(g_rec.state, c); +} + +/* ---------------------------------------------------------------- the command socket (mod-host's socket.h) */ + +int socket_send(int destination, const char *buffer, int size) +{ + (void)destination; + g_rec.nreplies++; + free(g_rec.reply); + g_rec.reply = NULL; + g_rec.reply_size = 0; + if (size > 0) + { + g_rec.reply = xmalloc((size_t)size); + memcpy(g_rec.reply, buffer, (size_t)size); + g_rec.reply_size = (size_t)size; + } + return size; +} diff --git a/fuzz/recorder.h b/fuzz/recorder.h new file mode 100644 index 0000000..bc27bb1 --- /dev/null +++ b/fuzz/recorder.h @@ -0,0 +1,95 @@ +/* SPDX-License-Identifier: GPL-3.0-or-later */ +/* + * This file is part of plugin-hostd. + * + * plugin-hostd is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * plugin-hostd is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with plugin-hostd. If not, see . + * + * Copyright (C) 2026 Pau Aliagas + */ + +/* The mod-host side of the daemon, replaced by a recorder for fuzz/fuzz_command.c: every function src/verbs.c calls + * below it (the supervisor, the pins, the descriptors, the LV2 refusals and the socket's send) is defined here. Each + * records the call it was handed, answers with the reply its verb declares, or with the refusal the input chose, and + * folds what an accepted call changes into a digest of the daemon's state. No socket, no process, no JACK, no file. */ + +#ifndef PLUGIN_HOSTD_FUZZ_RECORDER_H +#define PLUGIN_HOSTD_FUZZ_RECORDER_H + +#include +#include + +enum { + REC_ADD, REC_CALL, REC_REMOVE, REC_REMOVE_ALL, REC_CONNECT, REC_BROADCAST, REC_HOST_SETTING, REC_INSTANCE_VERB, + REC_WORKER_LIST, REC_INSTANCE_INFO, REC_RESET, REC_QUARANTINE_CLEAR, REC_POLICY_SET, REC_PIN_ACCEPT, + REC_POOL_CONFIG, REC_LV2_WORKER_SET, REC_WORKER_ENV, REC_CHAIN, REC_CHAIN_CLEAR, REC_AFTER_REPLY, REC_PINS_SET, + REC_PINS_CLEAR, REC_DESCRIPTORS_ADD, REC_DESCRIPTORS_GET, REC_LV2_REFUSALS_FORGET, + REC_FN_COUNT +}; + +#define REC_CALLS_MAX 16 +#define REC_ARGS_MAX 6 + +/* one call, its arguments as text: a number as %d, a line as its words joined by one space */ +struct rec_call { + int fn; + int nargs; + char *args[REC_ARGS_MAX]; +}; + +struct recorder { + /* chosen by the input for the message: refuse every call that can refuse, with this declared code */ + int refuse; + int code; + + /* what the message did */ + int ncalls; + struct rec_call calls[REC_CALLS_MAX]; + char *answer; /* a copy of the last reply text handed back to the daemon */ + int nreplies; /* socket_send calls */ + char *reply; /* the bytes of the last one */ + size_t reply_size; + + /* the daemon's state: a digest of every change an accepted call made */ + uint64_t state; + /* the host settings, which are kept as they were sent whatever the workers answer (PHD_HOST_SETTINGS) */ + uint64_t host_settings; +}; + +extern struct recorder g_rec; + +/* the declared refusal codes: mod-host's (host-errors.h) and the daemon's own (PHD_ERRORS) */ +extern const int rec_codes[]; +extern const int rec_ncodes; +int rec_code_declared(int code); + +/* mod-host answers a command it took "resp 0" */ +#define REC_MOD_HOST_SHAPE "resp 0" + +/* the declared reply shape of a verb of the daemon (PHD_VERBS, PHD_INSTANCE_VERBS), else mod-host's */ +const char *rec_shape_of(const char *verb, size_t len); + +/* forget what the last message did; the state stays */ +void rec_clear(void); + +/* whether two messages did the same: the same calls with the same arguments, in the same order */ +int rec_calls_equal(const struct rec_call *a, int na, const struct rec_call *b, int nb); + +/* a deep copy of the calls of the message, and its release */ +int rec_calls_copy(struct rec_call *dst); +void rec_calls_free(struct rec_call *calls, int n); + +/* the words of `text` up to `len` bytes, at spaces and tabs, joined by one space; malloc'd */ +char *rec_canonical(const char *text, size_t len); + +#endif diff --git a/fuzz/run.sh b/fuzz/run.sh new file mode 100755 index 0000000..bcba965 --- /dev/null +++ b/fuzz/run.sh @@ -0,0 +1,49 @@ +#!/bin/sh +# SPDX-License-Identifier: GPL-3.0-or-later +# Copyright (C) 2026 Pau Aliagas +# +# make fuzz-run: every harness fuzzed for FUZZ_TIME seconds from its seed corpus, one after the other. Red when any +# harness crashes, leaks, times out or runs out of memory; the input that did it is left in +# fuzz/findings/--, and the harness's whole log beside it as fuzz/findings/.log. +# +# New inputs the fuzzer finds go to a scratch corpus under the build directory, never into fuzz/corpus/, so a run +# leaves the tree as it found it. +# +# Usage: FUZZ_BUILD=build/fuzz FUZZ_TIME=60 run.sh ... +set -u +ROOT=$(cd "$(dirname "$0")/.." && pwd) +B=$ROOT/${FUZZ_BUILD:-build/fuzz} +T=${FUZZ_TIME:-60} +FINDINGS=$ROOT/fuzz/findings +mkdir -p "$FINDINGS" + +red= +for h in "$@"; do + bin=$B/fuzz_$h + work=$B/work/$h + rm -rf "$work" + mkdir -p "$work/new" + echo "== fuzz_$h: ${T}s from fuzz/corpus/$h ($(ls "$ROOT/fuzz/corpus/$h" | wc -l) seeds)" + # -jobs/-workers run the fuzzer as one child that logs to fuzz-0.log in the working directory; the parent exits + # non-zero when the child found anything + (cd "$work" && "$bin" -max_total_time="$T" -max_len=65536 -rss_limit_mb=2048 -timeout=25 -jobs=1 -workers=1 \ + -print_final_stats=1 -artifact_prefix="$FINDINGS/$h-" new "$ROOT/fuzz/corpus/$h") + rc=$? + log=$work/fuzz-0.log + if [ $rc -ne 0 ] || grep -qE '^SUMMARY: |ERROR: libFuzzer' "$log" 2>/dev/null; then + cp "$log" "$FINDINGS/$h.log" 2>/dev/null + sed -n '/ERROR: /,/^SUMMARY: /p' "$log" + grep -E 'Test unit written to' "$log" + echo "fuzz_$h: RED (exit $rc); input and log in fuzz/findings/" + red="$red $h" + else + grep -E '^#[0-9]+[[:space:]]+DONE|stat::number_of_executed_units' "$log" | tail -n 2 + echo "fuzz_$h: no crash in ${T}s" + fi +done + +if [ -n "$red" ]; then + echo "make fuzz-run: RED --$red" + exit 1 +fi +echo "make fuzz-run: every harness ran ${T}s without a crash" diff --git a/fuzz/sabotage.sh b/fuzz/sabotage.sh new file mode 100755 index 0000000..f4cd41d --- /dev/null +++ b/fuzz/sabotage.sh @@ -0,0 +1,98 @@ +#!/bin/sh +# SPDX-License-Identifier: GPL-3.0-or-later +# Copyright (C) 2026 Pau Aliagas +# +# make fuzz-sabotage: A PROPERTY NO MUTANT CAN BREAK CHECKS NOTHING. fuzz/mutants/-.patch is a +# one-line bug in the code the harness drives, one for each property the harness checks. This applies each, rebuilds +# the harness, and runs it over every seed of its corpus, one at a time. The mutant is found when at least one seed +# makes the harness abort and every seed that does aborts in check_ (or a check__), the +# function that holds that property's checks: the innermost check_ function of the stack is that one, and the crash +# is an abort(), not a sanitizer report. A seed that crashes anywhere else makes the mutant red, since the mutant then +# breaks a property it does not name. The source is restored with `git checkout --` on every exit path, and the +# sanitizer build is rebuilt as the tree is at the end. +# +# Usage: FUZZ_BUILD=build/fuzz MAKE=make sabotage.sh +set -u +ROOT=$(cd "$(dirname "$0")/.." && pwd) +cd "$ROOT" +B=${FUZZ_BUILD:-build/fuzz} +MAKE=${MAKE:-make} +OUT=$B/sabotage +mkdir -p "$OUT" + +patched= +restore() { + [ -n "$patched" ] && git checkout -- $patched + patched= +} +trap 'restore' EXIT +trap 'restore; exit 130' INT TERM + +if ! git diff --quiet -- src; then + echo "fuzz-sabotage: src/ has uncommitted changes; refusing to patch over them" >&2 + exit 2 +fi + +missed= +n=0 +for p in fuzz/mutants/*.patch; do + m=$(basename "$p" .patch) + h=${m%%-*} + prop=${m#*-} + fn=check_$(echo "$prop" | tr - _) + n=$((n + 1)) + files=$(git apply --numstat "$p" | awk '{ print $3 }') + if ! git apply "$p"; then + echo "$m: $p no longer applies" + missed="$missed $m" + continue + fi + patched=$files + echo "== $m: fuzz_$h against $(grep '^+[^+]' "$p" | sed 's/^+[[:space:]]*//')" + if ! $MAKE --no-print-directory "$B/fuzz_$h" >"$OUT/$m.build.log" 2>&1; then + cat "$OUT/$m.build.log" + echo "$m: the mutant does not build" + missed="$missed $m" + restore + continue + fi + found=0 + first= + other= + for seed in fuzz/corpus/$h/*; do + log=$OUT/$m.$(basename "$seed").log + "$B/fuzz_$h" -runs=1 -artifact_prefix="$OUT/$m-" "$seed" >"$log" 2>&1 && { rm -f "$log"; continue; } + # the innermost check_ function of the stack, and whether the harness aborted + where=$(grep -m1 -oE ' in check_[a-z_]+' "$log" | sed 's/^ in //') + if grep -q 'ERROR: libFuzzer: deadly signal' "$log" && echo "$where" | grep -qE "^$fn(_[a-z]+)*\$"; then + found=$((found + 1)) + [ -n "$first" ] || first=$(basename "$seed") + else + other="$other $(basename "$seed")(${where:-$(grep -m1 -E '^SUMMARY: ' "$log")})" + fi + done + restore + if [ "$found" -gt 0 ] && [ -z "$other" ]; then + echo "$m: found by $found seeds, first $first, every one in $fn" + elif [ -n "$other" ]; then + echo "$m: seeds that crashed outside $fn:$other" + echo "$m: RED -- the mutant breaks a property it does not name (logs in $OUT/)" + missed="$missed $m" + else + echo "$m: MUTANT NOT FOUND -- no seed breaks $fn" + missed="$missed $m" + fi +done + +# leave the sanitizer build as the tree is, not as the last mutant was +$MAKE --no-print-directory fuzz >"$OUT/rebuild.log" 2>&1 || { cat "$OUT/rebuild.log"; exit 1; } + +if [ "$n" -eq 0 ]; then + echo "make fuzz-sabotage: RED -- no mutant in fuzz/mutants/" + exit 1 +fi +if [ -n "$missed" ]; then + echo "make fuzz-sabotage: RED --$missed" + exit 1 +fi +echo "make fuzz-sabotage: all $n mutants found, each only by its own property" diff --git a/fuzz/syntax.h b/fuzz/syntax.h new file mode 100644 index 0000000..d9bd1c0 --- /dev/null +++ b/fuzz/syntax.h @@ -0,0 +1,178 @@ +/* SPDX-License-Identifier: GPL-3.0-or-later */ +/* + * This file is part of plugin-hostd. + * + * plugin-hostd is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * plugin-hostd is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with plugin-hostd. If not, see . + * + * Copyright (C) 2026 Pau Aliagas + */ + +/* The declared syntax of include/plugin-hostd/protocol.h read as words, the way tools/protocol-gen.c writes it into + * README.md and protocol/plugin-hostd.json: the arguments of a verb (" [own | pool: | default]"), + * the shape of its reply ("resp 0 "), and mod-host's command formats ("remove %i", "add %s %i ..."). + * + * An item of a syntax is a run of characters with no space outside <...> and [...]. "a | b" at the top is one word + * that is a or b; [...] is a word that may be left out, and one holding "..." may repeat without end. */ + +#ifndef PLUGIN_HOSTD_FUZZ_SYNTAX_H +#define PLUGIN_HOSTD_FUZZ_SYNTAX_H + +#include +#include +#include + +#define SYNTAX_UNBOUNDED INT_MAX + +/* the item that starts at or after *s, its length in *len; NULL when there is none */ +static inline const char *syntax_item(const char **s, size_t *len) +{ + const char *p = *s, *start; + int depth = 0; + + while (*p == ' ') + p++; + if (!*p) + return NULL; + start = p; + for (; *p && (depth > 0 || *p != ' '); p++) + { + if (*p == '<' || *p == '[') + depth++; + else if ((*p == '>' || *p == ']') && depth > 0) + depth--; + } + *len = (size_t)(p - start); + *s = p; + return start; +} + +static inline int syntax_has(const char *item, size_t len, const char *what) +{ + size_t n = strlen(what), i; + + for (i = 0; i + n <= len; i++) + if (!memcmp(item + i, what, n)) + return 1; + return 0; +} + +/* the fewest and the most words a declared argument syntax takes */ +static inline void syntax_arity(const char *syntax, int *min, int *max) +{ + const char *s = syntax, *item; + size_t len; + + *min = *max = 0; + while ((item = syntax_item(&s, &len))) + { + if (len == 1 && *item == '|') + { + /* the alternative of the word before it: no word of its own */ + syntax_item(&s, &len); + continue; + } + if (item[0] == '[' && item[len - 1] == ']') + { + if (syntax_has(item, len, "...")) + *max = SYNTAX_UNBOUNDED; + else if (*max != SYNTAX_UNBOUNDED) + (*max)++; + continue; + } + (*min)++; + if (*max != SYNTAX_UNBOUNDED) + (*max)++; + } +} + +/* the fewest and the most arguments of a mod-host command format, " %i %s ...": a %-word each, "..." for a tail + * of any length */ +static inline void syntax_format_arity(const char *format, int *min, int *max) +{ + const char *s = format, *item; + size_t len; + + *min = *max = 0; + syntax_item(&s, &len); + while ((item = syntax_item(&s, &len))) + { + if (len == 3 && !memcmp(item, "...", 3)) + *max = SYNTAX_UNBOUNDED; + else + { + (*min)++; + if (*max != SYNTAX_UNBOUNDED) + (*max)++; + } + } +} + +/* a reply of the declared shape: the template with every <...> written as 1, into buf; the length, or -1 when it does + * not fit */ +static inline int syntax_instantiate(const char *shape, char *buf, size_t size) +{ + size_t o = 0; + int depth = 0; + + for (; *shape; shape++) + { + char c = *shape; + + if (c == '<') + { + if (depth++ == 0) + c = '1'; + else + continue; + } + else if (c == '>' && depth > 0) + { + depth--; + continue; + } + else if (depth > 0) + continue; + if (o + 1 >= size) + return -1; + buf[o++] = c; + } + buf[o] = '\0'; + return (int)o; +} + +/* whether a reply has the declared shape: as many words as the shape has items, each literal item spelled the same, an + * item holding a <...> standing for any one word */ +static inline int syntax_reply_matches(const char *shape, const char *reply) +{ + const char *s = shape, *item; + size_t len; + + while ((item = syntax_item(&s, &len))) + { + size_t w = strcspn(reply, " "); + + if (w == 0) + return 0; + if (!syntax_has(item, len, "<") && (w != len || memcmp(item, reply, len))) + return 0; + reply += w; + if (*reply == ' ' && reply[1]) + reply++; + else if (*reply) + return 0; + } + return *reply == '\0'; +} + +#endif diff --git a/packaging/plugin-hostd.spec b/packaging/plugin-hostd.spec index 6f2a130..a190022 100644 --- a/packaging/plugin-hostd.spec +++ b/packaging/plugin-hostd.spec @@ -1,7 +1,7 @@ %bcond_without check Name: plugin-hostd -Version: 0.9.1 +Version: 0.9.2 Release: 1%{?dist} License: GPL-3.0-or-later Summary: Run LV2 and CLAP plugins in a live rig without one crash taking down the show @@ -13,7 +13,9 @@ BuildRequires: gcc BuildRequires: make BuildRequires: diffutils BuildRequires: python3 -BuildRequires: pkgconfig(mod-host-protocol) +# mod-host's protocol headers: param_info, remote_pages and track_info (mod-audio/mod-host#109) are in them from +# release 11 on, and protocol/mod-host.json is generated from them; a floor only +BuildRequires: mod-host-protocol-devel >= 0.10.6.f14a230-11 # the probe runner's default rates, the protocol's parameter scales and the probe output's shared words are read from # omx-contract's C headers, and the format tables are items of its schema 3 (3.0.0 is the first with the formatMap # kind, the extension bag and plugin.doc); a floor only @@ -114,6 +116,22 @@ make test-daemon check-generated %{_datadir}/omx-contract/contract.d/3/plugin-hostd/ %changelog +* Sat Oct 24 2026 Pau Aliagas - 0.9.2-1 +- worker_list, supervisor_reset and quit answer -902 to a word past + their grammar instead of acting on it: worker_list x no longer lists, + supervisor_reset all x no longer resets, and quit now no longer stops + the daemon. +- A tab at the end of a command is trimmed like a space: a command + ending in a carriage return and a tab no longer reaches the worker with + both. +- The end of a held gesture longer than 1024 bytes reaches the controller + whole, with its end word; before, it was cut short and the control stayed + held. +- The build needs mod-host's protocol headers of mod-host-protocol-devel + 0.10.6.f14a230-11 or newer (libmod-host-protocol-dev 0.1.0-5 or newer), the + first with param_info, remote_pages and track_info. +- Fuzz harnesses for the command parser and the reply stream (#144). + * Sat Oct 24 2026 Pau Aliagas - 0.9.1-1 - The preset schema, protocol/preset.schema.json, is installed in /usr/share/plugin-hostd with the two other schemas, in the development diff --git a/src/main.c b/src/main.c index d8351f1..06c39fa 100644 --- a/src/main.c +++ b/src/main.c @@ -35,7 +35,7 @@ #include "verbs.h" #ifndef VERSION -#define VERSION "0.9.1" +#define VERSION "0.9.2" #endif #define SOCKET_DEFAULT_PORT PHD_DEFAULT_COMMAND_PORT diff --git a/src/relay.c b/src/relay.c index 029d663..ff852de 100644 --- a/src/relay.c +++ b/src/relay.c @@ -34,8 +34,11 @@ #include "relay.h" -/* a worker line longer than this, with no NUL in it, is not a line of the protocol: it is dropped */ +/* a worker line longer than this, with no NUL in it, is not a line of the protocol: it is dropped; the fuzz harness + * (fuzz/fuzz_reply.c) builds the relay with a smaller one, so its inputs reach the drop */ +#ifndef LINE_LIMIT #define LINE_LIMIT (1024 * 1024) +#endif typedef struct SOURCE_T { int fd; @@ -117,6 +120,14 @@ static void gesture_seen(source_t *s, const char *line) } } +/* the end of a held gesture, its line up to the last word and the word end, as one line of whatever length; g_lock is + * held, so nothing is written between the two parts */ +static void gesture_end(const char *held) +{ + send_line(held, strlen(held)); + send_line(" " PHD_GESTURE_END, sizeof(" " PHD_GESTURE_END)); +} + /* a worker that is gone ends every gesture it held, so no control stays held at the controller; g_lock is held */ static void gestures_end(source_t *s) { @@ -124,10 +135,7 @@ static void gestures_end(source_t *s) for (n = 0; n < s->nheld; n++) { - char line[1024]; - - snprintf(line, sizeof(line), "%s " PHD_GESTURE_END, s->held[n]); - send_line(line, strlen(line) + 1); + gesture_end(s->held[n]); free(s->held[n]); } free(s->held); @@ -148,15 +156,12 @@ static void gestures_end_instance(source_t *s, int id) plen = strlen(prefix); while (n < s->nheld) { - char line[1024]; - if (strncmp(s->held[n], prefix, plen)) { n++; continue; } - snprintf(line, sizeof(line), "%s " PHD_GESTURE_END, s->held[n]); - send_line(line, strlen(line) + 1); + gesture_end(s->held[n]); free(s->held[n]); s->held[n] = s->held[--s->nheld]; } diff --git a/src/verbs.c b/src/verbs.c index cede3bc..2ad1a81 100644 --- a/src/verbs.c +++ b/src/verbs.c @@ -154,11 +154,15 @@ static char *handle(char *line, char **tok, int ntok) return atoi(tok[1]) == -1 ? sup_remove_all() : sup_remove(atoi(tok[1])); } if (!strcmp(verb, PHD_VERB_WORKER_LIST)) - return sup_worker_list(); + return ntok == 1 ? sup_worker_list() : sup_resp(ERR_INVALID_OPERATION); if (!strcmp(verb, PHD_VERB_INSTANCE_INFO)) return ntok == 2 ? sup_instance_info(atoi(tok[1])) : sup_resp(ERR_INVALID_OPERATION); if (!strcmp(verb, PHD_VERB_SUPERVISOR_RESET)) + { + if (ntok > 2) + return sup_resp(ERR_INVALID_OPERATION); return sup_reset(ntok > 1 ? tok[1] : PHD_WORD_ALL); + } if (!strcmp(verb, PHD_VERB_QUARANTINE_CLEAR)) return ntok == 2 ? sup_quarantine_clear(tok[1]) : sup_resp(ERR_INVALID_OPERATION); if (!strcmp(verb, PHD_VERB_POLICY_SET)) @@ -191,6 +195,8 @@ static char *handle(char *line, char **tok, int ntok) } if (verb_is(verb, QUIT)) { + if (ntok != 1) + return sup_resp(ERR_INVALID_OPERATION); g_quitting = 1; return sup_resp(SUCCESS); } @@ -212,7 +218,7 @@ void verbs_receive(msg_t *msg) int ntok = 0; end = line + strlen(line); - while (end > line && (end[-1] == '\n' || end[-1] == '\r' || end[-1] == ' ')) + while (end > line && (end[-1] == '\n' || end[-1] == '\r' || end[-1] == ' ' || end[-1] == '\t')) *--end = '\0'; copy = strdup(line); for (t = strtok_r(copy, " \t", &save); t && ntok < MAX_TOKENS; t = strtok_r(NULL, " \t", &save)) diff --git a/tests/daemon_test.py b/tests/daemon_test.py index d94923a..923a60e 100644 --- a/tests/daemon_test.py +++ b/tests/daemon_test.py @@ -32,6 +32,36 @@ def ready_line_and_empty_table(): d.close() +@test +def a_word_past_the_grammar_of_a_daemon_verb_is_refused(): + d = daemon() + try: + # worker_list takes no word, supervisor_reset at most one and quit none, as protocol.h declares them + d.expect("worker_list w1", "resp -902") + d.expect("supervisor_reset all w1", "resp -902") + d.expect("quit now", "resp -902") + d.expect("worker_list", "resp 0") + d.expect("supervisor_reset all", "resp 0") + finally: + d.close() + + +@test +def the_whitespace_that_ends_a_command_is_not_part_of_its_last_word(): + log = tempfile.mktemp(prefix="plugin-hostd-fake-log.") + d = daemon(env={"FAKE_LOG": log}) + try: + d.expect("add fake:a 0", "resp 0") + # a tab separates words as a space does, so a tab at the end, after a carriage return, is trimmed like one + d.expect("param_set 0 gain 0.5000\r\t", "resp 0") + # read as bytes: a carriage return the worker was sent must not be taken for the end of a line of the log + with open(log, "rb") as f: + got = [l.split(b" ", 1)[1] for l in f.read().split(b"\n") if b" param_set " in l] + check(got == [b"param_set 0 gain 0.5000"], "the worker gets the command without its trailing whitespace: %r" % got) + finally: + d.close() + + @test def placement_own_default_and_pool(): d = daemon() diff --git a/tests/gesture_alloc_test.c b/tests/gesture_alloc_test.c index 2ee0cca..1af0581 100644 --- a/tests/gesture_alloc_test.c +++ b/tests/gesture_alloc_test.c @@ -26,6 +26,8 @@ #include #include #include +#include +#include #include "../src/relay.c" @@ -84,6 +86,33 @@ int main(void) check(s.nheld == 0, "the end of a gesture never held is none"); gestures_end(&s); + + /* a held gesture longer than any fixed buffer is ended whole: its line up to the last word, then " end" */ + { + char line[3000], want[3000], got[4096]; + int sv[2]; + ssize_t n; + + memset(&s, 0, sizeof(s)); + snprintf(line, sizeof(line), "param_gesture 1 %02000d begin", 0); + gesture_seen(&s, line); + check(s.nheld == 1, "positive control: a long begin is held"); + snprintf(want, sizeof(want), "param_gesture 1 %02000d end", 0); + if (socketpair(AF_UNIX, SOCK_STREAM, 0, sv) != 0) + { + perror("socketpair"); + return 1; + } + g_client = sv[0]; + g_client_broken = 0; + gestures_end(&s); + g_client = -1; + n = recv(sv[1], got, sizeof(got), MSG_DONTWAIT); + check(n == (ssize_t)strlen(want) + 1 && !memcmp(got, want, strlen(want) + 1), + "the end of a 2000-byte gesture reaches the controller whole, with its end word and one NUL"); + close(sv[0]); + close(sv[1]); + } printf("gesture table: %d checks, %s\n", checks, failures ? "FAILED" : "ok"); return failures ? 1 : 0; }