Heap info leak via resident attribute data offset in ntfs_attrtontvattr β a_dataoff past record boundary
Summary
ntfs_subr.c:561 vap->va_datap=kmalloc(a_r.a_datalen). :563 memcpy(va_datap,(caddr_t)rap+rap->a_r.a_dataoff,a_r.a_datalen). a_dataoff uint16 from disk NO validation against record boundary. rap points into kmalloc MFT record buffer (typically 1024). a_dataoff>remaining: memcpy reads past record into adjacent M_TEMP slab kernel heap. va_datap later exposed via ntfs_readntvattr_plain:1596 to user read. Same issue for a_nameoff :536 and non-resident a_nr.a_dataoff :553. Trigger: crafted NTFS image resident $DATA a_dataoff=0x0F00. Fix: validate a_dataoff+a_datalen <= record_remaining before memcpy.
Discussion (0)
PoC verification
Evidence pack
findings/poc/DF-0788 Β· 18 files| File | Type | Description | Size | |
|---|---|---|---|---|
| harness.c | trigger-source | Deterministic userspace harness mirroring ntfs_attrtontvattr resident memcpy with guard page; proves OOB read via SIGSEGV | 10.6 KB | view raw |
| gen_ntfs_0788.py | trigger-source | Crafted NTFS image generator; corrupts ino 5 $INDEX_ROOT a_r.a_dataoff=0x0F80 to read 152B past record | 6.4 KB | view raw |
| ntfs_0788.img | evidence | Pre-generated crafted NTFS image (256KB) | 256.0 KB | β download |
| ntfs_clean.img | evidence | Clean NTFS image for regression testing | 256.0 KB | β download |
| build.sh | build-script | Builds harness + generates image | 369 B | view raw |
| run.sh | run-script | Runs harness in all modes | 775 B | view raw |
| build.log | build-log | Harness build output (cc -O2, rc=0) | 66 B | view raw |
| run.log | run-log | Harness decisive run: clean rc=0, oob rc=2 (SIGSEGV), fix rc=1 | 1.0 KB | view raw |
| panic.txt | panic-signature | Live trigger: mount succeeds (OOB read), then readdir hits sibling DF-0786 lockmgr panic | 1.5 KB | view raw |
| baseline_mount.log | run-log | Unpatched #0 kernel: mount_ntfs crafted image succeeds (OOB read confirmed) | 148 B | view raw |
| fix_run.log | run-log | Before/after contrast: unpatched mount succeeds vs patched mount fails EINVAL | 1.4 KB | view raw |
| fix_build.log | build-log | Single-fix nativekernel build log (rc=0, 0 errors) | 5.6 MB | β download |
| fix.diff | suggested-fix | git-apply-able fix: validate a_nameoff/a_dataoff/a_datalen against a_hdr.reclen | 2.2 KB | view raw |
| env.txt | environment | uname, cc version, sysctls, kldstat | 353 B | view raw |
| VERDICT.md | verdict | Full analysis: mechanism, reachability, impact, fix validation | 7.8 KB | β raw |
| README.md | readme | How to reproduce | 2.5 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-0788 β Heap info leak via resident attribute data offset in ntfs_attrtontvattr
Severity: Medium | Status: reproduced | Impact: heap OOB read / info leak
Summary
ntfs_attrtontvattr() (sys/vfs/ntfs/ntfs_subr.c:557-564) trusts the on-disk
u_int16_t a_r.a_dataoff field without validation. A crafted NTFS image can
set this offset past the MFT record buffer boundary, causing memcpy to read
adjacent kernel heap into an attribute buffer later exposed to userspace.
Reproduce
Prerequisites
- DragonFlyBSD master DEV guest ( GENERIC kernel, INVARIANTS ON)
- Python 3 (host-side, for image generation)
- The guest needs
cc(installed by default)
Build & run the harness (deterministic proof, no root needed)
./build.sh # builds harness.c + generates ntfs_0788.img (needs python3)
./run.sh # runs the harness: clean / oob_dataoff / oob_dataoff apply_fix
If python3 is not on the host/guest, generate the image separately:
python3 gen_ntfs_0788.py ntfs_0788.img # on host
scp ntfs_0788.img dfbsd-maxx:poc/DF-0788/
Expected output (harness)
mode=clean -> rc=0 memcpy within bounds (clean) mode=oob_dataoff -> rc=2 SIGSEGV -> OOB READ past record (LEAK CONFIRMED) mode=oob_dataoff apply_fix -> rc=1 FIX REJECTED malformed attribute (EINVAL)
Live kernel trigger (requires root)
vnconfig -c vn0 /root/ntfs_0788.img
mount_ntfs -o ro /dev/vn0 /mnt/ntfs # UNPATCHED: succeeds (OOB read happens)
# PATCHED: "Invalid argument" (EINVAL)
Files
| File | Description |
|---|---|
harness.c |
Deterministic userspace harness mirroring ntfs_attrtontvattr resident memcpy with guard page |
gen_ntfs_0788.py |
Crafted NTFS image generator (corrupts ino 5 $INDEX_ROOT a_dataoff) |
ntfs_0788.img |
Pre-generated crafted image (256 KB) |
build.sh / run.sh |
Reproducible build/run scripts |
fix.diff |
git-apply-able fix: validate offsets against a_hdr.reclen |
VERDICT.md |
Full analysis: mechanism, reachability, impact, fix validation |
build.log |
Harness build output |
run.log |
Harness run output (decisive) |
panic.txt |
Live-trigger panic signature (DF-0786 sibling, not DF-0788) |
baseline_mount.log |
Unpatched #0 kernel: mount succeeds (OOB read) |
fix_run.log |
Patched #1 kernel: mount fails EINVAL |
fix_build.log |
Single-fix kernel + module build log |
env.txt |
Guest environment (uname, cc, sysctls) |
DF-0788 β Heap info leak via resident attribute data offset in ntfs_attrtontvattr
Verdict: REPRODUCED (info leak / OOB heap read); FIX VALIDATED
Status: reproduced
Impact: leak (heap OOB read; up to ~152 bytes past MFT record buffer per trigger;
deterministically proven via harness; live mount trigger confirms reachability)
Confidence: certain
Severity: Medium (mount-time attacker-image heap disclosure; bounded by record/allocation geometry)
Root cause (confirmed by source trace)
ntfs_attrtontvattr() in sys/vfs/ntfs/ntfs_subr.c converts a raw on-disk
NTFS attribute into an in-memory struct ntvattr. For resident attributes
(not stored in cluster runs), it trusts two attacker-controlled u_int16_t
fields from the crafted image without any bounds check:
/* sys/vfs/ntfs/ntfs_subr.c β resident path */
557: vap->va_datalen = rap->a_r.a_datalen; /* u16 from disk, NO bound check */
558: vap->va_allocated = rap->a_r.a_datalen;
...
561: vap->va_datap = kmalloc(vap->va_datalen, M_NTFSRDATA, M_WAITOK);
563: memcpy(vap->va_datap,
564: (caddr_t) rap + rap->a_r.a_dataoff, /* u16 from disk, NO bound check */
rap->a_r.a_datalen); /* u16 from disk, NO bound check */
rap is a struct attr * pointing into the MFT record buffer:
/* sys/vfs/ntfs/ntfs_subr.c:263 */
263: mfrp = kmalloc(ntfs_bntob(ntmp->ntm_bpmftrec), M_TEMP, M_WAITOK); /* 1024 or 4096 bytes */
The attribute walk in ntfs_loadntnode() (lines 305β320) calls
ntfs_attrtontvattr() for each attribute. Because a_r.a_dataoff is a raw
u_int16_t read from disk with zero validation, a crafted image can set
a_dataoff past the record buffer boundary. The memcpy at line 563 then
reads a_datalen bytes starting at rap + a_dataoff β past the mfrp
allocation into adjacent M_TEMP slab heap. The leaked bytes are stored in
vap->va_datap and are later exfiltrable to userspace via:
/* sys/vfs/ntfs/ntfs_subr.c:1594 β resident read path */
1594: uiomove(vap->va_datap + roff, rsize, uio); /* copies leaked heap to userspace */
The same class of unvalidated-offset bug also affects:
- a_hdr.a_nameoff (line 536): reads a_namelen wchars from rap + a_nameoff
- a_nr.a_dataoff (line 553): non-resident run data offset (DF-0789 territory)
Reachability (confirmed)
The vulnerable memcpy fires at mount time during ntfs_mountfs():
sys/vfs/ntfs/ntfs_vfsops.c:393-403 ntfs_mountfs() calls VFS_VGET() for NTFS_MFTINO(0), NTFS_ROOTINO(5), NTFS_BITMAPINO(6) in sequence. β ntfs_vgetex() β ntfs_loadntnode() β attribute walk (lines 305-320) β ntfs_attrtontvattr(ap) β DF-0788 OOB read fires here
DF-0788 fires inside ntfs_attrtontvattr() on the first attribute, before
the DF-0787 unbounded-walk issue (which lives in off += reclen on
subsequent iterations).
Threat model: root-mountable crafted NTFS image (admin mounts or makes
mountable a malicious image; vfs.usermount=0 by default). The OOB read
happens silently at mount time β no panic from the leak itself (adjacent slab
is mapped), leaking adjacent M_TEMP kernel heap into the attribute buffer.
Reproduction evidence
1. Deterministic userspace harness (harness.c)
Mirrors the exact ntfs_attrtontvattr resident memcpy logic with a guard page
(PROT_NONE) immediately after the record buffer. Any OOB read faults
deterministically (SIGSEGV), proving the primitive regardless of slab layout.
mode=clean a_dataoff=0x0018 a_datalen=32 -> rc=0 memcpy within bounds (clean) mode=oob_dataoff a_dataoff=0x03c8 a_datalen=64 -> rc=2 SIGSEGV -> OOB READ past record mode=oob_dataoff apply_fix -> rc=1 FIX REJECTED malformed attr (EINVAL)
2. Live mount trigger (crafted NTFS image)
gen_ntfs_0788.py builds an NTFS image where ino 5 (root dir) has a resident
$INDEX_ROOT attribute with a_r.a_dataoff = 0x0F80 (3968). Since
rap is at mfrp+72 and a_datalen=208, the read target is
mfrp[72+3968] + 208 = mfrp[4248] > 4096 β 152 bytes past the buffer.
Unpatched #0 kernel: mount_ntfs succeeds (rc=0) β the OOB memcpy
completes, reading 152 bytes of adjacent heap into vap->va_datap. The
subsequent ls /mnt/ntfs hits the sibling DF-0786 lockmgr panic
(ntfs_ntreaddir), which is a different bug in the readdir path β it does
NOT block the DF-0788 leak, which already fired during mount.
vn0: MBR magic not found; assume a COMPATIBILITY_SLICE (s0) panic: lockmgr: locking against itself <-- DF-0786 sibling, NOT DF-0788 ntfs_ntreaddir() at ntfs_ntreaddir+0x58 ntfs_readdir() at ntfs_readdir+0xee
Impact characterization
- Primitive: heap OOB read (info leak). Up to
a_datalenbytes (max 65535) read fromrap + a_dataoffinto a freshlykmalloc'd buffer. - Realistic ceiling: The leaked bytes land in
vap->va_datapand are exfiltrable to userspace viantfs_readntvattr_plain():1594for resident$DATAattributes. On this specific crafted image, the leaked bytes corrupted the$INDEX_ROOTdata, and the readdir path hit the DF-0786 sibling panic before producing userspace output β but the leak itself already completed during mount (mount rc=0 = memcpy succeeded). - Not an escalation primitive: This is a pure read-only OOB. No write, no
corruption of kernel state (the leaked bytes go into a userspace-bound
buffer). No
uid=0chain is applicable (read-only info leak class). - KASLR relevance: KASLR is OFF on this guest; but on a hardened system, this leak could disclose kernel heap pointers/contents to defeat KASLR or ASLR.
Fix
fix.diff validates all three unvalidated offsets (a_nameoff, resident
a_dataoff, non-resident a_dataoff) against the attribute's own declared
a_hdr.reclen before any dereference. If offset + length > reclen, the
function returns EINVAL and the caller (ntfs_loadntnode) breaks the
attribute walk and propagates the error, causing mount to fail cleanly.
Key checks added (see fix.diff):
- reclen < sizeof(struct attrhdr) β reject (malformed header)
- a_nameoff + a_namelen*sizeof(wchar) > reclen β reject
- a_nr.a_dataoff >= reclen β reject (non-resident)
- a_r.a_dataoff + a_r.a_datalen > reclen β reject (resident, the DF-0788 fix)
Fix validation (Phase 8)
| Kernel | ntfs.ko | Image | Result |
|---|---|---|---|
| #0 unpatched | original (Jun 29) | crafted ntfs_0788.img |
mount SUCCEEDS (rc=0) β OOB read happens |
| #1 patched | rebuilt (Jul 10) | crafted ntfs_0788.img |
mount FAILS Invalid argument (EINVAL rc=71) β OOB read prevented |
| #1 patched | rebuilt (Jul 10) | clean ntfs_clean.img |
mount SUCCEEDS (rc=0) β no regression on valid images |
The ntfs module (ntfs.ko) is loadable, not built into the kernel. The fix
was applied to /usr/src/sys/vfs/ntfs/ntfs_subr.c, the module rebuilt via
nativekernel (which builds modules), and installed at /boot/kernel/ntfs.ko.
Before/after contrast:
- Baseline (#0): mount_ntfs -o ro /dev/vn0 /mnt/ntfs β rc=0, mount active
(OOB read silently completed)
- Patched (#1): mount_ntfs -o ro /dev/vn0 /mnt/ntfs β rc=71
Invalid argument (ntfs_attrtontvattr rejected malformed attribute)
- Patched (#1) + clean image: rc=0 (valid images unaffected)
PoC changes
Authored from scratch (no prior PoC existed for DF-0788):
- harness.c β deterministic OOB-read proof mirroring ntfs_attrtontvattr
resident memcpy with guard page
- gen_ntfs_0788.py β crafted NTFS image generator (extends DF-0786's
gen_ntfs.py scaffolding, corrupts ino 5's $INDEX_ROOT a_dataoff)
- build.sh / run.sh β reproducible build/run scripts
- fix.diff β git-apply-able fix validating offsets against a_hdr.reclen
Fix verification
fixedVALIDATED the fix: on the unpatched #0 kernel, mount_ntfs of the crafted ntfs_0788.img (a_dataoff=0x0F80) SUCCEEDS (rc=0) -- the OOB memcpy completes, reading 152 bytes past the 4096-byte MFT record into adjacent heap. On the patched #1 kernel with the rebuilt ntfs.ko, the SAME mount FAILS with 'mount_ntfs: /dev/vn0: Invalid argument' (rc=71 EINVAL) -- ntfs_attrtontvattr now rejects the malformed attribute before the memcpy. A clean NTFS image still mounts successfully on the patched kernel (rc=0), confirming no regression on valid images. The fix closes the bug.
BEFORE (#0 unpatched): mount_ntfs -o ro /dev/vn0 /mnt/ntfs -> MOUNT_RC=0, '/dev/vn0 on /mnt/ntfs (ntfs, read-only, local)' = OOB read happened. AFTER (#1 patched): mount_ntfs -o ro /dev/vn0 /mnt/ntfs -> 'mount_ntfs: /dev/vn0: Invalid argument' MOUNT_RC=71 = OOB read prevented. REGRESSION (#1 patched + clean image): MOUNT_RC=0 = valid images unaffected.
Confirmed kernel references
Detail
Exploit chain
none -- this is a pure read-only heap OOB read / info leak (no write, no corruption of kernel state). The leaked bytes go into a userspace-bound buffer (vap->va_datap -> uiomove). No escalation chain is applicable for this vulnerability class. The primitive is characterized: up to a_datalen (max 65535) bytes read from rap+a_dataoff into a kmalloc'd buffer; on this crafted image, 152 bytes past the 4096-byte MFT record. KASLR is OFF on this guest but on a hardened system this could disclose kernel heap pointers/contents to defeat KASLR.
Evidence (decisive lines)
HARNESS (deterministic): mode=clean a_dataoff=0x0018 -> rc=0 (within bounds); mode=oob_dataoff a_dataoff=0x03c8 -> rc=2 SIGSEGV OOB READ past record into guard page (LEAK CONFIRMED); mode=oob_dataoff apply_fix -> rc=1 FIX REJECTED EINVAL. LIVE MOUNT (#0 unpatched): vnconfig -c vn0 ntfs_0788.img; mount_ntfs -o ro /dev/vn0 /mnt/ntfs -> MOUNT_RC=0 (mount succeeds = OOB memcpy completed = 152B adjacent heap read). Subsequent ls -> panic: lockmgr locking against myself at ntfs_ntreaddir (sibling DF-0786, NOT DF-0788; the leak already fired at mount).
PoC changes
Authored from scratch (no prior PoC existed). Created: harness.c (deterministic OOB-read proof mirroring ntfs_attrtontvattr resident memcpy with guard page -- clean->rc=0, oob->rc=2 SIGSEGV, apply_fix->rc=1 EINVAL); gen_ntfs_0788.py (crafted NTFS image generator extending DF-0786's gen_ntfs.py scaffolding, corrupts ino 5 $INDEX_ROOT a_r.a_dataoff=0x0F80 so rap[72]+3968+208=4248>4096, OOB by 152B); build.sh/run.sh (reproducible scripts); fix.diff (validates a_nameoff/a_dataoff/a_datalen against a_hdr.reclen).
Verified recommended fix
In ntfs_attrtontvattr() (sys/vfs/ntfs/ntfs_subr.c), validate all on-disk offsets against the attribute's declared a_hdr.reclen before dereference: (1) reject if reclen < sizeof(struct attrhdr); (2) for names: a_nameoff + a_namelen*sizeof(wchar) must be <= reclen; (3) for non-resident: a_nr.a_dataoff < reclen; (4) for resident: a_r.a_dataoff + a_r.a_datalen <= reclen. Return EINVAL on violation. This is a new fix authored from line-accurate source verification (no prior fix proposal existed in the finding markdown). The full git-apply-able diff is in findings/poc/DF-0788/fix.diff.
Verdict
REPRODUCED. The bug is real: ntfs_attrtontvattr() (sys/vfs/ntfs/ntfs_subr.c:563) does memcpy(vap->va_datap, (caddr_t)rap + rap->a_r.a_dataoff, rap->a_r.a_datalen) where both a_r.a_dataoff and a_r.a_datalen are raw u_int16_t values read from the crafted NTFS image with ZERO bounds validation, and rap points into the kmalloc'd MFT record buffer (mfrp, typically 1024 or 4096 bytes, line 263). A crafted image with a_dataoff past the record boundary reads adjacent M_TEMP slab heap into vap->va_datap, which is later exfiltrable to userspace via uiomove at line 1594. Confirmed by: (1) a deterministic userspace harness (harness.c) mirroring the exact memcpy logic with a guard page -- the buggy path SIGSEGVs (rc=2) proving the OOB read; (2) a live mount_ntfs of a crafted image (ntfs_0788.img, ino 5 $INDEX_ROOT a_dataoff=0x0F80) on the unpatched #0 kernel -- mount SUCCEEDS (rc=0), meaning the memcpy completed and read 152 bytes past the 4096-byte buffer. The subsequent readdir hit the sibling DF-0786 lockmgr panic, but the DF-0788 leak already fired during mount. No upstream validation of a_dataoff exists anywhere in sys/vfs/ntfs/.
No comments yet.