From 72010daffe03cb7f2ff3fe4f1272abfba709515e Mon Sep 17 00:00:00 2001
From: Peter Hutterer <peter.hutterer@who-t.net>
Date: Mon, 20 Apr 2026 11:17:08 +1000
Subject: [PATCH xserver 2/9] sync: restart trigger list iteration in
 SyncChangeCounter after TriggerFired

This is the equivalent check to miSyncTriggerFence() from
commit f19ab94ba9c8 ("miext/sync: Fix use-after-free in miSyncTriggerFence()")

When a trigger fires via SyncAwaitTriggerFired, the resulting
FreeResource/FreeAwait call invokes SyncDeleteTriggerFromSyncObject for
every trigger in the same Await group. This unlinks and frees the
corresponding trigger list nodes - potentially including the node pnext
points to.

Fix by restarting iteration from the list head after a trigger fires, since
TriggerFired may have arbitrarily mutated the list. Triggers that have fired
are removed from the list by FreeAwait, so restarting cannot cause infinite
loops.

This vulnerability was discovered by:
Anonymous working with TrendAI Zero Day Initiative

ZDI-CAN-30164

Assisted-by: Claude:claude-opus-4-6
---
 Xext/sync.c | 23 ++++++++++++++++++++++-
 1 file changed, 22 insertions(+), 1 deletion(-)

diff --git ./Xext/sync.c ../Xext/sync.c
index 20ee8f620aef..d6bb5b4b6ce7 100644
--- ./Xext/sync.c
+++ ../Xext/sync.c
@@ -718,18 +718,39 @@ SyncChangeCounter(SyncCounter * pCounter, int64_t newval)
     SyncTriggerList *ptl, *pnext;
     int64_t oldval;
 
     oldval = SyncUpdateCounter(pCounter, newval);
 
     /* run through triggers to see if any become true */
     for (ptl = pCounter->sync.pTriglist; ptl; ptl = pnext) {
         pnext = ptl->next;
-        if ((*ptl->pTrigger->CheckTrigger) (ptl->pTrigger, oldval))
+        if ((*ptl->pTrigger->CheckTrigger) (ptl->pTrigger, oldval)) {
             (*ptl->pTrigger->TriggerFired) (ptl->pTrigger);
+            /* TriggerFired may have called SyncDeleteTriggerFromSyncObject
+             * for sibling triggers in the same Await group, freeing their
+             * trigger list nodes - potentially including pnext. Verify
+             * pnext is still on the counter's trigger list; if not,
+             * restart from the list head.
+             *
+             * Unlike miSyncTriggerFence() we cannot use a do/while
+             * restart loop here: counter trigger lists may contain alarm
+             * triggers which are not removed after firing and would cause
+             * an infinite loop when delta is 0.
+             */
+            if (pnext) {
+                SyncTriggerList *tmp;
+                for (tmp = pCounter->sync.pTriglist; tmp; tmp = tmp->next) {
+                    if (tmp == pnext)
+                        break;
+                }
+                if (!tmp)
+                    pnext = pCounter->sync.pTriglist;
+            }
+        }
     }
 
     if (IsSystemCounter(pCounter)) {
         SyncComputeBracketValues(pCounter);
     }
 }
 
 /* loosely based on dix/events.c/EventSelectForWindow */
-- 
2.53.0

