Skip to content

Commit 469fa2c

Browse files
committed
Fix data race in use of cmd output buffers.
The bytes.Buffers used to store the output of subprocesses are managed with a sync.Pool. However, before this change, they were being returned to the Pool while the slice underlying the Buffer was still in use by caller methods. If that Buffer was then used by another caller, reads and writes to the underlying slice had the potential race with one another (which is detectable by running with the "-race" flag enabled in a binary and/or a test). This change puts the responsibility on the caller of cmdOutput to put the buffer back in the pool once it is done reading data from it, ensuring no two methods use its slice at the same time. Signed-off-by: Erik Sipsma <[email protected]>
1 parent a2952bc commit 469fa2c

File tree

3 files changed

+55
-15
lines changed

3 files changed

+55
-15
lines changed

runc.go

+24-14
Original file line numberDiff line numberDiff line change
@@ -17,6 +17,7 @@
1717
package runc
1818

1919
import (
20+
"bytes"
2021
"context"
2122
"encoding/json"
2223
"errors"
@@ -72,11 +73,12 @@ type Runc struct {
7273
// List returns all containers created inside the provided runc root directory
7374
func (r *Runc) List(context context.Context) ([]*Container, error) {
7475
data, err := cmdOutput(r.command(context, "list", "--format=json"), false)
76+
defer putBuf(data)
7577
if err != nil {
7678
return nil, err
7779
}
7880
var out []*Container
79-
if err := json.Unmarshal(data, &out); err != nil {
81+
if err := json.Unmarshal(data.Bytes(), &out); err != nil {
8082
return nil, err
8183
}
8284
return out, nil
@@ -85,11 +87,12 @@ func (r *Runc) List(context context.Context) ([]*Container, error) {
8587
// State returns the state for the container provided by id
8688
func (r *Runc) State(context context.Context, id string) (*Container, error) {
8789
data, err := cmdOutput(r.command(context, "state", id), true)
90+
defer putBuf(data)
8891
if err != nil {
89-
return nil, fmt.Errorf("%s: %s", err, data)
92+
return nil, fmt.Errorf("%s: %s", err, data.String())
9093
}
9194
var c Container
92-
if err := json.Unmarshal(data, &c); err != nil {
95+
if err := json.Unmarshal(data.Bytes(), &c); err != nil {
9396
return nil, err
9497
}
9598
return &c, nil
@@ -154,8 +157,9 @@ func (r *Runc) Create(context context.Context, id, bundle string, opts *CreateOp
154157

155158
if cmd.Stdout == nil && cmd.Stderr == nil {
156159
data, err := cmdOutput(cmd, true)
160+
defer putBuf(data)
157161
if err != nil {
158-
return fmt.Errorf("%s: %s", err, data)
162+
return fmt.Errorf("%s: %s", err, data.String())
159163
}
160164
return nil
161165
}
@@ -233,8 +237,9 @@ func (r *Runc) Exec(context context.Context, id string, spec specs.Process, opts
233237
}
234238
if cmd.Stdout == nil && cmd.Stderr == nil {
235239
data, err := cmdOutput(cmd, true)
240+
defer putBuf(data)
236241
if err != nil {
237-
return fmt.Errorf("%s: %s", err, data)
242+
return fmt.Errorf("%s: %s", err, data.String())
238243
}
239244
return nil
240245
}
@@ -399,11 +404,12 @@ func (r *Runc) Resume(context context.Context, id string) error {
399404
// Ps lists all the processes inside the container returning their pids
400405
func (r *Runc) Ps(context context.Context, id string) ([]int, error) {
401406
data, err := cmdOutput(r.command(context, "ps", "--format", "json", id), true)
407+
defer putBuf(data)
402408
if err != nil {
403-
return nil, fmt.Errorf("%s: %s", err, data)
409+
return nil, fmt.Errorf("%s: %s", err, data.String())
404410
}
405411
var pids []int
406-
if err := json.Unmarshal(data, &pids); err != nil {
412+
if err := json.Unmarshal(data.Bytes(), &pids); err != nil {
407413
return nil, err
408414
}
409415
return pids, nil
@@ -412,11 +418,12 @@ func (r *Runc) Ps(context context.Context, id string) ([]int, error) {
412418
// Top lists all the processes inside the container returning the full ps data
413419
func (r *Runc) Top(context context.Context, id string, psOptions string) (*TopResults, error) {
414420
data, err := cmdOutput(r.command(context, "ps", "--format", "table", id, psOptions), true)
421+
defer putBuf(data)
415422
if err != nil {
416-
return nil, fmt.Errorf("%s: %s", err, data)
423+
return nil, fmt.Errorf("%s: %s", err, data.String())
417424
}
418425

419-
topResults, err := ParsePSOutput(data)
426+
topResults, err := ParsePSOutput(data.Bytes())
420427
if err != nil {
421428
return nil, fmt.Errorf("%s: ", err)
422429
}
@@ -606,10 +613,11 @@ type Version struct {
606613
// Version returns the runc and runtime-spec versions
607614
func (r *Runc) Version(context context.Context) (Version, error) {
608615
data, err := cmdOutput(r.command(context, "--version"), false)
616+
defer putBuf(data)
609617
if err != nil {
610618
return Version{}, err
611619
}
612-
return parseVersion(data)
620+
return parseVersion(data.Bytes())
613621
}
614622

615623
func parseVersion(data []byte) (Version, error) {
@@ -687,15 +695,17 @@ func (r *Runc) runOrError(cmd *exec.Cmd) error {
687695
return err
688696
}
689697
data, err := cmdOutput(cmd, true)
698+
defer putBuf(data)
690699
if err != nil {
691-
return fmt.Errorf("%s: %s", err, data)
700+
return fmt.Errorf("%s: %s", err, data.String())
692701
}
693702
return nil
694703
}
695704

696-
func cmdOutput(cmd *exec.Cmd, combined bool) ([]byte, error) {
705+
// callers of cmdOutput are expected to call putBuf on the returned Buffer
706+
// to ensure it is released back to the shared pool after use.
707+
func cmdOutput(cmd *exec.Cmd, combined bool) (*bytes.Buffer, error) {
697708
b := getBuf()
698-
defer putBuf(b)
699709

700710
cmd.Stdout = b
701711
if combined {
@@ -711,5 +721,5 @@ func cmdOutput(cmd *exec.Cmd, combined bool) ([]byte, error) {
711721
err = fmt.Errorf("%s did not terminate successfully", cmd.Args[0])
712722
}
713723

714-
return b.Bytes(), err
724+
return b, err
715725
}

runc_test.go

+27-1
Original file line numberDiff line numberDiff line change
@@ -16,7 +16,11 @@
1616

1717
package runc
1818

19-
import "testing"
19+
import (
20+
"context"
21+
"sync"
22+
"testing"
23+
)
2024

2125
func TestParseVersion(t *testing.T) {
2226
const data = `runc version 1.0.0-rc3
@@ -37,3 +41,25 @@ spec: 1.0.0-rc5-dev`
3741
t.Errorf("expected spec version 1.0.0-rc5-dev but received %s", v.Spec)
3842
}
3943
}
44+
45+
func TestParallelCmds(t *testing.T) {
46+
rc := &Runc{
47+
// we don't need a real runc, we just want to test running a caller of cmdOutput in parallel
48+
Command: "/bin/true",
49+
}
50+
var wg sync.WaitGroup
51+
52+
ctx, cancel := context.WithCancel(context.Background())
53+
defer cancel()
54+
55+
for i := 0; i < 256; i++ {
56+
wg.Add(1)
57+
go func() {
58+
defer wg.Done()
59+
// We just want to fail if there is a race condition detected by
60+
// "-race", so we ignore the (expected) error here.
61+
_, _ = rc.Version(ctx)
62+
}()
63+
}
64+
wg.Wait()
65+
}

utils.go

+4
Original file line numberDiff line numberDiff line change
@@ -57,6 +57,10 @@ func getBuf() *bytes.Buffer {
5757
}
5858

5959
func putBuf(b *bytes.Buffer) {
60+
if b == nil {
61+
return
62+
}
63+
6064
b.Reset()
6165
bytesBufferPool.Put(b)
6266
}

0 commit comments

Comments
 (0)