mirror of
https://gitlab.freedesktop.org/xorg/xserver.git
synced 2026-06-06 22:18:24 +02:00
test/pyxtest: add test for ScreenSaver CreateSaverWindow UAF (ZDI-CAN-30168)
Add screensaver protocol builders for SetAttributes, UnsetAttributes, and ForceScreenSaver, then add a regression test that reproduces the CreateSaverWindow use-after-free. The test sequence: 1. SetAttributes(root, 100x100, mask=0) - creates screen private with attr 2. ForceScreenSaver(Active) - creates the saver window 3. UnsetAttributes(root) - clears pPriv->attr to NULL 4. ForceScreenSaver(Active) - re-enters CreateSaverWindow Without the fix, step 4 triggers CheckScreenPrivate which finds all fields empty (attr=NULL, events=NULL, hasWindow=FALSE, installedMap=None), frees pPriv, and sets the screen private to NULL. The function then dereferences the freed pPriv->attr pointer, causing a use-after-free. Assisted-by: Claude:claude-opus-4-6 Part-of: <https://gitlab.freedesktop.org/xorg/xserver/-/merge_requests/2228>
This commit is contained in:
parent
471650430b
commit
57129a43b7
3 changed files with 170 additions and 1 deletions
|
|
@ -7,6 +7,9 @@ from dataclasses import dataclass
|
|||
|
||||
# ScreenSaver minor opcodes
|
||||
ScreenSaverQueryVersion = 0
|
||||
ScreenSaverSelectInput = 1
|
||||
ScreenSaverSetAttributes = 3
|
||||
ScreenSaverUnsetAttributes = 4
|
||||
ScreenSaverSuspend = 5
|
||||
|
||||
|
||||
|
|
@ -27,3 +30,73 @@ class SuspendRequest:
|
|||
length,
|
||||
self.suspend,
|
||||
)
|
||||
|
||||
|
||||
@dataclass
|
||||
class SetAttributesRequest:
|
||||
"""ScreenSaverSetAttributes request (28 bytes = 7 words, mask=0).
|
||||
|
||||
xScreenSaverSetAttributesReq:
|
||||
reqType(1) + saverReqType(1) + length(2)
|
||||
drawable(4)
|
||||
x(2) + y(2)
|
||||
width(2) + height(2)
|
||||
borderWidth(2) + c_class(1) + depth(1)
|
||||
visualID(4)
|
||||
mask(4)
|
||||
[values...]
|
||||
"""
|
||||
|
||||
opcode: int
|
||||
drawable: int
|
||||
x: int = 0
|
||||
y: int = 0
|
||||
width: int = 100
|
||||
height: int = 100
|
||||
border_width: int = 0
|
||||
c_class: int = 0 # CopyFromParent
|
||||
depth: int = 0 # CopyFromParent
|
||||
visual_id: int = 0 # CopyFromParent
|
||||
mask: int = 0
|
||||
values: bytes = b""
|
||||
|
||||
def to_bytes(self, byte_order: str = "<") -> bytes:
|
||||
total_bytes = 28 + len(self.values)
|
||||
pad_len = (4 - total_bytes % 4) % 4
|
||||
total_bytes += pad_len
|
||||
length = total_bytes // 4
|
||||
|
||||
header = struct.pack(
|
||||
f"{byte_order}BBHIhhHHHBBII",
|
||||
self.opcode,
|
||||
ScreenSaverSetAttributes,
|
||||
length,
|
||||
self.drawable,
|
||||
self.x,
|
||||
self.y,
|
||||
self.width,
|
||||
self.height,
|
||||
self.border_width,
|
||||
self.c_class,
|
||||
self.depth,
|
||||
self.visual_id,
|
||||
self.mask,
|
||||
)
|
||||
return header + self.values + b"\x00" * pad_len
|
||||
|
||||
|
||||
@dataclass
|
||||
class UnsetAttributesRequest:
|
||||
"""ScreenSaverUnsetAttributes request (8 bytes = 2 words)."""
|
||||
|
||||
opcode: int
|
||||
drawable: int
|
||||
|
||||
def to_bytes(self, byte_order: str = "<") -> bytes:
|
||||
return struct.pack(
|
||||
f"{byte_order}BBH I",
|
||||
self.opcode,
|
||||
ScreenSaverUnsetAttributes,
|
||||
2, # length = 2 words
|
||||
self.drawable,
|
||||
)
|
||||
|
|
|
|||
|
|
@ -10,6 +10,11 @@ CreateWindow = 1
|
|||
CreatePixmap = 53
|
||||
InternAtom = 16
|
||||
QueryExtension = 98
|
||||
ForceScreenSaverOpcode = 115
|
||||
|
||||
|
||||
ScreenSaverReset = 0
|
||||
ScreenSaverActive = 1
|
||||
|
||||
|
||||
def _pad(data: bytes) -> bytes:
|
||||
|
|
@ -116,3 +121,18 @@ class QueryExtensionRequest:
|
|||
)
|
||||
+ padded
|
||||
)
|
||||
|
||||
|
||||
@dataclass
|
||||
class ForceScreenSaver:
|
||||
"""X11 ForceScreenSaver request."""
|
||||
|
||||
mode: int
|
||||
|
||||
def to_bytes(self, byte_order: str = "<") -> bytes:
|
||||
return struct.pack(
|
||||
f"{byte_order}BBH",
|
||||
ForceScreenSaverOpcode,
|
||||
self.mode,
|
||||
1, # length = 1 word
|
||||
)
|
||||
|
|
|
|||
|
|
@ -6,10 +6,19 @@ import time
|
|||
|
||||
import pytest
|
||||
|
||||
from proto import screensaver
|
||||
from proto import screensaver, x11
|
||||
from xclient import Extension
|
||||
|
||||
|
||||
@pytest.fixture
|
||||
def screensaver_xclient(xclient):
|
||||
"""Provide an xclient with the MIT-SCREEN-SAVER extension queried."""
|
||||
ext = xclient.query_extension(Extension.MIT_SCREEN_SAVER)
|
||||
if not ext:
|
||||
pytest.skip("MIT-SCREEN-SAVER extension not available")
|
||||
return xclient, ext.opcode
|
||||
|
||||
|
||||
class TestScreenSaverSuspend:
|
||||
"""Tests for SProcScreenSaverSuspend vulnerabilities."""
|
||||
|
||||
|
|
@ -44,3 +53,70 @@ class TestScreenSaverSuspend:
|
|||
assert xserver.is_alive, (
|
||||
"Server crashed - SProcScreenSaverSuspend (CVE-2021-4010)"
|
||||
)
|
||||
|
||||
|
||||
class TestCreateSaverWindow:
|
||||
"""Tests for CreateSaverWindow use-after-free via CheckScreenPrivate."""
|
||||
|
||||
@pytest.mark.asan
|
||||
def test_create_saver_window_uaf(self, xserver, screensaver_xclient):
|
||||
"""
|
||||
ZDI-CAN-30168: CreateSaverWindow stores pPriv in a local
|
||||
variable at function entry. When an existing saver window is
|
||||
being replaced, it sets pPriv->hasWindow = FALSE and calls
|
||||
CheckScreenPrivate(). If pPriv->attr is NULL (cleared by a
|
||||
prior UnsetAttributes), pPriv->events is NULL, and
|
||||
pPriv->installedMap is None, CheckScreenPrivate frees pPriv
|
||||
and sets the screen private to NULL. The function then
|
||||
dereferences the freed pPriv->attr pointer on the next line.
|
||||
|
||||
Attack sequence:
|
||||
1. SetAttributes (creates pPriv with pPriv->attr set)
|
||||
2. ForceScreenSaver(Active) (creates saver window)
|
||||
3. UnsetAttributes (sets pPriv->attr = NULL)
|
||||
4. ForceScreenSaver(Active) (re-enters CreateSaverWindow → UAF)
|
||||
|
||||
Fixed by re-fetching pPriv from the screen private after
|
||||
CheckScreenPrivate returns.
|
||||
"""
|
||||
conn, opcode = screensaver_xclient
|
||||
|
||||
# Step 1: SetAttributes(root, 100x100, mask=0)
|
||||
# Creates pPriv with pPriv->attr set.
|
||||
req = screensaver.SetAttributesRequest(
|
||||
opcode=opcode,
|
||||
drawable=conn.root_window,
|
||||
width=100,
|
||||
height=100,
|
||||
mask=0,
|
||||
)
|
||||
conn.send_request(req.to_bytes())
|
||||
conn.flush_responses(timeout=0.5)
|
||||
|
||||
# Step 2: ForceScreenSaver(Active)
|
||||
# Activates the screen saver, creating the saver window.
|
||||
req = x11.ForceScreenSaver(mode=x11.ScreenSaverActive)
|
||||
conn.send_request(req.to_bytes())
|
||||
conn.flush_responses(timeout=0.5)
|
||||
|
||||
time.sleep(0.2)
|
||||
|
||||
# Step 3: UnsetAttributes(root)
|
||||
# Sets pPriv->attr = NULL but does not destroy the saver window.
|
||||
req = screensaver.UnsetAttributesRequest(
|
||||
opcode=opcode,
|
||||
drawable=conn.root_window,
|
||||
)
|
||||
conn.send_request(req.to_bytes())
|
||||
conn.flush_responses(timeout=0.5)
|
||||
|
||||
# Step 4: ForceScreenSaver(Active) again → triggers UAF.
|
||||
# CreateSaverWindow: cleanup block frees pPriv via
|
||||
# CheckScreenPrivate, then reads pPriv->attr from freed memory.
|
||||
req = x11.ForceScreenSaver(mode=x11.ScreenSaverActive)
|
||||
conn.send_request(req.to_bytes())
|
||||
time.sleep(0.5)
|
||||
|
||||
assert xserver.is_alive, (
|
||||
"Server crashed - CreateSaverWindow UAF (ZDI-CAN-30168)"
|
||||
)
|
||||
|
|
|
|||
Loading…
Add table
Reference in a new issue