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)
PoC verification
Evidence pack
findings/poc/DF-3941 Β· 12 files| File | Type | Description | Size | |
|---|---|---|---|---|
| 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 commandsbuild.log/run.logβ full untrimmed guest outputenv.txtβ guest uname + hardware-absence evidence + patched-source prooffix.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
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 thecrit_enter()block (cxm.c:2244-2260) covering thetsleep()and the snapshot ofbuffers_available; - the entire consumption loop (fixup +
uiomove, cxm.c:2262-2288) and theenc_pool.offset/enc_pool.readbookkeeping (cxm.c:2290-2296) run unlocked; enc_pool.offsetis a singlesize_tshared 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
- sys/dev/video/cxm/cxm.c:2211-2299 β cxm_read(), no serialization
- sys/dev/video/cxm/cxm.c:2270 β underflowing subtraction
- sys/dev/video/cxm/cxm.c:2276 β unbounded uiomove after clamp
- sys/dev/video/cxm/cxm.c:1119-1121 β unlocked reset racing readers
- sys/dev/video/cxm/cxm.c:2158-2161, 1885-1886 β fd-sharing not prevented, 0444 device
- sys/dev/video/cxm/cxm.c:1801-1829 β uninitialized DMA buffers
- sys/dev/video/cxm/cxm.h:66,70,74,183 β size_t size, ring, shared offset, CXM_SG_SEGMENT
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_testablefix.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)
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.
No comments yet.