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

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)

No comments yet.

PoC verification

Evidence pack

findings/poc/DF-5499 Β· 16 files
FileTypeDescriptionSize
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:

  1. Normal close(): deferred until every in-flight syscall on the file returns β€” the release strictly orders after the ioctl.
  2. umount -f of 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 and umount -f /mnt/pw executed, the console printed vflush: Warning, cannot destroy busy device vnode / vfs refused to unmount, error 16, and the rig's stat sysctl showed pass periph ... refcount=1 flags=0 valid β€” the release never ran. See console.log + run_full.log in this directory.
  3. 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 passsim KLD 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.

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
VERDICT.md
↓ download raw

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:

  1. 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.
  2. 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_testable
baseline no→ patch + rebuild →patched clean

Not built: Info-tier hardening; no observable behavior changes since the protected release path cannot currently trigger.

↓ fix.diffper-fix-DF-5499

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