FlawOpen
● Go Concurrency · Data Race

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.

Step 1

Share mutable state

Two or more goroutines receive or close over the same slice variable and backing array.

Step 2

Append concurrently

Each goroutine evaluates append(items, value). This reads and updates slice state and may write an element in the backing array.

Step 3

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.

Step 4

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

VULNERABLE PATTERN
// Caller shares the same slice variable across goroutines.
func appendUnsafe(items []int, value int) []int {
    return append(items, value)
}
HARDENED DEFENSE
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

← Browse Full Security Directory Explore Vulnerability Playbooks →