{"record":{"id":"91c2d57c0839d701","repo":"shadowsocks/shadowsocks-rust","slug":"cipher-is-none-91c2d5","errorCode":null,"errorMessage":"cipher is None","messagePattern":"cipher is None","errorType":"panic","errorClass":null,"httpStatus":null,"severity":"critical","filePath":"crates/shadowsocks/src/relay/tcprelay/aead_2022.rs","lineNumber":425,"sourceCode":"\n        self.salt = Some(Bytes::copy_from_slice(salt));\n\n        self.cipher = Some(cipher);\n        Ok(Some(data_length as usize)).into()\n    }\n\n    fn poll_read_length<S>(&mut self, cx: &mut task::Context<'_>, stream: &mut S) -> Poll<io::Result<Option<usize>>>\n    where\n        S: AsyncRead + Unpin + ?Sized,\n    {\n        let length_len = 2 + self.method.tag_len();\n\n        let n = ready!(self.poll_read_exact(cx, stream, length_len))?;\n        if n == 0 {\n            return Ok(None).into();\n        }\n\n        let cipher = self.cipher.as_mut().expect(\"cipher is None\");\n\n        let m = &mut self.buffer[..length_len];\n        let length = Self::decrypt_length(cipher, m)?;\n\n        Ok(Some(length)).into()\n    }\n\n    fn poll_read_data<S>(&mut self, cx: &mut task::Context<'_>, stream: &mut S, size: usize) -> Poll<ProtocolResult<()>>\n    where\n        S: AsyncRead + Unpin + ?Sized,\n    {\n        let data_len = size + self.method.tag_len();\n\n        let n = ready!(self.poll_read_exact(cx, stream, data_len))?;\n        if n == 0 {\n            return Err(io::Error::from(ErrorKind::UnexpectedEof).into()).into();\n        }\n","sourceCodeStart":407,"sourceCodeEnd":443,"githubUrl":"https://github.com/shadowsocks/shadowsocks-rust/blob/8eb0f0a65b1d976ab6bed5787327ef86529b0435/crates/shadowsocks/src/relay/tcprelay/aead_2022.rs#L407-L443","documentation":"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.","triggerScenarios":"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.","commonSituations":"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.","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."],"exampleFix":"// before\nlet cipher = self.cipher.as_mut().expect(\"cipher is None\");\n// after\nlet cipher = self.cipher.as_mut().ok_or_else(|| {\n    io::Error::new(io::ErrorKind::InvalidData, \"cipher not initialized (handshake incomplete)\")\n})?;","handlingStrategy":"type-guard","validationCode":"// before driving reads\nif !connection.is_handshake_complete() {\n    return Err(io::Error::new(io::ErrorKind::InvalidInput, \"AEAD-2022 handshake not complete\"));\n}","typeGuard":"fn cipher_ready(conn: &StreamContext) -> bool { conn.cipher.is_some() }","tryCatchPattern":"// poll loop\nmatch conn.poll_read(cx, &mut buf) {\n    Poll::Ready(Ok(Some(_))) => { /* forward data */ }\n    Poll::Ready(Ok(None)) => return, // EOF: stop polling, never resume\n    Poll::Ready(Err(e)) => { teardown(e); return; }\n    Poll::Pending => {}\n}","preventionTips":["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."],"tags":["rust","panic","aead-2022","state-machine","shadowsocks"],"backgroundTag":"internal-invariant-violation","analyzedSha":"8eb0f0a65b1d976ab6bed5787327ef86529b0435","analyzedAt":"2026-09-09T12:20:43.168Z","contentChangedAt":"2026-09-09T12:20:43.168Z","schemaVersion":2},"datasetVersion":"2026-09-14T11:17:12.474Z"}