You signed in with another tab or window. Reload to refresh your session.You signed out in another tab or window. Reload to refresh your session.You switched accounts on another tab or window. Reload to refresh your session.Dismiss alert
This is the tracking issue for the workspace-wide code quality review conducted on 2026-04-10. Sixteen per-crate review agents surveyed every crate in the repo, and every critical/high finding was independently verified — some with working exploit code built against the real crates.
Verification tally: 14 confirmed, 3 severity-downgraded, 4 agent false positives out of ~20 top-severity claims. Details in the section below.
Verified bugs with working exploits
Two bugs were reproduced with standalone Rust binaries built against the real crates:
RotateChannelKey has no permission check. An outsider (mallory) who is not a member, not an admin, and has never interacted with the server can emit a RotateChannelKey event and inject arbitrary encrypted-key bytes into ServerState::channel_keys. Confirmed with a 60-line binary against willow-state. See [state] RotateChannelKey has no permission or membership check #109.
Ratchet-counter DoS in open_content.derive_message_key loops from counter=1 to the attacker-controlled value, doing 2 HKDF-Expand ops per step, before AEAD verification. Measured: 1e6 counter = 1.0 s; u64::MAX counter ≈ 584 000 years on a single core. Any peer subscribed to a channel topic can freeze every recipient's CPU with one malformed packet. See [crypto] Bound ratchet_counter in open_content to fix CPU DoS #110.
For the record, these were flagged by reviewers but ruled out by direct testing or re-reading:
Transport nested-payload DoS — a real test binary was built; bincode 1.3 rejects a forged frame with a u64::MAX inner Vec length in ~150 ns with no pre-allocation. The existing MAX_DESER_SIZE outer cap is sufficient.
SignedMessage unbounded fields — SignedMessage is always packed/unpacked through willow_transport::pack/unpack, which bounds the entire blob at 256 KB; verify() uses TryInto<[u8; 32]>/TryInto<[u8; 64]> which reject wrong-length keys/sigs.
Actor Recipient::do_send semantically wrong — the dropped-oneshot pattern is documented on addr.rs:145-147 as correct for any M::Result type. Handler runs, value is computed, oneshot send silently fails. That is exactly "fire and forget".
PendingBuffer::evict_to non-deterministic — BTreeMap::keys().next() returns the smallest key in fully deterministic sorted order.
Dedup-before-trust race in handle_op — mechanism exists but is not exploitable with random-UUID op_ids; an attacker cannot predict future legitimate op_ids. Downgraded to a code-smell reorder in [app] Reorder dedup-before-trust check in handle_op #125.
Summary
This is the tracking issue for the workspace-wide code quality review conducted on 2026-04-10. Sixteen per-crate review agents surveyed every crate in the repo, and every critical/high finding was independently verified — some with working exploit code built against the real crates.
Verification tally: 14 confirmed, 3 severity-downgraded, 4 agent false positives out of ~20 top-severity claims. Details in the section below.
Verified bugs with working exploits
Two bugs were reproduced with standalone Rust binaries built against the real crates:
RotateChannelKeyhas no permission check. An outsider (mallory) who is not a member, not an admin, and has never interacted with the server can emit aRotateChannelKeyevent and inject arbitrary encrypted-key bytes intoServerState::channel_keys. Confirmed with a 60-line binary againstwillow-state. See [state] RotateChannelKey has no permission or membership check #109.open_content.derive_message_keyloops from counter=1 to the attacker-controlled value, doing 2 HKDF-Expand ops per step, before AEAD verification. Measured: 1e6 counter = 1.0 s; u64::MAX counter ≈ 584 000 years on a single core. Any peer subscribed to a channel topic can freeze every recipient's CPU with one malformed packet. See [crypto] Bound ratchet_counter in open_content to fix CPU DoS #110.Priority ordering
Critical — verified, fix first
RotateChannelKeyratchet_counterinopen_content(author, emoji)lock().unwrap()poison panic vectorsServer::adminsand friends privateconnection_events()placeholderMedium — verified, schedule after critical
derive_message_keyto avoid O(counter) replayEventHash → indexmap for message opslet _ =sites innetwork_bridge.rs0600perms + permission validation on loadSecretKeyandChannelKeyon drop.unwrap()calls inResulthelpersCallPagestate::select/state::mutatecallsProcess
let _ =onResultexpressionsSuggested sequencing
If picking the next two weeks of work in order:
Findings that did not survive verification
For the record, these were flagged by reviewers but ruled out by direct testing or re-reading:
u64::MAXinner Vec length in ~150 ns with no pre-allocation. The existingMAX_DESER_SIZEouter cap is sufficient.SignedMessageunbounded fields —SignedMessageis always packed/unpacked throughwillow_transport::pack/unpack, which bounds the entire blob at 256 KB;verify()usesTryInto<[u8; 32]>/TryInto<[u8; 64]>which reject wrong-length keys/sigs.Recipient::do_sendsemantically wrong — the dropped-oneshot pattern is documented onaddr.rs:145-147as correct for anyM::Resulttype. Handler runs, value is computed, oneshot send silently fails. That is exactly "fire and forget".PendingBuffer::evict_tonon-deterministic —BTreeMap::keys().next()returns the smallest key in fully deterministic sorted order.std::sync::mpsc::Sender(unbounded), sosend()only fails during shutdown. Downgraded to medium with narrower scope in [app] Audit let _ = sites in network_bridge.rs for swallowed errors #124.PendingBuffersilent event loss —evict_toactually returns the eviction count; only the single call site discards it. Downgraded to a log-only fix in [state] Log pending-buffer evictions instead of silently discarding #123.handle_op— mechanism exists but is not exploitable with random-UUID op_ids; an attacker cannot predict future legitimate op_ids. Downgraded to a code-smell reorder in [app] Reorder dedup-before-trust check in handle_op #125.Process recommendations
let _ = Resultbleeding. The silent-error pattern is endemic and cheap to lint. Tracked in [ci] Deny let _ = on Result expressions #131.willow-channel(data model) andwillow-state(authority). Tracked in [docs] Write one-page authority spec: what enforces what #132.Each child issue is scoped to roughly one PR and has the affected file and line range, a fix sketch, and a test to add. Pick one off the list and go.