From 8f5b9a94347aad4936c804ed1db10127d684a82f Mon Sep 17 00:00:00 2001 From: Puneet Dixit Date: Thu, 4 Jun 2026 13:28:04 +0530 Subject: [PATCH] Forward signals for exec subprocesses Signed-off-by: Deepak kudi --- cmd/sops/subcommand/exec/exec.go | 4 +- cmd/sops/subcommand/exec/exec_unix.go | 45 +++++++++++++++++ cmd/sops/subcommand/exec/exec_unix_test.go | 57 ++++++++++++++++++++++ cmd/sops/subcommand/exec/exec_windows.go | 4 ++ 4 files changed, 108 insertions(+), 2 deletions(-) create mode 100644 cmd/sops/subcommand/exec/exec_unix_test.go diff --git a/cmd/sops/subcommand/exec/exec.go b/cmd/sops/subcommand/exec/exec.go index 35f519c3cc..1a893ae295 100644 --- a/cmd/sops/subcommand/exec/exec.go +++ b/cmd/sops/subcommand/exec/exec.go @@ -121,7 +121,7 @@ func ExecWithFile(opts ExecOpts) error { cmd.Stdout = os.Stdout cmd.Stderr = os.Stderr - return cmd.Run() + return RunCommand(cmd) } func ExecWithEnv(opts ExecOpts) error { @@ -172,5 +172,5 @@ func ExecWithEnv(opts ExecOpts) error { cmd.Stdout = os.Stdout cmd.Stderr = os.Stderr - return cmd.Run() + return RunCommand(cmd) } diff --git a/cmd/sops/subcommand/exec/exec_unix.go b/cmd/sops/subcommand/exec/exec_unix.go index f36d6326af..83d378e178 100644 --- a/cmd/sops/subcommand/exec/exec_unix.go +++ b/cmd/sops/subcommand/exec/exec_unix.go @@ -6,12 +6,20 @@ package exec import ( "os" "os/exec" + "os/signal" "os/user" "path/filepath" "strconv" "syscall" ) +var forwardedSignals = []os.Signal{ + syscall.SIGHUP, + syscall.SIGINT, + syscall.SIGTERM, + syscall.SIGQUIT, +} + func ExecSyscall(command string, env []string) error { return syscall.Exec("/bin/sh", []string{"/bin/sh", "-c", command}, env) } @@ -20,6 +28,43 @@ func BuildCommand(command string) *exec.Cmd { return exec.Command("/bin/sh", "-c", command) } +func RunCommand(cmd *exec.Cmd) error { + if cmd.SysProcAttr == nil { + cmd.SysProcAttr = &syscall.SysProcAttr{} + } + cmd.SysProcAttr.Setpgid = true + + signals := make(chan os.Signal, 1) + signal.Notify(signals, forwardedSignals...) + + if err := cmd.Start(); err != nil { + signal.Stop(signals) + return err + } + + stopForwarding := make(chan struct{}) + forwardingDone := make(chan struct{}) + go func() { + defer close(forwardingDone) + for { + select { + case sig := <-signals: + if err := syscall.Kill(-cmd.Process.Pid, sig.(syscall.Signal)); err != nil { + log.WithError(err).Warn("Failed to forward signal to child process group") + } + case <-stopForwarding: + return + } + } + }() + + err := cmd.Wait() + signal.Stop(signals) + close(stopForwarding) + <-forwardingDone + return err +} + func WritePipe(pipe string, contents []byte) { handle, err := os.OpenFile(pipe, os.O_WRONLY, 0600) diff --git a/cmd/sops/subcommand/exec/exec_unix_test.go b/cmd/sops/subcommand/exec/exec_unix_test.go new file mode 100644 index 0000000000..ac83544b0d --- /dev/null +++ b/cmd/sops/subcommand/exec/exec_unix_test.go @@ -0,0 +1,57 @@ +//go:build !windows +// +build !windows + +package exec + +import ( + "errors" + "os" + "os/exec" + "path/filepath" + "syscall" + "testing" + "time" + + "github.com/stretchr/testify/require" +) + +func TestRunCommandForwardsSignalsToChildProcessGroup(t *testing.T) { + dir := t.TempDir() + readyFile := filepath.Join(dir, "ready") + signalFile := filepath.Join(dir, "signal") + + cmd := BuildCommand(` +trap 'echo term > "$SIGNAL_FILE"; exit 42' TERM +echo ready > "$READY_FILE" +while true; do sleep 1; done +`) + cmd.Env = append(os.Environ(), "READY_FILE="+readyFile, "SIGNAL_FILE="+signalFile) + + errCh := make(chan error, 1) + go func() { + errCh <- RunCommand(cmd) + }() + + require.Eventually(t, func() bool { + _, err := os.Stat(readyFile) + return err == nil + }, 3*time.Second, 25*time.Millisecond) + + require.NoError(t, syscall.Kill(os.Getpid(), syscall.SIGTERM)) + + select { + case err := <-errCh: + var exitErr *exec.ExitError + require.True(t, errors.As(err, &exitErr), "expected command to return an exit error") + require.Equal(t, 42, exitErr.ExitCode()) + case <-time.After(3 * time.Second): + if cmd.Process != nil { + _ = syscall.Kill(-cmd.Process.Pid, syscall.SIGKILL) + } + t.Fatal("command did not receive the forwarded signal") + } + + contents, err := os.ReadFile(signalFile) + require.NoError(t, err) + require.Equal(t, "term\n", string(contents)) +} diff --git a/cmd/sops/subcommand/exec/exec_windows.go b/cmd/sops/subcommand/exec/exec_windows.go index c4870ba62d..869f04b953 100644 --- a/cmd/sops/subcommand/exec/exec_windows.go +++ b/cmd/sops/subcommand/exec/exec_windows.go @@ -14,6 +14,10 @@ func BuildCommand(command string) *exec.Cmd { return exec.Command("cmd.exe", "/C", command) } +func RunCommand(cmd *exec.Cmd) error { + return cmd.Run() +} + func WritePipe(pipe string, contents []byte) { log.Fatal("fifos are not available on windows") }