libextractor

GNU libextractor
Log | Files | Refs | Submodules | README | LICENSE

commit 874f7c08f758db93b4305bd0ea216d0b09fdc214
parent b55537669126af9a1694f1983d58e98b712b666a
Author: Christian Grothoff <christian@grothoff.org>
Date:   Wed, 29 Jul 2026 13:27:42 +0200

fuzzing update

Diffstat:
M.gitignore | 14++++++++++++++
Acontrib/oss-fuzz/run_campaign.sh | 273+++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
Asrc/fuzz/CAMPAIGN.md | 238+++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
Msrc/fuzz/Makefile.am | 1+
Msrc/fuzz/README | 11+++++++++++
5 files changed, 537 insertions(+), 0 deletions(-)

diff --git a/.gitignore b/.gitignore @@ -80,6 +80,20 @@ src/main/test_ipc src/main/test_plugin_load_multi src/main/test_plugin_loading src/main/test_trivial +src/fuzz/.deps/ +src/fuzz/.libs/ +src/fuzz/Makefile +src/fuzz/Makefile.in +src/fuzz/*.o +src/fuzz/*.lo +src/fuzz/*.la +src/fuzz/*.log +src/fuzz/*.trs +src/fuzz/test-suite.log +src/fuzz/fuzz_* +!src/fuzz/fuzz_*.c +!src/fuzz/fuzz_*.h +src/fuzz/crashes/ src/plugins/.deps/ src/plugins/.libs/ src/plugins/Makefile diff --git a/contrib/oss-fuzz/run_campaign.sh b/contrib/oss-fuzz/run_campaign.sh @@ -0,0 +1,273 @@ +#!/bin/bash +# +# Run a weighted libextractor fuzzing campaign. +# +# This file is in the public domain. +# +# The weights are not guesses. They come from a measured 12-core hour on +# the 2026-07-29 tree: every target was given an identical 1200 s slice, +# the resulting corpora were replayed through a coverage build, and each +# target was credited with the library regions still uncovered in the +# source files it covers better than any other target. See +# ../../src/fuzz/CAMPAIGN.md for the data and for why a flat allocation +# wastes most of its budget. +# +# Re-measure with: +# src/fuzz/CAMPAIGN.md, section "Reproducing the measurement" +# +set -u + +BIN="${BIN:-}" +OUT="${OUT:-./campaign}" +CORES="${CORES:-$(nproc)}" +HOURS="${HOURS:-}" +PROFILE="${PROFILE:-nightly}" +WEIGHTS_FILE="" +FLOOR="${FLOOR:-300}" +DRYRUN=0 + +usage () +{ + cat <<EOF +usage: $0 -b BINDIR [-o OUTDIR] [-c CORES] [-t HOURS] [-p PROFILE] [-w FILE] + + -b BINDIR directory holding the built fuzz targets (build.sh's \$OUT) + -o OUTDIR where corpora, logs and artifacts go (default ./campaign) + -c CORES concurrent targets (default nproc) + -t HOURS wall-clock budget; overrides -p + -p PROFILE ci | nightly | deep (default nightly) + -w FILE weight table "target weight" per line, overrides the built-in + -F SECONDS per-target floor (default 300) + -n print the schedule and exit; run nothing + +Profiles, in wall-clock hours on \$CORES cores: + + ci every target gets the floor and nothing more. On 12 cores + this is about 15 minutes and it recovers ~93% of the edge + coverage a twenty-minute-per-target run reaches. Cheap + enough to run on every push. + nightly 2 h. The floor plus a headroom-weighted share. + deep 8 h. Past this the fitted curve says each additional 1% of + edge coverage costs more than 30 core-hours; spend it on + better generators or new targets instead. +EOF + exit "${1:-1}" +} + +while getopts "b:o:c:t:p:w:F:nh" o; do + case "$o" in + b) BIN="$OPTARG" ;; + o) OUT="$OPTARG" ;; + c) CORES="$OPTARG" ;; + t) HOURS="$OPTARG" ;; + p) PROFILE="$OPTARG" ;; + w) WEIGHTS_FILE="$OPTARG" ;; + F) FLOOR="$OPTARG" ;; + n) DRYRUN=1 ;; + h) usage 0 ;; + *) usage ;; + esac +done + +[ -n "${BIN}" ] || usage +[ -d "${BIN}" ] || { echo "no such directory: ${BIN}" >&2; exit 1; } + +if [ -z "${HOURS}" ]; then + case "${PROFILE}" in + ci) HOURS=0 ;; + nightly) HOURS=2 ;; + deep) HOURS=8 ;; + *) echo "unknown profile: ${PROFILE}" >&2; usage ;; + esac +fi + +# Addressable headroom in library regions, measured 2026-07-29. +# "Addressable" excludes two things that no runtime can reach: +# plugins/pack.c 89 regions of format codes that neither of its +# two callers ever asks for (fixed format strings) +# extractor_logging.c 2 regions live only in a debug build +# The third-party wrappers (gif jpeg tiff flac ogg archive mime) are +# damped to 35%: their uncovered regions are almost all error returns +# from giflib/libjpeg/libtiff/FLAC/libvorbis/libarchive/libmagic, which +# those projects fuzz themselves, and reaching them from a file input is +# disproportionately expensive. +read -r -d '' BUILTIN_WEIGHTS <<'EOF' +fuzz_ole2 119 +fuzz_unzip 119 +fuzz_msoffice 73 +fuzz_datasource 74 +fuzz_qt 56 +fuzz_rtf 56 +fuzz_png 48 +fuzz_ogg 26 +fuzz_flac 14 +fuzz_mime 10 +fuzz_dvi 10 +fuzz_odf 9 +fuzz_deb 7 +fuzz_convert 6 +fuzz_elf 6 +fuzz_man 4 +fuzz_nsfe 4 +fuzz_ps 3 +fuzz_real 3 +fuzz_sid 2 +fuzz_gif 1 +fuzz_jpeg 1 +fuzz_tiff 1 +fuzz_nsf 1 +fuzz_riff 1 +fuzz_applefile 0 +fuzz_archive 0 +fuzz_ipc 0 +fuzz_it 0 +fuzz_s3m 0 +fuzz_wav 0 +fuzz_xm 0 +fuzz_zip 0 +EOF + +if [ -n "${WEIGHTS_FILE}" ]; then + WEIGHTS="$(cat "${WEIGHTS_FILE}")" +else + WEIGHTS="${BUILTIN_WEIGHTS}" +fi + +mkdir -p "${OUT}/corpus" "${OUT}/logs" "${OUT}/artifacts" +export TMPDIR="${OUT}/tmp" +mkdir -p "${TMPDIR}" + +# Only schedule targets that were actually built. +AVAIL="" +while read -r t w; do + [ -n "${t}" ] || continue + [ -x "${BIN}/${t}" ] || continue + AVAIL="${AVAIL}${t} ${w} +" +done <<EOF +${WEIGHTS} +EOF + +NTARGETS=$(printf '%s' "${AVAIL}" | grep -c . || true) +[ "${NTARGETS}" -gt 0 ] || { echo "no targets found in ${BIN}" >&2; exit 1; } + +BUDGET=$(awk -v c="${CORES}" -v h="${HOURS}" 'BEGIN{printf "%d", c*h*3600}') +# No single job may outlast the campaign: with -t 8 a target given nine +# hours would still be running when everything else has finished, and +# the run would take nine hours rather than the eight that were asked +# for. Hence water-filling -- share out the budget by weight, clamp +# anything over the wall clock, share the remainder among the rest, and +# repeat until nothing else clamps. +CAP=$(awk -v h="${HOURS}" 'BEGIN{printf "%d", (h>0 ? h*3600 : 0)}') + +: > "${OUT}/jobs.txt" +printf '%s' "${AVAIL}" | awk -v floor="${FLOOR}" -v budget="${BUDGET}" \ + -v cap="${CAP}" ' + { name[NR]=$1; w[NR]=$2; n=NR } + END { + if (cap <= 0 || cap < floor) cap = floor; # ci profile: floor only + for (i=1; i<=n; i++) { s[i]=floor; capped[i]=(floor>=cap) } + extra = budget - floor*n + if (extra < 0) extra = 0 + for (round=0; round<64 && extra>0; round++) { + tw = 0 + for (i=1; i<=n; i++) if (!capped[i]) tw += w[i] + if (tw <= 0) break + spill = 0; moved = 0 + for (i=1; i<=n; i++) { + if (capped[i]) continue + add = extra * w[i] / tw + if (s[i] + add >= cap) { spill += s[i] + add - cap; s[i] = cap; capped[i]=1 } + else { s[i] += add } + moved = 1 + } + if (!moved) break + extra = spill + } + for (i=1; i<=n; i++) printf "%s:%d\n", name[i], s[i] + }' >> "${OUT}/jobs.txt" +# Longest first: with a fixed number of slots the long jobs must start +# early or the tail of the campaign runs on one core. +sort -t: -k2 -rn -o "${OUT}/jobs.txt" "${OUT}/jobs.txt" + +echo "=== libextractor campaign ===" +echo " targets ${NTARGETS}" +echo " cores ${CORES}" +echo " profile ${PROFILE} (${HOURS} h wall => $(awk -v b=${BUDGET} 'BEGIN{printf "%.1f", b/3600}') core-hours)" +echo " floor ${FLOOR}s" +echo " out ${OUT}" +awk -F: -v c="${CORES}" ' + { printf " %-18s %6d s %2d:%02d\n", $1, $2, $2/3600, ($2%3600)/60; s+=$2 } + END { printf " %-18s %6.1f core-hours over %d cores => %.1f h wall (perfect packing)\n", + "TOTAL", s/3600, c, s/3600/c } +' "${OUT}/jobs.txt" + +if [ "${DRYRUN}" = "1" ]; then + echo "(dry run: nothing executed)" + exit 0 +fi + +for z in "${BIN}"/*_seed_corpus.zip; do + [ -f "${z}" ] || continue + t=$(basename "${z}" _seed_corpus.zip) + [ -x "${BIN}/${t}" ] || continue + mkdir -p "${OUT}/corpus/${t}" + unzip -qo "${z}" -d "${OUT}/corpus/${t}" 2>/dev/null +done + +run_one () +{ + local spec="$1" t secs dict maxlen leaks rc + t="${spec%%:*}" + secs="${spec##*:}" + dict="" + [ -f "${BIN}/${t}.dict" ] && dict="-dict=${BIN}/${t}.dict" + maxlen=262144 + case "${t}" in + fuzz_convert) maxlen=4096 ;; + fuzz_ipc) maxlen=65536 ;; + esac + # ole2 drags in glib, whose one-time allocations LeakSanitizer reports + # as leaks on every input. They are not ours and they bury everything + # else, so leak detection is off for that target only. + leaks=1 + [ "${t}" = "fuzz_ole2" ] && leaks=0 + mkdir -p "${OUT}/artifacts/${t}" + echo "START ${t} (${secs}s)" + ASAN_OPTIONS="allocator_may_return_null=0:detect_stack_use_after_return=1:detect_leaks=${leaks}" \ + UBSAN_OPTIONS="print_stacktrace=1:report_error_type=1" \ + "${BIN}/${t}" "${OUT}/corpus/${t}" \ + -max_total_time="${secs}" -fork=1 \ + -ignore_crashes=1 -ignore_ooms=1 -ignore_timeouts=1 \ + -rss_limit_mb=2560 -timeout=25 -max_len="${maxlen}" \ + -print_final_stats=1 \ + -artifact_prefix="${OUT}/artifacts/${t}/" \ + ${dict} > "${OUT}/logs/${t}.log" 2>&1 + rc=$? + echo "DONE ${t} rc=${rc} artifacts=$(ls -1 "${OUT}/artifacts/${t}" 2>/dev/null | wc -l)" +} +export -f run_one +export BIN OUT + +date +%s > "${OUT}/started_at" +xargs -a "${OUT}/jobs.txt" -P "${CORES}" -I{} \ + bash -c 'run_one "$@"' _ {} 2>&1 | tee "${OUT}/campaign.log" +date +%s > "${OUT}/finished_at" + +echo +echo "=== campaign finished in $(( $(cat "${OUT}/finished_at") - $(cat "${OUT}/started_at") ))s ===" +echo "artifacts: $(find "${OUT}/artifacts" -type f | wc -l)" +cat <<EOF + +Now run the replay pass. -fork=1 reads only the child's exit status, so +a *recovering* UndefinedBehaviorSanitizer report leaves no artifact and +no trace in the log -- and seven of the twelve defects fixed in this +library were exactly that. The campaign above finds memory-safety bugs; +this finds the rest: + + for d in ${OUT}/corpus/*; do + t=\$(basename "\$d") + UBSAN_OPTIONS=halt_on_error=0 ASAN_OPTIONS=halt_on_error=0 \\ + ./src/fuzz/\$t --corpus-dir="\$d" + done 2>&1 | grep "runtime error" +EOF diff --git a/src/fuzz/CAMPAIGN.md b/src/fuzz/CAMPAIGN.md @@ -0,0 +1,238 @@ +# Running a libextractor fuzzing campaign + +This is the measured answer to two questions: **how should a fixed +number of core-hours be divided across the targets**, and **how long is +it still worth running**. Both answers come from a controlled 12-core +hour on the 2026-07-29 tree, not from intuition, and both are +uncomfortable: a flat allocation wastes most of its budget, and the +productive length of a campaign is far shorter than it looks. + +`../../contrib/oss-fuzz/run_campaign.sh` implements the conclusion. + + +## 1. The measurement + +Every target got an **identical** budget, so the curves are comparable +and the allocation could be derived from the data instead of assumed: + + 36 jobs x 1200 s / 12 cores = 3 full waves = 3715 s wall, 97% busy + +32 of the jobs were the targets; 4 were replicates of `fuzz_unzip`, +`fuzz_msoffice`, `fuzz_rtf` and `fuzz_datasource` with a different PRNG +seed and their own corpus, to find out how much of any difference is +just noise. + +The tree carried the twelve fixes from `issues.txt`. Result: + +| | | +|---|---| +| wall clock | 3715 s (62 min), 12 cores, 12.0 core-hours | +| crashes / leaks / OOM / timeouts | **0** — all 36 jobs reported `0/0/0` | +| artifacts | **0** | +| corpus | 1727 seed inputs grew to 6045 | +| replay of the whole result under ASan+UBSan | **0 sanitizer reports** | + +The replay pass is not optional and not redundant. `-fork=1` reads only +the child's exit status, so a *recovering* UBSan report leaves no +artifact and nothing in the log — and seven of the twelve fixed defects +were exactly that. + + +## 2. Feature counts are noise; edge coverage is not + +The four replicate pairs differ only in PRNG seed: + +| target | run 1 `ft` | run 2 `ft` | Δ | run 1 `cov` | run 2 `cov` | Δ | +|---|---|---|---|---|---|---| +| fuzz_rtf | 6608 | 7201 | **+9.0 %** | 1047 | 1072 | +2.4 % | +| fuzz_msoffice | 5332 | 5739 | **+7.6 %** | 1083 | 1100 | +1.6 % | +| fuzz_unzip | 4560 | 4430 | −2.9 % | 805 | 804 | −0.1 % | +| fuzz_datasource | 3254 | 3244 | −0.3 % | 650 | 650 | 0.0 % | + +libFuzzer's `ft` counter includes value-profile entries and is unbounded, +so it keeps rising after the fuzzer has stopped reaching new code. Any +ranking built on it at this scale is fitting noise. Everything below +therefore uses **edge coverage** and **llvm-cov region coverage**, both +of which reproduce to within about 2 %. + + +## 3. How long a target is worth running + +Seconds each target needed to reach a given fraction of the edge +coverage it ended the 1200 s slice with: + +| | median over 32 targets | +|---|---| +| 90 % of final | **12 s** | +| 95 % of final | **34 s** | +| 99 % of final | **93 s** | + +Only **4 of 32** targets were still gaining at t = 1200 s +(`fuzz_unzip`, `fuzz_rtf`, `fuzz_qt`, `fuzz_msoffice`); twelve had +converged within nine seconds. + +Aggregated over all 32 targets, the discovery rate collapses by a factor +of **340** across a single twenty-minute slice: + +| window | new edges | rate | +|---|---|---| +| 0 – 75 s | 2026 | 3039 edges/core-hour | +| 75 – 150 s | 117 | 175 edges/core-hour | +| 150 – 300 s | 80 | 60 edges/core-hour | +| 300 – 600 s | 61 | 23 edges/core-hour | +| 600 – 1200 s | 48 | **9 edges/core-hour** | + +87 % of everything the slice found arrived in its first 75 seconds. The +second half of the run — six of the twelve core-hours — produced 2 % of +the gain. + +Cumulative gain fits `G(t) = 1655 · t^0.050`, which extrapolates to: + +| per target | total edges | vs the 20-min slice | cost | +|---|---|---|---| +| 20 min | 2332 | — | 10.7 core-hours | +| 1 h | 2488 | +6.7 % | 32 core-hours | +| 2 h | 2576 | +10.5 % | 64 core-hours | +| 8 h | 2760 | +18.3 % | 256 core-hours | +| 24 h | 2915 | +25.0 % | 768 core-hours | +| 168 h | 3211 | +37.7 % | 5376 core-hours | + +Seventy-two times the budget buys a quarter more coverage. The marginal +cost per edge roughly doubles with every doubling of runtime: 0.14 +core-hours per edge going from 20 min to 1 h per target, 1.4 at 8 h, +3.9 at 24 h. + + +## 4. Where the remaining code actually is + +The campaign corpora were replayed through a coverage build. Including +`fuzz_ole2`, which was given its own matching 1200 s slice afterwards, +the union across all targets is **6488 of 7328 library regions, 88.5 %**. +The 840 that are left are not evenly spread, and not all of them are +reachable: + +| source file | regions | covered | left | best target | +|---|---|---|---|---| +| `common/unzip.c` | 674 | 555 | **119** | fuzz_unzip | +| `plugins/pack.c` | 128 | 39 | 89 → *unreachable* | fuzz_elf | +| `plugins/ole2_extractor.c` | 799 | 721 | 78 | fuzz_ole2 | +| `main/extractor_datasource.c` | 545 | 471 | 74 | fuzz_datasource | +| `plugins/msoffice_extractor.c` | 548 | 475 | 73 | fuzz_msoffice | +| `plugins/ogg_extractor.c` | 235 | 162 | 73 | fuzz_ogg | +| `plugins/qt_extractor.c` | 390 | 334 | 56 | fuzz_qt | +| `plugins/rtf_extractor.c` | 746 | 690 | 56 | fuzz_rtf | +| `plugins/png_extractor.c` | 315 | 267 | 48 | fuzz_png | +| `plugins/msoffice_biff.h` | 181 | 140 | 41 | fuzz_ole2 | +| `plugins/flac_extractor.c` | 130 | 89 | 41 | fuzz_flac | +| `plugins/mime_extractor.c` | 52 | 24 | 28 | fuzz_mime | +| everything else | | | ≤ 10 each | | + +Four findings matter more than the table: + +**`fuzz_ole2` was the highest-yield target in the tree and had never been +run.** Its 1200 s slice took `ole2_extractor.c` from 47.6 % to 90.2 % +— 341 new regions, 1023 regions/core-hour, against 780 for the next best +(`fuzz_elf`) and 150 for `fuzz_unzip`. It also covers +`plugins/msoffice_biff.h` better than `fuzz_msoffice` does (41 uncovered +against 64). It was excluded from `contrib/oss-fuzz/build.sh` because +the plugin needs libgsf's compiler flags rather than just a `-l`, and +nobody revisited the exclusion; issue 13 was waiting in it, reachable +from a file already in `src/plugins/testdata/`. Build it with +`LE_FUZZ_GSF=1`. Note that most of what it *reports* belongs to libgsf +and glib rather than to libextractor — see `issues.txt` — so it earns a +place in local campaigns but not in the OSS-Fuzz default set. + +**`plugins/pack.c`'s 89 uncovered regions are dead code.** It is a +general-purpose (un)packer with sixteen format codes. Its only two +callers pass fixed format strings — `"4bW16bH"`, `"WWW"`, +`"hhwwwwwhhhhhh"`, `"wwwwwwwwww"` and their upper-case variants — so no +input can reach the rest. `fuzz_applefile` looks like it has 102 +regions of headroom and actually has none; do not fund it. + +**`fuzz_zip`, `fuzz_odf` and `fuzz_msoffice` do not meaningfully cover +`unzip.c`.** Their corpora were merged with `fuzz_unzip`'s (961 inputs +total) and replayed: union coverage of `unzip.c` is 83.4 % against +`fuzz_unzip`'s 82.3 % alone. Three targets, 743 inputs, seven extra +regions. `unzip.c` should be funded through `fuzz_unzip` and nowhere +else; `zip_extractor.c` itself is already at 100 %. + +**What is left in `unzip.c` is the interesting part.** The uncovered +regions concentrate in `parse_current_file_coherency_header` (29) and +`unzip_open_using_ffd` (18) — the central-directory / local-header +consistency checks, which is precisely where a ZIP reader gets attacked. +Reaching them needs a better ZIP generator, not a longer run. + + +## 5. The recommendation + +**Balance.** Weight each target by the library regions still uncovered in +the files it covers better than any other target, excluding the +unreachable ones, and damp the third-party wrappers (gif, jpeg, tiff, +flac, ogg, archive, mime) to 35 % — their remaining regions are error +returns from giflib/libjpeg/libtiff/FLAC/libvorbis/libarchive/libmagic, +which those projects fuzz themselves. Give every target a 300 s floor so +that a regression anywhere still gets caught. That table is built into +`run_campaign.sh`. + +**Length.** Three tiers, and the top one is smaller than it looks: + +| profile | wall clock on 12 cores | core-hours | what it is for | +|---|---|---|---| +| `ci` | ~11 min (300 s floor, every target) | 2.8 | every push; recovers ~93 % of the edges a 20-min-per-target run reaches | +| `nightly` | 2 h | 24 | floor plus the headroom-weighted share | +| `deep` | 8 h | 96 | weekly; past here each additional 1 % of edge coverage costs >30 core-hours | + +No single job is ever given more than the wall clock, however heavy its +weight: a target handed nine hours inside an eight-hour campaign would +still be running when everything else had finished. `run_campaign.sh` +water-fills instead — share by weight, clamp at the wall clock, +redistribute the spill, repeat — so `-t 8` really does take eight hours. + +Do not run flat campaigns longer than `deep`. Beyond roughly 100 +core-hours per run the fitted curve says the money is better spent on: + +1. **A structure-aware ZIP generator**, for the `unzip.c` coherency + checks above — 119 regions, and the ones that matter. +2. **New targets.** `fuzz_ole2` had 419 uncovered regions and one live + defect purely because nobody had built it; that is a far better + return than another 100 core-hours on the 32 targets that were + already running. `fuzz_extract` is still absent from the libFuzzer + set for a good reason (it `dlopen()`s uninstrumented modules), but it + is worth running under the built-in driver. +3. **The replay pass after every fix.** Issue 12 existed only because + issue 02 fired first on almost every PNG input and masked it; it was + found by re-running after patching, not by running longer. + +The general shape: on this codebase a campaign is a **corpus-building +and regression exercise**, not a bug-discovery lottery. Of the twelve +defects fixed, four were found within 73 seconds and seven more were +reachable from the seed corpus with *no fuzzing at all* — replaying +`src/plugins/testdata/` under UBSan finds them. Exactly one needed +fuzzing time, and only after another defect was unmasked. + + +## 6. Reproducing the measurement + +```sh +# 1. build, out of tree, with gsf so ole2 is included +rsync -a --exclude=.git /path/to/libextractor/ /tmp/le/libextractor/ +( cd /tmp/le/libextractor && make distclean ) +SRC=/tmp/le WORK=/tmp/w OUT=/tmp/out SANITIZER=address \ + LE_FUZZ_EXTRA_PLUGINS=1 LE_FUZZ_GSF=1 \ + /tmp/le/libextractor/contrib/oss-fuzz/build.sh + +# 2. flat slice, for comparable curves +contrib/oss-fuzz/run_campaign.sh -b /tmp/out -o /tmp/camp -c 12 -F 1200 -t 0 + +# 3. coverage build, replay each corpus, diff against the seed corpus +SRC=/tmp/le WORK=/tmp/wc OUT=/tmp/cov SANITIZER=coverage \ + LE_FUZZ_EXTRA_PLUGINS=1 LE_FUZZ_GSF=1 \ + /tmp/le/libextractor/contrib/oss-fuzz/build.sh +LLVM_PROFILE_FILE=p.profraw /tmp/cov/fuzz_unzip -runs=0 /tmp/camp/corpus/fuzz_unzip +llvm-profdata merge -sparse p.profraw -o p.profdata +llvm-cov report /tmp/cov/fuzz_unzip -instr-profile=p.profdata +``` + +The per-job stats lines libFuzzer prints in fork mode +(`#N: cov: .. ft: .. corp: .. time: Ns job: M`) are a ready-made time +series; that is where sections 2 and 3 come from. diff --git a/src/fuzz/Makefile.am b/src/fuzz/Makefile.am @@ -63,6 +63,7 @@ noinst_HEADERS = \ EXTRA_DIST = \ README \ BUILD-INTEGRATION.md \ + CAMPAIGN.md \ corpus CLEANFILES = \ diff --git a/src/fuzz/README b/src/fuzz/README @@ -221,6 +221,17 @@ fix: For a real campaign use libFuzzer via `contrib/oss-fuzz/build.sh`; see `../../contrib/oss-fuzz/README`. +`CAMPAIGN.md` answers the two questions that come up as soon as the +campaign is longer than a coffee break -- how to split the budget across +the targets, and how long it is worth running -- from a measured +12-core hour rather than from intuition. Both answers are unobvious: +half the targets reach 99% of their final coverage within 93 seconds, +and a flat allocation spends most of its budget on targets that finished +in the first minute. `contrib/oss-fuzz/run_campaign.sh` implements the +conclusion: + + contrib/oss-fuzz/run_campaign.sh -b /path/to/build.sh-output -p nightly + Environment knobs, all read once at startup: LE_FUZZ_ITERATIONS iterations of the built-in driver