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

chopen() publishes CH_FLAG_OPEN / drops the secondary opener's acquire BEFORE cam_periph_hold(PCATCH): a signal-interrupted open burns one periph reference (refcount desync -> 0-with-open-fd -> premature free / silent unsigned underflow)

Summary

The DF-5273 signal-interrupt class via a different shape: chopen does the multi-open fixup BEFORE the PCATCH hold, so an interrupted open's error-path release drops a reference the flag state says someone else owns - refcount undercounts by one per interrupted open (cam_periph_hold already releases its own internally). Guest-verified deterministically with the evilcd2 rig (fake T_CHANGER): interrupted open#2 -> open#1 completes with refcount==0 while /dev/ch0 open; invalidate -> softc+periph kfree'd synchronously UNDER the open fd (lost device/removing entry in dmesg); without invalidation close wraps to 0xFFFFFFFF silently (the DF-5329 dead guard). Every other CAM periph driver does flag/fixup AFTER the hold - chopen is the outlier. Hold-before-publish fix validated on rebuilt kernel (2/1/0 no desync).

Discussion (0)

No comments yet.

PoC verification

Evidence pack

findings/poc/DF-5327 Β· 18 files
FileTypeDescriptionSize
README.md β€” 2.7 KB ↓ raw
VERDICT.md β€” 4.2 KB ↓ raw
evilcd2.c β€” 39.9 KB view raw
poc_chopen.c β€” 3.0 KB view raw
poc_chopen2.c β€” 2.6 KB view raw
poc_multopen.c β€” 3.4 KB view raw
poc_uaf.c β€” 3.9 KB view raw
build.sh β€” 226 B view raw
run.sh β€” 161 B view raw
build.log β€” 5.6 KB view raw
run.log β€” 1.6 KB view raw
runB.log β€” 856 B view raw
fixedA.log β€” 459 B view raw
race.log β€” 276 B view raw
poc_ref.out β€” 284 B view raw
env.txt β€” 385 B view raw
fix.diff β€” 869 B view raw
verdict.json β€” 4.4 KB view raw

DF-5327 evidence pack β€” chopen() sets CH_FLAG_OPEN before cam_periph_hold(PCATCH); interrupted hold burns the open reference (refcount desync β†’ 0-with-open-fd β†’ premature free / unsigned underflow)

Finding (one line)

chopen() (sys/bus/cam/scsi/scsi_ch.c:415-432) performs the "multi-open fixup" (set CH_FLAG_OPEN for the first opener / drop this open's cam_periph_acquire() for later openers) before cam_periph_hold(periph, PCATCH); when that hold is signal-interrupted, its error path (cam_periph_unlock + cam_periph_release) drops a reference that the flag state says someone else owns β€” every interrupted open decrements the peripheral refcount by one (the DF-5273 class, in the open path instead of the ioctl path, in a driver DF-5273 did not cover).

Consequences (all reproduced on the guest, deterministic)

  1. open#1 parked (device stall) + open#2 interrupted β†’ open#1 completes with refcount == 0 while /dev/ch0 is open β†’ any device removal (AC_LOST_DEVICE) runs camperiphfree() immediately: softc and periph kfree'd under the open descriptor (premature free / UAF-enabling state, same window DF-5273 describes).
  2. plain close on that fd β†’ cam_periph_release at 0 β†’ 0xFFFFFFFF (silent: DF-5329's dead guard).

Reproduce (guest, root; EVIL kernel = GENERIC+device ch, evilcd2.ko)

cd /root/df5327
sysctl -n debug.evilcd.watch=2       # refcnt/postmortem watch the ch periph
./run.sh                             # stage A then stage B

Stage A (poc_chopen): interrupt-burn + refcount 0-with-open-fd + close β†’ -1. Stage B (poc_chopen2): same burn, then invalidate β†’ chcleanup frees periph+softc while the fd is open, then debug.evilcd.sprayfake re-allocates the exact freed chunk with fully chosen contents.

Expected (vulnerable kernel) β€” stage A

[child2] open#2: fd=-1 errno=4(Interrupted system call) refcount=1
[parent] refcount while open#1 still parked: 1
[child1] open#1 returned: refcount=0 (*** expect 0 -- undercounted with ch0 OPEN ***)
[child1] after close: refcount=-1 (*** expect -1: unmatched release ***)

Rig

evilcd2.c (derived from the DF-5271/DF-5273 evilcd rig) adds: - target 1 = fake T_CHANGER (/dev/ch0), MODE SENSE pages 0x1d/0x1f - debug.evilcd.defer = park the next SCSI CCB (parks chgetparams) - debug.evilcd.watch = 2 β†’ refcnt/periphaddr/postmortem/sprayfake watch the ch periph - debug.evilcd.invalidate β†’ xpt_async(AC_LOST_DEVICE) on the target - debug.evilcd.sprayfake β†’ spray sizeof(cam_periph) chunks until one lands at the freed periph address; plant a fake (refcount=0x41414141)

root@dfbsd:~ # uname -a
DragonFly dfbsd 6.5-DEVELOPMENT #0 ... /usr/obj/usr/src/sys/EVIL  x86_64
VERDICT.md
↓ download raw

DF-5327 VERDICT

Status: REPRODUCED (deterministic, both stages) β€” fix validated

What was reproduced

Bug: chopen() (sys/bus/cam/scsi/scsi_ch.c:415-432) performs the multi-open fixup (set CH_FLAG_OPEN for the first opener / drop this open's cam_periph_acquire() for later openers) before cam_periph_hold(periph, PCATCH). When the hold is signal-interrupted, the error path drops one reference in addition to the one cam_periph_hold() already dropped internally β€” the peripheral refcount undercounts by one per interrupted open, exactly the DF-5273 class but via the open path and in a driver DF-5273 did not cover.

Chain proven on the guest (evilcd2 rig, fake T_CHANGER at target 1, /dev/ch0; debug.evilcd.defer parks chgetparams' MODE SENSE so a second open blocks in cam_periph_hold(PCATCH) and is interrupted by SIGALRM):

  1. open#1 parked (device-side stall) β†’ rc = 2 (its acquire + hold).
  2. open#2 interrupted (EINTR) β†’ chopen error path releases β†’ rc = 1 while open#1 is still inside chgetparams holding acquire+hold.
  3. open#1 completes β†’ unhold β†’ rc == 0 with /dev/ch0 OPEN ([child1] open#1 returned: refcount=0).
  4. Device removal (debug.evilcd.invalidate β†’ AC_LOST_DEVICE): cam_periph_invalidate() sees refcount==0 β†’ camperiphfree() β†’ chcleanup() β†’ kfree(softc) and kfree(periph) while the descriptor is open β€” dmesg shows (ch0:evilcd0:0:1:0): lost device + removing device entry; postmortem dumps the freed chunk.
  5. Same burn without invalidation: plain close() β†’ chclose()'s unconditional release at rc==0 β†’ 0xFFFFFFFF ([child1] after close: refcount=-1), silently (DF-5329's dead guard). Repeated cycles reach -2, -3, ... (zombie periph, never freed, no warning).

Ran 3+ times across two kernel builds; fully deterministic.

Exploitability assessment (honest)

  • Trigger: root or a RESTRICTEDROOT-capable process on a system with a medium-changer device (or any code path reaching chopen with a signal-interruptible hold). Same privilege gate as DF-5273.
  • The 0-with-open-fd state (step 3) is a premature-free enabler: any hot-unplug/LUN-replacement/rescan racing an open ch descriptor frees the periph + softc under it. The in-flight use-after-free window this creates (fd-owning thread inside chclose/chioctl between cam_extend_get() and cam_periph_lock()) is identical in shape to the window DF-5273 documents.
  • Attacker reallocation of the exact freed 144-byte chunk was attempted with the rig (16384-iteration spray + 4096 magazine-churn cycle, same-CPU pinned); the freed chunk stayed parked in a full zalloc magazine and was not recycled within the run ("sprayfake: chunk NOT reused"). Reallocation of this size class under sustained churn is standard slab behavior (demonstrated for the 32B class in DF-5271's pack); the exact-chunk groom is left as a bounded, documented miss.
  • uid=0 escalation: gated behind RESTRICTEDROOT β€” a root-triggerable refcount bug on this device family is a robustness/containment defect rather than an unprivβ†’priv vector, so no escalation chain was pursued (matches DF-5273's Medium classification).

Fix validation (MANDATORY for memcorrupt β€” done)

fix.diff swaps chopen to take the hold BEFORE the flag/fixup (mirrors cdopen/daopen/saopen). Applied in the guest's /usr/src, kernel rebuilt (EVIL = GENERIC + device ch), reinstalled, rebooted, exact same PoC:

step vulnerable patched
open#2 interrupted (rc while #1 parked) 1 2
open#1 returned, fd open 0 1
after close -1 0

(fixedA.log). Undercount and underflow GONE; behavior matches cdopen's proven-safe ordering.

Guest

DragonFly dfbsd 6.5-DEVELOPMENT #0 ... /usr/obj/usr/src/sys/EVIL x86_64 (GENERIC + device ch), evilcd2.ko rig; cc 8.3.

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

fix.diff applied in guest /usr/src, kernel rebuilt/reinstalled/rebooted, exact same PoC: interrupted open#2 leaves rc=2 (was 1), open#1 completes at rc=1 with fd open (was 0), close -> 0 (was -1). Desync and underflow eliminated; ordering now matches cdopen's proven-safe shape.

fixedA.log (patched run) vs run.log stage A (vulnerable run); fix.diff in pack
↓ fix.diffper-fix-DF-5327

Confirmed kernel references

Detail

Exploit chain

open#1 parked via device stall (holds CAM_PERIPH_LOCKED + acquire) -> open#2 signal-interrupted in cam_periph_hold -> chopen error path drops open#1's backing reference (flag still set) -> open#1 finishes at refcount==0 with fd open -> (a) AC_LOST_DEVICE frees periph+softc under the open descriptor, giving any in-flight chclose/chioctl between cam_extend_get() and cam_periph_lock() a use-after-free of the 144-byte M_CAMPERIPH chunk; or (b) plain close -> 0xFFFFFFFF zombie (silent per DF-5329). Escalation beyond the premature-free state is RESTRICTEDROOT-gated and was not chased.

Evidence (decisive lines)

run.log (stage A+B: '[child1] open#1 returned: refcount=0' then 'after close: refcount=-1'; stage B 'removing device entry' with fd open + postmortem dump of freed chunk 0xfffff8008d640ba0), fixedA.log (patched kernel: 2/1/0 -- bug gone), VERDICT.md table, dmesg in run.log

PoC changes

rig evilcd2.c derived from DF-5271/5273's evilcd: added target-1 fake T_CHANGER (inq type 0x08, MODE SENSE pages 0x1d/0x1f), watch=2 ch-periph refcnt/postmortem, defer (park SCSI CCBs), invalidate (AC_LOST_DEVICE), sprayfake; poc_chopen/poc_chopen2 fork children so the parent controls the defer valve (first draft deadlocked by arming defer after open#1)

Verified recommended fix

chopen(): take cam_periph_hold() BEFORE setting CH_FLAG_OPEN / dropping the secondary opener's acquire (mirror cdopen/daopen/saopen)

Verdict

chopen() sets CH_FLAG_OPEN (or drops a secondary opener's acquire) before cam_periph_hold(PCATCH); on a signal-interrupted hold its error path releases one reference too many, undercounting the periph refcount per interrupted open. Reproduced deterministically on the guest: after the interrupted open#2, open#1 completes with refcount==0 while /dev/ch0 is open; device removal then frees periph+softc under the open fd (premature free / UAF-enabling state), and a plain close wraps the count to 0xFFFFFFFF silently (DF-5329). Root-gated (SYSCAP_RESTRICTEDROOT) like the rest of the CAM family; exact-chunk attacker regroom was attempted but the freed chunk stayed parked in a full zalloc magazine (documented miss); no uid0 chase for the same reason as DF-5273 (privileged trigger).