@cryptotaxi247 / netdata-1 / commits / 6add43154

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