Skip to content

Security Deep

Security Deep #306

Workflow file for this run

name: Security Deep
# LAB-1140 PROOF BRANCH — trimmed to the atheris-fuzzing job only, byte-identical
# to the job on the fix branch (lab-1140-atheris-honest-red), so proof runs do
# not burn hours of unrelated kani/miri/sanitizer time on the self-hosted pool.
# Never to be merged.
on:
workflow_dispatch:
permissions:
contents: read
jobs:
atheris-fuzzing:
name: Atheris Python-Rust Fuzzing
runs-on: cachekit
timeout-minutes: 60
steps:
- uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6
# Pin to 3.11: atheris 2.3.0's newest prebuilt wheel is cp311. On a newer
# interpreter uv builds atheris from sdist, which needs clang + libFuzzer
# (find_libfuzzer.sh) — absent on the self-hosted runner, so install fails
# before any fuzzing runs. Both uv sync and uv run must pin, or uv run
# re-resolves to a different interpreter and re-triggers the source build.
- name: Install dependencies
run: |
uv sync --group dev --group fuzz --python 3.11
# Exit-code contract — no `|| true`, every non-zero exit fails the job.
# (scripts/fuzz-python.sh carries the same contract for local runs; keep
# the budgets and flags in the two files in sync.)
# - Pass is libFuzzer exiting 0 when -max_total_time=600 expires cleanly.
# - A crash, OOM, or uncaught Python exception exits non-zero and fails
# the job. atheris.Setup(sys.argv, ...) forwards flags to libFuzzer, so
# -artifact_prefix drops the reproducer in tests/fuzzing/artifacts/.
# No corpus dir is passed, so that directory only ever holds crash
# artifacts — a file there is always a finding, never corpus growth.
# - A hung input is caught by libFuzzer's own per-input watchdog
# (-timeout=60), which writes a timeout-* reproducer and exits
# non-zero. `timeout -k 30s 15m` is the backstop for hangs in native
# code where that watchdog can't fire: exit 124 is a deliberate
# FAILURE, not budget exhaustion — the fuzz budget is 600 s and budget
# exhaustion exits 0 above, so a target still alive at 15 min is hung
# (and libFuzzer traps SIGTERM, hence the -k hard kill).
# - Zero matched targets fails the job: a glob that stops matching must
# not "pass" having fuzzed nothing (LAB-1136 post-mortem pattern).
- name: Run Atheris fuzz targets (10 min each)
run: |
mkdir -p tests/fuzzing/artifacts
shopt -s nullglob
targets=(tests/fuzzing/fuzz_*.py)
if [ "${#targets[@]}" -eq 0 ]; then
echo "::error::no Atheris targets matched tests/fuzzing/fuzz_*.py — refusing to pass having fuzzed nothing"
exit 1
fi
for fuzz_target in "${targets[@]}"; do
echo "Fuzzing $fuzz_target..."
timeout -k 30s 15m uv run --python 3.11 python "$fuzz_target" \
-max_total_time=600 -timeout=60 -artifact_prefix=tests/fuzzing/artifacts/
done
# No "Report fuzzing results" step: now that `|| true` is gone, a crash
# fails its own fuzz step, so a trailing check could only ever run in the
# no-crash case and print ✅ — a named green step incapable of failing,
# the manufactured-evidence pattern the Extended Fuzzing job above already
# removed. (The old gate also inspected tests/fuzzing/corpus/, a directory
# nothing ever wrote to.) The fuzz steps' exit codes are the signal; this
# upload preserves the reproducer, which would otherwise die with the
# ephemeral runner.
- name: Upload crash artifacts
if: failure()
uses: actions/upload-artifact@bbbca2ddaa5d8feaa63e36b76fdaad77386f024f # v7
with:
name: atheris-crash-artifacts
path: tests/fuzzing/artifacts/
# Public repo: a crash reproducer is a ready-made PoC for anyone while
# it is retrievable. 7 days covers triage of a nightly red without
# leaving a month-long public exploit window.
retention-days: 7
if-no-files-found: warn