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<()>>
    where

View on GitHub (pinned to 8eb0f0a65b)

Solutions

  1. Treat EOF (`Ok(None)`) and any ProtocolError as terminal: stop polling and create a fresh stream per connection.
  2. Ensure stream initialization (deriving the session subkey from the request salt and installing the cipher) completed before reading.
  3. Never reuse or share the stream object across connections or tasks.
  4. 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

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.