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

Subnode dive in ntfs_ntlookupfile reads 8 bytes OOB when iep->reclen < sizeof(cn_t) or entry straddles buffer end

Summary

ntfs_subr.c:1007-1011 if(iep->ie_flag&NTFS_IEFLAG_SUBNODE){ cn=*(cn_t*)(rdbuf+aoff+iep->reclen-sizeof(cn_t)) }. aoff uint32 iep->reclen uint16 sizeof(cn_t)=8 size_t. reclen<8: subtraction underflows to ~SIZE_MAX rdbuf+huge OOB read. reclen large aoff+reclen>rdsize: 8-byte VCN read past rdbuf allocation. Walk loop :900-902 only guards rdsize>aoff (entry start) not rdsize>=aoff+iep->reclen (entry end). Same issue ntfs_ntreaddir:1176-1178. Trigger: crafted NTFS image B-tree index entry reclen=4 or reclen=0xFFFF. Fix: validate reclen>=sizeof(cn_t) and aoff+reclen<=rdsize before subnode dive.

Discussion (0)

No comments yet.

PoC verification

Evidence pack

findings/poc/DF-0791 Β· 17 files
FileTypeDescriptionSize
harness.c trigger-source deterministic guard-page replication of the ntfs_ntlookupfile subnode dive (clean/oob/tiny x apply_fix) 7.4 KB view raw
gen_ntfs_0791.py trigger-source crafted NTFS image generator; root-dir INDEX_ROOT entry 'zzzzzz' SUBNODE reclen=0xFFFF 7.2 KB view raw
ntfs_0791.img crafted-image generated crafted NTFS image (256 KB) 256.0 KB ↓ download
build.sh build-script cc -O2 -o harness harness.c 207 B view raw
run.sh run-script runs harness across modes with/without fix 414 B view raw
build.log build-log harness build output 95 B view raw
run.log run-log decisive harness run (oob SIGSEGV; fix EINVAL) 1.3 KB view raw
baseline_run.log run-log unpatched baseline: live stat sibling-blocked by lockmgr panic 557 B view raw
fix_build.log fix-build-log patched ntfs.ko build + disassembly of bounds check 1.1 KB view raw
fix_run.log fix-run-log patched ntfs.ko live test: dive still sibling-blocked 1.2 KB view raw
panic.txt panic-signature sibling lockmgr self-lock at ntfs_ntlookupfile+0x57 that blocks the live dive 1.4 KB view raw
env.txt environment uname, cc, ntfs.ko, vfs.usermount 478 B view raw
fix.diff suggested-fix validate reclen>=8 and aoff+reclen<=rdsize before the subnode VCN read 978 B view raw
VERDICT.md verdict full narrative: root cause, reachability, sibling-block, harness, fix validation 8.6 KB ↓ raw
README.md readme summary + how to reproduce 9.0 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 summary + how to reproduce
↓ download raw

DF-0791 β€” Subnode dive in ntfs_ntlookupfile reads 8 bytes OOB

Verdict: REPRODUCED (deterministic harness + source trace); FIX VALIDATED (harness + disassembly)

Status: reproduced Impact: leak β€” 8-byte heap OOB read (CWE-125) of the index-entry subnode VCN into a kernel-local cn_t (read-only primitive; no write, no escalation). The live directory-lookup path that hosts the dive is sibling-blocked on this kernel by a lockmgr: locking against itself panic that fires at the top of ntfs_ntlookupfile (ntfs_ntget, +0x57) before the INDEX_ROOT walk / dive β€” so the dive's OOB read is latent on the running kernel; the primitive is proven deterministically by the guard-page harness (the accepted reproduction when a sibling panic blocks the live path). Confidence: certain (source + disassembly + harness). Severity: Medium (matches finding).


The bug (confirmed by source trace)

File: sys/vfs/ntfs/ntfs_subr.c:1006-1011 (function ntfs_ntlookupfile)

 888:  rdbuf = kmalloc(blsize, M_TEMP, M_WAITOK);          // blsize = ir_size
 ...
 900:  for (; !(iep->ie_flag & NTFS_IEFLAG_LAST) && (rdsize > aoff);
 901:      aoff += iep->reclen,                              // <-- only entry-START guarded
 902:      iep = (struct attr_indexentry *) (rdbuf + aoff))
        { ... NTFS_UASTRICMP ... if (res > 0) break; ... }

 1006: /* Dive if possible */
 1007: if (iep->ie_flag & NTFS_IEFLAG_SUBNODE) {
 1010:     cn = *(cn_t *) (rdbuf + aoff +                    // aoff = u_int32_t
 1011:             iep->reclen - sizeof(cn_t));              // reclen = u_int16_t off disk
                                                             // sizeof(cn_t) = 8 (u_int64_t)
        ... ntfs_readattr(... ntfs_cntob(cn) ...) ...

iep->reclen is a raw u_int16_t read straight off disk with no bounds validation before the subnode-VCN dereference. The walk loop (:900-902) only guards the entry start (rdsize > aoff), never the entry end or the trailing 8-byte VCN. Two malformed shapes:

Shape Crafted reclen Effect
overshoot large (e.g. 0xFFFF) aoff+reclen-8 blows past rdbuf β†’ 8-byte heap OOB read into cn (CWE-125). With 0xFFFF the read lands ~65 KB past a 4 KB buffer.
under-sized < 8 the finding's summary claims a size_t underflow; in practice aoff >= sizeof(attr_indexroot)=32, so aoff+reclen-8 stays >= 0 and reads a wrong but in-bounds offset (a semantic bug, not OOB). The harness confirms tiny (reclen=2) reads in-bounds. The real OOB is the overshoot.

The leaked bytes land in the kernel-local cn, then used as a cluster offset (ntfs_cntob(cn)) for the next ntfs_readattr β€” i.e. the OOB read influences control flow / disk reads, it is not directly exfiltrated to userspace.

Reachability

ntfs_ntlookupfile is reached from ntfs_lookup (sys/vfs/ntfs/ntfs_vnops.c:712, vop_old_lookup) on any name lookup inside an NTFS directory:

stat /mnt/ntfs/<name>  -> namei -> VOP_LOOKUP(rootdir) -> ntfs_lookup
    -> ntfs_ntlookupfile -> INDEX_ROOT walk -> (res>0) break -> SUBNODE dive

ntfs_ntlookupattr (DF-0786's path) is not entered for a plain name lookup (no :attr spec), so DF-0786 does not gate this path.

However, on this guest kernel a sibling lockmgr self-lock fires first: stat/lookup of any name in a directory whose INDEX_ROOT holds a malformed entry panics with panic: lockmgr: locking against itself at ntfs_ntlookupfile+0x57 β€” disassembly shows +0x57 is the inlined ntfs_ntget β†’ LOCKMGR(&ip->i_lock, LK_EXCLUSIVE) (the very first action of the function, addl $0x1,0x70(%r14) usecount++ then lea 0x48(%r14),%rdi = &i_lock, then callq lockmgr), which is before the ntfs_ntvattrget(INDEX_ROOT) at +0x75 and long before the dive. The dive is therefore latent on the live kernel. (A clean-image lookup does not panic, so the self-lock is triggered by the malformed-image node state; it is a separate NTFS directory-lookup defect, not DF-0791.) Per the run brief, a deterministic code-level harness is the accepted reproduction when a sibling panic blocks the live path.

Reproduction β€” deterministic harness (harness.c)

Mirrors the exact kernel walk + dive against a buffer placed at the end of a writable page immediately before a PROT_NONE guard page, so any overshoot faults deterministically (SIGSEGV):

=== BUGGY dive (kernel behaviour on default GENERIC #0) ===
mode=clean  apply_fix=0 -> rc=0  dive read OK, cn=0xaaaa007a007a007a (in bounds)
mode=oob    apply_fix=0 -> rc=2  SIGSEGV -> OOB READ past rdbuf (dive offset 65559 >= rdsize 128)
mode=tiny   apply_fix=0 -> rc=0  dive read OK (reclen=2 -> offset 26, in bounds: not OOB)

=== FIXED dive (proposed fix rejects malformed entries) ===
mode=clean  apply_fix=0 -> rc=0  dive read OK (in bounds)
mode=oob    apply_fix=1 -> rc=-1 FIX REJECTED malformed entry (reclen=65535) -> EINVAL in kernel
mode=tiny   apply_fix=1 -> rc=-1 FIX REJECTED malformed entry (reclen=2) -> EINVAL in kernel
  • oob (reclen=0xFFFF) β†’ SIGSEGV: the dive reads 65 KB past the buffer. Proves the OOB read.
  • tiny (reclen=2) β†’ reads in-bounds at offset 26: the "reclen<8 underflow" variant is not an OOB given aoff>=32; the fix still rejects it (reclen < sizeof(cn_t)).
  • With apply_fix=1, both malformed shapes are rejected with EINVAL before the dereference.

Escalation assessment (no chain β€” read-only primitive)

This is a pure read (CWE-125). No write to attacker-chosen kernel memory: the only effect is reading adjacent heap into a cn_t used as a disk offset. Per Phase 6, a read-only primitive has no escalation chain to uid=0; the correct deliverable is the characterized impact ceiling: heap info-leak / influence-on-control-flow, and DoS when the OOB-derived offset faults. No uid0 chain is applicable.

Fix β€” fix.diff

Adds a bounds check inside the SUBNODE dive, before the VCN dereference:

  • iep->reclen >= sizeof(cn_t) β€” the trailing 8-byte VCN must fit inside the entry.
  • aoff <= rdsize && iep->reclen <= rdsize - aoff β€” the entry (incl. VCN) must fit inside the valid data region (overflow-safe: checked via subtraction). Since rdsize <= blsize for sane images this also keeps the read inside the kmalloc(blsize) allocation.
  • On violation: error = EINVAL, kprintf a diagnostic, goto fail (clean return; rdbuf freed by the existing fail: label).

Fix validation (Phase 8)

NTFS is optional ntfs (sys/conf/files) β†’ ships as /boot/kernel/ntfs.ko, a loadable module, not compiled into GENERIC. So the fix only required rebuilding ntfs.ko (KERNCONF=X86_64_GENERIC make in sys/vfs/ntfs/, warm obj) and installing it β€” no kernel rebuild or reboot.

  • Compiles: make rc=0; 43 text symbols (same as stock).
  • Bounds check present: disassembly of the patched ntfs_ntlookupfile shows, before the cn read, three checks all branching to the EINVAL path (0x16): cmp %edi,-0x7c(%rbp); jb (aoff>rdsize), cmp $0x7,%ax; jbe (reclen<8), cmp %edx,%esi; ja (reclen>rdsize-aoff); the error path does mov $0x16,%r12d (EINVAL) + kprintf("...malformed index entry...") + goto fail. The string ntfs_ntlookupfile: malformed index entry (reclen %u, aoff %u, rdsize %u) is in the patched module.
  • Harness before/after: unpatched logic β†’ OOB read (SIGSEGV); fixed logic β†’ EINVAL (no OOB read). Clean before/after at the code/harness level.
  • Live dive path: sibling-blocked (the lockmgr self-lock at +0x57 fires before the dive on both the unpatched and patched kernels, so a live before/after of the dive itself is not exercisable on this guest). The fix is therefore validated at the harness + disassembly level; it demonstrably closes the vulnerable code path.

Files

  • harness.c β€” deterministic guard-page replication of the dive read (clean/oob/tiny Γ— fix).
  • gen_ntfs_0791.py β€” crafted NTFS image generator (extends DF-0786 gen_ntfs.py); root-dir (ino 5) INDEX_ROOT holds one SUBNODE entry "zzzzzz" with reclen=0xFFFF.
  • ntfs_0791.img β€” generated crafted image (256 KB).
  • build.sh / run.sh β€” exact build/run.
  • fix.diff β€” standalone git apply-able fix.
  • build.log, run.log, fix_build.log, fix_run.log, panic.txt, env.txt, manifest.json, VERDICT.md.

How to reproduce

# 1. deterministic harness (no root needed)
scp -F dfbsd-qemu/config -q findings/poc/DF-0791/{harness.c,build.sh,run.sh} dfbsd-maxx:poc/DF-0791/
ssh -F dfbsd-qemu/config dfbsd-maxx 'cd poc/DF-0791 && sh build.sh && sh run.sh'
# expect: mode=oob apply_fix=0 -> SIGSEGV (OOB READ); apply_fix=1 -> EINVAL

# 2. fix validation (rebuild ntfs.ko module, install, confirm bounds check)
scp -F dfbsd-qemu/config -q findings/poc/DF-0791/fix.diff dfbsd:/root/
ssh -F dfbsd-qemu/config dfbsd 'cd /usr/src && patch -p1 < /root/fix.diff && \
    cd sys/vfs/ntfs && KERNCONF=X86_64_GENERIC make && cp ntfs.ko /boot/kernel/ntfs.ko'
strings /boot/kernel/ntfs.ko | grep "malformed index"   # confirm fix present
VERDICT.md verdict full narrative: root cause, reachability, sibling-block, harness, fix validation
↓ download raw

DF-0791 β€” VERDICT

Verdict: REPRODUCED (deterministic harness + source trace + disassembly); FIX VALIDATED (harness + disassembly)

field value
status reproduced
reproduced true (primitive proven deterministically; live dive path sibling-blocked)
impact leak β€” 8-byte heap OOB read (CWE-125) of the subnode VCN into a kernel-local cn_t
confidence certain
severity Medium (matches finding)

1. Root cause (confirmed by source trace)

ntfs_ntlookupfile() in sys/vfs/ntfs/ntfs_subr.c walks an NTFS directory's B-tree index looking up a name. When a comparison indicates the target name sorts after the current entry (NTFS_UASTRICMP > 0 β†’ break), the code checks whether that entry has a subnode (NTFS_IEFLAG_SUBNODE) and, if so, reads the child pointer (a cn_t = u_int64_t, 8 bytes) stored in the last 8 bytes of the entry:

/* sys/vfs/ntfs/ntfs_subr.c */
 888:  rdbuf = kmalloc(blsize, M_TEMP, M_WAITOK);            /* blsize = ir_size */
 ...
 900:  for (; !(iep->ie_flag & NTFS_IEFLAG_LAST) && (rdsize > aoff);
 901:      aoff += iep->reclen,
 902:      iep = (struct attr_indexentry *) (rdbuf + aoff))
        { ... if (res > 0) break; ... }

 1006: /* Dive if possible */
 1007: if (iep->ie_flag & NTFS_IEFLAG_SUBNODE) {
 1010:     cn = *(cn_t *) (rdbuf + aoff + iep->reclen - sizeof(cn_t));
 1012:     rdsize = blsize;
 1014:     error = ntfs_readattr(ntmp, ip, NTFS_A_INDX, "$I30",
 1015:                     ntfs_cntob(cn), rdsize, rdbuf, NULL);

iep->reclen is a raw u_int16_t read directly off the crafted image (sys/vfs/ntfs/ntfs.h:173) with zero validation before the dereference. The walk-loop guard (:900, rdsize > aoff) only checks the entry start is in-bounds; it never checks the entry end (aoff + reclen) or the trailing 8-byte VCN. Consequences:

  • Overshoot (reclen large, e.g. 0xFFFF): aoff + reclen - 8 lands far past rdbuf (a kmalloc(blsize) allocation) β†’ 8-byte heap OOB read (CWE-125). With reclen=0xFFFF the read is ~65 KB past a 4 KB buffer.
  • Under-sized (reclen < 8): the finding summary speculates a size_t underflow to ~SIZE_MAX. In the INDEX_ROOT walk aoff starts at sizeof(struct attr_indexroot) = 32 and only grows, so aoff+reclen-8 stays >= 0 and reads a wrong but in-bounds offset (a semantic bug, not OOB). The harness confirms tiny (reclen=2) reads in-bounds β€” the real OOB is the overshoot.

The leaked 8 bytes land in the kernel-local cn, used as ntfs_cntob(cn) (a cluster offset) for the following ntfs_readattr. So the read influences control flow / disk reads; it is not directly exfiltrated to userspace. No write primitive β‡’ no escalation chain (read-only class).

2. Reachability

ntfs_ntlookupfile is the vop_old_lookup implementation, reached from ntfs_lookup (sys/vfs/ntfs/ntfs_vnops.c:712) on any name lookup inside an NTFS directory. ntfs_ntlookupattr (DF-0786's path) is entered only for an attribute-qualified name (name:attr), so a plain stat /mnt/ntfs/<name> does not hit DF-0786. Threat model: a root-mountable crafted NTFS image (mount_ntfs is SYSCAP_RESTRICTEDROOT, vfs.usermount=0 verified) β€” the standard filesystem-image model (admin mounts / makes mountable an untrusted image; the lookup itself is unprivileged).

3. Live-path assessment β€” sibling-blocked

On this guest kernel (6.5-DEVELOPMENT #0), a sibling lockmgr self-lock fires at the very top of ntfs_ntlookupfile, before the INDEX_ROOT walk and the dive:

panic: lockmgr: locking against itself
lockmgr_exclusive() at lockmgr_exclusive+0x3e0
ntfs_ntlookupfile() at ntfs_ntlookupfile+0x57    ; <- inlined ntfs_ntget
ntfs_lookup() at ntfs_lookup+0x63

Disassembly (ntfs.ko, ntfs_ntlookupfile @ 0x4bd0) pins +0x57 to the inlined ntfs_ntget:

4c15: addl $0x1,0x70(%r14)   ; ip->i_usecount++
4c1a: lea  0x48(%r14),%rdi   ; &ip->i_lock
4c22: callq 4c27             ; -> lockmgr(LK_EXCLUSIVE)   [+0x57]
4c40: callq (ntfs_ntvattrget, NTFS_A_INDXROOT=0x90)       ; [+0x75] INDEX_ROOT fetch is AFTER

So the self-lock happens at the first LOCKMGR(&ip->i_lock, LK_EXCLUSIVE), before the INDEX_ROOT is even fetched β€” long before the dive at +0x2c0. The dive (and DF-0791's OOB read) is therefore latent on the running kernel. It reproducibly fires for the malformed image (3/3 patched attempts, plus unpatched attempts); a clean-image lookup does not panic, confirming the self-lock is a separate malformed-image/node-state directory-lookup defect, not DF-0791.

Per the run brief: "If a sibling panic blocks the live path, a deterministic code-level harness reproducing the unvalidated subnode-pointer read is acceptable." β€” the harness below is that reproduction.

4. Reproduction β€” deterministic harness (harness.c)

Mirrors the exact kernel walk + dive against a buffer placed at the end of a writable page immediately before a PROT_NONE guard page, so any overshoot faults deterministically (SIGSEGV). Output (unprivileged maxx):

=== BUGGY dive (kernel behaviour on default GENERIC #0) ===
mode=clean  apply_fix=0 -> rc=0  dive read OK, cn=0xaaaa007a007a007a (in bounds)
mode=oob    apply_fix=0 -> rc=2  SIGSEGV -> OOB READ past rdbuf (offset 65559 >= rdsize 128)
mode=tiny   apply_fix=0 -> rc=0  dive read OK (reclen=2 -> offset 26, in bounds: not OOB)

=== FIXED dive (proposed fix rejects malformed entries) ===
mode=oob    apply_fix=1 -> rc=-1 FIX REJECTED malformed entry (reclen=65535) -> EINVAL
mode=tiny   apply_fix=1 -> rc=-1 FIX REJECTED malformed entry (reclen=2) -> EINVAL
  • oob (reclen=0xFFFF) β†’ SIGSEGV: the dive reads 65 KB past the buffer. Proves the OOB read.
  • tiny (reclen=2) β†’ reads in-bounds: the "reclen<8 underflow" variant is not OOB given aoff>=32; the fix still rejects it (reclen < sizeof(cn_t)).
  • apply_fix=1 rejects both malformed shapes with EINVAL before the dereference.

5. Escalation assessment

Pure read (CWE-125). No write to attacker-chosen kernel memory. Per Phase 6 a read-only primitive has no escalation chain to uid=0; the deliverable is the characterized ceiling: heap info-leak / control-flow influence / DoS when the OOB-derived offset faults. No uid0 chain is applicable (valid hard blocker: read-only primitive).

6. Fix β€” fix.diff

Adds a bounds check inside the SUBNODE dive, before the VCN dereference:

  • iep->reclen >= sizeof(cn_t) β€” the trailing 8-byte VCN fits inside the entry.
  • aoff <= rdsize && iep->reclen <= rdsize - aoff β€” the entry (incl. VCN) fits inside the valid data region (overflow-safe via subtraction; since rdsize <= blsize for sane images this also keeps the read inside the kmalloc(blsize) allocation).
  • On violation: error = EINVAL, kprintf diagnostic, goto fail.

Minimal and targeted at the root cause (the missing bounds check). Does not touch the lockmgr sibling bug, the on-disk format, or the happy path for valid images.

7. Fix validation (Phase 8)

NTFS is optional ntfs (sys/conf/files) β†’ /boot/kernel/ntfs.ko, a loadable module (not in GENERIC). Fix = rebuild ntfs.ko only (no kernel rebuild/reboot).

  • Applies + compiles: patch -p1 hunk succeeded at line 1007; make rc=0; 43 text symbols (same as stock).
  • Bounds check present (disassembly): before the cn read, three checks all branch to the EINVAL path (mov $0x16,%r12d = EINVAL 22) + kprintf: cmp %edi,-0x7c(%rbp); jb (aoff>rdsize), cmp $0x7,%ax; jbe (reclen<8), cmp %edx,%esi; ja (reclen>rdsize-aoff). String ntfs_ntlookupfile: malformed index entry (reclen %u, aoff %u, rdsize %u) present.
  • Harness before/after: unpatched logic β†’ OOB read (SIGSEGV); fixed logic β†’ EINVAL (no OOB read). Clean before/after at the code/harness level.
  • Live dive path: the sibling lockmgr self-lock fires at +0x57 on both unpatched and patched kernels (same panic, same offset, verified on the patched module at 0x4a60+0x57=0x4ab7), so a live before/after of the dive itself is not exercisable on this guest. The fix is validated at the harness + disassembly level; it demonstrably closes the vulnerable code path.

8. PoC changes

Authored from scratch (no prior PoC existed for DF-0791): - harness.c β€” deterministic guard-page replication of the dive (clean/oob/tiny Γ— fix). - gen_ntfs_0791.py β€” crafted NTFS image generator extending DF-0786's gen_ntfs.py; root-dir (ino 5) INDEX_ROOT holds one SUBNODE entry "zzzzzz" with reclen=0xFFFF. - ntfs_0791.img β€” generated crafted image. - build.sh / run.sh β€” exact build/run. - fix.diff β€” standalone git apply-able fix at the root cause.

Fix verification

fixed
baseline reproduced→ patch + rebuild →patched clean

VALIDATED at the harness + disassembly level. The bug is a read-only OOB whose live directory-lookup path is sibling-blocked by a lockmgr self-lock panic at ntfs_ntlookupfile+0x57 (inlined ntfs_ntget) that fires BEFORE the dive on BOTH the unpatched and patched kernels (verified: patched module at 0x4a60+0x57=0x4ab7 panics identically, 3/3 attempts). So a live before/after of the dive itself is not exercisable on this guest, and the deterministic harness is the test vehicle. The patched ntfs.ko: (a) applies + compiles (make rc=0, 43 text symbols), (b) the bounds check is confirmed compiled in before the cn read by disassembly (cmp %edi,-0x7c; jb / cmp $0x7,%ax; jbe / cmp %edx,%esi; ja, all -> EINVAL path 'mov $0x16,%r12d' + kprintf 'ntfs_ntlookupfile: malformed index entry'), (c) the harness shows a clean before/after: unpatched logic -> OOB read SIGSEGV (mode=oob rc=2); fixed logic -> EINVAL (mode=oob apply_fix=1 rc=-1). The fix demonstrably closes the vulnerable code path.

BEFORE (unpatched harness): mode=oob apply_fix=0 -> rc=2 SIGSEGV -> OOB READ past rdbuf (dive offset 65559 >= rdsize 128)
AFTER  (fixed harness):   mode=oob apply_fix=1 -> rc=-1 FIX REJECTED malformed entry (reclen=65535) -> EINVAL in kernel
BEFORE (unpatched, tiny):  mode=tiny apply_fix=0 -> rc=0 (reads in-bounds; not OOB)
AFTER  (fixed, tiny):      mode=tiny apply_fix=1 -> rc=-1 FIX REJECTED (reclen=2 < 8) -> EINVAL
Disassembly of patched ntfs_ntlookupfile: bounds checks (aoff>rdsize / reclen<8 / reclen>rdsize-aoff) precede the cn read; failure -> EINVAL(0x16)+kprintf+goto fail.
↓ fix.diff6.5-DEVELOPMENT #0 (module-only fix: ntfs.ko rebuilt with fix.diff via 'KERNCONF=X86_64_GENERIC make' in sys/vfs/ntfs and installed to /boot/kernel/ntfs.ko; NTFS is 'optional ntfs' so no kernel rebuild/reboot was required)

Confirmed kernel references

Detail

Exploit chain

none (read-only primitive). DF-0791 is a pure 8-byte heap OOB READ (CWE-125): the leaked bytes land in a kernel-local cn_t used as ntfs_cntob(cn) (a cluster offset) for the following ntfs_readattr -- i.e. the read influences control flow / disk reads, it is NOT directly exfiltrated to userspace and is NOT a write. Per Phase 6 a read-only primitive has no escalation chain to uid=0; the valid deliverable is the characterized impact ceiling (heap info-leak / control-flow influence; DoS when the OOB-derived offset faults). No chain file authored (not applicable for a read-only class).

Evidence (decisive lines)

=== BUGGY dive ===
mode=clean apply_fix=0 -> rc=0 dive read OK, cn=0xaaaa007a007a007a (in bounds)
mode=oob   apply_fix=0 -> rc=2 SIGSEGV -> OOB READ past rdbuf (dive offset 65559 >= rdsize 128)
mode=tiny  apply_fix=0 -> rc=0 dive read OK (reclen=2 -> offset 26, in bounds: NOT OOB)
=== FIXED dive ===
mode=oob   apply_fix=1 -> rc=-1 FIX REJECTED malformed entry (reclen=65535) -> EINVAL in kernel
mode=tiny  apply_fix=1 -> rc=-1 FIX REJECTED malformed entry (reclen=2) -> EINVAL in kernel
--- live (sibling-blocked) ---
panic: lockmgr: locking against itself | ntfs_ntlookupfile() at ntfs_ntlookupfile+0x57 (inlined ntfs_ntget, BEFORE the dive at +0x2c0) | ntfs_lookup+0x63

PoC changes

Authored the full evidence pack from scratch (no prior DF-0791 PoC existed): harness.c (deterministic guard-page replication of the walk+dive with clean/oob/tiny modes and an apply_fix flag for direct before/after), gen_ntfs_0791.py (crafted NTFS image generator extending DF-0786's gen_ntfs.py; root-dir ino-5 INDEX_ROOT holds one SUBNODE entry 'zzzzzz' with reclen=0xFFFF), ntfs_0791.img (generated), build.sh/run.sh, VERDICT.md, README.md, manifest.json, and fix.diff. The harness models the exact kernel arithmetic (rdbuf+aoff+reclen-sizeof(cn_t)) and proved the overshoot is a genuine OOB read while the under-sized-reclen variant is not (aoff>=32).

Verified recommended fix

In ntfs_ntlookupfile's SUBNODE dive (sys/vfs/ntfs/ntfs_subr.c:1007), before cn=(cn_t)(rdbuf+aoff+iep->reclen-sizeof(cn_t)), validate the entry+VCN fits in the valid data: reject (error=EINVAL, kprintf diagnostic, goto fail) if iep->reclen < sizeof(cn_t) OR aoff > rdsize OR (u_int32_t)iep->reclen > rdsize - aoff (overflow-safe). matches finding proposal (the DB summary proposed 'validate reclen>=sizeof(cn_t) and aoff+reclen<=rdsize before subnode dive'). The full git-apply-able diff is in findings/poc/DF-0791/fix.diff. Note: ntfs_ntreaddir:1176 (also cited by the finding) has the same weak loop guard (rdsize>aoff, entry-start only) but does NOT perform the cn dereference, so it is a lesser hardening gap, not the same OOB read.

Verdict

REPRODUCED. The bug is real: ntfs_ntlookupfile's B-tree subnode dive at sys/vfs/ntfs/ntfs_subr.c:1010-1011 reads cn=(cn_t)(rdbuf+aoff+iep->reclen-8) where iep->reclen is a raw u_int16_t read straight off a crafted image with NO bounds validation, and the walk loop (:900) only guards the entry START (rdsize>aoff), never the entry END or the trailing 8-byte VCN. A crafted INDEX_ROOT entry with NTFS_IEFLAG_SUBNODE + reclen=0xFFFF makes the dive read ~65KB past the kmalloc(blsize) buffer (8-byte heap OOB read, CWE-125). Confirmed three ways: (1) source trace (no guard before the deref), (2) disassembly of ntfs.ko (the cn read has no preceding bounds check; the inlined ntfs_ntget/lockmgr is at +0x57, the cn read is at +0x2c0), and (3) a deterministic guard-page harness that SIGSEGVs on the OOB dive read. NOTE on the live kernel: the directory-lookup path that hosts the dive is sibling-blocked -- a lockmgr 'locking against itself' panic fires at ntfs_ntlookupfile+0x57 (the inlined ntfs_ntget, BEFORE the INDEX_ROOT fetch at +0x75 and long before the dive), reproducibly (3/3 patched + several unpatched attempts) for the malformed image; a clean-image lookup does not panic, confirming the self-lock is a separate malformed-image node-state directory-lookup defect, not DF-0791. Per the run brief, the deterministic harness is the accepted reproduction when a sibling panic blocks the live path. The finding's 'reclen<sizeof(cn_t) underflow' variant is technically inaccurate: aoff starts at sizeof(attr_indexroot)=32 and only grows, so aoff+reclen-8 stays in-bounds for small reclen (harness confirms tiny/reclen=2 reads in-bounds at offset 26 -- a semantic bug, not OOB); the genuine OOB is the overshoot (large reclen).