Skip to content

Commit 168982b

Browse files
feat[fuzz]: add WASM fuzzer support for wasmfuzz (#5575)
## Summary - Add support for building the array_ops fuzzer as a WASM binary for wasmfuzz - Native fuzzer targets (array_ops, file_io) continue to work unchanged - Add CI workflow to test WASM fuzzer for array_ops 🤖 Generated with [Claude Code](https://claude.com/claude-code) --------- Signed-off-by: Joe Isaacs <[email protected]> Co-authored-by: Claude <[email protected]>
1 parent 89b236b commit 168982b

File tree

11 files changed

+603
-241
lines changed

11 files changed

+603
-241
lines changed

.github/workflows/wasm-fuzz.yml

Lines changed: 88 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,88 @@
1+
name: WASM Fuzz
2+
3+
on:
4+
workflow_dispatch:
5+
schedule:
6+
# Run daily at 2 AM UTC
7+
- cron: "0 2 * * *"
8+
9+
permissions:
10+
contents: read
11+
12+
env:
13+
CARGO_TERM_COLOR: always
14+
15+
jobs:
16+
wasm-fuzz:
17+
name: "Build & Fuzz WASM"
18+
runs-on: ubuntu-latest
19+
timeout-minutes: 270
20+
steps:
21+
- uses: actions/checkout@v6
22+
23+
- uses: ./.github/actions/setup-rust
24+
with:
25+
repo-token: ${{ secrets.GITHUB_TOKEN }}
26+
toolchain: nightly
27+
targets: "wasm32-wasip1"
28+
components: "rust-src"
29+
30+
- name: Build WASM fuzz target
31+
run: |
32+
cargo +nightly build \
33+
--manifest-path fuzz/Cargo.toml \
34+
--target wasm32-wasip1 \
35+
--no-default-features \
36+
--features wasmfuzz \
37+
--release \
38+
--bin array_ops_wasm
39+
40+
- name: Install wabt tools
41+
run: sudo apt-get update && sudo apt-get install -y wabt
42+
43+
- name: Verify WASM exports
44+
run: |
45+
echo "Checking for required wasmfuzz exports..."
46+
wasm-objdump -x target/wasm32-wasip1/release/array_ops_wasm.wasm | grep -E "(LLVMFuzzerTestOneInput|wasmfuzz_malloc|wasmfuzz_free)"
47+
48+
- name: Install wasmfuzz
49+
run: cargo install --git https://github.com/CISPA-SysSec/wasmfuzz --locked
50+
51+
- name: Run wasmfuzz
52+
id: fuzz
53+
run: |
54+
mkdir -p corpus-wasm
55+
# Capture exit code - wasmfuzz exits with error on crash
56+
set +e
57+
wasmfuzz fuzz \
58+
--timeout=4h \
59+
--cores 2 \
60+
--dir corpus-wasm/ \
61+
target/wasm32-wasip1/release/array_ops_wasm.wasm
62+
FUZZ_EXIT=$?
63+
set -e
64+
echo "exit_code=$FUZZ_EXIT" >> $GITHUB_OUTPUT
65+
if [ $FUZZ_EXIT -ne 0 ]; then
66+
echo "crash_found=true" >> $GITHUB_OUTPUT
67+
fi
68+
69+
- name: Replay crash inputs
70+
if: steps.fuzz.outputs.crash_found == 'true'
71+
run: |
72+
echo "::error::Crash found during fuzzing! Replaying inputs for debug output..."
73+
for input in corpus-wasm/*; do
74+
echo "=== Replaying: $input ==="
75+
wasmfuzz run --trace target/wasm32-wasip1/release/array_ops_wasm.wasm "$input" || true
76+
done
77+
78+
- name: Upload corpus
79+
if: always()
80+
uses: actions/upload-artifact@v4
81+
with:
82+
name: corpus-wasm
83+
path: corpus-wasm/
84+
retention-days: 30
85+
86+
- name: Fail if crash found
87+
if: steps.fuzz.outputs.crash_found == 'true'
88+
run: exit 1

Cargo.lock

Lines changed: 1 addition & 0 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

fuzz/.cargo/config.toml

Lines changed: 20 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,20 @@
1+
# Configuration for wasmfuzz builds
2+
# These flags are based on wasmfuzz's build-rust-harness.py
3+
4+
[target.wasm32-wasip1]
5+
rustflags = [
6+
# Embed source for coverage reports
7+
"-Zembed-source=yes",
8+
"-Zdwarf-version=5",
9+
"-g",
10+
# Static linking
11+
"-Ctarget-feature=+crt-static",
12+
# lime1 CPU for wasmfuzz compatibility
13+
"-Ctarget-cpu=lime1",
14+
# Reactor mode for persistent fuzzing
15+
"-Zwasi-exec-model=reactor",
16+
]
17+
18+
[unstable]
19+
# Required for building std to wasm32-wasip1
20+
build-std = ["std", "panic_abort"]

fuzz/Cargo.toml

Lines changed: 26 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -17,24 +17,36 @@ version = { workspace = true }
1717
[package.metadata]
1818
cargo-fuzz = true
1919

20+
[features]
21+
default = ["native"]
22+
native = ["libfuzzer-sys", "zstd", "vortex-file"]
23+
wasmfuzz = []
24+
zstd = ["vortex-layout/zstd"]
25+
2026
[dependencies]
27+
# Always needed - arbitrary is used for input generation
28+
arbitrary = { workspace = true }
2129
itertools = { workspace = true }
22-
libfuzzer-sys = { workspace = true }
2330
strum = { workspace = true, features = ["derive"] }
24-
vortex = { workspace = true }
31+
32+
# Vortex core - no default features for WASM compatibility (files feature pulls in tokio)
33+
vortex = { path = "../vortex", default-features = false }
2534
vortex-array = { workspace = true, features = ["arbitrary", "test-harness"] }
2635
vortex-btrblocks = { workspace = true }
2736
vortex-buffer = { workspace = true }
2837
vortex-dtype = { workspace = true, features = ["arbitrary"] }
2938
vortex-error = { workspace = true }
30-
vortex-file = { workspace = true, features = ["tokio", "zstd"] }
3139
vortex-io = { workspace = true }
32-
vortex-layout = { workspace = true, features = ["zstd"] }
40+
vortex-layout = { workspace = true }
3341
vortex-mask = { workspace = true }
3442
vortex-scalar = { workspace = true, features = ["arbitrary"] }
3543
vortex-session = { workspace = true }
3644
vortex-utils = { workspace = true }
3745

46+
# Native-only: libfuzzer harness and file IO (won't compile to WASM)
47+
libfuzzer-sys = { workspace = true, optional = true }
48+
vortex-file = { workspace = true, optional = true }
49+
3850
[lints]
3951
workspace = true
4052

@@ -44,10 +56,20 @@ doc = false
4456
name = "array_ops"
4557
path = "fuzz_targets/array_ops.rs"
4658
test = false
59+
required-features = ["native"]
4760

4861
[[bin]]
4962
bench = false
5063
doc = false
5164
name = "file_io"
5265
path = "fuzz_targets/file_io.rs"
5366
test = false
67+
required-features = ["native"]
68+
69+
[[bin]]
70+
bench = false
71+
doc = false
72+
name = "array_ops_wasm"
73+
path = "fuzz_targets/array_ops_wasm.rs"
74+
test = false
75+
required-features = ["wasmfuzz"]

fuzz/build-wasmfuzz.sh

Lines changed: 53 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,53 @@
1+
# SPDX-License-Identifier: Apache-2.0
2+
# SPDX-FileCopyrightText: Copyright the Vortex contributors
3+
4+
#!/bin/bash
5+
# Build the vortex-fuzz crate for wasmfuzz
6+
#
7+
# This script builds the fuzzer binary for the wasm32-wasip1 target,
8+
# which can then be used with wasmfuzz for coverage-guided fuzzing.
9+
#
10+
# Prerequisites:
11+
# - Nightly Rust toolchain (for -Z flags)
12+
# - wasm32-wasip1 target: rustup +nightly target add wasm32-wasip1
13+
# - wasmfuzz: cargo install --git https://github.com/CISPA-SysSec/wasmfuzz
14+
#
15+
# Usage:
16+
# ./build-wasmfuzz.sh
17+
#
18+
# After building, run with wasmfuzz:
19+
# wasmfuzz fuzz --timeout=1h --cores 8 --dir corpus/ \
20+
# target/wasm32-wasip1/release/array_ops_wasm.wasm
21+
22+
set -e
23+
24+
SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
25+
cd "$SCRIPT_DIR/.."
26+
27+
echo "Building vortex-fuzz for wasm32-wasip1..."
28+
29+
# Build the WASM binary with nightly for -Z flags (build-std, embed-source, etc.)
30+
rustup run nightly cargo build \
31+
--manifest-path fuzz/Cargo.toml \
32+
--target wasm32-wasip1 \
33+
--no-default-features \
34+
--features wasmfuzz \
35+
--release \
36+
--bin array_ops_wasm
37+
38+
WASM_OUTPUT="target/wasm32-wasip1/release/array_ops_wasm.wasm"
39+
40+
if [ -f "$WASM_OUTPUT" ]; then
41+
echo ""
42+
echo "Build successful!"
43+
echo "Output: $WASM_OUTPUT"
44+
echo ""
45+
echo "To run with wasmfuzz:"
46+
echo " wasmfuzz fuzz --timeout=1h --cores 8 --dir corpus/ $WASM_OUTPUT"
47+
echo ""
48+
echo "See: https://github.com/CISPA-SysSec/wasmfuzz"
49+
else
50+
echo "Build completed but .wasm output not found at expected location."
51+
echo "Check target/wasm32-wasip1/release/ for outputs:"
52+
ls -la target/wasm32-wasip1/release/ 2>/dev/null || echo "Directory not found"
53+
fi

0 commit comments

Comments
 (0)