CVE-2020-26264 / CVSS 9.8

Go Slice Concurrency Race (CVE-2020-26264): How Goroutine Desync Forked Ethereum

How concurrent appends to Go slices mutate 3-word slice headers across goroutines, causing silent memory corruption, array reallocation desyncs, and blockchain consensus splits.

💡 Plain English Explainer (ELI5)

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.

Core Concepts & Key Terms

Slice Header (reflect.SliceHeader)
A 3-word data structure containing a raw memory pointer (`Data uintptr`), element length (`Len int`), and capacity (`Cap int`).
Array Reallocation Desync
When `append()` exceeds capacity, Go allocates a new, larger backing array. In unsynchronized concurrency, one goroutine receives the new pointer while others write to the stale backing array.
Data Race
Two or more concurrent goroutines accessing the same memory location where at least one access is a write, without synchronization.
Go Race Detector (`-race`)
The built-in compiler instrumentation flag that monitors memory accesses and reports unsynchronized concurrent operations.

Step-by-Step Attack Flow

Step 1

1. Concurrent Slice Appending

Multiple worker goroutines process incoming transactions or requests and append results directly to a shared slice: results = append(results, item).

Step 2

2. Capacity Limit Triggered

The slice reaches its capacity limit (`len == cap`). Goroutine A triggers a reallocation, creating a brand-new backing array and updating its local register.

Step 3

3. Split Memory State

Goroutine B simultaneously appends using the old capacity, overwriting memory in the previous backing array or corrupting the slice length (`Len`).

Step 4

4. Consensus Split / Memory Corruption

Nodes running the desynchronized state evaluate different cryptographic state roots, fracturing consensus across the distributed network.

Source Code: Flaw vs. Secure Implementation

VULNERABLE PATTERN
// 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
}
HARDENED DEFENSE
// 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
}

Engineering Hardening Checklist

← Browse Full Security Directory Explore Vulnerability Playbooks →