commit 336a8f28f600a46b1419874225b707c322decbab parent e47b2d1d36c4b9af044ad9fff17efe5b1069bfac Author: Christian Grothoff <christian@grothoff.org> Date: Wed, 29 Jul 2026 12:01:47 +0200 add OSSfuzz support Diffstat:
295 files changed, 6244 insertions(+), 65 deletions(-)
diff --git a/configure.ac b/configure.ac @@ -643,8 +643,24 @@ CFLAGS="$CFLAGS_OLD" CPPFLAGS="$CPPFLAGS_OLD" -AC_CHECK_PROG([HAVE_ZZUF],[zzuf], 1, 0) -AM_CONDITIONAL(HAVE_ZZUF, test 0 != $HAVE_ZZUF) +# Fuzzing harnesses (src/fuzz). They are only meaningful in a build with +# sanitizers, and they call functions that are hidden in the shared +# objects, so they need the static archives. +AC_MSG_CHECKING([whether to build the fuzzing harnesses]) +AC_ARG_ENABLE([fuzzing], + [AS_HELP_STRING([--enable-fuzzing], + [build the in-process fuzzing harnesses in src/fuzz and run them as ] + [part of "make check"; requires a static build of the library and is ] + [only really useful together with a sanitizer build [no]])], + [], [enable_fuzzing="no"]) +AS_IF([test "x$enable_fuzzing" = "xyes"], + [AS_IF([test "x$enable_static" = "xno"], + [AC_MSG_RESULT([no]) + AC_MSG_ERROR([--enable-fuzzing requires --enable-static])], + [AC_MSG_RESULT([yes])])], + [enable_fuzzing="no" + AC_MSG_RESULT([no])]) +AM_CONDITIONAL([ENABLE_FUZZING], [test "x$enable_fuzzing" = "xyes"]) LE_INTLINCL="" LE_LIBINTL="$LTLIBINTL" @@ -714,6 +730,7 @@ src/intlemu/Makefile src/common/Makefile src/main/Makefile src/plugins/Makefile +src/fuzz/Makefile ]) AX_CREATE_PKGCONFIG_INFO([libextractor.pc],,[-lextractor],[Metadata extraction library],,) diff --git a/contrib/oss-fuzz/Dockerfile b/contrib/oss-fuzz/Dockerfile @@ -0,0 +1,29 @@ +# Copyright 2026 Google LLC +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. + +FROM gcr.io/oss-fuzz-base/base-builder + +# autotools + gettext to run ./bootstrap, zlib because the container +# formats (deb, png, qt, and the ZIP based office formats) need it, and +# zip because make_seed_corpus.sh packages the seed corpora. +RUN apt-get update && \ + apt-get install -y --no-install-recommends \ + autoconf automake libtool pkg-config gettext autopoint \ + zlib1g-dev zip && \ + rm -rf /var/lib/apt/lists/* + +RUN git clone --depth 1 https://git.gnunet.org/libextractor.git libextractor + +WORKDIR libextractor +COPY build.sh $SRC/ diff --git a/contrib/oss-fuzz/README b/contrib/oss-fuzz/README @@ -0,0 +1,137 @@ +GNU libextractor -- OSS-Fuzz integration +======================================== + +This directory is the OSS-Fuzz project definition for libextractor. The +harness *sources* live in `src/fuzz/`; read `src/fuzz/README` first, it +explains what each target does and why the harnesses find things that +running `extract` over mutated files does not. + + build.sh compiles the harnesses against the engine + Dockerfile the OSS-Fuzz builder image + project.yaml project metadata + make_seed_corpus.sh builds the per-target seed corpora + dicts/ libFuzzer dictionaries + *.options per-target libFuzzer options + + +------------------------------------------------------------------- +1. What gets built +------------------------------------------------------------------- + +25 targets by default: four core targets (`fuzz_datasource`, `fuzz_ipc`, +`fuzz_convert`, `fuzz_unzip`) and one per in-tree parser +(`fuzz_applefile` ... `fuzz_zip`). + +Two things are deliberately left out. + +`fuzz_extract` needs `lt_dlopen()` to load the plugin modules, and this +build configures `--disable-shared` because the per-plugin targets link +the plugin sources directly. A target that loads uninstrumented shared +objects gives the engine no feedback from the code it is supposed to be +fuzzing, so shipping it here would be worse than not shipping it. It is +built and run in tree by `make -C src/fuzz check`. + +The plugins that wrap a third-party parser -- gif, jpeg, tiff, flac, +ogg, archive, mime -- are not built either. giflib, libjpeg, libtiff, +FLAC, libvorbis, libarchive and libmagic each have their own OSS-Fuzz +project; linking an uninstrumented copy here would attribute their bugs +to libextractor and would make the MemorySanitizer configuration +impossible. For a *local* campaign that wants the glue code covered -- +the part that turns what those libraries return into a metadata +callback, which is ours -- set `LE_FUZZ_EXTRA_PLUGINS=1` and build.sh +adds them. + +The ole2 plugin is a third case. It is excluded for the same reason -- +libgsf and glib are not instrumented here -- but unlike the wrappers +above it parses a good deal of the header itself before libgsf is ever +called, so leaving it out leaves *our* code unfuzzed. A signed-overflow +defect sat in exactly that code (issue 13) until a campaign covered the +target. Set `LE_FUZZ_GSF=1` to keep libgsf in the configure line and +build `fuzz_ole2`; it needs pkg-config to find `libgsf-1`, and warns and +skips the target if it cannot. Any local campaign should use it. + + +------------------------------------------------------------------- +2. Running it locally +------------------------------------------------------------------- + +build.sh works standalone on any machine with clang and +`libclang-rt-dev`. It never writes into the source tree, but it does +need a tree with no in-tree `config.status`: + + rsync -a --exclude=.git --exclude='.libs' --exclude='*.o' \ + /path/to/libextractor/ /tmp/le-src/ + + WORK=/tmp/le-work OUT=/tmp/le-out LE_SRC=/tmp/le-src \ + /tmp/le-src/contrib/oss-fuzz/build.sh + + unzip -q /tmp/le-out/fuzz_unzip_seed_corpus.zip -d /tmp/corpus + /tmp/le-out/fuzz_unzip /tmp/corpus \ + -dict=/tmp/le-out/fuzz_unzip.dict -max_total_time=600 + +On a Debian toolchain clang may not find libstdc++ on its own; add + + CXX="clang++ --gcc-install-dir=/usr/lib/gcc/x86_64-linux-gnu/15" + +Variables build.sh honours, all with defensive defaults so that the +script is runnable by hand: `SRC`, `LE_SRC`, `WORK`, `OUT`, `CC`, `CXX`, +`CFLAGS`, `CXXFLAGS`, `LIB_FUZZING_ENGINE`, `SANITIZER`, +`FUZZING_ENGINE`, `ARCHITECTURE`, plus the three local-only extras +`LE_FUZZ_EXTRA_PLUGINS`, `LE_FUZZ_GSF` and `LE_UBSAN_HALT`. + + FUZZING_ENGINE=afl ./build.sh # AFL++ targets + FUZZING_ENGINE=none ./build.sh # the harnesses' own driver, + # no engine needed at all + SANITIZER=undefined ./build.sh # UBSan-only campaign + +Via OSS-Fuzz's own helper: + + python infra/helper.py build_image libextractor + python infra/helper.py build_fuzzers --sanitizer address libextractor + python infra/helper.py check_build libextractor + python infra/helper.py run_fuzzer libextractor fuzz_unzip + + +------------------------------------------------------------------- +3. UndefinedBehaviorSanitizer is left recovering by default +------------------------------------------------------------------- + +The local `SANITIZER=address` default folds UBSan into the ASan build +but does *not* pass `-fno-sanitize-recover=undefined`. + +That is a deliberate departure from what OSS-Fuzz does, and the reason is +worth knowing before changing it: libextractor's parsers contain a +handful of signed-shift and misaligned-load sites that fire on almost +every input (`pack.c`, `riff_extractor.c`, `elf_extractor.c`, +`real_extractor.c`). With UBSan halting, every run dies within seconds +on one of those and ASan never gets to the memory-safety bugs, which are +the ones that matter. UBSan reports each source location once per +process either way, so nothing is lost from the logs. + +Set `LE_UBSAN_HALT=1` for the OSS-Fuzz behaviour, or use +`SANITIZER=undefined` for a campaign whose whole point is UB. On +OSS-Fuzz proper the two sanitizers are separate builds and this does not +arise. + + +------------------------------------------------------------------- +4. Seed corpora +------------------------------------------------------------------- + +`make_seed_corpus.sh` assembles three sources per target: + + 1. the checked-in `src/fuzz/corpus/<target>/`, which + `make -C src/fuzz refresh-corpus` regenerates from each harness' + built-in seeds; + 2. the real files under `src/plugins/testdata/`, mapped to targets by + filename prefix and prefixed with the harness' configuration bytes + (all zero, which selects exactly what production does). This is the + part that matters: a fuzzer that has to *invent* a valid ZIP central + directory before it reaches the interesting code will not get there; + 3. `src/fuzz/corpus/known-findings/<target>-*`, the reproducers of the + entries in `issues.txt`, so that each stays a permanent regression + in the OSS-Fuzz corpus too. + +The testdata files are deliberately not duplicated into +`src/fuzz/corpus/`: they are already in the tree, and the prefix is +cheap to add at build time. diff --git a/contrib/oss-fuzz/build.sh b/contrib/oss-fuzz/build.sh @@ -0,0 +1,467 @@ +#!/bin/bash -eu +# +# OSS-Fuzz build script for GNU libextractor. +# +# This file is in the public domain. +# +# It is executed inside the OSS-Fuzz base-builder image, which exports: +# +# $SRC parent directory of the checked-out sources +# ($SRC/libextractor, see Dockerfile) +# $WORK scratch directory for build artifacts +# $OUT where the finished fuzz targets must be installed +# $CC $CXX the instrumented compilers +# $CFLAGS $CXXFLAGS sanitizer + coverage flags; MUST be honoured and +# MUST NOT be replaced +# $LIB_FUZZING_ENGINE the fuzzing engine to link against +# $SANITIZER address | undefined | memory | coverage +# $FUZZING_ENGINE libfuzzer | afl | honggfuzz | centipede | none +# +# The same script can be run outside OSS-Fuzz for a local campaign; every +# variable above has a defensive default below. See contrib/oss-fuzz/README. + +# The shebang carries -eu, but "bash build.sh" silently drops it, and a +# harness that fails to link would then leave $OUT short one target while +# the script still exits 0. +set -eu + +SRC="${SRC:-$(cd "$(dirname "$0")/../../.." && pwd)}" +WORK="${WORK:-${SRC}/work}" +OUT="${OUT:-${SRC}/out}" +SANITIZER="${SANITIZER:-address}" +FUZZING_ENGINE="${FUZZING_ENGINE:-libfuzzer}" +ARCHITECTURE="${ARCHITECTURE:-x86_64}" + +# --- engine ---------------------------------------------------------------- +# +# Under OSS-Fuzz $CC, $CXX and $LIB_FUZZING_ENGINE come from the +# base-builder image and MUST be used as given, so all of this is dead +# code there. It fires only on a local run, where its job is to make +# "FUZZING_ENGINE=afl ./build.sh" produce a real AFL++ target rather than +# a libFuzzer one that happens to link. +# +# _le_cov_cflags is the coverage instrumentation the engine needs at +# compile time. Getting it wrong is the failure mode that matters: the +# build succeeds, the target runs, and it finds nothing, because the +# engine has no feedback signal at all. +case "${FUZZING_ENGINE}" in + libfuzzer) + CC="${CC:-clang}" + CXX="${CXX:-clang++}" + LIB_FUZZING_ENGINE="${LIB_FUZZING_ENGINE:--fsanitize=fuzzer}" + _le_cov_cflags="-fsanitize=fuzzer-no-link" + _le_main="engine" + ;; + afl) + CC="${CC:-afl-clang-fast}" + CXX="${CXX:-afl-clang-fast++}" + LIB_FUZZING_ENGINE="${LIB_FUZZING_ENGINE:-/usr/lib/afl/libAFLDriver.a}" + _le_cov_cflags="" + _le_main="engine" + ;; + honggfuzz) + CC="${CC:-hfuzz-clang}" + CXX="${CXX:-hfuzz-clang++}" + LIB_FUZZING_ENGINE="${LIB_FUZZING_ENGINE:-}" + _le_cov_cflags="" + _le_main="engine" + ;; + none) + # No engine: link the harnesses' own deterministic driver instead. + # This is what makes a sanitizer-only smoke test possible with no + # fuzzing engine installed at all. + CC="${CC:-clang}" + CXX="${CXX:-clang++}" + LIB_FUZZING_ENGINE="${LIB_FUZZING_ENGINE:-}" + _le_cov_cflags="" + _le_main="builtin" + ;; + *) + echo "ERROR: unknown FUZZING_ENGINE='${FUZZING_ENGINE}'" >&2 + echo " expected: libfuzzer | afl | honggfuzz | none" >&2 + exit 1 + ;; +esac + +case "${ARCHITECTURE}" in + x86_64) _le_arch_cflags="" ;; + i386) _le_arch_cflags="-m32 -no-pie" ;; + *) + echo "ERROR: unknown ARCHITECTURE='${ARCHITECTURE}'" >&2 + exit 1 + ;; +esac + +# --- $CFLAGS / $CXXFLAGS --------------------------------------------------- +# +# Dead code under OSS-Fuzz, which always exports these. On a local run +# the defaults have to make it a *real* fuzzing build; two flags decide +# that, and leaving either out produces a build that looks fine and finds +# nothing: +# +# -fsanitize=fuzzer-no-link installs libFuzzer's coverage +# instrumentation in every translation unit. Without it there is +# no feedback signal, `cov:` stays flat and the corpus never grows. +# a sanitizer libFuzzer only notices a crash the kernel +# delivers. ASan is what turns a silently tolerated over-read of +# a parser buffer -- the whole bug class this project has -- into +# a report. +if [ -z "${CFLAGS:-}" ]; then + _le_base_cflags="-O1 -fno-omit-frame-pointer -gline-tables-only" + + case "${SANITIZER}" in + address) + # OSS-Fuzz runs "address" and "undefined" as separate campaigns so + # that each report is attributed to one sanitizer. A local run has + # an afternoon at most, so the default folds UBSan into the ASan + # build: two oracles per CPU-hour instead of one. + # + # UBSan is deliberately left *recovering* here (no + # -fno-sanitize-recover): libextractor's parsers trip a handful of + # signed-shift and misaligned-load sites on almost every input, and + # halting on those would stop the run before ASan ever gets to the + # memory-safety bugs. UBSan reports each source location once per + # process, so they still all show up in the log. Set + # LE_UBSAN_HALT=1 for the OSS-Fuzz behaviour of dying on the first. + _le_san_cflags="-fsanitize=address,undefined" + _le_san_cflags="${_le_san_cflags} -fsanitize-address-use-after-scope" + if [ "${LE_UBSAN_HALT:-0}" != "0" ]; then + _le_san_cflags="${_le_san_cflags} -fno-sanitize-recover=undefined" + fi + ;; + undefined) + _le_san_cflags="-fsanitize=undefined -fno-sanitize-recover=undefined" + ;; + memory) + # Only usable when the C library and every dependency is + # instrumented too -- true inside the OSS-Fuzz image, essentially + # never on a distro toolchain. This is also why the build below + # disables every optional third-party parser: an uninstrumented + # libjpeg or GnuTLS would poison every run. + _le_san_cflags="-fsanitize=memory -fsanitize-memory-track-origins" + ;; + coverage) + _le_san_cflags="-fprofile-instr-generate -fcoverage-mapping" + ;; + none | "") + _le_san_cflags="" + ;; + *) + echo "ERROR: unknown SANITIZER='${SANITIZER}'" >&2 + exit 1 + ;; + esac + + CFLAGS="${_le_arch_cflags} ${_le_base_cflags} ${_le_san_cflags}" + if [ "${SANITIZER}" != "coverage" ]; then + CFLAGS="${CFLAGS} ${_le_cov_cflags}" + fi + unset _le_base_cflags _le_san_cflags +fi +CXXFLAGS="${CXXFLAGS:-${CFLAGS}}" +if [ "${_le_main}" = "builtin" ]; then + _le_no_main="" +else + _le_no_main="-DFUZZ_NO_MAIN" +fi +unset _le_arch_cflags _le_cov_cflags _le_main + +LE_SRC="${LE_SRC:-${SRC}/libextractor}" + +# Out-of-tree build: build.sh never modifies the checkout, which is what +# makes "run it twice" and "reproduce against a pristine tree" work. +BUILD="${WORK}/le-build" + +mkdir -p "${WORK}" "${OUT}" "${BUILD}" + +echo "=== libextractor OSS-Fuzz build ===" +echo " LE_SRC = ${LE_SRC}" +echo " BUILD = ${BUILD}" +echo " OUT = ${OUT}" +echo " SANITIZER = ${SANITIZER}" +echo " FUZZING_ENGINE = ${FUZZING_ENGINE}" +echo " LIB_FUZZING_ENGINE = ${LIB_FUZZING_ENGINE}" +echo " CC / CXX = ${CC} / ${CXX}" +echo " CFLAGS = ${CFLAGS}" + +# --------------------------------------------------------------------------- +# 1. Bootstrap (the git checkout ships no 'configure') +# --------------------------------------------------------------------------- +cd "${LE_SRC}" +if [ ! -x ./configure ]; then + echo "--- bootstrapping ---" + ./bootstrap || true + if [ ! -x ./configure ]; then + autoreconf -f -I m4 -i + fi +fi + +# --------------------------------------------------------------------------- +# 2. Configure +# --------------------------------------------------------------------------- +# Rationale for each flag: +# +# --enable-static +# fuzz_datasource, fuzz_unzip and fuzz_ipc call symbols that are +# compiled with $(HIDDEN_VISIBILITY_CFLAGS) and are therefore NOT +# exported from libextractor.so. Only the static archive can be +# linked, and OSS-Fuzz wants static targets anyway: a binary in +# $OUT must not depend on anything outside $OUT. +# --enable-fuzzing +# configures src/fuzz/Makefile. The harnesses are compiled by +# hand below, but keeping this on makes configure fail loudly if +# src/fuzz/ ever stops being wired up. +# --disable-shared +# the per-plugin targets link the plugin's own .c files, so no +# plugin module is loaded at run time and building them would only +# cost time. Note that this is what makes fuzz_extract useless in +# this configuration; see the $FUZZERS selection below. +# --disable-gsf --disable-glib +# step 3 only builds src/common and src/main, so no plugin that +# wraps a third-party parser is ever compiled here: those are +# fuzzed by their own OSS-Fuzz projects, and an uninstrumented copy +# of one would poison the MemorySanitizer build. Turning off the +# two that configure would otherwise pull into the *library* link +# keeps the dependency set at libc + zlib. +# LE_FUZZ_GSF=1 keeps libgsf in, which is what it takes to build the ole2 +# target (see $GSF_PLUGIN_FUZZER below). Off by default because libgsf +# and glib are not instrumented here, so every allocation they make is +# invisible to ASan and impossible under MemorySanitizer -- the same +# reason the other third-party wrappers are opt-in. Turn it on for a +# local campaign: ole2_extractor.c does a good deal of its own parsing +# before libgsf is ever called, and that part is ours. +_le_gsf_conf="--disable-gsf --disable-glib" +if [ "${LE_FUZZ_GSF:-0}" != "0" ]; then + _le_gsf_conf="" +fi + +cd "${BUILD}" +# shellcheck disable=SC2086 +"${LE_SRC}/configure" \ + --enable-static \ + --disable-shared \ + --with-pic \ + --enable-fuzzing \ + ${_le_gsf_conf} \ + --disable-dependency-tracking \ + CC="${CC}" \ + CFLAGS="${CFLAGS}" \ + LDFLAGS="${LDFLAGS:-}" + +# --------------------------------------------------------------------------- +# 3. Build the libraries the harnesses link against +# --------------------------------------------------------------------------- +make -j"$(nproc)" -C src/common +make -j"$(nproc)" -C src/main libextractor.la + +LE_LIB="${BUILD}/src/main/.libs/libextractor.a" +LE_COMMON="${BUILD}/src/common/.libs/libextractor_common.a" +for f in "${LE_LIB}" "${LE_COMMON}"; do + test -f "${f}" || { echo "ERROR: ${f} was not produced" >&2; exit 1; } +done + +# The static archives do not record their own dependencies, so read them +# off the libtool archives instead of hardcoding a list here. Which ones +# are needed depends on what configure found (-lbz2, -lltdl, -ltidy, +# -lapparmor, ...), so a hardcoded list would silently rot. +le_deps () +{ + sed -n "s/^dependency_libs='\(.*\)'\$/\1/p" "$1" 2>/dev/null +} +LE_SYSLIBS="$(le_deps "${BUILD}/src/main/libextractor.la") \ +$(le_deps "${BUILD}/src/common/libextractor_common.la")" +echo " LE_SYSLIBS = ${LE_SYSLIBS}" + +# --------------------------------------------------------------------------- +# 4. Compile the harnesses as fuzzing-engine translation units +# --------------------------------------------------------------------------- +LE_INCLUDES=( + -I"${BUILD}" + -I"${LE_SRC}" + -I"${LE_SRC}/src/include" + -I"${LE_SRC}/src/common" + -I"${LE_SRC}/src/main" + -I"${LE_SRC}/src/plugins" + -I"${LE_SRC}/src/fuzz" +) + +# fuzz_extract is deliberately absent: it dlopen()s the installed plugin +# modules, which this build does not produce (--disable-shared), and a +# target that loads uninstrumented shared objects gives the engine no +# feedback from the code it is meant to be fuzzing. It is built and run +# in tree by "make -C src/fuzz check" instead. +CORE_FUZZERS="fuzz_datasource fuzz_ipc fuzz_convert fuzz_unzip" + +# name:extra sources:extra libs (the plugin's own objects and its +# dependencies; keep in sync with src/fuzz/Makefile.am) +PLUGIN_FUZZERS=" +applefile:applefile_extractor.c pack.c: +dvi:dvi_extractor.c: +elf:elf_extractor.c pack.c: +it:it_extractor.c: +man:man_extractor.c: +nsf:nsf_extractor.c: +nsfe:nsfe_extractor.c: +ps:ps_extractor.c: +real:real_extractor.c: +riff:riff_extractor.c:-lm +rtf:rtf_extractor.c: +s3m:s3m_extractor.c: +sid:sid_extractor.c: +wav:wav_extractor.c: +xm:xm_extractor.c: +deb:deb_extractor.c:-lz +msoffice:msoffice_extractor.c:-lz +odf:odf_extractor.c:-lz +png:png_extractor.c:-lz +qt:qt_extractor.c:-lz +zip:zip_extractor.c:-lz +" + +# Plugins that wrap a third-party parser. NOT part of the OSS-Fuzz build: +# giflib, libjpeg, libtiff, FLAC, libvorbis, libarchive and libmagic all +# have their own OSS-Fuzz projects, an uninstrumented copy of one would +# attribute its bugs to libextractor, and it would make the +# MemorySanitizer configuration impossible. Set LE_FUZZ_EXTRA_PLUGINS=1 +# for a local campaign that wants the *glue* -- the part that turns what +# the library returns into a metadata callback, which is ours -- covered +# as well. +EXTRA_PLUGIN_FUZZERS=" +gif:gif_extractor.c:-lgif +jpeg:jpeg_extractor.c:-ljpeg +tiff:tiff_extractor.c:-ltiff +flac:flac_extractor.c:-lFLAC +ogg:ogg_extractor.c:-lvorbisfile -lvorbis -logg +archive:archive_extractor.c:-larchive +mime:mime_extractor.c:-lmagic +" + +# ole2 is separate from the list above because it is the one plugin that +# needs extra *compiler* flags rather than just a -l, so it can only be +# built when configure kept libgsf (LE_FUZZ_GSF=1). It is worth the +# trouble: ole2_extractor.c parses the header fields itself before it +# hands the stream to libgsf, and a signed-overflow defect sat in exactly +# that code until a campaign finally covered this target. +GSF_PLUGIN_FUZZER="" +if [ "${LE_FUZZ_GSF:-0}" != "0" ]; then + if _le_gsf_cflags="$(pkg-config --cflags libgsf-1 2>/dev/null)" && + _le_gsf_libs="$(pkg-config --libs libgsf-1 2>/dev/null)"; then + GSF_PLUGIN_FUZZER=" +ole2:ole2_extractor.c:${_le_gsf_libs} +" + LE_GSF_CFLAGS="${_le_gsf_cflags}" + else + echo "WARNING: LE_FUZZ_GSF=1 but pkg-config cannot find libgsf-1;" >&2 + echo " the ole2 target will be skipped." >&2 + fi +fi + +if [ "${LE_FUZZ_EXTRA_PLUGINS:-0}" != "0" ]; then + PLUGIN_FUZZERS="${PLUGIN_FUZZERS}${EXTRA_PLUGIN_FUZZERS}" +fi +PLUGIN_FUZZERS="${PLUGIN_FUZZERS}${GSF_PLUGIN_FUZZER}" + +# Upper-case the plugin name for -DLE_FUZZ_ID. +upper () { echo "$1" | tr '[:lower:]' '[:upper:]'; } + +build_one () +{ + local target="$1"; shift + local objs="" + local extra_libs="$1"; shift + local o + + for src in "$@"; do + o="${WORK}/${target}-$(basename "${src}" .c).o" + # shellcheck disable=SC2086 + $CC $CFLAGS ${_le_no_main} ${LE_EXTRA_CPPFLAGS:-} \ + "${LE_INCLUDES[@]}" -c "${src}" -o "${o}" + objs="${objs} ${o}" + done + # Link with $CXX: $LIB_FUZZING_ENGINE is a C++ archive for most engines. + # shellcheck disable=SC2086 + $CXX $CXXFLAGS ${objs} -o "${OUT}/${target}" \ + $LIB_FUZZING_ENGINE "${LE_LIB}" "${LE_COMMON}" ${extra_libs} \ + ${LE_SYSLIBS} -lz -lpthread -ldl +} + +FUZZERS="" + +for f in ${CORE_FUZZERS}; do + echo "--- building ${f} ---" + LE_EXTRA_CPPFLAGS="" \ + build_one "${f}" "" "${LE_SRC}/src/fuzz/${f}.c" + FUZZERS="${FUZZERS} ${f}" +done + +# Iterate line by line, not word by word: the middle field holds a +# space-separated source list. A pipe into `while read` would put the +# loop in a subshell and lose $FUZZERS, hence the IFS dance. +_le_oldifs="${IFS}" +IFS=' +' +for line in ${PLUGIN_FUZZERS}; do + IFS="${_le_oldifs}" + [ -n "${line}" ] || { IFS=' +'; continue; } + name="${line%%:*}" + rest="${line#*:}" + srcs="${rest%%:*}" + libs="${rest#*:}" + target="fuzz_${name}" + echo "--- building ${target} ---" + set -- + for s in ${srcs}; do + set -- "$@" "${LE_SRC}/src/plugins/${s}" + done + # ole2_extractor.c includes <gsf/gsf-*.h>, so it is the one plugin that + # needs include paths of its own; everything else compiles with the + # library's flags alone. + _le_plug_cppflags="-DLE_FUZZ_PLUGIN=${name} -DLE_FUZZ_ID=$(upper "${name}")" + if [ "${name}" = "ole2" ]; then + _le_plug_cppflags="${_le_plug_cppflags} ${LE_GSF_CFLAGS:-}" + fi + LE_EXTRA_CPPFLAGS="${_le_plug_cppflags}" \ + build_one "${target}" "${libs}" "${LE_SRC}/src/fuzz/fuzz_plugin.c" "$@" + FUZZERS="${FUZZERS} ${target}" + IFS=' +' +done +IFS="${_le_oldifs}" +unset _le_oldifs + +# --------------------------------------------------------------------------- +# 5. Seed corpora, dictionaries and .options files +# --------------------------------------------------------------------------- +FUZZBIN="${OUT}" "${LE_SRC}/contrib/oss-fuzz/make_seed_corpus.sh" \ + "${LE_SRC}" "${OUT}" + +for f in ${FUZZERS}; do + d="${LE_SRC}/contrib/oss-fuzz/dicts/${f}.dict" + [ -f "${d}" ] || d="${LE_SRC}/contrib/oss-fuzz/dicts/fuzz_plugin.dict" + cp "${d}" "${OUT}/${f}.dict" + o="${LE_SRC}/contrib/oss-fuzz/${f}.options" + [ -f "${o}" ] || o="${LE_SRC}/contrib/oss-fuzz/default.options" + cp "${o}" "${OUT}/${f}.options" +done + +# --------------------------------------------------------------------------- +# 6. Verify: every requested target must actually be in $OUT +# --------------------------------------------------------------------------- +# Belt and braces for the failure that matters most -- a build that +# reports success but ships nothing, which on OSS-Fuzz shows up only as a +# target that never runs. +_le_missing="" +for f in ${FUZZERS}; do + [ -x "${OUT}/${f}" ] || _le_missing="${_le_missing} ${f}" + [ -f "${OUT}/${f}_seed_corpus.zip" ] || + _le_missing="${_le_missing} ${f}_seed_corpus.zip" +done +if [ -n "${_le_missing}" ]; then + echo "ERROR: build did not produce:${_le_missing}" >&2 + exit 1 +fi +unset _le_missing + +echo "=== done; contents of \$OUT ===" +ls -la "${OUT}" diff --git a/contrib/oss-fuzz/default.options b/contrib/oss-fuzz/default.options @@ -0,0 +1,4 @@ +[libfuzzer] +max_len = 262144 +timeout = 25 +rss_limit_mb = 2560 diff --git a/contrib/oss-fuzz/dicts/fuzz_convert.dict b/contrib/oss-fuzz/dicts/fuzz_convert.dict @@ -0,0 +1,19 @@ +# Charset selector bytes plus the byte sequences that break converters. +"UTF-8" +"UTF-16BE" +"UTF-16LE" +"ISO-8859-1" +"CP1252" +"\xef\xbb\xbf" +"\xff\xfe" +"\xfe\xff" +"\xc0\x80" +"\xe0\x80\x80" +"\xf0\x80\x80\x80" +"\xed\xa0\x80" +"\xed\xbf\xbf" +"\xc3\xa4" +"\xe2\x82\xac" +"\xf0\x9f\x98\x80" +"\x80" +"\xff" diff --git a/contrib/oss-fuzz/dicts/fuzz_datasource.dict b/contrib/oss-fuzz/dicts/fuzz_datasource.dict @@ -0,0 +1,14 @@ +"\x1f\x8b\x08\x00" +"\x1f\x8b\x08\x04" +"\x1f\x8b\x08\x08" +"\x1f\x8b\x08\x10" +"\x1f\x8b\x08\x1c" +"BZh1" +"BZh9" +"\x31\x41\x59\x26\x53\x59" +"\x17\x72\x45\x38\x50\x90" +"\x78\x9c" +"\x78\xda" +"\x01\x00\x00\xff\xff" +"\xff\xff" +"\x00\x00\x00\x00" diff --git a/contrib/oss-fuzz/dicts/fuzz_extract.dict b/contrib/oss-fuzz/dicts/fuzz_extract.dict @@ -0,0 +1,142 @@ +# Dictionary shared by every fuzz_<plugin> target. +# +# libFuzzer inserts these verbatim, which is what gets a mutated input +# past a magic-number test that random bytes would never satisfy. The +# chunk tags matter as much as the magic numbers: most of these parsers +# are a loop over four-character tags, and a tag the loop recognises is +# what opens up the code behind it. + +# container magic +"\x89PNG\x0d\x0a\x1a\x0a" +"GIF87a" +"GIF89a" +"\xff\xd8\xff\xe0" +"\xff\xd8\xff\xe1" +"RIFF" +"WAVE" +"AVI " +"PK\x03\x04" +"PK\x01\x02" +"PK\x05\x06" +"PK\x07\x08" +"\x7fELF" +"%!PS-Adobe-" +"{\\rtf1" +"NESM\x1a" +"NSFE" +"PSID" +"RSID" +"SCRM" +"IMPM" +"Extended Module: " +"MThd" +"MTrk" +"fLaC" +"OggS" +"\xd0\xcf\x11\xe0\xa1\xb1\x1a\xe1" +"\x1f\x8b\x08" +"BZh9" +"!<arch>\x0a" +"debian-binary" +"\xed\xab\xee\xdb" +"\x00\x05\x16\x00" +"\x00\x05\x16\x07" +"\xf7\x02" +".RMF" +"II\x2a\x00" +"MM\x00\x2a" + +# PNG / generic big-endian chunk tags +"IHDR" +"PLTE" +"IDAT" +"IEND" +"tEXt" +"zTXt" +"iTXt" +"tIME" +"pHYs" +"gAMA" + +# RIFF / AVI / WAV +"LIST" +"INFO" +"fmt " +"data" +"INAM" +"IART" +"ICMT" +"ICRD" +"IGNR" +"ISFT" +"avih" +"strh" +"strf" +"movi" +"JUNK" + +# QuickTime / MP4 +"moov" +"mvhd" +"trak" +"mdia" +"minf" +"stbl" +"udta" +"meta" +"ilst" +"ftyp" +"mdat" +"cmov" +"\xa9nam" +"\xa9ART" +"\xa9cmt" + +# RealMedia +"PROP" +"MDPR" +"CONT" +"DATA" +"INDX" + +# ZIP members libextractor looks for +"mimetype" +"meta.xml" +"content.xml" +"META-INF/manifest.xml" +"docProps/core.xml" +"docProps/app.xml" +"word/document.xml" +"xl/workbook.xml" +"ppt/presentation.xml" +"application/vnd.oasis.opendocument." + +# XML / text markers +"<dc:title>" +"<dc:creator>" +"<meta:" +"<office:meta>" +"%%Title:" +"%%Creator:" +"%%Pages:" +"%%BoundingBox:" +"%%EOF" +".TH " +".SH " +"NAME" +"SYNOPSIS" +"\\info" +"\\title" +"\\author" +"\\*\\" +"\\u" +"\\'" + +# lengths and counters that tend to be the interesting ones +"\x00\x00\x00\x00" +"\xff\xff\xff\xff" +"\x7f\xff\xff\xff" +"\x80\x00\x00\x00" +"\x00\x00\x00\x01" +"\xff\xff" +"\x7f\xff" diff --git a/contrib/oss-fuzz/dicts/fuzz_ipc.dict b/contrib/oss-fuzz/dicts/fuzz_ipc.dict @@ -0,0 +1,18 @@ +# Opcodes and header shapes of the plugin -> core IPC protocol; see +# src/main/extractor_ipc.h. +"\x03" +"\x04" +"\x05" +"\x06" +"\x07" +# MetaMessage header: opcode, reserved, format, type, mime_length, +# value_size +"\x05\x00\x01\x00\x01\x00\x00\x00\x00\x00\x00\x00" +"\x05\x00\x02\x00\x01\x00\x00\x00\x00\x00\x00\x00" +"\x05\x00\x03\x00\x01\x00\x00\x00\x00\x00\x00\x00" +"\x00\x80" +"\x00\x00\x01\x00" +"\xff\xff" +"\xff\xff\xff\xff" +"text/plain\x00" +"image/png\x00" diff --git a/contrib/oss-fuzz/dicts/fuzz_plugin.dict b/contrib/oss-fuzz/dicts/fuzz_plugin.dict @@ -0,0 +1,142 @@ +# Dictionary shared by every fuzz_<plugin> target. +# +# libFuzzer inserts these verbatim, which is what gets a mutated input +# past a magic-number test that random bytes would never satisfy. The +# chunk tags matter as much as the magic numbers: most of these parsers +# are a loop over four-character tags, and a tag the loop recognises is +# what opens up the code behind it. + +# container magic +"\x89PNG\x0d\x0a\x1a\x0a" +"GIF87a" +"GIF89a" +"\xff\xd8\xff\xe0" +"\xff\xd8\xff\xe1" +"RIFF" +"WAVE" +"AVI " +"PK\x03\x04" +"PK\x01\x02" +"PK\x05\x06" +"PK\x07\x08" +"\x7fELF" +"%!PS-Adobe-" +"{\\rtf1" +"NESM\x1a" +"NSFE" +"PSID" +"RSID" +"SCRM" +"IMPM" +"Extended Module: " +"MThd" +"MTrk" +"fLaC" +"OggS" +"\xd0\xcf\x11\xe0\xa1\xb1\x1a\xe1" +"\x1f\x8b\x08" +"BZh9" +"!<arch>\x0a" +"debian-binary" +"\xed\xab\xee\xdb" +"\x00\x05\x16\x00" +"\x00\x05\x16\x07" +"\xf7\x02" +".RMF" +"II\x2a\x00" +"MM\x00\x2a" + +# PNG / generic big-endian chunk tags +"IHDR" +"PLTE" +"IDAT" +"IEND" +"tEXt" +"zTXt" +"iTXt" +"tIME" +"pHYs" +"gAMA" + +# RIFF / AVI / WAV +"LIST" +"INFO" +"fmt " +"data" +"INAM" +"IART" +"ICMT" +"ICRD" +"IGNR" +"ISFT" +"avih" +"strh" +"strf" +"movi" +"JUNK" + +# QuickTime / MP4 +"moov" +"mvhd" +"trak" +"mdia" +"minf" +"stbl" +"udta" +"meta" +"ilst" +"ftyp" +"mdat" +"cmov" +"\xa9nam" +"\xa9ART" +"\xa9cmt" + +# RealMedia +"PROP" +"MDPR" +"CONT" +"DATA" +"INDX" + +# ZIP members libextractor looks for +"mimetype" +"meta.xml" +"content.xml" +"META-INF/manifest.xml" +"docProps/core.xml" +"docProps/app.xml" +"word/document.xml" +"xl/workbook.xml" +"ppt/presentation.xml" +"application/vnd.oasis.opendocument." + +# XML / text markers +"<dc:title>" +"<dc:creator>" +"<meta:" +"<office:meta>" +"%%Title:" +"%%Creator:" +"%%Pages:" +"%%BoundingBox:" +"%%EOF" +".TH " +".SH " +"NAME" +"SYNOPSIS" +"\\info" +"\\title" +"\\author" +"\\*\\" +"\\u" +"\\'" + +# lengths and counters that tend to be the interesting ones +"\x00\x00\x00\x00" +"\xff\xff\xff\xff" +"\x7f\xff\xff\xff" +"\x80\x00\x00\x00" +"\x00\x00\x00\x01" +"\xff\xff" +"\x7f\xff" diff --git a/contrib/oss-fuzz/dicts/fuzz_unzip.dict b/contrib/oss-fuzz/dicts/fuzz_unzip.dict @@ -0,0 +1,18 @@ +"PK\x03\x04" +"PK\x01\x02" +"PK\x05\x06" +"PK\x07\x08" +"PK\x06\x06" +"PK\x06\x07" +"mimetype" +"meta.xml" +"content.xml" +"META-INF/manifest.xml" +"docProps/core.xml" +"word/document.xml" +"xl/workbook.xml" +"\x00\x00" +"\xff\xff" +"\xff\xff\xff\xff" +"\x00\x08" +"\x08\x00" diff --git a/contrib/oss-fuzz/fuzz_convert.options b/contrib/oss-fuzz/fuzz_convert.options @@ -0,0 +1,6 @@ +[libfuzzer] +# iconv conversion is O(n) and the interesting inputs are short +# multi-byte sequences, not long ones. +max_len = 4096 +timeout = 25 +rss_limit_mb = 2560 diff --git a/contrib/oss-fuzz/fuzz_extract.options b/contrib/oss-fuzz/fuzz_extract.options @@ -0,0 +1,6 @@ +[libfuzzer] +# One input is handed to every loaded plugin in turn, so executions are +# comparatively expensive; the timeout is correspondingly higher. +max_len = 262144 +timeout = 60 +rss_limit_mb = 4096 diff --git a/contrib/oss-fuzz/fuzz_ipc.options b/contrib/oss-fuzz/fuzz_ipc.options @@ -0,0 +1,7 @@ +[libfuzzer] +# The IPC message stream is short by construction: MAX_META_DATA caps a +# single metadata item at 32 KiB and the parser is a flat loop over +# fixed-size headers, so longer inputs only cost execution time. +max_len = 65536 +timeout = 25 +rss_limit_mb = 2560 diff --git a/contrib/oss-fuzz/make_seed_corpus.sh b/contrib/oss-fuzz/make_seed_corpus.sh @@ -0,0 +1,187 @@ +#!/bin/sh +# +# Build the seed corpora for the libextractor fuzz targets. +# +# This file is in the public domain. +# +# Usage: +# make_seed_corpus.sh SRCDIR OUTDIR [--plain] +# +# SRCDIR the libextractor source tree +# OUTDIR where to write <target>_seed_corpus.zip (OSS-Fuzz layout), +# or, with --plain, where to write the loose corpus files +# --plain write loose files into OUTDIR/<target>/ instead of zips; +# this is what "make -C src/fuzz refresh-corpus" wants +# +# Two things go into a corpus: +# +# 1. the harness' own built-in seeds, if the built binaries are +# available (they can write them out with --write-corpus=DIR), and +# +# 2. the real files under src/plugins/testdata/, which are the whole +# point: a fuzzer that has to *invent* a valid OLE2 FAT or a valid +# ZIP central directory before it reaches the interesting code will +# not get there in any reasonable time. Each is prefixed with the +# configuration bytes the harness expects, all zero, which selects +# exactly what production does (see fuzz_ec.h). +# +# The mapping from testdata file to target is by filename prefix, which +# is the convention src/plugins/testdata/ already follows. + +set -eu + +SRCDIR="${1:?usage: make_seed_corpus.sh SRCDIR OUTDIR [--plain]}" +OUTDIR="${2:?usage: make_seed_corpus.sh SRCDIR OUTDIR [--plain]}" +MODE="${3:-zip}" + +TESTDATA="${SRCDIR}/src/plugins/testdata" +FUZZBIN="${FUZZBIN:-}" + +mkdir -p "${OUTDIR}" +WORK=$(mktemp -d) +trap 'rm -rf "${WORK}"' EXIT + +# Number of leading zero bytes each harness wants in front of the file +# image. Keep in sync with LE_FUZZ_EC_PREFIX (fuzz_ec.h) and with the +# input formats documented at the top of the core harnesses. +prefix_len () +{ + case "$1" in + fuzz_extract) echo 2 ;; + fuzz_datasource) echo 4 ;; + fuzz_ipc) echo 0 ;; + fuzz_convert) echo 2 ;; + *) echo 4 ;; # fuzz_unzip and every fuzz_<plugin> + esac +} + +# Which testdata files seed which target. A target with no entry gets +# only its built-in seeds. +testdata_glob () +{ + case "$1" in + fuzz_applefile) echo "applefile_*" ;; + fuzz_archive) echo "archive_*" ;; + fuzz_deb) echo "deb_*" ;; + fuzz_dvi) echo "dvi_*" ;; + fuzz_elf) echo "chello-elf" ;; + fuzz_flac) echo "flac_*" ;; + fuzz_gif) echo "gif_*" ;; + fuzz_html) echo "html_*" ;; + fuzz_it) echo "it_*" ;; + fuzz_jpeg) echo "jpeg_* exiv2_* thumbnail_*" ;; + fuzz_man) echo "man_*" ;; + fuzz_mime) echo "*" ;; + fuzz_mpeg) echo "mpeg_*" ;; + fuzz_msoffice) echo "msoffice_*" ;; + fuzz_nsf) echo "nsf_*" ;; + fuzz_nsfe) echo "nsfe_*" ;; + fuzz_odf) echo "odf_*" ;; + fuzz_ogg) echo "ogg_*" ;; + fuzz_ole2) echo "ole2_* msoffice_biff4.xls" ;; + fuzz_png) echo "png_*" ;; + fuzz_ps) echo "ps_*" ;; + fuzz_qt) echo "gstreamer_sample_sorenson.mov" ;; + fuzz_real) echo "ra3.ra audiosig.rm" ;; + fuzz_riff) echo "riff_* wav_*" ;; + fuzz_rtf) echo "rtf_*" ;; + fuzz_s3m) echo "s3m_*" ;; + fuzz_sid) echo "sid_*" ;; + fuzz_tiff) echo "tiff_*" ;; + fuzz_wav) echo "wav_*" ;; + fuzz_xm) echo "xm_*" ;; + fuzz_zip) echo "zip_* odf_* msoffice_excel.xlsx msoffice_word.docx" ;; + fuzz_unzip) echo "zip_* odf_* msoffice_excel.xlsx msoffice_word.docx \ + msoffice_powerpoint.pptx" ;; + fuzz_datasource) echo "deb_* zip_* png_* ogg_*" ;; + fuzz_extract) echo "*" ;; + *) echo "" ;; + esac +} + +# All targets we know about. Kept explicit rather than derived from the +# build so that this script also works before anything is built. +TARGETS="fuzz_extract fuzz_datasource fuzz_ipc fuzz_convert fuzz_unzip \ +fuzz_applefile fuzz_dvi fuzz_elf fuzz_it fuzz_man fuzz_nsf fuzz_nsfe \ +fuzz_ps fuzz_real fuzz_riff fuzz_rtf fuzz_s3m fuzz_sid fuzz_wav fuzz_xm \ +fuzz_deb fuzz_msoffice fuzz_odf fuzz_png fuzz_qt fuzz_zip \ +fuzz_gif fuzz_jpeg fuzz_tiff fuzz_flac fuzz_ogg fuzz_archive fuzz_mime \ +fuzz_ole2 fuzz_mpeg fuzz_html" + +for t in ${TARGETS}; do + d="${WORK}/${t}" + mkdir -p "${d}" + + # 1a. the checked-in corpus, which "make -C src/fuzz refresh-corpus" + # regenerates from the harnesses' built-in seeds + if [ -d "${SRCDIR}/src/fuzz/corpus/${t}" ]; then + cp "${SRCDIR}/src/fuzz/corpus/${t}"/* "${d}/" 2>/dev/null || true + fi + + # 1b. built-in seeds straight from the binary, if one that understands + # --write-corpus is around. A binary linked against libFuzzer does + # NOT: our main() is compiled out, libFuzzer's own argument parser + # sees an unknown flag and starts fuzzing, so this must be bounded. + if [ -n "${FUZZBIN}" ] && [ -x "${FUZZBIN}/${t}" ]; then + timeout 10 "${FUZZBIN}/${t}" --write-corpus="${d}" >/dev/null 2>&1 || true + fi + + # 2. testdata, with the configuration prefix prepended + n=$(prefix_len "${t}") + if [ "${n}" -gt 0 ]; then + # a file of n zero bytes + : > "${WORK}/prefix" + i=0 + while [ "${i}" -lt "${n}" ]; do + printf '\000' >> "${WORK}/prefix" + i=$((i + 1)) + done + fi + # `set -f` matters: the glob list comes back from a command + # substitution and would otherwise be expanded against the *current* + # directory before the loop ever starts. + set -f + for g in $(testdata_glob "${t}"); do + set +f + for f in "${TESTDATA}"/${g}; do + [ -f "${f}" ] || continue + case "${f}" in + */README) continue ;; + esac + b=$(basename "${f}") + if [ "${n}" -gt 0 ]; then + cat "${WORK}/prefix" "${f}" > "${d}/td-${b}" + else + cp "${f}" "${d}/td-${b}" + fi + done + set -f + done + set +f + + # 3. reproducers of past findings, so that they stay permanent + # regressions in the OSS-Fuzz corpus as well + kf="${SRCDIR}/src/fuzz/corpus/known-findings" + if [ -d "${kf}" ]; then + for f in "${kf}/${t}"-*; do + [ -f "${f}" ] || continue + cp "${f}" "${d}/$(basename "${f}")" + done + fi + + # A target whose binary was not built and that has no testdata mapping + # would otherwise produce an empty archive, which `zip` refuses to + # create; give it a one-byte seed so that the corpus always exists. + if [ -z "$(ls -A "${d}" 2>/dev/null)" ]; then + printf '\000' > "${d}/empty" + fi + + if [ "${MODE}" = "--plain" ]; then + mkdir -p "${OUTDIR}/${t}" + cp "${d}"/* "${OUTDIR}/${t}/" + echo "corpus: ${OUTDIR}/${t} ($(ls -1 "${OUTDIR}/${t}" | wc -l) files)" + else + (cd "${d}" && zip -q -r "${OUTDIR}/${t}_seed_corpus.zip" . ) + echo "corpus: ${OUTDIR}/${t}_seed_corpus.zip ($(ls -1 "${d}" | wc -l) files)" + fi +done diff --git a/contrib/oss-fuzz/project.yaml b/contrib/oss-fuzz/project.yaml @@ -0,0 +1,18 @@ +homepage: "https://www.gnu.org/software/libextractor/" +language: c +primary_contact: "grothoff@gnu.org" +auto_ccs: + - "libextractor@gnu.org" +main_repo: "https://git.gnunet.org/libextractor.git" +file_github_issue: false +fuzzing_engines: + - libfuzzer + - afl + - honggfuzz +sanitizers: + - address + - undefined + - memory +architectures: + - x86_64 + - i386 diff --git a/src/Makefile.am b/src/Makefile.am @@ -3,4 +3,8 @@ if WANT_FRAMEWORK INTLEMU_SUBDIRS = intlemu endif -SUBDIRS = include $(INTLEMU_SUBDIRS) main common plugins . +if ENABLE_FUZZING + FUZZ_SUBDIRS = fuzz +endif + +SUBDIRS = include $(INTLEMU_SUBDIRS) main common plugins $(FUZZ_SUBDIRS) . diff --git a/src/fuzz/BUILD-INTEGRATION.md b/src/fuzz/BUILD-INTEGRATION.md @@ -0,0 +1,196 @@ +# Build integration for `src/fuzz/` + +> **Status: applied.** The three changes below are already present in +> this tree — `configure.ac` carries `--enable-fuzzing` and the +> `ENABLE_FUZZING` conditional, `AC_CONFIG_FILES` lists +> `src/fuzz/Makefile`, and `src/Makefile.am` adds `fuzz` to `SUBDIRS` +> under that conditional. This file is kept as the record of what +> changed and as the recipe for porting `src/fuzz/` to another branch. +> +> For running these same harnesses on OSS-Fuzz — a different build path +> that does **not** go through `src/fuzz/Makefile.am` — see +> `../../contrib/oss-fuzz/` and §5 at the end of this file. + +`src/fuzz/Makefile.am` is complete. Three files outside `src/fuzz/` +had to be touched, plus the removal of the script this directory +replaces. + +The harnesses are guarded by the automake conditional `ENABLE_FUZZING` +(`--enable-fuzzing`, default **no**), because they are only meaningful in +a build with sanitizers and because they need static archives of the +library: they call functions that are hidden in the shared objects. + +--- + +## 1. `configure.ac` + +### 1.1 The option and the conditional + +Added where the old `AC_CHECK_PROG([HAVE_ZZUF], ...)` pair used to be: + +```m4 +AC_MSG_CHECKING([whether to build the fuzzing harnesses]) +AC_ARG_ENABLE([fuzzing], + [AS_HELP_STRING([--enable-fuzzing], + [build the in-process fuzzing harnesses in src/fuzz and run them as ] + [part of "make check"; requires a static build of the library and is ] + [only really useful together with a sanitizer build [no]])], + [], [enable_fuzzing="no"]) +AS_IF([test "x$enable_fuzzing" = "xyes"], + [AS_IF([test "x$enable_static" = "xno"], + [AC_MSG_RESULT([no]) + AC_MSG_ERROR([--enable-fuzzing requires --enable-static])], + [AC_MSG_RESULT([yes])])], + [enable_fuzzing="no" + AC_MSG_RESULT([no])]) +AM_CONDITIONAL([ENABLE_FUZZING], [test "x$enable_fuzzing" = "xyes"]) +``` + +### 1.2 Register the new `Makefile` + +`src/fuzz/Makefile` added to the final `AC_CONFIG_FILES([...])` list. + +--- + +## 2. `src/Makefile.am` + +```make +if ENABLE_FUZZING + FUZZ_SUBDIRS = fuzz +endif + +SUBDIRS = include $(INTLEMU_SUBDIRS) main common plugins $(FUZZ_SUBDIRS) . +``` + +Automake derives `DIST_SUBDIRS` from all branches of `SUBDIRS` +automatically, so `make dist` keeps shipping `src/fuzz/` regardless of +the conditional. + +--- + +## 3. `src/plugins/Makefile.am` — remove the old zzuf test + +`fuzz_default.sh` is gone. Three hunks were removed with it: its +`EXTRA_DIST` entry, the + +```make +if HAVE_ZZUF + fuzz_tests=fuzz_default.sh +endif +``` + +block, and `$(fuzz_tests)` from `TESTS`. The `HAVE_ZZUF` conditional +itself is left in `configure.ac` because `contrib/coverage.sh` still +drives zzuf. + +--- + +## 4. Build and run + +```sh +./bootstrap +./configure --enable-fuzzing --enable-static \ + CC=clang \ + CFLAGS="-g -O1 -fno-omit-frame-pointer \ + -fsanitize=address,undefined \ + -fsanitize-address-use-after-scope" \ + LDFLAGS="-fsanitize=address,undefined" +make +make -C src/fuzz check +``` + +A longer session, and the corpus replay a CI job should run after a fix: + +```sh +make -C src/fuzz check LE_FUZZ_ITERATIONS=5000000 LE_FUZZ_SEED=$RANDOM +make -C src/fuzz check-corpus +``` + +--- + +## 5. Notes and caveats + +* **`--enable-static` is mandatory.** `fuzz_datasource`, `fuzz_unzip`, + `fuzz_ipc` and `fuzz_convert` call `EXTRACTOR_datasource_read_()`, + `EXTRACTOR_common_unzip_open()`, `EXTRACTOR_IPC_process_reply_()` and + friends. `src/main/Makefile.am` builds the library with + `$(HIDDEN_VISIBILITY_CFLAGS)` and an `-export-symbols-regex`, so those + symbols are not in `libextractor.so`. The `-static` in the per-target + `_LDFLAGS` makes libtool pick `.libs/libextractor.a`. + +* **`fuzz_extract` is the exception** — it deliberately does *not* link + `-static`, because it needs `lt_dlopen()` to find the plugin modules + at run time. It also needs `LIBEXTRACTOR_PREFIX` to point at them; + `AM_TESTS_ENVIRONMENT` sets it to `src/plugins/.libs`. + +* **Per-plugin targets compile plugin sources a second time.** Each + `fuzz_<plugin>` lists the plugin's `.c` files in + `nodist_<target>_SOURCES` with its own `_CPPFLAGS`, so automake gives + the objects a per-target prefix (`fuzz_riff-riff_extractor.o`) and + they never collide with the ones `src/plugins/` builds. Automake + warns about `subdir-objects` for these; the warning is about a future + automake changing where those objects land, not about the current + build being wrong. Enabling `subdir-objects` globally is the eventual + fix and is a tree-wide change, so it is deliberately not made here. + +* **The plugin list in `Makefile.am` must stay in sync with three other + places** when a target is added: `fuzz_plugin_name.h` (the format + description), `contrib/oss-fuzz/build.sh` (`PLUGIN_FUZZERS`) and + `contrib/oss-fuzz/make_seed_corpus.sh` (`testdata_glob`). §6 of the + README lists the steps. + +* `make check` here is a *smoke test*, not a campaign: 20000 iterations + per harness of the built-in generator. It exists so that a harness + that stops compiling, stops linking or starts crashing on its own seed + corpus is caught by an ordinary `make check`. + +--- + +## 6. The OSS-Fuzz build path (does not use this `Makefile.am`) + +`contrib/oss-fuzz/build.sh` builds the *same harness sources* for +libFuzzer/AFL++/honggfuzz without going through `src/fuzz/Makefile.am` +at all. It configures the library out of tree, then compiles each +harness by hand with `-DFUZZ_NO_MAIN` and links it against +`$LIB_FUZZING_ENGINE`. + +Two consequences for anyone editing `src/fuzz/`: + +* **`LLVMFuzzerTestOneInput()` must stay unconditional.** Only `main()` + may be inside `#ifndef FUZZ_NO_MAIN`; a harness whose fuzz target is + itself conditional silently produces an empty OSS-Fuzz binary. + +* **Anything the fuzz target needs must not live inside the + `#ifndef FUZZ_NO_MAIN` block of `fuzz_common.h`.** The PRNG, the crash + bookkeeping, `fuzz_report_finding()`, `fuzz_env_ulong()` and + `fuzz_ignore_sigpipe()` are outside it; the generator loop, the + mutator, the corpus walker and `main()` are inside. + + This split is easy to get wrong and the failure is silent, so it is + worth spelling out the shape of it. `signal (SIGPIPE, SIG_IGN)` + belongs to the *target*: under the built-in driver everything would + look perfect, while under `-DFUZZ_NO_MAIN` the call would vanish and + the process would die on the first `SIGPIPE` with no stack trace, no + artifact and no crash report — which reads exactly like a clean run + that found nothing. It is therefore `fuzz_ignore_sigpipe()`, called + from both the driver and every `LLVMFuzzerTestOneInput()`. + + The lesson generalises: a bug in this split cannot be caught by + `make -C src/fuzz check`, because that path always defines `main()`. + After touching `fuzz_common.h`, build the OSS-Fuzz way as well and + confirm the targets still run: + + ```sh + rsync -a --exclude=.git . /tmp/le-fuzz-src/ # a tree with no + # in-tree config.status + WORK=/tmp/le-fuzz-work OUT=/tmp/le-fuzz-out \ + LE_SRC=/tmp/le-fuzz-src /tmp/le-fuzz-src/contrib/oss-fuzz/build.sh + /tmp/le-fuzz-out/fuzz_unzip -runs=10000 + echo "exit=$?" # anything but 0 here is a bug in the harness, + # not a finding + ``` + +`contrib/oss-fuzz/` also relies on two things this directory provides: +`make -C src/fuzz refresh-corpus` (which regenerates `corpus/`) and the +`corpus/known-findings/` reproducers, which it packages into the +per-target `_seed_corpus.zip` so that they become permanent regressions. diff --git a/src/fuzz/Makefile.am b/src/fuzz/Makefile.am @@ -0,0 +1,405 @@ +# This Makefile.am is in the public domain + +# In-process fuzzing harnesses. See README in this directory. +# +# The harnesses are built only with --enable-fuzzing (see +# BUILD-INTEGRATION.md). They are ordinary check_PROGRAMS with a +# built-in deterministic driver, so they need neither clang nor +# libFuzzer nor AFL++; with those available the very same sources are +# compiled into libFuzzer/AFL++ targets by contrib/oss-fuzz/build.sh. + +SUBDIRS = . + +# Number of generate/mutate iterations per harness during "make check". +# Raise for a real fuzzing session, e.g. +# make LE_FUZZ_ITERATIONS=1000000 LE_FUZZ_SEED=42 check +LE_FUZZ_ITERATIONS = 20000 + +# PRNG seed; every run is exactly reproducible from (harness, seed). +LE_FUZZ_SEED = 1 + +# Where reproducers for failing inputs are written. +LE_FUZZ_CRASH_DIR = crashes + +# Set to 1 to also flag string metadata that is not 0-terminated. Off by +# default: it is a contract violation rather than a memory error, and it +# is common enough in the older plugins to drown everything else. +LE_FUZZ_STRICT_PROC = 0 + +AM_CPPFLAGS = \ + -I$(top_srcdir)/src/include \ + -I$(top_srcdir)/src/common \ + -I$(top_srcdir)/src/main \ + -I$(top_srcdir)/src/fuzz + +AM_TESTS_ENVIRONMENT = \ + LE_FUZZ_ITERATIONS="$(LE_FUZZ_ITERATIONS)" ; \ + export LE_FUZZ_ITERATIONS ; \ + LE_FUZZ_SEED="$(LE_FUZZ_SEED)" ; export LE_FUZZ_SEED ; \ + LE_FUZZ_CRASH_DIR="$(LE_FUZZ_CRASH_DIR)" ; \ + export LE_FUZZ_CRASH_DIR ; \ + LE_FUZZ_STRICT_PROC="$(LE_FUZZ_STRICT_PROC)" ; \ + export LE_FUZZ_STRICT_PROC ; \ + LIBEXTRACTOR_PREFIX="$(abs_top_builddir)/src/plugins/.libs" ; \ + export LIBEXTRACTOR_PREFIX ; + +if USE_COVERAGE + AM_CFLAGS = --coverage -O0 + XLIB = -lgcov +endif + +# fuzz_datasource, fuzz_unzip and fuzz_ipc call functions that are +# internal to the library and are therefore hidden in the shared object; +# they are linked against the static archives, which requires +# --enable-static (the default). +LE_LIB = $(top_builddir)/src/main/libextractor.la +LE_COMMON = $(top_builddir)/src/common/libextractor_common.la + +noinst_HEADERS = \ + fuzz_common.h \ + fuzz_ec.h \ + fuzz_plugin_name.h + +EXTRA_DIST = \ + README \ + BUILD-INTEGRATION.md \ + corpus + +CLEANFILES = \ + $(LE_FUZZ_CRASH_DIR)/*.bin + + +# --------------------------------------------------------------------- +# Core harnesses +# --------------------------------------------------------------------- + +CORE_FUZZERS = \ + fuzz_extract \ + fuzz_datasource \ + fuzz_ipc \ + fuzz_convert + +fuzz_extract_SOURCES = fuzz_extract.c +fuzz_extract_LDADD = $(LE_LIB) $(XLIB) + +fuzz_datasource_SOURCES = fuzz_datasource.c +fuzz_datasource_LDFLAGS = -static +fuzz_datasource_LDADD = $(LE_LIB) $(XLIB) + +fuzz_ipc_SOURCES = fuzz_ipc.c +fuzz_ipc_LDFLAGS = -static +fuzz_ipc_LDADD = $(LE_LIB) $(XLIB) + +fuzz_convert_SOURCES = fuzz_convert.c +fuzz_convert_LDFLAGS = -static +fuzz_convert_LDADD = $(LE_COMMON) $(LE_LIB) $(XLIB) + +if HAVE_ZLIB +UNZIP_FUZZER = fuzz_unzip +fuzz_unzip_SOURCES = fuzz_unzip.c +fuzz_unzip_LDFLAGS = -static +fuzz_unzip_LDADD = $(LE_COMMON) $(XLIB) -lz +endif + + +# --------------------------------------------------------------------- +# Per-plugin harnesses +# +# Each of these compiles fuzz_plugin.c against the plugin's own sources, +# so the parser is instrumented and no dlopen()/fork() is involved. The +# two -D flags are described in fuzz_plugin_name.h. +# --------------------------------------------------------------------- + +PLUGIN_DIR = $(top_srcdir)/src/plugins + +# Plugins that are always built: pure in-tree parsers with no external +# dependency. These are where libextractor's own bugs live. +PLAIN_PLUGIN_FUZZERS = \ + fuzz_applefile \ + fuzz_dvi \ + fuzz_elf \ + fuzz_it \ + fuzz_man \ + fuzz_nsf \ + fuzz_nsfe \ + fuzz_ps \ + fuzz_real \ + fuzz_riff \ + fuzz_rtf \ + fuzz_s3m \ + fuzz_sid \ + fuzz_wav \ + fuzz_xm + +fuzz_applefile_SOURCES = fuzz_plugin.c +fuzz_applefile_CPPFLAGS = $(AM_CPPFLAGS) -I$(PLUGIN_DIR) \ + -DLE_FUZZ_PLUGIN=applefile -DLE_FUZZ_ID=APPLEFILE +fuzz_applefile_LDADD = $(XLIB) +nodist_fuzz_applefile_SOURCES = \ + $(PLUGIN_DIR)/applefile_extractor.c $(PLUGIN_DIR)/pack.c + +fuzz_dvi_SOURCES = fuzz_plugin.c +fuzz_dvi_CPPFLAGS = $(AM_CPPFLAGS) -I$(PLUGIN_DIR) \ + -DLE_FUZZ_PLUGIN=dvi -DLE_FUZZ_ID=DVI +fuzz_dvi_LDADD = $(XLIB) $(SOCKET_LIBS) +nodist_fuzz_dvi_SOURCES = $(PLUGIN_DIR)/dvi_extractor.c + +fuzz_elf_SOURCES = fuzz_plugin.c +fuzz_elf_CPPFLAGS = $(AM_CPPFLAGS) -I$(PLUGIN_DIR) \ + -DLE_FUZZ_PLUGIN=elf -DLE_FUZZ_ID=ELF +fuzz_elf_LDADD = $(XLIB) $(SOCKET_LIBS) +nodist_fuzz_elf_SOURCES = \ + $(PLUGIN_DIR)/elf_extractor.c $(PLUGIN_DIR)/pack.c + +fuzz_it_SOURCES = fuzz_plugin.c +fuzz_it_CPPFLAGS = $(AM_CPPFLAGS) -I$(PLUGIN_DIR) \ + -DLE_FUZZ_PLUGIN=it -DLE_FUZZ_ID=IT +fuzz_it_LDADD = $(XLIB) +nodist_fuzz_it_SOURCES = $(PLUGIN_DIR)/it_extractor.c + +fuzz_man_SOURCES = fuzz_plugin.c +fuzz_man_CPPFLAGS = $(AM_CPPFLAGS) -I$(PLUGIN_DIR) \ + -DLE_FUZZ_PLUGIN=man -DLE_FUZZ_ID=MAN +fuzz_man_LDADD = $(XLIB) +nodist_fuzz_man_SOURCES = $(PLUGIN_DIR)/man_extractor.c + +fuzz_nsf_SOURCES = fuzz_plugin.c +fuzz_nsf_CPPFLAGS = $(AM_CPPFLAGS) -I$(PLUGIN_DIR) \ + -DLE_FUZZ_PLUGIN=nsf -DLE_FUZZ_ID=NSF +fuzz_nsf_LDADD = $(XLIB) +nodist_fuzz_nsf_SOURCES = $(PLUGIN_DIR)/nsf_extractor.c + +fuzz_nsfe_SOURCES = fuzz_plugin.c +fuzz_nsfe_CPPFLAGS = $(AM_CPPFLAGS) -I$(PLUGIN_DIR) \ + -DLE_FUZZ_PLUGIN=nsfe -DLE_FUZZ_ID=NSFE +fuzz_nsfe_LDFLAGS = -static +fuzz_nsfe_LDADD = $(LE_COMMON) $(XLIB) +nodist_fuzz_nsfe_SOURCES = $(PLUGIN_DIR)/nsfe_extractor.c + +fuzz_ps_SOURCES = fuzz_plugin.c +fuzz_ps_CPPFLAGS = $(AM_CPPFLAGS) -I$(PLUGIN_DIR) \ + -DLE_FUZZ_PLUGIN=ps -DLE_FUZZ_ID=PS +fuzz_ps_LDADD = $(XLIB) +nodist_fuzz_ps_SOURCES = $(PLUGIN_DIR)/ps_extractor.c + +fuzz_real_SOURCES = fuzz_plugin.c +fuzz_real_CPPFLAGS = $(AM_CPPFLAGS) -I$(PLUGIN_DIR) \ + -DLE_FUZZ_PLUGIN=real -DLE_FUZZ_ID=REAL +fuzz_real_LDADD = $(XLIB) +nodist_fuzz_real_SOURCES = $(PLUGIN_DIR)/real_extractor.c + +fuzz_riff_SOURCES = fuzz_plugin.c +fuzz_riff_CPPFLAGS = $(AM_CPPFLAGS) -I$(PLUGIN_DIR) \ + -DLE_FUZZ_PLUGIN=riff -DLE_FUZZ_ID=RIFF +fuzz_riff_LDADD = $(XLIB) -lm +nodist_fuzz_riff_SOURCES = $(PLUGIN_DIR)/riff_extractor.c + +fuzz_rtf_SOURCES = fuzz_plugin.c +fuzz_rtf_CPPFLAGS = $(AM_CPPFLAGS) -I$(PLUGIN_DIR) \ + -DLE_FUZZ_PLUGIN=rtf -DLE_FUZZ_ID=RTF +fuzz_rtf_LDFLAGS = -static +fuzz_rtf_LDADD = $(LE_COMMON) $(XLIB) +nodist_fuzz_rtf_SOURCES = $(PLUGIN_DIR)/rtf_extractor.c + +fuzz_s3m_SOURCES = fuzz_plugin.c +fuzz_s3m_CPPFLAGS = $(AM_CPPFLAGS) -I$(PLUGIN_DIR) \ + -DLE_FUZZ_PLUGIN=s3m -DLE_FUZZ_ID=S3M +fuzz_s3m_LDADD = $(XLIB) +nodist_fuzz_s3m_SOURCES = $(PLUGIN_DIR)/s3m_extractor.c + +fuzz_sid_SOURCES = fuzz_plugin.c +fuzz_sid_CPPFLAGS = $(AM_CPPFLAGS) -I$(PLUGIN_DIR) \ + -DLE_FUZZ_PLUGIN=sid -DLE_FUZZ_ID=SID +fuzz_sid_LDADD = $(XLIB) +nodist_fuzz_sid_SOURCES = $(PLUGIN_DIR)/sid_extractor.c + +fuzz_wav_SOURCES = fuzz_plugin.c +fuzz_wav_CPPFLAGS = $(AM_CPPFLAGS) -I$(PLUGIN_DIR) \ + -DLE_FUZZ_PLUGIN=wav -DLE_FUZZ_ID=WAV +fuzz_wav_LDADD = $(XLIB) +nodist_fuzz_wav_SOURCES = $(PLUGIN_DIR)/wav_extractor.c + +fuzz_xm_SOURCES = fuzz_plugin.c +fuzz_xm_CPPFLAGS = $(AM_CPPFLAGS) -I$(PLUGIN_DIR) \ + -DLE_FUZZ_PLUGIN=xm -DLE_FUZZ_ID=XM +fuzz_xm_LDADD = $(XLIB) +nodist_fuzz_xm_SOURCES = $(PLUGIN_DIR)/xm_extractor.c + + +# Plugins that need zlib; these are the container formats, and the ZIP +# based ones share src/common/unzip.c. +if HAVE_ZLIB +ZLIB_PLUGIN_FUZZERS = \ + fuzz_deb \ + fuzz_msoffice \ + fuzz_odf \ + fuzz_png \ + fuzz_qt \ + fuzz_zip + +fuzz_deb_SOURCES = fuzz_plugin.c +fuzz_deb_CPPFLAGS = $(AM_CPPFLAGS) -I$(PLUGIN_DIR) \ + -DLE_FUZZ_PLUGIN=deb -DLE_FUZZ_ID=DEB +fuzz_deb_LDADD = $(XLIB) -lz +nodist_fuzz_deb_SOURCES = $(PLUGIN_DIR)/deb_extractor.c + +fuzz_msoffice_SOURCES = fuzz_plugin.c +fuzz_msoffice_CPPFLAGS = $(AM_CPPFLAGS) -I$(PLUGIN_DIR) \ + -DLE_FUZZ_PLUGIN=msoffice -DLE_FUZZ_ID=MSOFFICE +fuzz_msoffice_LDFLAGS = -static +fuzz_msoffice_LDADD = $(LE_COMMON) $(XLIB) -lz +nodist_fuzz_msoffice_SOURCES = $(PLUGIN_DIR)/msoffice_extractor.c + +fuzz_odf_SOURCES = fuzz_plugin.c +fuzz_odf_CPPFLAGS = $(AM_CPPFLAGS) -I$(PLUGIN_DIR) \ + -DLE_FUZZ_PLUGIN=odf -DLE_FUZZ_ID=ODF +fuzz_odf_LDFLAGS = -static +fuzz_odf_LDADD = $(LE_COMMON) $(XLIB) -lz +nodist_fuzz_odf_SOURCES = $(PLUGIN_DIR)/odf_extractor.c + +fuzz_png_SOURCES = fuzz_plugin.c +fuzz_png_CPPFLAGS = $(AM_CPPFLAGS) -I$(PLUGIN_DIR) \ + -DLE_FUZZ_PLUGIN=png -DLE_FUZZ_ID=PNG +fuzz_png_LDFLAGS = -static +fuzz_png_LDADD = $(LE_COMMON) $(XLIB) -lz +nodist_fuzz_png_SOURCES = $(PLUGIN_DIR)/png_extractor.c + +fuzz_qt_SOURCES = fuzz_plugin.c +fuzz_qt_CPPFLAGS = $(AM_CPPFLAGS) -I$(PLUGIN_DIR) \ + -DLE_FUZZ_PLUGIN=qt -DLE_FUZZ_ID=QT +fuzz_qt_LDADD = $(XLIB) -lz +nodist_fuzz_qt_SOURCES = $(PLUGIN_DIR)/qt_extractor.c + +fuzz_zip_SOURCES = fuzz_plugin.c +fuzz_zip_CPPFLAGS = $(AM_CPPFLAGS) -I$(PLUGIN_DIR) \ + -DLE_FUZZ_PLUGIN=zip -DLE_FUZZ_ID=ZIP +fuzz_zip_LDFLAGS = -static +fuzz_zip_LDADD = $(LE_COMMON) $(XLIB) -lz +nodist_fuzz_zip_SOURCES = $(PLUGIN_DIR)/zip_extractor.c +endif + + +# Plugins that wrap a third-party parser. Their own code is thin, but +# the glue -- the part that turns what the library returns into a +# metadata callback -- is ours and is worth the same treatment. +if HAVE_GIF +GIF_FUZZER = fuzz_gif +fuzz_gif_SOURCES = fuzz_plugin.c +fuzz_gif_CPPFLAGS = $(AM_CPPFLAGS) -I$(PLUGIN_DIR) \ + -DLE_FUZZ_PLUGIN=gif -DLE_FUZZ_ID=GIF +fuzz_gif_LDADD = $(XLIB) -lgif +nodist_fuzz_gif_SOURCES = $(PLUGIN_DIR)/gif_extractor.c +endif + +if HAVE_JPEG +JPEG_FUZZER = fuzz_jpeg +fuzz_jpeg_SOURCES = fuzz_plugin.c +fuzz_jpeg_CPPFLAGS = $(AM_CPPFLAGS) -I$(PLUGIN_DIR) \ + -DLE_FUZZ_PLUGIN=jpeg -DLE_FUZZ_ID=JPEG +fuzz_jpeg_LDADD = $(XLIB) -ljpeg +nodist_fuzz_jpeg_SOURCES = $(PLUGIN_DIR)/jpeg_extractor.c +endif + +if HAVE_TIFF +TIFF_FUZZER = fuzz_tiff +fuzz_tiff_SOURCES = fuzz_plugin.c +fuzz_tiff_CPPFLAGS = $(AM_CPPFLAGS) -I$(PLUGIN_DIR) \ + -DLE_FUZZ_PLUGIN=tiff -DLE_FUZZ_ID=TIFF +fuzz_tiff_LDADD = $(XLIB) -ltiff +nodist_fuzz_tiff_SOURCES = $(PLUGIN_DIR)/tiff_extractor.c +endif + +if HAVE_FLAC +FLAC_FUZZER = fuzz_flac +fuzz_flac_SOURCES = fuzz_plugin.c +fuzz_flac_CPPFLAGS = $(AM_CPPFLAGS) -I$(PLUGIN_DIR) \ + -DLE_FUZZ_PLUGIN=flac -DLE_FUZZ_ID=FLAC +fuzz_flac_LDADD = $(XLIB) -lFLAC $(LE_LIBINTL) +nodist_fuzz_flac_SOURCES = $(PLUGIN_DIR)/flac_extractor.c +endif + +if HAVE_VORBISFILE +OGG_FUZZER = fuzz_ogg +fuzz_ogg_SOURCES = fuzz_plugin.c +fuzz_ogg_CPPFLAGS = $(AM_CPPFLAGS) -I$(PLUGIN_DIR) \ + -DLE_FUZZ_PLUGIN=ogg -DLE_FUZZ_ID=OGG +fuzz_ogg_LDADD = $(XLIB) -lvorbisfile -lvorbis -logg +nodist_fuzz_ogg_SOURCES = $(PLUGIN_DIR)/ogg_extractor.c +endif + +if HAVE_ARCHIVE +ARCHIVE_FUZZER = fuzz_archive +fuzz_archive_SOURCES = fuzz_plugin.c +fuzz_archive_CPPFLAGS = $(AM_CPPFLAGS) -I$(PLUGIN_DIR) \ + -DLE_FUZZ_PLUGIN=archive -DLE_FUZZ_ID=ARCHIVE +fuzz_archive_LDADD = $(XLIB) -larchive +nodist_fuzz_archive_SOURCES = $(PLUGIN_DIR)/archive_extractor.c +endif + +if HAVE_MAGIC +MIME_FUZZER = fuzz_mime +fuzz_mime_SOURCES = fuzz_plugin.c +fuzz_mime_CPPFLAGS = $(AM_CPPFLAGS) -I$(PLUGIN_DIR) \ + -DLE_FUZZ_PLUGIN=mime -DLE_FUZZ_ID=MIME +fuzz_mime_LDADD = $(XLIB) -lmagic +nodist_fuzz_mime_SOURCES = $(PLUGIN_DIR)/mime_extractor.c +endif + +if HAVE_GSF +OLE2_FUZZER = fuzz_ole2 +fuzz_ole2_SOURCES = fuzz_plugin.c +fuzz_ole2_CPPFLAGS = $(AM_CPPFLAGS) -I$(PLUGIN_DIR) $(GSF_CFLAGS) \ + -DLE_FUZZ_PLUGIN=ole2 -DLE_FUZZ_ID=OLE2 +fuzz_ole2_LDFLAGS = -static +fuzz_ole2_LDADD = $(LE_COMMON) $(XLIB) $(GSF_LIBS) +nodist_fuzz_ole2_SOURCES = $(PLUGIN_DIR)/ole2_extractor.c +endif + + +check_PROGRAMS = \ + $(CORE_FUZZERS) \ + $(UNZIP_FUZZER) \ + $(PLAIN_PLUGIN_FUZZERS) \ + $(ZLIB_PLUGIN_FUZZERS) \ + $(GIF_FUZZER) \ + $(JPEG_FUZZER) \ + $(TIFF_FUZZER) \ + $(FLAC_FUZZER) \ + $(OGG_FUZZER) \ + $(ARCHIVE_FUZZER) \ + $(MIME_FUZZER) \ + $(OLE2_FUZZER) + +TESTS = $(check_PROGRAMS) + +.NOTPARALLEL: + + +# Regenerate the on-disk seed corpus from the built-in one. +.PHONY: refresh-corpus +refresh-corpus: $(check_PROGRAMS) + for p in $(check_PROGRAMS) ; do \ + ./$$p --write-corpus=$(srcdir)/corpus || exit 1 ; \ + done + $(top_srcdir)/contrib/oss-fuzz/make_seed_corpus.sh \ + $(top_srcdir) $(srcdir)/corpus --plain + +# Replay the whole on-disk corpus through every harness; this is what a +# CI regression run should do after a crash has been fixed. +# +# corpus/known-findings/ is listed separately because --corpus-dir does +# not recurse. It holds the reproducers of the findings in issues.txt; +# anything failing there once its patch has been applied is a regression. +CORPUS_DIRS = \ + $(srcdir)/corpus \ + $(srcdir)/corpus/known-findings + +.PHONY: check-corpus +check-corpus: $(check_PROGRAMS) + for p in $(check_PROGRAMS) ; do \ + for d in $(CORPUS_DIRS) ; do \ + test -d $$d || continue ; \ + LIBEXTRACTOR_PREFIX="$(abs_top_builddir)/src/plugins/.libs" \ + ./$$p --corpus-dir=$$d || exit 1 ; \ + done ; \ + done diff --git a/src/fuzz/README b/src/fuzz/README @@ -0,0 +1,274 @@ +GNU libextractor -- in-process fuzzing harnesses +================================================ + +This directory replaces the old `src/plugins/fuzz_default.sh`, which ran +`zzuf` over the test corpus and invoked the `extract` *binary* once per +mutation. That approach had four problems, and each of them is what one +of the design decisions below is answering: + + * it forked a process per input, so it managed a few hundred + executions per second where an in-process harness manages hundreds of + thousands; + * `extract` runs plugins **out of process**, so a plugin that crashed + produced a dead child that the library dutifully restarted -- the + script saw a clean exit status and reported success; + * it had no sanitizer, so only a crash the kernel delivered was + noticed. A read of 200 bytes past the end of a 16 KiB shared memory + window is invisible without one, and that is precisely the shape of + almost every bug in this library; + * it had no coverage feedback, so the hundredth mutation of a file was + no more likely to reach new code than the first. + +Every harness here is *dual mode*: + + * it exports the libFuzzer entry point + + int LLVMFuzzerTestOneInput (const uint8_t *data, size_t size); + + so the same source links against clang/libFuzzer, AFL++ or + honggfuzz (see `../../contrib/oss-fuzz/`), and + + * it ships a **built-in standalone driver** (`fuzz_common.h`) with a + deterministic, seeded generator + mutator loop, so it is useful with + nothing but gcc and `-fsanitize=address,undefined`. + +The standalone driver is compiled unless `FUZZ_NO_MAIN` is defined. + + +------------------------------------------------------------------- +1. The harnesses +------------------------------------------------------------------- + +1.1 Per-plugin targets +---------------------- + +`fuzz_plugin.c` is compiled once per plugin, producing `fuzz_gif`, +`fuzz_png`, `fuzz_rtf` and so on -- 21 in-tree parsers plus one per +plugin that wraps a third-party library, when that library is present. +Each target links the plugin's own `.c` files, so every line of the +parser is instrumented and neither `dlopen()` nor `fork()` nor the IPC +layer is in the way. + +One target per format rather than one target for all of them, because +the corpus is what makes a format fuzzer work: `fuzz_png` starts from +PNG files and mutates PNGs, and its coverage feedback is not diluted by +inputs that only ever exercise the RTF tokeniser. + +1.2 Core targets +---------------- + +fuzz_extract end-to-end: `EXTRACTOR_plugin_add_defaults()` + + `EXTRACTOR_extract()`, plugins in-process. This is + the only target that covers plugin discovery, the + dispatch loop, and the plugins that wrap a + third-party library, all against the real plugin set. + +fuzz_datasource `src/main/extractor_datasource.c`: the first code in + the library to touch attacker bytes, and the only one + that runs *before* any plugin is consulted. The gzip + header walker (FEXTRA / FNAME / FCOMMENT / FHCRC) and + the "seek backwards through a decompressed stream" + path are the interesting parts. + +fuzz_unzip `src/common/unzip.c`, the in-tree ZIP reader shared + by the odf, msoffice and zip plugins -- hand-written + code descended from unzip 1.00, reachable from three + formats at once. The highest-value single target + here. + +fuzz_ipc `EXTRACTOR_IPC_process_reply_()`, the parser in the + *trusted parent* for a byte stream produced by an + *untrusted child*. That is the security boundary of + the whole out-of-process design, so it is fuzzed on + its own even though the function is short. + +fuzz_convert `EXTRACTOR_common_convert_to_utf8()` and the metatype + tables. The helper is called by nsfe, rtf, msoffice + and png with a length *and* a charset name that both + come out of the file being parsed. + + +------------------------------------------------------------------- +2. Why the harness finds things `extract` does not +------------------------------------------------------------------- + +The substance is in `fuzz_ec.h`, which models +`struct EXTRACTOR_ExtractContext` the way `extractor_plugin_main.c` +implements it, and then makes two of its properties enforceable. + +2.1 The read window is exact +---------------------------- + +`plugin_env_read()` hands the plugin a pointer *into a shared memory +window* and returns how many bytes are valid there. That is at most +`shm_map_size` -- 16 KiB by default -- and at most what is left of the +file. A plugin that asks for 100 KiB, gets 16 KiB, and then walks all +100 KiB is reading memory it was never given. In production that memory +is a live mmap, so nothing crashes, no test fails, and the bug is +invisible. + +Here every window is a fresh `malloc()` of *exactly* the returned byte +count, so ASAN's redzone starts at the first byte the plugin was not +promised. The window size is fuzzer-controlled (byte 0 of the input), +because "the read returned less than I asked for" is the single most +productive precondition in this library, and the 16 KiB default hides it +for every file smaller than that. + +2.2 The window slides +--------------------- + +A pointer from `read()` stays valid only while the window still covers +that part of the file. A `read()` or `seek()` that needs data outside +the window makes the core refill it, and the plugin's old pointer then +addresses *different file bytes* than it believes it holds. + +The model tracks the same window as `plugin_env_read()` and +`plugin_env_seek()` do, so a `seek (0, SEEK_CUR)` -- which never leaves +the window -- invalidates nothing, exactly as in production. When the +window really does slide, every slice handed out from it is freed, so a +retained pointer becomes an ASAN use-after-free. + +Getting this boundary right is the difference between a report worth +reading and noise: an earlier version invalidated on *every* seek and +immediately "found" a bug in png_extractor.c that does not exist under +either shipped implementation. `LE_FUZZ_STRICT_WINDOW=1` restores that +stricter behaviour on purpose, since it models the in-process +implementation, whose single `ctx->buf` really is overwritten by every +read. + +2.3 The metadata processor is an oracle +--------------------------------------- + +`transmit_reply()` writes `data_len` bytes starting at `data` to a pipe +and calls `strlen()` on the mime type. The harness touches exactly those +bytes, so a plugin that reports a length longer than its buffer is an +ASAN report here and a real out-of-bounds read in production. + +`LE_FUZZ_STRICT_PROC=1` additionally flags string metadata that is not +0-terminated. Off by default: that is a contract violation rather than a +memory error, and it is common enough in the older plugins to drown +everything else. + +2.4 Fault injection +------------------- + +Byte 1 of the input arms the failures that really can happen and that +plugins rarely handle: `get_size()` returning `UINT64_MAX` after a failed +IPC round, `read()` or `seek()` returning -1, the application asking to +stop after N items, and running the extract method twice against the same +context (plugins must not carry state from one file to the next -- the +same process handles every file in a directory walk). + + +------------------------------------------------------------------- +3. Input format +------------------------------------------------------------------- + +Every `fuzz_<plugin>` target and `fuzz_unzip` take: + + byte 0 read window size selector; 0 selects the production 16 KiB + byte 1 fault-injection bitmask (LE_FUZZ_FAULT_* in fuzz_ec.h) + byte 2 call index at which the injected fault fires + byte 3 auxiliary parameter + byte 4.. the file image + +An all-zero prefix is exactly what production does, so a corpus entry is +four zero bytes followed by a real file. That is what +`contrib/oss-fuzz/make_seed_corpus.sh` builds out of +`src/plugins/testdata/`. + +`fuzz_extract` uses a two-byte prefix, `fuzz_convert` a two-byte prefix, +`fuzz_datasource` four bytes, and `fuzz_ipc` none (its input is the raw +message stream). Each is documented at the top of its own source file. + + +------------------------------------------------------------------- +4. Building and running +------------------------------------------------------------------- + + ./bootstrap + ./configure --enable-fuzzing --enable-static \ + CC=clang \ + CFLAGS="-g -O1 -fno-omit-frame-pointer \ + -fsanitize=address,undefined" \ + LDFLAGS="-fsanitize=address,undefined" + make + make -C src/fuzz check + +`make check` runs every harness under its built-in driver for +`LE_FUZZ_ITERATIONS` iterations (20000 by default). + +Note that on a tree that does not carry the fixes from `issues.txt` this +`make check` *fails*, by design: the harnesses find those bugs within a +few thousand iterations. It cannot affect an ordinary build, because +`--enable-fuzzing` defaults to no and `src/fuzz/` is not even configured +without it. + +A longer session: + + make -C src/fuzz check LE_FUZZ_ITERATIONS=5000000 LE_FUZZ_SEED=$RANDOM + +Every run is fully reproducible from (harness, seed). A failing input is +dumped to `$LE_FUZZ_CRASH_DIR` and replayed with: + + ./fuzz_png --file=crashes/crash-fuzz_png-seed1-iter28.bin + +Replay the whole checked-in corpus, which is what CI should do after a +fix: + + make -C src/fuzz check-corpus + +For a real campaign use libFuzzer via `contrib/oss-fuzz/build.sh`; see +`../../contrib/oss-fuzz/README`. + +Environment knobs, all read once at startup: + + LE_FUZZ_ITERATIONS iterations of the built-in driver + LE_FUZZ_SEED PRNG seed + LE_FUZZ_TIMEOUT per-iteration watchdog, seconds (0 disables) + LE_FUZZ_CRASH_DIR where reproducers are written + LE_FUZZ_SKIP_SEEDS skip the built-in seed corpus at the start + LE_FUZZ_VERBOSE be chatty + LE_FUZZ_STRICT_WINDOW invalidate the read window on every call + LE_FUZZ_STRICT_PROC flag non-0-terminated string metadata + +`fuzz_extract` additionally needs `LIBEXTRACTOR_PREFIX` pointing at the +directory holding the built plugin modules, normally +`src/plugins/.libs`; `make check` sets it. + + +------------------------------------------------------------------- +5. The corpus +------------------------------------------------------------------- + +`corpus/<target>/` holds the harnesses' own built-in seeds and is +regenerated by + + make -C src/fuzz refresh-corpus + +It deliberately does *not* contain copies of `src/plugins/testdata/`: +those files are already in the tree, and +`contrib/oss-fuzz/make_seed_corpus.sh` prepends the configuration prefix +to them at build time instead. Run that script by hand to materialise +the full corpus for a local campaign. + +`corpus/known-findings/` holds the reproducers for the entries in +`issues.txt`. `make check-corpus` replays it, so each one stays a +permanent regression test. + + +------------------------------------------------------------------- +6. Adding a plugin target +------------------------------------------------------------------- + +1. Give the plugin an `LE_ID_*` number in `fuzz_plugin_name.h` and add + the `#elif` block with its magic bytes and body shape. This only + feeds the *generator*; getting it wrong costs coverage, never + soundness. +2. Add the target to `Makefile.am`, following one of the existing + blocks: `_SOURCES = fuzz_plugin.c`, `_CPPFLAGS` with the two `-D` + flags, `nodist_..._SOURCES` with the plugin's own sources, and the + libraries it needs. +3. Add it to `PLUGIN_FUZZERS` in `../../contrib/oss-fuzz/build.sh` and + to the `testdata_glob` table in + `../../contrib/oss-fuzz/make_seed_corpus.sh`. diff --git a/src/fuzz/corpus/fuzz_applefile/fuzz_applefile-000.bin b/src/fuzz/corpus/fuzz_applefile/fuzz_applefile-000.bin Binary files differ. diff --git a/src/fuzz/corpus/fuzz_applefile/fuzz_applefile-001.bin b/src/fuzz/corpus/fuzz_applefile/fuzz_applefile-001.bin Binary files differ. diff --git a/src/fuzz/corpus/fuzz_applefile/fuzz_applefile-002.bin b/src/fuzz/corpus/fuzz_applefile/fuzz_applefile-002.bin Binary files differ. diff --git a/src/fuzz/corpus/fuzz_applefile/fuzz_applefile-003.bin b/src/fuzz/corpus/fuzz_applefile/fuzz_applefile-003.bin Binary files differ. diff --git a/src/fuzz/corpus/fuzz_applefile/fuzz_applefile-004.bin b/src/fuzz/corpus/fuzz_applefile/fuzz_applefile-004.bin Binary files differ. diff --git a/src/fuzz/corpus/fuzz_applefile/fuzz_applefile-005.bin b/src/fuzz/corpus/fuzz_applefile/fuzz_applefile-005.bin Binary files differ. diff --git a/src/fuzz/corpus/fuzz_archive/fuzz_archive-000.bin b/src/fuzz/corpus/fuzz_archive/fuzz_archive-000.bin Binary files differ. diff --git a/src/fuzz/corpus/fuzz_archive/fuzz_archive-001.bin b/src/fuzz/corpus/fuzz_archive/fuzz_archive-001.bin Binary files differ. diff --git a/src/fuzz/corpus/fuzz_archive/fuzz_archive-002.bin b/src/fuzz/corpus/fuzz_archive/fuzz_archive-002.bin Binary files differ. diff --git a/src/fuzz/corpus/fuzz_archive/fuzz_archive-003.bin b/src/fuzz/corpus/fuzz_archive/fuzz_archive-003.bin Binary files differ. diff --git a/src/fuzz/corpus/fuzz_archive/fuzz_archive-004.bin b/src/fuzz/corpus/fuzz_archive/fuzz_archive-004.bin Binary files differ. diff --git a/src/fuzz/corpus/fuzz_archive/fuzz_archive-005.bin b/src/fuzz/corpus/fuzz_archive/fuzz_archive-005.bin Binary files differ. diff --git a/src/fuzz/corpus/fuzz_convert/fuzz_convert-000.bin b/src/fuzz/corpus/fuzz_convert/fuzz_convert-000.bin Binary files differ. diff --git a/src/fuzz/corpus/fuzz_convert/fuzz_convert-001.bin b/src/fuzz/corpus/fuzz_convert/fuzz_convert-001.bin Binary files differ. diff --git a/src/fuzz/corpus/fuzz_convert/fuzz_convert-002.bin b/src/fuzz/corpus/fuzz_convert/fuzz_convert-002.bin Binary files differ. diff --git a/src/fuzz/corpus/fuzz_convert/fuzz_convert-003.bin b/src/fuzz/corpus/fuzz_convert/fuzz_convert-003.bin Binary files differ. diff --git a/src/fuzz/corpus/fuzz_convert/fuzz_convert-004.bin b/src/fuzz/corpus/fuzz_convert/fuzz_convert-004.bin Binary files differ. diff --git a/src/fuzz/corpus/fuzz_convert/fuzz_convert-005.bin b/src/fuzz/corpus/fuzz_convert/fuzz_convert-005.bin Binary files differ. diff --git a/src/fuzz/corpus/fuzz_convert/fuzz_convert-006.bin b/src/fuzz/corpus/fuzz_convert/fuzz_convert-006.bin Binary files differ. diff --git a/src/fuzz/corpus/fuzz_convert/fuzz_convert-007.bin b/src/fuzz/corpus/fuzz_convert/fuzz_convert-007.bin Binary files differ. diff --git a/src/fuzz/corpus/fuzz_convert/fuzz_convert-008.bin b/src/fuzz/corpus/fuzz_convert/fuzz_convert-008.bin Binary files differ. diff --git a/src/fuzz/corpus/fuzz_convert/fuzz_convert-009.bin b/src/fuzz/corpus/fuzz_convert/fuzz_convert-009.bin Binary files differ. diff --git a/src/fuzz/corpus/fuzz_convert/fuzz_convert-010.bin b/src/fuzz/corpus/fuzz_convert/fuzz_convert-010.bin Binary files differ. diff --git a/src/fuzz/corpus/fuzz_convert/fuzz_convert-011.bin b/src/fuzz/corpus/fuzz_convert/fuzz_convert-011.bin Binary files differ. diff --git a/src/fuzz/corpus/fuzz_convert/fuzz_convert-012.bin b/src/fuzz/corpus/fuzz_convert/fuzz_convert-012.bin Binary files differ. diff --git a/src/fuzz/corpus/fuzz_convert/fuzz_convert-013.bin b/src/fuzz/corpus/fuzz_convert/fuzz_convert-013.bin Binary files differ. diff --git a/src/fuzz/corpus/fuzz_datasource/fuzz_datasource-000.bin b/src/fuzz/corpus/fuzz_datasource/fuzz_datasource-000.bin Binary files differ. diff --git a/src/fuzz/corpus/fuzz_datasource/fuzz_datasource-001.bin b/src/fuzz/corpus/fuzz_datasource/fuzz_datasource-001.bin Binary files differ. diff --git a/src/fuzz/corpus/fuzz_datasource/fuzz_datasource-002.bin b/src/fuzz/corpus/fuzz_datasource/fuzz_datasource-002.bin @@ -0,0 +1,3 @@ + BZh81AY&SYRZäö‡öð +è÷ÈËKYŒZ6³Fˆ›V ¡BÆŽh[B@nžÆ»{rnƒµGÍ1õš˜ÙZ._×6[™Ç¤áZ!Wi¨_5&Ìtý63ɧÄçû˶ºòàUØáŦPLÆø‚9Ɖ8×]fæeºwz½ YÌkÕòÆõkÐp”^CwÄôØe-¸zDɄފpWÍ÷í_XÖn¢4Åõ¼³gÄŽ`ª”±»ôIê+jŒÃ÷&íE:qdPR$‚ãŽÄæðÌH(n©kÒÝqYf èѬ2»ÒEu*X¿2¼ž +î +\ No newline at end of file diff --git a/src/fuzz/corpus/fuzz_datasource/fuzz_datasource-003.bin b/src/fuzz/corpus/fuzz_datasource/fuzz_datasource-003.bin Binary files differ. diff --git a/src/fuzz/corpus/fuzz_datasource/fuzz_datasource-004.bin b/src/fuzz/corpus/fuzz_datasource/fuzz_datasource-004.bin @@ -0,0 +1,4 @@ + –BZh21AY&SY$¿¥mya½ù£¸Ùì^ @ÓUªâÜ‚[ ìË/¬ 9ÌT‰'z+h„’{8FÌbó,é'ã{ÊŒÁŠ¡ÏJJ C²ïUû»)3+.‰kT±£f½.ÍÇÀ’’祡†o53áHµú¯c< +]¼ÈO:iI¯,Ÿƒ‘åÂÞ#}FË-ÐÜ‹!+°É;Nj(àÂ4bÃú .N~å+Ê=–i¦ÐiP“УЇ +hi]È:ŒdD|ãüñXpý…!lãóŸ.ÛA¶”Õ.¨JXÙž‘®.s¸ûLe¨CŒ>°A©ÚS"þMc¶:ÃŽ +&ÞXXÉ „ÃJ¯§kÝy7°Ã“xZë „ê7Ú´,[ròQ^s‡sã|†&áÇks½dAÍ3…‰±`ÛÏ‚SIk4éVE8›O"8c„ÄòS~ìüS4“6íŽ÷XZ±(É>tOoÅvW +\ No newline at end of file diff --git a/src/fuzz/corpus/fuzz_datasource/fuzz_datasource-005.bin b/src/fuzz/corpus/fuzz_datasource/fuzz_datasource-005.bin Binary files differ. diff --git a/src/fuzz/corpus/fuzz_datasource/fuzz_datasource-006.bin b/src/fuzz/corpus/fuzz_datasource/fuzz_datasource-006.bin Binary files differ. diff --git a/src/fuzz/corpus/fuzz_datasource/fuzz_datasource-007.bin b/src/fuzz/corpus/fuzz_datasource/fuzz_datasource-007.bin @@ -0,0 +1,2 @@ + *BZ3Ö¸]¯ì#ZtgÎNò¿ý2HÂn¹b¬T8(Ùu‡¼‰ež_¢²É×uI3l(ík9[¼fWIçÆ¾¾˜pµ—>ÈG é×m@úò³Ófå¿èÁ¸ÃÏ;ùMõÿ…uææ4Dµè$ ZÆÙåUåÕMsÖÂIÎTìó|,EÝ2[ò,è®n€¡mãi½wáf‘è@VOg_ºíí ó ‘-™è1 ª +ŪæžjÁÈü”c²ñ–«Ñöë/¿¤ +\ No newline at end of file diff --git a/src/fuzz/corpus/fuzz_datasource/fuzz_datasource-008.bin b/src/fuzz/corpus/fuzz_datasource/fuzz_datasource-008.bin Binary files differ. diff --git a/src/fuzz/corpus/fuzz_datasource/fuzz_datasource-009.bin b/src/fuzz/corpus/fuzz_datasource/fuzz_datasource-009.bin Binary files differ. diff --git a/src/fuzz/corpus/fuzz_deb/fuzz_deb-000.bin b/src/fuzz/corpus/fuzz_deb/fuzz_deb-000.bin Binary files differ. diff --git a/src/fuzz/corpus/fuzz_deb/fuzz_deb-001.bin b/src/fuzz/corpus/fuzz_deb/fuzz_deb-001.bin Binary files differ. diff --git a/src/fuzz/corpus/fuzz_deb/fuzz_deb-002.bin b/src/fuzz/corpus/fuzz_deb/fuzz_deb-002.bin Binary files differ. diff --git a/src/fuzz/corpus/fuzz_deb/fuzz_deb-003.bin b/src/fuzz/corpus/fuzz_deb/fuzz_deb-003.bin Binary files differ. diff --git a/src/fuzz/corpus/fuzz_deb/fuzz_deb-004.bin b/src/fuzz/corpus/fuzz_deb/fuzz_deb-004.bin Binary files differ. diff --git a/src/fuzz/corpus/fuzz_deb/fuzz_deb-005.bin b/src/fuzz/corpus/fuzz_deb/fuzz_deb-005.bin Binary files differ. diff --git a/src/fuzz/corpus/fuzz_dvi/fuzz_dvi-000.bin b/src/fuzz/corpus/fuzz_dvi/fuzz_dvi-000.bin Binary files differ. diff --git a/src/fuzz/corpus/fuzz_dvi/fuzz_dvi-001.bin b/src/fuzz/corpus/fuzz_dvi/fuzz_dvi-001.bin Binary files differ. diff --git a/src/fuzz/corpus/fuzz_dvi/fuzz_dvi-002.bin b/src/fuzz/corpus/fuzz_dvi/fuzz_dvi-002.bin Binary files differ. diff --git a/src/fuzz/corpus/fuzz_dvi/fuzz_dvi-003.bin b/src/fuzz/corpus/fuzz_dvi/fuzz_dvi-003.bin Binary files differ. diff --git a/src/fuzz/corpus/fuzz_dvi/fuzz_dvi-004.bin b/src/fuzz/corpus/fuzz_dvi/fuzz_dvi-004.bin Binary files differ. diff --git a/src/fuzz/corpus/fuzz_dvi/fuzz_dvi-005.bin b/src/fuzz/corpus/fuzz_dvi/fuzz_dvi-005.bin Binary files differ. diff --git a/src/fuzz/corpus/fuzz_elf/fuzz_elf-000.bin b/src/fuzz/corpus/fuzz_elf/fuzz_elf-000.bin Binary files differ. diff --git a/src/fuzz/corpus/fuzz_elf/fuzz_elf-001.bin b/src/fuzz/corpus/fuzz_elf/fuzz_elf-001.bin Binary files differ. diff --git a/src/fuzz/corpus/fuzz_elf/fuzz_elf-002.bin b/src/fuzz/corpus/fuzz_elf/fuzz_elf-002.bin Binary files differ. diff --git a/src/fuzz/corpus/fuzz_elf/fuzz_elf-003.bin b/src/fuzz/corpus/fuzz_elf/fuzz_elf-003.bin Binary files differ. diff --git a/src/fuzz/corpus/fuzz_elf/fuzz_elf-004.bin b/src/fuzz/corpus/fuzz_elf/fuzz_elf-004.bin Binary files differ. diff --git a/src/fuzz/corpus/fuzz_elf/fuzz_elf-005.bin b/src/fuzz/corpus/fuzz_elf/fuzz_elf-005.bin Binary files differ. diff --git a/src/fuzz/corpus/fuzz_extract/fuzz_extract-000.bin b/src/fuzz/corpus/fuzz_extract/fuzz_extract-000.bin Binary files differ. diff --git a/src/fuzz/corpus/fuzz_extract/fuzz_extract-001.bin b/src/fuzz/corpus/fuzz_extract/fuzz_extract-001.bin Binary files differ. diff --git a/src/fuzz/corpus/fuzz_extract/fuzz_extract-002.bin b/src/fuzz/corpus/fuzz_extract/fuzz_extract-002.bin Binary files differ. diff --git a/src/fuzz/corpus/fuzz_extract/fuzz_extract-003.bin b/src/fuzz/corpus/fuzz_extract/fuzz_extract-003.bin Binary files differ. diff --git a/src/fuzz/corpus/fuzz_extract/fuzz_extract-004.bin b/src/fuzz/corpus/fuzz_extract/fuzz_extract-004.bin Binary files differ. diff --git a/src/fuzz/corpus/fuzz_extract/fuzz_extract-005.bin b/src/fuzz/corpus/fuzz_extract/fuzz_extract-005.bin Binary files differ. diff --git a/src/fuzz/corpus/fuzz_extract/fuzz_extract-006.bin b/src/fuzz/corpus/fuzz_extract/fuzz_extract-006.bin Binary files differ. diff --git a/src/fuzz/corpus/fuzz_extract/fuzz_extract-007.bin b/src/fuzz/corpus/fuzz_extract/fuzz_extract-007.bin Binary files differ. diff --git a/src/fuzz/corpus/fuzz_extract/fuzz_extract-008.bin b/src/fuzz/corpus/fuzz_extract/fuzz_extract-008.bin Binary files differ. diff --git a/src/fuzz/corpus/fuzz_extract/fuzz_extract-009.bin b/src/fuzz/corpus/fuzz_extract/fuzz_extract-009.bin Binary files differ. diff --git a/src/fuzz/corpus/fuzz_extract/fuzz_extract-010.bin b/src/fuzz/corpus/fuzz_extract/fuzz_extract-010.bin Binary files differ. diff --git a/src/fuzz/corpus/fuzz_extract/fuzz_extract-011.bin b/src/fuzz/corpus/fuzz_extract/fuzz_extract-011.bin Binary files differ. diff --git a/src/fuzz/corpus/fuzz_extract/fuzz_extract-012.bin b/src/fuzz/corpus/fuzz_extract/fuzz_extract-012.bin Binary files differ. diff --git a/src/fuzz/corpus/fuzz_extract/fuzz_extract-013.bin b/src/fuzz/corpus/fuzz_extract/fuzz_extract-013.bin Binary files differ. diff --git a/src/fuzz/corpus/fuzz_extract/fuzz_extract-014.bin b/src/fuzz/corpus/fuzz_extract/fuzz_extract-014.bin Binary files differ. diff --git a/src/fuzz/corpus/fuzz_extract/fuzz_extract-015.bin b/src/fuzz/corpus/fuzz_extract/fuzz_extract-015.bin Binary files differ. diff --git a/src/fuzz/corpus/fuzz_extract/fuzz_extract-016.bin b/src/fuzz/corpus/fuzz_extract/fuzz_extract-016.bin Binary files differ. diff --git a/src/fuzz/corpus/fuzz_extract/fuzz_extract-017.bin b/src/fuzz/corpus/fuzz_extract/fuzz_extract-017.bin Binary files differ. diff --git a/src/fuzz/corpus/fuzz_extract/fuzz_extract-018.bin b/src/fuzz/corpus/fuzz_extract/fuzz_extract-018.bin Binary files differ. diff --git a/src/fuzz/corpus/fuzz_extract/fuzz_extract-019.bin b/src/fuzz/corpus/fuzz_extract/fuzz_extract-019.bin Binary files differ. diff --git a/src/fuzz/corpus/fuzz_extract/fuzz_extract-020.bin b/src/fuzz/corpus/fuzz_extract/fuzz_extract-020.bin Binary files differ. diff --git a/src/fuzz/corpus/fuzz_extract/fuzz_extract-021.bin b/src/fuzz/corpus/fuzz_extract/fuzz_extract-021.bin Binary files differ. diff --git a/src/fuzz/corpus/fuzz_extract/fuzz_extract-022.bin b/src/fuzz/corpus/fuzz_extract/fuzz_extract-022.bin Binary files differ. diff --git a/src/fuzz/corpus/fuzz_extract/fuzz_extract-023.bin b/src/fuzz/corpus/fuzz_extract/fuzz_extract-023.bin Binary files differ. diff --git a/src/fuzz/corpus/fuzz_extract/fuzz_extract-024.bin b/src/fuzz/corpus/fuzz_extract/fuzz_extract-024.bin Binary files differ. diff --git a/src/fuzz/corpus/fuzz_extract/fuzz_extract-025.bin b/src/fuzz/corpus/fuzz_extract/fuzz_extract-025.bin Binary files differ. diff --git a/src/fuzz/corpus/fuzz_extract/fuzz_extract-026.bin b/src/fuzz/corpus/fuzz_extract/fuzz_extract-026.bin Binary files differ. diff --git a/src/fuzz/corpus/fuzz_extract/fuzz_extract-027.bin b/src/fuzz/corpus/fuzz_extract/fuzz_extract-027.bin Binary files differ. diff --git a/src/fuzz/corpus/fuzz_extract/fuzz_extract-028.bin b/src/fuzz/corpus/fuzz_extract/fuzz_extract-028.bin Binary files differ. diff --git a/src/fuzz/corpus/fuzz_extract/fuzz_extract-029.bin b/src/fuzz/corpus/fuzz_extract/fuzz_extract-029.bin Binary files differ. diff --git a/src/fuzz/corpus/fuzz_extract/fuzz_extract-030.bin b/src/fuzz/corpus/fuzz_extract/fuzz_extract-030.bin Binary files differ. diff --git a/src/fuzz/corpus/fuzz_extract/fuzz_extract-031.bin b/src/fuzz/corpus/fuzz_extract/fuzz_extract-031.bin Binary files differ. diff --git a/src/fuzz/corpus/fuzz_extract/fuzz_extract-032.bin b/src/fuzz/corpus/fuzz_extract/fuzz_extract-032.bin Binary files differ. diff --git a/src/fuzz/corpus/fuzz_extract/fuzz_extract-033.bin b/src/fuzz/corpus/fuzz_extract/fuzz_extract-033.bin Binary files differ. diff --git a/src/fuzz/corpus/fuzz_extract/fuzz_extract-034.bin b/src/fuzz/corpus/fuzz_extract/fuzz_extract-034.bin Binary files differ. diff --git a/src/fuzz/corpus/fuzz_flac/fuzz_flac-000.bin b/src/fuzz/corpus/fuzz_flac/fuzz_flac-000.bin Binary files differ. diff --git a/src/fuzz/corpus/fuzz_flac/fuzz_flac-001.bin b/src/fuzz/corpus/fuzz_flac/fuzz_flac-001.bin Binary files differ. diff --git a/src/fuzz/corpus/fuzz_flac/fuzz_flac-002.bin b/src/fuzz/corpus/fuzz_flac/fuzz_flac-002.bin Binary files differ. diff --git a/src/fuzz/corpus/fuzz_flac/fuzz_flac-003.bin b/src/fuzz/corpus/fuzz_flac/fuzz_flac-003.bin Binary files differ. diff --git a/src/fuzz/corpus/fuzz_flac/fuzz_flac-004.bin b/src/fuzz/corpus/fuzz_flac/fuzz_flac-004.bin Binary files differ. diff --git a/src/fuzz/corpus/fuzz_flac/fuzz_flac-005.bin b/src/fuzz/corpus/fuzz_flac/fuzz_flac-005.bin Binary files differ. diff --git a/src/fuzz/corpus/fuzz_gif/fuzz_gif-000.bin b/src/fuzz/corpus/fuzz_gif/fuzz_gif-000.bin Binary files differ. diff --git a/src/fuzz/corpus/fuzz_gif/fuzz_gif-001.bin b/src/fuzz/corpus/fuzz_gif/fuzz_gif-001.bin Binary files differ. diff --git a/src/fuzz/corpus/fuzz_gif/fuzz_gif-002.bin b/src/fuzz/corpus/fuzz_gif/fuzz_gif-002.bin Binary files differ. diff --git a/src/fuzz/corpus/fuzz_gif/fuzz_gif-003.bin b/src/fuzz/corpus/fuzz_gif/fuzz_gif-003.bin Binary files differ. diff --git a/src/fuzz/corpus/fuzz_gif/fuzz_gif-004.bin b/src/fuzz/corpus/fuzz_gif/fuzz_gif-004.bin Binary files differ. diff --git a/src/fuzz/corpus/fuzz_gif/fuzz_gif-005.bin b/src/fuzz/corpus/fuzz_gif/fuzz_gif-005.bin Binary files differ. diff --git a/src/fuzz/corpus/fuzz_ipc/fuzz_ipc-000.bin b/src/fuzz/corpus/fuzz_ipc/fuzz_ipc-000.bin @@ -0,0 +1 @@ + +\ No newline at end of file diff --git a/src/fuzz/corpus/fuzz_ipc/fuzz_ipc-001.bin b/src/fuzz/corpus/fuzz_ipc/fuzz_ipc-001.bin Binary files differ. diff --git a/src/fuzz/corpus/fuzz_ipc/fuzz_ipc-002.bin b/src/fuzz/corpus/fuzz_ipc/fuzz_ipc-002.bin Binary files differ. diff --git a/src/fuzz/corpus/fuzz_ipc/fuzz_ipc-003.bin b/src/fuzz/corpus/fuzz_ipc/fuzz_ipc-003.bin Binary files differ. diff --git a/src/fuzz/corpus/fuzz_ipc/fuzz_ipc-004.bin b/src/fuzz/corpus/fuzz_ipc/fuzz_ipc-004.bin Binary files differ. diff --git a/src/fuzz/corpus/fuzz_ipc/fuzz_ipc-005.bin b/src/fuzz/corpus/fuzz_ipc/fuzz_ipc-005.bin Binary files differ. diff --git a/src/fuzz/corpus/fuzz_ipc/fuzz_ipc-006.bin b/src/fuzz/corpus/fuzz_ipc/fuzz_ipc-006.bin Binary files differ. diff --git a/src/fuzz/corpus/fuzz_ipc/fuzz_ipc-007.bin b/src/fuzz/corpus/fuzz_ipc/fuzz_ipc-007.bin Binary files differ. diff --git a/src/fuzz/corpus/fuzz_ipc/fuzz_ipc-008.bin b/src/fuzz/corpus/fuzz_ipc/fuzz_ipc-008.bin @@ -0,0 +1 @@ + +\ No newline at end of file diff --git a/src/fuzz/corpus/fuzz_ipc/fuzz_ipc-009.bin b/src/fuzz/corpus/fuzz_ipc/fuzz_ipc-009.bin Binary files differ. diff --git a/src/fuzz/corpus/fuzz_it/fuzz_it-000.bin b/src/fuzz/corpus/fuzz_it/fuzz_it-000.bin Binary files differ. diff --git a/src/fuzz/corpus/fuzz_it/fuzz_it-001.bin b/src/fuzz/corpus/fuzz_it/fuzz_it-001.bin Binary files differ. diff --git a/src/fuzz/corpus/fuzz_it/fuzz_it-002.bin b/src/fuzz/corpus/fuzz_it/fuzz_it-002.bin Binary files differ. diff --git a/src/fuzz/corpus/fuzz_it/fuzz_it-003.bin b/src/fuzz/corpus/fuzz_it/fuzz_it-003.bin Binary files differ. diff --git a/src/fuzz/corpus/fuzz_it/fuzz_it-004.bin b/src/fuzz/corpus/fuzz_it/fuzz_it-004.bin Binary files differ. diff --git a/src/fuzz/corpus/fuzz_it/fuzz_it-005.bin b/src/fuzz/corpus/fuzz_it/fuzz_it-005.bin Binary files differ. diff --git a/src/fuzz/corpus/fuzz_jpeg/fuzz_jpeg-000.bin b/src/fuzz/corpus/fuzz_jpeg/fuzz_jpeg-000.bin Binary files differ. diff --git a/src/fuzz/corpus/fuzz_jpeg/fuzz_jpeg-001.bin b/src/fuzz/corpus/fuzz_jpeg/fuzz_jpeg-001.bin Binary files differ. diff --git a/src/fuzz/corpus/fuzz_jpeg/fuzz_jpeg-002.bin b/src/fuzz/corpus/fuzz_jpeg/fuzz_jpeg-002.bin Binary files differ. diff --git a/src/fuzz/corpus/fuzz_jpeg/fuzz_jpeg-003.bin b/src/fuzz/corpus/fuzz_jpeg/fuzz_jpeg-003.bin Binary files differ. diff --git a/src/fuzz/corpus/fuzz_jpeg/fuzz_jpeg-004.bin b/src/fuzz/corpus/fuzz_jpeg/fuzz_jpeg-004.bin Binary files differ. diff --git a/src/fuzz/corpus/fuzz_jpeg/fuzz_jpeg-005.bin b/src/fuzz/corpus/fuzz_jpeg/fuzz_jpeg-005.bin Binary files differ. diff --git a/src/fuzz/corpus/fuzz_man/fuzz_man-000.bin b/src/fuzz/corpus/fuzz_man/fuzz_man-000.bin Binary files differ. diff --git a/src/fuzz/corpus/fuzz_man/fuzz_man-001.bin b/src/fuzz/corpus/fuzz_man/fuzz_man-001.bin Binary files differ. diff --git a/src/fuzz/corpus/fuzz_man/fuzz_man-002.bin b/src/fuzz/corpus/fuzz_man/fuzz_man-002.bin Binary files differ. diff --git a/src/fuzz/corpus/fuzz_man/fuzz_man-003.bin b/src/fuzz/corpus/fuzz_man/fuzz_man-003.bin Binary files differ. diff --git a/src/fuzz/corpus/fuzz_man/fuzz_man-004.bin b/src/fuzz/corpus/fuzz_man/fuzz_man-004.bin Binary files differ. diff --git a/src/fuzz/corpus/fuzz_man/fuzz_man-005.bin b/src/fuzz/corpus/fuzz_man/fuzz_man-005.bin Binary files differ. diff --git a/src/fuzz/corpus/fuzz_mime/fuzz_mime-000.bin b/src/fuzz/corpus/fuzz_mime/fuzz_mime-000.bin Binary files differ. diff --git a/src/fuzz/corpus/fuzz_mime/fuzz_mime-001.bin b/src/fuzz/corpus/fuzz_mime/fuzz_mime-001.bin Binary files differ. diff --git a/src/fuzz/corpus/fuzz_mime/fuzz_mime-002.bin b/src/fuzz/corpus/fuzz_mime/fuzz_mime-002.bin Binary files differ. diff --git a/src/fuzz/corpus/fuzz_mime/fuzz_mime-003.bin b/src/fuzz/corpus/fuzz_mime/fuzz_mime-003.bin Binary files differ. diff --git a/src/fuzz/corpus/fuzz_mime/fuzz_mime-004.bin b/src/fuzz/corpus/fuzz_mime/fuzz_mime-004.bin Binary files differ. diff --git a/src/fuzz/corpus/fuzz_mime/fuzz_mime-005.bin b/src/fuzz/corpus/fuzz_mime/fuzz_mime-005.bin Binary files differ. diff --git a/src/fuzz/corpus/fuzz_msoffice/fuzz_msoffice-000.bin b/src/fuzz/corpus/fuzz_msoffice/fuzz_msoffice-000.bin Binary files differ. diff --git a/src/fuzz/corpus/fuzz_msoffice/fuzz_msoffice-001.bin b/src/fuzz/corpus/fuzz_msoffice/fuzz_msoffice-001.bin Binary files differ. diff --git a/src/fuzz/corpus/fuzz_msoffice/fuzz_msoffice-002.bin b/src/fuzz/corpus/fuzz_msoffice/fuzz_msoffice-002.bin Binary files differ. diff --git a/src/fuzz/corpus/fuzz_msoffice/fuzz_msoffice-003.bin b/src/fuzz/corpus/fuzz_msoffice/fuzz_msoffice-003.bin Binary files differ. diff --git a/src/fuzz/corpus/fuzz_msoffice/fuzz_msoffice-004.bin b/src/fuzz/corpus/fuzz_msoffice/fuzz_msoffice-004.bin Binary files differ. diff --git a/src/fuzz/corpus/fuzz_msoffice/fuzz_msoffice-005.bin b/src/fuzz/corpus/fuzz_msoffice/fuzz_msoffice-005.bin Binary files differ. diff --git a/src/fuzz/corpus/fuzz_nsf/fuzz_nsf-000.bin b/src/fuzz/corpus/fuzz_nsf/fuzz_nsf-000.bin Binary files differ. diff --git a/src/fuzz/corpus/fuzz_nsf/fuzz_nsf-001.bin b/src/fuzz/corpus/fuzz_nsf/fuzz_nsf-001.bin Binary files differ. diff --git a/src/fuzz/corpus/fuzz_nsf/fuzz_nsf-002.bin b/src/fuzz/corpus/fuzz_nsf/fuzz_nsf-002.bin Binary files differ. diff --git a/src/fuzz/corpus/fuzz_nsf/fuzz_nsf-003.bin b/src/fuzz/corpus/fuzz_nsf/fuzz_nsf-003.bin Binary files differ. diff --git a/src/fuzz/corpus/fuzz_nsf/fuzz_nsf-004.bin b/src/fuzz/corpus/fuzz_nsf/fuzz_nsf-004.bin Binary files differ. diff --git a/src/fuzz/corpus/fuzz_nsf/fuzz_nsf-005.bin b/src/fuzz/corpus/fuzz_nsf/fuzz_nsf-005.bin Binary files differ. diff --git a/src/fuzz/corpus/fuzz_nsfe/fuzz_nsfe-000.bin b/src/fuzz/corpus/fuzz_nsfe/fuzz_nsfe-000.bin Binary files differ. diff --git a/src/fuzz/corpus/fuzz_nsfe/fuzz_nsfe-001.bin b/src/fuzz/corpus/fuzz_nsfe/fuzz_nsfe-001.bin Binary files differ. diff --git a/src/fuzz/corpus/fuzz_nsfe/fuzz_nsfe-002.bin b/src/fuzz/corpus/fuzz_nsfe/fuzz_nsfe-002.bin Binary files differ. diff --git a/src/fuzz/corpus/fuzz_nsfe/fuzz_nsfe-003.bin b/src/fuzz/corpus/fuzz_nsfe/fuzz_nsfe-003.bin Binary files differ. diff --git a/src/fuzz/corpus/fuzz_nsfe/fuzz_nsfe-004.bin b/src/fuzz/corpus/fuzz_nsfe/fuzz_nsfe-004.bin Binary files differ. diff --git a/src/fuzz/corpus/fuzz_nsfe/fuzz_nsfe-005.bin b/src/fuzz/corpus/fuzz_nsfe/fuzz_nsfe-005.bin Binary files differ. diff --git a/src/fuzz/corpus/fuzz_odf/fuzz_odf-000.bin b/src/fuzz/corpus/fuzz_odf/fuzz_odf-000.bin Binary files differ. diff --git a/src/fuzz/corpus/fuzz_odf/fuzz_odf-001.bin b/src/fuzz/corpus/fuzz_odf/fuzz_odf-001.bin Binary files differ. diff --git a/src/fuzz/corpus/fuzz_odf/fuzz_odf-002.bin b/src/fuzz/corpus/fuzz_odf/fuzz_odf-002.bin Binary files differ. diff --git a/src/fuzz/corpus/fuzz_odf/fuzz_odf-003.bin b/src/fuzz/corpus/fuzz_odf/fuzz_odf-003.bin Binary files differ. diff --git a/src/fuzz/corpus/fuzz_odf/fuzz_odf-004.bin b/src/fuzz/corpus/fuzz_odf/fuzz_odf-004.bin Binary files differ. diff --git a/src/fuzz/corpus/fuzz_odf/fuzz_odf-005.bin b/src/fuzz/corpus/fuzz_odf/fuzz_odf-005.bin Binary files differ. diff --git a/src/fuzz/corpus/fuzz_ogg/fuzz_ogg-000.bin b/src/fuzz/corpus/fuzz_ogg/fuzz_ogg-000.bin Binary files differ. diff --git a/src/fuzz/corpus/fuzz_ogg/fuzz_ogg-001.bin b/src/fuzz/corpus/fuzz_ogg/fuzz_ogg-001.bin Binary files differ. diff --git a/src/fuzz/corpus/fuzz_ogg/fuzz_ogg-002.bin b/src/fuzz/corpus/fuzz_ogg/fuzz_ogg-002.bin Binary files differ. diff --git a/src/fuzz/corpus/fuzz_ogg/fuzz_ogg-003.bin b/src/fuzz/corpus/fuzz_ogg/fuzz_ogg-003.bin Binary files differ. diff --git a/src/fuzz/corpus/fuzz_ogg/fuzz_ogg-004.bin b/src/fuzz/corpus/fuzz_ogg/fuzz_ogg-004.bin Binary files differ. diff --git a/src/fuzz/corpus/fuzz_ogg/fuzz_ogg-005.bin b/src/fuzz/corpus/fuzz_ogg/fuzz_ogg-005.bin Binary files differ. diff --git a/src/fuzz/corpus/fuzz_ole2/fuzz_ole2-000.bin b/src/fuzz/corpus/fuzz_ole2/fuzz_ole2-000.bin Binary files differ. diff --git a/src/fuzz/corpus/fuzz_ole2/fuzz_ole2-001.bin b/src/fuzz/corpus/fuzz_ole2/fuzz_ole2-001.bin Binary files differ. diff --git a/src/fuzz/corpus/fuzz_ole2/fuzz_ole2-002.bin b/src/fuzz/corpus/fuzz_ole2/fuzz_ole2-002.bin Binary files differ. diff --git a/src/fuzz/corpus/fuzz_ole2/fuzz_ole2-003.bin b/src/fuzz/corpus/fuzz_ole2/fuzz_ole2-003.bin Binary files differ. diff --git a/src/fuzz/corpus/fuzz_ole2/fuzz_ole2-004.bin b/src/fuzz/corpus/fuzz_ole2/fuzz_ole2-004.bin Binary files differ. diff --git a/src/fuzz/corpus/fuzz_ole2/fuzz_ole2-005.bin b/src/fuzz/corpus/fuzz_ole2/fuzz_ole2-005.bin Binary files differ. diff --git a/src/fuzz/corpus/fuzz_png/fuzz_png-000.bin b/src/fuzz/corpus/fuzz_png/fuzz_png-000.bin Binary files differ. diff --git a/src/fuzz/corpus/fuzz_png/fuzz_png-001.bin b/src/fuzz/corpus/fuzz_png/fuzz_png-001.bin Binary files differ. diff --git a/src/fuzz/corpus/fuzz_png/fuzz_png-002.bin b/src/fuzz/corpus/fuzz_png/fuzz_png-002.bin Binary files differ. diff --git a/src/fuzz/corpus/fuzz_png/fuzz_png-003.bin b/src/fuzz/corpus/fuzz_png/fuzz_png-003.bin Binary files differ. diff --git a/src/fuzz/corpus/fuzz_png/fuzz_png-004.bin b/src/fuzz/corpus/fuzz_png/fuzz_png-004.bin Binary files differ. diff --git a/src/fuzz/corpus/fuzz_png/fuzz_png-005.bin b/src/fuzz/corpus/fuzz_png/fuzz_png-005.bin Binary files differ. diff --git a/src/fuzz/corpus/fuzz_ps/fuzz_ps-000.bin b/src/fuzz/corpus/fuzz_ps/fuzz_ps-000.bin Binary files differ. diff --git a/src/fuzz/corpus/fuzz_ps/fuzz_ps-001.bin b/src/fuzz/corpus/fuzz_ps/fuzz_ps-001.bin Binary files differ. diff --git a/src/fuzz/corpus/fuzz_ps/fuzz_ps-002.bin b/src/fuzz/corpus/fuzz_ps/fuzz_ps-002.bin Binary files differ. diff --git a/src/fuzz/corpus/fuzz_ps/fuzz_ps-003.bin b/src/fuzz/corpus/fuzz_ps/fuzz_ps-003.bin Binary files differ. diff --git a/src/fuzz/corpus/fuzz_ps/fuzz_ps-004.bin b/src/fuzz/corpus/fuzz_ps/fuzz_ps-004.bin Binary files differ. diff --git a/src/fuzz/corpus/fuzz_ps/fuzz_ps-005.bin b/src/fuzz/corpus/fuzz_ps/fuzz_ps-005.bin Binary files differ. diff --git a/src/fuzz/corpus/fuzz_qt/fuzz_qt-000.bin b/src/fuzz/corpus/fuzz_qt/fuzz_qt-000.bin Binary files differ. diff --git a/src/fuzz/corpus/fuzz_qt/fuzz_qt-001.bin b/src/fuzz/corpus/fuzz_qt/fuzz_qt-001.bin Binary files differ. diff --git a/src/fuzz/corpus/fuzz_qt/fuzz_qt-002.bin b/src/fuzz/corpus/fuzz_qt/fuzz_qt-002.bin Binary files differ. diff --git a/src/fuzz/corpus/fuzz_qt/fuzz_qt-003.bin b/src/fuzz/corpus/fuzz_qt/fuzz_qt-003.bin Binary files differ. diff --git a/src/fuzz/corpus/fuzz_qt/fuzz_qt-004.bin b/src/fuzz/corpus/fuzz_qt/fuzz_qt-004.bin Binary files differ. diff --git a/src/fuzz/corpus/fuzz_qt/fuzz_qt-005.bin b/src/fuzz/corpus/fuzz_qt/fuzz_qt-005.bin Binary files differ. diff --git a/src/fuzz/corpus/fuzz_real/fuzz_real-000.bin b/src/fuzz/corpus/fuzz_real/fuzz_real-000.bin Binary files differ. diff --git a/src/fuzz/corpus/fuzz_real/fuzz_real-001.bin b/src/fuzz/corpus/fuzz_real/fuzz_real-001.bin Binary files differ. diff --git a/src/fuzz/corpus/fuzz_real/fuzz_real-002.bin b/src/fuzz/corpus/fuzz_real/fuzz_real-002.bin Binary files differ. diff --git a/src/fuzz/corpus/fuzz_real/fuzz_real-003.bin b/src/fuzz/corpus/fuzz_real/fuzz_real-003.bin Binary files differ. diff --git a/src/fuzz/corpus/fuzz_real/fuzz_real-004.bin b/src/fuzz/corpus/fuzz_real/fuzz_real-004.bin Binary files differ. diff --git a/src/fuzz/corpus/fuzz_real/fuzz_real-005.bin b/src/fuzz/corpus/fuzz_real/fuzz_real-005.bin Binary files differ. diff --git a/src/fuzz/corpus/fuzz_riff/fuzz_riff-000.bin b/src/fuzz/corpus/fuzz_riff/fuzz_riff-000.bin Binary files differ. diff --git a/src/fuzz/corpus/fuzz_riff/fuzz_riff-001.bin b/src/fuzz/corpus/fuzz_riff/fuzz_riff-001.bin Binary files differ. diff --git a/src/fuzz/corpus/fuzz_riff/fuzz_riff-002.bin b/src/fuzz/corpus/fuzz_riff/fuzz_riff-002.bin Binary files differ. diff --git a/src/fuzz/corpus/fuzz_riff/fuzz_riff-003.bin b/src/fuzz/corpus/fuzz_riff/fuzz_riff-003.bin Binary files differ. diff --git a/src/fuzz/corpus/fuzz_riff/fuzz_riff-004.bin b/src/fuzz/corpus/fuzz_riff/fuzz_riff-004.bin Binary files differ. diff --git a/src/fuzz/corpus/fuzz_riff/fuzz_riff-005.bin b/src/fuzz/corpus/fuzz_riff/fuzz_riff-005.bin Binary files differ. diff --git a/src/fuzz/corpus/fuzz_rtf/fuzz_rtf-000.bin b/src/fuzz/corpus/fuzz_rtf/fuzz_rtf-000.bin Binary files differ. diff --git a/src/fuzz/corpus/fuzz_rtf/fuzz_rtf-001.bin b/src/fuzz/corpus/fuzz_rtf/fuzz_rtf-001.bin Binary files differ. diff --git a/src/fuzz/corpus/fuzz_rtf/fuzz_rtf-002.bin b/src/fuzz/corpus/fuzz_rtf/fuzz_rtf-002.bin Binary files differ. diff --git a/src/fuzz/corpus/fuzz_rtf/fuzz_rtf-003.bin b/src/fuzz/corpus/fuzz_rtf/fuzz_rtf-003.bin Binary files differ. diff --git a/src/fuzz/corpus/fuzz_rtf/fuzz_rtf-004.bin b/src/fuzz/corpus/fuzz_rtf/fuzz_rtf-004.bin Binary files differ. diff --git a/src/fuzz/corpus/fuzz_rtf/fuzz_rtf-005.bin b/src/fuzz/corpus/fuzz_rtf/fuzz_rtf-005.bin Binary files differ. diff --git a/src/fuzz/corpus/fuzz_s3m/fuzz_s3m-000.bin b/src/fuzz/corpus/fuzz_s3m/fuzz_s3m-000.bin Binary files differ. diff --git a/src/fuzz/corpus/fuzz_s3m/fuzz_s3m-001.bin b/src/fuzz/corpus/fuzz_s3m/fuzz_s3m-001.bin Binary files differ. diff --git a/src/fuzz/corpus/fuzz_s3m/fuzz_s3m-002.bin b/src/fuzz/corpus/fuzz_s3m/fuzz_s3m-002.bin Binary files differ. diff --git a/src/fuzz/corpus/fuzz_s3m/fuzz_s3m-003.bin b/src/fuzz/corpus/fuzz_s3m/fuzz_s3m-003.bin Binary files differ. diff --git a/src/fuzz/corpus/fuzz_s3m/fuzz_s3m-004.bin b/src/fuzz/corpus/fuzz_s3m/fuzz_s3m-004.bin Binary files differ. diff --git a/src/fuzz/corpus/fuzz_s3m/fuzz_s3m-005.bin b/src/fuzz/corpus/fuzz_s3m/fuzz_s3m-005.bin Binary files differ. diff --git a/src/fuzz/corpus/fuzz_sid/fuzz_sid-000.bin b/src/fuzz/corpus/fuzz_sid/fuzz_sid-000.bin Binary files differ. diff --git a/src/fuzz/corpus/fuzz_sid/fuzz_sid-001.bin b/src/fuzz/corpus/fuzz_sid/fuzz_sid-001.bin Binary files differ. diff --git a/src/fuzz/corpus/fuzz_sid/fuzz_sid-002.bin b/src/fuzz/corpus/fuzz_sid/fuzz_sid-002.bin Binary files differ. diff --git a/src/fuzz/corpus/fuzz_sid/fuzz_sid-003.bin b/src/fuzz/corpus/fuzz_sid/fuzz_sid-003.bin Binary files differ. diff --git a/src/fuzz/corpus/fuzz_sid/fuzz_sid-004.bin b/src/fuzz/corpus/fuzz_sid/fuzz_sid-004.bin Binary files differ. diff --git a/src/fuzz/corpus/fuzz_sid/fuzz_sid-005.bin b/src/fuzz/corpus/fuzz_sid/fuzz_sid-005.bin Binary files differ. diff --git a/src/fuzz/corpus/fuzz_tiff/fuzz_tiff-000.bin b/src/fuzz/corpus/fuzz_tiff/fuzz_tiff-000.bin Binary files differ. diff --git a/src/fuzz/corpus/fuzz_tiff/fuzz_tiff-001.bin b/src/fuzz/corpus/fuzz_tiff/fuzz_tiff-001.bin Binary files differ. diff --git a/src/fuzz/corpus/fuzz_tiff/fuzz_tiff-002.bin b/src/fuzz/corpus/fuzz_tiff/fuzz_tiff-002.bin Binary files differ. diff --git a/src/fuzz/corpus/fuzz_tiff/fuzz_tiff-003.bin b/src/fuzz/corpus/fuzz_tiff/fuzz_tiff-003.bin Binary files differ. diff --git a/src/fuzz/corpus/fuzz_tiff/fuzz_tiff-004.bin b/src/fuzz/corpus/fuzz_tiff/fuzz_tiff-004.bin Binary files differ. diff --git a/src/fuzz/corpus/fuzz_tiff/fuzz_tiff-005.bin b/src/fuzz/corpus/fuzz_tiff/fuzz_tiff-005.bin Binary files differ. diff --git a/src/fuzz/corpus/fuzz_unzip/fuzz_unzip-000.bin b/src/fuzz/corpus/fuzz_unzip/fuzz_unzip-000.bin Binary files differ. diff --git a/src/fuzz/corpus/fuzz_unzip/fuzz_unzip-001.bin b/src/fuzz/corpus/fuzz_unzip/fuzz_unzip-001.bin Binary files differ. diff --git a/src/fuzz/corpus/fuzz_unzip/fuzz_unzip-002.bin b/src/fuzz/corpus/fuzz_unzip/fuzz_unzip-002.bin Binary files differ. diff --git a/src/fuzz/corpus/fuzz_unzip/fuzz_unzip-003.bin b/src/fuzz/corpus/fuzz_unzip/fuzz_unzip-003.bin Binary files differ. diff --git a/src/fuzz/corpus/fuzz_unzip/fuzz_unzip-004.bin b/src/fuzz/corpus/fuzz_unzip/fuzz_unzip-004.bin Binary files differ. diff --git a/src/fuzz/corpus/fuzz_unzip/fuzz_unzip-005.bin b/src/fuzz/corpus/fuzz_unzip/fuzz_unzip-005.bin Binary files differ. diff --git a/src/fuzz/corpus/fuzz_unzip/fuzz_unzip-006.bin b/src/fuzz/corpus/fuzz_unzip/fuzz_unzip-006.bin Binary files differ. diff --git a/src/fuzz/corpus/fuzz_unzip/fuzz_unzip-007.bin b/src/fuzz/corpus/fuzz_unzip/fuzz_unzip-007.bin Binary files differ. diff --git a/src/fuzz/corpus/fuzz_wav/fuzz_wav-000.bin b/src/fuzz/corpus/fuzz_wav/fuzz_wav-000.bin Binary files differ. diff --git a/src/fuzz/corpus/fuzz_wav/fuzz_wav-001.bin b/src/fuzz/corpus/fuzz_wav/fuzz_wav-001.bin Binary files differ. diff --git a/src/fuzz/corpus/fuzz_wav/fuzz_wav-002.bin b/src/fuzz/corpus/fuzz_wav/fuzz_wav-002.bin Binary files differ. diff --git a/src/fuzz/corpus/fuzz_wav/fuzz_wav-003.bin b/src/fuzz/corpus/fuzz_wav/fuzz_wav-003.bin Binary files differ. diff --git a/src/fuzz/corpus/fuzz_wav/fuzz_wav-004.bin b/src/fuzz/corpus/fuzz_wav/fuzz_wav-004.bin Binary files differ. diff --git a/src/fuzz/corpus/fuzz_wav/fuzz_wav-005.bin b/src/fuzz/corpus/fuzz_wav/fuzz_wav-005.bin Binary files differ. diff --git a/src/fuzz/corpus/fuzz_xm/fuzz_xm-000.bin b/src/fuzz/corpus/fuzz_xm/fuzz_xm-000.bin Binary files differ. diff --git a/src/fuzz/corpus/fuzz_xm/fuzz_xm-001.bin b/src/fuzz/corpus/fuzz_xm/fuzz_xm-001.bin Binary files differ. diff --git a/src/fuzz/corpus/fuzz_xm/fuzz_xm-002.bin b/src/fuzz/corpus/fuzz_xm/fuzz_xm-002.bin Binary files differ. diff --git a/src/fuzz/corpus/fuzz_xm/fuzz_xm-003.bin b/src/fuzz/corpus/fuzz_xm/fuzz_xm-003.bin Binary files differ. diff --git a/src/fuzz/corpus/fuzz_xm/fuzz_xm-004.bin b/src/fuzz/corpus/fuzz_xm/fuzz_xm-004.bin Binary files differ. diff --git a/src/fuzz/corpus/fuzz_xm/fuzz_xm-005.bin b/src/fuzz/corpus/fuzz_xm/fuzz_xm-005.bin Binary files differ. diff --git a/src/fuzz/corpus/fuzz_zip/fuzz_zip-000.bin b/src/fuzz/corpus/fuzz_zip/fuzz_zip-000.bin Binary files differ. diff --git a/src/fuzz/corpus/fuzz_zip/fuzz_zip-001.bin b/src/fuzz/corpus/fuzz_zip/fuzz_zip-001.bin Binary files differ. diff --git a/src/fuzz/corpus/fuzz_zip/fuzz_zip-002.bin b/src/fuzz/corpus/fuzz_zip/fuzz_zip-002.bin Binary files differ. diff --git a/src/fuzz/corpus/fuzz_zip/fuzz_zip-003.bin b/src/fuzz/corpus/fuzz_zip/fuzz_zip-003.bin Binary files differ. diff --git a/src/fuzz/corpus/fuzz_zip/fuzz_zip-004.bin b/src/fuzz/corpus/fuzz_zip/fuzz_zip-004.bin Binary files differ. diff --git a/src/fuzz/corpus/fuzz_zip/fuzz_zip-005.bin b/src/fuzz/corpus/fuzz_zip/fuzz_zip-005.bin Binary files differ. diff --git a/src/fuzz/corpus/known-findings/fuzz_applefile-issue06.bin b/src/fuzz/corpus/known-findings/fuzz_applefile-issue06.bin Binary files differ. diff --git a/src/fuzz/corpus/known-findings/fuzz_archive-issue04.bin b/src/fuzz/corpus/known-findings/fuzz_archive-issue04.bin @@ -0,0 +1 @@ +þ¨ÿÿ +\ No newline at end of file diff --git a/src/fuzz/corpus/known-findings/fuzz_datasource-issue05.bin b/src/fuzz/corpus/known-findings/fuzz_datasource-issue05.bin Binary files differ. diff --git a/src/fuzz/corpus/known-findings/fuzz_deb-issue03.bin b/src/fuzz/corpus/known-findings/fuzz_deb-issue03.bin @@ -0,0 +1,3 @@ +"ßi‹!<arch> +control.tar.gz 1343577 5 960 Homepage: 100644 4 ` +2 +\ No newline at end of file diff --git a/src/fuzz/corpus/known-findings/fuzz_deb-issue08.bin b/src/fuzz/corpus/known-findings/fuzz_deb-issue08.bin Binary files differ. diff --git a/src/fuzz/corpus/known-findings/fuzz_elf-issue09.bin b/src/fuzz/corpus/known-findings/fuzz_elf-issue09.bin Binary files differ. diff --git a/src/fuzz/corpus/known-findings/fuzz_ole2-issue13.bin b/src/fuzz/corpus/known-findings/fuzz_ole2-issue13.bin Binary files differ. diff --git a/src/fuzz/corpus/known-findings/fuzz_png-issue02.bin b/src/fuzz/corpus/known-findings/fuzz_png-issue02.bin @@ -0,0 +1,3 @@ +RSID‰PNG + +ÿÿÿÿÿÿÿ +\ No newline at end of file diff --git a/src/fuzz/corpus/known-findings/fuzz_png-issue12.bin b/src/fuzz/corpus/known-findings/fuzz_png-issue12.bin Binary files differ. diff --git a/src/fuzz/corpus/known-findings/fuzz_qt-issue10.bin b/src/fuzz/corpus/known-findings/fuzz_qt-issue10.bin Binary files differ. diff --git a/src/fuzz/corpus/known-findings/fuzz_real-issue11.bin b/src/fuzz/corpus/known-findings/fuzz_real-issue11.bin Binary files differ. diff --git a/src/fuzz/corpus/known-findings/fuzz_riff-issue07.bin b/src/fuzz/corpus/known-findings/fuzz_riff-issue07.bin Binary files differ. diff --git a/src/fuzz/corpus/known-findings/fuzz_unzip-issue01.bin b/src/fuzz/corpus/known-findings/fuzz_unzip-issue01.bin Binary files differ. diff --git a/src/fuzz/fuzz_common.h b/src/fuzz/fuzz_common.h @@ -0,0 +1,1085 @@ +/* + This file is part of libextractor. + Copyright (C) 2026 Christian Grothoff + + libextractor 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, or (at your + option) any later version. + + libextractor 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 libextractor; see the file COPYING. If not, write to the + Free Software Foundation, Inc., 51 Franklin Street, Fifth Floor, + Boston, MA 02110-1301, USA. + */ +/** + * @file fuzz/fuzz_common.h + * @brief shared, header-only fuzzing driver for the libextractor fuzzers + * @author Christian Grothoff + * + * Every harness in this directory is a single translation unit that + * includes this header. The header provides: + * - a deterministic, seeded PRNG (splitmix64 / xoshiro256**), + * - a generic byte-level mutator, + * - crash bookkeeping (the input of the currently running iteration is + * dumped to the crash directory whenever the process dies), + * - a standalone @c main() driver (generator + mutator loop, corpus + * replay, single-file replay) so that the harnesses are usable with + * a plain gcc + ASAN/UBSAN build, i.e. without clang/libFuzzer. + * + * The harness itself must provide: + * - @c LLVMFuzzerTestOneInput() (the actual fuzz target), + * - @c fuzz_generate() (a structure-aware input generator), + * - @c fuzz_seed_count() / @c fuzz_seed_get() (a built-in seed corpus). + * + * Define @c FUZZ_NO_MAIN when linking against libFuzzer or AFL++'s + * driver, which supply their own @c main(). Anything the fuzz *target* + * needs must stay outside the @c FUZZ_NO_MAIN block; see + * BUILD-INTEGRATION.md section 6. + */ +#ifndef LE_FUZZ_COMMON_H +#define LE_FUZZ_COMMON_H 1 + +#include <stdint.h> +#include <stddef.h> +#include <stdio.h> +#include <stdlib.h> +#include <string.h> +#include <unistd.h> +#include <fcntl.h> +#include <signal.h> +#include <errno.h> +#include <sys/stat.h> +#include <sys/types.h> +#include <dirent.h> + +#ifndef FUZZ_HARNESS_NAME +#define FUZZ_HARNESS_NAME "fuzz" +#endif + +/** + * Not every harness uses every helper; silence -Wunused-function. + */ +#define FUZZ_UNUSED __attribute__ ((unused)) + +/** + * Hard upper bound on the size of a single fuzz input. libextractor + * parses container formats whose interesting structures (a ZIP central + * directory, an OLE2 FAT, a RIFF chunk tree) live well past the first + * few kilobytes, so this is considerably larger than a protocol fuzzer + * would need. + */ +#ifndef FUZZ_MAX_INPUT +#define FUZZ_MAX_INPUT (256 * 1024) +#endif + +/** + * Default number of iterations of the built-in driver. Kept small so + * that "make check" stays in the "couple of seconds" range; raise with + * --iterations=N or the LE_FUZZ_ITERATIONS environment variable. + */ +#ifndef FUZZ_DEFAULT_ITERATIONS +#define FUZZ_DEFAULT_ITERATIONS 3000 +#endif + +/** + * Default per-iteration watchdog, in seconds. + */ +#ifndef FUZZ_DEFAULT_TIMEOUT +#define FUZZ_DEFAULT_TIMEOUT 20 +#endif + + +/* ------------------------------------------------------------------ */ +/* Interface to be implemented by each harness */ +/* ------------------------------------------------------------------ */ + +/** + * The fuzz target. Signature is the libFuzzer one on purpose, so that + * the very same harness can be linked with libFuzzer or AFL++. + */ +int +LLVMFuzzerTestOneInput (const uint8_t *data, + size_t size); + +struct fuzz_rng; + +/** + * Structure-aware generator used by the built-in standalone driver. + * Must write at most @a cap bytes to @a buf and return the number of + * bytes written. Purely random bytes almost never form a file that + * gets past a plugin's magic-number check, so this is what actually + * makes the gcc-only driver useful. + */ +FUZZ_UNUSED static size_t +fuzz_generate (struct fuzz_rng *rng, + uint8_t *buf, + size_t cap); + +/** + * @return number of entries in the built-in seed corpus + */ +FUZZ_UNUSED static size_t +fuzz_seed_count (void); + +/** + * @param idx index of the seed to retrieve + * @param[out] len set to the length of the seed + * @return pointer to the seed bytes + */ +FUZZ_UNUSED static const uint8_t * +fuzz_seed_get (size_t idx, + size_t *len); + + +/* ------------------------------------------------------------------ */ +/* Deterministic PRNG */ +/* ------------------------------------------------------------------ */ + +struct fuzz_rng +{ + uint64_t s[4]; +}; + + +FUZZ_UNUSED static uint64_t +fuzz_splitmix64 (uint64_t *x) +{ + uint64_t z; + + *x += UINT64_C (0x9E3779B97F4A7C15); + z = *x; + z = (z ^ (z >> 30)) * UINT64_C (0xBF58476D1CE4E5B9); + z = (z ^ (z >> 27)) * UINT64_C (0x94D049BB133111EB); + return z ^ (z >> 31); +} + + +FUZZ_UNUSED static void +fuzz_rng_seed (struct fuzz_rng *r, + uint64_t seed) +{ + uint64_t x = seed; + unsigned int i; + + for (i = 0; i < 4; i++) + r->s[i] = fuzz_splitmix64 (&x); +} + + +FUZZ_UNUSED static uint64_t +fuzz_rot64 (uint64_t x, + unsigned int k) +{ + return (x << k) | (x >> (64 - k)); +} + + +/** + * xoshiro256** -- small, fast, deterministic and identical on every + * platform, which is what we need for reproducible fuzzing runs. + */ +FUZZ_UNUSED static uint64_t +fuzz_next (struct fuzz_rng *r) +{ + const uint64_t res = fuzz_rot64 (r->s[1] * 5, 7) * 9; + const uint64_t t = r->s[1] << 17; + + r->s[2] ^= r->s[0]; + r->s[3] ^= r->s[1]; + r->s[1] ^= r->s[2]; + r->s[0] ^= r->s[3]; + r->s[2] ^= t; + r->s[3] = fuzz_rot64 (r->s[3], 45); + return res; +} + + +/** + * @return uniformly distributed value in [0, n), 0 if @a n is 0 + */ +FUZZ_UNUSED static uint32_t +fuzz_below (struct fuzz_rng *r, + uint32_t n) +{ + if (0 == n) + return 0; + return (uint32_t) (fuzz_next (r) % n); +} + + +FUZZ_UNUSED static uint8_t +fuzz_byte (struct fuzz_rng *r) +{ + return (uint8_t) (fuzz_next (r) & 0xFF); +} + + +/** + * @return true with a probability of 1/@a n + */ +FUZZ_UNUSED static int +fuzz_chance (struct fuzz_rng *r, + uint32_t n) +{ + return 0 == fuzz_below (r, n); +} + + +/** + * Append @a n little-endian bytes of @a v to @a buf. + * + * @param buf buffer to append to + * @param[in,out] len current length, updated + * @param cap capacity of @a buf + * @param v value to append + * @param n number of bytes of @a v to append + */ +FUZZ_UNUSED static void +fuzz_put_le (uint8_t *buf, + size_t *len, + size_t cap, + uint64_t v, + unsigned int n) +{ + unsigned int i; + + for (i = 0; i < n; i++) + { + if (*len >= cap) + return; + buf[(*len)++] = (uint8_t) (v >> (8 * i)); + } +} + + +/** + * Append @a n big-endian bytes of @a v to @a buf. + * + * @param buf buffer to append to + * @param[in,out] len current length, updated + * @param cap capacity of @a buf + * @param v value to append + * @param n number of bytes of @a v to append + */ +FUZZ_UNUSED static void +fuzz_put_be (uint8_t *buf, + size_t *len, + size_t cap, + uint64_t v, + unsigned int n) +{ + unsigned int i; + + for (i = 0; i < n; i++) + { + if (*len >= cap) + return; + buf[(*len)++] = (uint8_t) (v >> (8 * (n - 1 - i))); + } +} + + +/** + * Append @a n bytes from @a src to @a buf, truncating at @a cap. + */ +FUZZ_UNUSED static void +fuzz_put_mem (uint8_t *buf, + size_t *len, + size_t cap, + const void *src, + size_t n) +{ + if (*len + n > cap) + n = (*len > cap) ? 0 : (cap - *len); + memcpy (buf + *len, src, n); + *len += n; +} + + +/** + * Append the NUL-terminated string @a s (without its NUL) to @a buf. + */ +FUZZ_UNUSED static void +fuzz_put_str (uint8_t *buf, + size_t *len, + size_t cap, + const char *s) +{ + fuzz_put_mem (buf, len, cap, s, strlen (s)); +} + + +/* ------------------------------------------------------------------ */ +/* Global driver state */ +/* ------------------------------------------------------------------ */ + +/** + * Non-zero if the input of the current iteration comes from a trusted + * source (the built-in generator, the built-in seed corpus, --file or + * --corpus-dir) and has NOT been mutated afterwards. Harnesses use + * this to enable "ground truth" oracles. Random mutations invalidate + * such declarations, hence the flag. + */ +FUZZ_UNUSED static int fuzz_pristine; + +/** + * Non-zero to let the harness be chatty about what it is doing. + */ +FUZZ_UNUSED static int fuzz_verbose; + +/** + * Non-zero to skip the replay of the built-in seed corpus at the start + * of a run (useful when a seed is known to trigger an already-reported + * finding and one wants to look for others). + */ +FUZZ_UNUSED static int fuzz_skip_seeds; + +/* Only read by the built-in driver; under -DFUZZ_NO_MAIN (the libFuzzer + and AFL++ builds, see contrib/oss-fuzz/build.sh) they are written but + never read, which is not a defect. */ +FUZZ_UNUSED static uint64_t fuzz_cur_seed; +FUZZ_UNUSED static uint64_t fuzz_cur_iter; +static const uint8_t *fuzz_cur_input; +static size_t fuzz_cur_input_len; +static const char *fuzz_crash_dir = "crashes"; + +/* Pre-rendered, so that the death/signal handlers stay + async-signal-safe (no snprintf, no malloc). */ +static char fuzz_crash_path[512]; +static char fuzz_crash_msg[512]; +static volatile sig_atomic_t fuzz_dumped; + + +FUZZ_UNUSED static void +fuzz_write_all (int fd, + const void *buf, + size_t len) +{ + const char *p = (const char *) buf; + + while (0 != len) + { + ssize_t w = write (fd, p, len); + + if (0 >= w) + break; + p += w; + len -= (size_t) w; + } +} + + +FUZZ_UNUSED static void +fuzz_msg (const char *s) +{ + fuzz_write_all (STDERR_FILENO, s, strlen (s)); +} + + +/** + * Dump the input of the currently running iteration so that the + * failure can be replayed with --file=... Async-signal-safe. + */ +FUZZ_UNUSED static void +fuzz_dump_current (void) +{ + int fd; + + if (fuzz_dumped) + return; + fuzz_dumped = 1; + if ( (NULL == fuzz_cur_input) || + ('\0' == fuzz_crash_path[0]) ) + return; + (void) mkdir (fuzz_crash_dir, 0755); + fd = open (fuzz_crash_path, + O_WRONLY | O_CREAT | O_TRUNC, + 0644); + if (0 > fd) + { + fuzz_msg ("\n*** FUZZ: failed to write crash file ***\n"); + return; + } + fuzz_write_all (fd, fuzz_cur_input, fuzz_cur_input_len); + (void) close (fd); + fuzz_msg ("\n*** FUZZ: reproducer written to "); + fuzz_msg (fuzz_crash_path); + fuzz_msg (" ***\n*** FUZZ: "); + fuzz_msg (fuzz_crash_msg); + fuzz_msg (" ***\n"); +} + + +FUZZ_UNUSED static void +fuzz_death_callback (void) +{ + fuzz_dump_current (); +} + + +FUZZ_UNUSED static void +fuzz_sig_handler (int sig) +{ + fuzz_dump_current (); + if (SIGALRM == sig) + { + fuzz_msg ("*** FUZZ: HANG detected (watchdog fired) ***\n"); + _exit (99); + } + /* restore default handler and re-raise so that the usual + ASAN/abort diagnostics are produced */ + signal (sig, SIG_DFL); + raise (sig); +} + + +/** + * Report a logical (non-memory-safety) finding: dump the reproducer + * and abort so that the failure is impossible to overlook. + * + * @param what human readable description of the contract violation + */ +FUZZ_UNUSED static void +fuzz_report_finding (const char *what) +{ + size_t l = strlen (what); + + if (l >= sizeof (fuzz_crash_msg)) + l = sizeof (fuzz_crash_msg) - 1; + memcpy (fuzz_crash_msg, what, l); + fuzz_crash_msg[l] = '\0'; + fuzz_msg ("\n*** FUZZ FINDING: "); + fuzz_msg (fuzz_crash_msg); + fuzz_msg (" ***\n"); + fuzz_dump_current (); + abort (); +} + + +/* Weak declaration: resolved when built with ASAN (gcc or clang), + NULL otherwise. ASAN calls this right before it terminates the + process, which is the only reliable hook when abort_on_error=0. */ +extern void +__sanitizer_set_death_callback (void (*cb)(void)) __attribute__ ((weak)); + + +/** + * Ignore SIGPIPE. Idempotent, so it is safe to call on every execution. + * + * This deliberately lives OUTSIDE the #ifndef FUZZ_NO_MAIN block below, + * because it is needed by the fuzz *target*, not merely by the built-in + * driver: fuzz_extract runs plugins in-process but the core library + * still writes to pipes in some code paths, and fuzz_ipc writes into a + * socketpair whose peer end it closes on purpose. None of the external + * engines ignores SIGPIPE for us -- libFuzzer has no -handle_sigpipe + * flag at all -- so without this an OSS-Fuzz build simply stops fuzzing + * after a few dozen executions, with no report. + */ +FUZZ_UNUSED static void +fuzz_ignore_sigpipe (void) +{ + static int sigpipe_ignored; + + if (sigpipe_ignored) + return; + sigpipe_ignored = 1; + (void) signal (SIGPIPE, SIG_IGN); +} + + +/** + * Read an unsigned integer from the environment. + * + * @param name variable to read + * @param dflt value to return if unset or unparsable + * @return the configured value + */ +FUZZ_UNUSED static unsigned long +fuzz_env_ulong (const char *name, + unsigned long dflt) +{ + const char *e = getenv (name); + char *end; + unsigned long v; + + if (NULL == e) + return dflt; + errno = 0; + v = strtoul (e, &end, 0); + if ( (end == e) || + (0 != errno) ) + return dflt; + return v; +} + + +#ifndef FUZZ_NO_MAIN + +static void +fuzz_install_handlers (void) +{ + struct sigaction sa; + + if (NULL != __sanitizer_set_death_callback) + __sanitizer_set_death_callback (&fuzz_death_callback); + memset (&sa, 0, sizeof (sa)); + sa.sa_handler = &fuzz_sig_handler; + sigemptyset (&sa.sa_mask); + sa.sa_flags = 0; + (void) sigaction (SIGABRT, &sa, NULL); + (void) sigaction (SIGSEGV, &sa, NULL); + (void) sigaction (SIGBUS, &sa, NULL); + (void) sigaction (SIGILL, &sa, NULL); + (void) sigaction (SIGFPE, &sa, NULL); + (void) sigaction (SIGALRM, &sa, NULL); + fuzz_ignore_sigpipe (); +} + + +/** + * Remember which input we are about to feed to the target, and + * pre-render the name of the file it would be dumped to. + */ +static void +fuzz_set_current (const uint8_t *data, + size_t size, + const char *tag) +{ + fuzz_cur_input = data; + fuzz_cur_input_len = size; + fuzz_dumped = 0; + (void) snprintf (fuzz_crash_path, + sizeof (fuzz_crash_path), + "%s/crash-%s-seed%llu-iter%llu.bin", + fuzz_crash_dir, + FUZZ_HARNESS_NAME, + (unsigned long long) fuzz_cur_seed, + (unsigned long long) fuzz_cur_iter); + (void) snprintf (fuzz_crash_msg, + sizeof (fuzz_crash_msg), + "harness=%s seed=%llu iteration=%llu source=%s", + FUZZ_HARNESS_NAME, + (unsigned long long) fuzz_cur_seed, + (unsigned long long) fuzz_cur_iter, + tag); +} + + +/* ------------------------------------------------------------------ */ +/* Generic byte-level mutator */ +/* ------------------------------------------------------------------ */ + +static const uint8_t fuzz_interesting[] = { + 0x00, 0x01, 0x02, 0x04, 0x07, 0x08, 0x09, 0x0A, 0x0D, 0x10, 0x1A, + 0x20, 0x22, 0x25, 0x27, 0x2C, 0x2E, 0x2F, 0x30, 0x3A, 0x3B, 0x3D, + 0x5C, 0x7B, 0x7D, 0x7F, 0x80, 0xC0, 0xFE, 0xFF +}; + +/** + * Byte strings that are structurally meaningful to at least one of the + * formats libextractor parses: container magic numbers, chunk tags and + * the length fields that guard them. + */ +static const char *const fuzz_interesting_str[] = { + "RIFF", "WAVE", "AVI ", "LIST", "INFO", "fmt ", "data", + "PK\x03\x04", "PK\x01\x02", "PK\x05\x06", + "\x89PNG\r\n\x1a\n", "IHDR", "tEXt", "zTXt", "iTXt", "IEND", + "GIF89a", "GIF87a", "\xff\xd8\xff\xe0", "\xff\xd8\xff\xe1", "Exif", + "\x7f" "ELF", "%!PS-Adobe-", "%%Title:", "%%Creator:", + "{\\rtf1", "\\info", "\\title", "\\*\\", "\\u", "\\'", + "NESM\x1a", "NSFE", "PSID", "RSID", "SCRM", "IMPM", + "Extended Module: ", "MThd", "MTrk", "fLaC", "OggS", + "\x1f\x8b\x08", "BZh9", "!<arch>\n", "debian-binary", + "moov", "mvhd", "ftyp", "mdat", "cmov", + "\xd0\xcf\x11\xe0\xa1\xb1\x1a\xe1", + "\x00\x05\x16\x07\x00\x02\x00\x00", + "\xf7\x02", "\xf7\x03", "\xed\xab\xee\xdb", + "mimetypeapplication/vnd.oasis.opendocument.", + "meta.xml", "content.xml", "docProps/core.xml", "word/document.xml", + ".TH ", ".SH ", "\r\n", "\n", "\xff\xff\xff\xff", "\x00\x00\x00\x00" +}; + + +/** + * Apply a single random mutation to @a buf. + * + * @param rng the PRNG state + * @param[in,out] buf the buffer to mutate + * @param len current length + * @param cap capacity of @a buf + * @return new length + */ +static size_t +fuzz_mutate_once (struct fuzz_rng *rng, + uint8_t *buf, + size_t len, + size_t cap) +{ + uint32_t op; + + if (0 == len) + { + buf[0] = fuzz_byte (rng); + return 1; + } + op = fuzz_below (rng, 11); + switch (op) + { + case 0: /* bit flip */ + { + size_t p = fuzz_below (rng, (uint32_t) len); + + buf[p] = (uint8_t) (buf[p] ^ (1u << fuzz_below (rng, 8))); + break; + } + case 1: /* random byte */ + buf[fuzz_below (rng, (uint32_t) len)] = fuzz_byte (rng); + break; + case 2: /* interesting byte */ + buf[fuzz_below (rng, (uint32_t) len)] = + fuzz_interesting[fuzz_below (rng, + (uint32_t) (sizeof (fuzz_interesting)))]; + break; + case 3: /* add/sub small value */ + { + size_t p = fuzz_below (rng, (uint32_t) len); + + buf[p] = (uint8_t) (buf[p] + (int) fuzz_below (rng, 17) - 8); + break; + } + case 4: /* erase a run */ + { + size_t p = fuzz_below (rng, (uint32_t) len); + size_t n = 1 + fuzz_below (rng, (uint32_t) (len - p)); + + memmove (buf + p, buf + p + n, len - p - n); + len -= n; + break; + } + case 5: /* insert repeated byte */ + { + size_t p = fuzz_below (rng, (uint32_t) len + 1); + size_t n = 1 + fuzz_below (rng, 64); + uint8_t v = fuzz_byte (rng); + + if (len + n > cap) + n = cap - len; + if (0 == n) + break; + memmove (buf + p + n, buf + p, len - p); + memset (buf + p, v, n); + len += n; + break; + } + case 6: /* duplicate a chunk */ + { + size_t p = fuzz_below (rng, (uint32_t) len); + size_t n = 1 + fuzz_below (rng, (uint32_t) (len - p)); + size_t d = fuzz_below (rng, (uint32_t) len + 1); + + if (len + n > cap) + n = cap - len; + if (0 == n) + break; + memmove (buf + d + n, buf + d, len - d); + memmove (buf + d, buf + ((p >= d) ? (p + n) : p), n); + len += n; + break; + } + case 7: /* insert an interesting token */ + { + const char *s = + fuzz_interesting_str[fuzz_below (rng, + (uint32_t) + (sizeof (fuzz_interesting_str) + / sizeof (fuzz_interesting_str[0])))]; + size_t n = strlen (s); + size_t p = fuzz_below (rng, (uint32_t) len + 1); + + if (len + n > cap) + break; + memmove (buf + p + n, buf + p, len - p); + memcpy (buf + p, s, n); + len += n; + break; + } + case 8: /* swap two bytes */ + { + size_t a = fuzz_below (rng, (uint32_t) len); + size_t b = fuzz_below (rng, (uint32_t) len); + uint8_t t = buf[a]; + + buf[a] = buf[b]; + buf[b] = t; + break; + } + case 9: /* overwrite a 16/32 bit field with an + extreme value; length fields are where + the parser bugs are */ + { + static const uint32_t vals[] = { + 0, 1, 2, 0x7F, 0x80, 0xFF, 0x100, 0x7FFF, 0x8000, 0xFFFF, + 0x10000, 0x7FFFFFFF, 0x80000000, 0xFFFFFFFF + }; + size_t p = fuzz_below (rng, (uint32_t) len); + unsigned int n = (0 == fuzz_below (rng, 2)) ? 2 : 4; + uint32_t v = vals[fuzz_below (rng, + (uint32_t) (sizeof (vals) + / sizeof (vals[0])))]; + unsigned int k; + + if (p + n > len) + break; + /* Keep both loops braced. uncrustify 0.78 spins forever on an + unbraced if/else whose body is an unbraced for whose statement + carries a trailing comment, which hangs the pre-commit hook. */ + if (0 == fuzz_below (rng, 2)) + { + /* little endian */ + for (k = 0; k < n; k++) + buf[p + k] = (uint8_t) (v >> (8 * k)); + } + else + { + /* big endian */ + for (k = 0; k < n; k++) + buf[p + k] = (uint8_t) (v >> (8 * (n - 1 - k))); + } + break; + } + default: /* truncate */ + len = 1 + fuzz_below (rng, (uint32_t) len); + break; + } + return len; +} + + +/* ------------------------------------------------------------------ */ +/* Standalone driver */ +/* ------------------------------------------------------------------ */ + +static int +fuzz_run_file (const char *path) +{ + FILE *f; + uint8_t *buf; + size_t n; + + f = fopen (path, "rb"); + if (NULL == f) + { + fprintf (stderr, + "%s: cannot open '%s': %s\n", + FUZZ_HARNESS_NAME, + path, + strerror (errno)); + return 1; + } + buf = (uint8_t *) malloc (FUZZ_MAX_INPUT); + if (NULL == buf) + { + (void) fclose (f); + return 1; + } + n = fread (buf, 1, FUZZ_MAX_INPUT, f); + (void) fclose (f); + fuzz_pristine = 1; + fuzz_set_current (buf, n, path); + alarm (FUZZ_DEFAULT_TIMEOUT); + (void) LLVMFuzzerTestOneInput (buf, n); + alarm (0); + free (buf); + return 0; +} + + +static int +fuzz_run_corpus_dir (const char *dir) +{ + DIR *d; + struct dirent *de; + char path[1024]; + int ret = 0; + unsigned int cnt = 0; + + d = opendir (dir); + if (NULL == d) + { + fprintf (stderr, + "%s: cannot open corpus dir '%s': %s\n", + FUZZ_HARNESS_NAME, + dir, + strerror (errno)); + return 1; + } + while (NULL != (de = readdir (d))) + { + struct stat sb; + + if ('.' == de->d_name[0]) + continue; + (void) snprintf (path, sizeof (path), "%s/%s", dir, de->d_name); + if ( (0 != stat (path, &sb)) || + (! S_ISREG (sb.st_mode)) ) + continue; + fuzz_cur_iter = cnt++; + ret |= fuzz_run_file (path); + } + (void) closedir (d); + printf ("%s: replayed %u corpus file(s) from %s\n", + FUZZ_HARNESS_NAME, cnt, dir); + return ret; +} + + +static void +fuzz_usage (const char *argv0) +{ + printf ( + "Usage: %s [OPTIONS] [FILE...]\n" + "\n" + "In-process fuzzing harness '%s' for GNU libextractor.\n" + "\n" + " --iterations=N number of generate/mutate iterations (default %d)\n" + " --seed=N PRNG seed; runs are fully reproducible (default 1)\n" + " --corpus-dir=DIR replay every regular file in DIR and exit\n" + " --file=PATH replay a single input and exit (crash reproduction)\n" + " --crash-dir=DIR where to write reproducers (default 'crashes')\n" + " --timeout=SEC per-iteration watchdog (default %d, 0 disables)\n" + " --write-corpus=DIR write the built-in seed corpus to DIR and exit\n" + " --skip-seeds do not replay the built-in seed corpus first\n" + " --verbose be chatty about what the harness does\n" + " --help this text\n" + "\n" + "Environment: LE_FUZZ_ITERATIONS, LE_FUZZ_SEED, LE_FUZZ_TIMEOUT,\n" + " LE_FUZZ_CRASH_DIR, LE_FUZZ_VERBOSE, LE_FUZZ_SKIP_SEEDS\n" + "\n" + "Bare FILE arguments are equivalent to --file=FILE (libFuzzer-style).\n", + argv0, FUZZ_HARNESS_NAME, + (int) FUZZ_DEFAULT_ITERATIONS, (int) FUZZ_DEFAULT_TIMEOUT); +} + + +static int +fuzz_write_corpus (const char *dir) +{ + size_t i; + size_t n = fuzz_seed_count (); + + if ( (0 != mkdir (dir, 0755)) && + (EEXIST != errno) ) + { + fprintf (stderr, "%s: mkdir '%s': %s\n", + FUZZ_HARNESS_NAME, dir, strerror (errno)); + return 1; + } + for (i = 0; i < n; i++) + { + char path[1024]; + size_t len; + const uint8_t *s = fuzz_seed_get (i, &len); + FILE *f; + + (void) snprintf (path, sizeof (path), "%s/%s-%03u.bin", + dir, FUZZ_HARNESS_NAME, (unsigned int) i); + f = fopen (path, "wb"); + if (NULL == f) + { + fprintf (stderr, "%s: fopen '%s': %s\n", + FUZZ_HARNESS_NAME, path, strerror (errno)); + return 1; + } + if ( (0 != len) && + (len != fwrite (s, 1, len, f)) ) + { + (void) fclose (f); + return 1; + } + (void) fclose (f); + } + printf ("%s: wrote %u seed(s) to %s\n", + FUZZ_HARNESS_NAME, (unsigned int) n, dir); + return 0; +} + + +int +main (int argc, char *const *argv) +{ + uint64_t iterations = FUZZ_DEFAULT_ITERATIONS; + uint64_t seed = 1; + unsigned int timeout = FUZZ_DEFAULT_TIMEOUT; + const char *corpus_dir = NULL; + const char *single_file = NULL; + const char *write_corpus = NULL; + struct fuzz_rng rng; + uint8_t *buf; + uint64_t i; + int j; + const char *e; + int ret = 0; + + iterations = fuzz_env_ulong ("LE_FUZZ_ITERATIONS", iterations); + seed = fuzz_env_ulong ("LE_FUZZ_SEED", seed); + timeout = (unsigned int) fuzz_env_ulong ("LE_FUZZ_TIMEOUT", timeout); + e = getenv ("LE_FUZZ_CRASH_DIR"); + if (NULL != e) + fuzz_crash_dir = e; + fuzz_verbose = (0 != fuzz_env_ulong ("LE_FUZZ_VERBOSE", 0)); + fuzz_skip_seeds = (0 != fuzz_env_ulong ("LE_FUZZ_SKIP_SEEDS", 0)); + + for (j = 1; j < argc; j++) + { + const char *a = argv[j]; + + if (0 == strncmp (a, "--iterations=", 13)) + iterations = strtoull (a + 13, NULL, 10); + else if (0 == strncmp (a, "--seed=", 7)) + seed = strtoull (a + 7, NULL, 10); + else if (0 == strncmp (a, "--corpus-dir=", 13)) + corpus_dir = a + 13; + else if (0 == strncmp (a, "--file=", 7)) + single_file = a + 7; + else if (0 == strncmp (a, "--crash-dir=", 12)) + fuzz_crash_dir = a + 12; + else if (0 == strncmp (a, "--timeout=", 10)) + timeout = (unsigned int) strtoul (a + 10, NULL, 10); + else if (0 == strncmp (a, "--write-corpus=", 15)) + write_corpus = a + 15; + else if (0 == strcmp (a, "--skip-seeds")) + fuzz_skip_seeds = 1; + else if (0 == strcmp (a, "--verbose")) + fuzz_verbose = 1; + else if ( (0 == strcmp (a, "--help")) || + (0 == strcmp (a, "-h")) ) + { + fuzz_usage (argv[0]); + return 0; + } + else if ('-' == a[0]) + { + fprintf (stderr, "%s: unknown option '%s'\n", FUZZ_HARNESS_NAME, a); + fuzz_usage (argv[0]); + return 2; + } + else + single_file = a; + } + + fuzz_cur_seed = seed; + fuzz_install_handlers (); + + if (NULL != write_corpus) + return fuzz_write_corpus (write_corpus); + + if (NULL != single_file) + { + printf ("%s: replaying %s\n", FUZZ_HARNESS_NAME, single_file); + ret = fuzz_run_file (single_file); + printf ("%s: replay finished without a finding\n", FUZZ_HARNESS_NAME); + return ret; + } + if (NULL != corpus_dir) + return fuzz_run_corpus_dir (corpus_dir); + + buf = (uint8_t *) malloc (FUZZ_MAX_INPUT); + if (NULL == buf) + return 1; + fuzz_rng_seed (&rng, seed); + printf ("%s: seed=%llu iterations=%llu\n", + FUZZ_HARNESS_NAME, + (unsigned long long) seed, + (unsigned long long) iterations); + fflush (stdout); + + for (i = 0; i < iterations; i++) + { + size_t len; + const char *tag; + uint32_t mode; + + fuzz_cur_iter = i; + if ( (! fuzz_skip_seeds) && + (i < fuzz_seed_count ()) ) + { + size_t sl; + const uint8_t *s = fuzz_seed_get ((size_t) i, &sl); + + if (sl > FUZZ_MAX_INPUT) + sl = FUZZ_MAX_INPUT; + memcpy (buf, s, sl); + len = sl; + fuzz_pristine = 1; + tag = "builtin-seed"; + } + else + { + mode = fuzz_below (&rng, 100); + if (mode < 45) + { + fuzz_pristine = 1; + len = fuzz_generate (&rng, buf, FUZZ_MAX_INPUT); + tag = "generated"; + } + else + { + unsigned int k; + unsigned int nmut; + + if (mode < 80) + { + len = fuzz_generate (&rng, buf, FUZZ_MAX_INPUT); + tag = "generated+mutated"; + } + else + { + size_t sl; + const uint8_t *s; + + if (0 == fuzz_seed_count ()) + { + len = fuzz_generate (&rng, buf, FUZZ_MAX_INPUT); + } + else + { + s = fuzz_seed_get (fuzz_below (&rng, + (uint32_t) fuzz_seed_count ()), + &sl); + if (sl > FUZZ_MAX_INPUT) + sl = FUZZ_MAX_INPUT; + memcpy (buf, s, sl); + len = sl; + } + tag = "seed+mutated"; + } + fuzz_pristine = 0; + nmut = 1 + fuzz_below (&rng, 8); + for (k = 0; k < nmut; k++) + len = fuzz_mutate_once (&rng, buf, len, FUZZ_MAX_INPUT); + } + } + fuzz_set_current (buf, len, tag); + if (0 != timeout) + alarm (timeout); + (void) LLVMFuzzerTestOneInput (buf, len); + if (0 != timeout) + alarm (0); + } + free (buf); + printf ("%s: %llu iterations completed, no findings\n", + FUZZ_HARNESS_NAME, + (unsigned long long) iterations); + return ret; +} + + +#endif /* ! FUZZ_NO_MAIN */ + +#endif /* LE_FUZZ_COMMON_H */ diff --git a/src/fuzz/fuzz_convert.c b/src/fuzz/fuzz_convert.c @@ -0,0 +1,233 @@ +/* + This file is part of libextractor. + Copyright (C) 2026 Christian Grothoff + + libextractor 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, or (at your + option) any later version. + + libextractor 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 libextractor; see the file COPYING. If not, write to the + Free Software Foundation, Inc., 51 Franklin Street, Fifth Floor, + Boston, MA 02110-1301, USA. + */ +/** + * @file fuzz/fuzz_convert.c + * @brief fuzzer for the shared charset conversion helper and the + * metatype tables + * @author Christian Grothoff + * + * `EXTRACTOR_common_convert_to_utf8()` is called by nsfe, rtf, msoffice + * and png with a length and a charset name that all come out of the + * file being parsed. Its input is therefore attacker-controlled in + * three independent ways at once: the bytes, the length, and the name of + * the source encoding. + * + * The input buffer handed to it is `malloc()`ed at exactly the declared + * length and is deliberately *not* 0-terminated, because that is how the + * callers pass it: a helper that reaches for a terminator instead of + * honouring @a len reads past the end. + * + * Input format: + * + * byte 0 charset selector + * byte 1 behaviour bits: + * 0x01 also convert the result a second time + * 0x02 declare a length one byte longer than the + * allocation is -- OFF unless LE_FUZZ_MODEL_OVERLONG + * is set, since it models a *caller* bug, not one + * in the helper + * byte 2.. the bytes to convert + */ + +#define FUZZ_HARNESS_NAME "fuzz_convert" + +#include "fuzz_common.h" +#include "platform.h" +#include "extractor.h" +#include "convert.h" + +/** + * Charset names that plugins really pass, plus the shapes that a + * malformed file can produce. + */ +static const char *const cv_charsets[] = { + "UTF-8", "UTF-16BE", "UTF-16LE", "UTF-32", "ISO-8859-1", "ISO-8859-15", + "CP1252", "CP437", "CP850", "MACINTOSH", "KOI8-R", "SHIFT_JIS", + "EUC-JP", "GB18030", "BIG5", "ASCII", "ANSI_X3.4-1968", + "", "?", "//TRANSLIT", "UTF-8//IGNORE", "NOSUCHCHARSET", + "AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA" +}; + + +int +LLVMFuzzerTestOneInput (const uint8_t *data, + size_t size) +{ + const char *charset; + unsigned int flags; + size_t len; + char *in; + char *out; + + fuzz_ignore_sigpipe (); + if (size < 2) + return 0; + charset = cv_charsets[data[0] + % (sizeof (cv_charsets) / sizeof (char *))]; + flags = data[1]; + len = size - 2; + /* exact size, no terminator: exactly how the callers pass it */ + in = (char *) malloc ((0 == len) ? 1 : len); + if (NULL == in) + abort (); + if (0 != len) + memcpy (in, data + 2, len); + out = EXTRACTOR_common_convert_to_utf8 (in, len, charset); + if (NULL != out) + { + /* The result is documented as 0-terminated; strlen() proves it and + lands in the redzone if it is not. */ + volatile size_t l = strlen (out); + + (void) l; + if (0 != (flags & 0x01)) + { + char *out2 = EXTRACTOR_common_convert_to_utf8 (out, + strlen (out), + "UTF-8"); + + free (out2); + } + free (out); + } + free (in); + + /* The metatype tables are indexed by values that come from a plugin + (out of process: from an untrusted child), so walk them too. */ + { + enum EXTRACTOR_MetaType max = EXTRACTOR_metatype_get_max (); + int probe = (int) ((size_t) data[0] * 7 + data[1]); + + (void) EXTRACTOR_metatype_to_string ((enum EXTRACTOR_MetaType) probe); + (void) EXTRACTOR_metatype_to_description ((enum EXTRACTOR_MetaType) probe); + (void) EXTRACTOR_metatype_to_string (max); + (void) EXTRACTOR_metatype_to_description (max); + (void) EXTRACTOR_metatype_to_string ((enum EXTRACTOR_MetaType) -1); + (void) EXTRACTOR_metatype_to_description ((enum EXTRACTOR_MetaType) -1); + } + return 0; +} + + +/* ------------------------------------------------------------------ */ +/* Generator */ +/* ------------------------------------------------------------------ */ + +/** + * Byte sequences that are interesting to a charset converter: truncated + * multi-byte sequences, overlong encodings, surrogates and BOMs. + */ +static const char *const cv_atoms[] = { + "\xc3\xa4", "\xc3", "\xe2\x82\xac", "\xe2\x82", "\xe2", + "\xf0\x9f\x98\x80", "\xf0\x9f\x98", "\xf0", + "\xc0\x80", "\xe0\x80\x80", "\xf0\x80\x80\x80", + "\xed\xa0\x80", "\xed\xbf\xbf", "\xef\xbb\xbf", + "\xff\xfe", "\xfe\xff", "\x00\x00\xfe\xff", + "\x80", "\xff", "\xfe", "\x7f", "\x00", + "ABC", "abc", "\r\n", "\t", " " +}; + + +static size_t +fuzz_generate (struct fuzz_rng *rng, + uint8_t *buf, + size_t cap) +{ + size_t len = 0; + unsigned int n; + unsigned int i; + + if (cap < 16) + return 0; + buf[len++] = fuzz_byte (rng); + buf[len++] = fuzz_byte (rng); + n = 1 + fuzz_below (rng, 64); + for (i = 0; i < n; i++) + { + if (fuzz_chance (rng, 6)) + { + if (len < cap) + buf[len++] = fuzz_byte (rng); + } + else + { + const char *a = + cv_atoms[fuzz_below (rng, + (uint32_t) (sizeof (cv_atoms) / sizeof (char *)))]; + /* the atoms include embedded NULs, so use the recorded sizes + rather than strlen() where it matters */ + size_t al = strlen (a); + + if (0 == al) + al = 1; + if (len + al > cap) + break; + memcpy (buf + len, a, al); + len += al; + } + } + return len; +} + + +/* ------------------------------------------------------------------ */ +/* Seed corpus */ +/* ------------------------------------------------------------------ */ + +struct cv_seed +{ + const char *txt; + size_t len; +}; + +#define CSEED(t) { t, sizeof (t) - 1 } + +static const struct cv_seed cv_seeds[] = { + CSEED ("\x00\x00" "hello"), + CSEED ("\x00\x00" "\xc3\xa4\xc3\xb6\xc3\xbc"), + CSEED ("\x00\x00" "\xc3"), + CSEED ("\x00\x00" "\xed\xa0\x80"), + CSEED ("\x01\x00" "\x00h\x00i"), + CSEED ("\x02\x00" "h\x00i\x00"), + CSEED ("\x04\x00" "\xe4\xf6\xfc"), + CSEED ("\x06\x00" "\x80\x99\x9a"), + CSEED ("\x0b\x00" "\x82\xa0\x82\xa2"), + CSEED ("\x11\x00" "abc"), + CSEED ("\x15\x00" "abc"), + CSEED ("\x16\x00" "abc"), + CSEED ("\x00\x00"), + CSEED ("\x00\x01" "\xef\xbb\xbf" "abc") +}; + + +static size_t +fuzz_seed_count (void) +{ + return sizeof (cv_seeds) / sizeof (cv_seeds[0]); +} + + +static const uint8_t * +fuzz_seed_get (size_t idx, + size_t *len) +{ + *len = cv_seeds[idx].len; + return (const uint8_t *) cv_seeds[idx].txt; +} diff --git a/src/fuzz/fuzz_datasource.c b/src/fuzz/fuzz_datasource.c @@ -0,0 +1,427 @@ +/* + This file is part of libextractor. + Copyright (C) 2026 Christian Grothoff + + libextractor 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, or (at your + option) any later version. + + libextractor 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 libextractor; see the file COPYING. If not, write to the + Free Software Foundation, Inc., 51 Franklin Street, Fifth Floor, + Boston, MA 02110-1301, USA. + */ +/** + * @file fuzz/fuzz_datasource.c + * @brief fuzzer for the core datasource and its transparent decompression + * @author Christian Grothoff + * + * `src/main/extractor_datasource.c` is the first code in the library + * that touches attacker-controlled bytes, and it does so *before* any + * plugin is consulted: it sniffs for gzip and bzip2 wrappers and + * transparently decompresses them, maintaining its own seekable view + * over a stream that is not seekable underneath. The gzip header + * walker (FEXTRA / FNAME / FCOMMENT / FHCRC) and the "seek backwards in + * a decompressed stream" path are the interesting parts. + * + * Input format: + * + * byte 0 read chunk size selector + * byte 1 behaviour bits: + * 0x01 create from a file on disk instead of a buffer + * (exercises the mmap/read path in the datasource) + * 0x02 call get_size_() with force=1 up front, which is + * what makes the datasource decompress eagerly + * 0x04 perform the seek script backwards + * 0x08 stop at the first read error instead of continuing + * byte 2 number of operations in the seek/read script + * byte 3 seed selecting the individual operations + * byte 4.. the file image + */ + +#define FUZZ_HARNESS_NAME "fuzz_datasource" + +#include "fuzz_common.h" +#include "platform.h" +#include "extractor.h" +#include "extractor_datasource.h" + +#define DS_FROM_FILE 0x01 +#define DS_FORCE_SIZE 0x02 +#define DS_REVERSE 0x04 +#define DS_STOP_ERR 0x08 + +/** + * Same window sizes as the plugin harness; see fuzz_ec.h. + */ +static const size_t ds_chunks[] = { + 16 * 1024, 1, 2, 3, 7, 16, 64, 255, 256, 1024, 4096, 8192, 16383, + 32768, 65536 +}; + +/** + * Upper bound on the number of script operations, so that a + * pathological input cannot turn one execution into a fuzzing session. + */ +#define MAX_OPS 96 + + +/** + * Metadata callback; the datasource reports the compression type it + * detected through it. + */ +static int +ds_proc (void *cls, + const char *plugin_name, + enum EXTRACTOR_MetaType type, + enum EXTRACTOR_MetaFormat format, + const char *data_mime_type, + const char *data, + size_t data_len) +{ + volatile unsigned int sink = 0; + size_t i; + + (void) cls; + (void) type; + (void) format; + if (NULL == plugin_name) + fuzz_report_finding ("datasource reported a NULL plugin name"); + sink += (unsigned int) strlen (plugin_name); + if (NULL != data_mime_type) + sink += (unsigned int) strlen (data_mime_type); + for (i = 0; (NULL != data) && (i < data_len); i++) + sink += (unsigned char) data[i]; + (void) sink; + return 0; +} + + +int +LLVMFuzzerTestOneInput (const uint8_t *data, + size_t size) +{ + struct EXTRACTOR_Datasource *ds; + struct fuzz_rng rng; + const uint8_t *img; + size_t img_len; + size_t chunk; + unsigned int flags; + unsigned int ops; + unsigned int i; + int64_t fsize; + char tmpl[] = "/tmp/le-fuzz-ds-XXXXXX"; + int fd = -1; + + fuzz_ignore_sigpipe (); + if (size < 4) + return 0; + chunk = ds_chunks[data[0] % (sizeof (ds_chunks) / sizeof (ds_chunks[0]))]; + flags = data[1]; + ops = data[2] % MAX_OPS; + fuzz_rng_seed (&rng, data[3]); + img = data + 4; + img_len = size - 4; + + if (0 != (flags & DS_FROM_FILE)) + { + fd = mkstemp (tmpl); + if (-1 == fd) + return 0; + if ( (0 != img_len) && + (img_len != (size_t) write (fd, img, img_len)) ) + { + (void) close (fd); + (void) unlink (tmpl); + return 0; + } + (void) close (fd); + ds = EXTRACTOR_datasource_create_from_file_ (tmpl, + &ds_proc, + NULL); + } + else + { + ds = EXTRACTOR_datasource_create_from_buffer_ ((const char *) img, + img_len, + &ds_proc, + NULL); + } + if (NULL == ds) + { + if (0 != (flags & DS_FROM_FILE)) + (void) unlink (tmpl); + return 0; + } + fsize = EXTRACTOR_datasource_get_size_ (ds, + (0 != (flags & DS_FORCE_SIZE))); + for (i = 0; i < ops; i++) + { + uint32_t op = fuzz_below (&rng, 10); + + if (op < 5) + { + /* Exact-size destination: the datasource is told how many bytes + it may write, so writing more is an overflow in the caller's + buffer -- which in production is InProcessContext::buf. */ + size_t want = (op < 4) ? chunk : (1 + fuzz_below (&rng, 4096)); + unsigned char *buf = (unsigned char *) malloc ((0 == want) ? 1 : want); + ssize_t got; + + if (NULL == buf) + abort (); + got = EXTRACTOR_datasource_read_ (ds, buf, want); + if (got > (ssize_t) want) + { + free (buf); + fuzz_report_finding ("EXTRACTOR_datasource_read_() reported more " + "bytes than the buffer size it was given"); + } + free (buf); + if ( (0 > got) && + (0 != (flags & DS_STOP_ERR)) ) + break; + } + else if (op < 9) + { + static const int whences[] = { SEEK_SET, SEEK_CUR, SEEK_END }; + int whence = whences[fuzz_below (&rng, 3)]; + int64_t pos; + + switch (fuzz_below (&rng, 6)) + { + case 0: + pos = 0; + break; + case 1: + pos = (int64_t) fuzz_below (&rng, 4096); + break; + case 2: + pos = -(int64_t) fuzz_below (&rng, 4096); + break; + case 3: + pos = (0 > fsize) ? 0 : fsize; + break; + case 4: + pos = INT64_MAX; + break; + default: + pos = INT64_MIN; + break; + } + if ( (0 != (flags & DS_REVERSE)) && + (INT64_MIN != pos) ) + pos = -pos; + (void) EXTRACTOR_datasource_seek_ (ds, pos, whence); + } + else + { + (void) EXTRACTOR_datasource_get_size_ (ds, (int) fuzz_below (&rng, 2)); + } + } + EXTRACTOR_datasource_destroy_ (ds); + if (0 != (flags & DS_FROM_FILE)) + (void) unlink (tmpl); + return 0; +} + + +/* ------------------------------------------------------------------ */ +/* Generator */ +/* ------------------------------------------------------------------ */ + +/** + * Build a gzip member by hand. The point is the *header*: the optional + * FEXTRA / FNAME / FCOMMENT / FHCRC fields are variable length and are + * skipped by a hand-written walker in extractor_datasource.c, which is + * where a length that runs off the end of the buffer bites. + */ +static size_t +gen_gzip (struct fuzz_rng *rng, + uint8_t *buf, + size_t len, + size_t cap) +{ + uint8_t flg = 0; + unsigned int k; + + if (fuzz_chance (rng, 2)) + flg |= 0x04; /* FEXTRA */ + if (fuzz_chance (rng, 2)) + flg |= 0x08; /* FNAME */ + if (fuzz_chance (rng, 2)) + flg |= 0x10; /* FCOMMENT */ + if (fuzz_chance (rng, 3)) + flg |= 0x02; /* FHCRC */ + fuzz_put_mem (buf, &len, cap, "\x1f\x8b\x08", 3); + fuzz_put_le (buf, &len, cap, flg, 1); + fuzz_put_le (buf, &len, cap, 0, 4); /* mtime */ + fuzz_put_le (buf, &len, cap, 0, 1); /* xfl */ + fuzz_put_le (buf, &len, cap, 3, 1); /* os */ + if (0 != (flg & 0x04)) + { + /* the declared extra length is what matters; usually make it lie */ + uint16_t xlen = fuzz_chance (rng, 2) + ? (uint16_t) fuzz_below (rng, 0x10000) + : (uint16_t) fuzz_below (rng, 32); + + fuzz_put_le (buf, &len, cap, xlen, 2); + if (! fuzz_chance (rng, 3)) + for (k = 0; (k < xlen) && (len < cap); k++) + buf[len++] = fuzz_byte (rng); + } + if (0 != (flg & 0x08)) + { + unsigned int n = fuzz_below (rng, 64); + + for (k = 0; (k < n) && (len < cap); k++) + buf[len++] = (uint8_t) (1 + fuzz_below (rng, 254)); + /* half the time forget the terminator */ + if ( (! fuzz_chance (rng, 2)) && (len < cap) ) + buf[len++] = 0; + } + if (0 != (flg & 0x10)) + { + unsigned int n = fuzz_below (rng, 64); + + for (k = 0; (k < n) && (len < cap); k++) + buf[len++] = (uint8_t) (1 + fuzz_below (rng, 254)); + if ( (! fuzz_chance (rng, 2)) && (len < cap) ) + buf[len++] = 0; + } + if (0 != (flg & 0x02)) + fuzz_put_le (buf, &len, cap, fuzz_next (rng), 2); + /* deflate payload: mostly garbage, occasionally a valid stored block */ + if (fuzz_chance (rng, 3)) + { + uint16_t n = (uint16_t) fuzz_below (rng, 64); + + fuzz_put_le (buf, &len, cap, 0x01, 1); /* final, stored */ + fuzz_put_le (buf, &len, cap, n, 2); + fuzz_put_le (buf, &len, cap, (uint16_t) ~n, 2); + for (k = 0; (k < n) && (len < cap); k++) + buf[len++] = fuzz_byte (rng); + } + else + { + unsigned int n = fuzz_below (rng, 256); + + for (k = 0; (k < n) && (len < cap); k++) + buf[len++] = fuzz_byte (rng); + } + fuzz_put_le (buf, &len, cap, fuzz_next (rng), 4); /* crc32 */ + fuzz_put_le (buf, &len, cap, + fuzz_chance (rng, 2) ? fuzz_next (rng) : 0, 4); /* isize */ + return len; +} + + +static size_t +fuzz_generate (struct fuzz_rng *rng, + uint8_t *buf, + size_t cap) +{ + size_t len = 0; + uint32_t kind; + + if (cap < 128) + return 0; + buf[len++] = fuzz_byte (rng); + buf[len++] = (uint8_t) fuzz_below (rng, 16); + buf[len++] = fuzz_byte (rng); + buf[len++] = fuzz_byte (rng); + + kind = fuzz_below (rng, 10); + if (kind < 5) + { + len = gen_gzip (rng, buf, len, cap); + } + else if (kind < 8) + { + /* bzip2: the library rejects almost everything, so the value here is + in the header-sniffing code rather than in libbz2 */ + unsigned int n = fuzz_below (rng, 512); + unsigned int k; + + fuzz_put_mem (buf, &len, cap, "BZh", 3); + fuzz_put_le (buf, &len, cap, + (uint8_t) ('0' + 1 + fuzz_below (rng, 9)), 1); + fuzz_put_mem (buf, &len, cap, "\x31\x41\x59\x26\x53\x59", 6); + for (k = 0; (k < n) && (len < cap); k++) + buf[len++] = fuzz_byte (rng); + } + else + { + /* plain data, sometimes with a truncated compression magic so that + the sniffing code has to decide on very few bytes */ + unsigned int n = fuzz_below (rng, 1024); + unsigned int k; + + if (fuzz_chance (rng, 2)) + fuzz_put_mem (buf, &len, cap, + fuzz_chance (rng, 2) ? "\x1f\x8b" : "BZ", 2); + for (k = 0; (k < n) && (len < cap); k++) + buf[len++] = fuzz_byte (rng); + } + return len; +} + + +/* ------------------------------------------------------------------ */ +/* Seed corpus */ +/* ------------------------------------------------------------------ */ + +#define DS_NSEEDS 10 + +static uint8_t ds_seed_buf[DS_NSEEDS][1024]; +static size_t ds_seed_len[DS_NSEEDS]; +static int ds_seeds_ready; + + +static void +ds_build_seeds (void) +{ + struct fuzz_rng rng; + unsigned int i; + + if (ds_seeds_ready) + return; + ds_seeds_ready = 1; + for (i = 0; i < DS_NSEEDS; i++) + { + fuzz_rng_seed (&rng, 0xD5000u + i); + ds_seed_len[i] = fuzz_generate (&rng, + ds_seed_buf[i], + sizeof (ds_seed_buf[i])); + if (ds_seed_len[i] >= 4) + { + ds_seed_buf[i][0] = (uint8_t) i; + ds_seed_buf[i][1] = (uint8_t) (i % 16u); + ds_seed_buf[i][2] = 32; + } + } +} + + +static size_t +fuzz_seed_count (void) +{ + ds_build_seeds (); + return DS_NSEEDS; +} + + +static const uint8_t * +fuzz_seed_get (size_t idx, + size_t *len) +{ + ds_build_seeds (); + *len = ds_seed_len[idx]; + return ds_seed_buf[idx]; +} diff --git a/src/fuzz/fuzz_ec.h b/src/fuzz/fuzz_ec.h @@ -0,0 +1,574 @@ +/* + This file is part of libextractor. + Copyright (C) 2026 Christian Grothoff + + libextractor 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, or (at your + option) any later version. + + libextractor 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 libextractor; see the file COPYING. If not, write to the + Free Software Foundation, Inc., 51 Franklin Street, Fifth Floor, + Boston, MA 02110-1301, USA. + */ +/** + * @file fuzz/fuzz_ec.h + * @brief contract-exact model of `struct EXTRACTOR_ExtractContext` + * @author Christian Grothoff + * + * This is what makes the plugin harnesses find things that running + * `extract` over a mutated file does not. Two properties matter: + * + * 1. **The read window is exact.** `plugin_env_read()` in + * extractor_plugin_main.c hands the plugin a pointer *into the shared + * memory window* and returns how many bytes are valid there -- which + * is at most `shm_map_size` (16 KiB by default), and at most what is + * left of the file. A plugin that asks for 100 KiB and gets 16 KiB + * but then dereferences all 100 KiB reads memory it was never given. + * In production that memory is a live mmap, so nothing crashes and + * the bug is invisible; here every window is a fresh `malloc()` of + * *exactly* the returned byte count, so AddressSanitizer's redzone + * turns the same access into a hard error. + * + * The window size is fuzzer-controlled, because "read() returned less + * than I asked for" is the single most productive precondition in + * this library and a 16 KiB default hides it for every file smaller + * than that. + * + * 2. **The window slides.** The pointer a plugin got from `read()` stays + * valid only for as long as the shared memory window it points into + * still covers that part of the file. A `read()` or `seek()` that + * needs data outside the window makes the core refill it, and the + * plugin's old pointer then addresses *different file bytes* than the + * plugin believes it holds. The model tracks the same window as + * `plugin_env_read()`/`plugin_env_seek()` do -- so a `seek(0, + * SEEK_CUR)`, which never leaves the window, does not invalidate + * anything, exactly as in production -- and frees every slice handed + * out from a window when that window slides, so ASAN reports a + * retained pointer as a use-after-free. + * + * Set `LE_FUZZ_STRICT_WINDOW=1` to invalidate on *every* call instead. + * That models the in-process implementation, whose single `ctx->buf` + * is overwritten by each read; it finds more, at the price of also + * flagging plugins that are correct under the default out-of-process + * policy. + * + * On top of that the metadata processor is an oracle. The plugin-side + * `transmit_reply()` writes `data_len` bytes from `data` to a pipe and + * calls `strlen()` on the mime type, so this harness touches exactly the + * same bytes -- an over-long `data_len` is a real out-of-bounds read in + * production, not an artefact of the harness. + */ +#ifndef LE_FUZZ_EC_H +#define LE_FUZZ_EC_H 1 + +#include "platform.h" +#include "extractor.h" +#include "fuzz_common.h" + +/** + * Size of the shared memory window the core actually uses; must be kept + * in sync with #DEFAULT_SHM_SIZE in src/main/extractor.c. + */ +#define LE_FUZZ_DEFAULT_SHM (16 * 1024) + +/** + * Upper bound the plugin side enforces on a single meta data item; see + * #MAX_META_DATA in src/main/extractor_ipc.h. + */ +#define LE_FUZZ_MAX_META_DATA (32 * 1024) + +/** + * Number of configuration bytes that precede the file image in every + * plugin-harness input. An all-zero prefix means "no fault injection, + * real 16 KiB window", i.e. exactly what production does, so a corpus + * entry is just four zero bytes followed by the file. + */ +#define LE_FUZZ_EC_PREFIX 4 + +/** + * Candidate read-window sizes. Index 0 is the production value; the + * rest deliberately include sizes that force a short read on almost + * every call, and sizes just below/above a power of two. + */ +static const size_t le_fuzz_windows[] = { + LE_FUZZ_DEFAULT_SHM, 1, 2, 3, 5, 7, 15, 16, 31, 64, 127, 255, 256, + 511, 1024, 4095, 8192, 16383, 16385, 32768, 65536 +}; + +/** + * State behind the #EXTRACTOR_ExtractContext handed to the plugin. + */ +struct fuzz_ec_state +{ + /** + * The file image. Owned by the caller. + */ + const uint8_t *img; + + /** + * Number of bytes in #img. + */ + size_t img_len; + + /** + * Current read position, in [0, #img_len]. + */ + uint64_t pos; + + /** + * Size of the shared memory window, i.e. the largest number of bytes + * a single read() may return. Models `shm_map_size`. + */ + size_t window; + + /** + * File offset the current window starts at. Models `shm_off`. + */ + uint64_t win_off; + + /** + * Number of bytes of the file the current window holds. Models + * `shm_ready_bytes`. + */ + size_t win_len; + + /** + * Non-zero once a window has been established at all. + */ + int win_valid; + + /** + * Slices handed out from the current window. Each is an exact-size + * allocation, so ASAN's redzone starts right after the last byte the + * plugin was promised; all of them are freed when the window slides. + */ + uint8_t **slices; + + /** + * Number of entries in #slices. + */ + size_t slices_len; + + /** + * Capacity of #slices. Grown geometrically: a plugin can issue + * millions of one-byte reads inside a single window, and a + * grow-by-one realloc() would make the *harness* quadratic and turn + * every such input into a bogus "slow unit". + */ + size_t slices_cap; + + /** + * Number of read() calls so far. + */ + unsigned int reads; + + /** + * Number of seek() calls so far. + */ + unsigned int seeks; + + /** + * Number of proc() calls so far. + */ + unsigned int procs; + + /** + * Fault-injection bitmask, see #LE_FUZZ_FAULT_*. + */ + unsigned int faults; + + /** + * Call index at which an injected fault fires. + */ + unsigned int fault_at; + + /** + * Set once get_size() has returned UINT64_MAX; the failure is + * transient in production (one failed IPC round), so it only fires + * once here too. + */ + int size_failed; +}; + +#define LE_FUZZ_FAULT_SIZE 0x01 +#define LE_FUZZ_FAULT_READ 0x02 +#define LE_FUZZ_FAULT_SEEK 0x04 +#define LE_FUZZ_FAULT_STOP 0x08 +#define LE_FUZZ_RUN_TWICE 0x10 +#define LE_FUZZ_ODD_ALIGN 0x20 + +/** + * Cached value of $LE_FUZZ_STRICT_WINDOW. + */ +static int le_fuzz_strict_window = -1; + +/** + * Cached value of $LE_FUZZ_STRICT_PROC. + */ +static int le_fuzz_strict_proc = -1; + + +/** + * Free every slice handed out from the window that is about to be + * replaced. In production those pointers keep addressing live shared + * memory, but the bytes behind them are no longer the ones the plugin + * read, so any use of them is a defect; freeing turns that into an ASAN + * report. + * + * @param st harness state + */ +static void +le_fuzz_drop_slices (struct fuzz_ec_state *st) +{ + size_t i; + + for (i = 0; i < st->slices_len; i++) + free (st->slices[i]); + free (st->slices); + st->slices = NULL; + st->slices_len = 0; + st->slices_cap = 0; +} + + +/** + * Slide the window so that it starts at @a off. + * + * @param st harness state + * @param off new window start + */ +static void +le_fuzz_slide (struct fuzz_ec_state *st, + uint64_t off) +{ + le_fuzz_drop_slices (st); + st->win_off = off; + st->win_len = (size_t) (st->img_len - off); + if (st->win_len > st->window) + st->win_len = st->window; + st->win_valid = 1; +} + + +/** + * @return non-zero if @a off is covered by the current window + */ +static int +le_fuzz_in_window (const struct fuzz_ec_state *st, + uint64_t off) +{ + if (! st->win_valid) + return 0; + if (le_fuzz_strict_window) + return 0; + return ( (st->win_off <= off) && + (off < st->win_off + st->win_len) ); +} + + +/** + * Hand out an exact-size copy of @a n bytes starting at file offset + * @a off. The allocation is remembered so that it can be freed when the + * window slides. + * + * @param st harness state + * @param off file offset + * @param n number of bytes + * @return pointer the plugin may read @a n bytes from + */ +static uint8_t * +le_fuzz_slice (struct fuzz_ec_state *st, + uint64_t off, + size_t n) +{ + uint8_t *p = (uint8_t *) malloc ((0 == n) ? 1 : n); + + if (NULL == p) + abort (); + if (0 != n) + memcpy (p, st->img + off, n); + if (st->slices_len == st->slices_cap) + { + size_t cap = (0 == st->slices_cap) ? 64 : st->slices_cap * 2; + uint8_t **v = (uint8_t **) realloc (st->slices, + sizeof (uint8_t *) * cap); + + if (NULL == v) + abort (); + st->slices = v; + st->slices_cap = cap; + } + st->slices[st->slices_len++] = p; + return p; +} + + +/** + * Model of `plugin_env_read()` from src/main/extractor_plugin_main.c. + * + * @param cls a `struct fuzz_ec_state` + * @param[out] data set to the start of the window + * @param count number of bytes requested + * @return number of bytes available at @a data, -1 on error + */ +static ssize_t +le_fuzz_read (void *cls, + void **data, + size_t count) +{ + struct fuzz_ec_state *st = cls; + + *data = NULL; + st->reads++; + if ( (0 != (st->faults & LE_FUZZ_FAULT_READ)) && + (st->reads == st->fault_at) ) + return -1; /* models a failed IPC round */ + /* clamp to what is left of the file, exactly as plugin_env_read() + does (including its overflow guard) */ + if ( (count + st->pos > st->img_len) || + (count + st->pos < st->pos) ) + count = (size_t) (st->img_len - st->pos); + /* refill the window if the read starts outside it, as + plugin_env_read() does via its plugin_env_seek() call */ + if (! le_fuzz_in_window (st, st->pos)) + le_fuzz_slide (st, st->pos); + /* clamp to what the window holds */ + if (st->pos + count > st->win_off + st->win_len) + count = (size_t) (st->win_off + st->win_len - st->pos); + *data = le_fuzz_slice (st, st->pos, count); + st->pos += count; + return (ssize_t) count; +} + + +/** + * Model of `plugin_env_seek()` from src/main/extractor_plugin_main.c. + * + * @param cls a `struct fuzz_ec_state` + * @param pos offset to seek to + * @param whence SEEK_SET, SEEK_CUR or SEEK_END + * @return new absolute position, -1 on error + */ +static int64_t +le_fuzz_seek (void *cls, + int64_t pos, + int whence) +{ + struct fuzz_ec_state *st = cls; + uint64_t npos; + + st->seeks++; + if ( (0 != (st->faults & LE_FUZZ_FAULT_SEEK)) && + (st->seeks == st->fault_at) ) + return -1; + switch (whence) + { + case SEEK_CUR: + if ( (pos < 0) && + (st->pos < (uint64_t) (-pos)) ) + return -1; + pos = (int64_t) (st->pos + pos); + break; + case SEEK_END: + if (pos > 0) + return -1; + pos = (int64_t) (st->img_len + pos); + break; + case SEEK_SET: + break; + default: + return -1; + } + if ( (pos < 0) || + ((uint64_t) pos > st->img_len) ) + return -1; + npos = (uint64_t) pos; + /* plugin_env_seek() returns without touching the window whenever the + target is already inside it; otherwise the core refills it and every + pointer into the old window goes stale */ + if (! le_fuzz_in_window (st, npos)) + le_fuzz_slide (st, npos); + st->pos = npos; + return (int64_t) npos; +} + + +/** + * Model of `plugin_env_get_size()`. + * + * @param cls a `struct fuzz_ec_state` + * @return file size, UINT64_MAX when the injected IPC failure fires + */ +static uint64_t +le_fuzz_get_size (void *cls) +{ + struct fuzz_ec_state *st = cls; + + if ( (0 != (st->faults & LE_FUZZ_FAULT_SIZE)) && + (! st->size_failed) ) + { + st->size_failed = 1; + return UINT64_MAX; /* documented "IPC failure" return */ + } + return (uint64_t) st->img_len; +} + + +/** + * Metadata processor used as an oracle. Touches precisely the bytes + * that `transmit_reply()` in extractor_plugin_main.c touches. + * + * @param cls a `struct fuzz_ec_state` + * @param plugin_name name of the reporting plugin + * @param type metadata type + * @param format metadata format + * @param data_mime_type mime type of @a data, may be NULL + * @param data the metadata + * @param data_len number of bytes in @a data + * @return 0 to continue, 1 to abort + */ +static int +le_fuzz_proc (void *cls, + const char *plugin_name, + enum EXTRACTOR_MetaType type, + enum EXTRACTOR_MetaFormat format, + const char *data_mime_type, + const char *data, + size_t data_len) +{ + struct fuzz_ec_state *st = cls; + volatile unsigned int sink = 0; + size_t i; + + st->procs++; + if (NULL == plugin_name) + fuzz_report_finding ("plugin reported a NULL plugin name"); + /* transmit_reply() does strlen() on both of these */ + sink += (unsigned int) strlen (plugin_name); + if (NULL != data_mime_type) + sink += (unsigned int) strlen (data_mime_type); + if ( (NULL == data) && + (0 != data_len) ) + fuzz_report_finding ("plugin reported NULL data with a non-zero length"); + if (NULL != data) + { + /* transmit_reply() writes exactly data_len bytes starting at data; + reading them here is what production does */ + for (i = 0; i < data_len; i++) + sink += (unsigned char) data[i]; + if ( (le_fuzz_strict_proc) && + (0 != data_len) && + ( (EXTRACTOR_METAFORMAT_UTF8 == format) || + (EXTRACTOR_METAFORMAT_C_STRING == format) ) && + ('\0' != data[data_len - 1]) ) + fuzz_report_finding ("plugin reported a string metadata value that is " + "not 0-terminated"); + } + (void) sink; + (void) type; + if ( (0 != (st->faults & LE_FUZZ_FAULT_STOP)) && + (st->procs > st->fault_at) ) + return 1; /* application asked us to stop */ + return 0; +} + + +/** + * Read the harness-wide environment knobs once. + */ +static void +le_fuzz_ec_init_env (void) +{ + if (-1 != le_fuzz_strict_window) + return; + le_fuzz_strict_window = (0 != fuzz_env_ulong ("LE_FUZZ_STRICT_WINDOW", 0)); + le_fuzz_strict_proc = (0 != fuzz_env_ulong ("LE_FUZZ_STRICT_PROC", 0)); +} + + +/** + * Set up @a ec and @a st from a fuzz input. The first + * #LE_FUZZ_EC_PREFIX bytes are configuration, the rest is the file + * image. + * + * @param[out] ec context to fill in + * @param[out] st state to fill in + * @param data the fuzz input + * @param size number of bytes in @a data + * @return 0 if the input was too short to be usable + */ +static int +le_fuzz_ec_setup (struct EXTRACTOR_ExtractContext *ec, + struct fuzz_ec_state *st, + const uint8_t *data, + size_t size) +{ + size_t off; + + le_fuzz_ec_init_env (); + if (size < LE_FUZZ_EC_PREFIX) + return 0; + memset (st, 0, sizeof (*st)); + st->window = le_fuzz_windows[data[0] + % (sizeof (le_fuzz_windows) + / sizeof (le_fuzz_windows[0]))]; + st->faults = data[1]; + st->fault_at = data[2]; + off = LE_FUZZ_EC_PREFIX; + if ( (0 != (st->faults & LE_FUZZ_ODD_ALIGN)) && + (off < size) ) + off++; + st->img = data + off; + st->img_len = size - off; + memset (ec, 0, sizeof (*ec)); + ec->cls = st; + ec->config = NULL; + ec->read = &le_fuzz_read; + ec->seek = &le_fuzz_seek; + ec->get_size = &le_fuzz_get_size; + ec->proc = &le_fuzz_proc; + return 1; +} + + +/** + * Release everything @a st still owns. + * + * @param st state to clean up + */ +static void +le_fuzz_ec_cleanup (struct fuzz_ec_state *st) +{ + le_fuzz_drop_slices (st); + st->win_valid = 0; + st->win_off = 0; + st->win_len = 0; +} + + +/** + * Rewind @a st so that the same context can be handed to the extract + * method a second time. Plugins must be stateless across files. + * + * @param st state to reset + */ +static void +le_fuzz_ec_rewind (struct fuzz_ec_state *st) +{ + le_fuzz_ec_cleanup (st); + st->pos = 0; + st->reads = 0; + st->seeks = 0; + st->procs = 0; + st->size_failed = 0; +} + + +#endif /* LE_FUZZ_EC_H */ diff --git a/src/fuzz/fuzz_extract.c b/src/fuzz/fuzz_extract.c @@ -0,0 +1,377 @@ +/* + This file is part of libextractor. + Copyright (C) 2026 Christian Grothoff + + libextractor 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, or (at your + option) any later version. + + libextractor 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 libextractor; see the file COPYING. If not, write to the + Free Software Foundation, Inc., 51 Franklin Street, Fifth Floor, + Boston, MA 02110-1301, USA. + */ +/** + * @file fuzz/fuzz_extract.c + * @brief end-to-end fuzzer for the public EXTRACTOR_extract() entry point + * @author Christian Grothoff + * + * Where the per-plugin harnesses each drive one parser through a + * fabricated context, this one drives the *whole* library the way an + * application does: `EXTRACTOR_plugin_add_defaults()` followed by + * `EXTRACTOR_extract()`. That covers everything the per-plugin targets + * cannot reach -- + * + * - the datasource and its transparent gzip/bzip2 decompression, + * - plugin discovery, loading and the per-plugin option strings, + * - the dispatch loop in extractor.c, including the "plugin asked to + * stop" and "application asked to stop" paths, + * - the plugins that wrap a third-party library (libjpeg, libtiff, + * libgif, exiv2, ...), which the static per-plugin targets do not + * link, + * + * and it does so with the real plugin *set*, so one input is handed to + * every plugin that claims it. + * + * Plugins run in-process (#EXTRACTOR_OPTION_IN_PROCESS): a fuzzer that + * forks a child per plugin per input is not a fuzzer, and a crash in a + * child would be invisible to the engine. The out-of-process machinery + * itself is covered by fuzz_ipc. + * + * The plugin list is built once and reused: loading ~30 shared objects + * costs milliseconds and would otherwise dominate every execution. + * + * Input format: + * + * byte 0 behaviour bits: + * 0x01 extract from a file on disk rather than from memory + * 0x02 stop after the first metadata item + * 0x04 run the same input twice through the same plugin + * list (plugins must not carry state between files) + * byte 1 number of metadata items to accept before asking to stop + * byte 2.. the file image + */ + +#define FUZZ_HARNESS_NAME "fuzz_extract" + +#include "fuzz_common.h" +#include "platform.h" +#include "extractor.h" + +#define EX_FROM_FILE 0x01 +#define EX_STOP_FIRST 0x02 +#define EX_RUN_TWICE 0x04 + +/** + * The plugin list, built on first use. + */ +static struct EXTRACTOR_PluginList *plugins; + +/** + * Set once the (possibly failed) attempt to build #plugins was made. + */ +static int plugins_tried; + +/** + * Number of metadata items to accept before returning 1 from the + * processor. + */ +static unsigned int stop_after; + +/** + * Number of metadata items seen for the current input. + */ +static unsigned int seen; + + +/** + * Metadata callback. Touches exactly the bytes that the library + * promises are there, so that an over-long length or an unterminated + * mime type is an ASAN report rather than a silent pass. + */ +static int +ex_proc (void *cls, + const char *plugin_name, + enum EXTRACTOR_MetaType type, + enum EXTRACTOR_MetaFormat format, + const char *data_mime_type, + const char *data, + size_t data_len) +{ + volatile unsigned int sink = 0; + size_t i; + + (void) cls; + (void) type; + (void) format; + if (NULL == plugin_name) + fuzz_report_finding ("EXTRACTOR_extract() reported a NULL plugin name"); + sink += (unsigned int) strlen (plugin_name); + if (NULL != data_mime_type) + sink += (unsigned int) strlen (data_mime_type); + if ( (NULL == data) && + (0 != data_len) ) + fuzz_report_finding ("EXTRACTOR_extract() reported NULL data with a " + "non-zero length"); + for (i = 0; (NULL != data) && (i < data_len); i++) + sink += (unsigned char) data[i]; + (void) sink; + seen++; + return (seen > stop_after) ? 1 : 0; +} + + +/** + * Build the plugin list once. + * + * @return the list, NULL if no plugin could be found + */ +static struct EXTRACTOR_PluginList * +get_plugins (void) +{ + if (plugins_tried) + return plugins; + plugins_tried = 1; + plugins = EXTRACTOR_plugin_add_defaults (EXTRACTOR_OPTION_IN_PROCESS); + if (NULL == plugins) + fprintf (stderr, + "%s: no plugins found; set LIBEXTRACTOR_PREFIX to the " + "directory holding the built plugins (typically " + "src/plugins/.libs)\n", + FUZZ_HARNESS_NAME); + return plugins; +} + + +int +LLVMFuzzerTestOneInput (const uint8_t *data, + size_t size) +{ + struct EXTRACTOR_PluginList *pl; + unsigned int flags; + const char *img; + size_t img_len; + char tmpl[] = "/tmp/le-fuzz-ex-XXXXXX"; + int fd = -1; + + fuzz_ignore_sigpipe (); + if (size < 2) + return 0; + pl = get_plugins (); + if (NULL == pl) + return 0; + flags = data[0]; + stop_after = (0 != (flags & EX_STOP_FIRST)) ? 0 : data[1]; + img = (const char *) (data + 2); + img_len = size - 2; + + if (0 != (flags & EX_FROM_FILE)) + { + fd = mkstemp (tmpl); + if (-1 == fd) + return 0; + if ( (0 != img_len) && + (img_len != (size_t) write (fd, img, img_len)) ) + { + (void) close (fd); + (void) unlink (tmpl); + return 0; + } + (void) close (fd); + seen = 0; + EXTRACTOR_extract (pl, + tmpl, + NULL, 0, + &ex_proc, NULL); + if (0 != (flags & EX_RUN_TWICE)) + { + seen = 0; + EXTRACTOR_extract (pl, + tmpl, + NULL, 0, + &ex_proc, NULL); + } + (void) unlink (tmpl); + } + else + { + seen = 0; + EXTRACTOR_extract (pl, + NULL, + img, img_len, + &ex_proc, NULL); + if (0 != (flags & EX_RUN_TWICE)) + { + seen = 0; + EXTRACTOR_extract (pl, + NULL, + img, img_len, + &ex_proc, NULL); + } + } + return 0; +} + + +/* ------------------------------------------------------------------ */ +/* Generator */ +/* ------------------------------------------------------------------ */ + +/** + * Magic numbers of every format the shipped plugins claim. The + * generator picks one and appends noise: without a recognised magic the + * input is rejected by every plugin in microseconds and nothing is + * learned. + */ +static const struct +{ + const char *magic; + size_t len; +} ex_magics[] = { +#define M(s) { s, sizeof (s) - 1 } + M ("\x89PNG\r\n\x1a\n"), + M ("GIF89a"), + M ("GIF87a"), + M ("\xff\xd8\xff\xe0\x00\x10JFIF\x00"), + M ("\xff\xd8\xff\xe1"), + M ("II\x2a\x00\x08\x00\x00\x00"), + M ("MM\x00\x2a\x00\x00\x00\x08"), + M ("RIFF....WAVE"), + M ("RIFF....AVI "), + M ("\x7f" "ELF\x02\x01\x01\x00"), + M ("%!PS-Adobe-3.0\n"), + M ("{\\rtf1\\ansi"), + M ("NESM\x1a\x01"), + M ("NSFE"), + M ("PSID\x00\x02"), + M ("RSID\x00\x02"), + M ("SCRM"), + M ("IMPM"), + M ("Extended Module: "), + M ("MThd\x00\x00\x00\x06"), + M ("fLaC"), + M ("OggS\x00\x02"), + M ("PK\x03\x04"), + M ("\xd0\xcf\x11\xe0\xa1\xb1\x1a\xe1"), + M ("\x1f\x8b\x08\x00\x00\x00\x00\x00\x00\x03"), + M ("BZh9\x31\x41\x59\x26\x53\x59"), + M ("!<arch>\ndebian-binary "), + M ("\xed\xab\xee\xdb"), + M ("\x00\x05\x16\x00\x00\x02\x00\x00"), + M ("\xf7\x02"), + M ("\x00\x00\x00\x08moov"), + M (".RMF\x00\x00\x00\x12"), + M (".TH FUZZ 1\n.SH NAME\n"), + M ("<!DOCTYPE html><html><head><title>x</title>"), + M ("\x00\x00\x01\xba") +#undef M +}; + + +static size_t +fuzz_generate (struct fuzz_rng *rng, + uint8_t *buf, + size_t cap) +{ + size_t len = 0; + unsigned int idx; + unsigned int n; + unsigned int i; + + if (cap < 64) + return 0; + buf[len++] = fuzz_chance (rng, 3) + ? (uint8_t) fuzz_below (rng, 8) + : 0; + buf[len++] = fuzz_byte (rng); + idx = fuzz_below (rng, + (uint32_t) (sizeof (ex_magics) / sizeof (ex_magics[0]))); + fuzz_put_mem (buf, &len, cap, ex_magics[idx].magic, ex_magics[idx].len); + n = fuzz_below (rng, 2048); + for (i = 0; (i < n) && (len < cap); i++) + { + if (fuzz_chance (rng, 24)) + { + unsigned int k; + unsigned int r = 1 + fuzz_below (rng, 64); + uint8_t v = fuzz_chance (rng, 2) ? 0xFF : fuzz_byte (rng); + + for (k = 0; (k < r) && (len < cap); k++) + buf[len++] = v; + } + else if (fuzz_chance (rng, 16)) + { + unsigned int j = + fuzz_below (rng, (uint32_t) (sizeof (ex_magics) + / sizeof (ex_magics[0]))); + + fuzz_put_mem (buf, &len, cap, ex_magics[j].magic, ex_magics[j].len); + } + else + { + buf[len++] = fuzz_byte (rng); + } + } + return len; +} + + +/* ------------------------------------------------------------------ */ +/* Seed corpus */ +/* ------------------------------------------------------------------ */ + +/** + * One seed per magic number: the bare header with the default + * configuration. The substantial seeds come from + * src/plugins/testdata/ via contrib/oss-fuzz/make_seed_corpus.sh. + */ +#define EX_NSEEDS (sizeof (ex_magics) / sizeof (ex_magics[0])) + +static uint8_t ex_seed_buf[EX_NSEEDS][64]; +static size_t ex_seed_len[EX_NSEEDS]; +static int ex_seeds_ready; + + +static void +ex_build_seeds (void) +{ + size_t i; + + if (ex_seeds_ready) + return; + ex_seeds_ready = 1; + for (i = 0; i < EX_NSEEDS; i++) + { + size_t len = 0; + + ex_seed_buf[i][len++] = 0; + ex_seed_buf[i][len++] = 0xFF; + fuzz_put_mem (ex_seed_buf[i], &len, sizeof (ex_seed_buf[i]), + ex_magics[i].magic, ex_magics[i].len); + ex_seed_len[i] = len; + } +} + + +static size_t +fuzz_seed_count (void) +{ + ex_build_seeds (); + return EX_NSEEDS; +} + + +static const uint8_t * +fuzz_seed_get (size_t idx, + size_t *len) +{ + ex_build_seeds (); + *len = ex_seed_len[idx]; + return ex_seed_buf[idx]; +} diff --git a/src/fuzz/fuzz_ipc.c b/src/fuzz/fuzz_ipc.c @@ -0,0 +1,246 @@ +/* + This file is part of libextractor. + Copyright (C) 2026 Christian Grothoff + + libextractor 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, or (at your + option) any later version. + + libextractor 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 libextractor; see the file COPYING. If not, write to the + Free Software Foundation, Inc., 51 Franklin Street, Fifth Floor, + Boston, MA 02110-1301, USA. + */ +/** + * @file fuzz/fuzz_ipc.c + * @brief fuzzer for the core's parser of plugin replies + * @author Christian Grothoff + * + * The whole point of libextractor's out-of-process design is that a + * plugin -- and the third-party parser it wraps -- is *untrusted*: it + * runs in a child process precisely so that a memory-safety bug in it + * cannot reach the host application. That makes + * `EXTRACTOR_IPC_process_reply_()` the security boundary of the design: + * it is the code in the trusted parent that parses a byte stream + * produced by the untrusted child. A bug here defeats the entire + * sandbox, so it is worth fuzzing on its own even though the function + * is short. + * + * The harness feeds the message stream directly. The buffer handed in + * is `malloc()`ed at exactly the input length, so any read past the + * declared `size` -- the classic way a length-prefixed parser goes + * wrong -- is an ASAN report rather than a silent read of adjacent + * stack. + * + * Input format: raw message stream (no configuration prefix). + */ + +#define FUZZ_HARNESS_NAME "fuzz_ipc" + +#include "fuzz_common.h" +#include "platform.h" +#include "extractor.h" +#include "extractor_plugins.h" +#include "extractor_ipc.h" + + +/** + * Message processor; touches exactly what the parser promises. + */ +static void +ipc_proc (void *cls, + struct EXTRACTOR_PluginList *plugin, + enum EXTRACTOR_MetaType meta_type, + enum EXTRACTOR_MetaFormat meta_format, + const char *mime, + const void *value, + size_t value_len) +{ + volatile unsigned int sink = 0; + const unsigned char *v = value; + size_t i; + + (void) cls; + (void) plugin; + (void) meta_format; + if (meta_type >= EXTRACTOR_metatype_get_max ()) + fuzz_report_finding ("EXTRACTOR_IPC_process_reply_() passed on a meta " + "type outside [0, EXTRACTOR_metatype_get_max())"); + if (NULL != mime) + sink += (unsigned int) strlen (mime); + if ( (NULL == value) && + (0 != value_len) ) + fuzz_report_finding ("EXTRACTOR_IPC_process_reply_() passed on NULL " + "data with a non-zero length"); + for (i = 0; (NULL != v) && (i < value_len); i++) + sink += v[i]; + (void) sink; +} + + +int +LLVMFuzzerTestOneInput (const uint8_t *data, + size_t size) +{ + struct EXTRACTOR_PluginList plugin; + uint8_t *buf; + ssize_t ret; + + fuzz_ignore_sigpipe (); + if (0 == size) + return 0; + memset (&plugin, 0, sizeof (plugin)); + plugin.short_libname = (char *) "fuzz"; + plugin.libname = (char *) "libextractor_fuzz.so"; + /* exact-size copy: the redzone starts right after the last byte the + parser was told about */ + buf = (uint8_t *) malloc (size); + if (NULL == buf) + abort (); + memcpy (buf, data, size); + ret = EXTRACTOR_IPC_process_reply_ (&plugin, + buf, + size, + &ipc_proc, + NULL); + if (ret > (ssize_t) size) + fuzz_report_finding ("EXTRACTOR_IPC_process_reply_() consumed more " + "bytes than it was given"); + free (buf); + return 0; +} + + +/* ------------------------------------------------------------------ */ +/* Generator */ +/* ------------------------------------------------------------------ */ + +static size_t +fuzz_generate (struct fuzz_rng *rng, + uint8_t *buf, + size_t cap) +{ + size_t len = 0; + unsigned int n = 1 + fuzz_below (rng, 12); + unsigned int i; + + if (cap < 128) + return 0; + for (i = 0; i < n; i++) + { + uint32_t kind = fuzz_below (rng, 10); + + if (kind < 2) + { + fuzz_put_le (buf, &len, cap, MESSAGE_DONE, 1); + } + else if (kind < 4) + { + /* struct SeekRequestMessage */ + fuzz_put_le (buf, &len, cap, MESSAGE_SEEK, 1); + fuzz_put_le (buf, &len, cap, fuzz_byte (rng), 1); /* reserved */ + fuzz_put_le (buf, &len, cap, fuzz_next (rng), 2); /* whence */ + fuzz_put_le (buf, &len, cap, fuzz_next (rng), 4); /* req bytes */ + fuzz_put_le (buf, &len, cap, fuzz_next (rng), 8); /* offset */ + } + else if (kind < 9) + { + /* struct MetaMessage, with the length fields frequently lying + about how many bytes really follow */ + uint16_t mime_len; + uint32_t val_len; + uint16_t decl_mime; + uint32_t decl_val; + unsigned int k; + + mime_len = (uint16_t) fuzz_below (rng, 24); + val_len = fuzz_below (rng, 96); + decl_mime = fuzz_chance (rng, 3) + ? (uint16_t) fuzz_below (rng, 0x10000) + : mime_len; + decl_val = fuzz_chance (rng, 3) + ? (uint32_t) fuzz_next (rng) + : val_len; + fuzz_put_le (buf, &len, cap, MESSAGE_META, 1); + fuzz_put_le (buf, &len, cap, 0, 1); + fuzz_put_le (buf, &len, cap, fuzz_below (rng, 6), 2); /* format */ + fuzz_put_le (buf, &len, cap, + fuzz_chance (rng, 3) + ? fuzz_next (rng) + : fuzz_below (rng, 200), 2); /* type */ + fuzz_put_le (buf, &len, cap, decl_mime, 2); + fuzz_put_le (buf, &len, cap, decl_val, 4); + for (k = 0; (k < mime_len) && (len < cap); k++) + buf[len++] = (0 == k + 1 - mime_len) && (! fuzz_chance (rng, 3)) + ? 0 + : (uint8_t) (1 + fuzz_below (rng, 254)); + for (k = 0; (k < val_len) && (len < cap); k++) + buf[len++] = fuzz_byte (rng); + } + else + { + fuzz_put_le (buf, &len, cap, fuzz_byte (rng), 1); + } + } + return len; +} + + +/* ------------------------------------------------------------------ */ +/* Seed corpus */ +/* ------------------------------------------------------------------ */ + +struct ipc_seed +{ + const char *txt; + size_t len; +}; + +#define ISEED(t) { t, sizeof (t) - 1 } + +static const struct ipc_seed ipc_seeds[] = { + /* a bare DONE */ + ISEED ("\x03"), + /* SEEK, whence 0, 16 KiB requested, offset 0 */ + ISEED ("\x04\x00\x00\x00\x00\x40\x00\x00" + "\x00\x00\x00\x00\x00\x00\x00\x00"), + /* META with a 0-terminated mime type and a 4 byte value */ + ISEED ("\x05\x00\x01\x00\x01\x00\x0a\x00\x04\x00\x00\x00" + "text/plain" "abcd"), + /* META with a mime type that is NOT 0-terminated: must be rejected */ + ISEED ("\x05\x00\x01\x00\x01\x00\x04\x00\x00\x00\x00\x00" "abcd"), + /* META declaring more value bytes than are present */ + ISEED ("\x05\x00\x01\x00\x01\x00\x00\x00\xff\xff\x00\x00" "ab"), + /* META declaring a value larger than MAX_META_DATA */ + ISEED ("\x05\x00\x01\x00\x01\x00\x00\x00\x00\x00\x01\x00"), + /* META with an out-of-range meta type */ + ISEED ("\x05\x00\x01\x00\xff\xff\x00\x00\x01\x00\x00\x00" "x"), + /* truncated META header */ + ISEED ("\x05\x00\x01\x00"), + /* unknown opcode */ + ISEED ("\x7f"), + /* DONE followed by a truncated SEEK */ + ISEED ("\x03\x04\x00\x00") +}; + + +static size_t +fuzz_seed_count (void) +{ + return sizeof (ipc_seeds) / sizeof (ipc_seeds[0]); +} + + +static const uint8_t * +fuzz_seed_get (size_t idx, + size_t *len) +{ + *len = ipc_seeds[idx].len; + return (const uint8_t *) ipc_seeds[idx].txt; +} diff --git a/src/fuzz/fuzz_plugin.c b/src/fuzz/fuzz_plugin.c @@ -0,0 +1,462 @@ +/* + This file is part of libextractor. + Copyright (C) 2026 Christian Grothoff + + libextractor 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, or (at your + option) any later version. + + libextractor 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 libextractor; see the file COPYING. If not, write to the + Free Software Foundation, Inc., 51 Franklin Street, Fifth Floor, + Boston, MA 02110-1301, USA. + */ +/** + * @file fuzz/fuzz_plugin.c + * @brief generic in-process fuzzer for a single libextractor plugin + * @author Christian Grothoff + * + * This one source is compiled once per plugin. `-DLE_FUZZ_PLUGIN=gif` + * produces the `fuzz_gif` target, which calls + * `EXTRACTOR_gif_extract_method()` directly -- the plugin's own object + * files are linked in, so every line of the parser is instrumented, and + * no `dlopen()`, no `fork()` and no IPC is involved. + * + * The extraction context is the contract-exact model in fuzz_ec.h; read + * its header comment first, it is where the interesting properties are. + * + * Input format: + * + * byte 0 read window size selector (0 = the production 16 KiB) + * byte 1 fault-injection bitmask, see LE_FUZZ_FAULT_* in fuzz_ec.h + * byte 2 call index at which the injected fault fires + * byte 3 reserved, must be present + * byte 4.. the file image handed to the plugin + * + * An all-zero prefix is exactly what production does, so a corpus entry + * is just four zero bytes followed by a real file of the format; that is + * what contrib/oss-fuzz/make_seed_corpus.sh generates from + * src/plugins/testdata/. + */ + +#include "fuzz_plugin_name.h" + +#define FUZZ_HARNESS_NAME "fuzz_" LE_FUZZ_PLUGIN_STR + +#include "fuzz_common.h" +#include "fuzz_ec.h" + +/** + * The plugin's extract method, resolved at link time rather than through + * `lt_dlsym()`. + */ +extern void +LE_FUZZ_EXTRACT_METHOD (struct EXTRACTOR_ExtractContext *ec); + +/** + * The plugin's optional configuration hook. Declared weak: most plugins + * do not have one. + */ +extern const char * +LE_FUZZ_OPTIONS_METHOD (void) __attribute__ ((weak)); + + +/* ------------------------------------------------------------------ */ +/* The fuzz target */ +/* ------------------------------------------------------------------ */ + +int +LLVMFuzzerTestOneInput (const uint8_t *data, + size_t size) +{ + struct EXTRACTOR_ExtractContext ec; + struct fuzz_ec_state st; + + fuzz_ignore_sigpipe (); + if (! le_fuzz_ec_setup (&ec, &st, data, size)) + return 0; + if (NULL != LE_FUZZ_OPTIONS_METHOD) + ec.config = LE_FUZZ_OPTIONS_METHOD (); + LE_FUZZ_EXTRACT_METHOD (&ec); + if (0 != (st.faults & LE_FUZZ_RUN_TWICE)) + { + /* A plugin must not carry state from one file to the next: the same + process is reused for every file in a directory walk. */ + le_fuzz_ec_rewind (&st); + LE_FUZZ_EXTRACT_METHOD (&ec); + } + le_fuzz_ec_cleanup (&st); + return 0; +} + + +/* ------------------------------------------------------------------ */ +/* Structure-aware generator */ +/* ------------------------------------------------------------------ */ + +/* The body shapes -- LE_SHAPE_RAW (magic plus random bytes), + LE_SHAPE_CHUNK_BE (tag + big-endian length + payload, as in PNG and + QuickTime), LE_SHAPE_CHUNK_LE (RIFF/WAV/AVI), LE_SHAPE_TEXT + (line-oriented) and LE_SHAPE_ZIP -- are #defined in + fuzz_plugin_name.h, because that header has to select one before this + translation unit could declare an enum. */ + +/** + * Magic bytes and body shape of the format this target parses. Chosen + * per plugin at compile time by fuzz_plugin_name.h. + */ +static const char le_fuzz_magic[] = LE_FUZZ_MAGIC; + +/** + * Number of bytes in #le_fuzz_magic (the trailing NUL is not part of it + * unless the format really has one). + */ +#define LE_FUZZ_MAGIC_LEN (sizeof (le_fuzz_magic) - 1) + +/** + * Four-character tags that appear in the chunked formats. + */ +static const char *const le_fuzz_tags[] = { + "IHDR", "tEXt", "zTXt", "iTXt", "pHYs", "tIME", "IEND", "PLTE", + "LIST", "INFO", "fmt ", "data", "INAM", "IART", "ICMT", "ICRD", + "moov", "mvhd", "trak", "udta", "meta", "ilst", "\xa9nam", "cmov", + "avih", "strh", "strf", "movi", "JUNK", "AAAA", "\x00\x00\x00\x00" +}; + +/** + * Text fragments that appear in the line-oriented formats. + */ +static const char *const le_fuzz_lines[] = { + "%%Title: fuzz\n", "%%Creator: fuzz\n", "%%Pages: 1\n", + "%%BoundingBox: 0 0 1 1\n", "%%EOF\n", "%!PS-Adobe-3.0\n", + ".TH FUZZ 1 \"2026\" \"le\" \"fuzz\"\n", ".SH NAME\n", ".SH SYNOPSIS\n", + "{\\rtf1\\ansi\\deff0", "{\\info{\\title fuzz}}", "{\\*\\generator x}", + "\\u1234?", "\\'ff", "\\par ", "}", "{", "\\\\", + "<html><head><title>fuzz</title>", "<meta name=\"author\" content=\"x\">", + "\n", "\r\n", "\t", " " +}; + + +/** + * Append a chunk-structured body to @a buf. + * + * @param rng PRNG state + * @param buf output buffer + * @param[in,out] len current length + * @param cap capacity + * @param big non-zero for big-endian lengths + */ +static void +le_fuzz_gen_chunks (struct fuzz_rng *rng, + uint8_t *buf, + size_t *len, + size_t cap, + int big) +{ + unsigned int n = 1 + fuzz_below (rng, 12); + unsigned int i; + + for (i = 0; i < n; i++) + { + const char *tag = + le_fuzz_tags[fuzz_below (rng, + (uint32_t) (sizeof (le_fuzz_tags) + / sizeof (le_fuzz_tags[0])))]; + uint32_t plen = fuzz_below (rng, 96); + uint32_t decl; + unsigned int k; + + /* Every so often declare a length that has nothing to do with the + number of bytes actually present. This is the single most + productive mutation for these parsers. */ + if (fuzz_chance (rng, 3)) + { + static const uint32_t bogus[] = { + 0, 1, 0x7FFFFFFF, 0x80000000, 0xFFFFFFFF, 0xFFFF, 0x10000 + }; + + decl = bogus[fuzz_below (rng, + (uint32_t) (sizeof (bogus) + / sizeof (bogus[0])))]; + } + else + { + decl = plen; + } + if (big) + { + fuzz_put_mem (buf, len, cap, tag, 4); + fuzz_put_be (buf, len, cap, decl, 4); + } + else + { + fuzz_put_be (buf, len, cap, decl, 4); + fuzz_put_mem (buf, len, cap, tag, 4); + } + if (*len + plen > cap) + return; + for (k = 0; k < plen; k++) + buf[(*len)++] = fuzz_byte (rng); + } +} + + +/** + * Append a ZIP skeleton to @a buf: one local file header, one central + * directory entry and an end-of-central-directory record, with + * fuzzer-chosen (often inconsistent) offsets and sizes. + */ +static void +le_fuzz_gen_zip (struct fuzz_rng *rng, + uint8_t *buf, + size_t *len, + size_t cap) +{ + static const char *const names[] = { + "mimetype", "meta.xml", "content.xml", "docProps/core.xml", + "word/document.xml", "xl/workbook.xml", "ppt/presentation.xml", + "META-INF/manifest.xml", "a", "" + }; + const char *name = + names[fuzz_below (rng, (uint32_t) (sizeof (names) / sizeof (names[0])))]; + size_t nlen = strlen (name); + size_t lfh_off = *len; + uint32_t payload = fuzz_below (rng, 64); + unsigned int k; + + fuzz_put_mem (buf, len, cap, "PK\x03\x04", 4); + fuzz_put_le (buf, len, cap, 20, 2); /* version */ + fuzz_put_le (buf, len, cap, fuzz_byte (rng), 2); /* flags */ + fuzz_put_le (buf, len, cap, fuzz_chance (rng, 2) ? 0 : 8, 2); /* method */ + fuzz_put_le (buf, len, cap, 0, 4); /* time/date */ + fuzz_put_le (buf, len, cap, fuzz_next (rng), 4); /* crc */ + fuzz_put_le (buf, len, cap, + fuzz_chance (rng, 3) ? fuzz_next (rng) : payload, 4); + fuzz_put_le (buf, len, cap, + fuzz_chance (rng, 3) ? fuzz_next (rng) : payload, 4); + fuzz_put_le (buf, len, cap, nlen, 2); + fuzz_put_le (buf, len, cap, fuzz_chance (rng, 4) ? fuzz_below (rng, 64) : 0, + 2); + fuzz_put_mem (buf, len, cap, name, nlen); + if (*len + payload > cap) + return; + for (k = 0; k < payload; k++) + buf[(*len)++] = fuzz_byte (rng); + + { + size_t cd_off = *len; + + fuzz_put_mem (buf, len, cap, "PK\x01\x02", 4); + fuzz_put_le (buf, len, cap, 20, 2); + fuzz_put_le (buf, len, cap, 20, 2); + fuzz_put_le (buf, len, cap, 0, 2); + fuzz_put_le (buf, len, cap, 0, 2); + fuzz_put_le (buf, len, cap, 0, 4); + fuzz_put_le (buf, len, cap, 0, 4); + fuzz_put_le (buf, len, cap, payload, 4); + fuzz_put_le (buf, len, cap, payload, 4); + fuzz_put_le (buf, len, cap, nlen, 2); + fuzz_put_le (buf, len, cap, 0, 2); /* extra */ + fuzz_put_le (buf, len, cap, + fuzz_chance (rng, 4) ? fuzz_next (rng) : 0, 2); /* comment */ + fuzz_put_le (buf, len, cap, 0, 2); + fuzz_put_le (buf, len, cap, 0, 2); + fuzz_put_le (buf, len, cap, 0, 4); + fuzz_put_le (buf, len, cap, + fuzz_chance (rng, 4) ? fuzz_next (rng) : lfh_off, 4); + fuzz_put_mem (buf, len, cap, name, nlen); + + fuzz_put_mem (buf, len, cap, "PK\x05\x06", 4); + fuzz_put_le (buf, len, cap, 0, 2); + fuzz_put_le (buf, len, cap, 0, 2); + fuzz_put_le (buf, len, cap, + fuzz_chance (rng, 4) ? fuzz_next (rng) : 1, 2); + fuzz_put_le (buf, len, cap, + fuzz_chance (rng, 4) ? fuzz_next (rng) : 1, 2); + fuzz_put_le (buf, len, cap, *len - cd_off, 4); + fuzz_put_le (buf, len, cap, + fuzz_chance (rng, 4) ? fuzz_next (rng) : cd_off, 4); + fuzz_put_le (buf, len, cap, 0, 2); + } +} + + +static size_t +fuzz_generate (struct fuzz_rng *rng, + uint8_t *buf, + size_t cap) +{ + size_t len = 0; + unsigned int shape = LE_FUZZ_SHAPE; + + if (cap < 64) + return 0; + /* configuration prefix */ + buf[len++] = fuzz_chance (rng, 2) ? 0 : fuzz_byte (rng); + buf[len++] = fuzz_chance (rng, 3) ? 0 : (uint8_t) fuzz_below (rng, 0x40); + buf[len++] = fuzz_byte (rng); + buf[len++] = fuzz_byte (rng); + + /* magic; occasionally corrupted so that the reject path is covered + too, but usually intact so that the parser is actually entered */ + fuzz_put_mem (buf, &len, cap, le_fuzz_magic, LE_FUZZ_MAGIC_LEN); + if ( (LE_FUZZ_MAGIC_LEN > 0) && + fuzz_chance (rng, 12) ) + buf[LE_FUZZ_EC_PREFIX + fuzz_below (rng, (uint32_t) LE_FUZZ_MAGIC_LEN)] = + fuzz_byte (rng); + + switch (shape) + { + case LE_SHAPE_CHUNK_BE: + le_fuzz_gen_chunks (rng, buf, &len, cap, 1); + break; + case LE_SHAPE_CHUNK_LE: + /* RIFF: overall size field, form type, then LE chunks */ + fuzz_put_le (buf, &len, cap, + fuzz_chance (rng, 3) ? fuzz_next (rng) : cap, 4); + fuzz_put_mem (buf, &len, cap, + fuzz_chance (rng, 2) ? "WAVE" : "AVI ", 4); + le_fuzz_gen_chunks (rng, buf, &len, cap, 0); + break; + case LE_SHAPE_TEXT: + { + unsigned int n = 1 + fuzz_below (rng, 40); + unsigned int i; + + for (i = 0; i < n; i++) + { + if (fuzz_chance (rng, 6)) + { + unsigned int k; + unsigned int r = 1 + fuzz_below (rng, 200); + + for (k = 0; (k < r) && (len < cap); k++) + buf[len++] = fuzz_byte (rng); + } + else + { + fuzz_put_str (buf, &len, cap, + le_fuzz_lines[fuzz_below (rng, + (uint32_t) + (sizeof (le_fuzz_lines) + / sizeof (char *)))]); + } + } + break; + } + case LE_SHAPE_ZIP: + le_fuzz_gen_zip (rng, buf, &len, cap); + break; + case LE_SHAPE_RAW: + default: + { + unsigned int n = fuzz_below (rng, 1024); + unsigned int i; + + for (i = 0; (i < n) && (len < cap); i++) + { + if (fuzz_chance (rng, 16)) + { + /* a run of one value: length fields and counters love these */ + unsigned int k; + unsigned int r = 1 + fuzz_below (rng, 64); + uint8_t v = fuzz_chance (rng, 2) ? 0xFF : fuzz_byte (rng); + + for (k = 0; (k < r) && (len < cap); k++) + buf[len++] = v; + } + else + { + buf[len++] = fuzz_byte (rng); + } + } + break; + } + } + return len; +} + + +/* ------------------------------------------------------------------ */ +/* Built-in seed corpus */ +/* ------------------------------------------------------------------ */ + +/** + * The built-in seeds are deliberately minimal: the interesting seeds are + * the real files under src/plugins/testdata/, which + * contrib/oss-fuzz/make_seed_corpus.sh turns into corpus entries. These + * exist so that a bare `make -C src/fuzz check` still starts from + * something that gets past the magic-number test. + */ +#define LE_FUZZ_NSEEDS 6 + +static uint8_t le_fuzz_seed_buf[LE_FUZZ_NSEEDS][512]; +static size_t le_fuzz_seed_len[LE_FUZZ_NSEEDS]; +static int le_fuzz_seeds_ready; + + +static void +le_fuzz_build_seeds (void) +{ + struct fuzz_rng rng; + unsigned int i; + + if (le_fuzz_seeds_ready) + return; + le_fuzz_seeds_ready = 1; + for (i = 0; i < LE_FUZZ_NSEEDS; i++) + { + /* Seed 0 is the bare magic with the production configuration; the + others vary the window size, which is what most reliably changes + which code path a parser takes. */ + size_t len = 0; + uint8_t *b = le_fuzz_seed_buf[i]; + + fuzz_rng_seed (&rng, 0x5EED0000u + i); + if (0 == i) + { + /* the bare magic under the production configuration */ + len = LE_FUZZ_EC_PREFIX; + memset (b, 0, len); + fuzz_put_mem (b, &len, sizeof (le_fuzz_seed_buf[i]), + le_fuzz_magic, LE_FUZZ_MAGIC_LEN); + } + else + { + len = fuzz_generate (&rng, b, sizeof (le_fuzz_seed_buf[i])); + if (len < LE_FUZZ_EC_PREFIX) + len = LE_FUZZ_EC_PREFIX; + /* pin the configuration so that the seeds cover the knobs that + matter rather than whatever the PRNG happened to pick */ + b[0] = (uint8_t) (i + 1); /* window */ + b[1] = (uint8_t) ((4 == i) ? LE_FUZZ_RUN_TWICE : 0); /* faults */ + b[2] = (uint8_t) ((5 == i) ? 1 : 0); + b[3] = 0; + } + le_fuzz_seed_len[i] = len; + } +} + + +static size_t +fuzz_seed_count (void) +{ + le_fuzz_build_seeds (); + return LE_FUZZ_NSEEDS; +} + + +static const uint8_t * +fuzz_seed_get (size_t idx, + size_t *len) +{ + le_fuzz_build_seeds (); + *len = le_fuzz_seed_len[idx]; + return le_fuzz_seed_buf[idx]; +} diff --git a/src/fuzz/fuzz_plugin_name.h b/src/fuzz/fuzz_plugin_name.h @@ -0,0 +1,256 @@ +/* + This file is part of libextractor. + Copyright (C) 2026 Christian Grothoff + + libextractor 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, or (at your + option) any later version. + + libextractor 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 libextractor; see the file COPYING. If not, write to the + Free Software Foundation, Inc., 51 Franklin Street, Fifth Floor, + Boston, MA 02110-1301, USA. + */ +/** + * @file fuzz/fuzz_plugin_name.h + * @brief per-plugin compile-time configuration of fuzz_plugin.c + * @author Christian Grothoff + * + * fuzz_plugin.c is compiled once per plugin. The Makefile passes two + * defines per target: + * + * -DLE_FUZZ_PLUGIN=gif the plugin's short name, used to build the + * symbol names EXTRACTOR_gif_extract_method() + * and EXTRACTOR_gif_options() + * -DLE_FUZZ_ID=GIF selects the format description below + * + * The description is only used by the *generator* (the built-in driver's + * structure-aware input synthesiser). It is not a correctness + * constraint: getting it wrong costs coverage, never soundness, and a + * plugin with no entry here simply gets random bytes. + * + * To add a plugin: give it an LE_ID_* number, add the `#elif` block with + * its magic and body shape, and add the target to Makefile.am. + */ +#ifndef LE_FUZZ_PLUGIN_NAME_H +#define LE_FUZZ_PLUGIN_NAME_H 1 + +#ifndef LE_FUZZ_PLUGIN +#error "fuzz_plugin.c must be compiled with -DLE_FUZZ_PLUGIN=<short name>" +#endif +#ifndef LE_FUZZ_ID +#error "fuzz_plugin.c must be compiled with -DLE_FUZZ_ID=<ID>" +#endif + +#define LE_FUZZ_CAT2(a, b) a ## b +#define LE_FUZZ_CAT(a, b) LE_FUZZ_CAT2 (a, b) +#define LE_FUZZ_STR2(a) # a +#define LE_FUZZ_STR(a) LE_FUZZ_STR2 (a) + +/** + * The plugin's short name as a string literal, e.g. "gif". + */ +#define LE_FUZZ_PLUGIN_STR LE_FUZZ_STR (LE_FUZZ_PLUGIN) + +/** + * Name of the plugin's mandatory entry point. + */ +#define LE_FUZZ_EXTRACT_METHOD \ + LE_FUZZ_CAT (LE_FUZZ_CAT (EXTRACTOR_, LE_FUZZ_PLUGIN), _extract_method) + +/** + * Name of the plugin's optional configuration hook. + */ +#define LE_FUZZ_OPTIONS_METHOD \ + LE_FUZZ_CAT (LE_FUZZ_CAT (EXTRACTOR_, LE_FUZZ_PLUGIN), _options) + + +/* Body shapes; must match enum le_fuzz_shape in fuzz_plugin.c. They are + spelled as plain integers here because this header is included before + that enum exists. */ +#define LE_SHAPE_RAW 0 +#define LE_SHAPE_CHUNK_BE 1 +#define LE_SHAPE_CHUNK_LE 2 +#define LE_SHAPE_TEXT 3 +#define LE_SHAPE_ZIP 4 + +#define LE_ID_APPLEFILE 1 +#define LE_ID_ARCHIVE 2 +#define LE_ID_DEB 3 +#define LE_ID_DVI 4 +#define LE_ID_ELF 5 +#define LE_ID_FLAC 6 +#define LE_ID_GIF 7 +#define LE_ID_HTML 8 +#define LE_ID_IT 9 +#define LE_ID_JPEG 10 +#define LE_ID_MAN 11 +#define LE_ID_MIME 12 +#define LE_ID_MPEG 13 +#define LE_ID_MSOFFICE 14 +#define LE_ID_NSF 15 +#define LE_ID_NSFE 16 +#define LE_ID_ODF 17 +#define LE_ID_OGG 18 +#define LE_ID_PNG 19 +#define LE_ID_PS 20 +#define LE_ID_QT 21 +#define LE_ID_REAL 22 +#define LE_ID_RIFF 23 +#define LE_ID_RTF 24 +#define LE_ID_S3M 25 +#define LE_ID_SID 26 +#define LE_ID_TIFF 27 +#define LE_ID_WAV 28 +#define LE_ID_XM 29 +#define LE_ID_ZIP 30 +#define LE_ID_OLE2 31 + +#define LE_FUZZ_IDVAL LE_FUZZ_CAT (LE_ID_, LE_FUZZ_ID) + +#if LE_FUZZ_IDVAL == LE_ID_APPLEFILE +/* AppleSingle / AppleDouble: big-endian magic 0x00051600 / 0x00051607 */ +#define LE_FUZZ_MAGIC "\x00\x05\x16\x00\x00\x02\x00\x00" +#define LE_FUZZ_SHAPE LE_SHAPE_RAW + +#elif LE_FUZZ_IDVAL == LE_ID_ARCHIVE +#define LE_FUZZ_MAGIC "!<arch>\n" +#define LE_FUZZ_SHAPE LE_SHAPE_TEXT + +#elif LE_FUZZ_IDVAL == LE_ID_DEB +#define LE_FUZZ_MAGIC "!<arch>\ndebian-binary " +#define LE_FUZZ_SHAPE LE_SHAPE_TEXT + +#elif LE_FUZZ_IDVAL == LE_ID_DVI +/* DVI preamble: 0xF7 0x02 */ +#define LE_FUZZ_MAGIC "\xf7\x02" +#define LE_FUZZ_SHAPE LE_SHAPE_RAW + +#elif LE_FUZZ_IDVAL == LE_ID_ELF +#define LE_FUZZ_MAGIC "\x7f" "ELF\x01\x01\x01\x00" +#define LE_FUZZ_SHAPE LE_SHAPE_RAW + +#elif LE_FUZZ_IDVAL == LE_ID_FLAC +#define LE_FUZZ_MAGIC "fLaC" +#define LE_FUZZ_SHAPE LE_SHAPE_CHUNK_BE + +#elif LE_FUZZ_IDVAL == LE_ID_GIF +#define LE_FUZZ_MAGIC "GIF89a" +#define LE_FUZZ_SHAPE LE_SHAPE_RAW + +#elif LE_FUZZ_IDVAL == LE_ID_HTML +#define LE_FUZZ_MAGIC "<!DOCTYPE html>\n<html>" +#define LE_FUZZ_SHAPE LE_SHAPE_TEXT + +#elif LE_FUZZ_IDVAL == LE_ID_IT +/* Impulse Tracker module */ +#define LE_FUZZ_MAGIC "IMPM" +#define LE_FUZZ_SHAPE LE_SHAPE_RAW + +#elif LE_FUZZ_IDVAL == LE_ID_JPEG +#define LE_FUZZ_MAGIC "\xff\xd8\xff\xe0\x00\x10JFIF\x00" +#define LE_FUZZ_SHAPE LE_SHAPE_RAW + +#elif LE_FUZZ_IDVAL == LE_ID_MAN +#define LE_FUZZ_MAGIC ".TH " +#define LE_FUZZ_SHAPE LE_SHAPE_TEXT + +#elif LE_FUZZ_IDVAL == LE_ID_MIME +/* libmagic sniffs everything; no useful magic of our own */ +#define LE_FUZZ_MAGIC "" +#define LE_FUZZ_SHAPE LE_SHAPE_RAW + +#elif LE_FUZZ_IDVAL == LE_ID_MPEG +#define LE_FUZZ_MAGIC "\x00\x00\x01\xba" +#define LE_FUZZ_SHAPE LE_SHAPE_RAW + +#elif LE_FUZZ_IDVAL == LE_ID_MSOFFICE +/* OOXML is a ZIP */ +#define LE_FUZZ_MAGIC "" +#define LE_FUZZ_SHAPE LE_SHAPE_ZIP + +#elif LE_FUZZ_IDVAL == LE_ID_NSF +#define LE_FUZZ_MAGIC "NESM\x1a\x01" +#define LE_FUZZ_SHAPE LE_SHAPE_RAW + +#elif LE_FUZZ_IDVAL == LE_ID_NSFE +#define LE_FUZZ_MAGIC "NSFE" +#define LE_FUZZ_SHAPE LE_SHAPE_CHUNK_BE + +#elif LE_FUZZ_IDVAL == LE_ID_ODF +#define LE_FUZZ_MAGIC "" +#define LE_FUZZ_SHAPE LE_SHAPE_ZIP + +#elif LE_FUZZ_IDVAL == LE_ID_OGG +#define LE_FUZZ_MAGIC "OggS\x00\x02" +#define LE_FUZZ_SHAPE LE_SHAPE_RAW + +#elif LE_FUZZ_IDVAL == LE_ID_OLE2 +#define LE_FUZZ_MAGIC "\xd0\xcf\x11\xe0\xa1\xb1\x1a\xe1" +#define LE_FUZZ_SHAPE LE_SHAPE_RAW + +#elif LE_FUZZ_IDVAL == LE_ID_PNG +#define LE_FUZZ_MAGIC "\x89PNG\r\n\x1a\n" +#define LE_FUZZ_SHAPE LE_SHAPE_CHUNK_BE + +#elif LE_FUZZ_IDVAL == LE_ID_PS +#define LE_FUZZ_MAGIC "%!PS-Adobe-3.0\n" +#define LE_FUZZ_SHAPE LE_SHAPE_TEXT + +#elif LE_FUZZ_IDVAL == LE_ID_QT +/* QuickTime: size + 'moov' at the very start is the easy way in */ +#define LE_FUZZ_MAGIC "\x00\x00\x00\x08moov" +#define LE_FUZZ_SHAPE LE_SHAPE_CHUNK_BE + +#elif LE_FUZZ_IDVAL == LE_ID_REAL +#define LE_FUZZ_MAGIC ".RMF\x00\x00\x00\x12" +#define LE_FUZZ_SHAPE LE_SHAPE_CHUNK_BE + +#elif LE_FUZZ_IDVAL == LE_ID_RIFF +#define LE_FUZZ_MAGIC "RIFF" +#define LE_FUZZ_SHAPE LE_SHAPE_CHUNK_LE + +#elif LE_FUZZ_IDVAL == LE_ID_RTF +#define LE_FUZZ_MAGIC "{\\rtf1" +#define LE_FUZZ_SHAPE LE_SHAPE_TEXT + +#elif LE_FUZZ_IDVAL == LE_ID_S3M +/* the 'SCRM' tag lives at offset 0x2c, so the magic here is the whole + leading header up to and including it */ +#define LE_FUZZ_MAGIC \ + "fuzz s3m module\x00" "\x1a\x10\x00\x00" \ + "\x01\x00\x01\x00\x01\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00" \ + "\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00" "SCRM" +#define LE_FUZZ_SHAPE LE_SHAPE_RAW + +#elif LE_FUZZ_IDVAL == LE_ID_SID +#define LE_FUZZ_MAGIC "PSID\x00\x02\x00\x7c\x00\x7c" +#define LE_FUZZ_SHAPE LE_SHAPE_RAW + +#elif LE_FUZZ_IDVAL == LE_ID_TIFF +#define LE_FUZZ_MAGIC "II\x2a\x00\x08\x00\x00\x00" +#define LE_FUZZ_SHAPE LE_SHAPE_RAW + +#elif LE_FUZZ_IDVAL == LE_ID_WAV +#define LE_FUZZ_MAGIC "RIFF" +#define LE_FUZZ_SHAPE LE_SHAPE_CHUNK_LE + +#elif LE_FUZZ_IDVAL == LE_ID_XM +#define LE_FUZZ_MAGIC "Extended Module: " +#define LE_FUZZ_SHAPE LE_SHAPE_RAW + +#elif LE_FUZZ_IDVAL == LE_ID_ZIP +#define LE_FUZZ_MAGIC "" +#define LE_FUZZ_SHAPE LE_SHAPE_ZIP + +#else +#error "no format description for this LE_FUZZ_ID; add one above" +#endif + +#endif /* LE_FUZZ_PLUGIN_NAME_H */ diff --git a/src/fuzz/fuzz_unzip.c b/src/fuzz/fuzz_unzip.c @@ -0,0 +1,445 @@ +/* + This file is part of libextractor. + Copyright (C) 2026 Christian Grothoff + + libextractor 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, or (at your + option) any later version. + + libextractor 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 libextractor; see the file COPYING. If not, write to the + Free Software Foundation, Inc., 51 Franklin Street, Fifth Floor, + Boston, MA 02110-1301, USA. + */ +/** + * @file fuzz/fuzz_unzip.c + * @brief direct fuzzer for the in-tree ZIP reader, src/common/unzip.c + * @author Christian Grothoff + * + * The ZIP reader is shared by the odf, msoffice and zip plugins, so a + * bug here is reachable from three formats at once, and it is + * hand-written code descended from unzip 1.00 rather than a maintained + * third-party library. That combination makes it the highest-value + * single target in the tree. + * + * Every output buffer this harness passes is `malloc()`ed at *exactly* + * the size the API was told about, so ASAN's redzone turns a one-byte + * over-write into a hard error. Each call that takes a buffer size is + * additionally repeated with a deliberately too-small buffer: an API + * that is given a size must honour it. + * + * Input format: the fuzz_ec.h configuration prefix, then the ZIP image. + */ + +#define FUZZ_HARNESS_NAME "fuzz_unzip" + +#include "fuzz_common.h" +#include "fuzz_ec.h" +#include "unzip.h" + +/** + * Upper bound on the number of archive members walked per input; a + * malformed central directory can otherwise be made to loop for a very + * long time, which is a finding of its own but not one worth + * rediscovering on every execution. + */ +#define MAX_MEMBERS 64 + +/** + * Upper bound on the number of bytes decompressed per member. + */ +#define MAX_INFLATE (1024 * 1024) + + +static void * +xalloc (size_t n) +{ + void *p = malloc ((0 == n) ? 1 : n); + + if (NULL == p) + abort (); + return p; +} + + +/** + * Ask for the current member's metadata twice: once with buffers sized + * exactly as the first (size-query) call reported, and once with buffers + * that are deliberately too small. + * + * @param uf the open archive + * @param aux fuzzer-chosen shrink factor for the too-small pass + */ +static void +probe_current_file_info (struct EXTRACTOR_UnzipFile *uf, + unsigned int aux) +{ + struct EXTRACTOR_UnzipFileInfo fi; + char *name; + char *comment; + void *extra; + size_t small; + + memset (&fi, 0, sizeof (fi)); + if (EXTRACTOR_UNZIP_OK != + EXTRACTOR_common_unzip_get_current_file_info (uf, + &fi, + NULL, 0, + NULL, 0, + NULL, 0)) + return; + /* Contract-exact buffers. Note that the API is told the buffer size, + so it must not write more than that even if the archive declares a + longer name. */ + name = (char *) xalloc (fi.size_filename); + extra = xalloc (fi.size_file_extra); + comment = (char *) xalloc (fi.size_file_comment); + if (EXTRACTOR_UNZIP_OK != + EXTRACTOR_common_unzip_get_current_file_info (uf, + &fi, + name, + (uLong) fi.size_filename, + extra, + (uLong) fi.size_file_extra, + comment, + (uLong) + fi.size_file_comment)) + { + free (name); + free (extra); + free (comment); + return; + } + free (name); + free (extra); + free (comment); + + /* Too-small pass. */ + small = (0 == fi.size_filename) + ? 0 + : ((size_t) fi.size_filename * (aux % 100u)) / 100u; + name = (char *) xalloc (small); + (void) EXTRACTOR_common_unzip_get_current_file_info (uf, + &fi, + name, + (uLong) small, + NULL, 0, + NULL, 0); + free (name); + small = (0 == fi.size_file_comment) + ? 0 + : ((size_t) fi.size_file_comment * (aux % 100u)) / 100u; + comment = (char *) xalloc (small); + (void) EXTRACTOR_common_unzip_get_current_file_info (uf, + &fi, + NULL, 0, + NULL, 0, + comment, + (uLong) small); + free (comment); + small = (0 == fi.size_file_extra) + ? 0 + : ((size_t) fi.size_file_extra * (aux % 100u)) / 100u; + extra = xalloc (small); + (void) EXTRACTOR_common_unzip_get_current_file_info (uf, + &fi, + NULL, 0, + extra, (uLong) small, + NULL, 0); + free (extra); +} + + +/** + * Decompress the current member into exactly-sized chunks. + * + * @param uf the open archive + * @param chunk number of bytes to request per read + */ +static void +probe_read_current (struct EXTRACTOR_UnzipFile *uf, + size_t chunk) +{ + size_t total = 0; + + if (EXTRACTOR_UNZIP_OK != + EXTRACTOR_common_unzip_open_current_file (uf)) + return; + if (0 == chunk) + chunk = 1; + while (total < MAX_INFLATE) + { + /* exact-size destination: a decompressor that writes one byte too + many lands in the redzone */ + void *buf = xalloc (chunk); + ssize_t got = EXTRACTOR_common_unzip_read_current_file (uf, buf, chunk); + + if (0 < got) + { + if ((size_t) got > chunk) + { + free (buf); + fuzz_report_finding ("EXTRACTOR_common_unzip_read_current_file() " + "reported more bytes than the buffer size it " + "was given"); + } + total += (size_t) got; + } + free (buf); + if (0 >= got) + break; + } + (void) EXTRACTOR_common_unzip_close_current_file (uf); +} + + +int +LLVMFuzzerTestOneInput (const uint8_t *data, + size_t size) +{ + struct EXTRACTOR_ExtractContext ec; + struct fuzz_ec_state st; + struct EXTRACTOR_UnzipFile *uf; + unsigned int members; + unsigned int aux; + size_t chunk; + + fuzz_ignore_sigpipe (); + if (! le_fuzz_ec_setup (&ec, &st, data, size)) + return 0; + aux = data[3]; + chunk = (size_t) 1 << (aux % 17u); + uf = EXTRACTOR_common_unzip_open (&ec); + if (NULL == uf) + { + le_fuzz_ec_cleanup (&st); + return 0; + } + { + /* The global comment length is not exposed, so the only contract we + can check is that the function honours the size we hand it. */ + size_t clen = 1 + (aux % 512u); + char *c = (char *) xalloc (clen); + + (void) EXTRACTOR_common_unzip_get_global_comment (uf, c, clen); + free (c); + c = (char *) xalloc (1); + (void) EXTRACTOR_common_unzip_get_global_comment (uf, c, 1); + free (c); + } + if (EXTRACTOR_UNZIP_OK == + EXTRACTOR_common_unzip_go_to_first_file (uf)) + { + for (members = 0; members < MAX_MEMBERS; members++) + { + probe_current_file_info (uf, aux); + probe_read_current (uf, chunk); + if (EXTRACTOR_UNZIP_OK != + EXTRACTOR_common_unzip_go_to_next_file (uf)) + break; + } + } + /* Name lookup: the comparison walks the central directory again, and + the name comes straight from the input. */ + { + static const char *const names[] = { + "mimetype", "meta.xml", "content.xml", "docProps/core.xml", + "word/document.xml", "", "a" + }; + + (void) EXTRACTOR_common_unzip_go_find_local_file (uf, + names[aux + % (sizeof (names) + / sizeof (char *) + )], + (int) (aux % 3u)); + } + (void) EXTRACTOR_common_unzip_close (uf); + le_fuzz_ec_cleanup (&st); + return 0; +} + + +/* ------------------------------------------------------------------ */ +/* Generator */ +/* ------------------------------------------------------------------ */ + +/** + * Build a ZIP archive with @a nmem members. Sizes, offsets and counts + * are frequently made inconsistent on purpose: a central directory that + * disagrees with the local headers is the classic way into this parser. + */ +static size_t +fuzz_generate (struct fuzz_rng *rng, + uint8_t *buf, + size_t cap) +{ + static const char *const names[] = { + "mimetype", "meta.xml", "content.xml", "docProps/core.xml", + "word/document.xml", "xl/workbook.xml", "META-INF/manifest.xml", + "a", "", "../../etc/passwd", + "aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa" + }; + size_t len = 0; + size_t cd_off; + unsigned int nmem = 1 + fuzz_below (rng, 6); + unsigned int i; + size_t lfh[8]; + size_t nmlen[8]; + const char *nm[8]; + uint32_t csize[8]; + + if (cap < 256) + return 0; + buf[len++] = fuzz_chance (rng, 2) ? 0 : fuzz_byte (rng); + buf[len++] = fuzz_chance (rng, 3) ? 0 : (uint8_t) fuzz_below (rng, 0x40); + buf[len++] = fuzz_byte (rng); + buf[len++] = fuzz_byte (rng); + + for (i = 0; i < nmem; i++) + { + unsigned int k; + uint32_t payload = fuzz_below (rng, 128); + + nm[i] = names[fuzz_below (rng, + (uint32_t) (sizeof (names) / sizeof (char *)))]; + nmlen[i] = strlen (nm[i]); + lfh[i] = len; + csize[i] = payload; + fuzz_put_mem (buf, &len, cap, "PK\x03\x04", 4); + fuzz_put_le (buf, &len, cap, 20, 2); + fuzz_put_le (buf, &len, cap, + fuzz_chance (rng, 4) ? fuzz_byte (rng) : 0, 2); + fuzz_put_le (buf, &len, cap, fuzz_chance (rng, 2) ? 0 : 8, 2); + fuzz_put_le (buf, &len, cap, fuzz_next (rng), 4); + fuzz_put_le (buf, &len, cap, fuzz_next (rng), 4); /* crc */ + fuzz_put_le (buf, &len, cap, + fuzz_chance (rng, 4) ? fuzz_next (rng) : payload, 4); + fuzz_put_le (buf, &len, cap, + fuzz_chance (rng, 4) ? fuzz_next (rng) : payload, 4); + fuzz_put_le (buf, &len, cap, + fuzz_chance (rng, 6) ? fuzz_next (rng) : nmlen[i], 2); + fuzz_put_le (buf, &len, cap, + fuzz_chance (rng, 4) ? fuzz_below (rng, 4096) : 0, 2); + fuzz_put_mem (buf, &len, cap, nm[i], nmlen[i]); + if (len + payload > cap) + break; + for (k = 0; k < payload; k++) + buf[len++] = fuzz_chance (rng, 3) ? 0 : fuzz_byte (rng); + } + nmem = i; + cd_off = len; + for (i = 0; i < nmem; i++) + { + fuzz_put_mem (buf, &len, cap, "PK\x01\x02", 4); + fuzz_put_le (buf, &len, cap, 20, 2); + fuzz_put_le (buf, &len, cap, 20, 2); + fuzz_put_le (buf, &len, cap, + fuzz_chance (rng, 4) ? fuzz_byte (rng) : 0, 2); + fuzz_put_le (buf, &len, cap, fuzz_chance (rng, 2) ? 0 : 8, 2); + fuzz_put_le (buf, &len, cap, 0, 4); + fuzz_put_le (buf, &len, cap, fuzz_next (rng), 4); + fuzz_put_le (buf, &len, cap, + fuzz_chance (rng, 4) ? fuzz_next (rng) : csize[i], 4); + fuzz_put_le (buf, &len, cap, + fuzz_chance (rng, 4) ? fuzz_next (rng) : csize[i], 4); + fuzz_put_le (buf, &len, cap, + fuzz_chance (rng, 6) ? fuzz_next (rng) : nmlen[i], 2); + fuzz_put_le (buf, &len, cap, + fuzz_chance (rng, 5) ? fuzz_next (rng) : 0, 2); + fuzz_put_le (buf, &len, cap, + fuzz_chance (rng, 5) ? fuzz_next (rng) : 0, 2); + fuzz_put_le (buf, &len, cap, 0, 2); + fuzz_put_le (buf, &len, cap, 0, 2); + fuzz_put_le (buf, &len, cap, 0, 4); + fuzz_put_le (buf, &len, cap, + fuzz_chance (rng, 5) ? fuzz_next (rng) : lfh[i], 4); + fuzz_put_mem (buf, &len, cap, nm[i], nmlen[i]); + } + fuzz_put_mem (buf, &len, cap, "PK\x05\x06", 4); + fuzz_put_le (buf, &len, cap, 0, 2); + fuzz_put_le (buf, &len, cap, 0, 2); + fuzz_put_le (buf, &len, cap, + fuzz_chance (rng, 5) ? fuzz_next (rng) : nmem, 2); + fuzz_put_le (buf, &len, cap, + fuzz_chance (rng, 5) ? fuzz_next (rng) : nmem, 2); + fuzz_put_le (buf, &len, cap, + fuzz_chance (rng, 5) ? fuzz_next (rng) : len - cd_off, 4); + fuzz_put_le (buf, &len, cap, + fuzz_chance (rng, 5) ? fuzz_next (rng) : cd_off, 4); + { + /* An end-of-central-directory comment whose declared length runs + past the end of the file is the shortest path to a read past the + buffer. */ + size_t clen = fuzz_chance (rng, 3) ? fuzz_below (rng, 0x10000) : 0; + + fuzz_put_le (buf, &len, cap, clen, 2); + if (! fuzz_chance (rng, 2)) + { + size_t k; + + for (k = 0; (k < clen) && (len < cap); k++) + buf[len++] = fuzz_byte (rng); + } + } + return len; +} + + +/* ------------------------------------------------------------------ */ +/* Seed corpus */ +/* ------------------------------------------------------------------ */ + +#define LE_UNZIP_NSEEDS 8 + +static uint8_t unzip_seed_buf[LE_UNZIP_NSEEDS][1024]; +static size_t unzip_seed_len[LE_UNZIP_NSEEDS]; +static int unzip_seeds_ready; + + +static void +unzip_build_seeds (void) +{ + struct fuzz_rng rng; + unsigned int i; + + if (unzip_seeds_ready) + return; + unzip_seeds_ready = 1; + for (i = 0; i < LE_UNZIP_NSEEDS; i++) + { + fuzz_rng_seed (&rng, 0x21D0u + i); + unzip_seed_len[i] = fuzz_generate (&rng, + unzip_seed_buf[i], + sizeof (unzip_seed_buf[i])); + if (unzip_seed_len[i] >= LE_FUZZ_EC_PREFIX) + { + unzip_seed_buf[i][0] = (uint8_t) i; + unzip_seed_buf[i][1] = 0; + } + } +} + + +static size_t +fuzz_seed_count (void) +{ + unzip_build_seeds (); + return LE_UNZIP_NSEEDS; +} + + +static const uint8_t * +fuzz_seed_get (size_t idx, + size_t *len) +{ + unzip_build_seeds (); + *len = unzip_seed_len[idx]; + return unzip_seed_buf[idx]; +} diff --git a/src/plugins/Makefile.am b/src/plugins/Makefile.am @@ -21,7 +21,6 @@ PLUGINFLAGS = $(makesymbolic) $(LE_PLUGIN_LDFLAGS) SUBDIRS = . EXTRA_DIST = \ - fuzz_default.sh \ template_extractor.c \ testdata/applefile_test.applesingle \ testdata/audiosig.rm \ @@ -219,10 +218,6 @@ plugin_LTLIBRARIES = \ $(PLUGIN_TIFF) \ $(PLUGIN_ZLIB) -if HAVE_ZZUF - fuzz_tests=fuzz_default.sh -endif - check_PROGRAMS = \ test_applefile \ test_dvi \ @@ -264,7 +259,6 @@ check_PROGRAMS = \ if ENABLE_TEST_RUN TESTS = \ - $(fuzz_tests) \ $(check_PROGRAMS) endif diff --git a/src/plugins/fuzz_default.sh b/src/plugins/fuzz_default.sh @@ -1,56 +0,0 @@ -#!/bin/sh - -set -eu - -ZZSTARTSEED=0 -ZZSTOPSEED=100 -ret=0 -# fallbacks for direct, non-"make check" usage -if test x"${testdatadir:-NONE}" = xNONE"" -then - testdatadir=../../test -fi -if test x"${bindir:-NONE}" = xNONE"" -then - bindir=`grep "^prefix = " ./Makefile | cut -d ' ' -f 3` - bindir="$bindir/bin" -fi - -if test ! -x `which zzuf` -then - echo "zzuf not available, not running the test" - exit 77 -fi - -if test -x `which timeout` -then - TIMEOUT="timeout 15" -else - echo "timeout command not found, will not auto-timeout (may cause hang)" - TIMEOUT="" -fi - -for file in $testdatadir/test* -do - if test -f "$file" - then - tmpfile=`mktemp extractortmp.XXXXXX` || exit 1 - seed=$ZZSTARTSEED - trap "echo $tmpfile caused SIGSEGV ; exit 1" SEGV - while [ $seed -lt $ZZSTOPSEED ] - do - echo "file $file seed $seed" - zzuf -c -s $seed cat "$file" > "$tmpfile" - if ! $TIMEOUT $bindir/extract -i "$tmpfile" > /dev/null - then - echo "$tmpfile with seed $seed failed" - mv $tmpfile $tmpfile.keep - ret=1 - fi - seed=`expr $seed + 1` - done - rm -f "$tmpfile" - fi -done - -exit $ret