[newlib-cygwin] Cygwin: pty: bound the cursor-sync round-trip so an exiting process cannot hang

Takashi Yano via Cygwin-cvs <[email protected]> Tue, 30 Jun 2026 13:09:41 +0000 (GMT)
Newsgroups gmane.os.cygwin.cvs
Message-ID <[email protected]>
https://sourceware.org/git/gitweb.cgi?p=3Dnewlib-cygwin.git;h=3D24430fb239c=
1783bfd6b0135e8a529d11f487db3

commit 24430fb239c1783bfd6b0135e8a529d11f487db3
Author: Johannes Schindelin <[email protected]>
Date:   Thu Jun 25 13:41:42 2026 +0200

    Cygwin: pty: bound the cursor-sync round-trip so an exiting process can=
not hang
   =20
    The cursor-position fixup added in "Cygwin: pty: Fixup pty state after
    a cygwin app exits" runs from cleanup() on every foreground Cygwin-app
    exit while a pseudo console is active, and it waits on two unbounded
    loops for the master to answer the "ESC[6n" it just sent: one that
    spins until the pcon_start_pid slot is free, and one that spins until
    the master clears the slot again. pcon_start_pid is only ever cleared
    once master::write() parses the terminal's reply, so if that reply
    never comes, because the terminal is going away, the forwarding pipe
    is broken, or a previous requester died mid-handshake, the exiting
    process spins on yield() forever and never exits.
   =20
    Bound both waits with a three second deadline using GetTickCount64(),
    and on timeout clear our own pcon_start_pid slot, but only if it is
    still ours, so a give-up does not stomp a later requester. Also restore
    the pcon_start and pcon_start_pid reset that the same commit removed
    from close_pseudoconsole(); it is the backstop that keeps a requester
    which died without clearing its slot from wedging the next one. The
    worst case is now a slightly stale cursor after a timeout rather than a
    process that refuses to exit.
   =20
    Fixes: b34394d456b6 ("Cygwin: pty: Fixup pty state after a cygwin app e=
xits")
    Assisted-by: Opus 4.8
    Signed-off-by: Johannes Schindelin <[email protected]>
    Co-authored-by: Takashi Yano <[email protected]>
    Reviewed-by: Takashi Yano <[email protected]>

Diff:
---
 winsup/cygwin/fhandler/pty.cc | 30 +++++++++++++++++++++++++++++-
 1 file changed, 29 insertions(+), 1 deletion(-)

diff --git a/winsup/cygwin/fhandler/pty.cc b/winsup/cygwin/fhandler/pty.cc
index c1de25baa..ca85ae679 100644
--- a/winsup/cygwin/fhandler/pty.cc
+++ b/winsup/cygwin/fhandler/pty.cc
@@ -391,6 +391,7 @@ atexit_func (void)
 void
 fhandler_pty_slave::req_fixup_pcon_state (void)
 {
+  ULONGLONG deadline =3D GetTickCount64 () + 3000;
   while (true)
     {
       WaitForSingleObject (input_mutex, mutex_timeout);
@@ -398,6 +399,10 @@ fhandler_pty_slave::req_fixup_pcon_state (void)
 	break;
       /* Another request is on going. */
       ReleaseMutex (input_mutex);
+      if (GetTickCount64 () > deadline)
+	/* A previous requester is stuck; give up this sync rather than
+	   spin forever. */
+	return;
       yield ();
     }
=20
@@ -410,9 +415,25 @@ fhandler_pty_slave::req_fixup_pcon_state (void)
   get_ttyp ()->pcon_start_pid =3D myself->pid;
   WriteFile (get_output_handle (), "\033[6n", 4, &n, NULL);
   ReleaseMutex (input_mutex);
-  while (get_ttyp ()->pcon_start_pid)
+  deadline =3D GetTickCount64 () + 3000;
+  while (get_ttyp ()->pcon_start_pid && GetTickCount64 () <=3D deadline)
     /* wait for completion of fixing-up in master::write(). */
     yield ();
+  /* If the master never answered (e.g. the terminal is going away),
+     clear our own request so a stale pcon_start_pid cannot wedge the
+     next requester. */
+  if (get_ttyp ()->pcon_start_pid =3D=3D (pid_t) myself->pid)
+    {
+      WaitForSingleObject (input_mutex, mutex_timeout);
+      if (get_ttyp ()->pcon_start_pid =3D=3D (pid_t) myself->pid)
+	{
+	  get_ttyp ()->req_fixup_pcon_cur_pos =3D false;
+	  get_ttyp ()->req_xfer_input =3D false;
+	  get_ttyp ()->pcon_start =3D false;
+	  get_ttyp ()->pcon_start_pid =3D 0;
+	}
+      ReleaseMutex (input_mutex);
+    }
 }
=20
 void
@@ -4217,6 +4238,13 @@ fhandler_pty_slave::close_pseudoconsole (tty *ttyp, =
DWORD force_switch_to)
 	  ttyp->pcon_activated =3D false;
 	  ttyp->switch_to_nat_pipe =3D false;
 	  ttyp->nat_pipe_owner_pid =3D 0;
+	  /* Safety net: if a req_fixup_pcon_state() requester died without
+	     clearing its slot, do not leave pcon_start_pid set forever. */
+	  if (ttyp->pcon_start_pid =3D=3D myself->pid)
+	    {
+	      ttyp->pcon_start =3D false;
+	      ttyp->pcon_start_pid =3D 0;
+	    }
 	}
       if (ttyp->pcon_handle_ready_event)
 	{