Re: [PATCH v4] Cygwin: console: Fix undesired mode change at exit of non-cygwin apps
Takashi Yano <[email protected]> Sat, 18 Jul 2026 22:14:49 +0900
| Newsgroups | gmane.os.cygwin.patches |
|---|---|
| Message-ID | <[email protected]> |
Hi Johannes,
On Fri, 17 Jul 2026 22:50:11 +0200 (CEST)
Johannes Schindelin wrote:
> Hi Takashi,
>
> On Thu, 16 Jul 2026, Takashi Yano wrote:
>
> > Previously, if two non-cygwin apps are started and one of them
> > exits first, the other one loosed appropriate console mode, since
> > the first one restored it to tty::cygwin. This patch counts the
> > active console process whose pgid is pgid of the tty and if the
> > result is zero (means the last non-cygwin foreground process),
> > restore console mode.
>
> Thank you for v4 and for the follow-up correction. I verified that the
> published v4 applies cleanly to 0d516c2b1f4d and compiles and links a
> `new-cygwin1.dll`. I have not been able to establish the runtime behavior
> yet, so the following is from reading the code.
>
> About the follow-up, to remove that `CloseHandle ()` call: agreed. That
> correction addresses the duplicate CloseHandle() only, though; the two
> blocking issues below are independent of it.
>
> > To avoid race issue between apps modifying
> > console mode simultaneously, this patch also introduce a mutex
> > named `cons_mode_mutex`.
>
> Here is the first blocking problem, and it is a direct consequence of
> "guard all mode changes". The new mutex is not acquired in a consistent
> order with respect to `output_mutex`.
Indeed. Thanks for finding this.
> > Fixes: 48285aa36c2c ("Cygwin: console: Fix handling of Ctrl-S in Win7.")
> > Signed-off-by: Takashi Yano <[email protected]>
> > Reviewed-by: Johannes Schindelin <[email protected]>
> > ---
> > v2: Stop counting up/down the counter by itself.
> > Use num_active_non_cygwin_apps() instead.
> > v3: Guard setup_for_non_cygwin_app() by cons_mode_mutex as well.
> > v4: Guard all mode changes in console by cons_mode_mutex.
> >
> > winsup/cygwin/fhandler/console.cc | 87 ++++++++++++++++++++++++-
> > winsup/cygwin/local_includes/fhandler.h | 2 +
> > 2 files changed, 86 insertions(+), 3 deletions(-)
> >
> > diff --git a/winsup/cygwin/fhandler/console.cc b/winsup/cygwin/fhandler/console.cc
> > index d4c87f29f..5b9a87ebd 100644
> > --- a/winsup/cygwin/fhandler/console.cc
> > +++ b/winsup/cygwin/fhandler/console.cc
> > @@ -977,15 +977,59 @@ fhandler_console::setup_for_non_cygwin_app ()
> > console mode. */
> > if (get_ttyp ()->getpgid () == myself->pgid)
> > {
> > + WaitForSingleObject (cons_mode_mutex, INFINITE);
> > set_disable_master_thread (true, this);
> > set_input_mode (tty::native, &tc ()->ti, get_handle_set ());
> > set_output_mode (tty::native, &tc ()->ti, get_handle_set ());
> > + ReleaseMutex (cons_mode_mutex);
>
> In this path, `cons_mode_mutex` is taken first, and then
> `set_output_mode()` acquires `output_mutex` underneath it. The same
> nesting holds for `cleanup_for_non_cygwin_app()`, `bg_check()`, `open()`,
> `post_open_setup()`, `tcsetattr()`, and `set_console_mode_to_native()`:
> `cons_mode_mutex` first, `output_mutex` second.
>
> > }
> > }
> >
> > +static int
> > +num_active_non_cygwin_apps (pid_t pgid)
> > +{
> > + tmp_pathbuf tp;
> > + DWORD *list = (DWORD *) tp.c_get ();
> > + const DWORD buf_size = NT_MAX_PATH / sizeof (DWORD);
> > +
> > + DWORD buf_size1 = 1;
> > + DWORD num;
> > + /* The buffer of too large size does not seem to be expected by new condrv.
> > + https://github.com/microsoft/terminal/issues/18264#issuecomment-2515448548
> > + Use the minimum buffer size in the loop. */
> > + while ((num = GetConsoleProcessList (list, buf_size1)) > buf_size1)
> > + {
> > + if (num > buf_size)
> > + return 0;
> > + buf_size1 = num;
> > + }
> > + if (num == 0)
> > + return 0;
> > +
> > + int cnt = 0;
> > + for (DWORD i = 0; i < num; i++)
> > + {
> > + pinfo p (cygwin_pid (list[i]));
> > + if (!!p && p->pgid == pgid && ISSTATE (p, PID_NOTCYGWIN))
> > + cnt++;
> > + }
> > + return cnt;
> > +}
> > +
> > void
> > fhandler_console::cleanup_for_non_cygwin_app (handle_set_t *p)
> > {
> > + if (cygheap->ctty->tc()->pgid != myself->pgid)
> > + return;
> > +
> > + WaitForSingleObject (p->cons_mode_mutex, INFINITE);
> > + if (num_active_non_cygwin_apps (cygheap->ctty->tc()->pgid))
> > + {
> > + ReleaseMutex (p->cons_mode_mutex);
>
> The second blocking problem is a race window around process creation.
> `setup_for_non_cygwin_app()` runs before `CreateProcessW()` and releases
> `cons_mode_mutex` before the spawned process becomes visible: it is not
> yet in `GetConsoleProcessList()`, and there is no `pinfo` with
> `PID_NOTCYGWIN` and the matching `pgid` for it. During that interval a
> concurrent `cleanup_for_non_cygwin_app()` can count zero non-Cygwin apps
> and restore the Cygwin console mode, which is exactly the regression this
> patch sets out to fix. Keep in mind that the count is only reliable once
> the spawned process is both created and published. The pending native-mode
> state needs to remain represented across process creation and publication,
> with a rollback if `CreateProcessW()` fails.
I re-introduce a counter for this short period. This minimize the
risk that the stub process killed by taskkill, etc. while the
counter is active. However, as I mentioned in the previous mail,
we cannnot fully address this problem. What do you think?
> Two non-blocking points on this `num_active_non_cygwin_apps()` helper.
>
> First, latency. On each call this normally does two
> `GetConsoleProcessList()` calls (more if the process count changes between
> them) plus O(N) `cygwin_pid()` and `pinfo` construction, all under
> `cons_mode_mutex`, and v4 broadens who has to wait for that mutex. I am
> not claiming an observed slowdown; I have not measured it. Did you? If
> not, a cheap fast path seems worthwhile: this loop counts every match but
> only the question "is there at least one?" matters, so returning on the
> first match, and skipping `pinfo` construction entirely when
> `cygwin_pid()` returns 0, would bound the common case.
Done. Thanks.
> Second, error semantics. This returns 0 for three distinct situations: a
> genuine "no matching process", an API failure (`num == 0`), and output
> exceeding `buf_size`. But 0 is precisely the value that authorizes the
> caller to restore the Cygwin mode. Conflating "there are no non-Cygwin
> apps" with "I could not find out" means an enumeration failure silently
> triggers a mode restore. Please give the helper a distinct
> "unavailable/error" result so the caller can decline to restore when the
> count is not trustworthy.
In the error case, v5 patch print error message by system_printf(), and
restore the console mode to tty::cygwin. (Because, in cygwin environment,
the emphasis is placed on ensuring that cygwin apps behave correctly,
rather than on non‑Cygwin apps.)
> > @@ -2135,11 +2191,13 @@ fhandler_console::close (int flag)
> > if (shared_console_info[unit] && (dev_t) myself->ctty == get_device ()
> > && cons_mode_on_close (&handle_set) == tty::restore)
> > {
> > + WaitForSingleObject (cons_mode_mutex, INFINITE);
> > set_disable_master_thread (true, this);
> > if (con.curr_output_mode != tty::restore)
> > set_output_mode (tty::restore, &get_ttyp ()->ti, &handle_set);
>
> But `close()` here, and `char_command()` below, are already holding
> `output_mutex` at the point they reach this code, and only then acquire
> `cons_mode_mutex`. That is the opposite order.
I moved acquire_output_mutex() in close() to the place after the console
mode change in v5 patch. As for char_command() case, if the cons_mode_mutex
cannot be acquired immediately, give up the setting console mode there.
Even if we manage to get the mutex acquisition order right, which can be
overwritten by concurrent process/thread. That's a matter of luck.
> > if (con.curr_input_mode != tty::restore)
> > set_input_mode (tty::restore, &get_ttyp ()->ti, &handle_set);
> > + ReleaseMutex (cons_mode_mutex);
> > }
> >
> > if (shared_console_info[unit] && con.owner == GetCurrentProcessId ())
> > @@ -2196,6 +2254,8 @@ fhandler_console::close (int flag)
> > input_mutex = NULL;
> > CloseHandle (output_mutex);
> > output_mutex = NULL;
> > + CloseHandle (cons_mode_mutex);
> > + cons_mode_mutex = NULL;
> >
> > pcon_hand_over_proc ();
> >
> > @@ -2369,10 +2429,12 @@ int
> > fhandler_console::tcsetattr (int a, struct termios const *t)
> > {
> > get_ttyp ()->ti = *t;
> > + WaitForSingleObject (cons_mode_mutex, INFINITE);
> > if (con.curr_input_mode == tty::cygwin)
> > set_input_mode (tty::cygwin, t, &handle_set);
> > if (con.curr_output_mode == tty::cygwin)
>
> Two processes can therefore wait on each other indefinitely: one holding
> `output_mutex` and waiting for `cons_mode_mutex`, the other holding
> `cons_mode_mutex` and waiting for `output_mutex`. We need one consistent
> lock order across all of these sites. I do not want to prescribe a
> specific redesign as unquestionably correct; whether that means taking
> `output_mutex` before `cons_mode_mutex` everywhere, or narrowing
> `cons_mode_mutex` so it never nests over `output_mutex`, is your call. But
> the current mixed order is a showstopper.
I think I have fixed the problem you pointed out. Could you please
review v5 patch?
--
Takashi Yano <[email protected]>