Stack buffer overflow in VFAT long-filename reassembly via crafted FAT directory entry β no kernel stack canary
Summary
msdosfs_readdir :1665 win2unixfn(&nb,...) for each ATTR_WIN95 LFN entry. win2unixfn (msdosfs_conv.c:642) validates weCnt&WIN_CNT <= howmany(WIN_MAXLEN,WIN_CHARS)=howmany(255,13)=20 allows 20 slots max id=19. mbnambuf_write (msdosfs_conv.c:1044) slot=&nbp->nb_buf[id*WIN_CHARS]=nb_buf[19*13]=nb_buf[247]. count=strlen(name)=13 for ASCII (26 for KICONV). cumulative check :1047 bounds running total not absolute slot offset. shift check :1053-1056 only fires count>WIN_CHARS&&nb_len!=0 skipped first write. memcpy(slot,name,count) :1062 writes 13 bytes at 247 = bytes 247-259 overflow nb_buf[256] by 4 bytes. KICONV: 26 bytes = 17 bytes overflow. nb_buf is char[WIN_MAXLEN+1]=256 in struct mbnambuf stack-allocated in msdosfs_readdir :1486. DragonFly kernel has NO stack canary sys/conf/files:2219 libkern/stack_protector.c commented out. Overflow corrupts adjacent stack locals + potentially saved frame pointer/return address = kernel code exec. Trigger: crafted FAT image weCnt=0x54(20|WIN_LAST) all 13 UTF-16 positions non-null mount then ls/stat/open. msdosfs_lookup.c:274 same path. Fix: if(id*WIN_CHARS+count>sizeof(nb_buf)) return ENAMETOOLONG in mbnambuf_write.
Discussion (0)
PoC verification
Evidence pack
findings/poc/DF-0795 Β· 17 files| File | Type | Description | Size | |
|---|---|---|---|---|
| README.md | readme | human-facing overview, files index, repro steps | 3.7 KB | β raw |
| VERDICT.md | verdict | full narrative: bug path:line, stack-frame disasm, byte map, exploit-chain analysis (hard blocker), fix validation | 10.1 KB | β raw |
| craft_img.py | trigger-source | FAT image patcher: injects weCnt=0x54 LFN slot at root-dir offset 0 (id=19, 13 ASCII chars) | 2.9 KB | view raw |
| run.sh | trigger-source | mount+ls trigger script (root: vnconfig/mount) | 662 B | view raw |
| df0795_harness.c | trigger-source | deterministic userland replication of mbnambuf_write with poisoned stack frame; prints byte-by-byte corruption map | 6.7 KB | view raw |
| df0795_harness_fixed.c | trigger-source | same harness with the fix applied; ORIG vs FIXED before/after comparison | 2.8 KB | view raw |
| build.sh | build-log | no-op build script (Python trigger; harnesses built with cc -O2) | 398 B | view raw |
| run_baseline_kernel.log | run-log | decisive run on unpatched kernel #0 (silent, no panic) | 1.7 KB | view raw |
| run_patched_kernel.log | run-log | same run on patched kernel #1 (no regression) | 406 B | view raw |
| harness_before_fix.log | run-log | ORIG mbnambuf_write output: 4 bytes corrupted | 2.5 KB | view raw |
| harness_after_fix.log | run-log | FIXED mbnambuf_write output: 0 bytes corrupted, returns ENAMETOOLONG | 190 B | view raw |
| env.txt | environment | uname, kern.version, cc version, msdos kernel module id | 418 B | view raw |
| fix.diff | suggested-fix | git-apply-able fix: absolute-offset bounds check in mbnambuf_write before memcpy | 915 B | view raw |
| fix_build.log | build-log | module-only build log (fast path), rc=0 | 18.0 KB | view raw |
| fix_build_kernel.log | build-log | full single-fix nativekernel build log (#1 kernel), rc=0 | 5.6 MB | β download |
| ../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-0795 β Stack buffer overflow in VFAT long-filename reassembly
TL;DR
REPRODUCED. The OOB write is real (CWE-787): mbnambuf_write() writes
4 bytes (ASCII variant) or 17 bytes (KICONV variant) past nb_buf[256]
when a crafted FAT directory entry has weCnt = 0x54 (= WIN_LAST | 20,
so id = 19) and all 13 name positions are non-null. The finding's
path:line analysis is correct.
NOT EXPLOITABLE to uid=0. The overflow lands in struct dirent
dirbuf (in msdosfs_readdir) or in unused stack padding (in
msdosfs_lookup); it does not reach the saved frame pointer, return
address, or any function pointer / refcount / credential field. The
dirbuf fields it does corrupt are unconditionally overwritten before
use. There is therefore no control-flow primitive to convert β this
is a valid Phase-6 hard blocker. The "no kernel stack canary = code
execution" theory in the finding markdown is wrong: it conflates
"no canary" with "the overflow reaches the saved RIP", and on this
compilation the overflow does not reach the saved RIP.
Impact: integrity-only OOB write of attacker-controlled bytes into
a stack neighbor that is discarded before use. Real bug, defense-in-depth
fix warranted, but not a privilege escalation. Reported impact:
corruption (silent, contained), not uid0 and not even panic.
Files in this evidence pack
| File | Purpose |
|---|---|
craft_img.py |
FAT image patcher: injects a weCnt=0x54 LFN slot at root-dir offset 0 |
run.sh |
Mount+ls trigger (must run as root: needs vnconfig/mount) |
df0795_harness.c |
Deterministic userland replication of mbnambuf_write showing exact overflow bytes and what they hit (poisoned stack frame) |
df0795_harness_fixed.c |
Same harness with the fix applied; ORIG-vs-FIXED before/after comparison |
build.sh |
no-op for the trigger (Python); harnesses built with cc -O2 |
run_baseline_kernel.log |
Decisive run on unpatched kernel #0 (silent, no panic) |
run_patched_kernel.log |
Same run on patched kernel #1 (no regression) |
harness_before_fix.log |
ORIG mbnambuf_write output (4 bytes corrupted) |
harness_after_fix.log |
FIXED mbnambuf_write output (0 bytes corrupted, returns ENAMETOOLONG) |
env.txt |
Guest environment (uname, kern.version, msdos module id) |
fix.diff |
git apply-able fix adding the absolute-offset bounds check |
fix_build.log |
module-only build log (fast path) |
fix_build_kernel.log |
full single-fix nativekernel build log (#1) |
VERDICT.md |
Full narrative + stack-frame disassembly + byte map |
How to reproduce
# on host (has python3)
ssh dfbsd 'newfs_msdos -F 16 -C 16m /tmp/df0795.img' # make a fresh image
scp dfbsd:/tmp/df0795.img /tmp/df0795.img
python3 findings/poc/DF-0795/craft_img.py /tmp/df0795.img # patch it
scp /tmp/df0795.img dfbsd:/tmp/df0795.img
# on guest (root)
ssh dfbsd 'cd /tmp/df0795 && sh ./run.sh' # mount+ls the crafted image
# harness (build on guest)
ssh dfbsd 'cc -O2 -o /tmp/h /tmp/df0795/df0795_harness.c && /tmp/h'
Expected on the unpatched kernel
No panic. No kernel message. Empty directory listing (the LFN slot has no
matching 8.3 alias, so msdosfs_readdir skips it). The overflow happens
silently β 4 bytes of nb_buf overwrite dirbuf.d_ino low 32 bits,
which is then overwritten by dirbuf.d_fileno = ... before use. This
silent behavior is confirmed by the deterministic harness.
Expected on the patched kernel (with fix.diff applied)
Same observable behavior (no panic, empty listing), but the overflow is
prevented: mbnambuf_write returns ENAMETOOLONG for the id=19,
count=13 input. The harness shows the byte-level difference.
DF-0795 β VERDICT
Verdict: REPRODUCED (CWE-787 OOB write confirmed) β NOT EXPLOITABLE to uid=0
The OOB write is real and exactly as the finding describes. The
escalation theory ("no kernel stack canary β kernel code execution") is
wrong for this specific call site / compilation: the overflow bytes
do not reach the saved frame pointer or return address. They land
inside struct dirent dirbuf (msdosfs_readdir) or in unused stack
padding (msdosfs_lookup), and the corrupted dirbuf fields are
unconditionally overwritten before use. There is no control-flow
primitive to convert.
The bug (confirmed line-by-line)
msdosfs_readdir (sys/vfs/msdosfs/msdosfs_vnops.c:1665) calls
win2unixfn(&nb, ...) for each Win95 LFN slot. win2unixfn
(msdosfs_conv.c:642) validates:
if ((wep->weCnt & WIN_CNT) > howmany(WIN_MAXLEN, WIN_CHARS) || ...)
return -1;
howmany(255, 13) = ceil(255/13) = 20. The check is >, so
weCnt & WIN_CNT == 20 is allowed. With WIN_LAST set, that means
weCnt = 0x40 | 0x14 = 0x54, and id = (weCnt & WIN_CNT) - 1 = 19.
mbnambuf_write (msdosfs_conv.c:1031) then does:
slot = &nbp->nb_buf[id * WIN_CHARS]; /* = &nb_buf[19*13] = &nb_buf[247] */
count = strlen(name); /* = 13 for ASCII */
newlen = nbp->nb_len + count; /* = 0 + 13 = 13 */
if (newlen > WIN_MAXLEN || newlen > 127) ... /* 13 > 255? no β passes */
if (count > WIN_CHARS && nbp->nb_len != 0) /* 13 > 13? no β skipped */
...
memcpy(slot, name, count); /* writes nb_buf[247..259] */
nb_buf is char[WIN_MAXLEN+1] = char[256] (direntry.h:137). Bytes
nb_buf[256..259] are 4 bytes past the end. Confirmed.
For the KICONV variant (each of 13 chars becomes 2 bytes via
win2unixchr returning a non-zero high byte), count = 26, and the
overflow is 17 bytes (nb_buf[256..272]).
Stack-frame analysis (objdump of /boot/kernel/msdos.ko, gcc 8.3)
msdosfs_readdir (the dir-listing path)
Frame layout, recovered from the disassembly:
rbp+0x00 saved rbp
rbp-0x08 saved r15
rbp-0x10 saved r14
rbp-0x18 saved r13
rbp-0x20 saved r12
rbp-0x28 saved rbx
rbp-0x30 (8-byte gap, no live local)
...
rbp-0x140 struct dirent dirbuf { ; 272 bytes
+0 d_ino (uint64) ; = nb_buf[256..263]
+8 d_namlen (uint16) ; = nb_buf[264..265]
+10 d_type (uint8) ; = nb_buf[266]
+11 d_unused1 (uint8) ; = nb_buf[267]
+12 d_unused2 (uint32) ; = nb_buf[268..271]
+16 d_name[256] ; = nb_buf[272..527]
}
rbp-0x250 struct mbnambuf nb { ; 272 bytes
+0 nb_len (size_t)
+8 nb_last_id (int + 4 pad)
+16 nb_buf[256] ; overflows upward into dirbuf
}
Disassembly confirming the adjacency (memset of dirbuf.d_name starts at
-0x130, i.e. dirbuf + 0x10):
7662: lea -0x140(%rbp),%rax ; rax = &dirbuf 7669: lea 0x10(%rax),%rsi ; rsi = &dirbuf.d_name (= -0x130) 766f: mov %rsi,%rdi 7672: rep stos %rax,%es:(%rdi) ; memset(dirbuf.d_name, 0, 256)
mbnambuf_init(&nb) is called with lea -0x250(%rbp),%rdi (offset
-0x250). nb_buf lives at -0x250 + 16 = -0x240, and ends at
-0x240 + 256 = -0x140, which is exactly &dirbuf. The 4 ASCII
overflow bytes (nb_buf[256..259]) therefore land in dirbuf.d_ino low
32 bits. The 17-byte KICONV overflow lands in d_ino (8), d_namlen
(2), d_type (1), d_unused1 (1), d_unused2 (4), d_name[0] (1) β
all inside dirbuf, never reaching the saved registers at
-0x28..-0x01 or the saved rbp/ret at 0x00/+0x08.
Why the corruption is benign
dirbuf is filled per directory entry. After our malicious LFN slot is
processed (overflow happens), the loop continues. For the next entry:
- If it's an 8.3 alias (the normal case):
msdosfs_readdiroverwritesdirbuf.d_filenoatvnops.c:1684/1694/1698,dirbuf.d_typeat:1696/:1700, and eitherdirbuf.d_namlen = dos2unixfn(...)at:1704ormbnambuf_flush(&nb, &dirbuf)at:1712(which setsdp->d_namlen = nbp->nb_lenand copiesnb_buf[0..nb_len]intod_name). All corrupted fields are overwritten. - If it's
SLOT_EMPTY:goto outβdirbufis not consulted; the loop exits. - If it's another LFN slot:
continueβdirbufnot consulted.
In every branch, the corrupted dirbuf.d_ino (and for KICONV,
d_namlen/d_type/etc.) is either overwritten or never read. No info
leak, no integrity violation observable to userspace, no control-flow
change.
msdosfs_lookup (the path-lookup path)
Same nb on the stack at -0x140, but here there is no dirbuf
local β msdosfs_lookup doesn't fill a dirent, it just walks slots
looking for a name match. nb_buf[256] lands at -0x30, which is an
8-byte gap with no live local in it (objdump shows no references to
-0x30..-0x29 in the function body; the nearest live locals are at
-0x38 and below -0x140). So the overflow corrupts 4 bytes of unused
stack padding. No effect.
Trigger
craft_img.py patches a 16 MB FAT16 image (newfs_msdos -F 16 -C 16m)
at root-directory offset 33*512 = 16896 with a single 32-byte Win95
LFN slot:
00 : 54 weCnt = WIN_LAST(0x40) | 20 -> id=19 01-0A: 41 00 42 00 43 00 44 00 45 00 "ABCDE" (UTF-16 LE) 0B : 0f weAttributes = ATTR_WIN95 0C : 00 weReserved1 0D : 42 weChksum 0E-19: 46 00 47 00 48 00 49 00 4a 00 4b 00 "FGHIJK" 1A-1B: 00 00 weReserved2 1C-1F: 4c 00 4d 00 "LM"
Followed by SLOT_EMPTY (0x00 * 32) so the loop exits. Mount with
mount -t msdos /dev/vn0 /mnt and ls /mnt as the unprivileged user β
no panic, no message, empty listing. The overflow has fired (verified
by harness), but it's silent.
Deterministic harness
df0795_harness.c reproduces the exact mbnambuf_write code with a
poisoned stack frame and prints the byte-by-byte corruption map. Run on
the guest:
== DF-0795 ASCII variant (weCnt=0x54, id=19, count=13) ==
mbnambuf_write returned 0 (0 = OK, overflow happened)
slot offset = 19*13 = 247 (writes nb_buf[247..259])
overflow bytes = nb_buf[256..259] = 4 bytes past end
byte-by-byte corruption map:
nb_buf[256] -> dirbuf byte 0 (dirbuf.d_ino) = 0x4a 'J'
nb_buf[257] -> dirbuf byte 1 (dirbuf.d_ino) = 0x4b 'K'
nb_buf[258] -> dirbuf byte 2 (dirbuf.d_ino) = 0x4c 'L'
nb_buf[259] -> dirbuf byte 3 (dirbuf.d_ino) = 0x4d 'M'
== DF-0795 KICONV variant (weCnt=0x54, id=19, count=26) ==
overflow bytes = nb_buf[256..272] = 17 bytes past end
... lands in d_ino (8), d_namlen (2), d_type (1), d_unused1 (1),
d_unused2 (4), d_name[0] (1) -- ALL inside struct dirent
Exploit-chain attempt (Phase 6)
Not pursued past the characterization step, because the characterization itself establishes the hard blocker:
- The 4-byte (ASCII) overflow reaches only
dirbuf.d_ino[0..3]. - The 17-byte (KICONV) overflow reaches
dirbuf.d_ino[0..7],d_namlen,d_type,d_unused1,d_unused2,d_name[0]. - No function pointer, no
ucred *, nouid, no refcount, no saved%rbp, no return address is corrupted. - All corrupted fields are unconditionally overwritten by
msdosfs_readdirbeforevop_write_direntconsumes them.
This is the valid Phase-6 hard blocker: "the overflow only reaches fields/locals that don't yield a control-flow primitive, and the corrupted data is unconditionally overwritten before use." There is no primitive to groom, no victim object to corrupt, no pointer to redirect. Heap-style grooming is irrelevant β this is a stack buffer overflow into a known-inert neighbor.
The finding markdown's "no kernel stack canary β kernel code exec"
inference is incorrect for this call site: the absence of a canary
means a stack smash is undetected, but the smash still has to reach
the saved RIP to hijack control flow, and on this compilation it does
not. (Verified by objdump: nb_buf ends at -0x140, the saved-RIP
slot is at +0x08 β a 0x148 = 328-byte gap, of which the first 272
are dirbuf and the next ~48 are live locals / saved registers. A
4-byte or 17-byte write starting at -0x140 cannot reach +0x08.)
Fix
fix.diff adds the absolute-offset bounds check the finding proposes,
in mbnambuf_write just before the unchecked memcpy:
if (id * WIN_CHARS + count > sizeof(nbp->nb_buf))
return (ENAMETOOLONG);
This matches the finding markdown's ## Recommended fix proposal
verbatim. It is minimal, targeted at the root cause, and does not
change the legitimate LFN reassembly path (legitimate names with β€19
slots Γ β€13 chars stay within nb_buf).
Fix validation (Phase 8)
git apply --check(well,patch -p1 --dry-run) on/usr/src:Hunk #1 succeeded at 1058. done DRYRUN rc=0.makeinsys/vfs/msdosfsproduced a newmsdos.kowith the check compiled in (objdump at0x1082:cmp $0x100,%rbx; ja 0x10c0).- Full
make -j6 nativekernel KERNCONF=X86_64_GENERICbuild: rc=0. The rebuilt kernel'smbnambuf_writeat0xffffffff80716130has the samecmp $0x100,%rbx; jacheck at0xffffffff807161a2. - Booted
#1kernel, mounted the same malicious image, ranls: no panic, no regression, guest stayed up. - Deterministic before/after harness:
* ORIG
mbnambuf_write(id=19, count=13)β rc=0, 4 bytes corrupted. * FIXEDmbnambuf_write(id=19, count=13)β rc=-63 (ENAMETOOLONG), 0 bytes corrupted.
The fix closes the bug. fix_status: fixed.
Conclusion
A real memory-safety bug (CWE-787, 4-byte / 17-byte stack OOB write),
correctly diagnosed path:line, correctly fixed. Not a privilege
escalation on this kernel compilation β the overflow is contained
within struct dirent dirbuf (readdir) or unused stack padding
(lookup), and the corrupted fields are discarded before use. The
absence of a kernel stack canary does not help the attacker here
because the overflow physically cannot reach the saved RIP.
Fix verification
fixedVALIDATED. The bug is silent (no panic, no observable behavioral difference between fixed and unfixed) so the before/after proof is necessarily at the source + binary level, not behavioral: (1) patch -p1 --dry-run on /usr/src: Hunk #1 succeeded at 1058, rc=0; (2) module-only make in sys/vfs/msdosfs: rc=0, objdump of new mbnambuf_write shows the new check 'cmp $0x100,%rbx; ja' at 0x1082; (3) full make -j6 nativekernel KERNCONF=X86_64_GENERIC: === NK_DONE rc=0 ===, and the rebuilt kernel's mbnambuf_write at 0xffffffff80716130 has the same 'cmp $0x100,%rbx; ja' at 0xffffffff807161a2; (4) booted #1 kernel, mounted the SAME malicious image, ls: no panic, no regression, guest stayed up; (5) deterministic harness pair: ORIG returns rc=0 with 4 bytes past nb_buf corrupted, FIXED returns rc=-63 (ENAMETOOLONG) with 0 bytes corrupted. fix_baseline_reproduced=1 (ORIG harness + #0 mount+ls confirm the bug fires), fix_patched_reproduced=0 (FIXED harness confirms overflow prevented; #1 mount+ls confirms no regression). The fix closes the bug.
findings/poc/DF-0795/fix.diff (the git-apply-able diff); fix_build.log (module-only build, rc=0); fix_build_kernel.log (full nativekernel, === NK_DONE rc=0 ===); harness_before_fix.log: 'ORIG mbnambuf_write(id=19, count=13) rc=0 ; bytes-past-end corrupted=4'; harness_after_fix.log: 'FIXED mbnambuf_write(id=19, count=13) rc=-63 (-63=ENAMETOOLONG) ; bytes-past-end corrupted=0'; run_baseline_kernel.log (kernel #0, no panic, silent overflow); run_patched_kernel.log (kernel #1, no panic, no regression). objdump of patched kernel mbnambuf_write shows the new 'cmp $0x100,%rbx; ja ENAMETOOLONG-ret' at 0xffffffff807161a2-0xffffffff807161a9.
Confirmed kernel references
- sys/vfs/msdosfs/msdosfs_vnops.c:1486
- sys/vfs/msdosfs/msdosfs_vnops.c:1662
- sys/vfs/msdosfs/msdosfs_vnops.c:1665
- sys/vfs/msdosfs/msdosfs_conv.c:642
- sys/vfs/msdosfs/msdosfs_conv.c:1044
- sys/vfs/msdosfs/msdosfs_conv.c:1047
- sys/vfs/msdosfs/msdosfs_conv.c:1053
- sys/vfs/msdosfs/msdosfs_conv.c:1062
- sys/vfs/msdosfs/msdosfs_lookup.c:112
- sys/vfs/msdosfs/msdosfs_lookup.c:274
- sys/vfs/msdosfs/direntry.h:101
- sys/vfs/msdosfs/direntry.h:107
- sys/vfs/msdosfs/direntry.h:134
- sys/vfs/msdosfs/direntry.h:137
- sys/conf/files:2219
Detail
Exploit chain
BLOCKED by valid Phase-6 hard blocker: 'the overflow only reaches fields/locals that don't yield a control-flow primitive, and the corrupted data is unconditionally overwritten before use.' Bucket/victim/grooming is irrelevant (this is a stack-buffer overflow into a known-inert neighbor, not a slab UAF). Characterization (step 1 of Phase 6) itself establishes the blocker: the 4-byte ASCII overflow reaches only dirbuf.d_ino[0..3] (readdir) or 8 bytes of unused padding at -0x30 (lookup); the 17-byte KICONV overflow reaches dirbuf.d_ino..d_name[0] -- ALL inside struct dirent. None of these is a function pointer, ucred*, uid, refcount, saved rbp, or saved RIP. There is no primitive to groom, no victim to corrupt, no pointer to redirect. Heap grooming cannot help (different memory region; the victim is a stack frame, not a slab object). The finding's theory that 'no stack canary => code exec' conflates 'undetected smash' with 'smash reaches saved RIP'; on this gcc-8.3 compilation the smash is 4-17 bytes starting at rbp-0x140 and the saved RIP is at rbp+0x08, a 0x148=328-byte gap that the overflow cannot bridge. No chain file written because no chain is possible; the deterministic harness (df0795_harness.c) IS the characterization. Honest impact ceiling: integrity-only OOB write of attacker bytes into a discarded stack neighbor.
Evidence (decisive lines)
findings/poc/DF-0795/ holds the full evidence pack. Decisive snippets: (1) harness_before_fix.log -- 'ORIG mbnambuf_write(id=19, count=13) rc=0 ; bytes-past-end corrupted=4 (expected 4)' and the byte map showing nb_buf[256..259] -> dirbuf.d_ino[0..3] = 0x4a/4b/4c/4d ('JKLM'); the 17-byte KICONV variant corrupts d_ino(8)+d_namlen(2)+d_type(1)+d_unused1(1)+d_unused2(4)+d_name[0](1). (2) run_baseline_kernel.log -- mount+ls of the crafted FAT16 image on kernel #0: 'mount rc=0 ... ls rc=0 ... DONE -- no panic, no kernel message. Overflow was SILENT.' (3) objdump of msdosfs_readdir confirming dirbuf at -0x140 immediately follows nb at -0x250, and that mbnambuf_init is called with lea -0x250(%rbp),%rdi. (4) No panic.txt because there is no panic.
PoC changes
Created findings/poc/DF-0795/ from scratch. craft_img.py patches a fresh FAT16 image at root-dir offset 16896 with a 32-byte Win95 LFN slot (weCnt=0x54, ATTR_WIN95, 13 ASCII chars in name1/2/3 UTF-16 LE) followed by SLOT_EMPTY. run.sh does vnconfig+mount+ls+umount. df0795_harness.c is a verbatim userland transcription of mbnambuf_write with a poisoned frame, printing the byte-by-byte corruption map for both ASCII (4-byte) and KICONV (17-byte) variants -- this is the proof the overflow lands inside struct dirent. df0795_harness_fixed.c is the same code with the fix applied, demonstrating ORIG (4 bytes corrupted, rc=0) vs FIXED (0 bytes corrupted, rc=-ENAMETOOLONG).
Verified recommended fix
In sys/vfs/msdosfs/msdosfs_conv.c, mbnambuf_write, immediately before the memcpy at line 1062, add: if (id * WIN_CHARS + count > sizeof(nbp->nb_buf)) return (ENAMETOOLONG); This bounds the absolute write position (the existing cumulative check at :1047 only bounds nb_len+count, not id*13+count). MATCHES the finding markdown's ## Recommended fix proposal verbatim. The full git-apply-able diff lives in findings/poc/DF-0795/fix.diff.
Verdict
REPRODUCED (CWE-787 OOB write confirmed by source trace + objdump + deterministic harness); NOT EXPLOITABLE to uid0. The bug is exactly as described: msdosfs_conv.c:1044 mbnambuf_write sets slot=&nb_buf[idWIN_CHARS]; for weCnt=0x54 (WIN_LAST|20), id=19, slot=nb_buf[247]; msdosfs_conv.c:642 win2unixfn allows weCnt&WIN_CNT==20 (the >howmany(255,13)=>20 check uses strict >); memcpy(slot,name,13) at :1062 writes nb_buf[247..259], overflowing nb_buf[256] by 4 bytes (ASCII) or 17 bytes (KICONV count=26). Confirmed by df0795_harness.c with poisoned frame. HOWEVER the finding's 'no kernel stack canary => kernel code exec' inference is WRONG for this call site: objdump of /boot/kernel/msdos.ko shows that in msdosfs_readdir the compiler places 'struct dirent dirbuf' IMMEDIATELY after nb (nb at -0x250(%rbp) ending at -0x140, dirbuf at -0x140), so the 4 overflow bytes land in dirbuf.d_ino[0..3] (and the 17-byte KICONV variant lands in d_ino, d_namlen, d_type, d_unused1, d_unused2, d_name[0] -- ALL inside struct dirent). No function pointer, ucred, uid, refcount, saved rbp or saved RIP is touched (saved RIP is at +0x08, 0x148 bytes away; only 4-17 bytes are written). In msdosfs_lookup the overflow lands at -0x30(%rbp) which objdump confirms is unused stack padding (no live local). On top of that, msdosfs_readdir UNCONDITIONALLY overwrites dirbuf.d_fileno/d_type/d_namlen/d_name at vnops.c:1684/1694/1698/1704/1712 before vop_write_dirent consumes them. Result: silent OOB write of attacker-controlled bytes into a stack neighbor that is discarded before use. No panic, no leak, no control-flow change, no escalation primitive.
No comments yet.