#754·vegeta

Data races in DNSCaching: shared cache slice mutation and unsafe rand.Rand

Author: queeliusCreated Mar 25, 2026Updated Mar 25, 2026

Bug Description

The DNSCaching option has two data races that are triggered under concurrent load (i.e., normal vegeta operation):

1. Shared DNS cache slice mutated in-place

resolver.LookupHost() returns a direct reference to the cached []string slice. The DialContext closure then mutates this shared slice via:

  • rng.Shuffle(len(ips), ...) -- reorders elements in-place
  • firstOfEachIPFamily(ips) -- uses the ips[:0] append trick, overwriting the backing array

When multiple goroutines resolve the same host concurrently, they all get the same slice and race on its elements. This can corrupt the DNS cache entries, leading to connections to wrong IPs.

2. Unsafe rand.Rand shared across goroutines

A private rand.New(rand.NewSource(...)) is created once and used inside the DialContext closure. rand.Rand is explicitly documented as not safe for concurrent use. Under concurrent dial attempts, this is a data race.

Reproduction

go
func TestDNSCaching_ConcurrentDialRace(t *testing.T) {
    srv := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {}))
    defer srv.Close()
    u, _ := url.Parse(srv.URL)
    target := "http://localhost:" + u.Port()

    atk := NewAttacker(DNSCaching(0))
    tr := NewStaticTargeter(Target{Method: "GET", URL: target})
    a := &attack{name: "dns-race-test", began: time.Now()}

    var wg sync.WaitGroup
    for i := 0; i < 20; i++ {
        wg.Add(1)
        go func() {
            defer wg.Done()
            atk.hit(tr, a)
        }()
    }
    wg.Wait()
}

Running with go test -race triggers:

WARNING: DATA RACE
Read at ... firstOfEachIPFamily() attack.go:419
Previous write at ... firstOfEachIPFamily() attack.go:426

Fix

  1. Copy the ips slice before mutating it
  2. Use the global rand.Shuffle (concurrency-safe) instead of a private rand.Rand

PR forthcoming.