mirror of
https://github.com/X11Libre/xserver.git
synced 2026-10-21 01:13:38 +00:00
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:
parent
be57263415
commit
1f42cc1f00
5 changed files with 381 additions and 28 deletions
|
|
@ -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;
|
||||
|
|
|
|||
99
test/pyxtest/proto/xfixes.py
Normal file
99
test/pyxtest/proto/xfixes.py
Normal 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
|
||||
|
|
@ -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
|
||||
|
|
|
|||
77
test/pyxtest/proto/xtest.py
Normal file
77
test/pyxtest/proto/xtest.py
Normal 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))
|
||||
|
|
@ -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)"
|
||||
)
|
||||
|
|
|
|||
Loading…
Reference in a new issue