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)
PoC verification
Evidence pack
findings/poc/DF-0787 Β· 23 files| File | Type | Description | Size | |
|---|---|---|---|---|
| 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 |
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_ntfspanics withNTFS vattr: malloc limit exceeded, stack throughntfs_loadntnode+0x178. Guest dies at DDB. - Live (patched ntfs.ko):
mount_ntfsreturnsInvalid argument(RC=71), dmesg showsntfs_loadntnode: failed to load attr ino: 5, guest stays UP. Clean images still mount.
Preconditions
mount_ntfsis root-only (vfs.usermount=0on 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).
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_ntgeton 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)
loopandoob_attroffconfirm the infinite-loop branch (reclen==0 / walk into zero-tail).oob_reclenconfirms 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_reclenpath: bytes read pastmfrpflow throughntfs_attrtontvattrβvap->va_datap(ntfs_subr.c:561-564) and could be exposed to userspace via a subsequentread()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_attroffis in[sizeof(struct filerec), recsz)before forming the firstapptr. - 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 existingif (error) goto outpath then freesmfrpand returns cleanly. Mount fails withEINVALβ 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 offkprintf 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, sontfs_procfixupsstill passes and the buggy walk is actually reached.harness.cβ userspace replication ofntfs_loadntnode:305-320against a guard-page-backed 4096-byte record. Accepts aclean|loop|oob_attroff|oob_reclenmode and an optionalapply_fixflag 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
fixedVALIDATED. 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)
Confirmed kernel references
- sys/vfs/ntfs/ntfs_subr.c:263
- sys/vfs/ntfs/ntfs_subr.c:305
- sys/vfs/ntfs/ntfs_subr.c:306
- sys/vfs/ntfs/ntfs_subr.c:310
- sys/vfs/ntfs/ntfs_subr.c:311
- sys/vfs/ntfs/ntfs_subr.c:316
- sys/vfs/ntfs/ntfs_subr.c:318
- sys/vfs/ntfs/ntfs_subr.c:319
- sys/vfs/ntfs/ntfs_vfsops.c:393
- sys/vfs/ntfs/ntfs_vfsops.c:396
- sys/vfs/ntfs/ntfs_vfsops.c:717
- sys/vfs/ntfs/ntfs_vfsops.c:718
- sys/kern/kern_slaballoc.c:877
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).
No comments yet.