Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
7 changes: 7 additions & 0 deletions fuzz/.gitignore
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
# Build artifacts produced by build.sh
*.o
fuzz_pac
*.dSYM/
crash-*
leak-*
timeout-*
44 changes: 44 additions & 0 deletions fuzz/README.md
Original file line number Diff line number Diff line change
@@ -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:

```
<pac script> \0 <url> \0 <host>
```

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.
31 changes: 31 additions & 0 deletions fuzz/build.sh
Original file line number Diff line number Diff line change
@@ -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"
Binary file added fuzz/corpus/direct
Binary file not shown.
Binary file added fuzz/corpus/dnsdomainis
Binary file not shown.
Binary file added fuzz/corpus/isinnet_myip
Binary file not shown.
Binary file added fuzz/corpus/plainhost
Binary file not shown.
Binary file added fuzz/corpus/shexpmatch
Binary file not shown.
69 changes: 69 additions & 0 deletions fuzz/fuzz_pac.c
Original file line number Diff line number Diff line change
@@ -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 <stdarg.h>
#include <stddef.h>
#include <stdint.h>
#include <stdlib.h>
#include <string.h>

#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;
}