fix(go.d): fix goroutine leak and panic risk in Docker exec (#20881)
Ilya Mashchenko committed
Aug 24, 2025 at 11:17 UTC
6add43154c2d8e0a66885cf8e6c36fab29333346
1 file changed
+19
-16
src/go/plugin/go.d/pkg/dockerhost/dockerhost.go
+19
-16
@@ -8,6 +8,7 @@ import (
8
"fmt"
9
"os"
10
"strings"
11
+ "time"
12
13
typesContainer "github.com/docker/docker/api/types/container"
14
docker "github.com/docker/docker/client"
@@ -30,7 +31,6 @@ func FromEnv() string {
31
32
func Exec(ctx context.Context, container string, cmd string, args ...string) ([]byte, error) {
33
// based on https://github.com/moby/moby/blob/8e610b2b55bfd1bfa9436ab110d311f5e8a74dcb/integration/internal/container/exec.go#L38
33
-
34
addr := docker.DefaultDockerHost
35
if v := FromEnv(); v != "" {
36
addr = v
@@ -38,9 +38,8 @@ func Exec(ctx context.Context, container string, cmd string, args ...string) ([]
38
39
cli, err := docker.NewClientWithOpts(docker.WithHost(addr))
40
if err != nil {
41
- return nil, fmt.Errorf("failed to create docker client: %v", err)
41
+ return nil, fmt.Errorf("failed to create docker client: %w", err)
42
}
43
-
43
defer func() { _ = cli.Close() }()
44
45
cli.NegotiateAPIVersion(ctx)
@@ -53,45 +52,49 @@ func Exec(ctx context.Context, container string, cmd string, args ...string) ([]
52
53
createResp, err := cli.ContainerExecCreate(ctx, container, execCreateConfig)
54
if err != nil {
56
- return nil, fmt.Errorf("failed to container exec create ('%s'): %v", container, err)
55
+ return nil, fmt.Errorf("failed to container exec create (%s): %w", container, err)
56
}
57
58
attachResp, err := cli.ContainerExecAttach(ctx, createResp.ID, typesContainer.ExecAttachOptions{})
59
if err != nil {
61
- return nil, fmt.Errorf("failed to container exec attach ('%s'): %v", container, err)
60
+ return nil, fmt.Errorf("failed to container exec attach (%s): %w", container, err)
61
}
62
defer attachResp.Close()
63
64
var outBuf, errBuf bytes.Buffer
66
- done := make(chan error)
67
-
68
- defer close(done)
65
+ done := make(chan error, 1)
66
67
go func() {
68
_, err := stdcopy.StdCopy(&outBuf, &errBuf, attachResp.Reader)
72
- select {
73
- case done <- err:
74
- case <-ctx.Done():
75
- }
69
+ done <- err
70
}()
71
72
select {
73
case err := <-done:
74
if err != nil {
81
- return nil, fmt.Errorf("failed to read response from container ('%s'): %v", container, err)
75
+ return nil, fmt.Errorf("failed to read response from container (%s): %w", container, err)
76
}
77
case <-ctx.Done():
84
- return nil, fmt.Errorf("timed out reading response")
78
+ // Close connection to interrupt StdCopy
79
+ attachResp.Close()
80
+
81
+ select {
82
+ case <-done:
83
+ case <-time.After(150 * time.Millisecond):
84
+ // Don't wait too long, let it clean up in background
85
+ }
86
+
87
+ return nil, fmt.Errorf("timed out reading response: %w", ctx.Err())
88
}
89
90
inspResp, err := cli.ContainerExecInspect(ctx, createResp.ID)
91
if err != nil {
89
- return nil, fmt.Errorf("failed to container exec inspect ('%s'): %v", container, err)
92
+ return nil, fmt.Errorf("failed to container exec inspect (%s): %w", container, err)
93
}
94
95
if inspResp.ExitCode != 0 {
96
msg := strings.ReplaceAll(errBuf.String(), "\n", " ")
94
- return nil, fmt.Errorf("command returned non-zero exit code (%d), error: '%s'", inspResp.ExitCode, msg)
97
+ return nil, fmt.Errorf("command returned non-zero exit code (%d), error: %q", inspResp.ExitCode, msg)
98
}
99
100
return outBuf.Bytes(), nil