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

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)

No comments yet.

PoC verification

Evidence pack

findings/poc/DF-2555 Β· 11 files
FileTypeDescriptionSize
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
README.md readme PoC description and instructions
↓ download 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 ntvattrdef allocation (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 image
  • evil.ntfs β€” the crafted image with all-nonzero $AttrDef entry
  • safe.ntfs β€” control image with normal null-terminated $AttrDef name
  • fix.diff β€” git-apply-able fix bounding the loop
VERDICT.md verdict Full analysis and fix validation report
↓ download raw

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) where sizeof(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

fixed
baseline reproduced→ patch + rebuild →patched clean

VALIDATED: 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.
↓ fix.diffDragonFly 6.5-DEVELOPMENT #1: Sat Aug 8 20:29:50 UTC 2026 root@dfbsd:/usr/obj/usr/src/sys/X86_64_GENERIC

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.