{"record":{"id":"521e67b35c7e9bc6","repo":"rustfs/rustfs","slug":"unexpected-eof-while-reading-dare-ciphertext","errorCode":null,"errorMessage":"unexpected EOF while reading DARE ciphertext","messagePattern":"unexpected EOF while reading DARE ciphertext","errorType":"exception","errorClass":"io::Error","httpStatus":null,"severity":"error","filePath":"crates/rio-v2/src/encrypt_reader.rs","lineNumber":526,"sourceCode":"                        \"non-final DARE package must carry a full 64KiB payload: cipher={}, payload_len={}, sequence_number={}, header={:02x?}\",\n                        header[1], payload_len, *this.sequence_number, header\n                    ),\n                )));\n            }\n            if this.ciphertext_buf.len() < package_len {\n                this.ciphertext_buf.resize(package_len, 0);\n            }\n            *this.ciphertext_len = package_len;\n            *this.ciphertext_read = 0;\n\n            while *this.ciphertext_read < *this.ciphertext_len {\n                let mut read_buf = ReadBuf::new(&mut this.ciphertext_buf[*this.ciphertext_read..*this.ciphertext_len]);\n                match this.inner.as_mut().poll_read(cx, &mut read_buf) {\n                    Poll::Pending => return Poll::Pending,\n                    Poll::Ready(Ok(())) => {\n                        let n = read_buf.filled().len();\n                        if n == 0 {\n                            return Poll::Ready(Err(io::Error::new(\n                                io::ErrorKind::UnexpectedEof,\n                                \"unexpected EOF while reading DARE ciphertext\",\n                            )));\n                        }\n                        *this.ciphertext_read += n;\n                    }\n                    Poll::Ready(Err(err)) => return Poll::Ready(Err(err)),\n                }\n            }\n\n            match open_dare_package(\n                *this.key,\n                this.cipher,\n                *this.sequence_number,\n                *this.expected_base_nonce,\n                *header,\n                &this.ciphertext_buf[..*this.ciphertext_len],\n                this.ref_nonce,","sourceCodeStart":508,"sourceCodeEnd":544,"githubUrl":"https://github.com/rustfs/rustfs/blob/9e6e02ea09c86bedf44c7bd64a74ea02a0cff1de/crates/rio-v2/src/encrypt_reader.rs#L508-L544","documentation":"While DecryptReader fills ciphertext_buf for the current package, the underlying AsyncRead returned Ok with 0 filled bytes (a clean EOF) before the full package body (payload + 16-byte tag) arrived. The stream ends in the middle of a package whose header promised more bytes. It differs from 'DARE stream truncated before a finalized package', which fires at a package boundary when no final-flagged package was ever seen.","triggerScenarios":"Object content shorter than its recorded size (truncated upload, incomplete multipart complete); a network layer cutting the body early while the reader still expects payload + tag bytes; an inner range reader whose byte range ends before the current package completes; a writer that never flushed the trailing package.","commonSituations":"Proxy, CDN, or load balancer truncating GET responses; partial writes after disk-full; missing erasure-coded shards yielding short reads; metadata size disagreeing with actual object length after a failed overwrite.","solutions":["Compare the bytes the inner reader actually delivered against the object size in metadata; a mismatch means the backend view is stale or truncated — re-resolve the object and open a fresh stream.","If the cut is transient (connection drop), resume: re-open the reader from the last full package boundary (sequence_number = ciphertext_offset / 65552); decrypted bytes before the cut remain valid.","For uploads you control, ensure the encrypting writer flushes the final package before the object is completed.","If truncation persists across re-reads, investigate backend/disk health rather than retrying."],"exampleFix":"// before: a cut connection kills the whole read\nlet mut out = Vec::new();\nreader.read_to_end(&mut out).await?;\n\n// after: on UnexpectedEof, reopen and resume from the last full package boundary\nmatch reader.read_to_end(&mut out).await {\n    Err(e) if e.kind() == io::ErrorKind::UnexpectedEof => {\n        let seq = (ciphertext_pos / DARE_PACKAGE_SIZE) as u32;\n        reader = DecryptReader::new_with_sequence(reopen_from(seq), key, nonce, seq);\n        // continue reading; the decrypted prefix stays valid\n    }\n    result => result?,\n}","handlingStrategy":"retry","validationCode":"// Before decrypting, confirm the stream can plausibly hold the framed packages\nlet min_len = full_packages * 65_552 + if has_final { 16 + last_payload + 16 } else { 0 };\nif actual_object_len < min_len {\n    return Err(truncated_metadata(actual_object_len, min_len));\n}","typeGuard":null,"tryCatchPattern":"match reader.read(&mut buf).await {\n    Err(e) if e.kind() == io::ErrorKind::UnexpectedEof => {\n        // one bounded retry from the last full package boundary, then fail\n        let seq = (ciphertext_pos / DARE_PACKAGE_SIZE) as u32;\n        retry_once_with(DecryptReader::new_with_sequence(reopen(), key, nonce, seq)).await\n    }\n    other => other,\n}","preventionTips":["Validate object size against metadata before opening the decrypt reader.","Ensure uploads complete the final DARE package (EncryptReader finalizes on inner EOF) before the object is committed.","For network bodies, check Content-Length against bytes actually received before handing the stream to DecryptReader."],"tags":["dare","encryption","eof","truncated-stream","rust"],"backgroundTag":"truncated-stream-read","analyzedSha":"9e6e02ea09c86bedf44c7bd64a74ea02a0cff1de","analyzedAt":"2026-08-16T20:34:17.560Z","schemaVersion":2},"datasetVersion":"2026-08-16T23:17:17.608Z"}