A minimal Linux kernel fuzzer written in Rust, compatible with syzkaller's syz-executor binary protocol. It reuses the original syzkaller executor to run test programs, while reimplementing the fuzzer scheduling logic in Rust.
This project is a Rust reimplementation of syzkaller's core orchestration logic. It does not reimplement the executor (the in-kernel syscall runner). Instead, it communicates directly with the original syz-executor binary via the FlatRPC protocol. This allows focusing on fuzzer scheduling, program generation, and corpus management, while maintaining full wire-level compatibility with the syzkaller executor.
syzkaller-rust (this project)
│
├── Launch QEMU virtual machine
├── Copy syz-executor to VM via SCP and start it over SSH
├── Perform FlatRPC handshake with the executor runner over TCP
├── Generate / mutate test programs (syscall sequences)
├── Serialize programs into the executor binary format and send them
├── Receive coverage signals (kcov) and update the corpus
└── Detect kernel crashes (KASAN / KFENCE / panic / etc.)
| Module | Responsibility |
|---|---|
avoidance.rs |
Persist and reload learned timeout-avoidance state (edge/syscall failure counts) |
main.rs |
Entry point: parse config, launch manager |
manager.rs |
Main loop: VM lifecycle, FlatRPC handshake, fuzz loop |
program.rs |
Syscall descriptors (syzkaller internal IDs), program data structures, random filename generation |
exec.rs |
Serialize programs into the executor binary format (varint encoding, compatible with encodingexec.go) |
fuzzer.rs |
Program generation (random) and mutation (insert/remove/modify calls, argument mutation, splicing) |
corpus.rs |
Corpus: track discovered coverage signals, store programs that produce new signal, persist and reload snapshots |
crash.rs |
Kernel crash detection: recognize BUG/KASAN/KFENCE/panic patterns, save crash reports |
protocol.rs |
FlatRPC message encode/decode (size-prefixed FlatBuffers) |
flatrpc_generated.rs |
Auto-generated FlatBuffers code (from syzkaller's flatrpc.fbs) |
qemu.rs |
QEMU VM launch, SSH port forwarding, serial console output capture |
ssh.rs |
SSH command execution, SCP file transfer |
config.rs |
JSON configuration file parsing |
cargo)qemu-system-x86_64)syz-executor binary (statically compiled for linux/amd64)bullseye.img)cd syzkaller-rust
cargo build --release
Start from the checked-in template and keep your machine-specific paths in an untracked local file:
cp config.example.json config.local.json
Then edit config.local.json:
config.example.json already includes the recommended Linux core bundle target:
data/target-bundles/linux-amd64-core.json.
max_execs is optional. When set, the manager exits cleanly after that many
execution requests (including timed out ones), which is useful for bounded smoke tests and scripted
end-to-end checks.
target_bundle is also optional. When present, the runtime loads exported JSON
target metadata instead of syscall_descriptions:
{
"target_bundle": "data/target-bundles/linux-amd64-core.json"
}
RUST_LOG=info ./target/release/syzkaller-rust config.local.json
Log levels: error / warn / info / debug.
For a bounded end-to-end smoke run, use the dedicated CLI wrapper instead of
editing config.json by hand:
RUST_LOG=info ./target/release/syzkaller-rust smoke config.local.json
By default this:
max_execs=32data/target-bundles/linux-amd64-core.json when the config does not
already specify target_bundle or syscall_descriptions<workdir>/smoke/<target-slug>, resetting that directory before each runThat keeps bounded smoke corpus and crash artifacts separate from any longer
running fuzz session that uses the base workdir.
For a multi-target regression pass with isolated per-target workdirs, use the suite wrapper:
RUST_LOG=info ./target/release/syzkaller-rust smoke-suite config.local.json
By default this runs:
descriptions/linux/file-subset.txtdescriptions/linux/path-info-subset.txtdescriptions/linux/dirent-subset.txtdescriptions/linux/pipe-io-subset.txtdescriptions/linux/msg-io-subset.txtdescriptions/linux/recvmsg-io-subset.txtdescriptions/linux/recvmmsg-io-subset.txtdescriptions/linux/dgram-io-subset.txtdescriptions/linux/socket-io-subset.txtdescriptions/linux/sockopt-buf-subset.txtdescriptions/linux/sock-ifreq-subset.txtdescriptions/linux/sock-ifconf-subset.txtdescriptions/linux/sock-ethtool-subset.txtdescriptions/linux/pipe-fionread-subset.txtdescriptions/linux/accept-connect-subset.txtdescriptions/linux/socket-subset.txtdescriptions/linux/image-subset.txtdescriptions/linux/mm-subset.txtdescriptions/linux/process-subset.txtEach target gets its own clean workdir under workdir/smoke-suite/<slug>, so
corpus and artifact summaries do not bleed across targets.
You can override either one:
RUST_LOG=info ./target/release/syzkaller-rust smoke config.local.json 8
RUST_LOG=info ./target/release/syzkaller-rust smoke config.local.json 8 descriptions/linux/socket-io-subset.txt
RUST_LOG=info ./target/release/syzkaller-rust smoke config.local.json 1 descriptions/linux/image-subset.txt
RUST_LOG=info ./target/release/syzkaller-rust smoke config.local.json 4 bundle:data/target-bundles/linux-amd64-core.json
RUST_LOG=info ./target/release/syzkaller-rust smoke-suite config.local.json 4 descriptions/linux/mm-subset.txt descriptions/linux/process-subset.txt
RUST_LOG=info ./target/release/syzkaller-rust smoke-suite config.local.json 1 descriptions/linux/socket-io-subset.txt
The following directories are created under workdir/ at runtime:
- crashes/ — kernel crash reports (console output)
- timeouts/ — timed-out or executor-reported hanged programs, including their syscall shape and timeout-prone edge profile, for later triage
Timeout artifacts also preserve the latest guest serial tail, and when available
the syz-executor SSH stdout/stderr stream, so executor-side hangs can be
triaged without rerunning the whole smoke target immediately.
Timeout and hang artifacts now also include a lightweight per-request trace
showing whether the manager observed Executing, opaque State, or
ExecResult messages before the request stalled.
The fuzzer implements syzkaller's FlatRPC handshake protocol:
fuzzer → executor: ConnectRequest (version, architecture info)
executor → fuzzer: ConnectReply (supported features, timeout config)
executor → fuzzer: InfoRequest (process configuration request)
fuzzer → executor: InfoReply (coverage filter, signal filter)
--- handshake complete, enter execution loop ---
fuzzer → executor: ExecRequest (program data + exec options, via shared memory)
executor → fuzzer: ExecResult (per-call return values, coverage signals)
Wire format: 4-byte little-endian size prefix + FlatBuffers-encoded message body.
Test programs are passed to the executor as a stream of varints (zigzag-encoded):
uint64 num_calls
// for each call:
[COPYIN instructions]* // write data pages (filenames, buffer contents, etc.)
uint64 syscall_id // syzkaller internal ID (NOT the Linux syscall NR)
uint64 copyout_idx // copyout result index (!0 = no copyout)
uint64 num_args
[arg]* // type + value for each argument
uint64 EXEC_INSTR_EOF // !0u64 end marker
Important:
syscall_idis the index into the executor'ssyscalls[]table for the target platform, not the Linux syscall number (NR). For example,readhas syzkaller ID5186on linux/amd64 in the current bundled target, while its Linux NR is0.
The repository now ships two checked-in Linux bundle fixtures:
data/target-bundles/linux-amd64-smoke.jsondata/target-bundles/linux-amd64-core.jsonsmoke command
now uses by defaultThe linux-amd64-core target currently includes:
accept, bind, close, connect, dup3, eventfd2, fstat, getcwd, getegid, geteuid, getgid, getpgid, getpid, getsockopt, gettid, getuid, ioctl, listen, lseek, madvise, mkdirat, mmap, mprotect, mremap, msync, munmap, newfstatat, openat, pipe2, pread64, pwrite64, read, recvmsg, sendmmsg, sendmsg, setpgid, setsockopt, socket, socketpair, write, writev
To regenerate the curated core bundle from upstream syzkaller target metadata:
cd tools/syzkaller-target-bundle
go run . \
--syscalls-file ../../data/target-bundles/linux-amd64-core.syscalls \
--output ../../data/target-bundles/linux-amd64-core.json
You can inspect bundle coverage without booting a VM:
cargo run --quiet -- target-summary bundle:data/target-bundles/linux-amd64-core.json 12
Recommended bounded Linux run:
RUST_LOG=info ./target/release/syzkaller-rust smoke config.local.json 128 bundle:data/target-bundles/linux-amd64-core.json
The current Rust-side description parser supports a deliberately small subset of syzlang-oriented concepts:
const NAME = VALUEconstset NAME[SIZE] = ... and flagset NAME[SIZE] = ....txt.const constant files for linux/amd64 constant importsname.txt.const files when parsing name.txtinclude "file.txt" and directory fragment loadinginclude <linux/...> directives are accepted and ignored.txt and .txt.const fragments in sorted orderint8, int16, int32, int64, and intptrint16be[min:max], int32be[min:max], and int64be[min:max]const[AF_INET, int16] and flags[socket_type, int16]sockaddr_in { ... } [size[16]]array[int8, 4:8], array[qid, 1:3], and
default short arrays like array[int8]array[cmsghdr_like, 1:2]-style message
batches can derive both total byte size and element count without collapsing
back to opaque flat buffers{ count bytesize[data]; data array[...] }
can materialize and validate without falling back to raw buffersiovec-style layouts where nested len[...] fields track pointed-to
payload sizes and executor serialization emits nested copyinsmsghdr layouts, where optional inline
pointers derive zero lengths when absent and nested array[iovec] fields keep
per-element payload lengths and aggregate counts alignedrecv_msghdr, where inline ptr[out]
fields materialize zeroed reserved buffers with real capacities instead of
collapsing to opaque placeholder pointers[packed] / align[N] layout
semantics, so msghdr-style fixed headers and aligned trailing payload
containers derive real field offsets and total sizes instead of packed-byte
approximationssend_msghdr-style containers, where
msg_control ptr[in, array[cmsghdr_like, ...]] keeps element boundaries,
aligned cmsg_len values, and aggregate msg_controllen sizes consistent
through generation, validation, replay, and executor copyinarray[send_mmsghdr, 1:2], where each fixed-size
element embeds a full msghdr container plus its own per-message metadata,
so sendmmsg-style inputs keep nested lengths and optional pointers coherent
across multiple messages in one syscallsockaddr_arg [ v4 sockaddr_in v6 buffer[28:28] ], including
fixed-size [size[N]] unions and [varlen] unionsvma, vma[opt], vma[N], and vma[min:max]string, string["literal"], string[set_name],
string[filename], string[..., N], and stringnoz[...]names = "foo", "bar"type templates for aliases, structs, and unions, such as
type wrap[PAYLOAD] { payload PAYLOAD } and type alias_wrap[PAYLOAD] wrap[PAYLOAD]len[parent, intN] and bytesize[parent, intN]void fields plus offsetof[field, intN] for fixed-size struct
layouts, which is enough for small nlattr-style headerslen[arg:field],
bytesize[type_name:field], bytesize[parent:parent:field],
bytesize[syscall:arg], and nested `off$ claude mcp add syzkaller-rust \
-- python -m otcore.mcp_server <graph>