chdone() CH_CCB_PROBE reads and prints uninitialized stack announce_buf[80] to console/dmesg when cherror() returns 0 for a non-CAM_REQ_CMP completion (SCSI INTERMED / RECOVERED ERROR)
Summary
announce_buf is initialized only in the CAM_REQ_CMP and error!=0 branches, but cam_periph_error returns 0 for INTERMED/COND_MET and RECOVERED-ERROR CHECK conditions - device-controlled status bytes reach neither branch: announce_buf[0] is read uninitialized and xpt_announce_periph prints the whole buffer (%s) into world-readable dmesg. Guest-verified with the rig (invalidate + chprobe rescan + SCSI status 0x10): 3 cycles printed 3 DIFFERENT stale-stack strings right after the marker while normal attaches print the counts line. Init-at-declaration fix.
Discussion (0)
PoC verification
Evidence pack
findings/poc/DF-5333 Β· 13 files| File | Type | Description | Size | |
|---|---|---|---|---|
| README.md | β | 2.2 KB | β raw | |
| VERDICT.md | β | 4.2 KB | β raw | |
| evilcd3.c | β | 16.1 KB | view raw | |
| build.sh | β | 85 B | view raw | |
| run.sh | β | 109 B | view raw | |
| build.log | β | 132 B | view raw | |
| run.log | β | 1.2 KB | view raw | |
| run.2.log | β | 676 B | view raw | |
| fixedA.log | β | 1.4 KB | view raw | |
| env.txt | β | 188 B | view raw | |
| fix.diff | β | 447 B | view raw | |
| manifest.json | β | 1.2 KB | view raw | |
| verdict.json | β | 4.0 KB | view raw |
DF-5333 evidence pack β chdone() CH_CCB_PROBE reads and prints uninitialized stack announce_buf[80] when cherror() returns 0 for a non-CAM_REQ_CMP completion
Finding (one line)
chdone() (sys/bus/cam/scsi/scsi_ch.c:541) declares
char announce_buf[80]; and only initializes it in the
CAM_REQ_CMP branch (ksnprintf, :562) and in the error != 0 branch
(announce_buf[0] = '\0', :636). A device that completes the probe
MODE SENSE with SCSI status INTERMED (or CHECK CONDITION with a
RECOVERED-ERROR sense, SS_NOP) makes cam_periph_error() return
0 while the CAM status is not CAM_REQ_CMP
(cam_periph.c:1313-1318 SCSI_STATUS_INTERMED => error = 0;
cam_periph.c:1510-1513 SS_NOP => error = 0), so neither branch
runs: announce_buf[0] is read uninitialized at scsi_ch.c:639 and,
when non-zero, the whole buffer is printed to the console with %s by
xpt_announce_periph() (cam_xpt.c:1761-1763) β a kernel-stack info
leak into the console/dmesg ring (up to 80 bytes, or further until a
NUL).
Reproduce (guest, root; EVIL kernel = GENERIC + device ch)
cd /root/df5333 ./build.sh && ./run.sh
evilcd3.ko(same rig as DF-5332) fakes the T_CHANGER.debug.evilcd.probeannounce=1paints the sysctl thread's stack with 0x42 (~4KB, viaev_paint_deep), arms the one-shot INTERMED MODE SENSE, and rescans the bus inline on the same thread, so the attach-time probe (chstart CH_STATE_PROBE -> chdone CH_CCB_PROBE) runs on the groomed stack.- The rig's XPT_GET_TRAN_SETTINGS handler returns success (required β
xpt_announce_periph()bails before printing the string otherwise).
Expected (vulnerable kernel)
Console/dmesg shows:
[EVILCH] MODE SENSE page 0x1d -> INTERMED (error==0 path) ch0 at evilch0 bus 0 target 1 lun 0 ch0: <EVILCH OOBTEST 1.0> ... ch0: BBBBBBBBBBBBBBBBBB... <-- uninitialized stack, printed with %s
(the exact garbage depends on the stale stack contents; with the paint
knob it is deterministic 0x42 if the probe executes on the groomed
thread). With the patched kernel (fix.diff: announce_buf[0] = '\0';
up front) no garbage line can appear β the driver announces nothing or
the normal counts string only.
DF-5333 VERDICT
Status: REPRODUCED (3/3 attach cycles, varying garbage each time) β fix validated
Root cause (source-accurate)
chdone() CH_CCB_PROBE (sys/bus/cam/scsi/scsi_ch.c):
- :541
char announce_buf[80];β stack buffer, uninitialized. - :549-573 success branch (
CAM_REQ_CMP) fills it with ksnprintf. - :574-637 error branch: if
cherror(...)returns non-zero, the path ends withannounce_buf[0] = '\0'(:636). - BUT
cherror()βcam_periph_error()returns 0 for some completions that are NOT CAM_REQ_CMP: - cam_periph.c:1313-1318 β
csio->scsi_statusin {SCSI_STATUS_OK, COND_MET, INTERMED, INTERMED_COND_MET} βerror = 0; - cam_periph.c:1509-1513 β CHECK CONDITION whose sense maps to
SS_NOP(e.g. RECOVERED ERROR) βerror = 0. A device completing the attach-time probe MODE SENSE with SCSI status INTERMED (0x10) (CAM sets CAM_SCSI_STATUS_ERROR) therefore lands in neither branch:announce_buf[0]is read uninitialized at :639 and, when non-zero, the buffer is printed to console/dmesg byxpt_announce_periph()βkprintf("%s%d: %s\n", ..., announce_string)(cam_xpt.c:1761-1763). %s walks until a NUL β potentially past the 80-byte buffer.
How it was reproduced (guest, EVIL kernel, evilcd3.ko rig)
chdone() executes on the CAM cambio SWI thread (xpt_done queues CCBs to
sim->sim_doneq, cam_xpt.c:4891-4920). Choreography (run.sh):
- normal attach β chdone's success path runs
ksnprintf(announce_buf, ...)on that thread (seeds the stack slot with live data); debug.evilcd.invalidate=1destroys the ch periph;debug.evilcd.chprobe=1(one-shot) +debug.evilcd.rescan=1β re-attach; the rig completes the probe MODE SENSE with CAM_SCSI_STATUS_ERROR + scsi_status 0x10 (INTERMED):[EVILCH] MODE SENSE page 0x1d -> INTERMED (error==0 path);- chdone takes the uninit path;
xpt_announce_periphprints whatever stale bytes occupyannounce_bufon the SWI thread.
Result (run.log / run.2.log β three independent cycles)
[EVILCH] MODE SENSE page 0x1d -> INTERMED (error==0 path) ch0 at evilch0 bus 0 target 1 lun 0 ch0: <EVILCH OOBTEST 1.0 > Removable Changer SCSI-2 device ch0: 3.300MB/s transfers ch0: x^NO <-- uninitialized stack printed with %s
run #2: ch0: M-0r^NO (bytes 0xB0 0x72 0x8E 0x4E 0x4F),
run #3: ch0: M-^[BM^M(bytes 0xC0 0x60 0x1B 0x42 0x0D) β
different stale kernel-stack bytes each cycle, high-bit bytes consistent
with pointer/metadata residue from prior completions on the cambio
thread. A normal attach in between prints the legitimatech0: 8 slots, 0 drives, 1 picker, 0 portals` line β the INTERMED attach
prints garbage instead, proving the ksnprintf never ran.
Impact
Uninitialized kernel-stack disclosure to the console/dmesg ring (up to 80
bytes, possibly further until a NUL). dmesg is world-readable on the
standard DragonFly configuration (dmesg(8) reads the msg buffer without
privilege), so any local user can read the leaked bytes; content is
residue of the cambio SWI thread's prior work (other devices' completion
paths β sense buffers, pointers). Low severity (bounded size, console
only), but it is a genuine CWE-457 read-and-print of uninitialized stack
triggered by device-controlled status bytes.
Fix validation (done)
fix.diff adds announce_buf[0] = '\0'; immediately after the
declaration. Guest /usr/src patched (together with DF-5332's M_ZERO fix,
same rebuilt kernel), rebooted, exact same choreography rerun
(fixedA.log): the INTERMED attach announces the device but prints no
garbage ch0: <bytes> line β the branch cannot fire on uninitialized
data anymore.
Guest
DragonFly dfbsd 6.5-DEVELOPMENT #0: Fri Oct 2 22:55:55 UTC 2026
root@dfbsd:/usr/obj/usr/src/sys/EVIL x86_64 (GENERIC + device ch);
gcc (DragonFly) 8.3; evilcd3.ko rig.
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
fixedApplied fix.diff (announce_buf[0]='\0' up front, same rebuilt kernel as DF-5332's fix) and re-ran the exact same choreography 3 times on the patched kernel: every INTERMED attach is followed by NO announce block and NO garbage 'ch0:
findings/poc/DF-5333/fixedA.log (patched run: INTERMED marker is the last line, no announce after); findings/poc/DF-5333/run.log + run.2.log (vulnerable baseline with three distinct garbage strings)
Confirmed kernel references
Detail
Evidence (decisive lines)
run.log line 'ch0: x^NO' immediately after '[EVILCH] MODE SENSE page 0x1d -> INTERMED (error==0 path)'; run.2.log shows two further cycles with 'ch0: M-0r^NO' and 'ch0: M-`^[BM^M' (cat -v); fixedA.log (patched kernel) shows the INTERMED marker followed by NO announce block and NO garbage line across 3 cycles
PoC changes
Derived rig from DF-5327's evilcd2.c; added one-shot INTERMED MODE SENSE knob, invalidate knob (needed to destroy the periph so a rescan re-runs the attach probe), and made XPT_GET_TRAN_SETTINGS succeed (xpt_announce_periph bails before printing the string otherwise). Stack-paint approach abandoned after confirming chdone runs on the cambio SWI thread (xpt_done queues to sim_doneq, cam_xpt.c:4891); replaced with normal-attach-then-INTERMED-attach choreography on the same completion thread.
Verified recommended fix
Initialize announce_buf[0]='\0' at declaration in chdone()'s CH_CCB_PROBE case (scsi_ch.c:541-543)
Verdict
chdone()'s CH_CCB_PROBE handler declares char announce_buf[80] uninitialized and only fills it in the CAM_REQ_CMP branch or sets announce_buf[0]='\0' in the error!=0 branch; cam_periph_error() returns 0 for SCSI-status INTERMED (cam_periph.c:1313-1318) and for RECOVERED-ERROR sense (SS_NOP, cam_periph.c:1509-1513) even though the CCB is not CAM_REQ_CMP, so a changer completing the attach-time probe MODE SENSE with INTERMED drives chdone into neither branch: announce_buf[0] is read uninitialized (scsi_ch.c:639) and the buffer is printed to console/dmesg with %s by xpt_announce_periph (cam_xpt.c:1761-1763). Reproduced 3/3 attach cycles on the QEMU guest (evilcd3 rig, one-shot INTERMED mode sense): the announce line after the INTERMED marker printed stale kernel-stack bytes that differ per cycle (' x^NO', 0xB0 0x72 0x8E 0x4E 0x4F, 0xC0 0x60 0x1B 0x42 0x0D) while a normal attach prints the legitimate counts string -- proving ksnprintf never ran and the printed bytes are uninitialized cambio-SWI-thread stack residue (readable by any local user via dmesg).
No comments yet.