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)
PoC verification
Evidence pack
findings/poc/DF-3589 Β· 12 files| File | Type | Description | Size | |
|---|---|---|---|---|
| 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().
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
-
Unprivileged write is real. Every node is registered
CTLTYPE_INT | CTLFLAG_RW | CTLFLAG_ANYBODY(e.g. psm.c:6050-6056multiplicator, 6101-6107div_min, 6111-6117div_max).sysctl_root()(sys/kern/kern_sysctl.c:1446) skips theSYSCAP_NOSYSCTL_WRprivilege check exactly whenCTLFLAG_ANYBODYis set. -
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). -
The divisor is a 32-bit product of two such values. psm.c:4307-4309 clamps
div_xinto[div_min, div_max](both attacker-chosen), and psm.c:4337/4338 divides / takes remainder bydiv_x * multiplicator. Withdiv_min = div_max = 65536the clamp yieldsdiv_x = 65536regardless of thediv_len * div_max_x / lenseed;65536 * 65536is2^32β wraps to 0 inintβidivby zero β #DE β kernel panic. -
The trap is reachable on the first movement packet.
window_minis likewise ANYBODY-writable in [1,10] (psm.c:5761-5766); set to 1,psmsmoother()reaches the division as soon assmoother->queue_len >= 1(psm.c:4262 passes on the first queued packet). Finger pressure only needs to clearmin_pressure(default 32; also ANYBODY-writable in [0,255], psm.c:5732-5736). -
Machine-level proof on the guest toolchain.
divzero_expr.ccontains 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
- sys/dev/misc/psm/psm.c:4336-4344 β
/ (div_x * multiplicator)and% (div_x * multiplicator)(the trap) - sys/dev/misc/psm/psm.c:4307-4309 β div_x clamp into attacker range
- sys/dev/misc/psm/psm.c:5767-5780 β
arg < 1-only validation for MULTIPLICATOR/DIV_MIN/DIV_MAX/DIV_MAX_NA/DIV_LEN - sys/dev/misc/psm/psm.c:6048-6056, 6099-6117 β ANYBODY registration of multiplicator/div_min/div_max (elantech flavor: 7209-7211)
- sys/kern/kern_sysctl.c:1446 β ANYBODY bypasses write privilege check
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 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)
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.
No comments yet.