Subject: bug#75520: Circular code or data can hang Emacs unquittably

From: Pip Cet <[email-masked]>
Date: Sun, 12 Jan 2025 17:08:24 +0000

When evaluated in -Q, this code:

(funcall '(lambda () . #1=(t . #1#)))

causes an unquittable infloop.

IMHO, circular code isn't something we should support in any meaningful
way: it's perfectly okay for it to have unexpected results, and throw
any error message we can think of, or none, until I hit quit.  However,
I do think the loops should be quittable.

The easiest way to ensure this is to move the maybe_quit call in
eval_sub to the top of the function.  I mentioned this problem to Paul
Eggert and he provided the patch below, which also fixes two other cases
I'd overlooked.

There are similar issues with circular data: if Lisp code turns
process-environment into a circular structure, for example, call_process
will hang Emacs.  If this should be fixed, please let me know so I can
create a separate bug report and look for similar issues.

>From a2015ba2f81c67b1a77054f0715ba3d643504a98 Mon Sep 17 00:00:00 2001
From: Paul Eggert <[email-masked]>
Date: Sun, 5 Jan 2025 17:30:18 -0800
Subject: [PATCH] Some maybe_quit fixes in eval.c

* src/eval.c (skip_debugger):
Use FOR_EACH_TAIL to avoid unquittable infloops.
(eval_sub): Call maybe_quit earlier.
(funcall_lambda): Use FOR_EACH_TAIL rather than calling
maybe_quit each time through the loop.
---
 src/eval.c | 30 ++++++++++++++++--------------
 1 file changed, 16 insertions(+), 14 deletions(-)

diff --git a/src/eval.c b/src/eval.c
index 3e899db4436..2fa60af90af 100644
--- a/src/eval.c
+++ b/src/eval.c
@@ -2044,35 +2044,36 @@ wants_debugger (Lisp_Object list, Lisp_Object conditions)
 static bool
 skip_debugger (Lisp_Object conditions, Lisp_Object data)
 {
-  Lisp_Object tail;
-  bool first_string = 1;
+  Lisp_Object tail = Vdebug_ignored_errors;
+  bool first_string = true;
   Lisp_Object error_message;
 
   error_message = Qnil;
-  for (tail = Vdebug_ignored_errors; CONSP (tail); tail = XCDR (tail))
+
+  FOR_EACH_TAIL (tail)
     {
       if (STRINGP (XCAR (tail)))
 	{
 	  if (first_string)
 	    {
 	      error_message = Ferror_message_string (data);
-	      first_string = 0;
+	      first_string = false;
 	    }
 
 	  if (fast_string_match (XCAR (tail), error_message) >= 0)
-	    return 1;
+	    return true;
 	}
       else
 	{
-	  Lisp_Object contail;
+	  Lisp_Object contail = conditions;
 
-	  for (contail = conditions; CONSP (contail); contail = XCDR (contail))
+	  FOR_EACH_TAIL (contail)
 	    if (EQ (XCAR (tail), XCAR (contail)))
-	      return 1;
+	      return true;
 	}
     }
 
-  return 0;
+  return false;
 }
 
 /* Say whether SIGNAL is a `quit' error (or inherits from it).  */
@@ -2470,6 +2471,11 @@ grow_specpdl_allocation (void)
 Lisp_Object
 eval_sub (Lisp_Object form)
 {
+  /* Call maybe_quit now, rather than just before calling maybe_gc, so
+     that callers like Fand, Fprogn and Fsetq need not call maybe_quit
+     each time through their loops.  */
+  maybe_quit ();
+
   if (SYMBOLP (form))
     {
       /* Look up its binding in the lexical environment.
@@ -2483,8 +2489,6 @@ eval_sub (Lisp_Object form)
   if (!CONSP (form))
     return form;
 
-  maybe_quit ();
-
   maybe_gc ();
 
   if (++lisp_eval_depth > max_lisp_eval_depth)
@@ -3262,10 +3266,8 @@ funcall_lambda (Lisp_Object fun, ptrdiff_t nargs, Lisp_Object *arg_vector)
   bool optional = false;
   bool rest = false;
   bool previous_rest = false;
-  for (; CONSP (syms_left); syms_left = XCDR (syms_left))
+  FOR_EACH_TAIL (syms_left)
     {
-      maybe_quit ();
-
       Lisp_Object next = maybe_remove_pos_from_symbol (XCAR (syms_left));
       if (!BARE_SYMBOL_P (next))
 	xsignal1 (Qinvalid_function, fun);
-- 
2.45.2