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

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)

No comments yet.

PoC verification

Evidence pack

findings/poc/DF-5333 Β· 13 files
FileTypeDescriptionSize
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=1 paints the sysctl thread's stack with 0x42 (~4KB, via ev_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.

VERDICT.md
↓ download raw

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 with announce_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_status in {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 by xpt_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):

  1. normal attach β€” chdone's success path runs ksnprintf(announce_buf, ...) on that thread (seeds the stack slot with live data);
  2. debug.evilcd.invalidate=1 destroys the ch periph;
  3. 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);
  4. chdone takes the uninit path; xpt_announce_periph prints whatever stale bytes occupy announce_buf on 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

fixed
baseline reproduced→ patch + rebuild →patched clean

Applied 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: ' line (announce_buf[0] is unconditionally written before any read, so xpt_announce_periph is never reached from the failed-probe path). Vulnerable baseline: 3/4 cycles printed garbage (one cycle hit a zero first byte, matching the branch condition).

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)
↓ fix.diffper-fix-DF-5333

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