bug#81619: 30.1; draggingUpdated callback in nsterm.m invokes redisplay with waiting_for_input set, aborting Emacs

Eli Zaretskii <[email protected]>
Newsgroups gmane.emacs.bugs
Message-ID <[email protected]>
> From: Richard Garner <[email protected]>
> Date: Fri, 14 Aug 2026 15:58:18 +1000
> 
> 
> The bug
> -------
> 
> Dragging a file over an Emacs frame on macOS sometimes causes an instant
> Emacs abort, with no error message: Emacs simply disappears.
> 
> This appears to be because the NS drag callback runs redisplay in a
> fragile state where no Lisp signals are permitted.
> 
> The analysis
> ------------
> 
> My macOS crash logs gave me this traceback:
> 
> #8   emacs_abort
> #9   signal_or_quit
> #10  Fsignal
> #13  args_out_of_range
> #14  Faref
> #15  truncate-string-to-width          (mule-util, natively compiled)
> #17  mu4e--modeline-quote-and-truncate
> #19  mu4e--modeline-string
> #23  internal_condition_case_n
> #24  dsafe__call
> #25  display_mode_element              (x5)
> #30  Fformat_mode_line
> #40  internal_condition_case_n
> #41  dsafe__call
> #42  display_mode_element
> #43  display_mode_line
> #44  display_mode_lines
> #45  redisplay_window
> #48  redisplay_internal
> #49  -[EmacsView draggingUpdated:]
> #50  -[NSDragDestination _draggingUpdate]
> #52  NSCoreDragTrackingProc
> 
> Here you can see -[EmacsView draggingUpdated:] calling redisplay(); this
> is nsterm.m:9494. Inside redisplay(), the modelines are updated,
> including the elisp inside any :eval forms. In my config, one of these
> calls truncate-string-to-width from mule-util.el. This iterates over a
> string with aref inside a condition-case, and catches args-out-of-range
> as the loop terminator.
> 
> This is perfectly blameless (and stock) elisp, but here redisplay is
> being called while Emacs is idling in wait_reading_process_output with
> waiting_for_input set. In this state, any signal raised causes an
> instant abort (eval.c:1961). Even a handled signal is not safe: the
> condition-case in truncate-string-to-width is bypassed, as are
> redisplay's own guards at frames #23 and #40. signal_or_quit aborts
> without consulting any handlers, so Emacs vanishes without trace.
> 
> Running Emacs through lldb, I think I can trace the mechanism:
> 
> frame #18: Emacs`ns_select_1 + 784
> frame #19: Emacs`wait_reading_process_output + 3312
> frame #20: Emacs`sit_for + 384
> frame #21: Emacs`read_char + 4884
> 
> - read_char calls sit_for with reading = 1, from either the
>   echo-keystrokes wait or the auto-save idle wait;
> - because reading = 1, sit_for passes read_kbd = -1;
> - because read_kbd is negative, wait_reading_process_output calls
>   set_waiting_for_input.
> 
> Nothing in nsterm.m or nsmenu.m clears it around the NS event loop, so
> it is still set when AppKit calls into the view.
> 
> The fix
> -------
> 
> This issue has already been worked around elsewhere. dnd.el:155-157
> tries to do so from the elisp side with the comment "We avoid errors
> here, since on some systems this runs when waiting_for_input is
> non-zero, and that aborts on error."
> 
> However, nsterm.m:9380-9389, a hundred lines earlier, has a fix that
> works on the Objective C side; it says:
> 
> "If there is IO going on when redisplay is run here Emacs crashes. I
> think it's because this code will always be run within the run loop and
> for whatever reason processing input is dangerous. This technique was
> stolen wholesale from nsmenu.m and seems to work."
> 
> The place it was stolen from is nsmenu.m:201-254; propagating the
> wholesale theft further yields the following minimal patch against
> master:
> 
> --8<---------------cut here---------------start------------->8---
> --- a/src/nsterm.m
> +++ b/src/nsterm.m
> @@ -9491,7 +9491,19 @@
>    safe_calln (Vns_drag_motion_function, frame,
>  	      make_fixnum (x), make_fixnum (y));
>  
> -  redisplay ();
> +  /* Since redisplay can call arbitrary Lisp that may signal, we
> +     guard against calling it while waiting_for_input is set, in
> +     which state any signal causes an immediate abort.  */
> +  {
> +    bool owfi = waiting_for_input;
> +    waiting_for_input = 0;
> +    block_input ();
> +
> +    redisplay ();
> +
> +    unblock_input ();
> +    waiting_for_input = owfi;
> +  }
>  #endif
>  
>    unbind_to (count, Qnil);
> --8<---------------cut here---------------end--------------->8---
> 
> However, a manoeuvre that's good enough to steal twice maybe deserves
> its own helper.

Thanks.

Alan, any comments?
lmpx.com only provides a reader for public news (NNTP) servers. It is not affiliated with the servers or forums shown here and is not responsible for the content of articles, which is written by their respective authors.