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-placefirstOfEachIPFamily(ips)-- uses theips[: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
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:426Fix
- Copy the
ipsslice before mutating it - Use the global
rand.Shuffle(concurrency-safe) instead of a privaterand.Rand
PR forthcoming.
Source: tsenart/vegeta