diff --git a/.github/workflows/fuzz.yml b/.github/workflows/fuzz.yml new file mode 100644 index 00000000..d154813e --- /dev/null +++ b/.github/workflows/fuzz.yml @@ -0,0 +1,84 @@ +name: Fuzz + +on: + schedule: + # Nightly short fuzz run at 03:00 UTC + - cron: "0 3 * * *" + workflow_dispatch: + inputs: + max_total_time: + description: "Seconds to run the fuzzer" + default: "300" + +concurrency: + group: ${{ github.workflow }}-${{ github.ref }} + cancel-in-progress: true + +permissions: + contents: read + +defaults: + run: + shell: bash + +jobs: + fuzz: + name: Nightly Fuzz + runs-on: ubuntu-latest + permissions: + contents: read + issues: write # open an issue when a scheduled run finds a crash + steps: + - uses: taiki-e/checkout-action@7d1e50e93dc4fb3bba58f85018fadf77898aee8b # v1.4.2 + + - uses: actions-rust-lang/setup-rust-toolchain@46268bd060767258de96ed93c1251119784f2ab6 # v1.16.1 + with: + # cargo-fuzz requires a nightly toolchain + toolchain: nightly + cache-key: fuzz + + # Build from source: the binstall musl binary of cargo-fuzz defaults its + # build target to musl, whose std isn't installed on the gnu runner. + - run: cargo install cargo-fuzz + + # Carry the corpus across runs so nightly fuzzing keeps making progress + # instead of starting cold every night. + - uses: actions/cache@5a3ec84eff668545956fd18022155c47e93e2684 # v4.2.3 + with: + path: fuzz/corpus + key: fuzz-corpus-${{ github.run_id }} + restore-keys: fuzz-corpus- + + - name: Run fuzzer + working-directory: fuzz + run: cargo +nightly fuzz run --sanitizer none resolver -- -only_ascii=1 -max_total_time=${{ inputs.max_total_time || '300' }} + + - name: Upload crash artifacts + if: failure() + uses: actions/upload-artifact@ea165f8d65b6e75b540449e92b4886f43607fa02 # v4.6.2 + with: + name: fuzz-artifacts + path: fuzz/artifacts + if-no-files-found: ignore + + # Only scheduled runs file an issue; manual workflow_dispatch failures don't. + - name: Open issue on scheduled crash + if: failure() && github.event_name == 'schedule' + env: + GH_TOKEN: ${{ github.token }} + RUN_URL: ${{ github.server_url }}/${{ github.repository }}/actions/runs/${{ github.run_id }} + run: | + # Idempotent: creates the label, or updates it if it already exists. + gh label create fuzz-crash --force --color B60205 \ + --description "Crash found by the nightly fuzz run" + # Skip if a crash issue is already open, so a persisting crash isn't re-filed nightly. + if [ "$(gh issue list --state open --label fuzz-crash --json number --jq 'length')" -gt 0 ]; then + echo "An open fuzz crash issue already exists; skipping." + exit 0 + fi + gh issue create \ + --title "Nightly fuzz crash ($(date -u +%F))" \ + --label fuzz-crash \ + --body "The nightly fuzz run found a crash. Download the reproducing input from the **fuzz-artifacts** artifact on the failed run. + + Run: $RUN_URL" diff --git a/fuzz/Cargo.toml b/fuzz/Cargo.toml index bc2e09f1..417b2efa 100644 --- a/fuzz/Cargo.toml +++ b/fuzz/Cargo.toml @@ -19,5 +19,6 @@ doc = false bench = false [dependencies] +futures = "0.3" libfuzzer-sys = "0.4.13" rspack_resolver = { path = ".." } diff --git a/fuzz/fuzz_targets/resolver.rs b/fuzz/fuzz_targets/resolver.rs index 99623dbf..7e83b605 100644 --- a/fuzz/fuzz_targets/resolver.rs +++ b/fuzz/fuzz_targets/resolver.rs @@ -1,14 +1,44 @@ #![no_main] use libfuzzer_sys::fuzz_target; -use rspack_resolver::Resolver; +use rspack_resolver::{AliasValue, ResolveOptions, Resolver}; fuzz_target!(|data: &[u8]| { - if let Ok(s) = std::str::from_utf8(data) { - if s.chars().all(|s| !s.is_control()) { - let resolver = Resolver::default(); - let cwd = std::env::current_dir().unwrap(); - let _ = resolver.resolve(cwd, &s); - } + // First byte drives a few resolver-option toggles; the rest is the specifier. + let Some((&cfg, rest)) = data.split_first() else { + return; + }; + let Ok(specifier) = std::str::from_utf8(rest) else { + return; + }; + if specifier.chars().any(char::is_control) { + return; } + + let condition_names = if cfg & 0b001 == 0 { + vec!["node".into(), "import".into()] + } else { + vec!["node".into(), "require".into()] + }; + let extensions = if cfg & 0b010 == 0 { + vec![".js".into(), ".json".into(), ".node".into()] + } else { + vec![".ts".into(), ".tsx".into(), ".js".into()] + }; + let alias = if cfg & 0b100 == 0 { + vec![] + } else { + vec![("@".into(), vec![AliasValue::Path("./src".into())])] + }; + + let resolver = Resolver::new(ResolveOptions { + condition_names, + extensions, + alias, + symlinks: cfg & 0b1000 == 0, + ..ResolveOptions::default() + }); + let cwd = std::env::current_dir().unwrap(); + // `resolve` is async; the future must be driven or the body is a no-op. + let _ = futures::executor::block_on(resolver.resolve(cwd, specifier)); });