LZ77 back-reference offset not bounded to current output position - heap OOB read before output buffer in NTFS decompression
| Field | Value |
|---|---|
| ID | DF-0932 |
| Status | new |
| Severity | High |
| CVSS 3.1 | CVSS:3.1/AV:L/AC:L/PR:L/UI:N/S:U/C:H/I:N/A:H |
| CWE | CWE-125 Out-of-bounds Read |
| File | sys/vfs/ntfs/ntfs_compr.c |
| Lines | 74-84 |
| Area | vfs |
| Confidence | certain |
| Discovered | 2026-07-05 |
| Reported | pending |
| Known CVE | none |
| CVE match | dfly_specific |
Summary
ntfs_uncompblock computes the LZ77 displacement scaling (dshift/lmask)
from the current output position but never validates that the resulting
back-reference stays within already-produced output. With
attacker-controlled compressed data, the displacement can exceed pos,
so buf[pos+boff] reads before the start of the output buffer. The
leaked bytes are written into buf (uup) and returned to userspace
via the caller's uiomove/memcpy, yielding a kernel heap info leak;
on boundary page crossings it panics (local DoS).
Root cause
ntfs_compr.c:74-78 scales the offset/length split:
for (j = pos - 1, lmask = 0xFFF, dshift = 12; j >= 0x10; j >>= 1) {
dshift--;
lmask >>= 1;
}
The loop stops when (pos-1) >> k < 16, which permits offset bits
4+k and a maximum displacement of 2^(4+k). For positions in the
lower half of each doubling band (e.g. pos=17 β k=1 β max
displacement 32; pos=2049 β k=8 β max displacement 4096), the
maximum displacement is ~2Β·pos, i.e. strictly greater than pos.
ntfs_compr.c:79 then sets
boff = -1 - (GET_UINT16(cbuf + cpos) >> dshift) and ntfs_compr.c:81-84
copies:
for (j = 0; (j < blen) && (pos < NTFS_COMPBLOCK_SIZE); j++) {
buf[pos] = buf[pos + boff];
pos++;
}
with no check that pos + boff >= 0. Because the token is
attacker-controlled (cup is raw on-disk data, ntfs_subr.c:1696-1699),
the offset field can be set to its max so that pos + boff is negative.
- Simplest trigger: the very first token of block 0 β
pos=0,j=-1so the scaling loop does not run (dshift=12, max displacement 16); a token with the 4-bit offset field =0xFgivesboff=-16and readsbuf[-16..-1]. - For
pos~2049the underflow reaches ~2 KB beforebuf.
buf is uup + i*0x1000; for block i=0, buf==uup, so the
underflow reads heap memory preceding the uup allocation. The values
are copied into uup and the caller (ntfs_subr.c:1722-1725) ships
uup+off to the user. No bounds check exists anywhere on the path
(the only guard, if (new==0) at ntfs_compr.c:107, is dead because
ntfs_uncompblock returns len+3 >= 3).
Threat model & preconditions
- Attacker position: Anyone who can deliver a crafted NTFS image (USB, downloaded file, network share, removable media, NFS export of an NTFS volume).
- Privileges gained or impact: Deterministic kernel heap OOB read
of up to ~2 KB adjacent to the
uupallocation, contents returned to the reader (disclosure of neighboring slab objects β potentially credentials/keys), plus a realistic kernel-panic path if the underflow crosses into an unmapped page. - Required config or capabilities: NTFS filesystem mounted and the compressed file is read. Mounting requires root (or a setuid mount helper / automount / removable media / NFS export of an NTFS volume); once mounted, any local user permitted to read the file triggers decompression and the leak.
- Reachability:
mount -t ntfs+read()of the compressed file.
Proof of concept
PoC source: findings/poc/DF-0932/
Build & run
# 1. mkfs.ntfs a 1 MB image; create a small file; mark it compressed. # 2. Locate the file's non-resident $DATA attribute in the MFT and # overwrite the first 5 bytes of its compressed compression unit # on disk with: 0x02 0x80 0x01 0x00 0xF0 # 0x8002 = compressed block header, len=2 (payload 3 bytes) # 0x01 = ctag (bit0 set -> first token is a back-reference) # 0x00 0xF0 -> GET_UINT16=0xF000; at pos=0, dshift=12: # boff = -1 - (0xF000>>12) = -1 - 15 = -16 # blen = 3 + (0xF000 & 0xFFF) = 3 # -> reads buf[-16..-14] into buf[0..2] (leak) python3 patch_img.py base.ntfs evil.ntfs # 3. Mount and read: mount -t ntfs -o ro evil.ntfs /mnt dd if=/mnt/secret.bin of=/dev/null bs=4096 count=1 # any user
Expected output
The first 16 bytes returned by read() are kernel heap bytes that
preceded the uup allocation, not file content. Hex-dumping the read
buffer shows pointers/refcounts/data from neighboring slab objects.
Repeat with different read offsets and between other slab-consuming
syscalls to harvest varied heap content.
A kernel panic (page-fault on the underread page boundary) also confirms the bug.
Impact
- Reliable local kernel heap info leak (up to ~2 KB per read).
- Realistic local DoS via panic when the underread crosses an unmapped page boundary.
Recommended fix
Validate that every back-reference stays within already-produced output
before copying. Minimal, localized fix at ntfs_compr.c:79-81:
--- a/sys/vfs/ntfs/ntfs_compr.c
+++ b/sys/vfs/ntfs/ntfs_compr.c
@@ -78,6 +78,11 @@ ntfs_uncompblock(u_int8_t * buf, u_int8_t * cbuf)
lmask >>= 1;
}
boff = -1 - (GET_UINT16(cbuf + cpos) >> dshift);
blen = 3 + (GET_UINT16(cbuf + cpos) & lmask);
+ /* Back-reference must not reach before start of output. */
+ if (pos + boff < 0)
+ return (0);
for (j = 0; (j < blen) && (pos < NTFS_COMPBLOCK_SIZE); j++) {
buf[pos] = buf[pos + boff];
pos++;
Returning 0 makes ntfs_uncompunit (ntfs_compr.c:107-108) return
EINVAL, which ntfs_readattr propagates as a read error β the correct
outcome for a malformed compressed block.
References
sys/vfs/ntfs/ntfs_subr.c:1687-1690βuupallocated withkmalloc(M_WAITOK)(noM_ZERO).sys/vfs/ntfs/ntfs_subr.c:1696-1699, 1722-1725βuup+offshipped to the reader viauiomove/memcpy.sys/vfs/ntfs/ntfs_compr.c:107-108β the deadif (new == 0)guard.
Timeline
- 2026-07-05 Discovered during automated audit.
- pending Reported to DragonFlyBSD security contact.
Discussion (0)
PoC verification
Evidence pack
findings/poc/DF-0932 Β· 20 files| File | Type | Description | Size | |
|---|---|---|---|---|
| VERDICT.md | verdict | full narrative: bug, harness, live repro, fix, fix-validation before/after | 8.1 KB | β raw |
| README.md | readme | concise reproducer + expected output | 2.0 KB | β raw |
| harness.c | trigger-source | deterministic transcription of ntfs_uncompblock on a poisoned/sentinel buffer | 8.1 KB | view raw |
| craft_img.py | trigger-source | builds minimal mountable NTFS image with a compressed file whose LZNT1 trigger 02 80 01 FF FF drives pos+boff<0 | 17.4 KB | view raw |
| ntfs_evil.img | trigger-source | crafted NTFS image (524288 B); compressed file F at MFT record 32 | 512.0 KB | β download |
| build.sh | build-log | builds harness (guest) + ntfs_evil.img (host) | 538 B | view raw |
| run.sh | run-log | end-to-end: harness + mount + read as root/maxx | 1.1 KB | view raw |
| run.log | run-log | live in-kernel leak demonstration: kernel heap bytes returned to root and maxx | 1.9 KB | view raw |
| harness.log | run-log | harness output (variant 1 LEAK CONFIRMED; variant 2 SIGSEGV) | 1.4 KB | view raw |
| leak_sample.txt | leak-sample | raw leaked kernel heap bytes across runs (incl. kernel pointers and $UpCase table tail) | 2.4 KB | view raw |
| baseline_repro.log | run-log | baseline (#0) leak bytes (00 70 bd 00 08 ... c0 34 6a 00 08 ...) | 321 B | view raw |
| fix.diff | suggested-fix | git-apply-able: reject malformed back-ref with return 0 in ntfs_uncompblock | 1.2 KB | view raw |
| fix_build.log | build-log | patched ntfs.ko module build (rc=0) + sha256 | 572 B | view raw |
| fix_run.log | run-log | patched read returns EINVAL; before/after comparison | 1.6 KB | view raw |
| fix_run_maxx.log | run-log | patched read as unprivileged maxx: EINVAL, 0 bytes | 317 B | view raw |
| fix_baseline.log | run-log | baseline leak bytes (used for before/after) | 151 B | view raw |
| env.txt | environment | uname, cc, sysctl vfs.usermount=0, ntfs.ko sha256 | 549 B | view raw |
| manifest.json | verdict | this file | 4.3 KB | view 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-0932 β NTFS LZNT1 back-reference underflow (kernel heap info leak)
LZ77 back-reference offset is not bounded to the current output
position in ntfs_uncompblock. A crafted compressed NTFS file makes
buf[pos + boff] underflow buf and read up to ~2 KB of kernel heap
preceding the M_NTFSDECOMP uup allocation. The leaked bytes are
shipped to the reader via uiomove. Reachable by any reader of a
mounted NTFS compressed file.
Source
- Bug:
sys/vfs/ntfs/ntfs_compr.c:74-82 - Sink:
sys/vfs/ntfs/ntfs_subr.c:1723(uiomove(uup + off, tocopy, uio))
Trigger
A 5-byte LZNT1 block at the start of a compression unit:
0x02 0x80 0x01 0xFF 0xFF
header 0x8002 (compressed, len=2)
tag 0x01 (first sub-token = back-reference)
token 0xFFFF (LE) -> at pos=0 dshift=12 lmask=0xFFF:
boff = -1 - (0xFFFF>>12) = -16
blen = 3 + (0xFFFF & 0xFFF) = 4098
-> reads buf[-16..-1] (heap before uup) into buf[0..15]
Build & run
./build.sh # builds harness (guest) + ntfs_evil.img (host) ./run.sh # harness + live in-kernel reproduction
The live path expects the DragonFly guest up via dfbsd-qemu/vm.sh.
Expected
Deterministic harness (harness.c)
- Variant 1: preceding page = sentinel; output
buf[0..2]matches the sentinel tail. LEAK CONFIRMED. - Variant 2: preceding page PROT_NONE; SIGSEGV at
buf[-16].
Live kernel (#0 GENERIC, unpatched ntfs.ko)
cat /mnt/evil/Freturns 4096 bytes; the first 16 are kernel heap pointers (e.g.00 70 bd 00 08 00 00 00 c0 34 6a 00 08 00 00 00).- Reproducible as unprivileged user (uid 1001) when root mounts with
-o ro,-u=1001,-g=1001.
Patched (ntfs.ko with fix.diff)
cat /mnt/evil/FreturnsInvalid argument(EINVAL). 0 bytes.- Same for unprivileged maxx.
Fix
fix.diff: reject malformed back-references in ntfs_uncompblock:
if (pos + boff < 0)
return (0); /* ntfs_uncompunit maps new==0 to EINVAL */
See VERDICT.md for the full narrative.
DF-0932 β NTFS LZNT1 back-reference underflow (kernel heap info leak)
Verdict
REPRODUCED. Unprivileged kernel heap info leak reachable by any reader of a mounted NTFS compressed file. Fix validated.
The bug (line-accurate)
In sys/vfs/ntfs/ntfs_compr.c, the LZ77 back-reference decoder computes
a signed displacement boff from the compressed token stream and uses it
to index buf[] without checking that the resulting index stays within
the already-decompressed prefix:
/* ntfs_compr.c:74-78 -- scaling loop derives dshift/lmask from pos */
for (j = pos - 1, lmask = 0xFFF, dshift = 12;
j >= 0x10; j >>= 1) {
dshift--;
lmask >>= 1;
}
boff = -1 - (GET_UINT16(cbuf + cpos) >> dshift); /* :79 */
blen = 3 + (GET_UINT16(cbuf + cpos) & lmask); /* :80 */
for (j = 0; (j < blen) && (pos < NTFS_COMPBLOCK_SIZE); j++) {
buf[pos] = buf[pos + boff]; /* :82 -- BUG */
pos++;
}
At pos = 0..16 the scaling loop does not execute (j = pos-1 < 0x10),
so dshift stays at 12 and the maximum displacement is 16. A token of
0xF000..0xFFFF (top nibble = 0xF) yields boff = -1 - 15 = -16, and
the copy loop reads buf[pos + boff] = buf[-16..-1] β 16 bytes of
kernel heap memory preceding the uup (M_NTFSDECOMP) allocation.
The leaked bytes are written into uup[0..15] (and propagated through
the LZ77 sliding window when blen is large), then shipped to the
reader via uiomove(uup + off, tocopy, uio) at
sys/vfs/ntfs/ntfs_subr.c:1723.
At larger pos the same defect scales: e.g. at pos = 2049, dshift = 4
and a max-displacement token reads up to pos + boff = 2049 - 4096 = -2047
(about 2 KB underflow, matching the finding summary).
Reachability / threat model
ntfs_readattr (ntfs_subr.c:1677) takes the compression branch when
both va_compression and va_compressalg are set on the file's
non-resident $DATA attribute; both are attacker-controllable fields in
a crafted NTFS image. The decompression runs whenever the reader pulls
bytes whose compression unit has been only partially initialized
(init != 0 && init != COMPUNIT_CL).
The mount itself requires root (mount_ntfs is root-only; vfs.usermount
is OFF on this guest), but the read is a normal read(2) and works
identically for any user who can open the file. On the test guest, after
root mounts with -o ro,-u=1001,-g=1001 (a realistic admin-mount of an
attacker-supplied filesystem image, exactly the DF-0871/0873/0878
precedent), the unprivileged user maxx (uid 1001, not in wheel)
reproduces the leak byte-for-byte identically to root. This is therefore
an unprivileged kernel heap info leak (CWE-125), not a rootβkernel
hardening gap.
This is an info-leak (read) primitive, not corruption. There is no
privilege-escalation chain to derive from it directly β the impact
ceiling is disclosure of arbitrary kernel heap to a userland reader,
which can include struct ucred *, function pointers, and other
secrets useful for defeating KASLR / grooming a separate write-primitive.
Reproduction
Deterministic harness (harness.c)
Transcribes ntfs_uncompblock line-for-line. The output buffer is
placed at the start of a mapped page; the preceding page is either
filled with a recognisable 16-byte sentinel (variant 1) or marked
PROT_NONE (variant 2).
- Variant 1: the LZ77 token
0xF000atpos=0(boff=-16,blen=3) copies 3 bytes from the preceding page intobuf[0..2]. The bytes are visibly the sentinel tail, proving the read underflowed. - Variant 2: the same token with the preceding page
PROT_NONESIGSEGVs atbuf[-16], proving the dereference leaves the allocation entirely.
Output:
[harness] LEAK CONFIRMED: buf[0..2] == bytes from buf[-16..-14] [harness] SIGSEGV caught: buf[pos+boff] with pos=0, boff=-16
Live in-kernel reproduction (craft_img.py + mount + read)
craft_img.py builds a minimal mountable NTFS image whose root
directory has one normal file F (MFT record 32) whose non-resident
$DATA attribute is flagged compressed. The 16-cluster compression
unit is laid out as 1 allocated cluster (containing the 5-byte LZNT1
trigger 02 80 01 FF FF zero-padded to 4 KB) + 15 sparse clusters;
this gives init = 4096, forcing ntfs_readattr into the
ntfs_uncompunit branch.
The trigger 02 80 01 FF FF:
- header 0x8002 (compressed, len = 2 β block payload = 5 B)
- tag 0x01 (first sub-token is a back-reference)
- token 0xFFFF (LE): at pos=0, dshift=12, lmask=0xFFF,
boff = -16, blen = 4098 β reads buf[-16..-1] (16 B of heap
preceding uup) into buf[0..15], then the LZ77 sliding window
propagates the 16 leaked bytes across all of buf[0..4095].
Result on 6.5-DEVELOPMENT #0 GENERIC, after some heap-warming
activity (without it the slab neighbour happens to be a zero page):
$ cat /mnt/evil/F | head -c 16 | od -An -tx1 00 70 bd 00 08 00 00 00 c0 34 6a 00 08 00 00 00
Those are DragonFly kernel virtual addresses:
- 0x00000008_00bd7000
- 0x00000008_006a34c0
(Earlier in the session the same image leaked f8 ff f9 ff fa ff fb
ff fc ff fd ff fe ff ff ff β the tail of the $UpCase table the
kernel had loaded into RAM during mount. Either way: kernel heap.)
Both root and maxx (uid 1001) get identical bytes; the leak is
stable across reads within a session and varies with heap state.
run.log and leak_sample.txt hold the raw bytes.
Fix
fix.diff adds a single guard in ntfs_uncompblock before the
unchecked buf[pos + boff] dereference:
if (pos + boff < 0)
return (0);
ntfs_uncompunit already maps a new == 0 return from
ntfs_uncompblock to EINVAL (ntfs_compr.c:108), and
ntfs_uncompblock otherwise always returns len + 3 (>= 3), so
0 is an unambiguous error sentinel. The error propagates through
ntfs_readattr β ntfs_strategy β bread β ntfs_read β
read(2), which now returns EINVAL to the reader.
Fix validation
Built ntfs.ko standalone (the file is a KLD module, so no full
kernel rebuild is needed; the kernel itself does not contain ntfs
code). Applied fix.diff to the in-guest source, ran
make in /usr/src/sys/vfs/ntfs, copied the new ntfs.ko over
/boot/kernel/ntfs.ko, kldunload/kldload ntfs.
Before/after on the same malicious image, same heap-warming preface:
| State | First 16 B returned | RC |
|---|---|---|
| baseline (#0) | 00 70 bd 00 08 00 00 00 c0 34 6a 00 08 00 00 00 (kernel heap) |
0, 4096 B |
| patched (ntfs.ko) | cat: /mnt/evil/F: Invalid argument |
1, 0 B |
Identical result for the unprivileged maxx user. Fix is validated
(fix_status: fixed).
See fix_baseline.log, fix_run.log, fix_run_maxx.log,
fix_build.log, leak_sample.txt.
Files in this evidence pack
| File | Purpose |
|---|---|
harness.c |
deterministic transcription of ntfs_uncompblock (proves the underflow on a poisoned buffer) |
craft_img.py |
builds a mountable NTFS image whose compressed file F triggers the underflow |
ntfs_evil.img |
the crafted image (524288 B) |
build.sh |
builds the harness (guest) + image (host) |
run.sh |
end-to-end: harness + mount + read as root/maxx |
leak_sample.txt |
raw leaked kernel heap bytes across runs |
fix.diff |
git-apply-able one-line guard |
fix_build.log |
patched-ntfs.ko build log + sha256 |
fix_baseline.log |
baseline (#0) leak bytes |
fix_run.log |
patched read returns EINVAL, 0 bytes |
fix_run_maxx.log |
same as above as unprivileged maxx |
env.txt |
guest uname / cc / sysctls / module sha256 |
manifest.json |
machine-readable catalog |
Fix verification
fixedVALIDATED the fix: on the unpatched #0 GENERIC baseline (ntfs.ko sha256 aa8d83843b...), cat /mnt/evil/F returned 4096 B whose first 16 bytes were kernel heap pointers (00 70 bd 00 08 00 00 00 c0 34 6a 00 08 00 00 00) for both root and unprivileged maxx (RC=0). After applying fix.diff and rebuilding ntfs.ko (54431edf18...), the SAME image returns 'cat: /mnt/evil/F: Invalid argument' (EINVAL) for both root and maxx, RC=1, 0 bytes returned. The malformed back-reference is now rejected at ntfs_uncompblock via return 0, mapped to EINVAL by ntfs_uncompunit, and propagated to read(2). The leak is closed.
BEFORE (baseline #0 GENERIC, unpatched ntfs.ko): 'root read 1: 00 70 bd 00 08 00 00 00 c0 34 6a 00 08 00 00 00' (kernel heap, RC=0, 4096 B). AFTER (ntfs.ko with fix.diff): 'cat: /mnt/evil/F: Invalid argument' (RC=1, 0 B). Same for unprivileged maxx (uid 1001): before identical kernel heap leak; after EINVAL, 0 B.
Confirmed kernel references
- sys/vfs/ntfs/ntfs_compr.c:74
- sys/vfs/ntfs/ntfs_compr.c:75
- sys/vfs/ntfs/ntfs_compr.c:76
- sys/vfs/ntfs/ntfs_compr.c:77
- sys/vfs/ntfs/ntfs_compr.c:78
- sys/vfs/ntfs/ntfs_compr.c:79
- sys/vfs/ntfs/ntfs_compr.c:80
- sys/vfs/ntfs/ntfs_compr.c:81
- sys/vfs/ntfs/ntfs_compr.c:82
- sys/vfs/ntfs/ntfs_subr.c:1687
- sys/vfs/ntfs/ntfs_subr.c:1689
- sys/vfs/ntfs/ntfs_subr.c:1705
- sys/vfs/ntfs/ntfs_subr.c:1718
- sys/vfs/ntfs/ntfs_subr.c:1719
- sys/vfs/ntfs/ntfs_subr.c:1723
Detail
Exploit chain
none (pure info-leak / CWE-125 read primitive; no write, no corruption, no chain to derive). The bug reads up to 16 B of slab-neighbour heap per LZ77 token (and ~2 KB near pos~2049 per the finding); those bytes are shipped to any reader of a mounted NTFS compressed file. Realistic preconditions: an admin has mounted (or made mountable) an attacker-crafted NTFS image and mapped it to the user (-u/-g); the trigger is then a normal read(2) the unprivileged user issues. Same threat model as DF-0871/0873/0878 (admin-mount of attacker FS, ACCEPTABLE per the realistic-threat-model table).
Evidence (decisive lines)
harness: 'LEAK CONFIRMED: buf[0..2] == bytes from buf[-16..-14]' + 'SIGSEGV caught: buf[pos+boff] with pos=0, boff=-16'. live #0 GENERIC after heap warming: 'root read 1: 00 70 bd 00 08 00 00 00 c0 34 6a 00 08 00 00 00' (kernel heap pointers, stable across 5 reads). as maxx uid 1001: 'cat /mnt/evil/F | head -c 16 | od -An -tx1 -> 00 70 bd 00 08 00 00 00 c0 34 6a 00 08 00 00 00'. (earlier session also leaked f8 ff f9 ff fa ff fb ff fc ff rd ff fe ff ff ff, the $UpCase table tail from kernel RAM).
PoC changes
Created findings/poc/DF-0932/ from scratch. harness.c transcribes ntfs_uncompblock line-for-line on a poisoned buffer (variant 1: sentinel page -> LEAK CONFIRMED; variant 2: PROT_NONE page -> SIGSEGV). craft_img.py builds a complete mountable NTFS image (boot sector, $MFT/non-resident $DATA spanning 9 clusters, $AttrDef, root dir $INDEX_ROOT:$I30 with one normal file entry, $Bitmap, $UpCase) plus MFT record 32 = a compressed file whose non-resident $DATA has a_hdr.a_compression=1 + a_nr.a_compressalg=1 and a run list of 1 allocated + 15 sparse clusters (forcing init=4096 -> ntfs_uncompunit path); the allocated cluster holds the 5-byte LZNT1 trigger 02 80 01 FF FF zero-padded to 4 KB. build.sh / run.sh wrap the harness+image+mount+read sequence.
Verified recommended fix
In ntfs_uncompblock (sys/vfs/ntfs/ntfs_compr.c) before the inner copy at :82, add 'if (pos + boff < 0) return (0);'. ntfs_uncompunit already maps new==0 to EINVAL (ntfs_compr.c:108), and ntfs_uncompblock otherwise always returns len+3 (>=3), so 0 is an unambiguous error sentinel -- the error then propagates ntfs_readattr -> ntfs_strategy -> bread -> read(2) which returns EINVAL. The full git-apply-able diff is in findings/poc/DF-0932/fix.diff. Supersedes finding proposal (clamping boff so pos+boff>=0; returning 0/EINVAL is cleaner -- the malformed stream should be rejected, not silently repaired).
Verdict
REPRODUCED. The LZ77 back-reference decoder in ntfs_uncompblock (sys/vfs/ntfs/ntfs_compr.c:74-82) derives a signed displacement boff from the compressed token and indexes buf[pos+boff] with no check that pos+boff >= 0. At pos=0 the scaling loop (lines 74-78) does not execute so dshift stays 12; a token 0xF000..0xFFFF yields boff = -16 and the inner copy reads buf[-16..-1] -- 16 bytes of kernel heap preceding the M_NTFSDECOMP uup allocation. Those bytes ride uiomove(uup+off, tocopy, uio) at ntfs_subr.c:1723 straight to the reader. Confirmed two ways: (1) deterministic harness transcribing ntfs_uncompblock on a poisoned buffer shows buf[0..2] == sentinel bytes from buf[-16..-14] (LEAK CONFIRMED) and SIGSEGVs when the preceding page is PROT_NONE; (2) live on #0 GENERIC a crafted NTFS image with a compressed file F (LZNT1 trigger 02 80 01 FF FF) returns 4096 B to the reader whose first 16 bytes are kernel heap pointers (00 70 bd 00 08 00 00 00 c0 34 6a 00 08 00 00 00 = 0x0008_00bd_7000 / 0x0008_006a_34c0) -- identical for root and unprivileged maxx (uid 1001). This is an unprivileged kernel heap info leak (CWE-125), not corruption; impact ceiling is disclosure of kernel heap (useful for KASLR defeat / grooming a separate primitive), no escalation chain derivable.
No comments yet.