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

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)

No comments yet.

PoC verification

Evidence pack

findings/poc/DF-5284 Β· 22 files
FileTypeDescriptionSize
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.pagecode byte (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 matches SA_DEVICE_CONFIGURATION_PAGE (0x10) or SA_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
  1. kldload evilsa.ko β€” fake T_SEQUENTIAL target attaches as sa0.
  2. poc_comp /dev/nsa0 (pinned with usched :1): * opens the tape (mount with a valid compression page), * flips debug.evilsa.nocomp=1: MODE SENSE pages 0x0f/0x10 now fail with CAM_REQ_INVALID (β†’ EINVAL β†’ the fallback that sets NOCOMP), * debug.evilsa.spray16=1: allocates 4096 x 16B chunks, stamps the kernel pointer &ev_lock at [8..15], and frees the chunks whose addresses end in 0x10/0x90 last, so the chunk returned by the next kmalloc(16) (= ccomp) has freelist-linkage byte0 = 0x10 β€” steering the uninitialized switch into the branch that transmits, * issues MTCOMP (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.

VERDICT.md
↓ download raw

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)

  1. MTCOMP ioctl passes the quirk gate (scsi_sa.c:1261 β€” neither SA_QUIRK_NOCOMP nor SA_FLAG_COMP_UNSUPP set yet).
  2. sasetparams(SA_PARAM_COMPRESSION) kmallocs ccomp without M_ZERO (scsi_sa.c:2757) and immediately calls sagetparams() to fill it.
  3. evilsa rejects MODE SENSE pages 0x0f and 0x10 with CAM_REQ_INVALID; saerror()β†’cam_periph_error() maps that to EINVAL, so sagetparams() walks its fallback (scsi_sa.c:2593-2607): page 0x0f β†’ retry page 0x10 β†’ retry again after setting SA_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. ccomp still holds whatever was in the recycled heap chunk.
  4. bcopy(ccomp, cpage, 16) copies the stale bytes into the MODE SELECT page (scsi_sa.c:2796), the driver branches on the stale ccomp->hdr.pagecode byte (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 ccomp chunks 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

fixed
baseline reproduced→ patch + rebuild →patched clean

Same-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
↓ fix.diffper-fix-DF-5284

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.