diff --git a/CMakeLists.txt b/CMakeLists.txt index 6b7d1288e0..e23d46555a 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -244,6 +244,23 @@ else() option(ENABLE_SCE "enables Script Check Engine - an alternative checking engine that lets you use executables instead of OVAL for checks" ON) endif() +# ---------- FUZZING +option(ENABLE_FUZZING "build libFuzzer harnesses (fuzz/) and instrument the library with libFuzzer + ASan/UBSan. Requires a Clang toolchain." OFF) +if(ENABLE_FUZZING) + if(NOT CMAKE_C_COMPILER_ID MATCHES "Clang") + message(FATAL_ERROR "ENABLE_FUZZING requires Clang (libFuzzer). Re-run cmake with CC=clang CXX=clang++.") + endif() + # Instrument the whole library (and everything else) for libFuzzer coverage + # and catch memory/UB errors at runtime. fuzzer-no-link adds the coverage + # instrumentation without pulling libFuzzer's main() into every object; + # the harness target adds -fsanitize=fuzzer to get the driver. + set(OSCAP_FUZZING_FLAGS "-fsanitize=fuzzer-no-link,address,undefined -fno-omit-frame-pointer -g") + set(CMAKE_C_FLAGS "${CMAKE_C_FLAGS} ${OSCAP_FUZZING_FLAGS}") + set(CMAKE_CXX_FLAGS "${CMAKE_CXX_FLAGS} ${OSCAP_FUZZING_FLAGS}") + set(CMAKE_EXE_LINKER_FLAGS "${CMAKE_EXE_LINKER_FLAGS} -fsanitize=address,undefined") + set(CMAKE_SHARED_LINKER_FLAGS "${CMAKE_SHARED_LINKER_FLAGS} -fsanitize=address,undefined") +endif() + # ---------- OVAL FEATURE SWITCHES option(ENABLE_PROBES "build OVAL probes - each probe implements an OVAL test" TRUE) @@ -635,6 +652,9 @@ endif() add_subdirectory("compat") add_subdirectory("src") +if(ENABLE_FUZZING) + add_subdirectory("fuzz") +endif() add_subdirectory("utils") add_subdirectory("docs") add_subdirectory("dist") diff --git a/fuzz/.gitignore b/fuzz/.gitignore new file mode 100644 index 0000000000..21df6746be --- /dev/null +++ b/fuzz/.gitignore @@ -0,0 +1,23 @@ +# libFuzzer run artifacts (these patterns match anywhere; the curated +# regression inputs under reproducers/ are re-included below) +crash-* +oom-* +leak-* +timeout-* +*.profraw + +# Always keep the curated regression corpus, even though some are named crash-* +!reproducers/ +!reproducers/** + +# run-all.sh outputs +findings/ +logs/ +*.work/ + +# Fuzzing corpora are large (seeded from tests/, then grown by libFuzzer) and +# regenerable; they are not committed. Regression inputs live in reproducers/. +corpus/ +corpus_xccdf/ +corpus_arf/ +corpus_tailoring/ diff --git a/fuzz/CMakeLists.txt b/fuzz/CMakeLists.txt new file mode 100644 index 0000000000..08fd712103 --- /dev/null +++ b/fuzz/CMakeLists.txt @@ -0,0 +1,147 @@ +# libFuzzer harnesses for SCAP parsing / processing. +# +# Enabled with -DENABLE_FUZZING=ON. Requires a Clang toolchain (libFuzzer ships +# with clang). When enabled, the whole library is compiled with the libFuzzer +# coverage instrumentation plus AddressSanitizer/UndefinedBehaviorSanitizer (set +# from the top-level CMakeLists), and each harness is linked with +# -fsanitize=fuzzer to pull in the libFuzzer driver/main. + +set(FUZZ_INCLUDE_DIRS + "${CMAKE_CURRENT_SOURCE_DIR}" + "${CMAKE_SOURCE_DIR}/src/common/public" + "${CMAKE_SOURCE_DIR}/src/source/public" + "${CMAKE_SOURCE_DIR}/src/DS/public" + "${CMAKE_SOURCE_DIR}/src/XCCDF/public" + "${CMAKE_SOURCE_DIR}/src/XCCDF_POLICY/public" + "${CMAKE_SOURCE_DIR}/src/CPE/public" + "${CMAKE_SOURCE_DIR}/src/OVAL/public" + "${LIBXML2_INCLUDE_DIR}" +) + +# add_fuzzer( ) builds one libFuzzer executable linked against the +# instrumented library. +function(add_fuzzer name source) + add_executable(${name} ${source}) + target_include_directories(${name} PRIVATE ${FUZZ_INCLUDE_DIRS}) + target_link_libraries(${name} openscap) + target_compile_options(${name} PRIVATE -fsanitize=fuzzer) + target_link_options(${name} PRIVATE -fsanitize=fuzzer) +endfunction() + +add_fuzzer(scap_parse_fuzzer scap_parse_fuzzer.c) # dispatch-by-type parser +add_fuzzer(xccdf_policy_fuzzer xccdf_policy_fuzzer.c) # XCCDF policy/profile layer +add_fuzzer(validate_fuzzer validate_fuzzer.c) # XSD + Schematron validation +add_fuzzer(arf_fuzzer arf_fuzzer.c) # ARF / result data stream (RDS) +add_fuzzer(xccdf_tailoring_fuzzer xccdf_tailoring_fuzzer.c) # XCCDF tailoring + +set(FUZZ_TARGETS + scap_parse_fuzzer + xccdf_policy_fuzzer + validate_fuzzer + arf_fuzzer + xccdf_tailoring_fuzzer +) + +# Probe-content harnesses: these fuzz the data an OVAL probe parses out of the +# scanned filesystem (xinetd configs, /etc/passwd, /proc/net/tcp, text files, +# ...). They #include a probe's .c directly to reach its static parser, so they +# only build when probes are compiled into the library (-DENABLE_PROBES=ON) and +# they need the probe headers plus a few helper sources that are not exported +# from libopenscap.so (the same set tests/probes/xinetd uses). +if(ENABLE_PROBES) + set(PROBE_FUZZ_INCLUDE_DIRS + "${CMAKE_SOURCE_DIR}/src/OVAL/probes" + "${CMAKE_SOURCE_DIR}/src/OVAL/probes/public" + "${CMAKE_SOURCE_DIR}/src/OVAL/probes/SEAP/public" + "${CMAKE_SOURCE_DIR}/src/OVAL/probes/SEAP/generic/rbt" + "${CMAKE_SOURCE_DIR}/src/common" + ) + set(PROBE_FUZZ_EXTRA_SOURCES + "${CMAKE_SOURCE_DIR}/src/common/bfind.c" + "${CMAKE_SOURCE_DIR}/src/OVAL/probes/SEAP/generic/rbt/rbt_common.c" + "${CMAKE_SOURCE_DIR}/src/OVAL/probes/SEAP/generic/rbt/rbt_str.c" + "${CMAKE_SOURCE_DIR}/src/common/oscap_pcre.c" + ) + + # add_fuzzer_probe( [extra sources...]) is add_fuzzer() plus + # the probe include dirs and the helper sources needed to compile an + # #include-d probe .c. Extra per-harness sources are passed via ARGN. + # + # --gc-sections drops the probe's own *_probe_main (and the static helpers + # only it reaches, e.g. collect_item): we drive the parser functions + # directly, and those entry points would otherwise pull in non-exported + # probe-runtime symbols that are not linked here. + function(add_fuzzer_probe name source) + add_executable(${name} ${source} ${PROBE_FUZZ_EXTRA_SOURCES} ${ARGN}) + target_include_directories(${name} PRIVATE ${FUZZ_INCLUDE_DIRS} ${PROBE_FUZZ_INCLUDE_DIRS}) + target_link_libraries(${name} openscap) + target_compile_options(${name} PRIVATE -fsanitize=fuzzer -ffunction-sections -fdata-sections) + target_link_options(${name} PRIVATE -fsanitize=fuzzer -Wl,--gc-sections) + endfunction() + + add_fuzzer_probe(xinetd_probe_fuzzer xinetd_probe_fuzzer.c) # xinetd config parser + add_fuzzer_probe(routingtable_probe_fuzzer routingtable_probe_fuzzer.c + "${CMAKE_SOURCE_DIR}/src/OVAL/probes/SEAP/generic/strto.c") # /proc/net/route line parser + add_fuzzer_probe(shadow_probe_fuzzer shadow_probe_fuzzer.c) # /etc/shadow hash-method parser + list(APPEND FUZZ_TARGETS xinetd_probe_fuzzer routingtable_probe_fuzzer shadow_probe_fuzzer) + + # Some probe parsers (textfilecontent54's process_file, inetlisteningservers' + # read_tcp) call non-exported probe helpers directly -- probe_entobj_cmp (-> + # the OVAL comparison code), the item cache, oval_fts -- which --gc-sections + # cannot drop because the code we drive uses them. The shared library hides + # those symbols (C_VISIBILITY_PRESET hidden), so instead of linking the .so + # we link the library's *object* files (visibility only affects the dynamic + # symbol table, not static linking) and link the openscap target purely to + # inherit its external dependencies (libxml2, pcre2, ...). Mirrors the + # OBJECTS_TO_LINK_AGAINST list in src/CMakeLists.txt. + set(PROBE_FUZZ_FULL_OBJECTS + $ + $ + $ + $ + $ + $ + $ + $ + $ + $ + $ + $ + $ + $ + $ + ) + foreach(opt_obj compat_object yamlfilter_object crapi_object linux_probes_object) + if(TARGET ${opt_obj}) + list(APPEND PROBE_FUZZ_FULL_OBJECTS $) + endif() + endforeach() + + # add_fuzzer_probe_full( [extra sources...]) for harnesses that + # need the library's non-exported internals: links the composing object files + # (all symbols available at static-link time) plus the openscap target for its + # external link interface. No --gc-sections here -- all referenced symbols are + # present, so nothing needs to be pruned. + function(add_fuzzer_probe_full name source) + add_executable(${name} ${source} ${PROBE_FUZZ_FULL_OBJECTS} ${ARGN}) + target_include_directories(${name} PRIVATE ${FUZZ_INCLUDE_DIRS} ${PROBE_FUZZ_INCLUDE_DIRS}) + # Link the external deps openscap pulls in (libxml2, pcre2, ...), but NOT + # the openscap shared object itself: linking the .so on top of the same + # object files would define every global twice (an ASan ODR violation). + target_link_libraries(${name} $) + target_compile_options(${name} PRIVATE -fsanitize=fuzzer) + target_link_options(${name} PRIVATE -fsanitize=fuzzer) + endfunction() + + add_fuzzer_probe_full(textfilecontent54_probe_fuzzer textfilecontent54_probe_fuzzer.c) # text file + PCRE reader (whole-file) + add_fuzzer_probe_full(textfilecontent_probe_fuzzer textfilecontent_probe_fuzzer.c) # legacy text file + PCRE reader (per-line) + list(APPEND FUZZ_TARGETS textfilecontent54_probe_fuzzer textfilecontent_probe_fuzzer) + if(ENABLE_PROBES_LINUX) + add_fuzzer_probe_full(inetlisteningservers_probe_fuzzer inetlisteningservers_probe_fuzzer.c) # /proc/net/{tcp,udp} parser + add_fuzzer_probe_full(iflisteners_probe_fuzzer iflisteners_probe_fuzzer.c) # /proc/net/packet parser + list(APPEND FUZZ_TARGETS inetlisteningservers_probe_fuzzer iflisteners_probe_fuzzer) + endif() +endif() + +# Convenience target to build them all: `cmake --build . --target fuzzers` +add_custom_target(fuzzers DEPENDS ${FUZZ_TARGETS}) diff --git a/fuzz/README.md b/fuzz/README.md new file mode 100644 index 0000000000..a429bdf9a9 --- /dev/null +++ b/fuzz/README.md @@ -0,0 +1,206 @@ +# OpenSCAP fuzzers + +[libFuzzer](https://llvm.org/docs/LibFuzzer.html) harnesses that exercise the +SCAP file processing code paths (parse / resolve / validate). Requires a Clang +toolchain (libFuzzer ships with Clang). + +## Available harnesses + +| Binary | Entry point | Corpus dir | +|--------|-------------|------------| +| `scap_parse_fuzzer` | `oscap_source_get_scap_type()` then the matching importer (DS, ARF, XCCDF, all OVAL kinds, CPE) | `corpus/` | +| `xccdf_policy_fuzzer` | `xccdf_policy_model_new()` + `build_all_useful_policies()` + `xccdf_policy_resolve()` | `corpus_xccdf/` | +| `validate_fuzzer` | `oscap_source_validate()` + `oscap_source_validate_schematron()` | `corpus/` | +| `arf_fuzzer` | `ds_rds_session_*` — build the RDS index, walk reports/assets, extract reports | `corpus_arf/` | +| `xccdf_tailoring_fuzzer`| `xccdf_tailoring_import_source()` against an embedded benchmark | `corpus_tailoring/` | + +The harnesses above fuzz **SCAP XML documents** (the config a scanner consumes). +The harnesses below instead fuzz the **data an OVAL probe parses off the scanned +host's filesystem** — see [Probe-content fuzzing](#probe-content-fuzzing-scanned-filesystem). +They are built only when `-DENABLE_PROBES=ON`. + +| Binary | Entry point (probe parser) | Corpus dir | +|--------|----------------------------|------------| +| `xinetd_probe_fuzzer` | `xiconf_parse()` — the xinetd config-file parser | `corpus_probe_xinetd/` | +| `routingtable_probe_fuzzer` | `process_line_ip4()` / `process_line_ip6()` — `/proc/net/route` line parsers | `corpus_probe_routingtable/` | +| `shadow_probe_fuzzer` | `parse_enc_mth()` — `/etc/shadow` hash-method classifier | `corpus_probe_shadow/` | +| `textfilecontent54_probe_fuzzer` | `process_file()` — file read + PCRE match loop (whole-file) | `corpus_probe_textfilecontent54/` | +| `textfilecontent_probe_fuzzer` | legacy `process_file()` — file read + PCRE match loop (per-line) | `corpus_probe_textfilecontent/` | +| `inetlisteningservers_probe_fuzzer` | `read_tcp()`/`read_udp()`/`read_raw()` — `/proc/net/{tcp,udp,raw}` parsers | `corpus_probe_inetlisteningservers/` | +| `iflisteners_probe_fuzzer` | `read_packet()` — `/proc/net/packet` parser | `corpus_probe_iflisteners/` | + +Each harness is one `*_fuzzer.c` file in this directory. Corpora are seeded from +`tests/` and grown by the fuzzer; they are git-ignored (regenerable). + +## Build + +```sh +mkdir -p build && cd build +CC=clang CXX=clang++ cmake .. -DENABLE_FUZZING=ON -DENABLE_PROBES=OFF -DENABLE_SCE=OFF +cmake --build . --target fuzzers -j"$(nproc)" # builds all harnesses +``` + +`ENABLE_FUZZING` instruments the whole library with +`-fsanitize=fuzzer-no-link,address,undefined` and links each harness with +`-fsanitize=fuzzer`. (`-DENABLE_PROBES=OFF -DENABLE_SCE=OFF` just trims the build.) + +## Probe-content fuzzing (scanned filesystem) + +The XML harnesses fuzz the SCAP config, which on a real deployment is +static/trusted. The genuinely attacker-influenced input on a *scanned* host is +the data the OVAL probes read off that host's filesystem (`/etc/xinetd.d/*`, +`/proc/net/route`, `/etc/shadow`, text files, …). The `*_probe_fuzzer` harnesses +feed arbitrary bytes to those probe parsers. + +These need the probe code compiled in, so use a **separate build** with probes +enabled (keep the XML-config `build/` as-is): + +```sh +mkdir -p build-probe && cd build-probe +CC=clang CXX=clang++ cmake .. -DENABLE_FUZZING=ON -DENABLE_PROBES=ON -DENABLE_SCE=OFF +cmake --build . --target fuzzers -j"$(nproc)" +``` + +How they reach the parsers: each harness `#include`s the probe's `.c` to call +its `static` parser directly (the same trick `tests/probes/xinetd/` uses). The +probe's SEAP entry point (`*_probe_main`) is excluded or renamed so it does not +clash with the library or drag in the probe runtime, and `--gc-sections` drops +it. Bytes are fed via a tmpfs temp file (path parsers) or an in-memory buffer +(line/string parsers); see `probe_fuzz_common.h`. + +Leak detection is meaningful here (the harnesses free per iteration), so run +these with `ASAN_OPTIONS=detect_leaks=1`: + +```sh +cd build-probe +ASAN_OPTIONS=detect_leaks=1 UBSAN_OPTIONS=halt_on_error=0 \ + ./fuzz/xinetd_probe_fuzzer -max_len=65536 ../fuzz/corpus_probe_xinetd +``` + +Two linking styles are used (both in `fuzz/CMakeLists.txt`): + +- `add_fuzzer_probe()` — for parsers reachable with only exported symbols plus a + couple of self-contained helper sources. It links the shared library and uses + `--gc-sections` to drop the probe's `*_probe_main` (and the static helpers only + it reaches), which would otherwise pull in non-exported probe-runtime symbols. + Used by xinetd, routingtable, shadow. +- `add_fuzzer_probe_full()` — for parsers that themselves call non-exported + helpers (`textfilecontent54`'s `process_file` → `probe_entobj_cmp` → the OVAL + comparison code + item cache; `inetlisteningservers`'s `read_tcp`). The library + hides those symbols (`C_VISIBILITY_PRESET hidden`), so instead of the `.so` we + link the library's **object files** (visibility only affects the dynamic symbol + table, not static linking) and pull in just `openscap`'s external link + interface (libxml2, pcre2, …) via its `LINK_LIBRARIES` property. Linking the + `.so` *on top of* the objects would define every global twice (an ASan + ODR violation), so it is deliberately not linked. + +`passwd` is skipped on glibc: its custom `oscap_fgetpwent()` parser is +`#ifdef`'d out in favour of libc's `fgetpwent()`. + +## Run the fuzz tests + +Recommended sanitizer environment (LeakSanitizer is noisy on inputs the parser +intentionally rejects mid-parse; UBSan `halt_on_error=0` keeps benign +function-pointer-cast reports from aborting): + +```sh +export ASAN_OPTIONS=detect_leaks=0 UBSAN_OPTIONS=halt_on_error=0 +``` + +One harness on its corpus: + +```sh +cd build +./fuzz/scap_parse_fuzzer -max_len=65536 ../fuzz/corpus +./fuzz/xccdf_policy_fuzzer -max_len=65536 ../fuzz/corpus_xccdf +# validate_fuzzer needs the bundled schemas: +OSCAP_SCHEMA_PATH=$(pwd)/../schemas ./fuzz/validate_fuzzer -max_len=65536 ../fuzz/corpus +``` + +All harnesses in parallel (libFuzzer `-fork` mode; a crash/OOM/timeout in one +input is recorded and fuzzing continues). `run-all.sh` sets the sanitizer +options and `OSCAP_SCHEMA_PATH` automatically: + +```sh +fuzz/run-all.sh 3600 # duration in seconds; one fork child per harness +FORK=4 fuzz/run-all.sh 28800 # 4 fork children per harness +``` + +Findings land in `fuzz/findings//` (`crash-`/`oom-`/`timeout-`/`leak-`), +per-harness logs in `fuzz/logs/.log`; a per-harness summary is printed +at the end. Both dirs are git-ignored. + +## Coverage + +Build a second, coverage-instrumented tree, replay the corpus, and report with +`llvm-cov`: + +```sh +mkdir -p build-cov && cd build-cov +CC=clang CXX=clang++ cmake .. -DENABLE_FUZZING=ON -DENABLE_PROBES=OFF -DENABLE_SCE=OFF \ + -DCMAKE_C_FLAGS="-fprofile-instr-generate -fcoverage-mapping" \ + -DCMAKE_CXX_FLAGS="-fprofile-instr-generate -fcoverage-mapping" \ + -DCMAKE_EXE_LINKER_FLAGS="-fprofile-instr-generate -fcoverage-mapping" \ + -DCMAKE_SHARED_LINKER_FLAGS="-fprofile-instr-generate -fcoverage-mapping" +cmake --build . --target fuzzers -j"$(nproc)" + +# Replay the corpus (-runs=0 just executes the inputs, no fuzzing): +LLVM_PROFILE_FILE=cov.profraw ASAN_OPTIONS=detect_leaks=0 \ + ./fuzz/scap_parse_fuzzer -runs=0 ../fuzz/corpus + +llvm-profdata merge -sparse cov.profraw -o cov.profdata +# The library lives in a shared object, so pass it with -object: +llvm-cov report ./fuzz/scap_parse_fuzzer -object ./src/libopenscap.so* \ + -instr-profile=cov.profdata +# Per-file/line detail: +llvm-cov show ./fuzz/scap_parse_fuzzer -object ./src/libopenscap.so* \ + -instr-profile=cov.profdata src/OVAL/oval_parser.c +``` + +Merge several `*.profraw` (one per harness, via different `LLVM_PROFILE_FILE`) +before `report` to get combined coverage, and pass each harness with its own +`-object` to `llvm-cov`. + +## Replay / debug a crash + +A crashing input is written as `crash-` (or `oom-`/`timeout-`) in the +working dir, or under `fuzz/findings//` when using `run-all.sh`. +Curated regression inputs are in `fuzz/reproducers/`. + +Replay one input through the harness that produced it — the ASan report +(stack trace, fault address, allocation site) prints to stderr: + +```sh +cd build +ASAN_OPTIONS=detect_leaks=0 UBSAN_OPTIONS=halt_on_error=0 \ + ./fuzz/scap_parse_fuzzer ./crash- +# validate_fuzzer also needs: OSCAP_SCHEMA_PATH=$(pwd)/../schemas +``` + +> **Note:** a small number of reproducers (`crash-oval-set-mixed-type-double-free` +> and `crash-sds-index-checklist-null-strcmp`) trigger a UBSan +> wrong-function-pointer error rather than an ASan SEGV. Use +> `UBSAN_OPTIONS=halt_on_error=1` instead of `halt_on_error=0` for those. + +Under a debugger — make ASan/UBSan abort into the debugger on the faulting frame: + +```sh +ASAN_OPTIONS=abort_on_error=1:detect_leaks=0 UBSAN_OPTIONS=halt_on_error=1 \ + gdb --args ./fuzz/scap_parse_fuzzer ./crash- +(gdb) run +(gdb) bt # backtrace at the crash +# (lldb works the same: lldb -- ./fuzz/ ./crash-; run; bt) +``` + +Useful extras: +- Symbolized ASan traces need `llvm-symbolizer` on `PATH` (set + `ASAN_SYMBOLIZER_PATH=$(command -v llvm-symbolizer)` if needed). +- Minimize a crash to the smallest triggering input: + `./fuzz/ -minimize_crash=1 -exact_artifact_path=min ./crash-`. +- Replay all regression inputs (run from `build/`): + ```sh + for f in ../fuzz/reproducers/*; do + ASAN_OPTIONS=detect_leaks=0 UBSAN_OPTIONS=halt_on_error=0 \ + ./fuzz/scap_parse_fuzzer "$f" >/dev/null 2>&1 || echo "triggered: $f" + done + ``` diff --git a/fuzz/arf_fuzzer.c b/fuzz/arf_fuzzer.c new file mode 100644 index 0000000000..19a0c29ce5 --- /dev/null +++ b/fuzz/arf_fuzzer.c @@ -0,0 +1,81 @@ +/* + * libFuzzer harness for ARF / Result Data Stream (RDS) parsing + * (src/DS/rds.c, src/DS/rds_index.c, src/DS/ds_rds_session.c). + * + * ARF (Asset Reporting Format) result files are the output side of a scan and + * are routinely passed around and re-ingested, so their parser is a real attack + * surface. The base scap_parse_fuzzer only builds the RDS session; this harness + * goes further and walks the result-data-stream index (reports, assets, + * report-requests) and extracts the embedded reports, which is what drives the + * bulk of the RDS parsing code. + * + * Pipeline: + * ds_rds_session_new_from_source() open the ARF + * ds_rds_session_get_rds_idx() build & return the RDS index + * walk reports / assets / report-requests via the index iterators + * ds_rds_session_select_report(NULL) extract+parse the first report + * ds_rds_session_select_report_request(NULL) + */ + +#include +#include + +#include "fuzz_common.h" +#include "oscap_source.h" +#include "scap_ds.h" +#include "ds_rds_session.h" + +static void walk_index(struct rds_index *idx) +{ + if (idx == NULL) { + return; + } + + struct rds_report_index_iterator *rit = rds_index_get_reports(idx); + while (rds_report_index_iterator_has_more(rit)) { + struct rds_report_index *r = rds_report_index_iterator_next(rit); + rds_report_index_get_id(r); + } + rds_report_index_iterator_free(rit); + + struct rds_report_request_index_iterator *qit = rds_index_get_report_requests(idx); + while (rds_report_request_index_iterator_has_more(qit)) { + struct rds_report_request_index *q = rds_report_request_index_iterator_next(qit); + rds_report_request_index_get_id(q); + } + rds_report_request_index_iterator_free(qit); + + struct rds_asset_index_iterator *ait = rds_index_get_assets(idx); + while (rds_asset_index_iterator_has_more(ait)) { + struct rds_asset_index *a = rds_asset_index_iterator_next(ait); + struct rds_report_index_iterator *arit = rds_asset_index_get_reports(a); + while (rds_report_index_iterator_has_more(arit)) { + rds_report_index_iterator_next(arit); + } + rds_report_index_iterator_free(arit); + } + rds_asset_index_iterator_free(ait); +} + +int LLVMFuzzerTestOneInput(const uint8_t *data, size_t size) +{ + FUZZ_INIT(); + + struct oscap_source *source = + oscap_source_new_from_memory((const char *)data, size, "fuzz-arf.xml"); + if (source == NULL) { + return 0; + } + + struct ds_rds_session *session = ds_rds_session_new_from_source(source); + if (session != NULL) { + walk_index(ds_rds_session_get_rds_idx(session)); + // Returned sources are owned by the session; do not free them. + ds_rds_session_select_report(session, NULL); + ds_rds_session_select_report_request(session, NULL); + ds_rds_session_free(session); + } + + oscap_source_free(source); + return 0; +} diff --git a/fuzz/cov_driver.c b/fuzz/cov_driver.c new file mode 100644 index 0000000000..75c5e26d70 --- /dev/null +++ b/fuzz/cov_driver.c @@ -0,0 +1,45 @@ +/* + * Corpus-replay driver for coverage measurement. + * + * Designed to be invoked once per corpus file via xargs -P so crashes in + * one input don't abort the whole run. Each invocation runs exactly one + * input, writes its own profraw, and exits. Merge everything afterwards: + * + * ls fuzz/corpus | xargs -P8 -I{} sh -c \ + * "LLVM_PROFILE_FILE=build-cov/profiles/{}.profraw \ + * ASAN_OPTIONS=detect_leaks=0 \ + * build-cov/scap_parse_cov fuzz/corpus/{} 2>/dev/null || true" + * llvm-profdata merge -sparse build-cov/profiles/*.profraw -o combined.profdata + */ +#include +#include +#include + +int LLVMFuzzerTestOneInput(const unsigned char *data, size_t size); + +int main(int argc, char **argv) +{ + if (argc < 2) + return 0; + + FILE *f = fopen(argv[1], "rb"); + if (!f) + return 1; + fseek(f, 0, SEEK_END); + long sz = ftell(f); + rewind(f); + if (sz <= 0) { + fclose(f); + return 0; + } + unsigned char *buf = malloc((size_t)sz); + if (!buf) { + fclose(f); + return 1; + } + fread(buf, 1, (size_t)sz, f); + fclose(f); + LLVMFuzzerTestOneInput(buf, (size_t)sz); + free(buf); + return 0; +} diff --git a/fuzz/fuzz_common.h b/fuzz/fuzz_common.h new file mode 100644 index 0000000000..4cd8bcefe4 --- /dev/null +++ b/fuzz/fuzz_common.h @@ -0,0 +1,34 @@ +/* + * Shared setup for the OpenSCAP fuzz harnesses. + */ +#ifndef OPENSCAP_FUZZ_COMMON_H +#define OPENSCAP_FUZZ_COMMON_H + +#include +#include + +#include "oscap.h" + +/* + * One-time process initialization. Silences libxml2's error reporting (it would + * otherwise print a parse error to stderr for every malformed input, which both + * slows fuzzing down and buries real sanitizer reports) and initializes the + * library. Call from the top of LLVMFuzzerTestOneInput guarded by a static flag. + */ +static inline void fuzz_init_once(void) +{ + xmlSetGenericErrorFunc(NULL, NULL); + xmlSetStructuredErrorFunc(NULL, NULL); + oscap_init(); +} + +#define FUZZ_INIT() \ + do { \ + static int _fuzz_inited = 0; \ + if (!_fuzz_inited) { \ + fuzz_init_once(); \ + _fuzz_inited = 1; \ + } \ + } while (0) + +#endif /* OPENSCAP_FUZZ_COMMON_H */ diff --git a/fuzz/iflisteners_probe_fuzzer.c b/fuzz/iflisteners_probe_fuzzer.c new file mode 100644 index 0000000000..cd8470e037 --- /dev/null +++ b/fuzz/iflisteners_probe_fuzzer.c @@ -0,0 +1,84 @@ +/* + * libFuzzer harness for the OVAL iflisteners probe's /proc/net/packet parser. + * + * read_packet() reads /proc/net/packet and sscanf()s each line (packet-socket + * table from net/packet/af_packet.c). That procfs content comes off the + * scanned host, so we fuzz the line parsing directly. + * + * read_packet() hard-coded the "/proc/net/packet" path; it was refactored to + * take the path as a parameter (mirroring read_tcp() in the inetlisteningservers + * probe), so the harness can point it at a tmpfs file holding the fuzz bytes. + * + * read_packet() is static, so we #include the probe .c and build with + * add_fuzzer_probe_full() (it reaches non-exported helpers: probe_entobj_cmp, + * the item cache, oscap_enum_to_string). The non-static iflisteners_probe_main + * is renamed to avoid clashing with the copy in linux_probes_object. + * + * We skip collect_process_info() (it walks the real /proc and would make the + * run non-deterministic) and pass an empty inode list. The per-line sscanf + * parsing runs regardless of the list; the inode-matched enrichment path + * (get_interface/report_finding) is simply not taken. + * + * Build with -DENABLE_FUZZING=ON -DENABLE_PROBES=ON. + */ + +#include +#include +#include +#include + +#include "probe_fuzz_common.h" +#include "probe/probe.h" + +#define iflisteners_probe_main fuzz_ifl_probe_main +#include "../src/OVAL/probes/unix/linux/iflisteners_probe.c" +#undef iflisteners_probe_main + +static SEXP_t *g_ifname_ent; +static int g_ready; + +static void ifl_setup(void) +{ + SEXP_t *val = SEXP_string_newf("any"); + SEXP_t *obj = probe_obj_creat("iflisteners_object", NULL, + "interface_name", NULL, val, + NULL); + g_ifname_ent = probe_obj_getent(obj, "interface_name", 1); + SEXP_free(val); + SEXP_free(obj); + g_ready = (g_ifname_ent != NULL); +} + +int LLVMFuzzerTestOneInput(const uint8_t *data, size_t size) +{ + PROBE_FUZZ_INIT(); + if (!g_ready) { + ifl_setup(); + if (!g_ready) + return 0; + } + + const char *path = probe_fuzz_write_tmpfile(data, size); + if (path == NULL) + return 0; + + /* No item cache (ctx.icache == NULL): probe_item_collect() frees the item + * and returns -1, so we exercise the full parse without the icache worker + * thread (see textfilecontent54_probe_fuzzer.c). */ + probe_ctx ctx; + memset(&ctx, 0, sizeof(ctx)); + ctx.probe_out = probe_cobj_new(SYSCHAR_FLAG_UNKNOWN, NULL, NULL, NULL); + ctx.icache = NULL; + ctx.max_mem_ratio = OSCAP_PROBE_MEMORY_USAGE_RATIO_DEFAULT; + ctx.max_collected_items = OSCAP_PROBE_COLLECT_UNLIMITED; + + llist ll; + list_create(&ll); + + oval_schema_version_t over = OVAL_SCHEMA_VERSION(5.11); + read_packet(path, &ll, &ctx, over, g_ifname_ent); + + list_clear(&ll); + SEXP_free(ctx.probe_out); + return 0; +} diff --git a/fuzz/inetlisteningservers_probe_fuzzer.c b/fuzz/inetlisteningservers_probe_fuzzer.c new file mode 100644 index 0000000000..20515cb029 --- /dev/null +++ b/fuzz/inetlisteningservers_probe_fuzzer.c @@ -0,0 +1,102 @@ +/* + * libFuzzer harness for the OVAL inetlisteningservers probe's /proc/net parsers. + * + * read_tcp/read_udp/read_raw read /proc/net/{tcp,tcp6,udp,udp6,raw,raw6} and + * sscanf() each line into fields, then hex-decode the addresses (addr_convert). + * That procfs content comes from the scanned host, so we fuzz the line parsing + * directly. + * + * These functions are static, so we #include the probe .c. Because this harness + * is linked with the library's object files (add_fuzzer_probe_full), the .c's + * non-static SEAP entry points would clash with the copies in + * linux_probes_object; we rename them to harmless static, unused functions. + * + * read_*() take a path and open it, so the fuzz bytes are written to a tmpfs + * temp file. We skip collect_process_info() (it walks the real /proc and would + * make the run non-deterministic) and drive the parsers with an empty process + * list -- the line parsing/decoding we care about runs regardless. + * + * Build with -DENABLE_FUZZING=ON -DENABLE_PROBES=ON. + */ + +#include +#include +#include + +#include "probe_fuzz_common.h" + +/* struct probe_ctx, probe_icache_t / probe_icache_*, OSCAP_PROBE_* defaults -- + * inetlisteningservers_probe.c itself only pulls in the public probe-api.h. */ +#include "probe/probe.h" + +#define inetlisteningservers_probe_main fuzz_ils_probe_main +#define inetlisteningservers_probe_offline_mode_supported fuzz_ils_offline_mode_supported +#include "../src/OVAL/probes/unix/linux/inetlisteningservers_probe.c" +#undef inetlisteningservers_probe_main +#undef inetlisteningservers_probe_offline_mode_supported + +static struct server_info g_req; +static int g_ready; + +static void ils_setup(void) +{ + /* Build the three entities read_*() consult (protocol / local_address / + * local_port). Their exact values only affect the match filter, not the + * line parsing we are exercising. */ + SEXP_t *vproto = SEXP_string_newf("tcp"); + SEXP_t *vaddr = SEXP_string_newf("0.0.0.0"); + SEXP_t *vport = SEXP_number_newi_32(0); + SEXP_t *obj = probe_obj_creat("inetlisteningservers_object", NULL, + "protocol", NULL, vproto, + "local_address", NULL, vaddr, + "local_port", NULL, vport, + NULL); + g_req.protocol_ent = probe_obj_getent(obj, "protocol", 1); + g_req.local_address_ent = probe_obj_getent(obj, "local_address", 1); + g_req.local_port_ent = probe_obj_getent(obj, "local_port", 1); + SEXP_free(vproto); + SEXP_free(vaddr); + SEXP_free(vport); + SEXP_free(obj); + + g_ready = (g_req.protocol_ent != NULL && + g_req.local_address_ent != NULL && g_req.local_port_ent != NULL); +} + +int LLVMFuzzerTestOneInput(const uint8_t *data, size_t size) +{ + PROBE_FUZZ_INIT(); + if (!g_ready) { + ils_setup(); + if (!g_ready) + return 0; + } + + const char *path = probe_fuzz_write_tmpfile(data, size); + if (path == NULL) + return 0; + + /* No item cache (ctx.icache == NULL): probe_item_collect() frees the item + * and returns -1, so we exercise the full parse without the icache worker + * thread (see textfilecontent54_probe_fuzzer.c). */ + probe_ctx ctx; + memset(&ctx, 0, sizeof(ctx)); + ctx.probe_out = probe_cobj_new(SYSCHAR_FLAG_UNKNOWN, NULL, NULL, NULL); + ctx.icache = NULL; + ctx.max_mem_ratio = OSCAP_PROBE_MEMORY_USAGE_RATIO_DEFAULT; + ctx.max_collected_items = OSCAP_PROBE_COLLECT_UNLIMITED; + + /* Empty process list: we skip collect_process_info()'s real /proc walk; + * the inode lookups simply miss and report_finding() runs without a pid. */ + llist ll; + list_create(&ll); + + /* Feed the same fuzz file through each parser (their sscanf formats differ). */ + read_tcp(path, "tcp", &ll, &ctx, &g_req); + read_udp(path, "udp", &ll, &ctx, &g_req); + read_raw(path, "udp", &ll, &ctx, &g_req); + + list_clear(&ll); + SEXP_free(ctx.probe_out); + return 0; +} diff --git a/fuzz/probe_fuzz_common.h b/fuzz/probe_fuzz_common.h new file mode 100644 index 0000000000..76c36a6128 --- /dev/null +++ b/fuzz/probe_fuzz_common.h @@ -0,0 +1,108 @@ +/* + * Shared setup for the OpenSCAP OVAL *probe* fuzz harnesses. + * + * These harnesses fuzz the data an OVAL probe parses out of the *scanned + * filesystem* (e.g. /etc/passwd, xinetd config files, /proc/net/tcp, a text + * file matched by a regex) rather than a SCAP XML document. They reach the + * probe's parsing code by #include-ing the probe's .c file directly (the same + * trick tests/probes/xinetd/test_probe_xinetd.c uses) and calling the static + * parser, so they must be built with -DENABLE_PROBES=ON. + * + * This header provides: + * - PROBE_FUZZ_INIT(): one-time process init (silence libxml2, oscap_init()). + * - a reusable tmpfs-backed temp file for parsers that insist on a path. + * - a minimal probe_ctx + icache for parsers that emit via probe_item_collect. + * + * NOTE: fmemopen() and the /dev/shm temp file are glibc/Linux specific. That is + * fine: the probes targeted here are Linux/UNIX probes and the fuzzing build + * runs on Linux. + */ +#ifndef OPENSCAP_PROBE_FUZZ_COMMON_H +#define OPENSCAP_PROBE_FUZZ_COMMON_H + +#include +#include +#include +#include +#include +#include +#include +#include + +#include +#include + +#include "oscap.h" + +/* one-time process initialization (mirrors fuzz/fuzz_common.h) */ +static inline void probe_fuzz_init_once(void) +{ + xmlSetGenericErrorFunc(NULL, NULL); + xmlSetStructuredErrorFunc(NULL, NULL); + oscap_init(); +} + +#define PROBE_FUZZ_INIT() \ + do { \ + static int _pf_inited = 0; \ + if (!_pf_inited) { \ + probe_fuzz_init_once(); \ + _pf_inited = 1; \ + } \ + } while (0) + +/* + * A single tmpfs-backed file reused across iterations. Probes such as xinetd, + * inetlisteningservers and textfilecontent54 open a *path* and read it + * themselves, so the fuzz bytes have to live in a real file. Reusing one file + * (truncate + rewrite each iteration) avoids per-iteration mkstemp()/unlink() + * churn that would otherwise dominate runtime and exhaust inodes. + * + * The path includes the PID so that libFuzzer's -fork children (which all run + * the same binary) do not race on a single shared file and corrupt each + * other's input. + */ +#define PROBE_FUZZ_TMPDIR "/dev/shm" + +/* Returns a process-unique temp file path (stable for the life of the process). */ +static inline const char *probe_fuzz_tmppath(void) +{ + static char path[64]; + if (path[0] == '\0') + snprintf(path, sizeof(path), "%s/oscap_probe_fuzz_input.%ld", + PROBE_FUZZ_TMPDIR, (long)getpid()); + return path; +} + +/* Write the fuzz bytes to the reusable temp file; return its path or NULL. */ +static inline const char *probe_fuzz_write_tmpfile(const uint8_t *data, size_t size) +{ + const char *path = probe_fuzz_tmppath(); + int fd = open(path, O_WRONLY | O_CREAT | O_TRUNC, 0600); + if (fd == -1) + return NULL; + size_t off = 0; + while (off < size) { + ssize_t w = write(fd, data + off, size - off); + if (w <= 0) { + close(fd); + return NULL; + } + off += (size_t)w; + } + close(fd); + return path; +} + +/* NUL-terminated heap copy of the fuzz input, for buffer/line parsers. */ +static inline char *probe_fuzz_cstr(const uint8_t *data, size_t size) +{ + char *s = malloc(size + 1); + if (s == NULL) + return NULL; + memcpy(s, data, size); + s[size] = '\0'; + return s; +} + +#endif /* OPENSCAP_PROBE_FUZZ_COMMON_H */ diff --git a/fuzz/reproducers/crash-oval-set-mixed-type-double-free b/fuzz/reproducers/crash-oval-set-mixed-type-double-free new file mode 100644 index 0000000000..91253777b8 --- /dev/null +++ b/fuzz/reproducers/crash-oval-set-mixed-type-double-free @@ -0,0 +1 @@ +o:1 \ No newline at end of file diff --git a/fuzz/reproducers/crash-oval-state-version-null-atoi b/fuzz/reproducers/crash-oval-state-version-null-atoi new file mode 100644 index 0000000000..511b5023c1 --- /dev/null +++ b/fuzz/reproducers/crash-oval-state-version-null-atoi @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/fuzz/reproducers/crash-oval-varmodel-duplicate-id-null-frame b/fuzz/reproducers/crash-oval-varmodel-duplicate-id-null-frame new file mode 100644 index 0000000000..190f25879c --- /dev/null +++ b/fuzz/reproducers/crash-oval-varmodel-duplicate-id-null-frame @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/fuzz/reproducers/crash-rds-asset-missing-id-strcmp b/fuzz/reproducers/crash-rds-asset-missing-id-strcmp new file mode 100644 index 0000000000..9f34612277 --- /dev/null +++ b/fuzz/reproducers/crash-rds-asset-missing-id-strcmp @@ -0,0 +1,7 @@ + + + + +a + + diff --git a/fuzz/reproducers/crash-rds-isabout-null-asset b/fuzz/reproducers/crash-rds-isabout-null-asset new file mode 100644 index 0000000000..f993a58d7a --- /dev/null +++ b/fuzz/reproducers/crash-rds-isabout-null-asset @@ -0,0 +1,7 @@ + + + + +b + + diff --git a/fuzz/reproducers/crash-rds-relationship-missing-type b/fuzz/reproducers/crash-rds-relationship-missing-type new file mode 100644 index 0000000000..1f88fb0337 --- /dev/null +++ b/fuzz/reproducers/crash-rds-relationship-missing-type @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/fuzz/reproducers/crash-rds-report-missing-id-htable b/fuzz/reproducers/crash-rds-report-missing-id-htable new file mode 100644 index 0000000000..de9095882f --- /dev/null +++ b/fuzz/reproducers/crash-rds-report-missing-id-htable @@ -0,0 +1,3 @@ + + + diff --git a/fuzz/reproducers/crash-rds-select-report-null-index b/fuzz/reproducers/crash-rds-select-report-null-index new file mode 100644 index 0000000000..5cd0969adb --- /dev/null +++ b/fuzz/reproducers/crash-rds-select-report-null-index @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/fuzz/reproducers/crash-schematron-table-no-sentinel-oob b/fuzz/reproducers/crash-schematron-table-no-sentinel-oob new file mode 100644 index 0000000000..d3e39163e6 --- /dev/null +++ b/fuzz/reproducers/crash-schematron-table-no-sentinel-oob @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/fuzz/reproducers/crash-sds-catalog-cycle-recursion-oom b/fuzz/reproducers/crash-sds-catalog-cycle-recursion-oom new file mode 100644 index 0000000000..e4210fd06f --- /dev/null +++ b/fuzz/reproducers/crash-sds-catalog-cycle-recursion-oom @@ -0,0 +1,10 @@ + + + + + + + + + + diff --git a/fuzz/reproducers/crash-sds-component-id-use-after-free b/fuzz/reproducers/crash-sds-component-id-use-after-free new file mode 100644 index 0000000000..b7aab96684 --- /dev/null +++ b/fuzz/reproducers/crash-sds-component-id-use-after-free @@ -0,0 +1 @@ + diff --git a/fuzz/reproducers/crash-sds-component-missing-id-strcmp b/fuzz/reproducers/crash-sds-component-missing-id-strcmp new file mode 100644 index 0000000000..0013aed775 --- /dev/null +++ b/fuzz/reproducers/crash-sds-component-missing-id-strcmp @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/fuzz/reproducers/crash-sds-index-checklist-null-strcmp b/fuzz/reproducers/crash-sds-index-checklist-null-strcmp new file mode 100644 index 0000000000..6681f04185 --- /dev/null +++ b/fuzz/reproducers/crash-sds-index-checklist-null-strcmp @@ -0,0 +1,6 @@ + + + + + + diff --git a/fuzz/reproducers/crash-sds-index-select-null-index b/fuzz/reproducers/crash-sds-index-select-null-index new file mode 100644 index 0000000000..557d22a5cd --- /dev/null +++ b/fuzz/reproducers/crash-sds-index-select-null-index @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/fuzz/reproducers/crash-tailoring-malformed-profile-null b/fuzz/reproducers/crash-tailoring-malformed-profile-null new file mode 100644 index 0000000000..801848e209 --- /dev/null +++ b/fuzz/reproducers/crash-tailoring-malformed-profile-null @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/fuzz/reproducers/crash-xccdf-null-testresult-add b/fuzz/reproducers/crash-xccdf-null-testresult-add new file mode 100644 index 0000000000..214aacddc6 --- /dev/null +++ b/fuzz/reproducers/crash-xccdf-null-testresult-add @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/fuzz/reproducers/crash-xccdf-platform-missing-idref b/fuzz/reproducers/crash-xccdf-platform-missing-idref new file mode 100644 index 0000000000..469871fba2 --- /dev/null +++ b/fuzz/reproducers/crash-xccdf-platform-missing-idref @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/fuzz/reproducers/crash-xccdf-policy-cyclic-extends-via-creation b/fuzz/reproducers/crash-xccdf-policy-cyclic-extends-via-creation new file mode 100644 index 0000000000..2a192dd986 --- /dev/null +++ b/fuzz/reproducers/crash-xccdf-policy-cyclic-extends-via-creation @@ -0,0 +1 @@ +tt diff --git a/fuzz/reproducers/crash-xccdf-policy-cyclic-profile-extends b/fuzz/reproducers/crash-xccdf-policy-cyclic-profile-extends new file mode 100644 index 0000000000..97e5c69423 --- /dev/null +++ b/fuzz/reproducers/crash-xccdf-policy-cyclic-profile-extends @@ -0,0 +1 @@ + diff --git a/fuzz/reproducers/crash-xccdf-resolve-warning-list-oob b/fuzz/reproducers/crash-xccdf-resolve-warning-list-oob new file mode 100644 index 0000000000..26827da51d --- /dev/null +++ b/fuzz/reproducers/crash-xccdf-resolve-warning-list-oob @@ -0,0 +1 @@ +ttw diff --git a/fuzz/reproducers/hang-cpe-generator-parse-eof b/fuzz/reproducers/hang-cpe-generator-parse-eof new file mode 100644 index 0000000000..a283e8452f --- /dev/null +++ b/fuzz/reproducers/hang-cpe-generator-parse-eof @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/fuzz/reproducers/hang-cpe-item-parse-eof b/fuzz/reproducers/hang-cpe-item-parse-eof new file mode 100644 index 0000000000..a283e8452f --- /dev/null +++ b/fuzz/reproducers/hang-cpe-item-parse-eof @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/fuzz/reproducers/hang-cpe-platform-parse-eof b/fuzz/reproducers/hang-cpe-platform-parse-eof new file mode 100644 index 0000000000..02b4edf3ba --- /dev/null +++ b/fuzz/reproducers/hang-cpe-platform-parse-eof @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/fuzz/reproducers/hang-cpe-testexpr-invalid-operator b/fuzz/reproducers/hang-cpe-testexpr-invalid-operator new file mode 100644 index 0000000000..73346189fc --- /dev/null +++ b/fuzz/reproducers/hang-cpe-testexpr-invalid-operator @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/fuzz/reproducers/hang-rds-index-parse-nonadvancing b/fuzz/reproducers/hang-rds-index-parse-nonadvancing new file mode 100644 index 0000000000..953efc6f90 --- /dev/null +++ b/fuzz/reproducers/hang-rds-index-parse-nonadvancing @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/fuzz/reproducers/hang-xccdf-profile-remarks-nonremark-child b/fuzz/reproducers/hang-xccdf-profile-remarks-nonremark-child new file mode 100644 index 0000000000..4c5f24a5fb --- /dev/null +++ b/fuzz/reproducers/hang-xccdf-profile-remarks-nonremark-child @@ -0,0 +1 @@ + diff --git a/fuzz/routingtable_probe_fuzzer.c b/fuzz/routingtable_probe_fuzzer.c new file mode 100644 index 0000000000..1a268acc6e --- /dev/null +++ b/fuzz/routingtable_probe_fuzzer.c @@ -0,0 +1,63 @@ +/* + * libFuzzer harness for the OVAL routingtable probe's line parsers. + * + * The routingtable probe parses the kernel's /proc/net/route and + * /proc/net/ipv6_route tables: each line is tokenised and the hex-encoded + * address/flag fields are decoded (process_line_ip4 / process_line_ip6, with + * hexstring2bin / proc_ip{4,6}_to_string). That input comes straight from the + * scanned host's procfs, so we fuzz it directly. + * + * The line parsers are static, so we #include the probe .c. Its SEAP entry + * point routingtable_probe_main (and the static collect_item it reaches, which + * calls the non-exported probe_entobj_cmp) is renamed to a static, unused + * function so --gc-sections drops it -- we only drive the line parsers, which + * need nothing beyond the strto_uint*_hex helpers compiled in alongside. + * + * Build with -DENABLE_FUZZING=ON -DENABLE_PROBES=ON. + */ + +#include +#include +#include +#include + +#include "probe_fuzz_common.h" + +#define routingtable_probe_main static __attribute__((unused)) fuzz_rt_probe_main +#include "../src/OVAL/probes/unix/routingtable_probe.c" +#undef routingtable_probe_main + +int LLVMFuzzerTestOneInput(const uint8_t *data, size_t size) +{ + PROBE_FUZZ_INIT(); + + char *buf = probe_fuzz_cstr(data, size); + if (buf == NULL) + return 0; + + /* Feed each line to both the IPv4 and IPv6 parsers. They tokenise the + * line in place (strtok_r), so hand each parser its own copy. route_info + * holds only fixed-size buffers and static-string flag pointers, so + * nothing it fills needs to be freed. */ + char *save = NULL; + for (char *line = strtok_r(buf, "\n", &save); line != NULL; + line = strtok_r(NULL, "\n", &save)) { + struct route_info rt; + + char *l4 = strdup(line); + if (l4 != NULL) { + memset(&rt, 0, sizeof(rt)); + process_line_ip4(l4, &rt); + free(l4); + } + char *l6 = strdup(line); + if (l6 != NULL) { + memset(&rt, 0, sizeof(rt)); + process_line_ip6(l6, &rt); + free(l6); + } + } + + free(buf); + return 0; +} diff --git a/fuzz/run-all.sh b/fuzz/run-all.sh new file mode 100755 index 0000000000..4285135f7b --- /dev/null +++ b/fuzz/run-all.sh @@ -0,0 +1,148 @@ +#!/usr/bin/env bash +# +# Drive all OpenSCAP libFuzzer harnesses in parallel for an extended run. +# +# Each harness fuzzes its own corpus, writes crash/oom/timeout artifacts under +# findings//, and logs to logs/.log. New interesting inputs +# are added back into the harness's corpus directory so progress carries over +# between runs. +# +# Usage: +# fuzz/run-all.sh [duration_seconds] +# +# Environment overrides: +# BUILD build directory containing fuzz/ (default: ./build) +# SCHEMAS OSCAP_SCHEMA_PATH for validate_fuzzer (default: ./schemas) +# FORK libFuzzer -fork child processes per harness (default: 1) +# MAXLEN -max_len (default: 65536) +# RSS -rss_limit_mb (default: 4096) +# UNITTMO -timeout (per-input, seconds) (default: 25) +# +# Runs in -fork mode so a crash/OOM/timeout in one input does not stop the run: +# libFuzzer recycles the child, records the artifact, and keeps fuzzing. This +# also bounds memory (children are restarted), which matters for validate_fuzzer +# whose libxml2/libxslt caches grow across inputs. +# +# Examples: +# fuzz/run-all.sh 3600 # one hour, one process each +# JOBS=4 fuzz/run-all.sh 28800 # 8h, 4 workers per harness +# +set -u + +# Resolve repo root from this script's location so it works from anywhere. +SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)" +ROOT="$(cd "${SCRIPT_DIR}/.." && pwd)" + +DURATION="${1:-3600}" +BUILD="${BUILD:-${ROOT}/build}" +SCHEMAS="${SCHEMAS:-${ROOT}/schemas}" +FORK="${FORK:-1}" +MAXLEN="${MAXLEN:-65536}" +RSS="${RSS:-4096}" +UNITTMO="${UNITTMO:-25}" + +FUZZ_DIR="${ROOT}/fuzz" +OUT="${FUZZ_DIR}/findings" +LOGS="${FUZZ_DIR}/logs" + +# harness corpus-dir +# +# The probe-content harnesses (*_probe_fuzzer) only exist when the build was +# configured with -DENABLE_PROBES=ON; they are skipped automatically below if +# the binary is absent (e.g. the XML-config build uses -DENABLE_PROBES=OFF). +HARNESSES=( + "scap_parse_fuzzer:corpus" + "xccdf_policy_fuzzer:corpus_xccdf" + "validate_fuzzer:corpus" + "arf_fuzzer:corpus_arf" + "xccdf_tailoring_fuzzer:corpus_tailoring" + "xinetd_probe_fuzzer:corpus_probe_xinetd" + "routingtable_probe_fuzzer:corpus_probe_routingtable" + "shadow_probe_fuzzer:corpus_probe_shadow" + "textfilecontent54_probe_fuzzer:corpus_probe_textfilecontent54" + "textfilecontent_probe_fuzzer:corpus_probe_textfilecontent" + "inetlisteningservers_probe_fuzzer:corpus_probe_inetlisteningservers" + "iflisteners_probe_fuzzer:corpus_probe_iflisteners" +) + +# Sanitizer runtime options shared by every harness: +# - detect_leaks=0: the parsers intentionally abandon allocations on rejected +# input; leak reports would drown out memory-safety crashes. +# - halt_on_error=0: the OVAL code has many benign function-pointer-cast UBSan +# reports that would otherwise abort the run. +export ASAN_OPTIONS="detect_leaks=0:abort_on_error=1:${ASAN_OPTIONS:-}" +export UBSAN_OPTIONS="halt_on_error=0:print_stacktrace=1:${UBSAN_OPTIONS:-}" + +PIDS=() +cleanup() { echo; echo "[run-all] stopping…"; kill "${PIDS[@]}" 2>/dev/null; return 0; } +trap cleanup INT TERM + +echo "[run-all] duration=${DURATION}s fork/harness=${FORK} build=${BUILD}" +mkdir -p "${LOGS}" + +for entry in "${HARNESSES[@]}"; do + name="${entry%%:*}" + corpus="${FUZZ_DIR}/${entry##*:}" + bin="${BUILD}/fuzz/${name}" + + if [[ ! -x "${bin}" ]]; then + echo "[run-all] SKIP ${name}: not built (run: cmake --build '${BUILD}' --target fuzzers)" + continue + fi + mkdir -p "${OUT}/${name}" "${corpus}" + + # validate_fuzzer needs the bundled schemas to reach the deep schema code. + schema_env=() + [[ "${name}" == "validate_fuzzer" ]] && schema_env=(env "OSCAP_SCHEMA_PATH=${SCHEMAS}") + + # -fork mode writes its own fuzz-.log files into the cwd, so give each + # harness its own working directory under logs/. + workdir="${LOGS}/${name}.work" + mkdir -p "${workdir}" + + echo "[run-all] launch ${name} (corpus: ${entry##*:})" + ( + cd "${workdir}" || exit 1 + "${schema_env[@]}" "${bin}" \ + -fork="${FORK}" \ + -ignore_crashes=1 -ignore_ooms=1 -ignore_timeouts=1 \ + -max_total_time="${DURATION}" \ + -max_len="${MAXLEN}" \ + -rss_limit_mb="${RSS}" \ + -timeout="${UNITTMO}" \ + -print_final_stats=1 \ + -artifact_prefix="${OUT}/${name}/" \ + "${corpus}" + ) > "${LOGS}/${name}.log" 2>&1 & + PIDS+=("$!") +done + +if [[ ${#PIDS[@]} -eq 0 ]]; then + echo "[run-all] nothing to run." + exit 1 +fi + +echo "[run-all] ${#PIDS[@]} harness(es) running; logs in ${LOGS}/" +wait "${PIDS[@]}" +trap - INT TERM + +echo +echo "[run-all] ===== summary =====" +total=0 +for entry in "${HARNESSES[@]}"; do + name="${entry%%:*}" + # crash-/oom-/leak-/timeout- artifacts indicate findings; ignore corpus units. + mapfile -t finds < <(find "${OUT}/${name}" -maxdepth 1 -type f \ + \( -name 'crash-*' -o -name 'oom-*' -o -name 'leak-*' -o -name 'timeout-*' \) 2>/dev/null) + n=${#finds[@]} + total=$((total + n)) + if [[ ${n} -gt 0 ]]; then + echo " ${name}: ${n} finding(s)" + for f in "${finds[@]}"; do echo " ${f}"; done + else + echo " ${name}: clean" + fi +done +echo "[run-all] total findings: ${total}" +echo "[run-all] reproduce with: (validate_fuzzer needs OSCAP_SCHEMA_PATH=${SCHEMAS})" +exit 0 diff --git a/fuzz/scap_parse_fuzzer.c b/fuzz/scap_parse_fuzzer.c new file mode 100644 index 0000000000..5b390530d2 --- /dev/null +++ b/fuzz/scap_parse_fuzzer.c @@ -0,0 +1,187 @@ +/* + * libFuzzer harness for OpenSCAP SCAP file parsing. + * + * This harness feeds arbitrary bytes into the OpenSCAP parsing pipeline the + * same way an application would when it loads a SCAP file from disk: + * + * 1. wrap the bytes in an oscap_source (the library's single entry point for + * "here is a SCAP document"), + * 2. let the library sniff the document type, and + * 3. dispatch to the matching importer (data stream, XCCDF, OVAL, CPE, ...). + * + * Every importer ultimately drives the XML reader and the type-specific + * deserialization code, which is where parser crashes / segfaults live. + * + * Build it with the project's ENABLE_FUZZING CMake option (see fuzz/README.md), + * which compiles the whole library with the libFuzzer + AddressSanitizer + * instrumentation and links this file with -fsanitize=fuzzer. + */ + +#include +#include + +#include "fuzz_common.h" +#include "oscap_source.h" +#include "scap_ds.h" +#include "ds_sds_session.h" +#include "ds_rds_session.h" +#include "xccdf_benchmark.h" +#include "cpe_dict.h" +#include "cpe_lang.h" +#include "oval_definitions.h" +#include "oval_variables.h" +#include "oval_system_characteristics.h" +#include "oval_results.h" +#include "oval_directives.h" + +/* Exercise the source data stream / result data stream code paths. */ +static void fuzz_datastream(struct oscap_source *source) +{ + struct ds_sds_session *session = ds_sds_session_new_from_source(source); + if (session != NULL) { + /* NULL ids -> let the session guess; this walks the index, + * the catalogue and extracts/parses the selected components. */ + ds_sds_session_select_checklist(session, NULL, NULL, NULL); + ds_sds_session_free(session); + } +} + +static void fuzz_arf(struct oscap_source *source) +{ + struct ds_rds_session *session = ds_rds_session_new_from_source(source); + if (session != NULL) { + ds_rds_session_get_rds_idx(session); + ds_rds_session_free(session); + } +} + +static void fuzz_xccdf(struct oscap_source *source) +{ + struct xccdf_benchmark *benchmark = xccdf_benchmark_import_source(source); + if (benchmark != NULL) { + xccdf_benchmark_free(benchmark); + } +} + +static void fuzz_oval_definitions(struct oscap_source *source) +{ + struct oval_definition_model *model = oval_definition_model_import_source(source); + if (model != NULL) { + oval_definition_model_free(model); + } +} + +static void fuzz_oval_variables(struct oscap_source *source) +{ + struct oval_variable_model *model = oval_variable_model_import_source(source); + if (model != NULL) { + oval_variable_model_free(model); + } +} + +static void fuzz_oval_syschar(struct oscap_source *source) +{ + struct oval_definition_model *defs = oval_definition_model_new(); + struct oval_syschar_model *model = oval_syschar_model_new(defs); + if (model != NULL) { + oval_syschar_model_import_source(model, source); + oval_syschar_model_free(model); + } + oval_definition_model_free(defs); +} + +static void fuzz_oval_results(struct oscap_source *source) +{ + struct oval_definition_model *defs = oval_definition_model_new(); + struct oval_results_model *model = oval_results_model_new(defs, NULL); + if (model != NULL) { + oval_results_model_import_source(model, source); + oval_results_model_free(model); + } + oval_definition_model_free(defs); +} + +static void fuzz_oval_directives(struct oscap_source *source) +{ + struct oval_directives_model *model = oval_directives_model_new(); + if (model != NULL) { + oval_directives_model_import_source(model, source); + oval_directives_model_free(model); + } +} + +static void fuzz_cpe_dict(struct oscap_source *source) +{ + struct cpe_dict_model *model = cpe_dict_model_import_source(source); + if (model != NULL) { + cpe_dict_model_free(model); + } +} + +static void fuzz_cpe_lang(struct oscap_source *source) +{ + struct cpe_lang_model *model = cpe_lang_model_import_source(source); + if (model != NULL) { + cpe_lang_model_free(model); + } +} + +int LLVMFuzzerTestOneInput(const uint8_t *data, size_t size) +{ + FUZZ_INIT(); + + /* oscap_source copies the buffer, so the const input is never mutated. */ + struct oscap_source *source = + oscap_source_new_from_memory((const char *)data, size, "fuzz.xml"); + if (source == NULL) { + return 0; + } + + /* Determining the type already parses the document far enough to read the + * root element and namespaces, so this alone exercises a lot of code. */ + oscap_document_type_t type = oscap_source_get_scap_type(source); + + switch (type) { + case OSCAP_DOCUMENT_SDS: + fuzz_datastream(source); + break; + case OSCAP_DOCUMENT_ARF: + fuzz_arf(source); + break; + case OSCAP_DOCUMENT_XCCDF: + case OSCAP_DOCUMENT_XCCDF_TAILORING: + fuzz_xccdf(source); + break; + case OSCAP_DOCUMENT_OVAL_DEFINITIONS: + fuzz_oval_definitions(source); + break; + case OSCAP_DOCUMENT_OVAL_VARIABLES: + fuzz_oval_variables(source); + break; + case OSCAP_DOCUMENT_OVAL_SYSCHAR: + fuzz_oval_syschar(source); + break; + case OSCAP_DOCUMENT_OVAL_RESULTS: + fuzz_oval_results(source); + break; + case OSCAP_DOCUMENT_OVAL_DIRECTIVES: + fuzz_oval_directives(source); + break; + case OSCAP_DOCUMENT_CPE_DICTIONARY: + fuzz_cpe_dict(source); + break; + case OSCAP_DOCUMENT_CPE_LANGUAGE: + fuzz_cpe_lang(source); + break; + case OSCAP_DOCUMENT_UNKNOWN: + default: + /* Unknown type: still try the data stream and XCCDF importers, which + * are the most complex parsers and do their own validation. This keeps + * coverage high even when type detection bails out early. */ + fuzz_datastream(source); + break; + } + + oscap_source_free(source); + return 0; +} diff --git a/fuzz/shadow_probe_fuzzer.c b/fuzz/shadow_probe_fuzzer.c new file mode 100644 index 0000000000..ada65933ed --- /dev/null +++ b/fuzz/shadow_probe_fuzzer.c @@ -0,0 +1,41 @@ +/* + * libFuzzer harness for the OVAL shadow probe's password-hash parser. + * + * parse_enc_mth() inspects an /etc/shadow password field and classifies the + * hashing method from its prefix ($1$, $5$, $6$, _, ...). The shadow field is + * read off the scanned host, so we fuzz that classification directly. + * + * parse_enc_mth() is static, so we #include the probe .c. Its SEAP entry points + * (shadow_probe_main / shadow_probe_offline_mode_supported) and the static + * helpers only they reach are renamed to static, unused functions so that + * --gc-sections drops them; parse_enc_mth itself only uses exported SEXP API. + * + * Build with -DENABLE_FUZZING=ON -DENABLE_PROBES=ON. + */ + +#include +#include +#include + +#include "probe_fuzz_common.h" + +#define shadow_probe_main static __attribute__((unused)) fuzz_shadow_probe_main +#define shadow_probe_offline_mode_supported static __attribute__((unused)) fuzz_shadow_offline +#include "../src/OVAL/probes/unix/shadow_probe.c" +#undef shadow_probe_main +#undef shadow_probe_offline_mode_supported + +int LLVMFuzzerTestOneInput(const uint8_t *data, size_t size) +{ + PROBE_FUZZ_INIT(); + + char *pwd = probe_fuzz_cstr(data, size); + if (pwd == NULL) + return 0; + + SEXP_t *enc_mth = parse_enc_mth(pwd); + SEXP_free(enc_mth); + + free(pwd); + return 0; +} diff --git a/fuzz/textfilecontent54_probe_fuzzer.c b/fuzz/textfilecontent54_probe_fuzzer.c new file mode 100644 index 0000000000..b9680ed753 --- /dev/null +++ b/fuzz/textfilecontent54_probe_fuzzer.c @@ -0,0 +1,118 @@ +/* + * libFuzzer harness for the OVAL textfilecontent54 probe's file reader. + * + * process_file() calls non-exported probe helpers (probe_entobj_cmp -> the OVAL + * comparison code, plus the item cache), so this harness is built with + * add_fuzzer_probe_full() in fuzz/CMakeLists.txt, which links the library's + * object files directly (those internal symbols are hidden in the .so). + * + * textfilecontent54 reads an arbitrary file off the scanned host and runs a + * PCRE pattern over its contents (process_file(): read the whole file into a + * grown buffer, then oscap_pcre_get_substrings() in a loop, building an item + * per match). We fix the pattern to ".*" and fuzz the *file content*, which is + * the attacker-influenced part on a scanned system. + * + * process_file() is static, so we #include the probe .c to reach it. The probe + * also exposes two non-static symbols that already live in libopenscap.so + * (textfilecontent54_probe_main / ..._offline_mode_supported); we rename them + * via macros so this translation unit does not clash with the library at link + * time. + * + * process_file() emits matches through probe_item_collect(), which needs a + * probe_ctx with a live item cache. We build a minimal one: a real icache (its + * worker thread clears a barrier initialised for a single participant) plus a + * fresh collected-object per iteration. The OVAL "instance" entity the reader + * consults is built once with probe_obj_creat(). + * + * Build with -DENABLE_FUZZING=ON -DENABLE_PROBES=ON. + */ + +#include +#include +#include +#include + +#include "probe_fuzz_common.h" + +#define textfilecontent54_probe_main fuzz_tfc54_probe_main +#define textfilecontent54_probe_offline_mode_supported fuzz_tfc54_offline_mode_supported +#include "../src/OVAL/probes/independent/textfilecontent54_probe.c" +#undef textfilecontent54_probe_main +#undef textfilecontent54_probe_offline_mode_supported + +/* Fixed pattern: match everything, so the regex loop and item-building code + * run on whatever bytes the fuzzer produced. */ +static const char *const FUZZ_PATTERN = ".*"; + +static SEXP_t *g_instance_ent; +static oscap_pcre_t *g_regex; +static int g_ready; + +static void tfc54_setup(void) +{ + /* Build a fixed "instance" entity (value 1) the same way the probe's + * object would carry it, then extract the entity process_file() reads. */ + SEXP_t *inst_val = SEXP_number_newi_32(1); + SEXP_t *obj = probe_obj_creat("textfilecontent54_object", NULL, + "instance", NULL, inst_val, + NULL); + g_instance_ent = probe_obj_getent(obj, "instance", 1); + SEXP_free(inst_val); + SEXP_free(obj); + + char *err = NULL; + int erroff = -1; + g_regex = oscap_pcre_compile(FUZZ_PATTERN, OSCAP_PCRE_OPTS_UTF8, &err, &erroff); + if (err != NULL) + oscap_pcre_err_free(err); + + g_ready = (g_instance_ent != NULL && g_regex != NULL); +} + +int LLVMFuzzerTestOneInput(const uint8_t *data, size_t size) +{ + PROBE_FUZZ_INIT(); + if (!g_ready) { + tfc54_setup(); + if (!g_ready) + return 0; + } + + const char *path = probe_fuzz_write_tmpfile(data, size); + if (path == NULL) + return 0; + const char *slash = strrchr(path, '/'); + const char *file = slash ? slash + 1 : path; + + /* No item cache: with ctx.icache == NULL, probe_item_collect() frees the + * item and returns -1 (the parser handles that), so we exercise the full + * parse without the icache worker thread. That thread otherwise dominates + * runtime (~1 exec/s) and is unsafe under libFuzzer -fork. probe_out must + * stay non-NULL or probe_item_collect() would leak the item on its early + * return. */ + probe_ctx ctx; + memset(&ctx, 0, sizeof(ctx)); + ctx.probe_in = NULL; + ctx.probe_out = probe_cobj_new(SYSCHAR_FLAG_UNKNOWN, NULL, NULL, NULL); + ctx.icache = NULL; + ctx.filters = NULL; + ctx.offline_mode = 0; + ctx.max_mem_ratio = OSCAP_PROBE_MEMORY_USAGE_RATIO_DEFAULT; + ctx.collected_items = 0; + ctx.max_collected_items = OSCAP_PROBE_COLLECT_UNLIMITED; + ctx.blocked_paths = NULL; + + struct pfdata pfd; + memset(&pfd, 0, sizeof(pfd)); + pfd.pattern = (char *)FUZZ_PATTERN; /* read-only inside process_file */ + pfd.re_opts = OSCAP_PCRE_OPTS_UTF8; + pfd.instance_ent = g_instance_ent; + pfd.ctx = &ctx; + pfd.compiled_regex = g_regex; + + oval_schema_version_t over = OVAL_SCHEMA_VERSION(5.11); + process_file(NULL, PROBE_FUZZ_TMPDIR, file, &pfd, over, ctx.blocked_paths); + + SEXP_free(ctx.probe_out); + return 0; +} diff --git a/fuzz/textfilecontent_probe_fuzzer.c b/fuzz/textfilecontent_probe_fuzzer.c new file mode 100644 index 0000000000..2737da0fcf --- /dev/null +++ b/fuzz/textfilecontent_probe_fuzzer.c @@ -0,0 +1,62 @@ +/* + * libFuzzer harness for the legacy (OVAL 5.3) textfilecontent probe's reader. + * + * Like textfilecontent54 but distinct code: this process_file() matches the + * pattern line-by-line (the _54 variant slurps the whole file), so it is its + * own parsing surface. We fix the pattern to ".*" and fuzz the file content. + * + * process_file() is static and calls non-exported probe helpers (it emits via + * probe_item_collect), so this harness is built with add_fuzzer_probe_full() + * (links the library object files). The probe's non-static *_probe_main is + * renamed to avoid clashing with the copy in independent_probes_object. + * + * Build with -DENABLE_FUZZING=ON -DENABLE_PROBES=ON. + */ + +#include +#include +#include +#include + +#include "probe_fuzz_common.h" + +#define textfilecontent_probe_main fuzz_tfc_probe_main +#define textfilecontent_probe_offline_mode_supported fuzz_tfc_offline_mode_supported +#include "../src/OVAL/probes/independent/textfilecontent_probe.c" +#undef textfilecontent_probe_main +#undef textfilecontent_probe_offline_mode_supported + +static const char *const FUZZ_PATTERN = ".*"; + +int LLVMFuzzerTestOneInput(const uint8_t *data, size_t size) +{ + PROBE_FUZZ_INIT(); + + const char *path = probe_fuzz_write_tmpfile(data, size); + if (path == NULL) + return 0; + const char *slash = strrchr(path, '/'); + const char *file = slash ? slash + 1 : path; + + /* No item cache (ctx.icache == NULL): probe_item_collect() frees the item + * and returns -1, so we exercise the full parse without the icache worker + * thread (see textfilecontent54_probe_fuzzer.c). */ + probe_ctx ctx; + memset(&ctx, 0, sizeof(ctx)); + ctx.probe_out = probe_cobj_new(SYSCHAR_FLAG_UNKNOWN, NULL, NULL, NULL); + ctx.icache = NULL; + ctx.max_mem_ratio = OSCAP_PROBE_MEMORY_USAGE_RATIO_DEFAULT; + ctx.max_collected_items = OSCAP_PROBE_COLLECT_UNLIMITED; + + struct pfdata pfd; + memset(&pfd, 0, sizeof(pfd)); + pfd.pattern = (char *)FUZZ_PATTERN; /* process_file compiles it itself */ + pfd.filename_ent = NULL; /* unused by process_file */ + pfd.ctx = &ctx; + + oval_schema_version_t over = OVAL_SCHEMA_VERSION(5.11); + process_file(NULL, PROBE_FUZZ_TMPDIR, file, &pfd, over, ctx.blocked_paths); + + SEXP_free(ctx.probe_out); + return 0; +} diff --git a/fuzz/validate_fuzzer.c b/fuzz/validate_fuzzer.c new file mode 100644 index 0000000000..560ecd0039 --- /dev/null +++ b/fuzz/validate_fuzzer.c @@ -0,0 +1,44 @@ +/* + * libFuzzer harness for SCAP document *validation* (src/source/schematron.c, + * src/source/xslt.c and the libxml2 XSD validation path). + * + * `oscap ds sds-validate` and friends are a very common entry point and a + * distinct chunk of code from the object-model importers: XSD schema validation + * and Schematron (implemented as an XSLT transform). Both are driven here. + * + * Validation needs the bundled XML schemas. At runtime point the harness at + * them with the OSCAP_SCHEMA_PATH environment variable, e.g. + * + * OSCAP_SCHEMA_PATH=/schemas ./validate_fuzzer corpus + * + * Without it, schema lookups simply fail early (still exercises the dispatch / + * type-detection and error handling, just not the deep schema code). + */ + +#include +#include + +#include "fuzz_common.h" +#include "oscap_source.h" + +int LLVMFuzzerTestOneInput(const uint8_t *data, size_t size) +{ + FUZZ_INIT(); + + struct oscap_source *source = + oscap_source_new_from_memory((const char *)data, size, "fuzz-validate.xml"); + if (source == NULL) { + return 0; + } + + // Determining the type selects which schema(s) the validator will use. + if (oscap_source_get_scap_type(source) != OSCAP_DOCUMENT_UNKNOWN) { + // XSD validation: parses the document and walks the schema grammar. + oscap_source_validate(source, NULL, NULL); + // Schematron validation: compiles and applies the Schematron XSLT. + oscap_source_validate_schematron(source); + } + + oscap_source_free(source); + return 0; +} diff --git a/fuzz/xccdf_policy_fuzzer.c b/fuzz/xccdf_policy_fuzzer.c new file mode 100644 index 0000000000..ecabaeaaae --- /dev/null +++ b/fuzz/xccdf_policy_fuzzer.c @@ -0,0 +1,65 @@ +/* + * libFuzzer harness for the XCCDF *policy* layer (src/XCCDF_POLICY). + * + * The base scap_parse_fuzzer only parses an XCCDF benchmark into its object + * model. This harness goes one step further and drives the policy model, which + * is the code that resolves profiles, applies selectors, binds values and + * performs text substitution. None of that is reached by plain parsing, yet it + * runs purely on parsed content (no OVAL probes / no system access), so it is a + * good fuzzing target. + * + * Pipeline: + * xccdf_benchmark_import_source() parse the benchmark + * xccdf_policy_model_new() build a policy model (takes + * ownership of the benchmark) + * xccdf_policy_model_build_all_useful_policies() + * instantiate a policy per profile, + * resolving selectors & refinements + * xccdf_policy_resolve() for each policy resolve the selected items + */ + +#include +#include + +#include "fuzz_common.h" +#include "oscap_source.h" +#include "xccdf_benchmark.h" +#include "xccdf_policy.h" + +int LLVMFuzzerTestOneInput(const uint8_t *data, size_t size) +{ + FUZZ_INIT(); + + struct oscap_source *source = + oscap_source_new_from_memory((const char *)data, size, "fuzz-xccdf.xml"); + if (source == NULL) { + return 0; + } + + struct xccdf_benchmark *benchmark = xccdf_benchmark_import_source(source); + if (benchmark == NULL) { + oscap_source_free(source); + return 0; + } + + // xccdf_policy_model_new takes ownership of the benchmark and frees it in + // xccdf_policy_model_free, so we must not free the benchmark separately. + struct xccdf_policy_model *model = xccdf_policy_model_new(benchmark); + if (model != NULL) { + xccdf_policy_model_build_all_useful_policies(model); + + struct xccdf_policy_iterator *it = xccdf_policy_model_get_policies(model); + while (xccdf_policy_iterator_has_more(it)) { + struct xccdf_policy *policy = xccdf_policy_iterator_next(it); + xccdf_policy_resolve(policy); + } + xccdf_policy_iterator_free(it); + + xccdf_policy_model_free(model); + } else { + xccdf_benchmark_free(benchmark); + } + + oscap_source_free(source); + return 0; +} diff --git a/fuzz/xccdf_tailoring_fuzzer.c b/fuzz/xccdf_tailoring_fuzzer.c new file mode 100644 index 0000000000..686c3bb152 --- /dev/null +++ b/fuzz/xccdf_tailoring_fuzzer.c @@ -0,0 +1,80 @@ +/* + * libFuzzer harness for XCCDF tailoring parsing (src/XCCDF/tailoring.c). + * + * A tailoring file customizes an existing benchmark (overriding profiles, + * selecting/deselecting rules, refining values). It is parsed *against* a + * benchmark, so this harness imports one small fixed benchmark at startup and + * then feeds every fuzzer input to xccdf_tailoring_import_source() as the + * tailoring document. This reaches the tailoring parser and its profile / + * selector / value-refinement handling, which plain benchmark parsing skips. + */ + +#include +#include + +#include "fuzz_common.h" +#include "oscap_source.h" +#include "xccdf_benchmark.h" + +// A minimal but valid XCCDF 1.2 benchmark with a profile, a value and a rule so +// that tailoring documents have something to extend / select / refine. +static const char BASE_BENCHMARK[] = + "\n" + "\n" + " draft\n" + " 1.0\n" + " \n" + " base profile\n" + "