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)
PoC verification
Evidence pack
findings/poc/DF-5327 Β· 18 files| File | Type | Description | Size | |
|---|---|---|---|---|
| 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)
- 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). - plain close on that fd β
cam_periph_releaseat 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
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):
- open#1 parked (device-side stall) β rc = 2 (its acquire + hold).
- open#2 interrupted (EINTR) β chopen error path releases β rc = 1 while open#1 is still inside chgetparams holding acquire+hold.
- open#1 completes β unhold β rc == 0 with /dev/ch0 OPEN
(
[child1] open#1 returned: refcount=0). - 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. - 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()andcam_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
fixedfix.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
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).
No comments yet.