patches: 0008 fix - re-entrance guard + modiff gate + safety checks

Root cause: child frame path bypassed accessibilityUpdating guard.
Lisp calls in announceChildFrameCompletion triggered redisplay →
ns_update_end → postAccessibilityUpdates → infinite recursion.
This commit is contained in:
2026-02-28 16:11:23 +01:00
parent a8af58cff1
commit 659b9e2a1e

View File

@@ -1,4 +1,4 @@
From 959180846d5fb99044c57509c15de14451125119 Mon Sep 17 00:00:00 2001 From d108e94a713fde8c79082202471c6c92e3b7f276 Mon Sep 17 00:00:00 2001
From: Martin Sukany <martin@sukany.cz> From: Martin Sukany <martin@sukany.cz>
Date: Sat, 28 Feb 2026 16:01:29 +0100 Date: Sat, 28 Feb 2026 16:01:29 +0100
Subject: [PATCH 2/2] ns: announce child frame completion candidates for Subject: [PATCH 2/2] ns: announce child frame completion candidates for
@@ -16,6 +16,17 @@ find the selected candidate. Reuse ns_ax_face_is_selected from
the overlay patch to identify "current", "selected", and the overlay patch to identify "current", "selected", and
"selection" faces. "selection" faces.
Safety measures:
- The re-entrance guard (accessibilityUpdating) MUST come before the
child frame dispatch, because Lisp calls in the scan function can
trigger redisplay.
- BUF_MODIFF gating prevents redundant scans on every redisplay tick
and provides a secondary re-entrance guard.
- Frame state validation (WINDOWP, BUFFERP) handles partially
initialized child frames during creation.
- Buffer size limit (10000 chars) skips non-completion child frames
such as eldoc documentation or which-key popups.
Announce via AnnouncementRequested to NSApp with High priority. Announce via AnnouncementRequested to NSApp with High priority.
Use direct UAZoomChangeFocus (not the overlayZoomRect flag used Use direct UAZoomChangeFocus (not the overlayZoomRect flag used
for minibuffer overlay completion) because the child frame renders for minibuffer overlay completion) because the child frame renders
@@ -29,11 +40,11 @@ memory management complexity in static storage.
* src/nsterm.m (ns_ax_selected_child_frame_text): New function. * src/nsterm.m (ns_ax_selected_child_frame_text): New function.
(EmacsView announceChildFrameCompletion): New method. (EmacsView announceChildFrameCompletion): New method.
(EmacsView postAccessibilityUpdates): Dispatch to child frame (EmacsView postAccessibilityUpdates): Dispatch to child frame
handler for FRAME_PARENT_FRAME frames. handler for FRAME_PARENT_FRAME frames, under re-entrance guard.
--- ---
src/nsterm.h | 1 + src/nsterm.h | 1 +
src/nsterm.m | 163 +++++++++++++++++++++++++++++++++++++++++++++++++++ src/nsterm.m | 193 ++++++++++++++++++++++++++++++++++++++++++++++++++-
2 files changed, 164 insertions(+) 2 files changed, 193 insertions(+), 1 deletion(-)
diff --git a/src/nsterm.h b/src/nsterm.h diff --git a/src/nsterm.h b/src/nsterm.h
index 5c15639..21b2823 100644 index 5c15639..21b2823 100644
@@ -48,7 +59,7 @@ index 5c15639..21b2823 100644
@end @end
diff --git a/src/nsterm.m b/src/nsterm.m diff --git a/src/nsterm.m b/src/nsterm.m
index d13c5c7..59dc14d 100644 index d13c5c7..0b200f9 100644
--- a/src/nsterm.m --- a/src/nsterm.m
+++ b/src/nsterm.m +++ b/src/nsterm.m
@@ -7066,6 +7066,98 @@ ns_ax_selected_overlay_text (struct buffer *b, @@ -7066,6 +7066,98 @@ ns_ax_selected_overlay_text (struct buffer *b,
@@ -150,7 +161,7 @@ index d13c5c7..59dc14d 100644
/* Build accessibility text for window W, skipping invisible text. /* Build accessibility text for window W, skipping invisible text.
Populates *OUT_START with the buffer start charpos. Populates *OUT_START with the buffer start charpos.
Populates *OUT_RUNS with an array of visible runs and *OUT_NRUNS Populates *OUT_RUNS with an array of visible runs and *OUT_NRUNS
@@ -12299,6 +12391,68 @@ ns_ax_collect_windows (Lisp_Object window, EmacsView *view, @@ -12299,6 +12391,93 @@ ns_ax_collect_windows (Lisp_Object window, EmacsView *view,
The existing elements carry cached state (modiff, point) from the The existing elements carry cached state (modiff, point) from the
previous redisplay cycle. Rebuilding first would create fresh previous redisplay cycle. Rebuilding first would create fresh
elements with current values, making change detection impossible. */ elements with current values, making change detection impossible. */
@@ -164,9 +175,34 @@ index d13c5c7..59dc14d 100644
+- (void)announceChildFrameCompletion +- (void)announceChildFrameCompletion
+{ +{
+ static char *lastCandidate; + static char *lastCandidate;
+ static struct buffer *lastBuffer;
+ static EMACS_INT lastModiff;
+ +
+ /* Validate frame state --- child frames may be partially
+ initialized during creation. */
+ if (!WINDOWP (emacsframe->selected_window))
+ return;
+ struct window *w = XWINDOW (emacsframe->selected_window); + struct window *w = XWINDOW (emacsframe->selected_window);
+ if (!BUFFERP (w->contents))
+ return;
+ struct buffer *b = XBUFFER (w->contents); + struct buffer *b = XBUFFER (w->contents);
+
+ /* Only scan when the buffer content has actually changed.
+ This prevents redundant work on every redisplay tick and
+ also guards against re-entrance: if Lisp calls below
+ trigger redisplay, the modiff check short-circuits. */
+ EMACS_INT modiff = BUF_MODIFF (b);
+ if (b == lastBuffer && modiff == lastModiff)
+ return;
+ lastBuffer = b;
+ lastModiff = modiff;
+
+ /* Skip buffers larger than a typical completion popup.
+ This avoids scanning eldoc, which-key, or other child
+ frame buffers that are not completion UIs. */
+ if (BUF_ZV (b) - BUF_BEGV (b) > 10000)
+ return;
+
+ int selected_line = -1; + int selected_line = -1;
+ NSString *candidate + NSString *candidate
+ = ns_ax_selected_child_frame_text (b, w->contents, &selected_line); + = ns_ax_selected_child_frame_text (b, w->contents, &selected_line);
@@ -219,9 +255,17 @@ index d13c5c7..59dc14d 100644
- (void)postAccessibilityUpdates - (void)postAccessibilityUpdates
{ {
NSTRACE ("[EmacsView postAccessibilityUpdates]"); NSTRACE ("[EmacsView postAccessibilityUpdates]");
@@ -12307,6 +12461,15 @@ ns_ax_collect_windows (Lisp_Object window, EmacsView *view, @@ -12309,11 +12488,23 @@ ns_ax_collect_windows (Lisp_Object window, EmacsView *view,
if (!emacsframe || !ns_accessibility_enabled)
/* Re-entrance guard: VoiceOver callbacks during notification posting
can trigger redisplay, which calls ns_update_end, which calls us
- again. Prevent infinite recursion. */
+ again. Prevent infinite recursion. This MUST come before the
+ child frame check --- announceChildFrameCompletion makes Lisp
+ calls that can trigger redisplay. */
if (accessibilityUpdating)
return; return;
accessibilityUpdating = YES;
+ /* Child frame completion popup (Corfu, Company-box, etc.). + /* Child frame completion popup (Corfu, Company-box, etc.).
+ Child frames don't participate in the accessibility tree; + Child frames don't participate in the accessibility tree;
@@ -229,12 +273,13 @@ index d13c5c7..59dc14d 100644
+ if (FRAME_PARENT_FRAME (emacsframe)) + if (FRAME_PARENT_FRAME (emacsframe))
+ { + {
+ [self announceChildFrameCompletion]; + [self announceChildFrameCompletion];
+ accessibilityUpdating = NO;
+ return; + return;
+ } + }
+ +
/* Re-entrance guard: VoiceOver callbacks during notification posting /* Detect window tree change (split, delete, new buffer). Compare
can trigger redisplay, which calls ns_update_end, which calls us FRAME_ROOT_WINDOW --- if it changed, the tree structure changed. */
again. Prevent infinite recursion. */ Lisp_Object curRoot = FRAME_ROOT_WINDOW (emacsframe);
-- --
2.43.0 2.43.0