Ryad Aouak · Amir Ammar · Jon Kuçi · Pascal J. Kuschkowitz
The public repository is located at: https://github.com/Ryad2/Fuzzing
This repo contains the libpng 1.6.34 fuzzing setup, run wrappers, and report
artifacts for the lab. The final report-facing branch is
feat/campaigns-no-ubsan. The main report-facing runs are:
asan_forkqemunosan_forkasan_persistentsynthetic_bug
Build the Docker image:
docker build -t cs412-libpng-fuzz .Open a shell in the container from the repo root:
docker run --rm -it --user "$(id -u):$(id -g)" -v "$PWD:/work" -w /work cs412-libpng-fuzz
# On Windows
docker run --rm -it -v "${PWD}:/work" -w /work cs412-libpng-fuzzInside the container, run one of the wrappers:
bash runs/asan_fork.sh
bash runs/qemu.sh
bash runs/nosan_fork.sh
bash runs/asan_persistent.sh
bash runs/synthetic_bug.shEach script resumes an existing AFL++ session automatically if findings/<run>/default/ exists. Pass --force to wipe all previous data for that run, do a clean rebuild, and start from scratch:
bash runs/asan_fork.sh --force--force removes seeds_raw/, seeds/, crash_candidates/, showmap/, and findings/ for the run, then runs make clean && make build before launching AFL++.
.
├── Dockerfile
├── Makefile
├── report.tex
│
├── runs/
│ ├── common.sh # shared logic: seed gen, build, cmin, showmap, resume/force
│ ├── asan_fork.sh # 12h primary campaign (ASAN + fork server)
│ ├── qemu.sh # 5.7h binary-only campaign (no instrumentation, no ASAN)
│ ├── nosan_fork.sh # short run for Q8 throughput baseline (no ASAN)
│ ├── asan_persistent.sh # short run for Q8 persistent-mode throughput number
│ └── synthetic_bug.sh # 60s validation run with the injected heap overflow
│
├── src/
│ ├── harness.c # main fork-mode harness (used by asan_fork and nosan_fork)
│ ├── harness_persistent.c # persistent-mode harness (__AFL_LOOP)
│ ├── harness_plain.c # uninstrumented binary for QEMU mode
│ └── generate_libpng_seeds.py # generates the 30 startup-safe PNG seeds
│
├── patches/
│ ├── libpng-nocrc.patch # disables CRC checks so mutations reach the parser
│ └── libpng-synthetic-bug.patch # injects a one-byte heap overflow for Q5 validation
│
├── results/
│ ├── fuzzing_campaign.md # campaign summary and artifact index
│ ├── q8_speed_comparison.md # throughput table and edge counts for Q8
│ ├── asan_fork/
│ │ ├── screenshots/
│ │ │ └── 12h_run.png
│ │ ├── plots/
│ │ │ ├── edges.png
│ │ │ ├── exec_speed.png
│ │ │ ├── high_freq.png
│ │ │ └── low_freq.png
│ │ └── raw/
│ │ ├── fuzzer_stats
│ │ ├── plot_data
│ │ └── queue/ # full corpus snapshot (~2161 entries)
│ ├── qemu/
│ │ ├── screenshots/
│ │ │ └── qemu_5h.png
│ │ ├── plots/
│ │ │ ├── edges.png
│ │ │ └── exec_speed.png
│ │ └── raw/
│ │ ├── fuzzer_stats
│ │ ├── plot_data
│ │ └── queue/
│ ├── nosan_fork/
│ │ ├── nosan_fork.md
│ │ ├── screenshots/
│ │ │ └── nosan_8_min.png
│ │ └── raw/
│ │ ├── fuzzer_stats
│ │ └── plot_data
│ ├── asan_persistent/
│ │ ├── asan_persistent.md
│ │ ├── screenshots/
│ │ │ └── persistent_5min.png
│ │ └── raw/
│ │ ├── fuzzer_stats
│ │ └── plot_data
│ ├── synthetic_bug/
│ │ ├── screenshots/
│ │ │ ├── synthetic_afl_crash_found.png
│ │ │ └── 60_sec_1_crash.png
│ │ ├── crash_input/ # the single crashing input AFL++ saved
│ │ ├── triage/
│ │ │ ├── asan_trace.txt
│ │ │ ├── asan_trace_verbose.txt
│ │ │ └── run_stdout.txt
│ │ └── findings/ # full AFL++ output from the 60s synthetic run
│ │ └── default/
│ │ ├── fuzzer_stats
│ │ ├── crashes/ # contains the saved crashing input
│ │ └── queue/
│ └── q8_libpng_pngtest/
│ ├── README.md
│ └── raw/
│ └── fuzzer_stats # total_edges=7449 (library-only proxy for Q8)
│
├── findings/ # live AFL++ output dirs (not for the report)
│ └── <run_name>/
│ └── default/
│ ├── fuzzer_stats
│ ├── plot_data
│ ├── queue/
│ ├── crashes/
│ └── hangs/
│
├── seeds/ # minimized corpora (post afl-cmin), one dir per run
├── seeds_raw/ # raw generated PNGs before minimization, one dir per run
├── showmap/ # afl-showmap edge snapshots (edges.all, edges.unique)
│
├── crash_candidates/ # seeds that crash the target at startup, quarantined here
│ └── <run_name>/ # per-run subfolder; kept out of AFL++ corpus calibration
│
├── sure_crash/
│ ├── info.md # explains the CVE-2018-13785 file and why it's excluded
│ └── 01_rgb8_row_factor_divzero.png # exact crashing input for the historical bug
│
└── triage/
├── triage_notes.md
├── check_crashes/
│ └── crashes.<timestamp>/
│ ├── summary.csv
│ └── id_XXXX/ # per-crash: CASE.txt, png_info.txt, sha256, gdb traces
│ ├── CASE.txt
│ ├── png_info.txt
│ ├── gdb_nosan_interrupt_bt.txt
│ ├── gdb_qemu_interrupt_bt.txt
│ ├── nosan_short/
│ ├── nosan_long/
│ ├── qemu_short/
│ └── qemu_long/
└── check_hangs/
└── hangs.<timestamp>/
└── id_XXXX/
├── CASE.txt
├── nosan/
└── qemu/
All campaigns were run on Windows (WSL2) with a Ryzen 5 5600 and 16 GB RAM. The live findings/ directories are AFL++ output; results/ holds the cleaned report-facing copies.
All real campaign builds apply libpng-nocrc.patch, which disables CRC32 chunk verification so AFL++ mutations reach the actual parser, decompressor, and row-processing code instead of being rejected at the checksum gate. The synthetic bug validation uses a completely separate build (make build-asan-synthetic) with libpng-synthetic-bug.patch applied on top — this build is never used for any real campaign run.
Overall we did more than 100 hours of fuzzing across multiple machines and we had a lot of fun :)