passioctl() runs CAMIOCOMMAND/CAMGETPASSTHRU with no periph reference across unkillable blocking sleeps (DF-5273/5327 hold/release class) β latent premature-free/UAF, currently blocked by devfs close semantics
Summary
passioctl takes the periph without acquire (unlike passopen and the cd/ch/sa ioctl entries) then blocks in ccbwait with the sim lock dropped: if the refcount hit 0 the periph+softc+path would be freed under it (every outstanding CCB's path dangles, the sleep channel itself dangles). passsim-rig choreography PROVED it cannot reach 0 today (the ioctl's file ref defers d_close, vflush(FORCECLOSE) refuses busy vnodes, no revoke(2)) - filed as hardening since the invariant violation is real and devfs's unsynchronized count_dev ('XXX NOT SMP SAFE') or a future revoke re-opens the UAF. acquire/release-pair fix.
Discussion (0)
PoC verification
Evidence pack
findings/poc/DF-5499 Β· 16 files| File | Type | Description | Size | |
|---|---|---|---|---|
| README.md | β | 3.8 KB | β raw | |
| VERDICT.md | β | 3.2 KB | β raw | |
| passsim.c | β | 11.3 KB | view raw | |
| poc5499.c | β | 4.4 KB | view raw | |
| sset.c | β | 416 B | view raw | |
| Makefile | β | 54 B | β download | |
| build.sh | β | 66 B | view raw | |
| run.sh | β | 90 B | view raw | |
| build.log | β | 101 B | view raw | |
| run_full.log | β | 69 B | view raw | |
| run.log | β | 367 B | view raw | |
| console.log | β | 671 B | view raw | |
| env.txt | β | 241 B | view raw | |
| fix.diff | β | 424 B | view raw | |
| manifest.json | β | 1.1 KB | view raw | |
| verdict.json | β | 3.0 KB | view raw |
DF-5499 β passioctl() takes no periph reference across blocking CAMIOCOMMAND paths
What the finding is
passioctl() (sys/bus/cam/scsi/scsi_pass.c:451-456) looks a pass periph up
with cam_extend_get() and immediately cam_periph_lock()s it without any
cam_periph_acquire() β unlike passopen() (scsi_pass.c:319) and unlike the
sibling drivers' ioctl entries that use cam_periph_hold() (cd/ch/sa family,
cf. DF-5273/DF-5327). The ioctl then blocks with the sim lock dropped and no
reference held at:
cam_periph_getccb()βsim_lock_sleep(&periph->ccb_list, ...)(cam_periph.c:846-855), unkillable (no PCATCH);cam_periph_runccb()βcam_periph_ccbwait()βsim_lock_sleep(...)(cam_periph.c:863-872), unkillable;- the
cam_periph_mapmem()window (scsi_pass.c:577-579).
If the periph refcount could reach 0 while a thread is blocked there, the
next cam_periph_invalidate() (cam_periph.c:573-578) runs camperiphfree(),
freeing softc (passcleanup, scsi_pass.c:168-183), the periph itself
(cam_periph.c:630) and periph->path (cam_periph.c:629) β dangling every
outstanding CCB's ccb_h.path, the error-recovery saved_ccb_ptr into
softc->saved_ccb (cam_periph.c:1616), and the sleep address itself.
Why it is filed as Info / hardening (NOT reproduced as a UAF)
The open-file reference held by an in-flight ioctl() syscall keeps
devfs_spec_close() from ever firing d_close under the ioctl:
- Normal
close(): deferred until every in-flight syscall on the file returns β the release strictly orders after the ioctl. umount -fof a secondary devfs mount (the forced-reclaim door used by this PoC):vflush(FORCECLOSE)refuses busy device vnodes β proven live on the guest: with T1 blocked inside CAMIOCOMMAND andumount -f /mnt/pwexecuted, the console printedvflush: Warning, cannot destroy busy device vnode/vfs refused to unmount, error 16, and the rig'sstatsysctl showedpass periph ... refcount=1 flags=0 validβ the release never ran. See console.log + run_full.log in this directory.- DragonFly has no
revoke(2)for devfs (no VOP_REVOKE / vgonel path reachable from userspace).
Residual risk (why the hardening is still worth it): devfs's own
opencount = count_dev(dev) is documented XXX NOT SMP SAFE
(devfs_vnops.c:1146-1148); a miscount with the same cdev open through two
devfs mounts could fire d_close early, and passioctl's missing reference
is exactly what would convert that into a premature free / UAF. A future
revoke implementation would do the same.
Reproduce (negative-evidence rig)
- build.sh β compiles the
passsimKLD rig (fake CAM SIM with a direct-access LUN, hang/lost/poison/complete/stat sysctls) and the stage-driven PoC inside the guest. - run.sh β loads the rig, opens the new /dev/passN through a secondary devfs mount, blocks a thread in CAMIOCOMMAND, then walks the stages (forced umount β stat β AC_LOST_DEVICE β stat β poison β complete), printing the periph refcount at each step.
- Expected (and observed): refcount stays 1 across the forced unmount; periph freed only via the normal invalidate-after-close ordering; no UAF, no panic. This is the documented negative result.
Recommended hardening
Take a periph reference for the duration of the ioctl (see fix.diff):
periph = cam_extend_get(passperiphs, unit);
if (periph == NULL)
return(ENXIO);
+ if (cam_periph_acquire(periph) != CAM_REQ_CMP)
+ return(ENXIO);
cam_periph_lock(periph);
...
cam_periph_unlock(periph);
+ cam_periph_release(periph);
return(error);
Artifacts
- passsim.c, poc5499.c, sset.c, Makefile, build.sh, run.sh
- build.log, run_full.log (stage output), console.log (kernel side: refcount proof), run.log (process state post-run)
- verdict.json, manifest.json
DF-5499 VERDICT β not_reproduced (hardening-grade), with negative proof
Candidate
passioctl() (scsi_pass.c:437-525) runs CAMIOCOMMAND/CAMGETPASSTHRU on a periph obtained via cam_extend_get() with no cam_periph_acquire(), while blocking lock-dropped in cam_periph_getccb() (cam_periph.c:846-855), cam_periph_ccbwait() (cam_periph.c:863-872), and cam_periph_mapmem() (scsi_pass.c:577-579). Family class: DF-5273 / DF-5327 (periph hold/release balance). If refcount could reach 0 under a blocked ioctl, cam_periph_invalidate() (cam_periph.c:573-578) would camperiphfree() the periph, softc and path (cam_periph.c:582-631) under live kernel threads β UAF (dangling ccb_h.path on every outstanding CCB, dangling saved_ccb_ptr into softc->saved_ccb, dangling sleep address).
What was run (guest, DragonFly 6.5-DEVELOPMENT, X86_64_GENERIC)
passsim rig (this dir) creates a fake SIM + direct-access LUN β da0 +
/dev/pass1. PoC: open /mnt/pw/pass1 via secondary devfs, arm hang,
block T1 inside CAMIOCOMMAND (confirmed WCHAN cbwait via ps), then:
umount -f /mnt/pwβ console:vflush: Warning, cannot destroy busy device vnode,vfs refused to unmount, error 16; rig stat: refcount=1 flags=0 valid β the forced reclaim did NOT run dev_dclose; the open's reference was not dropped.- Repeated with AC_LOST_DEVICE ordering variants; the periph is only freed after the normal close-ordered release.
Why no UAF on this system
- The ioctl syscall holds a file reference for its duration, so vn_close (and thus d_close β passclose β cam_periph_release) can only run after the ioctl returns.
- vflush(FORCECLOSE) explicitly refuses busy device vnodes (their own comment: "the VXLOCK (force close) case can leave vnodes referencing a closed device. This might not occur now that our revoke is fixed" β devfs_vnops.c:1164-1166). Empirically confirmed above.
- No revoke(2)/VOP_REVOKE path exists for devfs on DragonFly.
Residual exposure (speculative, documented in the finding): devfs's
unsynchronized count_dev() (devfs_vnops.c:1147-1148 XXX NOT SMP SAFE)
could miscount with the same cdev open through two devfs mounts and fire
d_close early; a future revoke implementation would too. passioctl's
missing acquire is what would convert either into a premature periph free.
Bottom line
Real invariant violation in code shape (every other entry point holds), neutralized by DragonFly devfs close semantics today. Filed as Info hardening (defense-in-depth: take the reference), not as an exploitable UAF. Attempts: 3 full guest choreographies (2 with earlier rig revs, 1 final with refcount instrumentation).
Fix
fix.diff β one acquire/release pair in passioctl(). Not kernel-built for this Info-tier finding (no behavior change is observable by construction: the release path it protects cannot currently trigger).
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
not_testableNot built: Info-tier hardening; no observable behavior changes since the protected release path cannot currently trigger.
Confirmed kernel references
Detail
Evidence (decisive lines)
console.log (refcount=1 after umount -f with T1 blocked in CAMIOCOMMAND), run_full.log (stage output), VERDICT.md narrative; kernel refs scsi_pass.c:451-456, cam_periph.c:846-872, cam_periph.c:573-631, devfs_vnops.c:1139-1171
PoC changes
Built passsim KLD rig (hang/lost/poison/complete/stat sysctls) modeled on DF-5375 targsim; stage-driven poc5499 with periph-refcount instrumentation because the original one-shot choreography could not observe the refcount; sset helper because DragonFly sysctl(8) refuses PROC oids.
Verified recommended fix
Take cam_periph_acquire()/release() around the passioctl switch (fix.diff).
Verdict
passioctl() (scsi_pass.c:451-456) runs CAMIOCOMMAND with no cam_periph_acquire() while blocking lock-dropped in cam_periph_getccb()/ccbwait()/mapmem β the DF-5273/DF-5327 hold/release class. Guest choreography (passsim rig: fake SIM LUN, blocked CAMIOCOMMAND thread, forced devfs close + AC_LOST_DEVICE) proved the refcount CANNOT reach 0 under an in-flight ioctl on DragonFly: the ioctl syscall's file reference defers d_close, vflush(FORCECLOSE) refuses busy device vnodes (console: 'vflush: Warning, cannot destroy busy device vnode'; rig stat: refcount=1 flags=0 valid), and no revoke(2) exists. The UAF is therefore not reachable today; filed as Info hardening. Residual risk: devfs count_dev() is 'XXX NOT SMP SAFE' (devfs_vnops.c:1147-1148) and any future revoke/early-d_close would turn the missing reference into a premature camperiphfree() under live ioctl threads (dangling ccb_h.path on outstanding CCBs, dangling saved_ccb_ptr into softc).
No comments yet.