Subject: Re: bug#79201: 30.1.90; set-process-thread can permanently break fd_callback_info slots

From: Spencer Baugh <[email-masked]>
Date: Tue, 12 Aug 2025 18:16:12 -0400

Anyway, I found a reproducer for the other bug I mentioned, where
waiting_thread could also get contaminated and break fd_callback_info
slots.  It seems to be quite a bad bug: it will happen spontaneously
just from running multiple threads interacting with processes.

If you run the following simple Lisp code after applying the following
patch (which is just to make the breakage more obvious; without the
patch, Emacs would be left in a silently broken state), Emacs will abort
in a few seconds - though it probably varies based on your system.

This indicates that .waiting_thread is somehow being left at a non-NULL
value even after the thread has left wait_reading_process_output.

Any ideas?

(defun my-break-thread ()
  (let ((proc (make-process
               :name "foo"
               :command '("sleep" "1"))))
    (while (process-live-p proc)
      (accept-process-output proc 0.05))))

(while t
  (make-thread #'my-break-thread "thread1")
  (thread-join (make-thread #'my-break-thread "thread2")))

diff --git a/src/process.c b/src/process.c
index 1a5c99139a6..294c204ae96 100644
--- a/src/process.c
+++ b/src/process.c
@@ -467,6 +467,16 @@ make_lisp_proc (struct Lisp_Process *p)
 } fd_callback_info[FD_SETSIZE];

+static void
+assert_fd_callback_info_unused(int fd)
+{
+  eassert (0 <= fd && fd < FD_SETSIZE);
+  if (fd_callback_info[fd].waiting_thread != NULL) {
+    fprintf (stderr, "waiting_thread non-NULL\n");
+    terminate_due_to_signal (SIGABRT, INT_MAX);
+  }
+}
+
 /* Add a file descriptor FD to be monitored for when read is possible.
    When read is possible, call FUNC with argument DATA.  */
 
@@ -475,7 +485,6 @@ add_read_fd (int fd, fd_callback func, void *data)
 {
   add_keyboard_wait_descriptor (fd);
 
-  eassert (0 <= fd && fd < FD_SETSIZE);
   fd_callback_info[fd].func = func;
   fd_callback_info[fd].data = data;
 }
@@ -490,14 +499,13 @@ add_non_keyboard_read_fd (int fd, fd_callback func, void *data)
 static void
 add_process_read_fd (int fd)
 {
-  eassert (fd >= 0 && fd < FD_SETSIZE);
+  assert_fd_callback_info_unused(fd);
   eassert (fd_callback_info[fd].func == NULL);
 
   fd_callback_info[fd].flags &= ~KEYBOARD_FD;
   fd_callback_info[fd].flags |= FOR_READ;
   if (fd > max_desc)
     max_desc = fd;
-  eassert (0 <= fd && fd < FD_SETSIZE);
   fd_callback_info[fd].flags |= PROCESS_FD;
 }
 
@@ -522,7 +530,7 @@ delete_read_fd (int fd)
 void
 add_write_fd (int fd, fd_callback func, void *data)
 {
-  eassert (fd >= 0 && fd < FD_SETSIZE);
+  assert_fd_callback_info_unused(fd);
 
   fd_callback_info[fd].func = func;
   fd_callback_info[fd].data = data;
@@ -534,7 +542,7 @@ add_write_fd (int fd, fd_callback func, void *data)
 static void
 add_non_blocking_write_fd (int fd)
 {
-  eassert (fd >= 0 && fd < FD_SETSIZE);
+  assert_fd_callback_info_unused(fd);
   eassert (fd_callback_info[fd].func == NULL);
 
   fd_callback_info[fd].flags |= FOR_WRITE | NON_BLOCKING_CONNECT_FD;
@@ -8266,7 +8274,7 @@ remove_slash_colon (Lisp_Object name)
 add_keyboard_wait_descriptor (int desc)
 {
 #ifdef subprocesses /* Actually means "not MSDOS".  */
-  eassert (desc >= 0 && desc < FD_SETSIZE);
+  assert_fd_callback_info_unused(desc);
   fd_callback_info[desc].flags &= ~PROCESS_FD;
   fd_callback_info[desc].flags |= (FOR_READ | KEYBOARD_FD);
   if (desc > max_desc)