Skip to content

Commit ddafdfd

Browse files
committed
wip
Signed-off-by: Joe Isaacs <[email protected]>
1 parent 0dbdbdb commit ddafdfd

File tree

5 files changed

+300
-492
lines changed

5 files changed

+300
-492
lines changed

.github/workflows/wasm-fuzz.yml

Lines changed: 16 additions & 84 deletions
Original file line numberDiff line numberDiff line change
@@ -1,10 +1,6 @@
11
name: WASM Fuzz
22

33
on:
4-
pull_request:
5-
paths:
6-
- "fuzz/**"
7-
- ".github/workflows/wasm-fuzz.yml"
84
workflow_dispatch:
95
inputs:
106
fuzz_duration:
@@ -23,10 +19,10 @@ env:
2319
CARGO_TERM_COLOR: always
2420

2521
jobs:
26-
build-wasm-fuzz:
27-
name: "Build WASM Fuzzer"
22+
wasm-fuzz:
23+
name: "Build & Fuzz WASM"
2824
runs-on: ubuntu-latest
29-
timeout-minutes: 30
25+
timeout-minutes: 270
3026
steps:
3127
- uses: actions/checkout@v6
3228

@@ -47,120 +43,56 @@ jobs:
4743
--release \
4844
--bin array_ops_wasm
4945
50-
- name: Verify WASM binary exists
51-
run: |
52-
ls -lh target/wasm32-wasip1/release/array_ops_wasm.wasm
53-
file target/wasm32-wasip1/release/array_ops_wasm.wasm
54-
5546
- name: Install wabt tools
5647
run: sudo apt-get update && sudo apt-get install -y wabt
5748

5849
- name: Verify WASM exports
5950
run: |
6051
echo "Checking for required wasmfuzz exports..."
6152
wasm-objdump -x target/wasm32-wasip1/release/array_ops_wasm.wasm | grep -E "(LLVMFuzzerTestOneInput|wasmfuzz_malloc|wasmfuzz_free)"
62-
echo "All required exports present: LLVMFuzzerTestOneInput, wasmfuzz_malloc, wasmfuzz_free"
63-
64-
- name: Setup Wasmtime
65-
uses: bytecodealliance/actions/wasmtime/setup@v1
66-
67-
- name: Test WASM binary runs
68-
run: |
69-
wasmtime --version
70-
# Run the binary - it should exit cleanly (main() does nothing)
71-
wasmtime target/wasm32-wasip1/release/array_ops_wasm.wasm
72-
echo "WASM binary runs successfully"
73-
74-
- name: Upload WASM artifact
75-
uses: actions/upload-artifact@v4
76-
with:
77-
name: wasm-fuzzer
78-
path: target/wasm32-wasip1/release/array_ops_wasm.wasm
79-
retention-days: 7
80-
81-
test-wasmfuzz:
82-
name: "Fuzz with wasmfuzz"
83-
runs-on: ubuntu-latest
84-
# 4.5 hours to allow 4 hours of fuzzing + setup time
85-
timeout-minutes: 270
86-
needs: build-wasm-fuzz
87-
# Only run full fuzzing on schedule or manual dispatch, skip on PRs
88-
if: github.event_name == 'schedule' || github.event_name == 'workflow_dispatch'
89-
steps:
90-
- uses: actions/checkout@v6
91-
92-
- uses: ./.github/actions/setup-rust
93-
with:
94-
repo-token: ${{ secrets.GITHUB_TOKEN }}
95-
toolchain: nightly
96-
97-
- name: Download WASM artifact
98-
uses: actions/download-artifact@v4
99-
with:
100-
name: wasm-fuzzer
101-
path: ./wasm-fuzzer
10253
10354
- name: Install wasmfuzz
104-
id: install-wasmfuzz
105-
continue-on-error: true
106-
run: |
107-
cargo install --git https://github.com/CISPA-SysSec/wasmfuzz --locked
108-
echo "installed=true" >> $GITHUB_OUTPUT
55+
run: cargo install --git https://github.com/CISPA-SysSec/wasmfuzz --locked
10956

11057
- name: Determine fuzz duration
11158
id: duration
11259
run: |
11360
if [ "${{ github.event_name }}" = "workflow_dispatch" ]; then
11461
HOURS="${{ github.event.inputs.fuzz_duration }}"
11562
else
116-
# Default to 4 hours for scheduled runs
11763
HOURS="4"
11864
fi
11965
echo "hours=$HOURS" >> $GITHUB_OUTPUT
120-
echo "Fuzzing will run for $HOURS hour(s)"
12166
12267
- name: Run wasmfuzz
123-
if: steps.install-wasmfuzz.outputs.installed == 'true'
12468
run: |
125-
mkdir -p corpus crashes
126-
DURATION="${{ steps.duration.outputs.hours }}h"
127-
echo "Running wasmfuzz for $DURATION"
69+
mkdir -p corpus
12870
wasmfuzz fuzz \
129-
--timeout="$DURATION" \
71+
--timeout="${{ steps.duration.outputs.hours }}h" \
13072
--cores 2 \
13173
--dir corpus/ \
132-
./wasm-fuzzer/array_ops_wasm.wasm || true
133-
echo "wasmfuzz completed"
74+
target/wasm32-wasip1/release/array_ops_wasm.wasm
75+
76+
- name: Upload corpus
77+
uses: actions/upload-artifact@v4
78+
with:
79+
name: fuzz-corpus
80+
path: corpus/
81+
retention-days: 30
13482

13583
- name: Check for crashes
136-
if: steps.install-wasmfuzz.outputs.installed == 'true'
13784
run: |
13885
if [ -d "crashes" ] && [ "$(ls -A crashes 2>/dev/null)" ]; then
13986
echo "::error::Crashes found during fuzzing!"
14087
ls -la crashes/
14188
exit 1
142-
else
143-
echo "No crashes found"
14489
fi
14590
146-
- name: Upload corpus
147-
if: steps.install-wasmfuzz.outputs.installed == 'true'
148-
uses: actions/upload-artifact@v4
149-
with:
150-
name: fuzz-corpus
151-
path: corpus/
152-
retention-days: 30
153-
154-
- name: Upload crashes (if any)
155-
if: failure() && steps.install-wasmfuzz.outputs.installed == 'true'
91+
- name: Upload crashes
92+
if: failure()
15693
uses: actions/upload-artifact@v4
15794
with:
15895
name: fuzz-crashes
15996
path: crashes/
16097
retention-days: 90
16198

162-
- name: Report wasmfuzz status
163-
if: steps.install-wasmfuzz.outputs.installed != 'true'
164-
run: |
165-
echo "::warning::wasmfuzz failed to install (likely upstream dependency issue)"
166-
echo "The WASM binary was built successfully and can be used with wasmfuzz once it compiles"

fuzz/fuzz_targets/array_ops.rs

Lines changed: 18 additions & 199 deletions
Original file line numberDiff line numberDiff line change
@@ -4,220 +4,39 @@
44
#![no_main]
55
#![allow(clippy::unwrap_used, clippy::result_large_err)]
66

7-
use std::backtrace::Backtrace;
8-
97
use libfuzzer_sys::Corpus;
108
use libfuzzer_sys::fuzz_target;
119
use vortex_array::Array;
1210
use vortex_array::ArrayRef;
13-
use vortex_array::IntoArray;
14-
use vortex_array::arrays::ConstantArray;
15-
use vortex_array::compute::MinMaxResult;
16-
use vortex_array::compute::cast;
17-
use vortex_array::compute::compare;
18-
use vortex_array::compute::fill_null;
19-
use vortex_array::compute::filter;
20-
use vortex_array::compute::mask;
21-
use vortex_array::compute::min_max;
22-
use vortex_array::compute::sum;
23-
use vortex_array::compute::take;
24-
use vortex_array::search_sorted::SearchResult;
25-
use vortex_array::search_sorted::SearchSorted;
26-
use vortex_array::search_sorted::SearchSortedSide;
2711
use vortex_btrblocks::BtrBlocksCompressor;
2812
use vortex_error::VortexUnwrap;
2913
use vortex_error::vortex_panic;
30-
use vortex_fuzz::Action;
31-
use vortex_fuzz::CompressorStrategy;
3214
use vortex_fuzz::FuzzArrayAction;
33-
use vortex_fuzz::error::VortexFuzzError;
34-
use vortex_fuzz::error::VortexFuzzResult;
35-
use vortex_fuzz::sort_canonical_array;
15+
use vortex_fuzz::FuzzCompressor;
16+
use vortex_fuzz::run_fuzz_action;
3617
use vortex_layout::layouts::compact::CompactCompressor;
37-
use vortex_scalar::Scalar;
3818

39-
fuzz_target!(|fuzz_action: FuzzArrayAction| -> Corpus {
40-
let FuzzArrayAction { array, actions } = fuzz_action;
41-
let mut current_array = array.to_array();
42-
for (i, (action, expected)) in actions.into_iter().enumerate() {
43-
match action {
44-
Action::Compress(strategy) => {
45-
current_array = match strategy {
46-
CompressorStrategy::Default => BtrBlocksCompressor::default()
47-
.compress(current_array.to_canonical().as_ref())
48-
.vortex_unwrap(),
49-
CompressorStrategy::Compact => CompactCompressor::default()
50-
.compress(current_array.to_canonical().as_ref())
51-
.vortex_unwrap(),
52-
};
53-
assert_array_eq(&expected.array(), &current_array, i).unwrap();
54-
}
55-
Action::Slice(range) => {
56-
current_array = current_array.slice(range);
57-
assert_array_eq(&expected.array(), &current_array, i).unwrap();
58-
}
59-
Action::Take(indices) => {
60-
if indices.is_empty() {
61-
return Corpus::Reject;
62-
}
63-
current_array = take(&current_array, &indices).vortex_unwrap();
64-
assert_array_eq(&expected.array(), &current_array, i).unwrap();
65-
}
66-
Action::SearchSorted(s, side) => {
67-
// TODO(robert): Ideally we'd preserve the encoding perfectly but this is close enough
68-
let mut sorted = sort_canonical_array(&current_array).vortex_unwrap();
19+
/// Native compressor that supports both BtrBlocks and Compact strategies.
20+
struct NativeCompressor;
6921

70-
// If the current array is not in one of these canonical encodings, compress again.
71-
if !current_array.is_canonical() {
72-
sorted = BtrBlocksCompressor::default()
73-
.compress(&sorted)
74-
.vortex_unwrap();
75-
}
76-
assert_search_sorted(sorted, s, side, expected.search(), i).unwrap()
77-
}
78-
Action::Filter(mask) => {
79-
current_array = filter(&current_array, &mask).vortex_unwrap();
80-
assert_array_eq(&expected.array(), &current_array, i).unwrap();
81-
}
82-
Action::Compare(v, op) => {
83-
let compare_result = compare(
84-
&current_array,
85-
&ConstantArray::new(v.clone(), current_array.len()).into_array(),
86-
op,
87-
)
88-
.vortex_unwrap();
89-
if let Err(e) = assert_array_eq(&expected.array(), &compare_result, i) {
90-
vortex_panic!(
91-
"Failed to compare {}with {op} {v}\nError: {e}",
92-
current_array.display_tree()
93-
)
94-
}
95-
current_array = compare_result;
96-
}
97-
Action::Cast(to) => {
98-
let cast_result = cast(&current_array, &to).vortex_unwrap();
99-
if let Err(e) = assert_array_eq(&expected.array(), &cast_result, i) {
100-
vortex_panic!(
101-
"Failed to cast {} to dtype {to}\nError: {e}",
102-
current_array.display_tree()
103-
)
104-
}
105-
current_array = cast_result;
106-
}
107-
Action::Sum => {
108-
let sum_result = sum(&current_array).vortex_unwrap();
109-
assert_scalar_eq(&expected.scalar(), &sum_result, i).unwrap();
110-
}
111-
Action::MinMax => {
112-
let min_max_result = min_max(&current_array).vortex_unwrap();
113-
assert_min_max_eq(&expected.min_max(), &min_max_result, i).unwrap();
114-
}
115-
Action::FillNull(fill_value) => {
116-
current_array = fill_null(&current_array, &fill_value).vortex_unwrap();
117-
assert_array_eq(&expected.array(), &current_array, i).unwrap();
118-
}
119-
Action::Mask(mask_val) => {
120-
current_array = mask(&current_array, &mask_val).vortex_unwrap();
121-
assert_array_eq(&expected.array(), &current_array, i).unwrap();
122-
}
123-
Action::ScalarAt(indices) => {
124-
let expected_scalars = expected.scalar_vec();
125-
for (j, &idx) in indices.iter().enumerate() {
126-
let scalar = current_array.scalar_at(idx);
127-
assert_scalar_eq(&expected_scalars[j], &scalar, i).unwrap();
128-
}
129-
}
130-
}
22+
impl FuzzCompressor for NativeCompressor {
23+
fn compress_default(&self, array: &dyn Array) -> ArrayRef {
24+
BtrBlocksCompressor::default()
25+
.compress(array)
26+
.vortex_unwrap()
13127
}
132-
Corpus::Keep
133-
});
13428

135-
fn assert_search_sorted(
136-
array: ArrayRef,
137-
s: Scalar,
138-
side: SearchSortedSide,
139-
expected: SearchResult,
140-
step: usize,
141-
) -> VortexFuzzResult<()> {
142-
let search_result = array.search_sorted(&s, side);
143-
if search_result != expected {
144-
Err(VortexFuzzError::SearchSortedError(
145-
s,
146-
expected,
147-
array.to_array(),
148-
side,
149-
search_result,
150-
step,
151-
Backtrace::capture(),
152-
))
153-
} else {
154-
Ok(())
29+
fn compress_compact(&self, array: &dyn Array) -> ArrayRef {
30+
CompactCompressor::default().compress(array).vortex_unwrap()
15531
}
15632
}
15733

158-
fn assert_array_eq(lhs: &ArrayRef, rhs: &ArrayRef, step: usize) -> VortexFuzzResult<()> {
159-
if lhs.dtype() != rhs.dtype() {
160-
return Err(VortexFuzzError::DTypeMismatch(
161-
lhs.clone(),
162-
rhs.clone(),
163-
step,
164-
Backtrace::capture(),
165-
));
166-
}
167-
168-
if lhs.len() != rhs.len() {
169-
return Err(VortexFuzzError::LengthMismatch(
170-
lhs.len(),
171-
rhs.len(),
172-
lhs.to_array(),
173-
rhs.to_array(),
174-
step,
175-
Backtrace::capture(),
176-
));
177-
}
178-
for idx in 0..lhs.len() {
179-
let l = lhs.scalar_at(idx);
180-
let r = rhs.scalar_at(idx);
181-
182-
if l != r {
183-
return Err(VortexFuzzError::ArrayNotEqual(
184-
l,
185-
r,
186-
idx,
187-
lhs.clone(),
188-
rhs.clone(),
189-
step,
190-
Backtrace::capture(),
191-
));
34+
fuzz_target!(|fuzz_action: FuzzArrayAction| -> Corpus {
35+
match run_fuzz_action(fuzz_action, &NativeCompressor) {
36+
Ok(true) => Corpus::Keep,
37+
Ok(false) => Corpus::Reject,
38+
Err(e) => {
39+
vortex_panic!("{e}");
19240
}
19341
}
194-
Ok(())
195-
}
196-
197-
fn assert_scalar_eq(lhs: &Scalar, rhs: &Scalar, step: usize) -> VortexFuzzResult<()> {
198-
if lhs != rhs {
199-
return Err(VortexFuzzError::ScalarMismatch(
200-
lhs.clone(),
201-
rhs.clone(),
202-
step,
203-
Backtrace::capture(),
204-
));
205-
}
206-
Ok(())
207-
}
208-
209-
fn assert_min_max_eq(
210-
lhs: &Option<MinMaxResult>,
211-
rhs: &Option<MinMaxResult>,
212-
step: usize,
213-
) -> VortexFuzzResult<()> {
214-
if lhs != rhs {
215-
return Err(VortexFuzzError::MinMaxMismatch(
216-
lhs.clone(),
217-
rhs.clone(),
218-
step,
219-
Backtrace::capture(),
220-
));
221-
}
222-
Ok(())
223-
}
42+
});

0 commit comments

Comments
 (0)