mirror of
https://github.com/X11Libre/xserver.git
synced 2026-10-28 02:21:08 +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
|
void
|
||||||
present_clear_window_notifies(WindowPtr window)
|
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);
|
present_window_priv_ptr window_priv = present_window_priv(window);
|
||||||
|
|
||||||
if (!window_priv)
|
if (!window_priv)
|
||||||
return;
|
return;
|
||||||
|
|
||||||
xorg_list_for_each_entry(notify, &window_priv->notifies, window_list) {
|
xorg_list_for_each_entry_safe(notify, tmp, &window_priv->notifies, window_list) {
|
||||||
notify->window = NULL;
|
present_free_window_notify(notify);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -50,6 +50,7 @@ present_clear_window_notifies(WindowPtr window)
|
||||||
void
|
void
|
||||||
present_free_window_notify(present_notify_ptr notify)
|
present_free_window_notify(present_notify_ptr notify)
|
||||||
{
|
{
|
||||||
|
notify->window = NULL;
|
||||||
xorg_list_del(¬ify->window_list);
|
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
|
@dataclass
|
||||||
class ForceScreenSaver:
|
class ForceScreenSaver:
|
||||||
"""X11 ForceScreenSaver request."""
|
"""X11 ForceScreenSaver request."""
|
||||||
|
|
|
||||||
|
|
@ -2,8 +2,10 @@
|
||||||
#
|
#
|
||||||
# Tests for Present extension.
|
# Tests for Present extension.
|
||||||
|
|
||||||
|
import time
|
||||||
|
|
||||||
import pytest
|
import pytest
|
||||||
from proto import present, x11
|
from proto import present, sync, x11
|
||||||
from xclient import Extension, X11Error
|
from xclient import Extension, X11Error
|
||||||
|
|
||||||
|
|
||||||
|
|
@ -147,3 +149,86 @@ class TestPresentNotify:
|
||||||
f"{bad_window_errors} - notify window IDs not "
|
f"{bad_window_errors} - notify window IDs not "
|
||||||
"byte-swapped in sproc_present_pixmap"
|
"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