mirror of
https://github.com/X11Libre/xserver.git
synced 2026-11-15 05:23:40 +00:00
When a window is destroyed, present_clear_window_notifies()
does not remove the notify list node from the window. The notify
may then be freed as part of the window_priv, leaving dangling
pointers in place.
When the vblank owning the notify is later torn down,
present_free_window_notify() calls xorg_list_del() which
writes through these dangling pointers (use-after-free write).
This happens when Present cross-window notifies are used: a
PresentPixmap on window A can reference window B as a notify target.
An untriggered wait_fence keeps the vblank alive so that B can be
destroyed first, creating the dangling-pointer window.
Fix this by calling the free function we have (and moving the window
reset into that function too). present_clear_window_notifies is only
called from one place anyway.
This vulnerability was discovered by:
Anonymous working with TrendAI Zero Day Initiative
ZDI-CAN-31830
CVE-2026-93515
Signed-off-by: Peter Hutterer <peter.hutterer@who-t.net>
Assisted-by: Claude:claude-opus-4-6
Part-of: <https://gitlab.freedesktop.org/xorg/xserver/-/merge_requests/2341>
(cherry picked from commit 1b6955c310)
234 lines
8.1 KiB
Python
234 lines
8.1 KiB
Python
# SPDX-License-Identifier: MIT
|
|
#
|
|
# Tests for Present extension.
|
|
|
|
import time
|
|
|
|
import pytest
|
|
from proto import present, sync, x11
|
|
from xclient import Extension, X11Error
|
|
|
|
|
|
@pytest.fixture
|
|
def present_xclient(xclient):
|
|
"""Provide an xclient with Present initialized."""
|
|
ext = xclient.query_extension(Extension.PRESENT)
|
|
if not ext:
|
|
pytest.skip("Present extension not available")
|
|
|
|
req = present.QueryVersionRequest(opcode=ext.opcode)
|
|
xclient.send_request(req)
|
|
xclient.recv_response(timeout=5.0)
|
|
|
|
return xclient
|
|
|
|
|
|
@pytest.fixture
|
|
def present_xclient_swapped(xclient_swapped):
|
|
"""Provide a byte-swapped xclient with Present initialized."""
|
|
ext = xclient_swapped.query_extension(Extension.PRESENT)
|
|
if not ext:
|
|
pytest.skip("Present extension not available")
|
|
|
|
req = present.QueryVersionRequest(opcode=ext.opcode)
|
|
xclient_swapped.send_request(req)
|
|
xclient_swapped.recv_response(timeout=5.0)
|
|
|
|
return xclient_swapped
|
|
|
|
|
|
class TestPresentSelectInput:
|
|
@pytest.mark.swapped_client
|
|
def test_present_select_input_eid_swapped(self, xserver, present_xclient_swapped):
|
|
"""
|
|
sproc_present_select_input was missing swapl(&stuff->eid).
|
|
Without the swap, the eid fails LEGAL_NEW_RESOURCE because
|
|
the client-bits portion of the garbled XID doesn't match
|
|
clientAsMask → BadIDChoice error.
|
|
|
|
Fixed in commit a5ac3c871219 ("present: add missing byte
|
|
swapping for various fields").
|
|
"""
|
|
conn = present_xclient_swapped
|
|
ext = conn.query_extension(Extension.PRESENT)
|
|
|
|
win = conn.create_window()
|
|
eid = conn.alloc_id()
|
|
|
|
# Use a non-zero event_mask so the server reaches
|
|
# LEGAL_NEW_RESOURCE(eid). With event_mask=0 the server
|
|
# returns Success immediately without validating the eid.
|
|
PresentConfigureNotifyMask = 1
|
|
req = present.SelectInputRequest(
|
|
opcode=ext.opcode,
|
|
eid=eid,
|
|
window=win,
|
|
event_mask=PresentConfigureNotifyMask,
|
|
)
|
|
conn.send_request(req)
|
|
responses = conn.flush_responses(timeout=1.0)
|
|
|
|
assert xserver.is_alive, "Server crashed"
|
|
|
|
# With the fix: no error (void request succeeds silently).
|
|
# Without the fix: BadIDChoice error.
|
|
errors = [r for r in responses if isinstance(r, X11Error)]
|
|
assert len(errors) == 0, (
|
|
f"PresentSelectInput returned error(s): {errors} - "
|
|
"eid not swapped → BadIDChoice"
|
|
)
|
|
|
|
|
|
class TestPresentNotify:
|
|
"""Tests for PresentPixmap notify array byte-swap fix.
|
|
|
|
Fix: present: Fix missing byte swaps in sproc_present_pixmap()
|
|
|
|
The xPresentNotify array following the fixed header was not
|
|
byte-swapped at all. Each entry has window (CARD32) and serial
|
|
(CARD32) fields that need swapl(). Without swapping, a
|
|
byte-swapped client's window IDs are garbled, causing
|
|
dixLookupWindow to fail with BadWindow.
|
|
|
|
Fixed in commit 925edb6c9e ("present: Fix missing byte swaps in
|
|
sproc_present_pixmap()").
|
|
"""
|
|
|
|
@pytest.mark.swapped_client
|
|
def test_present_pixmap_notifies_window_swapped(
|
|
self, xserver, present_xclient_swapped
|
|
):
|
|
"""
|
|
sproc_present_pixmap was missing byte swaps for the variable-length
|
|
xPresentNotify array.
|
|
|
|
Send a PresentPixmap request with a notify entry whose window
|
|
field is a valid window created by this client. Without the swap,
|
|
the window ID is garbled and dixLookupWindow fails with BadWindow.
|
|
With the swap, the window ID is correctly interpreted.
|
|
|
|
Fixed in commit 925edb6c9e ("present: Fix missing byte swaps in
|
|
sproc_present_pixmap()").
|
|
"""
|
|
conn = present_xclient_swapped
|
|
ext = conn.query_extension(Extension.PRESENT)
|
|
|
|
win = conn.create_window()
|
|
pixmap = conn.create_pixmap()
|
|
|
|
# The notify window is the same window as the main request window.
|
|
# With the fix, the window ID in the notify is correctly swapped
|
|
# and the lookup succeeds. Without the fix, the garbled ID causes
|
|
# BadWindow.
|
|
notify = present.PresentNotify(window=win, serial=1)
|
|
|
|
req = present.PixmapRequest(
|
|
opcode=ext.opcode,
|
|
window=win,
|
|
pixmap=pixmap,
|
|
serial=0,
|
|
notifies=[notify],
|
|
)
|
|
conn.send_request(req)
|
|
responses = conn.flush_responses(timeout=1.0)
|
|
|
|
assert xserver.is_alive, "Server crashed"
|
|
|
|
# With the fix: either success (no error for void request) or
|
|
# a non-BadWindow error (e.g. BadMatch from the present
|
|
# implementation). The key point is no BadWindow (error 3).
|
|
# Without the fix: BadWindow because the notify's window ID
|
|
# was not byte-swapped.
|
|
bad_window_errors = [
|
|
r
|
|
for r in responses
|
|
if isinstance(r, X11Error) and r.error_code == x11.BadWindow
|
|
]
|
|
assert len(bad_window_errors) == 0, (
|
|
f"PresentPixmap returned BadWindow error(s): "
|
|
f"{bad_window_errors} - notify window IDs not "
|
|
"byte-swapped in sproc_present_pixmap"
|
|
)
|
|
|
|
@pytest.mark.asan
|
|
def test_cross_window_notify_uaf(self, xserver, present_xclient):
|
|
"""
|
|
ZDI-CAN-31830: Use-after-free write in Present cross-window
|
|
notify teardown.
|
|
|
|
A PresentPixmap request can include a notify list referencing
|
|
windows other than the target window. Each notify entry is
|
|
linked into both the vblank's notifies array and the
|
|
referenced window's notify list. When the notify-target
|
|
window (winB) is destroyed, we expect the notifies
|
|
to be cleaned up so that when the vblank is later torn
|
|
down (e.g. by destroying winA), we don't run into
|
|
any dangling pointers.
|
|
|
|
The bug requires an untriggered wait_fence to keep the vblank
|
|
alive across the window destructions.
|
|
"""
|
|
conn = present_xclient
|
|
present_ext = conn.query_extension(Extension.PRESENT)
|
|
if not present_ext:
|
|
pytest.skip("Present extension not available")
|
|
|
|
sync_ext = conn.query_extension(Extension.SYNC)
|
|
if not sync_ext:
|
|
pytest.skip("SYNC extension not available")
|
|
|
|
# Negotiate SYNC version
|
|
req = sync.InitializeRequest(opcode=sync_ext.opcode)
|
|
conn.send_request(req)
|
|
conn.recv_response(timeout=5.0)
|
|
|
|
# Create two windows and a pixmap
|
|
win_a = conn.create_window()
|
|
win_b = conn.create_window()
|
|
pixmap = conn.create_pixmap()
|
|
|
|
# Create an untriggered fence to keep the vblank alive
|
|
fence_id = conn.alloc_id()
|
|
req = sync.CreateFenceRequest(
|
|
opcode=sync_ext.opcode,
|
|
drawable=conn.root_window,
|
|
fence_id=fence_id,
|
|
initially_triggered=0,
|
|
)
|
|
conn.send_request(req)
|
|
conn.flush_responses(timeout=0.5)
|
|
|
|
# PresentPixmap on winA with notify targeting winB and an
|
|
# untriggered wait_fence. The fence prevents the vblank from
|
|
# completing, keeping the notify entries alive.
|
|
notify = present.PresentNotify(window=win_b, serial=1)
|
|
req = present.PixmapRequest(
|
|
opcode=present_ext.opcode,
|
|
window=win_a,
|
|
pixmap=pixmap,
|
|
serial=0,
|
|
wait_fence=fence_id,
|
|
notifies=[notify],
|
|
)
|
|
conn.send_request(req)
|
|
conn.flush_responses(timeout=0.5)
|
|
|
|
# Destroy winB: present_clear_window_notifies runs, but
|
|
# without the fix it does not unlink the notify node.
|
|
req = x11.DestroyWindowRequest(window=win_b)
|
|
conn.send_request(req)
|
|
conn.flush_responses(timeout=0.5)
|
|
|
|
# Destroy winA: triggers vblank teardown which calls
|
|
# present_destroy_notifies -> xorg_list_del on the dangling
|
|
# node -> UAF write.
|
|
req = x11.DestroyWindowRequest(window=win_a)
|
|
conn.send_request(req)
|
|
conn.flush_responses(timeout=0.5)
|
|
|
|
time.sleep(0.5)
|
|
|
|
assert xserver.is_alive, (
|
|
"Server crashed - Present cross-window notify "
|
|
"use-after-free (ZDI-CAN-31830)"
|
|
)
|