mirror of
https://gitlab.freedesktop.org/xorg/xserver.git
synced 2026-06-07 04:08:25 +02:00
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:
parent
acbc46e708
commit
b9ed4bd4c0
2 changed files with 174 additions and 7 deletions
|
|
@ -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).
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
Loading…
Add table
Reference in a new issue