ATTRLIST parsing DoS in ntfs_ntvattrget: reclen==0 infinite loop or reclen>len NULL deref panic
Summary
ntfs_subr.c:186-211 for(;len>0;aalp=nextaalp){ if(len>aalp->reclen){nextaalp=NTFS_NEXTREC(aalp)}else{nextaalp=NULL} len-=aalp->reclen }. aalp->reclen uint16 from disk attacker-controlled. reclen==0: nextaalp=aalp (NTFS_NEXTREC adds 0) len-=0 unchanged infinite loop kernel hang. reclen>len: nextaalp=NULL len underflows (size_t) huge loop iterates aalp=NULL deref aalp->reclen panic. Trigger: crafted NTFS image $ATTRIBUTE_LIST (type 0x20) entry reclen=0 or 0xFFFF. Fix: if(reclen<sizeof(attr_attrlist)||reclen>len) error=EINVAL break.
Discussion (0)
PoC verification
Evidence pack
findings/poc/DF-0790 Β· 17 files| File | Type | Description | Size | |
|---|---|---|---|---|
| harness.c | trigger-source | userspace replication of ntfs_ntvattrget ATTRLIST walk (196-236) with guard-page proof for reclen==0 loop + reclen>len NULL deref | 11.1 KB | view raw |
| gen_ntfs_0790.py | trigger-source | crafted NTFS image generator: ino-5 and mount-time ino-6 variants (loop/null) | 12.1 KB | view raw |
| ntfs_0790_mount_loop.img | crafted-image | mount-time trigger: ino 6 $ATTRIBUTE_LIST with reclen==0 entry β kernel hang during mount | 256.0 KB | β download |
| ntfs_0790_mount_null.img | crafted-image | mount-time trigger: ino 6 $ATTRIBUTE_LIST with reclen>len entry β NULL deref path | 256.0 KB | β download |
| build.sh | build-script | cc -O2 -o harness harness.c | 337 B | view raw |
| run.sh | run-script | runs harness in all modes (buggy + fixed) | 913 B | view raw |
| build.log | build-log | harness build output | 95 B | view raw |
| run.log | run-log | harness run output: 3 runs, all modes | 2.4 KB | view raw |
| hang_evidence.txt | panic-signature | live kernel hang proof: mount_loop image wedges guest during mount (rc=124) | 2.1 KB | view raw |
| env.txt | environment | uname, cc version, vfs.usermount | 426 B | view raw |
| fix.diff | suggested-fix | reject reclen==0/reclen>len in ntfs_ntvattrget ATTRLIST walk with EINVAL | 1.2 KB | view raw |
| fix_build.log | build-log | patched ntfs.ko standalone module build | 212 B | view raw |
| fix_run.log | run-log | Phase 8 before/after: unpatched hangs, patched returns EINVAL cleanly | 2.1 KB | view raw |
| VERDICT.md | verdict | full narrative: mechanism, reachability, reproduction, fix validation | 10.7 KB | β raw |
| README.md | readme | how to reproduce | 2.3 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-0790 β ATTRLIST parsing DoS in ntfs_ntvattrget
Verdict: REPRODUCED (live kernel hang + deterministic harness) β FIX VALIDATED
Impact: dos (kernel hang / local DoS via crafted NTFS image; no escalation)
Confidence: certain
What this is
ntfs_ntvattrget (sys/vfs/ntfs/ntfs_subr.c:196-236) walks the on-disk $ATTRIBUTE_LIST
to find an attribute referenced from another MFT record. The walk advances by aalp->reclen
without checking reclen == 0 (β infinite loop / kernel hang) or reclen > len
(β size_t underflow β NULL deref panic). A crafted NTFS image triggers it at mount time.
How to reproduce
1. Userspace harness (deterministic, unprivileged)
ssh -F dfbsd-qemu/config dfbsd-maxx 'mkdir -p poc/DF-0790'
scp -F dfbsd-qemu/config -q {harness.c,build.sh,run.sh} dfbsd-maxx:poc/DF-0790/
ssh -F dfbsd-qemu/config dfbsd-maxx 'cd poc/DF-0790 && sh build.sh && sh run.sh'
Expected: loop β rc=1 (iteration cap = infinite loop in kernel); null β rc=2 (SIGSEGV = NULL deref).
2. Live kernel trigger (root β EXPECTS HANG, guest dies)
python3 gen_ntfs_0790.py mount_loop ntfs_0790_mount_loop.img
scp -F dfbsd-qemu/config -q ntfs_0790_mount_loop.img dfbsd:/root/
./dfbsd-qemu/vm.sh run_root 'vnconfig -c vn0 /root/ntfs_0790_mount_loop.img && mount_ntfs -o ro /dev/vn0 /mnt/ntfs'
# ^mount(2) NEVER RETURNS β guest wedged (infinite loop in ntfs_ntvattrget)
./dfbsd-qemu/vm.sh reset with-src
3. Fix validation (rebuild ntfs.ko module β no kernel reboot needed)
scp -F dfbsd-qemu/config -q fix.diff dfbsd:/root/
./dfbsd-qemu/vm.sh run_root 'cd /usr/src && patch -p1 < /root/fix.diff && cd sys/vfs/ntfs && KERNCONF=X86_64_GENERIC make && cp ntfs.ko /boot/kernel/ntfs.ko'
# re-run step 2; mount now fails cleanly: "Invalid argument" (EINVAL), guest stays UP
# dmesg: ntfs_ntvattrget: malformed attrlist entry (reclen 0, len 26)
Files
harness.cβ deterministic userspace replication of the ATTRLIST walk (guard-page backed).gen_ntfs_0790.pyβ crafted NTFS image generator (ino-5 and mount-time ino-6 variants).fix.diffβ the validated fix (rejects reclen==0 / reclen>len with EINVAL).VERDICT.mdβ full narrative with path:line citations.hang_evidence.txt/fix_run.logβ live before/after evidence.
DF-0790 β ATTRLIST parsing DoS in ntfs_ntvattrget: reclen==0 infinite loop / reclen>len NULL deref
Verdict: REPRODUCED (live kernel hang on default GENERIC #0 + deterministic userspace harness) β FIX VALIDATED
The bug (confirmed by source trace + live hang + harness)
File: sys/vfs/ntfs/ntfs_subr.c:196-236 (function ntfs_ntvattrget)
Class: CWE-835 (infinite loop) + CWE-476 (NULL deref) β both stem from the same missing bounds check on aalp->reclen.
Severity (per finding): Medium. Confirmed: realistic impact ceiling is kernel hang / local DoS (no write primitive, no escalation).
186: len = lvap->va_datalen;
187: alpool = kmalloc(len, M_TEMP, M_WAITOK);
188: error = ntfs_readntvattr_plain(ntmp, ip, lvap, 0, len, alpool, &len, NULL);
...
193: aalp = (struct attr_attrlist *) alpool;
194: nextaalp = NULL;
196: for(; len > 0; aalp = nextaalp) {
202: if (len > aalp->reclen) {
203: nextaalp = NTFS_NEXTREC(aalp, struct attr_attrlist *); // = aalp + aalp->reclen
204: } else {
205: nextaalp = NULL;
206: }
207: len -= aalp->reclen; // <-- NO CHECK that reclen != 0, no check reclen <= len
...
236: }
struct attr_attrlist (ntfs.h:144-154): al_type(u32) reclen(u16) al_namelen(u8) al_nameoff(u8) al_vcnstart(u64) al_inumber(u32) reserved(u32) al_index(u16) al_name[1](u16).
NTFS_NEXTREC (ntfs.h:273): #define NTFS_NEXTREC(s, type) ((type)(((caddr_t) s) + (s)->reclen)).
Two malformed-image shapes, both unguarded:
| Variant | Crafted bytes | Effect in kernel |
|---|---|---|
| loop | ATTRLIST entry with reclen == 0 |
len -= 0 (unchanged); nextaalp = aalp + 0 == aalp; the for() never advances β infinite loop (CWE-835). Kernel spins at 100% CPU; mount(2) never returns. |
| null | ATTRLIST entry with reclen > len |
the else sets nextaalp = NULL; len -= reclen underflows size_t to ~2^64; next iteration: len > 0 true, aalp = NULL; the len > aalp->reclen check dereferences NULL β panic (CWE-476). |
Reachability (mount-time β before the sibling lockmgr panic)
ntfs_ntvattrget's ATTRLIST walk is entered when a requested attribute is NOT found inline in the MFT
record but a $ATTRIBUTE_LIST (type 0x20) IS present (ntfs_findvattr returns -1 with
lvap = the ATTRLIST ntvattr, ntfs_subr.c:133). The walk then reads the ATTRLIST data into a
heap buffer (alpool) and walks it looking for the attribute in other MFT records.
The mount-time trigger fires during ntfs_mountfs:
sys/vfs/ntfs/ntfs_vfsops.c:394-396 ntfs_mountfs() loops over {NTFS_MFTINO, NTFS_ROOTINO, NTFS_BITMAPINO} sys/vfs/ntfs/ntfs_vfsops.c:396 VFS_VGET(mp, NULL, NTFS_BITMAPINO, ...) sys/vfs/ntfs/ntfs_vfsops.c:796 .vfs_vget = ntfs_vget -> ntfs_vgetex(NTFS_A_DATA) sys/vfs/ntfs/ntfs_vfsops.c:745-746 ntfs_vgetex -> ntfs_filesize (for regular-file inos) sys/vfs/ntfs/ntfs_subr.c:1295 ntfs_filesize -> ntfs_ntvattrget(NTFS_A_DATA) sys/vfs/ntfs/ntfs_subr.c:175 ntfs_findvattr -> returns -1 (no inline $DATA; lvap=ATTRLIST) sys/vfs/ntfs/ntfs_subr.c:196-236 BUGGY ATTRLIST walk
The crafted image places a corrupted $ATTRIBUTE_LIST on ino 6 ($Bitmap) with no inline $DATA.
During mount, ntfs_filesize(ino 6) calls ntfs_ntvattrget(NTFS_A_DATA), which finds no inline
$DATA, enters the ATTRLIST walk, and hits the reclen==0 infinite loop. This fires during mount,
before any directory lookup, so the sibling DF-0786 lockmgr panic (which lives in ntfs_ntget on the
directory-lookup path) cannot interfere.
The threat model is the standard filesystem-image model: vfs.usermount=0 (verified) means the attacker
needs root to issue mount_ntfs (e.g. an admin auto-mounting an untrusted USB stick / image, or a
crafted image placed where root will mount it). The post-mount attribute request that reaches
ntfs_ntvattrget is unprivileged (any local user with execute on the mountpoint).
Reproduction β live kernel (default GENERIC #0)
Trigger (as root):
vnconfig -c vn0 /root/ntfs_0790_mount_loop.img mount_ntfs -o ro /dev/vn0 /mnt/ntfs
Result: kernel HANG. mount(2) never returns; the ssh command timed out after 30s (rc=124);
the guest became completely unresponsive (subsequent ssh status check also timed out, rc=124) β
the kernel is spinning at 100% CPU in the ntfs_ntvattrget ATTRLIST walk. No panic signature
(this is CWE-835 infinite loop, not a crash); the serial log shows a clean boot followed by the mount
command hanging forever. Confirmed local DoS.
Reproduction β userspace harness (deterministic complement)
harness.c replicates the exact ATTRLIST walk (ntfs_subr.c:196-236) against a buffer 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 (ATTRLIST exhausted) mode=loop apply_fix=0 -> rc=1 ITERATION CAP HIT -> would be infinite loop in kernel mode=null apply_fix=0 -> rc=2 SIGSEGV -> NULL deref / OOB read in kernel mode=oob_reclen apply_fix=0 -> rc=2 SIGSEGV -> NULL deref / OOB read in kernel === FIXED walk (proposed fix rejects malformed entries) === mode=clean apply_fix=1 -> rc=0 clean exit (ATTRLIST exhausted) mode=loop apply_fix=1 -> rc=-1 FIX REJECTED malformed entry (EINVAL in kernel) mode=null apply_fix=1 -> rc=-1 FIX REJECTED malformed entry (EINVAL in kernel) mode=oob_reclen apply_fix=1 -> rc=-1 FIX REJECTED malformed entry (EINVAL in kernel)
loopconfirms the infinite-loop branch (reclen==0, caught by iteration cap that the kernel does NOT have).nullconfirms the NULL-deref branch (reclen>len β size_t underflow β nextaalp==NULL β SIGSEGV).oob_reclenconfirms a bonus OOB-read branch (reclen pushes next ptr past buffer end β guard-page fault).
Escalation assessment (no chain possible β loop/NULL-deref only)
This is a control-flow primitive (infinite loop) or a NULL deref (panic). There is no write to
attacker-chosen kernel memory: the only effect is hanging the calling thread or crashing the kernel.
Per Phase 6, a pure DoS/panic primitive has no escalation chain to uid=0; the correct deliverable is
the characterized impact ceiling: definite local DoS (kernel hang requiring a reset, or NULL-deref panic).
Fix β fix.diff
Adds a bounds check at the top of the loop body, BEFORE any field dereference or walk advancement:
- Require the fixed entry header to fit in the remaining
len(len >= sizeof(struct attr_attrlist) - sizeof(u_int16_t), i.e. 26 bytes β the documented minimum ATTRLIST entry size, excluding the flexibleal_name[]tail). - Require
aalp->reclento be sane: at least the header size, and not greater thanlen. - On violation: set
error = EINVAL,kprintffor diagnostics,goto out(clean return;alpoolfreed).
This rejects both malformed shapes:
- reclen == 0 β caught by aalp->reclen < min_header β EINVAL (no infinite loop).
- reclen > len β caught by aalp->reclen > len β EINVAL (no size_t underflow, no NULL deref).
The fix is minimal and targeted at the root cause (the missing reclen bounds check). It does not
change the on-disk format, the NTFS_AALPCMP logic, the ntfs_vgetex path, or the happy path for
valid images.
Fix validation (Phase 8) β VALIDATED
ntfs.ko was rebuilt standalone (KERNCONF=X86_64_GENERIC make in sys/vfs/ntfs/, warm obj) and
installed to /boot/kernel/ntfs.ko. NTFS is optional ntfs (a loadable module, not compiled into
GENERIC), so only the module needed rebuilding β no kernel rebuild or reboot required.
Before (unpatched ntfs.ko, kernel #0)
mount_ntfs -o ro /dev/vn0 /mnt/ntfs # ntfs_0790_mount_loop.img -> mount(2) NEVER RETURNS. ssh timed out after 30s (rc=124). Guest completely wedged; kernel spinning in ntfs_ntvattrget.
After (patched ntfs.ko, same kernel #0)
mount_ntfs -o ro /dev/vn0 /mnt/ntfs # ntfs_0790_mount_loop.img mount_ntfs: /dev/vn0: Invalid argument MOUNT_RC=71 (<1s, guest UP) dmesg: ntfs_ntvattrget: malformed attrlist entry (reclen 0, len 26) mount_ntfs -o ro /dev/vn2 /mnt/ntfs2 # ntfs_0790_mount_null.img mount_ntfs: /dev/vn2: Invalid argument MOUNT_RC=71 (<1s, guest UP) dmesg: ntfs_ntvattrget: malformed attrlist entry (reclen 32, len 26)
Regression check (clean NTFS image still mounts)
mount_ntfs -o ro /dev/vn1 /mnt/ntfs # clean DF-0786 ntfs.img -> MOUNT_RC=0 /dev/vn1 on /mnt/ntfs (ntfs, read-only, local)
The fix closes the bug on the default GENERIC kernel without regressing valid images.
PoC changes
harness.cβ userspace replication ofntfs_ntvattrget:196-236against a guard-page-backed ATTRLIST buffer. Acceptsclean|loop|null|oob_reclenmode and an optionalapply_fixflag that runs the proposed fixed walker for direct before/after comparison.gen_ntfs_0790.pyβ crafted-image generator building on the proven DF-0786 NTFS scaffolding. Two trigger variants:- ino-5 (root dir) variant:
loop/nullmodes corrupt the root dir's$ATTRIBUTE_LIST. - mount-time variant:
mount_loop/mount_nullplace the corrupted$ATTRIBUTE_LISTon ino 6 ($Bitmap) with no inline$DATA, so the bug fires DURING mount viantfs_filesizeβ before the sibling DF-0786 lockmgr panic can interfere. build.sh/run.shβ exact reproducible build & run.run.shruns the harness (as maxx); the live mount trigger requires root and is documented inhang_evidence.txt.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)
scp -F dfbsd-qemu/config -q findings/poc/DF-0790/{harness.c,build.sh,run.sh} dfbsd-maxx:poc/DF-0790/
ssh -F dfbsd-qemu/config dfbsd-maxx 'cd poc/DF-0790 && sh build.sh && sh run.sh'
# 2. live kernel reproduction (root, EXPECTS HANG β guest dies, must reset after)
python3 findings/poc/DF-0790/gen_ntfs_0790.py mount_loop /tmp/ntfs_0790_mount_loop.img
scp -F dfbsd-qemu/config -q /tmp/ntfs_0790_mount_loop.img dfbsd:/root/
./dfbsd-qemu/vm.sh run_root 'vnconfig -c vn0 /root/ntfs_0790_mount_loop.img && mount_ntfs -o ro /dev/vn0 /mnt/ntfs'
# ^HANGS FOREVER β guest wedged
./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-0790/fix.diff dfbsd:/root/
./dfbsd-qemu/vm.sh run_root 'cd /usr/src && patch -p1 < /root/fix.diff && cd sys/vfs/ntfs && KERNCONF=X86_64_GENERIC make && cp ntfs.ko /boot/kernel/ntfs.ko'
# re-run step 2; mount now fails cleanly with EINVAL, guest stays UP
Fix verification
fixedVALIDATED the fix: ntfs_0790_mount_loop.img on the unpatched ntfs.ko (kernel #0) hangs the kernel forever during mount (mount(2) never returns, ssh rc=124, guest wedged -- the 'before' marker). On the single-fix ntfs.ko (same kernel #0, only the module rebuilt), the SAME mount fails cleanly with 'Invalid argument' (EINVAL, MOUNT_RC=71) in <1s and the guest stays fully responsive; dmesg shows 'ntfs_ntvattrget: malformed attrlist entry (reclen 0, len 26)' proving the fix's check fired and rejected the entry before the walk could loop. The null variant (reclen>len) is likewise rejected ('reclen 32, len 26'). A regression check with a clean NTFS image confirms it still mounts (MOUNT_RC=0). => fix closes the bug without regression.
BEFORE (unpatched ntfs.ko): mount_ntfs -o ro /dev/vn0 /mnt/ntfs -> mount(2) NEVER RETURNS, ssh rc=124, guest WEDGED (infinite loop). AFTER (patched ntfs.ko): mount_ntfs -o ro /dev/vn0 /mnt/ntfs -> 'Invalid argument' MOUNT_RC=71 in <1s, guest UP; dmesg: ntfs_ntvattrget: malformed attrlist entry (reclen 0, len 26). REGRESSION: clean image mount_ntfs -> MOUNT_RC=0 (mounts fine).
Confirmed kernel references
Detail
Exploit chain
none -- pure DoS/control-flow primitive (infinite loop or NULL deref). No write to attacker-chosen kernel memory; the only effect is hanging the calling thread or crashing the kernel. Per Phase 6, a pure loop/NULL-deref primitive has no escalation chain to uid=0; the realistic impact ceiling is local DoS (kernel hang requiring reset, or NULL-deref panic). The threat model is the standard filesystem-image model: root mounts a crafted NTFS image (admin auto-mounting an untrusted USB stick / image); the post-mount attribute lookup is unprivileged.
Evidence (decisive lines)
LIVE (unpatched #0): mount_ntfs -o ro /dev/vn0 /mnt/ntfs (ntfs_0790_mount_loop.img) -> mount(2) NEVER RETURNS, ssh timed out rc=124, guest wedged (status check rc=124), kernel spinning 100% CPU in ntfs_ntvattrget. No panic (CWE-835 infinite loop). HARNESS: mode=loop apply_fix=0 -> rc=1 ITERATION CAP HIT (would be infinite loop in kernel); mode=null apply_fix=0 -> rc=2 SIGSEGV (NULL deref in kernel); mode=clean -> rc=0. PATCHED: mount returns 'Invalid argument' (MOUNT_RC=71) in <1s, guest UP; dmesg: 'ntfs_ntvattrget: malformed attrlist entry (reclen 0, len 26)'.
PoC changes
Created the full evidence pack from scratch (finding had no prior PoC dir): harness.c (deterministic userspace replication of the ATTRLIST walk with guard-page proof for reclen==0 loop + reclen>len NULL deref + oob_reclen, with an apply_fix mode for direct before/after comparison); gen_ntfs_0790.py (crafted NTFS image generator with two trigger variants -- ino-5 root-dir and mount-time ino-6 $Bitmap -- the latter fires DURING mount before the sibling DF-0786 lockmgr panic); build.sh/run.sh; fix.diff (validated fix); VERDICT.md, README.md, manifest.json, full logs.
Verified recommended fix
In ntfs_ntvattrget's ATTRLIST walk loop (ntfs_subr.c:196), add a bounds check at the top of the loop body BEFORE any field dereference: require len >= sizeof(struct attr_attrlist)-sizeof(u_int16_t) (the 26-byte fixed header fits), aalp->reclen >= same minimum, and aalp->reclen <= len; on violation set error=EINVAL, kprintf for diagnostics, goto out (clean return, alpool freed). This rejects reclen==0 (infinite loop) and reclen>len (size_t underflow -> NULL deref) at the root cause. The fix is minimal, targeted, and does not regress valid images (clean NTFS image still mounts: MOUNT_RC=0). Matches the finding proposal's intent; authored as a single git-apply-able hunk in findings/poc/DF-0790/fix.diff.
Verdict
REPRODUCED. The bug is real: ntfs_ntvattrget (sys/vfs/ntfs/ntfs_subr.c:196-236) walks the on-disk $ATTRIBUTE_LIST advancing by aalp->reclen at line 207 with NO check that reclen != 0 or reclen <= len. A crafted NTFS image with a zero-length ATTRLIST entry (reclen==0) makes len -= 0 (unchanged) and NTFS_NEXTREC(aalp) == aalp (no advance) -> infinite loop -> kernel hang (CWE-835). A reclen>len entry makes the else-branch set nextaalp=NULL, then len -= reclen underflows size_t to ~2^64, and the next iteration dereferences aalp==NULL -> panic (CWE-476). Confirmed LIVE on default GENERIC #0: mounting ntfs_0790_mount_loop.img (corrupted $ATTRIBUTE_LIST on ino 6/$Bitmap, no inline $DATA) hangs the kernel during mount -- mount(2) never returns, guest wedged at 100% CPU (ssh timed out rc=124, status check rc=124). The bug fires DURING mount via ntfs_mountfs:396 -> VFS_VGET(BITMAPINO) -> ntfs_vgetex:745 -> ntfs_filesize -> ntfs_ntvattrget($DATA) -> ATTRLIST walk, BEFORE the sibling DF-0786 lockmgr panic can interfere. The deterministic userspace harness (harness.c) replicates the exact walk with a guard-page-backed buffer and confirms all three malformed shapes (loop/null/oob_reclen) deterministically.
No comments yet.