Skip to content

Commit 26c051c

Browse files
committed
Spoof PID across execve() on Windows
It's now possible with cosmo and redbean, to deliver a signal to a child process after it has called execve(). However the executed program needs to be compiled using cosmocc. The cosmo runtime WinMain() implementation now intercepts a _COSMO_PID environment variable that's set by execve(). It ensures the child process will use the same C:\ProgramData\cosmo\sigs file, which is where kill() will place the delivered signal. We are able to do this on Windows even better than NetBSD, which has a bug with this Fixes #1334
1 parent 9cc1bd0 commit 26c051c

File tree

8 files changed

+187
-21
lines changed

8 files changed

+187
-21
lines changed

libc/intrin/sig.c

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -667,6 +667,9 @@ textwindows int __sig_check(void) {
667667
return res;
668668
}
669669

670+
// this mutex is needed so execve() can shut down the signal worker
671+
pthread_mutex_t __sig_worker_lock;
672+
670673
// background thread for delivering inter-process signals asynchronously
671674
// this checks for undelivered process-wide signals, once per scheduling
672675
// quantum, which on windows should be every ~15ms or so, unless somehow
@@ -680,6 +683,7 @@ textwindows dontinstrument static uint32_t __sig_worker(void *arg) {
680683
__maps_track((char *)(((uintptr_t)sp + __pagesize - 1) & -__pagesize) - STKSZ,
681684
STKSZ);
682685
for (;;) {
686+
pthread_mutex_lock(&__sig_worker_lock);
683687

684688
// dequeue all pending signals and fire them off. if there's no
685689
// thread that can handle them then __sig_generate will requeue
@@ -724,6 +728,7 @@ textwindows dontinstrument static uint32_t __sig_worker(void *arg) {
724728
_pthread_unlock();
725729

726730
// wait until next scheduler quantum
731+
pthread_mutex_unlock(&__sig_worker_lock);
727732
Sleep(POLL_INTERVAL_MS);
728733
}
729734
return 0;

libc/intrin/terminatethisprocess.c

Lines changed: 0 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -18,7 +18,6 @@
1818
╚─────────────────────────────────────────────────────────────────────────────*/
1919
#include "libc/atomic.h"
2020
#include "libc/calls/sig.internal.h"
21-
#include "libc/intrin/kprintf.h"
2221
#include "libc/limits.h"
2322
#include "libc/nt/files.h"
2423
#include "libc/nt/memory.h"

libc/proc/execve-nt.greg.c

Lines changed: 34 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -17,13 +17,14 @@
1717
│ PERFORMANCE OF THIS SOFTWARE. │
1818
╚─────────────────────────────────────────────────────────────────────────────*/
1919
#include "libc/assert.h"
20+
#include "libc/calls/calls.h"
2021
#include "libc/calls/internal.h"
22+
#include "libc/calls/sig.internal.h"
2123
#include "libc/calls/struct/sigset.internal.h"
2224
#include "libc/calls/syscall-nt.internal.h"
2325
#include "libc/errno.h"
2426
#include "libc/fmt/itoa.h"
2527
#include "libc/intrin/fds.h"
26-
#include "libc/intrin/kprintf.h"
2728
#include "libc/mem/mem.h"
2829
#include "libc/nt/enum/processaccess.h"
2930
#include "libc/nt/enum/startf.h"
@@ -33,8 +34,10 @@
3334
#include "libc/nt/runtime.h"
3435
#include "libc/nt/struct/processinformation.h"
3536
#include "libc/nt/struct/startupinfo.h"
37+
#include "libc/nt/thunk/msabi.h"
3638
#include "libc/proc/describefds.internal.h"
3739
#include "libc/proc/ntspawn.h"
40+
#include "libc/runtime/internal.h"
3841
#include "libc/str/str.h"
3942
#include "libc/sysv/consts/at.h"
4043
#include "libc/sysv/consts/o.h"
@@ -43,23 +46,37 @@
4346
#include "libc/thread/thread.h"
4447
#ifdef __x86_64__
4548

49+
__msabi extern typeof(TerminateProcess) *const __imp_TerminateProcess;
50+
51+
extern pthread_mutex_t __sig_worker_lock;
52+
53+
static void sys_execve_nt_abort(sigset_t sigmask) {
54+
_pthread_unlock();
55+
pthread_mutex_unlock(&__sig_worker_lock);
56+
__sig_unblock(sigmask);
57+
}
58+
4659
textwindows int sys_execve_nt(const char *program, char *const argv[],
4760
char *const envp[]) {
4861

4962
// execve() needs to be @asyncsignalsafe
5063
sigset_t sigmask = __sig_block();
51-
_pthread_lock();
64+
pthread_mutex_lock(&__sig_worker_lock); // order matters
65+
_pthread_lock(); // order matters
5266

5367
// new process should be a child of our parent
5468
int64_t hParentProcess;
5569
int ppid = sys_getppid_nt();
5670
if (!(hParentProcess = OpenProcess(
5771
kNtProcessDupHandle | kNtProcessCreateProcess, false, ppid))) {
58-
_pthread_unlock();
59-
__sig_unblock(sigmask);
72+
sys_execve_nt_abort(sigmask);
6073
return -1;
6174
}
6275

76+
// inherit pid
77+
char pidvar[11 + 21];
78+
FormatUint64(stpcpy(pidvar, "_COSMO_PID="), __pid);
79+
6380
// inherit signal mask
6481
char maskvar[6 + 21];
6582
FormatUint64(stpcpy(maskvar, "_MASK="), sigmask);
@@ -84,22 +101,26 @@ textwindows int sys_execve_nt(const char *program, char *const argv[],
84101
if (!(fdspec = __describe_fds(g_fds.p, g_fds.n, &si, hParentProcess,
85102
&lpExplicitHandles, &dwExplicitHandleCount))) {
86103
CloseHandle(hParentProcess);
87-
_pthread_unlock();
88-
__sig_unblock(sigmask);
104+
sys_execve_nt_abort(sigmask);
89105
return -1;
90106
}
91107

108+
// inherit pending signals
109+
atomic_fetch_or_explicit(
110+
__sig.process,
111+
atomic_load_explicit(&__get_tls()->tib_sigpending, memory_order_acquire),
112+
memory_order_release);
113+
92114
// launch the process
93115
struct NtProcessInformation pi;
94116
int rc = ntspawn(&(struct NtSpawnArgs){
95-
AT_FDCWD, program, argv, envp, (char *[]){fdspec, maskvar, 0}, 0, 0,
96-
hParentProcess, lpExplicitHandles, dwExplicitHandleCount, &si, &pi});
117+
AT_FDCWD, program, argv, envp, (char *[]){fdspec, maskvar, pidvar, 0}, 0,
118+
0, hParentProcess, lpExplicitHandles, dwExplicitHandleCount, &si, &pi});
97119
__undescribe_fds(hParentProcess, lpExplicitHandles, dwExplicitHandleCount);
98120
if (rc == -1) {
99121
free(fdspec);
100122
CloseHandle(hParentProcess);
101-
_pthread_unlock();
102-
__sig_unblock(sigmask);
123+
sys_execve_nt_abort(sigmask);
103124
if (GetLastError() == kNtErrorSharingViolation) {
104125
return etxtbsy();
105126
} else {
@@ -112,12 +133,13 @@ textwindows int sys_execve_nt(const char *program, char *const argv[],
112133
if (DuplicateHandle(GetCurrentProcess(), pi.hProcess, hParentProcess, &handle,
113134
0, false, kNtDuplicateSameAccess)) {
114135
unassert(!(handle & 0xFFFFFFFFFF000000));
115-
TerminateThisProcess(0x23000000u | handle);
136+
__imp_TerminateProcess(-1, 0x23000000u | handle);
116137
} else {
117138
// TODO(jart): Why does `make loc` print this?
118139
// kprintf("DuplicateHandle failed w/ %d\n", GetLastError());
119-
TerminateThisProcess(ECHILD);
140+
__imp_TerminateProcess(-1, ECHILD);
120141
}
142+
__builtin_unreachable();
121143
}
122144

123145
#endif /* __x86_64__ */

libc/proc/execve.c

Lines changed: 45 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -36,14 +36,55 @@
3636
/**
3737
* Replaces current process with program.
3838
*
39+
* Your `prog` may be an actually portable executable or a platform
40+
* native binary (e.g. ELF, Mach-O, PE). On UNIX systems, your execve
41+
* implementation will try to find where the `ape` interpreter program
42+
* is installed on your system. The preferred location is `/usr/bin/ape`
43+
* except on Apple Silicon where it's `/usr/local/bin/ape`. The $TMPDIR
44+
* and $HOME locations that the APE shell script extracts the versioned
45+
* ape binaries to will also be checked as a fallback path. Finally, if
46+
* `prog` isn't an executable in any recognizable format, cosmo assumes
47+
* it's a bourne shell script and launches it under /bin/sh.
48+
*
49+
* The signal mask and pending signals are inherited by the new process.
50+
* Note the NetBSD kernel has a bug where pending signals are cleared.
51+
*
52+
* File descriptors that haven't been marked `O_CLOEXEC` through various
53+
* devices such as open() and fcntl() will be inherited by the executed
54+
* subprocess. The current file position of the duplicated descriptors
55+
* is shared across processes. On Windows, `prog` needs to be built by
56+
* cosmocc in order to properly inherit file descriptors. If a program
57+
* compiled by MSVC or Cygwin is launched instead, then only the stdio
58+
* file descriptors can be passed along.
59+
*
3960
* On Windows, `argv` and `envp` can't contain binary strings. They need
4061
* to be valid UTF-8 in order to round-trip the WIN32 API, without being
4162
* corrupted.
4263
*
43-
* On Windows, only file descriptors 0, 1 and 2 can be passed to a child
44-
* process in such a way that allows them to be automatically discovered
45-
* when the child process initializes. Cosmpolitan currently treats your
46-
* other file descriptors as implicitly O_CLOEXEC.
64+
* On Windows, cosmo execve uses parent spoofing to implement the UNIX
65+
* behavior of replacing the current process. Since POSIX.1 also needs
66+
* us to maintain the same PID number too, the _COSMO_PID environemnt
67+
* variable is passed to the child process which specifies a spoofed
68+
* PID. Whatever is in that variable will be reported by getpid() and
69+
* other cosmo processes will be able to send signals to the process
70+
* using that pid, via kill(). These synthetic PIDs which are only
71+
* created by execve could potentially overlap with OS assignments if
72+
* Windows recycles them. Cosmo avoids that by tracking handles of
73+
* subprocesses. Each process has its own process manager thread, to
74+
* associate pids with win32 handles, and execve will tell the parent
75+
* process its new handle when it changes. However it's not perfect.
76+
* There's still situations where processes created by execve() can
77+
* cause surprising things to happen. For an alternative, consider
78+
* posix_spawn() which is fastest and awesomest across all OSes.
79+
*
80+
* On Windows, support is currently not implemented for inheriting
81+
* setitimer() and alarm() into an executed process.
82+
*
83+
* On Windows, support is currently not implemented for inheriting
84+
* getrusage() statistics into an executed process.
85+
*
86+
* The executed process will share the same terminal and current
87+
* directory.
4788
*
4889
* @param program will not be PATH searched, see commandv()
4990
* @param argv[0] is the name of the program to run

libc/proc/kill-nt.c

Lines changed: 6 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -92,6 +92,7 @@ textwindows int sys_kill_nt(int pid, int sig) {
9292
int64_t handle, closeme = 0;
9393
if (!(handle = __proc_handle(pid))) {
9494
if ((handle = OpenProcess(kNtProcessTerminate, false, pid))) {
95+
STRACE("warning: kill() using raw win32 pid");
9596
closeme = handle;
9697
} else {
9798
goto OnError;
@@ -103,7 +104,7 @@ textwindows int sys_kill_nt(int pid, int sig) {
103104
// now that we know the process exists, if it has a shared memory file
104105
// then we can be reasonably certain it's a cosmo process which should
105106
// be trusted to deliver its signal, unless it's a nine exterminations
106-
if (pid > 0 && sig != 9) {
107+
if (pid > 0) {
107108
atomic_ulong *sigproc;
108109
if ((sigproc = __sig_map_process(pid, kNtOpenExisting))) {
109110
if (sig > 0)
@@ -112,12 +113,15 @@ textwindows int sys_kill_nt(int pid, int sig) {
112113
UnmapViewOfFile(sigproc);
113114
if (closeme)
114115
CloseHandle(closeme);
115-
return 0;
116+
if (sig != 9)
117+
return 0;
116118
}
117119
}
118120

119121
// perform actual kill
120122
// process will report WIFSIGNALED with WTERMSIG(sig)
123+
if (sig != 9)
124+
STRACE("warning: kill() sending %G via terminate", sig);
121125
bool32 ok = TerminateProcess(handle, sig);
122126
if (closeme)
123127
CloseHandle(closeme);

libc/proc/kill.c

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -35,6 +35,9 @@
3535
* signal a cosmo process. The targeting process will then notice that a
3636
* signal has been added and delivers to any thread as soon as possible.
3737
*
38+
* On Windows, the only signal that's guaranteed to work on non-cosmocc
39+
* processes is SIGKILL.
40+
*
3841
* On Windows, the concept of a process group isn't fully implemented.
3942
* Saying `kill(0, sig)` will deliver `sig` to all direct descendent
4043
* processes. Saying `kill(-pid, sig)` will be the same as saying

libc/runtime/winmain.greg.c

Lines changed: 35 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -300,6 +300,37 @@ static abi wontreturn void WinInit(const char16_t *cmdline) {
300300
(uintptr_t)(stackaddr + (stacksize - sizeof(struct WinArgs))));
301301
}
302302

303+
static int Atoi(const char16_t *str) {
304+
int c;
305+
unsigned x = 0;
306+
while ((c = *str++)) {
307+
if ('0' <= c && c <= '9') {
308+
x *= 10;
309+
x += c - '0';
310+
} else {
311+
return -1;
312+
}
313+
}
314+
return x;
315+
}
316+
317+
static abi int WinGetPid(const char16_t *var, bool *out_is_inherited) {
318+
uint32_t len;
319+
char16_t val[12];
320+
if ((len = __imp_GetEnvironmentVariableW(var, val, ARRAYLEN(val)))) {
321+
int pid = -1;
322+
if (len < ARRAYLEN(val))
323+
pid = Atoi(val);
324+
__imp_SetEnvironmentVariableW(var, NULL);
325+
if (pid > 0) {
326+
*out_is_inherited = true;
327+
return pid;
328+
}
329+
}
330+
*out_is_inherited = false;
331+
return __imp_GetCurrentProcessId();
332+
}
333+
303334
abi int64_t WinMain(int64_t hInstance, int64_t hPrevInstance,
304335
const char *lpCmdLine, int64_t nCmdShow) {
305336
static atomic_ulong fake_process_signals;
@@ -316,10 +347,12 @@ abi int64_t WinMain(int64_t hInstance, int64_t hPrevInstance,
316347
__imp_GetSystemInfo(&si);
317348
__pagesize = si.dwPageSize;
318349
__gransize = si.dwAllocationGranularity;
319-
__pid = __imp_GetCurrentProcessId();
350+
bool pid_is_inherited;
351+
__pid = WinGetPid(u"_COSMO_PID", &pid_is_inherited);
320352
if (!(__sig.process = __sig_map_process(__pid, kNtOpenAlways)))
321353
__sig.process = &fake_process_signals;
322-
atomic_store_explicit(__sig.process, 0, memory_order_release);
354+
if (!pid_is_inherited)
355+
atomic_store_explicit(__sig.process, 0, memory_order_release);
323356
cmdline = __imp_GetCommandLineW();
324357
#if SYSDEBUG
325358
// sloppy flag-only check for early initialization
Lines changed: 59 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,59 @@
1+
// Copyright 2024 Justine Alexandra Roberts Tunney
2+
//
3+
// Permission to use, copy, modify, and/or distribute this software for
4+
// any purpose with or without fee is hereby granted, provided that the
5+
// above copyright notice and this permission notice appear in all copies.
6+
//
7+
// THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL
8+
// WARRANTIES WITH REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED
9+
// WARRANTIES OF MERCHANTABILITY AND FITNESS. IN NO EVENT SHALL THE
10+
// AUTHOR BE LIABLE FOR ANY SPECIAL, DIRECT, INDIRECT, OR CONSEQUENTIAL
11+
// DAMAGES OR ANY DAMAGES WHATSOEVER RESULTING FROM LOSS OF USE, DATA OR
12+
// PROFITS, WHETHER IN AN ACTION OF CONTRACT, NEGLIGENCE OR OTHER
13+
// TORTIOUS ACTION, ARISING OUT OF OR IN CONNECTION WITH THE USE OR
14+
// PERFORMANCE OF THIS SOFTWARE.
15+
16+
#include <cosmo.h>
17+
#include <signal.h>
18+
#include <string.h>
19+
#include <sys/wait.h>
20+
#include <unistd.h>
21+
22+
sig_atomic_t gotsig;
23+
24+
void onsig(int sig) {
25+
gotsig = sig;
26+
}
27+
28+
int main(int argc, char* argv[]) {
29+
sigset_t ss;
30+
sigfillset(&ss);
31+
sigprocmask(SIG_BLOCK, &ss, 0);
32+
if (argc >= 2 && !strcmp(argv[1], "childe")) {
33+
signal(SIGUSR1, onsig);
34+
sigemptyset(&ss);
35+
sigsuspend(&ss);
36+
if (gotsig != SIGUSR1)
37+
return 2;
38+
} else {
39+
int child;
40+
if ((child = fork()) == -1)
41+
return 2;
42+
if (!child) {
43+
execlp(argv[0], argv[0], "childe", NULL);
44+
_Exit(127);
45+
}
46+
if (IsNetbsd()) {
47+
// NetBSD has a bug where pending signals don't inherit across
48+
// execve, even though POSIX.1 literally says you must do this
49+
sleep(1);
50+
}
51+
if (kill(child, SIGUSR1))
52+
return 3;
53+
int ws;
54+
if (wait(&ws) != child)
55+
return 4;
56+
if (ws)
57+
return 5;
58+
}
59+
}

0 commit comments

Comments
 (0)