Skip to content

fix(voice): correct ssrc↔user_id cache direction in DAVE decrypt fallback#3168

Closed
orarange wants to merge 1 commit intoPycord-Development:fix/voice-rec-2from
orarange:fix/ssrc-user-map-cache-direction
Closed

fix(voice): correct ssrc↔user_id cache direction in DAVE decrypt fallback#3168
orarange wants to merge 1 commit intoPycord-Development:fix/voice-rec-2from
orarange:fix/ssrc-user-map-cache-direction

Conversation

@orarange
Copy link

Summary

  • Fixed a cache write bug in decrypt_rtp() where a successfully inferred SSRC→user_id mapping was written to the wrong map (user_ssrc_map[uid] = ssrc) instead of ssrc_user_map[ssrc] = uid.
  • Added a for/else fallback that returns OPUS_SILENCE when all candidate user IDs fail to decrypt, preventing raw encrypted bytes from being passed downstream to sinks.

Problem

Inside PacketDecryptor.decrypt_rtp(), when ssrc_user_map does not yet contain a mapping for the incoming SSRC (a normal race with member_connect), the code iterates over every user ID known to the DAVE session and tries to decrypt with each one. On a successful decrypt it is supposed to cache ssrc → uid so the expensive scan is skipped on the next packet.

However, the cache write was inverted:

# Before (buggy)
self.client._connection.user_ssrc_map[int_uid] = packet.ssrc  # uid → ssrc (wrong map)

# After (fixed)
self.client._connection.ssrc_user_map[packet.ssrc] = int_uid  # ssrc → uid (correct map)

Because the write went into user_ssrc_map (uid→ssrc) instead of ssrc_user_map (ssrc→uid), the lookup on the next packet (state.ssrc_user_map.get(packet.ssrc)) always missed, causing the full brute-force loop to run on every single packet for the lifetime of the session.

Reproduction

Start voice receive in a DAVE-enabled VC. With DEBUG logging enabled, observe:

DAVE: inferred ssrc 12345 -> user_id 987654321 from decryption
DAVE: inferred ssrc 12345 -> user_id 987654321 from decryption
DAVE: inferred ssrc 12345 -> user_id 987654321 from decryption
... (repeats every packet indefinitely)

After this fix the log line appears only once per SSRC — the cached mapping is hit on all subsequent packets.

Changes

File Change
discord/voice/receive/reader.py Swap user_ssrc_map[uid] = ssrcssrc_user_map[ssrc] = uid
discord/voice/receive/reader.py Add for/else returning OPUS_SILENCE when all candidate decryptions fail

Why only this fix now

This is a clear, isolated implementation mistake — the wrong dict key/value order. It requires no new tests and is safe to merge independently.

The related epoch-transition passthrough (can_passthrough()) improvement involves more complex timing logic and needs separate testing and review. It will be submitted as a follow-up PR.

Related

…back

- `user_ssrc_map[uid] = ssrc` was written instead of `ssrc_user_map[ssrc] = uid`,
  causing the inferred mapping to be stored in the wrong dict and never hit on
  subsequent packets.
- Added a for/else fallback that returns OPUS_SILENCE when every candidate
  user_id fails to decrypt, preventing encrypted bytes from reaching sinks.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
@orarange orarange requested a review from a team as a code owner March 22, 2026 23:48
@orarange orarange requested review from Paillat-dev and removed request for a team March 22, 2026 23:48
Copilot AI review requested due to automatic review settings March 22, 2026 23:48
@github-project-automation github-project-automation bot moved this to Todo in Pycord Mar 22, 2026
@pycord-app
Copy link

pycord-app bot commented Mar 22, 2026

Thanks for opening this pull request!
Please make sure you have read the Contributing Guidelines and Code of Conduct.

This pull request can be checked-out with:

git fetch origin pull/3168/head:pr-3168
git checkout pr-3168

This pull request can be installed with:

pip install git+https://github.com/Pycord-Development/pycord@refs/pull/3168/head

Copy link
Contributor

Copilot AI left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Pull request overview

This PR targets DAVE voice receive decryption behavior in PacketDecryptor.decrypt_rtp() by improving the SSRC↔user_id inference path and avoiding downstream propagation of undecryptable payloads.

Changes:

  • Adjusts the inferred SSRC→user_id caching write during the DAVE “try all users” decrypt fallback.
  • Adds a for/else fallback to return OPUS_SILENCE when no candidate user ID can decrypt the packet.

💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.

)
# Successfully decrypted - cache the mapping for next time
self.client._connection.user_ssrc_map[int_uid] = packet.ssrc
self.client._connection.ssrc_user_map[packet.ssrc] = int_uid
Copy link

Copilot AI Mar 22, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

VoiceConnectionState.ssrc_user_map is a computed @property (return {v: k for k, v in self.user_ssrc_map.items()} in discord/voice/state.py), so mutating it here (ssrc_user_map[packet.ssrc] = int_uid) updates a temporary dict and does not persist. This means the cache will still miss on the next packet and the brute-force get_user_ids() scan will continue every time. To make caching effective, write to the canonical backing map (state.user_ssrc_map[int_uid] = packet.ssrc), or introduce a persistent SSRC→user_id map in the connection state and update that instead of the derived property.

Suggested change
self.client._connection.ssrc_user_map[packet.ssrc] = int_uid
self.client._connection.user_ssrc_map[int_uid] = packet.ssrc

Copilot uses AI. Check for mistakes.
@Paillat-dev
Copy link
Member

Paillat-dev commented Mar 22, 2026

Thanks but we'd prefer if humans did the work. Please keep the original pull request template.

@github-project-automation github-project-automation bot moved this from Todo to Done in Pycord Mar 22, 2026
@Paillat-dev Paillat-dev reopened this Mar 22, 2026
@Paillat-dev
Copy link
Member

Please note that I was planning on replacing this implémentation altogether (see my comment on the separate PR), to have some polling instead and wait for the dictionary to be populated by itself instead of brute forcing, even if that would mean delaying the processing of the first packets. I believe you also shared a similar approach in your previous comment

@plun1331 plun1331 closed this Mar 23, 2026
@orarange
Copy link
Author

Thanks for the review, and sorry for not following the PR template.

Good catch on the ssrc_user_map being a computed @property — I missed that it returns a new dict each time, so writing to it doesn't persist. The original write direction was actually correct; the real issue lies elsewhere.

I agree that replacing the brute-force scan with polling for the mapping to populate naturally is the better approach. I'll close this PR for now. I'm currently away from my desk, but once I'm back I'll test against #3159 properly and open a new PR with the template if I find anything worth contributing.

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

Labels

None yet

Projects

Status: Done

Development

Successfully merging this pull request may close these issues.

4 participants