xserver/test/pyxtest/test_xi.py
Peter Hutterer 8304192c3c dix: remove passive grabs referencing a device on removal
Passive grabs (GrabRec) store raw DeviceIntPtr values for both the
grabbed device and the modifier device without any lifetime management.
When a device is removed via XIRemoveMaster, CloseDevice() frees the
device struct, but leaves dangling pointers in grab->modifierDevice
for any grab that used this device.

Subsequent pointer events trigger CheckPassiveGrab() which dereferences
the dangling modifierDevice pointer for XI grabs (the CORE and XI2 paths
reassign it, but the XI path does not).

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

ZDI-CAN-31832

CVE-2026-93516

Assisted-by: Claude:claude-opus-4-6
Part-of: <https://gitlab.freedesktop.org/xorg/xserver/-/merge_requests/2341>
(cherry picked from commit be57263415)
Signed-off-by: Peter Hutterer <peter.hutterer@who-t.net>
2026-10-20 19:53:06 +02:00

715 lines
26 KiB
Python

# SPDX-License-Identifier: MIT
#
# Security tests for XI/XI2 (XInput) extension vulnerabilities.
import struct
import time
import pytest
from proto import x11, xfixes, xi, xtest
from xclient import BadLength, BadValue, Extension, X11Error, X11Reply
def find_device(
resp, name_prefix: str, use: int | None = None, byte_order: str = "<"
) -> int | None:
"""Parse an XIQueryDevice reply to find a master device by name.
Returns the device ID or None if not found.
"""
if not isinstance(resp, X11Reply):
return None
reply = xi.XIQueryDeviceReply.from_reply(resp.data, byte_order)
for device in reply.devices:
if (use is None or device.use == use) and device.name.startswith(name_prefix):
return device.deviceid
return None
@pytest.fixture
def xi_xclient(xclient):
"""Provide an xclient with XI2 initialized."""
ext = xclient.query_extension(Extension.XI)
if not ext:
pytest.skip("XInput extension not available")
req = xi.XIQueryVersionRequest(opcode=ext.opcode)
xclient.send_request(req)
xclient.recv_response(timeout=5.0)
return xclient
@pytest.fixture
def xi_xclient_swapped(xclient_swapped):
"""Provide a byte-swapped xclient with XI2 initialized."""
ext = xclient_swapped.query_extension(Extension.XI)
if not ext:
pytest.skip("XInput extension not available")
req = xi.XIQueryVersionRequest(opcode=ext.opcode)
xclient_swapped.send_request(req)
xclient_swapped.recv_response(timeout=5.0)
return xclient_swapped
class TestXIPassiveGrab:
"""Tests for XIPassiveGrabDevice/UngrabDevice vulnerabilities."""
def test_passive_grab_detail_above_255(self, xserver, xi_xclient):
"""
CVE-2022-46341 / ZDI-CAN-19381: OOB write via oversized detail
in XIPassiveGrabDevice.
The ``detail`` field is 32 bits on the wire but the server's
grab mask arrays are only 256 bits wide. A detail > 255
causes an OOB array access in the grab handling code.
The fix pretends that details > 255 are already grabbed,
returning a reply with XIAlreadyGrabbed status for every
requested modifier.
Fixed in commit 51eb63b0ee15 ("Xi: disallow passive grabs with a
detail > 255").
"""
opcode = xi_xclient.query_extension(Extension.XI).opcode
wid = xi_xclient.create_window()
req = xi.XIPassiveGrabDeviceRequest(
opcode=opcode,
grab_window=wid,
detail=256, # OOB: valid range is 0-255
grab_type=xi.XIGrabtypeButton,
)
xi_xclient.send_request(req)
resp = xi_xclient.recv_response(timeout=2.0)
assert xserver.is_alive, (
"Server crashed - OOB in XIPassiveGrabDevice (CVE-2022-46341)"
)
# The server returns a normal reply with every modifier marked
# as XIAlreadyGrabbed.
assert isinstance(resp, X11Reply), f"Expected a reply, got {resp}"
num_modifiers = struct.unpack_from("<H", resp.data, 8)[0]
assert num_modifiers > 0, "Expected at least one failed modifier"
for i in range(num_modifiers):
status = resp.data[32 + i * 8 + 4]
assert status == xi.XIAlreadyGrabbed, (
f"Modifier {i}: expected XIAlreadyGrabbed ({xi.XIAlreadyGrabbed}), "
f"got status {status}"
)
def test_passive_ungrab_detail_oob_write(self, xserver, xi_xclient):
"""
CVE-2022-46341 / ZDI-CAN-19381: OOB write in
ProcXIPassiveUngrabDevice via oversized detail value.
The ``detail`` field is 32 bits on the wire but
DeleteDetailFromMask() uses it to index into a 256-bit
(8 x CARD32) mask array. A detail > 255 writes past the end
of the allocated mask, corrupting heap memory.
The fix rejects detail > 255 early, returning BadValue.
Fixed in commit 51eb63b0ee15 ("Xi: disallow passive grabs with a
detail > 255").
"""
opcode = xi_xclient.query_extension(Extension.XI).opcode
wid = xi_xclient.create_window()
req = xi.XIPassiveUngrabDeviceRequest(
opcode=opcode,
grab_window=wid,
detail=256, # OOB: valid range is 0-255
grab_type=xi.XIGrabtypeButton,
)
xi_xclient.send_request(req)
resp = xi_xclient.recv_response(timeout=2.0)
assert xserver.is_alive, (
"Server crashed - OOB write in XIPassiveUngrabDevice (CVE-2022-46341)"
)
# The fix returns BadValue (error code 2)
assert isinstance(resp, X11Error), f"Expected an error reply, got {resp}"
assert resp.error_code == BadValue, (
f"Expected BadValue ({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}"
)
@pytest.mark.asan
def test_passive_grab_modifier_device_uaf(self, xserver, xi_xclient):
"""
ZDI-CAN-31832: Use-after-free in CheckPassiveGrab via
grab->modifierDevice after device removal.
XGrabDeviceButton creates a passive grab with grabtype XI. The grab
stores a raw DeviceIntPtr in grab->modifierDevice without any reference
counting or lifetime management.
When the modifier device (a master keyboard) is removed via
XIChangeHierarchy(XIRemoveMaster), CloseDevice frees
the device struct, but doesn't clear the now-dangling
grab->modifierDevice pointer.
A subsequent pointer event routed to the grab window triggers
CheckPassiveGrabsOnWindow -> CheckPassiveGrab, which
dereferences the freed device via gdev = grab->modifierDevice
then reads gdev->key.
"""
conn = xi_xclient
opcode = conn.query_extension(Extension.XI).opcode
# 1. Create a window for the grab target
wid = conn.create_window()
req = x11.MapWindowRequest(window=wid)
conn.send_request(req)
conn.flush_responses(timeout=0.5)
# 2. Create a new master device pair "uaf"
add_info = xi.XIAddMasterInfo(name="uaf", send_core=1, enable=1)
req = xi.XIChangeHierarchyRequest(
opcode=opcode,
num_changes=1,
changes_data=add_info.to_bytes("<"),
)
conn.send_request(req)
conn.flush_responses(timeout=1.0)
req = xi.XIQueryDeviceRequest(opcode=opcode, deviceid=xi.XIAllDevices)
conn.send_request(req)
resp = conn.recv_response(timeout=2.0)
kbd_id = find_device(resp, name_prefix="uaf", use=xi.XIMasterKeyboard)
if kbd_id is None:
pytest.fail("Could not find 'uaf' master keyboard device")
# 3. XGrabDeviceButton: passive grab on VCP with modifierDevice=kbd_id.
# This creates a grabtype=XI grab with modifierDevice pointing
# to the new master keyboard.
req = xi.XGrabDeviceButtonRequest(
opcode=opcode,
grab_window=wid,
grabbed_device=xi.VirtualCorePointer,
modifier_device=kbd_id,
button=0, # AnyButton
modifiers=0x8000, # AnyModifier
)
conn.send_request(req)
conn.flush_responses(timeout=0.5)
# 4. Remove the master device - this frees the device struct
rem_info = xi.XIRemoveMasterInfo(
deviceid=kbd_id,
return_mode=xi.XIFloating,
)
req = xi.XIChangeHierarchyRequest(
opcode=opcode,
num_changes=1,
changes_data=rem_info.to_bytes("<"),
)
conn.send_request(req)
conn.flush_responses(timeout=1.0)
# 6. Warp the pointer into the grab window to trigger
# CheckMotion -> ActivateEnterGrab -> CheckPassiveGrab -> UAF
req = x11.WarpPointerRequest(
dst_window=wid,
dst_x=50,
dst_y=50,
)
conn.send_request(req)
conn.flush_responses(timeout=0.5)
time.sleep(0.5)
assert xserver.is_alive, (
"Server crashed - passive grab modifierDevice "
"use-after-free on device removal (ZDI-CAN-31832)"
)
class TestXIChangeProperty:
"""Tests for XIChangeProperty vulnerabilities."""
def test_num_items_overflow_truncation(self, xserver, xi_xclient):
"""
CVE-2022-46344 / ZDI-CAN-19405: Integer truncation in
ProcXIChangeProperty length check.
``totalSize = num_items * (format / 8)`` was computed as a 32-bit
int. With format=32 and num_items=0x40000000, the multiplication
overflows to 0, passing the REQUEST_FIXED_SIZE check. The server
would then read num_items elements from beyond the request buffer.
The fix changed totalSize from ``int`` to ``uint64_t``.
Fixed in commit 8f454b793e1f ("Xi: avoid integer truncation in
length check of ProcXIChangeProperty").
"""
opcode = xi_xclient.query_extension(Extension.XI).opcode
prop_atom = xi_xclient.intern_atom("_TEST_OVERFLOW")
type_atom = xi_xclient.intern_atom("INTEGER")
# format=32 (4 bytes per item), num_items=0x40000000
# totalSize as int32: 0x40000000 * 4 = 0x100000000 → truncates to 0
# totalSize as uint64: 0x100000000 → fails REQUEST_FIXED_SIZE
req = xi.XIChangePropertyRequest(
opcode=opcode,
deviceid=xi.VirtualCorePointer,
property_atom=prop_atom,
type_atom=type_atom,
format=32,
mode=xi.PropModeReplace,
num_items=0x40000000,
data=b"", # no actual data
)
xi_xclient.send_request(req)
resp = xi_xclient.recv_response(timeout=2.0)
assert xserver.is_alive, (
"Server crashed - integer truncation in XIChangeProperty (CVE-2022-46344)"
)
# The server should reject with BadLength (16). Without the fix
# the truncated totalSize (0) passes REQUEST_FIXED_SIZE and the
# server tries to allocate 4 GB, failing with BadAlloc (11)
# instead.
assert isinstance(resp, X11Error), f"Expected an error, got {resp}"
assert resp.error_code == BadLength, (
f"Expected BadLength ({BadLength}), got error code {resp.error_code} - "
f"integer truncation not caught by length check"
)
def test_prepend_property_size_and_offset(self, xserver, xi_xclient):
"""
CVE-2023-5367 / ZDI-CAN-22153: Incorrect size and offset
calculation when prepending to XI device properties.
Two bugs in XIChangeDeviceProperty (and the identical RandR code):
1. new_value.size was set to ``len`` instead of ``total_len``
(new + existing), so the property lost the old data's size.
2. The old_data offset for PropModePrepend used
``prop_value->size`` instead of ``len``, placing old data at
the wrong position and writing out of bounds.
This test sets a property, then prepends to it and reads back
the result. On a fixed server, the property contains all values
in the correct order.
Fixed in commit 541ab2ecd41d ("Xi/randr: fix handling of
PropModeAppend/Prepend").
"""
opcode = xi_xclient.query_extension(Extension.XI).opcode
prop_atom = xi_xclient.intern_atom("_TEST_PREPEND")
type_atom = xi_xclient.intern_atom("INTEGER")
# Step 1: Set initial property with values [10, 20, 30]
initial_data = struct.pack("<III", 10, 20, 30)
req = xi.XIChangePropertyRequest(
opcode=opcode,
deviceid=xi.VirtualCorePointer,
property_atom=prop_atom,
type_atom=type_atom,
format=32,
mode=xi.PropModeReplace,
data=initial_data,
)
xi_xclient.send_request(req)
xi_xclient.flush_responses(timeout=0.5)
# Step 2: Prepend values [1, 2]
prepend_data = struct.pack("<II", 1, 2)
req = xi.XIChangePropertyRequest(
opcode=opcode,
deviceid=xi.VirtualCorePointer,
property_atom=prop_atom,
type_atom=type_atom,
format=32,
mode=xi.PropModePrepend,
data=prepend_data,
)
xi_xclient.send_request(req)
xi_xclient.flush_responses(timeout=0.5)
assert xserver.is_alive, (
"Server crashed - OOB write in XIChangeProperty prepend (CVE-2023-5367)"
)
# Step 3: Read back and verify
req = xi.XIGetPropertyRequest(
opcode=opcode,
deviceid=xi.VirtualCorePointer,
property_atom=prop_atom,
type_atom=type_atom,
)
xi_xclient.send_request(req)
resp = xi_xclient.recv_response(timeout=2.0)
assert isinstance(resp, X11Reply), f"Expected a reply, got {resp}"
num_items = struct.unpack_from("<I", resp.data, 16)[0]
assert num_items == 5, (
f"Expected 5 items (2 prepended + 3 original), got {num_items}"
)
values = struct.unpack_from(f"<{num_items}I", resp.data, 32)
assert values == (1, 2, 10, 20, 30), (
f"Expected (1, 2, 10, 20, 30), got {values}"
)
@pytest.mark.swapped_client
@pytest.mark.parametrize(
"change_cls,get_cls",
[
(xi.XIChangePropertyRequest, xi.XIGetPropertyRequest),
(xi.XChangeDevicePropertyRequest, xi.XGetDevicePropertyRequest),
],
ids=["xi2", "xi1"],
)
def test_change_property_data_format32_swapped(
self, xserver, xi_xclient_swapped, change_cls, get_cls
):
"""
SProcXIChangeProperty and SProcXChangeDeviceProperty did not
byte-swap the property data payload for format=32 properties.
Set a format=32 property from a byte-swapped client with known
values, then read it back. Without the fix, the stored values
have the wrong byte order, causing a round-trip mismatch.
Fixed in commit 243ef9bc2 ("Xi: Swap property data in
SProcXChangeDeviceProperty/SProcXIChangeProperty").
"""
conn = xi_xclient_swapped
ext = conn.query_extension(Extension.XI)
prop_atom = conn.intern_atom("_TEST_SWAP_FORMAT32")
type_atom = conn.intern_atom("INTEGER")
test_values = [0x12345678, 0xDEADBEEF, 42]
data = b""
for v in test_values:
data += struct.pack(">I", v)
req = change_cls(
opcode=ext.opcode,
deviceid=xi.VirtualCorePointer,
property_atom=prop_atom,
type_atom=type_atom,
format=32,
mode=xi.PropModeReplace,
data=data,
)
conn.send_request(req)
conn.flush_responses(timeout=0.5)
assert xserver.is_alive, "Server crashed during ChangeProperty"
req = get_cls(
opcode=ext.opcode,
deviceid=xi.VirtualCorePointer,
property_atom=prop_atom,
type_atom=type_atom,
)
conn.send_request(req)
resp = conn.recv_response(timeout=2.0)
assert isinstance(resp, X11Reply), f"Expected a reply, got {resp}"
# Both XI1 and XI2 GetProperty replies share the same layout
# for the fields we care about:
# bytes 16-19: num_items (CARD32)
# byte 20: format (CARD8)
# bytes 32+: property data
num_items = struct.unpack_from(">I", resp.data, 16)[0]
fmt = resp.data[20]
assert num_items == len(test_values), (
f"Expected {len(test_values)} items, got {num_items}"
)
assert fmt == 32, f"Expected format 32, got {fmt}"
values = struct.unpack_from(f">{num_items}I", resp.data, 32)
assert values == tuple(test_values), (
f"Property data round-trip failed: expected "
f"{[hex(v) for v in test_values]}, got "
f"{[hex(v) for v in values]} - property data not byte-swapped "
f"in SProc handler for {change_cls.__name__}"
)
class TestXIChangeDeviceControl:
@pytest.mark.swapped_client
@pytest.mark.xorg_only
def test_change_device_control_resolution_values_swapped(
self, xserver, xclient_swapped
):
"""
SProcXChangeDeviceControl did not byte-swap the resolution
values array for DEVICE_RESOLUTION.
Send a ChangeDeviceControl/DEVICE_RESOLUTION with a resolution
value that is valid in native byte order but out-of-range when
byte-reversed (e.g. 1000 = 0x000003E8 → 0xE8030000 reversed).
Without the fix: the garbled value exceeds max_resolution →
BadValue error.
With the fix: the correct value is in range → success reply
(or BadMatch if the device doesn't support it, but not BadValue).
Fixed in commit e24bd73e9d6f ("Xi: add missing byte-swap of
resolution values in SProcXChangeDeviceControl").
"""
conn = xclient_swapped
ext = conn.query_extension(Extension.XI)
if not ext:
pytest.skip("XInput extension not available")
req = xi.XIQueryVersionRequest(opcode=ext.opcode)
conn.send_request(req)
conn.recv_response(timeout=5.0)
ctl = xi.DeviceResolutionCtl(
first_valuator=0,
num_valuators=1,
resolutions=[1000],
)
ctl_bytes = ctl.to_bytes(">")
req = xi.XChangeDeviceControlRequest(
opcode=ext.opcode,
control=xi.DEVICE_RESOLUTION,
deviceid=xi.VirtualCorePointer,
control_data=ctl_bytes,
)
conn.send_request(req)
resp = conn.recv_response(timeout=2.0)
assert xserver.is_alive, "Server crashed"
# Without the fix: BadValue (error code 2) because the
# byte-reversed resolution 0xE8030000 exceeds max_resolution.
# With the fix: either a reply (success) or BadMatch (device
# doesn't support resolution control), but NOT BadValue.
if isinstance(resp, X11Error):
assert resp.error_code != BadValue, (
"ChangeDeviceControl returned BadValue - "
"resolution values not byte-swapped"
)
@pytest.mark.swapped_client
def test_change_device_control_resolution_unbounded_swap(
self, xserver, xi_xclient_swapped
):
"""Unbounded SwapLongs of DEVICE_RESOLUTION valuators."""
conn = xi_xclient_swapped
opcode = conn.query_extension(Extension.XI).opcode
bo = conn._byte_order
ctl = xi.DeviceResolutionCtl(
first_valuator=0,
num_valuators=255,
resolutions=[],
)
bad = xi.XChangeDeviceControlRequest(
opcode=opcode,
control=xi.DEVICE_RESOLUTION,
deviceid=xi.VirtualCorePointer,
control_data=ctl.to_bytes(bo),
)
canary = x11.InternAtomRequest(name="_TEST_CDC_UNBOUNDED_SWAP")
conn.send_request(bad.to_bytes(bo) + canary.to_bytes(bo))
conn.seq += 1
resp_bad = conn.recv_response(timeout=2.0)
resp_canary = conn.recv_response(timeout=2.0)
assert xserver.is_alive, "Server crashed"
assert isinstance(resp_bad, X11Error), f"Expected error, got {resp_bad}"
assert resp_bad.error_code == x11.BadLength, (
f"Expected BadLength (16), got {resp_bad.error_code}"
)
assert isinstance(resp_canary, X11Reply), (
f"InternAtom canary corrupted, got {resp_canary}"
)
atom = struct.unpack_from(f"{bo}I", resp_canary.data, 8)[0]
assert atom != 0, "InternAtom canary returned None atom"
class TestXIChangeCursor:
def test_change_cursor_null_window(self, xserver, xi_xclient):
"""
XIChangeCursor dereferences pWin even if it's not set
"""
opcode = xi_xclient.query_extension(Extension.XI).opcode
req = xi.XIChangeCursorRequest(
opcode=opcode,
window=0,
cursor=0,
deviceid=xi.VirtualCorePointer,
)
xi_xclient.send_request(req)
resp = xi_xclient.recv_response(timeout=5.0)
assert xserver.is_alive, "Server crashed"
# Without the fix: SegFault on a NULL WindowPtr
# With the fix: BadWindow
assert isinstance(resp, X11Error), f"Expected an error, got {resp}"
assert resp.error_code == x11.BadWindow, (
"ChangeCursor didn't return BadWindow for Window 0"
)
class TestXIBarrier:
@pytest.mark.asan
def test_barrier_leave_event_buffer_overflow(self, xserver, xi_xclient):
"""
ZDI-CAN-31938: Heap buffer overflow in input_constrain_cursor
from too many barrier leave events.
input_constrain_cursor() writes barrier hit/leave events
into a fixed-size internal event buffer (GetMaximumEventsNum()
= 100 slots). When all barriers are in the "released" state,
the first loop marks every barrier as hit (released barriers
skip the clamp, so dir never clears and iteration
continues). The second loop emits one event per hit barrier
with no capacity check.
Creating >100 barriers at the same position, releasing all
of them (by setting eventid=1 which matches the initial
barrier_event_id), and driving pointer motion across them
causes 200+ events to be written into the 100-slot buffer
which is generally considered a bad idea..
"""
conn = xi_xclient
xi_opcode = conn.query_extension(Extension.XI).opcode
xf = conn.query_extension(Extension.XFIXES)
if not xf:
pytest.skip("XFIXES extension not available")
xt = conn.query_extension(Extension.XTEST)
if not xt:
pytest.skip("XTEST extension not available")
req = xfixes.XFixesQueryVersionRequest(
opcode=xf.opcode, major_version=5, minor_version=0
)
conn.send_request(req)
resp = conn.recv_response(timeout=5.0)
if resp is None:
pytest.fail("XFixesQueryVersion got no response")
# Create 200 barriers at x=500, vertical line y=[0,4000].
# All at the same position so pointer motion crosses them all.
barriers = []
for _ in range(200):
bid = conn.alloc_id()
req = xfixes.XFixesCreatePointerBarrierRequest(
opcode=xf.opcode,
barrier=bid,
window=conn.root_window,
x1=500,
y1=0,
x2=500,
y2=4000,
directions=0,
num_devices=0,
)
conn.send_request(req)
barriers.append(bid)
conn.flush_responses(timeout=1.0)
# Release all barriers by sending XIBarrierReleasePointer
# with eventid=1 (matching the initial barrier_event_id).
# This puts all barriers into the "released" state.
req = xi.XIBarrierReleasePointerRequest(
opcode=xi_opcode,
barriers=[(xi.VirtualCorePointer, bid, 1) for bid in barriers],
)
conn.send_request(req)
conn.flush_responses(timeout=0.5)
# Position the pointer to the left of the barriers
req = x11.WarpPointerRequest(
dst_window=conn.root_window,
dst_x=100,
dst_y=2000,
)
conn.send_request(req)
conn.flush_responses(timeout=0.5)
# Use xtest to generate relative pointer motion
# crossing the barrier line at x=500 (motion dx=+500).
# This will trigger a BarrierHit on all 200 barriers.
req = xtest.XTestFakeInputRequest(
opcode=xt.opcode,
event_type=xtest.MotionNotify,
detail=1, # relative motion
root_x=500,
root_y=0,
)
conn.send_request(req)
conn.flush_responses(timeout=0.5)
time.sleep(0.5)
assert xserver.is_alive, (
"Server crashed - barrier leave event buffer overflow (ZDI-CAN-31938)"
)