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

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)

No comments yet.

PoC verification

Evidence pack

findings/poc/DF-5273 Β· 7 files
FileTypeDescriptionSize
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_testable
baseline reproduced→ patch + rebuild →patched clean

Fix 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.
per-fix-DF-5273

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