Prevent window enumeration deadlock and add unit tests.

For some time now, calls to EnumerateCapturableWindows could lead to a
deadlock if an application's main thread is waiting on the thread that
is running EnumerateCapturableWindows. This is because calls to
GetWindowText and GetWindowTextLength send a message to the window if
the window is owned by the current process. Since the main thread is
waiting on us, it will never reply to this message and we will hang.

This happens occasionally in Chromium when tearing down the
NativeDesktopMediaList object, e.g. when a user clicks "cancel" on
the capture target picker.

We can avoid this deadlock by checking if the window we are querying
is owned by the current process, and if it is then we must ensure it
is responding to messages before we call a GetWindowText* API.

This change also adds a unit test for this scenario. We create a
window and force it to be unresponsive by creating a deadlock, and
then call GetWindowList and (with the new changes) we should not
hang. Without the new changes to GetWindowListHandler, this test
would hang.

Change-Id: I2523cd735f96fd7ea60708c30cd22e5b525803f0
Bug: chromium:1152841
Reviewed-on: https://webrtc-review.googlesource.com/c/src/+/195365
Commit-Queue: Austin Orion <auorion@microsoft.com>
Reviewed-by: Jamie Walch <jamiewalch@chromium.org>
Cr-Commit-Position: refs/heads/master@{#32734}
This commit is contained in:
Austin Orion
2020-12-01 10:24:50 -08:00
committed by Commit Bot
parent 0496a41211
commit 449a78b1e2
4 changed files with 233 additions and 25 deletions

View File

@ -110,6 +110,7 @@ if (rtc_include_tests) {
"win/cursor_unittest_resources.rc",
"win/screen_capture_utils_unittest.cc",
"win/screen_capturer_win_directx_unittest.cc",
"win/window_capture_utils_unittest.cc",
]
}
deps = [
@ -117,7 +118,12 @@ if (rtc_include_tests) {
":desktop_capture_mock",
":primitives",
"../../rtc_base:checks",
# TODO(bugs.webrtc.org/9987): Remove this dep on rtc_base:rtc_base once
# rtc_base:threading is fully defined.
"../../rtc_base:rtc_base",
"../../rtc_base:rtc_base_approved",
"../../rtc_base:threading",
"../../system_wrappers",
"../../test:test_support",
]

View File

@ -36,15 +36,24 @@ struct GetWindowListParams {
DesktopCapturer::SourceList* const result;
};
// If a window is owned by the current process and unresponsive, then making a
// blocking call such as GetWindowText may lead to a deadlock.
//
// https://docs.microsoft.com/en-us/windows/win32/api/winuser/nf-winuser-getwindowtexta#remarks
bool CanSafelyMakeBlockingCalls(HWND hwnd) {
DWORD process_id;
GetWindowThreadProcessId(hwnd, &process_id);
if (process_id != GetCurrentProcessId() || IsWindowResponding(hwnd)) {
return true;
}
return false;
}
BOOL CALLBACK GetWindowListHandler(HWND hwnd, LPARAM param) {
GetWindowListParams* params = reinterpret_cast<GetWindowListParams*>(param);
DesktopCapturer::SourceList* list = params->result;
// Skip untitled window if ignoreUntitled specified
if (params->ignoreUntitled && GetWindowTextLength(hwnd) == 0) {
return TRUE;
}
// Skip invisible and minimized windows
if (!IsWindowVisible(hwnd) || IsIconic(hwnd)) {
return TRUE;
@ -58,16 +67,31 @@ BOOL CALLBACK GetWindowListHandler(HWND hwnd, LPARAM param) {
return TRUE;
}
// If ignoreUnresponsive is true then skip unresponsive windows. Set timout
// with 50ms, in case system is under heavy load, the check can wait longer
// but wont' be too long to delay the the enumeration.
const UINT uTimeout = 50; // ms
if (params->ignoreUnresponsive &&
!SendMessageTimeout(hwnd, WM_NULL, 0, 0, SMTO_ABORTIFHUNG, uTimeout,
nullptr)) {
if (params->ignoreUnresponsive && !IsWindowResponding(hwnd)) {
return TRUE;
}
DesktopCapturer::Source window;
window.id = reinterpret_cast<WindowId>(hwnd);
// GetWindowText* are potentially blocking operations if |hwnd| is
// owned by the current process, and can lead to a deadlock if the message
// pump is waiting on this thread. If we've filtered out unresponsive
// windows, this is not a concern, but otherwise we need to check if we can
// safely make blocking calls.
if (params->ignoreUnresponsive || CanSafelyMakeBlockingCalls(hwnd)) {
const size_t kTitleLength = 500;
WCHAR window_title[kTitleLength] = L"";
if (GetWindowTextLength(hwnd) != 0 &&
GetWindowTextW(hwnd, window_title, kTitleLength) > 0) {
window.title = rtc::ToUtf8(window_title);
}
}
// Skip windows when we failed to convert the title or it is empty.
if (params->ignoreUntitled && window.title.empty())
return TRUE;
// Capture the window class name, to allow specific window classes to be
// skipped.
//
@ -91,19 +115,6 @@ BOOL CALLBACK GetWindowListHandler(HWND hwnd, LPARAM param) {
if (wcscmp(class_name, L"Button") == 0)
return TRUE;
DesktopCapturer::Source window;
window.id = reinterpret_cast<WindowId>(hwnd);
const size_t kTitleLength = 500;
WCHAR window_title[kTitleLength] = L"";
if (GetWindowTextW(hwnd, window_title, kTitleLength) > 0) {
window.title = rtc::ToUtf8(window_title);
}
// Skip windows when we failed to convert the title or it is empty.
if (params->ignoreUntitled && window.title.empty())
return TRUE;
list->push_back(window);
return TRUE;
@ -252,6 +263,14 @@ bool IsWindowValidAndVisible(HWND window) {
return IsWindow(window) && IsWindowVisible(window) && !IsIconic(window);
}
bool IsWindowResponding(HWND window) {
// 50ms is chosen in case the system is under heavy load, but it's also not
// too long to delay window enumeration considerably.
const UINT uTimeoutMs = 50;
return SendMessageTimeout(window, WM_NULL, 0, 0, SMTO_ABORTIFHUNG, uTimeoutMs,
nullptr);
}
bool GetWindowList(int flags, DesktopCapturer::SourceList* windows) {
GetWindowListParams params(flags, windows);
return ::EnumWindows(&GetWindowListHandler,

View File

@ -71,6 +71,9 @@ bool IsWindowMaximized(HWND window, bool* result);
// visible, and that it is not minimized.
bool IsWindowValidAndVisible(HWND window);
// Checks if a window responds to a message within 50ms.
bool IsWindowResponding(HWND window);
enum GetWindowListFlags {
kNone = 0x00,
kIgnoreUntitled = 1 << 0,

View File

@ -0,0 +1,180 @@
/*
* Copyright (c) 2020 The WebRTC project authors. All Rights Reserved.
*
* Use of this source code is governed by a BSD-style license
* that can be found in the LICENSE file in the root of the source
* tree. An additional intellectual property rights grant can be found
* in the file PATENTS. All contributing project authors may
* be found in the AUTHORS file in the root of the source tree.
*/
#include "modules/desktop_capture/win/window_capture_utils.h"
#include <winuser.h>
#include <algorithm>
#include <memory>
#include <mutex>
#include "modules/desktop_capture/desktop_capturer.h"
#include "rtc_base/thread.h"
#include "test/gtest.h"
namespace webrtc {
namespace {
const char kWindowThreadName[] = "window_capture_utils_test_thread";
const WCHAR kWindowClass[] = L"WindowCaptureUtilsTestClass";
const WCHAR kWindowTitle[] = L"Window Capture Utils Test";
const int kWindowWidth = 300;
const int kWindowHeight = 200;
struct WindowInfo {
HWND hwnd;
HINSTANCE window_instance;
ATOM window_class;
};
WindowInfo CreateTestWindow(const WCHAR* window_title) {
WindowInfo info;
::GetModuleHandleExW(GET_MODULE_HANDLE_EX_FLAG_FROM_ADDRESS |
GET_MODULE_HANDLE_EX_FLAG_UNCHANGED_REFCOUNT,
reinterpret_cast<LPCWSTR>(&::DefWindowProc),
&info.window_instance);
WNDCLASSEXW wcex;
memset(&wcex, 0, sizeof(wcex));
wcex.cbSize = sizeof(wcex);
wcex.style = CS_HREDRAW | CS_VREDRAW;
wcex.hInstance = info.window_instance;
wcex.lpfnWndProc = &::DefWindowProc;
wcex.lpszClassName = kWindowClass;
info.window_class = ::RegisterClassExW(&wcex);
info.hwnd = ::CreateWindowW(kWindowClass, window_title, WS_OVERLAPPEDWINDOW,
CW_USEDEFAULT, CW_USEDEFAULT, kWindowWidth,
kWindowHeight, /*parent_window=*/nullptr,
/*menu_bar=*/nullptr, info.window_instance,
/*additional_params=*/nullptr);
::ShowWindow(info.hwnd, SW_SHOWNORMAL);
::UpdateWindow(info.hwnd);
return info;
}
void DestroyTestWindow(WindowInfo info) {
::DestroyWindow(info.hwnd);
::UnregisterClass(MAKEINTATOM(info.window_class), info.window_instance);
}
std::unique_ptr<rtc::Thread> SetUpUnresponsiveWindow(std::mutex& mtx,
WindowInfo& info) {
std::unique_ptr<rtc::Thread> window_thread;
window_thread = rtc::Thread::Create();
window_thread->SetName(kWindowThreadName, nullptr);
window_thread->Start();
window_thread->Invoke<void>(
RTC_FROM_HERE, [&info]() { info = CreateTestWindow(kWindowTitle); });
// Intentionally create a deadlock to cause the window to become unresponsive.
mtx.lock();
window_thread->PostTask(RTC_FROM_HERE, [&mtx]() {
mtx.lock();
mtx.unlock();
});
return window_thread;
}
} // namespace
TEST(WindowCaptureUtilsTest, GetWindowList) {
WindowInfo info = CreateTestWindow(kWindowTitle);
DesktopCapturer::SourceList window_list;
ASSERT_TRUE(GetWindowList(GetWindowListFlags::kNone, &window_list));
EXPECT_GT(window_list.size(), 0ULL);
EXPECT_NE(std::find_if(window_list.begin(), window_list.end(),
[&info](DesktopCapturer::Source window) {
return reinterpret_cast<HWND>(window.id) ==
info.hwnd;
}),
window_list.end());
DestroyTestWindow(info);
}
TEST(WindowCaptureUtilsTest, IncludeUnresponsiveWindows) {
std::mutex mtx;
WindowInfo info;
std::unique_ptr<rtc::Thread> window_thread =
SetUpUnresponsiveWindow(mtx, info);
EXPECT_FALSE(IsWindowResponding(info.hwnd));
DesktopCapturer::SourceList window_list;
ASSERT_TRUE(GetWindowList(GetWindowListFlags::kNone, &window_list));
EXPECT_GT(window_list.size(), 0ULL);
EXPECT_NE(std::find_if(window_list.begin(), window_list.end(),
[&info](DesktopCapturer::Source window) {
return reinterpret_cast<HWND>(window.id) ==
info.hwnd;
}),
window_list.end());
mtx.unlock();
window_thread->Invoke<void>(RTC_FROM_HERE,
[&info]() { DestroyTestWindow(info); });
window_thread->Stop();
}
TEST(WindowCaptureUtilsTest, IgnoreUnresponsiveWindows) {
std::mutex mtx;
WindowInfo info;
std::unique_ptr<rtc::Thread> window_thread =
SetUpUnresponsiveWindow(mtx, info);
EXPECT_FALSE(IsWindowResponding(info.hwnd));
DesktopCapturer::SourceList window_list;
ASSERT_TRUE(
GetWindowList(GetWindowListFlags::kIgnoreUnresponsive, &window_list));
EXPECT_EQ(std::find_if(window_list.begin(), window_list.end(),
[&info](DesktopCapturer::Source window) {
return reinterpret_cast<HWND>(window.id) ==
info.hwnd;
}),
window_list.end());
mtx.unlock();
window_thread->Invoke<void>(RTC_FROM_HERE,
[&info]() { DestroyTestWindow(info); });
window_thread->Stop();
}
TEST(WindowCaptureUtilsTest, IncludeUntitledWindows) {
WindowInfo info = CreateTestWindow(L"");
DesktopCapturer::SourceList window_list;
ASSERT_TRUE(GetWindowList(GetWindowListFlags::kNone, &window_list));
EXPECT_GT(window_list.size(), 0ULL);
EXPECT_NE(std::find_if(window_list.begin(), window_list.end(),
[&info](DesktopCapturer::Source window) {
return reinterpret_cast<HWND>(window.id) ==
info.hwnd;
}),
window_list.end());
DestroyTestWindow(info);
}
TEST(WindowCaptureUtilsTest, IgnoreUntitledWindows) {
WindowInfo info = CreateTestWindow(L"");
DesktopCapturer::SourceList window_list;
ASSERT_TRUE(GetWindowList(GetWindowListFlags::kIgnoreUntitled, &window_list));
EXPECT_EQ(std::find_if(window_list.begin(), window_list.end(),
[&info](DesktopCapturer::Source window) {
return reinterpret_cast<HWND>(window.id) ==
info.hwnd;
}),
window_list.end());
DestroyTestWindow(info);
}
} // namespace webrtc