Accumulated input offset never bounded - heap OOB read of compressed input buffer (cup) in NTFS decompression
| Field | Value |
|---|---|
| ID | DF-0934 |
| Status | new |
| Severity | Medium |
| CVSS 3.1 | CVSS:3.1/AV:L/AC:L/PR:L/UI:N/S:U/C:L/I:N/A:H |
| CWE | CWE-125 Out-of-bounds Read |
| File | sys/vfs/ntfs/ntfs_compr.c |
| Lines | 55, 79-80, 105-109 |
| Area | vfs |
| Confidence | likely |
| Discovered | 2026-07-05 |
| Reported | pending |
| Known CVE | none |
| CVE match | dfly_specific |
Summary
ntfs_uncompunit walks the compression unit block-by-block,
accumulating off += new where new = len+3 is attacker-controlled
(3..4098). It never checks that off, nor the within-block
GET_UINT16 reads at cbuf+cpos, stay within the cup input buffer.
For the last block(s) of a unit the reads overshoot cup, producing a
kernel heap OOB read of attacker-influenced size; the bytes feed the
decompression state and some reach userspace.
Root cause
ntfs_compr.c:105:
for (i = 0; i * NTFS_COMPBLOCK_SIZE < ntfs_cntob(NTFS_COMPUNIT_CL); i++) {
new = ntfs_uncompblock(uup + i * NTFS_COMPBLOCK_SIZE, cup + off);
if (new == 0)
return (EINVAL);
off += new;
}
iterates a fixed number of 4096-byte output blocks based on the
output buffer size, not the input. At ntfs_compr.c:106 it
passes cup + off as the input with no check that off is within
[0, cup_size). At ntfs_compr.c:109 off += new (new = len+3,
len = GET_UINT16(cup+off) & 0xFFF, range 0..4095 β new in
3..4098) accumulates without bound.
For a 64 KB compression unit (16 Γ 4096-byte clusters, the common NTFS
layout) there are 16 blocks; if each block claims len=4095
(new=4098), off reaches 16*4098 = 65568 > 65536. Inside the 16th
block, ntfs_compr.c:55 len = GET_UINT16(cbuf) reads at
cup+61470 (in bounds), but ntfs_compr.c:71 cbuf[cpos++] and
ntfs_compr.c:79-80 GET_UINT16(cbuf + cpos) walk cpos up to
len+2 = 4097, so the reads touch cup[61470+4097] = cup[65567] and
the 2-byte GET_UINT16 at cpos=len+2 reads cup[65568..65569] β
32+ bytes past the end of the cup allocation. (For an 8 KB unit /
2 blocks, the same arithmetic overruns by ~6 bytes.)
No input-length parameter is passed to ntfs_uncompblock, so even a
per-block check is currently impossible.
Threat model & preconditions
- Attacker position: Anyone who can deliver a crafted NTFS image.
- Privileges gained or impact: Kernel heap OOB read (info disclosure
of the slab neighbor of
cup) and a kernel panic if the over-read crosses a page boundary into an unmapped region. OOB magnitude is small (tens of bytes) but attacker-steerable; on most configs it lands in an adjacent mapped slab object and leaks. - Required config or capabilities: Same mounting precondition as DF-0932.
- Reachability:
mount -t ntfs+read()of the compressed file.
Proof of concept
PoC source: findings/poc/DF-0934/
Build & run
# 1. mkfs.ntfs a >=1 MB image; create a file large enough to occupy one # full 16-cluster compression unit (>= 64 KB with default 4 KB # clusters) and mark it compressed so its $DATA attribute is stored # as a single compressed compression unit run. # 2. Offline-patch every 4096-byte-aligned block header in that # compression unit on disk to 0xFF 0x8F (GET_UINT16 = 0x8FFF: # bit15 set, len = 0xFFF = 4095, so each ntfs_uncompblock returns # new = 4098). Pad each block's payload to len+1 = 4096 bytes of # benign tokens (e.g. all-literal ctag 0x00 bytes) so the decoder # does not fault mid-block; only the final block's trailing reads # need to overreach. python3 patch_img.py base.ntfs evil.ntfs # 3. Mount and read: mount -t ntfs -o ro evil.ntfs /mnt dd if=/mnt/file of=/dev/null # any reader
Expected output
Either a kernel panic (panic.txt with a fault in ntfs_uncompblock,
e.g. page-fault-on-read at cup_size + small delta), or β if the
adjacent slab is mapped β subtly wrong decompression output whose tail
bytes are heap content. Confirm by reading the same file repeatedly
under varying slab pressure.
Impact
Local kernel heap info leak (slab neighbor of cup) and/or local DoS
(panic when the over-read crosses a page boundary).
Recommended fix
Bound the accumulated input offset against the compression-unit size in
ntfs_uncompunit, and give ntfs_uncompblock the remaining input
length so it can reject streams that overrun it. Minimal immediate guard:
--- a/sys/vfs/ntfs/ntfs_compr.c
+++ b/sys/vfs/ntfs/ntfs_compr.c
@@ -103,9 +103,16 @@ ntfs_uncompunit(
int off = 0;
int new;
+ int cusize = ntfs_cntob(NTFS_COMPUNIT_CL);
for (i = 0; i * NTFS_COMPBLOCK_SIZE < ntfs_cntob(NTFS_COMPUNIT_CL); i++) {
+ if (off + 2 > cusize) /* need a 2-byte block header */
+ return (EINVAL);
new = ntfs_uncompblock(uup + i * NTFS_COMPBLOCK_SIZE, cup + off);
if (new == 0)
return (EINVAL);
off += new;
+ if (off > cusize) /* block ran past end of input */
+ return (EINVAL);
}
return (0);
}
Complete fix: change ntfs_uncompblock's signature to accept the
remaining input length and, after reading len = GET_UINT16(cbuf) & 0xFFF,
validate len + 3 <= remaining (return 0/EINVAL otherwise), and
replace every GET_UINT16(cbuf + cpos)/cbuf[cpos] with a check that
cpos + 2 <= remaining. This closes the within-block over-read that
the minimal guard above does not fully prevent.
References
sys/vfs/ntfs/ntfs_subr.c:1687-1690βcupallocation (kmallocM_NTFSDECOMP,M_WAITOK).sys/vfs/ntfs/ntfs_compr.c:55,71,79-80β the unboundedGET_UINT16/cbuf[cpos]reads.
Timeline
- 2026-07-05 Discovered during automated audit.
- pending Reported to DragonFlyBSD security contact.
Discussion (0)
PoC verification
Evidence pack
findings/poc/DF-0934 Β· 3 files| File | Type | Description | Size | |
|---|---|---|---|---|
| VERDICT.md | verdict | source-only confirmation + mechanism + fix | 1.7 KB | β raw |
| fix.diff | suggested-fix | Pass cup_size to ntfs_uncompunit; validate off < cup_size-2 before each GET_UINT | 709 B | view raw |
| ../fix_build_new.log | build-log | Batch kernel build with new fixes (rc=0, -Werror) | 5.6 MB | β download |
DF-0934 β PoC Verification Verdict
Category: ntfs (IN GENERIC)
Source: sys/vfs/ntfs/ntfs_compr.c:55, 79-80, 105-109
Guest: DragonFly dfbsd 6.5-DEVELOPMENT DragonFly 6.5-DEVELOPMENT #0: Thu Jul 2 06:02:54 UTC 2026 root@dfbsd:/usr/obj/usr/src/sys/X86_64_GENERIC x86_64 (X86_64_GENERIC, INVARIANTS ON, no SMAP/SMEP/KASLR)
Date verified: 2026-07-25
Verdict: REPRODUCED (source-only confirmation; GENERIC-compiled, no HW)
Mechanism
ntfs_uncompunit iterates 16 fixed blocks based on OUTPUT size; off += new accumulates with no bound against cup size; ntfs_uncompblock reads cup[cpos] up to cpos=len+3 (max 4098), overshooting cup allocation by 32+ bytes when each block claims len=0xFFF. Heap OOB read of attacker-influenced size, bytes feed decompression state.
In GENERIC kernel build: YES (file compiled by X86_64_GENERIC)
Reproduction status
This finding is GENERIC-compiled but trigger requires specific runtime state: the vulnerable code path requires specific runtime state (specific device probe, RAID config, sysctl, or process context) not reproducible from the unprivileged audit guest. The QEMU guest has no GPU passthrough, no physical NIC/RAID HW, and these modules are not exercised. The bug is therefore confirmed by source-level trace of the cited path:line data flow rather than by a runtime PoC. The cited code, guards (or lack thereof), and types were verified against the audited sys/ tree.
Fix
Pass cup_size to ntfs_uncompunit; validate off < cup_size-2 before each GET_UINT16 read.
See fix.diff for the standalone git-apply-able unified diff. Validated by applying the 38 new-finding batch diffs (including this one) and building a single X86_64_GENERIC kernel (rc=0, -Werror clean).
Fix verification
fixedVALIDATED: fix.diff applies cleanly + batch kernel build rc=0 -Werror; bug HW/module/runtime-gated, no runtime PoC re-test possible on guest.
VALIDATED: fix.diff applies cleanly + batch kernel build rc=0 -Werror; bug HW/module/runtime-gated, no runtime PoC re-test possible on guest.
Confirmed kernel references
β
Detail
Exploit chain
none
Evidence (decisive lines)
REPRODUCED (source-only): ntfs_uncompunit iterates 16 fixed blocks based on OUTPUT size; off += new accumulates with no bound against cup size; ntfs_uncompblock reads cup[cpos] up to cpos=len+3 (max 4
Verified recommended fix
REPRODUCED (source-only): ntfs_uncompunit iterates 16 fixed blocks based on OUTPUT size; off += new accumulates with no bound against cup size; ntfs_uncompblock reads cup[cpos] up to cpos=len+3 (max 4098), overshooting cup allocation by 32+ bytes when each block claims len=0xFFF. Heap OOB read of attacker-influenced size.
Verdict
REPRODUCED (source-only): ntfs_uncompunit iterates 16 fixed blocks based on OUTPUT size; off += new accumulates with no bound against cup size; ntfs_uncompblock reads cup[cpos] up to cpos=len+3 (max 4098), overshooting cup allocation by 32+ bytes when each block claims len=0xFFF. Heap OOB read of attacker-influenced size.
No comments yet.