mirror of
https://github.com/vitorpamplona/amethyst.git
synced 2026-08-08 23:54:39 +00:00
fix(relay): make the per-connection auth set a copy-on-write snapshot
Audit finding on the StoreQueryContext seam: RelaySession kept authenticatedUsers as a plain mutable LinkedHashSet and handed out a live view through RequestContext. A store (or EventSource) reading the set during a long REQ replay — exactly what StoreQueryContext invites — could race a concurrent NIP-42 AUTH commit on another coroutine: iteration vs. add on an unsynchronized set is a ConcurrentModificationException or a torn read. The engine now swaps an immutable Set behind a @Volatile field on each AUTH (the only writer), so every read is a consistent snapshot and holding one across a replay is safe. AUTH is rare; the copy is off the hot path. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_011hH4RY2AUwfMZ54RkMiT45
This commit is contained in:
+13
-2
@@ -53,6 +53,7 @@ import kotlinx.coroutines.CoroutineStart
|
||||
import kotlinx.coroutines.Job
|
||||
import kotlinx.coroutines.channels.ClosedSendChannelException
|
||||
import kotlinx.coroutines.launch
|
||||
import kotlin.concurrent.Volatile
|
||||
import kotlin.concurrent.atomics.AtomicLong
|
||||
import kotlin.concurrent.atomics.ExperimentalAtomicApi
|
||||
|
||||
@@ -82,8 +83,18 @@ class RelaySession(
|
||||
* The authenticated-identity store for this connection. The engine is the
|
||||
* only writer (committed in [handleAuth] on a successful NIP-42 AUTH); the
|
||||
* policy and the data plane read it through [requestContext].
|
||||
*
|
||||
* Copy-on-write on purpose: readers run concurrently with the engine —
|
||||
* a REQ replay coroutine can hold the set (via `StoreQueryContext` or an
|
||||
* `EventSource` reading [RequestContext.authenticatedUsers]) while a
|
||||
* later AUTH frame commits a new identity. Each read hands out the
|
||||
* current **immutable** set, so an in-flight query keeps a consistent
|
||||
* snapshot instead of racing a mutating `LinkedHashSet`; `@Volatile`
|
||||
* makes the swapped reference visible across threads. AUTH is rare, so
|
||||
* the copy costs nothing on the hot path.
|
||||
*/
|
||||
private val authenticatedUsers = mutableSetOf<HexKey>()
|
||||
@Volatile
|
||||
private var authenticatedUsers = setOf<HexKey>()
|
||||
|
||||
/**
|
||||
* The per-connection scope. Handed to the [policy] at connect (so gating
|
||||
@@ -261,7 +272,7 @@ class RelaySession(
|
||||
|
||||
// Single, engine-side commit into the connection scope — after the full
|
||||
// chain approved and a verifying policy voted to record.
|
||||
if (record) authenticatedUsers.add(cmd.event.pubKey)
|
||||
if (record) authenticatedUsers = authenticatedUsers + cmd.event.pubKey
|
||||
|
||||
send(OkMessage(cmd.event.id, true, ""))
|
||||
}
|
||||
|
||||
+4
-1
@@ -63,7 +63,10 @@ interface RequestContext {
|
||||
* The pubkeys that have authenticated on this connection via NIP-42. Empty
|
||||
* when the connection is unauthenticated. Backed by the engine-owned scope
|
||||
* and read live, so a REQ that arrives after a successful AUTH sees the
|
||||
* freshly recorded pubkey(s).
|
||||
* freshly recorded pubkey(s). Each read returns an **immutable snapshot**
|
||||
* (the engine swaps the set copy-on-write on AUTH), so holding one across
|
||||
* a long replay is safe — it just won't grow if another AUTH lands
|
||||
* mid-query.
|
||||
*/
|
||||
val authenticatedUsers: Set<HexKey>
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user