shadowsocks/shadowsocks-rust · critical
cipher is None
Error message
cipher is None
What it means
This panic comes from `self.cipher.as_mut().expect("cipher is None")` in `poll_read_length` of the AEAD-2022 TCP relay. The `cipher` field is an `Option` that is only populated after the server's fixed-length/variable-length header handshake completes; unwrapping it asserts the state machine invariant that decryption state exists whenever a length chunk is being read. The panic means the read half was polled (or continued to be polled) while the cipher was never initialized — typically after `Ok(None)` was returned or before handshake setup, or a resumed poll after the state was consumed.
Source
Thrown at crates/shadowsocks/src/relay/tcprelay/aead_2022.rs:425
self.salt = Some(Bytes::copy_from_slice(salt));
self.cipher = Some(cipher);
Ok(Some(data_length as usize)).into()
}
fn poll_read_length<S>(&mut self, cx: &mut task::Context<'_>, stream: &mut S) -> Poll<io::Result<Option<usize>>>
where
S: AsyncRead + Unpin + ?Sized,
{
let length_len = 2 + self.method.tag_len();
let n = ready!(self.poll_read_exact(cx, stream, length_len))?;
if n == 0 {
return Ok(None).into();
}
let cipher = self.cipher.as_mut().expect("cipher is None");
let m = &mut self.buffer[..length_len];
let length = Self::decrypt_length(cipher, m)?;
Ok(Some(length)).into()
}
fn poll_read_data<S>(&mut self, cx: &mut task::Context<'_>, stream: &mut S, size: usize) -> Poll<ProtocolResult<()>>
where
S: AsyncRead + Unpin + ?Sized,
{
let data_len = size + self.method.tag_len();
let n = ready!(self.poll_read_exact(cx, stream, data_len))?;
if n == 0 {
return Err(io::Error::from(ErrorKind::UnexpectedEof).into()).into();
}
View on GitHub (pinned to 8eb0f0a65b)
Solutions
- Do not poll the connection for reads after it returned `Ok(None)`/a protocol error — treat the stream as finished and drop it.
- Ensure the server handshake (fixed-length header with derived subkey) fully completed and set `self.cipher` before the variable-length read loop runs; check that the session subkey derivation was not skipped.
- Verify you are not resuming the same connection instance across reconnects; construct a fresh connection per TCP stream.
- If integrating manually, only call `poll_read` via the provided public API after the state machine reports ready, never on a partially initialized connection.
Example fix
// before
let cipher = self.cipher.as_mut().expect("cipher is None");
// after
let cipher = self.cipher.as_mut().ok_or_else(|| {
io::Error::new(io::ErrorKind::InvalidData, "cipher not initialized (handshake incomplete)")
})?; Defensive patterns
Strategy: type-guard
Validate before calling
// before driving reads
if !connection.is_handshake_complete() {
return Err(io::Error::new(io::ErrorKind::InvalidInput, "AEAD-2022 handshake not complete"));
} Type guard
fn cipher_ready(conn: &StreamContext) -> bool { conn.cipher.is_some() } Try / catch
// poll loop
match conn.poll_read(cx, &mut buf) {
Poll::Ready(Ok(Some(_))) => { /* forward data */ }
Poll::Ready(Ok(None)) => return, // EOF: stop polling, never resume
Poll::Ready(Err(e)) => { teardown(e); return; }
Poll::Pending => {}
} Prevention
- Never poll a connection after it returned EOF or a protocol error.
- Create one connection object per TCP stream; never reuse across reconnects.
- Track handshake completion state before enabling the read path.
- Keep shadowsocks-rust versions aligned between integration points.
When it happens
Trigger: Calling `poll_read` on the server-side `ServerConnection`/stream before `cipher` is set (handshake not completed), or polling the stream again after the handshake transition already took/consumed the cipher (double-poll of poll_read_length after the state advanced), leaving `self.cipher == None` when the length chunk is decrypted.
Common situations: Reusing a shadowsocks AEAD-2022 TCP stream after it already signaled end-of-stream (`Ok(None)`); wrapping the connection in a reader that polls before sending/deriving the session subkey; hand-rolling a relay loop that drives the Future after a protocol error was already returned; version drift where the handshake path changed and no longer installs the cipher.
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/91c2d57c0839d701.
Report an issue: GitHub.