1 #define _WIN32_WINNT _WIN32_WINNT_VISTA
2
3 #include "process.h"
4
5 #include <stdbool.h>
6 #include <stdlib.h>
7 #include <windows.h>
8
9 #include "error.h"
10 #include "macro.h"
11 #include "utf.h"
12
13 const HANDLE PROCESS_INVALID = INVALID_HANDLE_VALUE; // NOLINT
14
15 static const DWORD CREATION_FLAGS =
16 // Create each child process in a new process group so we don't send
17 // `CTRL-BREAK` signals to more than one child process in
18 // `process_terminate`.
19 CREATE_NEW_PROCESS_GROUP |
20 // Create each child process with a Unicode environment as we accept any
21 // UTF-16 encoded environment (including Unicode characters). Create each
22 CREATE_UNICODE_ENVIRONMENT |
23 // Create each child with an extended STARTUPINFOEXW structure so we can
24 // specify which handles should be inherited.
25 EXTENDED_STARTUPINFO_PRESENT;
26
27 // Argument escaping implementation is based on the following blog post:
28 // https://blogs.msdn.microsoft.com/twistylittlepassagesallalike/2011/04/23/everyone-quotes-command-line-arguments-the-wrong-way/
29
argument_should_escape(const char * argument)30 static bool argument_should_escape(const char *argument)
31 {
32 ASSERT(argument);
33
34 bool should_escape = false;
35
36 for (size_t i = 0; i < strlen(argument); i++) {
37 should_escape = should_escape || argument[i] == ' ' ||
38 argument[i] == '\t' || argument[i] == '\n' ||
39 argument[i] == '\v' || argument[i] == '\"';
40 }
41
42 return should_escape;
43 }
44
argument_escaped_size(const char * argument)45 static size_t argument_escaped_size(const char *argument)
46 {
47 ASSERT(argument);
48
49 size_t argument_size = strlen(argument);
50
51 if (!argument_should_escape(argument)) {
52 return argument_size;
53 }
54
55 size_t size = 2; // double quotes
56
57 for (size_t i = 0; i < argument_size; i++) {
58 size_t num_backslashes = 0;
59
60 while (i < argument_size && argument[i] == '\\') {
61 i++;
62 num_backslashes++;
63 }
64
65 if (i == argument_size) {
66 size += num_backslashes * 2;
67 } else if (argument[i] == '"') {
68 size += num_backslashes * 2 + 2;
69 } else {
70 size += num_backslashes + 1;
71 }
72 }
73
74 return size;
75 }
76
argument_escape(char * dest,const char * argument)77 static size_t argument_escape(char *dest, const char *argument)
78 {
79 ASSERT(dest);
80 ASSERT(argument);
81
82 size_t argument_size = strlen(argument);
83
84 if (!argument_should_escape(argument)) {
85 strcpy(dest, argument); // NOLINT
86 return argument_size;
87 }
88
89 const char *begin = dest;
90
91 *dest++ = '"';
92
93 for (size_t i = 0; i < argument_size; i++) {
94 size_t num_backslashes = 0;
95
96 while (i < argument_size && argument[i] == '\\') {
97 i++;
98 num_backslashes++;
99 }
100
101 if (i == argument_size) {
102 memset(dest, '\\', num_backslashes * 2);
103 dest += num_backslashes * 2;
104 } else if (argument[i] == '"') {
105 memset(dest, '\\', num_backslashes * 2 + 1);
106 dest += num_backslashes * 2 + 1;
107 *dest++ = '"';
108 } else {
109 memset(dest, '\\', num_backslashes);
110 dest += num_backslashes;
111 *dest++ = argument[i];
112 }
113 }
114
115 *dest++ = '"';
116
117 return (size_t)(dest - begin);
118 }
119
argv_join(const char * const * argv)120 static char *argv_join(const char *const *argv)
121 {
122 ASSERT(argv);
123
124 // Determine the size of the concatenated string first.
125 size_t joined_size = 1; // Count the NUL terminator.
126 for (int i = 0; argv[i] != NULL; i++) {
127 joined_size += argument_escaped_size(argv[i]);
128
129 if (argv[i + 1] != NULL) {
130 joined_size++; // Count whitespace.
131 }
132 }
133
134 char *joined = calloc(joined_size, sizeof(char));
135 if (joined == NULL) {
136 SetLastError(ERROR_NOT_ENOUGH_MEMORY);
137 return NULL;
138 }
139
140 char *current = joined;
141 for (int i = 0; argv[i] != NULL; i++) {
142 current += argument_escape(current, argv[i]);
143
144 // We add a space after each argument in the joined arguments string except
145 // for the final argument.
146 if (argv[i + 1] != NULL) {
147 *current++ = ' ';
148 }
149 }
150
151 *current = '\0';
152
153 return joined;
154 }
155
env_join_size(const char * const * env)156 static size_t env_join_size(const char *const *env)
157 {
158 ASSERT(env);
159
160 size_t joined_size = 1; // Count the NUL terminator.
161 for (int i = 0; env[i] != NULL; i++) {
162 joined_size += strlen(env[i]) + 1; // Count the NUL terminator.
163 }
164
165 return joined_size;
166 }
167
env_join(const char * const * env)168 static char *env_join(const char *const *env)
169 {
170 ASSERT(env);
171
172 char *joined = calloc(env_join_size(env), sizeof(char));
173 if (joined == NULL) {
174 SetLastError(ERROR_NOT_ENOUGH_MEMORY);
175 return NULL;
176 }
177
178 char *current = joined;
179 for (int i = 0; env[i] != NULL; i++) {
180 size_t to_copy = strlen(env[i]) + 1; // Include NUL terminator.
181 memcpy(current, env[i], to_copy);
182 current += to_copy;
183 }
184
185 *current = '\0';
186
187 return joined;
188 }
189
190 static const DWORD NUM_ATTRIBUTES = 1;
191
setup_attribute_list(HANDLE * handles,size_t num_handles)192 static LPPROC_THREAD_ATTRIBUTE_LIST setup_attribute_list(HANDLE *handles,
193 size_t num_handles)
194 {
195 ASSERT(handles);
196
197 int r = -1;
198
199 // Make sure all the given handles can be inherited.
200 for (size_t i = 0; i < num_handles; i++) {
201 r = SetHandleInformation(handles[i], HANDLE_FLAG_INHERIT,
202 HANDLE_FLAG_INHERIT);
203 if (r == 0) {
204 return NULL;
205 }
206 }
207
208 // Get the required size for `attribute_list`.
209 SIZE_T attribute_list_size = 0;
210 r = InitializeProcThreadAttributeList(NULL, NUM_ATTRIBUTES, 0,
211 &attribute_list_size);
212 if (r == 0 && GetLastError() != ERROR_INSUFFICIENT_BUFFER) {
213 return NULL;
214 }
215
216 LPPROC_THREAD_ATTRIBUTE_LIST attribute_list = malloc(attribute_list_size);
217 if (attribute_list == NULL) {
218 SetLastError(ERROR_NOT_ENOUGH_MEMORY);
219 return NULL;
220 }
221
222 r = InitializeProcThreadAttributeList(attribute_list, NUM_ATTRIBUTES, 0,
223 &attribute_list_size);
224 if (r == 0) {
225 free(attribute_list);
226 return NULL;
227 }
228
229 // Add the handles to be inherited to `attribute_list`.
230 r = UpdateProcThreadAttribute(attribute_list, 0,
231 PROC_THREAD_ATTRIBUTE_HANDLE_LIST, handles,
232 num_handles * sizeof(HANDLE), NULL, NULL);
233 if (r == 0) {
234 DeleteProcThreadAttributeList(attribute_list);
235 free(attribute_list);
236 return NULL;
237 }
238
239 return attribute_list;
240 }
241
242 #define NULSTR_FOREACH(i, l) \
243 for ((i) = (l); (i) && *(i) != L'\0'; (i) = wcschr((i), L'\0') + 1)
244
env_concat(const wchar_t * a,const wchar_t * b)245 static wchar_t *env_concat(const wchar_t *a, const wchar_t *b)
246 {
247 const wchar_t *i = NULL;
248 size_t size = 1;
249 wchar_t *c = NULL;
250
251 NULSTR_FOREACH(i, a) {
252 size += wcslen(i) + 1;
253 }
254
255 NULSTR_FOREACH(i, b) {
256 size += wcslen(i) + 1;
257 }
258
259 wchar_t *r = calloc(size, sizeof(wchar_t));
260 if (!r) {
261 return NULL;
262 }
263
264 c = r;
265
266 NULSTR_FOREACH(i, a) {
267 wcscpy(c, i);
268 c += wcslen(i) + 1;
269 }
270
271 NULSTR_FOREACH(i, b) {
272 wcscpy(c, i);
273 c += wcslen(i) + 1;
274 }
275
276 *c = L'\0';
277
278 return r;
279 }
280
env_setup(REPROC_ENV behavior,const char * const * extra)281 static wchar_t *env_setup(REPROC_ENV behavior, const char *const *extra)
282 {
283 wchar_t *env_parent_wstring = NULL;
284 char *env_extra = NULL;
285 wchar_t *env_extra_wstring = NULL;
286 wchar_t *env_wstring = NULL;
287
288 if (behavior == REPROC_ENV_EXTEND) {
289 env_parent_wstring = GetEnvironmentStringsW();
290 }
291
292 if (extra != NULL) {
293 env_extra = env_join(extra);
294 if (env_extra == NULL) {
295 goto finish;
296 }
297
298 size_t joined_size = env_join_size(extra);
299 ASSERT(joined_size <= INT_MAX);
300
301 env_extra_wstring = utf16_from_utf8(env_extra, (int) joined_size);
302 if (env_extra_wstring == NULL) {
303 goto finish;
304 }
305 }
306
307 env_wstring = env_concat(env_parent_wstring, env_extra_wstring);
308 if (env_wstring == NULL) {
309 goto finish;
310 }
311
312 finish:
313 FreeEnvironmentStringsW(env_parent_wstring);
314 free(env_extra);
315 free(env_extra_wstring);
316
317 return env_wstring;
318 }
319
process_start(HANDLE * process,const char * const * argv,struct process_options options)320 int process_start(HANDLE *process,
321 const char *const *argv,
322 struct process_options options)
323 {
324 ASSERT(process);
325
326 if (argv == NULL) {
327 return -ERROR_CALL_NOT_IMPLEMENTED;
328 }
329
330 ASSERT(argv[0] != NULL);
331
332 char *command_line = NULL;
333 wchar_t *command_line_wstring = NULL;
334 wchar_t *env_wstring = NULL;
335 wchar_t *working_directory_wstring = NULL;
336 LPPROC_THREAD_ATTRIBUTE_LIST attribute_list = NULL;
337 PROCESS_INFORMATION info = { PROCESS_INVALID, HANDLE_INVALID, 0, 0 };
338 int r = -1;
339
340 // Join `argv` to a whitespace delimited string as required by
341 // `CreateProcessW`.
342 command_line = argv_join(argv);
343 if (command_line == NULL) {
344 r = -(int) GetLastError();
345 goto finish;
346 }
347
348 // Convert UTF-8 to UTF-16 as required by `CreateProcessW`.
349 command_line_wstring = utf16_from_utf8(command_line, -1);
350 if (command_line_wstring == NULL) {
351 r = -(int) GetLastError();
352 goto finish;
353 }
354
355 // Idem for `working_directory` if it isn't `NULL`.
356 if (options.working_directory != NULL) {
357 working_directory_wstring = utf16_from_utf8(options.working_directory, -1);
358 if (working_directory_wstring == NULL) {
359 r = -(int) GetLastError();
360 goto finish;
361 }
362 }
363
364 env_wstring = env_setup(options.env.behavior, options.env.extra);
365 if (env_wstring == NULL) {
366 r = -(int) GetLastError();
367 goto finish;
368 }
369
370 // Windows Vista added the `STARTUPINFOEXW` structure in which we can put a
371 // list of handles that should be inherited. Only these handles are inherited
372 // by the child process. Other code in an application that calls
373 // `CreateProcess` without passing a `STARTUPINFOEXW` struct containing the
374 // handles it should inherit can still unintentionally inherit handles meant
375 // for a reproc child process. See https://stackoverflow.com/a/2345126 for
376 // more information.
377 HANDLE handles[] = { options.handle.exit, options.handle.in,
378 options.handle.out, options.handle.err };
379 size_t num_handles = ARRAY_SIZE(handles);
380
381 if (options.handle.out == options.handle.err) {
382 // CreateProcess doesn't like the same handle being specified twice in the
383 // `PROC_THREAD_ATTRIBUTE_HANDLE_LIST` attribute.
384 num_handles--;
385 }
386
387 attribute_list = setup_attribute_list(handles, num_handles);
388 if (attribute_list == NULL) {
389 r = -(int) GetLastError();
390 goto finish;
391 }
392
393 STARTUPINFOEXW extended_startup_info = {
394 .StartupInfo = { .cb = sizeof(extended_startup_info),
395 .dwFlags = STARTF_USESTDHANDLES | STARTF_USESHOWWINDOW,
396 // `STARTF_USESTDHANDLES`
397 .hStdInput = options.handle.in,
398 .hStdOutput = options.handle.out,
399 .hStdError = options.handle.err,
400 // `STARTF_USESHOWWINDOW`. Make sure the console window of
401 // the child process isn't visible. See
402 // https://github.com/DaanDeMeyer/reproc/issues/6 and
403 // https://github.com/DaanDeMeyer/reproc/pull/7 for more
404 // information.
405 .wShowWindow = SW_HIDE },
406 .lpAttributeList = attribute_list
407 };
408
409 LPSTARTUPINFOW startup_info_address = &extended_startup_info.StartupInfo;
410
411 // Child processes inherit the error mode of their parents. To avoid child
412 // processes creating error dialogs we set our error mode to not create error
413 // dialogs temporarily which is inherited by the child process.
414 DWORD previous_error_mode = SetErrorMode(SEM_NOGPFAULTERRORBOX);
415
416 SECURITY_ATTRIBUTES do_not_inherit = { .nLength = sizeof(SECURITY_ATTRIBUTES),
417 .bInheritHandle = false,
418 .lpSecurityDescriptor = NULL };
419
420 r = CreateProcessW(NULL, command_line_wstring, &do_not_inherit,
421 &do_not_inherit, true, CREATION_FLAGS, env_wstring,
422 working_directory_wstring, startup_info_address, &info);
423
424 SetErrorMode(previous_error_mode);
425
426 if (r == 0) {
427 r = -(int) GetLastError();
428 goto finish;
429 }
430
431 *process = info.hProcess;
432 r = 0;
433
434 finish:
435 free(command_line);
436 free(command_line_wstring);
437 free(env_wstring);
438 free(working_directory_wstring);
439 DeleteProcThreadAttributeList(attribute_list);
440 free(attribute_list);
441 handle_destroy(info.hThread);
442
443 return r < 0 ? r : 1;
444 }
445
process_pid(process_type process)446 int process_pid(process_type process)
447 {
448 ASSERT(process);
449 return (int) GetProcessId(process);
450 }
451
process_wait(HANDLE process)452 int process_wait(HANDLE process)
453 {
454 ASSERT(process);
455
456 int r = -1;
457
458 r = (int) WaitForSingleObject(process, INFINITE);
459 if ((DWORD) r == WAIT_FAILED) {
460 return -(int) GetLastError();
461 }
462
463 DWORD status = 0;
464 r = GetExitCodeProcess(process, &status);
465 if (r == 0) {
466 return -(int) GetLastError();
467 }
468
469 // `GenerateConsoleCtrlEvent` causes a process to exit with this exit code.
470 // Because `GenerateConsoleCtrlEvent` has roughly the same semantics as
471 // `SIGTERM`, we map its exit code to `SIGTERM`.
472 if (status == 3221225786) {
473 status = (DWORD) REPROC_SIGTERM;
474 }
475
476 return (int) status;
477 }
478
process_terminate(HANDLE process)479 int process_terminate(HANDLE process)
480 {
481 ASSERT(process && process != PROCESS_INVALID);
482
483 // `GenerateConsoleCtrlEvent` can only be called on a process group. To call
484 // `GenerateConsoleCtrlEvent` on a single child process it has to be put in
485 // its own process group (which we did when starting the child process).
486 BOOL r = GenerateConsoleCtrlEvent(CTRL_BREAK_EVENT, GetProcessId(process));
487
488 return r == 0 ? -(int) GetLastError() : 0;
489 }
490
process_kill(HANDLE process)491 int process_kill(HANDLE process)
492 {
493 ASSERT(process && process != PROCESS_INVALID);
494
495 // We use 137 (`SIGKILL`) as the exit status because it is the same exit
496 // status as a process that is stopped with the `SIGKILL` signal on POSIX
497 // systems.
498 BOOL r = TerminateProcess(process, (DWORD) REPROC_SIGKILL);
499
500 return r == 0 ? -(int) GetLastError() : 0;
501 }
502
process_destroy(HANDLE process)503 HANDLE process_destroy(HANDLE process)
504 {
505 return handle_destroy(process);
506 }
507