Race in runner initialization when copying the active rate limiter
Description
runner.New copies the ratelimit.Limiter returned by its constructor into a value field. The constructor has already started a goroutine that updates the limiter's atomic token count. Copying that state races with the goroutine and leaves the runner with a separate snapshot of the counters.
The unlimited constructor replenishes its token count every millisecond, making the race reproducible during normal runner initialization. The per-second and per-minute branches also copy the active limiter.
Reproduction
On upstream dev at d3b9d3d3443cfbd32513999a6cef1e5ce8ba256d, without feature changes:
GOMAXPROCS=2 go test -p 2 -race ./runner -run TestRunner_duplicate -count=10Environment: Go 1.26.0, Linux amd64 (Ubuntu 24.04 under WSL).
This failed with 10 reports of the same race:
WARNING: DATA RACE
Write:
github.com/projectdiscovery/ratelimit.(*Limiter).run()
github.com/projectdiscovery/[email protected]/ratelimit.go:49
Previous read:
github.com/projectdiscovery/httpx/runner.New()
runner/runner.go:423Expected behavior
The runner should retain the pointer returned by the limiter constructor, sharing the live state with the worker and avoiding a copy of atomics after use. This is an internal initialization problem; no change to the CLI's rate-limit semantics is needed.
Source: projectdiscovery/httpx