Off-by-one heap overflow in ntfs_ntlookupattr: NUL terminator written one byte past kmalloc(namelen)
Summary
ntfs_subr.c:826 (*attrname)=kmalloc(namelen). :827 memcpy(*attrname,name,namelen). :828 (*attrname)[namelen]=0 β index namelen one past end of allocation (valid 0..namelen-1). Single NUL-byte heap overflow into M_TEMP slab. namelen from user pathname with : separator (cnp->cn_namelen-fnamelen-1) attacker-controlled up to NAME_MAX. Trigger: stat /mnt/file:AAAA on ANY mounted NTFS volume no malicious image needed. Fix: kmalloc(namelen+1).
Discussion (0)
PoC verification
Evidence pack
findings/poc/DF-0786 Β· 13 files| File | Type | Description | Size | |
|---|---|---|---|---|
| trigger.c | trigger-source | Guard-page harness + live NTFS trigger for the off-by-one | 5.2 KB | view raw |
| gen_ntfs.py | trigger-source | Python NTFS image generator (minimal valid NTFS volume) | 15.3 KB | view raw |
| ntfs.img | test-data | Generated 256KB NTFS image (mounts successfully) | 256.0 KB | β download |
| build.sh | build-script | Build trigger + generate NTFS image | 510 B | view raw |
| run.sh | run-script | Run the harness or live trigger | 348 B | view raw |
| run_harness.log | run-log | Guard-page harness output: SIGSEGV at all namelen values | 833 B | view raw |
| panic.txt | panic-signature | Pre-existing NTFS lockmgr panic blocking live lookup | 1.0 KB | view raw |
| fix.diff | suggested-fix | kmalloc(namelen+1) β allocate room for NUL terminator | 361 B | view raw |
| fix_build.log | build-log | Single-fix kernel build output (35303 lines, rc=0) | 5.6 MB | β download |
| env.txt | environment | Guest uname, compiler, sysctl state | 640 B | view raw |
| VERDICT.md | verdict | Full analysis: mechanism, impact, fix | 6.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 |
DF-0786: Off-by-one heap overflow in ntfs_ntlookupattr
Verdict: REPRODUCED (off-by-one confirmed via source analysis + guard-page harness; live NTFS lookup blocked by pre-existing lockmgr panic)
The Bug
File: sys/vfs/ntfs/ntfs_subr.c:826-828
Function: ntfs_ntlookupattr()
CWE: CWE-787 Out-of-bounds Write
825: if (namelen) {
826: (*attrname) = kmalloc(namelen, M_TEMP, M_WAITOK); // alloc exactly namelen bytes (valid: 0..namelen-1)
827: memcpy((*attrname), name, namelen); // fills 0..namelen-1 (OK)
828: (*attrname)[namelen] = '\0'; // OFF-BY-ONE: writes at index namelen = 1 byte past end
829: }
The allocation is exactly namelen bytes. The valid indices are 0 through namelen-1. Writing '\0' at index namelen is one byte past the end of the allocation β a classic off-by-one heap overflow.
Reachability
The trigger path is:
1. An NTFS volume is mounted (root mount threat model β vfs.usermount=0, root-only)
2. A file is looked up with a : in its name: stat /mnt/ntfs/existingfile:ATTRNAME
3. ntfs_lookup() β ntfs_ntlookupfile() splits the name at :, extracting aname="ATTRNAME" with anamelen=strlen("ATTRNAME")
4. When the filename portion matches a directory index entry, ntfs_ntlookupattr(ntmp, aname, anamelen, ...) is called
5. The off-by-one fires: kmalloc(anamelen) + buf[anamelen]='\0'
Source trace:
- sys/vfs/ntfs/ntfs_vnops.c:712 β ntfs_lookup calls ntfs_ntlookupfile
- sys/vfs/ntfs/ntfs_subr.c:877-884 β name split at : β aname/anamelen
- sys/vfs/ntfs/ntfs_subr.c:924-927 β if (aname) ntfs_ntlookupattr(ntmp, aname, anamelen, ...)
- sys/vfs/ntfs/ntfs_subr.c:826-828 β the off-by-one
The anamelen is attacker-controlled up to NAME_MAX (255) minus the filename length. A value of 8, 16, 32, 64, etc. (slab bucket boundaries) causes the NUL byte to overflow into the adjacent slab chunk.
Reproduction Method
1. Guard-page harness (deterministic proof)
trigger.c -h replicates the exact allocation logic with a guard page:
- Places the buffer at the END of a writable page, followed by a PROT_NONE guard page
- buf[namelen] provably falls into the guard page β SIGSEGV
- Tested at namelen=1,2,4,8,16,32 β ALL fault, proving the write is ALWAYS past the allocation
2. Live NTFS mount + trigger (blocked by pre-existing lockmgr panic)
A minimal valid NTFS image (gen_ntfs.py) was crafted that mounts successfully:
- Boot sector, MFT records (ino 0-10), $UpCase, $AttrDef, $Bitmap, root directory with index entry "a"
- mount_ntfs -o ro /dev/vnN /mnt/ntfs succeeds
However, stat /mnt/ntfs/a:AAAAAAAA panics BEFORE reaching the off-by-one:
panic: lockmgr: locking against itself ntfs_ntlookupfile() at ntfs_ntlookupfile+0x57 ntfs_lookup() at ntfs_lookup+0x63
This is a pre-existing DragonFly NTFS locking bug β ntfs_ntget() tries to exclusively lock ip->i_lock when it is already held by the current thread. It affects ALL file lookups on mounted NTFS volumes in this kernel version (6.5-DEVELOPMENT #0), not just the off-by-one trigger. Both the lookup path (ntfs_ntlookupfile) and the readdir path (ntfs_ntreaddir) exhibit this panic.
Impact Assessment
On default GENERIC (INVARIANTS ON, use_weird_array=0, use_malloc_pattern=0):
- The off-by-one produces SILENT heap corruption β no panic, no KASSERT
- The slab allocator's INVARIANTS checks are bitmap-based (allocation status only, not content)
debug.use_weird_array=0means freed chunks are NOT poisoned β no content check on reallocation- The NUL byte (0x00) overwrites the first byte of the adjacent slab chunk
Slab bucket analysis:
kmalloc(namelen)rounds up to bucket sizes: 8, 16, 32, 64, 128, 256, 512, ...- When
namelenequals a bucket boundary (e.g., 8), the NUL byte overflows into the NEXT chunk - When
namelenis NOT a boundary (e.g., 7), the NUL byte lands in padding within the same chunk (still technically OOB but no adjacent-object corruption)
Escalation assessment:
- Primitive: 1-byte NUL write at a slab-bucket boundary, content=0x00 (not attacker-controlled)
- Preconditions: root-mounted NTFS volume (threat model: crafted image or existing NTFS)
- Live reachability: BLOCKED by pre-existing lockmgr panic (separate DragonFly NTFS bug)
- uid=0 assessment: Not achievable on this guest. The live NTFS lookup path is dead (lockmgr panic). Even if reachable, a 1-byte NUL write is an extremely weak primitive β zeroing the first byte of an adjacent object could theoretically corrupt a pointer's low byte, a refcount, or a flag, but converting this to privilege escalation would require: 1. Precise heap grooming to place a sensitive victim object adjacent 2. The victim's first byte being security-critical (uid low byte, function pointer low byte) 3. The zeroed value leading to a controllable condition None of these are achievable through the dead NTFS lookup path on this kernel.
Realistic impact ceiling:
- Silent heap corruption on a mounted NTFS volume when a filename with
:attrnameis looked up - Potential DoS if the corrupted adjacent object causes a later crash
- Theoretical privesc with extreme difficulty (weak primitive, dead lookup path)
- Rated Medium by the finding (appropriate given the preconditions and weak primitive)
Fix
fix.diff: Change kmalloc(namelen, ...) to kmalloc(namelen + 1, ...) β allocate one extra byte for the NUL terminator.
This is a minimal, targeted fix at the root cause. The namelen + 1 allocation provides valid indices 0 through namelen, so buf[namelen] = '\0' writes within bounds.
PoC Changes
gen_ntfs.pyβ Python NTFS image generator (crafts a minimal valid NTFS volume that mounts successfully, with a root directory index entry for "a")trigger.cβ C program with two modes: guard-page harness (proves the off-by-one deterministically) and live NTFS trigger (attempts stat on mounted volume)fix.diffβ git-apply-able fix:kmalloc(namelen + 1, M_TEMP, M_WAITOK)build.sh/run.shβ reproducible build and run scripts
Fix verification
not_testableNOT TESTABLE at runtime: the live NTFS lookup PoC cannot be exercised on the guest because the DragonFly NTFS lookup path panics with a pre-existing lockmgr error ('locking against myself' in ntfs_ntlookupfile->ntfs_ntget) before reaching the vulnerable ntfs_ntlookupattr code. The fix was validated at the code level: (1) fix.diff applies cleanly (patch -p1 succeeded, hunk #1 at line 823), (2) the single-fix kernel compiled successfully (make -j6 nativekernel rc=0, 35303-line build log), (3) the fixed kernel boots and runs (kern.version shows #1 with today's timestamp), (4) source inspection confirms the fix: line 826 now reads kmalloc(namelen + 1, M_TEMP, M_WAITOK), providing valid indices 0..namelen so buf[namelen]='\0' writes within bounds. The guard-page harness (trigger.c -h) proves the off-by-one exists in the original code logic; the fix changes that logic to eliminate it by construction.
Baseline (#0, unpatched) source: 826: (*attrname) = kmalloc(namelen, M_TEMP, M_WAITOK); 828: (*attrname)[namelen] = '\0'; // OFF-BY-ONE: namelen past allocation Fixed (#1, patched) source: 826: (*attrname) = kmalloc(namelen + 1, M_TEMP, M_WAITOK); 828: (*attrname)[namelen] = '\0'; // now within bounds (0..namelen valid) Guard-page harness (proves the off-by-one on unpatched logic): namelen=8: buf[8] = SIGSEGV (OOB WRITE CONFIRMED) Fixed kernel build: rc=0 (35303 lines, NK_DONE) Fixed kernel boot: DragonFly 6.5-DEVELOPMENT #1: Fri Jul 10 05:52:07 UTC 2026 Live runtime test: BLOCKED by pre-existing NTFS lockmgr panic (not the off-by-one)
Confirmed kernel references
Detail
Exploit chain
BLOCKED by valid hard blocker (dead code path): The live NTFS lookup path (ntfs_lookup -> ntfs_ntlookupfile -> ntfs_ntlookupattr) is unreachable on this kernel because ntfs_ntlookupfile+0x57 panics with 'lockmgr: locking against myself' in ntfs_ntget() before reaching the vulnerable code at line 826. This is a pre-existing DragonFly NTFS locking bug, not a limitation of the PoC. Even if the path were reachable, the primitive is extremely weak: a single NUL byte (0x00) written at a slab-bucket boundary (when namelen=8,16,32,...) into the adjacent slab chunk, with zero attacker control over the content. On default GENERIC (INVARIANTS ON, use_weird_array=0, use_malloc_pattern=0), this produces SILENT heap corruption with no panic or KASSERT -- the NUL byte overwrites the first byte of the adjacent object without detection. Converting a 1-byte NUL write to uid=0 would require: (1) precise heap grooming to place a security-critical victim adjacent (blocked by dead lookup path), (2) the victim's first byte being exploitable when zeroed. The dead code path is a valid hard blocker per Phase 6. The code-level harness (trigger.c) is the proof of the primitive.
Evidence (decisive lines)
Guard-page harness output (run as unprivileged maxx): namelen= 1: buf[1] = SIGSEGV (OOB WRITE CONFIRMED) namelen= 8: buf[8] = SIGSEGV (OOB WRITE CONFIRMED) namelen= 16: buf[16] = SIGSEGV (OOB WRITE CONFIRMED) namelen= 32: buf[32] = SIGSEGV (OOB WRITE CONFIRMED) Source (ntfs_subr.c:826-828): (*attrname) = kmalloc(namelen, M_TEMP, M_WAITOK); memcpy((*attrname), name, namelen); (*attrname)[namelen] = '\0'; // OFF-BY-ONE Live NTFS mount succeeds but lookup panics (pre-existing lockmgr bug): panic: lockmgr: locking against myself ntfs_ntlookupfile() at ntfs_ntlookupfile+0x57 ntfs_lookup() at ntfs_lookup+0x63
PoC changes
Created the entire PoC evidence pack from scratch (finding had no existing PoC folder). Wrote: (1) gen_ntfs.py -- Python NTFS image generator that crafts a minimal valid NTFS volume (boot sector + 11 MFT records + $UpCase + $AttrDef + $Bitmap + root dir with index entry 'a'); the image mounts successfully via mount_ntfs. (2) trigger.c -- C program with guard-page harness mode (proves off-by-one via SIGSEGV at buf[namelen]) and live NTFS trigger mode. (3) fix.diff -- kmalloc(namelen+1). (4) VERDICT.md, build.sh, run.sh, manifest.json. The live NTFS lookup path panics due to a pre-existing lockmgr bug in ntfs_ntget, which is documented in panic.txt and VERDICT.md.
Verified recommended fix
Change line 826 of sys/vfs/ntfs/ntfs_subr.c from kmalloc(namelen, M_TEMP, M_WAITOK) to kmalloc(namelen + 1, M_TEMP, M_WAITOK). This allocates one extra byte for the NUL terminator written at buf[namelen], eliminating the off-by-one. The fix is in findings/poc/DF-0786/fix.diff and supersedes any finding proposal (no finding markdown existed).
Verdict
REPRODUCED. The off-by-one heap overflow in ntfs_ntlookupattr (sys/vfs/ntfs/ntfs_subr.c:826-828) is confirmed via source analysis and a deterministic guard-page harness. The code allocates exactly namelen bytes via kmalloc(namelen, M_TEMP, M_WAITOK) then writes a NUL terminator at index namelen -- one byte past the allocation boundary. The guard-page harness (trigger.c -h) places the buffer at a page boundary and demonstrates SIGSEGV at buf[namelen] for ALL tested sizes (1,2,4,8,16,32), proving the write is ALWAYS out of bounds. The live NTFS trigger path (mount crafted image + stat /mnt/ntfs/a:AAAAAAAA) was fully staged: a minimal valid NTFS image was crafted (gen_ntfs.py) that mounts successfully, but the DragonFly NTFS lookup path panics with 'lockmgr: locking against myself' in ntfs_ntlookupfile->ntfs_ntget BEFORE reaching the vulnerable ntfs_ntlookupattr code. This is a pre-existing DragonFly NTFS locking bug affecting ALL file lookups on mounted NTFS volumes in this kernel version, not specific to the off-by-one. The off-by-one is therefore confirmed at the code/logic level but not exerciseable through the live filesystem path on this kernel.
No comments yet.