Re: [PATCH v3] Cygwin: console: Correct previous NOFLSH fix

Johannes Schindelin <[email protected]> Wed, 8 Jul 2026 16:58:57 +0200 (CEST)
Newsgroups gmane.os.cygwin.patches
Message-ID <[email protected]>
Hi Takashi,

Thank you for v3. Both concerns from v2 are addressed, and the "stty intr
^x; cat | non-cygwin-app" case now behaves as expected.

I have two non-blocking observations further below, and a question: Out of
curiosity, not a blocker: how does the `with_debugger_nat` branch actually
get reached in practice? gdb normally reads the console only at its own
prompt, i.e. when the inferior is stopped and thus not foreground, so the
pre-conditions do not obviously line up.

  Reviewed-by: Johannes Schindelin <[email protected]>

On Wed, 8 Jul 2026, Takashi Yano wrote:

> The previous fix for NOFLSH mode does not work as intended.
>=20
> discard_key_events(), added in "Cygwin: console: Fix NOFLSH behaviour a
> bit", loops on ReadConsoleInputW() until it has consumed the requested
> number of records, but ReadConsoleInputW() blocks while the console
> input buffer is empty. sigflush() calls it with a hard-coded count of
> one and no guarantee that a record is actually queued: in the
> master-thread path the signalling record has already been read out of
> the buffer before sigflush() runs, so the call blocks until, and then
> swallows, the user's next keystroke.
>=20
> To avoid this, this patch does not discard input when process_sigs()
> is called from cons_master_thread, where the value of `fh` is NULL,
> because discarding will be done in cons_master_thread.
>=20
> And because the ReadConsoleInputW() return value is unchecked, a failed
> read leaves the count indeterminate, so "n -=3D n1" can underflow and sp=
in.
> Check return value of ReadConsoleInputW() and abort if it fails.
>=20
> Moreover, discard_key_event(1) does not work as intended if the first
> key event is not a bKeyDown event correspoding to the signalling key.
> Use discard_key_events(0) instead. This means discarding input events
> to the current position processed. Since the key-strokes prior to the
> signalling key are already in the readahead buffer, so this call discard=
s
> only the signalling key. The important point here is to discard input
> before releasing input_mutex by release_input_mutex_if_necessary(),
> because, if not, cons_master_thread starts to process key events before
> discarding signalling key event because the thread can acquire
> input_mutex. This causes the signalling key is processed twice.
>=20
> One separate point: the `process_input_message()` caller wraps
> `discard_key_events()` in `acquire_attach_mutex()` + `attach_console
> (con.owner)`, but the `sigflush()` call site does not, so the
> `ReadConsoleInputW()` there runs against whatever console the calling
> process happens to be attached to. With the guard above the worst case
> is a no-op when the calling process happens not to be attached, so
> it would be more correct to move the attach into the helper itself.
>=20
> This patch also fixes two more special cases. One is done_with_debugger
> case. When `gdb cat` is executed and the `cat` is running, Ctrl-C
> discards all the key events including the events after Ctrl-C. This
> is because tcflush() is used for the purpose. Use discard_key_events(0)
> instead. The other case is not_signalled_but_done case. Previously,
> when `cat | non-cygwin-app` is executed and Ctrl-C is pressed, but
> the `Ctrl-C` is not VINTR, line_edit() wrongly returned
> line_edit_signalled even though `cat` is not signalled by Ctrl-C.
> In this case, `cat` should receive Ctrl-C as a input char, while
> `non-cygwin-app` has been killed by Ctrl-C. Fix this in line_edit().
>=20
> Fixes: 66324edf64a9 ("Cygwin: console: Fix NOFLSH behaviour a bit")
> Co-authored-by: Johannes Schindelin <[email protected]>
> Signed-off-by: Takashi Yano <[email protected]>
> Reviewed-by: Johannes Schindelin <[email protected]>
> ---
> v2: Use discard_key_events(0) instead of tcflush(TCIFLUSH), which
>     discards events to the current position processed.
> v3: Fix behaviour of two special cases: done_with_debugger and
>     not_signalled_but_done
>=20
>  winsup/cygwin/fhandler/console.cc       | 25 +++++++++++++---------
>  winsup/cygwin/fhandler/termios.cc       | 28 ++++++++++++++-----------
>  winsup/cygwin/local_includes/fhandler.h |  1 +
>  3 files changed, 32 insertions(+), 22 deletions(-)
>=20
> diff --git a/winsup/cygwin/fhandler/console.cc b/winsup/cygwin/fhandler/=
console.cc
> index 730bb0b45..cc4591c14 100644
> --- a/winsup/cygwin/fhandler/console.cc
> +++ b/winsup/cygwin/fhandler/console.cc
> @@ -1718,6 +1718,7 @@ fhandler_console::process_input_message (size_t le=
n)
>  	  continue;
>  	}
> =20
> +      num_input_events_processed =3D i + 1;

The new counter member is missing from the constructor's init list. It is
safe in practice because `cnew()` zero-fills, but adding it explicitly
would match the surrounding style.

>        num_chars +=3D nread;
>        if (toadd)
>  	{
> @@ -1748,17 +1749,11 @@ out:
>    /* Discard processed recored. */
>    DWORD discard_len =3D min (total_read, i + 1);
>    /* If input is signalled, do not discard input here because
> -     tcflush() is already called from line_edit(). */
> -  if (stat =3D=3D input_signalled && !(ti->c_lflag & NOFLSH))
> +     discard_key_events() is already called from line_edit(). */
> +  if (stat =3D=3D input_signalled)
>      discard_len =3D 0;
>    if (discard_len && (len || stat !=3D input_ok))
> -    {
> -      acquire_attach_mutex (mutex_timeout);
> -      DWORD resume_pid =3D attach_console (con.owner);
> -      discard_key_events (discard_len);
> -      detach_console (resume_pid, con.owner);
> -      release_attach_mutex ();
> -    }
> +    discard_key_events (discard_len);
>    return stat;
>  }
> =20
> @@ -1766,15 +1761,25 @@ void
>  fhandler_console::discard_key_events (size_t n)
>  {
>    DWORD discarded =3D 0;
> +  if (n =3D=3D 0)
> +    {
> +      n =3D num_input_events_processed;
> +      num_input_events_processed =3D 0;
> +    }
>    INPUT_RECORD input_rec[INREC_SIZE];
>    DWORD n1 =3D min (INREC_SIZE, n);
> +  acquire_attach_mutex (mutex_timeout);
> +  DWORD resume_pid =3D attach_console (con.owner);
>    while (n)
>      {
> -      ReadConsoleInputW (get_handle (), input_rec, n1, &n1);
> +      if (!ReadConsoleInputW (get_handle (), input_rec, n1, &n1) || !n1=
)
> +	break;
>        n -=3D n1;
>        discarded +=3D n1;
>        n1 =3D min (INREC_SIZE, n);
>      }
> +  detach_console (resume_pid, con.owner);
> +  release_attach_mutex ();
>    con.num_processed -=3D min (con.num_processed, discarded);
>  }
> =20
> diff --git a/winsup/cygwin/fhandler/termios.cc b/winsup/cygwin/fhandler/=
termios.cc
> index 605258731..9971bb1d9 100644
> --- a/winsup/cygwin/fhandler/termios.cc
> +++ b/winsup/cygwin/fhandler/termios.cc
> @@ -353,7 +353,10 @@ fhandler_termios::process_sigs (char c, tty* ttyp, =
fhandler_termios *fh)
>  	      fhandler_pty_common::attach_console_temporarily (p->dwProcessId)=
;
>  	  if (fh && p =3D=3D myself && being_debugged ())
>  	    { /* Avoid deadlock in gdb on console. */
> -	      fh->tcflush(TCIFLUSH);
> +	      if (fh->is_console ())
> +		fh->discard_key_events (0 /* to current position */);
> +	      else
> +		fh->tcflush(TCIFLUSH);
>  	      fh->release_input_mutex_if_necessary ();
>  	    }
>  	  /* CTRL_C_EVENT does not work for the process started with
> @@ -444,10 +447,14 @@ fhandler_termios::process_sigs (char c, tty* ttyp,=
 fhandler_termios *fh)
>  	goto not_a_sig;
> =20
>        termios_printf ("got interrupt %d, sending signal %d", c, sig);
> -      if (!(ti.c_lflag & NOFLSH) && fh)
> +      if (fh)
>  	{
> -	  fh->eat_readahead (-1);
> -	  fh->discard_input ();
> +	  if (!(ti.c_lflag & NOFLSH))
> +	    {
> +	      fh->eat_readahead (-1);
> +	      fh->discard_input ();
> +	    }
> +	  fh->discard_key_events (0 /* to current position */);
>  	}
>        if (fh)
>  	fh->release_input_mutex_if_necessary ();
> @@ -525,11 +532,12 @@ fhandler_termios::line_edit (const char *rptr, siz=
e_t nread, termios& ti,
>        switch (process_sigs (c, get_ttyp (), this))
>  	{
>  	case signalled:
> -	case not_signalled_but_done:
>  	case done_with_debugger:
>  	  sawsig =3D true;
>  	  get_ttyp ()->output_stopped &=3D ~BY_VSTOP;
>  	  continue;
> +	case not_signalled_but_done:
> +	  break;

Dropping `not_signalled_but_done` from the shared switch case also drops
the clearing of `BY_VSTOP` on this path. Probably fine, since no cygwin
signal is delivered here, but a sentence in the commit message would save
future git-blame archaeology.

Ciao,
Johannes

>  	case not_signalled_with_nat_reader:
>  	  disable_eof_key =3D true;
>  	  break;
> @@ -666,13 +674,9 @@ fhandler_termios::sigflush ()
>       be NULL while this is alive.  However, we can conceivably close a
>       ctty while exiting and that will zero this. */
>    if ((!have_execed || have_execed_cygwin) && tc ()
> -      && (tc ()->getpgid () =3D=3D myself->pgid))
> -    {
> -      if (!(tc ()->ti.c_lflag & NOFLSH))
> -	tcflush (TCIFLUSH);
> -      else
> -	discard_key_events (1);
> -    }
> +      && (tc ()->getpgid () =3D=3D myself->pgid)
> +      && !(tc ()->ti.c_lflag & NOFLSH))
> +    tcflush (TCIFLUSH);
>  }
> =20
>  pid_t
> diff --git a/winsup/cygwin/local_includes/fhandler.h b/winsup/cygwin/loc=
al_includes/fhandler.h
> index 8e9cbef4b..d11b3ec4f 100644
> --- a/winsup/cygwin/local_includes/fhandler.h
> +++ b/winsup/cygwin/local_includes/fhandler.h
> @@ -2201,6 +2201,7 @@ private:
>    HANDLE input_mutex, output_mutex;
>    handle_set_t handle_set;
>    _minor_t unit;
> +  size_t num_input_events_processed;
> =20
>    /* Used when we encounter a truncated multi-byte sequence.  The
>       lead bytes are stored here and revisited in the next write call. *=
/
> --=20
> 2.51.0
>=20
>=20