PDFuzzer is an LLM-assisted fuzzer for JavaScript engines embedded in PDF readers. It turns documented and undocumented API specifications into per-parameter context-free grammars (CFGs), optionally infers relationships between APIs, generates JavaScript-bearing PDF files, and executes them under a Windows monitor.
This repository contains the implementation, but not all generated data. In
particular, the grammar directory passed to fuzzing/run.py -p must be created
before the fuzzer is run. The Data Preparation section
identifies the producer or external prerequisite for each input used by the
fuzzer.
Run fuzzing campaigns only against software you are authorized to test. A disposable, isolated Windows VM is strongly recommended because the monitor opens untrusted PDFs, interacts with application windows, and terminates reader processes.
- Workflow
- Requirements
- Installation
- Data Preparation
- Running PDFuzzer
- Configuration
- Outputs
- Project Layout
- Troubleshooting
The latest PDFuzzer pipeline has two parallel branches after specification extraction: grammar generation and relationship inference.
Official manuals ──> API manual parser ───────────────┐
├─> unified API specs
Execution traces ──> differential analysis ──> LLM ──┘ │
├─> parameter-level CFGs ─────────┐
│ │
└─> candidate/symbolic relations ─┤
v
instantiate, sequence, and Z3-solve API calls
│
v
generate PDFs and monitor
The differential-analysis step that produces raw undocumented API signatures is external to this repository. PDFuzzer provides the subsequent semantic recovery, grammar generation, relationship inference, PDF generation, and monitoring stages.
- Python 3.8 or later.
- Windows for
fuzzing/run.pyandfuzzing/monitor.py. The current monitor imports Windows-only packages even when--dryis used. - At least one supported target reader:
- Adobe Acrobat Reader DC
- Foxit PDF Reader
- PDF-XChange Editor
- An OpenAI or Anthropic API key for LLM-backed preprocessing. Candidate
relationship inference specifically requires an OpenAI API key and uses
gpt-4o. - Network access while downloading the Adobe API manuals and invoking an LLM.
Clone the repository, create a virtual environment, and install its dependencies:
git clone https://github.com/ucsb-seclab/PDFuzzer.git
Set-Location PDFuzzer
python -m venv .venv
.\.venv\Scripts\Activate.ps1
python -m pip install -r requirements.txtSet the key for the model that will be used:
# PowerShell
$env:OPENAI_API_KEY = "your-openai-api-key"
$env:ANTHROPIC_API_KEY = "your-anthropic-api-key"# Bash
export OPENAI_API_KEY="your-openai-api-key"
export ANTHROPIC_API_KEY="your-anthropic-api-key"The examples below use gpt-4o, which is accepted by the current model
handler. Only the key for the selected provider is needed, except that
RAG_relation_infer_4o.py always uses OpenAI.
| Data | Producer | Consumer |
|---|---|---|
pre_fuzz/document_parser/output/ |
pre_fuzz/document_parser/web_scraper.py |
json_parser_web.py |
pre_fuzz/document_parser/json/ |
pre_fuzz/document_parser/json_parser_web.py |
Semantic recovery, grammar generation, and symbolic relationship inference |
pre_fuzz/data/undoc_<target>/ |
External differential analysis of execution traces; no generator is included in this repository | undoc_semantic_recovery.py |
pre_fuzz/results/undoc_<target>_description/ |
pre_fuzz/undoc_semantic_recovery.py |
Grammar generation and symbolic relationship inference |
fuzzing/data/object_grammar_param_all_<target>/ |
pre_fuzz/grammar_generator_param.py |
Required by fuzzing/run.py -p |
pre_fuzz/relation_infer/documents/all_apis.txt |
Included API-name list | RAG_relation_infer_4o.py |
pre_fuzz/relation_infer/documents/Undoc_APIs_4o.json |
External aggregate; no exporter is included in this repository | RAG_relation_infer_4o.py |
fuzzing/config/all_relation.json |
pre_fuzz/relation_infer/RAG_relation_infer_4o.py |
Required by fuzzing/run.py --relation |
fuzzing/config/all_symbolic.json |
pre_fuzz/relation_infer/Symbolic_relation_infer.py |
Required by fuzzing/run.py --symbolic |
fuzzing/test/*.pdf |
fuzzing/run.py |
fuzzing/monitor.py or fuzzing/run.py --run |
A basic run needs only a generated grammar tree. --relation additionally
requires all_relation.json; --symbolic additionally requires
all_symbolic.json. The full relationship-aware mode normally enables both
flags.
Several scripts resolve prompts/, documents/, config/, and test/
relative to the current working directory. Run each command from the directory
shown below. The commands use PowerShell and return to the repository root at
the end of each block unless stated otherwise.
From the repository root:
Set-Location pre_fuzz/document_parser
python web_scraper.py -m A
python web_scraper.py -m D
python json_parser_web.py
Set-Location ../..web_scraper.py accepts only -m A (the main JavaScript API manual) or
-m D (the Doc API manual). It writes extracted text to output/ and caches
downloaded HTML in result/. json_parser_web.py has no command-line
arguments; it converts its fixed ./output/ input into structured JSON under
./json/.
The resulting repository-relative paths are:
pre_fuzz/document_parser/output/
pre_fuzz/document_parser/json/
The -u input is a directory of raw signatures obtained through external
differential analysis. It must use this layout:
pre_fuzz/data/undoc_adobe/
└── <Object>/
├── methods/
│ └── <API>.json
└── properties/
└── <API>.json
Once those signatures are available, run the semantic recovery stage from
pre_fuzz/:
Set-Location pre_fuzz
python undoc_semantic_recovery.py `
-d document_parser/json `
-u data/undoc_adobe `
-o results/undoc_adobe_description `
-m gpt-4o
Set-Location ..Replace adobe with the target represented by the raw signatures. This stage
does not itself collect execution traces or perform differential analysis.
Generate grammars for the documented APIs from pre_fuzz/:
Set-Location pre_fuzz
python grammar_generator_param.py `
-i document_parser/json `
-o ../fuzzing/data/object_grammar_param_all_adobe `
-m gpt-4o
Set-Location ..If undocumented specifications were recovered in the previous step, add them to the same output tree:
Set-Location pre_fuzz
python grammar_generator_param.py `
-i results/undoc_adobe_description `
-o ../fuzzing/data/object_grammar_param_all_adobe `
-m gpt-4o
Set-Location ..The output contains one directory per object, API_INFO.json for each API,
and grammar.json for each parameter. This generated directory is the value
passed to run.py -p; it is not included in a clean checkout.
Create fuzzing/config/ before writing relationship outputs.
Candidate relationship inference uses the API list in
pre_fuzz/relation_infer/documents/all_apis.txt and writes
all_relation.json. The checked-in list contains one Object.API name per
line; it is not regenerated or synchronized with newly produced specifications.
Review and update it whenever the documented or undocumented spec set changes.
New-Item -ItemType Directory -Force fuzzing/config | Out-Null
Set-Location pre_fuzz/relation_infer
python RAG_relation_infer_4o.py `
-i documents/all_apis.txt `
-o ../../fuzzing/config/all_relation.json
Set-Location ../..Important: RAG_relation_infer_4o.py also has a hard-coded dependency on
pre_fuzz/relation_infer/documents/Undoc_APIs_4o.json. That aggregate
undocumented-specification file is not present in a clean checkout, and this
repository does not include a script that exports it. Supply the file before
running this step; the per-file output of undoc_semantic_recovery.py is not a
drop-in replacement for this aggregate file. Without it, candidate and full
relationship-aware modes cannot be prepared from the clean checkout; basic
grammar-based fuzzing remains available.
After candidate relationships have been generated, infer strong symbolic
relationships. The Adobe example below requires the Step 2 output at
pre_fuzz/results/undoc_adobe_description/:
Set-Location pre_fuzz/relation_infer
$env:PYTHONPATH = ".."
python Symbolic_relation_infer.py `
-d ../document_parser/json `
-u ../results/undoc_adobe_description `
-r ../../fuzzing/config/all_relation.json `
-o ../../fuzzing/config/all_symbolic.json `
-m gpt-4o
Set-Location ../..-d and -u must point to existing documented and undocumented specification
trees. If Step 2 was skipped, create an empty tree from the repository root and
pass ../results/undoc_empty to -u instead:
New-Item -ItemType Directory -Force pre_fuzz/results/undoc_empty | Out-NullSetting PYTHONPATH to .. lets the script import pre_fuzz/agentlib while it
is run from relation_infer/, where its prompt files are resolved.
All fuzzing commands must be run from fuzzing/ because the program resolves
data/, config/, test/, save/, and runlog.txt against the current
working directory:
Set-Location fuzzingThe current defaults in run.py are 30,000 PDF files (TNUM) and 2,048 API
calls per file (SNUM). These values are source constants, not CLI options.
Reduce them in run.py before a short smoke test.
python run.py `
-p data/object_grammar_param_all_adobe `
-t adobe `
--drypython run.py `
-p data/object_grammar_param_all_adobe `
-t adobeUse foxit or xchange as the target when the corresponding reader is
installed and configured. If the grammar tree includes target-specific
undocumented APIs, also generate and pass the matching
object_grammar_param_all_foxit or object_grammar_param_all_xchange tree.
This command requires both JSON files generated in the optional relationship stage:
python run.py `
-p data/object_grammar_param_all_adobe `
-t adobe `
--relation `
--symbolicpython run.py `
-p data/object_grammar_param_all_adobe `
-t adobe `
--runAlthough --run skips generation, -p remains required and the grammar tree
is loaded during initialization.
python monitor.py -t adobe -i 0.pdf-i is a file name under fuzzing/test/, not a path such as test/0.pdf.
The monitor adds the test/ prefix itself. Standalone monitor.py saves a
non-finish case under save/<status>/, but it does not append to runlog.txt.
| Option | Meaning |
|---|---|
-p, --base_directory |
Generated grammar tree; required in every mode |
-t, --target |
Reader monitor: adobe, foxit, or xchange |
--dry |
Generate PDFs without executing them |
--relation |
Load config/all_relation.json and prefer candidate-related APIs |
--symbolic |
Load config/all_symbolic.json and solve strong constraints with Z3 |
--run |
Execute existing PDFs from test/ instead of generating new ones |
The monitor currently uses these default paths:
Adobe: C:\Program Files (x86)\Adobe\Acrobat Reader DC\Reader\AcroRd32.exe
Foxit: C:\Program Files (x86)\Foxit Software\Foxit PDF Reader\FoxitPDFReader.exe
XChange: C:\Program Files\Tracker Software\PDF Editor\PDFXEdit.exe
If a reader is installed elsewhere, update the corresponding APP_PATH in
fuzzing/monitor.py.
fuzzing/config/blocklist.txt and fuzzing/config/limitlist.txt are optional.
When absent, they are treated as empty lists. Add one fully qualified API name
per line, using the Object.API form. These lists control API selection in the
relationship-aware generation path.
The paths below are fixed relative to fuzzing/:
config/all_relation.json
config/all_symbolic.json
The corresponding file must exist before its CLI flag is enabled.
When commands are run from fuzzing/, PDFuzzer writes:
fuzzing/
├── test/ # All generated PDF test cases
├── runlog.txt # Status lines for PDFs monitored through run.py
└── save/
└── <status>/ # Copies of non-finish cases, e.g. crash/hang/error
--dry creates files under fuzzing/test/ but does not create execution
results. The fuzzer does not automatically create the results/ or logs/
directories described by older versions of this README.
PDFuzzer/
├── pre_fuzz/
│ ├── agentlib/ # OpenAI/Anthropic model adapters
│ ├── document_parser/ # Manual downloader and JSON parser
│ │ ├── output/ # Generated manual text
│ │ └── json/ # Generated documented API specs
│ ├── relation_infer/ # Candidate and symbolic inference
│ ├── data/ # External raw undocumented signatures
│ ├── results/ # Generated undocumented specs
│ ├── grammar_generator_param.py # Produces the grammar tree used by -p
│ └── undoc_semantic_recovery.py # Enriches raw undocumented signatures
├── fuzzing/
│ ├── param_grammar/ # Runtime CFG/API generators
│ ├── data/ # Generated grammar trees
│ ├── config/ # Optional generated/provided config
│ ├── test/ # Runtime-generated PDFs
│ ├── save/ # Runtime-saved non-finish cases
│ ├── mPDF.py # Embeds generated JavaScript in PDFs
│ ├── monitor.py # Reader execution and monitoring
│ └── run.py # Main entry point
├── requirements.txt
└── README.md
Directories marked generated, external, or runtime-created do not necessarily exist in a clean checkout.
Use the working directories shown above:
- Run
web_scraper.pyandjson_parser_web.pyfrompre_fuzz/document_parser/. - Run
undoc_semantic_recovery.pyandgrammar_generator_param.pyfrompre_fuzz/. - Run both relationship scripts from
pre_fuzz/relation_infer/. - Run
run.pyandmonitor.pyfromfuzzing/.
Do not enable the corresponding relationship flag until its data preparation
stage has completed. Basic fuzzing without these flags needs only the grammar
tree supplied with -p.
Verify the installation path and update APP_PATH in fuzzing/monitor.py if
the reader is not installed at the default location.
Set OPENAI_API_KEY or ANTHROPIC_API_KEY in the same shell that launches the
preprocessing script. Candidate relationship inference always requires
OPENAI_API_KEY.