diff --git a/internal/app/starter/master_linux.go b/internal/app/starter/master_linux.go index 975f738bc3..9ddf56c4b9 100644 --- a/internal/app/starter/master_linux.go +++ b/internal/app/starter/master_linux.go @@ -24,8 +24,14 @@ import ( func Master(rpcSocket, masterSocket int, isInstance bool, containerPid int, engine *engines.Engine) { var fatal error var status syscall.WaitStatus - fatalChan := make(chan error, 1) + + // we could receive signal from child with CreateContainer call so we + // set the signal handler earlier to queue signals until MonitorContainer + // is called to handle them + signals := make(chan os.Signal, 1) + signal.Notify(signals) + ppid := os.Getppid() go func() { @@ -109,11 +115,6 @@ func Master(rpcSocket, masterSocket int, isInstance bool, containerPid int, engi go func() { var err error - - // catch all signals - signals := make(chan os.Signal, 1) - signal.Notify(signals) - status, err = engine.MonitorContainer(containerPid, signals) fatalChan <- err }() diff --git a/internal/pkg/runtime/engines/singularity/monitor.go b/internal/pkg/runtime/engines/singularity/monitor.go index 8ae114686e..ee60599aad 100644 --- a/internal/pkg/runtime/engines/singularity/monitor.go +++ b/internal/pkg/runtime/engines/singularity/monitor.go @@ -25,6 +25,12 @@ func (engine *EngineOperations) MonitorContainer(pid int, signals chan os.Signal continue } return status, nil + default: + if engine.EngineConfig.GetSignalPropagation() { + if err := syscall.Kill(pid, s.(syscall.Signal)); err != nil { + return status, fmt.Errorf("interrupted by signal %s", s.String()) + } + } } } } diff --git a/internal/pkg/runtime/engines/singularity/prepare_linux.go b/internal/pkg/runtime/engines/singularity/prepare_linux.go index 8734cd2e48..eacfd397e0 100644 --- a/internal/pkg/runtime/engines/singularity/prepare_linux.go +++ b/internal/pkg/runtime/engines/singularity/prepare_linux.go @@ -17,6 +17,7 @@ import ( "syscall" "github.com/sylabs/singularity/pkg/util/fs/proc" + "golang.org/x/sys/unix" specs "github.com/opencontainers/runtime-spec/specs-go" "github.com/sylabs/singularity/internal/pkg/buildcfg" @@ -748,9 +749,36 @@ func (e *EngineOperations) PrepareConfig(starterConfig *starter.Config) error { starterConfig.SetCapabilities(capabilities.Ambient, e.EngineConfig.OciConfig.Process.Capabilities.Ambient) } + // determine if engine need to propagate signals across processes + e.checkSignalPropagation() + return nil } +func (e *EngineOperations) checkSignalPropagation() { + // obtain the process group ID of the associated controlling + // terminal (if there's one). + pgrp := 0 + for i := 0; i <= 2; i++ { + // The two possible errors: + // - EBADF will return 0 as process group + // - ENOTTY will also return 0 as process group + pgrp, _ = unix.IoctlGetInt(i, unix.TIOCGPGRP) + // based on kernel source a 0 value for process group + // theorically be set but really not sure it can happen + // with linux tty behavior + if pgrp != 0 { + break + } + } + // cases we need to propagate signals to container process: + // - when pgrp == 0 because container won't run in a terminal + // - when process group is different from the process group controlling terminal + if pgrp == 0 || (pgrp > 0 && pgrp != syscall.Getpgrp()) { + e.EngineConfig.SetSignalPropagation(true) + } +} + func (e *EngineOperations) loadImages(starterConfig *starter.Config) error { images := make([]image.Image, 0) diff --git a/internal/pkg/runtime/engines/singularity/process_linux.go b/internal/pkg/runtime/engines/singularity/process_linux.go index 9c40bc448a..a48ce91f56 100644 --- a/internal/pkg/runtime/engines/singularity/process_linux.go +++ b/internal/pkg/runtime/engines/singularity/process_linux.go @@ -317,11 +317,21 @@ func (engine *EngineOperations) StartProcess(masterConn net.Conn) error { } default: signal := s.(syscall.Signal) + // EPERM and EINVAL are deliberately ignored because they can't be + // returned in this context, this process is PID 1, so it has the + // permissions to send signals to its childs and EINVAL would + // mean to update the Go runtime or the kernel to something more + // stable :) if isInstance { if err := syscall.Kill(-cmd.Process.Pid, signal); err == syscall.ESRCH { sylog.Debugf("No child process, exiting ...") os.Exit(128 + int(signal)) } + } else if engine.EngineConfig.GetSignalPropagation() { + if err := syscall.Kill(cmd.Process.Pid, signal); err == syscall.ESRCH { + sylog.Debugf("No child process, exiting ...") + os.Exit(128 + int(signal)) + } } } case err := <-errChan: diff --git a/pkg/runtime/engines/singularity/config/config.go b/pkg/runtime/engines/singularity/config/config.go index f134a94b56..46ee429a30 100644 --- a/pkg/runtime/engines/singularity/config/config.go +++ b/pkg/runtime/engines/singularity/config/config.go @@ -54,46 +54,47 @@ type FileConfig struct { // JSONConfig stores engine specific confguration that is allowed to be set by the user type JSONConfig struct { - ScratchDir []string `json:"scratchdir,omitempty"` - OverlayImage []string `json:"overlayImage,omitempty"` - BindPath []string `json:"bindpath,omitempty"` - NetworkArgs []string `json:"networkArgs,omitempty"` - Security []string `json:"security,omitempty"` - LibrariesPath []string `json:"librariesPath,omitempty"` - ImageList []image.Image `json:"imageList,omitempty"` - OpenFd []int `json:"openFd,omitempty"` - TargetGID []int `json:"targetGID,omitempty"` - Image string `json:"image"` - Workdir string `json:"workdir,omitempty"` - CgroupsPath string `json:"cgroupsPath,omitempty"` - HomeSource string `json:"homedir,omitempty"` - HomeDest string `json:"homeDest,omitempty"` - Command string `json:"command,omitempty"` - Shell string `json:"shell,omitempty"` - TmpDir string `json:"tmpdir,omitempty"` - AddCaps string `json:"addCaps,omitempty"` - DropCaps string `json:"dropCaps,omitempty"` - Hostname string `json:"hostname,omitempty"` - Network string `json:"network,omitempty"` - DNS string `json:"dns,omitempty"` - Cwd string `json:"cwd,omitempty"` - TargetUID int `json:"targetUID,omitempty"` - WritableImage bool `json:"writableImage,omitempty"` - WritableTmpfs bool `json:"writableTmpfs,omitempty"` - Contain bool `json:"container,omitempty"` - Nv bool `json:"nv,omitempty"` - CustomHome bool `json:"customHome,omitempty"` - Instance bool `json:"instance,omitempty"` - InstanceJoin bool `json:"instanceJoin,omitempty"` - BootInstance bool `json:"bootInstance,omitempty"` - RunPrivileged bool `json:"runPrivileged,omitempty"` - AllowSUID bool `json:"allowSUID,omitempty"` - KeepPrivs bool `json:"keepPrivs,omitempty"` - NoPrivs bool `json:"noPrivs,omitempty"` - NoHome bool `json:"noHome,omitempty"` - NoInit bool `json:"noInit,omitempty"` - DeleteImage bool `json:"deleteImage,omitempty"` - Fakeroot bool `json:"fakeroot,omitempty"` + ScratchDir []string `json:"scratchdir,omitempty"` + OverlayImage []string `json:"overlayImage,omitempty"` + BindPath []string `json:"bindpath,omitempty"` + NetworkArgs []string `json:"networkArgs,omitempty"` + Security []string `json:"security,omitempty"` + LibrariesPath []string `json:"librariesPath,omitempty"` + ImageList []image.Image `json:"imageList,omitempty"` + OpenFd []int `json:"openFd,omitempty"` + TargetGID []int `json:"targetGID,omitempty"` + Image string `json:"image"` + Workdir string `json:"workdir,omitempty"` + CgroupsPath string `json:"cgroupsPath,omitempty"` + HomeSource string `json:"homedir,omitempty"` + HomeDest string `json:"homeDest,omitempty"` + Command string `json:"command,omitempty"` + Shell string `json:"shell,omitempty"` + TmpDir string `json:"tmpdir,omitempty"` + AddCaps string `json:"addCaps,omitempty"` + DropCaps string `json:"dropCaps,omitempty"` + Hostname string `json:"hostname,omitempty"` + Network string `json:"network,omitempty"` + DNS string `json:"dns,omitempty"` + Cwd string `json:"cwd,omitempty"` + TargetUID int `json:"targetUID,omitempty"` + WritableImage bool `json:"writableImage,omitempty"` + WritableTmpfs bool `json:"writableTmpfs,omitempty"` + Contain bool `json:"container,omitempty"` + Nv bool `json:"nv,omitempty"` + CustomHome bool `json:"customHome,omitempty"` + Instance bool `json:"instance,omitempty"` + InstanceJoin bool `json:"instanceJoin,omitempty"` + BootInstance bool `json:"bootInstance,omitempty"` + RunPrivileged bool `json:"runPrivileged,omitempty"` + AllowSUID bool `json:"allowSUID,omitempty"` + KeepPrivs bool `json:"keepPrivs,omitempty"` + NoPrivs bool `json:"noPrivs,omitempty"` + NoHome bool `json:"noHome,omitempty"` + NoInit bool `json:"noInit,omitempty"` + DeleteImage bool `json:"deleteImage,omitempty"` + Fakeroot bool `json:"fakeroot,omitempty"` + SignalPropagation bool `json:"signalPropagation,omitempty"` } // NewConfig returns singularity.EngineConfig with a parsed FileConfig @@ -498,3 +499,17 @@ func (e *EngineConfig) GetDeleteImage() bool { func (e *EngineConfig) SetDeleteImage(delete bool) { e.JSON.DeleteImage = delete } + +// SetSignalPropagation sets if engine must propagate signals from +// master process -> container process when PID namespace is disabled +// or from master process -> sinit process -> container +// process when PID namespace is enabled +func (e *EngineConfig) SetSignalPropagation(propagation bool) { + e.JSON.SignalPropagation = propagation +} + +// GetSignalPropagation returns if engine propagate signals across +// processes (see SetSignalPropagation) +func (e *EngineConfig) GetSignalPropagation() bool { + return e.JSON.SignalPropagation +}