Skip to content

Implementing C++ and Rust wrappers packages for native bindings with parsing and reading #63

Implementing C++ and Rust wrappers packages for native bindings with parsing and reading

Implementing C++ and Rust wrappers packages for native bindings with parsing and reading #63

Workflow file for this run

name: Fuzz
on:
pull_request:
branches: [master, develop]
schedule:
# Nightly. Coverage-guided fuzzing is only useful with sustained runtime, so this is where the
# real budget goes; the `check` job below is what gates pull requests.
- cron: '0 3 * * *'
workflow_dispatch:
inputs:
seconds:
description: Seconds to fuzz each target
required: false
default: '600'
concurrency:
group: fuzz-${{ github.ref }}
cancel-in-progress: true
permissions:
contents: read
jobs:
libfuzzer:
name: libFuzzer (${{ matrix.target }})
# Too slow to gate a pull request; PRs get the `check` job instead.
if: github.event_name != 'pull_request'
runs-on: ubuntu-latest
strategy:
fail-fast: false
matrix:
target: [xlsx, xlsx-memory, xlsb, xlsb-memory, xls, csv, csv-sniff]
steps:
- name: Checkout
uses: actions/checkout@v7
- name: Setup .NET
uses: actions/setup-dotnet@v6
with:
dotnet-version: '10.0.x'
- name: Install SharpFuzz CLI
run: dotnet tool install --global SharpFuzz.CommandLine
- name: Build libfuzzer-dotnet
run: |
wget -q https://raw.githubusercontent.com/Metalnem/libfuzzer-dotnet/master/libfuzzer-dotnet.cc
clang -fsanitize=fuzzer libfuzzer-dotnet.cc -o libfuzzer-dotnet
chmod +x libfuzzer-dotnet
- name: Publish harness
run: |
dotnet publish tests/ExcelReader.Fuzz/ExcelReader.Fuzz.csproj -c Release -o fuzz-out
chmod +x fuzz-out/ExcelReader.Fuzz
# MUST run before instrumentation. Seed generation drives the writers through the ordinary
# runtime, and an instrumented assembly writes coverage into a shared-memory region that only
# exists when libfuzzer-dotnet is the one launching the process — running it standalone kills
# the process with an AccessViolationException. Do not reorder these two steps.
- name: Generate seed corpus
run: |
mkdir -p corpus
dotnet fuzz-out/ExcelReader.Fuzz.dll seeds corpus
# Excel-authored files make far better seeds than anything we can synthesise.
cp RealExcel.xlsb RealExcel.xlsx corpus/ 2>/dev/null || true
cp tests/ExcelReader.Tests/data/*.xlsx corpus/ 2>/dev/null || true
# Inputs that previously crashed a target: keeping them in the corpus guards those paths.
cp tests/ExcelReader.Fuzz/corpus/* corpus/ 2>/dev/null || true
ls -la corpus
# SharpFuzz rewrites the assembly under test to emit coverage. Only ExcelReader.Core is
# instrumented: the harness itself is not the code under test, and instrumenting it would
# dilute the coverage signal the engine steers by. Everything after this step must run under
# libfuzzer-dotnet.
- name: Instrument ExcelReader.Core
run: sharpfuzz fuzz-out/ExcelReader.Core.dll
# Two things this invocation gets right, both of which fail confusingly otherwise:
# * target_path is the published apphost, not `dotnet`. libfuzzer-dotnet passes --target_args
# through as a SINGLE argv entry rather than splitting on spaces, so
# `--target_path=dotnet --target_args="Foo.dll csv"` hands `dotnet` one bogus path; it
# prints usage and exits, surfacing as "short read: expected 4 bytes, got 0 bytes".
# * the target is selected via FUZZ_TARGET, not --target_args. SharpFuzz's
# Fuzzer.LibFuzzer.Run parses argv itself and treats a lone argument as a single input file
# to replay, so our own argument would be swallowed as a corpus path.
- name: Fuzz
env:
FUZZ_TARGET: ${{ matrix.target }}
run: |
./libfuzzer-dotnet \
--target_path=fuzz-out/ExcelReader.Fuzz \
corpus \
-max_total_time=${{ github.event.inputs.seconds || '600' }} \
-max_len=131072 \
-timeout=25 \
-rss_limit_mb=2048 \
-print_final_stats=1 \
-artifact_prefix=crash-
- name: Upload crashes
if: failure()
uses: actions/upload-artifact@v7
with:
name: fuzz-crashes-${{ matrix.target }}
path: |
crash-*
timeout-*
oom-*
if-no-files-found: ignore
check:
name: Harness smoke check
runs-on: ubuntu-latest
steps:
- name: Checkout
uses: actions/checkout@v7
- name: Setup .NET
uses: actions/setup-dotnet@v6
with:
dotnet-version: '10.0.x'
- name: Build
run: dotnet build tests/ExcelReader.Fuzz/ExcelReader.Fuzz.csproj -c Release
- name: Generate corpus
run: |
mkdir -p corpus
dotnet run --project tests/ExcelReader.Fuzz -c Release --no-build -- seeds corpus
cp RealExcel.xlsb RealExcel.xlsx corpus/ 2>/dev/null || true
cp tests/ExcelReader.Tests/data/*.xlsx corpus/ 2>/dev/null || true
cp tests/ExcelReader.Fuzz/corpus/* corpus/ 2>/dev/null || true
# No engine, no native tooling: runs every target over the corpus plus deterministic mutations,
# and self-checks the oracle first so a green run cannot mean "the oracle accepts everything".
- name: Run every target over mutated corpus
run: dotnet run --project tests/ExcelReader.Fuzz -c Release --no-build -- check corpus 200 1