cdioctl() drops an unmatched cam_periph_release() when cam_periph_hold(PCATCH) is signal-interrupted: periph refcount underflow -> premature free / UAF window
Summary
cam_periph_hold is internally balanced (releases its own acquire on sleep failure) but cdioctl's hold-failure path adds an EXTRA release - every signal-interrupted ioctl decrements the periph refcount once: demonstrated 1->0->-1 live via debug sysctl on a valid open periph. Later device invalidation then runs camperiphfree (disk_destroy, kfree(softc)) while fds are still open - racing ioctls/closes hit freed periph/softc. Guest-verified fork PoC (holder child stalls in CDIOCPAUSE; parent self-interrupts CDIOCPLAYMSF with 100ms SIGALRM no-SA_RESTART -> EINTR + refcount burn each time). Root-gated. Delete-the-unmatched-release fix.
Discussion (0)
PoC verification
Evidence pack
findings/poc/DF-5273 Β· 7 files| File | Type | Description | Size | |
|---|---|---|---|---|
| poc_ref.c | β | 2.6 KB | view raw | |
| evilcd.c | β | 26.7 KB | view raw | |
| build.sh | β | 85 B | view raw | |
| run.sh | β | 173 B | view raw | |
| refcount_burn.log | β | 274 B | view raw | |
| README.md | β | 3.3 KB | β raw | |
| verdict.json | β | 4.3 KB | view raw |
DF-5273 evidence pack β cdioctl() refcount burn on interrupted hold
Finding (one line)
cdioctl() (sys/bus/cam/scsi/scsi_cd.c:1930-1934) reacts to a
cam_periph_hold(periph, PCATCH) failure β which cam_periph_hold()
itself already unwinds internally (cam_periph.c:395 acquire, :402 release)
β with an extra, unmatched cam_periph_release(periph). Every
signal-interrupted ioctl attempt therefore decrements the peripheral
refcount by one: the refcount can be driven to 0 and negative on a valid,
open device, and β when the periph is later invalidated β the
CAM_PERIPH_INVALID && refcount == 1 branch of cam_periph_release()
(cam_periph.c:352-357, 371-374) frees the periph (softc, disk, sysctls)
while file descriptors are still open β use-after-free window in
cdioctl/cdclose between cam_extend_get() and cam_periph_lock().
Reproduce (guest, root; evilcd kld loaded)
cd /root/df5271 && cc -O -o poc_ref poc_ref.c timeout 45 usched :1 ./poc_ref /dev/cd1
Expected (vulnerable kernel)
[poc] initial cd periph refcount: 1 [poc] round 0 burn 0: ioctl rc=-1 errno=4(Interrupted system call) refcount=1 [poc] round 0 burn 1: ioctl rc=-1 errno=4(Interrupted system call) refcount=0 [poc] round 0 burn 2: ioctl rc=-1 errno=4(Interrupted system call) refcount=-1
One burned reference per EINTR-interrupted ioctl; no close() anywhere.
(The holder child keeps the periph held via a device-stalled CDIOCPAUSE;
the parent's CDIOCPLAYMSF blocks in cam_periph_hold and is
self-interrupted with SIGALRM after 100 ms β sigaction installed without
SA_RESTART so the sleep returns EINTR.) Across earlier exploratory runs
the refcount reached -3 and -5 the same way; debug.evilcd.refcnt reads
the live periph's refcount field directly.
Why it matters
cam_periph_hold()'s acquire/release is balanced by design; the extra
release in cdioctl's error path is the only unbalanced drop in the driver's
ioctl path (contrast cdopen() scsi_cd.c:1017/1028-1031, which does
acquire before holding, making its failure path balanced). Effects:
- refcount underflow (demonstrated: 1 β 0 β -1);
- with the device invalidated (unplug) and refcount == 1 at release time,
camperiphfree() runs with open fds β subsequent ioctl/close race the
free between cam_extend_get() (scsi_cd.c:1918) and cam_periph_lock
(scsi_cd.c:1922) β UAF on periph/softc.
Not escalated further here: the trigger set (privileged open + concurrent holder + timed signal + hot-unplug) makes the full UAF racy; the refcount underflow itself is deterministic and is demonstrated.
Files
poc_ref.cβ self-contained fork-based PoC (holder child + timed SIGALRM burns + refcount reads).evilcd.cβ the fake CD (stalls CDIOCPAUSE;debug.evilcd.refcnt,debug.evilcd.unstall).run.log(see DF-5271 pack for the shared rig logs),refcount_burn.logβ the decisive output above.fix.diffβ one-line fix (delete the unmatched release).
Fix
--- a/sys/bus/cam/scsi/scsi_cd.c
+++ b/sys/bus/cam/scsi/scsi_cd.c
@@ -1931,7 +1931,6 @@ cdioctl(struct dev_ioctl_args *ap)
if ((error = cam_periph_hold(periph, PCATCH)) != 0) {
cam_periph_unlock(periph);
- cam_periph_release(periph);
return (error);
}
(cam_periph_hold already drops its own acquire on failure.)
Fix verification
not_testableFix is a one-line deletion of the unbalanced release; not separately kernel-validated in this run (the rebuilt validation kernel carried the DF-5271 and DF-5272 fixes).
static: cam_periph.c:395 acquire vs :402 release symmetry inside cam_periph_hold; scsi_cd.c:1932 is the only unbalanced release on that path.
Confirmed kernel references
Detail
Exploit chain
privileged open fd + concurrent holder ioctl (device-stalled command keeps CAM_PERIPH_LOCKED set) + timed signal to a second ioctl thread -> cam_periph_hold returns EINTR -> cdioctl's unmatched cam_periph_release -> repeat until refcount==1 -> invalidate device (unplug: AC_LOST_DEVICE -> CAM_PERIPH_INVALID) -> one more release hits cam_periph_release's INVALID&&refcount==1 branch -> camperiphfree() (cdcleanup: disk_destroy, kfree(softc)) while fds are open -> racing ioctl/close between cam_extend_get() and cam_periph_lock()/softc deref -> UAF.
Evidence (decisive lines)
refcount_burn.log: '[poc] initial cd periph refcount: 1' then three 'ioctl rc=-1 errno=4(Interrupted system call) refcount=1/0/-1' lines; debug.evilcd.refcnt reads the live periph->refcount field. README.md quotes the earlier -3/-5 runs.
PoC changes
Threaded PoC (pthread + pthread_kill) hung because signal() installs SA_RESTART (the interrupted hold auto-restarts and re-blocks) and DFly pthreads made the kill/join racy; rewritten as a self-contained fork-based program: holder child issues CDIOCPAUSE on the shared fd (evilcd stalls it), parent arms a 100ms SIGALRM itimer (sigaction, sa_flags=0) around CDIOCPLAYMSF. The burner must share the holder's fd: opening /dev/cd1 while the periph is held returns EAGAIN.
Verified recommended fix
Delete the unmatched cam_periph_release(periph) in cdioctl()'s cam_periph_hold failure path (cam_periph_hold already unwinds its own acquire).
Verdict
REPRODUCED (refcount burn): with a device-stalled CDIOCPAUSE holding the periph, each CDIOCPLAYMSF self-interrupted by SIGALRM (sigaction without SA_RESTART -> EINTR) returns Interrupted system call and decrements the cd periph refcount by exactly one, with no close() anywhere: initial refcount 1, then 1 -> 0 -> -1 printed per burn (earlier runs: 2 -> -3 -> -5 cumulative). Root cause verified statically: cam_periph_hold() acquires on entry and releases its own acquire when the PCATCH sleep fails (cam_periph.c:395,402) - it is balanced - so cdioctl()'s extra cam_periph_release() on the hold-failure path (scsi_cd.c:1932) is the only unbalanced drop (cdopen()'s identical-looking path is balanced because cdopen acquires first at :1017).
No comments yet.