From 01600042cdd8a73813263cdba71cf430503d0f53 Mon Sep 17 00:00:00 2001 From: Stefan Haller Date: Sun, 9 Aug 2026 08:17:24 +0200 Subject: [PATCH] Guard taskKey with the mutex that already guards the task ID MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit taskKey is written on the goroutine NewTask spawns, under taskIDMutex, but GetTaskKey read it without the lock — and the string renders in tasks_adapter.go call that from the UI thread while a previous task's goroutine may be writing. A Go string is a two-word value, so a torn read can pair one string's pointer with another's length and index out of bounds, not merely return the wrong key. Take the lock in GetTaskKey, and read the field directly at the one call site that already holds it. No test: the failure needs two goroutines to interleave inside a two-word assignment, which nothing can schedule deterministically. Co-Authored-By: Claude Opus 5 (1M context) --- pkg/tasks/tasks.go | 17 +++++++++++++---- 1 file changed, 13 insertions(+), 4 deletions(-) diff --git a/pkg/tasks/tasks.go b/pkg/tasks/tasks.go index df7791aaf..869f03dd5 100644 --- a/pkg/tasks/tasks.go +++ b/pkg/tasks/tasks.go @@ -61,9 +61,13 @@ type ViewBufferManager struct { writer io.Writer waitingMutex deadlock.Mutex - taskIDMutex deadlock.Mutex - Log *logrus.Entry - newTaskID int + // Guards newTaskID and taskKey, which identify the most recently requested + // task. Both are written on the goroutine NewTask spawns, and taskKey is + // read from the UI thread (GetTaskKey), so neither may be touched without + // holding this. + taskIDMutex deadlock.Mutex + Log *logrus.Entry + newTaskID int // The channel by which the currently-running task is told to read more // lines (e.g. as the user scrolls). Held in an atomic because it's swapped // out as tasks come and go while ReadLines/ReadToEnd read it from the UI @@ -115,6 +119,9 @@ type LinesToRead struct { } func (self *ViewBufferManager) GetTaskKey() string { + self.taskIDMutex.Lock() + defer self.taskIDMutex.Unlock() + return self.taskKey } @@ -488,7 +495,9 @@ func (self *ViewBufferManager) NewTask(f func(TaskOpts) error, key string) error return } - resetOrigin := self.GetTaskKey() != key && self.onNewKey != nil + // Read taskKey directly: we already hold the mutex that guards it, and + // GetTaskKey would take it again. + resetOrigin := self.taskKey != key && self.onNewKey != nil self.taskKey = key self.taskIDMutex.Unlock()