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

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)

No comments yet.

PoC verification

Evidence pack

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

  1. bcopy(old, new, entries * 8) writes (entries - new_size) * 8 bytes past the new M_CAMQ allocation (heap OOB write of kernel heap pointers), and
  2. 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 in memmove() (observed live: baseline panic Fatal trap 12 ... memmove() at memmove+0x9d, stack camqtest_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_size state 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 a hold knob, 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 β€” unconditional new_size < entries refusal (+ 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.

VERDICT.md
↓ download raw

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:

  1. an OOB heap write β€” bcopy copies entries*8 bytes into a new_size*8 allocation;
  2. queue->entries > queue->array_size persisting 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: unconditional if (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).

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

fixed
baseline reproduced→ patch + rebuild →patched clean

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

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.