Nightly bug-hunt flagged ServerBoss as a potential race condition: tasks (a mutableListOf) was iterated inside a scope.launch body that executed after the protecting mutex was already released, and serverJob = null was written in a finally block without holding the mutex.
Two races:
Tasks list: start() held the mutex only to call scope.launch { tasks.forEach { … } }. The coroutine body ran after the mutex was released, so a concurrent addTask() could mutate tasks mid-iteration, risking ConcurrentModificationException.
serverJob reset: serverJob = null in the finally block ran without the mutex. A concurrent start() could see a non-null serverJob and skip relaunching, even though the old job had already been cancelled but not yet cleared.
tasks while still inside mutex.withLock before launching the coroutine: val taskSnapshot = tasks.toList(). The coroutine iterates the immutable copy.serverJob = null cleanup in withContext(NonCancellable) { mutex.withLock { serverJob = null } } so the assignment is atomic with respect to the mutex even during cancellation.mutableList that is protected by a Mutex must only be iterated while holding that mutex (or via an immutable snapshot taken under the lock). Iterating it from a sibling coroutine that launched after the lock was released is equivalent to no protection.Job references reset in finally blocks are write-accessed from outside the lock elsewhere; protect them consistently with withContext(NonCancellable) { mutex.withLock { … } } to avoid a TOCTOU race between job-null check and assignment.