From acc2a2985e938b9d95c6097def9431011845e7ef Mon Sep 17 00:00:00 2001 From: Daneel Date: Sat, 28 Feb 2026 18:45:30 +0100 Subject: [PATCH] patches: add 0009 resource safety hardening + update 0007/0008 MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit New patch 0009 fixes HIGH severity issues from Opus review: - Announcement coalescing (50ms debounce) - cachedText retain+autorelease in accessibilityValue - EmacsView dealloc: nil out emacsView on all AX elements - Nil guards on protocol methods + overlayZoomActive 0007 updated: revert accidental em-dash→triple-dash, add overlayZoomActive nil guards 0008 updated: specpdl exception safety for accessibilityUpdating, lastChildFrameBuffer staticpro Series now 9 patches total (0001-0006 unchanged, 0007-0009 new/updated). --- ...lay-completion-candidates-for-VoiceO.patch | 34 ++--- ...d-frame-completion-candidates-for-Vo.patch | 47 +++++-- ...ceOver-accessibility-resource-safety.patch | 124 ++++++++++++++++++ 3 files changed, 179 insertions(+), 26 deletions(-) create mode 100644 patches/0009-ns-harden-VoiceOver-accessibility-resource-safety.patch diff --git a/patches/0007-ns-announce-overlay-completion-candidates-for-VoiceO.patch b/patches/0007-ns-announce-overlay-completion-candidates-for-VoiceO.patch index 967f7e9..6c68088 100644 --- a/patches/0007-ns-announce-overlay-completion-candidates-for-VoiceO.patch +++ b/patches/0007-ns-announce-overlay-completion-candidates-for-VoiceO.patch @@ -1,7 +1,7 @@ -From 6e907a1000a8b138976d6a906e40449fdf1a61c5 Mon Sep 17 00:00:00 2001 +From 8712cf8f567f3b0c02cc70a93aff931faa3a2df3 Mon Sep 17 00:00:00 2001 From: Martin Sukany Date: Sat, 28 Feb 2026 14:46:25 +0100 -Subject: [PATCH 1/2] ns: announce overlay completion candidates for VoiceOver +Subject: [PATCH 1/3] ns: announce overlay completion candidates for VoiceOver Completion frameworks such as Vertico, Ivy, and Icomplete render candidates via overlay before-string/after-string properties rather @@ -52,8 +52,8 @@ Independent overlay branch, BUF_CHARS_MODIFF gating, candidate announcement with overlay Zoom rect storage. --- src/nsterm.h | 3 + - src/nsterm.m | 331 +++++++++++++++++++++++++++++++++++++++++++++------ - 2 files changed, 298 insertions(+), 36 deletions(-) + src/nsterm.m | 333 +++++++++++++++++++++++++++++++++++++++++++++------ + 2 files changed, 300 insertions(+), 36 deletions(-) diff --git a/src/nsterm.h b/src/nsterm.h index 51c30ca..5c15639 100644 @@ -77,7 +77,7 @@ index 51c30ca..5c15639 100644 BOOL font_panel_active; NSFont *font_panel_result; diff --git a/src/nsterm.m b/src/nsterm.m -index 1780194..143e784 100644 +index 1780194..c1fc3cb 100644 --- a/src/nsterm.m +++ b/src/nsterm.m @@ -3258,7 +3258,12 @@ ns_draw_window_cursor (struct window *w, struct glyph_row *glyph_row, @@ -403,7 +403,7 @@ index 1780194..143e784 100644 self.cachedPoint = point; NSDictionary *change = @{ -@@ -8789,16 +8938,126 @@ ns_ax_completion_text_for_span (EmacsAccessibilityBuffer *elem, +@@ -8789,16 +8938,128 @@ ns_ax_completion_text_for_span (EmacsAccessibilityBuffer *elem, BOOL markActive = !NILP (BVAR (b, mark_active)); /* --- Text changed (edit) --- */ @@ -428,7 +428,8 @@ index 1780194..143e784 100644 + if (chars_modiff != self.cachedCharsModiff) + { + self.cachedCharsModiff = chars_modiff; -+ self.emacsView->overlayZoomActive = NO; ++ if (self.emacsView) ++ self.emacsView->overlayZoomActive = NO; + [self postTextChangedNotification:point]; + textDidChange = YES; + } @@ -516,7 +517,8 @@ index 1780194..143e784 100644 + (minibuffer exit, C-g, etc.) or overlay has no + recognizable selection face. Return Zoom to the + text cursor. */ -+ self.emacsView->overlayZoomActive = NO; ++ if (self.emacsView) ++ self.emacsView->overlayZoomActive = NO; + } } @@ -534,7 +536,7 @@ index 1780194..143e784 100644 { ptrdiff_t oldPoint = self.cachedPoint; BOOL oldMarkActive = self.cachedMarkActive; -@@ -8966,7 +9225,7 @@ ns_ax_completion_text_for_span (EmacsAccessibilityBuffer *elem, +@@ -8966,7 +9227,7 @@ ns_ax_completion_text_for_span (EmacsAccessibilityBuffer *elem, /* =================================================================== @@ -543,7 +545,7 @@ index 1780194..143e784 100644 =================================================================== */ /* Scan visible range of window W for interactive spans. -@@ -9157,7 +9416,7 @@ ns_ax_scan_interactive_spans (struct window *w, +@@ -9157,7 +9418,7 @@ ns_ax_scan_interactive_spans (struct window *w, - (BOOL) isAccessibilityFocused { /* Read the cached point stored by EmacsAccessibilityBuffer on the main @@ -552,7 +554,7 @@ index 1780194..143e784 100644 EmacsAccessibilityBuffer *pb = self.parentBuffer; if (!pb) return NO; -@@ -9174,7 +9433,7 @@ ns_ax_scan_interactive_spans (struct window *w, +@@ -9174,7 +9435,7 @@ ns_ax_scan_interactive_spans (struct window *w, dispatch_async (dispatch_get_main_queue (), ^{ /* lwin is a Lisp_Object captured by value. This is GC-safe because Lisp_Objects are tagged integers/pointers that @@ -561,7 +563,7 @@ index 1780194..143e784 100644 Emacs. The WINDOW_LIVE_P check below guards against the window being deleted between capture and execution. */ if (!WINDOWP (lwin) || NILP (Fwindow_live_p (lwin))) -@@ -9200,7 +9459,7 @@ ns_ax_scan_interactive_spans (struct window *w, +@@ -9200,7 +9461,7 @@ ns_ax_scan_interactive_spans (struct window *w, @end @@ -570,7 +572,7 @@ index 1780194..143e784 100644 Methods are kept here (same .m file) so they access the ivars declared in the @interface ivar block. */ @implementation EmacsAccessibilityBuffer (InteractiveSpans) -@@ -11922,7 +12181,7 @@ ns_ax_collect_windows (Lisp_Object window, EmacsView *view, +@@ -11922,7 +12183,7 @@ ns_ax_collect_windows (Lisp_Object window, EmacsView *view, if (WINDOW_LEAF_P (w)) { @@ -579,7 +581,7 @@ index 1780194..143e784 100644 EmacsAccessibilityBuffer *elem = [existing objectForKey:[NSValue valueWithPointer:w]]; if (!elem) -@@ -11956,7 +12215,7 @@ ns_ax_collect_windows (Lisp_Object window, EmacsView *view, +@@ -11956,7 +12217,7 @@ ns_ax_collect_windows (Lisp_Object window, EmacsView *view, } else { @@ -588,7 +590,7 @@ index 1780194..143e784 100644 Lisp_Object child = w->contents; while (!NILP (child)) { -@@ -12068,7 +12327,7 @@ ns_ax_collect_windows (Lisp_Object window, EmacsView *view, +@@ -12068,7 +12329,7 @@ ns_ax_collect_windows (Lisp_Object window, EmacsView *view, accessibilityUpdating = YES; /* Detect window tree change (split, delete, new buffer). Compare @@ -597,7 +599,7 @@ index 1780194..143e784 100644 Lisp_Object curRoot = FRAME_ROOT_WINDOW (emacsframe); if (!EQ (curRoot, lastRootWindow)) { -@@ -12077,12 +12336,12 @@ ns_ax_collect_windows (Lisp_Object window, EmacsView *view, +@@ -12077,12 +12338,12 @@ ns_ax_collect_windows (Lisp_Object window, EmacsView *view, } /* If tree is stale, rebuild FIRST so we don't iterate freed diff --git a/patches/0008-ns-announce-child-frame-completion-candidates-for-Vo.patch b/patches/0008-ns-announce-child-frame-completion-candidates-for-Vo.patch index 3a213af..23e4c60 100644 --- a/patches/0008-ns-announce-child-frame-completion-candidates-for-Vo.patch +++ b/patches/0008-ns-announce-child-frame-completion-candidates-for-Vo.patch @@ -1,7 +1,7 @@ -From 8564e4989f5f358092bd1494c3894a42974ee6e1 Mon Sep 17 00:00:00 2001 +From 7d20ec80aa0d4ca97fa789f9b85389e25d2ff719 Mon Sep 17 00:00:00 2001 From: Martin Sukany Date: Sat, 28 Feb 2026 16:01:29 +0100 -Subject: [PATCH 2/2] ns: announce child frame completion candidates for +Subject: [PATCH 2/3] ns: announce child frame completion candidates for VoiceOver Completion frameworks such as Corfu, Company-box, and similar @@ -43,8 +43,8 @@ childFrameCompletionActive flag. refocus parent buffer element when child frame closes. --- src/nsterm.h | 2 + - src/nsterm.m | 279 ++++++++++++++++++++++++++++++++++++++++++++++++++- - 2 files changed, 278 insertions(+), 3 deletions(-) + src/nsterm.m | 306 ++++++++++++++++++++++++++++++++++++++++++++++++++- + 2 files changed, 305 insertions(+), 3 deletions(-) diff --git a/src/nsterm.h b/src/nsterm.h index 5c15639..8b34300 100644 @@ -67,7 +67,7 @@ index 5c15639..8b34300 100644 @end diff --git a/src/nsterm.m b/src/nsterm.m -index 143e784..da1a319 100644 +index c1fc3cb..abecb4c 100644 --- a/src/nsterm.m +++ b/src/nsterm.m @@ -7066,6 +7066,110 @@ ns_ax_selected_overlay_text (struct buffer *b, @@ -181,7 +181,7 @@ index 143e784..da1a319 100644 /* Build accessibility text for window W, skipping invisible text. Populates *OUT_START with the buffer start charpos. Populates *OUT_RUNS with an array of visible runs and *OUT_NRUNS -@@ -12311,6 +12415,122 @@ ns_ax_collect_windows (Lisp_Object window, EmacsView *view, +@@ -12313,6 +12417,146 @@ ns_ax_collect_windows (Lisp_Object window, EmacsView *view, The existing elements carry cached state (modiff, point) from the previous redisplay cycle. Rebuilding first would create fresh elements with current values, making change detection impossible. */ @@ -289,6 +289,30 @@ index 143e784..da1a319 100644 +static EMACS_INT lastChildFrameModiff; +static char *lastChildFrameCandidate; + ++/* Child frame completion dedup state. File-scope so that ++ lastChildFrameBuffer can be staticpro'd against GC. */ ++static Lisp_Object lastChildFrameBuffer; ++static EMACS_INT lastChildFrameModiff; ++static char *lastChildFrameCandidate; ++ ++/* Reset the re-entrance guard when unwinding past ++ postAccessibilityUpdates due to a Lisp signal (longjmp). ++ Without this, a signal during Lisp calls (e.g. Fget_char_property ++ in overlay or child frame scanning) would leave ++ accessibilityUpdating = YES permanently, suppressing all future ++ accessibility notifications. */ ++static void ++ns_ax_reset_accessibility_updating (void *view) ++{ ++ ((EmacsView *)view)->accessibilityUpdating = NO; ++} ++ ++/* Child frame completion dedup state. File-scope so that ++ lastChildFrameBuffer can be staticpro'd against GC. */ ++static Lisp_Object lastChildFrameBuffer; ++static EMACS_INT lastChildFrameModiff; ++static char *lastChildFrameCandidate; ++ +/* Reset the re-entrance guard when unwinding past + postAccessibilityUpdates due to a Lisp signal (longjmp). + Without this, a signal during Lisp calls (e.g. Fget_char_property @@ -304,7 +328,7 @@ index 143e784..da1a319 100644 - (void)postAccessibilityUpdates { NSTRACE ("[EmacsView postAccessibilityUpdates]"); -@@ -12321,10 +12541,60 @@ ns_ax_collect_windows (Lisp_Object window, EmacsView *view, +@@ -12323,10 +12567,60 @@ ns_ax_collect_windows (Lisp_Object window, EmacsView *view, /* Re-entrance guard: VoiceOver callbacks during notification posting can trigger redisplay, which calls ns_update_end, which calls us @@ -366,7 +390,7 @@ index 143e784..da1a319 100644 /* Detect window tree change (split, delete, new buffer). Compare FRAME_ROOT_WINDOW --- if it changed, the tree structure changed. */ -@@ -12355,7 +12625,7 @@ ns_ax_collect_windows (Lisp_Object window, EmacsView *view, +@@ -12357,7 +12651,7 @@ ns_ax_collect_windows (Lisp_Object window, EmacsView *view, NSAccessibilityFocusedUIElementChangedNotification); lastSelectedWindow = emacsframe->selected_window; @@ -375,7 +399,7 @@ index 143e784..da1a319 100644 return; } -@@ -12399,7 +12669,7 @@ ns_ax_collect_windows (Lisp_Object window, EmacsView *view, +@@ -12401,7 +12695,7 @@ ns_ax_collect_windows (Lisp_Object window, EmacsView *view, NSAccessibilityFocusedUIElementChangedNotification); } @@ -384,12 +408,15 @@ index 143e784..da1a319 100644 } /* ---- Cursor position for Zoom (via accessibilityBoundsForRange:) ---- -@@ -14341,6 +14611,9 @@ syms_of_nsterm (void) +@@ -14343,6 +14637,12 @@ syms_of_nsterm (void) DEFSYM (Qns_ax_completion, "completion"); DEFSYM (Qns_ax_completions_highlight, "completions-highlight"); DEFSYM (Qns_ax_backtab, "backtab"); + + lastChildFrameBuffer = Qnil; ++ staticpro (&lastChildFrameBuffer); ++ ++ lastChildFrameBuffer = Qnil; + staticpro (&lastChildFrameBuffer); /* Qmouse_face and Qkeymap are defined in textprop.c / keymap.c. */ Fput (Qalt, Qmodifier_value, make_fixnum (alt_modifier)); diff --git a/patches/0009-ns-harden-VoiceOver-accessibility-resource-safety.patch b/patches/0009-ns-harden-VoiceOver-accessibility-resource-safety.patch new file mode 100644 index 0000000..c0fe25b --- /dev/null +++ b/patches/0009-ns-harden-VoiceOver-accessibility-resource-safety.patch @@ -0,0 +1,124 @@ +From 0812e650c24f90bda79368078fa0ad45c18f39d2 Mon Sep 17 00:00:00 2001 +From: T +Date: Sat, 28 Feb 2026 18:45:14 +0100 +Subject: [PATCH 3/3] ns: harden VoiceOver accessibility resource safety + +Fix several resource safety issues found during maintainer review: + +* Announcement coalescing: add 50ms minimum interval between + AnnouncementRequested notifications to prevent VoiceOver speech + synthesizer stalls from rapid-fire high-priority interruptions + (e.g. holding C-n in a completion list). + +* cachedText thread safety: return [[cachedText retain] autorelease] + from accessibilityValue to prevent use-after-free when the main + thread replaces cachedText while the AX server thread is still + using the previous value. + +* EmacsView dealloc safety: nil out emacsView back-references on + all accessibility elements before releasing them. Queued + dispatch_async blocks that hold a retained element reference would + otherwise access a dangling emacsView pointer. + +* Nil guards: add emacsView nil checks in accessibilityParent, + accessibilityWindow, accessibilityTopLevelUIElement, and + overlayZoomActive access sites. + +* src/nsterm.m (ns_ax_post_notification_with_info): Add timestamp + coalescing for AnnouncementRequested. +(accessibilityValue): Return retained+autoreleased cachedText. +(dealloc): Nil out emacsView on all accessibility elements. +(accessibilityParent, accessibilityWindow) +(accessibilityTopLevelUIElement): Add nil guards. +--- + src/nsterm.m | 38 +++++++++++++++++++++++++++++++++++++- + 1 file changed, 37 insertions(+), 1 deletion(-) + +diff --git a/src/nsterm.m b/src/nsterm.m +index abecb4c..3724b05 100644 +--- a/src/nsterm.m ++++ b/src/nsterm.m +@@ -7521,11 +7521,32 @@ ns_ax_post_notification (id element, + }); + } + ++/* Minimum interval between AnnouncementRequested notifications ++ (in seconds). VoiceOver can stall if overwhelmed with rapid-fire ++ high-priority announcements that each interrupt the previous ++ utterance. 50ms lets the speech synthesizer start before the ++ next interruption. */ ++#define NS_AX_ANNOUNCE_MIN_INTERVAL 0.05 ++ + static inline void + ns_ax_post_notification_with_info (id element, + NSAccessibilityNotificationName name, + NSDictionary *info) + { ++ /* Coalesce AnnouncementRequested: skip if the previous one was ++ less than NS_AX_ANNOUNCE_MIN_INTERVAL seconds ago. Prevents ++ speech synthesizer stalls from rapid-fire high-priority ++ interruptions (e.g. holding C-n in a completion list). */ ++ if ([name isEqualToString: ++ NSAccessibilityAnnouncementRequestedNotification]) ++ { ++ static CFAbsoluteTime lastAnnouncementTime; ++ CFAbsoluteTime now = CFAbsoluteTimeGetCurrent (); ++ if (now - lastAnnouncementTime < NS_AX_ANNOUNCE_MIN_INTERVAL) ++ return; ++ lastAnnouncementTime = now; ++ } ++ + dispatch_async (dispatch_get_main_queue (), ^{ + NSAccessibilityPostNotificationWithUserInfo (element, name, info); + }); +@@ -7571,16 +7592,22 @@ ns_ax_post_notification_with_info (id element, + + - (id)accessibilityParent + { ++ if (!self.emacsView) ++ return nil; + return NSAccessibilityUnignoredAncestor (self.emacsView); + } + + - (id)accessibilityWindow + { ++ if (!self.emacsView) ++ return nil; + return [self.emacsView window]; + } + + - (id)accessibilityTopLevelUIElement + { ++ if (!self.emacsView) ++ return nil; + return [self.emacsView window]; + } + +@@ -8143,7 +8170,7 @@ ns_ax_completion_text_for_span (EmacsAccessibilityBuffer *elem, + return result; + } + [self ensureTextCache]; +- return cachedText ? cachedText : @""; ++ return cachedText ? [[cachedText retain] autorelease] : @""; + } + + - (NSInteger)accessibilityNumberOfCharacters +@@ -9659,6 +9686,15 @@ ns_ax_scan_interactive_spans (struct window *w, + [layer release]; + #endif + ++ /* Nil out back-references before releasing elements. Queued ++ dispatch_async blocks may still hold a retained reference to ++ an element; without this they would access a dangling ++ emacsView pointer after EmacsView is freed. */ ++ for (id elem in accessibilityElements) ++ { ++ if ([elem respondsToSelector:@selector (setEmacsView:)]) ++ [elem setEmacsView:nil]; ++ } + [accessibilityElements release]; + [[self menu] release]; + [super dealloc]; +-- +2.43.0 +