From d0bb614965ed220ecb5e346193c701e61f270c1c Mon Sep 17 00:00:00 2001 From: pshu Date: Wed, 17 Jun 2026 09:48:22 +0800 Subject: [PATCH 1/9] test(fuzz): drive async resolve, widen options, add nightly CI The fuzz target silently became a no-op when `resolve` turned async: the returned future was dropped instead of awaited, so the resolution algorithm never ran. Drive it with `block_on`, vary a few resolver options (condition names, extensions, alias, symlinks) from the input for wider coverage, and run it nightly via a scheduled workflow. --- .github/workflows/fuzz.yml | 58 +++++++++++++++++++++++++++++++++++ fuzz/Cargo.toml | 1 + fuzz/fuzz_targets/resolver.rs | 44 +++++++++++++++++++++----- 3 files changed, 96 insertions(+), 7 deletions(-) create mode 100644 .github/workflows/fuzz.yml diff --git a/.github/workflows/fuzz.yml b/.github/workflows/fuzz.yml new file mode 100644 index 00000000..16bf1dab --- /dev/null +++ b/.github/workflows/fuzz.yml @@ -0,0 +1,58 @@ +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 + 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 + + - uses: cargo-bins/cargo-binstall@d125de8b4538541574fd9357b6feb61c8486464b # main + - run: cargo binstall --no-confirm 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 diff --git a/fuzz/Cargo.toml b/fuzz/Cargo.toml index 6d0377b9..04ea6578 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.12" 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)); }); From 2122994664ac1e58d11bf5fe304af078bed4939e Mon Sep 17 00:00:00 2001 From: pshu Date: Wed, 17 Jun 2026 10:20:23 +0800 Subject: [PATCH 2/9] ci(fuzz): open an issue when a scheduled run crashes GitHub's built-in failure notification for scheduled workflows only pings the user who last edited the cron, subject to their personal settings, so a nightly fuzz crash can go unnoticed. File a GitHub issue on scheduled-run failure instead (deduped against an already-open one). Manual workflow_dispatch failures are excluded. --- .github/workflows/fuzz.yml | 21 +++++++++++++++++++++ 1 file changed, 21 insertions(+) diff --git a/.github/workflows/fuzz.yml b/.github/workflows/fuzz.yml index 16bf1dab..6c0d1297 100644 --- a/.github/workflows/fuzz.yml +++ b/.github/workflows/fuzz.yml @@ -25,6 +25,9 @@ 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 @@ -56,3 +59,21 @@ jobs: 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: | + # Skip if a crash issue is already open, so a persisting crash isn't re-filed nightly. + if [ "$(gh issue list --state open --search 'Nightly fuzz crash in:title' --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))" \ + --body "The nightly fuzz run found a crash. Download the reproducing input from the **fuzz-artifacts** artifact on the failed run. + + Run: $RUN_URL" From 5fc2f0a55ad8b3de719ef62a3d1e3f9d4aa6fe3e Mon Sep 17 00:00:00 2001 From: pshu Date: Wed, 17 Jun 2026 10:32:52 +0800 Subject: [PATCH 3/9] ci(fuzz): label the crash issue with fuzz-crash Tag the auto-filed crash issue with a fuzz-crash label for filtering, and use that label as the dedup key. The step creates the label idempotently (--force) so it works without pre-creating it in the repo. --- .github/workflows/fuzz.yml | 6 +++++- 1 file changed, 5 insertions(+), 1 deletion(-) diff --git a/.github/workflows/fuzz.yml b/.github/workflows/fuzz.yml index 6c0d1297..736bccfc 100644 --- a/.github/workflows/fuzz.yml +++ b/.github/workflows/fuzz.yml @@ -67,13 +67,17 @@ jobs: 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 --search 'Nightly fuzz crash in:title' --json number --jq 'length')" -gt 0 ]; then + 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" From 1d4dda39eb72f1cfc838b5f02eab9ed29728b265 Mon Sep 17 00:00:00 2001 From: pshu Date: Wed, 17 Jun 2026 10:35:27 +0800 Subject: [PATCH 4/9] ci(fuzz): [temp] trigger on PRs touching fuzz files Lets this PR exercise the Fuzz job in real Actions before merge, since workflow_dispatch/schedule only work once the file is on the default branch. To be removed before merge so the workflow stays nightly-only. --- .github/workflows/fuzz.yml | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/.github/workflows/fuzz.yml b/.github/workflows/fuzz.yml index 736bccfc..5285da4a 100644 --- a/.github/workflows/fuzz.yml +++ b/.github/workflows/fuzz.yml @@ -9,6 +9,11 @@ on: max_total_time: description: "Seconds to run the fuzzer" default: "300" + # TEMP: validate the workflow on this PR; remove before merge to keep it nightly-only. + pull_request: + paths: + - ".github/workflows/fuzz.yml" + - "fuzz/**" concurrency: group: ${{ github.workflow }}-${{ github.ref }} From 96750a3cef0a4abf5cebcc3d7d3ac86cf4e26666 Mon Sep 17 00:00:00 2001 From: pshu Date: Wed, 17 Jun 2026 10:39:36 +0800 Subject: [PATCH 5/9] fix(ci): build cargo-fuzz from source to avoid musl target default MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit cargo-binstall ships a musl binary, so `cargo binstall cargo-fuzz` installed the musl build, which defaults its build target to x86_64-unknown-linux-musl — whose std isn't on the gnu runner, failing with "can't find crate for core". Build cargo-fuzz from source instead. --- .github/workflows/fuzz.yml | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/.github/workflows/fuzz.yml b/.github/workflows/fuzz.yml index 5285da4a..ec0db8d1 100644 --- a/.github/workflows/fuzz.yml +++ b/.github/workflows/fuzz.yml @@ -42,8 +42,9 @@ jobs: toolchain: nightly cache-key: fuzz - - uses: cargo-bins/cargo-binstall@d125de8b4538541574fd9357b6feb61c8486464b # main - - run: cargo binstall --no-confirm cargo-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. From 33b29b5e4adbf0bb39e13ac57364adeb3c756c15 Mon Sep 17 00:00:00 2001 From: pshu Date: Wed, 17 Jun 2026 10:46:56 +0800 Subject: [PATCH 6/9] ci(fuzz): drop the temporary pull_request trigger The PR-triggered run validated the workflow end-to-end (cargo-fuzz install, instrumented build, 300s fuzz, failure-only steps skipping on success). Revert to nightly schedule + manual dispatch only. --- .github/workflows/fuzz.yml | 5 ----- 1 file changed, 5 deletions(-) diff --git a/.github/workflows/fuzz.yml b/.github/workflows/fuzz.yml index ec0db8d1..d154813e 100644 --- a/.github/workflows/fuzz.yml +++ b/.github/workflows/fuzz.yml @@ -9,11 +9,6 @@ on: max_total_time: description: "Seconds to run the fuzzer" default: "300" - # TEMP: validate the workflow on this PR; remove before merge to keep it nightly-only. - pull_request: - paths: - - ".github/workflows/fuzz.yml" - - "fuzz/**" concurrency: group: ${{ github.workflow }}-${{ github.ref }} From 54311e52b4e321e6a3c29008ad38ef24fcf38cc5 Mon Sep 17 00:00:00 2001 From: pshu Date: Wed, 17 Jun 2026 11:00:10 +0800 Subject: [PATCH 7/9] ci(fuzz): run from repo root so the harness sees the real tree The harness resolves against std::env::current_dir(). With working-directory: fuzz that was the near-empty fuzz crate, so the campaign barely exercised real resolution. Running from the repo root (cargo fuzz auto-discovers fuzz/) raised corpus-replay edge coverage from 1156 to 1368. Corpus/artifact paths stay under fuzz/. --- .github/workflows/fuzz.yml | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/.github/workflows/fuzz.yml b/.github/workflows/fuzz.yml index d154813e..ea8b27a4 100644 --- a/.github/workflows/fuzz.yml +++ b/.github/workflows/fuzz.yml @@ -49,8 +49,9 @@ jobs: key: fuzz-corpus-${{ github.run_id }} restore-keys: fuzz-corpus- + # Run from the repo root so the harness's current_dir() is the real + # package tree (package.json, src, fixtures), not the near-empty fuzz crate. - 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 From 6578fdd1bd425b170744052ee7911868033ed49e Mon Sep 17 00:00:00 2001 From: pshu Date: Wed, 17 Jun 2026 11:03:37 +0800 Subject: [PATCH 8/9] ci(fuzz): [temp] re-add pull_request trigger to validate root-cwd run --- .github/workflows/fuzz.yml | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/.github/workflows/fuzz.yml b/.github/workflows/fuzz.yml index ea8b27a4..e557963d 100644 --- a/.github/workflows/fuzz.yml +++ b/.github/workflows/fuzz.yml @@ -9,6 +9,11 @@ on: max_total_time: description: "Seconds to run the fuzzer" default: "300" + # TEMP: validate the workflow on this PR; remove before merge to keep it nightly-only. + pull_request: + paths: + - ".github/workflows/fuzz.yml" + - "fuzz/**" concurrency: group: ${{ github.workflow }}-${{ github.ref }} From 0d83d2ea378cfe07ef7886c6792c82153af2e02f Mon Sep 17 00:00:00 2001 From: pshu Date: Wed, 17 Jun 2026 11:21:35 +0800 Subject: [PATCH 9/9] Revert "ci(fuzz): run from repo root" and drop temp PR trigger Measured the cwd effect rigorously (same binary, same corpus): repo root gives identical coverage to fuzz/ (~1369 edges) but ~14% lower throughput, because random specifiers rarely form valid paths/package names, so the fs-dependent code is unreached regardless of the tree. Keep working-directory: fuzz. Restores the validated-green workflow. --- .github/workflows/fuzz.yml | 8 +------- 1 file changed, 1 insertion(+), 7 deletions(-) diff --git a/.github/workflows/fuzz.yml b/.github/workflows/fuzz.yml index e557963d..d154813e 100644 --- a/.github/workflows/fuzz.yml +++ b/.github/workflows/fuzz.yml @@ -9,11 +9,6 @@ on: max_total_time: description: "Seconds to run the fuzzer" default: "300" - # TEMP: validate the workflow on this PR; remove before merge to keep it nightly-only. - pull_request: - paths: - - ".github/workflows/fuzz.yml" - - "fuzz/**" concurrency: group: ${{ github.workflow }}-${{ github.ref }} @@ -54,9 +49,8 @@ jobs: key: fuzz-corpus-${{ github.run_id }} restore-keys: fuzz-corpus- - # Run from the repo root so the harness's current_dir() is the real - # package tree (package.json, src, fixtures), not the near-empty fuzz crate. - 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