Skip to content

Commit

Permalink
fix(emissary): signal SIGINT/SIGTERM in windows correctly
Browse files Browse the repository at this point in the history
emissary tries to send a signal but `os/Process.Kill` only supports
sending SIGKILL and returns an error for all other cases.

Using code found in hcsshim this changes signal handling in emissary for
windows by translating SIGINT and SIGTERM to their appropriate windows
signal and sending it to the process.

Signed-off-by: Michael Weibel <michael@helio.exchange>
  • Loading branch information
mweibel committed Oct 2, 2024
1 parent 5310c39 commit 6c4d643
Showing 1 changed file with 114 additions and 4 deletions.
118 changes: 114 additions & 4 deletions workflow/executor/os-specific/signal_windows.go
Original file line number Diff line number Diff line change
@@ -1,14 +1,21 @@
package os_specific

import (
"fmt"
"os"
"syscall"
"unsafe"

"github.com/argoproj/argo-workflows/v3/util/errors"
"golang.org/x/sys/windows"
)

var (
Term = os.Interrupt

modkernel32 = windows.NewLazySystemDLL("kernel32.dll")

procCreateRemoteThread = modkernel32.NewProc("CreateRemoteThread")
)

func CanIgnoreSignal(s os.Signal) bool {
Expand All @@ -19,11 +26,23 @@ func Kill(pid int, s syscall.Signal) error {
if pid < 0 {
pid = -pid // // we cannot kill a negative process on windows
}
p, err := os.FindProcess(pid)
if err != nil {
return err

winSignal := -1
switch s {
case syscall.SIGTERM:
winSignal = windows.CTRL_SHUTDOWN_EVENT
case syscall.SIGINT:
winSignal = windows.CTRL_C_EVENT
}
return p.Signal(s)

if winSignal == -1 {
p, err := os.FindProcess(pid)
if err != nil {
return err
}
return p.Kill()
}
return signalProcess(uint32(pid), winSignal)
}

func Setpgid(a *syscall.SysProcAttr) {
Expand All @@ -37,3 +56,94 @@ func Wait(process *os.Process) error {
}
return err
}

// signalProcess sends the specified signal to a process.
//
// Code +/- copied from: https://github.com/microsoft/hcsshim/blob/1d69a9c658655b77dd4e5275bff99caad6b38416/internal/jobcontainers/process.go#L251
// License: MIT
// Author: Microsoft
func signalProcess(pid uint32, signal int) error {
hProc, err := windows.OpenProcess(windows.PROCESS_TERMINATE, true, pid)
if err != nil {
return fmt.Errorf("failed to open process: %w", err)
}
defer func() {
_ = windows.Close(hProc)
}()

// We can't use GenerateConsoleCtrlEvent since that only supports CTRL_C_EVENT and CTRL_BREAK_EVENT.
// Instead, to handle an arbitrary signal we open a CtrlRoutine thread inside the target process and
// give it the specified signal to handle. This is safe even with ASLR as even though kernel32.dll's
// location will be randomized each boot, it will be in the same address for every process. This is why
// we're able to get the address from a different process and use this as the start address for the routine
// that the thread will run.
//
// Note: This is a hack which is not officially supported.
k32, err := windows.LoadLibrary("kernel32.dll")
if err != nil {
return fmt.Errorf("failed to load kernel32 library: %w", err)
}
defer func() {
_ = windows.FreeLibrary(k32)
}()

proc, err := windows.GetProcAddress(k32, "CtrlRoutine")
if err != nil {
return fmt.Errorf("failed to load CtrlRoutine: %w", err)
}

threadHandle, err := createRemoteThread(hProc, nil, 0, proc, uintptr(signal), 0, nil)
if err != nil {
return fmt.Errorf("failed to open remote thread in target process %d: %w", pid, err)
}
defer func() {
_ = windows.Close(windows.Handle(threadHandle))
}()
return nil
}

// Following code has been generated using github.com/Microsoft/go-winio/tools/mkwinsyscall and inlined
// for easier usage

// HANDLE CreateRemoteThread(
//
// HANDLE hProcess,
// LPSECURITY_ATTRIBUTES lpThreadAttributes,
// SIZE_T dwStackSize,
// LPTHREAD_START_ROUTINE lpStartAddress,
// LPVOID lpParameter,
// DWORD dwCreationFlags,
// LPDWORD lpThreadId
//
// );
func createRemoteThread(process windows.Handle, sa *windows.SecurityAttributes, stackSize uint32, startAddr uintptr, parameter uintptr, creationFlags uint32, threadID *uint32) (handle windows.Handle, err error) {
r0, _, e1 := syscall.SyscallN(procCreateRemoteThread.Addr(), uintptr(process), uintptr(unsafe.Pointer(sa)), uintptr(stackSize), uintptr(startAddr), uintptr(parameter), uintptr(creationFlags), uintptr(unsafe.Pointer(threadID)))
handle = windows.Handle(r0)
if handle == 0 {
err = errnoErr(e1)
}
return
}

// Do the interface allocations only once for common
// Errno values.
const (
errnoERROR_IO_PENDING = 997
)

var (
errERROR_IO_PENDING error = syscall.Errno(errnoERROR_IO_PENDING)
errERROR_EINVAL error = syscall.EINVAL
)

// errnoErr returns common boxed Errno values, to prevent
// allocations at runtime.
func errnoErr(e syscall.Errno) error {
switch e {
case 0:
return errERROR_EINVAL
case errnoERROR_IO_PENDING:
return errERROR_IO_PENDING
}
return e
}

0 comments on commit 6c4d643

Please sign in to comment.