Win32 input mode breaks WSL .exe interop #20852

Closed
opened 2026-01-31 07:25:52 +00:00 by claunia · 14 comments
Owner

Originally created by @lhecker on GitHub (Nov 20, 2023).

Originally assigned to: @lhecker on GitHub.

Windows Terminal version

1.19.2682.0

Windows build number

No response

Steps to reproduce

Expected Behavior

Input is preserved as it is typed.

Actual Behavior

There are two independent issues at play here:

  • ConPTY deadlocks on startup
    ConhostInternalGetSet::ReturnResponse translates a VT response into a sequence of INPUT_RECORDs and calls InputBuffer::Write. That one calls TerminalInput::HandleKey which translates KEY_EVENTs type into Win32 input sequences. In other words, VT responses are being double-encoded in conhost.
    At some point when ConPTY starts, VtIo::StartIfNeeded() is called which calls RequestCursor() under WSL (= "\x1b[6n" request) and waits for a response. ConPTY now deadlocks because it won't double-decode the response.
  • Console apps receive still-encoded events as input
    Even with that issue fixed, there's still a change in behavior, as the Win32 console application still receives undecoded Win32 input sequences as a series of INPUT_RECORDs. I.e. typing "a" yields 2 dozen INPUT_RECORDs.

Note to self:

  • Retest #16266 after fixing this
Originally created by @lhecker on GitHub (Nov 20, 2023). Originally assigned to: @lhecker on GitHub. ### Windows Terminal version 1.19.2682.0 ### Windows build number _No response_ ### Steps to reproduce * Replace system conhost.exe with a recent v1.19 OpenConsole.exe (I can send `sfpcopy.exe` (the recommended tool for this) on request if anyone wants to test this.) * Launch WSL * Run an .exe that reads input via `ReadConsoleInputW`, for instance: https://gist.github.com/lhecker/43e562d5a1370d2582bb5d6eed4e4f3e ### Expected Behavior Input is preserved as it is typed. ### Actual Behavior There are two independent issues at play here: * ConPTY deadlocks on startup `ConhostInternalGetSet::ReturnResponse` translates a VT response into a sequence of `INPUT_RECORD`s and calls `InputBuffer::Write`. That one calls `TerminalInput::HandleKey` which translates `KEY_EVENT`s type into Win32 input sequences. In other words, VT responses are being double-encoded in conhost. At some point when ConPTY starts, `VtIo::StartIfNeeded()` is called which calls `RequestCursor()` under WSL (= `"\x1b[6n"` request) and waits for a response. ConPTY now deadlocks because it won't double-decode the response. * Console apps receive still-encoded events as input Even with that issue fixed, there's still a change in behavior, as the Win32 console application still receives undecoded Win32 input sequences as a series of `INPUT_RECORD`s. I.e. typing "a" yields 2 dozen `INPUT_RECORD`s. Note to self: * Retest #16266 after fixing this
claunia added the Issue-BugNeeds-Tag-FixProduct-ConptyPriority-0zInbox-Bug labels 2026-01-31 07:25:53 +00:00
Author
Owner

@lhecker commented on GitHub (Nov 20, 2023):

@j4james FYI this regression was caused by #15476, but I haven't really understood yet why it happens or how to fix it. I'll try to figure it out later, but if you immediately know what the issue is (and it's not a problem for you) let me know, and I'll fix it ASAP. 🙂

@lhecker commented on GitHub (Nov 20, 2023): @j4james FYI this regression was caused by #15476, but I haven't really understood yet why it happens or how to fix it. I'll try to figure it out later, but if you immediately know what the issue is (and it's not a problem for you) let me know, and I'll fix it ASAP. 🙂
Author
Owner

@zadjii-msft commented on GitHub (Nov 20, 2023):

@craigloewen-msft as an FYI. We may have broken windows interop for WSL for insiders. We're looking into it.

@zadjii-msft commented on GitHub (Nov 20, 2023): @craigloewen-msft as an FYI. We may have broken windows interop for WSL for insiders. We're looking into it.
Author
Owner
@zadjii-msft commented on GitHub (Nov 20, 2023): See also: * #15083 * #15296
Author
Owner

@lhecker commented on GitHub (Nov 20, 2023):

Probably also causes #16307

@lhecker commented on GitHub (Nov 20, 2023): Probably also causes #16307
Author
Owner

@j4james commented on GitHub (Nov 20, 2023):

From what @DHowett said in https://github.com/microsoft/terminal/pull/15476#discussion_r1210897525, my understanding was that the conpty DLL was tightly coupled to the openconsole version. So I'm wondering whether it's possible that you're getting a version mismatch by replacing the system conhost, if you aren't also using the corresponding conpty DLL?

If it's not something like that, I have no idea what the issue could be. But I haven't had a chance to look at the code in detail (and probably won't have much time again until the weekend).

@j4james commented on GitHub (Nov 20, 2023): From what @DHowett said in https://github.com/microsoft/terminal/pull/15476#discussion_r1210897525, my understanding was that the conpty DLL was tightly coupled to the openconsole version. So I'm wondering whether it's possible that you're getting a version mismatch by replacing the system conhost, if you aren't also using the corresponding conpty DLL? If it's not something like that, I have no idea what the issue could be. But I haven't had a chance to look at the code in detail (and probably won't have much time again until the weekend).
Author
Owner

@j4james commented on GitHub (Nov 20, 2023):

I have an idea what the problem might be, but I don't really know how WSL works behind the scenes so my understanding may be incorrect.

  1. I'm assuming when you launch a Windows executable from within WSL it acts like a third-party conpty client, and creates a conpty connection.
  2. I'm assuming it would do this without the PSEUDOCONSOLE_WIN32_INPUT_MODE flag, but since that flag was removed in PR 15475, we now act as if it's always set.
  3. Without that flag set, conhost would not previously have sent DECSET sequences to enable win32 input mode and focus event mode, but now it does that regardless.
  4. It was assumed that change would be harmless, because a client that didn't understand those modes would ignore them, but I suspect WSL is actually passing them up to the parent terminal.
  5. This would result in the parent terminal enabling win32 input mode, and from then on any keystrokes would be sent to WSL in a format that it doesn't understand (possibly also affecting VT query responses).

If the above assumptions are correct, we're probably going to need to revert most if not all of PR #15476 in order to fix this, and come up with another solution for #15461.

@j4james commented on GitHub (Nov 20, 2023): I have an idea what the problem might be, but I don't really know how WSL works behind the scenes so my understanding may be incorrect. 1. I'm assuming when you launch a Windows executable from within WSL it acts like a third-party conpty client, and creates a conpty connection. 2. I'm assuming it would do this *without* the `PSEUDOCONSOLE_WIN32_INPUT_MODE` flag, but since that flag was removed in PR 15475, we now act as if it's always set. 3. Without that flag set, conhost would not previously have sent `DECSET` sequences to enable win32 input mode and focus event mode, but now it does that regardless. 4. It was assumed that change would be harmless, because a client that didn't understand those modes would ignore them, but I suspect WSL is actually passing them up to the parent terminal. 5. This would result in the parent terminal enabling win32 input mode, and from then on any keystrokes would be sent to WSL in a format that it doesn't understand (possibly also affecting VT query responses). If the above assumptions are correct, we're probably going to need to revert most if not all of PR #15476 in order to fix this, and come up with another solution for #15461.
Author
Owner

@lhecker commented on GitHub (Nov 21, 2023):

The way I see it, the fundamental problem is that the same Mode::Win32 flag is being used for both "input" into and "output" from the InputBuffer and indiscriminately of the purpose of the read.

By default it should only take affect when INPUT_RECORDs are being read for communication between a terminal and ConPTY but since we only have a single InputBuffer::Read() and a single InputBuffer::Write() function it taints even all of the public APIs that don't get those win32-input-sequences. This is especially weird for internal APIs like the aforementioned ConhostInternalGetSet::ReturnResponse which is already is text-based and hence the double-encoding. So, if I don't misunderstand the purpose of the Win32 DECSET sequence I think it's actually fine that it's being passed up through WSL. It just shouldn't be immediately visible to console clients.

Do we support console clients requesting win32-input-sequences? If so, then that doesn't change the above IMO, because a client requesting it shouldn't change the encoding for any other attached client.

Fixing the double-encoding is trivial with just a couple lines changed, but fixing the Win32 sequences being visible to console clients is difficult since it requires us to delay encoding to the time of Read() and make it dependent on the requester. I've started writing a modernized InputBuffer but it's fairly messy given our current architecture and the vast compatibility requirements. 🤦

I suspect we'll end up having to revert most of #15476 after all... 😢

@lhecker commented on GitHub (Nov 21, 2023): The way I see it, the fundamental problem is that the same `Mode::Win32` flag is being used for both "input" into and "output" from the `InputBuffer` and indiscriminately of the purpose of the read. By default it should only take affect when `INPUT_RECORD`s are being read for communication between a terminal and ConPTY but since we only have a single `InputBuffer::Read()` and a single `InputBuffer::Write()` function it taints even all of the public APIs that don't get those win32-input-sequences. This is especially weird for internal APIs like the aforementioned `ConhostInternalGetSet::ReturnResponse` which is already is text-based and hence the double-encoding. So, if I don't misunderstand the purpose of the Win32 `DECSET` sequence I think it's actually fine that it's being passed up through WSL. It just shouldn't be immediately visible to console clients. Do we support console clients requesting win32-input-sequences? If so, then that doesn't change the above IMO, because a client requesting it shouldn't change the encoding for any other attached client. Fixing the double-encoding is trivial with just a couple lines changed, but fixing the Win32 sequences being visible to console clients is difficult since it requires us to delay encoding to the time of `Read()` and make it dependent on the requester. I've started writing a modernized `InputBuffer` but it's fairly messy given our current architecture and the vast compatibility requirements. 🤦 I suspect we'll end up having to revert most of #15476 after all... 😢
Author
Owner

@DHowett commented on GitHub (Nov 21, 2023):

Do we support console clients requesting win32-input-sequences?

I don't see why we shouldn't! It's not different from enabling mouse mode with DECSET 1003.

If so, then that doesn't change the above IMO, because a client requesting it shouldn't change the encoding for any other attached client.

As above, it's not different from enabling mouse mode with DECSET 1003. Unfortunately, we can't know who's requesting it and on whose behalf it is being requested. Even if we track the client application, how do we know that ssh.exe isn't passing input to two different processes--one of which supports win32 input, and one of which does not?

@DHowett commented on GitHub (Nov 21, 2023): > Do we support console clients requesting win32-input-sequences? I don't see why we shouldn't! It's not different from enabling mouse mode with `DECSET` `1003`. > If so, then that doesn't change the above IMO, because a client requesting it shouldn't change the encoding for any other attached client. As above, it's not different from enabling mouse mode with `DECSET` `1003`. Unfortunately, we can't _know_ who's requesting it and on whose behalf it is being requested. Even if we track the client application, how do we know that `ssh.exe` isn't passing input to two different processes--one of which supports win32 input, and one of which does not?
Author
Owner

@lhecker commented on GitHub (Nov 21, 2023):

Even if we track the client application, how do we know that ssh.exe isn't passing input to two different processes--one of which supports win32 input, and one of which does not?

In my vision, win32-input-sequences would be requested automatically by ConPTY at all times, just like it works right now. It's just that console clients (= in case of SSH on the server side) should not see those in form of 20 INPUT_RECORDs per actual input character if they didn't ask for it. That part is easy to do as far as I can see.

@lhecker commented on GitHub (Nov 21, 2023): > Even if we track the client application, how do we know that `ssh.exe` isn't passing input to two different processes--one of which supports win32 input, and one of which does not? In my vision, win32-input-sequences would be requested automatically by ConPTY at all times, just like it works right now. It's just that console clients (= in case of SSH on the server side) should not see those in form of 20 `INPUT_RECORD`s per actual input character if they didn't ask for it. That part is easy to do as far as I can see.
Author
Owner

@lhecker commented on GitHub (Nov 21, 2023):

See... I'm actually an idiot. The reason it ACTUALLY happens is because of this:
12318d97d0/src/terminal/parser/stateMachine.cpp (L2139-L2142)

confused meme

Unfortunately, that statement is entirely wrong. The most basic example for this is that our input buffer is a basic

char buffer[256];

and if your input pipe got more characters than that, the comment's assumption will be broken, because the read will be split up into multiple chunks. Second of all, however, WSL doesn't use anonymous pipes and so they can split up inputs into arbitrary chunks down to individual bytes. That's why we see broken win32 input sequences randomly in WSL.

@lhecker commented on GitHub (Nov 21, 2023): See... I'm actually an idiot. The reason it _ACTUALLY_ happens is because of this: https://github.com/microsoft/terminal/blob/12318d97d0d9df09e05b5dd64622ff4f5b09382b/src/terminal/parser/stateMachine.cpp#L2139-L2142 ![confused meme](https://media.tenor.com/VKtUy6VLhXIAAAAC/ni%C3%B1a-asco.gif) _Unfortunately_, that statement is entirely wrong. The most basic example for this is that our input buffer is a basic ```cpp char buffer[256]; ``` and if your input pipe got more characters than that, the comment's assumption will be broken, because the read will be split up into multiple chunks. Second of all, however, WSL doesn't use anonymous pipes and so they can split up inputs into arbitrary chunks down to individual bytes. That's why we see broken win32 input sequences randomly in WSL.
Author
Owner

@DHowett commented on GitHub (Nov 21, 2023):

That part is easy to do as far as I can see.

Yyeehehhhhh.. eh. Eaugh.

When an application requests ENABLE_VIRTUAL_TERMINAL_INPUT under ConPTY, we pass the input received through ConPTY directly through to the application. It allows the terminal and the client application to support input modalities that conhost doesn't know about.

@DHowett commented on GitHub (Nov 21, 2023): > That part is easy to do as far as I can see. Yyeehehhhhh.. eh. Eaugh. When an application requests `ENABLE_VIRTUAL_TERMINAL_INPUT` under ConPTY, we pass the input received through ConPTY _directly through_ to the application. It allows the terminal and the client application to support input modalities that conhost doesn't know about.
Author
Owner

@j4james commented on GitHub (Nov 26, 2023):

I did a little hack experiment with fixing the ConhostInternalGetSet::ReturnResponse implementation, so it sends the response directly to InputBuffer::_HandleTerminalInputCallback (instead of creating input events, which get converted into VT sequences, which then converted back into input events).

This fixes the problem with VT query responses not working correctly in conhost when win32-input mode is set (I'm almost sure we already had an issue for that, but I couldn't find it). It also fixes the conpty deadlock on startup that you're getting when running an exe from within WSL.

I can't reproduce the second problem though. Running your ReadConsoleInputW test app, I only see two events per key press, i.e. the up and down events. The keypresses are not showing up as a bunch of undecoded win32-input sequences.

That said, you still have a problem when you exit the exe and return to WSL, because the terminal is still in win32-input mode at that point. Since your typical WSL shell is not going to be expecting that, you get a lot of garbage output at the prompt when you type.

I can work around that by adding a win32-input reset after the exe call, e.g. testinput.exe; printf "\e[?9001l", but that's obviously not a practical solution for users. Someone needs to be responsible for restoring that mode, and I'm not sure where that should happen.

@j4james commented on GitHub (Nov 26, 2023): I did a little hack experiment with fixing the `ConhostInternalGetSet::ReturnResponse` implementation, so it sends the response directly to `InputBuffer::_HandleTerminalInputCallback` (instead of creating input events, which get converted into VT sequences, which then converted back into input events). This fixes the problem with VT query responses not working correctly in conhost when win32-input mode is set (I'm almost sure we already had an issue for that, but I couldn't find it). It also fixes the conpty deadlock on startup that you're getting when running an exe from within WSL. I can't reproduce the second problem though. Running your `ReadConsoleInputW` test app, I only see two events per key press, i.e. the up and down events. The keypresses are not showing up as a bunch of undecoded win32-input sequences. That said, you still have a problem when you exit the exe and return to WSL, because the terminal is still in win32-input mode at that point. Since your typical WSL shell is not going to be expecting that, you get a lot of garbage output at the prompt when you type. I can work around that by adding a win32-input reset after the exe call, e.g. `testinput.exe; printf "\e[?9001l"`, but that's obviously not a practical solution for users. Someone needs to be responsible for restoring that mode, and I'm not sure where that should happen.
Author
Owner

@zadjii-msft commented on GitHub (Jan 31, 2024):

We're pretty sure this was fixed, just not actually marked as closed.

@zadjii-msft commented on GitHub (Jan 31, 2024): We're pretty sure this was fixed, just not actually marked as closed.
Author
Owner

@mintty commented on GitHub (Jun 8, 2025):

The issue according to its title persists in 1.22; not sure whether it's completely the same. Two symptoms are:

  • With OpenConsole patched into conhost, running wsl.exe sends DECSET 9001 h to the host terminal (which makes no sense as it is no defined DEC private mode sequence anywhere else).
  • When you emit the sequence within WSL (echo -e "\e[?9001h"), interaction is completely broken; weird sequences (maybe Win32 Input Mode records interrpreted as plain text) appear as input and you are lost interactively.

Test case: host terminal is mintty, no Windows console mode application involved.
Please reopen or indicate that a new issue should be opened.

@mintty commented on GitHub (Jun 8, 2025): The issue according to its title persists in 1.22; not sure whether it's completely the same. Two symptoms are: - With OpenConsole patched into conhost, running wsl.exe sends `DECSET 9001 h` to the host terminal (which makes no sense as it is no defined DEC private mode sequence anywhere else). - When you emit the sequence within WSL (`echo -e "\e[?9001h"`), interaction is completely broken; weird sequences (maybe Win32 Input Mode records interrpreted as plain text) appear as input and you are lost interactively. Test case: host terminal is mintty, no Windows console mode application involved. Please reopen or indicate that a new issue should be opened.
Sign in to join this conversation.
1 Participants
Notifications
Due Date
No due date set.
Dependencies

No dependencies set.

Reference: starred/terminal#20852