cam_periph_alloc() TOCTOU on the borrowed cam_periph_find() pointer: camperiphfree() can free the periph between find() and the unlocked NEW_DEV_FOUND/deferred_callback stores (UAF write), and camperiphfree() can see the flag set before deferred_callback is published (NULL/garbage indirect call)
Summary
cam_periph_find returns a borrowed pointer after dropping the buses lock; alloc then reads/writes periph->flags/deferred_callback/deferred_ac with NO lock - the dying INVALID periph's last release can complete camperiphfree (TAILQ_REMOVE + kfree) inside the window so alloc's stores land in freed memory; symmetrically camperiphfree can observe flag=1 with callback still NULL and call it. Hot-plug/LUN-replacement racing the final close. Bounded guest race attempted (200 create/destroy cycles x 4000 open/close churns, 90s): no crash - window is a handful of instructions; stands on the lock-domain proof. One-buses-lock-hold + store-callback-before-flag fix (compilable diff in pack).
Discussion (0)
PoC verification
Evidence pack
findings/poc/DF-5328 Β· 6 files| File | Type | Description | Size | |
|---|---|---|---|---|
| README.md | β | 2.7 KB | β raw | |
| VERDICT.md | β | 3.1 KB | β raw | |
| race_alloc.sh | β | 982 B | view raw | |
| race_fdchurn.c | β | 346 B | view raw | |
| fix.diff | β | 2.6 KB | view raw | |
| verdict.json | β | 2.6 KB | view raw |
DF-5328 evidence pack β cam_periph_alloc() TOCTOU on the borrowed cam_periph_find() pointer: camperiphfree() can free the periph between find() and the NEW_DEV_FOUND/deferred_callback stores
Finding (one line)
cam_periph_alloc() (sys/bus/cam/cam_periph.c:176-190) calls
cam_periph_find() β which returns a borrowed pointer with no
reference and drops the buses lock before returning β and then reads
and writes periph->flags (CAM_PERIPH_NEW_DEV_FOUND) and
periph->deferred_callback / deferred_ac with no lock held; the
last cam_periph_release() on the dying INVALID periph can complete
camperiphfree() (cam_periph.c:581-632: TAILQ_REMOVE -> chcleanup/cdcleanup
-> kfree(softc) -> kfree(periph)) inside that window, so alloc's stores
land in freed memory (UAF write), and symmetrically camperiphfree()'s
CAM_PERIPH_NEW_DEV_FOUND test at cam_periph.c:604 can see the flag set
before deferred_callback is stored, calling a NULL/garbage function
pointer at cam_periph.c:625.
Why it is real (code proof)
cam_periph_find()(cam_periph.c:292-316) takesxpt_lock_buses(), finds, unlocks, returns the raw pointer. No refcount.cam_periph_alloc():176periph = cam_periph_find(path, name);then :178-183 touchesperiph->flags/periph->deferred_callback/periph->deferred_acunlocked.camperiphfree()runs underxpt_lock_buses()fromcam_periph_release()'s slow path (:368-377) orcam_periph_invalidate()(:573-578) and frees the periph (:630). Nothing serializes it against alloc's unlocked stores.- Precondition: a periph of the same driver on the same path that is CAM_PERIPH_INVALID but not yet freed (refcount > 0, e.g. held by an open fd or an in-flight probe) whose last reference drops exactly while a rescan rediscovers the LUN (hot-plug / LUN replacement / rescan racing close).
Verification status: NOT reproduced on the guest (window measured in
a few instructions; see VERDICT.md for the honest analysis)
The interleaving requires the freeing thread to complete
TAILQ_REMOVE+dtor+kfree between alloc's find() return and its first
flag store β a window of a handful of instructions on the allocating
thread. A statistical race (rig-driven invalidate/rescan loops) gives
an estimated <<1e-3 hit rate per cycle and could not demonstrate it
within the run budget. The finding stands on the code-level proof
above (same borrowed-pointer pattern FreeBSD has since restructured).
Reproduce attempt rig
evilcd2.c provides debug.evilcd.invalidate (AC_LOST_DEVICE) and
debug.evilcd.rescan (XPT_SCAN_BUS) to drive create/destroy cycles;
race_alloc.sh loops open/close/invalidate/rescan against /dev/ch0.
No crash/ anomalies observed in bounded runs.
DF-5328 VERDICT
Status: NOT REPRODUCED (bounded guest attempt; code-proven race, confidence "likely")
The claim
cam_periph_alloc() (sys/bus/cam/cam_periph.c:176-190) uses the
borrowed, unlocked cam_periph_find() result:
periph = cam_periph_find(path, name); /* buses lock dropped inside */
if (periph != NULL) {
if ((periph->flags & CAM_PERIPH_INVALID) != 0
&& (periph->flags & CAM_PERIPH_NEW_DEV_FOUND) == 0) {
periph->flags |= CAM_PERIPH_NEW_DEV_FOUND; /* unlocked WRITE */
periph->deferred_callback = ac_callback; /* unlocked WRITE */
periph->deferred_ac = code;
return (CAM_REQ_INPROG);
camperiphfree() (cam_periph.c:581-632, from cam_periph_release()
:368-377 or cam_periph_invalidate() :573-578) removes the periph from
the units list and ultimately kfree(periph) at :630. Two races:
(a) UAF write: the last release completes camperiphfree between
alloc's find() return and its flag store -> the three stores land
in freed memory (M_CAMPERIPH, 144 bytes).
(b) NULL/garbage indirect call: camperiphfree() reads
CAM_PERIPH_NEW_DEV_FOUND at :604 under the buses lock, but the
flag was stored BEFORE deferred_callback in alloc's unlocked
sequence -> camperiphfree can see flag=1 with callback still NULL
-> periph->deferred_callback(NULL, ...) at :625 -> panic/garbage
call.
Why the guest attempt could not hit it
The interleaving window is the handful of instructions between
find()'s unlock-return and the first flag store on the allocating
thread, and the freeing thread must complete TAILQ_REMOVE + dtor +
kfree inside it. Precondition: a same-driver INVALID periph with a live
last reference (open fd / in-flight probe) on the exact path being
rescanned.
Rig attempt (race_alloc.sh, race_fdchurn.c): ~200
invalidate+rescan cycles interleaved with ~4000 open/close churns on
/dev/ch0 across 90 s on the vulnerable kernel. No crash, no anomalies,
refcount stable at 0 at quiesce. Expected hit rate per cycle is far
below 1e-3; the bounded attempt is documented as evidence of diligence,
not as refutation β the code path stands on the lock-domain analysis
above (FreeBSD restructured this same area; the borrowed-pointer
pattern is the defect).
Reproduce attempt
cd /root/df5327 && sh race_alloc.sh # ~90 s bounded loop
(rig sysctls debug.evilcd.invalidate / .rescan drive create/destroy)
Fix
fix.diff: perform the lookup and the deferred bookkeeping under one
xpt_lock_buses() hold (serializing with camperiphfree's
TAILQ_REMOVE/flag read) and store deferred_callback/deferred_ac
BEFORE setting CAM_PERIPH_NEW_DEV_FOUND. Not kernel-rebuilt (race not
reproduced; patch is advisory from the analysis).
Guest
DragonFly dfbsd 6.5-DEVELOPMENT (EVIL kernel), evilcd2.ko rig.
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_testablefix.diff authored from the analysis; not kernel-validated (race not reproduced at runtime)
fix.diff; VERDICT.md lock-domain analysis
Confirmed kernel references
Detail
Evidence (decisive lines)
VERDICT.md (full interleaving analysis), race.log (bounded attempt, no anomalies), README.md
PoC changes
race rig built from the evilcd2 rig (invalidate/rescan sysctls already present); no seed existed
Verified recommended fix
perform the units-list lookup and the deferred bookkeeping under one xpt_lock_buses() hold, storing deferred_callback/deferred_ac BEFORE the NEW_DEV_FOUND flag (fix.diff)
Verdict
cam_periph_alloc() touches periph->flags/deferred_callback/deferred_ac unlocked on the borrowed cam_periph_find() result; camperiphfree() (buses-locked remove + dtor + kfree, plus the flag read at :604) is not serialized against those stores. Bounded guest race (200 invalidate/rescan cycles x 4000 open/close churns, 90s) produced no hit -- the window is a few instructions wide and requires the dying periph's last reference to drop exactly as a rescan rediscovers the LUN (hot-plug/close race). Finding stands on the lock-domain analysis; not demonstrated at runtime.
No comments yet.