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

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)

No comments yet.

PoC verification

Evidence pack

findings/poc/DF-0790 Β· 17 files
FileTypeDescriptionSize
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
README.md readme how to reproduce
↓ download 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.
VERDICT.md verdict full narrative: mechanism, reachability, reproduction, fix validation
↓ download raw

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)
  • loop confirms the infinite-loop branch (reclen==0, caught by iteration cap that the kernel does NOT have).
  • null confirms the NULL-deref branch (reclen>len β†’ size_t underflow β†’ nextaalp==NULL β†’ SIGSEGV).
  • oob_reclen confirms 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 flexible al_name[] tail).
  • Require aalp->reclen to be sane: at least the header size, and not greater than len.
  • On violation: set error = EINVAL, kprintf for diagnostics, goto out (clean return; alpool freed).

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 of ntfs_ntvattrget:196-236 against a guard-page-backed ATTRLIST buffer. Accepts clean|loop|null|oob_reclen mode and an optional apply_fix flag 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/null modes corrupt the root dir's $ATTRIBUTE_LIST.
  • mount-time variant: mount_loop/mount_null place the corrupted $ATTRIBUTE_LIST on ino 6 ($Bitmap) with no inline $DATA, so the bug fires DURING mount via ntfs_filesize β€” before the sibling DF-0786 lockmgr panic can interfere.
  • build.sh / run.sh β€” exact reproducible build & run. run.sh runs the harness (as maxx); the live mount trigger requires root and is documented in hang_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

fixed
baseline reproduced→ patch + rebuild →patched clean

VALIDATED 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).
↓ fix.diffDragonFly 6.5-DEVELOPMENT #0: Thu Jul 2 06:02:54 UTC 2026 (same kernel #0; only /boot/kernel/ntfs.ko rebuilt standalone from patched sys/vfs/ntfs/ntfs_subr.c -- ntfs is 'optional ntfs', a loadable module not compiled into GENERIC; sha256 a3a5e498f73f4cbdbad3ee760499df7565d9ddc3f363f2e46e6d43a9966d7a02)

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.