mirror of
https://gitlab.freedesktop.org/xorg/xserver.git
synced 2026-06-07 00:38:20 +02:00
byxtest: add test cases for the RECORD extension CVEs of the last years
Commit 2902b78535 ("Fix XRecordRegisterClients() Integer underflow")
Assisted-by: Claude:claude-claude-opus-4-6
Part-of: <https://gitlab.freedesktop.org/xorg/xserver/-/merge_requests/2187>
This commit is contained in:
parent
845eb56df2
commit
33f3066ddb
3 changed files with 150 additions and 0 deletions
|
|
@ -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',
|
||||
]
|
||||
|
|
|
|||
82
test/pyxtest/proto/record.py
Normal file
82
test/pyxtest/proto/record.py
Normal file
|
|
@ -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
|
||||
67
test/pyxtest/test_record.py
Normal file
67
test/pyxtest/test_record.py
Normal file
|
|
@ -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)"
|
||||
)
|
||||
Loading…
Add table
Reference in a new issue