FreeRDP
Loading...
Searching...
No Matches
TestThreadCreateProcess.c
1
2#include <stdio.h>
3#include <stdlib.h>
4#include <string.h>
5#include <winpr/crt.h>
6#include <winpr/tchar.h>
7#include <winpr/synch.h>
8#include <winpr/thread.h>
9#include <winpr/environment.h>
10#include <winpr/pipe.h>
11#include <winpr/library.h>
12
13#ifdef _WIN32
14#include <windows.h>
15#else
16#include <unistd.h>
17/* whitebox: a WinPR HANDLE for a pipe is a pointer to a heap object that does not survive
18 * exec() (the address space is replaced), so a *raw fd* - not the HANDLE value - is what needs
19 * to be handed to a re-exec'd child for the inheritance probe below. winpr_Handle_getFd() is
20 * WinPR-internal (not part of the public API) but this test lives in the same source tree and
21 * is deliberately testing that internal fd-inheritance behavior, so reaching in here is fair
22 * game. */
23#include "../../handle/handle.h"
24#endif
25
26#define TESTENV_A "HELLO=WORLD"
27#define TESTENV_T _T(TESTENV_A)
28
29#ifdef _WIN32
30WINPR_ATTR_NODISCARD
31static unsigned long long handle_to_probe_value(HANDLE h)
32{
33 return (unsigned long long)(UINT_PTR)h;
34}
35#else
36WINPR_ATTR_NODISCARD
37static unsigned long long handle_to_probe_value(HANDLE h)
38{
39 return (unsigned long long)winpr_Handle_getFd(h);
40}
41#endif
42
43/* Child-mode entry point for the handle-inheritance tests below (see run_inherit_case()): the
44 * parent re-execs this same test binary with "--probe-handle <value>", where <value> identifies
45 * the handle/fd under test. This process tries to write to it and reports OPEN/CLOSED on its own
46 * stdout - which is always wired via STARTF_USESTDHANDLES regardless of the inheritance logic
47 * under test - so the parent can read the result back. */
48WINPR_ATTR_NODISCARD
49static int probe_handle_and_report(const char* valueStr)
50{
51 BOOL ok = FALSE;
52
53#ifdef _WIN32
54 HANDLE h = (HANDLE)(UINT_PTR)strtoull(valueStr, nullptr, 10);
55 DWORD written = 0;
56 ok = WriteFile(h, "PING", 4, &written, nullptr) && (written == 4);
57#else
58 const long fd = strtol(valueStr, nullptr, 0);
59 if ((fd >= INT32_MIN) || (fd <= INT32_MAX))
60 ok = (write(WINPR_ASSERTING_INT_CAST(int, fd), "PING", 4) == 4);
61#endif
62
63 printf(ok ? "OPEN\n" : "CLOSED\n");
64 (void)fflush(stdout);
65 return 0;
66}
67
68typedef enum
69{
70 MODE_NO_INHERIT, /* bInheritHandles = FALSE */
71 MODE_INHERIT_NO_LIST, /* bInheritHandles = TRUE, no handle list: broad inherit */
72 MODE_HANDLE_LIST_WITH_PROBE, /* bInheritHandles = TRUE, list = [probe handle] */
73 MODE_HANDLE_LIST_WITHOUT_PROBE /* bInheritHandles = TRUE, list = [unrelated handle] */
74} InheritTestMode;
75
76/* Spawns a child (this same test binary, re-invoked in probe mode) and checks whether
77 * `probeHandle` survived into it, per `mode`, matching it against `expectOpen`. This exercises
78 * the CreateProcess() code path end to end - so it validates identical behavior on Linux, macOS
79 * (both going through winpr/libwinpr/thread/process.c) and Windows (going through the real Win32
80 * CreateProcess, which this is a conformance check of). */
81WINPR_ATTR_NODISCARD
82static int run_inherit_case(const char* exePath, const char* label, HANDLE probeHandle,
83 InheritTestMode mode, BOOL expectOpen)
84{
85 SECURITY_ATTRIBUTES saAttr = { sizeof(SECURITY_ATTRIBUTES), nullptr, TRUE };
86 HANDLE outRead = nullptr;
87 HANDLE outWrite = nullptr;
88 int result = 1;
89
90 if (!CreatePipe(&outRead, &outWrite, &saAttr, 0))
91 {
92 printf("[%s] CreatePipe(out) failed\n", label);
93 return 1;
94 }
95
96 char cmdline[1024] = WINPR_C_ARRAY_INIT;
97 (void)snprintf(cmdline, sizeof(cmdline), "\"%s\" TestThreadCreateProcess --probe-handle %llu",
98 exePath, handle_to_probe_value(probeHandle));
99
100 const BOOL bInheritHandles = (mode != MODE_NO_INHERIT);
101 const BOOL useExtended =
102 (mode == MODE_HANDLE_LIST_WITH_PROBE) || (mode == MODE_HANDLE_LIST_WITHOUT_PROBE);
103
104 PROCESS_INFORMATION pi = WINPR_C_ARRAY_INIT;
105 BOOL created = FALSE;
106 LPPROC_THREAD_ATTRIBUTE_LIST attrList = nullptr;
107 STARTUPINFOEXA siEx = WINPR_C_ARRAY_INIT;
108 STARTUPINFOA si = WINPR_C_ARRAY_INIT;
109
110 STARTUPINFOA* siPtr = &si;
111 DWORD createFlags = 0;
112 HANDLE handles[2] = { outWrite, probeHandle };
113
114 if (useExtended)
115 {
116 SIZE_T size = 0;
117 (void)InitializeProcThreadAttributeList(nullptr, 1, 0, &size);
118 attrList = (LPPROC_THREAD_ATTRIBUTE_LIST)malloc(size);
119 if (!attrList || !InitializeProcThreadAttributeList(attrList, 1, 0, &size))
120 {
121 printf("[%s] InitializeProcThreadAttributeList failed\n", label);
122 free(attrList);
123 CloseHandle(outRead);
124 CloseHandle(outWrite);
125 return 1;
126 }
127
128 /* real Windows treats the handle list as exclusive even for hStdOutput/hStdError: if
129 * outWrite isn't in it too, the child wouldn't get a usable stdout handle at all, and
130 * this test's own OPEN/CLOSED result-capture mechanism would break. Only the probe
131 * handle's presence is what actually varies between the two list-based cases. */
132 const size_t handleCount = (mode == MODE_HANDLE_LIST_WITH_PROBE) ? 2 : 1;
133 if (!UpdateProcThreadAttribute(attrList, 0, PROC_THREAD_ATTRIBUTE_HANDLE_LIST,
134 (void*)handles, handleCount * sizeof(HANDLE), nullptr,
135 nullptr))
136 {
137 printf("[%s] UpdateProcThreadAttribute failed\n", label);
138 DeleteProcThreadAttributeList(attrList);
139 free(attrList);
140 CloseHandle(outRead);
141 CloseHandle(outWrite);
142 return 1;
143 }
144
145 siEx.StartupInfo.cb = sizeof(siEx);
146 siEx.StartupInfo.dwFlags = STARTF_USESTDHANDLES;
147 siEx.StartupInfo.hStdOutput = outWrite;
148 siEx.StartupInfo.hStdError = outWrite;
149 siEx.lpAttributeList = attrList;
150
151 siPtr = (LPSTARTUPINFOA)&siEx;
152 createFlags = EXTENDED_STARTUPINFO_PRESENT;
153 }
154 else
155 {
156 si.cb = sizeof(si);
157 si.dwFlags = STARTF_USESTDHANDLES;
158 si.hStdOutput = outWrite;
159 si.hStdError = outWrite;
160 }
161
162 created = CreateProcessA(nullptr, cmdline, nullptr, nullptr, bInheritHandles, createFlags,
163 nullptr, nullptr, siPtr, &pi);
164
165 if (attrList)
166 {
167 DeleteProcThreadAttributeList(attrList);
168 free(attrList);
169 }
170
171 CloseHandle(outWrite);
172
173 if (!created)
174 {
175 printf("[%s] CreateProcessA failed, error=%" PRIu32 "\n", label, GetLastError());
176 CloseHandle(outRead);
177 return 1;
178 }
179
180 if (WaitForSingleObject(pi.hProcess, 5000) != WAIT_OBJECT_0)
181 {
182 printf("[%s] child did not exit in time\n", label);
183 }
184 else
185 {
186 char buf[64] = WINPR_C_ARRAY_INIT;
187 DWORD read_bytes = 0;
188 (void)ReadFile(outRead, buf, sizeof(buf) - 1, &read_bytes, nullptr);
189
190 const BOOL open = strstr(buf, "OPEN") != nullptr;
191 result = (open != expectOpen);
192 printf("[%s] expected %s, got '%s' -> %s\n", label, expectOpen ? "OPEN" : "CLOSED", buf,
193 result ? "FAIL" : "OK");
194 }
195
196 CloseHandle(outRead);
197 CloseHandle(pi.hProcess);
198 CloseHandle(pi.hThread);
199 return result;
200}
201
202/* Regression test for a TOCTOU: PROC_THREAD_ATTRIBUTE_HANDLE_LIST only stores the raw HANDLE
203 * values handed to UpdateProcThreadAttribute() (matching real Windows' documented contract - the
204 * caller's buffer, not its individual HANDLEs' underlying objects, is what must survive). Nothing
205 * used to stop a caller from closing one of the *listed* HANDLEs before CreateProcess() actually
206 * reads the list, and the WINPR_HANDLE struct it points to could then be freed and reused before
207 * CreateProcessExA() got around to reading it via the (now stale) pointer stored in the list.
208 *
209 * CloseHandle() now always runs the handle's real close op (releasing its OS resource, e.g. the
210 * fd) immediately, but only frees the WINPR_HANDLE struct once every reference to it (including
211 * UpdateProcThreadAttribute()'s own, released by DeleteProcThreadAttributeList()) has been
212 * released - until then it's converted to a harmless placeholder (see winpr_Handle_ConvertToNone
213 * in nonehandle.c). So this must not crash or read freed memory - and, matching confirmed real
214 * Windows behavior, CreateProcess() must fail outright with ERROR_INVALID_PARAMETER rather than
215 * silently proceeding without the already-closed handle (see the pre-fork validation loop in
216 * process.c). */
217WINPR_ATTR_NODISCARD
218static int TestHandleListEarlyCloseIsSafe(const char* exePath)
219{
220 SECURITY_ATTRIBUTES saAttr = { sizeof(SECURITY_ATTRIBUTES), nullptr, TRUE };
221 HANDLE probeRead = nullptr;
222 HANDLE probeWrite = nullptr;
223 HANDLE outRead = nullptr;
224 HANDLE outWrite = nullptr;
225 int result = 1;
226
227 if (!CreatePipe(&probeRead, &probeWrite, &saAttr, 0))
228 {
229 printf("[early-close] CreatePipe(probe) failed\n");
230 return 1;
231 }
232 if (!CreatePipe(&outRead, &outWrite, &saAttr, 0))
233 {
234 printf("[early-close] CreatePipe(out) failed\n");
235 CloseHandle(probeRead);
236 CloseHandle(probeWrite);
237 return 1;
238 }
239
240 char cmdline[1024] = WINPR_C_ARRAY_INIT;
241 (void)snprintf(cmdline, sizeof(cmdline), "\"%s\" TestThreadCreateProcess --probe-handle %llu",
242 exePath, handle_to_probe_value(probeWrite));
243
244 SIZE_T size = 0;
245 (void)InitializeProcThreadAttributeList(nullptr, 1, 0, &size);
246 LPPROC_THREAD_ATTRIBUTE_LIST attrList = (LPPROC_THREAD_ATTRIBUTE_LIST)malloc(size);
247 if (!attrList || !InitializeProcThreadAttributeList(attrList, 1, 0, &size))
248 {
249 printf("[early-close] InitializeProcThreadAttributeList failed\n");
250 free(attrList);
251 CloseHandle(probeRead);
252 CloseHandle(probeWrite);
253 CloseHandle(outRead);
254 CloseHandle(outWrite);
255 return 1;
256 }
257
258 HANDLE handles[2] = { outWrite, probeWrite };
259 if (!UpdateProcThreadAttribute(attrList, 0, PROC_THREAD_ATTRIBUTE_HANDLE_LIST, (void*)handles,
260 sizeof(handles), nullptr, nullptr))
261 {
262 printf("[early-close] UpdateProcThreadAttribute failed\n");
263 DeleteProcThreadAttributeList(attrList);
264 free(attrList);
265 CloseHandle(probeRead);
266 CloseHandle(probeWrite);
267 CloseHandle(outRead);
268 CloseHandle(outWrite);
269 return 1;
270 }
271
272 /* the TOCTOU: close our own reference to probeWrite *before* CreateProcess() reads the
273 * attribute list. The list's own reference (taken by UpdateProcThreadAttribute() above) must
274 * be what keeps the underlying object alive from here on. */
275 CloseHandle(probeWrite);
276
277 STARTUPINFOEXA siEx = WINPR_C_ARRAY_INIT;
278 siEx.StartupInfo.cb = sizeof(siEx);
279 siEx.StartupInfo.dwFlags = STARTF_USESTDHANDLES;
280 siEx.StartupInfo.hStdOutput = outWrite;
281 siEx.StartupInfo.hStdError = outWrite;
282 siEx.lpAttributeList = attrList;
283
284 PROCESS_INFORMATION pi = WINPR_C_ARRAY_INIT;
285 const BOOL created =
286 CreateProcessA(nullptr, cmdline, nullptr, nullptr, TRUE, EXTENDED_STARTUPINFO_PRESENT,
287 nullptr, nullptr, (LPSTARTUPINFOA)&siEx, &pi);
288 const DWORD lastError = GetLastError();
289
290 DeleteProcThreadAttributeList(attrList);
291 free(attrList);
292 CloseHandle(outWrite);
293 CloseHandle(probeRead);
294 CloseHandle(outRead);
295
296 if (created)
297 {
298 printf("[early-close] expected CreateProcessA to fail (a listed handle was already "
299 "closed), but it succeeded -> FAIL\n");
300 CloseHandle(pi.hProcess);
301 CloseHandle(pi.hThread);
302 return 1;
303 }
304
305 result = (lastError != ERROR_INVALID_PARAMETER);
306 printf("[early-close] expected CreateProcessA to fail with ERROR_INVALID_PARAMETER, got "
307 "error=%" PRIu32 " -> %s\n",
308 lastError, result ? "FAIL" : "OK");
309 return result;
310}
311
312/* Covers the bInheritHandles / PROC_THREAD_ATTRIBUTE_HANDLE_LIST matrix documented for
313 * CreateProcess() on real Windows, which winpr/libwinpr/thread/process.c replicates on
314 * Linux/macOS:
315 * - bInheritHandles=FALSE: nothing inherits, even a marked-inheritable handle is closed.
316 * - bInheritHandles=TRUE, no handle list: every inheritable handle is inherited.
317 * - bInheritHandles=TRUE, handle list present: only the listed handles are inherited, even
318 * other inheritable handles are not. */
319WINPR_ATTR_NODISCARD
320static int TestHandleInheritance(void)
321{
322 char exePath[4096] = WINPR_C_ARRAY_INIT;
323 if (GetModuleFileNameA(nullptr, exePath, sizeof(exePath)) == 0)
324 {
325 printf("GetModuleFileNameA failed\n");
326 return 1;
327 }
328
329 SECURITY_ATTRIBUTES saAttr = { sizeof(SECURITY_ATTRIBUTES), nullptr, TRUE };
330 HANDLE probeRead = nullptr;
331 HANDLE probeWrite = nullptr;
332 if (!CreatePipe(&probeRead, &probeWrite, &saAttr, 0))
333 {
334 printf("CreatePipe(probe) failed\n");
335 return 1;
336 }
337
338 int rc = 0;
339 rc |= run_inherit_case(exePath, "bInheritHandles=FALSE", probeWrite, MODE_NO_INHERIT, FALSE);
340 rc |= run_inherit_case(exePath, "bInheritHandles=TRUE, no list", probeWrite,
341 MODE_INHERIT_NO_LIST, TRUE);
342 rc |= run_inherit_case(exePath, "handle list CONTAINS probe", probeWrite,
343 MODE_HANDLE_LIST_WITH_PROBE, TRUE);
344 rc |= run_inherit_case(exePath, "handle list does NOT contain probe", probeWrite,
345 MODE_HANDLE_LIST_WITHOUT_PROBE, FALSE);
346
347 CloseHandle(probeRead);
348 CloseHandle(probeWrite);
349
350 rc |= TestHandleListEarlyCloseIsSafe(exePath);
351 return rc;
352}
353
354int TestThreadCreateProcess(int argc, char* argv[])
355{
356 if ((argc >= 3) && (strcmp(argv[1], "--probe-handle") == 0))
357 return probe_handle_and_report(argv[2]);
358
359 BOOL status = 0;
360 DWORD exitCode = 0;
361 LPCTSTR lpApplicationName = nullptr;
362
363#ifdef _WIN32
364 TCHAR lpCommandLine[200] = _T("cmd /C set");
365#else
366 TCHAR lpCommandLine[200] = _T("printenv");
367#endif
368
369 // LPTSTR lpCommandLine;
370 LPSECURITY_ATTRIBUTES lpProcessAttributes = nullptr;
371 LPSECURITY_ATTRIBUTES lpThreadAttributes = nullptr;
372 BOOL bInheritHandles = 0;
373 DWORD dwCreationFlags = 0;
374 LPVOID lpEnvironment = nullptr;
375 LPCTSTR lpCurrentDirectory = nullptr;
376 STARTUPINFO StartupInfo = WINPR_C_ARRAY_INIT;
377 PROCESS_INFORMATION ProcessInformation = WINPR_C_ARRAY_INIT;
378 LPTCH lpszEnvironmentBlock = nullptr;
379 HANDLE pipe_read = nullptr;
380 HANDLE pipe_write = nullptr;
381 char buf[1024] = WINPR_C_ARRAY_INIT;
382 DWORD read_bytes = 0;
383 int ret = 0;
384 SECURITY_ATTRIBUTES saAttr;
385
386 WINPR_UNUSED(argc);
387 WINPR_UNUSED(argv);
388
389 lpszEnvironmentBlock = GetEnvironmentStrings();
390
391 lpApplicationName = nullptr;
392
393 lpProcessAttributes = nullptr;
394 lpThreadAttributes = nullptr;
395 bInheritHandles = FALSE;
396 dwCreationFlags = 0;
397#ifdef _UNICODE
398 dwCreationFlags |= CREATE_UNICODE_ENVIRONMENT;
399#endif
400 lpEnvironment = lpszEnvironmentBlock;
401 lpCurrentDirectory = nullptr;
402 StartupInfo.cb = sizeof(STARTUPINFO);
403
404 status = CreateProcess(lpApplicationName, lpCommandLine, lpProcessAttributes,
405 lpThreadAttributes, bInheritHandles, dwCreationFlags, lpEnvironment,
406 lpCurrentDirectory, &StartupInfo, &ProcessInformation);
407
408 if (!status)
409 {
410 printf("CreateProcess failed. error=%" PRIu32 "\n", GetLastError());
411 return 1;
412 }
413
414 if (WaitForSingleObject(ProcessInformation.hProcess, 5000) != WAIT_OBJECT_0)
415 {
416 printf("Failed to wait for first process. error=%" PRIu32 "\n", GetLastError());
417 return 1;
418 }
419
420 exitCode = 0;
421 status = GetExitCodeProcess(ProcessInformation.hProcess, &exitCode);
422
423 printf("GetExitCodeProcess status: %" PRId32 "\n", status);
424 printf("Process exited with code: 0x%08" PRIX32 "\n", exitCode);
425
426 (void)CloseHandle(ProcessInformation.hProcess);
427 (void)CloseHandle(ProcessInformation.hThread);
428 FreeEnvironmentStrings(lpszEnvironmentBlock);
429
430 /* Test stdin,stdout,stderr redirection */
431
432 saAttr.nLength = sizeof(SECURITY_ATTRIBUTES);
433 saAttr.bInheritHandle = TRUE;
434 saAttr.lpSecurityDescriptor = nullptr;
435
436 if (!CreatePipe(&pipe_read, &pipe_write, &saAttr, 0))
437 {
438 printf("Pipe creation failed. error=%" PRIu32 "\n", GetLastError());
439 return 1;
440 }
441
442 bInheritHandles = TRUE;
443
444 ZeroMemory(&StartupInfo, sizeof(STARTUPINFO));
445 StartupInfo.cb = sizeof(STARTUPINFO);
446 StartupInfo.hStdOutput = pipe_write;
447 StartupInfo.hStdError = pipe_write;
448 StartupInfo.dwFlags = STARTF_USESTDHANDLES;
449
450 ZeroMemory(&ProcessInformation, sizeof(PROCESS_INFORMATION));
451
452 if (!(lpEnvironment = calloc(1, sizeof(TESTENV_T) + sizeof(TCHAR))))
453 {
454 printf("Failed to allocate environment buffer. error=%" PRIu32 "\n", GetLastError());
455 return 1;
456 }
457 memcpy(lpEnvironment, (void*)TESTENV_T, sizeof(TESTENV_T));
458
459 status = CreateProcess(lpApplicationName, lpCommandLine, lpProcessAttributes,
460 lpThreadAttributes, bInheritHandles, dwCreationFlags, lpEnvironment,
461 lpCurrentDirectory, &StartupInfo, &ProcessInformation);
462
463 free(lpEnvironment);
464
465 if (!status)
466 {
467 (void)CloseHandle(pipe_read);
468 (void)CloseHandle(pipe_write);
469 printf("CreateProcess failed. error=%" PRIu32 "\n", GetLastError());
470 return 1;
471 }
472
473 if (WaitForSingleObject(ProcessInformation.hProcess, 5000) != WAIT_OBJECT_0)
474 {
475 printf("Failed to wait for second process. error=%" PRIu32 "\n", GetLastError());
476 return 1;
477 }
478
479 ZeroMemory(buf, sizeof(buf));
480 ReadFile(pipe_read, buf, sizeof(buf) - 1, &read_bytes, nullptr);
481 if (!strstr((const char*)buf, TESTENV_A))
482 {
483 printf("No or unexpected data read from pipe\n");
484 ret = 1;
485 }
486
487 (void)CloseHandle(pipe_read);
488 (void)CloseHandle(pipe_write);
489
490 exitCode = 0;
491 status = GetExitCodeProcess(ProcessInformation.hProcess, &exitCode);
492
493 printf("GetExitCodeProcess status: %" PRId32 "\n", status);
494 printf("Process exited with code: 0x%08" PRIX32 "\n", exitCode);
495
496 (void)CloseHandle(ProcessInformation.hProcess);
497 (void)CloseHandle(ProcessInformation.hThread);
498
499 if (ret == 0)
500 ret = TestHandleInheritance();
501
502 return ret;
503}