//go:build !windows package main // The real root-command starter: /bin/sh -c in its own process group, so a // deadline or the host going away ends every child with it. The daemon is // already root when this runs (handleRootExec checked), so nothing is // dropped or raised; the environment is a plain root login's, not the // daemon's own, so no credential the daemon holds in its environment leaks // into a command. import ( "errors" "io" "os" "os/exec" "os/user" "sync" "syscall" "time" ) type shellProc struct { cmd *exec.Cmd once sync.Once wait sync.Once code int } func startRootExec(command string, stdout, stderr io.Writer, cgroupFD int) (rootExecProc, error) { home := "/root" if u, err := user.Lookup("root"); err == nil && u.HomeDir != "" { home = u.HomeDir } if st, err := os.Stat(home); err != nil || !st.IsDir() { home = "/" } cmd := exec.Command("/bin/sh", "-c", command) cmd.Dir = home cmd.Env = []string{ "HOME=" + home, "USER=root", "LOGNAME=root", "SHELL=/bin/sh", "PATH=/usr/local/sbin:/usr/local/bin:/usr/sbin:/usr/bin:/sbin:/bin", } if lang := os.Getenv("LANG"); lang != "" { cmd.Env = append(cmd.Env, "LANG="+lang) } cmd.Stdin = nil // /dev/null: nothing reads from the host cmd.Stdout = stdout cmd.Stderr = stderr cmd.SysProcAttr = &syscall.SysProcAttr{Setpgid: true} // Inside the connection watch's cgroup from the start (rootexec_watch_linux.go). rootExecIntoCgroup(cmd.SysProcAttr, cgroupFD) // A child left in the background holding the output open must not hold // the answer for ever: once the shell has exited, its output gets this // long to close. Longer than rootExecKillGrace, so Kill's SIGKILL lands // before the output is cut off. cmd.WaitDelay = rootExecOutputGrace if err := cmd.Start(); err != nil { return nil, err } return &shellProc{cmd: cmd}, nil } func (p *shellProc) Wait() int { p.wait.Do(func() { err := p.cmd.Wait() p.code = 0 if err != nil { if ee, ok := err.(*exec.ExitError); ok { p.code = ee.ExitCode() if p.code < 0 { if st, ok := ee.Sys().(syscall.WaitStatus); ok && st.Signaled() { p.code = 128 + int(st.Signal()) } } } else if p.cmd.ProcessState != nil { // exec.ErrWaitDelay: the shell exited and a background child kept // the output open past the grace; the shell's own status stands. p.code = p.cmd.ProcessState.ExitCode() } else { p.code = 1 } } }) return p.code } // rootExecSignalGroup signals the process group pgid (sig 0 only asks whether // any member is left). A var so the real-shell tests can fence it. It never // takes a pgid of 0 or 1: kill(0) is the daemon's own group, kill(-1) every // process it can reach. var rootExecSignalGroup = func(pgid int, sig syscall.Signal) error { if pgid <= 1 { return syscall.EINVAL } return syscall.Kill(-pgid, sig) } // rootExecGroupPoll is how often Kill looks whether the group is gone. // rootExecKillSettle bounds the look after SIGKILL: a killed process ends as // soon as it next runs, but one whose new parent never collects it stays a // zombie, still counted in its group, so that look cannot wait for an empty // group the way the grace before it does. const ( rootExecGroupPoll = 25 * time.Millisecond rootExecKillSettle = 250 * time.Millisecond ) // Kill sends the whole group SIGTERM, and SIGKILL after rootExecKillGrace // unless every member has gone by then. What decides is the group, not the // shell: a child that ignores SIGTERM outlives its shell, and Wait returns as // soon as the shell has exited and the output has closed (or WaitDelay has // passed), so Wait returning says nothing about the rest of the group. // // Like a terminal's kill it does not reach what a command detached on purpose // (setsid, nohup into a new session, a systemd-run unit); the transcript // shows the command that did it. func (p *shellProc) Kill() { p.once.Do(func() { pgid := p.cmd.Process.Pid go p.Wait() // collect the shell, so a dead one does not keep the group alive _ = rootExecSignalGroup(pgid, syscall.SIGTERM) gone := false for end := time.Now().Add(rootExecKillGrace); !gone && time.Now().Before(end); { time.Sleep(rootExecGroupPoll) gone = errors.Is(rootExecSignalGroup(pgid, 0), syscall.ESRCH) } if !gone { _ = rootExecSignalGroup(pgid, syscall.SIGKILL) // SIGKILL cannot be caught, but a process ends only when it next // runs; the answer waits a moment for that, so the host is not told // a command is over while part of it is still running. for end := time.Now().Add(rootExecKillSettle); !gone && time.Now().Before(end); { gone = errors.Is(rootExecSignalGroup(pgid, 0), syscall.ESRCH) if !gone { time.Sleep(rootExecGroupPoll) } } } p.Wait() }) }