How concurrent appends to Go slices mutate 3-word slice headers across goroutines, causing silent memory corruption, array reallocation desyncs, and blockchain consensus splits.
In Go, a 'slice' is not an array. It is a tiny 3-part index card pointing to an array in memory. If two workers try to write to the index card at the exact same microsecond, one worker makes the array bigger and points to a new room, while the other worker keeps writing to the old room. In 2020, this exact bug in Go-Ethereum caused different servers to calculate different numbers, splitting the entire Ethereum blockchain.
Multiple worker goroutines process incoming transactions or requests and append results directly to a shared slice: results = append(results, item).
The slice reaches its capacity limit (`len == cap`). Goroutine A triggers a reallocation, creating a brand-new backing array and updating its local register.
Goroutine B simultaneously appends using the old capacity, overwriting memory in the previous backing array or corrupting the slice length (`Len`).
Nodes running the desynchronized state evaluate different cryptographic state roots, fracturing consensus across the distributed network.
// VULNERABLE: Concurrent Slice Append Without Synchronization
package main
import (
"sync"
)
type Transaction struct {
Hash string
Amount int64
}
// CRITICAL FLAW (CVE-2020-26264):
// Concurrent goroutines calling append() on the same slice!
// Causes memory corruption, lost writes, and state hash divergences!
func ProcessTransactionsUnsafe(txs []Transaction) []Transaction {
var validTxs []Transaction
var wg sync.WaitGroup
for _, tx := range txs {
wg.Add(1)
go func(t Transaction) {
defer wg.Done()
if t.Amount > 0 {
// RACE CONDITION: Mutates 3-word slice header concurrently!
validTxs = append(validTxs, t)
}
}(tx)
}
wg.Wait()
return validTxs
}
// SECURE: Thread-Safe Mutex Critical Section Or Channel Fan-In
package main
import (
"sync"
)
type Transaction struct {
Hash string
Amount int64
}
// FIX PATTERN 1: Mutex Protected Append
type SafeTxPool struct {
mu sync.Mutex
txs []Transaction
}
func (p *SafeTxPool) Append(tx Transaction) {
p.mu.Lock()
defer p.mu.Unlock()
p.txs = append(p.txs, tx)
}
// FIX PATTERN 2: Idiomatic Go Channel Aggregation (Zero Shared Memory)
func ProcessTransactionsSafe(txs []Transaction) []Transaction {
txChan := make(chan Transaction, len(txs))
var wg sync.WaitGroup
for _, tx := range txs {
wg.Add(1)
go func(t Transaction) {
defer wg.Done()
if t.Amount > 0 {
txChan <- t
}
}(tx)
}
// Wait in background and close channel when complete
go func() {
wg.Wait()
close(txChan)
}()
var validTxs []Transaction
for t := range txChan {
validTxs = append(validTxs, t)
}
return validTxs
}
sync.Mutex or sync.RWMutex lock.go test -race ./....make([]T, 0, expectedCap)) if indices are written concurrently to distinct index slots.