Go Slice Data Races: Safe Concurrent append in Go
Why concurrent append to a shared Go slice races, how to synchronize it, and how to test fixes with the Go race detector.
💡 Plain English Explainer (ELI5)
Imagine several workers writing names onto one shared list while moving it to a larger page when it fills. Without an agreed handoff, they can overwrite each other or disagree about which page is current. In Go, protect the shared slice variable and its backing array with one synchronization strategy.
Core Concepts & Key Terms
- Slice descriptor
- A slice value describes a segment of an array using a pointer, length, and capacity. The Go language specification does not promise a fixed in-memory layout for it.
- append
- The built-in append returns an updated slice value. It may reuse the backing array or allocate a new one when capacity is insufficient; callers must use the returned value.
- Data race
- Two goroutines access the same memory concurrently, at least one access writes, and no synchronization orders the accesses.
- Go race detector (-race)
- Go instrumentation that detects races exercised by a run; it cannot prove that unexecuted paths are race-free.
Step-by-Step Attack Flow
CVE correction: CVE-2020-26264 was a Geth denial-of-service flaw involving a malicious LES GetProofsV2 request, not concurrent slice appends. The distinct Geth consensus flaw was CVE-2020-26265 and involved account recreation after self-destruction, not a Go slice race.
Share mutable state
Two or more goroutines receive or close over the same slice variable and backing array.
Append concurrently
Each goroutine evaluates append(items, value). This reads and updates slice state and may write an element in the backing array.
Unsynchronized operations overlap
Conflicting accesses without a mutex, channel handoff, or other synchronization form a data race. The race result is not a valid coordination mechanism.
Serialize or assign ownership
Protect every access to the shared collection with the same lock, or send results to one goroutine that owns and updates the slice.
Go Code: Unsafe Shared Append vs. Mutex-Protected Collection
// Caller shares the same slice variable across goroutines.
func appendUnsafe(items []int, value int) []int {
return append(items, value)
}
package collector
import "sync"
type IntCollector struct {
mu sync.Mutex
items []int
}
func (c *IntCollector) Append(value int) {
c.mu.Lock()
defer c.mu.Unlock()
c.items = append(c.items, value)
}
func (c *IntCollector) Snapshot() []int {
c.mu.Lock()
defer c.mu.Unlock()
return append([]int(nil), c.items...)
}
The lock must cover every read and write of items, including snapshots. Preallocating capacity does not remove the race: the slice variable and backing-array elements remain shared mutable state. A channel fan-in is another valid design when one goroutine owns the result slice.
Engineering Hardening Checklist
- Give each mutable slice one goroutine owner, or guard every access with the same
sync.Mutex/sync.RWMutex. - Do not assume spare capacity makes concurrent calls to
appendsafe; the slice variable and element writes still require synchronization. - Run relevant tests and representative workloads with
go test -race ./...; treat it as a detector, not a proof. - Copy data before handing it to another goroutine when ownership must be independent; document whether returned slices alias internal storage.