Bound the group-name claim queue
The sender decides how many claims land in it: anyone who can reach us can add one per envelope, and it is drained by a layer that may not run on every path — a poll that never reaches Session.receive, or a path added later that forgets. An unbounded list whose growth a stranger controls is a slow leak with somebody else's hand on the tap. Capped at 256, oldest dropped first, well above any real group count: a claim is only ever acted on for a group we are actually in, so the cap bounds the queue rather than rationing legitimate traffic. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
This commit is contained in:
co-authored by
Claude Opus 4.8
parent
41533baae6
commit
fc12aaa591
@@ -55,6 +55,21 @@ class Conversations(
|
||||
*/
|
||||
private val claimedGroupNames = mutableListOf<Triple<Long, String, String>>()
|
||||
|
||||
/**
|
||||
* ⚠️ **A cap, because the sender chooses how many of these to send.**
|
||||
* Anyone who can reach us can put one claim in this list per envelope, and
|
||||
* the list is only drained by a layer that may not run — a poll that never
|
||||
* reaches `Session.receive`, a code path added later that forgets. An
|
||||
* unbounded list whose growth a stranger controls is a slow memory leak
|
||||
* with somebody else's hand on the tap.
|
||||
*
|
||||
* Well above any real group count: a claim is only ever ACTED on for a
|
||||
* group we are actually in, and the cap exists to bound the queue, not to
|
||||
* ration legitimate traffic. Oldest goes first, since a newer claim is the
|
||||
* one more likely to still be true.
|
||||
*/
|
||||
private val maxClaimedGroupNames = 256
|
||||
|
||||
/** Take the pending claims, leaving none behind. */
|
||||
fun takeClaimedGroupNames(): List<Triple<Long, String, String>> =
|
||||
synchronized(claimedGroupNames) {
|
||||
@@ -1339,7 +1354,12 @@ class Conversations(
|
||||
val gid = content.raw?.optString("gid")?.takeIf { it.isNotEmpty() }
|
||||
val gname = content.raw?.optString("gname")?.takeIf { it.isNotEmpty() }
|
||||
if (gid != null && gname != null) {
|
||||
synchronized(claimedGroupNames) { claimedGroupNames += Triple(fromUser, gid, gname) }
|
||||
synchronized(claimedGroupNames) {
|
||||
claimedGroupNames += Triple(fromUser, gid, gname)
|
||||
while (claimedGroupNames.size > maxClaimedGroupNames) {
|
||||
claimedGroupNames.removeAt(0)
|
||||
}
|
||||
}
|
||||
}
|
||||
// 🔴 **The batch is stored first, and independently.** Peer
|
||||
// delivery keys live inside our OWN delivery record, so the
|
||||
|
||||
@@ -232,6 +232,27 @@ class GroupNameTest {
|
||||
assertEquals(0, server.requestCount)
|
||||
}
|
||||
|
||||
/**
|
||||
* ⚠️ **The sender decides how many claims to send, so the queue is capped.**
|
||||
* It is drained by a layer that may not run on every path, and an unbounded
|
||||
* list whose growth a stranger controls is a slow leak with someone else's
|
||||
* hand on the tap.
|
||||
*/
|
||||
@Test
|
||||
fun `the claim queue is bounded`() {
|
||||
val conversations = conversations(vault())
|
||||
repeat(400) { i ->
|
||||
conversations.consumeControl(99L, Content.parse(
|
||||
SealedSender.deliveryKeyEnvelope(
|
||||
ByteArray(32), groupId = "g$i", groupName = "n$i")))
|
||||
}
|
||||
|
||||
val drained = conversations.takeClaimedGroupNames()
|
||||
assertEquals(256, drained.size)
|
||||
// Oldest dropped, newest kept: a later claim is likelier to still hold.
|
||||
assertEquals("g399", drained.last().second)
|
||||
}
|
||||
|
||||
// -- Surviving a reinstall -------------------------------------------------
|
||||
|
||||
/**
|
||||
|
||||
Reference in New Issue
Block a user