Windows Version
Microsoft Windows [Version 10.0.22000] (Windows 11 Pro x64)
WSL Version
2.7.11.0 (Microsoft Store package version)
Are you using WSL 1 or WSL 2?
Kernel Version
Not relevant to the observed race; it occurs in the Windows-side wsl.exe console-state handling. The exact kernel version was not collected while another long-running session was active.
Distro Version
The default WSL 2 distribution was used only to run POSIX sh and sleep; no distro-specific behavior is involved.
Other Software
Repro Steps
Open a fresh, disposable PowerShell window. The following script snapshots the original input mode, starts two overlapping wsl.exe clients in the same inherited console, prints the mode timeline, and restores the original mode in finally so that the test window is not left broken.
Add-Type @'
using System;
using System.ComponentModel;
using System.Runtime.InteropServices;
public static class ConsoleInputMode
{
private const int STD_INPUT_HANDLE = -10;
[DllImport("kernel32.dll", SetLastError = true)]
private static extern IntPtr GetStdHandle(int nStdHandle);
[DllImport("kernel32.dll", SetLastError = true)]
private static extern bool GetConsoleMode(IntPtr handle, out uint mode);
[DllImport("kernel32.dll", SetLastError = true)]
private static extern bool SetConsoleMode(IntPtr handle, uint mode);
public static uint Read()
{
IntPtr handle = GetStdHandle(STD_INPUT_HANDLE);
uint mode;
if (!GetConsoleMode(handle, out mode))
throw new Win32Exception(Marshal.GetLastWin32Error());
return mode;
}
public static void Write(uint mode)
{
IntPtr handle = GetStdHandle(STD_INPUT_HANDLE);
if (!SetConsoleMode(handle, mode))
throw new Win32Exception(Marshal.GetLastWin32Error());
}
}
'@
function Start-SleepingWsl([int] $Seconds) {
$startInfo = [Diagnostics.ProcessStartInfo]::new()
$startInfo.FileName = "$env:SystemRoot\System32\wsl.exe"
$startInfo.Arguments = "-- sh -c `"sleep $Seconds`""
$startInfo.UseShellExecute = $false
[Diagnostics.Process]::Start($startInfo)
}
$initial = [ConsoleInputMode]::Read()
$a = $null
$b = $null
try {
'Initial : 0x{0:X}' -f $initial
$a = Start-SleepingWsl 3
$deadline = (Get-Date).AddSeconds(5)
do {
Start-Sleep -Milliseconds 10
$duringA = [ConsoleInputMode]::Read()
} while ($duringA -eq $initial -and (Get-Date) -lt $deadline)
'A active : 0x{0:X}' -f $duringA
if ($duringA -eq $initial) {
throw 'WSL did not change the console input mode before the timeout.'
}
$b = Start-SleepingWsl 7
Start-Sleep -Milliseconds 500
$a.WaitForExit()
'After A exits : 0x{0:X} (A={1}, B still running={2})' -f `
[ConsoleInputMode]::Read(), $a.ExitCode, (-not $b.HasExited)
$b.WaitForExit()
$final = [ConsoleInputMode]::Read()
'After B exits : 0x{0:X} (B={1})' -f $final, $b.ExitCode
}
finally {
if ($a -and -not $a.HasExited) { $a.WaitForExit() }
if ($b -and -not $b.HasExited) { $b.WaitForExit() }
[ConsoleInputMode]::Write($initial)
'Restored : 0x{0:X}' -f [ConsoleInputMode]::Read()
}
Representative output from the isolated test:
Initial : 0x1F7
A active : 0x3D8
After A exits : 0x1F7 (A=0, B still running=True)
After B exits : 0x3D8 (B=0)
Restored : 0x1F7
Control tests:
- A single
wsl.exe invocation restores the original mode.
- 30 strictly serial
wsl.exe -- true invocations restored correctly 30/30 times.
- One WSL process overlapping an ordinary PowerShell process restored correctly in either exit order.
- PowerShell-to-WSL execution with inherited or redirected stdout/stderr restored correctly in 15/15 tests.
- Two overlapping WSL clients restore correctly when the client that started first also exits last.
- The failure is deterministic when client A saves the original mode, client B saves A's temporary mode, A exits first, and B exits last.
Expected Behavior
After all wsl.exe clients attached to a console have exited, the console input mode should be the mode that existed before the first client started. One client's temporary WSL mode should never become the final persistent mode solely because overlapping clients exited in a different order.
Actual Behavior
The final input mode is 0x3D8, even though both WSL processes exit successfully. This leaves ENABLE_VIRTUAL_TERMINAL_INPUT (0x200) enabled and also reflects WSL's other temporary flag changes.
For native Windows TUIs that consume Win32/crossterm key events, arrow and editing keys are then translated into VT sequences and their tails can appear as literal text: arrows become [A/[B/[C/[D, Delete becomes [3~, and Enter/Backspace/control-key handling can fail. The console can remain broken after WSL exits until the original input mode is restored or the console is recreated.
The behavior matches WSL 2.7.11's ConsoleState implementation:
For overlapping clients, the observed sequence is:
A saves original mode, then applies WSL mode
B saves A's WSL mode
A exits and restores original mode
B exits and restores the WSL mode it saved
Possible fix directions include coordinating console-state ownership across overlapping wsl.exe clients, reference-counting per console input buffer, or avoiding restoration of a snapshot known to represent another live client's temporary state.
Diagnostic Logs
No private application/session logs are required for this standalone reproduction. I can collect WSL diagnostics for the isolated test if maintainers need them, but I have intentionally not attached unrelated project data.
Windows Version
Microsoft Windows [Version 10.0.22000](Windows 11 Pro x64)WSL Version
2.7.11.0(Microsoft Store package version)Are you using WSL 1 or WSL 2?
Kernel Version
Not relevant to the observed race; it occurs in the Windows-side
wsl.execonsole-state handling. The exact kernel version was not collected while another long-running session was active.Distro Version
The default WSL 2 distribution was used only to run POSIX
shandsleep; no distro-specific behavior is involved.Other Software
0.145.0exposed the real-world impact in a native Windows TUI.Repro Steps
Open a fresh, disposable PowerShell window. The following script snapshots the original input mode, starts two overlapping
wsl.execlients in the same inherited console, prints the mode timeline, and restores the original mode infinallyso that the test window is not left broken.Representative output from the isolated test:
Control tests:
wsl.exeinvocation restores the original mode.wsl.exe -- trueinvocations restored correctly 30/30 times.Expected Behavior
After all
wsl.execlients attached to a console have exited, the console input mode should be the mode that existed before the first client started. One client's temporary WSL mode should never become the final persistent mode solely because overlapping clients exited in a different order.Actual Behavior
The final input mode is
0x3D8, even though both WSL processes exit successfully. This leavesENABLE_VIRTUAL_TERMINAL_INPUT(0x200) enabled and also reflects WSL's other temporary flag changes.For native Windows TUIs that consume Win32/crossterm key events, arrow and editing keys are then translated into VT sequences and their tails can appear as literal text: arrows become
[A/[B/[C/[D, Delete becomes[3~, and Enter/Backspace/control-key handling can fail. The console can remain broken after WSL exits until the original input mode is restored or the console is recreated.The behavior matches WSL 2.7.11's
ConsoleStateimplementation:CONIN$mode and applies WSL's temporary mode: https://github.com/microsoft/WSL/blob/2.7.11/src/windows/common/ConsoleState.cpp#L65-L86For overlapping clients, the observed sequence is:
Possible fix directions include coordinating console-state ownership across overlapping
wsl.execlients, reference-counting per console input buffer, or avoiding restoration of a snapshot known to represent another live client's temporary state.Diagnostic Logs
No private application/session logs are required for this standalone reproduction. I can collect WSL diagnostics for the isolated test if maintainers need them, but I have intentionally not attached unrelated project data.