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:
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