Ignore SIGURG on Linux.

In go1.14+, SIGURG is used by the runtime to handle preemtable system
calls.
In practice this signal caught *frequently*.

For reference:

https://go.googlesource.com/proposal/+/master/design/24543-non-cooperative-preemption.md
https://github.com/golang/go/issues/37942

Signed-off-by: Brian Goff <cpuguy83@gmail.com>
This commit is contained in:
Brian Goff 2021-01-13 17:45:35 +00:00
parent 9a3fdc1d64
commit fff164c22e
10 changed files with 196 additions and 32 deletions

View File

@ -97,7 +97,8 @@ func runAttach(dockerCli command.Cli, opts *attachOptions) error {
} }
if opts.proxy && !c.Config.Tty { if opts.proxy && !c.Config.Tty {
sigc := ForwardAllSignals(ctx, dockerCli, opts.container) sigc := notfiyAllSignals()
go ForwardAllSignals(ctx, dockerCli, opts.container, sigc)
defer signal.StopCatch(sigc) defer signal.StopCatch(sigc)
} }

View File

@ -32,6 +32,7 @@ type fakeClient struct {
containerExportFunc func(string) (io.ReadCloser, error) containerExportFunc func(string) (io.ReadCloser, error)
containerExecResizeFunc func(id string, options types.ResizeOptions) error containerExecResizeFunc func(id string, options types.ResizeOptions) error
containerRemoveFunc func(ctx context.Context, container string, options types.ContainerRemoveOptions) error containerRemoveFunc func(ctx context.Context, container string, options types.ContainerRemoveOptions) error
containerKillFunc func(ctx context.Context, container, signal string) error
Version string Version string
} }
@ -154,3 +155,10 @@ func (f *fakeClient) ContainerExecResize(_ context.Context, id string, options t
} }
return nil return nil
} }
func (f *fakeClient) ContainerKill(ctx context.Context, container, signal string) error {
if f.containerKillFunc != nil {
return f.containerKillFunc(ctx, container, signal)
}
return nil
}

View File

@ -131,7 +131,8 @@ func runContainer(dockerCli command.Cli, opts *runOptions, copts *containerOptio
return runStartContainerErr(err) return runStartContainerErr(err)
} }
if opts.sigProxy { if opts.sigProxy {
sigc := ForwardAllSignals(ctx, dockerCli, createResponse.ID) sigc := notfiyAllSignals()
go ForwardAllSignals(ctx, dockerCli, createResponse.ID, sigc)
defer signal.StopCatch(sigc) defer signal.StopCatch(sigc)
} }

View File

@ -0,0 +1,57 @@
package container
import (
"context"
"fmt"
"os"
gosignal "os/signal"
"github.com/docker/cli/cli/command"
"github.com/docker/docker/pkg/signal"
"github.com/sirupsen/logrus"
)
// ForwardAllSignals forwards signals to the container
//
// The channel you pass in must already be setup to receive any signals you want to forward.
func ForwardAllSignals(ctx context.Context, cli command.Cli, cid string, sigc <-chan os.Signal) {
var s os.Signal
for {
select {
case s = <-sigc:
case <-ctx.Done():
return
}
if s == signal.SIGCHLD || s == signal.SIGPIPE {
continue
}
// In go1.14+, the go runtime issues SIGURG as an interrupt to support pre-emptable system calls on Linux.
// Since we can't forward that along we'll check that here.
if isRuntimeSig(s) {
continue
}
var sig string
for sigStr, sigN := range signal.SignalMap {
if sigN == s {
sig = sigStr
break
}
}
if sig == "" {
fmt.Fprintf(cli.Err(), "Unsupported signal: %v. Discarding.\n", s)
continue
}
if err := cli.Client().ContainerKill(ctx, cid, sig); err != nil {
logrus.Debugf("Error sending signal: %s", err)
}
}
}
func notfiyAllSignals() chan os.Signal {
sigc := make(chan os.Signal, 128)
gosignal.Notify(sigc)
return sigc
}

View File

@ -0,0 +1,11 @@
package container
import (
"os"
"golang.org/x/sys/unix"
)
func isRuntimeSig(s os.Signal) bool {
return s == unix.SIGURG
}

View File

@ -0,0 +1,57 @@
package container
import (
"context"
"os"
"syscall"
"testing"
"time"
"github.com/docker/cli/internal/test"
"golang.org/x/sys/unix"
"gotest.tools/v3/assert"
)
func TestIgnoredSignals(t *testing.T) {
ignoredSignals := []syscall.Signal{unix.SIGPIPE, unix.SIGCHLD, unix.SIGURG}
for _, s := range ignoredSignals {
t.Run(unix.SignalName(s), func(t *testing.T) {
ctx, cancel := context.WithCancel(context.Background())
defer cancel()
var called bool
client := &fakeClient{containerKillFunc: func(ctx context.Context, container, signal string) error {
called = true
return nil
}}
cli := test.NewFakeCli(client)
sigc := make(chan os.Signal)
defer close(sigc)
done := make(chan struct{})
go func() {
ForwardAllSignals(ctx, cli, t.Name(), sigc)
close(done)
}()
timer := time.NewTimer(30 * time.Second)
defer timer.Stop()
select {
case <-timer.C:
t.Fatal("timeout waiting to send signal")
case sigc <- s:
case <-done:
}
// cancel the context so ForwardAllSignals will exit after it has processed the signal we sent.
// This is how we know the signal was actually processed and are not introducing a flakey test.
cancel()
<-done
assert.Assert(t, !called, "kill was called")
})
}
}

View File

@ -0,0 +1,9 @@
// +build !linux
package container
import "os"
func isRuntimeSig(_ os.Signal) bool {
return false
}

View File

@ -0,0 +1,48 @@
package container
import (
"context"
"os"
"testing"
"time"
"github.com/docker/cli/internal/test"
"github.com/docker/docker/pkg/signal"
)
func TestForwardSignals(t *testing.T) {
ctx, cancel := context.WithCancel(context.Background())
defer cancel()
called := make(chan struct{})
client := &fakeClient{containerKillFunc: func(ctx context.Context, container, signal string) error {
close(called)
return nil
}}
cli := test.NewFakeCli(client)
sigc := make(chan os.Signal)
defer close(sigc)
go ForwardAllSignals(ctx, cli, t.Name(), sigc)
timer := time.NewTimer(30 * time.Second)
defer timer.Stop()
select {
case <-timer.C:
t.Fatal("timeout waiting to send signal")
case sigc <- signal.SignalMap["TERM"]:
}
if !timer.Stop() {
<-timer.C
}
timer.Reset(30 * time.Second)
select {
case <-called:
case <-timer.C:
t.Fatal("timeout waiting for signal to be processed")
}
}

View File

@ -74,7 +74,8 @@ func runStart(dockerCli command.Cli, opts *startOptions) error {
// We always use c.ID instead of container to maintain consistency during `docker start` // We always use c.ID instead of container to maintain consistency during `docker start`
if !c.Config.Tty { if !c.Config.Tty {
sigc := ForwardAllSignals(ctx, dockerCli, c.ID) sigc := notfiyAllSignals()
ForwardAllSignals(ctx, dockerCli, c.ID, sigc)
defer signal.StopCatch(sigc) defer signal.StopCatch(sigc)
} }

View File

@ -95,32 +95,3 @@ func MonitorTtySize(ctx context.Context, cli command.Cli, id string, isExec bool
} }
return nil return nil
} }
// ForwardAllSignals forwards signals to the container
func ForwardAllSignals(ctx context.Context, cli command.Cli, cid string) chan os.Signal {
sigc := make(chan os.Signal, 128)
signal.CatchAll(sigc)
go func() {
for s := range sigc {
if s == signal.SIGCHLD || s == signal.SIGPIPE {
continue
}
var sig string
for sigStr, sigN := range signal.SignalMap {
if sigN == s {
sig = sigStr
break
}
}
if sig == "" {
fmt.Fprintf(cli.Err(), "Unsupported signal: %v. Discarding.\n", s)
continue
}
if err := cli.Client().ContainerKill(ctx, cid, sig); err != nil {
logrus.Debugf("Error sending signal: %s", err)
}
}
}()
return sigc
}