Guard taskKey with the mutex that already guards the task ID

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) <noreply@anthropic.com>
This commit is contained in:
Stefan Haller 2026-08-09 08:17:24 +02:00
parent 1868167a7a
commit 01600042cd

View file

@ -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()