Medium severity5.3NVD Advisory· Published Apr 22, 2026· Updated Apr 24, 2026
CVE-2026-34062
CVE-2026-34062
Description
nimiq-libp2p is a Nimiq network implementation based on libp2p. Prior to version 1.3.0, MessageCodec::read_request and read_response call read_to_end() on inbound substreams, so a remote peer can send only a partial frame and keep the substream open. because Behaviour::new also sets with_max_concurrent_streams(1000), the node exposes a much larger stalled-slot budget than the library default. The patch for this vulnerability is formally released as part of v1.3.0. No known workarounds are available.
Affected products
1Patches
1c021a5337b80Fix request/response codec reading entire stream before size validation
1 file changed · +150 −52
network-libp2p/src/dispatch/codecs/mod.rs+150 −52 modified@@ -15,15 +15,42 @@ use nimiq_network_interface::network; /// Size of a u64 #[allow(unused_qualifications)] // Remove with a MSVR >= 1.80 const U64_LENGTH: usize = mem::size_of::<u64>(); -const MAX_REQUEST_SIZE: u64 = network::MIN_SUPPORTED_REQ_SIZE as u64 + U64_LENGTH as u64; -const MAX_RESPONSE_SIZE: u64 = network::MIN_SUPPORTED_RESP_SIZE as u64 + U64_LENGTH as u64; +const MAX_REQUEST_SIZE: u64 = network::MIN_SUPPORTED_REQ_SIZE as u64; +const MAX_RESPONSE_SIZE: u64 = network::MIN_SUPPORTED_RESP_SIZE as u64; #[derive(Default, Debug, Clone)] pub struct MessageCodec; pub type IncomingRequest = Vec<u8>; pub type OutgoingResponse = Vec<u8>; +async fn read_message<T>(io: &mut T, max_size: u64) -> io::Result<Option<Vec<u8>>> +where + T: AsyncRead + Unpin + Send, +{ + let mut len_bytes = [0u8; U64_LENGTH]; + match io.read_exact(&mut len_bytes).await { + Ok(()) => {} + Err(e) if e.kind() == io::ErrorKind::UnexpectedEof => return Ok(None), + Err(e) => return Err(e), + } + + let len = u64::from_be_bytes(len_bytes); + if len > max_size { + return Err(io::Error::new( + io::ErrorKind::InvalidData, + format!("Received data size ({len} bytes) exceeds maximum ({max_size} bytes)"), + )); + } + + let mut payload = vec![0u8; len as usize]; + match io.read_exact(&mut payload).await { + Ok(()) => Ok(Some(payload)), + Err(e) if e.kind() == io::ErrorKind::UnexpectedEof => Ok(None), + Err(e) => Err(e), + } +} + #[async_trait::async_trait] impl request_response::Codec for MessageCodec { type Protocol = StreamProtocol; @@ -34,31 +61,7 @@ impl request_response::Codec for MessageCodec { where T: AsyncRead + Unpin + Send, { - let mut vec = Vec::new(); - io.take(MAX_REQUEST_SIZE).read_to_end(&mut vec).await?; - if vec.len() < U64_LENGTH { - return Ok(None); - } - let mut len_bytes = [0u8; U64_LENGTH]; - len_bytes.copy_from_slice(&vec[..U64_LENGTH]); - let len = u64::from_be_bytes(len_bytes) as usize; - - if len as u64 > MAX_REQUEST_SIZE { - return Err(io::Error::new( - io::ErrorKind::InvalidData, - format!( - "Received data size ({len} bytes) exceeds maximum ({MAX_REQUEST_SIZE} bytes)" - ), - )); - } - - if vec.len() - U64_LENGTH >= len { - // Skip the length header we already read - vec.drain(..U64_LENGTH); - Ok(Some(vec)) - } else { - Ok(None) - } + read_message(io, MAX_REQUEST_SIZE).await } async fn read_response<T>( @@ -69,31 +72,7 @@ impl request_response::Codec for MessageCodec { where T: AsyncRead + Unpin + Send, { - let mut vec = Vec::new(); - io.take(MAX_RESPONSE_SIZE).read_to_end(&mut vec).await?; - if vec.len() < U64_LENGTH { - return Ok(None); - } - let mut len_bytes = [0u8; U64_LENGTH]; - len_bytes.copy_from_slice(&vec[..U64_LENGTH]); - let len = u64::from_be_bytes(len_bytes) as usize; - - if len as u64 > MAX_RESPONSE_SIZE { - return Err(io::Error::new( - io::ErrorKind::InvalidData, - format!( - "Received data size ({len} bytes) exceeds maximum ({MAX_RESPONSE_SIZE} bytes)" - ), - )); - } - - if vec.len() - U64_LENGTH >= len { - // Skip the length header we already read - vec.drain(..U64_LENGTH); - Ok(Some(vec)) - } else { - Ok(None) - } + read_message(io, MAX_RESPONSE_SIZE).await } async fn write_request<T>( @@ -106,6 +85,15 @@ impl request_response::Codec for MessageCodec { T: AsyncWrite + Send + Unpin, { let src = req.expect("No data to write"); + if src.len() as u64 > MAX_REQUEST_SIZE { + return Err(io::Error::new( + io::ErrorKind::InvalidData, + format!( + "Request data size ({} bytes) exceeds maximum ({MAX_REQUEST_SIZE} bytes)", + src.len() + ), + )); + } io.write_all(&(src.len() as u64).to_be_bytes()).await?; io.write_all(&src).await?; Ok(()) @@ -121,8 +109,118 @@ impl request_response::Codec for MessageCodec { T: AsyncWrite + Unpin + Send, { let src = res.expect("No data to write"); + if src.len() as u64 > MAX_RESPONSE_SIZE { + return Err(io::Error::new( + io::ErrorKind::InvalidData, + format!( + "Response data size ({} bytes) exceeds maximum ({MAX_RESPONSE_SIZE} bytes)", + src.len() + ), + )); + } io.write_all(&(src.len() as u64).to_be_bytes()).await?; io.write_all(&src).await?; Ok(()) } } + +#[cfg(test)] +mod tests { + use std::{ + io, + pin::Pin, + task::{Context, Poll}, + time::Duration, + }; + + use futures::{io::Cursor, AsyncRead}; + use libp2p::{request_response::Codec as _, StreamProtocol}; + use nimiq_test_log::test; + use nimiq_time::timeout; + + use super::MessageCodec; + + struct NoEofReader { + data: Vec<u8>, + pos: usize, + } + + impl NoEofReader { + fn new(data: Vec<u8>) -> Self { + Self { data, pos: 0 } + } + } + + impl AsyncRead for NoEofReader { + fn poll_read( + mut self: Pin<&mut Self>, + _cx: &mut Context<'_>, + buf: &mut [u8], + ) -> Poll<io::Result<usize>> { + if self.pos < self.data.len() { + let n = (self.data.len() - self.pos).min(buf.len()); + buf[..n].copy_from_slice(&self.data[self.pos..self.pos + n]); + self.pos += n; + return Poll::Ready(Ok(n)); + } + + Poll::Pending + } + } + + #[test(tokio::test)] + async fn read_request_completes_without_waiting_for_eof() { + let protocol = StreamProtocol::new("/nimiq/reqres/0.0.1"); + let payload = b"nimiq".to_vec(); + let mut bytes = (payload.len() as u64).to_be_bytes().to_vec(); + bytes.extend_from_slice(&payload); + + let mut reader = NoEofReader::new(bytes); + let mut codec = MessageCodec; + let result = timeout( + Duration::from_millis(100), + codec.read_request(&protocol, &mut reader), + ) + .await + .expect("read_request should not wait for EOF") + .expect("read_request should succeed"); + + assert_eq!(result, Some(payload)); + } + + #[test(tokio::test)] + async fn read_response_completes_without_waiting_for_eof() { + let protocol = StreamProtocol::new("/nimiq/reqres/0.0.1"); + let payload = b"albatross".to_vec(); + let mut bytes = (payload.len() as u64).to_be_bytes().to_vec(); + bytes.extend_from_slice(&payload); + + let mut reader = NoEofReader::new(bytes); + let mut codec = MessageCodec; + let result = timeout( + Duration::from_millis(100), + codec.read_response(&protocol, &mut reader), + ) + .await + .expect("read_response should not wait for EOF") + .expect("read_response should succeed"); + + assert_eq!(result, Some(payload)); + } + + #[test(tokio::test)] + async fn read_request_returns_none_for_truncated_message_with_eof() { + let protocol = StreamProtocol::new("/nimiq/reqres/0.0.1"); + let mut bytes = (6_u64).to_be_bytes().to_vec(); + bytes.extend_from_slice(b"nim"); + + let mut reader = Cursor::new(bytes); + let mut codec = MessageCodec; + let result = codec + .read_request(&protocol, &mut reader) + .await + .expect("read_request should not error on truncated EOF"); + + assert_eq!(result, None); + } +}
Vulnerability mechanics
Generated by null/stub on May 9, 2026. Inputs: CWE entries + fix-commit diffs from this CVE's patches. Citations validated against bundle.
References
3News mentions
0No linked articles in our index yet.