mirror of
https://github.com/elicpeter/nyx.git
synced 2026-06-06 19:35:13 +02:00
217 lines
6.7 KiB
YAML
217 lines
6.7 KiB
YAML
name: Fuzz
|
|
|
|
on:
|
|
pull_request:
|
|
branches: ["master"]
|
|
paths:
|
|
- "src/**"
|
|
- "fuzz/**"
|
|
- "Cargo.toml"
|
|
- "Cargo.lock"
|
|
- ".github/workflows/fuzz.yml"
|
|
schedule:
|
|
# Long-form weekly run, Sundays at 06:00 UTC.
|
|
- cron: "0 6 * * 0"
|
|
workflow_dispatch:
|
|
|
|
permissions:
|
|
contents: read
|
|
|
|
concurrency:
|
|
group: ${{ github.workflow }}-${{ github.event.pull_request.number || github.ref }}
|
|
cancel-in-progress: true
|
|
|
|
jobs:
|
|
fuzz:
|
|
name: fuzz-${{ matrix.target }}
|
|
runs-on: ubuntu-latest
|
|
strategy:
|
|
fail-fast: false
|
|
matrix:
|
|
target: [scan_bytes, extract_summaries, cross_file_taint]
|
|
steps:
|
|
- uses: actions/checkout@v6
|
|
|
|
# cargo-fuzz needs nightly for the libFuzzer codegen flags.
|
|
- uses: actions-rust-lang/setup-rust-toolchain@v1
|
|
with:
|
|
toolchain: nightly
|
|
cache: true
|
|
cache-workspaces: |
|
|
.
|
|
fuzz
|
|
|
|
- uses: taiki-e/install-action@v2
|
|
with:
|
|
tool: cargo-fuzz
|
|
|
|
- uses: actions/setup-node@v6
|
|
with:
|
|
node-version: 20
|
|
cache: npm
|
|
cache-dependency-path: frontend/package-lock.json
|
|
|
|
- name: Build frontend
|
|
working-directory: frontend
|
|
run: |
|
|
npm ci
|
|
npm run build
|
|
|
|
- name: Restore fuzz corpus
|
|
uses: actions/cache@v5
|
|
with:
|
|
path: fuzz/corpus/${{ matrix.target }}
|
|
key: fuzz-corpus-${{ matrix.target }}-${{ github.sha }}
|
|
restore-keys: |
|
|
fuzz-corpus-${{ matrix.target }}-
|
|
|
|
# The harness reads inputs as <lang_idx_byte><source>, so we prefix
|
|
# each seed with its language index here at stage time. Files in
|
|
# fuzz/seed_corpus/ are committed as plain source without the byte
|
|
# because some IDEs strip 0x00 on save.
|
|
- name: Layer seed corpus
|
|
run: |
|
|
set -euo pipefail
|
|
target=${{ matrix.target }}
|
|
dest="fuzz/corpus/$target"
|
|
mkdir -p "$dest"
|
|
ext_to_idx() {
|
|
case "$1" in
|
|
rs) echo 0 ;;
|
|
js) echo 1 ;;
|
|
ts) echo 2 ;;
|
|
py) echo 3 ;;
|
|
go) echo 4 ;;
|
|
java) echo 5 ;;
|
|
rb) echo 6 ;;
|
|
php) echo 7 ;;
|
|
c) echo 8 ;;
|
|
cpp) echo 9 ;;
|
|
*) return 1 ;;
|
|
esac
|
|
}
|
|
stage() {
|
|
src="$1"
|
|
ext="${src##*.}"
|
|
idx=$(ext_to_idx "$ext") || return 0
|
|
hash=$(sha256sum "$src" | cut -c1-16)
|
|
out="$dest/seed-${ext}-${hash}"
|
|
[ -e "$out" ] && return 0
|
|
printf '%b' "$(printf '\\%03o' "$idx")" > "$out"
|
|
cat "$src" >> "$out"
|
|
}
|
|
for f in benches/fixtures/sample.*; do
|
|
[ -e "$f" ] && stage "$f"
|
|
done
|
|
while IFS= read -r f; do
|
|
stage "$f"
|
|
done < <(find tests/benchmark/corpus -type f \( \
|
|
-name '*.rs' -o -name '*.js' -o -name '*.ts' \
|
|
-o -name '*.py' -o -name '*.go' -o -name '*.java' \
|
|
-o -name '*.rb' -o -name '*.php' -o -name '*.c' \
|
|
-o -name '*.cpp' \))
|
|
if [ -d "fuzz/seed_corpus/$target" ]; then
|
|
while IFS= read -r f; do
|
|
stage "$f"
|
|
done < <(find "fuzz/seed_corpus/$target" -type f \( \
|
|
-name '*.rs' -o -name '*.js' -o -name '*.ts' \
|
|
-o -name '*.py' -o -name '*.go' -o -name '*.java' \
|
|
-o -name '*.rb' -o -name '*.php' -o -name '*.c' \
|
|
-o -name '*.cpp' \))
|
|
fi
|
|
echo "Corpus dir: $(ls "$dest" | wc -l) files"
|
|
|
|
- name: Choose fuzz duration
|
|
id: budget
|
|
run: |
|
|
if [ "${{ github.event_name }}" = "schedule" ] || [ "${{ github.event_name }}" = "workflow_dispatch" ]; then
|
|
echo "seconds=18000" >> "$GITHUB_OUTPUT"
|
|
else
|
|
echo "seconds=600" >> "$GITHUB_OUTPUT"
|
|
fi
|
|
|
|
- name: Run fuzz target
|
|
run: |
|
|
cargo fuzz run --target x86_64-unknown-linux-gnu ${{ matrix.target }} -- \
|
|
-max_total_time=${{ steps.budget.outputs.seconds }} \
|
|
-max_len=65536 \
|
|
-timeout=60 \
|
|
-rss_limit_mb=8192 \
|
|
-dict=fuzz/dict/all.dict
|
|
|
|
- name: Upload crash artifacts
|
|
if: failure()
|
|
uses: actions/upload-artifact@v7
|
|
with:
|
|
name: fuzz-artifacts-${{ matrix.target }}-${{ github.run_id }}
|
|
path: fuzz/artifacts/${{ matrix.target }}/
|
|
if-no-files-found: ignore
|
|
retention-days: 14
|
|
|
|
harness-fuzz:
|
|
name: harness-fuzz-${{ matrix.cap }}
|
|
runs-on: ubuntu-latest
|
|
# Run only on schedule and manual dispatch — 50 k iterations per cap is
|
|
# too slow for PR checks but is the right cadence for weekly corpus growth.
|
|
if: github.event_name == 'schedule' || github.event_name == 'workflow_dispatch'
|
|
strategy:
|
|
fail-fast: false
|
|
matrix:
|
|
include:
|
|
- cap: sql_query
|
|
harness: tests/dynamic_fixtures/python/sqli_positive.py
|
|
- cap: code_exec
|
|
harness: tests/dynamic_fixtures/python/cmdi_positive.py
|
|
- cap: file_io
|
|
harness: tests/dynamic_fixtures/python/fileio_positive.py
|
|
- cap: ssrf
|
|
harness: tests/dynamic_fixtures/python/ssrf_positive.py
|
|
- cap: html_escape
|
|
harness: tests/dynamic_fixtures/python/xss_positive.py
|
|
steps:
|
|
- uses: actions/checkout@v6
|
|
|
|
- uses: actions-rust-lang/setup-rust-toolchain@v1
|
|
with:
|
|
cache: true
|
|
cache-workspaces: |
|
|
.
|
|
fuzz/dynamic_corpus
|
|
|
|
- uses: actions/setup-node@v6
|
|
with:
|
|
node-version: 20
|
|
cache: npm
|
|
cache-dependency-path: frontend/package-lock.json
|
|
|
|
- name: Build frontend
|
|
working-directory: frontend
|
|
run: |
|
|
npm ci
|
|
npm run build
|
|
|
|
- name: Build nyx-dynamic-corpus
|
|
working-directory: fuzz/dynamic_corpus
|
|
run: cargo build
|
|
|
|
- uses: actions/setup-python@v6
|
|
with:
|
|
python-version: "3.x"
|
|
|
|
- name: Run harness fuzzer — ${{ matrix.cap }}
|
|
run: |
|
|
fuzz/dynamic_corpus/target/debug/nyx-dynamic-corpus run \
|
|
--cap ${{ matrix.cap }} \
|
|
--spec-hash "ci-${{ matrix.cap }}" \
|
|
--harness-cmd "python3 ${{ matrix.harness }}" \
|
|
--iterations 50000 \
|
|
--output fuzz-discovered
|
|
|
|
- name: Upload discovered candidates
|
|
if: always()
|
|
uses: actions/upload-artifact@v7
|
|
with:
|
|
name: harness-fuzz-${{ matrix.cap }}-${{ github.run_id }}
|
|
path: fuzz-discovered/
|
|
if-no-files-found: ignore
|
|
retention-days: 30
|