Skip to content

fix(send): fail DM send when every per-device encrypt fails - #652

Merged
jlucaso1 merged 2 commits into
mainfrom
audit/dm-fanout-fail-on-all-device-encrypt-fail
May 28, 2026
Merged

jlucaso1 merged 2 commits into
mainfrom
audit/dm-fanout-fail-on-all-device-encrypt-fail

Conversation

@jlucaso1

Copy link
Copy Markdown
Collaborator

Finding (WA Web parity audit, area: send/encrypt)

prepare_dm_stanza (wacore/src/send.rs) extends participant_nodes from encrypt_for_devices, which logs+skips each device whose encryption fails (missing prekey bundle, etc.) and returns Ok with whatever succeeded. When ALL recipient devices fail, participant_nodes is empty, yet the code unconditionally built <participants> and returned Ok — shipping an empty stanza and reporting success. The message is silently dropped with no error and no retry trigger.

WA Web WAWebSendMsgCreateFanoutStanza (encryptAndSendUserMsg) rejects with encryption fail for all devices when the per-device encrypt set is empty.

Change

Return Err when participant_nodes.is_empty() && total_devices > 0, mirroring WA Web. Only the all-fail case changes (previously a silent no-op); partial success is unaffected.

Testing

cargo build -p wacore, cargo clippy -p wacore --tests, cargo test -p wacore (send tests pass). The all-fail path requires the full Signal store adapter (src/), so a dedicated test belongs at integration level; the guard is a minimal invariant verified by reading the fanout path.

Draft — one of several from a WA Web parity audit; review independently.

prepare_dm_stanza built an empty <participants> stanza and returned Ok
when encrypt_for_devices skipped every recipient device (e.g. all bundles
missing), silently dropping the message with no error and no retry. WA Web
encryptAndSendUserMsg rejects with "encryption fail for all devices".
Mirror that: return Err when participant_nodes is empty but there were
recipient devices, so the caller's error path fires.

Note: the all-fail path needs the full Signal store adapter (src/), so a
dedicated unit test belongs at integration level; the guard itself is a
minimal invariant verified by reading the fanout path.
@coderabbitai

coderabbitai Bot commented May 28, 2026

Copy link
Copy Markdown

Review Change Stack

Warning

Review limit reached

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

More reviews will be available in 11 minutes and 34 seconds. Learn how PR review limits work.

Your organization has run out of usage credits. Purchase more in the billing tab.

⌛ How to resolve this issue?

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.

We recommend that you space out your commits to avoid hitting the rate limit.

🚦 How do rate limits work?

CodeRabbit enforces hourly rate limits for each developer per organization.

Our paid plans include higher PR review limits than trial, open-source, and free plans. In all cases, reviews become available again over time. During sustained high-volume PR review activity, CodeRabbit may temporarily slow when the next review becomes available.

Please see our Fair Usage Limits Policy for further information.

ℹ️ Review info
⚙️ Run configuration

Configuration used: Repository UI

Review profile: ASSERTIVE

Plan: Pro Plus

Run ID: 34425076-e968-4915-b20c-937790467e2d

📥 Commits

Reviewing files that changed from the base of the PR and between d1a7584 and 55a06e8.

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

Walkthrough

prepare_dm_stanza adds a fail-fast validation guard that returns an explicit error when encryption fails for all recipient devices, preventing silent message drops from empty participant stanzas.

Changes

DM Send Error Handling

Layer / File(s) Summary
Fail-fast validation for encryption failures
wacore/src/send.rs
A guard check is added after device partitioning and encryption attempts. If no participant nodes were produced despite having devices to encrypt to, an explicit anyhow! error is returned instead of proceeding with an empty <participants> stanza that would silently drop the message.

Estimated code review effort

🎯 1 (Trivial) | ⏱️ ~3 minutes

Possibly related PRs

  • oxidezap/whatsapp-rust#484: Both PRs touch the DM send/stanza-preparation flow in send.rs; #484 changes which recipient device JIDs are encrypted and fed into DM stanza construction, while this PR adds an error when prepare_dm_stanza ends up with zero participant nodes after encryption attempts.
  • oxidezap/whatsapp-rust#451: Both PRs modify wacore/src/send.rs within prepare_dm_stanza around DM device partitioning/encryption behavior; #451 changes how sender/own devices are classified for the partition, and this PR adds an explicit error when encryption fails for all those partitioned recipient devices.
  • oxidezap/whatsapp-rust#524: Both PRs modify the DM send-path logic in wacore/src/send.rs specifically prepare_dm_stanza's device-partitioning/participant construction so the participant list doesn't end up empty; #524 reworks multi-device target selection while this PR adds an explicit error when all per-device encryption attempts fail.

Suggested labels

breaking-change


Look, this is exactly the kind of thing that needs to happen. We can't have messages silently disappearing because encryption failed. Either it works or the user knows it doesn't work—there's no in-between. The guard is tight, focused, and prevents a real class of bugs. Ships cleanly.

🚥 Pre-merge checks | ✅ 5
✅ Passed checks (5 passed)
Check name Status Explanation
Title check ✅ Passed The title directly and specifically captures the main change: failing DM sends when all per-device encryption attempts fail, which is exactly what the changeset implements.
Description check ✅ Passed The description thoroughly explains the bug, the WA Web parity requirement, and the specific fix implemented, all directly related to the changeset.
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.

✏️ Tip: You can configure your own custom pre-merge checks in the settings.

✨ Finishing Touches
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Commit unit tests in branch audit/dm-fanout-fail-on-all-device-encrypt-fail

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 and usage tips.

@jlucaso1
jlucaso1 marked this pull request as ready for review May 28, 2026 21:51

@chatgpt-codex-connector chatgpt-codex-connector 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.

💡 Codex Review

Here are some automated review suggestions for this pull request.

Reviewed commit: d1a7584bf7

ℹ️ About Codex in GitHub

Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you

  • Open a pull request for review
  • Mark a draft as ready
  • Comment "@codex review".

If Codex has suggestions, it will comment; otherwise it will react with 👍.

Codex can also answer questions or update the PR. Try commenting "@codex address that feedback".

Comment thread wacore/src/send.rs Outdated
@coderabbitai

coderabbitai Bot commented May 28, 2026

Copy link
Copy Markdown

Actionable comments posted: 0

@github-actions

github-actions Bot commented May 28, 2026

Copy link
Copy Markdown

Benchmark Results

67 unchanged benchmark(s)
Benchmark Current Baseline Change
reporting_token_benchmark::content_extraction_group::bench_content_extraction simple:setup_simple_message() 3,933 3,933 +0.0%
reporting_token_benchmark::content_extraction_group::bench_content_extraction extended:setup_extended_message() 12,038 12,038 +0.0%
reporting_token_benchmark::key_derivation_group::bench_key_derivation 43,514 43,514 +0.0%
reporting_token_benchmark::token_calculation_group::bench_token_calculation 19,365 19,365 +0.0%
reporting_token_benchmark::full_generation_group::bench_full_token_generation simple:setup_full_gen_simple() 68,579 68,579 +0.0%
reporting_token_benchmark::full_generation_group::bench_full_token_generation extended:setup_full_gen_extended() 76,679 76,679 +0.0%
reporting_token_benchmark::message_encoding_group::bench_message_encoding simple:setup_simple_message() 2,230 2,230 +0.0%
reporting_token_benchmark::message_encoding_group::bench_message_encoding extended:setup_extended_message() 5,988 5,988 +0.0%
send_receive_benchmark::dm_send::bench_dm_send text:setup_dm_send() 181,405 181,127 +0.2%
send_receive_benchmark::dm_recv::bench_dm_recv text:setup_dm_recv() 2,205,397 2,205,397 +0.0%
send_receive_benchmark::group_send::bench_group_send group_10:setup_group_send_10() 888,072 887,932 +0.0%
send_receive_benchmark::group_send::bench_group_send group_50:setup_group_send_50() 1,027,401 1,027,339 +0.0%
send_receive_benchmark::group_send::bench_group_send group_256:setup_group_send_256() 1,759,617 1,760,413 -0.0%
send_receive_benchmark::group_send_skdm::bench_group_send_skdm skdm_10:setup_group_skdm_10() 1,126,011 1,125,409 +0.1%
send_receive_benchmark::group_send_skdm::bench_group_send_skdm skdm_50:setup_group_skdm_50() 2,104,546 2,111,321 -0.3%
send_receive_benchmark::group_send_skdm::bench_group_send_skdm skdm_256:setup_group_skdm_256() 7,152,796 7,205,152 -0.7%
send_receive_benchmark::group_recv::bench_group_recv text:setup_group_recv() 12,571,197 12,690,452 -0.9%
binary_benchmark::marshal_group::bench_marshal_allocating 71,326 71,326 +0.0%
binary_benchmark::marshal_group::bench_marshal_auto_allocating 71,379 71,379 +0.0%
binary_benchmark::marshal_group::bench_marshal_exact_allocating 98,446 98,446 +0.0%
binary_benchmark::marshal_group::bench_marshal_reusing_buffer 78,826 78,826 +0.0%
binary_benchmark::marshal_group::bench_marshal_reusing_buffer_vec_writer 71,426 71,426 +0.0%
binary_benchmark::marshal_group::bench_marshal_long_string 7,593 7,593 +0.0%
binary_benchmark::marshal_group::bench_marshal_auto_long_string 7,636 7,636 +0.0%
binary_benchmark::marshal_group::bench_marshal_exact_long_string 9,348 9,348 +0.0%
binary_benchmark::marshal_group::bench_marshal_huge_bytes_allocating 530,583 530,583 +0.0%
binary_benchmark::marshal_group::bench_marshal_auto_huge_bytes_allocating 530,151 530,151 +0.0%
binary_benchmark::marshal_group::bench_marshal_exact_huge_bytes_allocating 531,506 531,506 +0.0%
binary_benchmark::marshal_group::bench_marshal_many_children_allocating 8,506,239 8,506,239 +0.0%
binary_benchmark::marshal_group::bench_marshal_auto_many_children_allocating 8,450,491 8,450,491 +0.0%
binary_benchmark::marshal_group::bench_marshal_exact_many_children_allocating 19,678,026 19,678,026 +0.0%
binary_benchmark::unmarshal_group::bench_unmarshal small:setup_small_marshaled() 2,468 2,468 +0.0%
binary_benchmark::unmarshal_group::bench_unmarshal large:setup_large_marshaled() 33,558 33,558 +0.0%
binary_benchmark::unpack_group::bench_unpack_uncompressed 787 787 +0.0%
binary_benchmark::unpack_group::bench_unpack_compressed 526,732 526,732 +0.0%
binary_benchmark::attr_parser_group::bench_attr_parser attr_lookup:setup_attr_marshaled() 4,986 4,986 +0.0%
binary_benchmark::roundtrip_group::bench_roundtrip small:setup_small_marshaled() 5,315 5,315 +0.0%
binary_benchmark::roundtrip_group::bench_roundtrip large:setup_large_marshaled() 61,874 61,874 +0.0%
binary_benchmark::roundtrip_group::bench_roundtrip_auto small:setup_small_marshaled() 5,347 5,347 +0.0%
binary_benchmark::roundtrip_group::bench_roundtrip_auto large:setup_large_marshaled() 61,942 61,942 +0.0%
binary_benchmark::roundtrip_group::bench_roundtrip_exact small:setup_small_marshaled() 6,734 6,734 +0.0%
binary_benchmark::roundtrip_group::bench_roundtrip_exact large:setup_large_marshaled() 85,585 85,585 +0.0%
binary_benchmark::child_iteration_group::bench_get_children_by_tag 477,570 477,570 +0.0%
binary_benchmark::jid_optimization_group::bench_jid_to_owned_access jid_access:setup_jid_heavy_marshaled() 11,563 11,563 +0.0%
numeric_attr_benchmark::bench_group::bench_baseline_u32 396 396 +0.0%
numeric_attr_benchmark::bench_group::bench_proposed_u32 120 120 +0.0%
numeric_attr_benchmark::bench_group::bench_baseline_u64 439 439 +0.0%
numeric_attr_benchmark::bench_group::bench_proposed_u64 153 153 +0.0%
numeric_attr_benchmark::bench_group::bench_baseline_i64 499 499 +0.0%
numeric_attr_benchmark::bench_group::bench_proposed_i64 162 162 +0.0%
numeric_attr_benchmark::bench_group::bench_baseline_loop_100_u64 44,624 44,624 +0.0%
numeric_attr_benchmark::bench_group::bench_proposed_loop_100_u64 16,424 16,424 +0.0%
libsignal_benchmark::dm_group::bench_dm_session_establishment setup:setup_dm_users() 17,452,396 17,183,090 +1.6%
libsignal_benchmark::dm_group::bench_dm_encrypt_first_message first_msg:setup_dm_session() 157,179 157,179 +0.0%
libsignal_benchmark::dm_group::bench_dm_decrypt_first_message decrypt_prekey:setup_dm_with_first_message() 5,513,975 5,513,975 +0.0%
libsignal_benchmark::dm_group::bench_dm_encrypt_subsequent_message subsequent:setup_established_dm_session() 157,539 157,539 +0.0%
libsignal_benchmark::group_messaging_group::bench_group_create_distribution_message create:setup_group_sender() 296,767 296,767 +0.0%
libsignal_benchmark::group_messaging_group::bench_group_encrypt_message encrypt:setup_group_with_distribution() 706,282 706,282 +0.0%
libsignal_benchmark::group_messaging_group::bench_group_decrypt_message decrypt:setup_group_with_encrypted_message() 12,620,427 12,543,061 +0.6%
libsignal_benchmark::conversation_group::bench_full_dm_conversation full:setup_conversation_data() 27,647,989 27,372,887 +1.0%
libsignal_benchmark::signature_group::bench_signature_creation sign:setup_keypair_with_message() 3,467,011 3,467,011 +0.0%
libsignal_benchmark::signature_group::bench_signature_verification verify:setup_keypair_with_message() 125,029,473 125,772,563 -0.6%
libsignal_benchmark::signature_group::bench_key_generation keygen 2,830,452 2,830,452 +0.0%
libsignal_benchmark::session_optimization_group::bench_decrypt_with_previous_session previous_session:setup_with_archived_sessions() 46,566 46,566 +0.0%
libsignal_benchmark::session_optimization_group::bench_out_of_order_decryption out_of_order:setup_out_of_order_messages() 5,197,012 5,197,012 +0.0%
libsignal_benchmark::session_optimization_group::bench_promote_matching_session promote:setup_promote_matching_session() 360,648 360,648 +0.0%
libsignal_benchmark::session_optimization_group::bench_message_key_eviction eviction:setup_message_key_eviction() 14,255,917 14,255,917 +0.0%
No significant changes detected.

total_devices counted the own sending device too (excluded by
partition_dm_devices), so the error count was off by one. Base the empty
fanout check on the devices actually encrypted for.
@jlucaso1
jlucaso1 merged commit c786a33 into main May 28, 2026
9 checks passed
@jlucaso1
jlucaso1 deleted the audit/dm-fanout-fail-on-all-device-encrypt-fail branch May 28, 2026 22:43
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