{"record":{"id":"636129d75d528dcf","repo":"shadowsocks/shadowsocks-rust","slug":"cipher-is-none","errorCode":null,"errorMessage":"cipher is None","messagePattern":"cipher is None","errorType":"panic","errorClass":null,"httpStatus":null,"severity":"error","filePath":"crates/shadowsocks/src/relay/tcprelay/aead.rs","lineNumber":226,"sourceCode":"        let cipher = Cipher::new(self.method, key, salt);\n\n        self.cipher = Some(cipher);\n\n        Ok(()).into()\n    }\n\n    fn poll_read_length<S>(&mut self, cx: &mut task::Context<'_>, stream: &mut S) -> Poll<ProtocolResult<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>(\n        &mut self,\n        cx: &mut task::Context<'_>,\n        context: &Context,\n        stream: &mut S,\n        size: usize,\n    ) -> Poll<ProtocolResult<()>>\n    where\n        S: AsyncRead + Unpin + ?Sized,\n    {\n        let data_len = size + self.method.tag_len();","sourceCodeStart":208,"sourceCodeEnd":244,"githubUrl":"https://github.com/shadowsocks/shadowsocks-rust/blob/8eb0f0a65b1d976ab6bed5787327ef86529b0435/crates/shadowsocks/src/relay/tcprelay/aead.rs#L208-L244","documentation":"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.","triggerScenarios":"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.","commonSituations":"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.","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"],"exampleFix":"// before\nlet cipher = self.cipher.as_mut().expect(\"cipher is None\");\n// after\nlet cipher = self.cipher.as_mut().ok_or_else(|| ProtocolError::InvalidState)?;","handlingStrategy":"type-guard","validationCode":"// check reader state before polling\nif reader.cipher_is_none() { return Err(ProtocolError::InvalidState.into()); }","typeGuard":"fn cipher_ready(r: &DecryptedReader) -> bool { r.has_cipher() }","tryCatchPattern":"match reader.poll_read(cx, stream, buf) {\n    Poll::Ready(Err(e)) if e.kind() == ErrorKind::Other => /* re-init stream */,\n    other => other,\n}","preventionTips":["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"],"tags":["rust","aead","panic","state-machine","tcp"],"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"}