shadowsocks/shadowsocks-rust · critical
cipher is None
Error message
cipher is None
What it means
Panic from `self.cipher.as_mut().expect("cipher is None")` in `poll_read_decrypted` of the AEAD-2022 stream (stream.rs). At this point the code decrypts a buffered packet in place; the `cipher` Option must hold the decryption context installed during the handshake. `None` means the stream was driven into the decryption step without ever having its cipher initialized (or after it was consumed/moved), violating the connection state machine.
Source
Thrown at crates/shadowsocks/src/relay/tcprelay/stream.rs:118
let key = unsafe { &*(key.as_ref() as *const _) };
ready!(self.poll_read_iv(cx, context, stream, key))?;
self.buffer.clear();
self.buffer.truncate(0);
self.state = DecryptReadState::Read;
self.has_handshaked = true;
}
DecryptReadState::Read => {
let before_n = buf.filled().len();
ready!(Pin::new(stream).poll_read(cx, buf))?;
let after_n = buf.filled().len();
if before_n == after_n {
return Ok(()).into();
}
let m = &mut buf.filled_mut()[before_n..];
let cipher = self.cipher.as_mut().expect("cipher is None");
if !cipher.decrypt_packet(m) {
return Err(ProtocolError::DecryptError).into();
}
return Ok(()).into();
}
}
}
}
fn poll_read_iv<S>(
&mut self,
cx: &mut task::Context<'_>,
context: &Context,
stream: &mut S,
key: &[u8],
) -> Poll<ProtocolResult<()>>
whereView on GitHub (pinned to 8eb0f0a65b)
Solutions
- Treat EOF (`Ok(None)`) and any ProtocolError as terminal: stop polling and create a fresh stream per connection.
- Ensure stream initialization (deriving the session subkey from the request salt and installing the cipher) completed before reading.
- Never reuse or share the stream object across connections or tasks.
- Swap the expect for `ok_or_else` returning a ProtocolError to convert misuse into a recoverable error.
Example fix
// before
let cipher = self.cipher.as_mut().expect("cipher is None");
// after
let Some(cipher) = self.cipher.as_mut() else {
return Err(ProtocolError::DecryptError).into();
}; Defensive patterns
Strategy: type-guard
Validate before calling
if stream.cipher.is_none() { return Err(io::Error::new(io::ErrorKind::InvalidInput, "cipher not initialized")); } Type guard
fn can_decrypt(s: &Aead2022Stream) -> bool { s.cipher.is_some() } Try / catch
loop {
match stream.poll_read(cx, &mut buf) {
Poll::Ready(Ok(None)) | Poll::Ready(Err(_)) => break, // terminal
Poll::Ready(Ok(Some(n))) => forward(&buf[..n]),
Poll::Pending => return Poll::Pending,
}
} Prevention
- Stop polling after EOF or decrypt errors; rebuild the stream.
- Verify initialization (nonce + cipher install) before reading.
- Do not share stream instances across tasks.
When it happens
Trigger: Polling reads on an AEAD-2022 stream whose cipher was never set — handshake init not run (no server nonce/subkey setup), reads attempted after a previous `Ok(None)` EOF, or a double poll after the state machine transitioned and cleared the cipher.
Common situations: Proxy loops continuing to poll after the peer closed (EOF already seen); reusing a connection instance after a decrypt error; building a stream manually and skipping the `set_server_nonce`/cipher installation; upgrading the crate and relying on old init sequencing.
Understand the failure class
Background: "This is a bug, please report it": internal invariant violations, unreachable panics, and SNH errors explained — this error's family across 47 libraries.
Related errors
AI-assisted analysis of shadowsocks/shadowsocks-rust@8eb0f0a65b (2026-09-09).
Data as JSON: /api/errors/5dce60f4dd4301de.
Report an issue: GitHub.