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