Files
Workflow config file is invalid. Please check your config file: invalid jobs: yaml: unmarshal errors: line 107: cannot unmarshal !!str `${{ !ma...` into bool
Sylvestre Ledru 9598faf708 fuzz: add differential fuzzer against GNU grep
Add a cargo-fuzz harness (fuzz_grep) that runs uu_grep and GNU grep on
the same generated args/input and panics on any divergence, using the
vendored uufuzz crate (adapted from uutils/coreutils to depend on
crates.io uucore rather than a path).

A CI workflow (.github/workflows/fuzzing.yml) builds the uufuzz
examples, builds the fuzzer, and runs it for 60s. fuzz_grep is marked
should_pass: false / continue-on-error since it currently surfaces real
GNU-compatibility gaps (e.g. uu_grep rejects a repeated -m, GNU accepts).
2026-06-02 10:13:13 +02:00

163 lines
6.2 KiB
YAML

name: Fuzzing
# spell-checker:ignore (people) taiki-e
# spell-checker:ignore (misc) fuzzer uufuzz
env:
CARGO_INCREMENTAL: "0"
on:
push:
branches: [ main, master ]
pull_request:
branches: [ main, master ]
permissions:
contents: read # to fetch code (actions/checkout)
# End the current execution if there is a new changeset in the PR.
concurrency:
group: ${{ github.workflow }}-${{ github.ref }}
cancel-in-progress: ${{ github.ref != 'refs/heads/main' }}
jobs:
uufuzz-examples:
name: Build and test uufuzz examples
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
with:
persist-credentials: false
- name: Install Rust
uses: dtolnay/rust-toolchain@stable
- name: Build uufuzz library
run: |
cd fuzz/uufuzz
cargo build --release
- name: Run uufuzz tests
run: |
cd fuzz/uufuzz
cargo test --lib
- name: Build and run uufuzz examples
run: |
cd fuzz/uufuzz
echo "Building all examples..."
cargo build --examples --release
# Run all examples except integration_testing (which has FD issues in CI)
for example in examples/*.rs; do
example_name=$(basename "$example" .rs)
if [ "$example_name" != "integration_testing" ]; then
cargo run --example "$example_name" --release
fi
done
fuzz-build:
name: Build the fuzzers
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
with:
persist-credentials: false
- name: Install Rust
uses: dtolnay/rust-toolchain@stable
- name: Install `cargo-fuzz`
uses: taiki-e/install-action@v2
with:
tool: cargo-fuzz
- name: Emulate a nightly toolchain
run: |
echo "RUSTC_BOOTSTRAP=1" >> "${GITHUB_ENV}"
- name: Run `cargo-fuzz build`
# Force the correct target
# https://github.com/rust-fuzz/cargo-fuzz/issues/398
run: cargo fuzz build --target $(rustc --print host-tuple)
fuzz-run:
needs: fuzz-build
name: Fuzz
runs-on: ubuntu-latest
timeout-minutes: 5
env:
RUN_FOR: 60
strategy:
fail-fast: false
matrix:
test-target:
# fuzz_grep is a differential fuzzer against GNU grep; it currently
# surfaces compatibility differences, so it is not expected to pass yet.
- { name: fuzz_grep, should_pass: false }
steps:
- uses: actions/checkout@v4
with:
persist-credentials: false
- name: Install Rust
uses: dtolnay/rust-toolchain@stable
- name: Install `cargo-fuzz`
uses: taiki-e/install-action@v2
with:
tool: cargo-fuzz
- name: Emulate a nightly toolchain
run: |
echo "RUSTC_BOOTSTRAP=1" >> "${GITHUB_ENV}"
- name: Run ${{ matrix.test-target.name }} for ${{ env.RUN_FOR }} seconds
id: run_fuzzer
shell: bash
continue-on-error: ${{ !matrix.test-target.should_pass }}
run: |
mkdir -p fuzz/stats
STATS_FILE="fuzz/stats/${{ matrix.test-target.name }}.txt"
# Force the correct target
# https://github.com/rust-fuzz/cargo-fuzz/issues/398
cargo fuzz run --target $(rustc --print host-tuple) ${{ matrix.test-target.name }} -- -max_total_time=${{ env.RUN_FOR }} -timeout=${{ env.RUN_FOR }} -detect_leaks=0 -print_final_stats=1 2>&1 | tee "$STATS_FILE"
# Save should_pass value for later inspection
echo "${{ matrix.test-target.should_pass }}" > "fuzz/stats/${{ matrix.test-target.name }}.should_pass"
# Print stats to job output for immediate visibility
echo "----------------------------------------"
echo "FUZZING STATISTICS FOR ${{ matrix.test-target.name }}"
echo "----------------------------------------"
echo "Runs: $(grep -q "stat::number_of_executed_units" "$STATS_FILE" && grep "stat::number_of_executed_units" "$STATS_FILE" | awk '{print $2}' || echo "unknown")"
echo "Execution Rate: $(grep -q "stat::average_exec_per_sec" "$STATS_FILE" && grep "stat::average_exec_per_sec" "$STATS_FILE" | awk '{print $2}' || echo "unknown") execs/sec"
echo "New Units: $(grep -q "stat::new_units_added" "$STATS_FILE" && grep "stat::new_units_added" "$STATS_FILE" | awk '{print $2}' || echo "unknown")"
echo "Expected: ${{ matrix.test-target.should_pass }}"
if grep -q "SUMMARY: " "$STATS_FILE"; then
echo "Status: $(grep "SUMMARY: " "$STATS_FILE" | head -1)"
else
echo "Status: Completed"
fi
echo "----------------------------------------"
# Add summary to GitHub step summary
echo "### Fuzzing Results for ${{ matrix.test-target.name }}" >> "$GITHUB_STEP_SUMMARY"
echo "" >> "$GITHUB_STEP_SUMMARY"
echo "| Metric | Value |" >> "$GITHUB_STEP_SUMMARY"
echo "|--------|-------|" >> "$GITHUB_STEP_SUMMARY"
if grep -q "stat::number_of_executed_units" "$STATS_FILE"; then
echo "| Runs | $(grep "stat::number_of_executed_units" "$STATS_FILE" | awk '{print $2}') |" >> "$GITHUB_STEP_SUMMARY"
fi
if grep -q "stat::average_exec_per_sec" "$STATS_FILE"; then
echo "| Execution Rate | $(grep "stat::average_exec_per_sec" "$STATS_FILE" | awk '{print $2}') execs/sec |" >> "$GITHUB_STEP_SUMMARY"
fi
if grep -q "stat::new_units_added" "$STATS_FILE"; then
echo "| New Units | $(grep "stat::new_units_added" "$STATS_FILE" | awk '{print $2}') |" >> "$GITHUB_STEP_SUMMARY"
fi
echo "| Should pass | ${{ matrix.test-target.should_pass }} |" >> "$GITHUB_STEP_SUMMARY"
if grep -q "SUMMARY: " "$STATS_FILE"; then
echo "| Status | $(grep "SUMMARY: " "$STATS_FILE" | head -1) |" >> "$GITHUB_STEP_SUMMARY"
else
echo "| Status | Completed |" >> "$GITHUB_STEP_SUMMARY"
fi
echo "" >> "$GITHUB_STEP_SUMMARY"
- name: Upload Stats
if: always()
uses: actions/upload-artifact@v4
with:
name: fuzz-stats-${{ matrix.test-target.name }}
path: |
fuzz/stats/${{ matrix.test-target.name }}.txt
fuzz/stats/${{ matrix.test-target.name }}.should_pass
retention-days: 5