Random NTSTATUS when calling user32.dll!ConsoleControl under WoW64 #20263

Open
opened 2026-01-31 07:08:26 +00:00 by claunia · 12 comments
Owner

Originally created by @o-sdn-o on GitHub (Jul 20, 2023).

Windows Terminal version

No response

Windows build number

10.0.19045.3086

Other Software

No response

Steps to reproduce

Build the following code for 32-bit target and run on 64-bit host

#include <iostream>
#include <windows.h>

typedef enum _CONSOLECONTROL {
    Reserved1,
    ConsoleNotifyConsoleApplication,
    Reserved2,
    ConsoleSetCaretInfo,
    Reserved3,
    ConsoleSetForeground,
    ConsoleSetWindowOwner,
    ConsoleEndTask,
} CONSOLECONTROL;

typedef struct _CONSOLEENDTASK {
    HANDLE ProcessId;
    HWND hwnd;
    ULONG ConsoleEventCode;
    ULONG ConsoleFlags;
} CONSOLEENDTASK, * PCONSOLEENDTASK;

typedef NTSTATUS(WINAPI* ConsoleControl)(
    _In_ CONSOLECONTROL Command,
    _In_reads_bytes_(ConsoleInformationLength) PVOID ConsoleInformation,
    _In_ DWORD ConsoleInformationLength
    );

int main()
{
    auto user32_dll = ::LoadLibraryExA("user32.dll", nullptr, LOAD_LIBRARY_SEARCH_SYSTEM32);
    auto ConsoleControl = (::ConsoleControl)::GetProcAddress(user32_dll, "ConsoleControl");
    auto count = DWORD{};
    auto rec = INPUT_RECORD{};
    auto task = CONSOLEENDTASK{ .ProcessId = (HANDLE)::GetCurrentProcessId(), .ConsoleEventCode = CTRL_C_EVENT };
    std::cout << "Press any key to generate event. Press Esc to exit.\n";
    while (true)
    {
        ::ReadConsoleInputW(::GetStdHandle(STD_INPUT_HANDLE), &rec, 1, &count);
        if (rec.EventType == KEY_EVENT && rec.Event.KeyEvent.bKeyDown)
        {
            if (rec.Event.KeyEvent.wVirtualKeyCode == VK_ESCAPE) return 0;
            auto status = ConsoleControl(ConsoleEndTask, &task, sizeof(task));
            std::cout << "status: 0x" << std::hex << status << "\n";
        }
    }
}

Expected Behavior

Output

Press any key to generate event. Press Esc to exit.
status: 0x0
status: 0x0
status: 0x0
status: 0x0

Actual Behavior

Output

Press any key to generate event. Press Esc to exit.
status: 0xb94a7
status: 0xb94ca
status: 0xb94db
status: 0xb94e6
status: 0xb955a
status: 0xb956b
status: 0xb9572
status: 0xb95fc
status: 0xb9603
status: 0xb962b
status: 0xb969b
Originally created by @o-sdn-o on GitHub (Jul 20, 2023). ### Windows Terminal version _No response_ ### Windows build number 10.0.19045.3086 ### Other Software _No response_ ### Steps to reproduce Build the following code for 32-bit target and run on 64-bit host ```c++ #include <iostream> #include <windows.h> typedef enum _CONSOLECONTROL { Reserved1, ConsoleNotifyConsoleApplication, Reserved2, ConsoleSetCaretInfo, Reserved3, ConsoleSetForeground, ConsoleSetWindowOwner, ConsoleEndTask, } CONSOLECONTROL; typedef struct _CONSOLEENDTASK { HANDLE ProcessId; HWND hwnd; ULONG ConsoleEventCode; ULONG ConsoleFlags; } CONSOLEENDTASK, * PCONSOLEENDTASK; typedef NTSTATUS(WINAPI* ConsoleControl)( _In_ CONSOLECONTROL Command, _In_reads_bytes_(ConsoleInformationLength) PVOID ConsoleInformation, _In_ DWORD ConsoleInformationLength ); int main() { auto user32_dll = ::LoadLibraryExA("user32.dll", nullptr, LOAD_LIBRARY_SEARCH_SYSTEM32); auto ConsoleControl = (::ConsoleControl)::GetProcAddress(user32_dll, "ConsoleControl"); auto count = DWORD{}; auto rec = INPUT_RECORD{}; auto task = CONSOLEENDTASK{ .ProcessId = (HANDLE)::GetCurrentProcessId(), .ConsoleEventCode = CTRL_C_EVENT }; std::cout << "Press any key to generate event. Press Esc to exit.\n"; while (true) { ::ReadConsoleInputW(::GetStdHandle(STD_INPUT_HANDLE), &rec, 1, &count); if (rec.EventType == KEY_EVENT && rec.Event.KeyEvent.bKeyDown) { if (rec.Event.KeyEvent.wVirtualKeyCode == VK_ESCAPE) return 0; auto status = ConsoleControl(ConsoleEndTask, &task, sizeof(task)); std::cout << "status: 0x" << std::hex << status << "\n"; } } } ``` ### Expected Behavior Output ``` Press any key to generate event. Press Esc to exit. status: 0x0 status: 0x0 status: 0x0 status: 0x0 ``` ### Actual Behavior Output ``` Press any key to generate event. Press Esc to exit. status: 0xb94a7 status: 0xb94ca status: 0xb94db status: 0xb94e6 status: 0xb955a status: 0xb956b status: 0xb9572 status: 0xb95fc status: 0xb9603 status: 0xb962b status: 0xb969b ```
claunia added the Product-ConhostIssue-BugPriority-3Area-Server labels 2026-01-31 07:08:27 +00:00
Author
Owner

@DHowett commented on GitHub (Jul 20, 2023):

Now that we've documented it, we have to reckon with all the weird corner cases! Alright, so, here goes.

  • ConsoleControl was never specified for use in a WoW64 context. I will update the documentation to make this clear.
    • These APIs were designed when the console host was moved out of csrss and was guaranteed to be locked to the architecture of the kernel. That requirement hasn't changed, unfortunately. Even for OpenConsole built from this repository.
  • ConsoleEndTask contains some authentication checks to ensure that the caller holds the console (condrv, really) Server handle for the given process. It will not work in isolation.

Now, the interesting thing for you is that ConsoleControl implements ConsoleEndTask in the client application by brokering over to csrss, and all other console controls by passing them along to win32k. It's very likely that the bitness problem is unsolveable, because ConsoleControl re-packs the csrss message locally before sending it along.

Right now, this is by design. The docs need to be updated, however. 😄

@DHowett commented on GitHub (Jul 20, 2023): Now that we've documented it, we have to reckon with all the weird corner cases! Alright, so, here goes. - `ConsoleControl` was never specified for use in a WoW64 context. I will update the documentation to make this clear. - These APIs were designed when the console host was moved out of csrss and was guaranteed to be locked to the architecture of the kernel. That requirement hasn't changed, unfortunately. Even for OpenConsole built from this repository. - `ConsoleEndTask` contains some authentication checks to ensure that the caller holds the console (condrv, really) _Server_ handle for the given process. It will not work in isolation. Now, the interesting thing for you is that `ConsoleControl` implements `ConsoleEndTask` in the client application by brokering over to csrss, and all other console controls by passing them along to win32k. It's very likely that the bitness problem is unsolveable, because ConsoleControl re-packs the csrss message locally before sending it along. Right now, this is by design. The docs need to be updated, however. :smile:
Author
Owner

@o-sdn-o commented on GitHub (Jul 20, 2023):

I want to clarify two points

  • If we step down a level and use ntdll.dll!CsrClientCallServer everything works fine. We are using it now as a workaround for this issue.
  • Every time I observe the processor instruction pointer at random point in memory after exiting the 64-bit mode inside the ConsoleControl call on WoW.
@o-sdn-o commented on GitHub (Jul 20, 2023): I want to clarify two points - If we step down a level and use ntdll.dll!CsrClientCallServer everything works fine. We are using it now as a workaround for this issue. - Every time I observe the processor instruction pointer at random point in memory after exiting the 64-bit mode inside the ConsoleControl call on WoW.
Author
Owner

@o-sdn-o commented on GitHub (Jul 21, 2023):

ConsoleControl implements ConsoleEndTask in the client application by brokering over to csrss, and all other console controls by passing them along to win32k. It's very likely that the bitness problem is unsolveable, because ConsoleControl re-packs the csrss message locally before sending it along.

I don't see any reason why the ConsoleControl can't detect that it's running under WoW64 (::IsWow64Process2(::GetCurrentProcess()) and repackage the csrss message accordingly for the 64-bit architecture. Now we are forced to work around this using a low-level undocumented API.

Right now, this is by design.

Ok. Fine. This is a WT design. Windows fundamental design provides support for running 32-bit programs on 64-bit Windows. Moreover, 32-bit terminal emulators function perfectly under WoW64 seamlessly operating both 32-bit and 64-bit applications. The only misbehaving component is user32.dll!ConsoleControl, which is a high-level primitive wrapper that appears to have a buffer overflow and subsequent stack corruption that corrupts the return address.

FYI, this all works smoothly, regardless of whether it is a 32-bit or 64-bit executable, terminal, or application in any combination:

image
image

@o-sdn-o commented on GitHub (Jul 21, 2023): > `ConsoleControl` implements `ConsoleEndTask` in the client application by brokering over to csrss, and all other console controls by passing them along to win32k. It's very likely that the bitness problem is unsolveable, because ConsoleControl re-packs the csrss message locally before sending it along. I don't see any reason why the ConsoleControl can't detect that it's running under WoW64 (::IsWow64Process2(::GetCurrentProcess()) and repackage the csrss message accordingly for the 64-bit architecture. Now we are forced to work around this using a low-level undocumented API. > Right now, this is by design. Ok. Fine. This is a WT design. Windows fundamental design provides support for running 32-bit programs on 64-bit Windows. Moreover, 32-bit terminal emulators function perfectly under WoW64 seamlessly operating both 32-bit and 64-bit applications. The only misbehaving component is user32.dll!ConsoleControl, which is a high-level primitive wrapper that appears to have a buffer overflow and subsequent stack corruption that corrupts the return address. FYI, this all works smoothly, regardless of whether it is a 32-bit or 64-bit executable, terminal, or application in any combination: ![image](https://github.com/microsoft/terminal/assets/11535558/771b06ab-710f-4955-928f-58f7bf0c5bab) ![image](https://github.com/microsoft/terminal/assets/11535558/d662e8c8-2776-4793-a17c-5ee06908898b)
Author
Owner

@zadjii-msft commented on GitHub (Jul 21, 2023):

If I may - why are you using this API in the first place? To what end? I just wanna know a little more about how it's being used in the wild

@zadjii-msft commented on GitHub (Jul 21, 2023): If I may - why are you using this API in the first place? To what end? I just wanna know a little more about how it's being used in the wild
Author
Owner

@o-sdn-o commented on GitHub (Jul 21, 2023):

Our terminal hosts console applications and calling user32.dll!ConsoleControl is a way to tell the application about the Ctrl+Break, Ctrl+C, Close, Logoff, Shutdown event. That is, asynchronously call their custom handler for these events. This is the same as SIGHUP/SIGINT/SIGTERM etc.

@o-sdn-o commented on GitHub (Jul 21, 2023): Our terminal hosts console applications and calling user32.dll!ConsoleControl is a way to tell the application about the Ctrl+Break, Ctrl+C, Close, Logoff, Shutdown event. That is, asynchronously call their custom handler for these events. This is the same as SIGHUP/SIGINT/SIGTERM etc.
Author
Owner

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

@zadjii-msft In short, @o-sdn-o has written a conhost clone and integrated it directly into vtm. That's why they need this API: Can't have a complete conhost without Ctrl+C functionality. 😅 vtm is obviously not feature-complete, but it works under the most popular situations already, as shown above, which is super impressive IMO. But a 32-Bit vtm doesn't work on a 64-Bit host [edit: due to this API], because conhost was always tied to the bitness of the host and so was any API it ever used.

@lhecker commented on GitHub (Jul 21, 2023): @zadjii-msft In short, @o-sdn-o has written a conhost clone and integrated it directly into vtm. That's why they need this API: Can't have a complete conhost without Ctrl+C functionality. 😅 vtm is obviously not feature-complete, but it works under the most popular situations already, as shown above, which is super impressive IMO. But a 32-Bit vtm doesn't work on a 64-Bit host [edit: due to this API], because conhost was always tied to the bitness of the host and so was any API it ever used.
Author
Owner

@o-sdn-o commented on GitHub (Jul 21, 2023):

But a 32-Bit vtm doesn't work on a 64-Bit host

32-bit vtm works fine on a 64-bit host, but for this we have to use undocumented APIs to workaround the mono-bitness in used32.dll!ConsoleControl.

@o-sdn-o commented on GitHub (Jul 21, 2023): > But a 32-Bit vtm doesn't work on a 64-Bit host 32-bit vtm works fine on a 64-bit host, but for this we have to use undocumented APIs to workaround the mono-bitness in used32.dll!ConsoleControl.
Author
Owner

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

Yep, that's what I wanted to convey with that sentence. 🙂
I should probably add that my previous comment is neither in favor of nor against fixing the API. Personally speaking, I'm mostly impartial about it. I share both, your concern about it being a public user32 API now, and Dustin's expression of it being an API that's simply tied to the host-bitness, because it always was.

@lhecker commented on GitHub (Jul 21, 2023): Yep, that's what I wanted to convey with that sentence. 🙂 I should probably add that my previous comment is neither in favor of nor against fixing the API. Personally speaking, I'm mostly impartial about it. I share both, your concern about it being a public user32 API now, and Dustin's expression of it being an API that's simply tied to the host-bitness, because it always was.
Author
Owner

@o-sdn-o commented on GitHub (Jul 21, 2023):

I'm not really worried about using an undocumented API, but that it would be nice to be able to use the official API where possible.

@o-sdn-o commented on GitHub (Jul 21, 2023): I'm not really worried about using an undocumented API, but that it would be nice to be able to use the official API where possible.
Author
Owner

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

Just to level-set and perhaps dispel some misinformation:

This is a WT design.

ConsoleControl was authored some time before Windows NT 3.51 was released. Almost nobody on the team was capable of high-level decision-making during the time at which this once-internal API was created, on account of many of us being children. It was not designed specifically for Windows Terminal, and it was not even designed for conhost (which did not exist at the time.) I promise you we're not just out here shipping private APIs to benefit our own first-party open-source terminal emulator!

Unfortunately, exposing the weird internals of our APIs and how they are generally broadly incompatible with the rest of the public API surface is part of the territory. That's something we could get better at!

I am not against fixing this API. We documented it publicly, and it should work for anyone who needs it. However, it is going to have to come in alongside "document the driver interface" and "make the driver interface WoW64-compatible". You've done great work on your console server implementation (in fact, you fixed a bunch of the WoW64 issues! Thanks!), and I appreciate you telling us about the problems you've encountered.

I am also not saying that it is hard to fix. The NT APIs don't typically use IsWow64Process and friends, opting instead for something lower-level, but it's definitely doable (and probably easily doable.)

However, if we do fix ConsoleControl: we don't have a business justification for servicing it to earlier versions of Windows, so we're probably only going to be able to fix it for whatever the next full OS swap version of Windows is. That's probably not going to help you over there on 10.0.19045. I'm sorry about that.

@DHowett commented on GitHub (Jul 21, 2023): Just to level-set and perhaps dispel some misinformation: > This is a WT design. `ConsoleControl` was authored some time before Windows NT 3.51 was released. Almost nobody on the team was capable of high-level decision-making during the time at which this once-internal API was created, on account of many of us being children. It was not designed specifically for Windows Terminal, and it was not even designed for conhost (which did not exist at the time.) I promise you we're not just out here shipping private APIs to benefit our own first-party open-source terminal emulator! Unfortunately, exposing the weird internals of our APIs and how they are generally broadly incompatible with the rest of the public API surface _is part of the territory._ That's something we could get better at! **I am not against fixing this API.** We documented it publicly, and it should work for anyone who needs it. However, it is going to have to come in alongside "document the driver interface" and "make the driver interface WoW64-compatible". You've done great work on your console server implementation (in fact, you fixed a bunch of the WoW64 issues! Thanks!), and I appreciate you telling us about the problems you've encountered. I am also not saying that it is hard to fix. The NT APIs don't typically use `IsWow64Process` and friends, opting instead for something lower-level, but it's definitely doable (and probably easily doable.) However, if we _do_ fix `ConsoleControl`: we don't have a business justification for servicing it to earlier versions of Windows, so we're probably only going to be able to fix it for whatever the next full OS swap version of Windows is. That's probably not going to help you over there on 10.0.19045. I'm sorry about that.
Author
Owner

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

We've talked about it a bit more offline and agreed to put this on our backlog. 🙂
We'll almost assuredly not backport such a fix, because we simply can't (as Dustin explained above). But fixing it in Windows 12 is better than not fixing it at all.

@lhecker commented on GitHub (Jul 21, 2023): We've talked about it a bit more offline and agreed to put this on our backlog. 🙂 We'll almost assuredly not backport such a fix, because we simply can't (as Dustin explained above). But fixing it in Windows 12 is better than not fixing it at all.
Author
Owner

@malxau commented on GitHub (Aug 29, 2023):

Disclaimer: I've never used these APIs and am reading about them just now. All comments are firmly in the category of "interested observer."

What would the fix be?

If somebody told me to use this API tomorrow, I'd query the bitness of the kernel, and pass either a 32 bit or 64 bit structure based on which kernel my program runs on. But if I did that, a later kernel side change that changes the interpretation of the structure would break my program. My program needs to know which structure the kernel is expecting. Since the fix can't be atomically backported to all systems in existence, programs need to face this inconsistency.

I'm wondering if the fix requires defining new information classes for the new structure interpretation. When run on an older kernel, these would always fail as opposed to being misinterpreted.

@malxau commented on GitHub (Aug 29, 2023): Disclaimer: I've never used these APIs and am reading about them just now. All comments are firmly in the category of "interested observer." What would the fix be? If somebody told me to use this API tomorrow, I'd query the bitness of the kernel, and pass either a 32 bit or 64 bit structure based on which kernel my program runs on. But if I did that, a later kernel side change that changes the interpretation of the structure would break my program. My program needs to know which structure the kernel is expecting. Since the fix can't be atomically backported to all systems in existence, programs need to face this inconsistency. I'm wondering if the fix requires defining new information classes for the new structure interpretation. When run on an older kernel, these would always fail as opposed to being misinterpreted.
Sign in to join this conversation.
1 Participants
Notifications
Due Date
No due date set.
Dependencies

No dependencies set.

Reference: starred/terminal#20263