Skip to content

fix(voip): snapshot _remoteCandidates to avoid concurrent-modification in onAnswerReceived#2375

Open
nemeCIS6 wants to merge 1 commit into
famedly:mainfrom
nemeCIS6:fix/onanswerreceived-candidate-race
Open

fix(voip): snapshot _remoteCandidates to avoid concurrent-modification in onAnswerReceived#2375
nemeCIS6 wants to merge 1 commit into
famedly:mainfrom
nemeCIS6:fix/onanswerreceived-candidate-race

Conversation

@nemeCIS6

Copy link
Copy Markdown

Problem

CallSession.onAnswerReceived (and onNegotiateReceived) apply buffered ICE candidates with:

for (final candidate in _remoteCandidates) {
  await pc!.addCandidate(candidate);
}

Because the loop awaits inside a for-in over _remoteCandidates, an incoming trickle-ICE event during the await can mutate the same list — _remoteCandidates.add(...) in the candidate handler, or _remoteCandidates.clear() on hangup/restart. That throws:

Unhandled Exception: Concurrent modification during iteration: Instance(length:0) of '_GrowableList'.
#0  ListIterator.moveNext (dart:_internal/iterable.dart:365)
#1  CallSession.onAnswerReceived (package:matrix/src/voip/call_session.dart:400)
#2  VoIP.onCallAnswer (package:matrix/src/voip/voip.dart:621)

onAnswerReceived then aborts before applying the remaining candidates / sending select_answer, leaving the caller's ICE negotiation incomplete. On connections that rely on candidates arriving mid-loop (relay/TURN), the caller's media never fully connects — observed in production as frozen outgoing video. Reproduced live on an Android 16 device the instant the callee answered.

Fix

Iterate a snapshot so concurrent add/clear can't break the loop:

for (final candidate in _remoteCandidates.toList()) {
  await pc!.addCandidate(candidate);
}

Applied to both candidate-draining loops (onAnswerReceived and onNegotiateReceived). No behavior change beyond removing the race — any candidates added during the loop are still buffered in _remoteCandidates and applied on the next negotiation pass, as before.

🤖 Generated with Claude Code

…Received

The candidate loops await pc.addCandidate() inside a for-in over
_remoteCandidates. During the await, trickle-ICE events mutate the same
list (add at onCandidatesReceived, clear on hangup/restart), throwing
'Concurrent modification during iteration' — aborting onAnswerReceived
before the remaining candidates are applied, so the caller's media can
fail to connect (frozen video on networks that rely on late candidates).

Iterate a .toList() snapshot so concurrent add/clear can't break the loop.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
@CLAassistant

Copy link
Copy Markdown

CLA assistant check
Thank you for your submission! We really appreciate it. Like many open source projects, we ask that you sign our Contributor License Agreement before we can accept your contribution.
You have signed the CLA already but the status is still pending? Let us recheck it.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants