[DefTerm] Some invocations can cause deadlocks in terminal + another GUI application in Attach mode #18603

Closed
opened 2026-01-31 06:19:02 +00:00 by claunia · 4 comments
Owner

Originally created by @DHowett on GitHub (Oct 4, 2022).

The following conditions must be true:

  1. Terminal is set as the default
  2. Terminal is set to "Attach" new instances "to the most recently used window"
  3. Terminal is running

If so, the following application will deadlock:

#define WIN32_LEAN_AND_MEAN
#include <windows.h>
#include <stdlib.h>
#include <string_view>

int APIENTRY wWinMain(HINSTANCE hInstance, HINSTANCE, LPWSTR, int)
{
	static constexpr auto szWindowClass{ L"BlahWindowClass" };
	static constexpr std::wstring_view szMessage{ L"Launch Windows Terminal w/ \"Attach to most recent...\" set\nClick anywhere to deadlock" };

	auto WndProc = [](HWND hWnd, UINT message, WPARAM wParam, LPARAM lParam) -> LRESULT {
		switch (message)
		{
		case WM_LBUTTONUP:
			system("cmd /s /c echo hi");
			break;
		case WM_PAINT:
		{
			PAINTSTRUCT ps;
			HDC hdc = BeginPaint(hWnd, &ps);
			RECT r{ 0, 0, 500, 200 };
			DrawTextW(hdc, szMessage.data(), szMessage.size(), &r, DT_LEFT);
			EndPaint(hWnd, &ps);
		}
		break;
		case WM_DESTROY:
			PostQuitMessage(0);
			break;
		default:
			return DefWindowProcW(hWnd, message, wParam, lParam);
		}
		return 0;
	};

	WNDCLASSEXW wcex{};
	wcex.cbSize = sizeof(WNDCLASSEX);
	wcex.lpfnWndProc = static_cast<LRESULT(CALLBACK*)(HWND, UINT, WPARAM, LPARAM)>(WndProc);
	wcex.hInstance = hInstance;
	wcex.hCursor = LoadCursorW(nullptr, (LPCWSTR)IDC_ARROW);
	wcex.hbrBackground = (HBRUSH)(COLOR_WINDOW + 1);
	wcex.lpszClassName = szWindowClass;
	RegisterClassExW(&wcex);

	HWND hWnd = CreateWindowExW(0, szWindowClass, L"Title", WS_OVERLAPPEDWINDOW, CW_USEDEFAULT, 0, 500, 200, nullptr, nullptr, hInstance, nullptr);

	ShowWindow(hWnd, SW_SHOWDEFAULT);
	UpdateWindow(hWnd);

	MSG msg;
	while (GetMessage(&msg, nullptr, 0, 0))
	{
		TranslateMessage(&msg);
		DispatchMessage(&msg);
	}

	return (int)msg.wParam;
}

Save it as foo.cpp and compile:

cl /std:c++17 foo.cpp /link onecoreuap.lib

Launch foo.exe and follow the instruction.


Windows Terminal attempts to bring itself to the foreground by calling AttachThreadInput, BringToForeground and AttachThreadInput in rapid succession.
When the application that is currently in the foreground is calling out to system() to spawn a commandline application, BringToForeground will fail: nobody
is running a message loop, and therefore no win32 messages are being drained. That application is waiting for system() to return before processing further
messages.

This may seem like a theoretical problem, but I encountered it while trying to play The Battle for Wesnoth while on vacation 😀

Originally created by @DHowett on GitHub (Oct 4, 2022). The following conditions must be true: 1. Terminal is set as the default 2. Terminal is set to "Attach" new instances "to the most recently used window" 3. Terminal is running If so, the following application will deadlock: ```c++ #define WIN32_LEAN_AND_MEAN #include <windows.h> #include <stdlib.h> #include <string_view> int APIENTRY wWinMain(HINSTANCE hInstance, HINSTANCE, LPWSTR, int) { static constexpr auto szWindowClass{ L"BlahWindowClass" }; static constexpr std::wstring_view szMessage{ L"Launch Windows Terminal w/ \"Attach to most recent...\" set\nClick anywhere to deadlock" }; auto WndProc = [](HWND hWnd, UINT message, WPARAM wParam, LPARAM lParam) -> LRESULT { switch (message) { case WM_LBUTTONUP: system("cmd /s /c echo hi"); break; case WM_PAINT: { PAINTSTRUCT ps; HDC hdc = BeginPaint(hWnd, &ps); RECT r{ 0, 0, 500, 200 }; DrawTextW(hdc, szMessage.data(), szMessage.size(), &r, DT_LEFT); EndPaint(hWnd, &ps); } break; case WM_DESTROY: PostQuitMessage(0); break; default: return DefWindowProcW(hWnd, message, wParam, lParam); } return 0; }; WNDCLASSEXW wcex{}; wcex.cbSize = sizeof(WNDCLASSEX); wcex.lpfnWndProc = static_cast<LRESULT(CALLBACK*)(HWND, UINT, WPARAM, LPARAM)>(WndProc); wcex.hInstance = hInstance; wcex.hCursor = LoadCursorW(nullptr, (LPCWSTR)IDC_ARROW); wcex.hbrBackground = (HBRUSH)(COLOR_WINDOW + 1); wcex.lpszClassName = szWindowClass; RegisterClassExW(&wcex); HWND hWnd = CreateWindowExW(0, szWindowClass, L"Title", WS_OVERLAPPEDWINDOW, CW_USEDEFAULT, 0, 500, 200, nullptr, nullptr, hInstance, nullptr); ShowWindow(hWnd, SW_SHOWDEFAULT); UpdateWindow(hWnd); MSG msg; while (GetMessage(&msg, nullptr, 0, 0)) { TranslateMessage(&msg); DispatchMessage(&msg); } return (int)msg.wParam; } ``` Save it as `foo.cpp` and compile: ``` cl /std:c++17 foo.cpp /link onecoreuap.lib ``` Launch `foo.exe` and follow the instruction. --- Windows Terminal attempts to bring itself to the foreground by calling `AttachThreadInput`, `BringToForeground` and `AttachThreadInput` in rapid succession. When the application that is currently in the foreground is calling out to `system()` to spawn a commandline application, `BringToForeground` will fail: nobody is running a message loop, and therefore no win32 messages are being drained. That application is waiting for `system()` to return before processing further messages. This may seem like a theoretical problem, but I encountered it while trying to play [The Battle for Wesnoth](https://www.wesnoth.org/) while on vacation :grinning:
Author
Owner

@ghost commented on GitHub (Oct 18, 2022):

:tada:This issue was addressed in #14195, which has now been successfully released as Windows Terminal v1.15.2874.🎉

Handy links:

@ghost commented on GitHub (Oct 18, 2022): :tada:This issue was addressed in #14195, which has now been successfully released as `Windows Terminal v1.15.2874`.:tada: Handy links: * [Release Notes](https://github.com/microsoft/terminal/releases/tag/v1.15.2874) * [Store Download](https://www.microsoft.com/store/apps/9n8g5rfz9xk3?cid=storebadge&ocid=badge)
Author
Owner

@ghost commented on GitHub (Dec 14, 2022):

:tada:This issue was addressed in #14195, which has now been successfully released as Windows Terminal Preview v1.16.3463.0 and v1.16.3464.0.🎉

Handy links:

@ghost commented on GitHub (Dec 14, 2022): :tada:This issue was addressed in #14195, which has now been successfully released as `Windows Terminal Preview v1.16.3463.0 and v1.16.3464.0`.:tada: Handy links: * [Release Notes](https://github.com/microsoft/terminal/releases/tag/v1.16.3464.0) * [Store Download](https://www.microsoft.com/store/apps/9n8g5rfz9xk3?cid=storebadge&ocid=badge)
Author
Owner

@FuPeiJiang commented on GitHub (Jan 10, 2026):

@DHowett

#14195 does fix the deadlock, but now it's a delay of 5000ms, which is a bit long

SendMessageTimeoutW(oldForegroundWindow, WM_NULL, 0, 0, SMTO_NOTIMEOUTIFNOTHUNG | SMTO_BLOCK | SMTO_ABORTIFHUNG, 1000, nullptr)

using SMTO_NOTIMEOUTIFNOTHUNG ignores the 1000ms timeout, and it takes the internal timeout of 5 seconds for it to be detected as hung (since it was newly/recently hung)

https://learn.microsoft.com/en-us/windows/win32/api/winuser/nf-winuser-ishungappwindow

I really have no solution though, only decreasing the timeout to 50ms

SendMessageTimeoutW(oldForegroundWindow, WM_NULL, 0, 0, SMTO_BLOCK | SMTO_ABORTIFHUNG, 50, nullptr)

A potential solution:
Since newly spawned processes can always call SetForegroundWindow (or equivalent) (for reasons I'm not sure of)
WindowsTerminal.exe could just spawn SetForegroundWindow.exe passing the HWND as command line arg, instead of calling a function, the exe could be made incredibly small using https://github.com/ayaka14732/TinyPE-on-Win10
Since there are already other .exe in the distributed folder, SetForegroundWindow.exe wouldn't be too out of place

The real "theoretical" problem would be a hang happening between the two AttachThreadInput, after the thread has been detected as "not hung"

@FuPeiJiang commented on GitHub (Jan 10, 2026): @DHowett #14195 does fix the deadlock, but now it's a delay of 5000ms, which is a bit long ```cpp SendMessageTimeoutW(oldForegroundWindow, WM_NULL, 0, 0, SMTO_NOTIMEOUTIFNOTHUNG | SMTO_BLOCK | SMTO_ABORTIFHUNG, 1000, nullptr) ``` using `SMTO_NOTIMEOUTIFNOTHUNG` ignores the 1000ms timeout, and it takes the internal timeout of 5 seconds for it to be detected as hung (since it was newly/recently hung) <https://learn.microsoft.com/en-us/windows/win32/api/winuser/nf-winuser-ishungappwindow> I really have no solution though, only decreasing the timeout to 50ms ```cpp SendMessageTimeoutW(oldForegroundWindow, WM_NULL, 0, 0, SMTO_BLOCK | SMTO_ABORTIFHUNG, 50, nullptr) ``` A potential solution: Since newly spawned processes can always call `SetForegroundWindow` (or equivalent) (for reasons I'm not sure of) WindowsTerminal.exe could just spawn `SetForegroundWindow.exe` passing the HWND as command line arg, instead of calling a function, the exe could be made incredibly small using https://github.com/ayaka14732/TinyPE-on-Win10 Since there are already other `.exe` in the distributed folder, `SetForegroundWindow.exe` wouldn't be too out of place The real "theoretical" problem would be a hang happening between the two `AttachThreadInput`, after the thread has been detected as "not hung"
Author
Owner

@FuPeiJiang commented on GitHub (Jan 10, 2026):

some documentation about this behavior

If the windows use the same input queue, the message is sent synchronously, first to the window procedure of the top-level window being deactivated, then to the window procedure of the top-level window being activated.

https://learn.microsoft.com/en-us/windows/win32/inputdev/wm-activate

Five Things Every Win32 Developer Should Know

Synchronous Focus

  • A has focus, B calls SetFocus(hwndB)
  • B must wait for A to release focus since
    input is now synchronous
  • If A is hung, then B hangs too

Synchronous Input

  • In the queue are two input messages, first
    a message for A, then a message for B.
  • B calls GetMessage – sees message for A
    and says “I can’t process my input until A
    processes its input first”
  • If A is hung, then B never processes input
@FuPeiJiang commented on GitHub (Jan 10, 2026): some documentation about this behavior > If the windows use the same input queue, the message is sent synchronously, first to the window procedure of the top-level window being deactivated, then to the window procedure of the top-level window being activated. <https://learn.microsoft.com/en-us/windows/win32/inputdev/wm-activate> > Five Things Every Win32 Developer Should Know Synchronous Focus * A has focus, B calls SetFocus(hwndB) * B must wait for A to release focus since input is now synchronous * If A is hung, then B hangs too Synchronous Input * In the queue are two input messages, first a message for A, then a message for B. * B calls GetMessage – sees message for A and says “I can’t process my input until A processes its input first” * If A is hung, then B never processes input
Sign in to join this conversation.
1 Participants
Notifications
Due Date
No due date set.
Dependencies

No dependencies set.

Reference: starred/terminal#18603