You signed in with another tab or window. Reload to refresh your session.You signed out in another tab or window. Reload to refresh your session.You switched accounts on another tab or window. Reload to refresh your session.Dismiss alert
Followup to #234 / [SEC-V-05]. The first PR for #234 caps individual entries (display name + typing channel) at 128 chars on receipt, which bounds per-entry memory. This issue tracks the remaining two mitigations needed to fully close the unbounded-growth surface.
The maps in question are ephemeral — never persisted to the DAG, but they live for the entire process lifetime, so an attacker who can deliver wire messages can still grow them by count even with per-entry caps in place:
state_actors::ProfileState.names: HashMap<EndpointId, String> — one entry per ProfileAnnounce sender. Each fresh attacker key = one new map entry. Last-write-wins per key, so old entries stick around forever.
state_actors::NetworkMeta.typing_peers: HashMap<EndpointId, (String, u64)> — one entry per peer that ever sent a TypingIndicator. Stale entries are filtered in the view layer (TYPING_INDICATOR_TTL_MS) but never removed from the map.
Work to do
Timer sweep on typing_peers. Drop entries older than ~TYPING_INDICATOR_TTL_MS (5s) on a periodic tick. Either piggy-back on the presence tick driver in connect.rs or run a small dedicated task. Today the view filters stale entries on render — which means the map is the source of truth for "everyone who ever typed" and grows unboundedly even with the per-entry cap from PR [SEC-V-05] ProfileState.names / ChatMetaState.typing_peers accept unbounded attacker-supplied strings #234.
Total-entries LRU cap on both maps. Pick a reasonable upper bound (suggested: ~10k entries each — comfortable headroom for a busy server but bounds memory at a few MB even with worst-case 128-char names). On overflow, evict the least-recently-touched entry. Crate lru is already used elsewhere in the workspace if we want a ready-made impl, otherwise a small hand-rolled wrapper around IndexMap is fine.
Add tests:
typing-sweep: insert N entries, advance time past TTL, run sweep, assert map empty.
LRU profile cap: insert cap + 1 distinct senders, assert map size == cap and oldest sender's entry is gone.
Notes
These mitigations are not blockers for the per-entry caps already shipped — they're additive defense in depth.
See the per-entry cap consts in crates/client/src/listeners.rs (MAX_DISPLAY_NAME_LEN, MAX_TYPING_CHANNEL_LEN) for the units / per-entry budget the LRU cap should size against.
Context
Followup to #234 / [SEC-V-05]. The first PR for #234 caps individual entries (display name + typing channel) at 128 chars on receipt, which bounds per-entry memory. This issue tracks the remaining two mitigations needed to fully close the unbounded-growth surface.
The maps in question are ephemeral — never persisted to the DAG, but they live for the entire process lifetime, so an attacker who can deliver wire messages can still grow them by count even with per-entry caps in place:
state_actors::ProfileState.names: HashMap<EndpointId, String>— one entry perProfileAnnouncesender. Each fresh attacker key = one new map entry. Last-write-wins per key, so old entries stick around forever.state_actors::NetworkMeta.typing_peers: HashMap<EndpointId, (String, u64)>— one entry per peer that ever sent aTypingIndicator. Stale entries are filtered in the view layer (TYPING_INDICATOR_TTL_MS) but never removed from the map.Work to do
typing_peers. Drop entries older than ~TYPING_INDICATOR_TTL_MS(5s) on a periodic tick. Either piggy-back on the presence tick driver inconnect.rsor run a small dedicated task. Today the view filters stale entries on render — which means the map is the source of truth for "everyone who ever typed" and grows unboundedly even with the per-entry cap from PR [SEC-V-05]ProfileState.names/ChatMetaState.typing_peersaccept unbounded attacker-supplied strings #234.lruis already used elsewhere in the workspace if we want a ready-made impl, otherwise a small hand-rolled wrapper aroundIndexMapis fine.cap + 1distinct senders, assert map size == cap and oldest sender's entry is gone.Notes
crates/client/src/listeners.rs(MAX_DISPLAY_NAME_LEN,MAX_TYPING_CHANNEL_LEN) for the units / per-entry budget the LRU cap should size against.ProfileState.names/ChatMetaState.typing_peersaccept unbounded attacker-supplied strings #234.