diff --git a/fuzz/.gitignore b/fuzz/.gitignore new file mode 100644 index 0000000..4261a3c --- /dev/null +++ b/fuzz/.gitignore @@ -0,0 +1,7 @@ +# Build artifacts produced by build.sh +*.o +fuzz_pac +*.dSYM/ +crash-* +leak-* +timeout-* diff --git a/fuzz/README.md b/fuzz/README.md new file mode 100644 index 0000000..599857d --- /dev/null +++ b/fuzz/README.md @@ -0,0 +1,44 @@ +# Fuzzing pacparser + +A [libFuzzer](https://llvm.org/docs/LibFuzzer.html) harness for the PAC parsing +path. It drives `pacparser_parse_pac_string()` followed by +`pacparser_find_proxy()`, exercising pacparser's own C layer and the PAC helper +functions (`myIpAddress`, `dnsResolve`, `isInNet`, `shExpMatch`, ...) on top of +the embedded QuickJS engine. + +## Input format + +Each input is split on NUL bytes into up to three fields: + +``` + \0 \0 +``` + +Missing `url`/`host` fall back to fixed defaults, so a bare PAC script is a +valid input too. The `fuzz/corpus/` directory contains a few readable seeds. + +## Build + +Needs a clang that ships libFuzzer (upstream LLVM clang or the OSS-Fuzz +toolchain). Apple's stock clang does not include it — install LLVM via Homebrew +on macOS: + +```sh +CC=/opt/homebrew/opt/llvm/bin/clang ./fuzz/build.sh # macOS (Homebrew LLVM) +./fuzz/build.sh # Linux with clang +``` + +## Run + +```sh +./fuzz/fuzz_pac fuzz/corpus # replay/extend the seed corpus +./fuzz/fuzz_pac -max_total_time=60 fuzz/corpus +``` + +Set `ASAN_OPTIONS=detect_leaks=0` if you only want to chase memory-corruption +bugs and not leaks. + +## OSS-Fuzz + +`build.sh` honours `$CC`, `$CFLAGS`, and `$LIB_FUZZING_ENGINE`, so it can be +called directly from an OSS-Fuzz `build.sh` with no changes. diff --git a/fuzz/build.sh b/fuzz/build.sh new file mode 100755 index 0000000..b1f3f4d --- /dev/null +++ b/fuzz/build.sh @@ -0,0 +1,31 @@ +#!/usr/bin/env bash +# Build the libFuzzer harness with AddressSanitizer. +# +# Needs a clang that ships libFuzzer. Apple's stock clang does not, so on macOS +# point CC at Homebrew LLVM: CC=/opt/homebrew/opt/llvm/bin/clang ./fuzz/build.sh +# Honours $CC/$CFLAGS/$LIB_FUZZING_ENGINE so OSS-Fuzz can call it unchanged. +set -euo pipefail + +HERE="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)" +ROOT="$(cd "$HERE/.." && pwd)" +SRC="$ROOT/src" +OUT="${OUT:-$HERE}" +mkdir -p "$OUT" + +CC="${CC:-clang}" +SANITIZER_FLAGS="${SANITIZER_FLAGS:--fsanitize=address,fuzzer-no-link}" +CFLAGS="${CFLAGS:--g -O1 $SANITIZER_FLAGS}" +# OSS-Fuzz passes the engine via $LIB_FUZZING_ENGINE; standalone uses -fsanitize=fuzzer. +ENGINE="${LIB_FUZZING_ENGINE:--fsanitize=fuzzer}" + +echo "[*] CC=$CC" +echo "[*] building QuickJS" +$CC $CFLAGS -fPIC -c "$SRC/quickjs/quickjs.c" -o "$OUT/quickjs.o" + +echo "[*] building pacparser" +$CC $CFLAGS -I"$SRC/quickjs" -DVERSION='"fuzz"' -c "$SRC/pacparser.c" -o "$OUT/pacparser.o" + +echo "[*] linking fuzz_pac" +$CC $CFLAGS $ENGINE -I"$SRC" "$HERE/fuzz_pac.c" "$OUT/pacparser.o" "$OUT/quickjs.o" -o "$OUT/fuzz_pac" + +echo "[+] built $OUT/fuzz_pac" diff --git a/fuzz/corpus/direct b/fuzz/corpus/direct new file mode 100644 index 0000000..b545c74 Binary files /dev/null and b/fuzz/corpus/direct differ diff --git a/fuzz/corpus/dnsdomainis b/fuzz/corpus/dnsdomainis new file mode 100644 index 0000000..7061378 Binary files /dev/null and b/fuzz/corpus/dnsdomainis differ diff --git a/fuzz/corpus/isinnet_myip b/fuzz/corpus/isinnet_myip new file mode 100644 index 0000000..23ac7c6 Binary files /dev/null and b/fuzz/corpus/isinnet_myip differ diff --git a/fuzz/corpus/plainhost b/fuzz/corpus/plainhost new file mode 100644 index 0000000..cb769e2 Binary files /dev/null and b/fuzz/corpus/plainhost differ diff --git a/fuzz/corpus/shexpmatch b/fuzz/corpus/shexpmatch new file mode 100644 index 0000000..42d8c96 Binary files /dev/null and b/fuzz/corpus/shexpmatch differ diff --git a/fuzz/fuzz_pac.c b/fuzz/fuzz_pac.c new file mode 100644 index 0000000..fc52165 --- /dev/null +++ b/fuzz/fuzz_pac.c @@ -0,0 +1,69 @@ +// libFuzzer harness for the PAC parsing path. QuickJS is already fuzzed +// upstream, so the target here is pacparser's C layer on top of it. See +// fuzz/README.md for build/run instructions and the input format. + +#include +#include +#include +#include +#include + +#include "pacparser.h" + +// Discard pacparser's error/alert output; otherwise it floods the fuzzer log. +static int silent_printer(const char *fmt, va_list ap) { + (void)fmt; + (void)ap; + return 0; +} + +int LLVMFuzzerInitialize(int *argc, char ***argv) { + (void)argc; + (void)argv; + pacparser_set_error_printer(silent_printer); + return 0; +} + +// Returns the next NUL-delimited field as a heap string the caller must free. +static char *take_field(const uint8_t *data, size_t size, size_t *pos) { + size_t start = *pos; + size_t i = start; + while (i < size && data[i] != '\0') i++; + size_t len = i - start; + char *out = (char *)malloc(len + 1); + if (!out) return NULL; + memcpy(out, data + start, len); + out[len] = '\0'; + *pos = (i < size) ? i + 1 : size; + return out; +} + +int LLVMFuzzerTestOneInput(const uint8_t *data, size_t size) { + if (size == 0) return 0; + + size_t pos = 0; + char *script = take_field(data, size, &pos); + char *url = take_field(data, size, &pos); + char *host = take_field(data, size, &pos); + if (!script || !url || !host) { + free(script); + free(url); + free(host); + return 0; + } + if (url[0] == '\0') { free(url); url = strdup("http://example.com/"); } + if (host[0] == '\0') { free(host); host = strdup("example.com"); } + + // A fresh engine per input keeps iterations independent. + if (pacparser_init()) { + if (pacparser_parse_pac_string(script)) { + pacparser_find_proxy(url, host); // result is owned and freed by pacparser + } + pacparser_cleanup(); + } + + free(script); + free(url); + free(host); + return 0; +}