Heap buffer overflow in $AttrDef name copy via unterminated wchar name
Summary
$AttrDef translation loop at :458-460 copies wchar names from on-disk attrdef into narrower ntvattrdef using unbounded do/while loop terminating only on zero wchar. Source wchar[64]=128 bytes larger than destination char[64]=64 bytes. Crafted NTFS image with $AttrDef entry whose name field has all 64 wchars non-zero causes loop to write past end of destination array corrupting adjacent heap memory. Loop has no upper bound on j runs until ad.ad_name[j] evaluates zero. j>=64 OOB writes past ntm_ad[i].ad_name into ad_namelen/ad_type and past struct into heap. kmalloc rounds to slab boundaries small overflows land in internal fragmentation.
Discussion (0)
PoC verification
Evidence pack
findings/poc/DF-2555 Β· 11 files| File | Type | Description | Size | |
|---|---|---|---|---|
| gen_image.py | trigger-source | Python script to generate crafted NTFS image with all-nonzero $AttrDef entry | 10.4 KB | view raw |
| evil.ntfs | trigger-image | Crafted NTFS image (159744 bytes) with overflow-triggering $AttrDef | 156.0 KB | β download |
| safe.ntfs | control-image | Control NTFS image with normal null-terminated $AttrDef name | 156.0 KB | β download |
| build.sh | build-script | Build script that runs gen_image.py | 153 B | view raw |
| run.sh | run-script | Run script that mounts the crafted image | 652 B | view raw |
| fix.diff | suggested-fix | git-apply-able fix: bound do/while loop to destination buffer size | 567 B | view raw |
| fix_build.log | build-log | Full single-fix kernel build output (35732 lines, rc=0) | 5.6 MB | β download |
| fix_run.log | run-log | Patched kernel test: j=63 (bounded) on evil image | 104 B | view raw |
| env.txt | environment | uname, cc version | 299 B | view raw |
| VERDICT.md | verdict | Full analysis and fix validation report | 4.8 KB | β raw |
| README.md | readme | PoC description and instructions | 2.0 KB | β raw |
DF-2555: NTFS $AttrDef heap buffer overflow
Vulnerability
The $AttrDef translation loop at sys/vfs/ntfs/ntfs_vfsops.c:458-460 copies
wchar attribute names from the on-disk $AttrDef file into the in-memory
ntvattrdef structure using an unbounded do/while loop:
j = 0;
do {
ntmp->ntm_ad[i].ad_name[j] = ad.ad_name[j];
} while(ad.ad_name[j++]);
The source ad.ad_name is wchar[NTFS_ATTRNAME_MAXLEN] = wchar[0x40] =
u_int16_t[64] = 128 bytes (64 wchars).
The destination ntm_ad[i].ad_name is char[0x40] = 64 bytes.
If a crafted NTFS image has an $AttrDef entry whose name field has 64+ non-zero
wchars with no NUL terminator within 64 wchars, and the following struct fields
(ad_type, reserved1, ad_flag, ad_minlen, ad_maxlen) are also non-zero,
the loop continues past the 64-byte destination buffer, overflowing into adjacent
heap memory.
Build
python3 gen_image.py evil.ntfs # generates the crafted image
Run
# As root on DragonFlyBSD:
kldload ntfs
vnconfig -c vn0 evil.ntfs
mount_ntfs -o ro /dev/vn0 /mnt/ # overflow happens at mount time
Expected behavior
The mount succeeds (the overflow is silent β the slab allocator doesn't detect
small intra-chunk overflows immediately). With an instrumented kernel, the loop
counter j reaches 80 (vs the 63-element limit), proving a 16-byte heap
overflow past the 64-byte ad_name buffer.
Impact
- Trigger: root mounts a crafted NTFS image (e.g. from removable media)
- Primitive: heap buffer overflow of 8-16+ bytes past a
struct ntvattrdefallocation (72 bytes, slab zone index 8, chunk size 72 bytes) - Overflow target: adjacent slab chunk in the same 72-byte zone
- Characterization: verified with instrumented module showing j=80 (max=63)
Files
gen_image.pyβ Python script to generate the crafted NTFS imageevil.ntfsβ the crafted image with all-nonzero $AttrDef entrysafe.ntfsβ control image with normal null-terminated $AttrDef namefix.diffβ git-apply-able fix bounding the loop
DF-2555: NTFS $AttrDef heap buffer overflow β VERDICT
Verdict: REPRODUCED (heap overflow confirmed, fix validated)
Mechanism
The $AttrDef translation loop at sys/vfs/ntfs/ntfs_vfsops.c:457-461 copies
wchar attribute names from the on-disk $AttrDef file into the in-memory
ntvattrdef structure using an unbounded do/while loop:
j = 0;
do {
ntmp->ntm_ad[i].ad_name[j] = ad.ad_name[j]; // char β wchar (truncation)
} while(ad.ad_name[j++]); // no bounds check on j
The source ad.ad_name is wchar[NTFS_ATTRNAME_MAXLEN] = u_int16_t[0x40] =
128 bytes (64 wchars, sys/vfs/ntfs/ntfs.h:207).
The destination ntm_ad[i].ad_name is char[0x40] = 64 bytes
(sys/vfs/ntfs/ntfs.h:216).
When a crafted NTFS image has an $AttrDef entry whose name field (64 wchars)
is entirely non-zero AND whose following struct fields (ad_type, reserved1,
ad_flag, ad_minlen, ad_maxlen β 32 more bytes = 16 wchars) are also
non-zero, the loop continues past the 64-byte destination buffer. After the
source struct ends (160 bytes), the loop reads stack residue until a zero wchar
is encountered.
Confirmed with instrumented module:
- Safe image (null-terminated name): j=21 (within 64-byte limit) β
- Evil image (all non-zero): j=80 β 16-byte heap overflow past the 64-byte
ad_name buffer, writing 8 bytes past the 72-byte struct ntvattrdef
allocation into adjacent slab memory.
Primitive characterization
- Write size: up to 16+ bytes past the 64-byte destination buffer
- Allocation:
kmalloc(num * sizeof(struct ntvattrdef), M_NTFSMNT, M_WAITOK)wheresizeof(struct ntvattrdef)= 72 bytes (char[64] + int + uint32_t) - Slab zone: zoneindex(72) β 8-byte-aligned chunks, zone index 8, chunk size exactly 72 bytes. Overflow of 8 bytes goes into the adjacent slab chunk.
- Content: attacker-controlled (truncated wchar values from crafted image)
- Trigger: root mounts a crafted NTFS image (e.g. from removable media)
- Impact ceiling: heap corruption of adjacent slab objects; root-triggered at mount time (valid hard blocker for uid0 escalation β the corruption is in the mount path, not an unprivileged syscall surface)
Exploit chain
Not escalated to uid0. The overflow fires in the mount path (ntfs_mountfs),
which requires root to issue mount_ntfs. This is a root-triggered heap
overflow β the privilege boundary crossed is rootβkernel, not unprivβroot.
Per the Phase 6 bright-line rule, the valid hard blocker applies: the vulnerable
code path is reachable only from a root mount operation. A malicious image on
removable media auto-mounted by an admin is the realistic threat model (Medium
severity, consistent with the finding's classification).
The primitive IS characterized: 8-16 bytes of attacker-controlled heap overflow into adjacent 72-byte slab chunks, silently corrupting heap metadata on the default GENERIC kernel (INVARIANTS does not detect intra-chunk overflows with DragonFlyBSD's bitmap-based slab tracking).
Fix
Replace the unbounded do/while with a bounded for loop:
/* Before (vulnerable): */
j = 0;
do {
ntmp->ntm_ad[i].ad_name[j] = ad.ad_name[j];
} while(ad.ad_name[j++]);
ntmp->ntm_ad[i].ad_namelen = j - 1;
/* After (fixed): */
for (j = 0; j < (int)sizeof(ntmp->ntm_ad[i].ad_name) - 1 &&
ad.ad_name[j] != 0; j++)
ntmp->ntm_ad[i].ad_name[j] = ad.ad_name[j];
ntmp->ntm_ad[i].ad_name[j] = '\0';
ntmp->ntm_ad[i].ad_namelen = j;
The fix bounds j to sizeof(ad_name) - 1 = 63, ensuring the loop never writes
past the 64-byte buffer. Names longer than 63 characters are truncated and
null-terminated.
PoC changes
Authored gen_image.py from scratch (the PoC dir was empty). The Python script
generates a minimal but valid NTFS image with:
- Valid boot sector (OEM ID "NTFS ", BPB with bps=512, spc=1, mftrecsz=2)
- MFT entries 0-10 with proper fixup arrays
- Entry 4 ($AttrDef) with a crafted resident $DATA containing one entry with all
160 bytes set to 0x41 (non-zero) + a zero terminator entry
- Entry 5 ($Root) with a properly named ($I30) INDEX_ROOT for an empty directory
- Entry 6 ($Bitmap) with cluster bitmap
- Entry 10 ($UpCase) with non-resident $DATA containing a 128KB toupper table
Also generated safe.ntfs as a control image with a normal null-terminated name.
Fix validation
- Baseline (#0 kernel, unpatched): evil image mounts; instrumented module
shows
j=80(16-byte overflow) β BUG PRESENT - Patched (#1 kernel, with fix.diff): evil image mounts; instrumented module
shows
j=63(bounded, no overflow) β BUG FIXED - Safe image on both:
j=21(within bounds) β CONTROL OK
The fix.diff applies cleanly, compiles, and the single-fix kernel boots and mounts both images correctly without panic.
Fix verification
fixedVALIDATED: evil.ntfs image triggers j=80 (16-byte heap overflow) on unpatched #0 baseline kernel (confirmed via instrumented ntfs module). On single-fix #1 kernel with fix.diff applied (bounded for loop), same evil.ntfs yields j=63 (loop stops at buffer boundary, no overflow). Fix closes the bug β loop can never write past 64-byte ad_name buffer.
baseline (#0 unpatched): j=80 (max=63) β 16-byte OVERFLOW confirmed. patched (#1 fix.diff): j=63 (max=63) β loop bounded, NO overflow. Both images mount successfully on both kernels. Fix prevents overflow without breaking normal NTFS mounts.
Confirmed kernel references
Detail
Exploit chain
Root-triggered heap overflow at mount time (ntfs_mountfs). Primitive: 8-16+ bytes of attacker-controlled heap overflow past a 72-byte struct ntvattrdef allocation (M_NTFSMNT, slab zone index 8, chunk size exactly 72 bytes). Overflow writes into adjacent slab chunk. NOT escalated to uid0: vulnerability fires in mount path requiring root mount_ntfs β root->kernel corruption (root->kernel game-over by definition; valid hard blocker per Phase 6). Malicious NTFS image on auto-mounted removable media is the realistic threat model (Medium severity). No exploit.c written β primitive characterized by instrumented module output (j=80 vs max=63), not a slab-grooming chain, because privilege boundary is root->kernel not unpriv->root.
Evidence (decisive lines)
Instrumented module output (UNPATCHED #0 kernel): evil.ntfs: DF2555: entry 0: j=80 (max=63) ok <- 16-byte OVERFLOW; safe.ntfs: DF2555: entry 0: j=21 (max=63) ok <- within bounds. Instrumented module output (PATCHED #1 kernel with fix.diff): evil.ntfs: DF2555FIX: entry 0: j=63 (max=63) ok-bounded <- FIX WORKS. Baseline mount: BASELINE_MOUNT_RC=0 (overflow happens silently). Patched mount: PATCHED_MOUNT_RC=0 (no overflow, loop bounded at 63).
PoC changes
Authored gen_image.py from scratch (PoC dir empty). Generates minimal valid NTFS image (159744 bytes, 312 sectors): valid boot sector, MFT entries 0-10 with fixup arrays, entry 4 ($AttrDef) with crafted resident $DATA (one entry all-0x41 bytes + zero terminator), entry 5 ($Root) with properly named ($I30) INDEX_ROOT, entry 6 ($Bitmap), entry 10 ($UpCase) with non-resident $DATA (128KB toupper table). safe.ntfs as control. Fixed attribute name placement bug ($I30 name wrong offset initially causing separate lockmgr panic during readdir).
Verified recommended fix
Replace unbounded do/while loop at ntfs_vfsops.c:457-461 with bounded for loop: 'for (j = 0; j < (int)sizeof(ntmp->ntm_ad[i].ad_name) - 1 && ad.ad_name[j] != 0; j++) ntmp->ntm_ad[i].ad_name[j] = ad.ad_name[j]; ntmp->ntm_ad[i].ad_name[j] = 0; ntmp->ntm_ad[i].ad_namelen = j;'. Bounds j to 63 (last valid index of 64-byte char array), preventing any write past buffer. Addresses root cause directly.
Verdict
REPRODUCED. The unbounded do/while loop at sys/vfs/ntfs/ntfs_vfsops.c:458-460 copies wchar names from on-disk $AttrDef into a narrower char[64] destination with NO bounds check. With a crafted NTFS image whose $AttrDef entry has all 160 bytes non-zero (64 wchars of name + 32 bytes of struct fields), an instrumented kernel module proves the loop runs for j=80 iterations β writing 16 bytes past the 64-byte ad_name buffer and 8 bytes past the 72-byte struct ntvattrdef allocation into adjacent slab memory. The safe control image (null-terminated name) correctly stops at j=21. The overflow is silent on default GENERIC because DragonFlyBSD's slab allocator uses bitmap-based INVARIANTS tracking that does not detect intra-zone overflows immediately; the mount succeeds without panic.
No comments yet.