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

Unprivileged kernel panic: CTLFLAG_ANYBODY psm sysctls allow the smoother divisor product div_x*multiplicator to wrap to zero (divide-by-zero in psmsmoother)

Summary

hw.psm.synaptics.* (and elantech.*) are registered CTLFLAG_RW|CTLFLAG_ANYBODY - writable by any local user. synaptics_sysctl() validates only arg >= 1 with no upper bound for multiplicator/div_min/div_max/div_max_na/div_len. psmsmoother() then divides and takes remainder by the 32-bit product div_x*multiplicator where div_x is clamped into the attacker-chosen [div_min,div_max]. div_min=div_max=multiplicator=65536 makes the product 2^32, which wraps to 0; the emitted idiv raises #DE - an unrecoverable kernel panic on the first touchpad movement packet. Values persist, so every touchpad use re-panics the machine until an admin resets the nodes. VERIFIED: trigger.sh (real-hardware procedure) + divzero_expr.c - the exact kernel expression compiled by the guest's clang, run as uid 1001: product prints 0, division traps SIGFPE exit 136 (the userland face of the same idiv #DE; corroborated in guest /var/log/messages 'exited on signal 8'). Hardware-gated not_reproduced on this guest (its pointer is a plain IntelliMouse Explorer - the ANYBODY sysctl tree is never instantiated; verified 'unknown oid'). Any local user on any Synaptics/Elantech-equipped machine: persistent unprivileged kernel DoS via three sysctl writes plus one finger movement. Fix: bound the tunables + widen divisor to 64-bit.

Discussion (0)

No comments yet.

PoC verification

Evidence pack

findings/poc/DF-3589 Β· 12 files
FileTypeDescriptionSize
README.md β€” 3.6 KB ↓ raw
VERDICT.md β€” 5.2 KB ↓ raw
divzero_expr.c β€” 1.2 KB view raw
trigger.sh β€” 721 B view raw
build.sh β€” 98 B view raw
run.sh β€” 191 B view raw
build.log β€” 223 B view raw
run.log β€” 461 B view raw
env.txt β€” 617 B view raw
fix.diff β€” 1.5 KB view raw
manifest.json β€” 984 B view raw
verdict.json β€” 3.7 KB view raw

DF-3589 β€” Unprivileged kernel panic: CTLFLAG_ANYBODY psm sysctls allow integer overflow to a zero divisor in psmsmoother()

What

hw.psm.synaptics.* (and the hw.psm.elantech.* flavor) sysctls are registered with CTLFLAG_ANYBODY β€” any local user may write them (sys/kern/kern_sysctl.c:1446 skips SYSCAP_NOSYSCTL_WR for ANYBODY nodes). The write validator synaptics_sysctl() (sys/dev/misc/psm/psm.c:5708-5816) only enforces arg >= 1 for multiplicator, div_min, div_max, div_max_na, div_len (psm.c:5767-5780) β€” no upper bound.

The motion smoother then computes (psm.c:4307-4309):

div_x = div_len * div_max_x / len;
div_x = imin(div_max_x, div_x);
div_x = imax(div_min, div_x);          /* div_min <= div_x <= div_max */

and divides by the 32-bit product (psm.c:4336-4344):

smoother->squelch_x += smoother->avg_dx;
dxp = smoother->squelch_x / (div_x * multiplicator);
smoother->squelch_x = smoother->squelch_x % (div_x * multiplicator);

With div_min = div_max = 65536 and multiplicator = 65536 the product div_x * multiplicator is 2^32, which wraps to 0 in signed 32-bit arithmetic (UB at the C level; on the shipped clang/gcc codegen it is a plain imul followed by idiv). idiv with a zero divisor raises #DE β€” in kernel context an immediate, unrecoverable panic. The % on line 4338 traps identically.

psmsmoother() runs from proc_synaptics() (psm.c:3639) and proc_elantech() (psm.c:4804), i.e. on touchpad movement packets, for any finger whose pressure β‰₯ syninfo.min_pressure. Setting hw.psm.synaptics.window_min=1 (allowed range 1..10) makes the very first movement packet reach the division.

Who

Any local user on any machine whose psm(4) probe found a Synaptics touchpad, an Elantech touchpad, or a Synaptics-on-active-mux device β€” i.e. the overwhelming majority of laptops. No privileges, no special groups (the whole point of ANYBODY was letting desktop users tune sensitivity β€” the same knob lets them overflow the divisor).

Reproduce (real hardware)

./trigger.sh        # sysctl writes as the unprivileged user
# then move a finger on the touchpad
# -> kernel panics: divide by zero in psmsmoother

Reproduce (this audit's QEMU guest) β€” NOT possible

The guest's emulated PS/2 device probes as "IntelliMouse Explorer" (generic vendortype), so enable_synaptics()/enable_elantech() never succeeded, the ANYBODY sysctl tree was never created, and the smoother path is unreachable:

$ sysctl hw.psm.synaptics.multiplicator=65536
sysctl: unknown oid 'hw.psm.synaptics.multiplicator'

What WAS verified on the guest is the arithmetic itself, with the exact expression compiled by the guest's own clang and executed in userland (divzero_expr.c, build/run below): the product wraps to 0 and the division traps with SIGFPE (exit 136, "Floating point exception (core dumped)") β€” the userland manifestation of the same idiv #DE that panics the kernel. The kernel-side control flow up to the division (level of indirection removed: value ranges, clamps, and call ordering) is cited line-by-line in VERDICT.md.

Build / run (guest, unprivileged)

cc -O2 -Wall -o divzero_expr divzero_expr.c
./divzero_expr ; echo exit=$?
# div_x=65536 multiplicator=65536
# int32 product div_x*multiplicator = 0 (wrapped; 2^32 == 0)
# Floating point exception (core dumped)   exit=136

Fix

Bound the ANYBODY-writable values that feed the divisor (see fix.diff): cap multiplicator, div_min, div_max, div_max_na, div_len at 100000 and/or widen the divisor product to 64-bit in psmsmoother().

VERDICT.md
↓ download raw

DF-3589 β€” VERDICT

Status: not_reproduced on this guest (hardware-gated); mechanism proven to the trap level with the guest's own toolchain. Confidence in the bug: certain. Impact class: unprivileged local kernel panic (DoS).

Why it did not reproduce on this guest

psmsmoother() is only called from proc_synaptics() (sys/dev/misc/psm/psm.c:3639) and proc_elantech() (psm.c:4804). Those parsers run only when the probe identified the device as MOUSE_MODEL_SYNAPTICS or MOUSE_MODEL_ELANTECH, and β€” decisive for the sysctl attack vector β€” the hw.psm.synaptics.* sysctl tree that makes the values unprivileged-writable is created only inside a successful enable_synaptics() probe (psm.c:6741-6744, synaptics_sysctl_create_tree) or elantech_init_synaptics() (psm.c:7209-7211).

The QEMU guest's i8042 aux device answers the IntelliMouse-Explorer probe (dmesg: psm0: model IntelliMouse Explorer, device ID 4), so:

$ sysctl hw.psm.synaptics.multiplicator=65536
sysctl: unknown oid 'hw.psm.synaptics.multiplicator'
$ sysctl -N hw.psm.synaptics
sysctl: unknown oid 'hw.psm.synaptics'

No node to write, no smoother to run β€” not_reproduced on this guest is an honest hardware-precondition result, not a refutation. QEMU does not emulate a Synaptics touchpad, so no guest configuration short of patching the kernel would instantiate the tree.

Why the mechanism is certain anyway

  1. Unprivileged write is real. Every node is registered CTLTYPE_INT | CTLFLAG_RW | CTLFLAG_ANYBODY (e.g. psm.c:6050-6056 multiplicator, 6101-6107 div_min, 6111-6117 div_max). sysctl_root() (sys/kern/kern_sysctl.c:1446) skips the SYSCAP_NOSYSCTL_WR privilege check exactly when CTLFLAG_ANYBODY is set.

  2. Validation is lower-bound-only. synaptics_sysctl() case block psm.c:5767-5780: MULTIPLICATOR, WEIGHT_, DIV_MIN, DIV_MAX, DIV_MAX_NA, DIV_LEN, VSCROLL_DIV_ β†’ if (arg < 1) return (EINVAL); Any value up to INT_MAX (2147483647) is accepted and stored (psm.c:5813).

  3. The divisor is a 32-bit product of two such values. psm.c:4307-4309 clamps div_x into [div_min, div_max] (both attacker-chosen), and psm.c:4337/4338 divides / takes remainder by div_x * multiplicator. With div_min = div_max = 65536 the clamp yields div_x = 65536 regardless of the div_len * div_max_x / len seed; 65536 * 65536 is 2^32 β†’ wraps to 0 in int β†’ idiv by zero β†’ #DE β†’ kernel panic.

  4. The trap is reachable on the first movement packet. window_min is likewise ANYBODY-writable in [1,10] (psm.c:5761-5766); set to 1, psmsmoother() reaches the division as soon as smoother->queue_len >= 1 (psm.c:4262 passes on the first queued packet). Finger pressure only needs to clear min_pressure (default 32; also ANYBODY-writable in [0,255], psm.c:5732-5736).

  5. Machine-level proof on the guest toolchain. divzero_expr.c contains the exact expression with the exact attacker values, compiled by the guest's clang and run as the unprivileged user:

div_x=65536 multiplicator=65536 int32 product div_x*multiplicator = 0 (wrapped; 2^32 == 0) Floating point exception (core dumped) exit=136

exit 136 = 128+8 = SIGFPE, the userland face of the same idiv #DE that panics the kernel. The compiler cannot elide the division (the divisor is a runtime product), so the kernel codegen is the same imul/idiv sequence.

Exploit chain

ssh user@laptop β†’ sysctl hw.psm.synaptics.multiplicator=65536 + div_min=65536 + div_max=65536 + window_min=1 (all permitted to unprivileged users by CTLFLAG_ANYBODY) β†’ wait for/touch the touchpad β†’ kernel divide-by-zero panic. Values persist across the panic reboot-free relogin (sysctl is runtime), so the panic repeats on every touchpad use until an admin resets the nodes: a persistent, unprivileged, touchpad-DoS-to-panic. No memory corruption; privesc is not in scope for this class (fixed-point arithmetic trap).

Fix validation

Not possible on this guest (node tree absent), so fix_status is not_testable rather than fixed. fix.diff bounds the five divisor- feeding nodes at 100000 (well above any sane touchpad tuning, far below overflow: 100000^2 = 10^10 needs 64 bits, hence the companion 64-bit widening of the product in psmsmoother for defense in depth).

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

Baseline cannot be produced on this guest (sysctl tree absent), so patched-vs-baseline kernel builds were not performed. fix.diff bounds the five divisor-feeding sysctls at 100000 and widens the divisor product to int64 in psmsmoother(); with either half applied a zero divisor is impossible (div_x>=1, multiplicator>=1, 64-bit product >= 1). git apply --check passes.

findings/poc/DF-3589/fix.diff (git apply --check OK)
↓ fix.diffper-fix-DF-3589

Confirmed kernel references

Detail

Exploit chain

unprivileged 'sysctl hw.psm.synaptics.multiplicator=65536 div_min=65536 div_max=65536 window_min=1' (CTLFLAG_ANYBODY bypasses SYSCAP_NOSYSCTL_WR, kern_sysctl.c:1446) -> touch/move finger on touchpad -> psmsmoother() computes squelch_x / (65536*65536) -> int32 product wraps to 0 -> idiv #DE -> kernel panic; values persist so every subsequent touch re-panics until admin resets the nodes.

Evidence (decisive lines)

run.log (divzero_expr output: product=0, SIGFPE exit 136, plus 'unknown oid' proof the guest lacks the nodes); VERDICT.md (line-by-line kernel-side argument: value ranges, clamps, reachability on packet #1); fix.diff (git apply --check OK)

PoC changes

Authored from scratch (no seed): divzero_expr.c isolates the exact kernel expression for toolchain-level proof; trigger.sh is the real-hardware procedure.

Verified recommended fix

Bound multiplicator/div_min/div_max/div_max_na/div_len at 100000 in synaptics_sysctl() and compute the smoother divisor as int64 (fix.diff)

Verdict

Not reproducible ON THIS GUEST: the attack needs the CTLFLAG_ANYBODY sysctl tree hw.psm.synaptics. (or hw.psm.elantech.), which is created only after a successful Synaptics/Elantech probe (psm.c:6741-6744, 7209-7211); the QEMU i8042 emulates a plain IntelliMouse Explorer, so the nodes do not exist (verified: 'sysctl: unknown oid') and psmsmoother() never runs. The mechanism itself is proven to the machine-code level with the guest's own toolchain: the divisor expression from psm.c:4337 (div_x clamped into [div_min,div_max] times multiplicator, all ANYBODY-writable with only an 'arg < 1' lower bound, psm.c:5767-5780) with div_min=div_max=multiplicator=65536 wraps the int32 product to exactly 0 and the idiv traps -- userland SIGFPE exit 136 on the identical expression (divzero_expr.c), kernel equivalent is an unrecoverable divide-by-zero panic on the first touchpad movement packet (window_min=1 also writable). On any Synaptics/Elantech laptop this is a persistent unprivileged kernel panic; trigger.sh provides the hardware procedure.