Skip to content

feat(relay): per-variant wire-message size caps (closes #233)#366

Merged
intendednull merged 3 commits into
mainfrom
claude/audit-draft-issue-233-syncbatch-caps
Apr 25, 2026
Merged

feat(relay): per-variant wire-message size caps (closes #233)#366
intendednull merged 3 commits into
mainfrom
claude/audit-draft-issue-233-syncbatch-caps

Conversation

@intendednull
Copy link
Copy Markdown
Owner

@intendednull intendednull commented Apr 25, 2026

Defines per-variant size caps on WireMessage and enforces them in unpack_wire as defense-in-depth on top of the transport-level MAX_DESER_SIZE (256 KB) cap.

Chosen defaults

Cap Bytes Variants
Body 256 KB Event, SyncBatch, Worker — user-generated bodies + batched payloads
Default 64 KB ProfileAnnouncedisplay_name unbounded today
Announce 16 KB TopicAnnounce — sized to MAX_TOPICS short strings
Signaling 4 KB SyncRequest, TypingIndicator, VoiceJoin, VoiceLeave, VoiceSignal, JoinRequest, JoinResponse, JoinDenied — ids + short strings only

Where enforced

Centralized in unpack_wire (crates/common/src/wire.rs). Every decode site benefits with zero per-site plumbing:

  • crates/relay/src/lib.rstopic_announce_listener
  • crates/worker/src/actors/network.rsparse_worker_message, parse_server_message
  • crates/worker/src/actors/heartbeat.rs
  • All client-side decode paths (via willow-common)

Behavior on oversize

The decoded message is dropped and a tracing::warn! is emitted with variant, size, cap, and signer. unpack_wire returns None, which every existing call site already treats as "ignore this message".

Tradeoffs

Post-decode rather than pre-decode. A pre-decode cap would need to peek at the bincode variant tag before allocating, which means plumbing the variant discriminant out of unpack_envelope. Post-decode is simpler, still bounded above by MAX_DESER_SIZE, and makes the per-variant cap a defense-in-depth signal+drop rather than the primary allocation guard. Runner-up rejected because the wiring cost exceeded the marginal allocation savings (factor of 4-64x bounded vs. unbounded by 256 KB).

Tests

  • per_variant_caps_are_sized_appropriately — sanity-checks the ordering of the cap tiers.
  • oversize_signaling_message_is_rejected — packs an 8 KB TypingIndicator (within MAX_DESER_SIZE, over the 4 KB signaling cap) and confirms unpack_wire rejects it.
  • All 32 existing willow-common tests still pass.

cargo check -p willow-common -p willow-relay -p willow-worker is clean.

Closes #233.


Generated by Claude Code

@intendednull
Copy link
Copy Markdown
Owner Author

choose a reasonable default for the max size associated method and use that

Define per-variant size caps on `WireMessage` and enforce them in
`unpack_wire` as defense-in-depth on top of the transport-level
`MAX_DESER_SIZE` (256 KB) cap.

Caps:
- Event / SyncBatch / Worker: 256 KB (full envelope budget for body-
  carrying variants).
- ProfileAnnounce: 64 KB (default — display_name unbounded today).
- TopicAnnounce: 16 KB (sized to MAX_TOPICS short strings).
- SyncRequest, TypingIndicator, VoiceJoin/Leave/Signal,
  JoinRequest/Response/Denied: 4 KB (signaling / control plane only
  carries ids and short strings).

Enforcement is centralized in `unpack_wire`: any decoded message whose
re-serialized size exceeds its variant cap is dropped with
`tracing::warn!`. This means relay (`unpack_wire` in
`topic_announce_listener`), worker (`parse_worker_message`,
`parse_server_message`), client, and any future caller all benefit
without per-site changes.

Tradeoff: post-decode rather than pre-decode. A pre-decode cap would
need to read the bincode variant tag before allocating, which would
mean plumbing the variant discriminant out of `unpack_envelope`.
Post-decode is simpler, still bounded above by `MAX_DESER_SIZE`, and
makes the per-variant cap a defense-in-depth signal+drop rather than
the primary allocation guard. Acceptable.

Closes #233.

https://claude.ai/code/session_01RWdbBYuYhstPWaKYCaYAK4
@intendednull intendednull changed the title [draft] feat(relay): per-variant wire-message caps (refs #233) feat(relay): per-variant wire-message size caps (closes #233) Apr 25, 2026
@intendednull intendednull marked this pull request as ready for review April 25, 2026 18:49
Copy link
Copy Markdown
Owner Author

Done. Picked these defaults:

  • 256 KB (full envelope) for body-carrying variants: Event, SyncBatch, Worker.
  • 64 KB default for ProfileAnnounce (display_name unbounded today).
  • 16 KB for TopicAnnounce (sized to MAX_TOPICS short strings).
  • 4 KB for the signaling / control-plane variants: SyncRequest, TypingIndicator, VoiceJoin, VoiceLeave, VoiceSignal, JoinRequest, JoinResponse, JoinDenied.

Enforcement is centralized in unpack_wire so relay, worker, and client all pick it up automatically. Oversize messages get dropped with a tracing::warn! (variant + size + cap + signer). Post-decode rather than pre-decode — pre-decode would have meant plumbing the bincode variant tag out of unpack_envelope, which costs more than it buys given the transport-level 256 KB cap is still the primary allocation guard.

Two new tests cover the cap ordering and the oversize-rejection path. PR is out of draft.


Generated by Claude Code

The 16 KB ANNOUNCE_CAP introduced in the previous commit was sized
against an imagined "hundreds of topics" upper bound, but the relay's
actual limits are MAX_TOPICS = 10_000 with MAX_TOPIC_LEN = 256. Even
moderately-sized legitimate TopicAnnounce messages from a relay running
near MAX_TOPICS already exceed 16 KB, and the existing
`topic_announce_listener_enforces_max_topics_cap` test (which broadcasts
MAX_TOPICS + 1 short topics in a single announce) was being silently
dropped by the per-variant cap before the relay's loop could enforce
MAX_TOPICS — the real source of truth.

Fix: collapse TopicAnnounce into the body-carrying variant group with
the full MAX_DESER_SIZE budget. The relay's per-topic validation
(`topic_str_is_valid` + the MAX_TOPICS counter) does the real work for
this variant; the per-variant cap was just fighting legitimate traffic.

Also drop the now-unused `ANNOUNCE_CAP` constant and update
`per_variant_caps_are_sized_appropriately` to assert the new ordering
(signaling < profile < event) plus the explicit equality between
TopicAnnounce and the event-cap.

https://claude.ai/code/session_01RWdbBYuYhstPWaKYCaYAK4
@intendednull intendednull merged commit 1e1aa37 into main Apr 25, 2026
5 checks passed
@intendednull intendednull deleted the claude/audit-draft-issue-233-syncbatch-caps branch April 25, 2026 20:29
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

[SEC-V-02] 256 KB envelope cap is the only app-layer gate; SyncBatch.events has no element cap

2 participants