ksmbd Attack Surface & SMB2/SMB3 Protocol Fuzzing (syzkaller)
Overview
This page abstracts practical techniques to exercise and fuzz the Linux in-kernel SMB server (ksmbd) using syzkaller. It focuses on expanding the protocol attack surface through configuration, building a stateful harness capable of chaining SMB2 operations, generating grammar-valid PDUs, biasing mutations into weakly-covered code paths, and leveraging syzkaller features such as focus_areas and ANYBLOB. While the original research enumerates specific CVEs, here we emphasise the reusable methodology and concrete snippets you can adapt to your own setups.
Target scope: SMB2/SMB3 over TCP. Kerberos and RDMA are intentionally out-of-scope to keep the harness simple.
Expand ksmbd Attack Surface via Configuration
By default, a minimal ksmbd setup leaves large parts of the server untested. Enable the following features to drive the server through additional parsers/handlers and reach deeper code paths:
- Global-level
- Durable handles
- Server multi-channel
- SMB2 leases
- Per-share-level
- Oplocks (on by default)
- VFS objects
Enabling these increases execution in modules such as: - smb2pdu.c (command parsing/dispatch) - ndr.c (NDR encode/decode) - oplock.c (oplock request/break) - smbacl.c (ACL parsing/enforcement) - vfs.c (VFS ops) - vfs_cache.c (lookup cache)
Notes - Exact options depend on your distro’s ksmbd userspace (ksmbd-tools). Review /etc/ksmbd/ksmbd.conf and per-share sections to enable durable handles, leases, oplocks and VFS objects. - Multi-channel and durable handles alter state machines and lifetimes, often surfacing UAF/refcount/OOB bugs under concurrency.
Minimal lab configuration (adjust to the options your kernel/userspace build actually exposes):
[global]
map to guest = bad user
guest account = nobody
max connections = 65536
smb2 max credits = 8192
smb2 leases = yes
server multi channel support = yes
durable handles = yes
[fuzz]
path = /srv/ksmbd/fuzz
guest ok = yes
oplocks = yes
vfs objects = acl_xattr streams_xattr
Why these toggles matter
- server multi channel support is documented as experimental in current ksmbd.conf(5), which makes it a good fuzz-only knob for race/lifetime bugs.
- acl_xattr and streams_xattr move traffic into Security Descriptor and alternate-data-stream backends instead of only the ordinary file I/O fast path.
Authentication and Rate-Limiting Adjustments for Fuzzing
SMB3 needs a valid session. Implementing Kerberos in harnesses adds complexity, so prefer NTLM/guest for fuzzing:
- Allow guest access and set map to guest = bad user so unknown users fall back to GUEST.
- Accept NTLMv2 (patch policy if disabled). This keeps the handshake simple while exercising SMB3 code paths.
- Patch out strict credit checks when experimenting (post-hardening for CVE-2024-50285 made simultaneous-op crediting stricter). Otherwise, rate-limits can reject fuzzed sequences too early.
- Increase max connections (e.g., to 65536) to avoid early rejections during high-throughput fuzzing.
Caution: These relaxations are to facilitate fuzzing only. Do not deploy with these settings in production.
Stateful Harness: Extract Resources and Chain Requests
SMB is stateful: many requests depend on identifiers returned by prior responses (SessionId, TreeID, FileID pairs). Your harness must parse responses and reuse IDs within the same program to reach deep handlers (e.g., smb2_create → smb2_ioctl → smb2_close).
Example snippet to process a response buffer (skipping the +4B NetBIOS PDU length) and cache IDs:
// process response. does not contain +4B PDU length
void process_buffer(int msg_no, const char *buffer, size_t received) {
uint16_t cmd_rsp = u16((const uint8_t *)(buffer + CMD_OFFSET));
switch (cmd_rsp) {
case SMB2_TREE_CONNECT:
if (received >= TREE_ID_OFFSET + sizeof(uint32_t))
tree_id = u32((const uint8_t *)(buffer + TREE_ID_OFFSET));
break;
case SMB2_SESS_SETUP:
// first session setup response carries session_id
if (msg_no == 0x01 && received >= SESSION_ID_OFFSET + sizeof(uint64_t))
session_id = u64((const uint8_t *)(buffer + SESSION_ID_OFFSET));
break;
case SMB2_CREATE:
if (received >= CREATE_VFID_OFFSET + sizeof(uint64_t)) {
persistent_file_id = u64((const uint8_t *)(buffer + CREATE_PFID_OFFSET));
volatile_file_id = u64((const uint8_t *)(buffer + CREATE_VFID_OFFSET));
}
break;
default:
break;
}
}
Tips - Keep one fuzzer process sharing authentication/state: better stability and coverage with ksmbd’s global/session tables. syzkaller still injects concurrency by marking ops async, rerun internally. - Syzkaller’s experimental reset_acc_state can reset global state but may introduce heavy slowdown. Prefer stability and focus fuzzing instead.
Prefer a Hybrid Harness Over One Giant Pseudo-Syscall
If you keep extending the setup, use the custom pseudo-syscall mainly for the bootstrap steps that are annoying to express declaratively (negotiate/session-setup/tree-connect), then export the returned identifiers as syzkaller resources for follow-up operations. syzkaller explicitly discourages overusing pseudo-syscalls, and a hybrid model makes minimization/crossover noticeably less painful.
Example sketch:
resource ksmbd_sess[int64]
resource ksmbd_tree[int32]
resource ksmbd_fid[int64]
syz_ksmbd_bootstrap(..., sess ptr[out, ksmbd_sess], tree ptr[out, ksmbd_tree])
syz_ksmbd_create(..., sess ksmbd_sess, tree ksmbd_tree, fid ptr[out, ksmbd_fid])
syz_ksmbd_setinfo_acl(..., sess ksmbd_sess, tree ksmbd_tree, fid ksmbd_fid, ...)
syz_ksmbd_close(..., sess ksmbd_sess, tree ksmbd_tree, fid ksmbd_fid)
This keeps ordering information visible to the fuzzer instead of hiding the whole protocol behind one blob-oriented helper.
Grammar-Driven SMB2 Generation (Valid PDUs)
Translate the Microsoft Open Specifications SMB2 structures into a fuzzer grammar so your generator produces structurally valid PDUs, which systematically reach dispatchers and IOCTL handlers.
Example (SMB2 IOCTL request):
smb2_ioctl_req {
Header_Prefix SMB2Header_Prefix
Command const[0xb, int16]
Header_Suffix SMB2Header_Suffix
StructureSize const[57, int16]
Reserved const[0, int16]
CtlCode union_control_codes
PersistentFileId const[0x4, int64]
VolatileFileId const[0x0, int64]
InputOffset offsetof[Input, int32]
InputCount bytesize[Input, int32]
MaxInputResponse const[65536, int32]
OutputOffset offsetof[Output, int32]
OutputCount len[Output, int32]
MaxOutputResponse const[65536, int32]
Flags int32[0:1]
Reserved2 const[0, int32]
Input array[int8]
Output array[int8]
} [packed]
This style forces correct structure sizes/offsets and dramatically improves coverage versus blind mutation.
Prioritise CREATE Contexts, ACLs, and Named Streams
Recent upstream fixes landed in parser families that are easy to miss if the corpus only contains generic open/read/close traffic. Give these paths dedicated grammar entries and seed packets:
- CREATE contexts: model lease and durable-handle blobs explicitly instead of mutating the whole create-context chain as one opaque buffer.
- Security descriptors / ACLs: fuzz
SET_INFOandQUERY_INFOwith malformed owner/group/DACL offsets, ACE counts, and partial descriptors whileacl_xattris enabled. - Named streams: with
streams_xattr, open paths such asfile:streamand exerciseCREATE -> WRITE/READ -> CLOSEusing large offsets, reconnects, and sparse lengths.
That bias is worthwhile because recent bug fixes landed in create-lease parsing, durable-handle context parsing, and DACL offset validation. If those objects stay opaque, syzkaller tends to spend mutations on packet noise instead of the fields that actually gate parser depth.
Directed Fuzzing With focus_areas
Use syzkaller’s experimental focus_areas to overweight specific functions/files that currently have weak coverage. Example JSON:
{
"focus_areas": [
{"filter": {"functions": ["smb_check_perm_dacl"]}, "weight": 20.0},
{"filter": {"files": ["^fs/smb/server/"]}, "weight": 2.0},
{"weight": 1.0}
]
}
This helps construct valid ACLs that hit arithmetic/overflow paths in smbacl.c. For instance, a malicious Security Descriptor with an oversized dacloffset reproduces an integer-overflow.
Reproducer builder (minimal Python):
def build_sd():
import struct
sd = bytearray(0x14)
sd[0x00] = 0x00; sd[0x01] = 0x00
struct.pack_into('<H', sd, 0x02, 0x0001)
struct.pack_into('<I', sd, 0x04, 0x78)
struct.pack_into('<I', sd, 0x08, 0x00)
struct.pack_into('<I', sd, 0x0C, 0x10000)
struct.pack_into('<I', sd, 0x10, 0xFFFFFFFF) # dacloffset
while len(sd) < 0x78:
sd += b'A'
sd += b"\x01\x01\x00\x00\x00\x00\x00\x00" # minimal DACL
sd += b"\xCC" * 64
return bytes(sd)
Breaking Coverage Plateaus With ANYBLOB
syzkaller’s anyTypes (ANYBLOB/ANYRES) allow collapsing complex structures into blobs that mutate generically. Seed a new corpus from public SMB pcaps and convert payloads into syzkaller programs calling your pseudo-syscall (e.g., syz_ksmbd_send_req):
# Extract SMB payloads to JSON
# tshark -r smb2_dac_sample.pcap -Y "smb || smb2" -T json -e tcp.payload > packets.json
import json, os
os.makedirs("corpus", exist_ok=True)
with open("packets.json") as f:
data = json.load(f)
# adjust indexing to your tshark JSON structure
packets = [e["_source"]["layers"]["tcp.payload"] for e in data]
for i, pkt in enumerate(packets):
pdu = pkt[0]
pdu_size = len(pdu) // 2 # hex string length → bytes
with open(f"corpus/packet_{i:03d}.txt", "w") as f:
f.write(
f"syz_ksmbd_send_req(&(&(0x7f0000000340))=ANY=[@ANYBLOB=\"{pdu}\"], {hex(pdu_size)}, 0x0, 0x0)"
)
This jump-starts exploration and can immediately trigger UAFs (e.g., in ksmbd_sessions_deregister) while lifting coverage a few percent.
Higher-value captures to seed on purpose
- Lease negotiation / lease-break traces
- Durable-handle reconnects and reconnect failures
- SET_INFO / QUERY_INFO packets carrying Security Descriptors
- Named-stream paths (file:stream) when streams_xattr is enabled
- IOCTL-heavy traces (FSCTL/pipe/share-management paths) rather than only directory enumeration
Sanitizers: Beyond KASAN
- KASAN remains the primary detector for heap bugs (UAF/OOB).
- KCSAN often yields false positives or low-severity data races in this target.
- UBSAN/KUBSAN can catch declared-bounds mistakes that KASAN misses due to array-index semantics. Example:
id = le32_to_cpu(psid->sub_auth[psid->num_subauth - 1]);
struct smb_sid {
__u8 revision; __u8 num_subauth; __u8 authority[NUM_AUTHS];
__le32 sub_auth[SID_MAX_SUB_AUTHORITIES]; /* sub_auth[num_subauth] */
} __attribute__((packed));
Setting num_subauth = 0 triggers an in-struct OOB read of sub_auth[-1], caught by UBSAN’s declared-bounds checks.
Throughput and Parallelism Notes
- A single fuzzer process (shared auth/state) tends to be significantly more stable for ksmbd and still surfaces races/UAFs thanks to syzkaller’s internal async executor.
- With multiple VMs, you can still hit hundreds of SMB commands/second overall. Function-level coverage around ~60% of fs/smb/server and ~70% of smb2pdu.c is attainable, though state-transition coverage is under-represented by such metrics.
Practical Checklist
- Enable durable handles, leases, multi-channel, oplocks, and VFS objects in ksmbd.
- Allow guest and map-to-guest; accept NTLMv2. Patch out credit limits and raise max connections for fuzzer stability.
- Build a stateful harness that caches SessionId/TreeID/FileIDs and chains create → ioctl → close.
- Use a grammar for SMB2 PDUs to maintain structural validity.
- Use focus_areas to overweight weakly-covered functions (e.g., smbacl.c paths like smb_check_perm_dacl).
- Seed with ANYBLOB from real pcaps to break plateaus; pack seeds with syz-db for reuse.
- Run with KASAN + UBSAN; triage UBSAN declared-bounds reports carefully.
References
- Doyensec – ksmbd Fuzzing (Part 2): https://blog.doyensec.com/2025/09/02/ksmbd-2.html
- syzkaller: https://github.com/google/syzkaller
- ANYBLOB/anyTypes (commit 9fe8aa4): https://github.com/google/syzkaller/commit/9fe8aa4
- Async executor change (commit fd8caa5): https://github.com/google/syzkaller/commit/fd8caa5
- syz-db: https://github.com/google/syzkaller/tree/master/tools/syz-db
- KASAN: https://docs.kernel.org/dev-tools/kasan.html
- UBSAN/KUBSAN: https://docs.kernel.org/dev-tools/ubsan.html
- KCSAN: https://docs.kernel.org/dev-tools/kcsan.html
- Microsoft Open Specifications (SMB): https://learn.microsoft.com/openspecs/
- Wireshark Sample Captures: https://wiki.wireshark.org/SampleCaptures
- Background reading: pwning.tech “Tickling ksmbd: fuzzing SMB in the Linux kernel”; Dongliang Mu’s syzkaller notes
- ksmbd.conf(5): https://manpages.debian.org/unstable/ksmbd-tools/ksmbd.conf.5.en.html
- syzkaller pseudo-syscalls: https://github.com/google/syzkaller/blob/master/docs/pseudo_syscalls.md