Coverage-guided fuzzing for Swift packages using libFuzzer.
Declare targets in a nested Fuzzing package; swift-fuzz handles instrumentation,
input storage, crash reproduction and coverage reporting.
- Swift 6.3+, with the libFuzzer runtime. Use an official Swift Docker image on Linux or a swift.org toolchain on macOS; Xcode's bundled toolchain does not include libFuzzer.
- macOS 26+ when running on macOS. Declare that minimum in the fuzzing package; your library can keep its existing deployment target.
- Targets use a C shim on Swift 6.3. With Swift 6.4+,
fuzz-init --standalonegenerates a single Swift executable instead. Target layouts
Create Fuzzing/Package.swift inside your repository:
// swift-tools-version: 6.3
import PackageDescription
let package = Package(
name: "Fuzzing",
platforms: [.macOS(.v26)],
dependencies: [
.package(path: "../"),
.package(url: "https://github.com/brokenhandsio/swift-fuzz.git", from: "1.0.0"),
],
targets: []
)From Fuzzing/, scaffold a target:
swift package --allow-writing-to-package-directory fuzz-init JSONParsingPaste the printed target declarations into the manifest and add your library dependency. For the default paired layout:
targets: [
.executableTarget(
name: "JSONParsing",
dependencies: ["JSONParsingTarget"],
path: "FuzzTargets/JSONParsingShim"
),
.target(
name: "JSONParsingTarget",
dependencies: [
.product(name: "Fuzzing", package: "swift-fuzz"),
.product(name: "YourLibrary", package: "YourRepo"),
],
path: "FuzzTargets/JSONParsing",
plugins: [.plugin(name: "FuzzTargetPlugin", package: "swift-fuzz")]
),
]Replace YourLibrary with your product name and YourRepo with the directory
name of the path dependency. The command creates sources and prints the stanza;
it does not edit the manifest.
Edit FuzzTargets/JSONParsing/JSONParsing.swift to exercise your API. For example,
this target parses arbitrary JSON with Foundation:
import Foundation
import Fuzzing
let fuzzTargets: @Sendable () -> Void = {
FuzzTarget.bytes("JSONParsing") { bytes in
_ = try? JSONSerialization.jsonObject(with: Data(bytes), options: .fragmentsAllowed)
}
}swift package --allow-writing-to-package-directory fuzz JSONParsing --time 60Expected parse errors can be ignored; traps and sanitizer failures produce a non-zero exit, a saved crash input, and a reproduction command.
| Entry point | Input |
|---|---|
FuzzTarget(_:_:) |
Borrowed Span<UInt8>; no copy |
FuzzTarget.bytes(_:_:) |
Owned [UInt8] |
FuzzTarget.structured(_:_:) |
inout FuzzedDataProvider |
FuzzTarget.async(_:_:) |
Owned [UInt8], async body |
FuzzTarget.structuredAsync(_:_:) |
FuzzedDataProvider, async body |
The owned forms copy the input once. Providers are Sendable; copies consume
their shared immutable input independently. All forms support strict memory safety.
FuzzTarget.structured("Decode") { data in
let depth = data.integer(in: 1...64)
let strict = data.bool()
_ = try? MyParser.parse(data.remainingBytes(), maximumDepth: depth, strict: strict)
}The provider reads payload bytes from the front and control values from the back.
It handles exhausted input without throwing. Use text() or chunk() for each
field, and remainingText() or remainingBytes() for the last payload.
Structured input and custom types
FuzzTarget.async("Routing") { bytes in
_ = try? await app.testable().sendRequest(makeRequest(bytes))
}Async bodies must not require the main actor: the fuzzing thread blocks until
they finish. Stalled bodies abort after 60 seconds; override with
FUZZ_ASYNC_TIMEOUT. Prefer synchronous targets for synchronous APIs.
Declare multiple targets in the same fuzzTargets closure and select them by
name. Names must be unique across the package, ignoring ASCII case, and match
[A-Za-z_][A-Za-z0-9_-]{0,127}. llvm-symbolizer and llvm-symbolizer-swift are
reserved. Names identify input directories and OSS-Fuzz executables.
Upgrading from 0.4.x: compact selection and optionalText() change how saved
inputs decode. Review important regression inputs before upgrading. The 1.x
decoding contract and migration instructions are in Input compatibility.
Run commands from the fuzzing package:
swift package --allow-writing-to-package-directory fuzz <target> [options]
--list List logical targets (no target argument required).
--time <seconds> Limit fuzzing time.
--jobs <n> Run parallel fuzzing processes.
--replay Replay seeds, working corpus and saved crashes.
--reproduce <path> Run one saved input.
--minimize-corpus Reduce the working corpus without changing seeds.
--minimize-crash <path> Shrink a crashing input in place.
--coverage Report source files reached by existing inputs.
--uncovered Also list unreached functions and all files.
--include-dependencies Include fetched dependencies in coverage reports.
--release Build in release configuration.
--sanitizers <list> Override fuzzer,address; --no-asan uses fuzzer only.
Other -flags pass through to libFuzzer, including -max_len, -rss_limit_mb
and -dict. Defaults enable value profiling and disable leak checking. On Linux,
swift-fuzz disables Swift's backtrace handler so libFuzzer can save crash inputs.
Path inside Fuzzing/ |
Purpose | Git policy |
|---|---|---|
Seeds/<Target>/ |
Curated inputs; never modified by fuzzing or minimization | Commit |
Corpus/<Target>/ |
Accumulated discoveries | Can be entirely ignored |
Crashes/<Target>/ |
Saved failing inputs; included in replay | Commit after fixing the bug |
Dictionaries/<Target>.dict |
Optional libFuzzer dictionary | Commit |
Missing working directories are created automatically. Starting from seeds alone works, but discarding a corpus loses search progress; more runtime does not guarantee recovering the same coverage. Preserve important discoveries as seeds or regression tests and retain corpus snapshots separately when useful.
.build/
/Corpus/
/fuzz-*.log
/crash-*
/leak-*
/timeout-*
/oom-*Keep the artifact patterns anchored so they do not hide files inside Crashes/.
For an already tracked corpus, git rm -r --cached -- Corpus removes it from
Git while keeping local files.
swift package --allow-writing-to-package-directory fuzz Decode --minimize-corpus
swift package --allow-writing-to-package-directory \
fuzz Decode --minimize-crash Crashes/Decode/crash-abc123Minimization is specific to the current build and feature settings. Crash minimization preserves a crash, which may differ from the original failure; keep important originals. Minimization and recovery
swift package --allow-writing-to-package-directory fuzz Decode --coverageReports include the fuzzing package and local path dependencies by default.
Use --include-dependencies for fetched dependencies or --uncovered for all
unreached files and functions. swift-fuzz's own sources are excluded.
On macOS, add --disable-sandbox for symbolized coverage and crash locations:
swift package --disable-sandbox --allow-writing-to-package-directory \
fuzz Decode --coverageswift package --allow-writing-to-package-directory generate-oss-fuzz-script \
--contact maintainer@example.comGenerates OSSFuzz/ with a Dockerfile, build helpers, project metadata and a
validation script. The repository defaults to Git origin; use --repository
and --output to override the repository and destination.
- The builder pins Swift 6.3.3 independently of OSS-Fuzz's toolchain updates.
For standalone targets, select a pinned Swift 6.4 Ubuntu 24.04 image with
--swift-image. - Each logical target becomes a native executable with its seeds, dictionary,
options and SwiftPM resources. Corpus packaging requires
--include-corpus. - Repeat
--exclude-targetfor known failures that cannot pass OSS-Fuzz's build checks yet. The build rejects misspelled exclusions. - The default is x86_64 with address sanitizer.
--sanitizers address,threadopts into thread sanitizer; validate every advertised configuration. - Regeneration preserves
Dockerfile,project.yamlandbuild.sh, and refuses to overwrite edits to generated helpers. Keep.swift-fuzz-generated.json.
Follow OSSFuzz/README.md to customize the integration and copy the submission
files into an OSS-Fuzz checkout. From that checkout, validate before submitting:
bash projects/PROJECT/validate.sh PROJECTThe script checks the advertised sanitizers and runs source coverage with the
packaged seeds. Existing integrations without generation state should use a new
--output directory and migrate their customizations.
Replay committed inputs on every pull request:
- name: Replay fuzz inputs
working-directory: Fuzzing
run: swift package --allow-writing-to-package-directory fuzz Decode --replayRun longer fuzzing jobs on a schedule, upload crash artifacts on failure, and save/restore the working corpus between runs. CI examples
See Examples for runnable harnesses and Contributing for development and validation commands.