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