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?