From 33f3066ddb58daed3a8125125c7662576ad4ab45 Mon Sep 17 00:00:00 2001 From: Peter Hutterer Date: Mon, 20 Apr 2026 18:14:38 +1000 Subject: [PATCH] byxtest: add test cases for the RECORD extension CVEs of the last years Commit 2902b78535ec ("Fix XRecordRegisterClients() Integer underflow") Assisted-by: Claude:claude-claude-opus-4-6 Part-of: --- test/pyxtest/meson.build | 1 + test/pyxtest/proto/record.py | 82 ++++++++++++++++++++++++++++++++++++ test/pyxtest/test_record.py | 67 +++++++++++++++++++++++++++++ 3 files changed, 150 insertions(+) create mode 100644 test/pyxtest/proto/record.py create mode 100644 test/pyxtest/test_record.py diff --git a/test/pyxtest/meson.build b/test/pyxtest/meson.build index e1e76aa6b..669b8a487 100644 --- a/test/pyxtest/meson.build +++ b/test/pyxtest/meson.build @@ -56,6 +56,7 @@ if pytest.found() # This needs to be kept in sync with the test_foo.py files in the tree tests_pyxtest = [ 'test_randr.py', + 'test_record.py', 'test_xi.py', 'test_xkb.py', ] diff --git a/test/pyxtest/proto/record.py b/test/pyxtest/proto/record.py new file mode 100644 index 000000000..3db154dbc --- /dev/null +++ b/test/pyxtest/proto/record.py @@ -0,0 +1,82 @@ +# SPDX-License-Identifier: MIT +# +# RECORD extension protocol request builders + +import struct +from dataclasses import dataclass, field + +# RECORD minor opcodes +RecordQueryVersion = 0 +RecordCreateContext = 1 +RecordRegisterClients = 2 + +RECORD_MAJOR = 1 +RECORD_MINOR = 13 + + +@dataclass +class QueryVersionRequest: + """RecordQueryVersion request.""" + + opcode: int + major: int = RECORD_MAJOR + minor: int = RECORD_MINOR + + def to_bytes(self, byte_order: str = "<") -> bytes: + return struct.pack( + f"{byte_order}BBHHH", + self.opcode, + RecordQueryVersion, + 2, + self.major, + self.minor, + ) + + +@dataclass +class CreateContextRequest: + """RecordCreateContext request. + + Header is 20 bytes, followed by nClients CARD32 client IDs, + then nRanges xRecordRange structs (32 bytes each). + """ + + opcode: int + context_id: int + element_header: int = 0 + client_ids: list[int] = field(default_factory=list) + ranges_data: bytes = b"" + n_clients_override: int | None = None + n_ranges_override: int | None = None + + def to_bytes(self, byte_order: str = "<") -> bytes: + n_clients = ( + self.n_clients_override + if self.n_clients_override is not None + else len(self.client_ids) + ) + n_ranges = ( + self.n_ranges_override + if self.n_ranges_override is not None + else len(self.ranges_data) // 32 + ) + + client_data = b"" + for cid in self.client_ids: + client_data += struct.pack(f"{byte_order}I", cid) + + total_bytes = 20 + len(client_data) + len(self.ranges_data) + pad_len = (4 - total_bytes % 4) % 4 + total_bytes += pad_len + + header = struct.pack( + f"{byte_order}BBH I B xxx I I", + self.opcode, + RecordCreateContext, + total_bytes // 4, + self.context_id, + self.element_header, + n_clients, + n_ranges, + ) + return header + client_data + self.ranges_data + b"\x00" * pad_len diff --git a/test/pyxtest/test_record.py b/test/pyxtest/test_record.py new file mode 100644 index 000000000..76d31c60b --- /dev/null +++ b/test/pyxtest/test_record.py @@ -0,0 +1,67 @@ +# SPDX-License-Identifier: MIT +# +# Security tests for RECORD extension vulnerabilities. + +import time + +import pytest + +from proto import record +from xclient import Extension + + +@pytest.fixture +def record_xclient_swapped(xclient_swapped): + """Provide a byte-swapped xclient with RECORD initialized.""" + ext = xclient_swapped.query_extension(Extension.RECORD) + if not ext: + pytest.skip("RECORD extension not available") + + req = record.QueryVersionRequest(opcode=ext.opcode) + xclient_swapped.send_request(req.to_bytes(">")) + xclient_swapped.recv_response(timeout=5.0) + + return xclient_swapped, ext.opcode + + +class TestRecordCreateContext: + """Tests for RECORD CreateContext/RegisterClients vulnerabilities.""" + + @pytest.mark.swapped_client + @pytest.mark.asan + def test_swap_create_register_integer_underflow( + self, xserver, record_xclient_swapped + ): + """ + CVE-2020-14362 / ZDI-CAN-11574: SwapCreateRegister() used + stuff->length (attacker-controlled 16-bit wire field) instead + of client->req_len for bounds checking nClients. + + With big requests or a carefully crafted length field, the + subtraction ``stuff->length - header_size`` can underflow, + allowing nClients to pass the check and causing OOB reads + during client ID swapping. + + Fixed in commit 2902b78535ec ("Fix XRecordRegisterClients() Integer + underflow"). + """ + conn, opcode = record_xclient_swapped + + ctx_id = conn.alloc_id() + + # Send CreateContext claiming many clients but with + # minimal actual data. + req = record.CreateContextRequest( + opcode=opcode, + context_id=ctx_id, + client_ids=[0], # One client ID + ranges_data=b"\x00" * 32, # One range (32 bytes) + n_clients_override=100, # Claim 100 clients + n_ranges_override=1, + ) + conn.send_request(req.to_bytes(">")) + time.sleep(0.5) + + assert xserver.is_alive, ( + "Server crashed - integer underflow in SwapCreateRegister (CVE-2020-14362)" + )