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

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)

No comments yet.

PoC verification

Evidence pack

findings/poc/DF-5462 Β· 12 files
FileTypeDescriptionSize
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: ptopen sys/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 β€” calls cam_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) whenever index >= nelem (i.e. at every 8th attached unit), while cam_extend_get() (:81-88) does an unlocked check-then-load of ea->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.ntargets targets answering INQUIRY (rest are absent)
  • debug.ptsim.maxtarget scan bound reported in XPT_PATH_INQ
  • debug.ptsim.rescan=1 run XPT_SCAN_BUS
  • debug.ptsim.stamp=1/0 start/stop kernel stamper threads that kmalloc/memset(0x41..)/kfree chunks of every size class cam_extend_set frees (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 in cam_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).
VERDICT.md
↓ download raw

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:
  • ptopen sys/bus/cam/scsi/scsi_pt.c:153 cam_extend_get(ptperiphs, unit)
  • ptclose :189
  • ptstrategy :219
  • ptioctl :632
  • writer, sim lock deliberately dropped around it:
  • ptctor :311-312 cam_periph_unlock(); cam_extend_set(...)
  • the racing free:
  • cam_extend_set sys/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_array cam_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=EVILPT clean 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.patched section 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_testable
baseline no→ patch + rebuild →patched clean

Baseline 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
↓ fix.diffper-fix-DF-5462

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.