1f22b15030
On Linux, when (os/exec.Cmd).SysProcAttr.Pdeathsig is set, the signal will be sent to the process when the OS thread on which cmd.Start() was executed dies. The runtime terminates an OS thread when a goroutine exits after being wired to the thread with runtime.LockOSThread(). If other goroutines are allowed to be scheduled onto a thread which called cmd.Start(), an unrelated goroutine could cause the thread to be terminated and prematurely signal the command. See https://github.com/golang/go/issues/27505 for more information. Prevent started subprocesses with Pdeathsig from getting signaled prematurely by wiring the starting goroutine to the OS thread until the subprocess has exited. No other goroutines can be scheduled onto a locked thread so it will remain alive until unlocked or the daemon process exits. Signed-off-by: Cory Snider <csnider@mirantis.com>
123 lines
2.9 KiB
Go
123 lines
2.9 KiB
Go
package portmapper
|
|
|
|
import (
|
|
"fmt"
|
|
"io"
|
|
"net"
|
|
"os"
|
|
"os/exec"
|
|
"runtime"
|
|
"strconv"
|
|
"syscall"
|
|
"time"
|
|
)
|
|
|
|
const userlandProxyCommandName = "docker-proxy"
|
|
|
|
func newProxyCommand(proto string, hostIP net.IP, hostPort int, containerIP net.IP, containerPort int, proxyPath string) (userlandProxy, error) {
|
|
path := proxyPath
|
|
if proxyPath == "" {
|
|
cmd, err := exec.LookPath(userlandProxyCommandName)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
path = cmd
|
|
}
|
|
|
|
args := []string{
|
|
path,
|
|
"-proto", proto,
|
|
"-host-ip", hostIP.String(),
|
|
"-host-port", strconv.Itoa(hostPort),
|
|
"-container-ip", containerIP.String(),
|
|
"-container-port", strconv.Itoa(containerPort),
|
|
}
|
|
|
|
return &proxyCommand{
|
|
cmd: &exec.Cmd{
|
|
Path: path,
|
|
Args: args,
|
|
SysProcAttr: &syscall.SysProcAttr{
|
|
Pdeathsig: syscall.SIGTERM, // send a sigterm to the proxy if the creating thread in the daemon process dies (https://go.dev/issue/27505)
|
|
},
|
|
},
|
|
wait: make(chan error, 1),
|
|
}, nil
|
|
}
|
|
|
|
// proxyCommand wraps an exec.Cmd to run the userland TCP and UDP
|
|
// proxies as separate processes.
|
|
type proxyCommand struct {
|
|
cmd *exec.Cmd
|
|
wait chan error
|
|
}
|
|
|
|
func (p *proxyCommand) Start() error {
|
|
r, w, err := os.Pipe()
|
|
if err != nil {
|
|
return fmt.Errorf("proxy unable to open os.Pipe %s", err)
|
|
}
|
|
defer r.Close()
|
|
p.cmd.ExtraFiles = []*os.File{w}
|
|
|
|
// As p.cmd.SysProcAttr.Pdeathsig is set, the signal will be sent to the
|
|
// process when the OS thread on which p.cmd.Start() was executed dies.
|
|
// If the thread is allowed to be released back into the goroutine
|
|
// thread pool, the thread could get terminated at any time if a
|
|
// goroutine gets scheduled onto it which calls runtime.LockOSThread()
|
|
// and exits without a matching number of runtime.UnlockOSThread()
|
|
// calls. Ensure that the thread from which Start() is called stays
|
|
// alive until the proxy or the daemon process exits to prevent the
|
|
// proxy from getting terminated early. See https://go.dev/issue/27505
|
|
// for more details.
|
|
started := make(chan error)
|
|
go func() {
|
|
runtime.LockOSThread()
|
|
defer runtime.UnlockOSThread()
|
|
err := p.cmd.Start()
|
|
started <- err
|
|
if err != nil {
|
|
return
|
|
}
|
|
p.wait <- p.cmd.Wait()
|
|
}()
|
|
if err := <-started; err != nil {
|
|
return err
|
|
}
|
|
w.Close()
|
|
|
|
errchan := make(chan error, 1)
|
|
go func() {
|
|
buf := make([]byte, 2)
|
|
r.Read(buf)
|
|
|
|
if string(buf) != "0\n" {
|
|
errStr, err := io.ReadAll(r)
|
|
if err != nil {
|
|
errchan <- fmt.Errorf("Error reading exit status from userland proxy: %v", err)
|
|
return
|
|
}
|
|
|
|
errchan <- fmt.Errorf("Error starting userland proxy: %s", errStr)
|
|
return
|
|
}
|
|
errchan <- nil
|
|
}()
|
|
|
|
select {
|
|
case err := <-errchan:
|
|
return err
|
|
case <-time.After(16 * time.Second):
|
|
return fmt.Errorf("Timed out proxy starting the userland proxy")
|
|
}
|
|
}
|
|
|
|
func (p *proxyCommand) Stop() error {
|
|
if p.cmd.Process != nil {
|
|
if err := p.cmd.Process.Signal(os.Interrupt); err != nil {
|
|
return err
|
|
}
|
|
return <-p.wait
|
|
}
|
|
return nil
|
|
}
|