mirror of
https://github.com/block/buzz.git
synced 2026-11-13 04:41:12 +00:00
## Problem Four request paths converted a client-supplied `usize` filter limit with an inline `as` cast before clamping: `(l as i64).min(max)` (REQ query params, bridge feed) and `(l as u32).min(max)` (WS NIP-50 search, bridge channel window). - A limit above `i64::MAX` turns negative (`usize::MAX as i64 == -1`). On REQ and the bridge `/query` general path it reaches Postgres as `LIMIT -1`, which fails the query. On the bridge feed it trips `feed_count >= limit`, so the feed comes back empty. - A limit that is a multiple of 2^32 wraps to `0` through `as u32`. That returns no rows (one row in the window, via `.max(1)`) instead of the page max. Bridge thread and HTTP search already clamped before casting. COUNT drops the row limit, so it was not affected. ## Change - Add `buzz_core::filter::clamp_limit(limit, default, max) -> u32`. It returns `default` when the limit is absent; otherwise it saturates the request to `u32` and then caps it at `max`. - Use it at every filter-limit site: `filter_to_query_params` and WS search in `handlers/req.rs`; channel window, feed, thread and HTTP search in `api/bridge.rs`. The inline casts are deleted, and `BRIDGE_FEED_MAX_LIMIT` is now `u32`. - Each site keeps its default, max and `limit: 0` behaviour. In-range limits are unchanged. ## Testing Failing before the fix (`cargo test -p buzz-relay --lib huge_filter_limit`): ``` assertion `left == right` failed left: Some(-1) right: Some(1000) test result: FAILED. 0 passed; 1 failed ``` After: - `cargo test -p buzz-core --lib clamp_limit` → 1 passed (table test covering absent, 0, in-range, at/over max, the u32 boundary and `usize::MAX`) - `cargo test -p buzz-relay --lib -- huge_filter_limit req:: bridge` → 152 passed, 0 failed (135 Postgres tests ignored) - `cargo clippy -p buzz-core -p buzz-relay --all-targets -- -D warnings` → clean; `cargo fmt --all` → clean - pre-push hooks (incl. rust-tests) passed ## Not covered No Postgres-backed tests were run locally. The bridge call sites rely on the shared helper's table test; they have no per-handler tests. ## Review Every server-side read of `Filter.limit` now goes through `clamp_limit`. The per-site default, max and `limit: 0` behaviour is unchanged. No inline casts are left. Scores: minimalism 9, elegance 9, correctness 9. Blockers: none. Fixed in review: - `PAGE_LIMIT_MAX` had been inserted between `filter_to_query_params`'s doc comment and the function, so the doc comment ended up on the const. Moved the const above it. - Corrected the problem statement. The bridge feed returned an empty feed rather than a Postgres error, COUNT was never affected, and thread and HTTP search were already safe. Optional: - WS search, bridge window and bridge feed have no per-handler regression test. Reverting one of them to an inline cast would not fail any test. - `bridge.rs` now has two functions called `clamp_limit`: the local moderation `clamp_limit(Option<i64>)` and `buzz_core::filter::clamp_limit`. They don't collide, but they are easy to confuse. ## Exercised on a local relay **Commit tested:** `88d76f7f1`. Dev profile, isolated harness relay on :3030, authenticated channel member. Human rate limits were raised so seeding wasn't throttled. I seeded 1,100 kind-9 events in one channel. The filters were sent as raw JSON so the large integers arrived exactly. | Path | `limit` 18446744073709551615 | `limit` 4294967296 | `limit` 50 (control) | |---|---|---|---| | WS `REQ` | EOSE after 1000 | EOSE after 1000 | EOSE after 50 | | WS `REQ` with `search` | EOSE after 1000 | EOSE after 1000 | EOSE after 50 | | HTTP `/query` | 200, 1000 events | 200, 1000 | 200, 50 | | HTTP `/query` with `search` | 200, 500 | 200, 500 | 200, 50 | | HTTP `/query` `top_level` (channel window) | 200, 201 (200 + bounds event) | 200, 201 | 200, 51 | | HTTP `/query` `feed_types` | 200, 100 | 200, 100 | 200, 50 | Every huge limit came back as that path's maximum. None of them produced a database error, and none came back empty. The relay log has no error lines, and the access log records the raw `limit` as 18446744073709551615 with `outcome:"served"`. **Postgres-gated tests:** the PR adds or modifies none. As a related check I ran `cargo test -p buzz-relay --lib -- --ignored limit`. Run in parallel, it gave 10 passed and 1 failed: `rate_limited_query_logs_one_denied_line`. That test passes on its own, and the whole set gives 11 passed with `--test-threads=1`. It looks like interference between tests that share the access-log capture, not a problem with this change. **Not exercised:** - Bridge thread path. - A before/after comparison against main. Signed-off-by: David Grochowski <dgrochowski@squareup.com>