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)
PoC verification
Evidence pack
findings/poc/DF-4227 Β· 11 files| File | Type | Description | Size | |
|---|---|---|---|---|
| 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
cyapa_detach()β cyapa.c:545-571 β never checkssc->count(open fds), never wakes readers blocked incyaparead(cyapa.c:749lksleep), neverdestroy_dev()s the cdev made bymake_dev()(cyapa.c:505-511); it only callsdev_ops_remove_minor()(cyapa.c:567).cyaparead()β cyapa.c:616 fetchessc = CYAPA_SOFTC(minor(dev))and cyapa.c:626 callscyapa_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)
- Root opens
/dev/cyapa0-67(0600 root:wheel β Xorg on a laptop does). kldunload cyapa(module, not built into kernel βsys/dev/smbus/cyapa/MakefileKMOD) βdriver_module_handlerMOD_UNLOAD (subr_bus.c:3648-3655) βdevclass_delete_driverβdevice_detachβcyapa_detachβdevclass_delete_devicesetsdc->devices[unit] = NULL(sys/kern/subr_bus.c:1164).- 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. - Next
read()/write()/kqueueon the open fd:dev_dwrite/dev_dreaddispatches throughdev->si_ops(&cyapa_ops, a static struct inside the module β freed after unload) intocyaparead, which doesCYAPA_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. - 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
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
-
The module exists and is loadable, but never attaches on this guest. Live on the QEMU guest (run.log / guest-module-lifecycle.log): -
makein/usr/src/sys/dev/smbus/cyapabuildscyapa.kocleanly. -kldloadsucceeds; the fourdebug.cyapa_*sysctls appear (they are module-static, registered at load). -ls /dev/cyapa*β ENOENT;pciconf -lshows 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 | addronly whensmbus_trans()gets a response;cyapa_probe(cyapa.c:464) matches only unit0x467. No controller + no trackpad β no attach β no devnode β the read()/write()/kqfilter paths cannot be entered. -
The NULL-deref chain is proven from source, line by line: -
cyapareadcyapa.c:616sc = 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 oncedevclass_delete_device(subr_bus.c:1152-1164) executeddc->devices[dev->unit] = NULLβ which happens insidedevice_detach()duringkldunloadviadevclass_delete_driver(subr_bus.c:3648-3655, detach loop at 879-887). -cyapareadthen callscyapa_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_detachcalls onlydev_ops_remove_minor(cyapa.c:567) βdevfs_destroy_dev_by_ops(sys/vfs/devfs/devfs_core.c:863, worker 1546-1560,devfs_destroy_dev_worker1520-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.cdev_dwritedispatches viadev->si_opsβ a static struct in the module image, freed bykldunload). - Readers blocked inlksleep(&sc->blocked, &sc->lk, PCATCH, "cyablk", 0)(cyapa.c:749) are never woken bycyapa_detach(it only wakes&sc->poll_flags), so they sleep against a dying softc. -
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)
- 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).
Recommended fix
cyapa_detach: return EBUSY whilesc->count != 0(open fds exist), wake blocked readers (wakeup(&sc->blocked)),destroy_dev(sc->devnode),lockuninit(&sc->lk).cyaparead/cyapawrite/cyapakqfilter: NULL-checksclike 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_testableFix 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)
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).
No comments yet.