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

Unbounded attribute walk in ntfs_loadntnode: OOB read + infinite loop via fr_attroff/reclen==0

Summary

ntfs_subr.c:305 off=mfrp->fr_attroff (uint16 from disk). :310 while(ap->a_hdr.a_type != -1) { ntfs_attrtontvattr(ap) off+=ap->a_hdr.reclen (uint32 from disk) }. NO bounds check off against record size (ntfs_bntob(ntm_bpmftrec) typically 1024). NO check reclen!=0. fr_attroff>recsize: first ap deref OOB heap read. reclen==0: off never advances infinite kernel loop hang. reclen>remaining: continues OOB until page fault or random 0xFFFFFFFF type word. OOB bytes passed to ntfs_attrtontvattr copied to va_datap exposed to userspace. Trigger: crafted NTFS image mount. Fix: validate off<recsize and reclen>=sizeof(attrhdr) and off+reclen<=recsize and off+reclen>off.

Discussion (0)

No comments yet.

PoC verification

Evidence pack

findings/poc/DF-0787 Β· 23 files
FileTypeDescriptionSize
gen_ntfs_0787.py trigger-source crafts 3 malformed NTFS images (loop/oob_a/oob_r) by corrupting only the ino-5 MFT record of an otherwise-mountable image 7.2 KB view raw
harness.c trigger-source userspace replication of ntfs_loadntode:305-320 with guard page; modes clean/loop/oob_attroff/oob_reclen + apply_fix 7.7 KB view raw
ntfs_loop.img crafted-image MFT ino5 with attribute reclen=0 -> infinite loop / slab panic 256.0 KB ↓ download
ntfs_oob_a.img crafted-image MFT ino5 with fr_attroff=0x0FF0 -> walk into zero-tail -> same loop 256.0 KB ↓ download
ntfs_oob_r.img crafted-image MFT ino5 with reclen=0x1000 -> OOB read past mfrp allocation 256.0 KB ↓ download
build.sh build-script cc the harness; regenerates images if python3 present 456 B view raw
run.sh run-script runs the harness; 'run.sh live' (root) reproduces the live kernel panic 1.6 KB view raw
fix.diff suggested-fix git-apply-able: bounds-check the walk by recsz and reclen; rejects malformed entries with EINVAL 2.0 KB view raw
build.log build-log harness cc output (guest cc 8.3) 65 B view raw
run.log run-log live panic signature + harness output (buggy vs fix) 2.3 KB view raw
harness_run.log run-log isolated harness output across all 4 modes x {buggy,fix} 737 B view raw
panic.txt panic-signature panic stack from dfbsd-qemu/boot.log 2.0 KB view raw
env.txt environment kern.version, cc --version, vfs.usermount 99 B view raw
fix_build.log build-log patched ntfs.ko standalone build output 1.4 KB view raw
fix_run.log run-log post-fix mount results: EINVAL x3, dmesg 'failed to load attr ino: 5', guest UP 778 B view raw
logs/boot.loop.baseline.log boot-log full serial log of loop-variant panic (baseline) 13.5 KB view raw
logs/boot.oob_a.baseline.log boot-log full serial log of oob_a-variant panic (baseline) 13.5 KB view raw
logs/boot.oob_r.baseline.log boot-log full serial log of oob_r-variant panic (baseline) 13.5 KB view raw
logs/boot.patched_STILL_PANICS.log boot-log (negative evidence) full serial log when only the main kernel was rebuilt without rebuilding ntfs.ko β€” same panic, proving ntfs lives in the module 13.5 KB view raw
logs/boot.baseline.final.log boot-log final clean baseline panic capture (loop variant) before fix validation 13.5 KB view raw
logs/build_run.user.log run-log early combined build+run capture 362 B view raw
VERDICT.md verdict full narrative: bug, reachability, repro, fix validation 11.5 KB ↓ raw
README.md readme human-facing reproduce instructions 2.5 KB ↓ raw
README.md readme human-facing reproduce instructions
↓ download raw

DF-0787 β€” Reproduce

Bug

Unbounded attribute walk in ntfs_loadntnode (sys/vfs/ntfs/ntfs_subr.c:305-320). The loop reads fr_attroff (uint16) and each attribute's reclen (uint32) from the on-disk MFT record with no bound check against the in-memory record size, so a crafted NTFS image causes (a) an infinite loop / slab-allocation panic if reclen==0 or fr_attroff points into a zero-tail, or (b) an OOB heap read if reclen overshoots the record. Triggered at mount time (before the DF-0786 lockmgr bug).

Files

file purpose
gen_ntfs_0787.py builds three crafted NTFS images (loop / oob_a / oob_r) by corrupting only the ino-5 (root dir) MFT record of an otherwise-valid image
ntfs_loop.img reclen=0 β†’ infinite loop β†’ slab panic
ntfs_oob_a.img fr_attroff=0x0FF0 β†’ walk reads zero-tail β†’ same loop
ntfs_oob_r.img reclen=0x1000 β†’ next iter OOB read past mfrp
harness.c userspace replication of ntfs_loadntnode:305-320 with a guard page; accepts clean|loop|oob_attroff|oob_reclen and apply_fix
build.sh compiles the harness + (if python3 available) regenerates the images
run.sh runs the harness; run.sh live (root) also runs the live mount
fix.diff git-apply-able fix at the root cause
VERDICT.md full narrative + evidence
manifest.json artifact catalog

Build

./build.sh            # builds ./harness (+ images if python3 present)

Run

./run.sh              # userspace harness, deterministic (no root needed)
./run.sh live         # + live mount test (REQUIRES ROOT, panics the guest)

Expected

  • Harness, buggy mode: loop/oob_attroff β†’ "ITERATION CAP HIT" (infinite loop); oob_reclen β†’ SIGSEGV (OOB read).
  • Harness, fix mode: all three corrupt modes β†’ "FIX REJECTED input" (EINVAL); clean unchanged.
  • Live (unpatched kernel): mount_ntfs panics with NTFS vattr: malloc limit exceeded, stack through ntfs_loadntnode+0x178. Guest dies at DDB.
  • Live (patched ntfs.ko): mount_ntfs returns Invalid argument (RC=71), dmesg shows ntfs_loadntnode: failed to load attr ino: 5, guest stays UP. Clean images still mount.

Preconditions

  • mount_ntfs is root-only (vfs.usermount=0 on stock DragonFly). Threat model: admin mounts attacker-supplied NTFS image / USB stick.
  • Reachable at mount time; does NOT require the post-mount lookup path (so the DF-0786 lockmgr panic does not block this finding).
VERDICT.md verdict full narrative: bug, reachability, repro, fix validation
↓ download raw

DF-0787 β€” Unbounded attribute walk in ntfs_loadntnode: OOB read + infinite loop

Verdict: REPRODUCED (live kernel panic on default GENERIC #0 + userspace guard-page harness) β†’ FIX VALIDATED

The bug (confirmed by source trace + live panic)

File: sys/vfs/ntfs/ntfs_subr.c:305-320 (function ntfs_loadntnode) Class: CWE-835 (infinite loop) + CWE-125 (OOB read) β€” both stem from the same missing bound check. Severity (per finding): Medium. Confirmed: realistic impact ceiling is kernel panic / DoS (no write primitive).

263:    mfrp = kmalloc(ntfs_bntob(ntmp->ntm_bpmftrec), M_TEMP, M_WAITOK);   // 4096-byte alloc
...
305:    off = mfrp->fr_attroff;                                              // uint16 from disk β€” UNCHECKED
306:    ap  = (struct attr *) ((caddr_t)mfrp + off);                         // ptr formed before any bound
...
310:    while (ap->a_hdr.a_type != -1) {                                     // deref of possibly-OOB ap
311:        error = ntfs_attrtontvattr(ntmp, &nvap, ap);                     // allocates a fresh ntvattr each iter
...
318:        off += ap->a_hdr.reclen;                                         // uint32 from disk β€” UNCHECKED
319:        ap  = (struct attr *) ((caddr_t)mfrp + off);                     // ptr may now be OOB
320:    }

The walk reads fr_attroff (uint16) and each attribute's reclen (uint32) directly from the on-disk MFT record with no bound check against the in-memory record size (ntfs_bntob(ntm_bpmftrec)). Three crafted-record shapes all exploit the same gap:

Variant Crafted bytes Effect in kernel
loop first attr's reclen = 0 off never advances; while-loop spins forever, each iteration leaking a struct ntvattr
oob_a fr_attroff = 0x0FF0 (past attr list, inside 4 KiB alloc) first ap->a_hdr.a_type deref reads bytes that are not on-disk attributes; in our image the tail is zero, so reclen=0 and it falls into the same loop
oob_r first attr's reclen = 0x1000 after iteration 1, off = attroff + 4096, deref past mfrp allocation β†’ reads adjacent kernel heap

Reachability (mount-time, BEFORE the DF-0786 lockmgr panic)

sibling finding DF-0786 documented that the live directory-LOOKUP path on a mounted NTFS volume panics with lockmgr: locking against itself inside ntfs_ntget(). That does not block this finding. The two bugs fire on different paths:

  • DF-0786 fires during ntfs_lookup β†’ ntfs_ntlookupfile β†’ ntfs_ntget on an already-locked ntnode (i.e. after mount, during path resolution).
  • DF-0787 fires during the mount-time load of the system MFT records:
sys/vfs/ntfs/ntfs_vfsops.c:393-403   ntfs_mountfs() loops over {NTFS_MFTINO, NTFS_ROOTINO, NTFS_BITMAPINO}
sys/vfs/ntfs/ntfs_vfsops.c:396          VFS_VGET(mp, NULL, pi[i], ...)
sys/vfs/ntfs/ntfs_vfsops.c:807          .vfs_vget = ntfs_vget
sys/vfs/ntfs/ntfs_vfsops.c:794-796      ntfs_vget -> ntfs_vgetex(... VG_DONTLOADIN not set ...)
sys/vfs/ntfs/ntfs_vfsops.c:717-718      if (!(ip->i_flag & IN_LOADED)) ntfs_loadntnode(ntmp, ip)
sys/vfs/ntfs/ntfs_subr.c:253            ntfs_loadntnode() runs the buggy walk

For freshly-created system MFT ntnodes the lockinit() at ntfs_subr.c:391 makes the lock brand-new, so ntfs_ntget()'s LOCKMGR(LK_EXCLUSIVE) succeeds on the first try. The lockmgr panic never fires here. Mounting the crafted image is sufficient to trigger DF-0787.

The threat model is the standard one for filesystem-image bugs: vfs.usermount=0 (verified on this guest) means the attacker needs root to issue the mount_ntfs (e.g. an admin auto-mounting an untrusted USB stick / image, or a crafted image placed where root will mount it). The PoC trigger is one command: mount_ntfs -o ro <crafted.img> /mnt.

Reproduction β€” live kernel (default GENERIC #0)

Trigger (as root):

vnconfig -c vn0 /root/ntfs_loop.img
mount_ntfs -o ro /dev/vn0 /mnt/ntfs

Result: kernel panic. mount(2) never returns; the guest drops to the DDB prompt. Identical panic for ntfs_loop.img, ntfs_oob_a.img, ntfs_oob_r.img. Serial-log signature:

panic: NTFS vattr: malloc limit exceeded
cpuid = 5
Trace beginning at frame 0xfffff80117fe2e30
_kmalloc() at _kmalloc+0xb09 0xffffffff806578c9
_kmalloc() at _kmalloc+0xb09 0xffffffff806578c9
ntfs_attrtontvattr() at ntfs_attrtontvattr+0x35 0xffffffff82602685
ntfs_loadntnode() at ntfs_loadntnode+0x178 0xffffffff82604278
ntfs_vgetex() at ntfs_vgetex+0x1ec 0xffffffff826010ac
ntfs_vget() at ntfs_vget+0x29 0xffffffff82601219
Debugger("panic")
Stopped at Debugger+0x7c: movb $0,0xbdaf09(%rip)
db>

Why the panic message says "malloc limit exceeded" and not "infinite loop": because the unbounded walk calls ntfs_attrtontvattr() on each iteration, which kmalloc()s a fresh struct ntvattr (and for resident attrs, a separate va_datap buffer) and LIST_INSERT_HEADs it into ip->i_valist (ntfs_subr.c:311-316). The walk never frees them. Eventually the per-malloc-type slab ceiling (kern_slaballoc.c:863-878) trips and panics. The root cause is the unbounded walk β€” the slab panic is the symptom.

Reproduction β€” userspace guard-page harness (deterministic complement)

harness.c replicates the exact walk against a 4096-byte record placed at the end of a writable page with a PROT_NONE guard page after it. Output (run as unprivileged maxx):

--- BUGGY walk (kernel behaviour on default GENERIC #0) ---
mode=clean        apply_fix=0    -> rc=0  clean exit (end-of-attributes reached)
mode=loop         apply_fix=0    -> rc=1  ITERATION CAP HIT -> would be infinite loop in kernel
mode=oob_attroff  apply_fix=0    -> rc=1  ITERATION CAP HIT -> would be infinite loop in kernel
mode=oob_reclen   apply_fix=0    -> rc=2  SIGSEGV -> OOB read past record (into adjacent slab in kernel)
  • loop and oob_attroff confirm the infinite-loop branch (reclen==0 / walk into zero-tail).
  • oob_reclen confirms the OOB-read branch (next iter deref past the allocation, caught by the guard page as SIGSEGV β€” in the kernel this reads adjacent slab heap until a page boundary).

Escalation assessment (no chain possible β€” read/loop only)

This is a read-only / control-flow primitive. There is no write to attacker-chosen kernel memory: the only writes are kernel-internal struct ntvattr allocations the attacker cannot shape. Per Phase 6 of the procedure, a pure read/loop primitive has no escalation chain to uid=0; the correct deliverable is the characterized impact ceiling, which is:

  • Definite kernel panic / DoS (the demonstrated outcome on default GENERIC, INVARIANTS ON)
  • Theoretical info-leak via the oob_reclen path: bytes read past mfrp flow through ntfs_attrtontvattr β†’ vap->va_datap (ntfs_subr.c:561-564) and could be exposed to userspace via a subsequent read() on the mounted file. On default GENERIC this is masked because the walk always reaches the slab-allocation panic first.

No uid=0 is achievable; this is honestly reported as impact=panic.

Fix β€” fix.diff

Binds the walk by the in-memory MFT record size and rejects malformed attributes with EINVAL before dereferencing them:

  • Validate fr_attroff is in [sizeof(struct filerec), recsz) before forming the first ap ptr.
  • Inside the loop: require off + sizeof(struct attrhdr) <= recsz (the header must fit).
  • Require ap->a_hdr.reclen ∈ [sizeof(struct attrhdr), recsz - off] (rejects 0, tiny, and overflow).
  • On any violation: set error = EINVAL, break; the existing if (error) goto out path then frees mfrp and returns cleanly. Mount fails with EINVAL β€” no panic, no leak.

The fix is minimal and targeted at the root cause (the missing bound). It does not change the on-disk format, the structure of ntfs_attrtontvattr, or the happy path for valid images.

This supersedes the finding markdown's sketch ("Fix: validate off=sizeof(attrhdr) and off+reclen<=recsize and off+reclen>off") β€” same intent, expressed as a single hunk with proper error handling and a kprintf for diagnostics.

Fix validation (Phase 8) β€” VALIDATED

Built ntfs.ko standalone (KERNCONF=X86_64_GENERIC make in sys/vfs/ntfs/) using the warm obj at /usr/obj/usr/src/sys/X86_64_GENERIC. The full make nativekernel was also attempted first and observed to produce the same patched ntfs.ko; the standalone module build is faster and is the recommended path since ntfs is optional ntfs (a module, not compiled into GENERIC).

Before (unpatched ntfs.ko, kernel #0)

mount_ntfs -o ro /dev/vn0 /mnt/ntfs   # ntfs_loop.img
-> panic: NTFS vattr: malloc limit exceeded      [guest dead, db> prompt]

(Same panic for ntfs_oob_a.img and ntfs_oob_r.img.)

After (patched ntfs.ko, kernel #0 β€” only the module was rebuilt)

ntfs_loop.img  -> mount_ntfs: /dev/vn0: Invalid argument   MOUNT_RC=71
ntfs_oob_a.img -> mount_ntfs: /dev/vn0: Invalid argument   MOUNT_RC=71
ntfs_oob_r.img -> mount_ntfs: /dev/vn0: Invalid argument   MOUNT_RC=71
dmesg: ntfs_loadntnode: failed to load attr ino: 5
       ntfs_vget: CAN'T LOAD ATTRIBUTES FOR INO: 5
guest: UP, no panic, ntfs.ko auto-loaded then auto-unloaded cleanly

Regression check (clean NTFS image still mounts)

mount_ntfs -o ro /dev/vn0 /mnt/ntfs   # clean.img
-> MOUNT_RC=0   (mount succeeds; subsequent ls hangs on the pre-existing
                 DF-0786 lockmgr bug, which is unrelated to this fix)

The fix closes the bug on the default GENERIC kernel without regressing valid images.

PoC changes

  • gen_ntfs_0787.py β€” crafted-image generator that builds on the proven DF-0786 NTFS scaffolding and corrupts only the ino-5 (root dir) MFT record. Three modes: loop (reclen=0), oob_a (fr_attroff past attr list), oob_r (reclen=4096). All other MFT records and the boot sector are byte-identical to the proven-mountable DF-0786 image, so ntfs_procfixups still passes and the buggy walk is actually reached.
  • harness.c β€” userspace replication of ntfs_loadntnode:305-320 against a guard-page-backed 4096-byte record. Accepts a clean|loop|oob_attroff|oob_reclen mode and an optional apply_fix flag that runs the proposed fixed walker for direct before/after comparison.
  • build.sh / run.sh β€” exact reproducible build & run. run.sh live (as root) reproduces the kernel panic; run.sh (as maxx) runs the deterministic harness.
  • fix.diff β€” standalone, git apply-able unified diff fixing the bug at the root cause.

How to reproduce (for a teammate)

# 1. userspace harness (deterministic, no root)
ssh dfbsd-maxx 'mkdir -p poc/DF-0787'
scp -F dfbsd-qemu/config -q findings/poc/DF-0787/{harness.c,build.sh,run.sh} dfbsd-maxx:poc/DF-0787/
ssh -F dfbsd-qemu/config dfbsd-maxx 'cd poc/DF-0787 && sh build.sh && sh run.sh'

# 2. live kernel reproduction (root, EXPECTS PANIC β€” guest dies, must reset after)
scp -F dfbsd-qemu/config -q findings/poc/DF-0787/gen_ntfs_0787.py <host>
python3 gen_ntfs_0787.py loop /tmp/ntfs_loop.img
scp -F dfbsd-qemu/config -q /tmp/ntfs_loop.img dfbsd:/root/
./dfbsd-qemu/vm.sh run_root 'vnconfig -c vn0 /root/ntfs_loop.img && mount_ntfs -o ro /dev/vn0 /mnt/ntfs'
./dfbsd-qemu/vm.sh log    # see panic: NTFS vattr: malloc limit exceeded
./dfbsd-qemu/vm.sh reset with-src

# 3. fix validation (rebuild ntfs.ko, install, re-run same PoC)
scp -F dfbsd-qemu/config -q findings/poc/DF-0787/fix.diff dfbsd:/root/
./dfbsd-qemu/vm.sh run_root 'cd /usr/src && patch -p1 < /root/fix.diff && cd sys/vfs/ntfs && make && cp ntfs.ko /boot/kernel/ntfs.ko'
# (re-run step 2; mount now fails cleanly with EINVAL, guest stays UP)

Fix verification

fixed
baseline reproduced→ patch + rebuild →patched clean

VALIDATED. Baseline (unpatched /boot/kernel/ntfs.ko on kernel #0): mount_ntfs on ntfs_loop.img / ntfs_oob_a.img / ntfs_oob_r.img PANICS with 'NTFS vattr: malloc limit exceeded' through ntfs_loadntnode+0x178, guest dies at DDB. Patched (rebuilt ntfs.ko containing the new bound checks, installed to /boot/kernel/ntfs.ko, kernel #0 unchanged): same three mounts now return mount_ntfs: /dev/vn0: Invalid argument (MOUNT_RC=71) with dmesg 'ntfs_loadntnode: failed to load attr ino: 5' / 'ntfs_vget: CAN'T LOAD ATTRIBUTES FOR INO: 5', guest stays UP, ntfs.ko auto-loads and unloads cleanly. Regression check: a CLEAN ntfs image still mounts successfully (MOUNT_RC=0). The fix closes the bug on the default GENERIC kernel without regressing valid images.

BEFORE (unpatched ntfs.ko, kernel #0):
  mount_ntfs -o ro /dev/vn0 /mnt/ntfs   [ntfs_loop.img]
  -> panic: NTFS vattr: malloc limit exceeded
     ntfs_loadntnode+0x178 / ntfs_attrtontvattr+0x35 / _kmalloc+0xb09
     Stopped at Debugger+0x7c   [guest dead at db>]
  (identical panic for ntfs_oob_a.img and ntfs_oob_r.img)

AFTER (patched ntfs.ko, kernel #0 unchanged):
  ntfs_loop.img  -> mount_ntfs: /dev/vn0: Invalid argument   MOUNT_RC=71
  ntfs_oob_a.img -> mount_ntfs: /dev/vn0: Invalid argument   MOUNT_RC=71
  ntfs_oob_r.img -> mount_ntfs: /dev/vn0: Invalid argument   MOUNT_RC=71
  dmesg: ntfs_loadntnode: failed to load attr ino: 5
         ntfs_vget: CAN'T LOAD ATTRIBUTES FOR INO: 5
  guest: UP (kldstat shows ntfs.ko resident after mount), no panic
  clean.img: MOUNT_RC=0 (no regression)
↓ fix.diffDragonFly 6.5-DEVELOPMENT #0: Thu Jul 2 06:02:54 UTC 2026 (kernel unchanged; ntfs.ko rebuilt from patched source at /usr/src/sys/vfs/ntfs and installed to /boot/kernel/ntfs.ko -- the ntfs code lives in the module, not in the main kernel)

Confirmed kernel references

Detail

Exploit chain

Not applicable (read-only / control-flow primitive, no write). The bug gives an unbounded loop and/or OOB read of the mfrp allocation (4096 bytes M_TEMP); the only writes are kernel-internal struct ntvattr allocations the attacker cannot shape. The realistic impact ceiling is the demonstrated kernel panic / DoS on default GENERIC. The oob_reclen variant additionally performs an OOB heap read past mfrp into adjacent slab (proven by the userspace guard-page harness as SIGSEGV at the guard page), which on a non-INVARIANTS kernel could leak adjacent kernel memory back to userspace via va_datap, but on default GENERIC the slab-panic pre-empts the leak. No escalation chain to uid=0 is possible. Honest impact = panic/DoS.

Evidence (decisive lines)

Baseline (unpatched ntfs.ko, kernel #0), mount_ntfs -o ro /dev/vn0 /mnt/ntfs on crafted ntfs_loop.img: panic: NTFS vattr: malloc limit exceeded / _kmalloc+0xb09 / ntfs_attrtontvattr+0x35 / ntfs_loadntnode+0x178 / ntfs_vgetex+0x1ec / ntfs_vget+0x29 / Stopped at Debugger+0x7c / db> (guest dead). Identical panic for ntfs_oob_a.img and ntfs_oob_r.img. Userspace guard-page harness (run as maxx): clean->rc=0, loop->rc=1 ITERATION CAP HIT (infinite loop), oob_attroff->rc=1 ITERATION CAP HIT, oob_reclen->rc=2 SIGSEGV (OOB read).

PoC changes

Wrote findings/poc/DF-0787/ from scratch (no prior PoC folder existed). gen_ntfs_0787.py builds on the proven DF-0786 NTFS scaffolding and corrupts ONLY the ino-5 (root dir) MFT record so ntfs_procfixups still passes and the buggy walk is reached; emits 3 variants (loop/oob_a/oob_r). harness.c is a userspace replication of ntfs_loadntnode:305-320 with a guard-page-backed 4096-byte record, accepting clean|loop|oob_attroff|oob_reclen and an apply_fix flag that runs the proposed fixed walker for direct before/after comparison. build.sh/run.sh wrap them; run.sh live reproduces the kernel panic as root. fix.diff is a standalone git-apply-able unified diff fixing the bug at the root cause (bound the walk by recsz; reject malformed reclen/off with EINVAL).

Verified recommended fix

In ntfs_loadntnode (sys/vfs/ntfs/ntfs_subr.c around line 305-320), bound the attribute walk by the in-memory record size: (1) reject fr_attroff outside [sizeof(struct filerec), recsz) before forming the first ap pointer; (2) inside the loop require off + sizeof(struct attrhdr) <= recsz (header must fit); (3) require ap->a_hdr.reclen in [sizeof(struct attrhdr), recsz - off] (rejects 0, tiny, and overflow). On any violation set error=EINVAL and break; the existing 'if (error) goto out' path frees mfrp and returns cleanly so mount fails with EINVAL instead of looping/OOB-reading/panicking. Full git-apply-able diff in findings/poc/DF-0787/fix.diff. Supersedes the finding markdown's sketch (same intent, expressed as a single hunk with proper error handling and a diagnostic kprintf).

Verdict

REPRODUCED on default GENERIC kernel #0 (INVARIANTS ON). The cited unbounded attribute walk in ntfs_loadntnode (sys/vfs/ntfs/ntfs_subr.c:305-320) is real and reachable at MOUNT time, BEFORE the DF-0786 lockmgr panic (which fires only on directory LOOKUP after mount). Mounting a crafted NTFS image whose ino-5 (root dir) MFT record has a malformed attribute (reclen=0, fr_attroff past the attr list, or reclen past record end) drives the while() at line 310 into an unbounded loop. Each iteration calls ntfs_attrtontvattr() which kmalloc's a fresh struct ntvattr and LIST_INSERT_HEAD's it into ip->i_valist without ever freeing; the per-malloc-type slab ceiling eventually trips and the kernel panics with 'NTFS vattr: malloc limit exceeded' (kern_slaballoc.c:877). Stack: ntfs_vget -> ntfs_vgetex -> ntfs_loadntnode+0x178 -> ntfs_attrtontvattr+0x35 -> _kmalloc. All three crafted-image variants (loop/oob_a/oob_r) panic identically. Reachability was traced line-by-line: ntfs_mountfs:393-403 VFS_VGETs the system MFT records, each -> ntfs_vgetex:717-718 -> ntfs_loadntnode. Lockmgr bug does NOT block this finding (freshly lockinit'd ntnodes for system records). Threat model: vfs.usermount=0, so root mounts attacker image (admin auto-mount of hostile USB/image).