libextractor

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

commit bd9842a1253e552c628118e61a062f679e31ed4e
parent 8975fde974153e97261709829be1d2262a0204fc
Author: Christian Grothoff <christian@grothoff.org>
Date:   Mon,  2 Nov 2026 00:37:12 +0100

fuzz: cover the full-text plugin targets

odf and ole2 never got fulltext.c added to the oss-fuzz build, so both
failed to link, and pdf had no target there at all despite having one in
src/fuzz/Makefile.am -- so the full-text code could not be fuzzed.
The fourth prefix byte selects the plugin configuration and every
testdata seed had it unset, which is "no options", so the full-text paths
were not covered.

No issues were found fuzzing MHD with the new logic (but several issues
were found and reported upstream).

Diffstat:
Mcontrib/oss-fuzz/build.sh | 52++++++++++++++++++++++++++++++++++++++++++++++------
Mcontrib/oss-fuzz/make_seed_corpus.sh | 44+++++++++++++++++++++++++++++++++++++++++++-
2 files changed, 89 insertions(+), 7 deletions(-)

diff --git a/contrib/oss-fuzz/build.sh b/contrib/oss-fuzz/build.sh @@ -312,7 +312,7 @@ wav:wav_extractor.c: xm:xm_extractor.c: deb:deb_extractor.c:-lz msoffice:msoffice_extractor.c:-lz -odf:odf_extractor.c:-lz +odf:odf_extractor.c fulltext.c:-lz png:png_extractor.c:-lz qt:qt_extractor.c:-lz zip:zip_extractor.c:-lz @@ -364,7 +364,7 @@ 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} +ole2:ole2_extractor.c fulltext.c:${_le_gsf_libs} " LE_GSF_CFLAGS="${_le_gsf_cflags}" else @@ -373,10 +373,33 @@ ole2:ole2_extractor.c:${_le_gsf_libs} fi fi +# pdf is the third such case, and it is excluded here for the same +# reason as ole2: libpoppler is not instrumented in this build and has +# its own OSS-Fuzz project, so its bugs must not be attributed to us. +# But the "fulltext" option made pdf_extractor.cc a good deal more than +# a thin wrapper -- the line grouping, the two-column detection, the +# margin filtering and all of pagelayout.c run on poppler's *output* and +# are ours -- so a local campaign wants it. Set LE_FUZZ_POPPLER=1. +# Note that the plugin is C++; build_one dispatches on the extension. +POPPLER_PLUGIN_FUZZER="" +if [ "${LE_FUZZ_POPPLER:-0}" != "0" ]; then + if _le_pop_cflags="$(pkg-config --cflags poppler-cpp 2>/dev/null)" && + _le_pop_libs="$(pkg-config --libs poppler-cpp 2>/dev/null)"; then + POPPLER_PLUGIN_FUZZER=" +pdf:pdf_extractor.cc fulltext.c pagelayout.c:${_le_pop_libs} -lm +" + LE_POPPLER_CFLAGS="${_le_pop_cflags}" + else + echo "WARNING: LE_FUZZ_POPPLER=1 but pkg-config cannot find" >&2 + echo " poppler-cpp; the pdf 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}" +PLUGIN_FUZZERS="${PLUGIN_FUZZERS}${POPPLER_PLUGIN_FUZZER}" # Upper-case the plugin name for -DLE_FUZZ_ID. upper () { echo "$1" | tr '[:lower:]' '[:upper:]'; } @@ -389,10 +412,23 @@ build_one () 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}" + o="${WORK}/${target}-$(basename "${src}" | sed 's/\.[^.]*$//').o" + # pdf_extractor.cc is C++. Dispatch on the extension rather than + # letting the driver guess: clang would compile a .cc as C++ anyway, + # but it would do so with $CFLAGS, and OSS-Fuzz passes the two + # separately precisely because they are not interchangeable. + case "${src}" in + *.cc | *.cpp | *.cxx) + # shellcheck disable=SC2086 + $CXX $CXXFLAGS ${_le_no_main} ${LE_EXTRA_CPPFLAGS:-} \ + "${LE_INCLUDES[@]}" -c "${src}" -o "${o}" + ;; + *) + # shellcheck disable=SC2086 + $CC $CFLAGS ${_le_no_main} ${LE_EXTRA_CPPFLAGS:-} \ + "${LE_INCLUDES[@]}" -c "${src}" -o "${o}" + ;; + esac objs="${objs} ${o}" done # Link with $CXX: $LIB_FUZZING_ENGINE is a C++ archive for most engines. @@ -438,6 +474,10 @@ for line in ${PLUGIN_FUZZERS}; do if [ "${name}" = "ole2" ]; then _le_plug_cppflags="${_le_plug_cppflags} ${LE_GSF_CFLAGS:-}" fi + # pdf_extractor.cc includes <poppler/cpp/*.h>; same story as ole2. + if [ "${name}" = "pdf" ]; then + _le_plug_cppflags="${_le_plug_cppflags} ${LE_POPPLER_CFLAGS:-}" + fi LE_EXTRA_CPPFLAGS="${_le_plug_cppflags}" \ build_one "${target}" "${libs}" "${LE_SRC}/src/fuzz/fuzz_plugin.c" "$@" FUZZERS="${FUZZERS} ${target}" diff --git a/contrib/oss-fuzz/make_seed_corpus.sh b/contrib/oss-fuzz/make_seed_corpus.sh @@ -55,6 +55,24 @@ prefix_len () esac } +# How many configurations the harness offers, i.e. the length of +# LE_FUZZ_CONFIGS in src/fuzz/fuzz_plugin_name.h. The fourth prefix byte +# selects one of them (fuzz_plugin.c), so a target listed here gets one +# seed per configuration instead of just the default. Keep in sync with +# that header: a count that is too high only wastes a few duplicate +# seeds (the index is taken modulo the real length), one that is too low +# silently leaves a configuration unseeded. +num_configs () +{ + case "$1" in + fuzz_dvi | fuzz_pdf | fuzz_html | fuzz_odf | fuzz_ole2 | fuzz_ebook) + echo 2 ;; # NULL, "fulltext" + fuzz_thumbnailffmpeg | fuzz_thumbnailgst) + echo 5 ;; # NULL, srt, 3x gallery + *) echo 1 ;; + esac +} + # Which testdata files seed which target. A target with no entry gets # only its built-in seeds. testdata_glob () @@ -171,7 +189,31 @@ for t in ${TARGETS}; do esac b=$(basename "${f}") if [ "${n}" -gt 0 ]; then - cat "${WORK}/prefix" "${f}" > "${d}/td-${b}" + # One copy per plugin configuration. An all-zero prefix selects + # configuration 0, which for every plugin is "no options" -- so + # without this loop a target whose interesting code is reached + # only through an option would be seeded entirely in the dark. + # It is not a small effect: on the fulltext plugins the + # "fulltext" configuration is worth between +285 (ole2) and + # +1938 (pdf) edges over the default, i.e. most of what those + # plugins can execute at all. The fuzzer can flip the byte + # itself, but it would be starting from a corpus in which none + # of that code has ever run, and corpus distillation keeps + # pulling it back to the basin it already knows. + nc=$(num_configs "${t}") + c=0 + while [ "${c}" -lt "${nc}" ]; do + if [ "${c}" -eq 0 ]; then + cat "${WORK}/prefix" "${f}" > "${d}/td-${b}" + else + # prefix with the last byte replaced by the config index + dd if="${WORK}/prefix" bs=1 count=$((n - 1)) 2>/dev/null \ + > "${WORK}/cprefix" + printf "\\$(printf '%03o' "${c}")" >> "${WORK}/cprefix" + cat "${WORK}/cprefix" "${f}" > "${d}/td-c${c}-${b}" + fi + c=$((c + 1)) + done else cp "${f}" "${d}/td-${b}" fi