mirror of
https://github.com/X11Libre/xserver.git
synced 2026-10-26 01:59:57 +00:00
present: unlink notifies from window list in present_clear_window_notifies
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)
This commit is contained in:
parent
02359bf013
commit
5d3b8520ec
3 changed files with 180 additions and 4 deletions
|
|
@ -32,14 +32,14 @@
|
|||
void
|
||||
present_clear_window_notifies(WindowPtr window)
|
||||
{
|
||||
present_notify_ptr notify;
|
||||
present_notify_ptr notify, tmp;
|
||||
present_window_priv_ptr window_priv = present_window_priv(window);
|
||||
|
||||
if (!window_priv)
|
||||
return;
|
||||
|
||||
xorg_list_for_each_entry(notify, &window_priv->notifies, window_list) {
|
||||
notify->window = NULL;
|
||||
xorg_list_for_each_entry_safe(notify, tmp, &window_priv->notifies, window_list) {
|
||||
present_free_window_notify(notify);
|
||||
}
|
||||
}
|
||||
|
||||
|
|
@ -50,6 +50,7 @@ present_clear_window_notifies(WindowPtr window)
|
|||
void
|
||||
present_free_window_notify(present_notify_ptr notify)
|
||||
{
|
||||
notify->window = NULL;
|
||||
xorg_list_del(¬ify->window_list);
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -470,6 +470,96 @@ class PolyText8Request:
|
|||
)
|
||||
|
||||
|
||||
@dataclass
|
||||
class DestroyWindowRequest:
|
||||
"""X11 DestroyWindow request (opcode 4).
|
||||
|
||||
Wire format:
|
||||
CARD8 opcode (4)
|
||||
CARD8 unused
|
||||
CARD16 length (2)
|
||||
CARD32 window
|
||||
"""
|
||||
|
||||
window: int
|
||||
|
||||
def to_bytes(self, byte_order: str = "<") -> bytes:
|
||||
return struct.pack(
|
||||
f"{byte_order}BBH I",
|
||||
4, # DestroyWindow opcode
|
||||
0,
|
||||
2, # length = 2 words
|
||||
self.window,
|
||||
)
|
||||
|
||||
|
||||
@dataclass
|
||||
class MapWindowRequest:
|
||||
"""X11 MapWindow request (opcode 8).
|
||||
|
||||
Wire format:
|
||||
CARD8 opcode (8)
|
||||
CARD8 unused
|
||||
CARD16 length (2)
|
||||
CARD32 window
|
||||
"""
|
||||
|
||||
window: int
|
||||
|
||||
def to_bytes(self, byte_order: str = "<") -> bytes:
|
||||
return struct.pack(
|
||||
f"{byte_order}BBH I",
|
||||
8, # MapWindow opcode
|
||||
0,
|
||||
2, # length = 2 words
|
||||
self.window,
|
||||
)
|
||||
|
||||
|
||||
@dataclass
|
||||
class WarpPointerRequest:
|
||||
"""X11 WarpPointer request (opcode 41).
|
||||
|
||||
Wire format:
|
||||
CARD8 opcode (41)
|
||||
CARD8 unused
|
||||
CARD16 length (6)
|
||||
CARD32 srcWindow (None = 0)
|
||||
CARD32 dstWindow (None = 0)
|
||||
INT16 srcX
|
||||
INT16 srcY
|
||||
CARD16 srcWidth
|
||||
CARD16 srcHeight
|
||||
INT16 dstX
|
||||
INT16 dstY
|
||||
"""
|
||||
|
||||
dst_window: int = 0
|
||||
dst_x: int = 0
|
||||
dst_y: int = 0
|
||||
src_window: int = 0
|
||||
src_x: int = 0
|
||||
src_y: int = 0
|
||||
src_width: int = 0
|
||||
src_height: int = 0
|
||||
|
||||
def to_bytes(self, byte_order: str = "<") -> bytes:
|
||||
return struct.pack(
|
||||
f"{byte_order}BBH II hh HH hh",
|
||||
41, # WarpPointer opcode
|
||||
0,
|
||||
6, # length = 6 words (24 bytes)
|
||||
self.src_window,
|
||||
self.dst_window,
|
||||
self.src_x,
|
||||
self.src_y,
|
||||
self.src_width,
|
||||
self.src_height,
|
||||
self.dst_x,
|
||||
self.dst_y,
|
||||
)
|
||||
|
||||
|
||||
@dataclass
|
||||
class ForceScreenSaver:
|
||||
"""X11 ForceScreenSaver request."""
|
||||
|
|
|
|||
|
|
@ -2,8 +2,10 @@
|
|||
#
|
||||
# Tests for Present extension.
|
||||
|
||||
import time
|
||||
|
||||
import pytest
|
||||
from proto import present, x11
|
||||
from proto import present, sync, x11
|
||||
from xclient import Extension, X11Error
|
||||
|
||||
|
||||
|
|
@ -147,3 +149,86 @@ class TestPresentNotify:
|
|||
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)"
|
||||
)
|
||||
|
|
|
|||
Loading…
Reference in a new issue