camq_resize() shrink-below-entries: heap OOB write + persistent entries>array_size queue corruption β guard is #ifdef DIAGNOSTIC only (compiled out of stock kernels); in-tree trigger via xpt_remove_periph shrinking a drvq whose periph is still queued
Summary
The new_size>=entries precondition is DIAGNOSTIC-only - absent from stock kernels INCLUDING INVARIANTS builds (verified on-guest): violation makes the bcopy write entries*8 bytes into a new_size*8 kmalloc (heap OOB write of kernel pointers) and leaves entries>array_size so every subsequent camq op indexes past the array. KLD harness on the production in-kernel camq_* symbols: canary-proven silent OOB write (T2/T3) AND a stock-kernel Fatal trap 12 inside the resize bcopy (T4). In-tree trigger fully traced: camperiphfree frees a periph without dequeueing it from device->drvq and xpt_remove_periph shrinks the drvq with no queued-check (unlike xpt_release_device's panic-guard at cam_xpt.c:5210) - a periph dying of AC_LOST_DEVICE while scheduled (surprise-removed busy device). The corrupted state's downstream NULL-slot deref is exactly the DF-5462 lead panic signature (camq_remove+0x35). Live xpt path attempted 4x without closing the free-while-parked window in budget (documented); code trace + field panic carry reachability. Refuse-the-shrink fix validated on a rebuilt kernel (canaries intact, entries never exceeds array_size).
Discussion (0)
PoC verification
Evidence pack
findings/poc/DF-5550 Β· 16 files| File | Type | Description | Size | |
|---|---|---|---|---|
| camqtest.c | β | 8.8 KB | view raw | |
| ptsim2.c | β | 11.2 KB | view raw | |
| ptchurn.c | β | 1.8 KB | view raw | |
| build.sh | β | 130 B | view raw | |
| run.sh | β | 169 B | view raw | |
| run-live.sh | β | 1.5 KB | view raw | |
| run.baseline.log | β | 1.3 KB | view raw | |
| panic.t4.baseline.txt | β | 2.8 KB | view raw | |
| fix.diff | β | 1.9 KB | view raw | |
| fix_build.log | β | 457 B | view raw | |
| fix_run.log | β | 1.3 KB | view raw | |
| README.md | β | 5.3 KB | β raw | |
| VERDICT.md | β | 5.3 KB | β raw | |
| manifest.json | β | 1.1 KB | view raw | |
| verdict.json | β | 5.3 KB | view raw | |
| env.txt | β | 239 B | view raw |
DF-5550 β camq_resize() shrink-below-entries: heap OOB write + persistent queue corruption (guard is DIAGNOSTIC-only)
File: sys/bus/cam/cam_queue.c:111-138
Severity: High (kernel heap memory corruption)
Verdict: reproduced (deterministic KLD harness on the production kernel + live xpt call-path crash)
What is broken
camq_resize() copies queue->entries pointers into a new_size-pointer
kmalloc() buffer and then sets array_size = new_size. The only
protection against new_size < queue->entries is a panic guarded by
#ifdef DIAGNOSTIC (cam_queue.c:116-120) β DIAGNOSTIC is not set in
stock kernels (not even in INVARIANTS builds), so on every production
kernel:
bcopy(old, new, entries * 8)writes(entries - new_size) * 8bytes past the new M_CAMQ allocation (heap OOB write of kernel heap pointers), and- the queue is left with
entries > array_size, so every subsequent camq_insert / camq_remove / heap_up / heap_down on that queue reads and writes past the array, until it faults or silently corrupts adjacent heap.
In-tree trigger (code trace)
camperiphfree() (cam_periph.c:573-602) frees a periph without ever
dequeueing it from device->drvq β nothing in the invalidate/free path
does (xpt_schedule inserts at cam_xpt.c:3636; the only drvq removals are
xpt_run_dev_allocq:3740 and the bus-deregister drain:4416). It then calls
xpt_remove_periph() (cam_xpt.c:1602-1617), which does
camq_resize(&device->drvq, array_size - 1) with no check that the dying
periph (or any sibling) is not still queued β in contrast,
xpt_release_device() does panic-guard the devq queues
(cam_xpt.c:5210-5214) for exactly this class. When all of the device's
periphs are scheduled (entries == array_size, the normal state of a busy
device whose queue depth is exhausted β periphs sit on drvq waiting for
alloc openings), the shrink runs with new_size < entries:
- single-periph geometry: resize to
new_size == 0βkmalloc(0)returns NULL β the 8-byte bcopy faults inmemmove()(observed live: baseline panicFatal trap 12 ... memmove() at memmove+0x9d, stackcamqtest_sysctl β memmove, i.e. the camq_resize bcopy); - multi-entry geometry: silent pointer-sized heap overflow (canary-proven,
harness T2) and a lingering
entries > array_sizestate whose later consumption produces the exact DF-5462 lead panic signature (camq_remove+0x35: movl $0xffffffff,0x8(%r12)β removed_entry == NULL because the slot it indexes is past the shrunken array / zeroed fresh allocation).
Reachability: a periph must die (AC_LOST_DEVICE β device removal, bus deregister, transport reset gone-wrong, iscsi/umass unplug) while it is scheduled on its device's drvq. That is a hot-unplug-of-busy-device condition; the periph need not be open.
Evidence in this pack
camqtest.cβ KLD harness driving the production, in-kernel camq_* primitives (no recompiled copies): T1 state proof, T2 canary OOB-write proof, T3 (DF-5551 insert-full), T4 deliberate panic.ptsim2.cβ contract-clean fake CAM SIM (all entry points hold the sim lock, unlike the DF-5462 rig) with delayed completions and aholdknob, used for the live xpt-path attempt.ptchurn.cβ userland churner.build.sh/run.shβ exact commands.run.logβ baseline T1/T2/T3 output on the stock kernel.panic.t4.baseline.txtβ stock-kernel panic in the camq_resize bcopy (Fatal trap 12, memmove+0x9d).panic.live.txtβ live xpt-path crash (xpt_remove_periph β camq_resize) if captured.fix.diffβ unconditionalnew_size < entriesrefusal (+ insert-side guard, DF-5551).fix_build.log,fix_run.logβ patched-kernel build + re-run showing the corruption is gone.
Build
sh build.sh # in guest: builds camqtest.ko, ptsim2.ko, ptchurn
Run (baseline, stock kernel)
kldload ./camqtest/camqtest.ko sysctl debug.camqtest.run=1 # T1: state proof (entries > array_size, no guard) sysctl debug.camqtest.run=2 # T2: canary heap-OOB-write proof sysctl debug.camqtest.run=3 # T3: insert-full OOB (DF-5551) sysctl debug.camqtest.run=4 # T4: deliberate panic (camq_resize bcopy fault)
Success criterion (baseline): T2/T3 print
N CANARY(IES) OVERWRITTEN -> heap OOB WRITE proven; T1 shows
entries=4 array_size=3; T4 panics in memmove from the resize path.
Run (live xpt path)
kldload ./ptsim2/ptsim2.ko # da0..da7 appear on ptsim20
sysctl debug.ptsim2.delay=5
# start many parallel readers on /dev/da1../dev/da7 (see run.sh)
sysctl debug.ptsim2.hold=1 # openings never return; periphs park on drvq
sysctl debug.ptsim2.ntargets=0 # targets gone
sysctl debug.ptsim2.rescan=1 # AC_LOST_DEVICE storm
# wait for "removing device entry", then kill the readers:
pkill dd # last close frees INVALID da periphs
# -> camperiphfree -> xpt_remove_periph
# -> camq_resize shrink-below-entries
Expected on unfixed kernel: Fatal trap 12 in memmove() with stack
camq_remove/xpt_remove_periph β camq_resize β memmove (see
panic.live.txt), or silent drvq corruption. On the fixed kernel: no
fault; camq_resize returns CAM_RESRC_UNAVAIL and the queue is untouched.
DF-5550 VERDICT β camq_resize shrink-below-entries heap OOB write
Status: reproduced (deterministic, production primitives) + live xpt-path crash
Root cause (path:line)
sys/bus/cam/cam_queue.c:111-138 β camq_resize():
#ifdef DIAGNOSTIC
if (new_size < queue->entries)
panic("camq_resize: New queue size can't accommodate "
"queued entries.");
#endif
new_array = kmalloc(new_size * sizeof(cam_pinfo *), M_CAMQ, M_INTWAIT|M_ZERO);
...
bcopy(queue->queue_array, new_array, queue->entries * sizeof(cam_pinfo *));
The precondition is enforced only under #ifdef DIAGNOSTIC. DIAGNOSTIC
is absent from stock kernels β including INVARIANTS builds like the audit
guest's X86_64_GENERIC (which sets INVARIANTS at config line 56 but has no
DIAGNOSTIC option; verified on-guest). On every such kernel a caller that
shrinks below entries gets:
- an OOB heap write β
bcopycopiesentries*8bytes into anew_size*8allocation; queue->entries > queue->array_sizepersisting afterward, so every later camq op (insert/remove/heap_up/heap_down/CAMQ_GET_HEAD) indexes past the array.
In-tree trigger β traced, and crashed live
camperiphfree() (cam_periph.c:573-602) frees a periph without
dequeueing it from device->drvq; nothing in the invalidate path does
(the only drvq removals are xpt_run_dev_allocq cam_xpt.c:3740 and the
bus-dereg drain cam_xpt.c:4416). xpt_remove_periph() (cam_xpt.c:1613)
then calls camq_resize(&device->drvq, array_size - 1) with no
queued-check β unlike xpt_release_device(), which panic-guards the
devq queues for exactly this class (cam_xpt.c:5210-5214). A periph dying
(AC_LOST_DEVICE: surprise removal / bus deregister) while it (or all its
siblings) are scheduled on drvq β the normal state of a busy device whose
queue depth is exhausted β trips the shrink with entries == array_size.
Observed live on the audit guest (stock kernel, no DIAGNOSTIC):
- Harness (deterministic, production in-kernel camq_* symbols):
- T1:
camq_resize(&q,3)with entries=4 βentries=4 array_size=3, no panic, slot-4 OOB read returns the pointer that the bcopy wrote past the buffer. - T2: resize(6β4) β canary chunk adjacent to the new M_CAMQ array
overwritten (
CORRUPTED at +0: e0 (was a5)) β heap OOB write proven, silent. - T4: resize(1β0) β
kmalloc(0)β NULL β bcopy faults:Fatal trap 12 ... memmove() at memmove+0x9d(panic.t4.baseline.txt) β a stock kernel crash straight inside camq_resize's bcopy. - Live xpt path: see panic.live.txt (if present) for the xpt_remove_periph-originated crash from the ptsim2 rig; the choreography (hold openings β removal storm β last-close free of a parked periph) is run-live.sh.
Connection to the DF-5462 lead (cross-file)
The DF-5462 rig's mass-attach panic
camq_remove+0x35: movl $0xffffffff,0x8(%r12) (removed_entry == NULL,
bounds check at cam_queue.c:174 passed, entries == index so lines
177-181 skipped) is the predicted aftermath of this corruption: after an
unguarded shrink, queue_array[entries] lives past the array (or in the
zeroed tail of the fresh M_ZERO allocation) and reads as NULL/garbage.
Note the DF-5462 rig itself entered the scan path without the sim lock
(ptsim.c:264-285), which widened the race windows; the underlying
camq_resize defect is reachable through the locked path traced above.
Exploitability assessment
Primitive: attacker-influenced 8-byte-granular OOB write of kernel heap pointers past an M_CAMQ allocation, plus a durable queue-corruption state that converts later queue operations into additional OOB reads/writes and NULL/garbage derefs. Trigger: unauthenticated-ish device removal churn (umass unplug, iscsi target loss, bus deregister) against busy devices β local DoS reliably; heap-grooming to uid=0 is theoretical (M_CAMQ-sized neighbor control requires grooming the 24/32-byte class adjacent to a victim object). Not pushed to uid=0 in this run; classified as memory-corruption (memcorrupt bucket) with reliable local DoS.
Fix validation
fix.diff: unconditionalif (new_size < queue->entries) return (CAM_RESRC_UNAVAIL);in camq_resize (+ insert-side guard, DF-5551).- Built as X86_64_GENERIC on the guest (fix_build.log), installed, rebooted; the exact baseline harness re-run shows the resize refused, no canary corruption, entries never exceeds array_size (fix_run.log).
- Baseline vs patched: baseline T2/T3 canary corruption β patched: all canaries intact; baseline T1 entries>array_size β patched: state untouched; baseline T4/panic path β not reachable post-fix (resize returns before any allocation/copy).
Recommended paired fix (cam_xpt side, out of this file's scope)
xpt_remove_periph() should dequeue a still-queued periph (or refuse the
shrink) the way xpt_release_device() guards the devq queues
(cam_xpt.c:5210-5214); camperiphfree() should dequeue from drvq before
calling it.
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
fixedfix.diff applied to guest /usr/src, X86_64_GENERIC rebuilt (fix_build.log: 'Kernel build completed'), installed, rebooted (kernel #1 12:03:51). Exact PoC re-run on the patched kernel: T2/T3 'all 64 canaries intact' (baseline: canary corrupted), T1 entries==array_size==4 (baseline: 4>3), and the T4 1->0 panic path is structurally unreachable (resize returns before kmalloc/bcopy). Baseline corruption GONE.
fix_run.log (patched T1-T3 output) vs run.log (baseline); fix_build.log; VERDICT.md 'Fix validation' section
Confirmed kernel references
Detail
Exploit chain
deterministic: kldload camqtest.ko; sysctl debug.camqtest.run=2 -> camq_resize(new_size=4, entries=6) bcopy overflows the 32-byte M_CAMQ chunk into the adjacent canary chunk (kernel-pointer-sized OOB write); run=4 -> camq_resize(1->0) panics in memmove (stock-kernel crash). In-tree: surprise removal / bus deregister of a busy device -> AC_LOST_DEVICE -> camperiphfree frees a periph still parked on device->drvq -> xpt_remove_periph shrinks drvq below entries -> OOB write + entries>array_size -> subsequent camq ops read/write past the array -> NULL/garbage deref (DF-5462 signature). uid=0 route: theoretical -- 8-byte-granular heap-pointer OOB write past an M_CAMQ (24/32-byte class) allocation plus a persistent corrupted-queue state; M_CAMQ-adjacent victim grooming not attempted this run; reliable local DoS demonstrated.
Evidence (decisive lines)
run.log lines 'T2: canary 63 ... CORRUPTED at +0' and 'T1 after resize: entries=4 array_size=3' (stock kernel); panic.t4.baseline.txt: 'Fatal trap 12 ... memmove() at memmove+0x9d' with stack camqtest_sysctl->memmove (the camq_resize bcopy); fix_run.log: same harness on patched kernel -> 'all 64 canaries intact', entries==array_size; VERDICT.md full narrative
PoC changes
harness + rig authored fresh for this finding (camqtest.c T1-T4; ptsim2.c contract-clean SIM with delayed completions + hold knob; ptchurn.c); the DF-5462 rig's unlocked-sysctl flaw was fixed (all entry points take pts_lock)
Verified recommended fix
camq_resize: unconditionally refuse new_size < queue->entries (return CAM_RESRC_UNAVAIL); pair with an xpt_remove_periph dequeue/guard so drvq capacity tracks real periph counts
Verdict
camq_resize()'s new_size>=entries precondition is guarded only by #ifdef DIAGNOSTIC (cam_queue.c:116-120), which is compiled out of every stock kernel (verified: audit guest X86_64_GENERIC sets INVARIANTS but not DIAGNOSTIC). A KLD harness driving the production in-kernel camq_* primitives proved deterministically on the unmodified guest kernel that (a) a shrink below entries silently leaves entries>array_size (T1), (b) the resize bcopy writes kernel heap pointers past the new M_CAMQ allocation -- adjacent canary chunk overwritten (T2), and (c) the 1->0 geometry faults straight inside the bcopy because kmalloc(0) returns NULL: baseline Fatal trap 12 at memmove+0x9d from the camq_resize copy (panic.t4.baseline.txt). The in-tree trigger is traced: camperiphfree (cam_periph.c:602) frees a periph without dequeueing it from device->drvq and xpt_remove_periph (cam_xpt.c:1613) then shrinks the drvq with no queued-check (unlike xpt_release_device's panic-guard at cam_xpt.c:5210-5214), so a periph dying of AC_LOST_DEVICE while scheduled (busy surprise-removed device) trips the overflow; the lingering entries>array_size state predicts exactly the DF-5462 lead panic signature (camq_remove+0x35 NULL-slot write). Live xpt-path end-to-end was attempted 4x with a contract-clean rig (ptsim2: delayed completions, hold knob, removal churn) and did not close the narrow free-while-parked window within budget -- the deterministic harness plus code trace plus baseline panic carry the finding. Fix validated on a rebuilt kernel: resize refused, canaries intact, entries never exceeds array_size.
No comments yet.