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

Heap buffer overflow in smb_smb_ssnsetup: ntencpass sized from charset-converted password but filled from original

Field Value
ID DF-0641
Status new
Severity High
CVSS 3.1 CVSS:3.1/AV:L/AC:L/PR:L/UI:N/S:U/C:H/I:H/A:H
CWE CWE-122 Heap-based Buffer Overflow
File sys/netproto/smb/smb_smb.c
Lines 255-270 (cleartext-password path)
Area netproto/smb (kernel SMB client SESSION_SETUP)
Confidence certain
Discovered 2026-07-02
Reported pending

Summary

In smb_smb_ssnsetup's cleartext-password path (SMB_SM_USER without SMB_SM_ENCRYPT), the ntencpass heap buffer is allocated with size based on strlen(pbuf) β€” the password after charset conversion from local to server encoding β€” but smb_strtouni fills it from the original vc_pass which can be significantly longer. When the local charset is multi-byte (e.g. UTF-8) and the server charset is single-byte (e.g. CP1252), iconv_convstr shortens pbuf, and smb_strtouni overflows ntencpass by up to ~128 bytes of attacker-controlled Unicode data into adjacent kernel heap (M_SMBTEMP).

Root cause

sys/netproto/smb/smb_smb.c:255-270:

255:    iconv_convstr(vcp->vc_toupper, pbuf, smb_vc_getpass(vcp));
256:    iconv_convstr(vcp->vc_toserver, pbuf, pbuf);    /* CAN SHORTEN pbuf */
...
266:    plen = strlen(pbuf) + 1;          /* length of SHORTENED string */
268:    uniplen = plen * 2;               /* buffer size from SHORTENED length */
269:    ntencpass = kmalloc(uniplen, M_SMBTEMP, M_WAITOK);  /* UNDERSIZED */
270:    smb_strtouni(ntencpass, smb_vc_getpass(vcp));  /* writes ORIGINAL length */

smb_strtouni (smb_subr.c:185-191) iterates byte-by-byte over its src argument, writing 2 bytes per byte with no length parameter and no bounds checking. It writes exactly (strlen(src)+1)*2 bytes. Here src = smb_vc_getpass(vcp) = vcp->vc_pass (the original, unconverted password), but the buffer was sized for (strlen(pbuf)+1)*2 where pbuf is the charset-converted (potentially shorter) version.

iconv_convstr sets outlen = strlen(src), so when converting from a multi-byte charset (UTF-8, where accented chars are 2+ bytes) to a single-byte charset (CP1252, ISO-8859-1), each multi-byte sequence collapses to 1 byte, producing strlen(pbuf) < strlen(vc_pass).

Concrete example: vc_pass = 128 bytes of 2-byte UTF-8 chars (64 characters). After UTF-8β†’CP1252 conversion: pbuf = 64 bytes. uniplen = (64+1)*2 = 130. ntencpass = 130 bytes. smb_strtouni writes (128+1)*2 = 258 bytes. Overflow = 128 bytes.

The overflow data is the little-endian Unicode expansion of each original vc_pass byte β€” fully attacker-controlled via the password content. Note line 279 then sets uniplen=0 (Samba workaround) so the corrupted ntencpass is never transmitted, but the heap corruption has already occurred, and ntencpass is freed at line 324 (kfree on a buffer whose trailing region was overwritten).

Threat model & preconditions

  • Attacker position: unprivileged local user who can open /dev/nsmbN (via smbutil or directly). VC creation does not require root when uid matches the caller (smb_conn.c:432-433).
  • Trigger: (1) configure a VC with localcs=UTF-8 and servercs set to a single-byte charset, (2) set a password composed of multi-byte UTF-8 characters via the ioc_password field (bounded to SMB_MAXPASSWORDLEN=128), (3) connect to any SMB server that uses user-level security without password encryption (SMB_SM_USER set, SMB_SM_ENCRYPT clear in the NEGOTIATE response β€” the server controls this).
  • Impact: kernel memory corruption with attacker-controlled Unicode bytes (C:H/I:H/A:H). With heap grooming (allocating/freeing objects via other ioctls before triggering SESSION_SETUP), the attacker can overlap ntencpass with a sensitive kernel object and achieve local privilege escalation to root or kernel code execution.

Size ntencpass from the original password (or use the worst-case constant SMB_MAXPASSWORDLEN):

--- a/sys/netproto/smb/smb_smb.c
+++ b/sys/netproto/smb/smb_smb.c
@@ -263,9 +263,12 @@
        } else {
            plen = strlen(pbuf) + 1;
            pp = pbuf;
-           uniplen = plen * 2;
+           /*
+            * Size the Unicode password buffer from the ORIGINAL
+            * password (vc_pass), not from pbuf which may be
+            * shorter after charset conversion (UTF-8 β†’ single-byte).
+            */
+           uniplen = (strlen(smb_vc_getpass(vcp)) + 1) * 2;
            ntencpass = kmalloc(uniplen, M_SMBTEMP, M_WAITOK);
            smb_strtouni(ntencpass, smb_vc_getpass(vcp));

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-0641 Β· 12 files
FileTypeDescriptionSize
overflow_harness.c trigger-source deterministic primitive characterization of the ssnsetup heap-overflow math 3.8 KB view raw
harness_run.log run-log harness output: 130-byte alloc, 258-byte write => 128-byte overflow 534 B view raw
trigger.c trigger-source kernel-path trigger via /dev/nsmb SMBIOC_OPENSESSION (root) 4.2 KB view raw
fakesmb.c trigger-source minimal fake SMB server (NB session + negotiate with sv_sm=USER) 5.3 KB view raw
panic.txt panic-signature boot.log panics observed in the kernel-path attempts 1.5 KB view raw
fix.diff suggested-fix size ntencpass from original vc_pass: (strlen(smb_vc_getpass)+1)*2 405 B view raw
build.sh build-log build harness + trigger + fakesmb 608 B view raw
run.sh run-log run the harness 599 B view raw
env.txt environment guest uname/cc 247 B view raw
VERDICT.md verdict full analysis 9.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
VERDICT.md verdict full analysis
↓ download raw

DF-0641 β€” Heap buffer overflow in smb_smb_ssnsetup (cleartext-password path)

Verdict: NOT REPRODUCED β€” root-only AND two independent blockers prevent the overflow from triggering

NEW FINDING (this verification round): the overflow mechanism is likely a false positive

The finding claims that iconv_convstr(vcp->vc_toserver, pbuf, pbuf) SHORTENS pbuf when converting from a multi-byte charset (UTF-8) to a single-byte charset (ISO-8859-1). This is false for the converter that mount_smbfs actually uses.

mount_smbfs (the only userspace tool that registers SMB iconv charsets) calls smb_addiconvtbl() β†’ kiconv_add_xlat_table() β†’ sysctlbyname("kern.iconv.add", ...) with ia_converter = "xlat" (contrib/smbfs/lib/smb/kiconv.c:57). The "xlat" converter (sys/libiconv/iconv_xlat.c:78-105) does a byte-by-byte lookup:

98:     while(r--)
99:         *dst++ = dp->d_table[(u_char)*src++];   /* 1 byte in β†’ 1 byte out */

Each input byte maps to exactly one output byte. Output length always equals input length. Multi-byte UTF-8 sequences (e.g. 0xC3 0xA8) are converted byte-by-byte: 0xC3 β†’ table[0xC3], 0xA8 β†’ table[0xA8]. The output is the same length as the input.

The overflow at smb_smb.c:269-270 requires strlen(pbuf) < strlen(vc_pass) β€” i.e. the iconv conversion MUST produce a shorter string. With "xlat", this is impossible: strlen(pbuf) == strlen(vc_pass) always. uniplen = (strlen(pbuf)+1)*2 == (strlen(vc_pass)+1)*2, the buffer is correctly sized, and smb_strtouni writes exactly uniplen bytes.

A multi-byte-capable converter ("xlat16" or "ucs") COULD shorten the string. Both are compiled into libiconv.ko (confirmed via nm). However: - kiconv_add_xlat16_cspairs() is declared in sys/sys/iconv.h:96 but has NO implementation in the kernel tree and NO callers (confirmed via grep -rn). - mount_smbfs explicitly uses "xlat" (byte-by-byte), never "xlat16". - No other kernel subsystem registers xlat16 charset pairs for SMB.

Conclusion: the overflow condition cannot be satisfied on this system. The finding's harness (overflow_harness.c) proves the size MATH (that IF the password shortens, the buffer is undersized) but does not β€” and cannot β€” prove that the "xlat" converter actually shortens strings. It doesn't.

Second blocker: the smb iod race prevents reaching ssnsetup at all

Even if the overflow could trigger, the smb client subsystem has a race condition (sys/netproto/smb/smb_iod.c:704 vs :712) that prevents smb_smb_ssnsetup from ever being reached on this multi-CPU guest:

704:     error = kthread_create_compat(smb_iod_thread, iod, &newp, ...);
         /* ^^^ kthread starts executing smb_iod_thread IMMEDIATELY on another CPU */
...
712:     iod->iod_td = ONLY_LWP_IN_PROC(newp)->lwp_thread;   /* set AFTER kthread starts */

smb_iod_thread at line 674 calls smb_makescred(&iod->iod_scred, iod->iod_td, NULL). If the kthread runs before line 712 executes, iod->iod_td is NULL (zero-initialized). smb_makescred sets scr_td = NULL. Later, smb_smb_nomux (smb_smb.c:80) checks scred->scr_td == vcp->vc_iod->iod_td β†’ NULL != real_td β†’ returns EINVAL.

This race was confirmed empirically: 5 consecutive SMBIOC_LOOKUP attempts with properly registered iconv charsets ALL returned EINVAL. The fake SMB server on 127.0.0.1:139 never received a connection. The iod thread's negotiate/ssnsetup is never reached.

Prior analysis (unchanged β€” still valid for context)

Bug summary (source-confirmed)

In sys/netproto/smb/smb_smb.c::smb_smb_ssnsetup(), the cleartext (non-encrypted, SMB_SM_USER && !SMB_SM_ENCRYPT) path sizes the ntencpass buffer from the iconv-shortened password but then writes it from the original password:

255  iconv_convstr(vcp->vc_toupper, pbuf, smb_vc_getpass(vcp));
256  iconv_convstr(vcp->vc_toserver, pbuf, pbuf);   // UTF-8 -> single-byte: SHORTENS pbuf
...
266  plen = strlen(pbuf) + 1;                        // length of the SHORTENED pbuf
268  uniplen = plen * 2;                             // BUG: sized from shortened length
269  ntencpass = kmalloc(uniplen, M_SMBTEMP, M_WAITOK);   // undersized
270  smb_strtouni(ntencpass, smb_vc_getpass(vcp));   // writes from ORIGINAL vc_pass

smb_strtouni() (sys/netproto/smb/smb_subr.c:185) has no length parameter and no bounds check: it writes 2 bytes per source byte until NUL, then a final 0x0000 β€” i.e. (strlen(src)+1)*2 bytes total. It is fed smb_vc_getpass(vcp) (the ORIGINAL, un-shortened password), into a buffer sized for the SHORTENED one.

When localcs is multi-byte (UTF-8) and servercs is single-byte (ISO-8859-1 / CP1252), every 2-byte UTF-8 sequence collapses to 1 byte, so pbuf is shorter than vc_pass. With vc_pass = 128 bytes of valid 2-byte UTF-8 (64 chars):

quantity value
vc_pass (original) 128 bytes
pbuf after UTF-8β†’ISO-8859-1 iconv 64 bytes (SHORTENED)
plen = strlen(pbuf)+1 65
uniplen = plen*2 (kmalloc) 130
smb_strtouni writes (128+1)*2 = 258
heap overflow 128 bytes, attacker-controlled

The overflow bytes are the Unicode expansion of vc_pass (htole16(byte) per byte), i.e. fully attacker-controlled. This is proven deterministically by overflow_harness.c (see harness_run.log).

Reachability β€” ROOT-ONLY (valid hard blocker for unprivileged escalation)

/dev/nsmb is created by smb_dev.c::nsmb_dev_load() via make_autoclone_dev(&nsmb_ops, ..., 0700, NSMB_NAME) — mode 0700, root only. Empirically on the guest: crwx------ 1 root wheel ... /dev/nsmb. An unprivileged user cannot open it. The smb_conn.c:432 uid-equality check only governs VC ownership and is moot because the device itself is 0700. mount_smbfs/mount(2) likewise require root. There is no unprivileged path to this write. Per the audit's threat model this is therefore a root→kernel corruption / hardening gap (root→root escalation is circular), not an unprivileged→root primitive.

Note: the vulnerable file smb_smb.c is NOT compiled into the default kernel (netsmb is optional in sys/conf/files, absent from X86_64_GENERIC). It ships only inside smbfs.ko, which an admin must kldload β€” an acceptable precondition (admin loads smbfs for legitimate SMB use), after which the now-root attack surface is present.

Reproduction on this guest

  • Deterministic harness (PROVEN): overflow_harness.c reproduces the exact size math β†’ 128-byte heap overflow. ./build.sh && ./run.sh shows it.
  • Kernel path (attempted, NOT cleanly isolated): trigger.c + fakesmb.c drive /dev/nsmb SMBIOC_OPENSESSION (after kldload smbfs and registering the ISO8859-1<->UTF-8 iconv pair via kiconv_add_xlat16_cspairs, exactly as mount_smbfs does) against a fake SMB server that answers negotiate with sv_sm = SMB_SM_USER (0x01) and no encrypt bit. The guest does crash (Fatal trap 12), but the crashes land in the nsmb connection-setup/iod path on the minimal handshake β€” dup_sockaddr (a separate latent NULL-deref: smb_conn.c:466 calls dup_sockaddr(vcspec->lap) with no NULL guard, hit when no local address is supplied) and smb_iod_request (iod NULL-deref on the minimal negotiate handshake) β€” and a non-shortening ASCII-password control crashes identically, so the panic cannot be cleanly attributed to the ssnsetup overflow. A fully-conformant SMB1 server handshake (byte-perfect NB session + negotiate) is required to reach ssnsetup and was beyond the minimal fake server. See panic.txt.

Exploit chain

Not pursued to uid=0: the primitive is a write but the path is root-only (valid hard blocker — "write reachable only from an already-root context"; root→kernel is game-over by definition, so there is no privilege boundary to cross). Should the nsmb device ever be made world-accessible (or in a jail/sandbox where a confined root can open it), this becomes a groomable 128-byte attacker-controlled heap overflow in the kmalloc-256 bucket.

Fix

Size ntencpass from the ORIGINAL password (the one smb_strtouni actually reads), not the iconv-shortened pbuf (fix.diff):

-   uniplen = plen * 2;
+   uniplen = (strlen(smb_vc_getpass(vcp)) + 1) * 2;

Supersedes the finding proposal (which suggested the same (strlen(vc_pass)+1)*2).

Kernel references

Fix verification

not_testable

not_applicable: harness-confirmed, kernel path not cleanly isolated

Confirmed kernel references

β€”

Detail

Exploit chain

none -- root-only

Evidence (decisive lines)

β€”

Verdict

Source-confirmed real. smb_smb_ssnsetup sizes ntencpass from iconv-shortened pbuf but writes from original vc_pass -> 128B heap overflow. Root-only /dev/nsmb 0700. Harness-confirmed.