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

atkbdc drain/poll loops reset their budget on every received byte with no overall cap β€” unbounded kernel busy-loop under i8042/AUX data flood (empty_both_buffers got the cap, these four did not)

Summary

empty_kbd_buffer and empty_aux_buffer reset t=wait whenever ANY_BUFFER_FULL is set, and the two read_*_no_wait loops run while the opposite channel's buffer stays full - a controller/EC that keeps presenting data wedges the calling kernel thread indefinitely. The file itself proves the scenario is real: empty_both_buffers carries an explicit waited cap added because 'Intel/IBM blades will hang in this procedure'; the four siblings never got it. LIVE-verified the affected layer is unpriv-reachable: /dev/psm0 is 0666 and uid 1999 drove ioctls through psm -> send_aux_command -> the atkbdc poll chain. Flood needs hostile EC/host device model. Port-the-cap fix.

Discussion (0)

No comments yet.

PoC verification

Evidence pack

findings/poc/DF-4415 Β· 10 files
FileTypeDescriptionSize
psm_reach.c β€” 3.0 KB view raw
build.sh β€” 97 B view raw
run.sh β€” 118 B view raw
build.log β€” 486 B view raw
run.log β€” 486 B view raw
VERDICT.md β€” 2.8 KB ↓ raw
README.md β€” 1.8 KB ↓ raw
fix.diff β€” 1.7 KB view raw
verdict.json β€” 2.5 KB view raw
env.txt β€” 585 B view raw

DF-4415 β€” atkbdc.c unbounded polling loops under i8042/AUX data flood (missing the timeout cap that empty_both_buffers has)

Build

cc -O2 -Wall -o /tmp/psm_reach DF-4415/psm_reach.c

Run (as an unprivileged user)

/tmp/psm_reach

What it demonstrates (reproduced on the audit guest)

  • /dev/psm0 is created world read/write (crw-rw-rw-) β€” any local user reaches the psm driver and, through it, the atkbdc controller layer (atkbdc0 backing psm0 per dmesg).
  • MOUSE_GETHWINFO and MOUSE_SETMODE succeed for uid 1999 β€” MOUSE_SETMODE drives psm β†’ send_aux_command β†’ write_aux_command β†’ wait_while_controller_busy() etc., i.e. the atkbdc busy-wait pollers, on every call. Worst observed round-trip 7.4 ms with the healthy QEMU i8042 (the pollers only burn their full 70-200 ms Γ— 3 retries when the controller is slow/wedged).

What it cannot demonstrate from guest userspace

The genuinely unbounded loops need the controller/EC to keep presenting data faster than the code drains it (hostile EC, or a VM host's i8042 device model). Guest userspace has no port-I/O path (/dev/io is root-only crw-------), so the flood cannot be injected from inside the guest; those loops are:

  • empty_kbd_buffer() β€” sys/dev/misc/kbd/atkbdc.c:865-882 (t = wait reset on every byte)
  • empty_aux_buffer() β€” atkbdc.c:905-922 (same pattern)
  • read_kbd_data_no_wait() β€” atkbdc.c:799-804 (while (f == KBDS_AUX_BUFFER_FULL))
  • read_aux_data_no_wait() β€” atkbdc.c:838-843 (while (f == KBDS_KBD_BUFFER_FULL))

empty_both_buffers() (atkbdc.c:934-978) carries an explicit overall cap (waited, added for "Intel/IBM blades" that exhibit exactly this controller-flood hang) β€” the four siblings above never got the same treatment.

VERDICT.md
↓ download raw

DF-4415 β€” VERDICT

status: not_reproduced for the unbounded loop itself (needs controller/EC data flood β€” hardware injection); the unprivileged reachability of the affected code paths WAS reproduced live

What was reproduced on the guest (DF 6.5-DEVELOPMENT, atkbdc0+psm0, QEMU i8042)

  • /dev/psm0 is world read/write (crw-rw-rw-), /dev/kbd0 /dev/kbd1 are root-only.
  • As uid 1999: open("/dev/psm0") OK; MOUSE_GETHWINFO returns a real PS/2 mouse (buttons=5, iftype=3, hwid=4 β€” psm0: model IntelliMouse Explorer, device ID 4 on atkbdc0); 20Γ— MOUSE_SETMODE all succeed, worst latency 0.0074 s.
  • Every MOUSE_SETMODE reprograms the aux device through send_aux_command() (atkbdc.c:647-670) β†’ write_aux_command() (:616) β†’ write_controller_command() (:586) β†’ wait_while_controller_busy() (:417-439) and wait_for_aux_ack() (:545-569) β€” the DELAY()-busy poll chain. On a healthy controller this is single-digit milliseconds; the code burns up to ~70-200 ms Γ— KBD_MAXRETRY(3) per command when the controller stalls (TOTALDELAY-bounded β€” a CPU-burn amplifier for any unprivileged user spamming psm ioctls, but bounded).

The actual defect (not demonstrable from guest userspace)

Four drain loops reset their budget on every received byte and have no overall cap, so a controller that keeps streaming data wedges the kernel thread indefinitely:

  • empty_kbd_buffer() atkbdc.c:865-882 β€” t = wait; on each byte
  • empty_aux_buffer() atkbdc.c:905-922 β€” same
  • read_kbd_data_no_wait() atkbdc.c:799-804 β€” while (f == KBDS_AUX_BUFFER_FULL) keeps reading
  • read_aux_data_no_wait() atkbdc.c:838-843 β€” mirror image

The maintainer-acknowledged precedent is in the same file: empty_both_buffers() atkbdc.c:959-966 has a waited cap with the comment "Some systems (Intel/IBM blades) do not have keyboard devices and will hang in this procedure. Time out … to avoid this hang". The four siblings never received the cap.

Threat model

Malicious/slow EC or i8042 device model (embedded coreboot ECs, VM hosts β€” the file's own quirk table documents "imperfect i8042 emulation" coreboot ECs at atkbdc.c:112-128), or a controller wedge during unprivileged ioctl storms through /dev/psm0. Impact: permanent kernel busy-loop (thread wedge / CPU starvation) β†’ Low (hardware precondition).

Fix

Port the waited cap to the two empty_*_buffer() functions and bound the two read_*_no_wait() drain loops. See fix.diff.

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 not meaningful on this guest: the unbounded behavior needs a controller data flood that guest userspace cannot inject; a patched-kernel build would show no behavioral difference.

fix.diff (waited caps + bounded drains)
↓ fix.diffper-fix-DF-4415

Confirmed kernel references

Detail

Evidence (decisive lines)

run.log (unpriv reachability reproduced); VERDICT.md (four uncapped loops vs the capped sibling); dmesg in env.txt (atkbdc0/psm0 present)

PoC changes

MOUSE_GETINFO/mouseinfo_t (FreeBSD-isms) -> MOUSE_GETHWINFO/mousehw_t per DFly sys/mouse.h

Verified recommended fix

Port empty_both_buffers' overall 'waited' cap to empty_kbd_buffer/empty_aux_buffer and bound the read_*_no_wait drain loops

Verdict

The unprivileged REACHABILITY of the atkbdc poll layer reproduced live: uid 1999 opened world-writable /dev/psm0, MOUSE_GETHWINFO returned the real PS/2 mouse (buttons=5 iftype=3 hwid=4), and 20x MOUSE_SETMODE drove psm -> send_aux_command -> wait_while_controller_busy/wait_for_aux_ack (worst 7.3ms on the healthy QEMU i8042) - see run.log. The DEFECT itself (unbounded loops when the controller floods data: empty_kbd_buffer :865-882, empty_aux_buffer :905-922, read_kbd_data_no_wait :799-804, read_aux_data_no_wait :838-843 - none carries the 'waited' cap that empty_both_buffers :959-966 has for exactly this scenario) cannot be exercised from guest userspace: injecting an i8042 data flood needs hardware/host-level access (/dev/io is root-only). Hardware-preconditioned local DoS (Low).