Skip to content

Fuzzing

Fuzzing #750

Workflow file for this run

name: Fuzzing
on:
push:
branches: [main]
pull_request:
branches: [main]
schedule:
# Run fuzzing weekly on Sundays at midnight UTC
- cron: "0 0 * * 0"
workflow_dispatch:
inputs:
fuzz_duration:
description: "Fuzzing duration in seconds per target (push=120, PR=300, dispatch default=600)"
required: false
default: "600"
permissions:
contents: read
jobs:
atheris-fuzz:
name: Python Fuzzing (Atheris) - ${{ matrix.shard_name }}
runs-on: ubuntu-latest
timeout-minutes: 90
strategy:
fail-fast: false
matrix:
include:
- shard_name: Shard 1/3
shard_id: 1
shard_key: shard-1-of-3
- shard_name: Shard 2/3
shard_id: 2
shard_key: shard-2-of-3
- shard_name: Shard 3/3
shard_id: 3
shard_key: shard-3-of-3
env:
MEOW_TEST_MODE: "1"
MEOW_PRODUCTION_MODE: "0" # Required alongside MEOW_TEST_MODE to allow export_key() in tests
MEOW_CRYPTO_BACKEND: rust
steps:
- uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1
- name: Set up Python
uses: actions/setup-python@5fda3b95a4ea91299a34e894583c3862153e4b97 # v7.0.0
with:
python-version: "3.12"
- uses: dtolnay/rust-toolchain@d0592fe69e35bc8f12e3dbaf9ad2694d976cb8e3 # stable
with:
toolchain: stable
- name: Install dependencies
# Editable install (-e .) for local source - can't hash-pin
run: |
sudo apt-get update
sudo apt-get install -y \
libzbar0 libgl1 libglib2.0-0 \
build-essential pkg-config libssl-dev \
libpcsclite-dev libudev-dev
pip install --require-hashes -r requirements-pip.lock
pip install --require-hashes -r requirements-ci.lock
pip install --require-hashes -r requirements.lock
pip install --require-hashes -r requirements-dev.lock || true
pip install --no-deps -e .
- name: Build and install Rust crypto backend
run: |
cd rust_crypto
maturin build --release --out dist
python -m pip install --force-reinstall --no-deps dist/*.whl
cd ..
- name: Create corpus directories
run: |
mkdir -p fuzz/corpus/manifest
mkdir -p fuzz/corpus/qr
mkdir -p fuzz/corpus/fountain
mkdir -p fuzz/crashes
- name: Seed corpus with valid samples
run: |
python fuzz/seed_corpus.py
- name: Set fuzz duration
run: |
echo "DURATION=${{ github.event_name == 'push' && '120' || github.event.inputs.fuzz_duration || '300' }}" >> "$GITHUB_ENV"
- name: Run Atheris shard targets
run: |
run_target() {
local label="$1"
shift
echo "=== $label ==="
timeout "${DURATION}s" "$@" || RC=$?
RC=${RC:-0}
[ "$RC" -eq 0 ] || [ "$RC" -eq 124 ] || exit "$RC"
unset RC
}
case "${{ matrix.shard_id }}" in
1)
run_target "Fuzz manifest parser" python fuzz/fuzz_manifest.py fuzz/corpus/manifest
run_target "Fuzz fountain decoder" python fuzz/fuzz_fountain.py fuzz/corpus/fountain
run_target "Fuzz crypto operations" python fuzz/fuzz_crypto.py fuzz/corpus/manifest
run_target "Fuzz guard page memory safety" python fuzz/fuzz_windows_guard.py
run_target "Fuzz mouse gesture auth" python fuzz/fuzz_mouse_gesture.py
run_target "Fuzz tamper detection" python fuzz/fuzz_tamper_detection.py
run_target "Fuzz adversarial stego rotation" python fuzz/fuzz_adversarial_stego.py
;;
2)
run_target "Fuzz ratchet (frame encrypt/decrypt)" python fuzz/fuzz_ratchet.py
run_target "Fuzz manifest signing (Ed25519/ML-DSA)" python fuzz/fuzz_manifest_signing.py
run_target "Fuzz PQ ratchet beacon" python fuzz/fuzz_pq_ratchet_beacon.py
run_target "Fuzz master ratchet" python fuzz/fuzz_master_ratchet.py
run_target "Fuzz Schrodinger encode/decode" python fuzz/fuzz_schrodinger.py
run_target "Fuzz crypto backend (Rust FFI)" python fuzz/fuzz_crypto_backend.py
run_target "Fuzz Shamir secret sharing" python fuzz/fuzz_shamir.py
;;
3)
run_target "Fuzz memory guard operations" python fuzz/fuzz_memory_guard.py
run_target "Fuzz dual stream encoding" python fuzz/fuzz_dual_stream.py
run_target "Fuzz multi-layer stego" python fuzz/fuzz_stego_multilayer.py
run_target "Fuzz PQ hybrid PQXDH (ML-KEM-768/1024 + X25519)" python fuzz/fuzz_pq_hybrid.py
run_target "Fuzz X25519 forward secrecy" python fuzz/fuzz_x25519_fs.py
run_target "Fuzz time-lock duress & content expiry" python fuzz/fuzz_timelock_expiry.py
run_target "Fuzz forensic cleanup" python fuzz/fuzz_forensic_cleanup.py
;;
*)
echo "Unknown shard: ${{ matrix.shard_id }}"
exit 1
;;
esac
# ── Crash gate (MUST fail the build on any crash) ───────────────────
- name: Check for crashes
run: |
if [ -n "$(ls -A fuzz/crashes 2>/dev/null)" ]; then
echo "🚨 Crashes found!"
ls -la fuzz/crashes/
exit 1
else
echo "βœ… No crashes found"
fi
- name: Upload crash artifacts
if: failure()
uses: actions/upload-artifact@043fb46d1a93c77aae656e7c1c64a875d1fc6a0a # v7.0.1
with:
name: fuzz-crashes-${{ matrix.shard_key }}-${{ github.run_id }}
path: fuzz/crashes/
retention-days: 30
afl-fuzz:
name: AFL++ Fuzzing (Native) - ${{ matrix.shard_name }}
runs-on: ubuntu-latest
timeout-minutes: 60
strategy:
fail-fast: false
matrix:
include:
- shard_name: Shard 1/3
shard_id: 1
shard_key: shard-1-of-3
- shard_name: Shard 2/3
shard_id: 2
shard_key: shard-2-of-3
- shard_name: Shard 3/3
shard_id: 3
shard_key: shard-3-of-3
steps:
- uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1
- name: Install AFL++
run: |
sudo apt-get update
sudo apt-get install -y afl++ python3-dev
- name: Set up Python
uses: actions/setup-python@5fda3b95a4ea91299a34e894583c3862153e4b97 # v7.0.0
with:
python-version: "3.12"
- uses: dtolnay/rust-toolchain@d0592fe69e35bc8f12e3dbaf9ad2694d976cb8e3 # stable
with:
toolchain: stable
- name: Install dependencies
# Editable install for local source - can't hash-pin
run: |
sudo apt-get install -y \
libzbar0 \
build-essential pkg-config libssl-dev \
libpcsclite-dev libudev-dev
pip install --require-hashes -r requirements-pip.lock
pip install --require-hashes -r requirements-ci.lock
pip install --require-hashes -r requirements.lock
pip install --no-deps -e ".[dev]"
- name: Build and install Rust crypto backend
run: |
cd rust_crypto
maturin build --release --out dist
python -m pip install --force-reinstall --no-deps dist/*.whl
cd ..
- name: Create corpus
run: |
mkdir -p fuzz/afl-corpus
mkdir -p fuzz/afl-output/${{ matrix.shard_key }}
python fuzz/seed_corpus.py --afl
- name: Run AFL++ shard
run: |
cd fuzz
case "${{ matrix.shard_id }}" in
1)
echo "Running AFL shard 1/3 with default mutation strategy"
;;
2)
echo "Running AFL shard 2/3 with aggressive havoc expansion"
export AFL_EXPAND_HAVOC_NOW=1
;;
3)
echo "Running AFL shard 3/3 with queue shuffling and trim disabled"
export AFL_SHUFFLE_QUEUE=1
export AFL_DISABLE_TRIM=1
;;
*)
echo "Unknown AFL shard: ${{ matrix.shard_id }}"
exit 1
;;
esac
timeout ${{ github.event.inputs.fuzz_duration || '300' }}s \
py-afl-fuzz -i afl-corpus -o afl-output/${{ matrix.shard_key }} -m none -- \
python afl_fuzz_manifest.py || true
- name: Check AFL++ results
run: |
if [ -d "fuzz/afl-output/${{ matrix.shard_key }}/default/crashes" ] && [ -n "$(ls -A fuzz/afl-output/${{ matrix.shard_key }}/default/crashes 2>/dev/null)" ]; then
echo "🚨 AFL++ found crashes!"
ls -la fuzz/afl-output/${{ matrix.shard_key }}/default/crashes/
exit 1
else
echo "βœ… No AFL++ crashes found"
fi
- name: Upload AFL++ results
if: always()
uses: actions/upload-artifact@043fb46d1a93c77aae656e7c1c64a875d1fc6a0a # v7.0.1
with:
name: afl-results-${{ matrix.shard_key }}
path: fuzz/afl-output/${{ matrix.shard_key }}/
retention-days: 7