DragonFlyBSD Kernel Audit
← triage · dashboard
DF-0871

Heap overflow in $AttrDef name copy via unbounded wchar-to-char do/while walk

Summary

ntfs_vfsops.c:444 kmalloc(num*sizeof(ntvattrdef)=72B each). :458-460 do{ ntmp->ntm_ad[i].ad_name[j]=ad.ad_name[j] }while(ad.ad_name[j++]) NO bound on j. ad_name source is wchar[64] in 160-byte struct attrdef. All 64 wchars + 16 wchars of ad_type/reserved1/ad_flag/ad_minlen/ad_maxlen non-zero => j runs 0..79. j=72..79 writes past ntmp->ntm_ad[i] into ntmp->ntm_ad[i+1].ad_name[0..7] heap OOB write not repaired by post-loop assignments. Crafted NTFS image mount = heap corruption M_NTFSMNT. Slab grooming potential priv-esc.

Discussion (0)

No comments yet.

PoC verification

Evidence pack

findings/poc/DF-0871 · 24 files
FileTypeDescriptionSize
craft_img.py trigger-source NTFS image crafter: evil $AttrDef (all-nonzero 160-byte records) 11.0 KB view raw
craft_benign.py trigger-source control crafter: short NUL-terminated $AttrDef names (no overflow) 2.3 KB view raw
harness.c trigger-source deterministic overflow harness (BUGGY do/while, poisoned allocator) 7.5 KB view raw
harness_fixed.c exploit-chain fixed harness mirroring fix.diff (bounded do/while + NUL-terminate) 2.5 KB view raw
build.sh build-script exact guest build: images + harnesses 802 B view raw
run.sh run-script exact run: harness + live mount churn 2.6 KB view raw
build.log build-log harness build output 120 B view raw
baseline_harness.log run-log baseline #0 harness: SIGSEGV at j=72 504 B view raw
harness_run.log run-log buggy harness SIGSEGV + cross-entry corruption report 2.8 KB view raw
harness_compare.log run-log buggy-vs-fixed harness before/after 811 B view raw
mount_attempt.log run-log live mount attempts: silent corruption + churn 688 B view raw
panic.txt panic-signature Fatal trap 12 / kqueue_register+0x526 fault 0xffffffffffffffff 859 B view raw
fix.diff suggested-fix git-apply-able: bound do/while to sizeof(ad_name) + NUL-terminate 593 B view raw
fix_apply.log fix-log patch application + patched region verification 831 B view raw
fix_build.log fix-build-log single-fix ntfs.ko build (cc 8.3, rc=0) 15.8 KB view raw
fix_run.log fix-run-log patched module: 30 evil-image mount cycles, 0 panics 557 B view raw
env.txt environment uname, cc version, vfs.usermount, module sha256 536 B view raw
ntfs_evil.img test-image crafted NTFS image, num=2 evil $AttrDef entries 512.0 KB ↓ download
ntfs_evil_n1.img test-image crafted NTFS image, num=1 evil $AttrDef entry 512.0 KB ↓ download
ntfs_benign.img test-image control NTFS image, well-formed $AttrDef 512.0 KB ↓ download
README.md readme human-readable reproduction guide 9.2 KB ↓ raw
VERDICT.md verdict full narrative: mechanism, evidence, impact, fix validation 8.1 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 human-readable reproduction guide
↓ download raw

DF-0871 — PoC evidence pack

Heap overflow in NTFS $AttrDef name copy via unbounded wcharchar do/while walk. File: sys/vfs/ntfs/ntfs_vfsops.c:444,458-460. CWE-787 (OOB write). Severity: High.

The bug (confirmed line-by-line)

ntfs_mountfs() parses the on-disk $AttrDef table (MFT record 4) at mount time. For every 160-byte struct attrdef it reads, it allocates a 72-byte struct ntvattrdef and copies the attribute name with an unbounded do/while:

/* sys/vfs/ntfs/ntfs_vfsops.c */
ntmp->ntm_ad = kmalloc(num * sizeof(struct ntvattrdef), M_NTFSMNT, M_WAITOK);  /* :444 */
...
for (i=0;i<num;i++){
    ...
    j = 0;                                                       /* :457 */
    do {
        ntmp->ntm_ad[i].ad_name[j] = ad.ad_name[j];              /* :459  DEST is char[0x40]=64 */
    } while(ad.ad_name[j++]);                                     /* :460  NO bound on j */
    ntmp->ntm_ad[i].ad_namelen = j - 1;                          /* :461 */
    ntmp->ntm_ad[i].ad_type = ad.ad_type;                        /* :462 */
}

Type facts (sys/vfs/ntfs/ntfs.h): - typedef u_int16_t wchar; (ntfs.h:37) - struct attrdef { wchar ad_name[0x40]; u32 ad_type; u32 r1[2]; u32 ad_flag; u64 min; u64 max; } = 160 B (ntfs.h:206) - struct ntvattrdef { char ad_name[0x40]; int ad_namelen; u32 ad_type; } = 72 B (ntfs.h:215)

The destination ad_name is char[64] but the loop index j is bounded only by finding a NUL wchar in the source ad.ad_name[64]. If the on-disk $AttrDef record has no NUL wchar anywhere in its 64-wide-char name and its trailing 32 B (ad_type/reserved1/ad_flag/ad_minlen/ad_maxlen, which alias ad.ad_name[64..79]) are also all non-zero half-words, the walk runs j = 0..80+. The write ntmp->ntm_ad[i].ad_name[j] then:

j destination byte effect
0..63 ntvattrdef[i].ad_name[0..63] in-bounds
64..67 ntvattrdef[i].ad_namelen repaired by :461
68..71 ntvattrdef[i].ad_type repaired by :462
72.. ntvattrdef[i+1].ad_name[0..] (cross-entry) or, for the last entry, past the whole kmalloc() HEAP OOB WRITE into M_NTFSMNT slab

The post-loop assignments at :461-462 "repair" the within-object bytes (ad_namelen/ad_type of entry i) but do not touch the cross-entry / past-allocation bytes written at j>=72. Those stay corrupted.

Reproduction

A) Deterministic harness (harness.c)

A faithful userspace transcription of :444,457-462 backed by a poisoned allocator (victim array at the end of a guard-paged page). No slab luck or INVARIANTS needed — the overflow is detected byte-exactly.

  • buggy, num=1: do/while writes dest[j] for j>=72SIGSEGV at the first byte past the 72-byte ntvattrdef object. Deterministic proof the write leaves the object.
  • buggy, num=2: entry[0]'s walk overwrites 72 bytes of entry[1].ad_name (cross-entry corruption, the finding's claimed primitive), then the last entry's overflow crosses the allocation → SIGSEGV.
  • fixed (harness_fixed.c, mirrors fix.diff): clean exit 0, 0 bytes past the allocation, name length capped at 63 + NUL-terminated.

B) Live kernel (ntfs.ko on #0 GENERIC, INVARIANTS ON)

A crafted NTFS image (craft_img.py, 1–2 evil 160-byte $AttrDef records whose entire 160 bytes are non-zero half-words + a zero terminator) is mounted with vnconfig + mount_ntfs (root). Each mount runs the unbounded copy. During a churn loop the slab gets groomed so the corrupted ntm_ad chunk is adjacent to a victim object; the 0xFF bytes the evil data writes (truncated low-bytes of the 0xFFFFFFFF trailing fields) corrupt a victim pointer to 0xffffffffffffffff, and its later dereference faults:

Fatal trap 12: page fault while in kernel mode
fault virtual address  = 0xffffffffffffffff
instruction pointer     = 0x8:0xffffffff8063b1c6
current process         = 1015
Stopped at kqueue_register+0x526:  movq (%r15),%rdx

(panic.txt.) Controlled experiment (causation): - benign image (short NUL-terminated $AttrDef names → walk stops j<64, no overflow): 10 mount/unmount cycles, 0 panics, guest UP. - evil image: panic during churn (above).

The live panic is timing/layout-dependent (heap grooming); the harness is the deterministic proof and reproduces on every run.

Impact / threat model

  • Primitive: attacker-content-controlled heap OOB write into the M_NTFSMNT slab zone (kmalloc-128/kmalloc-256). The written bytes are the low bytes of attacker-chosen on-disk wchars, so content is shapable.
  • Trigger: mount -t ntfs of a crafted image. Root-only in the default config (vfs.usermount=0; mount_ntfs/vnconfig not setuid).
  • Realistic threat: a malicious NTFS image (USB / download / multi-user system / automount) causes kernel heap corruption and/or panic when mounted by root — the classic untrusted-filesystem-image attack surface. High severity (kernel memory corruption from untrusted data).
  • uid=0 escalation: the trigger requires root credential, so there is no unprivileged→root privilege boundary to cross end-to-end (root→kernel is game-over by definition). This is a valid hard blocker for an uid0 verdict. The primitive could be slab-groomed to escalate if an unprivileged user could mount the image (e.g. an admin sets vfs.usermount=1 and provides an attacker-owned, device-accessible image), but that is a conditional scenario, not the default.

Fix (fix.diff)

Bound the do/while to the destination array length and force NUL-termination:

-           } while(ad.ad_name[j++]);
+           } while(ad.ad_name[j++] &&
+               j < (int)sizeof(ntmp->ntm_ad[i].ad_name));
+           /* DF-0871: force NUL-termination within the fixed-size buffer
+            * so the wchar->char walk can never overrun ntvattrdef.ad_name. */
+           ntmp->ntm_ad[i].ad_name[sizeof(ntmp->ntm_ad[i].ad_name) - 1] = '\0';

This is a minimal, targeted fix at the root cause. git apply --check passes. Supersedes/matches the finding markdown's proposal.

Phase 8 — fix validation (single-fix ntfs.ko)

  • Applied fix.diff to in-guest /usr/src/sys/vfs/ntfs/ntfs_vfsops.c (git apply), rebuilt ntfs.ko (cd /usr/src/sys/vfs/ntfs && make, rc=0, fix_build.log), hot-swapped (kldunload/kldload, sha256 dbc96b62…).
  • Before (unpatched #0): buggy harness SIGSEGVs at j=72 (deterministic); live evil-image churn → kqueue_register panic (panic.txt).
  • After (patched ntfs.ko): fixed harness exit 0, 0 bytes past the allocation (harness_compare.log); 30 evil-image mount cycles (15× num=1
  • 15× num=2) → 0 panics, guest UP (fix_run.log). Benign image still mounts and parses attributes (functional regression check OK).

fix_status: fixed.

Files

file purpose
craft_img.py NTFS image crafter: evil $AttrDef (all-nonzero) records
craft_benign.py control crafter: short NUL-terminated $AttrDef names
harness.c deterministic overflow harness (BUGGY do/while)
harness_fixed.c same harness mirroring fix.diff (bounded + NUL-term)
build.sh/run.sh exact repro scripts
build.log harness build output
harness_run.log buggy harness SIGSEGV + cross-entry report
harness_compare.log buggy-vs-fixed before/after
baseline_harness.log baseline (#0) harness confirmation
mount_attempt.log live mount attempts (silent corruption + churn)
panic.txt live kernel panic signature (kqueue_register fault)
fix.diff git-apply-able one-hunk fix
fix_apply.log patch application + patched region
fix_build.log single-fix ntfs.ko build output
fix_run.log patched-module live re-test (30 cycles, 0 panics)
env.txt guest uname / cc / sysctls / module sha256
manifest.json machine-readable catalog
VERDICT.md full narrative

Reproduce

On the repo root (guest up on with-src):

cd findings/poc/DF-0871
scp -F ../../../dfbsd-qemu/config craft_img.py craft_benign.py harness.c harness_fixed.c dfbsd:/root/df0871/
./../../../dfbsd-qemu/vm.sh run_root 'cd /root/df0871 && python3 craft_img.py ntfs_evil.img 2 && cc -O2 -o harness harness.c && (./harness 2; echo exit=$?)'
# deterministic: SIGSEGV at j=72 + cross-entry corruption reported
VERDICT.md verdict full narrative: mechanism, evidence, impact, fix validation
↓ download raw

DF-0871 — VERDICT

Verdict: REPRODUCED (heap OOB write / corruption, with a live panic on GENERIC). Impact: corruption → manifests as panic (kqueue_register victim deref). Confidence: certain. Fix: VALIDATED (fixed).


Root-cause confirmation

ntfs_mountfs() (sys/vfs/ntfs/ntfs_vfsops.c) parses the on-disk $AttrDef table at mount time. For each 160-byte struct attrdef it allocates a 72-byte struct ntvattrdef and copies the attribute name with an unbounded wchar→char do/while:

Type facts (confirmed in sys/vfs/ntfs/ntfs.h): - ntfs.h:37typedef u_int16_t wchar; - ntfs.h:203#define NTFS_ATTRNAME_MAXLEN 0x40 (= 64) - ntfs.h:206struct attrdef { wchar ad_name[0x40]; u32 ad_type; u32 r1[2]; u32 ad_flag; u64 min; u64 max; } = 160 B - ntfs.h:215struct ntvattrdef { char ad_name[0x40]; int ad_namelen; u32 ad_type; } = 72 B

The index j is bounded only by a NUL wchar in the source ad.ad_name (which is wchar[64]). If a crafted $AttrDef record has no NUL wchar anywhere in its 64-wide-char name and its trailing 32 B (ad_type / reserved1 / ad_flag / ad_minlen / ad_maxlen, aliasing ad.ad_name[64..79]) are all non-zero half-words, the walk runs j = 0..80+. The destination write ntmp->ntm_ad[i].ad_name[j] then:

  • j=0..63 → in-bounds name bytes
  • j=64..67ntvattrdef[i].ad_namelen (repaired by :461)
  • j=68..71ntvattrdef[i].ad_type (repaired by :462)
  • j=72..ntvattrdef[i+1].ad_name[0..] (cross-entry) or, for the last entry, past the whole kmalloc()HEAP OOB WRITE into M_NTFSMNT.

The post-loop assignments (:461-462) repair the within-object bytes of entry i but do not touch the bytes written at j>=72. Those stay corrupted, exactly as the finding claims.

Evidence

A. Deterministic harness (harness.c)

Faithful userspace transcription of :444,457-462 with a poisoned allocator (victim array at the end of a guard-paged page). The source is presented as an oversized wchar[256] buffer (all non-zero up to a final NUL) so the do/while is genuinely unbounded — defeating the compiler's ad_name[64] array-bounds UB assumption that would otherwise let a naïve transcription silently "pass".

  • buggy, num=1: writes dest[j] for j>=72SIGSEGV at the first byte past the 72-byte object. Deterministic proof the write leaves the object.
  • buggy, num=2: entry[0]'s walk overwrote 72 bytes of entry[1].ad_name (cross-entry corruption — the finding's claimed primitive), then the last entry's overflow crosses the 144-byte allocation → SIGSEGV.
  • fixed (harness_fixed.c, mirrors fix.diff): exit 0, 0 bytes past the allocation, name length capped at 63 + NUL-terminated.

(harness_run.log, harness_compare.log.)

B. Live kernel (#0 GENERIC, INVARIANTS ON)

Crafted NTFS image (craft_img.py): 1–2 evil 160-byte $AttrDef records whose entire 160 bytes are non-zero half-words + an all-zero terminator. vnconfig + mount_ntfs (root) runs the unbounded copy on every mount. During a churn loop the slab is groomed so the corrupted ntm_ad chunk is adjacent to a victim object; the 0xFF bytes the evil data writes (truncated low-bytes of the 0xFFFFFFFF trailing fields) corrupt a victim pointer to 0xffffffffffffffff, whose later dereference faults:

Fatal trap 12: page fault while in kernel mode
fault virtual address  = 0xffffffffffffffff
Stopped at kqueue_register+0x526:  movq (%r15),%rdx

(panic.txt.)

Controlled experiment (causation): - benign image (craft_benign.py, short NUL-terminated $AttrDef names → walk stops j<64, no overflow): 10 mount/unmount cycles, 0 panics, guest UP. - evil image: kqueue_register panic during churn (above).

The live panic is timing/layout-dependent (heap-grooming); the harness is the deterministic proof and fires on every run. A single clean mount of the evil image usually produces silent within-allocation/cross-entry corruption (no immediate INVARIANTS trip because the bytes land in slab padding within the same chunk); repeated mounts groom the slab into a configuration where the corrupted chunk borders a dereferenced victim and the kernel faults.

Impact / threat model & escalation ceiling

  • Primitive: attacker-content-controlled heap OOB write into the M_NTFSMNT slab zone (kmalloc-128 for num=1, kmalloc-256 for num=2). The written bytes are the low bytes of attacker-chosen on-disk wchars, so the content is shapable.
  • Trigger: mount -t ntfs of a crafted image. Root-only in the default config (vfs.usermount=0; mount_ntfs/vnconfig not setuid — verified in env.txt).
  • Realistic threat: a malicious NTFS image (USB / download / multi-user system / automount) causes kernel heap corruption and/or panic when mounted by root — the classic untrusted-filesystem-image attack surface. High severity (kernel memory corruption from untrusted on-disk data).
  • uid=0 escalation — BLOCKED by a valid hard blocker: the write is reachable only from an already-root credential (the mount syscall requires root). Per the audit's bright-line rule, root→kernel is game-over by definition; there is no unprivileged→root privilege boundary to cross end-to-end. The primitive could be slab-groomed to escalate if an unprivileged user could mount the image (e.g. an admin sets vfs.usermount=1 and provides an attacker-owned, device-accessible image), but that is a conditional scenario, not the default config. Honest impact is therefore heap corruption / DoS (panic) from a malicious filesystem image.

PoC changes (vs. the seeded scaffolding)

This pack was built from scratch (no seeded sources). Reuses the proven DF-0785 NTFS image builder. Key additions: - craft_img.py — DF-0871 variant: evil $AttrDef (all-nonzero 160-byte records) + well-formed root dir ir_size==datalen so only the mount-time $AttrDef bug is exercised (DF-0785's lookup bug isolated). - craft_benign.py — negative-control crafter (short NUL-terminated names). - harness.c / harness_fixed.c — deterministic transcriptions. v2 of the buggy harness uses an oversized source buffer to defeat the compiler's array-bounds UB assumption (v1 silently passed at -O2).

Fix (fix.diff) — VALIDATED

Minimal, targeted fix at the root cause — bound the do/while to the destination array length and force NUL-termination:

-           } while(ad.ad_name[j++]);
+           } while(ad.ad_name[j++] &&
+               j < (int)sizeof(ntmp->ntm_ad[i].ad_name));
+           /* DF-0871: force NUL-termination within the fixed-size buffer
+            * so the wchar->char walk can never overrun ntvattrdef.ad_name. */
+           ntmp->ntm_ad[i].ad_name[sizeof(ntmp->ntm_ad[i].ad_name) - 1] = '\0';

git apply --check: OK. Supersedes/matches the finding markdown's proposal.

Phase 8 — single-fix ntfs.ko (hot-swap)

  • git apply of fix.diff to in-guest /usr/src/sys/vfs/ntfs/ntfs_vfsops.c; rebuilt ntfs.ko (cd /usr/src/sys/vfs/ntfs && make, cc 8.3, rc=0 — fix_build.log); hot-swapped (kldunload → install → kldload, sha256 dbc96b6262bd579da8a7b050f490047048e6b5ed4f5fd338324d3848788e021d).
kernel / module harness live evil-image mount
unpatched #0 GENERIC SIGSEGV at j=72 panic kqueue_register+0x526 (fault 0xffffffffffffffff)
fixed ntfs.ko clean exit 0, 0 B over 30 cycles, 0 panics, guest UP

Benign image still mounts and parses attributes on the fixed module (functional regression check OK). → fix_status: fixed.

Fix verification

fixed
baseline reproduced→ patch + rebuild →patched clean

VALIDATED the fix. git apply --check passes; fix.diff applied to in-guest /usr/src/sys/vfs/ntfs/ntfs_vfsops.c, ntfs.ko rebuilt (rc=0) and hot-swapped. BEFORE (unpatched #0 ntfs.ko): deterministic harness SIGSEGVs at j=72 + cross-entry corruption; live crafted-image mount churn -> kernel panic kqueue_register+0x526 (fault 0xffffffffffffffff). AFTER (patched ntfs.ko): harness_fixed exit 0 with 0 bytes past the allocation; 30 evil-image mount cycles (15x num=1 + 15x num=2) -> 0 panics, guest UP; benign image still mounts and parses attributes (functional regression OK). The fix closes the bug.

BEFORE baseline: harness '[harness] SIGSEGV caught -> do/while wrote PAST the 72-byte allocation.'; live 'Fatal trap 12 ... fault virtual address = 0xffffffffffffffff ... Stopped at kqueue_register+0x526'. AFTER patched module: harness_fixed 'bytes written past the 72-byte allocation = 0 (expect 0)'; live 'PATCHED_CHURN_DONE' after 30 cycles with 0 panics and guest UP (grep -c panic = 0).
↓ fix.diff6.5-DEVELOPMENT #0 (kernel unchanged) + single-fix ntfs.ko hot-swapped (built from patched /usr/src/sys/vfs/ntfs, cc 8.3, rc=0; module sha256 dbc96b6262bd579da8a7b050f490047048e6b5ed4f5fd338324d3848788e021d). Hot-swap (kldunload/kldload) used because ntfs is a loadable KLD module and the bug is entirely within it.

Confirmed kernel references

Detail

Exploit chain

Primitive: attacker-content-controlled heap OOB write into M_NTFSMNT slab zone (kmalloc-128 for num=1, kmalloc-256 for num=2); written bytes are the low bytes of attacker-chosen on-disk wchars, so content is shapable. Grooming: repeated vnconfig/mount of the crafted image churns the slab so the corrupted ntm_ad chunk borders a dereferenced victim (observed: a kqueue-related object). Conversion: the corrupted 0xFF-bytes turned a victim pointer into 0xffffffffffffffff, dereferenced in kqueue_register -> fatal page fault (panic). OUTCOME: blocked by a VALID hard blocker -- the trigger is mount -t ntfs, which is ROOT-ONLY in the default config (vfs.usermount=0; mount_ntfs and vnconfig are not setuid -- verified in env.txt). Root->kernel is game-over by definition; there is no unprivileged->root privilege boundary to cross end-to-end, so an honest uid0 verdict is not supportable on the default GENERIC. The primitive COULD be slab-groomed to escalate IF an unprivileged user could mount the image (e.g. an admin sets vfs.usermount=1 + provides an attacker-owned, device-accessible image), but that is a conditional scenario, not the default. Demonstrated impact is heap corruption / DoS (kernel panic) from a malicious filesystem image. Harness/chain file written: harness.c + harness_fixed.c (no separate escalation module, since the root-only mount is the blocker).

Evidence (decisive lines)

DETERMINISTIC (harness, every run): '[harness] SIGSEGV caught -> do/while wrote PAST the 72-byte allocation.' and '[harness] cross-entry demo: entry[0] do/while overwrote 72 byte(s) of entry[1].ad_name'. FIXED harness: 'bytes written past the 72-byte allocation = 0'. LIVE (#0 GENERIC): 'Fatal trap 12: page fault while in kernel mode / fault virtual address = 0xffffffffffffffff / Stopped at kqueue_register+0x526: movq (%r15),%rdx' (panic.txt). CONTROL: benign image = 10 mount cycles, 0 panics, guest UP.

PoC changes

Built the evidence pack from scratch (no seeded sources), reusing the proven DF-0785 NTFS image builder. craft_img.py: DF-0871 variant with evil $AttrDef (all-nonzero 160-byte records) and a WELL-FORMED root dir (ir_size==datalen) to isolate the mount-time bug from DF-0785's lookup bug. craft_benign.py: negative-control crafter (short NUL-terminated names). harness.c/harness_fixed.c: deterministic transcriptions of :444,457-462 with a poisoned allocator; v2 of the buggy harness uses an oversized source buffer to defeat the compiler's ad_name[64] array-bounds UB assumption (v1 silently passed at -O2).

Verified recommended fix

In sys/vfs/ntfs/ntfs_vfsops.c:460, bound the do/while to the destination array length and force NUL-termination: change } while(ad.ad_name[j++]); to } while(ad.ad_name[j++] && j < (int)sizeof(ntmp->ntm_ad[i].ad_name)); and add ntmp->ntm_ad[i].ad_name[sizeof(ntmp->ntm_ad[i].ad_name) - 1] = '\0'; so the wchar->char walk can never overrun the 64-byte ntvattrdef.ad_name. Minimal, targeted at the root cause. Supersedes/matches the finding markdown's proposal. Full git-apply-able diff in findings/poc/DF-0871/fix.diff.

Verdict

REPRODUCED. The bug is real and confirmed line-by-line: ntfs_mountfs() at sys/vfs/ntfs/ntfs_vfsops.c:458-460 copies the $AttrDef attribute name with an unbounded do/while (while(ad.ad_name[j++])) whose destination ntmp->ntm_ad[i].ad_name is only char[0x40]=64 (ntfs.h:215) while the source ad.ad_name is wchar[64]=128B inside a 160-byte struct attrdef (ntfs.h:206, wchar=u_int16_t per ntfs.h:37). A crafted $AttrDef record with no NUL wchar anywhere in its 160 bytes makes j run 0..80+, so writes at j>=72 leave the 72-byte ntvattrdef object (cross-entry into ntvattrdef[i+1].ad_name, or past the whole kmalloc for the last entry) -- a heap OOB write into the M_NTFSMNT slab. The post-loop assignments (:461-462) repair only the within-object bytes (j=64..71), not the j>=72 corruption. Deterministic harness SIGSEGVs at j=72 (poisoned allocator) and shows 72 bytes of cross-entry corruption; live on #0 GENERIC a crafted-image mount churn grooms the slab so the 0xFF bytes (truncated low-bytes of the 0xFFFFFFFF trailing fields) corrupt an adjacent victim pointer to 0xffffffffffffffff, whose dereference panics in kqueue_register+0x526 (panic.txt). Controlled: a benign image (short NUL-terminated names, walk stops j<64) survives 10 mount cycles with 0 panics; the evil image panics.