Skip to content

Commit

Permalink
Merge pull request #110 from swiftwasm/katei/fuzz
Browse files Browse the repository at this point in the history
Add fuzz testing infrastructure and fix first-round crashes
  • Loading branch information
kateinoigakukun authored Jul 24, 2024
2 parents 5799d37 6e254ca commit 18408ab
Show file tree
Hide file tree
Showing 22 changed files with 345 additions and 81 deletions.
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

0 comments on commit 18408ab

Please sign in to comment.