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

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)

No comments yet.

PoC verification

Evidence pack

findings/poc/DF-3590 Β· 14 files
FileTypeDescriptionSize
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:

  1. 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;
  2. reprogram the pointing device β€” MOUSE_SETMODE changes sampling rate, resolution, acceleration and operation level, issuing real i8042/aux command sequences (psm.c:2720-2805, 2788-2795);
  3. force raw level β€” MOUSE_SETLEVEL to PSM_LEVEL_NATIVE (2) breaks the userspace protocol contract for a running moused(8)/X (psm.c:2811-2816);
  4. transact with the controller β€” MOUSE_READSTATE/MOUSE_READDATA send PSMC_SEND_DEV_STATUS/PSMC_SEND_DEV_DATA (psm.c:2831-2844);
  5. 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;
  6. 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.

VERDICT.md
↓ download raw

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)

  1. Open by unprivileged user β†’ psm_cdev_open has no permission gate beyond the devfs mode 0666 (psm.c:2208-2239); psmopen β†’ doopen enables the aux device and turns on IRQ12 (psm.c:1211, 1243-1253). Observed: open O_RDWR succeeded.
  2. Device reprogramming persisted: MOUSE_SETMODE with rate=20 β†’ later MOUSE_GETMODE reports rate=20 (kernel state written at psm.c:2797-2802; controller transaction via set_mouse_sampling_rate). Observed.
  3. Level forced to NATIVE β†’ MOUSE_SETLEVEL (psm.c:2811-2816), level=2 packetsize=4 afterwards; a subsequent moused(8) would mis-parse the byte stream. Observed.
  4. Controller transaction MOUSE_READDATA returned live device data (08 00 00). Observed.
  5. 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).
  6. Shared-controller/keyboard downtime: each mutating ioctl runs block_mouse_data() (psm.c:2539-2575) which sets KBD_DISABLE_KBD_PORT | KBD_DISABLE_KBD_INT for the whole i8042 transaction, then unblock_mouse_data() restores. Measured: 6.8-7.0 ms per MOUSE_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_READDATA bounds data->len against the array size with an unsigned compare that also rejects negatives (psm.c:2834-2835). No OOB.
  • psmintr byte-append overflow: pb->ipacket[16] can hold at most mode.packetsize (≀ 6, from the vendortype table psm.c:710-748) because appends only happen while inputbytes < packetsize (psm.c:3044); the KKASSERT at psm.c:3036 never fires for any packetsize the driver can legitimately have; MOUSE_SETMODE cannot write sc->mode.packetsize (psm.c:2797-2802 copies only rate/resolution/accelfactor/level).
  • Ring buffers: psmsoftintr queue insertion (psm.c:5231-5241) and psmread wrap-copy (psm.c:2514-2533) are correctly bounded.
  • hardirq β†’ lockmgr race (psmintr calling psmsoftintr directly from IRQ12 context, psm.c:3157 β†’ lockmgr at 4955 on the sleepable sc->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_SETLEVEL unlocked 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

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 reproduced→ patch + rebuild →patched clean

Baseline 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)
↓ fix.diffper-fix-DF-3590

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).