diff options
| author | Paul Buetow <paul@buetow.org> | 2026-05-13 10:30:29 +0300 |
|---|---|---|
| committer | Paul Buetow <paul@buetow.org> | 2026-05-13 10:30:29 +0300 |
| commit | 6d407096405520d5e157235e52773b9a4f3e4396 (patch) | |
| tree | 9d12467565769c02a62ff60518e19d94f5984f85 /internal | |
| parent | a21c653c9939ac82b181709dc745f017fb3b8a8a (diff) | |
fix: prevent goroutine leak in tuiTraceStarterFromRunTrace via done channel
Replace the buffered errCh (capacity 1) with an unbuffered channel plus a
dedicated done channel that is closed via defer when the outer function
returns for any reason (ctx cancellation, startedCh signal, or startup
error). The trace goroutine selects on both errCh and done when delivering
its result, guaranteeing it can always exit and is never left blocked
waiting on a channel that nobody will drain.
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Diffstat (limited to 'internal')
| -rw-r--r-- | internal/ior.go | 24 |
1 files changed, 21 insertions, 3 deletions
diff --git a/internal/ior.go b/internal/ior.go index a10f1c6..6e8111a 100644 --- a/internal/ior.go +++ b/internal/ior.go @@ -268,6 +268,12 @@ func makeTUIEventLoopConfigurer(ctx context.Context, cfg flags.Config, rt *tuiRu // makeTUIEventLoopConfigurer, and starts the trace in a goroutine, signalling // the TUI once BPF probes are attached (via startedCh) or returning an error // if startup fails. +// +// A dedicated done channel is closed by a defer when the outer function +// returns for any reason (ctx cancellation, successful start, or startup +// error). The trace goroutine selects on both errCh and done when delivering +// its result, so it can always exit regardless of which exit arm the outer +// caller took. func tuiTraceStarterFromRunTrace( baseCfg flags.Config, startTrace func(context.Context, flags.Config, chan<- struct{}, func(*eventLoop)) error, @@ -288,14 +294,26 @@ func tuiTraceStarterFromRunTrace( configureEl := makeTUIEventLoopConfigurer(ctx, cfg, rt) startedCh := make(chan struct{}) - errCh := make(chan error, 1) + // errCh carries at most one result from the trace goroutine to the + // outer select below. done is closed on return so the goroutine can + // always exit even when the outer caller already left via startedCh or + // ctx.Done() and nobody is draining errCh. + errCh := make(chan error) + done := make(chan struct{}) + defer close(done) + go func() { err := startTrace(ctx, cfg, startedCh, configureEl) if bindings, ok := runtime.RuntimeBindingsFromContext(ctx); ok { bindings.SetLiveFilterSetter(nil) } - errCh <- err - close(errCh) + // Deliver the result only if the caller is still selecting. + // done is closed when the outer function returns, so the goroutine + // will always proceed through this select and never block. + select { + case errCh <- err: + case <-done: + } }() select { |
