{"record":{"id":"d92e9529f23ed268","repo":"atuinsh/atuin","slug":"frame-exceeds-maximum-length","errorCode":null,"errorMessage":"frame exceeds maximum length","messagePattern":"frame exceeds maximum length","errorType":"exception","errorClass":"std::io::Error","httpStatus":null,"severity":"error","filePath":"crates/atuin-pty-proxy/src/protocol.rs","lineNumber":152,"sourceCode":"/// Read one frame. Returns `Ok(None)` on a clean EOF at a frame boundary.\n///\n/// Unknown frame types are returned as-is: the transport layer does not\n/// decide policy (the server closes on unknown client frames; clients skip\n/// unknown server frames for forward compatibility).\n///\n/// # Errors\n///\n/// Fails on EOF mid-frame, on a length above [`MAX_FRAME_LEN`], or on any\n/// underlying read error.\npub fn read_frame(reader: &mut impl Read) -> io::Result<Option<(u8, Vec<u8>)>> {\n    let mut header = [0u8; 5];\n    if !read_exact_or_eof(reader, &mut header)? {\n        return Ok(None);\n    }\n    let frame_type = header[0];\n    let len = u32::from_be_bytes([header[1], header[2], header[3], header[4]]) as usize;\n    if len > MAX_FRAME_LEN {\n        return Err(io::Error::new(\n            io::ErrorKind::InvalidData,\n            \"frame exceeds maximum length\",\n        ));\n    }\n    let mut payload = vec![0u8; len];\n    reader.read_exact(&mut payload)?;\n    Ok(Some((frame_type, payload)))\n}\n\n/// Fill `buf` completely. Returns `Ok(false)` on EOF before the first byte,\n/// `Ok(true)` when full; EOF partway through is an [`io::ErrorKind::UnexpectedEof`].\nfn read_exact_or_eof(reader: &mut impl Read, buf: &mut [u8]) -> io::Result<bool> {\n    let mut filled = 0;\n    while filled < buf.len() {\n        match reader.read(&mut buf[filled..]) {\n            Ok(0) => {\n                if filled == 0 {\n                    return Ok(false);","sourceCodeStart":134,"sourceCodeEnd":170,"githubUrl":"https://github.com/atuinsh/atuin/blob/202f6ad98ee0da165c35cdb2afbc5b13d6ab81a1/crates/atuin-pty-proxy/src/protocol.rs#L134-L170","documentation":"Returned by read_frame in the pty-proxy protocol crate. Every frame starts with a 5-byte header: one frame-type byte plus a u32 big-endian payload length. If that length exceeds MAX_FRAME_LEN (1 MiB, protocol.rs:70), read_frame returns io::ErrorKind::InvalidData with this message rather than allocating a hostile or corrupt buffer. It almost always means the byte stream is not actually frame-encoded data, or the two endpoints disagree on the protocol.","triggerScenarios":"Calling read_frame on a socket/stream carrying something other than pty-proxy frames (raw TLS, HTTP, shell output); a desync where a previous frame was misparsed so the reader is now interpreting payload bytes as a header; a version mismatch where one side emits a different header layout; a malicious peer writing a crafted length prefix.","commonSituations":"Connecting the proxy client to the wrong port or a TLS-enabled endpoint; a legacy peer (the post-accept greeting/classify_greeting distinguishes V2 from legacy) that never speaks frames; upgrading one side of the proxy pair and not the other; feeding a captured/truncated pcap replay into the parser.","solutions":["Verify both endpoints run the same atuin/pty-proxy version and speak the V2 frame protocol","Confirm the reader is attached to the correct endpoint and that the peer sends the magic greeting before frames","Treat the connection as poisoned: close it and reconnect rather than trying to resync mid-stream","If writing a sender, ensure payloads are chunked under 1 MiB (encode_frame already asserts this on the write side)"],"exampleFix":"// before\nlet (frame_type, payload) = read_frame(&mut stream)?.expect(\"frame\");\n\n// after\nmatch read_frame(&mut stream)? {\n    Some((frame_type, payload)) => { /* handle */ }\n    None => { /* clean EOF at frame boundary */ }\n}\n// and on write: assert payload.len() <= MAX_FRAME_LEN before encode_frame","handlingStrategy":"try-catch","validationCode":"// Before reading frames, confirm the peer speaks V2 by sniffing the magic greeting\nlet mut first = [0u8; 4];\nlet n = read_with_timeout(&mut stream, &mut first)?;\nif classify_greeting(&first[..n]) == Greeting::Legacy {\n    // do not feed this stream to read_frame\n}","typeGuard":null,"tryCatchPattern":"match read_frame(&mut stream) {\n    Ok(Some(frame)) => { /* handle */ }\n    Ok(None) => { /* clean close */ }\n    Err(e) if e.kind() == std::io::ErrorKind::InvalidData => {\n        // length prefix > 1 MiB: stream is desynced or wrong protocol.\n        // Do NOT continue reading; close and reconnect.\n    }\n    Err(e) => return Err(e.into()),\n}","preventionTips":["Pin both ends of the proxy to the same Atuin version","Always write frames via encode_frame so headers and lengths stay consistent","Chunk large payloads under MAX_FRAME_LEN (1 MiB) before encoding","Validate the peer's greeting before entering the frame loop"],"tags":["protocol","framing","io","rust","atuin","pty"],"backgroundTag":"message-too-large","analyzedSha":"202f6ad98ee0da165c35cdb2afbc5b13d6ab81a1","analyzedAt":"2026-08-16T19:30:24.731Z","schemaVersion":2},"datasetVersion":"2026-08-16T23:17:17.608Z"}