channel: reclaim departed-member +f flood counters and expired +J entries on the tick (slow high-churn leak)

This commit is contained in:
Jean Chevronnet 2026-08-17 20:37:51 +00:00
parent 92a3f5ba86
commit d6cc813aad
3 changed files with 45 additions and 0 deletions

View file

@ -608,6 +608,28 @@ impl Server {
}
}
/// Drop per-member flood state left behind by users who have left a channel
/// (a slow leak on high-churn channels — Uids are never reused, so departed
/// entries never get overwritten), and +J rejoin-block entries whose window has
/// elapsed. Called from the background tick.
pub fn purge_flood_state(&mut self) {
let now = now();
for ch in self.channels.values_mut() {
// +f message-time counters only matter for current members
if !ch.msgflood_hits.is_empty() {
let members = &ch.members;
ch.msgflood_hits.retain(|uid, _| members.contains_key(uid));
}
// +J recent-kick timestamps only matter within the (current) rejoin-block
// window; once it's elapsed — or +J is off — the entry can't block anyone.
if !ch.recent_kicks.is_empty() {
let window = ch.modes.kicknorejoin.unwrap_or(0) as u64;
ch.recent_kicks
.retain(|_, ts| window > 0 && now.saturating_sub(*ts) < window);
}
}
}
/// Force `uid` out of `chan` (SAPART / SVSPART enforcement): announce the PART
/// to the channel and to links, drop the membership, reap the channel if empty.
/// No-op if the user isn't a member.

View file

@ -639,6 +639,7 @@ impl Ircd {
self.server.ping_links(); // keepalive on every server link
self.server.purge_xlines(); // drop expired server bans
self.server.purge_tbans(); // lift expired timed channel bans (TBAN)
self.server.purge_flood_state(); // reclaim per-member +f/+J state of departed users
for m in &mut self.modules {
m.on_tick(&mut self.server); // timer-driven modules (e.g. reputation)
}

View file

@ -1278,6 +1278,28 @@ mod tests {
Server::new(Config::default(), tx, Arc::new(AtomicU64::new(1)))
}
// The tick purge reclaims per-member flood state of users who left, and +J
// rejoin-block entries whose window elapsed — the slow high-churn leak.
#[test]
fn purge_flood_state_reclaims_departed_and_expired() {
use crate::channels::{Channel, Member};
let mut s = srv();
let mut c = Channel::new("#c");
c.members.insert(1, Member::default()); // uid 1 is a current member
c.msgflood_hits.insert(1, vec![now()]); // member -> kept
c.msgflood_hits.insert(99, vec![now()]); // departed -> dropped
c.modes.kicknorejoin = Some(60);
c.recent_kicks.insert(99, now().saturating_sub(120)); // expired -> dropped
c.recent_kicks.insert(88, now()); // still within window -> kept
s.channels.insert("#c".into(), c);
s.purge_flood_state();
let ch = &s.channels["#c"];
assert!(ch.msgflood_hits.contains_key(&1));
assert!(!ch.msgflood_hits.contains_key(&99), "departed member's +f state dropped");
assert!(!ch.recent_kicks.contains_key(&99), "expired +J entry dropped");
assert!(ch.recent_kicks.contains_key(&88), "fresh +J entry kept");
}
#[test]
fn resolved_host_applied_only_when_configured() {
let mut s = srv(); // use_resolved_host = true (default)