Skip to content

fix(send): lock per-device sessions across the group SKDM fan-out - #990

Merged
jlucaso1 merged 3 commits into
mainfrom
claude/fix-group-skdm-session-lock
Jul 6, 2026
Merged

fix(send): lock per-device sessions across the group SKDM fan-out#990
jlucaso1 merged 3 commits into
mainfrom
claude/fix-group-skdm-session-lock

Conversation

@jlucaso1

@jlucaso1 jlucaso1 commented Jul 6, 2026

Copy link
Copy Markdown
Collaborator

What

Hold the same per-device pairwise session locks the DM send path uses across the group SKDM fan-out, so a group send and a concurrent DM (or another group send) sharing a device can't advance that device's Signal ratchet at once.

Why (bug)

prepare_group_stanza's SKDM pairwise fan-out (encrypt_for_devices_with_sessions) mutates each target device's pairwise Signal session (load → advance chain → store, non-atomic). But it ran under only sender_key_lock(group + sender) — the per-(group,sender) chain lock. The DM path guards those same sessions with a disjoint mutex: session_lock_for(<device protocol addr>) (build_session_lock_keys / session_mutexes_for).

Two different lock keys ⇒ no mutual exclusion. Group sends aren't per-chat locked (matches WA Web), so a concurrent group send + DM — or two cold group sends sharing a recipient/own-companion device — could both read chain index N and both store N+1, dropping one advance. If the lost output was the SKDM, that member never receives the sender key and every subsequent skmsg to the group is undecryptable until a retry re-distributes. (Gated on a cold/evicted-session window; the warm cache path self-heals.)

How

  • New SendContextResolver::lock_device_sessions(&[Jid]) -> SessionLockGuard hook, default no-op (tests/benches don't race). SessionLockGuard is an opaque RAII holder in wacore; the concrete guards live in the platform crate (the per-address lock cache isn't part of the portable core).
  • Client implements it by reusing build_session_lock_keys + session_mutexes_for — the exact same key derivation (resolve_encryption_jid, sorted by cmp_for_lock_order) and ordering the DM path uses, so both serialize on the identical mutexes. Consistent global lock order ⇒ no deadlock; the group path acquires sender_key_lock → session locks, and no path acquires the reverse.
  • prepare_group_stanza holds the guard across the fan-out only; the sender_key_lock still covers the sender-key chain itself. Zero overhead on warm sends (no SKDM targets ⇒ hook not reached).

Tests

  • group_skdm_lock_shares_dm_per_device_session_mutex — acquiring the group SKDM lock for a device makes the DM path's per-device mutex (session_mutexes_for) contended, and it releases when the guard drops. Proves both paths share the same mutex.
  • cargo fmt / clippy clean; full wacore send suite (95) + main-crate send tests (89) pass.

The group SKDM pairwise fan-out (encrypt_for_devices_with_sessions) mutates each
target device's pairwise Signal session, but it ran under only the per-(group,
sender) sender_key_lock — never the per-device session lock the DM send path
holds. Two different lock keys mean no mutual exclusion, so a concurrent group
send + DM (or two cold group sends sharing a recipient/own-companion device) could
both read chain index N and both store N+1, dropping one ratchet advance. If the
lost output was the SKDM, that member never gets the sender key and every
subsequent skmsg to the group is undecryptable until a retry re-distributes.

Hold the same per-device session locks the DM path uses across the fan-out, via a
new SendContextResolver::lock_device_sessions hook (default no-op) that the Client
implements by reusing build_session_lock_keys + session_mutexes_for — identical key
derivation (resolve_encryption_jid, sorted by cmp_for_lock_order), so both paths
serialize on the same mutexes with no deadlock. The sender_key_lock still covers
the sender-key chain itself.
@chatgpt-codex-connector

Copy link
Copy Markdown

You have reached your Codex usage limits for code reviews. You can see your limits in the Codex usage dashboard.
To continue using code reviews, add credits to your account and enable them for code reviews in your settings.

@coderabbitai

coderabbitai Bot commented Jul 6, 2026

Copy link
Copy Markdown

Review Change Stack

Warning

Review limit reached

@jlucaso1, you've reached your PR review limit, so we couldn't start this review.

Next review available in: 23 minutes

Enable usage-based reviews in Billing to review now. Otherwise, wait until the next included review is available.
You're only billed for reviews past your plan's rate limits ($0.25/file).

How can I continue?

After more reviews become available, a review can be triggered using the @coderabbitai review command as a PR comment. Alternatively, push new commits to this PR.

To avoid repeated limits, reduce automatic review volume by pausing incremental auto-reviews earlier, using label-based review opt-in, excluding WIP or generated PR titles, or requesting reviews manually when the PR is ready. If your team needs uninterrupted high-volume reviews, an organization admin can enable usage-based reviews.

How do review limits work?

CodeRabbit enforces per-developer PR review limits for each organization. Most developers receive the normal plan review availability.

For paid Pro and Pro+ PR reviews, CodeRabbit uses adaptive limits for sustained high-volume activity. When a developer's recent PR review activity reaches the 95th percentile or higher among CodeRabbit users, additional reviews become available more gradually as earlier reviews age out of the rolling window.

Please refer docs for additional details.

Review details
⚙️ Run configuration

Configuration used: Repository UI (base), Organization UI (inherited)

Review profile: ASSERTIVE

Plan: Pro Plus

Run ID: d59534bf-beb7-45b2-b944-6d04b69cecb2

📥 Commits

Reviewing files that changed from the base of the PR and between 3f3a7c0 and c30da10.

📒 Files selected for processing (1)
  • wacore/src/send/group.rs
📝 Walkthrough

Walkthrough

This PR adds a SessionLockGuard, exposes lock_device_sessions on SendContextResolver, implements per-device session locking in Client, wires that lock into group SKDM fan-out, and adds a regression test that checks the DM and group paths share the same mutex.

Changes

Per-device session locking for group SKDM fan-out

Layer / File(s) Summary
SessionLockGuard type and trait method
wacore/src/client/context.rs
Adds the SessionLockGuard RAII wrapper with none()/hold() constructors and a default no-op lock_device_sessions method on SendContextResolver.
Client lock acquisition
src/client/context_impl.rs
Implements lock_device_sessions for Client, deriving per-device session-lock keys, locking the matching mutexes, and returning a guard that keeps them held.
Group SKDM locking integration
wacore/src/send/group.rs
prepare_group_stanza now acquires and drops a session guard around SKDM-related encryption, with explicit lock ordering relative to the sender-key chain lock.
Mutex-sharing regression test
src/send/mod.rs
Adds an async test that verifies the group SKDM lock blocks the DM per-device session mutex while held, then releases it after drop.

Estimated code review effort: 3 (Moderate) | ~25 minutes

Sequence Diagram(s)

sequenceDiagram
  participant prepare_group_stanza as prepare_group_stanza
  participant SendContextResolver as SendContextResolver
  participant SessionLockGuard as SessionLockGuard
  participant SKDMEncrypt as encrypt_for_devices_with_sessions
  participant StanzaBuild as stanza build

  prepare_group_stanza->>SendContextResolver: lock_device_sessions(target devices)
  SendContextResolver-->>prepare_group_stanza: SessionLockGuard
  prepare_group_stanza->>SKDMEncrypt: encrypt SKDM with session_guard held
  SKDMEncrypt-->>prepare_group_stanza: encrypted SKDM
  prepare_group_stanza->>SessionLockGuard: drop session_guard
  prepare_group_stanza->>StanzaBuild: build remaining stanza
Loading

Possibly related PRs

Suggested labels: api-design

Suggested reviewers: greptile-apps, cubic-dev-ai

🚥 Pre-merge checks | ✅ 5
✅ Passed checks (5 passed)
Check name Status Explanation
Title check ✅ Passed The title clearly summarizes the main change: locking per-device sessions during group SKDM fan-out.
Description check ✅ Passed The description is directly related to the change and accurately explains the lock-sharing fix and added test.
Docstring Coverage ✅ Passed No functions found in the changed files to evaluate docstring coverage. Skipping docstring coverage check.
Linked Issues check ✅ Passed Check skipped because no linked issues were found for this pull request.
Out of Scope Changes check ✅ Passed Check skipped because no linked issues were found for this pull request.
✨ Finishing Touches
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Commit unit tests in branch claude/fix-group-skdm-session-lock

Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out.

❤️ Share

Comment @coderabbitai help to get the list of available commands.

@greptile-apps

greptile-apps Bot commented Jul 6, 2026

Copy link
Copy Markdown

Greptile Summary

This PR fixes a race condition in group SKDM fan-out by acquiring the same per-device pairwise session mutexes the DM send path already uses, so a concurrent group send and DM (or two group sends sharing a device) can no longer advance the same device's Signal ratchet simultaneously and silently drop a chain step.

  • SessionLockGuard — new opaque RAII wrapper added to wacore that holds an arbitrary platform-supplied guard value until dropped; allows wacore to manage lock lifetimes without depending on the concrete mutex cache.
  • lock_device_sessions hook — added to SendContextResolver with a no-op default; Client implements it by reusing build_session_lock_keys + session_mutexes_for (the exact same sorted key derivation the DM path uses), and the guard is held across X3DH setup + SKDM fan-out then explicitly dropped before the skmsg encrypt to avoid head-of-line blocking on concurrent DM sends.
  • Testgroup_skdm_lock_shares_dm_per_device_session_mutex verifies both paths contend on the same physical mutex for a shared device.

Confidence Score: 5/5

Safe to merge — the fix correctly closes the group SKDM / DM pairwise session race without introducing new deadlocks.

Lock ordering (per-device session mutexes → session_setup_lock → sender_key_lock) is globally consistent across every send path, and the implementation reuses the exact same sorted-key derivation the DM path already relies on. The RAII guard correctly covers X3DH setup and SKDM fan-out, is released before skmsg encrypt, and drops correctly on every error path. The test verifies shared mutex identity end-to-end. No logic bugs found.

No files require special attention.

Important Files Changed

Filename Overview
wacore/src/send/group.rs Adds session_guard acquisition before sender_key_lock in prepare_group_stanza; lock ordering (session → sender-key) is consistent across all send paths and deadlock-free.
wacore/src/client/context.rs Adds SessionLockGuard RAII type and lock_device_sessions trait method with no-op default; design is clean and wasm32/native dual-target safe.
src/client/context_impl.rs Client implements lock_device_sessions by reusing build_session_lock_keys + session_mutexes_for, ensuring identical mutex identity to the DM path.
src/send/mod.rs Adds test group_skdm_lock_shares_dm_per_device_session_mutex that correctly verifies both paths contend on the same mutex; fictitious 555 phone number satisfies the no-PII rule.

Sequence Diagram

%%{init: {'theme': 'neutral'}}%%
sequenceDiagram
    participant GS as Group Send
    participant DM as DM Send
    participant SM as Session Mutexes
    participant SSL as session_setup_lock
    participant SKL as sender_key_lock

    GS->>SM: lock_device_sessions sorted → session_guard
    Note over SM: DM to same device blocks
    DM-->>SM: try acquire → blocks
    GS->>SSL: session_setup_lock → _setup_guard
    GS->>GS: ensure_sessions_for_devices
    GS->>SSL: drop _setup_guard
    GS->>SKL: sender_key_lock → chain_guard
    GS->>GS: create SKDM + encrypt_for_devices
    GS->>SM: drop session_guard
    SM-->>DM: mutex acquired
    GS->>GS: encrypt_group_message skmsg
    GS->>SKL: drop chain_guard
Loading
%%{init: {'theme': 'base', 'themeVariables': {"darkMode": true, "background": "#0d1117", "primaryColor": "#21262d", "primaryTextColor": "#e6edf3", "primaryBorderColor": "#8b949e", "lineColor": "#8b949e", "textColor": "#e6edf3", "edgeLabelBackground": "#161b22", "actorBkg": "#21262d", "actorBorder": "#8b949e", "actorTextColor": "#e6edf3", "actorLineColor": "#8b949e", "signalColor": "#8b949e", "signalTextColor": "#e6edf3", "noteBkgColor": "#373320", "noteBorderColor": "#d4a72c", "noteTextColor": "#f0e6c0", "labelBoxBkgColor": "#21262d", "labelBoxBorderColor": "#8b949e", "labelTextColor": "#e6edf3", "loopTextColor": "#e6edf3", "activationBkgColor": "#30363d", "activationBorderColor": "#8b949e"}}}%%
sequenceDiagram
    participant GS as Group Send
    participant DM as DM Send
    participant SM as Session Mutexes
    participant SSL as session_setup_lock
    participant SKL as sender_key_lock

    GS->>SM: lock_device_sessions sorted → session_guard
    Note over SM: DM to same device blocks
    DM-->>SM: try acquire → blocks
    GS->>SSL: session_setup_lock → _setup_guard
    GS->>GS: ensure_sessions_for_devices
    GS->>SSL: drop _setup_guard
    GS->>SKL: sender_key_lock → chain_guard
    GS->>GS: create SKDM + encrypt_for_devices
    GS->>SM: drop session_guard
    SM-->>DM: mutex acquired
    GS->>GS: encrypt_group_message skmsg
    GS->>SKL: drop chain_guard
Loading

Reviews (3): Last reviewed commit: "perf(send): release the per-device sessi..." | Re-trigger Greptile

Comment thread wacore/src/send/group.rs Outdated
Comment thread src/client/context_impl.rs Outdated
@github-actions

github-actions Bot commented Jul 6, 2026

Copy link
Copy Markdown

📦 Binary size report

Metric main PR Δ
bin size (stripped) 10.78 MiB 10.78 MiB +3.78 KiB (+0.03%) 🔺
bin .text 8.78 MiB 8.79 MiB +3.19 KiB (+0.04%) 🔺
bin allocated (text+data+bss) 10.78 MiB 10.78 MiB +4.13 KiB (+0.04%) 🔺
llvm-lines wacore 504,289 504,304 +15 (+0.00%) 🔺
llvm-lines wacore copies 17,275 17,277 +2 (+0.01%) 🔺
llvm-lines whatsapp-rust lib 751,079 751,733 +654 (+0.09%) 🔺
llvm-lines whatsapp-rust lib copies 24,487 24,518 +31 (+0.13%) 🔺
deps crates (Cargo.lock) 466 466 0
.text per crate
Crate main PR Δ
.text whatsapp_rust 1.61 MiB 1.61 MiB +1.62 KiB (+0.10%) 🔺
.text wacore 530.46 KiB 531.22 KiB +777 B (+0.14%) 🔺
.text wacore_binary 157.49 KiB 157.49 KiB 0
.text wacore_libsignal 178.73 KiB 178.73 KiB 0
.text wacore_appstate 156.42 KiB 156.42 KiB 0
.text wacore_noise 26.05 KiB 26.05 KiB 0
.text waproto 1.60 MiB 1.60 MiB 0
.text whatsapp_rust_sqlite_storage 512.98 KiB 512.98 KiB 0
.text whatsapp_rust_tokio_transport 43.61 KiB 43.61 KiB 0
.text whatsapp_rust_ureq_http_client 10.47 KiB 10.47 KiB 0
.text std 1022.24 KiB 1022.96 KiB +733 B (+0.07%) 🔺
.text other deps 2.95 MiB 2.95 MiB 0
Top movers (cargo-bloat attribution)
Crate main PR Δ
whatsapp_rust 1.61 MiB 1.61 MiB +1.62 KiB (+0.10%)

Baseline: 8382d3d51 (latest main run) · Head: 96a672479 · Graphs

@cubic-dev-ai cubic-dev-ai Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

No issues found across 4 files

Confidence score: 5/5

  • Automated review surfaced no issues in the provided summaries.
  • No files require special attention.

Requires human review: Adds per-device session locking in the group send path to fix a concurrency bug. This is a non-trivial logic change in core messaging code that requires human review to verify lock ordering and deadlock safety.

Re-trigger cubic

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

Actionable comments posted: 1

🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

Inline comments:
In `@wacore/src/send/group.rs`:
- Around line 426-443: The device-level serialization is still missing around
the session setup path in the group send flow, so a concurrent send can race the
prekey/X3DH state mutation before the per-device lock is acquired. Update the
group send logic in the same area as ensure_sessions_for_devices and
encrypt_for_devices_with_sessions so the lock from
resolver.lock_device_sessions(distribution_list) covers the session
establishment step as well, not just the encryption call; keep the existing
per-device guard pattern used by the DM path and apply it consistently around
both operations.
🪄 Autofix (Beta)

Fix all unresolved CodeRabbit comments on this PR:

  • Push a commit to this branch (recommended)
  • Create a new PR with the fixes

ℹ️ Review info
⚙️ Run configuration

Configuration used: Repository UI (base), Organization UI (inherited)

Review profile: ASSERTIVE

Plan: Pro Plus

Run ID: 012839f5-db18-4b6d-8c07-45475f2a50d7

📥 Commits

Reviewing files that changed from the base of the PR and between 8382d3d and d76de3a.

📒 Files selected for processing (4)
  • src/client/context_impl.rs
  • src/send/mod.rs
  • wacore/src/client/context.rs
  • wacore/src/send/group.rs

Comment thread wacore/src/send/group.rs Outdated
The per-device session locks wrapped only the SKDM encrypt fan-out, but
ensure_sessions_for_devices (prekey fetch + X3DH) also writes each target's
pairwise session and ran under only the per-group session_setup_lock — so a
concurrent DM could still race the cold-path session creation. The DM path holds
its per-device locks across all of prepare_dm_stanza (setup + encrypt), so match
it: acquire the locks before the setup and hold them through the fan-out.

Acquired before sender_key_lock, giving the whole send path a single
session -> sender-key lock order (no path takes the reverse), and the inner
per-fan-out acquisition is removed so the (non-reentrant) mutexes are taken once.

@cubic-dev-ai cubic-dev-ai Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

1 issue found across 2 files (changes from recent commits).

Prompt for AI agents (unresolved issues)

Check if these issues are valid — if so, understand the root cause of each and fix them. If appropriate, use sub-agents to investigate and fix each issue separately.


<file name="wacore/src/send/group.rs">

<violation number="1" location="wacore/src/send/group.rs:497">
P2: The `session_guard` is dropped here alongside `chain_guard`, but it has been held unnecessarily through `encrypt_group_message`, which only touches the sender-key store and does not use pairwise sessions. This extends the per-device mutex critical section beyond the SKDM fan-out and X3DH setup that actually need it, causing avoidable head-of-line blocking for concurrent DMs to the same devices. Consider dropping `session_guard` immediately after the `encrypt_for_devices_with_sessions` fan-out (before `encrypt_group_message`) while keeping `chain_guard` through the skmsg encrypt.</violation>
</file>

Reply with feedback, questions, or to request a fix.

Re-trigger cubic

Comment thread wacore/src/send/group.rs Outdated
…rypt

encrypt_group_message only advances the sender-key chain (guarded by chain_guard),
not any pairwise session, so drop session_guard right after the SKDM fan-out
instead of holding it across the skmsg encrypt. Shortens the per-device critical
section to only the X3DH setup + fan-out that actually need it, avoiding
head-of-line blocking a concurrent DM to a shared device.

@cubic-dev-ai cubic-dev-ai Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

0 issues found across 1 file (changes from recent commits).

Requires human review: Adds locking to group send path to prevent race conditions on pairwise sessions. Touches core send logic and concurrency; requires human review.

Re-trigger cubic

@jlucaso1
jlucaso1 merged commit 52fd363 into main Jul 6, 2026
17 checks passed
@jlucaso1
jlucaso1 deleted the claude/fix-group-skdm-session-lock branch July 6, 2026 23:59
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant