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

cxm(4): cxm_read() consumes the DMA ring with no serialization β€” a stale ring offset paired with a different buffer underflows `size - offset` (size_t) and uiomove()s past the 61440-byte DMA buffer; read/write desync also discloses never-DMA'd (uninitialized) kernel heap via read(2)

Summary

/dev/cxmN is 0444 and only a second open is refused, so one fd shared across fork() gives unprivileged processes concurrent cxm_read() callers. The consumption loop and the enc_pool offset/read/write bookkeeping run entirely unlocked. A stale offset of buffer i paired with a later buffer j makes `bufs[current].size - offset` underflow, clamped only by uio_resid, and uiomove leaks up to uio_resid bytes of kernel heap past the 61440-byte buffer; desync variants also return never-DMA'd bus_dmamem memory (never zeroed). Hardware-gated at runtime (needs a CX23416 card); guest has none - source-proven, fix compile-validated. Any local user with read access on a machine with a Hauppauge PVR-250/350 and capture active: repeated racing reads disclose kernel heap.

Discussion (0)

No comments yet.

PoC verification

Evidence pack

findings/poc/DF-3941 Β· 12 files
FileTypeDescriptionSize
cxm_read_race.c β€” 3.9 KB view raw
build.sh β€” 105 B view raw
run.sh β€” 99 B view raw
build.log β€” 272 B view raw
run.log β€” 564 B view raw
env.txt β€” 210 B view raw
fix.diff β€” 2.6 KB view raw
fix_build.log β€” 909 B view raw
VERDICT.md β€” 4.6 KB ↓ raw
README.md β€” 1.5 KB ↓ raw
verdict.json β€” 4.0 KB view raw
manifest.json β€” 1.3 KB view raw

DF-3941 β€” cxm(4): unlocked ring consumption in cxm_read() β†’ size_t underflow β†’ OOB kernel-heap read via read(2)

File: sys/dev/video/cxm/cxm.c (pass 2) Severity: Medium Β· Confidence: likely Β· Hardware-gated (needs a CX23415/CX23416 capture card to drive the DMA ring)

Build

cc -O2 -Wall -o /tmp/cxm_read_race cxm_read_race.c

(guest: DragonFly 6.5-DEVELOPMENT, cc 8.3 β€” builds clean)

Run

/tmp/cxm_read_race

Expected

  • On real hardware (PVR-250/350, capture running): the forked racing readers trigger the ring-state desync; success criterion = read() data that cannot come from the DMA ring (non-MPEG-framed bulk data / kernel heap contents), or reads of never-DMA'd buffers.
  • On the audit QEMU guest: open("/dev/cxm0"): No such file or directory β€” the guest has no Conexant PCI device (verified: no 14f1:8800/8802 on the PCI bus, no cxm module, no /dev/cxm*). This is the documented, honest outcome of the run on this machine (see run.log).

Contents

  • cxm_read_race.c β€” trigger source (forked racing readers, partial-read offsets, anomaly detector)
  • build.sh / run.sh β€” exact commands
  • build.log / run.log β€” full untrimmed guest output
  • env.txt β€” guest uname + hardware-absence evidence + patched-source proof
  • fix.diff β€” git-apply-able fix (driver lock + defensive offset clamp), compile-validated against the guest kernel tree (fix_build.log)
  • VERDICT.md, manifest.json, verdict.json
VERDICT.md
↓ download raw

DF-3941 VERDICT β€” cxm_read() ring race

Bottom line

inconclusive at runtime (hardware absent on the audit guest); the defect is proven at source level. The guest has no Conexant CX23415/16 PCI device, so the trigger cannot execute past open("/dev/cxm0") here. The analysis below traces the bug line-by-line and shows why it is real on any machine with the card.

The bug

/dev/cxmN is created world-readable (make_dev(..., 0444, ...), cxm.c:1885-1886) and cxm_open() refuses only a second open (sc->is_opened, cxm.c:2158-2161). One open file descriptor shared across fork() therefore gives any unprivileged user N concurrent cxm_read() callers on the same softc β€” and nothing in the driver serializes them:

  • the only synchronization in cxm_read() is the crit_enter() block (cxm.c:2244-2260) covering the tsleep() and the snapshot of buffers_available;
  • the entire consumption loop (fixup + uiomove, cxm.c:2262-2288) and the enc_pool.offset/enc_pool.read bookkeeping (cxm.c:2290-2296) run unlocked;
  • enc_pool.offset is a single size_t shared by the whole ring (cxm.h:74), and the pair (offset, read) is stored non-atomically at cxm.c:2290-2296.

Primitive 1 β€” offset/buffer mismatch β†’ size_t underflow β†’ OOB read

A partial read of buffer i leaves offset = X < size_i paired with read = i. A racing context that then consumes buffer i fully stores offset = 0 and advances read β†’ j. A third context that read sc->enc_pool.offset (cxm.c:2262) before that store but evaluates the loop initializer i = sc->enc_pool.read (cxm.c:2264) after the advance holds the mismatched pair (X, j) and computes

nbytes = sc->enc_pool.bufs[current].size - offset;   /* cxm.c:2270 */

which underflows (both size_t, cxm.h:66) whenever X > size_j (buffer sizes are per-DMA-request values, min(req.size, CXM_SG_SEGMENT), cxm.c:1485-1487 β€” they differ between buffers and need not be multiples of anything). The only clamp is against the user's request length (cxm.c:2273-2274), so

uiomove(sc->enc_pool.bufs[current].vaddr + offset, nbytes, ap->a_uio);  /* :2276 */

copies up to uio_resid bytes starting at vaddr + X β€” past the 61440-byte (CXM_SG_SEGMENT, cxm.h:183) bus_dmamem_alloc'd DMA buffer β€” into userspace: a kernel-heap disclosure via read(2).

Primitive 2 β€” read/write desync β†’ uninitialized kernel heap disclosure

Two racing readers can both snapshot buffers_available and both advance sc->enc_pool.read (cxm.c:2293-2296), pushing read past write (subsequent buffers_available = write - read (+CXM_SG_BUFFERS), cxm.c:2257-2259). The same desync arises when METEORCAPTUR STOP_CONT/CONTINOUS (cxm.c:2555-2569) races an in-flight read: cxm_start_encoder() resets offset/read/write (cxm.c:1119-1121) while the reader's stale locals overwrite them afterwards. read() then returns buffers never written by DMA β€” the buffers are bus_dmamem_alloc()'d and never zeroed (cxm.c:1806-1829), i.e. leftover kernel heap.

Both primitives are the same root cause: the ring state machine is unserialized.

Why it did not run here

[-] open(/dev/cxm0): No such file or directory

Guest checks (run.log / env.txt): no 14f1:8800/8802 PCI device, no cxm KLD, no /dev/cxm*, driver not in X86_64_GENERIC. The full trigger is built and kept in this pack for hardware-equipped verification.

Fix validation

fix.diff adds a driver struct lock serializing the ring consumption and the two state resets (start_encoder, close), plus a defensive offset > bufs[i].size clamp. Compile-validated on the guest's kernel tree with -Werror (fix_build.log: cxm.ko relinked, rc=0). Runtime baseline/patched comparison is impossible without the card β€” fix_status: not_testable for that reason only.

Kernel references

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

fix.diff (driver lock + defensive offset clamp) applied to guest /usr/src and the cxm KLD rebuilt with -Werror rc=0 (fix_build.log). Runtime baseline/patched comparison impossible: no CX23416 hardware on the guest.

fix_build.log (patched cxm.c compiled + cxm.ko relinked, CXM_FIX_MAKE_RC=0); fix.diff is git-apply-able (git apply --check passed on the audit tree)
↓ fix.diffper-fix-DF-3941

Confirmed kernel references

Detail

Exploit chain

unprivileged open(/dev/cxm0) (mode 0444) -> fork N readers on the shared fd -> start capture (read auto-start or METEORCAPTUR) -> race partial-read offset saves vs full-consumption/read-advance (or race METEORCAPTUR STOP/CONTINOUS against an in-flight read) -> size/buffer mismatch -> size_t underflow at cxm.c:2270 -> uiomove OOB read past 61440-byte DMA buffer into kernel heap -> disclosure to userspace (repeatable). No write primitive identified.

Evidence (decisive lines)

VERDICT.md (line-by-line interleaving); run.log (open ENOENT on guest + hardware-absence checks); fix_build.log (patched cxm.ko builds clean); fix.diff (lock + defensive clamp)

PoC changes

Seed sketch rewritten: full trigger program with fork-shared-fd racing readers, partial-read offset generation, MPEG-framing anomaly detector, and honest no-hardware path

Verified recommended fix

Serialize ring consumption (driver lock around cxm_read consumption + start_encoder/close resets) and clamp offset to bufs[i].size before the subtraction (fix.diff)

Verdict

Hardware-gated: the audit QEMU guest has no Conexant CX23415/16 PCI device (no 14f1:8800/8802, no cxm KLD, no /dev/cxm*), so the trigger stops at open(ENOENT) β€” runtime reproduction inconclusive on this machine. The defect itself is source-proven: cxm_read() (sys/dev/video/cxm/cxm.c:2211-2299) consumes the DMA ring with no serialization; enc_pool.offset is a single shared size_t (cxm.h:74) saved non-atomically with the read pointer (cxm.c:2290-2296). Racing readers on one fork-shared fd (open() is world-readable, cxm.c:1885-1886, and only a second OPEN is refused, cxm.c:2158) β€” or a METEORCAPTUR restart racing an in-flight read (reset at cxm.c:1119-1121) β€” pair a stale offset X with a different buffer j, making bufs[current].size - offset (cxm.c:2270) underflow (size_t); the only clamp is uio_resid (cxm.c:2273), so uiomove (cxm.c:2276) copies up to uio_resid bytes past the 61440-byte DMA buffer => kernel-heap disclosure via read(2). The simpler desync makes read() return never-DMA'd, never-zeroed bus_dmamem_alloc buffers (cxm.c:1806). Fix compile-validated (-Werror) on the guest kernel tree.