ZDI-CAN-33549: ksmbd SMB Direct Integer Underflow, 8 Bytes to Kernel Crash

Every Linux machine running ksmbd with SMB Direct is one crafted SMB2 WRITE away from a kernel crash. One line of code, an 8-byte heap allocation, and a size_t that wraps to 0xFFFFFFFFFFFFFFFF.

I built a full SMB Direct client from scratch (RDMA connection management, SMB2 session negotiation, NTLMv2 authentication, the whole stack) to trigger a single integer underflow deep inside the kernel’s RDMA descriptor processing. The result: KASAN reports a slab-out-of-bounds write of 1024 bytes past an 8-byte kmalloc. Then the kernel follows a pointer full of my 0x41 bytes and hits a general protection fault.

ZDI-CAN-33549. CVSS 9.0. Fixed in commit 5b8ce2e. PoC, check script, detection rules: github.com/al4n4n/ZDI-CAN-33549.

Background: what ksmbd is and why it matters

ksmbd is an in-kernel SMB/CIFS server that landed in Linux 5.15 as a high-performance alternative to Samba. Unlike Samba, which runs as a userspace daemon with its own address space, ksmbd handles SMB requests directly in kernel context. Every ksmbd bug is a kernel bug: there’s no sandboxing, no ASLR at the process level, no graceful crash recovery. When ksmbd crashes, the whole machine goes down.

SMB Direct (SMB over RDMA) is the fast path. Instead of copying data through TCP socket buffers, the client and server register RDMA memory regions and do direct DMA reads and writes into each other’s memory. This is how enterprise NAS appliances and high-performance storage clusters move data at wire speed. The trade-off: the kernel trusts the client’s RDMA scatter/gather descriptors to stay within the allocated buffer. When they don’t, the DMA engine writes directly into whatever sits next to the buffer in the SLAB allocator.

Any deployment running ksmbd with CONFIG_SMB_SERVER_SMBDIRECT enabled and an RDMA-capable network (InfiniBand, RoCE, iWARP, or even the software emulator soft-RoCE) is exposed. The attacker needs to authenticate to any share (NTLMv2 with valid credentials is sufficient, and in many environments default or test accounts exist), but after that a single SMB2 WRITE request triggers the bug.

The vulnerable code

The function smbdirect_connection_rdma_xmit() in fs/smb/smbdirect/rw.c processes a list of RDMA descriptors from the client. Each descriptor says “read/write N bytes at offset M into the buffer.” The server iterates the list, subtracting each descriptor’s length from buf_len, which tracks how many bytes of buffer space remain.

The intent is straightforward: if a descriptor exceeds the remaining buffer, clamp it and stop. Here’s what happens instead:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
for (i = 0; i < desc_len / sizeof(*desc); i++) {
if (!buf_len)
break;

desc_buf_len = le32_to_cpu(desc[i].length);

if (desc_buf_len > buf_len) {
desc_buf_len = buf_len; // (1) clamp to remaining space
desc[i].length = cpu_to_le32(desc_buf_len);
buf_len = 0; // (2) <-- THE BUG: zeroes buf_len
}

// ... register the RDMA region ...

buf_len -= desc_buf_len; // (3) 0 - desc_buf_len = SIZE_MAX
desc_num++;
}

Let me trace it step by step for our trigger values (buf_len = 8, desc[0].length = 4096):

  1. desc_buf_len (4096) > buf_len (8): the descriptor exceeds the buffer
  2. desc_buf_len = buf_len → desc_buf_len = 8: clamp to remaining space
  3. buf_len = 0: mark buffer as consumed
  4. buf_len -= desc_buf_len → 0 - 8: unsigned underflow

Since buf_len is a size_t (unsigned 64-bit), 0 - 8 wraps to 0xFFFFFFFFFFFFFFF8. Now buf_len is effectively infinite. The loop continues to the next descriptor. The if (!buf_len) break; check at the top of the loop passes (because SIZE_MAX is very much nonzero) and the second descriptor’s full length is accepted without any bounds check.

The fix is to delete the buf_len = 0; line. Without it, after clamping desc_buf_len = buf_len, the subtraction buf_len -= desc_buf_len naturally produces 0. The loop breaks on the next iteration via the existing if (!buf_len) break; check. One line removed. That’s the whole fix.

Walking the exploit: from underflow to kernel crash

Here’s what happens inside the kernel when the exploit fires. This is the part that matters: the mechanism that turns a one-line off-by-one in buffer tracking into attacker-controlled heap corruption.

Step 1: The allocation. When ksmbd receives an SMB2 WRITE with RemainingBytes=8, it calls kvmalloc(8) inside smb2_write(). The SLAB allocator hands back an 8-byte object from the kmalloc-8 cache. This is a tightly packed cache: objects are back-to-back in memory, separated by nothing but the SLAB allocator’s internal metadata and other live objects.

Step 2: The first descriptor. Our desc[0].length = 4096 triggers the clamping path. The code clamps it to 8 bytes (the buffer size), registers the RDMA region for those 8 bytes, and then the underflow fires. buf_len wraps to SIZE_MAX.

Step 3: The second descriptor. With buf_len at SIZE_MAX, our desc[1].length = 8192 passes all checks. The kernel registers an 8192-byte RDMA region starting at the base of our 8-byte buffer.

Step 4: The DMA write. The soft-RoCE driver (rxe) executes the RDMA operation. rxe_mr_copy() does a memcpy of 8192 bytes into an 8-byte buffer. The first 8 bytes land in the allocated object. The remaining 8184 bytes overwrite whatever sits next to it in the kmalloc-8 SLAB page: other kernel objects, their function pointers, their internal linked lists.

Step 5: The corruption spreads. Our write payload is filled with 0x41 bytes. These overwrite the next pointers of adjacent SLAB objects. When ksmbd later tries to tear down the SMB Direct connection, it calls smbdirect_connection_destroy_mem_pools(), which walks the SLAB free list. It dereferences a pointer that now contains 0x4141414141414141. The kernel tries to access that non-canonical address and hits a general protection fault.

This is the critical insight: the attacker fully controls the content being DMA’d into the heap. In this PoC, I used 0x41 bytes to make the corruption visible in register dumps. A real exploit would place a crafted structure here (a fake kmem_cache descriptor, a corrupted sk_buff, or a function pointer overwrite) to turn the heap corruption into controlled code execution. I didn’t build that part, because demonstrating the mechanism is the point. The jump from “attacker controls 8KB of kernel heap” to “attacker controls RIP” is an engineering exercise, not a research question.

The trigger

The PoC, s3rig.c, implements a complete SMB Direct client over RDMA from scratch: rdma_cm for connection management, ibverbs for RDMA operations, and a full NTLMv2 authentication stack. The exploit path:

  1. RDMA connect to ksmbd (port 445)
  2. SMB Direct negotiation (version 0x0100)
  3. SMB2 NEGOTIATE (dialect 3.0.2)
  4. SMB2 SESSION_SETUP with NTLMv2 challenge-response
  5. SMB2 TREE_CONNECT to a share
  6. SMB2 CREATE to open a file
  7. SMB2 WRITE with RemainingBytes=8 and two RDMA descriptors:
    • desc[0].length = 4096 → triggers the clamp + underflow
    • desc[1].length = 8192 → writes 8KB OOB past the 8-byte buffer

The WRITE tells ksmbd: “I have 8 bytes of data to write, here are the RDMA regions where it lives.” ksmbd allocates 8 bytes, and the underflow lets the second descriptor DMA 8192 bytes directly past the end of that allocation.

Building the lab

This isn’t a trivial bug to reproduce. You need a kernel built with ksmbd, SMB Direct, soft-RoCE (the software RDMA emulator), and KASAN, all compiled in, not as modules. I used a QEMU VM running Linux 7.3-rc3 (the last vulnerable version) with a Debian Trixie rootfs:

1
2
3
4
5
CONFIG_SMB_SERVER=y
CONFIG_SMB_SERVER_SMBDIRECT=y
CONFIG_RDMA_RXE=y
CONFIG_INFINIBAND=y
CONFIG_KASAN=y # generic inline mode

Inside the VM: create a soft-RoCE device over eth0, configure ksmbd with a test share and user, and build s3rig. The repo includes setup.sh to automate all of this.

One gotcha that cost me hours of debugging: OpenSSL 3.x in Debian Trixie doesn’t load the legacy provider by default. EVP_md4() silently returns NULL, which means the MD4 hash at the core of NTLMv2 produces garbage. Every authentication attempt failed with STATUS_LOGON_FAILURE (0xc000006d). I wrote a standalone test program that computed NT hashes with and without the legacy provider, compared against known-good values, and confirmed the root cause. The fix is two lines at the start of main():

1
2
OSSL_PROVIDER_load(NULL, "legacy");
OSSL_PROVIDER_load(NULL, "default");

Lab topology

Everything runs inside the same QEMU VM. The PoC (s3rig) talks to ksmbd through a soft-RoCE device (rxe0), the same RDMA transport path a real attacker would use over InfiniBand or RoCE hardware:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
┌───────────────────────────────────────────────────────────────────────────┐
│ │
│ s3rig (userspace) ksmbd + soft-RoCE (in-kernel) │
│ ┌────────────────┐ RDMA (rxe0) ┌──────────────────────────┐ │
│ │ rdma_connect │──────────────►│ smbdirect port 445 │ │
│ │ SMBDirect neg │ │ │ │
│ │ NTLMv2 auth │ │ smb2_write(): │ │
│ │ TREE_CONNECT │ │ kvmalloc(8) → kmalloc-8 │ │
│ │ CREATE file │ │ │ │
│ │ │ DMA write │ smbdirect_rdma_xmit(): │ │
│ │ SMB2 WRITE: │──────────────►│ desc[0]=4096 → wrap │ │
│ │ Remain=8 │ │ desc[1]=8192 → OOB │ │
│ │ desc[0,1] │ │ buf_len → SIZE_MAX │ │
│ └────────────────┘ └──────────────────────────┘ │
│ │ │ │
│ │ exit code │ logs to dmesg │
│ ▼ ▼ │
│ poc.sh / verify.sh "BUG: KASAN: slab-out-of-bounds │
│ (crash = VULNERABLE) in rxe_mr_copy" │
│ GPF @ 0x4141414141414141 │
└───────────────────────────────────────────────────────────────────────────┘

The crash

Control run: baseline

First, a benign single-descriptor RDMA write within the buffer bounds:

1
2
3
4
5
6
7
S3RIG: target 10.0.2.15:445 unc='\\10.0.2.15\share' file='s3-victim.bin'
S3RIG: rdma connected via device 'rxe0'
S3RIG: SMB2 NEGOTIATE rsp 224 bytes status=0x00000000 dialect=0x0302
S3RIG: SESSION_SETUP[1] status=0x00000000 session=0x1
S3RIG: TREE_CONNECT '\\10.0.2.15\share' status=0x00000000
S3RIG: CONTROL benign_rdma_write=ok
S3RIG: RUN-COMPLETE

No crash. dmesg clean. The full SMB Direct + SMB2 + NTLMv2 stack works.

Exploit run: the OOB write

1
2
3
S3RIG: dirA SMB2 WRITE RemainingBytes=8 desc[0].length=4096 desc[1].length=8192
S3RIG: TRIGGER dirA sent status=0x00000000
S3RIG: RUN-COMPLETE

Then the kernel speaks:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
BUG: KASAN: slab-out-of-bounds in rxe_mr_copy+0x381/0x560
Write of size 1024 at addr ffff88810882d788 by task kworker/u16:1/42

Workqueue: rxe_wq do_work
Call Trace:
<TASK>
__asan_memcpy+0x3c/0x60
rxe_mr_copy+0x381/0x560
copy_data+0x1c2/0x5c0
rxe_completer+0x2102/0x38c0

Allocated by task 68:
__kvmalloc_node_noprof+0x215/0x5c0
smb2_write+0x1a93/0x3530
handle_ksmbd_work+0x616/0x1bc0

The buggy address belongs to the object at ffff88810882d780
which belongs to the cache kmalloc-8 of size 8
The buggy address is located 0 bytes to the right of
allocated 8-byte region [ffff88810882d780, ffff88810882d788)

KASAN tells the whole story: smb2_write() allocated 8 bytes. rxe_mr_copy() wrote 1024 bytes starting at the first byte past the allocation. The 0 bytes to the right confirms the write starts immediately after the object.

Then the kernel follows our corrupted pointer:

1
2
3
4
5
6
7
8
9
10
Oops: general protection fault, probably for non-canonical address 0x4141414141414141
Workqueue: ksmbd-conn-release __ksmbd_conn_release_work
RIP: 0010:qlist_move_cache+0x65/0x100
RCX: 4141414141414141
RSI: 4141414141414141
Call Trace:
kasan_quarantine_remove_cache+0x109/0x180
kmem_cache_destroy+0x6d/0x1c0
smbdirect_connection_destroy_mem_pools+0x1c6/0x3c0
__ksmbd_conn_release_work+0x86/0xb0

0x4141414141414141 in both RCX and RSI. That’s our write pattern. The kernel followed a pointer from a corrupted SLAB object into memory we control. The crash happens in the KASAN quarantine cleanup path, but only because KASAN’s metadata happens to be what gets corrupted first. Without KASAN, the crash would occur earlier and in a different code path, wherever the first overwritten pointer gets dereferenced.

Negative control: patched kernel

Same payload, same VM, kernel with buf_len = 0; removed:

1
2
3
4
S3RIG: TRIGGER dirA sent status=0x00000000
S3RIG: RUN-COMPLETE
$ dmesg | grep -c "BUG:\|Oops"
0

Zero crashes across multiple runs. The fix works.

Impact

Claim Status Evidence
Integer underflow in buf_len Proven KASAN: slab-out-of-bounds write of 1024 bytes past 8-byte allocation
Heap OOB write via RDMA Proven Write at ffff88810882d788, 0 bytes right of kmalloc-8 region
Attacker-controlled heap corruption Proven GPF at 0x4141414141414141, our 0x41 write pattern in kernel registers
Kernel crash (DoS) Proven Oops + kworker crash in smbdirect_connection_destroy_mem_pools
Remote code execution Not proven Overflow writes attacker data to adjacent SLAB objects; demonstrates the mechanism for RCE
Patched version safe Confirmed Negative control: 0 crashes on patched kernel

Who’s affected: Any Linux machine running ksmbd with SMB Direct enabled and an RDMA-capable network. This includes enterprise NAS appliances, high-performance storage clusters, and cloud RDMA deployments where containers share a ksmbd-enabled host kernel. The attacker needs any authenticated session: a test account, a guest share, anything that gets past NTLMv2.

Detection

The repo includes Suricata and Sigma rules.

  • Network (Suricata): Detects SMB2 WRITE with suspiciously small RemainingBytes. Note that SMB Direct traffic may bypass network IDS entirely on native RDMA fabrics; this rule works best on soft-RoCE or when traffic is mirrored.
  • Host (Sigma): Matches BUG: KASAN: slab-out-of-bounds and general protection fault patterns in kernel logs, anchored to ksmbd and rxe stack frames.

Best mitigation: update the kernel. The fix is in mainline as commit 5b8ce2e.

Fix

Namjae Jeon’s fix removes the buf_len = 0; line. Without it, after clamping desc_buf_len = buf_len, the subtraction buf_len -= desc_buf_len naturally produces 0. The loop breaks on the next iteration via the existing if (!buf_len) break; check.

The fix patches the introduction commit e628bf939aaf. Affected: all kernels from that commit through 7.3-rc3. Fixed: 7.3-rc3+ with the patch applied, and any backport that removes the buf_len = 0; line.

Timeline

Date Event
2026 HYEONTAE LEE discovers and reports to ZDI (ZDI-CAN-33549)
2026-10-21 Fix committed to linux.git by Namjae Jeon
2026-10-24 This PoC and write-up published

Reproduce it yourself

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
# Clone the repo
git clone https://github.com/al4n4n/ZDI-CAN-33549
cd ZDI-CAN-33549

# Check if a target is vulnerable (non-destructive)
python3 check.py <target-ip>

# In the VM (as root): set up ksmbd + soft-RoCE + build s3rig
bash setup.sh

# Run the PoC (control + exploit)
bash poc.sh <target-ip>

# Full verification dashboard
bash verify.sh <target-ip> s3guest test ./poc/s3rig

Every claim in this post can be verified by running the PoC against a vulnerable kernel in a QEMU VM. The repo has everything: kernel config, setup script, build system, detection rules.

References