fix(security): Tier C F-01/F-02/F-03 insider resource-exhaustion caps
A room ticket holder is an authenticated insider; signatures only prove keypair ownership, not a distinct human. Previously such a member could exhaust a victim's memory/tasks/dials without bound. Add caps + dedup at the gossip/core/UI boundaries (no wire/protocol change, no new deps): F-01 (gossip): cap the roster at MAX_ACTIVE_PEERS (32) — new authors are rejected when full, existing peers' updates always pass; sanitize each announced EndpointAddr (<=8 addrs, relay-URL <=256 bytes, drop Custom); replace (set_endpoint_info) instead of unioning attacker address history. F-02 (core): gate chat image auto-fetch — only roster authors qualify, (author, attachment_id) is deduped, and a 4-permit pool bounds concurrent detached fetch tasks (RAII AutoFetchGuard releases permit + dedup marker). Chat text is still shown (already sanitized); the user-initiated "Save" fetch is unchanged. Non-roster sock-puppet chat can no longer spawn tasks. F-03 (app): replace the unbounded AVATAR_HANDLE_CACHE map with a bounded, byte-equality-keyed LRU (avatar::ByteLru, cap 64) — fixes both unbounded growth from an endless stream of distinct valid avatars and the prior 64-bit-hash-collision-shows-wrong-avatar bug. Pure seams (sanitize_endpoint_addr, admit_into_roster, should_auto_fetch, ByteLru) + 6 adversarial/unit tests. 413 lib tests, clippy --all-targets clean, release build green. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
This commit is contained in:
+19
-14
@@ -5526,30 +5526,35 @@ fn avatar_badge<'a>(name: &str, key: &str, size: f32) -> Element<'a, AppMessage>
|
||||
.into()
|
||||
}
|
||||
|
||||
/// Maximum distinct avatar images we keep handles for. Each avatar is bounded to
|
||||
/// 48 KiB / 256×256 at ingest, so a 64-entry LRU caps this cache at a few MB
|
||||
/// regardless of how many distinct avatars peers publish over time (Tier C F-03).
|
||||
const AVATAR_CACHE_CAP: usize = 64;
|
||||
|
||||
thread_local! {
|
||||
/// Cache of avatar image handles, keyed by a hash of the PNG bytes, so the
|
||||
/// SAME `image::Handle` (and thus the same GPU texture id) is reused across
|
||||
/// Bounded cache of avatar image handles, keyed by PNG content, so the SAME
|
||||
/// `image::Handle` (and thus the same GPU texture id) is reused across
|
||||
/// redraws. `image::Handle::from_bytes` mints a fresh *unique* id on every
|
||||
/// call, so building handles inline in `view()` made iced re-upload the
|
||||
/// texture on every repaint — including the redraws fired on each mouse move —
|
||||
/// which showed up as constant flicker. Lives on the (single) UI thread.
|
||||
static AVATAR_HANDLE_CACHE: std::cell::RefCell<HashMap<u64, iced::widget::image::Handle>> =
|
||||
std::cell::RefCell::new(HashMap::new());
|
||||
/// which showed up as constant flicker. A peer can publish an unbounded stream
|
||||
/// of distinct valid avatars over a session, so the cache is an LRU (bounded +
|
||||
/// byte-equality keyed) rather than a plain map (Tier C F-03). Lives on the
|
||||
/// (single) UI thread.
|
||||
static AVATAR_HANDLE_CACHE:
|
||||
std::cell::RefCell<crate::avatar::ByteLru<iced::widget::image::Handle>> =
|
||||
std::cell::RefCell::new(crate::avatar::ByteLru::new(AVATAR_CACHE_CAP));
|
||||
}
|
||||
|
||||
/// A stable image handle for these exact PNG bytes (cached by content hash), so
|
||||
/// it keeps the same id across redraws — see [`AVATAR_HANDLE_CACHE`].
|
||||
/// A stable image handle for these exact PNG bytes (cached by content), so it
|
||||
/// keeps the same id across redraws — see [`AVATAR_HANDLE_CACHE`].
|
||||
fn cached_image_handle(bytes: bytes::Bytes) -> iced::widget::image::Handle {
|
||||
use std::hash::{Hash, Hasher};
|
||||
let mut hasher = std::collections::hash_map::DefaultHasher::new();
|
||||
bytes.as_ref().hash(&mut hasher);
|
||||
let key = hasher.finish();
|
||||
AVATAR_HANDLE_CACHE.with(|cache| {
|
||||
cache
|
||||
.borrow_mut()
|
||||
.entry(key)
|
||||
.or_insert_with(|| iced::widget::image::Handle::from_bytes(bytes))
|
||||
.clone()
|
||||
.get_or_insert(bytes.as_ref(), || {
|
||||
iced::widget::image::Handle::from_bytes(bytes.clone())
|
||||
})
|
||||
})
|
||||
}
|
||||
|
||||
|
||||
Reference in New Issue
Block a user