|
| 1 | +#!/bin/sh |
| 2 | +# This file is part of KASLD - https://github.com/bcoles/kasld |
| 3 | +# |
| 4 | +# check-fuzz-harnesses — the fuzz harnesses must still build against the tree. |
| 5 | +# |
| 6 | +# A harness under tests/fuzz/ names the parser it drives by #including the |
| 7 | +# source file holding it, which makes it the only test that follows the |
| 8 | +# orchestrator's internals rather than its output. That is also how it rots: |
| 9 | +# move a global into another object, or retire one, and the harness stops |
| 10 | +# linking while every other test stays green. |
| 11 | +# |
| 12 | +# `make fuzz` is deliberately outside the default build graph, so that nobody |
| 13 | +# without clang is stopped by its absence — which also means nothing in `make |
| 14 | +# test` ever discovers that a harness stopped compiling. Five of six had been |
| 15 | +# broken for some time before this guard existed: the seed corpora, the |
| 16 | +# documentation and the security posture all described fuzzing that could not |
| 17 | +# be run. |
| 18 | +# |
| 19 | +# The check drives `make fuzz` rather than reassembling its command line, so it |
| 20 | +# cannot pass while the target it is guarding fails. It also pairs each harness |
| 21 | +# with its seed corpus: a harness with no corpus starts from nothing every run. |
| 22 | +# |
| 23 | +# Soft dependency: without a compiler that can link -fsanitize=fuzzer this |
| 24 | +# SKIPs, loudly, so a missing toolchain reads as unchecked rather than as |
| 25 | +# clean. |
| 26 | +# |
| 27 | +# Usage: tests/check-fuzz-harnesses (also run by `make lint` / `make test`) |
| 28 | +# Env: FUZZ_CC (default: clang), JOBS (parallelism for the build) |
| 29 | +# --- |
| 30 | +# <bcoles@gmail.com> |
| 31 | + |
| 32 | +set -u |
| 33 | +ROOT=$(CDPATH='' cd -- "$(dirname -- "$0")/.." && pwd) |
| 34 | +FUZZ_DIR=$ROOT/tests/fuzz |
| 35 | +CORPUS_DIR=$FUZZ_DIR/corpus |
| 36 | +# shellcheck source=tests/lib/guard-scope.sh |
| 37 | +. "$ROOT/tests/lib/guard-scope.sh" |
| 38 | + |
| 39 | +if [ -t 1 ] || [ -n "${KASLD_COLOR:-}" ]; then |
| 40 | + RED=$(printf '\033[31m'); GREEN=$(printf '\033[32m') |
| 41 | + YELLOW=$(printf '\033[33m'); RESET=$(printf '\033[0m') |
| 42 | +else |
| 43 | + RED=; GREEN=; YELLOW=; RESET= |
| 44 | +fi |
| 45 | + |
| 46 | +FUZZ_CC=${FUZZ_CC:-clang} |
| 47 | +if ! command -v "$FUZZ_CC" >/dev/null 2>&1; then |
| 48 | + printf '%scheck-fuzz-harnesses: SKIP%s (%s not installed)\n' \ |
| 49 | + "$YELLOW" "$RESET" "$FUZZ_CC" |
| 50 | + exit 0 |
| 51 | +fi |
| 52 | + |
| 53 | +TMP=$(mktemp -d "${TMPDIR:-/tmp}/kasld-fuzz.XXXXXX") || { |
| 54 | + printf '%scheck-fuzz-harnesses: FAIL%s — mktemp\n' "$RED" "$RESET"; exit 1; } |
| 55 | +trap 'rm -rf "$TMP"' EXIT |
| 56 | + |
| 57 | +# A compiler can exist without shipping libFuzzer, and the failure then lands at |
| 58 | +# link time inside the build below, where it would read as a broken harness. |
| 59 | +# Settle which it is up front, on a file that is known good. |
| 60 | +cat >"$TMP/probe.c" <<'EOF' |
| 61 | +#include <stddef.h> |
| 62 | +#include <stdint.h> |
| 63 | +int LLVMFuzzerTestOneInput(const uint8_t *d, size_t n) { |
| 64 | + (void)d; |
| 65 | + (void)n; |
| 66 | + return 0; |
| 67 | +} |
| 68 | +EOF |
| 69 | +if ! "$FUZZ_CC" -fsanitize=fuzzer,address,undefined "$TMP/probe.c" \ |
| 70 | + -o "$TMP/probe" >"$TMP/probe.log" 2>&1; then |
| 71 | + printf '%scheck-fuzz-harnesses: SKIP%s (%s cannot link -fsanitize=fuzzer)\n' \ |
| 72 | + "$YELLOW" "$RESET" "$FUZZ_CC" |
| 73 | + exit 0 |
| 74 | +fi |
| 75 | + |
| 76 | +# Every harness, and the corpus its name implies: fuzz_parse_meta.c is seeded |
| 77 | +# from corpus/parse_meta/. |
| 78 | +n=0 |
| 79 | +no_corpus= |
| 80 | +for f in "$FUZZ_DIR"/fuzz_*.c; do |
| 81 | + [ -f "$f" ] || continue |
| 82 | + n=$((n + 1)) |
| 83 | + base=$(basename "$f" .c) |
| 84 | + seeds=$CORPUS_DIR/${base#fuzz_} |
| 85 | + if [ ! -d "$seeds" ]; then |
| 86 | + no_corpus="$no_corpus $base" |
| 87 | + fi |
| 88 | +done |
| 89 | + |
| 90 | +if [ -n "$no_corpus" ]; then |
| 91 | + printf '%scheck-fuzz-harnesses: FAIL%s — harness with no seed corpus:%s\n' \ |
| 92 | + "$RED" "$RESET" "$no_corpus" |
| 93 | + printf ' expected %s/<name>/ for each tests/fuzz/fuzz_<name>.c\n' \ |
| 94 | + "$CORPUS_DIR" |
| 95 | + exit 1 |
| 96 | +fi |
| 97 | + |
| 98 | +guard_scope "check-fuzz-harnesses" "$n" 4 |
| 99 | + |
| 100 | +# Build through the real target. MAKEFLAGS is cleared because this may run |
| 101 | +# under `make lint`, whose jobserver this build is not a part of. |
| 102 | +jobs=${JOBS:-$(nproc 2>/dev/null || echo 1)} |
| 103 | +if ! MAKEFLAGS='' MFLAGS='' make -C "$ROOT" -j"$jobs" fuzz \ |
| 104 | + >"$TMP/build.log" 2>&1; then |
| 105 | + # The backticks quote a make target, not a command substitution. |
| 106 | + # shellcheck disable=SC2016 |
| 107 | + printf '%scheck-fuzz-harnesses: FAIL%s — `make fuzz` does not build\n' \ |
| 108 | + "$RED" "$RESET" |
| 109 | + sed -n '/error\|Error\|undefined/p' "$TMP/build.log" | head -20 |
| 110 | + exit 1 |
| 111 | +fi |
| 112 | + |
| 113 | +# `make fuzz` reports success for what it chose to build; assert that is every |
| 114 | +# harness in the tree, so a harness the build never reaches cannot pass here as |
| 115 | +# one that built cleanly. |
| 116 | +missing= |
| 117 | +for f in "$FUZZ_DIR"/fuzz_*.c; do |
| 118 | + [ -f "$f" ] || continue |
| 119 | + base=$(basename "$f" .c) |
| 120 | + [ -x "$ROOT/build/fuzz/$base" ] || missing="$missing $base" |
| 121 | +done |
| 122 | + |
| 123 | +if [ -n "$missing" ]; then |
| 124 | + # shellcheck disable=SC2016 |
| 125 | + printf '%scheck-fuzz-harnesses: FAIL%s — `make fuzz` built no binary for:%s\n' \ |
| 126 | + "$RED" "$RESET" "$missing" |
| 127 | + exit 1 |
| 128 | +fi |
| 129 | + |
| 130 | +printf '%scheck-fuzz-harnesses: OK%s (%d harnesses build and link, each seeded)\n' \ |
| 131 | + "$GREEN" "$RESET" "$n" |
| 132 | +exit 0 |
0 commit comments