Xi: add bounds check for barrier events in input_constrain_cursor

input_constrain_cursor() writes barrier hit and leave events into the
InputEventList buffer without checking how many slots remain.
The buffer is allocated by the DDX at GetMaximumEventsNum() (100) entries
and may contain one raw event already. A client can create more than 100
pointer barriers, pre-arm them as released via XIBarrierReleasePointer,
and then trigger a single pointer motion that crosses all barriers
simultaneously. The leave event loop iterates every hit barrier and
writes one event per barrier, overflowing the 100-slot buffer.

In an ideal world we'd change the InternalEvent* pointer to a struct
that contains allocated size, number of events and the pointer itself
but we're crossing public APIs here and that requires an ABI bump.

So we make do with cutting off the number of barrier events at 64 which
should be low enough to not trigger the issue. However, for the 100+
barrier case this means we only send BarierHit events, not BarrierRelease.
Potentially leaving a client stuck.

We mitigate that problem by simply refusing to create more than 32
barriers per screen (which on its own would also fix the issue, but hey,
'doppelt gemoppelt' as they say).

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

ZDI-CAN-31938

CVE-2026-93519

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:17:12 +10:00
commit 1f42cc1f00
5 changed files with 381 additions and 28 deletions

View file

@ -416,6 +416,18 @@ input_constrain_cursor(DeviceIntPtr dev, ScreenPtr screen,
InternalEvent *barrier_events = events;
DeviceIntPtr master;
/* We cannot know how many events are available in *events, but
* we know it all DDX very protbably allocated them with
* GetMaximumEventsNum() and fill_pointer_events() may have added one.
*
* Let's cap at a maximum of 64 barrier events which is way more than
* we'll need but low enough that we shouldn't OOB on *events.
*
* Changing the signatures to get some sz_events in here is an ABI change
* through miPointerSetPosition so... weeping face.
*/
const int MAX_BARRIER_EVENTS = 64;
if (nevents)
*nevents = 0;
@ -460,7 +472,6 @@ input_constrain_cursor(DeviceIntPtr dev, ScreenPtr screen,
if (pbd->barrier_event_id == pbd->release_event_id)
continue;
ev.type = ET_BarrierHit;
barrier_clamp_to_barrier(nearest, dir, &x, &y);
if (barrier_is_vertical(nearest)) {
@ -472,19 +483,22 @@ input_constrain_cursor(DeviceIntPtr dev, ScreenPtr screen,
current_y = y;
}
ev.flags = 0;
ev.event_id = pbd->barrier_event_id;
ev.barrierid = c->id;
if (*nevents < MAX_BARRIER_EVENTS) {
ev.type = ET_BarrierHit;
ev.flags = 0;
ev.event_id = pbd->barrier_event_id;
ev.barrierid = c->id;
ev.dt = new_sequence ? 0 : ms - pbd->last_timestamp;
ev.window = c->window;
ev.dt = new_sequence ? 0 : ms - pbd->last_timestamp;
ev.window = c->window;
/* root x/y is filled in later */
barrier_events->barrier_event = ev;
barrier_events++;
*nevents += 1;
}
pbd->last_timestamp = ms;
/* root x/y is filled in later */
barrier_events->barrier_event = ev;
barrier_events++;
*nevents += 1;
}
xorg_list_for_each_entry(c, &cs->barriers, entry) {
@ -503,29 +517,31 @@ input_constrain_cursor(DeviceIntPtr dev, ScreenPtr screen,
continue;
pbd->hit = FALSE;
ev.type = ET_BarrierLeave;
if (pbd->barrier_event_id == pbd->release_event_id)
flags |= XIBarrierPointerReleased;
ev.flags = flags;
ev.event_id = pbd->barrier_event_id;
ev.barrierid = c->id;
ev.dt = ms - pbd->last_timestamp;
ev.window = c->window;
pbd->last_timestamp = ms;
/* root x/y is filled in later */
barrier_events->barrier_event = ev;
barrier_events++;
*nevents += 1;
/* If we've left the hit box, this is the
* start of a new event ID. */
pbd->barrier_event_id++;
if (*nevents < MAX_BARRIER_EVENTS) {
ev.type = ET_BarrierLeave;
if (pbd->barrier_event_id == pbd->release_event_id)
flags |= XIBarrierPointerReleased;
ev.flags = flags;
ev.event_id = pbd->barrier_event_id;
ev.barrierid = c->id;
ev.dt = ms - pbd->last_timestamp;
ev.window = c->window;
/* root x/y is filled in later */
barrier_events->barrier_event = ev;
barrier_events++;
*nevents += 1;
}
}
out:
@ -557,8 +573,10 @@ CreatePointerBarrierClient(ClientPtr client,
int size;
int i;
struct PointerBarrierClient *ret;
struct PointerBarrierClient *counter;
CARD16 *in_devices;
DeviceIntPtr dev;
size_t nbarriers = 0;
size = sizeof(*ret) + sizeof(DeviceIntPtr) * stuff->num_devices;
ret = malloc(size);
@ -578,6 +596,16 @@ CreatePointerBarrierClient(ClientPtr client,
screen = pWin->drawable.pScreen;
cs = GetBarrierScreen(screen);
/* Only allow for a maximum of 32 barriers to be created. This
* should be more than enough and prevents issues in
* input_constrain_cursor, see MAX_BARRIER_EVENTS in that
* function */
xorg_list_for_each_entry(counter, &cs->barriers, entry) {
nbarriers++;
if (nbarriers >= 32)
return BadAlloc;
}
ret->screen = screen;
ret->window = stuff->window;
ret->num_devices = stuff->num_devices;

View file

@ -0,0 +1,99 @@
# SPDX-License-Identifier: MIT
#
# XFIXES extension protocol request builders for security testing.
import struct
from dataclasses import dataclass
# XFixes minor opcodes
XFixesQueryVersion = 0
XFixesCreatePointerBarrier = 31
XFixesDestroyPointerBarrier = 32
@dataclass
class XFixesQueryVersionRequest:
"""XFixesQueryVersion request (12 bytes).
Wire format:
CARD8 reqType (XFIXES major opcode)
CARD8 xfixesReqType (0)
CARD16 length (3)
CARD32 majorVersion
CARD32 minorVersion
"""
opcode: int
major_version: int = 5
minor_version: int = 0
def to_bytes(self, byte_order: str = "<") -> bytes:
return struct.pack(
f"{byte_order}BBH II",
self.opcode,
XFixesQueryVersion,
3, # 12 bytes = 3 words
self.major_version,
self.minor_version,
)
@dataclass
class XFixesCreatePointerBarrierRequest:
"""XFixesCreatePointerBarrier request.
Wire format (xXFixesCreatePointerBarrierReq):
CARD8 reqType (XFIXES major opcode)
CARD8 xfixesReqType (31)
CARD16 length
CARD32 barrier (XID)
CARD32 window
INT16 x1
INT16 y1
INT16 x2
INT16 y2
CARD32 directions
CARD16 pad
CARD16 num_devices
<devices> (num_devices * CARD16, padded)
"""
opcode: int
barrier: int
window: int
x1: int
y1: int
x2: int
y2: int
directions: int = 0
num_devices: int = 0
devices: list[int] | None = None
def to_bytes(self, byte_order: str = "<") -> bytes:
devs = self.devices or []
dev_data = b""
for d in devs:
dev_data += struct.pack(f"{byte_order}H", d)
# Pad device list to 4-byte boundary
pad_len = (4 - len(dev_data) % 4) % 4 if dev_data else 0
dev_data += b"\x00" * pad_len
total = 28 + len(dev_data)
length = total // 4
header = struct.pack(
f"{byte_order}BBH II hh hh I HH",
self.opcode,
XFixesCreatePointerBarrier,
length,
self.barrier,
self.window,
self.x1,
self.y1,
self.x2,
self.y2,
self.directions,
0, # pad
self.num_devices,
)
return header + dev_data

View file

@ -20,6 +20,7 @@ XIPassiveGrabDevice = 54
XIPassiveUngrabDevice = 55
XIChangeProperty = 57
XIGetProperty = 59
XIBarrierReleasePointer = 61
# XIChangeHierarchy change types
XIAddMaster = 1
@ -703,3 +704,51 @@ class XIQueryDeviceReply:
)
return cls(devices=devices)
@dataclass
class XIBarrierReleasePointerRequest:
"""XIBarrierReleasePointer request (XI2 minor opcode 61).
Wire format (xXIBarrierReleasePointerReq):
CARD8 reqType (XI major opcode)
CARD8 ReqType (61)
CARD16 length
CARD32 num_barriers
<barriers> (num_barriers * xXIBarrierReleasePointerInfo)
Each xXIBarrierReleasePointerInfo (12 bytes):
CARD16 deviceid
CARD16 pad
CARD32 barrier
CARD32 eventid
"""
opcode: int
barriers: list[tuple[int, int, int]]
"""List of (deviceid, barrier_id, eventid)"""
def to_bytes(self, byte_order: str = "<") -> bytes:
num_barriers = len(self.barriers)
total = 8 + num_barriers * 12
length = total // 4
header = struct.pack(
f"{byte_order}BBH I",
self.opcode,
XIBarrierReleasePointer,
length,
num_barriers,
)
payload = b""
for deviceid, barrier_id, eventid in self.barriers:
payload += struct.pack(
f"{byte_order}HH II",
deviceid,
0, # pad
barrier_id,
eventid,
)
return header + payload

View file

@ -0,0 +1,77 @@
# SPDX-License-Identifier: MIT
#
# XTEST extension protocol request builders for security testing.
import struct
from dataclasses import dataclass
# XTEST minor opcodes
XTestGetVersion = 0
XTestCompareCursor = 1
XTestFakeInput = 2
# X11 event types used with FakeInput
KeyPress = 2
KeyRelease = 3
ButtonPress = 4
ButtonRelease = 5
MotionNotify = 6
@dataclass
class XTestFakeInputRequest:
"""XTestFakeInput request.
Wire format (xXTestFakeInputReq):
CARD8 reqType (XTEST major opcode)
CARD8 xtReqType (2)
CARD16 length (9)
xEvent fake_event (32 bytes)
CARD32 pad (4 bytes, brings total to 36)
The embedded xEvent has the standard X event layout.
For MotionNotify with detail=1, it is a relative motion event.
The root_x and root_y fields specify the motion delta for
relative events.
"""
opcode: int
event_type: int = MotionNotify
detail: int = 0 # 1 = relative for MotionNotify
time: int = 0 # CurrentTime
root_window: int = 0 # None
root_x: int = 0
root_y: int = 0
def to_bytes(self, byte_order: str = "<") -> bytes:
# Build the 32-byte xEvent structure
# xEvent layout for key/button/motion:
# type(1) + detail(1) + sequenceNumber(2) +
# time(4) + root(4) + event(4) + child(4) +
# rootX(2) + rootY(2) + eventX(2) + eventY(2) +
# state(2) + sameScreen(1) + pad(1)
event = struct.pack(
f"{byte_order}BBH I III hh hh HBx",
self.event_type,
self.detail,
0, # sequenceNumber (ignored)
self.time,
self.root_window,
0, # event window
0, # child window
self.root_x,
self.root_y,
0, # eventX
0, # eventY
0, # state
0, # sameScreen
)
# Request header + event + padding to make 36 bytes total
header = struct.pack(
f"{byte_order}BBH",
self.opcode,
XTestFakeInput,
9, # 36 bytes = 9 words
)
return header + event + b"\x00" * (36 - 4 - len(event))

View file

@ -6,7 +6,8 @@ import struct
import time
import pytest
from proto import x11, xi
from proto import x11, xfixes, xi, xtest
from xclient import Extension, X11Error, X11Reply
@ -612,3 +613,102 @@ class TestXIChangeCursor:
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)"
)