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

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).

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

Timeline

  • 2026-07-05 Discovered during automated audit.
  • pending Reported to DragonFlyBSD security contact.

Discussion (0)

No comments yet.

PoC verification

Evidence pack

findings/poc/DF-0934 Β· 3 files
FileTypeDescriptionSize
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
VERDICT.md verdict source-only confirmation + mechanism + fix
↓ download raw

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

fixed
baseline reproduced→ patch + rebuild →patched clean

VALIDATED: 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.
↓ fix.diffcombined build rc=0

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.