Skip to content

fix(send): isolate a group device's session-setup failure from the cohort - #996

Merged
jlucaso1 merged 4 commits into
mainfrom
claude/fix-group-skdm-per-device-isolation
Jul 7, 2026
Merged

fix(send): isolate a group device's session-setup failure from the cohort#996
jlucaso1 merged 4 commits into
mainfrom
claude/fix-group-skdm-per-device-isolation

Conversation

@jlucaso1

@jlucaso1 jlucaso1 commented Jul 7, 2026

Copy link
Copy Markdown
Collaborator

What

Isolate a single group device's session-setup failure so it no longer aborts the SKDM distribution to the whole cohort.

Why (bug)

ensure_sessions_for_devices returned Err (Ok(Err(e)) => return Err(e)) as soon as any device's process_prekey_bundle failed. In prepare_group_stanza that Err nulls session_plan, and the entire SKDM fan-out is gated on if let Some(plan) = session_plan — so every other target gets no SKDM even though its session established fine. The skmsg still ships, the phash covers the full set (server reports no mismatch), and on ACK the full cohort is marked has_key=true.

External members recover via a retry receipt (mark_forget_sender_key). But an own companion hits mark_forget_sender_key with exclude_own_devices=true, which filters own-user JIDs and returns early with no DB write — so the companion stays has_key=true forever and can never decrypt our group messages from that device until an unrelated full rotation (participant removal / PN↔LID migration).

WA Web's GroupKeyDistributionMsg wraps each device's encrypt in its own try/catch and drops only the failing one (GroupSkmsgJob swallows the ensureE2ESessions error and continues) — one member's failure never suppresses the rest.

How

  • In the session-setup spawn loop, change Ok(Err(e)) => return Err(e) to log and skip that one device. The sessionless device is then skipped by the encrypt fan-out (which already skips devices without a session), and every other device is distributed normally. The encrypt fan-out loop and the 406 batch path were already isolated; this brings the setup path in line.

Tests

  • group_skdm_setup_failure_is_isolated_to_the_bad_device — a group send with one valid-bundle device and one whose bundle fails X3DH: prepare_group_stanza now succeeds and the good device still receives its pairwise SKDM (participants has exactly one child). Before the fix, the failing device nulled session_plan, so there was no participants node at all (the .expect would panic).
  • cargo fmt / clippy clean; full wacore send suite (96) passes.

Residual (separate follow-up)

The primary harm (an unrelated device's failure orphaning own companions) is closed. A narrower window remains: if an own companion's own setup fails, it's still marked has_key=true from the full-cohort mark. Fully closing that needs the own-device has_key mark gated on actual SKDM encryption (skdm_encrypted_devices), which I've left as a focused follow-up to keep this send-path change minimal.

…hort

ensure_sessions_for_devices aborted the whole batch (return Err) when any single
device's process_prekey_bundle failed. In prepare_group_stanza that Err nulls
session_plan, so the entire SKDM fan-out is skipped: every other target — external
members AND our own companion devices — gets no SKDM even though its session
established fine, while the skmsg still ships and the full cohort is marked
has_key=true on ACK. External devices recover via a retry receipt, but an own
companion's retry hits mark_forget_sender_key's exclude_own_devices no-op, so it
stays keyless and can never decrypt our group messages until an unrelated full
rotation.

Skip just the failing device (log + continue) instead of aborting, mirroring WA
Web's GroupKeyDistributionMsg per-device try/catch: the sessionless device is
dropped by the encrypt fan-out and the rest are distributed normally.

Residual (separate follow-up): if an own companion's OWN setup fails it is still
marked has_key=true from the full-cohort mark; fully closing that needs the
own-device mark gated on actual SKDM encryption.
@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 7, 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: 16 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: fbfe73cc-8bc4-490a-bba5-c5b897869031

📥 Commits

Reviewing files that changed from the base of the PR and between f08d750 and fcca55d.

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

Walkthrough

Per-device session setup failures in ensure_sessions_for_devices now log a warning and skip the affected device instead of aborting the whole operation. Tests add a signed_prekey_bundle() helper producing a verifiable bundle and a new test validating that bad-device failures are isolated from SKDM group fanout.

Changes

Session Setup Failure Isolation

Layer / File(s) Summary
Non-fatal per-device failure handling
wacore/src/send/encrypt.rs
Spawn task errors for individual devices now log a warning and are skipped instead of returning an error that halts session establishment for all devices.
Verifiable prekey bundle test helper and isolation test
wacore/src/send/tests.rs
Adds signed_prekey_bundle() producing a signature-verifiable bundle, updates prekey_fetch_runs_outside_chain_lock to use it, and adds group_skdm_setup_failure_is_isolated_to_the_bad_device verifying SKDM only reaches the good device.

Estimated code review effort: 2 (Simple) | ~12 minutes

Sequence Diagram(s)

sequenceDiagram
  participant Caller
  participant EnsureSessions as ensure_sessions_for_devices
  participant GoodDevice
  participant BadDevice

  Caller->>EnsureSessions: request session setup for devices
  EnsureSessions->>GoodDevice: spawn session setup task
  EnsureSessions->>BadDevice: spawn session setup task
  GoodDevice-->>EnsureSessions: session established
  BadDevice-->>EnsureSessions: setup failed
  EnsureSessions->>EnsureSessions: warn and skip BadDevice
  EnsureSessions-->>Caller: return success (GoodDevice sessions ready)
Loading

Possibly related PRs

Metadata

Look, I need this code to work reliably at scale — billions of messages, zero excuses. This fix is exactly the kind of resilience I want: one bad device shouldn't take down the whole fan-out. That's a win. Ship it, but I expect the tests to be airtight, no exceptions.

🚥 Pre-merge checks | ✅ 5
✅ Passed checks (5 passed)
Check name Status Explanation
Title check ✅ Passed The title accurately summarizes the main fix: one device's session-setup failure no longer aborts the whole cohort.
Description check ✅ Passed The description is directly related to the send-path fix and the added test coverage.
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-per-device-isolation

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 7, 2026

Copy link
Copy Markdown

Greptile Summary

Isolates per-device session-setup failures in ensure_sessions_for_devices so that one participant's X3DH failure no longer suppresses SKDM delivery to all other cohort members. The fix brings the setup loop in line with the encrypt fan-out and the 406 batch path, both of which already skipped individual devices rather than aborting the whole cohort.

  • encrypt.rs: Changes Ok(Err(e)) => return Err(e) to log-and-continue, letting the sessionless device be naturally dropped by the existing encrypt fan-out (which already skips devices with no session).
  • tests.rs: Extracts a reusable signed_prekey_bundle() helper (replacing three inline copies) and adds group_skdm_setup_failure_is_isolated_to_the_bad_device to assert the good device still receives its SKDM when one device's bundle fails X3DH.

Confidence Score: 5/5

Safe to merge — the change is a minimal one-line fix with a direct regression test, and the new behaviour matches the existing skip logic already used for the SpawnCanceled and 406 paths.

The fix is well-scoped: only one branch in the spawn-result match is changed, the error already carries the device address for log diagnostics, and the remaining code paths are untouched. The new test confirms the good device receives its SKDM even when a sibling device fails X3DH. No new unsafe code, no data-loss path, and the residual own-companion mark issue is explicitly deferred with context.

No files require special attention.

Important Files Changed

Filename Overview
wacore/src/send/encrypt.rs One-line fix converting a hard-abort into a log-and-skip; the error message already captures the device address for diagnostics. Logic is consistent with the existing SpawnCanceled and 406 skip patterns.
wacore/src/send/tests.rs Deduplicates three inline bundle-construction blocks into a named helper with a doc comment clarifying its good/bad contract, and adds a regression test that directly exercises the fixed code path.

Flowchart

%%{init: {'theme': 'neutral'}}%%
flowchart TD
    A[ensure_sessions_for_devices] --> B{spawn result}
    B -- "Ok(Ok(Some(jid)))" --> C[on_local_identity_change]
    B -- "Ok(Ok(None))" --> D[continue]
    B -- "Ok(Err(e)) — BEFORE" --> E["return Err(e) ❌\nnulls session_plan"]
    B -- "Ok(Err(e)) — AFTER FIX" --> F["log::warn + skip device ✅\nsessionless device dropped by fan-out"]
    B -- "Err(SpawnCanceled)" --> G[log::warn + skip device]
    C --> D
    F --> D
    G --> D
    D --> H{more devices?}
    H -- yes --> B
    H -- no --> I[Ok SessionPlan]
    I --> J[prepare_group_stanza: encrypt fan-out]
    J --> K{session exists?}
    K -- yes --> L[pairwise SKDM sent]
    K -- no --> M[device skipped]
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"}}}%%
flowchart TD
    A[ensure_sessions_for_devices] --> B{spawn result}
    B -- "Ok(Ok(Some(jid)))" --> C[on_local_identity_change]
    B -- "Ok(Ok(None))" --> D[continue]
    B -- "Ok(Err(e)) — BEFORE" --> E["return Err(e) ❌\nnulls session_plan"]
    B -- "Ok(Err(e)) — AFTER FIX" --> F["log::warn + skip device ✅\nsessionless device dropped by fan-out"]
    B -- "Err(SpawnCanceled)" --> G[log::warn + skip device]
    C --> D
    F --> D
    G --> D
    D --> H{more devices?}
    H -- yes --> B
    H -- no --> I[Ok SessionPlan]
    I --> J[prepare_group_stanza: encrypt fan-out]
    J --> K{session exists?}
    K -- yes --> L[pairwise SKDM sent]
    K -- no --> M[device skipped]
Loading

Reviews (4): Last reviewed commit: "style: rustfmt the trimmed import block" | Re-trigger Greptile

Comment thread wacore/src/send/encrypt.rs Outdated
Comment thread wacore/src/send/tests.rs

@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.

2 issues found across 2 files

Confidence score: 3/5

  • In wacore/src/send/encrypt.rs, the setup-failure path can still persist a device as has_key=true even when no SKDM was encrypted, which risks marking broken recipients as ready and causing downstream send/encryption failures; carry setup-failed devices through planning/results (or block has_key=true in that branch) before merging.
  • In wacore/src/send/tests.rs, duplicated signed PreKeyBundle fixture construction can drift from the canonical setup and let validity-rule changes be updated in one place but not the other; extract a shared helper to keep test expectations aligned and reduce future regression risk.
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/encrypt.rs">

<violation number="1" location="wacore/src/send/encrypt.rs:616">
P1: When a device reaches this setup-failure branch, it can still be persisted as `has_key=true` even though no SKDM was encrypted for it. Consider carrying the setup-failed device list through the plan/result or otherwise excluding these failures from `PreparedGroupStanza.skdm_devices`, especially for own companions that do not have a retry-receipt repair path.</violation>
</file>

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

Re-trigger cubic

// GroupKeyDistributionMsg wraps each device's encrypt in try/catch
// and drops only the failing one; the skipped device just stays
// sessionless and is skipped by the fan-out below.
Ok(Err(e)) => {

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

P1: When a device reaches this setup-failure branch, it can still be persisted as has_key=true even though no SKDM was encrypted for it. Consider carrying the setup-failed device list through the plan/result or otherwise excluding these failures from PreparedGroupStanza.skdm_devices, especially for own companions that do not have a retry-receipt repair path.

Prompt for AI agents
Check if this issue is valid — if so, understand the root cause and fix it. At wacore/src/send/encrypt.rs, line 616:

<comment>When a device reaches this setup-failure branch, it can still be persisted as `has_key=true` even though no SKDM was encrypted for it. Consider carrying the setup-failed device list through the plan/result or otherwise excluding these failures from `PreparedGroupStanza.skdm_devices`, especially for own companions that do not have a retry-receipt repair path.</comment>

<file context>
@@ -604,7 +604,18 @@ pub async fn ensure_sessions_for_devices(
+                // GroupKeyDistributionMsg wraps each device's encrypt in try/catch
+                // and drops only the failing one; the skipped device just stays
+                // sessionless and is skipped by the fan-out below.
+                Ok(Err(e)) => {
+                    log::warn!("Group session setup failed for a device, skipping it: {e}");
+                }
</file context>

Comment thread wacore/src/send/tests.rs Outdated
@github-actions

github-actions Bot commented Jul 7, 2026

Copy link
Copy Markdown

📦 Binary size report

Metric main PR Δ
bin size (stripped) 10.80 MiB 10.81 MiB +4.25 KiB (+0.04%) 🔺
bin .text 8.81 MiB 8.81 MiB +256 B (+0.00%) 🔺
bin allocated (text+data+bss) 10.80 MiB 10.80 MiB +4.00 KiB (+0.04%) 🔺
llvm-lines wacore 504,310 504,310 0
llvm-lines wacore copies 17,277 17,277 0
llvm-lines whatsapp-rust lib 757,319 757,359 +40 (+0.01%) 🔺
llvm-lines whatsapp-rust lib copies 24,575 24,575 0
deps crates (Cargo.lock) 466 466 0
.text per crate
Crate main PR Δ
.text whatsapp_rust 1.62 MiB 1.62 MiB 0
.text wacore 531.22 KiB 531.46 KiB +244 B (+0.04%) 🔺
.text wacore_binary 157.70 KiB 157.70 KiB 0
.text wacore_libsignal 178.73 KiB 178.73 KiB 0
.text wacore_appstate 156.45 KiB 156.45 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 1.00 MiB 1.00 MiB 0
.text other deps 2.95 MiB 2.95 MiB 0

Baseline: 5f07cb9aa (latest main run) · Head: 680331a51 · Graphs

Deduplicate the verifiable-PreKeyBundle fixture into signed_prekey_bundle() (used
by the chain-lock and cohort-isolation tests) and trim the per-device isolation
comment to why-only. (review feedback)
greptile-apps[bot]
greptile-apps Bot previously approved these changes Jul 7, 2026

@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/tests.rs`:
- Around line 805-828: The new signed_prekey_bundle() helper is not being used
where the same PreKeyBundle setup is still duplicated in established_stores(),
so the refactor is only half applied. Update established_stores() to call
signed_prekey_bundle() instead of manually generating the receiver, spk, opk,
signature, and PreKeyBundle::new data, keeping the helper as the single source
of truth for this shared test setup.
🪄 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: fe4f8edc-0c01-4b5d-869e-950b92958635

📥 Commits

Reviewing files that changed from the base of the PR and between 4eecf0e and f08d750.

📒 Files selected for processing (2)
  • wacore/src/send/encrypt.rs
  • wacore/src/send/tests.rs

Comment thread wacore/src/send/tests.rs

@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 2 files (changes from recent commits).

Requires human review: Auto-approval blocked by 1 unresolved issue from previous reviews.

Re-trigger cubic

Route established_stores() and setup_session() through signed_prekey_bundle() too,
so the verifiable-bundle fixture lives in one place. (review feedback)
@greptile-apps
greptile-apps Bot dismissed their stale review July 7, 2026 02:16

Dismissed because a newer commit was pushed; Greptile will re-review the current head.

@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: Auto-approval blocked by 1 unresolved issue from previous reviews.

Re-trigger cubic

greptile-apps[bot]
greptile-apps Bot previously approved these changes Jul 7, 2026
@greptile-apps
greptile-apps Bot dismissed their stale review July 7, 2026 02:23

Dismissed because a newer commit was pushed; Greptile will re-review the current head.

@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: Auto-approval blocked by 1 unresolved issue from previous reviews.

Re-trigger cubic

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.

1 participant