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
NoiseTcpWriteHalf::try_write_frame encrypts the frame (advancing the Noise nonce) before it attempts the socket write.
If the write returns WouldBlock, the method returns Ok(false) and discards both the ciphertext and the already-consumed nonce.
The next write — whether a retry of the same frame or a new one — is sealed under a later nonce while the peer's decryptor is still expecting the discarded one. The peer's next decryption fails its MAC check and the connection is permanently desynchronized.
Partial writes have a related problem: a ciphertext prefix can reach the socket while the remainder is discarded.
ChaCha20-Poly1305 is deterministic given (key, nonce, plaintext), and the Noise transport nonce is a strictly monotonic counter.
Once the frame is encrypted, it is bound to a specific nonce.
The Ok(false) contract ("not ready, retry later") is therefore unfulfillable: there is no way to retry that frame under the same nonce. The retry seals a different nonce, the peer's decryptor is out of step, authentication fails, and the connection dies (or, with a partial write, the peer receives a truncated ciphertext it can never reconcile).
Contrast the read half — it is safe by construction:
try_read_frame returns Ok(None) on WouldBlockbefore any decryption is attempted. The decrypt nonce is only ever consumed once a complete frame is present.
Impact
Not a confidentiality breach — the connection fails closed (MAC rejection), not open.
It is an availability/liveness and API-contract bug: a caller using the documented Ok(false) retry semantics will always tear down its own connection the moment the socket is briefly not ready.
Current exposure:try_write_frame has no in-repo callers, so no shipping binary hits this today. It is a latent public API, and the contract as documented invites exactly the misuse that triggers the bug.
The nonce desync is precisely the class of failure the state split was meant to make impossible — but it can't fix an API whose control flow consumes the nonce before the I/O outcome is known:
Retain pending ciphertext + write offset in NoiseTcpWriteHalf, so a WouldBlock/partial write preserves the already-sealed bytes and completes them on the next call. This keeps the non-blocking API and makes Ok(false) honest.
Remove the non-blocking write API (or make it always-encrypt-at-write), eliminating the impossible contract. Simplest if no caller needs it.
At minimum, document the hazard and change the return type so Ok(false) cannot be mistaken for "nothing happened".
NoiseTcpWriteHalf::try_write_frameencrypts the frame (advancing the Noise nonce) before it attempts the socket write.If the write returns
WouldBlock, the method returnsOk(false)and discards both the ciphertext and the already-consumed nonce.The next write — whether a retry of the same frame or a new one — is sealed under a later nonce while the peer's decryptor is still expecting the discarded one. The peer's next decryption fails its MAC check and the connection is permanently desynchronized.
Partial writes have a related problem: a ciphertext prefix can reach the socket while the remainder is discarded.
Location
sv2-apps/stratum-apps/src/network_helpers/noise_stream.rs
Lines 212 to 221 in 3281c04
Why this is a bug
ChaCha20-Poly1305 is deterministic given
(key, nonce, plaintext), and the Noise transport nonce is a strictly monotonic counter.Once the frame is encrypted, it is bound to a specific nonce.
The
Ok(false)contract ("not ready, retry later") is therefore unfulfillable: there is no way to retry that frame under the same nonce. The retry seals a different nonce, the peer's decryptor is out of step, authentication fails, and the connection dies (or, with a partial write, the peer receives a truncated ciphertext it can never reconcile).Contrast the read half — it is safe by construction:
sv2-apps/stratum-apps/src/network_helpers/noise_stream.rs
Lines 290 to 304 in 3281c04
try_read_framereturnsOk(None)onWouldBlockbefore any decryption is attempted. The decrypt nonce is only ever consumed once a complete frame is present.Impact
Ok(false)retry semantics will always tear down its own connection the moment the socket is briefly not ready.try_write_framehas no in-repo callers, so no shipping binary hits this today. It is a latent public API, and the contract as documented invites exactly the misuse that triggers the bug.Notes / context
TransportEncryptState.sv2-apps/stratum-apps/src/network_helpers/noise_stream.rs
Line 162 in 3281c04
Possible fixes
NoiseTcpWriteHalf, so aWouldBlock/partial write preserves the already-sealed bytes and completes them on the next call. This keeps the non-blocking API and makesOk(false)honest.Ok(false)cannot be mistaken for "nothing happened".