pyxtest: add tests for XI property data byte-swap fix

Add tests for commit b243ef9bc2 ("Xi: Swap property data in
SProcXChangeDeviceProperty/SProcXIChangeProperty").

Both tests set a format=32 property from a byte-swapped client and
read it back, verifying the values round-trip correctly. Without the
property data swap, the stored values have the wrong byte order.

Part-of: <https://gitlab.freedesktop.org/xorg/xserver/-/merge_requests/2187>
This commit is contained in:
Peter Hutterer 2026-05-05 10:41:27 +10:00 committed by Marge Bot
parent acbc46e708
commit b9ed4bd4c0
2 changed files with 174 additions and 7 deletions

View file

@ -7,6 +7,8 @@ from dataclasses import dataclass
# XI (v1) minor opcodes
XChangeDeviceControl = 35
XChangeDeviceProperty = 37
XGetDeviceProperty = 39
# XI2 minor opcodes
XIQueryVersion = 47
@ -176,7 +178,6 @@ class XIChangePropertyRequest:
mode: int = PropModeReplace
data: bytes = b""
num_items: int | None = None
length_override: int | None = None
def to_bytes(self, byte_order: str = "<") -> bytes:
num_items = (
@ -188,12 +189,7 @@ class XIChangePropertyRequest:
total_bytes = 20 + len(self.data)
pad_len = (4 - total_bytes % 4) % 4
total_bytes += pad_len
length = (
self.length_override
if self.length_override is not None
else total_bytes // 4
)
length = total_bytes // 4
header = struct.pack(
f"{byte_order}BBH HBB II I",
@ -237,6 +233,85 @@ class XIGetPropertyRequest:
)
@dataclass
class XChangeDevicePropertyRequest:
"""XChangeDeviceProperty request (XI v1, minor opcode 37).
Wire format (xChangeDevicePropertyReq):
opcode(1) + ReqType(1) + length(2) + property(4) + type(4) +
deviceid(1) + format(1) + mode(1) + pad(1) + nUnits(4)
Total header: 20 bytes, followed by property data.
"""
opcode: int
deviceid: int
property_atom: int
type_atom: int
format: int = 32
mode: int = PropModeReplace
data: bytes = b""
num_items: int | None = None
def to_bytes(self, byte_order: str = "<") -> bytes:
num_items = (
self.num_items
if self.num_items is not None
else (len(self.data) // (self.format // 8) if self.data else 0)
)
total_bytes = 20 + len(self.data)
pad_len = (4 - total_bytes % 4) % 4
total_bytes += pad_len
length = total_bytes // 4
header = struct.pack(
f"{byte_order}BBH II BBBx I",
self.opcode,
XChangeDeviceProperty,
length,
self.property_atom,
self.type_atom,
self.deviceid,
self.format,
self.mode,
num_items,
)
return header + self.data + b"\x00" * pad_len
@dataclass
class XGetDevicePropertyRequest:
"""XGetDeviceProperty request (XI v1, minor opcode 39).
Wire format (xGetDevicePropertyReq):
opcode(1) + ReqType(1) + length(2) + property(4) + type(4) +
longOffset(4) + longLength(4) + deviceid(1) + delete(1) + pad(2)
Total: 24 bytes = 6 words.
"""
opcode: int
deviceid: int
property_atom: int
type_atom: int = 0 # AnyPropertyType
offset: int = 0
req_length: int = 0xFFFF
delete: bool = False
def to_bytes(self, byte_order: str = "<") -> bytes:
return struct.pack(
f"{byte_order}BBH II II BBxx",
self.opcode,
XGetDeviceProperty,
6, # 24 bytes = 6 words
self.property_atom,
self.type_atom,
self.offset,
self.req_length,
self.deviceid,
1 if self.delete else 0,
)
@dataclass
class XChangeDeviceControlRequest:
"""XChangeDeviceControl request (XI v1, minor opcode 35).

View file

@ -23,6 +23,19 @@ def xi_xclient(xclient):
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.to_bytes(">"))
xclient_swapped.recv_response(timeout=5.0)
return xclient_swapped
class TestXIPassiveGrab:
"""Tests for XIPassiveGrabDevice/UngrabDevice vulnerabilities."""
@ -237,6 +250,85 @@ class TestXIChangeProperty:
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.to_bytes(">"))
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.to_bytes(">"))
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