|
| 1 | +# lws fuzzing |
| 2 | + |
| 3 | +Coverage-guided fuzzing of lws' untrusted-input parsers, using clang's |
| 4 | +libFuzzer + AddressSanitizer. Everything runs locally with no dependency |
| 5 | +on any external fuzzing infrastructure. |
| 6 | + |
| 7 | +## Quick start |
| 8 | + |
| 9 | +Requirements: `clang` and the libFuzzer runtime (Debian-ish: |
| 10 | +`clang-19 libclang-rt-19-dev`), plus `libgnutls28-dev` if you want the |
| 11 | +`qpack` target (it needs the h3 role, which needs a QUIC-capable TLS |
| 12 | +provider; without gnutls that one target is silently skipped). |
| 13 | + |
| 14 | +```sh |
| 15 | +./fuzz/run.sh # build + 60s per target against all targets |
| 16 | +./fuzz/run.sh 3600 # an hour per target |
| 17 | +./fuzz/run.sh 600 lejp qpack # selected targets only |
| 18 | +BUILD=~/fz ./fuzz/run.sh # non-default build dir |
| 19 | +``` |
| 20 | + |
| 21 | +Corpora accumulate per-target in `<build>/fuzz/corpus-<name>/` across runs |
| 22 | +(so coverage keeps advancing between campaigns), seeded from the committed |
| 23 | +inputs in `fuzz/fuzz-<name>/seeds/`. Anything the fuzzer finds is written |
| 24 | +to `<build>/fuzz/crash-<sha1>`; re-run it directly on the artifact file to |
| 25 | +reproduce: |
| 26 | + |
| 27 | +```sh |
| 28 | +./build-fuzz/bin/fuzz-qpack build-fuzz/fuzz/crash-<sha1> # repro under ASan |
| 29 | +``` |
| 30 | + |
| 31 | +`clang` is autodetected if there is no `clang` binary (eg `clang-19`). |
| 32 | +Additional cmake options can be passed via `FUZZ_CMAKE_OPTS`. |
| 33 | + |
| 34 | +## Reading the output |
| 35 | + |
| 36 | +- `Done N runs in Ts` for a target means its time slice completed with no |
| 37 | + findings. Corpus (`corp: N/MKb`) and coverage (`cov:`) growing between |
| 38 | + runs is the normal steady state, not a problem. |
| 39 | +- Parser error logs during fuzzing are the *expected* face of malformed |
| 40 | + input being rejected, not findings — the harnesses silence them to keep |
| 41 | + throughput. Set `LWS_FUZZ_VERBOSE=1` to replay a specific input with |
| 42 | + logs, e.g. `LWS_FUZZ_VERBOSE=1 ./build-fuzz/bin/fuzz-qpack <artifact>`. |
| 43 | +- An actual finding is an ASan/UBSan report on stderr plus a |
| 44 | + `crash-<sha1>` artifact in `<build>/fuzz/`, and `run.sh` exits nonzero. |
| 45 | +- `Ctrl-C` mid-campaign is safe; corpora reached so far are kept, and each |
| 46 | + target also self-terminates at its `-max_total_time`. |
| 47 | + |
| 48 | +## CI / ctest |
| 49 | + |
| 50 | +The same build registers a fast smoke test per target (`-runs=0`, runs |
| 51 | +each committed seed exactly once under ASan): |
| 52 | + |
| 53 | +```sh |
| 54 | +CC=clang-19 cmake .. --fresh -DLWS_WITH_FUZZERS=ON -DLWS_WITH_CBOR=ON |
| 55 | +cmake --build . --parallel |
| 56 | +ctest -R fuzz- # seconds, deterministic |
| 57 | +``` |
| 58 | + |
| 59 | +`LWS_WITH_FUZZERS` implies whole-lib `-fsanitize=fuzzer-no-link,address` |
| 60 | +instrumentation, so it should stay OFF for normal builds and normal CI. |
| 61 | +Long campaigns belong on a dedicated runner or a nightly job via |
| 62 | +`./fuzz/run.sh`. |
| 63 | + |
| 64 | +## Targets |
| 65 | + |
| 66 | +| target | parser under test | notes | |
| 67 | +|---|---|---| |
| 68 | +| `fuzz-lejp` | lejp JSON parser (`lib/misc/lejp.c`) | policy, JOSE, RPC JSON; fed in two chunks to cover partial-input states | |
| 69 | +| `fuzz-lecp` | lecp CBOR parser (`lib/misc/lecp.c`) | needs `-DLWS_WITH_CBOR=ON` | |
| 70 | +| `fuzz-qpack` | native QPACK decoders (`lib/roles/h3/qpack.c`) | first byte selects encoder-stream vs header-block decode; needs h3 (`LWS_WITH_HTTP3` + gnutls) | |
| 71 | +| `fuzz-upng` | stateful PNG decoder (`lib/misc/upng.c`) | seeds from `test-apps/*.png` | |
| 72 | +| `fuzz-lhp` | HTML5 + CSS parser (`lib/misc/lhp.c`) | builds a dlo document per input and destroys it; leaks and heap errors in teardown are caught | |
| 73 | +| `fuzz-h1` | h1 server header/body parser (`lib/roles/http/`) | evil-peer target, see below | |
| 74 | +| `fuzz-h2` | h2 framing + hpack (`lib/roles/h2/`) | evil-peer: the fuzz input is h2 frames after a canned h2c upgrade + connection preface | |
| 75 | +| `fuzz-ws` | ws server frame parser (`lib/roles/ws/`) | evil-peer: the fuzz input is client frames after a canned upgrade handshake | |
| 76 | + |
| 77 | +The `fuzz-h1`, `fuzz-h2` and `fuzz-ws` targets use the shared evil-peer |
| 78 | +helper in [peer.h](peer.h): one real, adopted server-side connection per |
| 79 | +input over a socketpair, fed the fuzz bytes as if received from the peer, |
| 80 | +serviced deterministically via `lws_service_fd()`, then hung up on so the |
| 81 | +close paths run too. This exercises the production wsi state machines, |
| 82 | +not just the parsing functions in isolation. The same pattern will work |
| 83 | +for any other adoptable server-side parser (eg, mqtt). |
| 84 | + |
| 85 | +When a crash is found: reproduce on the artifact, minimize it, fix, then |
| 86 | +commit a minimized seed under the target's `seeds/` and add the same input |
| 87 | +as a case to the corresponding api-test where one exists, so it stays |
| 88 | +covered in normal CI. |
| 89 | + |
| 90 | +## Survey: what to harness next |
| 91 | + |
| 92 | +The wsi-bound h1, h2/hpack and ws parsers are covered via `peer.h`. The |
| 93 | +remaining untrusted-input surfaces, easiest first: |
| 94 | + |
| 95 | + - mqtt rx parser (`lib/roles/mqtt/mqtt.c`): an evil-peer target with a |
| 96 | + canned CONNECT prelude, same shape as `fuzz-ws` |
| 97 | + - JPEG decoder (`lib/misc/jpeg.c`, `LWS_WITH_JPEG`) — standalone, same |
| 98 | + shape as `fuzz-upng` |
| 99 | + - async DNS wire parser (`lib/system/async-dns/async-dns-parse.c`) |
| 100 | + - auth-dns zone parser (`lib/system/auth-dns/`) |
| 101 | + - COSE sign/validate (`lib/cose/`), jrpc (`lib/misc/jrpc/`), |
| 102 | + dht messages (`lib/misc/dht`), sshd userauth / bipacket |
| 103 | + - `lws_tokenize`, `lws_b64_decode`, iso8601 and friends — standalone, |
| 104 | + cheap to add |
| 105 | + - wt (WebTransport) and full h3/QUIC framing: these need the UDP/QUIC |
| 106 | + stack stood up, a larger project than a socketpair |
| 107 | + |
| 108 | +Fault injection (`lws_fi`) can additionally be used from inside harnesses |
| 109 | +to fail the Nth allocation during parsing, which is where most parser |
| 110 | +lifetime bugs hide. |
0 commit comments