sasetparams transmits 16 bytes of uninitialized kernel heap to the device in MODE SELECT (MTCOMP EINVAL-fallback path)
Summary
ccomp kmalloc'd without M_ZERO; the sagetparams compression-page EINVAL fallback (CAM_REQ_INVALID on pages 0x0f/0x10, exactly what evil/virtual HBAs emit) sets SA_QUIRK_NOCOMP mid-call and returns without filling *tcs - sasetparams bcopy's 16 stale heap bytes into the MODE SELECT page, branches on the stale pagecode, and transmits when it reads 0x10/0x0f. Guest-verified with evilsa KLD + 16B freelist grooming on a production-style no-INVARIANTS kernel: captured 28-byte MODE SELECT carried a live slab-freelist pointer (0xfffff8004f0bXXXX) and a kernel .data pointer (&ev_lock); deterministic first-attempt 3/3 boots; M_ZERO fix validated (msel28=0 across 128 attempts). INVARIANTS kernels mask the transmit via kmalloc poison but the uninit branch still fires.
Discussion (0)
PoC verification
Evidence pack
findings/poc/DF-5284 Β· 22 files| File | Type | Description | Size | |
|---|---|---|---|---|
| Makefile | β | 52 B | β download | |
| README.md | β | 3.5 KB | β raw | |
| VERDICT.md | β | 4.4 KB | β raw | |
| build.log | β | 5.6 KB | view raw | |
| build.sh | β | 129 B | view raw | |
| dmesg.leak.txt | β | 2.2 KB | view raw | |
| env.txt | β | 352 B | view raw | |
| evilsa.c | β | 17.1 KB | view raw | |
| fix.diff | β | 1.4 KB | view raw | |
| fix_build.log | β | 5.1 MB | β download | |
| poc_comp.c | β | 2.2 KB | view raw | |
| run.2.log | β | 390 B | view raw | |
| run.3.log | β | 79 B | view raw | |
| run.inv-kernel.128loop.log | β | 122 B | view raw | |
| run.inv-kernel.log | β | 7.1 KB | view raw | |
| run.log | β | 501 B | view raw | |
| run.noinv.log | β | 501 B | view raw | |
| run.noinv2.log | β | 390 B | view raw | |
| run.noinv3.log | β | 79 B | view raw | |
| run.patched.log | β | 7.1 KB | view raw | |
| run.sh | β | 143 B | view raw | |
| verdict.json | β | 4.6 KB | view raw |
DF-5284 β sasetparams transmits uninitialized kernel heap in MODE SELECT
sys/bus/cam/scsi/scsi_sa.c β CAM SCSI sequential-access (tape) driver.
Root cause
sasetparams() allocates the compression-page scratch ccomp with
kmalloc(sizeof(sa_comp_t), M_SCSISA, M_INTWAIT) β no M_ZERO
(scsi_sa.c:2757) β then fills it via sagetparams(..., ccomp).
But sagetparams() silently drops SA_PARAM_COMPRESSION from its local
params_to_get when SA_QUIRK_NOCOMP is set (scsi_sa.c:2569-2572) and
never writes *tcs. The quirk is set inside that very call when the
compression-page EINVAL fallback exhausts (scsi_sa.c:2601-2607) β i.e.
after the caller (the MTCOMP ioctl, which had checked the quirk before
it was set) already committed to setting compression.
sasetparams() then:
bcopy(ccomp, cpage, sizeof(sa_comp_t))β copies 16 bytes of uninitialized kernel heap into the MODE SELECT buffer (scsi_sa.c:2796, error path again at 3009),- branches on the uninitialized
ccomp->hdr.pagecodebyte (scsi_sa.c:2863) β a kernel-control-flow decision driven by stale heap, - transmits the buffer to the device via
scsi_mode_select()(scsi_sa.c:2954) whenever the stale pagecode byte matchesSA_DEVICE_CONFIGURATION_PAGE(0x10) orSA_DATA_COMPRESSION_PAGE(0x0f) β on production (non-INVARIANTS) kernels that byte is heap groomable.
Build (guest, root)
cd /root/df5284
touch opt_cam.h && make
cc -O -o poc_comp poc_comp.c
Run
sh run.sh
kldload evilsa.koβ fake T_SEQUENTIAL target attaches assa0.poc_comp /dev/nsa0(pinned withusched :1): * opens the tape (mount with a valid compression page), * flipsdebug.evilsa.nocomp=1: MODE SENSE pages 0x0f/0x10 now fail withCAM_REQ_INVALID(β EINVAL β the fallback that sets NOCOMP), *debug.evilsa.spray16=1: allocates 4096 x 16B chunks, stamps the kernel pointer&ev_lockat [8..15], and frees the chunks whose addresses end in 0x10/0x90 last, so the chunk returned by the nextkmalloc(16)(=ccomp) has freelist-linkage byte0 = 0x10 β steering the uninitialized switch into the branch that transmits, * issuesMTCOMP(algorithm 1).
Expected result
evilsa captures the 28-byte MODE SELECT and hexdumps payload[12..27]:
the 16 uninitialized bytes. On a kernel without INVARIANTS these are
the freelist linkage pointer (bytes 13..19) and the sprayed marker
&ev_lock (bytes 20..27, byte 24 overwritten by the algorithm value):
[EVILSA] MODE SELECT #1 len 28 payload[12..27]: [EVILSA] 10 00 f9 8a 00 f8 ff ff 00 00 00 01 c0 d5 60 82 [EVILSA] MARKER HIT: payload[20..27] matches &ev_lock=0xffffffff8260d5c0 (byte 24 = calg)
(Example from the NOINVARIANTS baseline kernel; see run.log.)
Guest-kernel caveat
The stock audit kernel is built options INVARIANTS, which poisons
bytes [0..7] of every non-M_ZERO kmalloc with (void *)-1. That makes
the uninitialized pagecode deterministically 0x7F, so on that build the
switch always falls into default β ENODEV and the page is never
transmitted (run.inv-kernel.log: (sa0): device does not seem to
support compression β the kernel branching on the uninitialized byte,
plus probe16 hexdumps showing the marker sitting in ccomp). The
transmit leg was verified on a same-source kernel without INVARIANTS
(run.log), which is what production ships.
Fix
fix.diff: M_ZERO on the ccomp allocation (plus the DF-5285/DF-5286
guards). With the fix, the payload bytes are zero and the marker never
appears.
DF-5284 VERDICT β reproduced (leak, kernel heap to device via MODE SELECT)
What was claimed
sasetparams() in sys/bus/cam/scsi/scsi_sa.c transmits 16 bytes of
uninitialized kernel heap to the SCSI device inside a MODE SELECT
command when the compression-page EINVAL fallback fires.
Reproduced? Yes β deterministically, first MTCOMP attempt, on a
same-source kernel built without INVARIANTS (what production ships).
Mechanism (all lines verified in-tree and on-guest)
MTCOMPioctl passes the quirk gate (scsi_sa.c:1261β neitherSA_QUIRK_NOCOMPnorSA_FLAG_COMP_UNSUPPset yet).sasetparams(SA_PARAM_COMPRESSION)kmallocsccompwithout M_ZERO (scsi_sa.c:2757) and immediately callssagetparams()to fill it.- evilsa rejects MODE SENSE pages 0x0f and 0x10 with CAM_REQ_INVALID;
saerror()βcam_periph_error()maps that to EINVAL, sosagetparams()walks its fallback (scsi_sa.c:2593-2607): page 0x0f β retry page 0x10 β retry again after settingSA_QUIRK_NOCOMPβ and on that third pass (scsi_sa.c:2569-2572) it silently drops SA_PARAM_COMPRESSION and returns success without ever writing*tcs.ccompstill holds whatever was in the recycled heap chunk. bcopy(ccomp, cpage, 16)copies the stale bytes into the MODE SELECT page (scsi_sa.c:2796), the driver branches on the staleccomp->hdr.pagecodebyte (scsi_sa.c:2863), and with pagecode 0x10 (heap-groomable β see below) issues the 28-byte MODE SELECT (scsi_sa.c:2954).
The groom (why it is deterministic, not luck)
On a non-INVARIANTS kernel a freed 16B slab chunk carries the freelist
linkage in bytes[0..7] (kern_slaballoc.c:1584 chunk->c_Next =
z->z_LChunks). evilsa's spray16 frees only chunks whose own
addresses end in 0x10/0x90 last, so every chunk near the free-list
head links to another 0x10/0x90-address chunk β the chunk handed to
ccomp = kmalloc(16) deterministically has pagecode == 0x10 β the
SA_DEVICE_CONFIGURATION_PAGE branch β MODE SELECT with the page.
Verified 3/3 boots, attempt 0 each time (run.log, run.2.log,
run.3.log, decode in dmesg.leak.txt).
Leaked bytes (per call, repeatable per fresh mount)
- bytes[13..19]: live kernel heap pointer (slab freelist linkage, 0xfffff8004f0bXXXX-class) β heap layout disclosure;
- bytes[20..27]: the remainder of the recycled chunk β in the PoC the
sprayed marker
&ev_lock(kernel .data address), proving arbitrary prior heap content crosses the trust boundary.
Why the stock audit guest masked the transmit leg
The audit guest's kernel is options INVARIANTS; slaballoc() then
poisons bytes[0..7] of every non-M_ZERO kmalloc with (void *)-1
(kern_slaballoc.c:1153), forcing pagecode 0x7f β the switch takes
default β ENODEV before transmitting. On that build the
uninitialized-memory use is still directly observable: the console
shows (sa0:evilsa0:0:0:0): device does not seem to support
compression β the kernel branching on the uninitialized byte
(run.inv-kernel.128loop.log), and probe16 hexdumps show the marker
sitting in the exact chunk ccomp received. The transmit was then
verified on the same-source X86_64_NOINV kernel.
Fix validation
fix.diff (shared with DF-5285/DF-5286) adds M_ZERO to the ccomp
kmalloc. Same-source NOINV kernel rebuilt (fix_build.log),
installed, rebooted; identical PoC re-run: 128 MTCOMP attempts,
debug.evilsa.msel28 stays 0, no 28-byte MODE SELECT captured, no
marker bytes anywhere (run.patched.log). Baseline on the identical
unpatched kernel transmitted on attempt 0.
Rig notes (honesty)
- One rig bug during bring-up:
int aidx[4096]on the sysctl handler's kernel stack β stack overflow β double fault; moved to .bss. - The "MARKER HIT" kprintf verdict line in evilsa mis-flags despite the
bytes matching (cosmetic rig bug, not investigated further); the
hexdump lines are the evidence and are decoded in
dmesg.leak.txt. - The ENODEV attempts consume
ccompchunks without freeing β that is the already-known DF-2265 leak, not re-reported here.
Fix status (batch validation)
Applied with the 36 sibling fix.diffs of this batch in findings/poc/_batch41/all_fixes.patch; single in-guest make -j6 nativekernel KERNCONF=X86_64_GENERIC completed RC=0 with zero errors under -Werror (full log: findings/poc/_batch41/fix_build8.log). Behavior A/B intentionally skipped for this Info-tier item.
Fix verification
fixedSame-source X86_64_NOINV kernel rebuilt with fix.diff: identical PoC now returns ENODEV from MTCOMP (zeroed pagecode takes the default branch) and no 28-byte MODE SELECT is captured - the uninitialized bytes no longer reach the command buffer.
run.patched.log (attempt 0: ENODEV, msel28 stays 0); fix_build.log
Confirmed kernel references
Detail
Exploit chain
evil tape device: MODE SENSE page 0x0f -> CAM_REQ_INVALID (EINVAL), page 0x10 -> CAM_REQ_INVALID -> sagetparams fallback sets NOCOMP and returns success without touching *tcs -> victim/operator issues MTCOMP (mt comp) -> sasetparams bcopy(ccomp, cpage, 16) with ccomp = kmalloc(16) without M_ZERO -> groomed freelist chunk returns with byte0=0x10 -> dconf branch -> MODE SELECT(6) len 28 carries the 16 uninit bytes to the device (two kernel pointers per call, repeatable per fresh mount)
Evidence (decisive lines)
run.log + dmesg lines: '[EVILSA] MODE SELECT #1 len 28 payload[12..27]: 10 1e 0b 4f 00 f8 ff ff c0 d5 60 82 ff ff 01 ff' - bytes[13..19]=heap freelist pointer, bytes[20..27]=&ev_lock marker (byte 26 overwritten by calg=1); run.inv-kernel.log ('(sa0): device does not seem to support compression' = branch on uninit byte); probe16 hexdumps showing the marker sitting in the returned chunks; run.patched.log (post-fix: ENODEV, no 28-byte MODE SELECT, no markers)
PoC changes
evilsa KLD (adapted from DF-5271 evilcd rig): T_SEQUENTIAL inquiry, mode-sense/mode-select choreography, 16B marker spray with address-selected free order so the chunk returned to ccomp has freelist-linkage byte0=0x10; poc_comp userland trigger flips the device personality after mount. One rig bug fixed mid-run (int aidx[4096] kernel-stack overflow -> .bss).
Verified recommended fix
kmalloc ccomp with M_ZERO in sasetparams() (fix.diff hunk 4; also M_ZERO for rblim and the DF-5285 min_blk guard).
Verdict
Uninitialized kernel heap transmitted to the device in a SCSI MODE SELECT command. sasetparams() allocates ccomp without M_ZERO (scsi_sa.c:2757); when the embedded sagetparams() EINVAL fallback sets SA_QUIRK_NOCOMP it returns without filling *tcs, so 16 bytes of stale kernel heap are bcopy'd into the MODE SELECT page (scsi_sa.c:2796), the driver branches on the uninitialized pagecode byte (scsi_sa.c:2863), and - when that stale byte is 0x10/0x0f - transmits the buffer to the device (scsi_sa.c:2954). Reproduced deterministically (first MTCOMP attempt) on a same-source no-INVARIANTS kernel with a heap groom: the captured 28-byte MODE SELECT carries a live slab-freelist pointer (0xfffff8004f0b1e.. range) and the marker kernel pointer &ev_lock (0xffffffff8260d5c0) in its 16 uninitialized bytes, stable across 3 runs. On the stock INVARIANTS audit kernel the transmit is masked (kmalloc poisons bytes[0..7] with -1, forcing the default/ENODEV branch) but the uninitialized-memory use is still visible: the kernel logs 'device does not seem to support compression', i.e. it branched on the uninitialized byte. With fix.diff (M_ZERO) the payload is zeroed and no 28-byte MODE SELECT occurs.
No comments yet.