Two Jon Gjengset-style reviews across the full Rust codebase (~5300 lines). Review #1 found 18 issues, fixed 11. Review #2 found 10 new issues. All 17 issues resolved: 14 ✅ fixed, 3 ⏭️ deferred (1 LOW + 2 MEDIUM not-a-bug/not-worth-fixing).
- ⬜ Open
- 🔧 In Progress
- ✅ Fixed
- ⏭️ Deferred (won't fix now)
File: lib/src/hit.rs:189
Problem: duration.as_nanos() as u64 silently wraps for durations > ~584 years. Also used in metrics.rs for serialization of total, mean, duration, wait, and all percentile fields.
Fix: Replaced as u64 with u64::try_from(duration.as_nanos()).unwrap_or(u64::MAX) to saturate. 1 test added.
File: lib/src/metrics.rs:173
Problem: self.latencies.total / self.requests as u32 — the as u32 truncates, potentially to 0, causing divide-by-zero panic.
Fix: Replaced with self.latencies.total.div_f64(self.requests as f64). Test added that confirms no panic with requests > u32::MAX.
File: lib/src/attack.rs:30-43
Problem: All fields are pub, making the API fragile for library consumers. No builder or new() method.
Fix: Added AttackBuilder with Attack::builder(name, client, pacer, targets) constructor. Required fields (name, client, pacer, targets) are positional; optional fields (duration, workers, max_workers, timeout, max_body, redirects, chunked, stop) have sensible defaults. All fields now private.
File: lib/src/hit.rs:37-41
Problem: #[async_trait] adds a Box::pin allocation on every encode/decode call. In the attack hot loop, this is per-request overhead.
Fix: Removed the Codec trait entirely. JsonCodec, CsvCodec, MsgpackCodec now have inherent async fn encode/decode methods. The trait was never used for dynamic dispatch (dyn Codec), only to bring methods into scope. Zero boxing overhead.
File: lib/src/target.rs:29-37
Problem: Targets<R: AsyncBufRead> forces a type parameter even for Static which doesn't use R. This bleeds into Attack<C, P, R>.
Fix: Made TargetRead trait non-generic and object-safe (with Send bound). Targets::Lazy now stores Box<dyn TargetRead>. Erased R from Targets and Attack<C, P, R> → Attack<C, P>. Manual Debug and Default impls added for Targets.
File: lib/src/attack.rs:308-316
Problem: Every non-error response allocates a HashMap<String, Vec<String>> of response headers.
Resolution: Not worth fixing. Headers ARE used — serialized in every Hit output (JSON/CSV/msgpack). The HashMap allocation (~100-300 bytes) is negligible vs network I/O per request. Using HeaderMap directly would require custom serde impls for marginal gain.
File: lib/src/attack.rs:161
Problem: targets.lock().await.decode().await holds the Mutex guard across the .decode().await point. In lazy mode, this serializes all target reads and blocks all workers.
Resolution: Not a practical issue. Only the single pacer task calls decode; workers receive targets through a channel. The lock can't be split from decode since Lazy reader state requires exclusive access during the async read. Issue #23 fix (body cache) makes decode fast.
File: lib/src/attack.rs:317-319 (fast path) and 392-394 (slow path)
Problem: to_bytes(res.into_body()).await?.to_vec() reads the ENTIRE response body into memory, then truncates to max_body. A target returning gigabytes will OOM the attacker.
Fix: Replaced with read_body() streaming helper that accumulates chunk-by-chunk up to max_body bytes, then discards the rest. Also added drain_body() for redirect drain paths. 6 tests added.
File: lib/src/resolver.rs:310
Problem: tokio::task::spawn_blocking(move || dns_resolve(...)) creates a new OS thread for every DNS resolution when using --resolvers. At high RPS this exhausts thread pool limits.
Fix: Replaced dns_resolve with dns_resolve_async using tokio::net::UdpSocket and tokio::time::timeout. No more spawn_blocking in custom resolver path. 1 async integration test added with mock DNS server.
File: lib/src/resolver.rs:228
Problem: UdpSocket::bind("0.0.0.0:0") inside dns_resolve allocates a new ephemeral port and FD per query. Under load this exhausts port range and FD limits.
Fix: Replaced blocking std::net::UdpSocket with async tokio::net::UdpSocket. Socket is now lightweight async I/O instead of blocking OS thread + FD. Further optimization to share a socket pool deferred — async sockets are cheap and DNS caching (TTL) already reduces query volume.
File: lib/src/resolver.rs:239-251
Problem: The DNS response is parsed but the Transaction ID is never checked against the request's ID. Accepts spoofed DNS responses.
Fix: build_dns_query now returns (Vec<u8>, u16) with the transaction ID. parse_dns_response takes expected_id: u16 and rejects mismatches. 2 tests added: ID validation and unique ID generation.
File: lib/src/target.rs:221
Problem: @body_path syntax calls tokio::fs::read(body_path).await inside decode_http, which runs on every call in lazy mode — filesystem I/O per request under the targets lock.
Fix: Added body_cache: HashMap<String, Bytes> to TargetReaderInner. First @path read populates cache; subsequent reads for same path return cached Bytes::clone(). 1 test added verifying cache hit after file modification.
File: lib/src/pacer.rs:52
Problem: self.freq * (elapsed.as_nanos() / self.per.as_nanos()) — the integer division truncates to 0 for all t < per, then jumps.
Resolution: Not a bug. The expected_hits check is only a fast-path optimization; the interval/delta calculation on lines 58-69 correctly handles sub-period pacing regardless. Matches vegeta's Go implementation. The pacing behavior is correct.
File: lib/src/hit.rs:174-175
Problem: let len = u32::from_be_bytes(len_buf) as usize; let mut data = vec![0u8; len]; — a corrupted or malicious input declaring len = 4GB causes immediate OOM.
Fix: Added MAX_MSGPACK_FRAME = 64MB constant. Length prefix checked before allocation; returns error if exceeded. 1 test added.
File: lib/src/metrics.rs:177
Problem: std::mem::take(&mut self.latencies.latencies) destroys the latency data on first call. A second close() call produces zero percentiles silently.
Fix: Added closed: bool guard field. close() returns early if already closed. 1 test added.
File: lib/src/metrics.rs:148-149
Problem: hit.code.to_string() allocates a new String for every request to use as the BTreeMap<String, u64> key.
Fix: Changed to BTreeMap<u16, u64> with custom serialize_status_codes to maintain JSON compatibility. Zero allocations per add() call for status tracking.
File: lib/src/proxy.rs:233-247
Problem: stream.read_exact(&mut byte) in a loop — one syscall per byte to parse the CONNECT response headers.
Fix: Replaced with 512-byte chunk reads using stream.read(&mut chunk). Scans accumulated buffer for \r\n\r\n terminator. Also added EOF detection.