Skip to content

Add fuzz testing infrastructure and fix first-round crashes #110

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Merged
merged 7 commits into from
Jul 24, 2024
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
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
22 changes: 22 additions & 0 deletions FuzzTesting/Package.swift
Original file line number Diff line number Diff line change
@@ -0,0 +1,22 @@
// swift-tools-version: 5.10

import PackageDescription

let package = Package(
name: "FuzzTesting",
products: [
.library(name: "FuzzTranslator", type: .static, targets: ["FuzzTranslator"]),
],
dependencies: [
.package(path: "../"),
],
targets: [
.target(name: "FuzzTranslator", dependencies: [
.product(name: "WasmKit", package: "WasmKit")
]),
]
)

for target in package.targets {
target.swiftSettings = [.unsafeFlags(["-Xfrontend", "-sanitize=fuzzer,address"])]
}
38 changes: 38 additions & 0 deletions FuzzTesting/README.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,38 @@
# Fuzz Testing

This subdirectory contains some [libFuzzer](https://www.llvm.org/docs/LibFuzzer.html) fuzzing targets for WasmKit.

> [!WARNING]
> libFuzzer does not work with the latest Swift runtime library on macOS for some reason. Run the fuzzing targets on Linux for now.

## Requirements

- [Open Source Swift Toolchain](https://swift.org/install) - Xcode toolchain does not contain fuzzing supoort, so you need to install the open source toolchain.
- [wasm-tools](https://github.com/bytecodealliance/wasm-tools) - Required to generate random seed corpora


## Running the Fuzzing Targets

1. Generate seed corpora for the fuzzing targets:
```sh
./fuzz.py seed
```
2. Run the fuzzing targets, where `<target>` is one of the fuzzing targets available in `./Sources` directory:
```sh
./fuzz.py run <target>
```
3. Once the fuzzer finds a crash, it will generate a test case in the `FailCases/<target>` directory.


## Reproducing Crashes

To reproduce a crash found by the fuzzer

1. Build the fuzzer executable:
```sh
./fuzz.py build <target>
```
2. Run the fuzzer executable with the test case:
```sh
./.build/debug/<target> <testcase>
```
13 changes: 13 additions & 0 deletions FuzzTesting/Sources/FuzzTranslator/FuzzTranslator.swift
Original file line number Diff line number Diff line change
@@ -0,0 +1,13 @@
import WasmKit

@_cdecl("LLVMFuzzerTestOneInput")
public func FuzzCheck(_ start: UnsafePointer<UInt8>, _ count: Int) -> CInt {
let bytes = Array(UnsafeBufferPointer(start: start, count: count))
do {
var module = try WasmKit.parseWasm(bytes: bytes)
try module.materializeAll()
} catch {
// Ignore errors
}
return 0
}
118 changes: 118 additions & 0 deletions FuzzTesting/fuzz.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,118 @@
#!/usr/bin/env python3

import argparse
import os
import subprocess

class CommandRunner:
def __init__(self, verbose: bool = False, dry_run: bool = False):
self.verbose = verbose
self.dry_run = dry_run

def run(self, args, **kwargs):
if self.verbose or self.dry_run:
print(' '.join(args))
if self.dry_run:
return
return subprocess.run(args, **kwargs)


def main():
parser = argparse.ArgumentParser(description='Build fuzzer')
# Common options
parser.add_argument(
'-v', '--verbose', action='store_true', help='Print commands')
parser.add_argument(
'-n', '--dry-run', action='store_true',
help='Print commands but do not execute them')

# Subcommands
subparsers = parser.add_subparsers(required=True)

available_targets = list(os.listdir('Sources'))

build_parser = subparsers.add_parser('build', help='Build the fuzzer')
build_parser.add_argument(
'target_name', type=str, help='Name of the target', choices=available_targets)
build_parser.set_defaults(func=build)

run_parser = subparsers.add_parser('run', help='Run the fuzzer')
run_parser.add_argument(
'target_name', type=str, help='Name of the target', choices=available_targets)
run_parser.add_argument(
'--skip-build', action='store_true',
help='Skip building the fuzzer')
run_parser.add_argument(
'args', nargs=argparse.REMAINDER,
help='Arguments to pass to the fuzzer')
run_parser.set_defaults(func=run)

seed_parser = subparsers.add_parser(
'seed', help='Generate seed corpus for the fuzzer')
seed_parser.set_defaults(func=seed)

args = parser.parse_args()
runner = CommandRunner(verbose=args.verbose, dry_run=args.dry_run)
args.func(args, runner)


def seed(args, runner):
def generate_seed_corpus(output_path: str):
args = [
"wasm-tools", "smith", "-o", output_path
]
# Random stdin input
stdin = os.urandom(1024)
process = subprocess.Popen(args, stdin=subprocess.PIPE)
process.communicate(input=stdin)
if process.returncode != 0:
raise Exception(f"Failed to generate seed corpus: {output_path}")

output_dir = ".build/fuzz-corpus"
os.makedirs(output_dir, exist_ok=True)

for i in range(100):
output = f"{output_dir}/corpus-{i}.wasm"
generate_seed_corpus(output)
print(f"Generated seed corpus: {output}")


def executable_path(target_name: str) -> str:
return f'./.build/debug/{target_name}'


def build(args, runner: CommandRunner):
print(f'Building fuzzer for {args.target_name}')

runner.run([
'swift', 'build', '--product', args.target_name
], check=True)

print('Building fuzzer executable')
output = executable_path(args.target_name)
runner.run([
'swiftc', f'./.build/debug/lib{args.target_name}.a', '-g',
'-sanitize=fuzzer,address', '-o', output
], check=True)

print('Fuzzer built successfully: ', output)


def run(args, runner: CommandRunner):

if not args.skip_build:
build(args, runner)

print('Running fuzzer')

artifact_dir = f'./FailCases/{args.target_name}/'
os.makedirs(artifact_dir, exist_ok=True)
fuzzer_args = [
executable_path(args.target_name), './.build/fuzz-corpus',
f'-artifact_prefix={artifact_dir}'
] + args.args
runner.run(fuzzer_args, env={'SWIFT_BACKTRACE': 'enable=off'})


if __name__ == '__main__':
main()
5 changes: 3 additions & 2 deletions Sources/WasmKit/ModuleParser.swift
Original file line number Diff line number Diff line change
Expand Up @@ -138,9 +138,10 @@ func parseModule<Stream: ByteStream>(stream: Stream, features: WasmFeatureSet =
tables: module.tables
)
let allocator = module.allocator
let functions = codes.enumerated().map { [hasDataCount = parser.hasDataCount, features] index, code in
let functions = try codes.enumerated().map { [hasDataCount = parser.hasDataCount, features] index, code in
// SAFETY: The number of typeIndices is guaranteed to be the same as the number of codes
let funcTypeIndex = typeIndices[index]
let funcType = module.types[Int(funcTypeIndex)]
let funcType = try translatorContext.resolveType(funcTypeIndex)
return GuestFunction(
type: typeIndices[index], locals: code.locals, allocator: allocator,
body: {
Expand Down
Loading