Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
84 changes: 84 additions & 0 deletions .github/workflows/fuzz.yml
Original file line number Diff line number Diff line change
@@ -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"

Comment thread
stormslowly marked this conversation as resolved.
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"
1 change: 1 addition & 0 deletions fuzz/Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -19,5 +19,6 @@ doc = false
bench = false

[dependencies]
futures = "0.3"
libfuzzer-sys = "0.4.13"
rspack_resolver = { path = ".." }
44 changes: 37 additions & 7 deletions fuzz/fuzz_targets/resolver.rs
Original file line number Diff line number Diff line change
@@ -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();
Comment thread
stormslowly marked this conversation as resolved.
// `resolve` is async; the future must be driven or the body is a no-op.
let _ = futures::executor::block_on(resolver.resolve(cwd, specifier));
});
Loading