fix: agent security and linting improvements - #3831
Conversation
- Fix goroutine leak in RAG file watcher - Prevent NUL byte truncation injection in script shell env vars - Fix errorlint, noctx, and bodyclose linter warnings on Windows - Restore and manage necessary gosec directives
|
Same here @Piyush0049 Please provide more details about the issue you are trying to solve. Is it something which happens often? What are the symptoms? Right now the PR doesn't provide enough context to understand if you are fixing a bug or if these are just code reviews by an LLM which could easily hallucinate bugs. Thanks |
|
@aheritier here is a detailed file-by-file breakdown of the 6 files modified in this PR: 1. Bug Fixes & Security (2 Files)
2. Windows Cross-Platform Linter Cleanups (4 Files)
Summary (Before vs After)
|
docker-agent
left a comment
There was a problem hiding this comment.
Assessment: 🔴 CRITICAL
One high-severity confirmed bug was found in the new goroutine-lifecycle code added by this PR.
|
Thanks for the PR! One thing needs fixing before this can go in: In watchCtx, cancel := context.WithCancel(ctx)
t.cancelWatcher = cancel // set unconditionally
if err := t.manager.Initialize(ctx); err != nil {
cancel()
return ... // watcherDone is still nil
}
t.watcherDone = make(chan struct{})
// Stop():
if t.cancelWatcher != nil {
t.cancelWatcher()
<-t.watcherDone // nil channel -> blocks forever
}This matters because Reproduced locally on this branch with a mock strategy whose Note that commit ec033bb is what introduced it: it moved Minimal fix: if t.watcherDone != nil {
<-t.watcherDone
}Cleaner: create the channel up front and |
|
@dgageot I have pushed a commit addressing the changes you asked to do. Please do let me know if any other changes are required. |
Sayt-0
left a comment
There was a problem hiding this comment.
Review summary
The head of the branch builds, tests pass, and golangci-lint run is clean on both linux and GOOS=windows (verified: main shows 7 issues with GOOS=windows, this branch shows 0). The RAG lifecycle fix addresses a real leak: the manager events channel is never closed by Close(), so forwardEvents previously only exited via the parent context.
Four small changes are requested before merge, plus two non-blocking notes:
| # | File | Issue | Blocking |
|---|---|---|---|
| 1 | pkg/selfupdate/exec_windows.go |
exec.CommandContext(context.Background(), ...) bypasses noctx instead of following the repo convention (//nolint:noctx // reason) |
yes |
| 2 | pkg/desktop/transport/transport_test.go |
Change replaces a working, documented nolint with dead code; no linter flags this line on main | yes |
| 3 | pkg/tools/builtin/rag/rag_test.go |
Deadlock regression test cannot fail in bounded time (t.Context().Done() never fires during the test body) |
yes |
| 4 | pkg/selfupdate/exec_windows.go |
asExitError doc comment is now stale; helper reduced to a one-line wrapper |
yes |
| 5 | pkg/tools/builtin/rag/rag.go |
Stop() signals but does not await forwardEvents; callback may fire after Stop() returns |
no |
| 6 | pkg/tools/builtin/shell/script_shell.go |
NUL check does not cover toolset-level env; "silent truncation" framing is inaccurate for Go | no |
The //nolint:gosec annotations in the two cmd_windows.go files are correct and well justified, no action needed there.
| } | ||
|
|
||
| cmd := exec.Command(path, childArgs...) //nolint:gosec // path is our own freshly installed binary | ||
| cmd := exec.CommandContext(context.Background(), path, childArgs...) |
There was a problem hiding this comment.
exec.CommandContext(context.Background(), ...) silences noctx without changing behavior: the cmd.Cancel it installs can never fire. The repo convention for intentionally context-free commands is a //nolint:noctx directive with a reason, see pkg/tools/builtin/shell/shell.go:192, pkg/tools/builtin/backgroundjobs/backgroundjobs.go:223, pkg/shellpath/shellpath.go:117, pkg/tui/tui.go:3042.
| cmd := exec.CommandContext(context.Background(), path, childArgs...) | |
| cmd := exec.Command(path, childArgs...) //nolint:noctx // re-exec must outlive any request-scoped context |
Dropping the old //nolint:gosec is fine on its own since G204 is globally excluded in .golangci.yml.
| resp, err := ft.RoundTrip(req) | ||
| if resp != nil && resp.Body != nil { | ||
| resp.Body.Close() | ||
| } |
There was a problem hiding this comment.
This hunk should be reverted. resp is asserted Nil two lines below, so the close branch is unreachable dead code. The original //nolint:bodyclose // resp is nil on error, checked below is both accurate and currently silent: linting this package on main reports no issue here, on linux or with GOOS=windows.
| select { | ||
| case <-done: | ||
| // Success: Stop returned without deadlocking | ||
| case <-t.Context().Done(): | ||
| t.Fatal("Test context canceled") |
There was a problem hiding this comment.
t.Context() is only canceled after the test function returns, so <-t.Context().Done() can never fire while this select is blocking. If the deadlock regresses, the test hangs until the global go test timeout (10 minutes by default) instead of failing fast. A bounded timeout keeps the test useful as a deadlock detector:
select {
case <-done:
case <-time.After(5 * time.Second):
t.Fatal("Stop() deadlocked after a failed Start()")
}| // 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) | ||
| } |
There was a problem hiding this comment.
Two issues with the new form:
- The doc comment ("kept separate so exec_unix.go does not need to import errors") no longer matches the code, and
exec_unix.goalready importserrorsforerrors.Is(err, syscall.EXDEV). - The helper is now a one-line wrapper around
errors.Aswith no remaining purpose.
Simpler to delete the helper and inline the call at the call site:
var exitErr *exec.ExitError
if errors.As(err, &exitErr) {
os.Exit(exitErr.ExitCode())
}Behavior is equivalent here since cmd.Run() returns *exec.ExitError unwrapped.
| if t.cancelWatcher != nil { | ||
| t.cancelWatcher() | ||
| } | ||
| if t.watcherDone != nil { | ||
| <-t.watcherDone | ||
| } |
There was a problem hiding this comment.
Non-blocking: Stop() cancels watchCtx and waits for the StartFileWatcher goroutine, but forwardEvents is only signaled, not awaited. Since the events channel is buffered (500), a queued event can still invoke eventCallback after Stop() has returned and manager.Close() has run. If the goal is the clean termination described in the PR, tracking forwardEvents with the same done mechanism (or a sync.WaitGroup covering both goroutines) would close the gap. Otherwise the PR description should be softened to "signaled on Stop".
| 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 | ||
| } |
There was a problem hiding this comment.
Non-blocking, two notes on scope and framing:
- Coverage: only the per-tool
toolConfig.Envis checked. The toolset-level env from the sameconfig.yaml(NewToolSet, lines 32-39, viaenvironment.ExpandAll) reachesenvCopyunchecked. If NUL bytes are worth guarding against, that source should be covered too. - Framing: Go's
os/execalready rejects NUL in env,EINVALon Unix and "invalid environment variable" on Windows (Go 1.19.3+, CVE-2022-41716), so nothing is silently truncated. The check is still worthwhile as a clearer error message and matches the existing params-side guard below, but the "security fix" framing in the PR description overstates it.
- Use nolint:noctx for reExecProcess - Revert dead code in transport_test.go - Bound rag test deadlock timeout - Inline asExitError helper - Use WaitGroup for RAG Stop() synchronization - Guard toolset env against NUL bytes and reframe comment
- Use nolint:noctx for reExecProcess - Revert dead code in transport_test.go - Bound rag test deadlock timeout - Inline asExitError helper - Use WaitGroup for RAG Stop() synchronization - Guard toolset env against NUL bytes and reframe comment
3ee3168 to
58744ab
Compare
|
@Sayt-0 I have fixed this PR too. Please do let me now if any other changes are required. |
Description
This PR introduces critical fixes for agent stability and defense in depth security, alongside resolving native Windows linting warnings.
Key Changes
forwardEventsandStartFileWatchergoroutines are cleanly terminated whenStop()is called, preventing silent memory and file descriptor leaks across tool reloads.\x00validation to static environment variables inscript_shell.go. This prevents malicious or malformedconfig.yamlfiles from injecting NUL bytes that would silently truncate the process environment at the kernelexecveboundary.errorlint,noctx, andbodyclosewarnings in Windows specific files (usingerrors.As,context.Background(), and explicit body closure) and safely managedgosecdirectives to ensure a perfectly cleangolangci-lint runstate.Testing Instructions
golangci-lint runreports 0 issues.task testpasses successfully.