Xi: validate modifier values in ProcXIPassiveUngrabDevice

ProcXIPassiveUngrabDevice() passes client-supplied modifier values
directly to DeletePassiveGrabFromList() without validating them.

DeletePassiveGrabFromList() may reach DeleteDetailFromMask() which uses
BITCLEAR(mask, modifier). The mask is allocated as 8 Mask words (256
bits). A modifier value > 255 causes BITCLEAR to index mask[modifier>>5]
past the allocation, resulting in a controlled single-bit-clear at an
attacker-chosen heap offset.

Add the same modifier validation that CheckGrabValues() uses: reject
modifiers that are not XIAnyModifier and have bits set outside
AllModifiersMask.

This vulnerability was discovered by:
  Anonymous working with TrendAI Zero Day Initiative

ZDI-CAN-32366

CVE-2026-93523

Signed-off-by: Peter Hutterer <peter.hutterer@who-t.net>
Assisted-by: Claude:claude-opus-4-6
Part-of: <https://gitlab.freedesktop.org/xorg/xserver/-/merge_requests/2341>
This commit is contained in:
Peter Hutterer 2026-08-26 17:04:20 +10:00
commit 37a1847a60
2 changed files with 63 additions and 0 deletions

View file

@ -46,6 +46,10 @@
#include "misc.h"
#include "inpututils.h"
#define AllModifiersMask ( \
ShiftMask | LockMask | ControlMask | Mod1Mask | Mod2Mask | \
Mod3Mask | Mod4Mask | Mod5Mask )
int _X_COLD
SProcXIPassiveGrabDevice(ClientPtr client)
{
@ -350,6 +354,14 @@ ProcXIPassiveUngrabDevice(ClientPtr client)
if (rc != Success)
return rc;
modifiers = (uint32_t *) &stuff[1];
for (i = 0; i < stuff->num_modifiers; i++, modifiers++) {
if (*modifiers != XIAnyModifier && (*modifiers & ~AllModifiersMask)) {
client->errorValue = *modifiers;
return BadValue;
}
}
mod_dev = (IsFloating(dev)) ? dev : GetMaster(dev, MASTER_KEYBOARD);
tempGrab = AllocGrab(NULL);

View file

@ -3,6 +3,7 @@
# Security tests for XI/XI2 (XInput) extension vulnerabilities.
import struct
import time
import pytest
from proto import x11, xi
@ -119,6 +120,56 @@ class TestXIPassiveGrab:
f"Expected BadValue ({x11.BadValue}), got error code {resp.error_code}"
)
@pytest.mark.asan
def test_passive_ungrab_modifier_oob_write(self, xserver, xi_xclient):
"""
ZDI-CAN-32366: ProcXIPassiveUngrabDevice does not validate modifier
values. The grab path (ProcXIPassiveGrabDevice) validates modifiers
via CheckGrabValues(), but the ungrab path passes them directly to
DeletePassiveGrabFromList(). BITCLEAR(mask, modifier) with
modifier > 255 indexes mask[modifier>>5] past the 8-word (32-byte)
mask allocation, causing a controlled single-bit-clear at an
attacker-chosen heap offset.
The fix adds the same modifier validation to the ungrab path.
"""
opcode = xi_xclient.query_extension(Extension.XI).opcode
wid = xi_xclient.create_window()
# First create a valid grab so DeletePassiveGrabFromList has
# something to match against
grab_req = xi.XIPassiveGrabDeviceRequest(
opcode=opcode,
grab_window=wid,
detail=1,
grab_type=xi.XIGrabtypeButton,
)
xi_xclient.send_request(grab_req)
xi_xclient.recv_response(timeout=2.0)
# Now try to ungrab with an oversized modifier (0x10000)
# This would cause BITCLEAR to write past the mask allocation
req = xi.XIPassiveUngrabDeviceRequest(
opcode=opcode,
grab_window=wid,
detail=1,
grab_type=xi.XIGrabtypeButton,
modifiers=[0x10000], # > 255: OOB write via BITCLEAR
)
xi_xclient.send_request(req)
resp = xi_xclient.recv_response(timeout=2.0)
time.sleep(0.5)
assert xserver.is_alive, (
"Server crashed - XIPassiveUngrabDevice modifier OOB write: "
"BITCLEAR(mask, modifier) with modifier > 255"
)
assert isinstance(resp, X11Error), f"Expected an error, got {resp}"
assert resp.error_code == x11.BadValue, (
f"Expected BadValue ({x11.BadValue}), got error code {resp.error_code}"
)
class TestXIChangeProperty:
"""Tests for XIChangeProperty vulnerabilities."""