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

cyapa detach/module-unload leaves open fds, blocked readers and kqueue hooks on a vanished softc β€” NULL-deref panic / dispatch through freed module ops

Summary

cyapa_detach never refuses to detach while fds are open, never wakes readers blocked in lksleep, and never destroy_dev's the cdev. read/write/kqfilter fetch the softc WITHOUT the NULL check that open/close/ioctl have. After kldunload, devclass clears the softc pointer, the open fd still holds the cdev (devfs does not drain fds), and the next read()/write()/kqueue dispatches through module ops memory freed by the unload into cyaparead, which lockmgrs on NULL - kernel panic; the post-unload dispatch through freed module ops is a control-flow-corruption candidate if the module memory is reused. Verified live on the guest: module builds/loads, sysctls register, no attach (no SMBus hw), clean unload. Refuse-EBUSY + wakeup + NULL-check fix.

Discussion (0)

No comments yet.

PoC verification

Evidence pack

findings/poc/DF-4227 Β· 11 files
FileTypeDescriptionSize
README.md β€” 2.8 KB ↓ raw
VERDICT.md β€” 4.1 KB ↓ raw
trigger_detach_uaf.c β€” 1.7 KB view raw
build.sh β€” 110 B view raw
run.sh β€” 140 B view raw
run.log β€” 695 B view raw
run.2.log β€” 851 B view raw
env.txt β€” 576 B view raw
fix.diff β€” 1.1 KB view raw
manifest.json β€” 1.5 KB view raw
verdict.json β€” 3.0 KB view raw

DF-4227 β€” cyapa: detach/module-unload leaves open fds, blocked readers and kqueue hooks on a vanished softc β†’ NULL-deref panic / dispatch through freed module ops

Where

sys/dev/smbus/cyapa/cyapa.c

  • cyapa_detach() β€” cyapa.c:545-571 β€” never checks sc->count (open fds), never wakes readers blocked in cyaparead (cyapa.c:749 lksleep), never destroy_dev()s the cdev made by make_dev() (cyapa.c:505-511); it only calls dev_ops_remove_minor() (cyapa.c:567).
  • cyaparead() β€” cyapa.c:616 fetches sc = CYAPA_SOFTC(minor(dev)) and cyapa.c:626 calls cyapa_lock(sc) β€” no NULL check (open/close/ioctl have one; read/write/kqfilter do not).
  • cyapawrite() β€” cyapa.c:789 β†’ cyapa.c:801 same missing check.
  • cyapakqfilter() β€” cyapa.c:1069 β†’ cyapa.c:1083 same missing check.

Why it faults (source-certain chain)

  1. Root opens /dev/cyapa0-67 (0600 root:wheel β€” Xorg on a laptop does).
  2. kldunload cyapa (module, not built into kernel β€” sys/dev/smbus/cyapa/Makefile KMOD) β†’ driver_module_handler MOD_UNLOAD (subr_bus.c:3648-3655) β†’ devclass_delete_driver β†’ device_detach β†’ cyapa_detach β†’ devclass_delete_device sets dc->devices[unit] = NULL (sys/kern/subr_bus.c:1164).
  3. The open fd still holds a cdev reference: devfs_destroy_dev_by_ops (devfs_core.c:863, worker at 1546) only unlinks the devfs node and drops refs β€” it does NOT drain open file descriptors.
  4. Next read()/write()/kqueue on the open fd: dev_dwrite/dev_dread dispatches through dev->si_ops (&cyapa_ops, a static struct inside the module β€” freed after unload) into cyaparead, which does CYAPA_SOFTC(minor) = devclass_get_softc(cyapa_devclass, unit) β†’ NULL (subr_bus.c:932-951) β†’ cyapa_lock(NULL) β†’ lockmgr(&sc->lk) on a near-NULL address β†’ kernel panic. If dispatch itself happens after module memory is freed/reused, this is a call through freed memory instead.
  5. A reader already blocked in lksleep(&sc->blocked, &sc->lk, ...) at cyapa.c:749 is never woken by detach and sleeps on a softc that dies with the driver.

Trigger (real hardware)

# on a machine with a Cypress APA trackpad (SMBus addr 0x67):
./trigger_detach_uaf &          # opens /dev/cyapa0-67, blocks in read()
sleep 1 && kldunload cyapa.ko   # panic in the reader

On this audit guest (QEMU)

The attack surface cannot exist: no SMBus controller, and cyapa children are only created for responding SMBus addresses (bus/smbus/smbus.c:120-140, unit = (smbus_unit << 11) | 0x400 | addr), so the driver never attaches and /dev/cyapa* never appears. Verified live β€” see run.log (module builds, loads, sysctls appear, no attach, unloads cleanly).

Build / run (guest, as root)

cc -O2 -Wall -o trigger_detach_uaf trigger_detach_uaf.c
./trigger_detach_uaf
VERDICT.md
↓ download raw

DF-4227 β€” VERDICT

Status: untested (kernel-side reproduction impossible on this guest β€” the device driver attack surface is physically absent; bug is source-certain and the mechanism is fully traced below).

What was verified, and how

  1. The module exists and is loadable, but never attaches on this guest. Live on the QEMU guest (run.log / guest-module-lifecycle.log): - make in /usr/src/sys/dev/smbus/cyapa builds cyapa.ko cleanly. - kldload succeeds; the four debug.cyapa_* sysctls appear (they are module-static, registered at load). - ls /dev/cyapa* β†’ ENOENT; pciconf -l shows no SMBus controller. - Reason (source): a cyapa child only exists if an SMBus address probe succeeded β€” smbus_probe_device() (sys/bus/smbus/smbus.c:120-140) adds a child with unit (smbus_unit << 11) | 0x0400 | addr only when smbus_trans() gets a response; cyapa_probe (cyapa.c:464) matches only unit 0x467. No controller + no trackpad β‡’ no attach β‡’ no devnode β‡’ the read()/write()/kqfilter paths cannot be entered.

  2. The NULL-deref chain is proven from source, line by line: - cyaparead cyapa.c:616 sc = CYAPA_SOFTC(minor(dev)); macro at cyapa.c:389-390 β†’ devclass_get_softc(cyapa_devclass, unit). - devclass_get_softc (sys/kern/subr_bus.c:932-951) returns NULL once devclass_delete_device (subr_bus.c:1152-1164) executed dc->devices[dev->unit] = NULL β€” which happens inside device_detach() during kldunload via devclass_delete_driver (subr_bus.c:3648-3655, detach loop at 879-887). - cyaparead then calls cyapa_lock(sc) at cyapa.c:626 with sc==NULL (no NULL check β€” unlike cyapaopen/cyapaclose/cyapaioctl) β†’ lockmgr(&sc->lk, …) dereferences NULL+offsetof(struct cyapa_softc, lk) β†’ page-0 fault β†’ panic. - Open fds are not drained by teardown: cyapa_detach calls only dev_ops_remove_minor (cyapa.c:567) β†’ devfs_destroy_dev_by_ops (sys/vfs/devfs/devfs_core.c:863, worker 1546-1560, devfs_destroy_dev_worker 1520-1546) which unlinks the node and drops refs, but the open file descriptor keeps the cdev alive (sys/vfs/devfs/devfs_vnops.c:1366 β†’ sys/kern/kern_device.c dev_dwrite dispatches via dev->si_ops β€” a static struct in the module image, freed by kldunload). - Readers blocked in lksleep(&sc->blocked, &sc->lk, PCATCH, "cyablk", 0) (cyapa.c:749) are never woken by cyapa_detach (it only wakes &sc->poll_flags), so they sleep against a dying softc.

  3. Trigger requirements on real hardware: root (device is 0600 root:wheel) to open the node and root to kldunload. The reader whose read() faults may be any process holding the fd (e.g. an fd inherited from Xorg by an unprivileged child). Severity accordingly Low (already-privileged trigger), memory-corruption class.

Exploit chain (not developed β€” hardware-gated)

  1. root: open /dev/cyapa0-67 (or inherit fd) β†’ 2. root: kldunload cyapa.ko β†’ 3. read(fd) β†’ NULL-deref panic (deterministic), or dispatch through freed module memory (if the ops slab is reused: control-flow candidate, 0x3000-byte module β€” reuse by kmalloc(M_DRM…)/network slabs is plausible on a loaded system but not demonstrated here).
  • cyapa_detach: return EBUSY while sc->count != 0 (open fds exist), wake blocked readers (wakeup(&sc->blocked)), destroy_dev(sc->devnode), lockuninit(&sc->lk).
  • cyaparead/cyapawrite/cyapakqfilter: NULL-check sc like the other entry points. See fix.diff.

Fix validation

Not possible on this guest (device cannot attach; the fix touches attach/ detach paths that are unreachable in QEMU). fix_status = not_testable.

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 validation requires the cyapa device to attach (detach/read paths unreachable in QEMU); fix.diff is line-accurate against the verified root cause and applies cleanly to sys/.

findings/poc/DF-4227/fix.diff (git apply --check clean)
↓ fix.diffper-fix-DF-4227

Confirmed kernel references

Detail

Exploit chain

root opens /dev/cyapa0-67 -> kldunload cyapa.ko -> read(fd) on inherited/held descriptor -> lockmgr(&sc->lk) with sc==NULL -> panic (deterministic); alternative post-unload dispatch through freed module ops memory is a control-flow candidate not demonstrated.

Evidence (decisive lines)

run.log (guest module lifecycle: load OK, no /dev/cyapa*, unload OK), run.2.log (trigger stops at open ENOENT), VERDICT.md (line-by-line chain), fix.diff (applies clean vs sys/)

PoC changes

n/a - pack authored fresh; trigger_detach_uaf.c written from scratch for this finding

Verified recommended fix

cyapa_detach: return EBUSY while sc->count != 0, wakeup(&sc->blocked); add NULL sc checks to cyaparead/cyapawrite/cyapakqfilter (see fix.diff)

Verdict

Source-certain NULL-deref/UAF: after kldunload cyapa (or devctl detach) an open /dev/cyapa fd dispatches into cyaparead/cyapawrite/cyapakqfilter which fetch the softc via devclass_get_softc (NULL after subr_bus.c:1164) and immediately lockmgr on it (cyapa.c:626) -> kernel panic; dispatch itself is through module ops freed by the unload. Not exercisable on the QEMU audit guest: no SMBus controller and cyapa children are only created for responding addresses (smbus.c:120-140), so the driver never attaches and /dev/cyapa never exists - verified live (module builds, loads, sysctls register, no attach, clean unload; see run.log). Trigger on real hardware needs root for kldunload; severity Low (memory-corruption class, privileged trigger).