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

  1. Ensure the salt/header phase completes and initializes the cipher before any poll_read on the data stream
  2. Replace expect with an error: return ProtocolError::Initialize/InvalidState instead of panicking
  3. 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

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.