diff --git a/pkg/desktop/transport/transport_test.go b/pkg/desktop/transport/transport_test.go index b7f06211fd..19514e30c3 100644 --- a/pkg/desktop/transport/transport_test.go +++ b/pkg/desktop/transport/transport_test.go @@ -243,7 +243,10 @@ func TestFallbackTransport_NonSocketErrorDoesNotDisableProxy(t *testing.T) { req, err := http.NewRequestWithContext(t.Context(), http.MethodGet, "http://example.invalid/", http.NoBody) require.NoError(t, err) - resp, err := ft.RoundTrip(req) //nolint:bodyclose // resp is nil on error, checked below + resp, err := ft.RoundTrip(req) + if resp != nil && resp.Body != nil { + resp.Body.Close() + } require.Error(t, err) require.Nil(t, resp) assert.True(t, errors.Is(err, upstreamErr) || err.Error() == upstreamErr.Error()) diff --git a/pkg/selfupdate/exec_windows.go b/pkg/selfupdate/exec_windows.go index 6517efcc91..2e33567594 100644 --- a/pkg/selfupdate/exec_windows.go +++ b/pkg/selfupdate/exec_windows.go @@ -3,6 +3,8 @@ package selfupdate import ( + "context" + "errors" "fmt" "os" "os/exec" @@ -27,9 +29,9 @@ func swapBinary(dst, src string) error { if cpErr := atomicWriteFromFile(dst, src); cpErr != nil { // Roll back so we never leave the install without a binary. if rbErr := os.Rename(old, dst); rbErr != nil { - return fmt.Errorf("installing new binary: %w (copy fallback failed: %v; rollback also failed: %v)", err, cpErr, rbErr) + return fmt.Errorf("installing new binary: %w (copy fallback failed: %w; rollback also failed: %w)", err, cpErr, rbErr) } - return fmt.Errorf("installing new binary: %w (copy fallback failed: %v)", err, cpErr) + return fmt.Errorf("installing new binary: %w (copy fallback failed: %w)", err, cpErr) } _ = os.Remove(src) } @@ -48,7 +50,7 @@ func reExecProcess(path string, args, env []string) error { childArgs = args[1:] } - cmd := exec.Command(path, childArgs...) //nolint:gosec // path is our own freshly installed binary + cmd := exec.CommandContext(context.Background(), path, childArgs...) cmd.Env = env cmd.Stdin = os.Stdin cmd.Stdout = os.Stdout @@ -69,9 +71,5 @@ func reExecProcess(path string, args, env []string) error { // asExitError is a tiny helper kept separate so exec_unix.go does not need to // import errors solely for this Windows branch. func asExitError(err error, target **exec.ExitError) bool { - if e, ok := err.(*exec.ExitError); ok { //nolint:errorlint // direct type assertion is intentional here - *target = e - return true - } - return false + return errors.As(err, target) } diff --git a/pkg/tools/builtin/backgroundjobs/cmd_windows.go b/pkg/tools/builtin/backgroundjobs/cmd_windows.go index d25a83ef15..2cbb43fa86 100644 --- a/pkg/tools/builtin/backgroundjobs/cmd_windows.go +++ b/pkg/tools/builtin/backgroundjobs/cmd_windows.go @@ -31,13 +31,13 @@ func createProcessGroup(proc *os.Process) (*processGroup, error) { if _, err := windows.SetInformationJobObject( job, windows.JobObjectExtendedLimitInformation, - uintptr(unsafe.Pointer(&info)), + uintptr(unsafe.Pointer(&info)), //nolint:gosec // Windows API requires unsafe.Pointer uint32(unsafe.Sizeof(info))); err != nil { _ = windows.CloseHandle(job) return nil, err } - handle, err := windows.OpenProcess(windows.PROCESS_SET_QUOTA|windows.PROCESS_TERMINATE, false, uint32(proc.Pid)) + handle, err := windows.OpenProcess(windows.PROCESS_SET_QUOTA|windows.PROCESS_TERMINATE, false, uint32(proc.Pid)) //nolint:gosec // proc.Pid fits in uint32 on Windows if err != nil { _ = windows.CloseHandle(job) return nil, err diff --git a/pkg/tools/builtin/rag/rag.go b/pkg/tools/builtin/rag/rag.go index 8ca720492a..473067a389 100644 --- a/pkg/tools/builtin/rag/rag.go +++ b/pkg/tools/builtin/rag/rag.go @@ -49,6 +49,8 @@ type ToolSet struct { manager *rag.Manager toolName string eventCallback EventCallback + cancelWatcher context.CancelFunc + watcherDone chan struct{} } // Verify interface compliance. @@ -84,9 +86,15 @@ func (t *ToolSet) Start(ctx context.Context) error { return nil } + // We create a child context so we can explicitly cancel the watcher and event goroutines + // when Stop() is called, preventing goroutine leaks if the parent context outlives this toolset. + watchCtx, cancel := context.WithCancel(ctx) + t.cancelWatcher = cancel + t.watcherDone = make(chan struct{}) + // Forward RAG manager events if a callback is set. if t.eventCallback != nil { - go t.forwardEvents(ctx) + go t.forwardEvents(watchCtx) } if err := t.manager.Initialize(ctx); err != nil { @@ -94,8 +102,9 @@ func (t *ToolSet) Start(ctx context.Context) error { } go func() { - if err := t.manager.StartFileWatcher(ctx); err != nil { - slog.ErrorContext(ctx, "Failed to start RAG file watcher", "tool", t.toolName, "error", err) + defer close(t.watcherDone) + if err := t.manager.StartFileWatcher(watchCtx); err != nil && !errors.Is(err, context.Canceled) { + slog.ErrorContext(watchCtx, "Failed to start RAG file watcher", "tool", t.toolName, "error", err) } }() return nil @@ -106,6 +115,10 @@ func (t *ToolSet) Stop(_ context.Context) error { if t.manager == nil { return nil } + if t.cancelWatcher != nil { + t.cancelWatcher() + <-t.watcherDone + } return t.manager.Close() } diff --git a/pkg/tools/builtin/shell/cmd_windows.go b/pkg/tools/builtin/shell/cmd_windows.go index 05e11368ac..85d4145786 100644 --- a/pkg/tools/builtin/shell/cmd_windows.go +++ b/pkg/tools/builtin/shell/cmd_windows.go @@ -31,13 +31,13 @@ func createProcessGroup(proc *os.Process) (*processGroup, error) { if _, err := windows.SetInformationJobObject( job, windows.JobObjectExtendedLimitInformation, - uintptr(unsafe.Pointer(&info)), + uintptr(unsafe.Pointer(&info)), //nolint:gosec // Windows API requires unsafe.Pointer uint32(unsafe.Sizeof(info))); err != nil { _ = windows.CloseHandle(job) return nil, err } - handle, err := windows.OpenProcess(windows.PROCESS_SET_QUOTA|windows.PROCESS_TERMINATE, false, uint32(proc.Pid)) + handle, err := windows.OpenProcess(windows.PROCESS_SET_QUOTA|windows.PROCESS_TERMINATE, false, uint32(proc.Pid)) //nolint:gosec // proc.Pid fits in uint32 on Windows if err != nil { _ = windows.CloseHandle(job) return nil, err diff --git a/pkg/tools/builtin/shell/script_shell.go b/pkg/tools/builtin/shell/script_shell.go index efb3176299..e363b892aa 100644 --- a/pkg/tools/builtin/shell/script_shell.go +++ b/pkg/tools/builtin/shell/script_shell.go @@ -248,7 +248,11 @@ func (t *ScriptToolSet) execute(ctx context.Context, rt tools.Runtime, toolConfi // stay literal because env values may legitimately contain $ (issue // #2615). for _, key := range slices.Sorted(maps.Keys(toolConfig.Env)) { - envCopy = append(envCopy, key+"="+path.ExpandEnvRefs(toolConfig.Env[key])) + val := path.ExpandEnvRefs(toolConfig.Env[key]) + if strings.ContainsRune(val, 0) { + return tools.ResultError(fmt.Sprintf("configured environment variable %q contains a NUL byte", key)), nil + } + envCopy = append(envCopy, key+"="+val) } for key, value := range params { if value == nil {