World-writable /dev/psm0 with zero privilege checks in psmioctl(): any local user can reprogram the PS/2 device, hold it hostage from moused/X (root gets EBUSY), and starve the shared i8042 keyboard port
Summary
psmattach() creates the cdev mode 0666 and psmioctl() performs no privilege check on any command. REPRODUCED live on the audit guest as uid 1001: open (enables the aux device and arms IRQ12), MOUSE_SETMODE reprogramming of rate/resolution (persisted across calls, verified via MOUSE_GETMODE), MOUSE_SETLEVEL to PSM_LEVEL_NATIVE, MOUSE_READDATA controller<->device transactions. Opens are exclusive, so an unprivileged holder makes root's open() fail with EBUSY - moused(8)/X/syscons denied the mouse indefinitely. Every mutating ioctl runs block_mouse_data() which disables the keyboard port + interrupt on the shared i8042: measured 6.8-7.2 ms per call, so a tight unprivileged loop keeps the console keyboard effectively dead (50-call hammer = 6.84 ms/call downtime). 56 ioctls, 0 failures. Additionally 1535+ monitor-injected PS/2 events with concurrent unprivileged readers exercised the full interrupt path without a panic (negative result for the hardirq-lockmgr hunt). No memory corruption, no escalation. Fix: make_dev 0600 (or priv_check on mutating ioctls).
Discussion (0)
PoC verification
Evidence pack
findings/poc/DF-3590 Β· 14 files| File | Type | Description | Size | |
|---|---|---|---|---|
| README.md | β | 3.4 KB | β raw | |
| VERDICT.md | β | 4.8 KB | β raw | |
| psm_ioctl_poc.c | β | 4.7 KB | view raw | |
| hold_probe.c | β | 499 B | view raw | |
| mouse_reader.c | β | 1.0 KB | view raw | |
| inject_mouse.py | β | 1.1 KB | view raw | |
| build.sh | β | 143 B | view raw | |
| run.sh | β | 146 B | view raw | |
| build.log | β | 270 B | view raw | |
| run.log | β | 1.1 KB | view raw | |
| env.txt | β | 778 B | view raw | |
| fix.diff | β | 303 B | view raw | |
| manifest.json | β | 1011 B | view raw | |
| verdict.json | β | 3.3 KB | view raw |
DF-3590 β /dev/psm0 is world-writable (0666) and psmioctl() has no privilege check
What
psmattach() creates the device node world read/write
(sys/dev/misc/psm/psm.c:2025):
sc->cdev = make_dev(&psm_ops, unit, 0, 0, 0666, "psm%d", unit);
psmioctl() (psm.c:2629-2935) contains not one priv_check() /
capability check. Consequently any local user can:
- open the device β
psmopen()enables the PS/2 aux device and arms IRQ12 (psm.c:1211, 1243-1246): an unprivileged user toggles shared hardware interrupt state; - reprogram the pointing device β
MOUSE_SETMODEchanges sampling rate, resolution, acceleration and operation level, issuing real i8042/aux command sequences (psm.c:2720-2805, 2788-2795); - force raw level β
MOUSE_SETLEVELtoPSM_LEVEL_NATIVE(2) breaks the userspace protocol contract for a running moused(8)/X (psm.c:2811-2816); - transact with the controller β
MOUSE_READSTATE/MOUSE_READDATAsendPSMC_SEND_DEV_STATUS/PSMC_SEND_DEV_DATA(psm.c:2831-2844); - deny the device to everyone β the driver allows a single open
(
psm_cdev_open, psm.c:2222-2224:PSM_OPENβEBUSY), so an unprivileged process that holds the fd open forever excludes moused(8)/X/syscons from the mouse β root gets EBUSY; - starve the console keyboard β every mutating ioctl funnels through
block_mouse_data()(psm.c:2539-2575), which disables the keyboard port and keyboard interrupt on the shared i8042 for the duration of the controller transaction. Measured on the guest: ~6.8 ms of keyboard-disabled time per ioctl; a tight unprivileged loop keeps the shared controller (and keyboard input) continuously disabled.
Reproduced on the audit guest (uid 1001 maxx)
$ ./psm_ioctl_poc uid=1001 opened /dev/psm0 (O_RDWR): OK -- device is now ENABLED (aux IRQ12 armed) MOUSE_SETMODE(rate=20,res=LOW): OK (7.0 ms of i8042 transaction, kbd port disabled during it) MOUSE_SETLEVEL(NATIVE=2): OK MOUSE_READDATA: OK, device returned 3 bytes: 08 00 00 [after attacks ] level=2 rate=20 resolution=-2 accelfactor=0 packetsize=4 PERSISTED: unprivileged user changed mouse sampling rate to 20 50x MOUSE_SETMODE hammer: 340.5 ms total (6.81 ms per call of kbd-controller downtime) == 56 ioctls issued as uid 1001, 0 failures == # while maxx holds the device open: # root open(/dev/psm0): Device busy <-- moused(8)/X would fail to start
Additionally the interrupt path was stress-exercised by injecting 1535+ QEMU PS/2 mouse events through the monitor socket while two unprivileged reader threads held the device open (irq12 count +1598 in 25 s) β guest stayed healthy (this was the hunt for a hardirq/lockmgr race; negative, see VERDICT.md).
Build / run (on the DragonFly guest)
cc -O2 -Wall -o psm_ioctl_poc psm_ioctl_poc.c cc -O2 -Wall -o hold_probe hold_probe.c ./psm_ioctl_poc # phases 1-6 ./psm_ioctl_poc hold & # then run ./hold_probe as root: EBUSY
Fix
See fix.diff β restrict the node to root (0600), matching how the rest
of the input stack (syscons, kbd) is only usable through privileged
daemons on multi-user systems. moused(8) and X run privileged on
DragonFly today, so the permission change costs nothing functionally;
alternatively add priv_check(td, PRIV_DRIVER) to the mutating ioctls if
read access for unprivileged clients must be retained.
DF-3590 β VERDICT
Status: reproduced. Impact: unprivileged local DoS (console input stack). Confidence: certain.
What was run, as whom, on what
Guest: DragonFly dfbsd 6.5-DEVELOPMENT #0 X86_64_GENERIC (stock
INVARIANTS kernel), QEMU/KVM with emulated i8042 + PS/2 mouse;
psm0: model IntelliMouse Explorer, device ID 4; /dev/psm0 mode
crw-rw-rw- root:wheel. PoC built and executed entirely as uid 1001
(maxx), no groups beyond its own.
Reproduced observations (each mapped to code)
- Open by unprivileged user β
psm_cdev_openhas no permission gate beyond the devfs mode 0666 (psm.c:2208-2239);psmopenβdoopenenables the aux device and turns on IRQ12 (psm.c:1211, 1243-1253). Observed: open O_RDWR succeeded. - Device reprogramming persisted:
MOUSE_SETMODEwith rate=20 β laterMOUSE_GETMODEreportsrate=20(kernel state written at psm.c:2797-2802; controller transaction viaset_mouse_sampling_rate). Observed. - Level forced to NATIVE β
MOUSE_SETLEVEL(psm.c:2811-2816),level=2 packetsize=4afterwards; a subsequent moused(8) would mis-parse the byte stream. Observed. - Controller transaction
MOUSE_READDATAreturned live device data (08 00 00). Observed. - Exclusive-hold denial: while uid 1001 held the fd open, root
open()βDevice busy(single-open enforcement psm.c:2222-2224). moused(8)/X/syscons cannot acquire the mouse until the unprivileged holder releases β a trivially-scriptable, persistent, unprivileged console-DoS. Observed (hold_probe). - Shared-controller/keyboard downtime: each mutating ioctl runs
block_mouse_data()(psm.c:2539-2575) which setsKBD_DISABLE_KBD_PORT | KBD_DISABLE_KBD_INTfor the whole i8042 transaction, thenunblock_mouse_data()restores. Measured: 6.8-7.0 ms perMOUSE_SETMODE; 50 back-to-back calls = 340 ms during which the keyboard port isεε€ disabled. An unprivileged tight loop keeps the console keyboard effectively dead. Observed (timing).
What was hunted and killed (negative results worth recording)
- Memory safety on the ioctl paths: every copyout source is a
kernel-resident struct written under
sc->lock(or constants);MOUSE_READSTATE/MOUSE_READDATAboundsdata->lenagainst the array size with an unsigned compare that also rejects negatives (psm.c:2834-2835). No OOB. psmintrbyte-append overflow:pb->ipacket[16]can hold at mostmode.packetsize(β€ 6, from the vendortype table psm.c:710-748) because appends only happen whileinputbytes < packetsize(psm.c:3044); theKKASSERTat psm.c:3036 never fires for any packetsize the driver can legitimately have;MOUSE_SETMODEcannot writesc->mode.packetsize(psm.c:2797-2802 copies only rate/resolution/accelfactor/level).- Ring buffers:
psmsoftintrqueue insertion (psm.c:5231-5241) andpsmreadwrap-copy (psm.c:2514-2533) are correctly bounded. - hardirq β
lockmgrrace (psmintr callingpsmsoftintrdirectly from IRQ12 context, psm.c:3157 β lockmgr at 4955 on the sleepablesc->lock): stress-tested with 1535+ monitor-injected PS/2 events while two reader threads held the device (irq count +1598/25 s): no panic, no wedge. Uncontended acquisition from hardirq evidently works on this kernel; the hazard remains architectural but is not demonstrably exploitable β not filed as a finding. MOUSE_SETLEVELunlocked int write racing psmintr/psmsoftintr: aligned int, no tearing, no memory-safety consequence.
Exploit chain
Not a memory-corruption bug β no escalation path. Chain is the finding
itself: unprivileged user opens, reprograms, and/or holds /dev/psm0,
breaking mouse and degrading keyboard input for the whole console until
administrator intervention.
Fix validation
Baseline reproduced (above). Kernel-rebuild validation of the one-line
make_dev permission change was not performed (Low-severity permission
finding, not a memory-corruption class; fix semantics are devfs-level
and fully determined by the diff). fix.diff passes
git apply --check.
Kernel references
- sys/dev/misc/psm/psm.c:2025 β
make_dev(... 0666 ...) - sys/dev/misc/psm/psm.c:2629-2935 β
psmioctl(no priv_check anywhere) - sys/dev/misc/psm/psm.c:2720-2805 β MOUSE_SETMODE reprograms device
- sys/dev/misc/psm/psm.c:2811-2816 β MOUSE_SETLEVEL unlocked level flip
- sys/dev/misc/psm/psm.c:2539-2575 β block_mouse_data disables keyboard
- sys/dev/misc/psm/psm.c:2222-2224 β single-open EBUSY denial
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_testableBaseline reproduced. The fix is a devfs-permission change (make_dev 0666->0600, psm.c:2025); rebuild validation skipped per contract (not a memory-corruption finding). With 0600 the unprivileged open fails at devfs and every demonstrated phase is unreachable.
findings/poc/DF-3590/fix.diff (git apply --check OK)
Confirmed kernel references
Detail
Exploit chain
unprivileged open(/dev/psm0) [0666, no perm check] -> (a) hold fd -> EBUSY for moused/X/syscons (persistent mouse DoS); (b) ioctl hammer -> ~7ms keyboard-port-disable per call on the shared i8042 (console input starvation); (c) MOUSE_SETMODE/MOUSE_SETLEVEL -> persistent device misconfiguration breaking the userspace mouse protocol.
Evidence (decisive lines)
run.log (full unprivileged run: 56 ioctls, 0 failures, rate persisted, root 'Device busy'); VERDICT.md (observation->code mapping + negative results); env.txt (guest kernel, device mode crw-rw-rw-, model IntelliMouse Explorer)
PoC changes
Seed sketch used MOUSE_SETRATE/MOUSE_SETRESOLUTION which are '#if 0 notyet' in sys/mouse.h (those psm.c ioctl branches are dead code); rewrote to the live MOUSE_SETMODE path; added hold/EBUSY phase, ioctl timing, and root-side hold_probe.
Verified recommended fix
Create /dev/psm0 with mode 0600 (or add priv_check on mutating ioctls if unprivileged read access must be kept)
Verdict
Reproduced live on the QEMU guest as uid 1001: /dev/psm0 is created 0666 and psmioctl() has no privilege check, so an unprivileged user opened the device (arming the aux device + IRQ12), reprogrammed sampling rate/resolution via MOUSE_SETMODE (state persisted, verified with MOUSE_GETMODE), forced PSM_LEVEL_NATIVE, performed MOUSE_READDATA controller transactions, and measured ~6.8ms of shared-i8042 keyboard-disabled time per ioctl (tight loop = console keyboard starvation). Holding the fd open denied the device to root (EBUSY) -- moused(8)/X/syscons cannot use the mouse while an unprivileged user holds it. No memory corruption; no escalation path. Also stress-injected 1535+ PS/2 events via QEMU monitor with concurrent readers: no panic (hardirq lockmgr race killed as a finding).
No comments yet.