From cdf556e7d26c9ef5a468cedcac0b613fa0dede0e Mon Sep 17 00:00:00 2001 From: Justine Tunney Date: Sat, 14 Oct 2023 10:29:26 -0700 Subject: [PATCH] Implement signal handler tail recursion GNU Make on Windows now appears to be working reliably. This change also fixes a bug where, after fork the Windows thread handle wasn't reset and that caused undefined behavior using SetThreadContext() with our signals --- libc/calls/sig.c | 167 ++++++++++++------- libc/intrin/sig.c | 4 +- libc/nt/master.sh | 4 + libc/nt/process.h | 11 ++ libc/nt/psapi/EnumProcessModules.S | 18 +++ libc/nt/psapi/EnumProcessModulesEx.S | 18 +++ libc/nt/psapi/EnumProcesses.S | 18 +++ libc/nt/psapi/GetModuleBaseNameW.S | 18 +++ libc/proc/fork-nt.c | 5 +- libc/proc/fork.c | 49 ++++-- libc/runtime/enable_tls.c | 10 +- test/libc/proc/handkill_test.c | 151 ++++++++++++++++++ test/libc/thread/pthread_kill_test.c | 1 + tool/build/build.mk | 1 + tool/build/killall.c | 229 +++++++++++++++++++++++++++ 15 files changed, 627 insertions(+), 77 deletions(-) create mode 100644 libc/nt/psapi/EnumProcessModules.S create mode 100644 libc/nt/psapi/EnumProcessModulesEx.S create mode 100644 libc/nt/psapi/EnumProcesses.S create mode 100644 libc/nt/psapi/GetModuleBaseNameW.S create mode 100644 test/libc/proc/handkill_test.c create mode 100644 tool/build/killall.c diff --git a/libc/calls/sig.c b/libc/calls/sig.c index 9c0c6a8daef..e7919cc217a 100644 --- a/libc/calls/sig.c +++ b/libc/calls/sig.c @@ -97,6 +97,32 @@ textwindows void __sig_delete(int sig) { ALLOW_SIGNALS; } +static textwindows int __sig_getter(struct CosmoTib *tib, atomic_ulong *sigs) { + int sig; + sigset_t bit, pending, masked, deliverable; + for (;;) { + pending = atomic_load_explicit(sigs, memory_order_acquire); + masked = atomic_load_explicit(&tib->tib_sigmask, memory_order_acquire); + if ((deliverable = pending & ~masked)) { + sig = _bsf(deliverable) + 1; + bit = 1ull << (sig - 1); + if (atomic_fetch_and_explicit(sigs, ~bit, memory_order_acq_rel) & bit) { + return sig; + } + } else { + return 0; + } + } +} + +static textwindows int __sig_get(struct CosmoTib *tib) { + int sig; + if (!(sig = __sig_getter(tib, &tib->tib_sigpending))) { + sig = __sig_getter(tib, &__sig.pending); + } + return sig; +} + static textwindows bool __sig_should_use_altstack(unsigned flags, struct CosmoTib *tib) { if (!(flags & SA_ONSTACK)) { @@ -146,34 +172,49 @@ static textwindows sigaction_f __sig_handler(unsigned rva) { } textwindows int __sig_raise(int sig, int sic) { - unsigned rva, flags; + + // create machine context object struct PosixThread *pt = _pthread_self(); ucontext_t ctx = {.uc_sigmask = pt->tib->tib_sigmask}; - if (!__sig_start(pt, sig, &rva, &flags)) return 0; - if (flags & SA_RESETHAND) { - STRACE("resetting %G handler", sig); - __sighandrvas[sig] = (int32_t)(intptr_t)SIG_DFL; - } - siginfo_t si = {.si_signo = sig, .si_code = sic}; struct NtContext nc; nc.ContextFlags = kNtContextFull; GetThreadContext(GetCurrentThread(), &nc); _ntcontext2linux(&ctx, &nc); - pt->tib->tib_sigmask |= __sighandmask[sig]; - if (!(flags & SA_NODEFER)) { - pt->tib->tib_sigmask |= 1ull << (sig - 1); - } - NTTRACE("entering raise(%G) signal handler %t with mask %s → %s", sig, - __sig_handler(rva), DescribeSigset(0, &ctx.uc_sigmask), - DescribeSigset(0, (sigset_t *)&pt->tib->tib_sigmask)); - __sig_handler(rva)(sig, &si, &ctx); - NTTRACE("leaving raise(%G) signal handler %t with mask %s → %s", sig, - __sig_handler(rva), - DescribeSigset(0, (sigset_t *)&pt->tib->tib_sigmask), - DescribeSigset(0, &ctx.uc_sigmask)); - atomic_store_explicit(&pt->tib->tib_sigmask, ctx.uc_sigmask, - memory_order_release); - return (flags & SA_RESTART) ? 2 : 1; + + // process signal(s) + int handler_was_called = 0; + do { + // start the signal + unsigned rva, flags; + if (__sig_start(pt, sig, &rva, &flags)) { + if (flags & SA_RESETHAND) { + STRACE("resetting %G handler", sig); + __sighandrvas[sig] = (int32_t)(intptr_t)SIG_DFL; + } + + // update the signal mask in preparation for signal handller + sigset_t blocksigs = __sighandmask[sig]; + if (!(flags & SA_NODEFER)) blocksigs |= 1ull << (sig - 1); + ctx.uc_sigmask = atomic_fetch_or_explicit( + &pt->tib->tib_sigmask, blocksigs, memory_order_acq_rel); + + // call the user's signal handler + char ssbuf[2][128]; + siginfo_t si = {.si_signo = sig, .si_code = sic}; + STRACE("__sig_raise(%G, %t) mask %s → %s", sig, __sig_handler(rva), + (DescribeSigset)(ssbuf[0], 0, &ctx.uc_sigmask), + (DescribeSigset)(ssbuf[1], 0, (sigset_t *)&pt->tib->tib_sigmask)); + __sig_handler(rva)(sig, &si, &ctx); + (void)ssbuf; + + // restore the original signal mask + atomic_store_explicit(&pt->tib->tib_sigmask, ctx.uc_sigmask, + memory_order_release); + handler_was_called |= (flags & SA_RESTART) ? 2 : 1; + } + sic = SI_KERNEL; + } while ((sig = __sig_get(pt->tib))); + return handler_was_called; } // cancels blocking operations being performed by signaled thread @@ -220,17 +261,48 @@ textwindows void __sig_cancel(struct PosixThread *pt, int sig, unsigned flags) { WakeByAddressSingle(blocker); } +// the user's signal handler callback is composed with this trampoline static textwindows wontreturn void __sig_tramp(struct SignalFrame *sf) { - atomic_fetch_add_explicit(&__sig.count, 1, memory_order_relaxed); int sig = sf->si.si_signo; - sigset_t blocksigs = __sighandmask[sig]; - if (!(sf->flags & SA_NODEFER)) blocksigs |= 1ull << (sig - 1); - sf->ctx.uc_sigmask = atomic_fetch_or_explicit( - &__get_tls()->tib_sigmask, blocksigs, memory_order_acq_rel); - __sig_handler(sf->rva)(sig, &sf->si, &sf->ctx); - atomic_store_explicit(&__get_tls()->tib_sigmask, sf->ctx.uc_sigmask, - memory_order_release); - __sig_restore(&sf->ctx); + struct CosmoTib *tib = __get_tls(); + struct PosixThread *pt = (struct PosixThread *)tib->tib_pthread; + for (;;) { + atomic_fetch_add_explicit(&__sig.count, 1, memory_order_relaxed); + + // update the signal mask in preparation for signal handller + sigset_t blocksigs = __sighandmask[sig]; + if (!(sf->flags & SA_NODEFER)) blocksigs |= 1ull << (sig - 1); + sf->ctx.uc_sigmask = atomic_fetch_or_explicit(&tib->tib_sigmask, blocksigs, + memory_order_acq_rel); + + // call the user's signal handler + char ssbuf[2][128]; + STRACE("__sig_tramp(%G, %t) mask %s → %s", sig, __sig_handler(sf->rva), + (DescribeSigset)(ssbuf[0], 0, &sf->ctx.uc_sigmask), + (DescribeSigset)(ssbuf[1], 0, (sigset_t *)&tib->tib_sigmask)); + __sig_handler(sf->rva)(sig, &sf->si, &sf->ctx); + (void)ssbuf; + + // restore the signal mask that was used by the interrupted code + // this may have been modified by the signal handler in the callback + atomic_store_explicit(&tib->tib_sigmask, sf->ctx.uc_sigmask, + memory_order_release); + + // jump back into original code if there aren't any pending signals + do { + if (!(sig = __sig_get(tib))) { + __sig_restore(&sf->ctx); + } + } while (!__sig_start(pt, sig, &sf->rva, &sf->flags)); + + // tail recurse into another signal handler + sf->si.si_signo = sig; + sf->si.si_code = SI_KERNEL; + if (sf->flags & SA_RESETHAND) { + STRACE("resetting %G handler", sig); + __sighandrvas[sig] = (int32_t)(intptr_t)SIG_DFL; + } + } } static int __sig_killer(struct PosixThread *pt, int sig, int sic) { @@ -266,7 +338,7 @@ static int __sig_killer(struct PosixThread *pt, int sig, int sic) { if ((pt->tib->tib_sigmask & (1ull << (sig - 1))) || !((uintptr_t)__executable_start <= nc.Rip && nc.Rip < (uintptr_t)__privileged_start)) { - STRACE("enqueing %G on %d", sig, _pthread_tid(pt)); + STRACE("enqueing %G on %d rip %p", sig, _pthread_tid(pt), nc.Rip); pt->tib->tib_sigpending |= 1ull << (sig - 1); ResumeThread(th); __sig_cancel(pt, sig, flags); @@ -345,11 +417,11 @@ textwindows void __sig_generate(int sig, int sic) { if (mark) { STRACE("generating %G by killing %d", sig, _pthread_tid(mark)); __sig_killer(mark, sig, sic); + _pthread_unref(mark); } else { STRACE("all threads block %G so adding to pending signals of process", sig); __sig.pending |= 1ull << (sig - 1); } - _pthread_unref(mark); ALLOW_SIGNALS; } @@ -539,34 +611,17 @@ __msabi textwindows dontinstrument bool32 __sig_console(uint32_t dwCtrlType) { return true; } -static textwindows int __sig_checker(atomic_ulong *sigs, struct CosmoTib *tib) { - int sig, handler_was_called = 0; - sigset_t bit, pending, masked, deliverable; - pending = atomic_load_explicit(sigs, memory_order_acquire); - masked = atomic_load_explicit(&tib->tib_sigmask, memory_order_acquire); - if ((deliverable = pending & ~masked)) { - do { - sig = _bsf(deliverable) + 1; - bit = 1ull << (sig - 1); - if (atomic_fetch_and_explicit(sigs, ~bit, memory_order_acq_rel) & bit) { - STRACE("found pending %G we can raise now", sig); - handler_was_called |= __sig_raise(sig, SI_KERNEL); - } - } while ((deliverable &= ~bit)); - } - return handler_was_called; -} - // returns 0 if no signal handlers were called, otherwise a bitmask // consisting of `1` which means a signal handler was invoked which // didn't have the SA_RESTART flag, and `2`, which means SA_RESTART // handlers were called (or `3` if both were the case). textwindows int __sig_check(void) { - int handler_was_called = false; - struct CosmoTib *tib = __get_tls(); - handler_was_called |= __sig_checker(&tib->tib_sigpending, tib); - handler_was_called |= __sig_checker(&__sig.pending, tib); - return handler_was_called; + int sig; + if ((sig = __sig_get(__get_tls()))) { + return __sig_raise(sig, SI_KERNEL); + } else { + return 0; + } } textstartup void __sig_init(void) { diff --git a/libc/intrin/sig.c b/libc/intrin/sig.c index c33121a49bf..03c037c5635 100644 --- a/libc/intrin/sig.c +++ b/libc/intrin/sig.c @@ -29,7 +29,7 @@ struct Signals __sig; sigset_t __sig_block(void) { if (IsWindows()) { return atomic_exchange_explicit(&__get_tls()->tib_sigmask, -1, - memory_order_acq_rel); + memory_order_acquire); } else { sigset_t res, neu = -1; sys_sigprocmask(SIG_SETMASK, &neu, &res); @@ -55,7 +55,7 @@ textwindows int __sig_enqueue(int sig) { textwindows sigset_t __sig_beginwait(sigset_t waitmask) { return atomic_exchange_explicit(&__get_tls()->tib_sigmask, waitmask, - memory_order_acq_rel); + memory_order_acquire); } textwindows void __sig_finishwait(sigset_t m) { diff --git a/libc/nt/master.sh b/libc/nt/master.sh index 45677c1d224..471f246959d 100755 --- a/libc/nt/master.sh +++ b/libc/nt/master.sh @@ -598,6 +598,10 @@ imp 'PdhOpenQuery' PdhOpenQueryW pdh 3 # Creates a new query that is # PSAPI.DLL # # Name Actual DLL Arity +imp 'EnumProcessModules' EnumProcessModules psapi 4 +imp 'EnumProcessModulesEx' EnumProcessModulesEx psapi 5 +imp 'EnumProcesses' EnumProcesses psapi 3 +imp 'GetModuleBaseName' GetModuleBaseNameW psapi 4 imp 'GetProcessImageFileName' GetProcessImageFileNameW psapi 3 imp 'GetProcessMemoryInfo' GetProcessMemoryInfo psapi 3 diff --git a/libc/nt/process.h b/libc/nt/process.h index 8be6870292b..7f212753519 100644 --- a/libc/nt/process.h +++ b/libc/nt/process.h @@ -78,6 +78,17 @@ int64_t CreateToolhelp32Snapshot(uint32_t dwFlags, uint32_t th32ProcessID); bool32 Process32First(int64_t hSnapshot, struct NtProcessEntry32 *in_out_lppe); bool32 Process32Next(int64_t hSnapshot, struct NtProcessEntry32 *out_lppe); +bool32 EnumProcesses(uint32_t *out_lpidProcess, uint32_t cb, + uint32_t *out_lpcbNeeded) paramsnonnull(); +bool32 EnumProcessModules(int64_t hProcess, int64_t *out_lphModule, uint32_t cb, + uint32_t *out_lpcbNeeded) paramsnonnull(); +bool32 EnumProcessModulesEx(int64_t hProcess, int64_t *out_lphModule, + uint32_t cb, uint32_t *out_lpcbNeeded, + uint32_t dwFilterFlag) paramsnonnull(); +uint32_t GetModuleBaseName(int64_t hProcess, int64_t opt_hModule, + char16_t *out_lpBaseName, uint32_t nSize) + paramsnonnull(); + #if ShouldUseMsabiAttribute() #include "libc/nt/thunk/process.inc" #endif /* ShouldUseMsabiAttribute() */ diff --git a/libc/nt/psapi/EnumProcessModules.S b/libc/nt/psapi/EnumProcessModules.S new file mode 100644 index 00000000000..1ef1a774455 --- /dev/null +++ b/libc/nt/psapi/EnumProcessModules.S @@ -0,0 +1,18 @@ +#include "libc/nt/codegen.h" +.imp psapi,__imp_EnumProcessModules,EnumProcessModules + + .text.windows + .ftrace1 +EnumProcessModules: + .ftrace2 +#ifdef __x86_64__ + push %rbp + mov %rsp,%rbp + mov __imp_EnumProcessModules(%rip),%rax + jmp __sysv2nt +#elif defined(__aarch64__) + mov x0,#0 + ret +#endif + .endfn EnumProcessModules,globl + .previous diff --git a/libc/nt/psapi/EnumProcessModulesEx.S b/libc/nt/psapi/EnumProcessModulesEx.S new file mode 100644 index 00000000000..b9526f3cd33 --- /dev/null +++ b/libc/nt/psapi/EnumProcessModulesEx.S @@ -0,0 +1,18 @@ +#include "libc/nt/codegen.h" +.imp psapi,__imp_EnumProcessModulesEx,EnumProcessModulesEx + + .text.windows + .ftrace1 +EnumProcessModulesEx: + .ftrace2 +#ifdef __x86_64__ + push %rbp + mov %rsp,%rbp + mov __imp_EnumProcessModulesEx(%rip),%rax + jmp __sysv2nt6 +#elif defined(__aarch64__) + mov x0,#0 + ret +#endif + .endfn EnumProcessModulesEx,globl + .previous diff --git a/libc/nt/psapi/EnumProcesses.S b/libc/nt/psapi/EnumProcesses.S new file mode 100644 index 00000000000..46682c40c36 --- /dev/null +++ b/libc/nt/psapi/EnumProcesses.S @@ -0,0 +1,18 @@ +#include "libc/nt/codegen.h" +.imp psapi,__imp_EnumProcesses,EnumProcesses + + .text.windows + .ftrace1 +EnumProcesses: + .ftrace2 +#ifdef __x86_64__ + push %rbp + mov %rsp,%rbp + mov __imp_EnumProcesses(%rip),%rax + jmp __sysv2nt +#elif defined(__aarch64__) + mov x0,#0 + ret +#endif + .endfn EnumProcesses,globl + .previous diff --git a/libc/nt/psapi/GetModuleBaseNameW.S b/libc/nt/psapi/GetModuleBaseNameW.S new file mode 100644 index 00000000000..6210a49ace8 --- /dev/null +++ b/libc/nt/psapi/GetModuleBaseNameW.S @@ -0,0 +1,18 @@ +#include "libc/nt/codegen.h" +.imp psapi,__imp_GetModuleBaseNameW,GetModuleBaseNameW + + .text.windows + .ftrace1 +GetModuleBaseName: + .ftrace2 +#ifdef __x86_64__ + push %rbp + mov %rsp,%rbp + mov __imp_GetModuleBaseNameW(%rip),%rax + jmp __sysv2nt +#elif defined(__aarch64__) + mov x0,#0 + ret +#endif + .endfn GetModuleBaseName,globl + .previous diff --git a/libc/proc/fork-nt.c b/libc/proc/fork-nt.c index d0e7a7fa6a0..bdcd0db5bcc 100644 --- a/libc/proc/fork-nt.c +++ b/libc/proc/fork-nt.c @@ -395,6 +395,7 @@ textwindows int sys_fork_nt(uint32_t dwCreationFlags) { __set_tls(tib); __morph_tls(); __tls_enabled_set(true); + // get new main thread handle // clear pending signals tib->tib_sigpending = 0; atomic_store_explicit(&__sig.pending, 0, memory_order_relaxed); @@ -404,9 +405,9 @@ textwindows int sys_fork_nt(uint32_t dwCreationFlags) { if (ftrace_stackdigs) { _weaken(__hook)(_weaken(ftrace_hook), _weaken(GetSymbolTable)()); } - // reset console + // reset core runtime services + __proc_wipe(); __keystroke_wipe(); - // reset alarms if (_weaken(__itimer_wipe)) { _weaken(__itimer_wipe)(); } diff --git a/libc/proc/fork.c b/libc/proc/fork.c index 945c0a47f60..ac69bbd0c38 100644 --- a/libc/proc/fork.c +++ b/libc/proc/fork.c @@ -27,21 +27,22 @@ #include "libc/intrin/dll.h" #include "libc/intrin/strace.internal.h" #include "libc/intrin/weaken.h" +#include "libc/nt/files.h" #include "libc/nt/process.h" #include "libc/nt/runtime.h" #include "libc/nt/synchronization.h" +#include "libc/nt/thread.h" #include "libc/proc/proc.internal.h" #include "libc/runtime/internal.h" #include "libc/runtime/runtime.h" +#include "libc/runtime/syslib.internal.h" #include "libc/sysv/consts/sig.h" #include "libc/thread/posixthread.internal.h" #include "libc/thread/tls.h" int _fork(uint32_t dwCreationFlags) { struct Dll *e; - struct CosmoTib *tib; int ax, dx, tid, parent; - struct PosixThread *me, *other; parent = __pid; (void)parent; BLOCK_SIGNALS; @@ -55,33 +56,55 @@ int _fork(uint32_t dwCreationFlags) { ax = sys_fork_nt(dwCreationFlags); } if (!ax) { + + // get new process id if (!IsWindows()) { dx = sys_getpid().ax; } else { dx = GetCurrentProcessId(); } __pid = dx; - tib = __get_tls(); - me = (struct PosixThread *)tib->tib_pthread; - dll_remove(&_pthread_list, &me->list); + + // turn other threads into zombies + // we can't free() them since we're monopolizing all locks + // we assume the operating system already reclaimed system handles + struct CosmoTib *tib = __get_tls(); + struct PosixThread *pt = (struct PosixThread *)tib->tib_pthread; + dll_remove(&_pthread_list, &pt->list); for (e = dll_first(_pthread_list); e; e = dll_next(_pthread_list, e)) { - other = POSIXTHREAD_CONTAINER(e); - atomic_store_explicit(&other->pt_status, kPosixThreadZombie, + atomic_store_explicit(&POSIXTHREAD_CONTAINER(e)->pt_status, + kPosixThreadZombie, memory_order_relaxed); + atomic_store_explicit(&POSIXTHREAD_CONTAINER(e)->tib->tib_syshand, 0, memory_order_relaxed); } - dll_make_first(&_pthread_list, &me->list); + dll_make_first(&_pthread_list, &pt->list); + + // get new main thread id tid = IsLinux() || IsXnuSilicon() ? dx : sys_gettid(); atomic_store_explicit(&tib->tib_tid, tid, memory_order_relaxed); - atomic_store_explicit(&me->ptid, tid, memory_order_relaxed); - atomic_store_explicit(&me->pt_canceled, false, memory_order_relaxed); + atomic_store_explicit(&pt->ptid, tid, memory_order_relaxed); + + // get new system thread handle + intptr_t syshand = 0; + if (IsXnuSilicon()) { + syshand = __syslib->__pthread_self(); + } else if (IsWindows()) { + DuplicateHandle(GetCurrentProcess(), GetCurrentThread(), + GetCurrentProcess(), &syshand, 0, false, + kNtDuplicateSameAccess); + } + atomic_store_explicit(&tib->tib_syshand, syshand, memory_order_relaxed); + + // we can't be canceled if the canceler no longer exists + atomic_store_explicit(&pt->pt_canceled, false, memory_order_relaxed); + + // run user fork callbacks if (__threaded && _weaken(_pthread_onfork_child)) { _weaken(_pthread_onfork_child)(); } - if (IsWindows()) { - __proc_wipe(); - } STRACE("fork() → 0 (child of %d)", parent); } else { + // this is the parent process if (__threaded && _weaken(_pthread_onfork_parent)) { _weaken(_pthread_onfork_parent)(); } diff --git a/libc/runtime/enable_tls.c b/libc/runtime/enable_tls.c index 747ed282feb..ccc96fb990e 100644 --- a/libc/runtime/enable_tls.c +++ b/libc/runtime/enable_tls.c @@ -27,6 +27,7 @@ #include "libc/intrin/weaken.h" #include "libc/macros.internal.h" #include "libc/nt/files.h" +#include "libc/nt/process.h" #include "libc/nt/runtime.h" #include "libc/nt/synchronization.h" #include "libc/nt/thread.h" @@ -191,10 +192,11 @@ textstartup void __enable_tls(void) { tib->tib_locale = (intptr_t)&__c_dot_utf8_locale; tib->tib_pthread = (pthread_t)&_pthread_static; if (IsWindows()) { - intptr_t threadhand, pseudo = GetCurrentThread(); - DuplicateHandle(GetCurrentProcess(), pseudo, GetCurrentProcess(), - &threadhand, 0, true, kNtDuplicateSameAccess); - atomic_store_explicit(&tib->tib_syshand, threadhand, memory_order_relaxed); + intptr_t hThread; + DuplicateHandle(GetCurrentProcess(), GetCurrentThread(), + GetCurrentProcess(), &hThread, 0, false, + kNtDuplicateSameAccess); + atomic_store_explicit(&tib->tib_syshand, hThread, memory_order_relaxed); } else if (IsXnuSilicon()) { tib->tib_syshand = __syslib->__pthread_self(); } diff --git a/test/libc/proc/handkill_test.c b/test/libc/proc/handkill_test.c new file mode 100644 index 00000000000..8bdc14083e3 --- /dev/null +++ b/test/libc/proc/handkill_test.c @@ -0,0 +1,151 @@ +/*-*- mode:c;indent-tabs-mode:nil;c-basic-offset:2;tab-width:8;coding:utf-8 -*-│ +│vi: set net ft=c ts=2 sts=2 sw=2 fenc=utf-8 :vi│ +╞══════════════════════════════════════════════════════════════════════════════╡ +│ Copyright 2023 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 "libc/atomic.h" +#include "libc/calls/calls.h" +#include "libc/calls/struct/sigaction.h" +#include "libc/calls/struct/sigset.h" +#include "libc/dce.h" +#include "libc/nt/process.h" +#include "libc/nt/thread.h" +#include "libc/runtime/clktck.h" +#include "libc/runtime/runtime.h" +#include "libc/str/str.h" +#include "libc/sysv/consts/sig.h" +#include "libc/testlib/subprocess.h" +#include "libc/testlib/testlib.h" +#include "libc/thread/posixthread.internal.h" +#include "libc/thread/thread.h" + +// test we can: +// 1. raise the same signal inside the signal handler +// 2. that the signal will trigger immediately after handling + +struct SharedMemory { + pthread_t target; + atomic_bool ready; + atomic_bool got_signal; + atomic_bool handler_returned; +} * shm; + +void OnSig(int sig) { + signal(SIGUSR1, SIG_DFL); + raise(SIGUSR1); + shm->got_signal = true; +} + +void SetUpOnce(void) { + signal(SIGUSR1, OnSig); + shm = _mapshared(sizeof(*shm)); +} + +void TearDownOnce(void) { + munmap(shm, sizeof(*shm)); +} + +void SetUp(void) { + bzero(shm, sizeof(*shm)); +} + +void *Killer(void *arg) { + ASSERT_EQ(0, pthread_kill(shm->target, SIGUSR1)); + return 0; +} + +void *Killed(void *arg) { + shm->ready = true; + while (!shm->got_signal) donothing; + shm->handler_returned = true; + return 0; +} + +TEST(handkill, raise) { + SPAWN(fork); + raise(SIGUSR1); + shm->handler_returned = true; + TERMS(SIGUSR1); + EXPECT_TRUE(shm->got_signal); + EXPECT_FALSE(shm->handler_returned); +} + +TEST(handkill, main2thread_async) { + SPAWN(fork); + pthread_t th; + shm->target = pthread_self(); + pthread_create(&th, 0, Killed, 0); + while (!shm->ready) donothing; + ASSERT_EQ(0, pthread_kill(th, SIGUSR1)); + ASSERT_EQ(0, pthread_join(th, 0)); + TERMS(SIGUSR1); + EXPECT_TRUE(shm->got_signal); + EXPECT_FALSE(shm->handler_returned); +} + +TEST(handkill, thread2main_async) { + SPAWN(fork); + pthread_t th; + shm->target = pthread_self(); + pthread_create(&th, 0, Killer, 0); + while (!shm->got_signal) donothing; + shm->handler_returned = true; + pthread_join(th, 0); + TERMS(SIGUSR1); + EXPECT_TRUE(shm->got_signal); + EXPECT_FALSE(shm->handler_returned); +} + +TEST(handkill, thread2thread_async) { + SPAWN(fork); + pthread_t th; + pthread_create(&shm->target, 0, Killed, 0); + pthread_create(&th, 0, Killer, 0); + pthread_join(shm->target, 0); + pthread_join(th, 0); + TERMS(SIGUSR1); + EXPECT_TRUE(shm->got_signal); + EXPECT_FALSE(shm->handler_returned); +} + +TEST(handkill, process_async) { + if (IsWindows()) return; + SPAWN(fork); + shm->ready = true; + while (!shm->got_signal) donothing; + shm->handler_returned = true; + PARENT(); + while (!shm->ready) donothing; + ASSERT_SYS(0, 0, kill(child, SIGUSR1)); + WAIT(term, SIGUSR1); + EXPECT_TRUE(shm->got_signal); + EXPECT_FALSE(shm->handler_returned); +} + +TEST(handkill, process_pause) { + if (IsWindows()) return; + SPAWN(fork); + shm->ready = true; + pause(); + shm->handler_returned = true; + PARENT(); + while (!shm->ready) donothing; + usleep(1e6 / CLK_TCK * 2); + ASSERT_SYS(0, 0, kill(child, SIGUSR1)); + WAIT(term, SIGUSR1); + EXPECT_TRUE(shm->got_signal); + EXPECT_FALSE(shm->handler_returned); +} diff --git a/test/libc/thread/pthread_kill_test.c b/test/libc/thread/pthread_kill_test.c index a74810606c5..e6040359ba1 100644 --- a/test/libc/thread/pthread_kill_test.c +++ b/test/libc/thread/pthread_kill_test.c @@ -22,6 +22,7 @@ #include "libc/dce.h" #include "libc/errno.h" #include "libc/intrin/kprintf.h" +#include "libc/runtime/clktck.h" #include "libc/runtime/runtime.h" #include "libc/sock/sock.h" #include "libc/sock/struct/sockaddr.h" diff --git a/tool/build/build.mk b/tool/build/build.mk index ada3f0b9887..7d1b0d74b5c 100644 --- a/tool/build/build.mk +++ b/tool/build/build.mk @@ -34,6 +34,7 @@ TOOL_BUILD_DIRECTDEPS = \ LIBC_MEM \ LIBC_NEXGEN32E \ LIBC_NT_KERNEL32 \ + LIBC_NT_PSAPI \ LIBC_NT_USER32 \ LIBC_NT_WS2_32 \ LIBC_PROC \ diff --git a/tool/build/killall.c b/tool/build/killall.c new file mode 100644 index 00000000000..1d5f9ea8aee --- /dev/null +++ b/tool/build/killall.c @@ -0,0 +1,229 @@ +/*-*- mode:c;indent-tabs-mode:nil;c-basic-offset:2;tab-width:8;coding:utf-8 -*-│ +│vi: set net ft=c ts=2 sts=2 sw=2 fenc=utf-8 :vi│ +╞══════════════════════════════════════════════════════════════════════════════╡ +│ Copyright 2023 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 "libc/dce.h" +#include "libc/limits.h" +#include "libc/macros.internal.h" +#include "libc/mem/mem.h" +#include "libc/nt/enum/formatmessageflags.h" +#include "libc/nt/enum/lang.h" +#include "libc/nt/enum/processaccess.h" +#include "libc/nt/process.h" +#include "libc/nt/runtime.h" +#include "libc/runtime/runtime.h" +#include "libc/stdio/internal.h" +#include "libc/stdio/stdio.h" +#include "libc/str/str.h" +#include "libc/sysv/consts/sig.h" +#include "libc/x/x.h" +#include "third_party/getopt/getopt.internal.h" + +/** + * @fileoverview Mass Process Killer for Windows. + * + * Bad things sometimes happen during development, such as fork bombs. + * Under such circumstances, the Windows operating system itself remains + * remarkably stable (much more so than Linux would in these cases) but + * the tools on Windows for managing processes do not scale gracefully; + * GUIs like the Task Manager and Process Explorer become unresponsive + * when the system has a nontrivial number of processes, leaving no way + * to kill the processes. This tool is designed to make it easy to kill + * processes, particularly in large numbers, via a simple CLI interface. + */ + +static const char *prog; +static char16_t **filters; +static uint32_t pids[10000]; + +static wontreturn void PrintUsage(int rc, FILE *f) { + fprintf(f, + "Usage: %s [-nshv] NAME...\n" + "\tNAME\tcase-insensitive process name substring filter\n" + "\t-n\tdry run (print matching processes but do not kill)\n" + "\t-v\tverbose mode\n" + "\t-s\tsilent mode\n" + "\t-h\tshow help\n", + prog); + exit(rc); +} + +static wontreturn void OutOfMemory(void) { + fprintf(stderr, "%s: out of memory\n", prog); + exit(1); +} + +static void *Calloc(size_t n, size_t z) { + void *p; + if (!(p = calloc(n, z))) OutOfMemory(); + return p; +} + +static void ConvertStringToLowercase16(char16_t *s) { + while (*s) { + *s = towlower(*s); + ++s; + } +} + +static bool ShouldKillProcess(char16_t *name) { + if (!*filters) { + return true; + } + for (char16_t **f = filters; *f; ++f) { + if (strstr16(name, *f)) { + return true; + } + } + return false; +} + +static int64_t MyOpenProcess(uint32_t pid) { + return OpenProcess( + kNtProcessTerminate | kNtProcessQueryInformation | kNtProcessVmRead, + false, pid); +} + +static bool GetProcessName(int64_t hand, char16_t name[hasatleast PATH_MAX]) { + int64_t hMod; + uint32_t cbNeeded; + *name = 0; + if (EnumProcessModules(hand, &hMod, sizeof(hMod), &cbNeeded)) { + GetModuleBaseName(hand, hMod, name, PATH_MAX); + } + return !!name[0]; +} + +static char16_t *DescribeError(int err) { + static char16_t msg[256]; + FormatMessage(kNtFormatMessageFromSystem | kNtFormatMessageIgnoreInserts, 0, + err, MAKELANGID(kNtLangNeutral, kNtSublangDefault), msg, + ARRAYLEN(msg), 0); + return chomp16(msg); +} + +int main(int argc, char *argv[]) { + int i, j; + + // get program name + prog = argv[0] ? argv[0] : "killall"; + + // sanity check environment + if (!IsWindows()) { + fprintf(stderr, "%s: this program is intended for windows\n", prog); + exit(1); + } + + // try to minimize terminal writes slowing us down + setvbuf(stdin, NULL, _IONBF, 0); + + // get flags + int opt; + bool dryrun = false; + bool silent = false; + bool verbose = false; + while ((opt = getopt(argc, argv, "nhsv")) != -1) { + switch (opt) { + case 'n': + dryrun = true; + break; + case 's': + silent = true; + break; + case 'v': + verbose = true; + break; + case 'h': + PrintUsage(0, stdout); + default: + PrintUsage(1, stderr); + } + } + + // get names of things to kill + filters = Calloc(argc, sizeof(char16_t *)); + for (j = 0, i = optind; i < argc; ++i) { + char16_t *filter; + if ((filter = utf8to16(argv[i], -1, 0)) && *filter) { + ConvertStringToLowercase16(filter); + filters[j++] = filter; + } + } + if (!j && !dryrun) { + fprintf(stderr, "%s: missing operand\n", prog); + exit(1); + } + + // outer loop + int count = 0; + int subcount; + do { + + // get pids of all processes on system + uint32_t n; + if (!EnumProcesses(pids, sizeof(pids), &n)) { + fprintf(stderr, "%s: EnumProcesses() failed: %s\n", prog, + DescribeError(GetLastError())); + exit(1); + } + n /= sizeof(uint32_t); + + // kill matching processes + int64_t hProcess; + char16_t name[PATH_MAX]; + for (subcount = i = 0; i < n; i++) { + if (!pids[i]) continue; + if ((hProcess = MyOpenProcess(pids[i]))) { + if (GetProcessName(hProcess, name)) { + ConvertStringToLowercase16(name); + if (ShouldKillProcess(name)) { + if (dryrun) { + if (!silent) { + printf("%5u %hs\n", pids[i], name); + } + ++subcount; + } else if (TerminateProcess(hProcess, SIGKILL)) { + if (!silent) { + printf("%5u %hs killed\n", pids[i], name); + } + ++subcount; + } else { + printf("%5u %hs %hs\n", pids[i], name, + DescribeError(GetLastError())); + } + } + } else if (verbose) { + fprintf(stderr, "%s: GetProcessName(%u) failed: %hs\n", prog, pids[i], + DescribeError(GetLastError())); + } + CloseHandle(hProcess); + } else if (verbose) { + fprintf(stderr, "%s: OpenProcess(%u) failed: %hs\n", prog, pids[i], + DescribeError(GetLastError())); + } + } + + // we don't stop until we've confirmed they're all gone + count += subcount; + } while (!dryrun && subcount); + + if (!silent && !count) { + fprintf(stderr, "%s: no processes found\n", prog); + } + + return 0; +}