mirror of
https://github.com/jart/cosmopolitan.git
synced 2025-01-31 03:27:39 +00:00
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
This commit is contained in:
parent
9cc1bd04b2
commit
26c051c297
8 changed files with 187 additions and 21 deletions
|
@ -667,6 +667,9 @@ textwindows int __sig_check(void) {
|
|||
return res;
|
||||
}
|
||||
|
||||
// this mutex is needed so execve() can shut down the signal worker
|
||||
pthread_mutex_t __sig_worker_lock;
|
||||
|
||||
// background thread for delivering inter-process signals asynchronously
|
||||
// this checks for undelivered process-wide signals, once per scheduling
|
||||
// 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) {
|
|||
__maps_track((char *)(((uintptr_t)sp + __pagesize - 1) & -__pagesize) - STKSZ,
|
||||
STKSZ);
|
||||
for (;;) {
|
||||
pthread_mutex_lock(&__sig_worker_lock);
|
||||
|
||||
// dequeue all pending signals and fire them off. if there's no
|
||||
// thread that can handle them then __sig_generate will requeue
|
||||
|
@ -724,6 +728,7 @@ textwindows dontinstrument static uint32_t __sig_worker(void *arg) {
|
|||
_pthread_unlock();
|
||||
|
||||
// wait until next scheduler quantum
|
||||
pthread_mutex_unlock(&__sig_worker_lock);
|
||||
Sleep(POLL_INTERVAL_MS);
|
||||
}
|
||||
return 0;
|
||||
|
|
|
@ -18,7 +18,6 @@
|
|||
╚─────────────────────────────────────────────────────────────────────────────*/
|
||||
#include "libc/atomic.h"
|
||||
#include "libc/calls/sig.internal.h"
|
||||
#include "libc/intrin/kprintf.h"
|
||||
#include "libc/limits.h"
|
||||
#include "libc/nt/files.h"
|
||||
#include "libc/nt/memory.h"
|
||||
|
|
|
@ -17,13 +17,14 @@
|
|||
│ PERFORMANCE OF THIS SOFTWARE. │
|
||||
╚─────────────────────────────────────────────────────────────────────────────*/
|
||||
#include "libc/assert.h"
|
||||
#include "libc/calls/calls.h"
|
||||
#include "libc/calls/internal.h"
|
||||
#include "libc/calls/sig.internal.h"
|
||||
#include "libc/calls/struct/sigset.internal.h"
|
||||
#include "libc/calls/syscall-nt.internal.h"
|
||||
#include "libc/errno.h"
|
||||
#include "libc/fmt/itoa.h"
|
||||
#include "libc/intrin/fds.h"
|
||||
#include "libc/intrin/kprintf.h"
|
||||
#include "libc/mem/mem.h"
|
||||
#include "libc/nt/enum/processaccess.h"
|
||||
#include "libc/nt/enum/startf.h"
|
||||
|
@ -33,8 +34,10 @@
|
|||
#include "libc/nt/runtime.h"
|
||||
#include "libc/nt/struct/processinformation.h"
|
||||
#include "libc/nt/struct/startupinfo.h"
|
||||
#include "libc/nt/thunk/msabi.h"
|
||||
#include "libc/proc/describefds.internal.h"
|
||||
#include "libc/proc/ntspawn.h"
|
||||
#include "libc/runtime/internal.h"
|
||||
#include "libc/str/str.h"
|
||||
#include "libc/sysv/consts/at.h"
|
||||
#include "libc/sysv/consts/o.h"
|
||||
|
@ -43,23 +46,37 @@
|
|||
#include "libc/thread/thread.h"
|
||||
#ifdef __x86_64__
|
||||
|
||||
__msabi extern typeof(TerminateProcess) *const __imp_TerminateProcess;
|
||||
|
||||
extern pthread_mutex_t __sig_worker_lock;
|
||||
|
||||
static void sys_execve_nt_abort(sigset_t sigmask) {
|
||||
_pthread_unlock();
|
||||
pthread_mutex_unlock(&__sig_worker_lock);
|
||||
__sig_unblock(sigmask);
|
||||
}
|
||||
|
||||
textwindows int sys_execve_nt(const char *program, char *const argv[],
|
||||
char *const envp[]) {
|
||||
|
||||
// execve() needs to be @asyncsignalsafe
|
||||
sigset_t sigmask = __sig_block();
|
||||
_pthread_lock();
|
||||
pthread_mutex_lock(&__sig_worker_lock); // order matters
|
||||
_pthread_lock(); // order matters
|
||||
|
||||
// new process should be a child of our parent
|
||||
int64_t hParentProcess;
|
||||
int ppid = sys_getppid_nt();
|
||||
if (!(hParentProcess = OpenProcess(
|
||||
kNtProcessDupHandle | kNtProcessCreateProcess, false, ppid))) {
|
||||
_pthread_unlock();
|
||||
__sig_unblock(sigmask);
|
||||
sys_execve_nt_abort(sigmask);
|
||||
return -1;
|
||||
}
|
||||
|
||||
// inherit pid
|
||||
char pidvar[11 + 21];
|
||||
FormatUint64(stpcpy(pidvar, "_COSMO_PID="), __pid);
|
||||
|
||||
// inherit signal mask
|
||||
char maskvar[6 + 21];
|
||||
FormatUint64(stpcpy(maskvar, "_MASK="), sigmask);
|
||||
|
@ -84,22 +101,26 @@ textwindows int sys_execve_nt(const char *program, char *const argv[],
|
|||
if (!(fdspec = __describe_fds(g_fds.p, g_fds.n, &si, hParentProcess,
|
||||
&lpExplicitHandles, &dwExplicitHandleCount))) {
|
||||
CloseHandle(hParentProcess);
|
||||
_pthread_unlock();
|
||||
__sig_unblock(sigmask);
|
||||
sys_execve_nt_abort(sigmask);
|
||||
return -1;
|
||||
}
|
||||
|
||||
// inherit pending signals
|
||||
atomic_fetch_or_explicit(
|
||||
__sig.process,
|
||||
atomic_load_explicit(&__get_tls()->tib_sigpending, memory_order_acquire),
|
||||
memory_order_release);
|
||||
|
||||
// launch the process
|
||||
struct NtProcessInformation pi;
|
||||
int rc = ntspawn(&(struct NtSpawnArgs){
|
||||
AT_FDCWD, program, argv, envp, (char *[]){fdspec, maskvar, 0}, 0, 0,
|
||||
hParentProcess, lpExplicitHandles, dwExplicitHandleCount, &si, &pi});
|
||||
AT_FDCWD, program, argv, envp, (char *[]){fdspec, maskvar, pidvar, 0}, 0,
|
||||
0, hParentProcess, lpExplicitHandles, dwExplicitHandleCount, &si, &pi});
|
||||
__undescribe_fds(hParentProcess, lpExplicitHandles, dwExplicitHandleCount);
|
||||
if (rc == -1) {
|
||||
free(fdspec);
|
||||
CloseHandle(hParentProcess);
|
||||
_pthread_unlock();
|
||||
__sig_unblock(sigmask);
|
||||
sys_execve_nt_abort(sigmask);
|
||||
if (GetLastError() == kNtErrorSharingViolation) {
|
||||
return etxtbsy();
|
||||
} else {
|
||||
|
@ -112,12 +133,13 @@ textwindows int sys_execve_nt(const char *program, char *const argv[],
|
|||
if (DuplicateHandle(GetCurrentProcess(), pi.hProcess, hParentProcess, &handle,
|
||||
0, false, kNtDuplicateSameAccess)) {
|
||||
unassert(!(handle & 0xFFFFFFFFFF000000));
|
||||
TerminateThisProcess(0x23000000u | handle);
|
||||
__imp_TerminateProcess(-1, 0x23000000u | handle);
|
||||
} else {
|
||||
// TODO(jart): Why does `make loc` print this?
|
||||
// kprintf("DuplicateHandle failed w/ %d\n", GetLastError());
|
||||
TerminateThisProcess(ECHILD);
|
||||
__imp_TerminateProcess(-1, ECHILD);
|
||||
}
|
||||
__builtin_unreachable();
|
||||
}
|
||||
|
||||
#endif /* __x86_64__ */
|
||||
|
|
|
@ -36,14 +36,55 @@
|
|||
/**
|
||||
* Replaces current process with program.
|
||||
*
|
||||
* Your `prog` may be an actually portable executable or a platform
|
||||
* native binary (e.g. ELF, Mach-O, PE). On UNIX systems, your execve
|
||||
* implementation will try to find where the `ape` interpreter program
|
||||
* is installed on your system. The preferred location is `/usr/bin/ape`
|
||||
* except on Apple Silicon where it's `/usr/local/bin/ape`. The $TMPDIR
|
||||
* and $HOME locations that the APE shell script extracts the versioned
|
||||
* ape binaries to will also be checked as a fallback path. Finally, if
|
||||
* `prog` isn't an executable in any recognizable format, cosmo assumes
|
||||
* it's a bourne shell script and launches it under /bin/sh.
|
||||
*
|
||||
* The signal mask and pending signals are inherited by the new process.
|
||||
* Note the NetBSD kernel has a bug where pending signals are cleared.
|
||||
*
|
||||
* File descriptors that haven't been marked `O_CLOEXEC` through various
|
||||
* devices such as open() and fcntl() will be inherited by the executed
|
||||
* subprocess. The current file position of the duplicated descriptors
|
||||
* is shared across processes. On Windows, `prog` needs to be built by
|
||||
* cosmocc in order to properly inherit file descriptors. If a program
|
||||
* compiled by MSVC or Cygwin is launched instead, then only the stdio
|
||||
* file descriptors can be passed along.
|
||||
*
|
||||
* On Windows, `argv` and `envp` can't contain binary strings. They need
|
||||
* to be valid UTF-8 in order to round-trip the WIN32 API, without being
|
||||
* corrupted.
|
||||
*
|
||||
* On Windows, only file descriptors 0, 1 and 2 can be passed to a child
|
||||
* process in such a way that allows them to be automatically discovered
|
||||
* when the child process initializes. Cosmpolitan currently treats your
|
||||
* other file descriptors as implicitly O_CLOEXEC.
|
||||
* On Windows, cosmo execve uses parent spoofing to implement the UNIX
|
||||
* behavior of replacing the current process. Since POSIX.1 also needs
|
||||
* us to maintain the same PID number too, the _COSMO_PID environemnt
|
||||
* variable is passed to the child process which specifies a spoofed
|
||||
* PID. Whatever is in that variable will be reported by getpid() and
|
||||
* other cosmo processes will be able to send signals to the process
|
||||
* using that pid, via kill(). These synthetic PIDs which are only
|
||||
* created by execve could potentially overlap with OS assignments if
|
||||
* Windows recycles them. Cosmo avoids that by tracking handles of
|
||||
* subprocesses. Each process has its own process manager thread, to
|
||||
* associate pids with win32 handles, and execve will tell the parent
|
||||
* process its new handle when it changes. However it's not perfect.
|
||||
* There's still situations where processes created by execve() can
|
||||
* cause surprising things to happen. For an alternative, consider
|
||||
* posix_spawn() which is fastest and awesomest across all OSes.
|
||||
*
|
||||
* On Windows, support is currently not implemented for inheriting
|
||||
* setitimer() and alarm() into an executed process.
|
||||
*
|
||||
* On Windows, support is currently not implemented for inheriting
|
||||
* getrusage() statistics into an executed process.
|
||||
*
|
||||
* The executed process will share the same terminal and current
|
||||
* directory.
|
||||
*
|
||||
* @param program will not be PATH searched, see commandv()
|
||||
* @param argv[0] is the name of the program to run
|
||||
|
|
|
@ -92,6 +92,7 @@ textwindows int sys_kill_nt(int pid, int sig) {
|
|||
int64_t handle, closeme = 0;
|
||||
if (!(handle = __proc_handle(pid))) {
|
||||
if ((handle = OpenProcess(kNtProcessTerminate, false, pid))) {
|
||||
STRACE("warning: kill() using raw win32 pid");
|
||||
closeme = handle;
|
||||
} else {
|
||||
goto OnError;
|
||||
|
@ -103,7 +104,7 @@ textwindows int sys_kill_nt(int pid, int sig) {
|
|||
// now that we know the process exists, if it has a shared memory file
|
||||
// then we can be reasonably certain it's a cosmo process which should
|
||||
// be trusted to deliver its signal, unless it's a nine exterminations
|
||||
if (pid > 0 && sig != 9) {
|
||||
if (pid > 0) {
|
||||
atomic_ulong *sigproc;
|
||||
if ((sigproc = __sig_map_process(pid, kNtOpenExisting))) {
|
||||
if (sig > 0)
|
||||
|
@ -112,12 +113,15 @@ textwindows int sys_kill_nt(int pid, int sig) {
|
|||
UnmapViewOfFile(sigproc);
|
||||
if (closeme)
|
||||
CloseHandle(closeme);
|
||||
return 0;
|
||||
if (sig != 9)
|
||||
return 0;
|
||||
}
|
||||
}
|
||||
|
||||
// perform actual kill
|
||||
// process will report WIFSIGNALED with WTERMSIG(sig)
|
||||
if (sig != 9)
|
||||
STRACE("warning: kill() sending %G via terminate", sig);
|
||||
bool32 ok = TerminateProcess(handle, sig);
|
||||
if (closeme)
|
||||
CloseHandle(closeme);
|
||||
|
|
|
@ -35,6 +35,9 @@
|
|||
* signal a cosmo process. The targeting process will then notice that a
|
||||
* signal has been added and delivers to any thread as soon as possible.
|
||||
*
|
||||
* On Windows, the only signal that's guaranteed to work on non-cosmocc
|
||||
* processes is SIGKILL.
|
||||
*
|
||||
* On Windows, the concept of a process group isn't fully implemented.
|
||||
* Saying `kill(0, sig)` will deliver `sig` to all direct descendent
|
||||
* processes. Saying `kill(-pid, sig)` will be the same as saying
|
||||
|
|
|
@ -300,6 +300,37 @@ static abi wontreturn void WinInit(const char16_t *cmdline) {
|
|||
(uintptr_t)(stackaddr + (stacksize - sizeof(struct WinArgs))));
|
||||
}
|
||||
|
||||
static int Atoi(const char16_t *str) {
|
||||
int c;
|
||||
unsigned x = 0;
|
||||
while ((c = *str++)) {
|
||||
if ('0' <= c && c <= '9') {
|
||||
x *= 10;
|
||||
x += c - '0';
|
||||
} else {
|
||||
return -1;
|
||||
}
|
||||
}
|
||||
return x;
|
||||
}
|
||||
|
||||
static abi int WinGetPid(const char16_t *var, bool *out_is_inherited) {
|
||||
uint32_t len;
|
||||
char16_t val[12];
|
||||
if ((len = __imp_GetEnvironmentVariableW(var, val, ARRAYLEN(val)))) {
|
||||
int pid = -1;
|
||||
if (len < ARRAYLEN(val))
|
||||
pid = Atoi(val);
|
||||
__imp_SetEnvironmentVariableW(var, NULL);
|
||||
if (pid > 0) {
|
||||
*out_is_inherited = true;
|
||||
return pid;
|
||||
}
|
||||
}
|
||||
*out_is_inherited = false;
|
||||
return __imp_GetCurrentProcessId();
|
||||
}
|
||||
|
||||
abi int64_t WinMain(int64_t hInstance, int64_t hPrevInstance,
|
||||
const char *lpCmdLine, int64_t nCmdShow) {
|
||||
static atomic_ulong fake_process_signals;
|
||||
|
@ -316,10 +347,12 @@ abi int64_t WinMain(int64_t hInstance, int64_t hPrevInstance,
|
|||
__imp_GetSystemInfo(&si);
|
||||
__pagesize = si.dwPageSize;
|
||||
__gransize = si.dwAllocationGranularity;
|
||||
__pid = __imp_GetCurrentProcessId();
|
||||
bool pid_is_inherited;
|
||||
__pid = WinGetPid(u"_COSMO_PID", &pid_is_inherited);
|
||||
if (!(__sig.process = __sig_map_process(__pid, kNtOpenAlways)))
|
||||
__sig.process = &fake_process_signals;
|
||||
atomic_store_explicit(__sig.process, 0, memory_order_release);
|
||||
if (!pid_is_inherited)
|
||||
atomic_store_explicit(__sig.process, 0, memory_order_release);
|
||||
cmdline = __imp_GetCommandLineW();
|
||||
#if SYSDEBUG
|
||||
// sloppy flag-only check for early initialization
|
||||
|
|
59
test/posix/pending_signal_execve_test.c
Normal file
59
test/posix/pending_signal_execve_test.c
Normal file
|
@ -0,0 +1,59 @@
|
|||
// Copyright 2024 Justine Alexandra Roberts Tunney
|
||||
//
|
||||
// Permission to use, copy, modify, and/or distribute this software for
|
||||
// any purpose with or without fee is hereby granted, provided that the
|
||||
// above copyright notice and this permission notice appear in all copies.
|
||||
//
|
||||
// THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL
|
||||
// WARRANTIES WITH REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED
|
||||
// WARRANTIES OF MERCHANTABILITY AND FITNESS. IN NO EVENT SHALL THE
|
||||
// AUTHOR BE LIABLE FOR ANY SPECIAL, DIRECT, INDIRECT, OR CONSEQUENTIAL
|
||||
// DAMAGES OR ANY DAMAGES WHATSOEVER RESULTING FROM LOSS OF USE, DATA OR
|
||||
// PROFITS, WHETHER IN AN ACTION OF CONTRACT, NEGLIGENCE OR OTHER
|
||||
// TORTIOUS ACTION, ARISING OUT OF OR IN CONNECTION WITH THE USE OR
|
||||
// PERFORMANCE OF THIS SOFTWARE.
|
||||
|
||||
#include <cosmo.h>
|
||||
#include <signal.h>
|
||||
#include <string.h>
|
||||
#include <sys/wait.h>
|
||||
#include <unistd.h>
|
||||
|
||||
sig_atomic_t gotsig;
|
||||
|
||||
void onsig(int sig) {
|
||||
gotsig = sig;
|
||||
}
|
||||
|
||||
int main(int argc, char* argv[]) {
|
||||
sigset_t ss;
|
||||
sigfillset(&ss);
|
||||
sigprocmask(SIG_BLOCK, &ss, 0);
|
||||
if (argc >= 2 && !strcmp(argv[1], "childe")) {
|
||||
signal(SIGUSR1, onsig);
|
||||
sigemptyset(&ss);
|
||||
sigsuspend(&ss);
|
||||
if (gotsig != SIGUSR1)
|
||||
return 2;
|
||||
} else {
|
||||
int child;
|
||||
if ((child = fork()) == -1)
|
||||
return 2;
|
||||
if (!child) {
|
||||
execlp(argv[0], argv[0], "childe", NULL);
|
||||
_Exit(127);
|
||||
}
|
||||
if (IsNetbsd()) {
|
||||
// NetBSD has a bug where pending signals don't inherit across
|
||||
// execve, even though POSIX.1 literally says you must do this
|
||||
sleep(1);
|
||||
}
|
||||
if (kill(child, SIGUSR1))
|
||||
return 3;
|
||||
int ws;
|
||||
if (wait(&ws) != child)
|
||||
return 4;
|
||||
if (ws)
|
||||
return 5;
|
||||
}
|
||||
}
|
Loading…
Reference in a new issue