xserver/test/pyxtest/proto/glx.py
Peter Hutterer d1f51894f0 test/pyxtest: add test for GLX ChangeDrawableAttributes OOB read (ZDI-CAN-30165)
Add GLX extension protocol builders (proto/glx.py) and a regression test
that reproduces the reversed length check in ChangeDrawableAttributes.

The test creates a GLX context on the root visual, binds it with
MakeCurrent (which auto-creates a GLXDrawable), then sends a
ChangeDrawableAttributes request with length=3 (12 bytes) but
numAttribs=2100. Without the fix, the reversed comparison operator (<
instead of >) would let this undersized request pass validation, and
DoChangeDrawableAttributes would iterate 2100 attribute pairs, reading
far past the 12-byte request buffer.

Assisted-by: Claude:claude-opus-4-6
Part-of: <https://gitlab.freedesktop.org/xorg/xserver/-/merge_requests/2228>
2026-06-01 08:31:59 +10:00

106 lines
2.6 KiB
Python

# SPDX-License-Identifier: MIT
#
# GLX extension protocol request builders for security testing.
import struct
from dataclasses import dataclass
# GLX minor opcodes
GLXCreateContext = 3
GLXMakeCurrent = 5
GLXChangeDrawableAttributes = 30
# GLX drawable attribute keys
GLX_EVENT_MASK = 0x801F
@dataclass
class CreateContextRequest:
"""glxCreateContext request (24 bytes = 6 words)."""
opcode: int
context_id: int
visual: int
screen: int = 0
share_list: int = 0
is_direct: int = 1
def to_bytes(self, byte_order: str = "<") -> bytes:
return struct.pack(
f"{byte_order}BBH IIII B xxx",
self.opcode,
GLXCreateContext,
6, # length = 6 words
self.context_id,
self.visual,
self.screen,
self.share_list,
self.is_direct,
)
@dataclass
class MakeCurrentRequest:
"""glxMakeCurrent request (16 bytes = 4 words)."""
opcode: int
drawable: int
context_id: int
old_context_tag: int = 0
def to_bytes(self, byte_order: str = "<") -> bytes:
return struct.pack(
f"{byte_order}BBH III",
self.opcode,
GLXMakeCurrent,
4, # length = 4 words
self.drawable,
self.context_id,
self.old_context_tag,
)
@dataclass
class ChangeDrawableAttributesRequest:
"""glxChangeDrawableAttributes request.
Header is 12 bytes (3 words): opcode, glxCode, length, drawable, numAttribs.
Followed by numAttribs * 2 CARD32 values (key/value pairs).
The length_override and num_attribs_override fields allow crafting
intentionally malformed requests for security testing.
"""
opcode: int
drawable: int
num_attribs: int = 0
attribs: bytes = b""
length_override: int | None = None
num_attribs_override: int | None = None
def to_bytes(self, byte_order: str = "<") -> bytes:
total_bytes = 12 + len(self.attribs)
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
)
num_attribs = (
self.num_attribs_override
if self.num_attribs_override is not None
else self.num_attribs
)
header = struct.pack(
f"{byte_order}BBH II",
self.opcode,
GLXChangeDrawableAttributes,
length,
self.drawable,
num_attribs,
)
return header + self.attribs + b"\x00" * pad_len