Lockless cam_extend array: ptopen/ptclose/ptstrategy/ptioctl UAF read races ptctor's cam_extend_set growth (kfree of ea->ps) β kernel pointer used from freed memory
Summary
Every pt entry point resolves its periph via the completely unsynchronized cam_extend array while ptctor grows it (kmalloc/bcopy/kfree of ea->ps every 8th unit) with the sim lock deliberately dropped: a reader loading ea->ps before the swap and dereferencing after the kfree consumes a struct cam_periph* from freed memory - cam_periph_acquire refcount++ (UAF write if reused), then softc deref and lock on a forged pointer. Root-gated (0600 + RESTRICTEDROOT) but the same cam_extend defect backs da/sa/cd/ch/ses/pass where attach churn is ordinary hot-plug. 512-target ptsim rig + 8 racers (~150M opens, 90+ growth windows): window is 1-2 instructions, not reproduced (one unexplained wedge consistent with a hit, not claimed). Leaf-lock-serialization fix. Lead logged: deterministic camq_remove NULL-deref panic during attach churn (cam_xpt owner).
Discussion (0)
PoC verification
Evidence pack
findings/poc/DF-5462 Β· 12 files| File | Type | Description | Size | |
|---|---|---|---|---|
| README.md | β | 2.8 KB | β raw | |
| VERDICT.md | β | 5.2 KB | β raw | |
| ptsim.c | β | 10.9 KB | view raw | |
| race5462.c | β | 2.9 KB | view raw | |
| build.sh | β | 151 B | view raw | |
| run.sh | β | 142 B | view raw | |
| fix.diff | β | 2.4 KB | view raw | |
| fix_build.log | β | 5.6 MB | β download | |
| env.txt | β | 304 B | view raw | |
| panic.camq-lead.txt | β | 33.5 KB | view raw | |
| verdict.json | β | 3.4 KB | view raw | |
| manifest.json | β | 1.1 KB | view raw |
DF-5462 β lockless cam_extend array race: ptopen/ptstrategy/ptioctl UAF read vs ptctor growth
What this demonstrates
scsi_pt.c looks its periph up through the shared, completely
unsynchronized cam_extend array:
- readers:
ptopensys/bus/cam/scsi/scsi_pt.c:153,ptclose:189,ptstrategy:219,ptioctl:632 βcam_extend_get(ptperiphs, unit)with no lock held; - writer:
ptctor:311-312 β callscam_extend_set()with the sim lock dropped; - root cause:
cam_extend_set()(sys/bus/cam/cam_extend.c:54-78) grows the array with kmalloc/bcopy/kfree(ea->ps) wheneverindex >= nelem(i.e. at every 8th attached unit), whilecam_extend_get()(:81-88) does an unlocked check-then-load ofea->ps[index].
A reader that loads ea->ps before the swap but dereferences
ps[index] after the kfree reads freed kernel memory and uses it
as a struct cam_periph * (cam_periph_acquire() β periph->refcount++,
then periph->softc, cam_periph_lock()...).
All pt entry points are root-gated (ptopen :149
caps_priv_check_self(SYSCAP_RESTRICTEDROOT); node 0600 root:operator
ptctor :320-322), so this is a privileged-trigger race β but the same
cam_extend defect backs da/sa/cd/ch/ses/pass lookups,
where attach churn is ordinary hot-plug.
Rig
ptsim.ko β fake CAM SIM ("ptsim") whose 512 LUN-0 targets answer
INQUIRY as T_PROCESSOR so the pt driver attaches to them.
Knobs:
debug.ptsim.ntargetstargets answering INQUIRY (rest are absent)debug.ptsim.maxtargetscan bound reported in XPT_PATH_INQdebug.ptsim.rescan=1run XPT_SCAN_BUSdebug.ptsim.stamp=1/0start/stop kernel stamper threads that kmalloc/memset(0x41..)/kfree chunks of every size classcam_extend_setfrees (64..4096 B), so a raced reader that lands in the freed chunk sees 0x4141414141414141 instead of the old (still valid) pointers and faults visibly incam_periph_acquire().
Prereq
Guest kernel must contain device pt (NOT in X86_64_GENERIC):
cd /usr/src/sys/config cp X86_64_GENERIC EVILPT && printf "device\t\tpt\n" >> EVILPT cd /usr/src && make -j6 nativekernel KERNCONF=EVILPT && make installkernel KERNCONF=EVILPT reboot
Reproduce (guest, root)
./build.sh # builds ptsim.ko + race5462
./run.sh # attach pt0..pt7, start 8 racers + stampers,
# then ntargets=512 + rescan -> 63 growth windows
Expected
- Clean miss:
RACER-DONE opens=... other=0, guest stays up (window not hit this boot; the freed chunk usually still holds the old valid pointers, so even most hits are silent). - Hit: reader gets a stamped/garbage periph pointer β kernel page fault
in
cam_periph_acquire(periph->refcount++at 0x41414141...) β panic on the serial console (dfbsd-qemu/boot.log).
DF-5462 VERDICT
Status: not_reproduced (race proven in code; window never hit in budgeted attempts)
The defect is real and provable by inspection
Unsynchronized shared-memory access between CAM periph attach/detach and
every periph entry point, on the cam_extend array scsi_pt.c uses for
unit lookup:
- readers, NO lock held:
ptopensys/bus/cam/scsi/scsi_pt.c:153cam_extend_get(ptperiphs, unit)ptclose:189ptstrategy:219ptioctl:632- writer, sim lock deliberately dropped around it:
ptctor:311-312cam_periph_unlock(); cam_extend_set(...)- the racing free:
cam_extend_setsys/bus/cam/cam_extend.c:54-78 β on every growth (index >= nelem, i.e. at every 8th unit):kmalloc(new)βbcopyβkfree(ea->ps)βea->ps = new, with no lock anywhere (struct extend_arraycam_extend.c:26-29 has no lock;cam_extend_release:90-97 stores NULL the same way).
Interleaving that corrupts: a reader in cam_extend_get
(cam_extend.c:81-88) loads ea->ps (old pointer), the growth frees the
old array, the reader's subsequent ea->ps[index] load reads freed
kernel memory and the value is used as struct cam_periph *:
cam_periph_acquire() (cam_periph.c:319-329 periph->refcount++ β UAF
WRITE if the chunk was reused), then periph->softc deref and
cam_periph_lock() in ptopen:157-159.
All pt entry points are root-gated (ptopen:149
caps_priv_check_self(SYSCAP_RESTRICTEDROOT); node 0600 root:operator,
ptctor:320-322). The same cam_extend defect also backs the lookup
paths of da/sa/cd/ch/ses/pass (cam_extend users), where attach churn is
ordinary hot-plug β but those drivers are equally permission-gated on
their raw device nodes.
Verification attempts (all on the QEMU guest, root)
Rig: ptsim.ko fake CAM SIM, 512 LUN-0 targets answering INQUIRY as
T_PROCESSOR so pt attaches; growth driven in batches of 8 units (each
batch = exactly one cam_extend_set growth/kfree); race5462 runs 8
processes doing open/ioctl/close on /dev/pt0../dev/pt7 (~130k
opens/s/child, ~780k/s aggregate); optional kernel stamper threads
kmalloc/memset/kfree chunks of every freed size class so that a raced
reader would see 0x00 (NULL periph β visible ENXIO anomaly) or 0x41
(non-canonical periph β GP fault in cam_periph_acquire) instead of the
still-valid old pointers.
| boot | config | windows | opens | result |
|---|---|---|---|---|
| run 2 | byte 0x00, batchesβ256 | ~31 | ~78M | no ENXIO anomaly; 3 of 8 racer children (units 0,1,2) never completed and the system lost exec capability β consistent with a hit whose garbage periph blocked in cam_periph_lock, but NOT provable (ssh session lost, no stack) |
| run 3 | byte 0x41, batchesβ256 (stopped ~111 nodes) | ~13 | ~150M total w/ run4 | clean |
| run 4 | byte 0x41, batchesβ512 (stopped ~231 nodes) | ~29 | β | clean |
| cycle 5 | byte 0x41, batchesβ512 (stopped ~161 nodes) | ~19 | ~120M | guest hit the UNRELATED camq_remove NULL-deref panic (below), not the extend race |
| cycle 6,7 | aborted after cycle 5 triage | β | β | β |
Aggregate: ~5 boots, ~90+ growth windows, >150M racing open/ioctl/close
executions, zero race-specific manifestations (no 0x4141... fault, no
spurious ENXIO burst). Estimated hit probability per growth window with
the 1-2 instruction ea->psβps[index] gap and ~780k opens/s is
~0.1-0.3%; the observed zero hits over ~90 windows is consistent with
that estimate, i.e. the experiment simply needs more cycles than the
budget allowed.
Independent lead discovered by the rig (not this finding)
Mass attach churn (~100-160 targets, batched or single scan)
deterministically triggers a NULL-deref write panic in
camq_remove+0x35 (removed_entry->index = -1 on a NULL queue slot;
fault VA 0x8; cambio SWI context) β a queue-array hole / double-remove
class bug in the xpt scheduler queue path. Captured in
panic.camq-lead.txt. Filed as a lead for the cam_xpt/cam_queue owner;
it capped this experiment's per-boot window count.
Fix validation
fix.diff serializes cam_extend_{set,get,release} behind a leaf
lockmgr lock (cam_extend_lk, SI_SUB_DRIVERS SYSINIT; shared for
readers, exclusive for writers; internal cam_extend_get_locked helper
avoids the releaseβget recursion). Lock order is strictly
sim/buses β camext (leaf), taken nowhere else β no inversion.
Because the baseline race never reproduced, the fix could not be behaviorally A/B'd; it was build- and load-path validated on the guest:
- applied to /usr/src,
make nativekernel KERNCONF=EVILPTclean build (fix_build.log), installed, booted (kernel #1 22:30:30); - rig attaches 8 pt devices under the lock, batched growth 8β64 with
stampers active: 23,137,834 racing opens, zero anomalies, zero
panics, guest healthy (
run.patchedsection of run.log summary).
fix_status: not_testable (baseline unreproduced) with a passing build/boot/growth smoke test.
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_testableBaseline race never reproduced in budget, so no behavioral A/B was possible; the fix kernel built cleanly, booted, attached 64 pt devices through 7 locked growth events under 23.1M racing opens with zero anomalies or deadlocks.
fix_build.log (full clean nativekernel build); VERDICT.md fix-validation section; patched smoke run inline in run summary
Confirmed kernel references
Detail
Evidence (decisive lines)
VERDICT.md (interleaving analysis + attempt table); ptsim.c (rig); race5462.c (racer); run.sh; panic.camq-lead.txt (independent camq_remove NULL-deref lead captured mid-experiment); env.txt
PoC changes
PoC authored from scratch for this finding (no seed): fake CAM SIM exposing T_PROCESSOR targets (ptsim.c) because T_PROCESSOR devices do not exist on the guest; batched 8-unit growth choreography after single-scan mass attach tripped an unrelated camq_remove panic; heap-stamper threads to make freed-array reuse visible; racer with shared-memory stats and console heartbeat. Required building a custom guest kernel (EVILPT = X86_64_GENERIC + device pt) because device pt is not in GENERIC.
Verified recommended fix
Serialize cam_extend_set/get/release with a leaf lockmgr lock (see fix.diff)
Verdict
The lockless cam_extend race is proven in code (ptopen/ptclose/ptstrategy/ptioctl read ptperiphs with no lock vs ptctor's cam_extend_set kmalloc/bcopy/kfree growth with the sim lock dropped) but did not manifest in ~5 guest boots / ~90+ growth windows / >150M racing opens; the ~1-2 instruction window puts the per-window hit probability at ~0.1-0.3%, consistent with zero observed hits. Run #2 left an unexplained wedge (3 racer children stuck, system-wide exec hang) consistent with a hit landing in cam_periph_lock, but without a stack it is not provable and is not claimed. Root-gated on every path.
No comments yet.