summaryrefslogtreecommitdiff
path: root/internal
diff options
context:
space:
mode:
authorPaul Buetow <paul@buetow.org>2026-05-13 10:30:29 +0300
committerPaul Buetow <paul@buetow.org>2026-05-13 10:30:29 +0300
commit6d407096405520d5e157235e52773b9a4f3e4396 (patch)
tree9d12467565769c02a62ff60518e19d94f5984f85 /internal
parenta21c653c9939ac82b181709dc745f017fb3b8a8a (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.go24
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 {