Subject: bug#79014: 31.0.50; igc: infinite loop

From: Gerd Möllmann <[email-masked]>
Date: Mon, 14 Jul 2025 15:50:58 +0200

Óscar Fuentes via "Bug reports for GNU Emacs, the Swiss army knife of
text editors" <[email-masked]> writes:

> On a long-lived emacs session (two weeks of intensive use), while
> reverting a versioned file with C-x v u (something that I did multiple
> times before on the same session) Emacs entered an infinite loop using
> 100% CPU, ignoring C-g and SIGUSR2 and had to be killed eventually after
> attaching gdb and obtaining the backtraces below.

Thanks. That's a recursive call to igc_on_idle from Lisp being called
whle it is running. Looks like something like this is needed:

diff --git a/src/igc.c b/src/igc.c
index 155b4db4e69..4922352a563 100644
--- a/src/igc.c
+++ b/src/igc.c
@@ -4080,33 +4080,45 @@ buffer_step (struct igc_buffer_it *it)
   return false;
 }
 
+static bool inhibit_on_idle = false;
+
+static void
+allow_on_idle (void)
+{
+  inhibit_on_idle = false;
+}
+
 void
 igc_on_idle (void)
 {
+  if (igc_state != IGC_STATE_USABLE)
+    return;
+
+  /* Note that truncate_undo_list and maybe others my call Lisp, which
+     means that we might be called recursively.  Ignore such recursive
+     calls.  */
+  if (inhibit_on_idle)
+    return;
+  specpdl_ref count = SPECPDL_INDEX ();
+  record_unwind_protect_void (allow_on_idle);
+  inhibit_on_idle = true;
+
   shrink_regexp_cache ();
 
   struct igc_buffer_it buffer_it = make_buffer_it ();
   IGC_WITH_CLOCK (clock, 0.1)
   {
     bool work_done = false;
-    switch (igc_state)
-      {
-      case IGC_STATE_INITIAL:
-      case IGC_STATE_USABLE_PARKED:
-      case IGC_STATE_DEAD:
-	return;
-
-      case IGC_STATE_USABLE:
-	work_done |= process_one_message (global_igc);
-	work_done |= buffer_step (&buffer_it);
-	work_done |= arena_step ();
-	break;
-      }
+    work_done |= process_one_message (global_igc);
+    work_done |= buffer_step (&buffer_it);
+    work_done |= arena_step ();
 
     /* Don't always exhaust the max time we want to spend here.  */
     if (!work_done)
       break;
   }
+
+  unbind_to (count, Qnil);
 }
 
 static mps_ap_t