Skip to content

Commit 667a574

Browse files
authored
gh-112278: Improve error handling in wmi module and tests (GH-117818)
1 parent 3e51096 commit 667a574

File tree

2 files changed

+94
-25
lines changed

2 files changed

+94
-25
lines changed

Lib/test/test_wmi.py

Lines changed: 21 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -1,17 +1,31 @@
11
# Test the internal _wmi module on Windows
22
# This is used by the platform module, and potentially others
33

4+
import time
45
import unittest
5-
from test.support import import_helper, requires_resource
6+
from test.support import import_helper, requires_resource, LOOPBACK_TIMEOUT
67

78

89
# Do this first so test will be skipped if module doesn't exist
910
_wmi = import_helper.import_module('_wmi', required_on=['win'])
1011

1112

13+
def wmi_exec_query(query):
14+
# gh-112278: WMI maybe slow response when first call.
15+
try:
16+
return _wmi.exec_query(query)
17+
except BrokenPipeError:
18+
pass
19+
except WindowsError as e:
20+
if e.winerror != 258:
21+
raise
22+
time.sleep(LOOPBACK_TIMEOUT)
23+
return _wmi.exec_query(query)
24+
25+
1226
class WmiTests(unittest.TestCase):
1327
def test_wmi_query_os_version(self):
14-
r = _wmi.exec_query("SELECT Version FROM Win32_OperatingSystem").split("\0")
28+
r = wmi_exec_query("SELECT Version FROM Win32_OperatingSystem").split("\0")
1529
self.assertEqual(1, len(r))
1630
k, eq, v = r[0].partition("=")
1731
self.assertEqual("=", eq, r[0])
@@ -28,7 +42,7 @@ def test_wmi_query_repeated(self):
2842
def test_wmi_query_error(self):
2943
# Invalid queries fail with OSError
3044
try:
31-
_wmi.exec_query("SELECT InvalidColumnName FROM InvalidTableName")
45+
wmi_exec_query("SELECT InvalidColumnName FROM InvalidTableName")
3246
except OSError as ex:
3347
if ex.winerror & 0xFFFFFFFF == 0x80041010:
3448
# This is the expected error code. All others should fail the test
@@ -42,19 +56,19 @@ def test_wmi_query_repeated_error(self):
4256
def test_wmi_query_not_select(self):
4357
# Queries other than SELECT are blocked to avoid potential exploits
4458
with self.assertRaises(ValueError):
45-
_wmi.exec_query("not select, just in case someone tries something")
59+
wmi_exec_query("not select, just in case someone tries something")
4660

4761
@requires_resource('cpu')
4862
def test_wmi_query_overflow(self):
4963
# Ensure very big queries fail
5064
# Test multiple times to ensure consistency
5165
for _ in range(2):
5266
with self.assertRaises(OSError):
53-
_wmi.exec_query("SELECT * FROM CIM_DataFile")
67+
wmi_exec_query("SELECT * FROM CIM_DataFile")
5468

5569
def test_wmi_query_multiple_rows(self):
5670
# Multiple instances should have an extra null separator
57-
r = _wmi.exec_query("SELECT ProcessId FROM Win32_Process WHERE ProcessId < 1000")
71+
r = wmi_exec_query("SELECT ProcessId FROM Win32_Process WHERE ProcessId < 1000")
5872
self.assertFalse(r.startswith("\0"), r)
5973
self.assertFalse(r.endswith("\0"), r)
6074
it = iter(r.split("\0"))
@@ -69,6 +83,6 @@ def test_wmi_query_threads(self):
6983
from concurrent.futures import ThreadPoolExecutor
7084
query = "SELECT ProcessId FROM Win32_Process WHERE ProcessId < 1000"
7185
with ThreadPoolExecutor(4) as pool:
72-
task = [pool.submit(_wmi.exec_query, query) for _ in range(32)]
86+
task = [pool.submit(wmi_exec_query, query) for _ in range(32)]
7387
for t in task:
7488
self.assertRegex(t.result(), "ProcessId=")

PC/_wmimodule.cpp

Lines changed: 73 additions & 18 deletions
Original file line numberDiff line numberDiff line change
@@ -8,6 +8,11 @@
88
// Version history
99
// 2022-08: Initial contribution (Steve Dower)
1010

11+
// clinic/_wmimodule.cpp.h uses internal pycore_modsupport.h API
12+
#ifndef Py_BUILD_CORE_BUILTIN
13+
# define Py_BUILD_CORE_MODULE 1
14+
#endif
15+
1116
#define _WIN32_DCOM
1217
#include <Windows.h>
1318
#include <comdef.h>
@@ -39,6 +44,8 @@ struct _query_data {
3944
LPCWSTR query;
4045
HANDLE writePipe;
4146
HANDLE readPipe;
47+
HANDLE initEvent;
48+
HANDLE connectEvent;
4249
};
4350

4451

@@ -75,12 +82,18 @@ _query_thread(LPVOID param)
7582
IID_IWbemLocator, (LPVOID *)&locator
7683
);
7784
}
85+
if (SUCCEEDED(hr) && !SetEvent(data->initEvent)) {
86+
hr = HRESULT_FROM_WIN32(GetLastError());
87+
}
7888
if (SUCCEEDED(hr)) {
7989
hr = locator->ConnectServer(
8090
bstr_t(L"ROOT\\CIMV2"),
8191
NULL, NULL, 0, NULL, 0, 0, &services
8292
);
8393
}
94+
if (SUCCEEDED(hr) && !SetEvent(data->connectEvent)) {
95+
hr = HRESULT_FROM_WIN32(GetLastError());
96+
}
8497
if (SUCCEEDED(hr)) {
8598
hr = CoSetProxyBlanket(
8699
services, RPC_C_AUTHN_WINNT, RPC_C_AUTHZ_NONE, NULL,
@@ -184,6 +197,24 @@ _query_thread(LPVOID param)
184197
}
185198

186199

200+
static DWORD
201+
wait_event(HANDLE event, DWORD timeout)
202+
{
203+
DWORD err = 0;
204+
switch (WaitForSingleObject(event, timeout)) {
205+
case WAIT_OBJECT_0:
206+
break;
207+
case WAIT_TIMEOUT:
208+
err = WAIT_TIMEOUT;
209+
break;
210+
default:
211+
err = GetLastError();
212+
break;
213+
}
214+
return err;
215+
}
216+
217+
187218
/*[clinic input]
188219
_wmi.exec_query
189220
@@ -226,7 +257,11 @@ _wmi_exec_query_impl(PyObject *module, PyObject *query)
226257

227258
Py_BEGIN_ALLOW_THREADS
228259

229-
if (!CreatePipe(&data.readPipe, &data.writePipe, NULL, 0)) {
260+
data.initEvent = CreateEvent(NULL, TRUE, FALSE, NULL);
261+
data.connectEvent = CreateEvent(NULL, TRUE, FALSE, NULL);
262+
if (!data.initEvent || !data.connectEvent ||
263+
!CreatePipe(&data.readPipe, &data.writePipe, NULL, 0))
264+
{
230265
err = GetLastError();
231266
} else {
232267
hThread = CreateThread(NULL, 0, _query_thread, (LPVOID*)&data, 0, NULL);
@@ -238,6 +273,19 @@ _wmi_exec_query_impl(PyObject *module, PyObject *query)
238273
}
239274
}
240275

276+
// gh-112278: If current user doesn't have permission to query the WMI, the
277+
// function IWbemLocator::ConnectServer will hang for 5 seconds, and there
278+
// is no way to specify the timeout. So we use an Event object to simulate
279+
// a timeout. The initEvent will be set after COM initialization, it will
280+
// take a longer time when first initialized. The connectEvent will be set
281+
// after connected to WMI.
282+
if (!err) {
283+
err = wait_event(data.initEvent, 1000);
284+
if (!err) {
285+
err = wait_event(data.connectEvent, 100);
286+
}
287+
}
288+
241289
while (!err) {
242290
if (ReadFile(
243291
data.readPipe,
@@ -259,28 +307,35 @@ _wmi_exec_query_impl(PyObject *module, PyObject *query)
259307
CloseHandle(data.readPipe);
260308
}
261309

262-
// Allow the thread some time to clean up
263-
switch (WaitForSingleObject(hThread, 1000)) {
264-
case WAIT_OBJECT_0:
265-
// Thread ended cleanly
266-
if (!GetExitCodeThread(hThread, (LPDWORD)&err)) {
267-
err = GetLastError();
268-
}
269-
break;
270-
case WAIT_TIMEOUT:
271-
// Probably stuck - there's not much we can do, unfortunately
272-
if (err == 0 || err == ERROR_BROKEN_PIPE) {
273-
err = WAIT_TIMEOUT;
310+
if (hThread) {
311+
// Allow the thread some time to clean up
312+
int thread_err;
313+
switch (WaitForSingleObject(hThread, 100)) {
314+
case WAIT_OBJECT_0:
315+
// Thread ended cleanly
316+
if (!GetExitCodeThread(hThread, (LPDWORD)&thread_err)) {
317+
thread_err = GetLastError();
318+
}
319+
break;
320+
case WAIT_TIMEOUT:
321+
// Probably stuck - there's not much we can do, unfortunately
322+
thread_err = WAIT_TIMEOUT;
323+
break;
324+
default:
325+
thread_err = GetLastError();
326+
break;
274327
}
275-
break;
276-
default:
328+
// An error on our side is more likely to be relevant than one from
329+
// the thread, but if we don't have one on our side we'll take theirs.
277330
if (err == 0 || err == ERROR_BROKEN_PIPE) {
278-
err = GetLastError();
331+
err = thread_err;
279332
}
280-
break;
333+
334+
CloseHandle(hThread);
281335
}
282336

283-
CloseHandle(hThread);
337+
CloseHandle(data.initEvent);
338+
CloseHandle(data.connectEvent);
284339
hThread = NULL;
285340

286341
Py_END_ALLOW_THREADS

0 commit comments

Comments
 (0)