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

cxm(4) REMOTE_GETKEY: cxm_ir_key() tsleeps 50 ms inside an unserialized ioctl while cxm_detach frees the softc and destroys sc->iicbus β€” use-after-free (IR-path twin of DF-4753/DF-3941 family)

Summary

The IR path parks 50ms in tsleep with the softc only stack-referenced while detach tears down the iic children and frees the softc - the waker then dispatches through a dangling device_t (compounded by DF-4983's permanent no-op invalidation). Third instance of the driver-wide no-locking family, covered by neither sibling. Unpriv trigger on the 0444 cdev; root kldunload frees. Hardware-gated. Lock-drain fix (subsumed by the pending DF-4753 fix).

Discussion (0)

No comments yet.

PoC verification

Evidence pack

findings/poc/DF-5212 Β· 12 files
FileTypeDescriptionSize
README.md β€” 2.8 KB ↓ raw
VERDICT.md β€” 4.8 KB ↓ raw
cxm_ir_getkey.c β€” 1.7 KB view raw
build.sh β€” 208 B view raw
run.sh β€” 172 B view raw
build.log β€” 18.0 KB view raw
fix_build.log β€” 17.9 KB view raw
run.log β€” 613 B view raw
env.txt β€” 199 B view raw
fix.diff β€” 3.9 KB view raw
manifest.json β€” 1.1 KB view raw
verdict.json β€” 4.7 KB view raw

DF-5212 β€” cxm(4) REMOTE_GETKEY ioctl vs detach use-after-free (cxm_ir.c path)

What

cxm_ir_key() (sys/dev/video/cxm/cxm_ir.c:100-117) β€” reached from the unprivileged REMOTE_GETKEY ioctl (cxm.c:2894-2898, _IOR('x', 71, struct{u_char[3]}), ioctl_bt848.h:266-269) on the world-readable 0444 /dev/cxmN cdev (cxm.c:1884-1885, no priv_check anywhere in cxm_ioctl, cxm.c:2306) β€” can sleep 50 ms inside the kernel (tsleep(..., hz/20) at cxm_ir.c:114) with the softc referenced only from the sleeping thread's stack, then perform a second i2c transaction on sc->iicbus (cxm_ir.c:116). cxm_detach() (cxm.c:1944-2004) deletes the iicbus children (cxm.c:1993-1996) and frees the softc without draining in-flight ioctls β€” dev_ops_remove_minor (cxm.c:1957) only dead-ectors new calls β€” so a concurrent kldunload cxm during the 50 ms window leaves the waker touching freed memory (use-after-free read of sc->iicbus + method dispatch on a dangling device_t). Third instance of the driver-wide no-locking family already filed as DF-3941 (read path) and DF-4753 (msp34xxx audio path); this file/path was not covered by either.

Build

On the QEMU DragonFly guest (see env.txt):

# trigger
cc -O2 -Wall -o /root/df5212/cxm_ir_getkey /root/df5212/cxm_ir_getkey.c
# baseline + patched module builds (both rc=0, -Werror):
cp -r /usr/src/sys/dev/video/cxm /root/df5212/base && (cd /root/df5212/base && make)   # build.log
# apply fix.diff to cxm.c/cxm.h in /usr/src/sys/dev/video/cxm, then:
cp -r /usr/src/sys/dev/video/cxm /root/df5212/fix && (cd /root/df5212/fix && make)     # fix_build.log

Run

On a host with a Hauppauge-style Conexant iTVC15/16 card (none exists on this guest):

/root/df5212/cxm_ir_getkey &                 # victim: parks >= 50 ms per ioctl
while :; do kldunload cxm; kldload cxm; done # root: the freeing half

Expected (on hardware): panic within a few unload iterations β€” the sleeper resumes into the freed softc, reads sc->iicbus, and dispatches through the destroyed iicbus device's vtable.

Observed (this guest): run.log β€” trigger exits 3 hardware absent: open(/dev/cxm0): No such file or directory; kldload cxm succeeds (rc=0) but no /dev/cxm* appears and no vendor-0x4444 PCI device exists, so the victim ioctl cannot be instantiated. Runtime reproduction is hardware-gated (same gate as DF-4753, established independently there). The structural defect is proven by source trace in VERDICT.md.

Fix

fix.diff β€” serialize the cdev ioctl entry against cxm_detach with a softc lock (drained in detach). Subsumed by the pending DF-4753 fix (which wraps the whole cxm_ioctl switch); compile-validated here as the full cxm.ko module with -Werror (fix_build.log, rc=0).

VERDICT.md
↓ download raw

DF-5212 β€” VERDICT

Status: inconclusive (hardware-gated). Impact if triggered: panic / UAF (memcorrupt family). Confidence in the structural defect: certain.

The path, line by line

  1. Unprivileged entry. /dev/cxmN is created 0444 root:wheel (sys/dev/video/cxm/cxm.c:1884-1885), so any local user can open it read-only. cxm_ioctl (cxm.c:2306) contains no priv_check/ capability check anywhere and takes no lock (the driver has none β€” struct cxm_softc, cxm.h:216-264, has no lock member).

  2. The sleeping leg. case REMOTE_GETKEY: (cxm.c:2894-2898) casts ap->a_data to struct bktr_remote * and calls cxm_ir_key(sc, (char *)remote, sizeof(*remote)). Buffer math is sound: the ioctl is _IOR('x', 71, struct bktr_remote) with a 3-byte payload (sys/dev/video/bktr/ioctl_bt848.h:266-269), DragonFly's sys_ioctl allocates/zeros/copyouts exactly IOCPARM_LEN(cmd) bytes (sys/kern/sys_generic.c:668,696,730), and iicbb_read fills exactly len bytes (sys/bus/iicbus/iicbb.c:409-424) β€” no OOB, no uninitialized disclosure on this path. cxm_ir_key (cxm_ir.c:100-117) runs the i2c read; on failure it tsleep(&sc->iicbus, 0, "IR", hz / 20) β€” 50 ms (cxm_ir.c:114) β€” then re-reads through sc->iicbus (cxm_ir.c:116). Every iteration of the trigger loop parks the thread β‰₯ 50 ms in-kernel holding nothing but a stack reference to sc.

  3. The freeing leg (privileged). kldunload cxm β†’ cxm_detach (cxm.c:1944-2004): dev_ops_remove_minor (cxm.c:1957) only dead-ectors new ioctl dispatches (kern_conf.c semantics, per the DF-4753 verification); in-flight sleepers are not drained. Detach then frees the DMA maps (cxm.c:1962-1976), detaches/deletes the iic children (cxm.c:1993-1996) β€” destroying the very sc->iicbus device the sleeper will use at cxm_ir.c:116 β€” and releases the softc's resources.

  4. The collision. The waker dereferences freed softc memory (sc->iicbus at cxm_ir.c:104,116), calls iicbus_start on a dangling device_t (NULL/garbage vtable dispatch β†’ panic; compounding hazard: sc->iicbus invalidation in cxm_iic_detach is already a no-op, DF-4983, so the pointer dangles even without winning the 50 ms race precisely).

Same structural class as DF-4753 (msp34xxx path, verified on this same guest) and DF-4419 (bktr twin); this is the cxm IR path, not covered by either finding.

Why not reproduced here

The QEMU guest has no Conexant iTVC15/16 PCI capture card: kldload cxm returns 0 but /dev/cxm* never appears and pciconf -l shows no vendor-0x4444 device (run.log). The victim ioctl therefore cannot be instantiated; the race cannot be exercised. This is the identical gate DF-4753 hit with the audio path.

Exploit ceiling (on hardware)

Unprivileged victim thread (any user) + privileged unload loop (root). The freeing half is privileged, hence severity Low. With a controlled heap reclaim of the freed softc before wake, code-exec is theoretically reachable; not developed (hardware-gated).

Fix validation

fix.diff (softc lock + ioctl entry serialization + detach drain β€” minimal standalone subset of the pending DF-4753 fix, which wraps the whole cxm_ioctl switch and thereby covers this REMOTE_GETKEY path as well) compiles clean as the full cxm.ko module with -Werror (fix_build.log, rc=0) against guest source 6.5-DEVELOPMENT. Behavioral before/after requires cxm hardware; baseline module also builds rc=0 (build.log). /usr/src/sys/dev/video/cxm restored pristine after the build (sha256 re-verified, run.log).

Negative results (classes hunted and killed in this file)

  • Decoder/pulse/key-table OOB: cxm_ir.c has no decoder, tables or maps β€” it returns 3 raw i2c bytes; parsing lives in userland. Killed.
  • cdev read/buf sizes: REMOTE_GETKEY 3-byte OUT payload matches kernel copyout size exactly (sys_generic.c:668,730); iicbb_read writes exactly len (iicbb.c:409-424); received ≀ len and is never used as an index. Killed.
  • Uninitialized-memory leak via partial read: the IOC_OUT buffer is pre-zeroed by sys_ioctl (sys_generic.c:696) and iicbb fills all 3 bytes on the success path. Killed.
  • cxm_ir_init (cxm_ir.c:85-96): attach-time 1-byte read into unsigned char key[1], bounded. Killed.
  • Sleep-DoS: each REMOTE_GETKEY parks β‰₯ 50 ms (cxm_ir.c:114); unbounded looping is ordinary per-thread sleep cost, no global resource held. Not filed.

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

inconclusive
baseline no→ patch + rebuild →patched clean

fix.diff (cxm.h lock member + lockinit + cxm_ioctl wrapper around cxm_ioctl_locked + detach drain + lockuninit) compiles clean as the full cxm.ko module (-Werror, rc=0) on guest 6.5-DEVELOPMENT source (fix_build.log); baseline module also rc=0 (build.log). Behavioral validation requires cxm hardware absent from this guest.

fix_build.log (patched module rc=0); build.log (baseline rc=0); fix.diff; run.log tail (pristine-src sha256 re-verified after restore)
↓ fix.diffper-fix-DF-5212

Confirmed kernel references

Detail

Exploit chain

unprivileged: open /dev/cxmN O_RDONLY (0444) -> ioctl(REMOTE_GETKEY) in a loop, each iteration parking >= 50 ms at cxm_ir.c:114; privileged: 'while :; do kldunload cxm; kldload cxm; done' frees the softc mid-sleep; on wake: UAF read of sc->iicbus + iicbus_start method dispatch on a destroyed device_t -> panic. Code-exec ceiling exists via controlled reclaim of the freed softc before wake but was not developed: hardware-gated and the freeing half is privileged (severity Low).

Evidence (decisive lines)

VERDICT.md (line-by-line trace incl. the buffer-math negative results); run.log (trigger exit 3, kldload cxm rc=0, no /dev/cxm*, no vendor-0x4444 device, pristine-src sha256 re-verify); build.log vs fix_build.log (baseline and lock-patched cxm.ko both rc=0, -Werror); fix.diff (softc lock + detach drain; subsumed by pending DF-4753 fix); cxm_ir_getkey.c (victim half; REMOTE_GETKEY constant via sys/ioccom.h _IOR, matches ioctl_bt848.h:269)

PoC changes

Authored fresh (no seed): REMOTE_GETKEY defined via _IOR('x',71,struct bktr_remote) from sys/ioccom.h so userland/kernel encodings cannot drift (kernel: ioctl_bt848.h:269); trigger exits 3 with an explicit hardware-gated message when /dev/cxm0 is absent.

Verified recommended fix

Serialize cdev ioctls against cxm_detach (softc lock drained in detach) - see fix.diff; fully subsumed by the pending DF-4753 fix.

Verdict

Structural defect certain by source trace; runtime reproduction hardware-gated. cxm_ir_key() (sys/dev/video/cxm/cxm_ir.c:100-117) is reached from the unprivileged REMOTE_GETKEY ioctl (cxm.c:2894-2898) on the 0444 /dev/cxmN cdev (cxm.c:1884-1885; zero priv_check/locks in cxm_ioctl, cxm.c:2306). On a failed i2c read it tsleeps 50 ms (cxm_ir.c:114) with the softc referenced only from the sleeping thread's stack, then transacts again on sc->iicbus (cxm_ir.c:116). cxm_detach (cxm.c:1944-2004) deletes the iic children (cxm.c:1993-1996) and frees the softc without draining in-flight ioctls - dev_ops_remove_minor (cxm.c:1957) only dead-vectors NEW calls - so kldunload cxm during the 50 ms window makes the waker read freed memory and dispatch through a dangling device_t (UAF -> panic; compounded by DF-4983's no-op iicbus invalidation). Third instance of the driver-wide no-locking family (DF-3941 read path, DF-4753 msp34xxx audio path) - the IR path was covered by neither. On this guest: trigger compiles and runs but exits 3 'hardware absent' - kldload cxm rc=0 yet no /dev/cxm* and no vendor-0x4444 PCI device (run.log), same gate DF-4753 hit. Buffer-math hunt on the same path is negative: _IOR 3-byte payload == kernel copyout size (sys_generic.c:668,696,730) and iicbb_read fills exactly len (iicbb.c:409-424).