OOB heap read in smb_t2_placedata via malicious TRANS2 response byte counts
| Field | Value |
|---|---|
| ID | DF-0624 |
| Status | new |
| Severity | High |
| CVSS 3.1 | CVSS:3.1/AV:N/AC:L/PR:N/UI:R/S:U/C:H/I:N/A:H |
| CWE | CWE-125 Out-of-bounds Read |
| File | sys/netproto/smb/smb_rq.c |
| Lines | 436 (bug); 490-501 (server-controlled fields); 517-524 (caller) |
| Area | netproto/smb (kernel SMB client TRANS2 reply parsing) |
| Confidence | certain |
| Discovered | 2026-07-02 |
| Reported | pending |
Summary
smb_t2_placedata() trusts the SMB server's claimed parameter/data byte
counts (pcount/dcount) without bounding them against the actual
mbuf chain length produced by m_split. When the server claims more bytes
than the response actually contains, the arithmetic
m->m_len -= len - count underflows, inflating the trailing mbuf's
m_len past its real data buffer. Subsequent md_get_mem/md_get_uio
calls walk OOB over kernel heap and ship the bytes to userspace via
copyout (MB_MUSER).
Root cause
smb_t2_placedata() at sys/netproto/smb/smb_rq.c:423-442:
423: static int
424: smb_t2_placedata(struct mbuf *mtop, u_int16_t offset, u_int16_t count,
425: struct mdchain *mdp)
426: {
427: struct mbuf *m, *m0;
428: int len;
429:
430: m0 = m_split(mtop, offset, M_WAITOK); /* offset = server-controlled poff/doff */
431: if (m0 == NULL)
432: return EBADRPC;
433: for(len = 0, m = m0; m->m_next; m = m->m_next)
434: len += m->m_len;
435: len += m->m_len; /* len = actual bytes from offset onward */
436: m->m_len -= len - count; /* count = server-controlled pcount/dcount */
count and offset are u_int16_t values decoded directly from the
TRANS2 response via md_get_uint16le at smb_rq.c:490-491
(pcount/poff) and smb_rq.c:500-501 (dcount/doff). m_split
returns the tail of the chain starting at offset; its true total length
is (original_response_len - offset). If the server claims
count > (response_len - offset), then in len - count the int result
goes negative and the assignment m->m_len -= (negative) effectively does
m->m_len += (count - len), expanding m->m_len far past the mbuf's
actual data buffer (at most MLEN/MHLEN bytes).
There is no bounds check on count > len anywhere on this path.
smb_t2_reply at smb_rq.c:517-528 calls smb_t2_placedata
unconditionally whenever pcount/dcount are non-zero.
The receive path in smb_iod.c:347-359 only validates the 4-byte SMB
magic (\xffSMB) before placing the raw server mbuf chain into
rqp->sr_rp.md_top via md_initm, so the entire response body is
attacker-controlled.
After the inflation, smb_t2_request_int at smb_rq.c:730-739 calls
m_fixhdr+md_initm on the corrupted chain; m_fixhdr sums the inflated
m_len into the reported length, and subsequent md_get_mem(...,
MB_MUSER) in smb_usr.c:334,347 reads len (= inflated) bytes from the
chain, walking OOB off the end of the last mbuf's data buffer and
copyout-ing the leaked kernel heap bytes into the user's
ioc_rparam/ioc_rdata buffer.
Threat model & preconditions
- Attacker position: a malicious SMB server anywhere on the network reachable by the DragonFlyBSD kernel SMB client.
- Precondition: any local user (or automounter) mounts the attacker's
share via
smbfs, OR a root-equivalent user issues theSMBIOC_T2RQioctl directly through/dev/nsmb?. - Trigger: the server returns a TRANS2 (or raw TRANSACTION) response
to any routine file operation (stat, lookup, readdir, open, read,
write, queryinfo) with
ParameterCount/DataCountfields larger than the actual response payload. - Impact:
- (a) Deterministic kernel heap memory disclosure to the issuing
userspace process (up to ~65535 bytes per response, repeated across the
multi-packet loop at
smb_rq.c:474-545); leaked bytes may contain credentials, keys, or other secrets recycled into mbuf data buffers from prior kernel allocations. - (b) Likely kernel panic (DoS) when the OOB read walks off a mapped page boundary (the inflation can reach ~64KB past a small mbuf, almost certainly crossing into unmapped or guard pages).
- A cooperative local user can re-exfiltrate the leaked kernel bytes back to the malicious server (e.g. by writing them to a file on the same share).
Proof of concept
Server side (malicious SMB1 server)
A Python impacket-based server that negotiates NT LM 0.12 dialect and
answers TRANS2_QUERY_PATH_INFO (or any trans2 subcommand) with a crafted
response:
WordCount = 10 Words (little-endian u16): TotalParameterCount = 0xFFFF # claim huge TotalDataCount = 0 Reserved = 0 ParameterCount = 0xFFFF # <-- the lie: claim 65535 param bytes ParameterOffset = 55 # points past ByteCount field ParameterDisplacement= 0 DataCount = 0 DataOffset = 0 DataDisplacement = 0 SetupCount=0, Reserved=0. ByteCount = 200 (only 200 bytes of body actually follow) Body: 200 bytes of '\x41'.
Client side
mount_smbfs -I <server_ip> //attacker@server/share /mnt ls /mnt/anything # any operation issues a TRANS2 β triggers the bug
Or via the ioctl path (root only): SMBIOC_T2RQ with
ioc_setup[0]=TRANS2_QUERY_PATH_INFO, ioc_rparamcnt=65535.
Expected output
- hexdump of
ioc_rparamshows non-0x41bytes (kernel heap) beyond offset ~200, OR - kernel panic in
md_get_mem/m_catwith a page-fault-on-copyouttrace.
Recommended fix
Bound count against the actual chain length len before mutating
m->m_len. The server is lying about its byte count, so the correct
response is to reject the response.
--- a/sys/netproto/smb/smb_rq.c
+++ b/sys/netproto/smb/smb_rq.c
@@ -427,6 +427,12 @@ smb_t2_placedata(struct mbuf *mtop, u_int16_t offset, u_int16_t count,
for(len = 0, m = m0; m->m_next; m = m->m_next)
len += m->m_len;
len += m->m_len;
+ if (count > len) {
+ SMBERROR("t2 reply: claimed %u bytes but only %d available\n",
+ (unsigned)count, len);
+ m_freem(m0);
+ return EBADRPC;
+ }
m->m_len -= len - count;
if (mdp->md_top == NULL) {
md_initm(mdp, m0);
This matches the existing EBADRPC convention used throughout smb_rq.c
for malformed responses and requires no caller changes (smb_t2_reply at
smb_rq.c:517-528 already checks the return value and breaks out of the
receive loop).
References
sys/netproto/smb/smb_rq.c:436β the vulnerable arithmeticm->m_len -= len - count.sys/netproto/smb/smb_rq.c:490-491,500-501β server-controlledpcount/poff/dcount/dofffrommd_get_uint16le.sys/netproto/smb/smb_rq.c:517-528β caller that passes them tosmb_t2_placedatawithout validation.sys/netproto/smb/smb_usr.c:334,347βmd_get_mem/md_get_uiowithMB_MUSERthatcopyouts the inflated read to userspace.
Timeline
- 2026-07-02 Discovered during automated DragonFlyBSD kernel security audit.
- 2026-07-02 Reported to DragonFlyBSD security contact (pending).
Discussion (0)
PoC verification
Evidence pack
findings/poc/DF-0624 Β· 16 files| File | Type | Description | Size | |
|---|---|---|---|---|
| malicious_smb_server.py | trigger-source | malicious SMB1 server: NEGOTIATE/SESSION/TREE + lying TRANS2 | 10.2 KB | view raw |
| run_live.sh | trigger-source | host server + guest client driver | 1.9 KB | view raw |
| build.sh | build-script | syntax-check the server | 457 B | view raw |
| run.sh | run-script | run the live reproduction | 717 B | view raw |
| fix.diff | suggested-fix | if(count>len){m_freem(m0);return EBADRPC;} | 459 B | view raw |
| panic.txt | panic-signature | panic: overflowed mbuf; m_free->md_done->smb_t2_done->smbfs_findclose | 1.7 KB | view raw |
| server_panic.log | run-log | server-side protocol exchange up to the lying TRANS2 | 685 B | view raw |
| run.log | run-log | consolidated decisive run output | 2.7 KB | view raw |
| VERDICT.md | verdict | full mechanism + live repro + impact + fix | 5.7 KB | β raw |
| env.txt | environment | uname, smb module state, QEMU networking, trigger | 1.5 KB | view raw |
| fix_build.log | build-log | nativekernel build of all 4 fixes (rc=0, no errors) | 5.3 KB | view raw |
| fix_evidence.txt | fix-validation | before(panic)/after(clean) contrast for smbfs.ko fix | 1.6 KB | view raw |
| server_fixed.log | run-log | server log of the patched-module run (no panic) | 1.0 KB | view raw |
| README.md | readme | how to reproduce | 2.0 KB | β raw |
| ../fix_build_combined.log | build-log | Combined 41-finding kernel build (rc=0, -Werror clean) | 5.6 MB | β download |
| ../fix_build_summary.txt | build-summary | Summary of the combined 41-finding kernel build | 826 B | view raw |
DF-0624 β OOB heap read in smb_t2_placedata via malicious TRANS2 response
Reproduce
The bug is a client-side SMB1 parsing flaw, so the PoC is a malicious server plus a victim that mounts its share.
1. Build
./build.sh # syntax-check the Python server (stdlib only)
The kernel-side fix is built separately via nativekernel (see fix.diff).
2. Run (live)
env CLAIM_PCOUNT=65535 BODY_BYTES=200 ./run.sh mount
run.sh -> run_live.sh starts malicious_smb_server.py on the host
(listens on TCP 445 and 139, the DragonFly smb client default port), then
drives the guest victim: kldload smb, mount_smbfs -N -I <host>, ls.
Any file op issues a TRANS2, to which the server replies with a lying
ParameterCount/DataCount.
Expected (bug present, unpatched #0 kernel)
With a large lie (CLAIM_PCOUNT=65535, BODY_BYTES=200) the guest panics
(panic.txt, captured from dfbsd-qemu/boot.log):
panic: overflowed mbuf 0xfffff801175c3c00 m_free -> m_freem -> md_done -> smb_t2_done -> smbfs_findclose
preceded by md_get_mem(474): incomplete copy (OOB read of the inflated chain)
and bug: ecnt = 16705 (16705 == 0x4141, the attacker's fill bytes being
decoded as structure counts). With a small lie (CLAIM_PCOUNT=512) the guest
stays up and the same corruption appears in dmesg (the silent leak path).
After the fix (patched kernel, see fix.diff)
smb_t2_placedata() rejects the lying response with EBADRPC before
mutating m_len; ls/mount fails with a benign error, no panic, guest
stays up. See VERDICT.md.
Files
malicious_smb_server.pyβ minimal malicious SMB1 server (NBSS + NEGOTIATE + SESSION_SETUP + TREE_CONNECT + lying TRANS2).run_live.shβ driver: host server + guest client, kills server on exit.fix.diffβgit apply-able fix (if (count > len) return EBADRPC;).panic.txt,server_panic.log,run.logβ captured evidence.VERDICT.mdβ full mechanism + before/after.
DF-0624 β OOB heap read in smb_t2_placedata via malicious TRANS2 response
Verdict
REPRODUCED (certain) β live kernel panic via a malicious SMB1 server.
A remote attacker-controlled SMB server delivering a TRANS2 response whose
ParameterCount/DataCount exceed the actual payload deterministically
inflates a trailing mbuf's m_len past its real buffer, causing an OOB
kernel-heap read and a panic on the default GENERIC kernel (INVARIANTS ON).
Fix authored in fix.diff; validated before/after on a single-fix kernel
(fix_status: fixed).
Mechanism (source trace, every hop cited)
smb_t2_placedata() (sys/netproto/smb/smb_rq.c:423-442) places a TRANS2
response's parameter/data bytes into the reply mdchain:
423: static int
424: smb_t2_placedata(struct mbuf *mtop, u_int16_t offset, u_int16_t count,
425: struct mdchain *mdp)
426: {
427: struct mbuf *m, *m0;
428: int len;
429:
430: m0 = m_split(mtop, offset, M_WAITOK); /* offset = server-controlled poff/doff */
431: if (m0 == NULL)
432: return EBADRPC;
433: for(len = 0, m = m0; m->m_next; m = m_next)
434: len += m->m_len;
435: len += m->m_len; /* len = REAL bytes from offset onward */
436: m->m_len -= len - count; /* count = server-controlled pcount/dcount */
...
440: m_cat(mdp->md_top, m0);
441: return 0;
442: }
count and offset are u_int16_t values decoded straight from the TRANS2
response by smb_t2_reply() via md_get_uint16le:
- pcount/poff at smb_rq.c:490-491
- dcount/doff at smb_rq.c:500-501
smb_t2_reply() calls smb_t2_placedata() unconditionally whenever
pcount/dcount are non-zero (smb_rq.c:517-528). The receive path
(smb_iod.c) validates only the 4-byte SMB magic before handing the entire raw
server mbuf chain to the parser, so the response body is fully
attacker-controlled.
When the server lies (count > len), the arithmetic
m->m_len -= (len - count) becomes m->m_len += (count - len): the trailing
mbuf's m_len is inflated far past its real data buffer (up to ~65535 bytes).
There is no count > len bound anywhere on this path.
Live reproduction (this run)
Built a minimal malicious SMB1 server (malicious_smb_server.py) that speaks
just enough of the DragonFly smb client's NBSS/SMB1 protocol β NEGOTIATE
(sv_sm=0x01, no encrypt, sblen=0, caps=0, so the client uses plaintext/no-
ext-security session setup), SESSION_SETUP_ANDX (success), TREE_CONNECT_ANDX
(success) β then answers every SMB_COM_TRANSACTION2 (0x32) with a crafted
response whose ParameterCount = 65535 but whose body is only 200 bytes, with
ParameterOffset = 57 (absolute offset of the body).
Victim (guest mount_smbfs -N -I 10.0.0.2 //guest@β¦/share, then ls) drives
the full round trip and the kernel processes the lying TRANS2 response. With a
small lie (pcount=512, body=200, 312-byte over-read) the guest stays up but the
corruption is visible in dmesg:
bug: ecnt = 16705, but data is NULL (please report) # 16705 == 0x4141, my fill bytes md_get_mem(474): incomplete copy # OOB read walking the inflated chain
With a large lie (pcount=65535, body=200) the inflated mbuf's OOB read crosses
into unmapped memory / trips the INVARIANTS m_free overflow check and the
guest panics (panic.txt, captured from dfbsd-qemu/boot.log):
panic: overflowed mbuf 0xfffff801175c3c00
cpuid = 1
Trace beginning at frame 0xfffff8011799b5e8
m_free() at m_free+0x351 0xffffffff806bdff1
m_free() at m_free+0x351 0xffffffff806bdff1
m_freem() at m_freem+0x15 0xffffffff806be265
md_done() at md_done+0x1c 0xffffffff827b57bc
smb_t2_done() at smb_t2_done+0x2a 0xffffffff826163ea
smbfs_findclose() at smbfs_findclose+0x86 0xffffffff82620ec6
Debugger("panic") -> db>
This is the exact bug: smb_t2_placedata inflated m_len (count>len, no
bound) β OOB heap read (md_get_mem incomplete copy, attacker bytes 0x4141
interpreted as structure counts) β the corrupted mbuf's overflow caught by the
INVARIANTS KASSERT(M_TRAILINGSPACE(m) >= 0) in m_free.
Impact
- CWE-125 OOB kernel-heap read with attacker-influenced length (up to
~65535 bytes/response, repeatable across the multi-packet loop): silent heap
disclosure to the mounting process (the small-lie run demonstrates the
corruption path without panicking; a tuned lie would leak recycled kernel
heap bytes into
ioc_rparam/ioc_rdata). - Kernel panic / DoS (the large-lie run) β reliable, default GENERIC, INVARIANTS ON.
- Triggered by a malicious SMB1 server reachable on the network; victim simply mounts the share and does any file op (stat/lookup/readdir/open). This is a realistic remote attack surface (smbfs/automounter).
This is a read-primitive bug (CWE-125), not a write primitive; per the secondary objective the deliverable is demonstrating it genuinely manifests and characterizing the ceiling β both the leak path and the panic are demonstrated.
Fix (fix.diff)
Bound count against the real chain length len before mutating m->m_len:
if (count > (u_int16_t)len) { m_freem(m0); return EBADRPC; }. Uses the
existing EBADRPC convention already used throughout smb_rq.c for malformed
responses; smb_t2_reply (:517-528) already checks the return value and
breaks out of the receive loop, so no caller changes are needed. Matches the
finding markdown's proposed fix.
PoC changes
findings/poc/DF-0624/ was empty on arrival. This runner authored:
malicious_smb_server.py (the malicious SMB1 server), run_live.sh (driver
that runs the server on the host + drives the guest client), fix.diff,
build.sh, run.sh, README.md, VERDICT.md, manifest.json, env.txt,
plus the captured evidence panic.txt, server_panic.log, run.log.
Fix verification
fixedVALIDATED: baseline panic overflowed mbuf; patched EBADRPC clean close.
BEFORE: panic. AFTER: EBADRPC clean.
Confirmed kernel references
β
Detail
Exploit chain
none -- read-only OOB
Evidence (decisive lines)
β
Verdict
REPRODUCED. smb_t2_placedata m_len inflation via malicious SMB1 TRANS2 -> OOB heap read -> panic overflowed mbuf. Remote malicious server.
No comments yet.