Skip to content

Commit 1caf1a7

Browse files
author
Aaron Lehmann
committed
Lock to OS thread when Pdeathsig is set
See golang/go#27505 for context. Pdeathsig isn't safe to set without locking to the current OS thread, because otherwise thread termination will send the signal, which isn't the desired behavior. I discovered this while troubleshooting a problem that turned out to be unrelated, but I think it's necessary for correctness. Signed-off-by: Aaron Lehmann <[email protected]>
1 parent 2e979ca commit 1caf1a7

File tree

2 files changed

+85
-26
lines changed

2 files changed

+85
-26
lines changed

monitor.go

Lines changed: 45 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -18,6 +18,7 @@ package runc
1818

1919
import (
2020
"os/exec"
21+
"runtime"
2122
"syscall"
2223
"time"
2324
)
@@ -32,15 +33,18 @@ type Exit struct {
3233
Status int
3334
}
3435

35-
// ProcessMonitor is an interface for process monitoring
36+
// ProcessMonitor is an interface for process monitoring.
3637
//
3738
// It allows daemons using go-runc to have a SIGCHLD handler
3839
// to handle exits without introducing races between the handler
39-
// and go's exec.Cmd
40-
// These methods should match the methods exposed by exec.Cmd to provide
41-
// a consistent experience for the caller
40+
// and go's exec.Cmd.
41+
//
42+
// ProcessMonitor also provides a StartLocked method which is similar to
43+
// Start, but locks the goroutine used to start the process to an OS thread
44+
// (for example: when Pdeathsig is set).
4245
type ProcessMonitor interface {
4346
Start(*exec.Cmd) (chan Exit, error)
47+
StartLocked(*exec.Cmd) (chan Exit, error)
4448
Wait(*exec.Cmd, chan Exit) (int, error)
4549
}
4650

@@ -72,6 +76,43 @@ func (m *defaultMonitor) Start(c *exec.Cmd) (chan Exit, error) {
7276
return ec, nil
7377
}
7478

79+
// StartLocked is like Start, but locks the goroutine used to start the process to
80+
// the OS thread for use-cases where the parent thread matters to the child process
81+
// (for example: when Pdeathsig is set).
82+
func (m *defaultMonitor) StartLocked(c *exec.Cmd) (chan Exit, error) {
83+
started := make(chan error)
84+
ec := make(chan Exit, 1)
85+
go func() {
86+
runtime.LockOSThread()
87+
defer runtime.UnlockOSThread()
88+
89+
if err := c.Start(); err != nil {
90+
started <- err
91+
return
92+
}
93+
close(started)
94+
var status int
95+
if err := c.Wait(); err != nil {
96+
status = 255
97+
if exitErr, ok := err.(*exec.ExitError); ok {
98+
if ws, ok := exitErr.Sys().(syscall.WaitStatus); ok {
99+
status = ws.ExitStatus()
100+
}
101+
}
102+
}
103+
ec <- Exit{
104+
Timestamp: time.Now(),
105+
Pid: c.Process.Pid,
106+
Status: status,
107+
}
108+
close(ec)
109+
}()
110+
if err := <-started; err != nil {
111+
return nil, err
112+
}
113+
return ec, nil
114+
}
115+
75116
func (m *defaultMonitor) Wait(c *exec.Cmd, ec chan Exit) (int, error) {
76117
e := <-ec
77118
return e.Status, nil

runc.go

Lines changed: 40 additions & 22 deletions
Original file line numberDiff line numberDiff line change
@@ -61,11 +61,22 @@ const (
6161
type Runc struct {
6262
// Command overrides the name of the runc binary. If empty, DefaultCommand
6363
// is used.
64-
Command string
65-
Root string
66-
Debug bool
67-
Log string
68-
LogFormat Format
64+
Command string
65+
Root string
66+
Debug bool
67+
Log string
68+
LogFormat Format
69+
// PdeathSignal sets a signal the child process will receive when the
70+
// parent dies.
71+
//
72+
// When Pdeathsig is set, command invocations will call runtime.LockOSThread
73+
// to prevent OS thread termination from spuriously triggering the
74+
// signal. See https://github.com/golang/go/issues/27505 and
75+
// https://github.com/golang/go/blob/126c22a09824a7b52c019ed9a1d198b4e7781676/src/syscall/exec_linux.go#L48-L51
76+
//
77+
// A program with GOMAXPROCS=1 might hang because of the use of
78+
// runtime.LockOSThread. Callers should ensure they retain at least one
79+
// unlocked thread.
6980
PdeathSignal syscall.Signal // using syscall.Signal to allow compilation on non-unix (unix.Syscall is an alias for syscall.Signal)
7081
Setpgid bool
7182

@@ -83,7 +94,7 @@ type Runc struct {
8394

8495
// List returns all containers created inside the provided runc root directory
8596
func (r *Runc) List(context context.Context) ([]*Container, error) {
86-
data, err := cmdOutput(r.command(context, "list", "--format=json"), false, nil)
97+
data, err := r.cmdOutput(r.command(context, "list", "--format=json"), false, nil)
8798
defer putBuf(data)
8899
if err != nil {
89100
return nil, err
@@ -97,7 +108,7 @@ func (r *Runc) List(context context.Context) ([]*Container, error) {
97108

98109
// State returns the state for the container provided by id
99110
func (r *Runc) State(context context.Context, id string) (*Container, error) {
100-
data, err := cmdOutput(r.command(context, "state", id), true, nil)
111+
data, err := r.cmdOutput(r.command(context, "state", id), true, nil)
101112
defer putBuf(data)
102113
if err != nil {
103114
return nil, fmt.Errorf("%s: %s", err, data.String())
@@ -157,6 +168,13 @@ func (o *CreateOpts) args() (out []string, err error) {
157168
return out, nil
158169
}
159170

171+
func (r *Runc) startCommand(cmd *exec.Cmd) (chan Exit, error) {
172+
if r.PdeathSignal != 0 {
173+
return Monitor.StartLocked(cmd)
174+
}
175+
return Monitor.Start(cmd)
176+
}
177+
160178
// Create creates a new container and returns its pid if it was created successfully
161179
func (r *Runc) Create(context context.Context, id, bundle string, opts *CreateOpts) error {
162180
args := []string{"create", "--bundle", bundle}
@@ -174,14 +192,14 @@ func (r *Runc) Create(context context.Context, id, bundle string, opts *CreateOp
174192
cmd.ExtraFiles = opts.ExtraFiles
175193

176194
if cmd.Stdout == nil && cmd.Stderr == nil {
177-
data, err := cmdOutput(cmd, true, nil)
195+
data, err := r.cmdOutput(cmd, true, nil)
178196
defer putBuf(data)
179197
if err != nil {
180198
return fmt.Errorf("%s: %s", err, data.String())
181199
}
182200
return nil
183201
}
184-
ec, err := Monitor.Start(cmd)
202+
ec, err := r.startCommand(cmd)
185203
if err != nil {
186204
return err
187205
}
@@ -263,14 +281,14 @@ func (r *Runc) Exec(context context.Context, id string, spec specs.Process, opts
263281
opts.Set(cmd)
264282
}
265283
if cmd.Stdout == nil && cmd.Stderr == nil {
266-
data, err := cmdOutput(cmd, true, opts.Started)
284+
data, err := r.cmdOutput(cmd, true, opts.Started)
267285
defer putBuf(data)
268286
if err != nil {
269287
return fmt.Errorf("%w: %s", err, data.String())
270288
}
271289
return nil
272290
}
273-
ec, err := Monitor.Start(cmd)
291+
ec, err := r.startCommand(cmd)
274292
if err != nil {
275293
return err
276294
}
@@ -309,7 +327,7 @@ func (r *Runc) Run(context context.Context, id, bundle string, opts *CreateOpts)
309327
if opts != nil && opts.IO != nil {
310328
opts.Set(cmd)
311329
}
312-
ec, err := Monitor.Start(cmd)
330+
ec, err := r.startCommand(cmd)
313331
if err != nil {
314332
return -1, err
315333
}
@@ -382,7 +400,7 @@ func (r *Runc) Stats(context context.Context, id string) (*Stats, error) {
382400
if err != nil {
383401
return nil, err
384402
}
385-
ec, err := Monitor.Start(cmd)
403+
ec, err := r.startCommand(cmd)
386404
if err != nil {
387405
return nil, err
388406
}
@@ -404,7 +422,7 @@ func (r *Runc) Events(context context.Context, id string, interval time.Duration
404422
if err != nil {
405423
return nil, err
406424
}
407-
ec, err := Monitor.Start(cmd)
425+
ec, err := r.startCommand(cmd)
408426
if err != nil {
409427
rd.Close()
410428
return nil, err
@@ -448,7 +466,7 @@ func (r *Runc) Resume(context context.Context, id string) error {
448466

449467
// Ps lists all the processes inside the container returning their pids
450468
func (r *Runc) Ps(context context.Context, id string) ([]int, error) {
451-
data, err := cmdOutput(r.command(context, "ps", "--format", "json", id), true, nil)
469+
data, err := r.cmdOutput(r.command(context, "ps", "--format", "json", id), true, nil)
452470
defer putBuf(data)
453471
if err != nil {
454472
return nil, fmt.Errorf("%s: %s", err, data.String())
@@ -462,7 +480,7 @@ func (r *Runc) Ps(context context.Context, id string) ([]int, error) {
462480

463481
// Top lists all the processes inside the container returning the full ps data
464482
func (r *Runc) Top(context context.Context, id string, psOptions string) (*TopResults, error) {
465-
data, err := cmdOutput(r.command(context, "ps", "--format", "table", id, psOptions), true, nil)
483+
data, err := r.cmdOutput(r.command(context, "ps", "--format", "table", id, psOptions), true, nil)
466484
defer putBuf(data)
467485
if err != nil {
468486
return nil, fmt.Errorf("%s: %s", err, data.String())
@@ -647,7 +665,7 @@ func (r *Runc) Restore(context context.Context, id, bundle string, opts *Restore
647665
if opts != nil && opts.IO != nil {
648666
opts.Set(cmd)
649667
}
650-
ec, err := Monitor.Start(cmd)
668+
ec, err := r.startCommand(cmd)
651669
if err != nil {
652670
return -1, err
653671
}
@@ -691,7 +709,7 @@ type Version struct {
691709

692710
// Version returns the runc and runtime-spec versions
693711
func (r *Runc) Version(context context.Context) (Version, error) {
694-
data, err := cmdOutput(r.command(context, "--version"), false, nil)
712+
data, err := r.cmdOutput(r.command(context, "--version"), false, nil)
695713
defer putBuf(data)
696714
if err != nil {
697715
return Version{}, err
@@ -753,7 +771,7 @@ func (r *Runc) args() (out []string) {
753771
// <stderr>
754772
func (r *Runc) runOrError(cmd *exec.Cmd) error {
755773
if cmd.Stdout != nil || cmd.Stderr != nil {
756-
ec, err := Monitor.Start(cmd)
774+
ec, err := r.startCommand(cmd)
757775
if err != nil {
758776
return err
759777
}
@@ -763,7 +781,7 @@ func (r *Runc) runOrError(cmd *exec.Cmd) error {
763781
}
764782
return err
765783
}
766-
data, err := cmdOutput(cmd, true, nil)
784+
data, err := r.cmdOutput(cmd, true, nil)
767785
defer putBuf(data)
768786
if err != nil {
769787
return fmt.Errorf("%s: %s", err, data.String())
@@ -773,14 +791,14 @@ func (r *Runc) runOrError(cmd *exec.Cmd) error {
773791

774792
// callers of cmdOutput are expected to call putBuf on the returned Buffer
775793
// to ensure it is released back to the shared pool after use.
776-
func cmdOutput(cmd *exec.Cmd, combined bool, started chan<- int) (*bytes.Buffer, error) {
794+
func (r *Runc) cmdOutput(cmd *exec.Cmd, combined bool, started chan<- int) (*bytes.Buffer, error) {
777795
b := getBuf()
778796

779797
cmd.Stdout = b
780798
if combined {
781799
cmd.Stderr = b
782800
}
783-
ec, err := Monitor.Start(cmd)
801+
ec, err := r.startCommand(cmd)
784802
if err != nil {
785803
return nil, err
786804
}

0 commit comments

Comments
 (0)