Skip to content

Review of upstream PR #983 (oxidezap/whatsapp-rust) - #3

Open
marchugon wants to merge 6 commits into
fr-review-base-pr983from
fr-review-head-pr983
Open

Review of upstream PR #983 (oxidezap/whatsapp-rust)#3
marchugon wants to merge 6 commits into
fr-review-base-pr983from
fr-review-head-pr983

Conversation

@marchugon

Copy link
Copy Markdown
Owner

FriendlyReviewer review of oxidezap/whatsapp-rust#983.


Original PR description:

Summary

Second of the recv post-decrypt dispatch series (follow-up to oxidezap#981). This one narrows the per-sender session lock so it covers only the Signal ratchet crypto, not the downstream plaintext handling and app dispatch.

process_session_enc_batch held the per-sender ratchet guard (session_lock_for(signal_address)) across the entire batch — including handle_decrypted_plaintext, which does protobuf decode, SKDM / app-state-key / LID-migration / PDO / history-sync handling, and the app dispatch. None of that touches the Signal session ratchet — only message_decrypt does — so the lock only needs to cover the crypto.

Change

  • Buffer each decrypted plaintext into a DeferredPlaintext during the locked decrypt loop, release the guard, then run handle_decrypted_plaintext over the drained buffer, unlocked.
  • The rare PN→LID migration retry helper buffers its plaintext into the same queue and returns a small MigrationDecryptResult enum, so multi-payload dispatch order stays uniform. This also collapses four verbose outcome-merge call sites into a match.

A concurrent stanza from the same sender device — e.g. the same participant across two group chats, or the detached LID-migration task, both of which contend on session_lock_for(signal_address) — can now start decrypting while this one dispatches.

Motivation

This matches whatsmeow, which holds the per-sender session lock only around the libsignal decrypt call and handles the decrypted message unlocked. Holding it across dispatch serializes same-sender cross-chat work behind unrelated downstream I/O.

Correctness

  • Delivery order is unchanged. Per-chat in-order delivery is owned by the serial chat-lane worker (handlers/message.rs), which processes one stanza at a time through full dispatch — not by this guard. Releasing the guard earlier cannot reorder delivery.
  • SKDM still precedes group decrypt. The buffer drains before the function returns, so a pkmsg's sender-key distribution is applied before PASS 2's skmsg decrypt reads that sender key.
  • Ratchet stays serialized. message_decrypt (the only session-ratchet mutation) still runs under the guard; handle_decrypted_plaintext only touches sender-key / app-state stores + dispatch, and is already invoked lock-free on the group, newsletter, and PDO paths today.
  • Outcome flags (decrypted / duplicate / dispatched / skdm_only / plaintext_failed / had_failure) are all finalized before the function returns, so the PASS 2 gate and the ack fallbacks are unaffected.

Testing

  • cargo test -p whatsapp-rust --lib — 933 passed, 0 failed (incl. the message::tests decrypt-path suite), deterministic across runs.
  • cargo clippy -p whatsapp-rust --tests — clean.
  • CI green: E2E Tests, wasm32 release, Build & Test, Build & Lint (all features), Test Stable (no-simd), Clippy, Format, Binary Size (−1.5 KiB), and the CodSpeed benchmarks. No new dependencies.

Notes

  • A reviewer raised a durability concern: dropping the guard lets a same-sender/different-chat stanza reach the stanza-end whole-cache flush before this message commits its pending-inbound row, so a crash in that window could persist the ratchet advance without a durable row. This is a pre-existing property, not introduced here — the live-path flush already persists any uncommitted stanza's ratchet via cross-sender concurrency (permit=N, no cross-sender lock), so this change only marginally widens the racing set, and on the session path it touches that is almost entirely SKDM-only decrypts (no durable row, acked by design) or already-serialized 1:1 chats (chat == sender). A proper fix — a live-path row-before-flush barrier, like the offline drain's commit-batcher — is a separate follow-up.

Generated by Claude Code

claude added 6 commits July 4, 2026 21:39
process_session_enc_batch held the per-sender ratchet lock across the whole
batch, including handle_decrypted_plaintext (protobuf decode, SKDM/app-state/
LID/PDO/history handling, and app dispatch). None of that touches the Signal
session ratchet — only message_decrypt does — so the lock only needs to cover
the crypto.

Buffer each decrypted plaintext during the locked decrypt loop, release the
guard, then run handle_decrypted_plaintext unlocked. A concurrent stanza from
the same sender (e.g. the same device across two group chats, or the detached
LID-migration task) can now start decrypting while this one dispatches.

This matches whatsmeow, which holds the per-sender lock only around the
libsignal decrypt. Correctness is preserved:
- Per-chat delivery order is owned by the serial chat-lane worker, not this
  guard, so releasing it early cannot reorder delivery.
- The buffer drains before the function returns, so a pkmsg's SKDM is still
  applied before PASS 2's group (skmsg) decrypt reads the sender key.
- The PN->LID migration retry helper now buffers its plaintext too and returns
  a small result enum, keeping multi-payload dispatch order uniform and
  collapsing the four verbose outcome-merge call sites into a match.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_0116cfRpumbEv3H4qUNmtNk4
Self-review cleanup: replace the positional (&'static str, Vec<u8>, u8)
buffer tuple with a named DeferredPlaintext struct so the migration helper
signature and each push/drain site read for themselves, and trim the two
verbose block comments to their load-bearing invariant. No behavior change.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_0116cfRpumbEv3H4qUNmtNk4
Self-review follow-up: the comment said the guard is held "across the entire
batch", but the lock-scope change releases it before the plaintext drain. Say
"across the decrypt loop" and note the early release. Comment only.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_0116cfRpumbEv3H4qUNmtNk4
try_pn_to_lid_migration_decrypt drops and re-acquires the per-sender session
guard around the migration loop; every return after the re-acquire must leave
the guard Some or the caller silently loses same-sender serialization for the
next batch payload. Make that load-bearing invariant explicit with a
debug_assert at the re-acquire boundary.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_0116cfRpumbEv3H4qUNmtNk4
…ng it

The debug_assert added in the previous commit sat immediately after
`*session_guard = Some(...)`, so it was tautological — it could never observe a
missing hand-back on a later return path. Drop it and keep the invariant as a
comment at the reacquire boundary (flagged by cubic and CodeRabbit).

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_0116cfRpumbEv3H4qUNmtNk4
…ypt bench

handle_decrypted_plaintext never moves self — every callee takes &self /
&Arc<Self> — so take self: &Arc<Self> and drop the per-payload Arc clone at the
session-drain and group-path call sites.

Also add an ignored steady-state session-decrypt benchmark
(bench_session_decrypt_throughput) that drives the real process_session_enc_batch
against established Signal sessions; deterministic CPU/allocation profiles come
from running the same binary under valgrind (callgrind/dhat).

Profiling shows the decrypt path is crypto-bound (~93% message_decrypt: SHA-256 +
curve25519); the batch orchestration this touches is ~0.06% of instructions, so
the clone removal is a hygiene win, not a throughput change. 933 lib tests pass,
clippy clean.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_0116cfRpumbEv3H4qUNmtNk4
@friendlyreviewer-staging

friendlyreviewer-staging Bot commented Jul 5, 2026

Copy link
Copy Markdown

Hi there 👋

⛅ Tech
3 medium, 1 low

The Merge Request introduces two key architectural improvements: lock narrowing in handle_decrypted_plaintext to release the per-sender guard before plaintext dispatch, and extraction of MigrationDecryptResult to simplify merge-site semantics. Both changes are logically sound and backward-compatible. However, critical test-coverage gaps remain: (1) no concurrency test validates that same-sender stanzas can interleave during dispatch under the narrow lock; (2) no test verifies FIFO ordering of deferred buffer for multi-payload session batches. Additionally, a minor fragility in variable shadowing and a type-unsafe benchmark helper should be addressed for long-term maintainability.


Worth checking

  • 🟡 src/message/receive.rs (L1126)
    Missing concurrency test: No functional test validates that concurrent same-sender stanzas can start decryption while one dispatches, potentially deadlocking or failing under the narrowed lock. The existing benchmark is single-threaded and does not cover this scenario.
  • 🟡 src/message/receive.rs (L1139)
    Missing ordering test: No test verifies that deferred buffer preserves FIFO ordering for multi-payload session batches (e.g., pkmsg followed by msg). Wire-order correctness is not explicitly validated.
  • 🟡 src/message/receive.rs (L931)
    Variable shadowing fragility: The local enc_type is shadowed from EncType to &'static str. While currently correct because as_wire_str() returns &'static str, a future refactor could silently break this code. Use a distinct variable name (e.g., enc_type_str) to decouple from the static lifetime guarantee.

Small things (take or leave)

  • 🔵 src/message/tests.rs (L10277)
    Benchmark helper type-unsafe: The bench_feed helper takes enc_type: &'static str and round-trips through string → Enum → string. An invalid type string would cause an unwrap() panic. Prefer using EncPayload::from_parts with an EncType variant to avoid the string round-trip and improve type safety.

Review time: 3m 4s

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