shadowsocks/shadowsocks-rust · error
cipher is None
Error message
cipher is None
What it means
In the AEAD TCP relay, poll_read_length unwraps self.cipher with expect("cipher is None"). The cipher field is set once the stream is initialized (after the salt is read and the key derived) and taken/None'd during shutdown or before initialization. Reading a length before the cipher exists means the reader is used in a state it should never reach — reading the encrypted length chunk requires an initialized AEAD context.
Source
Thrown at crates/shadowsocks/src/relay/tcprelay/aead.rs:226
let cipher = Cipher::new(self.method, key, salt);
self.cipher = Some(cipher);
Ok(()).into()
}
fn poll_read_length<S>(&mut self, cx: &mut task::Context<'_>, stream: &mut S) -> Poll<ProtocolResult<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<'_>,
context: &Context,
stream: &mut S,
size: usize,
) -> Poll<ProtocolResult<()>>
where
S: AsyncRead + Unpin + ?Sized,
{
let data_len = size + self.method.tag_len();View on GitHub (pinned to 8eb0f0a65b)
Solutions
- Ensure the salt/header phase completes and initializes the cipher before any poll_read on the data stream
- Replace expect with an error: return ProtocolError::Initialize/InvalidState instead of panicking
- Check that the reader is not polled after takeover/shutdown (cipher taken) — re-create the stream instead
Example fix
// before
let cipher = self.cipher.as_mut().expect("cipher is None");
// after
let cipher = self.cipher.as_mut().ok_or_else(|| ProtocolError::InvalidState)?; Defensive patterns
Strategy: type-guard
Validate before calling
// check reader state before polling
if reader.cipher_is_none() { return Err(ProtocolError::InvalidState.into()); } Type guard
fn cipher_ready(r: &DecryptedReader) -> bool { r.has_cipher() } Try / catch
match reader.poll_read(cx, stream, buf) {
Poll::Ready(Err(e)) if e.kind() == ErrorKind::Other => /* re-init stream */,
other => other,
} Prevention
- Never poll the decrypted stream before the salt/handshake phase completes
- Do not share one DecryptedReader across tasks
- Re-create the reader after any takeover/reset instead of reusing it
When it happens
Trigger: Reading from the decrypted stream before the AEAD handshake populated `cipher` (e.g. salt not yet consumed), or after the cipher was taken out (replace/finish/shutdown path), typically from calling poll_read on the stream in the wrong poll order.
Common situations: Bugs in custom streams/futures that poll the AEAS-decrypted reader concurrently or before initialization; reusing a DecryptedReader after handshake completion code moved the cipher out; transport reset/reconnect logic not re-initializing the reader.
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/636129d75d528dcf.
Report an issue: GitHub.