From 4f339be8790949cd2559a060f6c1d0d9a98a80ef Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Andr=C3=A9=20Roth?= Date: Mon, 25 May 2026 15:55:43 +0000 Subject: [PATCH] fix(task): Eliminate data race in RunTaskInBackground return value MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit RunTaskInBackground() previously returned *task AFTER releasing list.Lock() and sending the task to the consumer queue. This created a data race: 1. list.queue <- task (consumer receives) 2. Consumer: list.Lock() → task.State = RUNNING → list.Unlock() 3. RunTaskInBackground: return *task (struct copy WITHOUT lock) Steps 2 and 3 can execute concurrently — consumer writes task.State while RunTaskInBackground reads the entire struct via copy. Fix: Copy the task struct BEFORE unlocking, while list.Lock() is still held. At this point the task was just created and no other goroutine can access it, so the copy is guaranteed consistent (always State=IDLE). The returned copy is a snapshot of the initial task state, which is what callers expect — the task ID and name for tracking purposes. Safety invariant maintained: - I4: All struct copies happen while list.Lock() is held Changes: - task/list.go: RunTaskInBackground() copies *task before unlock, returns the pre-made copy instead of dereferencing after unlock --- task/list.go | 6 +++++- 1 file changed, 5 insertions(+), 1 deletion(-) diff --git a/task/list.go b/task/list.go index bd36afe2..4af459cd 100644 --- a/task/list.go +++ b/task/list.go @@ -211,6 +211,10 @@ func (list *List) RunTaskInBackground(name string, resources []string, process P list.wg.Add(1) task.wgTask.Add(1) + // Copy task while still holding the lock to avoid racing with consumer + // setting State=RUNNING after receiving from queue + taskCopy := *task + // add task to queue for processing if resources are available // if not, task will be queued by the consumer once resources are available tasks := list.usedResources.UsedBy(resources) @@ -223,7 +227,7 @@ func (list *List) RunTaskInBackground(name string, resources []string, process P list.Unlock() } - return *task, nil + return taskCopy, nil } // Clear removes finished tasks from list