Skip to content

timeafterleak (28th linter): 3 prod loop+select time.After sites leak a timer per iteration — convert to time.NewTimer/Stop, the [Content truncated due to length] #39180

@github-actions

Description

@github-actions

Summary

timeafterleak (the 28th custom analyzer, registered in cmd/linters/main.go:75, landed via #39133) flags time.After(...) used as the channel-receive in a select case enclosed by a for/range loop. Each loop iteration allocates a fresh *time.Timer that is not garbage-collected until it fires, even when another case (ctx.Done()) is selected first. The analyzer is not yet enforced in CI (.github/workflows/cgo.yml:1122 LINTER_FLAGS lists 13 flags — -httpnoctx was just added — but not -timeafterleak).

Running the analyzer's exact pattern against production reveals 3 real violations, all the canonical select { case <-ctx.Done(): case <-time.After(d): } poll/retry idiom inside a loop:

Site Loop Why it leaks
pkg/cli/docker_images.go:199 for attempt := ... retry loop w/ exponential backoff new timer every retry; ctx.Done() path leaks the pending timer
pkg/cli/add_interactive_workflow.go:41 for i := range 5 status poll new timer every poll iteration
pkg/cli/mcp_inspect_mcp_scripts_server.go:78 for time.Now().Before(deadline) readiness poll new timer every poll iteration

Two other time.After sites are correctly not flagged and need no change: pkg/cli/mcp_inspect_inspector.go:226 (select inside a go func(){}() — FuncLit boundary, no enclosing loop) and pkg/cli/update_check.go:315 (one-shot select, not in a loop).

Impact

Each affected loop holds one orphaned time.Timer per iteration until its duration elapses. With docker_images.go exponential backoff the durations grow (waitTime *= 2), so a cancelled multi-retry pull can pin several timers for tens of seconds. Low-severity but real per the analyzer's own contract, and these 3 sites block turning the linter on.

Recommendation

Minimal per-site fix (stop the timer on the preempting ctx.Done() path):

timer := time.NewTimer(d)
select {
case <-timer.C:
    // continue
case <-ctx.Done():
    timer.Stop()
    return ctx.Err()
}

Then append -timeafterleak to the LINTER_FLAGS list at cgo.yml:1122 so the pattern stays gone. This mirrors the just-landed -httpnoctx enforce-readiness flow (#39016) and earlier tolowerequalfold/strconvparse conversions.

Validation checklist

  • Convert all 3 sites to time.NewTimer + Stop() on the cancel path
  • make golint-custom LINTER_FLAGS="-timeafterleak -test=false" reports zero violations
  • Add -timeafterleak to cgo.yml:1122
  • Behavior unchanged (timeout semantics identical; only allocation lifetime differs)

Effort: Small–Moderate (3 mechanical conversions + 1 CI line).

Generated by Sergo R36.

Generated by 🤖 Sergo - Serena Go Expert · 237.8 AIC · ⌖ 12.9 AIC · ⊞ 5.3K ·

  • expires on Jun 20, 2026, 9:26 PM UTC-08:00

Metadata

Metadata

Labels

cookieIssue Monster Loves Cookies!sergo

Type

No type
No fields configured for issues without a type.

Projects

No projects

Milestone

No milestone

Relationships

None yet

Development

No branches or pull requests

Issue actions