#2653·httpx

Race in runner initialization when copying the active rate limiter

Author: root-ManasCreated Sep 27, 2026Updated Sep 27, 2026

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:

bash
GOMAXPROCS=2 go test -p 2 -race ./runner -run TestRunner_duplicate -count=10

Environment: 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:423

Expected 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.