Skip to content

Commit

Permalink
Ignore SIGURG on Linux.
Browse files Browse the repository at this point in the history
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
golang/go#37942

Signed-off-by: Brian Goff <cpuguy83@gmail.com>
(cherry picked from commit fff164c)
Signed-off-by: Sebastiaan van Stijn <github@gone.nl>
  • Loading branch information
cpuguy83 authored and thaJeztah committed Feb 4, 2021
1 parent 48d30b5 commit 3c87f01
Show file tree
Hide file tree
Showing 10 changed files with 196 additions and 32 deletions.
3 changes: 2 additions & 1 deletion cli/command/container/attach.go
Original file line number Diff line number Diff line change
Expand Up @@ -97,7 +97,8 @@ func runAttach(dockerCli command.Cli, opts *attachOptions) error {
}

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)
}

Expand Down
8 changes: 8 additions & 0 deletions cli/command/container/client_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -32,6 +32,7 @@ type fakeClient struct {
containerExportFunc func(string) (io.ReadCloser, error)
containerExecResizeFunc func(id string, options types.ResizeOptions) error
containerRemoveFunc func(ctx context.Context, container string, options types.ContainerRemoveOptions) error
containerKillFunc func(ctx context.Context, container, signal string) error
Version string
}

Expand Down Expand Up @@ -154,3 +155,10 @@ func (f *fakeClient) ContainerExecResize(_ context.Context, id string, options t
}
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
}
3 changes: 2 additions & 1 deletion cli/command/container/run.go
Original file line number Diff line number Diff line change
Expand Up @@ -131,7 +131,8 @@ func runContainer(dockerCli command.Cli, opts *runOptions, copts *containerOptio
return runStartContainerErr(err)
}
if opts.sigProxy {
sigc := ForwardAllSignals(ctx, dockerCli, createResponse.ID)
sigc := notfiyAllSignals()
go ForwardAllSignals(ctx, dockerCli, createResponse.ID, sigc)
defer signal.StopCatch(sigc)
}

Expand Down
57 changes: 57 additions & 0 deletions cli/command/container/signals.go
Original file line number Diff line number Diff line change
@@ -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
}
11 changes: 11 additions & 0 deletions cli/command/container/signals_linux.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,11 @@
package container

import (
"os"

"golang.org/x/sys/unix"
)

func isRuntimeSig(s os.Signal) bool {
return s == unix.SIGURG
}
57 changes: 57 additions & 0 deletions cli/command/container/signals_linux_test.go
Original file line number Diff line number Diff line change
@@ -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")
})
}
}
9 changes: 9 additions & 0 deletions cli/command/container/signals_notlinux.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,9 @@
// +build !linux

package container

import "os"

func isRuntimeSig(_ os.Signal) bool {
return false
}
48 changes: 48 additions & 0 deletions cli/command/container/signals_test.go
Original file line number Diff line number Diff line change
@@ -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")
}

}
3 changes: 2 additions & 1 deletion cli/command/container/start.go
Original file line number Diff line number Diff line change
Expand Up @@ -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`
if !c.Config.Tty {
sigc := ForwardAllSignals(ctx, dockerCli, c.ID)
sigc := notfiyAllSignals()
ForwardAllSignals(ctx, dockerCli, c.ID, sigc)
defer signal.StopCatch(sigc)
}

Expand Down
29 changes: 0 additions & 29 deletions cli/command/container/tty.go
Original file line number Diff line number Diff line change
Expand Up @@ -95,32 +95,3 @@ func MonitorTtySize(ctx context.Context, cli command.Cli, id string, isExec bool
}
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
}

0 comments on commit 3c87f01

Please sign in to comment.