[review] matrix-rtc: LiveKit RoomEvent::Disconnected is swallowed with no recovery (subscriber reaper can wipe real participants) #66
Labels
No labels
bug
duplicate
enhancement
help wanted
invalid
question
wontfix
No milestone
No project
No assignees
1 participant
Notifications
Due date
No due date set.
Dependencies
No dependencies set.
Reference
dark/nether-voicebridge#66
Loading…
Add table
Add a link
Reference in a new issue
No description provided.
Delete branch "%!s()"
Deleting a branch is permanent. Although the deleted branch may continue to exist for a short time before it actually gets removed, it CANNOT be undone in most cases. Continue?
A dropped LiveKit connection (SFU restart / network blip) is logged and the event task simply breaks — the RTC run loop is never told, so
joined/active_sessionstay set against a dead room with no reconnect or teardown. Worst case (subscriber): the still-armed stale-membership reaper compares live roster against the dead room and empties real humans'm.call.memberafter the grace. Highest-impact theme in the review.F7-4 — P2 / LIVE / CONFIRMED
Location:
crates/matrix-rtc/src/rtc/session.rs:407@968da57Defect: handle_room_events silently exits on RoomEvent::Disconnected without notifying its owner: rtc::run keeps
joined = true/active_session = Somewith a dead LiveKit room — there is no reconnect or teardown trigger, and the reaper tick (rtc/mod.rs:300-309) keeps comparing m.call.member state against the dead room's (now empty) remote_participants, so it starts emptying REAL users' call memberships after the 45s reap grace.Failure scenario: LiveKit SFU restarts mid-call → the subscriber room fires Disconnected and the event task returns → Matrix→Discord audio is silently dead for the rest of the call (puppets stay, fed nothing), and on the next reaper ticks every live human's m.call.member is classified phantom and emptied, flapping them out of the Element Call roster; recovery requires all humans to leave so call_active drops and a fresh join_livekit runs.
Verifier: Re-derived from code: (1) handle_room_events breaks out on RoomEvent::Disconnected with only a warn log (session.rs:405-408); the task handle is stored as
_event_taskand never awaited — LiveSession exposes no completion/health signal. (2) rtc::run's loop (rtc/mod.rs:201-311) transitions only on call_active_rx and shutdown; (true,true) is a no-op, sojoinedstays true andactive_sessionstays Some with a dead room — no reconnect or teardown path exists (the supervisor's "no reconnect yet" arm never fires because run never exits). (3) Puppets are deliberately kept on track-end (session.rs:300-304, "membership owns release"), so Matrix→Discord is silently dead with puppets present. (4) The reaper guard is onlyactive_session.is_some()(mod.rs:302). Verified against livekit rust-sdks v0.7.45: the SDK auto-reconnects before emitting Disconnected, and handle_restarting removes ALL remote participants; so in the reconnect-exhausted path (SFU outage beyond retry budget / expired lk-jwt on restart) remote_participants() is empty at Disconnected time → live_roster_mxids=∅ → classify_stale_memberships reaps every real user's m.call.member after the 45s grace (exact behavior pinned by the existing absent_user_reaped_only_after_grace test for an empty live set). Nuances that don't break the claim: a brief SFU blip that reconnects successfully self-heals (Disconnected never fires — trigger requires reconnect exhaustion); a direct disconnect without a restart phase leaves a frozen stale snapshot instead of an empty one (existing members spared, but new EC joiners get reaped); and in the empty-roster variant the reaper's own emptying flips call_active false → EC clients re-posting membership can accidentally trigger a fresh join, so recovery may be a roster flap rather than strictly "all humans leave" (frozen-snapshot variant matches the claim's recovery exactly). No test or doc comment pins the silent exit as intended. P2 LIVE as claimed.Regression test sketch: Not required (P2), but the reaper half is locally pinnable: in crates/matrix-rtc/src/matrix.rs tests (classify_stale_memberships module), the empty-live-set reap is already covered; the missing regression is at the session layer — extract the Disconnected handling into a testable seam (e.g. handle_room_events signals its exit via a watch/oneshot that rtc::run selects on) and add a test in rtc/session.rs asserting that feeding RoomEvent::Disconnected into handle_room_events fires the exit signal, plus a mod.rs-level test that the signal clears joined/active_session (stopping the reaper) or triggers rejoin.
F8-1 — P1 / LIVE / UNVERIFIED (needs live)
Location:
crates/matrix-rtc/src/rtc/session.rs:405@968da57Defect: RoomEvent::Disconnected is logged and swallowed (the event task just breaks): rtc/mod.rs's run loop is never told, so
joinedstays true, the subscriber never reconnects, and the still-armed reaper tick (rtc/mod.rs:300-309) compares live m.call.member state against the dead room's roster and empties REAL participants' memberships after reap_grace.Failure scenario: LiveKit SFU container restarts (or a network blip drops the room) mid-call with 3 humans present → Matrix→Discord audio goes silent with no recovery attempt; ~45 s later the reaper sees an empty live_roster_mxids() and wipes all 3 humans' m.call.member (visible roster disruption in Element Call); the bridge only recovers via the accidental call-active flap that wipe causes, churning every puppet.
Verifier: verifier unavailable
F8-4 — P2 / LIVE / UNVERIFIED (needs live)
Location:
crates/matrix-rtc/src/rtc/ghost.rs:1185@968da57Defect: A ghost's LiveKit RoomEvent::Disconnected is logged, the drain task breaks and closes the room, but run_ghost/pump_audio are never signalled — the ghost keeps 'publishing' into a dead source (capture_frame errors rate-limited to a warn every 50) with no reconnect, and its m.call.member stays posted.
Failure scenario: SFU drops one ghost's connection mid-call → that Discord speaker is silently inaudible in Element Call (while still shown as a call member) until the speaker leaves and rejoins the voice channel or the bridge restarts.
Verifier: verifier unavailable
Filed from an automated multi-agent code review of
master@968da57(8 scoped finders + adversarial verification). Every finding below was re-derived from the code by an independent verifier. Fixes for the confirmed local-verifiable defects (allocator anchor panic F1-1/F1-2/F1-3/F2-1/F2-2, capture-sink panic F2-7, config env-fallback F5-4, opt-out RMW race F5-2) already landed on branchrefactor/review-and-split.Shipped on master
05a8740(3 commits, rebased over #38's389a53d— testkit call site updated for the newconnect_and_publishsignature).What changed (all
crates/matrix-rtc/src/rtc/):session.rs:LiveSessiongains a dedicateddisconnectedtoken, fired whenhandle_room_eventsends (terminalDisconnectedor event-stream close).Reconnecting/Reconnectednow logged INFO.mod.rs:run()selects on that token → tears down (keeping puppet routes) + rejoins via the idempotentjoin_livekit; clears reaper grace clocks; keeps membership posted (no retract+repost flap). Reap arm skips while the disconnect signal is pending; amembership_lingeringflag + guarded(false,false)arm retract a membership if the call ends mid-recovery. Newkeys::PeerKeyCacherecords every delivered peer key and replays it into the fresh provider on rejoin (closes the post-rejoin decrypt gap without roster flicker).ghost.rs: drain loop extracted todrain_room_events(fires a per-attemptdisconnectedtoken);pump_audioreturnsPumpExit;run_ghostreconnect loop (backoff 2→30s, drops stale buffered frames) republishes on room death.Verified: workspace
cargo test+ clippy-D warningsgreen (matrix-rtc 81 tests, incl. 8 new).Live-proven end-to-end 2026-07-10 on staging (unencrypted Light Voice, both directions live; SFU severed via host-side
iptables REJECTto the SFU IP only — Matrix/Cloudflare untouched, no other participant affected):connection lost — SDK auto-reconnectingINFO fired, no false teardown — recovery is correctly gated on terminal Disconnected.LiveKit room disconnected — tearing down for rejoin→join_livekitretry; ghostghost LiveKit room disconnected — reconnecting→ backoff loop.m.call.memberreaps across the whole outage (the P1 harm this issue was filed for).@dark+first frame, ghostreconnected — resuming publish; audio confirmed both ways by the operator, 0 errors.Note: the encrypted-room path couldn't be used to validate because of a pre-existing, unrelated receive-side decryptor wedge discovered during this test (frames-before-key → permanent silence), now filed as #73. #66's recovery logic is media-E2EE-agnostic and its peer-key replay is unit-tested, so #66 is not blocked by #73.
Unreleased — rides in the next tag. Out of scope (left open): GhostPool supervision of self-exited ghost tasks (#67).