Skip to content

refactor: codebase quality audit — safety, tests, DRY, dep hygiene - #404

Merged
jlucaso1 merged 12 commits into
mainfrom
refactor/codebase-quality-audit
Mar 21, 2026
Merged

refactor: codebase quality audit — safety, tests, DRY, dep hygiene#404
jlucaso1 merged 12 commits into
mainfrom
refactor/codebase-quality-audit

Conversation

@jlucaso1

@jlucaso1 jlucaso1 commented Mar 21, 2026

Copy link
Copy Markdown
Collaborator

Summary

Comprehensive quality pass from a full codebase audit. All changes are non-breaking.

Security

  • Semaphore generation counter — add AtomicU64 generation to message_processing_semaphore. Bumped on every swap (disconnect + offline sync); message handlers check generation before AND after acquiring permits, closing the TOCTOU window during .await.
  • Handle Mutex poison gracefully — all 4 semaphore lock sites now use match with into_inner() instead of .unwrap()/.expect().
  • DeviceCommand::SetAdvSecretKey — persist rotated adv_secret_key in pair-code flow. HMAC verification remains disabled until QR pairing also rotates the key (documented in TODO).

Test coverage (+22 new tests)

  • hash.rs: 16 tests (was 0) — SHA-256 NIST vector, HMAC-SHA256 RFC 4231 vector, finalize_into bounds, error paths
  • aes_cbc.rs: 6 new tests (was 1) — NIST AES-256-CBC vector, roundtrip, empty/boundary, wrong key, bad ciphertext

DRY / consistency

  • Simplify .map_err(|e| anyhow!(e))? in upload.rs (crypto errors auto-convert)
  • Standardize Result<T> alias in messages.rs
  • Standardize &Arc<Client>&Client in Profile, ChatActions, MediaReupload (none clone the Arc)
  • Remove unused _max_bytes param, redundant imports, duplicate doc comments

Dependency hygiene

  • Remove itertools from wacore-libsignal (replaced .find_position() with std .position())
  • Move uuid to workspace dependencies

Test plan

  • cargo clippy --all --tests — zero warnings
  • cargo test --workspace --exclude e2e-tests — all tests pass
  • Validated with both default and --no-default-features
  • E2E pairing tests pass (HMAC verification correctly skipped for QR flow)

Summary by CodeRabbit

  • Bug Fixes

    • Improved robustness of message processing during offline sync with generation tracking to prevent stale operations.
    • Enhanced pairing flow with persistent key management.
  • Tests

    • Added comprehensive cryptographic function tests for AES-256-CBC encryption and HMAC-SHA256 verification.
  • Chores

    • Consolidated workspace dependency management.
    • Simplified internal API signatures and removed unused code imports.

- Handle Mutex poison gracefully in message_processing_semaphore
  (client.rs, message.rs) using match instead of .unwrap(). Scoped
  block ensures MutexGuard is dropped before .await points.
- Remove redundant #[allow(unused_imports)] in appstate_sync.rs — the
  test module already imports these types directly.
- Remove unused _max_bytes parameter from extract_content_uint() in
  prekeys.rs — all callers passed 4 but the value was never used.
- Change Profile, ChatActions, MediaReupload from &Arc<Client> to
  &Client — none of them clone the Arc or spawn tasks, so the
  indirection was unnecessary. Widens the API (non-Arc callers can
  now use these features).
- Replace commented-out MAC verification block in pair.rs with a
  structured TODO comment explaining the dependency on adv_secret_key
  persistence (pair_code.rs:321).
hash.rs (was 0 tests, now 12):
- SHA-256 known-answer (NIST "abc" vector)
- HMAC-SHA256 known-answer (RFC 4231 Test Case 2)
- finalize_sha256_array() correctness
- finalize_into() with correct and undersized buffers
- output_size() for all variants
- Unknown algorithm error handling

aes_cbc.rs (was 1 test, now 8):
- Basic encrypt/decrypt roundtrip
- NIST AES-256-CBC known-answer vector
- Empty plaintext (produces one padding block)
- Exact block boundary (16 bytes + full padding block)
- Decrypt with wrong key returns error
- Decrypt with invalid ciphertext length returns error
- Large data roundtrip
- Replace verbose .map_err(|e| anyhow::anyhow!(e)) with plain ? operator
  where the error type already implements std::error::Error (crypto::Error
  is converted automatically by anyhow)
- Standardize Result type signatures in messages.rs to use the anyhow
  Result<T> alias consistently instead of mixing with Result<T, anyhow::Error>
- Remove itertools from wacore-libsignal — only .find_position() was
  used, replaced with std .position() (identical behavior)
- Move uuid to workspace dependencies (shared by libsignal + e2e-tests)
- Move subtle to workspace dependencies
@coderabbitai

coderabbitai Bot commented Mar 21, 2026

Copy link
Copy Markdown

Warning

Rate limit exceeded

@jlucaso1 has exceeded the limit for the number of commits that can be reviewed per hour. Please wait 9 minutes and 38 seconds before requesting another review.

⌛ How to resolve this issue?

After the wait time has elapsed, 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 have higher rate limits than the trial, open-source and free plans. In all cases, we re-allow further reviews after a brief timeout.

Please see our FAQ for further information.

ℹ️ Review info
⚙️ Run configuration

Configuration used: Organization UI

Review profile: ASSERTIVE

Plan: Pro

Run ID: 812e815b-567d-49c0-a3c3-ab21723c01be

📥 Commits

Reviewing files that changed from the base of the PR and between 74b0bd1 and f376414.

📒 Files selected for processing (3)
  • src/client.rs
  • src/client/sessions.rs
  • src/message.rs
📝 Walkthrough

Walkthrough

Refactors many feature handles to borrow &Client instead of &Arc<Client>, centralizes workspace dependency declarations (uuid/itertools/futures), improves mutex poison handling and semaphore generation checks, persists rotated adv_secret_key via a new DeviceCommand, and adds extensive libsignal crypto unit tests.

Changes

Cohort / File(s) Summary
Workspace / Manifests
Cargo.toml, tests/e2e/Cargo.toml, wacore/libsignal/Cargo.toml
Moved uuid/itertools/futures uses to workspace-managed declarations ({ workspace = true }) and aligned dev-dependencies/features.
Client feature handles
src/features/chat_actions.rs, src/features/media_reupload.rs, src/features/profile.rs
Feature handle structs/constructors and client accessors changed to borrow &Client (removed &Arc<Client> requirements); accessor signatures changed from self: &Arc<Self>&self.
Semaphore / Mutex robustness
src/client.rs, src/message.rs, src/client/sessions.rs
Replaced lock().unwrap()/expect() with poison-tolerant match + into_inner() cloning; added message_semaphore_generation: Arc<AtomicU64> and generation checks around async semaphore acquire to drop stale permits.
Pair-code & persistence
src/pair_code.rs, wacore/src/store/commands.rs
handle_pair_code_notification now captures rotated adv_secret_key and sends DeviceCommand::SetAdvSecretKey([u8;32]) to persistence manager; added DeviceCommand::SetAdvSecretKey variant and apply logic.
Crypto unit tests
wacore/libsignal/src/crypto/aes_cbc.rs, wacore/libsignal/src/crypto/hash.rs
Added comprehensive AES-CBC and hash/HMAC unit tests (KATs, padding edge cases, negative tests).
API surface / error simplifications
wacore/src/messages.rs, wacore/src/upload.rs
Switched explicit Result<..., anyhow::Error> to crate Result<T> alias; replaced manual anyhow::anyhow! mappings with ? propagation in media encryption.
Small refactors / cleanup
src/appstate_sync.rs, wacore/libsignal/src/protocol/sender_keys.rs, wacore/src/iq/prekeys.rs, wacore/src/pair.rs
Removed unused imports/attributes, replaced itertools::find_position() with enumerate().find(), removed unused _max_bytes param and adjusted parsing, added // TODO(security) note where HMAC verification is skipped.

Sequence Diagram(s)

sequenceDiagram
  participant PairCodeHandler as PairCodeHandler
  participant PairCodeUtils as PairCodeUtils
  participant Persistence as PersistenceManager
  participant IQSender as IQSender

  PairCodeHandler->>PairCodeUtils: prepare_key_bundle(pair_code_data)
  Note right of PairCodeUtils: returns (bundle, rotated_adv_secret_key)
  PairCodeUtils-->>PairCodeHandler: (bundle, rotated_adv_secret_key)
  PairCodeHandler->>Persistence: process_command(DeviceCommand::SetAdvSecretKey(rotated_adv_secret_key))
  Persistence-->>PairCodeHandler: Ok / Err (ignored)
  PairCodeHandler->>IQSender: send companion_finish IQ (with bundle)
  IQSender-->>PairCodeHandler: send result
Loading

Estimated code review effort

🎯 4 (Complex) | ⏱️ ~45 minutes

Possibly related PRs

Poem

🐰 I swapped my Arc for a gentle borrow,

I nibbled at locks so no panics tomorrow,
Tests hopped in with ciphers and keys,
A secret key tucked where state agrees,
Workspace trimmed—carrots for code and sorrow 🥕

🚥 Pre-merge checks | ✅ 3
✅ Passed checks (3 passed)
Check name Status Explanation
Description Check ✅ Passed Check skipped - CodeRabbit’s high-level summary is enabled.
Title check ✅ Passed The title directly describes the main objectives of this repository-wide quality pass: safety fixes, test coverage improvements, code consistency (DRY), and dependency hygiene—all of which are clearly reflected in the comprehensive changeset across multiple files.
Docstring Coverage ✅ Passed Docstring coverage is 100.00% which is sufficient. The required threshold is 80.00%.

✏️ 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 refactor/codebase-quality-audit

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

Copy link
Copy Markdown
Collaborator Author

@claude review

@claude

claude Bot commented Mar 21, 2026

Copy link
Copy Markdown

Claude finished @jlucaso1's task in 1m 53s —— View job


PR Review: Codebase Quality Audit

  • Read and analyze all changed files
  • Review safety fixes (client.rs, message.rs, pair.rs)
  • Review new test coverage (hash.rs, aes_cbc.rs)
  • Review DRY/consistency changes
  • Review dependency hygiene changes
  • Post review feedback

Overall: This is a clean, well-scoped quality pass. All changes are correct and non-breaking. Approving with a few minor suggestions below.

Safety fixes — looks good

Mutex poison handling (src/client.rs:1017-1023, src/message.rs:499-502): Correct pattern — poisoned.into_inner() recovers the data since the semaphore replacement is itself an atomic, safe operation. The scoped block in client.rs ensuring the MutexGuard is dropped before .await is the right call (MutexGuard is !Send).

pair.rs TODO (wacore/src/pair.rs:147-151): Much better than the commented-out code block. The structured TODO clearly documents why MAC verification is disabled and what needs to happen first (persist rotated adv_secret_key). The cross-reference to pair_code.rs:321 is a nice touch.

Test coverage — excellent

The 22 new tests in hash.rs and aes_cbc.rs are well-structured:

  • NIST/RFC known-answer vectors provide confidence in correctness against authoritative sources
  • Boundary conditions (empty input, exact block boundary, wrong key, bad ciphertext length) cover the important edge cases
  • finalize_into buffer size checks verify the error paths
  • Algorithm alias coverage ensures the string-based API accepts all documented names

One minor note: the test_aes_cbc_decrypt_wrong_key test (aes_cbc.rs) asserts result.is_err() but doesn't verify the kind of error. With AES-CBC + PKCS7, wrong-key decryption usually fails on padding validation, but could theoretically produce valid-looking padding with a different plaintext. This is fine for a unit test — just noting it's technically probabilistic (with ~1/256 chance of false padding match for a 14-byte plaintext). Not worth changing.

DRY / consistency — correct

upload.rs: The .map_err(|e| anyhow::anyhow!(e)) removal is correct — CryptographicHash/CryptographicMac errors implement std::error::Error, so ? converts them via anyhow::Error::from() automatically.

Feature struct &Arc<Client>&Client: Verified that none of Profile, ChatActions, or MediaReupload clone the Arc or spawn tasks needing 'static ownership. All call sites go through Arc<Client> which auto-derefs to &Client, so this is seamless. Good simplification.

messages.rs Result<T, anyhow::Error>Result<T>: Clean — the Result alias is already in scope via use anyhow::Result.

prekeys.rs _max_bytes removal: Confirmed the parameter was genuinely unused — extract_content_uint always reads into a [0u8; 4] regardless.

Dependency hygiene — clean

itertools removal from wacore-libsignal: The replacement .iter().enumerate().find(|(_, state)| ...) in sender_keys.rs:386 is idiomatic and avoids pulling in a full dependency for a single call. The tuple destructuring (index, _state) matches the original find_position semantics exactly.

uuid workspace consolidation: Correctly moves the dep to the workspace [workspace.dependencies] table and references it with { workspace = true, features = ["v4"] } in both [dev-dependencies] and tests/e2e/Cargo.toml.

futures workspace reference in libsignal dev-deps: Good catch.

Minor note: The PR description mentions moving subtle to workspace, but the diff shows subtle = "2.6.1" remains as a local dep in wacore/libsignal/Cargo.toml. This is cosmetic — the description is slightly inaccurate but the code is correct as-is.

One suggestion for a follow-up

The chat_actions.rs doc comment change at line 453 creates a duplicate line:

/// Access chat management actions (archive, pin, mute, star).
///
/// Access chat management actions (archive, pin, mute, star).
pub fn chat_actions(&self) -> ChatActions<'_> {

The old text was "Requires Arc<Client> because app state mutations need key access." — now that the Arc requirement is removed, a simple single-line /// Access chat management actions (archive, pin, mute, star). would be cleaner. Fix this →


@github-actions

github-actions Bot commented Mar 21, 2026

Copy link
Copy Markdown

🐰 Bencher Report

Branchrefactor/codebase-quality-audit
Testbedubuntu-latest
Click to view all benchmark results
BenchmarkInstructionsBenchmark Result
instructions
(Result Δ%)
Upper Boundary
instructions
(Limit %)
binary_benchmark::attr_parser_group::bench_attr_parser attr_lookup:setup_attr_marshaled()📈 view plot
🚷 view threshold
6,197.00
(-7.09%)Baseline: 6,670.06
7,003.56
(88.48%)
binary_benchmark::child_iteration_group::bench_get_children_by_tag📈 view plot
🚷 view threshold
524,304.00
(-34.13%)Baseline: 796,004.92
835,805.16
(62.73%)
binary_benchmark::jid_optimization_group::bench_jid_to_owned_access jid_access:setup_jid_heavy_marshaled()📈 view plot
🚷 view threshold
20,868.00
(-7.75%)Baseline: 22,620.66
23,751.70
(87.86%)
binary_benchmark::marshal_group::bench_marshal_allocating📈 view plot
🚷 view threshold
98,202.00
(-21.06%)Baseline: 124,396.52
130,616.35
(75.18%)
binary_benchmark::marshal_group::bench_marshal_auto_allocating📈 view plot
🚷 view threshold
98,230.00
(-14.43%)Baseline: 114,799.03
120,538.98
(81.49%)
binary_benchmark::marshal_group::bench_marshal_auto_huge_bytes_allocating📈 view plot
🚷 view threshold
532,948.00
(-0.16%)Baseline: 533,811.19
560,501.75
(95.08%)
binary_benchmark::marshal_group::bench_marshal_auto_long_string📈 view plot
🚷 view threshold
15,870.00
(-6.90%)Baseline: 17,045.83
17,898.12
(88.67%)
binary_benchmark::marshal_group::bench_marshal_auto_many_children_allocating📈 view plot
🚷 view threshold
14,715,201.00
(-11.51%)Baseline: 16,629,502.56
17,460,977.69
(84.27%)
binary_benchmark::marshal_group::bench_marshal_exact_allocating📈 view plot
🚷 view threshold
118,358.00
(-27.98%)Baseline: 164,337.23
172,554.09
(68.59%)
binary_benchmark::marshal_group::bench_marshal_exact_huge_bytes_allocating📈 view plot
🚷 view threshold
534,378.00
(-0.16%)Baseline: 535,227.50
561,988.88
(95.09%)
binary_benchmark::marshal_group::bench_marshal_exact_long_string📈 view plot
🚷 view threshold
17,919.00
(-6.17%)Baseline: 19,098.28
20,053.20
(89.36%)
binary_benchmark::marshal_group::bench_marshal_exact_many_children_allocating📈 view plot
🚷 view threshold
28,066,347.00
(-29.24%)Baseline: 39,662,233.27
41,645,344.94
(67.39%)
binary_benchmark::marshal_group::bench_marshal_huge_bytes_allocating📈 view plot
🚷 view threshold
533,387.00
(-0.16%)Baseline: 534,250.19
560,962.70
(95.08%)
binary_benchmark::marshal_group::bench_marshal_long_string📈 view plot
🚷 view threshold
15,843.00
(-9.94%)Baseline: 17,591.42
18,470.99
(85.77%)
binary_benchmark::marshal_group::bench_marshal_many_children_allocating📈 view plot
🚷 view threshold
14,716,627.00
(-11.51%)Baseline: 16,630,471.89
17,461,995.49
(84.28%)
binary_benchmark::marshal_group::bench_marshal_reusing_buffer📈 view plot
🚷 view threshold
107,945.00
(-16.91%)Baseline: 129,911.80
136,407.39
(79.13%)
binary_benchmark::marshal_group::bench_marshal_reusing_buffer_vec_writer📈 view plot
🚷 view threshold
98,302.00
(-14.42%)Baseline: 114,871.03
120,614.58
(81.50%)
binary_benchmark::roundtrip_group::bench_roundtrip large:setup_large_marshaled()📈 view plot
🚷 view threshold
90,974.00
(-7.00%)Baseline: 97,818.70
102,709.64
(88.57%)
binary_benchmark::roundtrip_group::bench_roundtrip small:setup_small_marshaled()📈 view plot
🚷 view threshold
7,378.00
(-4.53%)Baseline: 7,728.31
8,114.73
(90.92%)
binary_benchmark::roundtrip_group::bench_roundtrip_auto large:setup_large_marshaled()📈 view plot
🚷 view threshold
91,005.00
(-2.66%)Baseline: 93,492.45
98,167.07
(92.70%)
binary_benchmark::roundtrip_group::bench_roundtrip_auto small:setup_small_marshaled()📈 view plot
🚷 view threshold
7,401.00
(+0.54%)Baseline: 7,361.04
7,729.10
(95.76%)
binary_benchmark::roundtrip_group::bench_roundtrip_exact large:setup_large_marshaled()📈 view plot
🚷 view threshold
106,790.00
(-2.28%)Baseline: 109,277.45
114,741.32
(93.07%)
binary_benchmark::roundtrip_group::bench_roundtrip_exact small:setup_small_marshaled()📈 view plot
🚷 view threshold
8,913.00
(+0.45%)Baseline: 8,873.04
9,316.70
(95.67%)
binary_benchmark::unmarshal_group::bench_unmarshal large:setup_large_marshaled()📈 view plot
🚷 view threshold
41,989.00
(-10.11%)Baseline: 46,710.36
49,045.88
(85.61%)
binary_benchmark::unmarshal_group::bench_unmarshal small:setup_small_marshaled()📈 view plot
🚷 view threshold
2,717.00
(-5.71%)Baseline: 2,881.43
3,025.50
(89.80%)
binary_benchmark::unpack_group::bench_unpack_compressed📈 view plot
🚷 view threshold
556,092.00
(+3.27%)Baseline: 538,488.08
565,412.49
(98.35%)
binary_benchmark::unpack_group::bench_unpack_uncompressed📈 view plot
🚷 view threshold
771.00
(-0.40%)Baseline: 774.06
812.77
(94.86%)
libsignal_benchmark::conversation_group::bench_full_dm_conversation full:setup_conversation_data()📈 view plot
🚷 view threshold
27,692,416.00
(-0.06%)Baseline: 27,708,759.25
29,094,197.21
(95.18%)
libsignal_benchmark::dm_group::bench_dm_decrypt_first_message decrypt_prekey:setup_dm_with_first_message()📈 view plot
🚷 view threshold
5,544,554.00
(-0.08%)Baseline: 5,549,007.18
5,826,457.54
(95.16%)
libsignal_benchmark::dm_group::bench_dm_encrypt_first_message first_msg:setup_dm_session()📈 view plot
🚷 view threshold
178,094.00
(+0.02%)Baseline: 178,049.50
186,951.97
(95.26%)
libsignal_benchmark::dm_group::bench_dm_encrypt_subsequent_message subsequent:setup_established_dm_session()📈 view plot
🚷 view threshold
178,905.00
(+0.02%)Baseline: 178,860.79
187,803.83
(95.26%)
libsignal_benchmark::dm_group::bench_dm_session_establishment setup:setup_dm_users()📈 view plot
🚷 view threshold
17,283,255.00
(-0.04%)Baseline: 17,290,032.65
18,154,534.29
(95.20%)
libsignal_benchmark::group_messaging_group::bench_group_create_distribution_message create:setup_group_sender()📈 view plot
🚷 view threshold
299,485.00
(+1.14%)Baseline: 296,122.39
310,928.51
(96.32%)
libsignal_benchmark::group_messaging_group::bench_group_decrypt_message decrypt:setup_group_with_encrypted_message()📈 view plot
🚷 view threshold
12,355,969.00
(-1.90%)Baseline: 12,594,815.60
13,224,556.38
(93.43%)
libsignal_benchmark::group_messaging_group::bench_group_encrypt_message encrypt:setup_group_with_distribution()📈 view plot
🚷 view threshold
719,113.00
(+0.44%)Baseline: 715,957.74
751,755.63
(95.66%)
libsignal_benchmark::session_optimization_group::bench_decrypt_with_previous_session previous_session:setup_with_archived_sessions()📈 view plot
🚷 view threshold
43,539.00
(+3.77%)Baseline: 41,958.19
44,056.10
(98.83%)
libsignal_benchmark::session_optimization_group::bench_message_key_eviction eviction:setup_message_key_eviction()📈 view plot
🚷 view threshold
15,561,842.00
(+0.00%)Baseline: 15,561,732.21
16,339,818.82
(95.24%)
libsignal_benchmark::session_optimization_group::bench_out_of_order_decryption out_of_order:setup_out_of_order_messages()📈 view plot
🚷 view threshold
5,509,042.00
(-0.03%)Baseline: 5,510,521.16
5,786,047.21
(95.21%)
libsignal_benchmark::session_optimization_group::bench_promote_matching_session promote:setup_promote_matching_session()📈 view plot
🚷 view threshold
958,490.00
(-0.03%)Baseline: 958,738.91
1,006,675.85
(95.21%)
libsignal_benchmark::signature_group::bench_key_generation keygen📈 view plot
🚷 view threshold
2,830,506.00
(+0.25%)Baseline: 2,823,507.88
2,964,683.28
(95.47%)
libsignal_benchmark::signature_group::bench_signature_creation sign:setup_keypair_with_message()📈 view plot
🚷 view threshold
3,452,844.00
(-0.73%)Baseline: 3,478,290.71
3,652,205.25
(94.54%)
libsignal_benchmark::signature_group::bench_signature_verification verify:setup_keypair_with_message()📈 view plot
🚷 view threshold
123,971,658.00
(-1.14%)Baseline: 125,407,092.92
131,677,447.56
(94.15%)
reporting_token_benchmark::content_extraction_group::bench_content_extraction extended:setup_extended_message()📈 view plot
🚷 view threshold
11,800.00
(+0.01%)Baseline: 11,798.92
12,388.87
(95.25%)
reporting_token_benchmark::content_extraction_group::bench_content_extraction simple:setup_simple_message()📈 view plot
🚷 view threshold
3,822.00
(-0.04%)Baseline: 3,823.35
4,014.52
(95.20%)
reporting_token_benchmark::full_generation_group::bench_full_token_generation extended:setup_full_gen_extended()📈 view plot
🚷 view threshold
87,705.00
(-0.25%)Baseline: 87,927.48
92,323.85
(95.00%)
reporting_token_benchmark::full_generation_group::bench_full_token_generation simple:setup_full_gen_simple()📈 view plot
🚷 view threshold
79,748.00
(-0.28%)Baseline: 79,974.56
83,973.29
(94.97%)
reporting_token_benchmark::key_derivation_group::bench_key_derivation📈 view plot
🚷 view threshold
51,008.00
(-0.02%)Baseline: 51,019.69
53,570.67
(95.22%)
reporting_token_benchmark::message_encoding_group::bench_message_encoding extended:setup_extended_message()📈 view plot
🚷 view threshold
5,754.00
(+0.10%)Baseline: 5,748.13
6,035.54
(95.34%)
reporting_token_benchmark::message_encoding_group::bench_message_encoding simple:setup_simple_message()📈 view plot
🚷 view threshold
2,119.00
(+0.03%)Baseline: 2,118.29
2,224.21
(95.27%)
reporting_token_benchmark::token_calculation_group::bench_token_calculation📈 view plot
🚷 view threshold
21,920.00
(+0.02%)Baseline: 21,916.12
23,011.93
(95.25%)
🐰 View full continuous benchmarking report in Bencher

@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: 4

Caution

Some comments are outside the diff and can’t be posted inline due to platform limitations.

⚠️ Outside diff range comments (1)
wacore/src/iq/prekeys.rs (1)

63-74: ⚠️ Potential issue | 🟠 Major

Reject malformed integer nodes instead of defaulting to 0.

Line 63 currently collapses missing/invalid integer bytes to 0, and Lines 240, 251, and 267 consume that for required key identifiers. This can silently accept malformed digest payloads and produce invalid key IDs.

Suggested fix
-fn extract_content_uint(node: Option<&Node>) -> u32 {
-    node.and_then(|n| match &n.content {
-        Some(NodeContent::Bytes(b)) => {
-            let mut buf = [0u8; 4];
-            let len = b.len().min(4);
-            buf[4 - len..].copy_from_slice(&b[..len]);
-            Some(u32::from_be_bytes(buf))
-        }
-        _ => None,
-    })
-    .unwrap_or(0)
+fn extract_content_uint(node: Option<&Node>) -> Result<u32, anyhow::Error> {
+    let bytes = node
+        .and_then(|n| match &n.content {
+            Some(NodeContent::Bytes(b)) => Some(b.as_slice()),
+            _ => None,
+        })
+        .ok_or_else(|| anyhow!("missing integer bytes"))?;
+
+    if bytes.is_empty() || bytes.len() > 4 {
+        return Err(anyhow!("invalid integer byte length: {}", bytes.len()));
+    }
+
+    let mut buf = [0u8; 4];
+    buf[4 - bytes.len()..].copy_from_slice(bytes);
+    Ok(u32::from_be_bytes(buf))
 }
-        let reg_id = extract_content_uint(Some(reg_node));
+        let reg_id = extract_content_uint(Some(reg_node))?;
...
-                extract_content_uint(skey.get_optional_child("id")),
+                extract_content_uint(skey.get_optional_child("id"))?,
...
-        let prekey_ids = digest_node
+        let prekey_ids = digest_node
             .get_optional_child("list")
             .and_then(|list| list.children())
             .map(|children| {
                 children
                     .iter()
                     .filter(|child| child.tag == "key")
-                    .map(|child| extract_content_uint(Some(child)))
-                    .collect()
+                    .map(|child| extract_content_uint(Some(child)))
+                    .collect::<Result<Vec<_>, _>>()
             })
+            .transpose()?
             .unwrap_or_default();

Also applies to: 240-241, 251-252, 267-268

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@wacore/src/iq/prekeys.rs` around lines 63 - 74, extract_content_uint
currently returns 0 for missing/invalid integer bytes which masks malformed
payloads; change its signature to return Option<u32> (or Result<u32, _>) and
have it return None (or Err) on any non-Bytes content or wrong-length/malformed
bytes instead of unwrap_or(0). Then update all call sites that assume a default
0 (the places that use this function for required key identifiers) to handle the
Option/Result by rejecting the message or propagating the error instead of
treating 0 as a valid id so malformed integer nodes are rejected early.
🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.

Inline comments:
In `@src/client.rs`:
- Around line 1019-1025: The semaphore reset can be bypassed by stale Arc
clones; add an epoch/generation counter paired with
self.message_processing_semaphore (e.g., a AtomicU64 or AtomicUsize field like
message_semaphore_epoch) and bump it whenever you replace the Semaphore in the
reconnect/reset code (the block that currently swaps
self.message_processing_semaphore to a new async_lock::Semaphore). Update permit
acquisition logic (the code in src/message.rs that clones/acquires the
semaphore) to capture the current epoch at acquisition and validate it before
treating the permit as valid (reject or drop permits whose captured epoch
differs from the current epoch), and ensure any permit wrappers include the
epoch so stale permits are detected and refused.

In `@src/features/chat_actions.rs`:
- Around line 451-454: The rustdoc for the chat_actions accessor repeats the
same summary line twice; edit the doc comment above pub fn chat_actions(&self)
-> ChatActions<'_> to remove the duplicate sentence so only a single summary
line describing "Access chat management actions (archive, pin, mute, star)."
remains; ensure the remaining doc uses the existing wording and that
ChatActions<'_> and chat_actions are unchanged.

In `@src/message.rs`:
- Around line 499-502: The semaphore lock handling for
message_processing_semaphore is inconsistent: replace usages that call .expect()
(referenced in the code paths using message_processing_semaphore in the
functions in sessions.rs and client.rs) with the same graceful poison recovery
used in message.rs — i.e., call .lock() and match on Ok(guard) => guard.clone(),
Err(poisoned) => poisoned.into_inner().clone() (or equivalent pattern) so all
sites consistently recover from a poisoned mutex without panicking; apply this
change to the functions that currently use .expect() on
message_processing_semaphore.

In `@wacore/src/pair.rs`:
- Around line 147-151: The TODO referencing persisting adv_secret_key via
DeviceCommand::SetAdvSecretKey is misleading because that variant doesn't exist;
update the comment in pair.rs (near the HMAC verification TODO) to either (a)
state explicitly that a new DeviceCommand::SetAdvSecretKey variant must be
implemented as a prerequisite for enabling HMAC verification, or (b) replace the
reference with a clear description of the required persistence mechanism (e.g.,
"persist rotated adv_secret_key to device storage via a new command/flow") and
mark it as a blocker, and also add a cross-reference to pair_code.rs where the
persistence should be implemented; keep the note that ED25519 account signature
verification remains the primary authentication.

---

Outside diff comments:
In `@wacore/src/iq/prekeys.rs`:
- Around line 63-74: extract_content_uint currently returns 0 for
missing/invalid integer bytes which masks malformed payloads; change its
signature to return Option<u32> (or Result<u32, _>) and have it return None (or
Err) on any non-Bytes content or wrong-length/malformed bytes instead of
unwrap_or(0). Then update all call sites that assume a default 0 (the places
that use this function for required key identifiers) to handle the Option/Result
by rejecting the message or propagating the error instead of treating 0 as a
valid id so malformed integer nodes are rejected early.
🪄 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: Organization UI

Review profile: ASSERTIVE

Plan: Pro

Run ID: 39879c20-26b2-48be-94fa-43e4963c8757

📥 Commits

Reviewing files that changed from the base of the PR and between 0f66fd7 and 9edba90.

⛔ Files ignored due to path filters (1)
  • Cargo.lock is excluded by !**/*.lock
📒 Files selected for processing (16)
  • Cargo.toml
  • src/appstate_sync.rs
  • src/client.rs
  • src/features/chat_actions.rs
  • src/features/media_reupload.rs
  • src/features/profile.rs
  • src/message.rs
  • tests/e2e/Cargo.toml
  • wacore/libsignal/Cargo.toml
  • wacore/libsignal/src/crypto/aes_cbc.rs
  • wacore/libsignal/src/crypto/hash.rs
  • wacore/libsignal/src/protocol/sender_keys.rs
  • wacore/src/iq/prekeys.rs
  • wacore/src/messages.rs
  • wacore/src/pair.rs
  • wacore/src/upload.rs
💤 Files with no reviewable changes (1)
  • src/appstate_sync.rs

Comment thread src/client.rs Outdated
Comment thread src/features/chat_actions.rs Outdated
Comment thread src/message.rs Outdated
Comment thread wacore/src/pair.rs Outdated
- Fix remaining .expect() on message_processing_semaphore in
  sessions.rs and client.rs test helper to use match + into_inner(),
  consistent with the pattern in client.rs:1020 and message.rs:499
- Remove duplicate doc comment on chat_actions() accessor
- Clarify pair.rs TODO: DeviceCommand::SetAdvSecretKey does not exist
  yet — must be implemented as a prerequisite for HMAC verification
Workers could clone the semaphore Arc just before a disconnect swaps
it, then acquire permits on the abandoned semaphore. The generation
counter (AtomicU64) is bumped on every swap; message handlers compare
it before/after cloning to reject stale references.

Also simplifies verbose comments across client.rs, message.rs, pair.rs.
- Add DeviceCommand::SetAdvSecretKey([u8; 32]) variant
- Persist rotated adv_secret_key in pair_code.rs after key bundle prep
- Re-enable the HMAC verification check in do_pair_crypto() that was
  disabled since the key wasn't being persisted
- ED25519 signature verification remains as the primary auth gate;
  HMAC now provides defense-in-depth

@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

♻️ Duplicate comments (1)
src/client.rs (1)

1019-1024: ⚠️ Potential issue | 🟠 Major

Stale Arc<Semaphore> clones can still bypass this reset.

src/message.rs:497-507 clones the current semaphore Arc before awaiting acquire_arc(). Any task that grabs the old Arc just before Line 1024 can keep acquiring permits on the abandoned semaphore after reconnect, so cleanup_connection_state() does not reliably restore the single-permit gate. That leaves stale message-processing work running after teardown has already cleared caches/state, and the same swap pattern is also used in src/client/sessions.rs Lines 41-45.

Pair the semaphore with a generation/epoch (for example alongside connection_generation) and discard permits/work captured from stale generations.

Run this read-only check to confirm the swap/acquire pattern across the affected sites:

#!/bin/bash
set -euo pipefail

echo '--- src/client.rs:1017-1025 ---'
sed -n '1017,1025p' src/client.rs

echo
echo '--- src/client/sessions.rs:40-46 ---'
sed -n '40,46p' src/client/sessions.rs

echo
echo '--- src/message.rs:497-507 ---'
sed -n '497,507p' src/message.rs

Expected result: the reset sites replace the stored Arc<async_lock::Semaphore>, while src/message.rs clones that Arc and then awaits acquire_arc(), which is what lets pre-swap clones outlive the reset.

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@src/client.rs` around lines 1019 - 1024, The semaphore swap in
cleanup_connection_state (the block that replaces the stored Arc<Semaphore>) can
be bypassed by tasks that cloned the old Arc before the swap (see the acquire
pattern in message.rs where a clone is taken prior to await acquire_arc()), so
add a generation/epoch alongside the semaphore (e.g., reuse/extend
connection_generation) and store them together (or wrap in a small struct) so
that callers capture both the Arc<Semaphore> and the current generation when
they clone; after acquiring a permit, the caller must compare the captured
generation to the current generation and if it differs immediately drop/release
the permit and fail the operation, and apply the same pattern to the sessions
swap (the same Arc-swap in sessions.rs) so stale clones cannot continue working
against a torn-down connection.
🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.

Inline comments:
In `@wacore/src/pair.rs`:
- Around line 149-150: Replace the hardcoded line-number reference
"src/pair_code.rs:321" with a durable symbol-based reference to the function or
item that performs the persistence step in src/pair_code.rs (i.e., refer to the
concrete persistence function/name used to persist pair code such as the
function that implements the persistence step in pair_code.rs), updating the
comment in pair.rs to mention that symbol instead of a line number.

---

Duplicate comments:
In `@src/client.rs`:
- Around line 1019-1024: The semaphore swap in cleanup_connection_state (the
block that replaces the stored Arc<Semaphore>) can be bypassed by tasks that
cloned the old Arc before the swap (see the acquire pattern in message.rs where
a clone is taken prior to await acquire_arc()), so add a generation/epoch
alongside the semaphore (e.g., reuse/extend connection_generation) and store
them together (or wrap in a small struct) so that callers capture both the
Arc<Semaphore> and the current generation when they clone; after acquiring a
permit, the caller must compare the captured generation to the current
generation and if it differs immediately drop/release the permit and fail the
operation, and apply the same pattern to the sessions swap (the same Arc-swap in
sessions.rs) so stale clones cannot continue working against a torn-down
connection.
🪄 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: Organization UI

Review profile: ASSERTIVE

Plan: Pro

Run ID: 653bfe56-b264-4371-8d83-926ce6fc4316

📥 Commits

Reviewing files that changed from the base of the PR and between 9edba90 and 02d309e.

📒 Files selected for processing (4)
  • src/client.rs
  • src/client/sessions.rs
  • src/features/chat_actions.rs
  • wacore/src/pair.rs

Comment thread wacore/src/pair.rs Outdated
The HMAC check fails for QR-paired devices because adv_secret_key is
only rotated in the pair-code flow (via SetAdvSecretKey). QR pairing
keeps the initial random key from Device::new(), which won't match
the server's HMAC. Keep the verification disabled until both pairing
paths persist the correct key.

The SetAdvSecretKey command and pair-code persistence are retained
for when QR pairing key rotation is implemented.
@jlucaso1

Copy link
Copy Markdown
Collaborator Author

@claude review

@claude

claude Bot commented Mar 21, 2026

Copy link
Copy Markdown

Claude finished @jlucaso1's task in 1m 19s —— View job


PR Review: Codebase Quality Audit

  • Read and analyze all changed files
  • Review security fixes (semaphore, mutex poison, HMAC)
  • Review new test coverage (hash.rs, aes_cbc.rs)
  • Review DRY/consistency changes
  • Review dependency hygiene changes
  • Post review feedback

Overall: Clean, well-scoped quality pass. All changes are correct and non-breaking. One real issue found (TOCTOU race in generation check), rest is minor.

Security fixes

Semaphore generation counter (src/client.rs:298-302, src/message.rs:498-512): Good addition. The AtomicU64 generation counter paired with the semaphore swap addresses the stale-Arc problem correctly. However, there's a TOCTOU race in message.rs:498-512:

let generation = self.message_semaphore_generation.load(SeqCst);  // T1
let semaphore = match self.message_processing_semaphore.lock() {  // T2
    Ok(guard) => guard.clone(),
    Err(poisoned) => poisoned.into_inner().clone(),
};
if generation != self.message_semaphore_generation.load(SeqCst) { // T3
    return;
}
let _global_permit = semaphore.acquire_arc().await;                // T4

A generation bump can happen between T3 and T4 (the .await point). The check at T3 only validates the semaphore wasn't swapped between T1-T3, but .acquire_arc().await can suspend arbitrarily. In practice this is low-risk since the old semaphore Arc draining is benign (old workers finish normally), and the generation check is already a significant improvement over nothing. But if you want airtight protection, you'd need a post-acquire generation recheck. Worth noting for a follow-up, not a blocker.

Mutex poison handling (src/client.rs:1019-1023, src/client/sessions.rs:43-46, src/message.rs:501-503, src/client.rs:3794-3796): All 4 lock sites now use match + into_inner() consistently. The scoped blocks in client.rs and sessions.rs correctly ensure MutexGuard (which is !Send) is dropped before any .await. Good.

DeviceCommand::SetAdvSecretKey (wacore/src/store/commands.rs, src/pair_code.rs:334-340): Clean implementation. The new variant persists the rotated adv_secret_key after key bundle preparation in pair-code flow. The TODO in pair.rs:147-151 correctly documents that QR pairing still uses the initial random key, and HMAC verification remains skipped until both paths persist the correct key. ED25519 signature verification remains as primary auth.

Test coverage — excellent

The 22 new tests in hash.rs and aes_cbc.rs are well-structured with NIST/RFC known-answer vectors, boundary conditions, error paths, and algorithm alias coverage. No issues found.

DRY / consistency — correct

  • &Arc<Client>&Client in Profile, ChatActions, MediaReupload: Verified none clone the Arc or need 'static ownership. Arc<Client> auto-derefs, so call sites are seamless.
  • upload.rs: .map_err(|e| anyhow!(e)) removal is correct — the errors implement std::error::Error, so ? auto-converts.
  • messages.rs: Result<T, anyhow::Error>Result<T> using the in-scope alias. Clean.
  • prekeys.rs _max_bytes removal: Confirmed unused — always reads into [0u8; 4].

Dependency hygiene — clean

  • itertools removal: .iter().enumerate().find() in sender_keys.rs:386 is idiomatic replacement for find_position().
  • uuid workspace consolidation: Correct workspace = true references.
  • futures workspace reference: Good catch.

Note: PR description mentions moving subtle to workspace, but the diff shows subtle = "2.6.1" remains local in wacore/libsignal/Cargo.toml. Description is slightly inaccurate but code is correct as-is.

Minor note

Previous review flagged a duplicate doc comment on chat_actions(). Looking at the current diff, this was already fixed — the old Requires Arc<Client> line was removed and only one summary line remains.


View job

The semaphore generation could change during the .acquire_arc().await
suspension point. Add a second generation check after acquiring the
permit so stale permits from a previous connection are dropped.

@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: 3

🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.

Inline comments:
In `@src/client.rs`:
- Around line 1019-1025: The current code increments
message_semaphore_generation before replacing the Arc inside
message_processing_semaphore, making (new_generation, old_semaphore) visible;
change the order so you first replace the Arc (assign *guard =
Arc::new(async_lock::Semaphore::new(1))) and only after dropping/releasing the
guard call message_semaphore_generation.fetch_add(1, Ordering::SeqCst) so the
generation bump always corresponds to the new semaphore instance and prevents
stale clones from being observed as current.

In `@src/client/sessions.rs`:
- Around line 41-47: The generation bump races with exposing the new 64-permit
semaphore: in sessions.rs the code currently increments
message_semaphore_generation before replacing message_processing_semaphore,
allowing stale Arcs to appear current; to fix, acquire the mutex, replace the
Arc with the new async_lock::Semaphore::new(64) while still holding the guard,
then increment message_semaphore_generation (fetch_add) before releasing the
guard so the swap and bump are atomic from readers' perspective; extract this
sequence into a helper (e.g., swap_message_semaphore or
set_message_semaphore_generation) and call it from both this site and
cleanup_connection_state() to keep behavior consistent.

In `@src/message.rs`:
- Around line 496-522: The current logic may drop live messages when
message_semaphore_generation changes; instead make the sequence robust by
looping to obtain a stable (generation, semaphore) pair: inside a retry loop,
lock message_processing_semaphore, clone the Arc semaphore and read
message_semaphore_generation into a local variable, then release the mutex and
call acquire_arc(), and after acquiring re-read message_semaphore_generation —
if it differs from the saved generation, drop/release the acquired permit and
retry the loop; only proceed when the saved generation equals the current
generation so the acquired permit corresponds to the current semaphore. Ensure
this uses the existing symbols message_semaphore_generation,
message_processing_semaphore, acquire_arc, and the local _global_permit
semantics so you don't leak permits on retries.
🪄 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: Organization UI

Review profile: ASSERTIVE

Plan: Pro

Run ID: 3b6b5a82-9c21-45e1-8a4b-76a9e2e2068d

📥 Commits

Reviewing files that changed from the base of the PR and between 02d309e and 74b0bd1.

📒 Files selected for processing (6)
  • src/client.rs
  • src/client/sessions.rs
  • src/message.rs
  • src/pair_code.rs
  • wacore/src/pair.rs
  • wacore/src/store/commands.rs

Comment thread src/client.rs Outdated
Comment thread src/client/sessions.rs Outdated
Comment on lines +41 to +47
self.message_semaphore_generation
.fetch_add(1, std::sync::atomic::Ordering::SeqCst);
let mut guard = match self.message_processing_semaphore.lock() {
Ok(g) => g,
Err(poisoned) => poisoned.into_inner(),
};
*guard = std::sync::Arc::new(async_lock::Semaphore::new(64));

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🔴 Critical

Keep the new generation paired with the new 64-permit semaphore.

This has the same atomicity bug as cleanup_connection_state(): the generation is advanced before the mutex stops exposing the old 1-permit semaphore. That leaves a window where stale Arcs can still look current during the offline-sync → parallel-processing handoff. Swap first, then bump while the guard is still held. I’d also extract this into a small helper so both swap sites stay consistent.

Proposed fix
             {
-                self.message_semaphore_generation
-                    .fetch_add(1, std::sync::atomic::Ordering::SeqCst);
                 let mut guard = match self.message_processing_semaphore.lock() {
                     Ok(g) => g,
                     Err(poisoned) => poisoned.into_inner(),
                 };
                 *guard = std::sync::Arc::new(async_lock::Semaphore::new(64));
+                self.message_semaphore_generation
+                    .fetch_add(1, std::sync::atomic::Ordering::SeqCst);
             }
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@src/client/sessions.rs` around lines 41 - 47, The generation bump races with
exposing the new 64-permit semaphore: in sessions.rs the code currently
increments message_semaphore_generation before replacing
message_processing_semaphore, allowing stale Arcs to appear current; to fix,
acquire the mutex, replace the Arc with the new async_lock::Semaphore::new(64)
while still holding the guard, then increment message_semaphore_generation
(fetch_add) before releasing the guard so the swap and bump are atomic from
readers' perspective; extract this sequence into a helper (e.g.,
swap_message_semaphore or set_message_semaphore_generation) and call it from
both this site and cleanup_connection_state() to keep behavior consistent.

Comment thread src/message.rs
Comment on lines +496 to +522
// Acquire global processing permit (1 during offline sync, N after).
// Generation check rejects stale Arc clones from a previous connection.
let generation = self
.message_semaphore_generation
.load(std::sync::atomic::Ordering::SeqCst);
let semaphore = match self.message_processing_semaphore.lock() {
Ok(guard) => guard.clone(),
Err(poisoned) => poisoned.into_inner().clone(),
};
if generation
!= self
.message_semaphore_generation
.load(std::sync::atomic::Ordering::SeqCst)
{
log::debug!("Stale semaphore generation, skipping message batch");
return;
}
let _global_permit = semaphore.acquire_arc().await;
// Post-acquire recheck: generation could have changed during the .await
if generation
!= self
.message_semaphore_generation
.load(std::sync::atomic::Ordering::SeqCst)
{
log::debug!("Semaphore generation changed during acquire, dropping stale permit");
return;
}

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🔴 Critical

Retry generation mismatches instead of dropping the batch.

message_semaphore_generation is bumped before the mutex-protected swap in src/client.rs and src/client/sessions.rs, so a change between Line 498 and Line 501 can still leave semaphore pointing at the current Arc. Returning on Line 510 or Line 520 then silently drops a live message during reconnect/offline-sync races, including the same-connection 1→64 transition in complete_offline_sync. This path should retry until it gets a stable (generation, semaphore) pair, and reacquire if the generation changes while awaiting the permit.

🔁 Suggested fix
-        let generation = self
-            .message_semaphore_generation
-            .load(std::sync::atomic::Ordering::SeqCst);
-        let semaphore = match self.message_processing_semaphore.lock() {
-            Ok(guard) => guard.clone(),
-            Err(poisoned) => poisoned.into_inner().clone(),
-        };
-        if generation
-            != self
-                .message_semaphore_generation
-                .load(std::sync::atomic::Ordering::SeqCst)
-        {
-            log::debug!("Stale semaphore generation, skipping message batch");
-            return;
-        }
-        let _global_permit = semaphore.acquire_arc().await;
-        // Post-acquire recheck: generation could have changed during the .await
-        if generation
-            != self
-                .message_semaphore_generation
-                .load(std::sync::atomic::Ordering::SeqCst)
-        {
-            log::debug!("Semaphore generation changed during acquire, dropping stale permit");
-            return;
-        }
+        let _global_permit = loop {
+            let generation = self
+                .message_semaphore_generation
+                .load(std::sync::atomic::Ordering::SeqCst);
+            let semaphore = match self.message_processing_semaphore.lock() {
+                Ok(guard) => guard.clone(),
+                Err(poisoned) => poisoned.into_inner().clone(),
+            };
+
+            if generation
+                != self
+                    .message_semaphore_generation
+                    .load(std::sync::atomic::Ordering::SeqCst)
+            {
+                log::debug!("Semaphore generation changed while cloning, retrying");
+                continue;
+            }
+
+            let permit = semaphore.acquire_arc().await;
+
+            if generation
+                == self
+                    .message_semaphore_generation
+                    .load(std::sync::atomic::Ordering::SeqCst)
+            {
+                break permit;
+            }
+
+            log::debug!("Semaphore generation changed during acquire, retrying");
+        };
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
// Acquire global processing permit (1 during offline sync, N after).
// Generation check rejects stale Arc clones from a previous connection.
let generation = self
.message_semaphore_generation
.load(std::sync::atomic::Ordering::SeqCst);
let semaphore = match self.message_processing_semaphore.lock() {
Ok(guard) => guard.clone(),
Err(poisoned) => poisoned.into_inner().clone(),
};
if generation
!= self
.message_semaphore_generation
.load(std::sync::atomic::Ordering::SeqCst)
{
log::debug!("Stale semaphore generation, skipping message batch");
return;
}
let _global_permit = semaphore.acquire_arc().await;
// Post-acquire recheck: generation could have changed during the .await
if generation
!= self
.message_semaphore_generation
.load(std::sync::atomic::Ordering::SeqCst)
{
log::debug!("Semaphore generation changed during acquire, dropping stale permit");
return;
}
// Acquire global processing permit (1 during offline sync, N after).
// Generation check rejects stale Arc clones from a previous connection.
let _global_permit = loop {
let generation = self
.message_semaphore_generation
.load(std::sync::atomic::Ordering::SeqCst);
let semaphore = match self.message_processing_semaphore.lock() {
Ok(guard) => guard.clone(),
Err(poisoned) => poisoned.into_inner().clone(),
};
if generation
!= self
.message_semaphore_generation
.load(std::sync::atomic::Ordering::SeqCst)
{
log::debug!("Semaphore generation changed while cloning, retrying");
continue;
}
let permit = semaphore.acquire_arc().await;
if generation
== self
.message_semaphore_generation
.load(std::sync::atomic::Ordering::SeqCst)
{
break permit;
}
log::debug!("Semaphore generation changed during acquire, retrying");
};
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@src/message.rs` around lines 496 - 522, The current logic may drop live
messages when message_semaphore_generation changes; instead make the sequence
robust by looping to obtain a stable (generation, semaphore) pair: inside a
retry loop, lock message_processing_semaphore, clone the Arc semaphore and read
message_semaphore_generation into a local variable, then release the mutex and
call acquire_arc(), and after acquiring re-read message_semaphore_generation —
if it differs from the saved generation, drop/release the acquired permit and
retry the loop; only proceed when the saved generation equals the current
generation so the acquired permit corresponds to the current semaphore. Ensure
this uses the existing symbols message_semaphore_generation,
message_processing_semaphore, acquire_arc, and the local _global_permit
semantics so you don't leak permits on retries.

The generation bump and Arc swap must happen under the same mutex hold
so readers always see a consistent (generation, semaphore) pair.

Writer (swap_message_semaphore helper):
  1. Lock mutex
  2. Replace Arc
  3. Bump generation
  4. Unlock
Both client.rs (disconnect) and sessions.rs (offline sync) now use
the same helper.

Reader (message.rs):
  Read generation inside the mutex (not before it) so the
  (generation, Arc) pair is always consistent. Removed the redundant
  pre-acquire check — it was always true with correct ordering. Only
  the post-acquire check remains for races during .await.
@jlucaso1
jlucaso1 merged commit 3c4efb1 into main Mar 21, 2026
8 checks passed
@jlucaso1
jlucaso1 deleted the refactor/codebase-quality-audit branch March 21, 2026 20:08
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