From 3881278499228265c6254367b992d215d7f497ca Mon Sep 17 00:00:00 2001 From: Justin Kovacich Date: Wed, 22 Apr 2026 15:11:43 -0400 Subject: [PATCH 1/9] Max buffer size set to 1500, avoid heap allocation/vecs, overflow returns an error, added unit tests --- src/client/error.rs | 8 ++-- src/client/mod.rs | 27 +++++++---- src/client/socket_manager.rs | 77 ++++++++++++++++++++++++++---- src/lib.rs | 7 +++ src/server/error.rs | 11 +++++ src/server/event_publisher.rs | 88 +++++++++++++++++++++++++++-------- 6 files changed, 176 insertions(+), 42 deletions(-) diff --git a/src/client/error.rs b/src/client/error.rs index 1e5c304..97ce2f1 100644 --- a/src/client/error.rs +++ b/src/client/error.rs @@ -39,9 +39,11 @@ pub enum Error { /// An E2E protection or checking error occurred. #[error(transparent)] E2e(#[from] crate::e2e::Error), - /// A fixed-capacity internal structure is full. The argument names the - /// structure so bare-metal users can size the corresponding compile-time - /// constant up (e.g. `"unicast_sockets"`). + /// A fixed-capacity internal structure is full. The argument is a + /// lowercase `snake_case` tag naming the resource; grep the crate for + /// the tag to find the compile-time constant that governs it. Current + /// tags: `"unicast_sockets"` (→ `UNICAST_SOCKETS_CAP`), `"udp_buffer"` + /// (→ `crate::UDP_BUFFER_SIZE`). #[error("internal capacity exceeded: {0}")] Capacity(&'static str), } diff --git a/src/client/mod.rs b/src/client/mod.rs index 8cb2cb8..8866e5c 100644 --- a/src/client/mod.rs +++ b/src/client/mod.rs @@ -3,16 +3,23 @@ //! # Memory footprint //! //! The client's internal `Inner` state is allocated inline rather than on -//! the heap. With the default capacity constants used by the client -//! internals — `REQUEST_QUEUE_CAP=32`, `PENDING_RESPONSES_CAP=64`, and -//! `UNICAST_SOCKETS_CAP=8` in `inner.rs`, plus `SESSION_CAP=64` in -//! `session.rs` — `Inner

` occupies on the order of **8–12 KiB**, -//! depending on `sizeof::

()` and `sizeof::>()`. On -//! `std + tokio`, this is allocated on the heap when the run-loop is -//! spawned, so the overhead is invisible to callers. On the bare-metal -//! port (future), whoever drives the future must arrange storage for it -//! (either a `static` or a heap allocator); these capacity constants are -//! the primary knobs for trimming this footprint. +//! the heap. With the default capacity constants declared in `inner.rs` — +//! `REQUEST_QUEUE_CAP=32`, `PENDING_RESPONSES_CAP=64`, `UNICAST_SOCKETS_CAP=8`, +//! and `SESSION_CAP=64` — `Inner

` occupies on the order of **8–12 KiB**, +//! depending on `sizeof::

()` and `sizeof::>()`. +//! +//! In addition, each `SocketManager`'s spawn loop holds a persistent +//! `[u8; UDP_BUFFER_SIZE]` receive/send buffer (1500 bytes) and transiently +//! allocates a second `[u8; UDP_BUFFER_SIZE]` on the stack for E2E-protect +//! output — so an active socket-loop future carries **~3 KiB** of buffer +//! state on top of its control-plane fields. With `UNICAST_SOCKETS_CAP=8` +//! sockets bound, the per-client buffer budget is therefore ~24 KiB. +//! +//! On `std + tokio`, all of this is allocated on the heap when each future +//! is spawned, so the overhead is invisible to callers. On the bare-metal +//! port (future), whoever drives the futures must arrange storage for them +//! (either a `static` or a heap allocator); the capacity constants plus +//! [`crate::UDP_BUFFER_SIZE`] are the knobs for trimming this footprint. mod error; mod inner; mod service_registry; diff --git a/src/client/socket_manager.rs b/src/client/socket_manager.rs index 13a8f6c..0b60a7b 100644 --- a/src/client/socket_manager.rs +++ b/src/client/socket_manager.rs @@ -1,5 +1,6 @@ use crate::{ - e2e::{E2ECheckStatus, E2EKey, E2ERegistry, PROFILE4_HEADER_SIZE}, + UDP_BUFFER_SIZE, + e2e::{E2ECheckStatus, E2EKey, E2ERegistry}, protocol::{Message, MessageView, sd}, traits::{PayloadWireFormat, WireFormat}, }; @@ -9,7 +10,6 @@ use std::{ net::{IpAddr, Ipv4Addr, SocketAddr, SocketAddrV4}, sync::{Arc, Mutex}, task::{Context, Poll}, - vec, }; use tokio::{net::UdpSocket, select, sync::mpsc}; use tracing::{error, info, trace}; @@ -212,7 +212,7 @@ where e2e_registry: Arc>, ) { tokio::spawn(async move { - let mut buf = vec![0; 1400]; + let mut buf = [0u8; UDP_BUFFER_SIZE]; loop { select! { result = socket.recv_from(&mut buf) => { @@ -271,22 +271,35 @@ where } }; - // Apply E2E protect if configured + // Apply E2E protect if configured. `protected` + // is a disjoint stack buffer, so the input can + // be borrowed directly out of `buf[16..]` with + // no intermediate copy. { let key = E2EKey::from_message_id(send_message.message.header().message_id()); let mut registry = e2e_registry.lock().expect("e2e registry lock poisoned"); if registry.contains_key(&key) { - let original_payload = buf[16..message_length].to_vec(); let upper_header: [u8; 8] = buf[8..16].try_into().expect("upper header slice"); - let mut protected = vec![0u8; original_payload.len() + PROFILE4_HEADER_SIZE]; - match registry.protect(key, &original_payload, upper_header, &mut protected) { + let mut protected = [0u8; UDP_BUFFER_SIZE]; + let result = registry.protect( + key, + &buf[16..message_length], + upper_header, + &mut protected, + ); + match result { Some(Ok(protected_len)) => { + if 16 + protected_len > UDP_BUFFER_SIZE { + error!( + "E2E-protected payload ({} bytes) exceeds UDP_BUFFER_SIZE ({}); dropping send", + 16 + protected_len, UDP_BUFFER_SIZE + ); + let _ = send_message.response.send(Err(Error::Capacity("udp_buffer"))); + continue; + } #[allow(clippy::cast_possible_truncation)] let new_length: u32 = 8 + protected_len as u32; buf[4..8].copy_from_slice(&new_length.to_be_bytes()); - if 16 + protected_len > buf.len() { - buf.resize(16 + protected_len, 0); - } buf[16..16 + protected_len].copy_from_slice(&protected[..protected_len]); message_length = 16 + protected_len; } @@ -332,6 +345,7 @@ mod tests { use super::*; use crate::protocol::sd::test_support::{TestPayload, empty_sd_header}; use std::format; + use std::vec; type TestSocketManager = SocketManager; @@ -562,4 +576,47 @@ mod tests { "reboot flag stays Continuous after wrap" ); } + + #[tokio::test] + async fn send_e2e_protected_payload_exceeding_udp_buffer_returns_capacity_error() { + use crate::RawPayload; + use crate::e2e::{E2EProfile, Profile4Config}; + use crate::protocol::{Header, MessageId, MessageType, MessageTypeField, ReturnCode}; + + // Register an E2E profile so the protect branch runs. + let message_id = MessageId::new_from_service_and_method(0x1234, 0x5678); + let key = E2EKey::from_message_id(message_id); + let mut reg = E2ERegistry::new(); + reg.register(key, E2EProfile::Profile4(Profile4Config::new(0, 15))); + let e2e_registry = Arc::new(Mutex::new(reg)); + + let mut sm = SocketManager::::bind(0, e2e_registry).unwrap(); + + // Craft a message whose raw-encoded size fits UDP_BUFFER_SIZE (16-byte + // header + 1480-byte payload = 1496 bytes) but whose E2E-protected + // size does not (payload grows by PROFILE4_HEADER_SIZE = 12, pushing + // the total to 1508 bytes, 8 over MTU). + let payload_bytes = [0u8; 1480]; + let payload = RawPayload::from_payload_bytes(message_id, &payload_bytes).unwrap(); + let header = Header::new( + message_id, + 0x0001_0001, + 0x01, + 0x01, + MessageTypeField::new(MessageType::Request, false), + ReturnCode::Ok, + payload_bytes.len(), + ); + let message = Message::new(header, payload); + + let target = SocketAddrV4::new(Ipv4Addr::LOCALHOST, 9999); + let err = sm + .send(target, message) + .await + .expect_err("E2E-protected oversize message must error"); + match err { + Error::Capacity(tag) => assert_eq!(tag, "udp_buffer"), + other => panic!("expected Error::Capacity(\"udp_buffer\"), got {other:?}"), + } + } } diff --git a/src/lib.rs b/src/lib.rs index 78877b3..ed0ab4f 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -92,6 +92,13 @@ #[cfg(feature = "std")] extern crate std; +/// Maximum size, in bytes, of UDP datagrams produced by the `client` and +/// `server` send paths. Sized to Ethernet MTU; messages larger than this +/// cannot be serialized and will error out. Every outgoing stack buffer in +/// the crate is sized to this constant — bare-metal ports with a smaller +/// link MTU may want to lower it by forking. +pub const UDP_BUFFER_SIZE: usize = 1500; + /// SOME/IP client for discovering services and exchanging messages. #[cfg(feature = "client")] pub mod client; diff --git a/src/server/error.rs b/src/server/error.rs index 9d80d9a..1d17780 100644 --- a/src/server/error.rs +++ b/src/server/error.rs @@ -1,7 +1,11 @@ use thiserror::Error; /// Errors that can occur during SOME/IP server operations. +/// +/// Marked `#[non_exhaustive]` so future variants (transport-specific errors +/// in upcoming releases) can be added without a breaking change. #[derive(Error, Debug)] +#[non_exhaustive] pub enum Error { /// A SOME/IP protocol-level error. #[error(transparent)] @@ -12,6 +16,13 @@ pub enum Error { /// An E2E protection or checking error occurred. #[error(transparent)] E2e(#[from] crate::e2e::Error), + /// A fixed-capacity internal structure is full (e.g. a stack send + /// buffer smaller than the outgoing message). The argument is a + /// lowercase `snake_case` tag naming the resource; grep the crate for + /// the tag to find the compile-time constant that governs it. Current + /// tags: `"udp_buffer"` (→ `crate::UDP_BUFFER_SIZE`). + #[error("internal capacity exceeded: {0}")] + Capacity(&'static str), } impl From for Error { diff --git a/src/server/event_publisher.rs b/src/server/event_publisher.rs index caecdb6..6b8d8fc 100644 --- a/src/server/event_publisher.rs +++ b/src/server/event_publisher.rs @@ -2,12 +2,11 @@ use super::Error; use super::subscription_manager::SubscriptionManager; -use crate::e2e::{E2EKey, E2ERegistry, PROFILE4_HEADER_SIZE}; +use crate::UDP_BUFFER_SIZE; +use crate::e2e::{E2EKey, E2ERegistry}; use crate::protocol::{Header, Message}; use crate::traits::{PayloadWireFormat, WireFormat}; use std::sync::{Arc, Mutex}; -use std::vec; -use std::vec::Vec; use tokio::net::UdpSocket; use tokio::sync::RwLock; @@ -70,11 +69,13 @@ impl EventPublisher { return Ok(0); } - // Serialize the message once - let mut buffer = Vec::new(); - message.encode(&mut buffer)?; + // Serialize the message into a stack buffer sized to MTU. + let mut buffer = [0u8; UDP_BUFFER_SIZE]; + let mut message_length = message.encode_to_slice(&mut buffer)?; - // Apply E2E protect if configured + // Apply E2E protect if configured. The `protected` stack buffer is + // disjoint from `buffer`, so we can read the unprotected payload + // directly out of `buffer[16..]` without a separate copy. { let key = E2EKey::from_message_id(message.header().message_id()); let mut registry = self @@ -82,17 +83,30 @@ impl EventPublisher { .lock() .expect("e2e registry lock poisoned"); if registry.contains_key(&key) { - let message_length = buffer.len(); - let original_payload = buffer[16..message_length].to_vec(); let upper_header: [u8; 8] = buffer[8..16].try_into().expect("upper header slice"); - let mut protected = vec![0u8; original_payload.len() + PROFILE4_HEADER_SIZE]; - match registry.protect(key, &original_payload, upper_header, &mut protected) { + let mut protected = [0u8; UDP_BUFFER_SIZE]; + let result = registry.protect( + key, + &buffer[16..message_length], + upper_header, + &mut protected, + ); + match result { Some(Ok(protected_len)) => { + if 16 + protected_len > UDP_BUFFER_SIZE { + tracing::error!( + "E2E-protected payload ({} bytes) exceeds UDP_BUFFER_SIZE ({}); \ + dropping publish", + 16 + protected_len, + UDP_BUFFER_SIZE + ); + return Err(Error::Capacity("udp_buffer")); + } #[allow(clippy::cast_possible_truncation)] let new_length: u32 = 8 + protected_len as u32; buffer[4..8].copy_from_slice(&new_length.to_be_bytes()); - buffer.resize(16 + protected_len, 0); buffer[16..16 + protected_len].copy_from_slice(&protected[..protected_len]); + message_length = 16 + protected_len; } Some(Err(e)) => { tracing::error!("E2E protect error: {:?}", e); @@ -102,16 +116,18 @@ impl EventPublisher { } } + let datagram = &buffer[..message_length]; + // Send to all subscribers let mut sent_count = 0; for subscriber in &subscribers { - match self.socket.send_to(&buffer, subscriber.address).await { + match self.socket.send_to(datagram, subscriber.address).await { Ok(_) => { sent_count += 1; tracing::trace!( "Sent event to subscriber {} ({} bytes)", subscriber.address, - buffer.len() + message_length ); } Err(e) => { @@ -173,15 +189,25 @@ impl EventPublisher { payload.len(), ); - // Serialize header + payload - let mut buffer = Vec::new(); - header.encode(&mut buffer)?; - buffer.extend_from_slice(payload); + // Serialize header + payload into a stack buffer sized to MTU. + let mut buffer = [0u8; UDP_BUFFER_SIZE]; + let header_len = header.encode_to_slice(&mut buffer)?; + let total_len = header_len + payload.len(); + if total_len > UDP_BUFFER_SIZE { + tracing::error!( + "raw event ({} bytes) exceeds UDP_BUFFER_SIZE ({}); dropping publish", + total_len, + UDP_BUFFER_SIZE + ); + return Err(Error::Capacity("udp_buffer")); + } + buffer[header_len..total_len].copy_from_slice(payload); + let datagram = &buffer[..total_len]; // Send to all subscribers let mut sent_count = 0; for subscriber in &subscribers { - match self.socket.send_to(&buffer, subscriber.address).await { + match self.socket.send_to(datagram, subscriber.address).await { Ok(_) => { sent_count += 1; } @@ -309,6 +335,8 @@ mod tests { use super::*; use crate::protocol::sd::test_support::{TestPayload, empty_sd_header}; use std::net::{Ipv4Addr, SocketAddrV4}; + use std::vec; + use std::vec::Vec; fn test_registry() -> Arc> { Arc::new(Mutex::new(E2ERegistry::new())) @@ -393,6 +421,28 @@ mod tests { assert_eq!(count, 0); } + #[tokio::test] + async fn test_publish_raw_event_exceeds_udp_buffer_returns_capacity_error() { + let subscriptions = Arc::new(RwLock::new(SubscriptionManager::new())); + let addr = SocketAddrV4::new(Ipv4Addr::LOCALHOST, 9999); + { + let mut mgr = subscriptions.write().await; + mgr.subscribe(0x5B, 1, 0x01, addr); + } + let (publisher, _) = make_publisher(subscriptions).await; + + // Payload = UDP_BUFFER_SIZE forces total (header + payload) over the cap. + let too_big = vec![0u8; UDP_BUFFER_SIZE]; + let err = publisher + .publish_raw_event(0x5B, 1, 0x01, 0x8001, 0x0001, 0x01, 0x01, &too_big) + .await + .expect_err("oversize payload must error, not report Ok(0)"); + match err { + Error::Capacity(tag) => assert_eq!(tag, "udp_buffer"), + other => panic!("expected Error::Capacity(\"udp_buffer\"), got {other:?}"), + } + } + #[tokio::test] async fn test_publish_raw_event_with_subscriber() { let subscriptions = Arc::new(RwLock::new(SubscriptionManager::new())); From 3b4c10f2750acd0671499911e63ae6f84f45e980 Mon Sep 17 00:00:00 2001 From: Justin Kovacich Date: Thu, 23 Apr 2026 10:25:37 -0400 Subject: [PATCH 2/9] Responding to PR Feedback --- src/client/socket_manager.rs | 58 +++++++++++++++++++++++++++++++++++ src/lib.rs | 10 ++++-- src/server/error.rs | 7 +++-- src/server/event_publisher.rs | 14 +++++++++ 4 files changed, 83 insertions(+), 6 deletions(-) diff --git a/src/client/socket_manager.rs b/src/client/socket_manager.rs index 0b60a7b..a903ada 100644 --- a/src/client/socket_manager.rs +++ b/src/client/socket_manager.rs @@ -257,6 +257,20 @@ where message = tx_rx.recv() => { if let Some(send_message) = message { trace!("Sending: {:?}", &send_message); + // Fail fast with the capacity error rather than + // letting `encode` report a less-actionable + // protocol I/O error when it runs out of + // buffer. Matches the E2E-overflow arm below + // and the server event_publisher path. + let required_size = send_message.message.required_size(); + if required_size > UDP_BUFFER_SIZE { + error!( + "outgoing message ({} bytes) exceeds UDP_BUFFER_SIZE ({}); dropping send", + required_size, UDP_BUFFER_SIZE + ); + let _ = send_message.response.send(Err(Error::Capacity("udp_buffer"))); + continue; + } let mut message_length = match send_message.message.encode(&mut buf.as_mut_slice()) { Ok(length) => length, Err(e) => { @@ -619,4 +633,48 @@ mod tests { other => panic!("expected Error::Capacity(\"udp_buffer\"), got {other:?}"), } } + + /// Messages whose raw encoded size already exceeds `UDP_BUFFER_SIZE` + /// — with no E2E in play — must be rejected up front with + /// `Error::Capacity("udp_buffer")` rather than bubbling out the + /// less-actionable protocol I/O error that `encode` would report + /// after running out of buffer. + #[tokio::test] + async fn send_raw_message_exceeding_udp_buffer_returns_capacity_error() { + use crate::RawPayload; + use crate::protocol::{Header, MessageId, MessageType, MessageTypeField, ReturnCode}; + + let message_id = MessageId::new_from_service_and_method(0x1234, 0x5678); + // No E2E registered — goes straight through the pre-encode check. + let e2e_registry = Arc::new(Mutex::new(E2ERegistry::new())); + let mut sm = SocketManager::::bind(0, e2e_registry).unwrap(); + + // 16-byte header + 1485-byte payload = 1501 bytes, one over the cap. + let payload_bytes = [0u8; 1485]; + let payload = RawPayload::from_payload_bytes(message_id, &payload_bytes).unwrap(); + let header = Header::new( + message_id, + 0x0001_0001, + 0x01, + 0x01, + MessageTypeField::new(MessageType::Request, false), + ReturnCode::Ok, + payload_bytes.len(), + ); + let message = Message::new(header, payload); + assert!( + message.required_size() > UDP_BUFFER_SIZE, + "fixture must actually exceed the cap for this test to exercise the new path", + ); + + let target = SocketAddrV4::new(Ipv4Addr::LOCALHOST, 9999); + let err = sm + .send(target, message) + .await + .expect_err("raw oversize message must error"); + match err { + Error::Capacity(tag) => assert_eq!(tag, "udp_buffer"), + other => panic!("expected Error::Capacity(\"udp_buffer\"), got {other:?}"), + } + } } diff --git a/src/lib.rs b/src/lib.rs index ed0ab4f..bd523c9 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -92,9 +92,13 @@ #[cfg(feature = "std")] extern crate std; -/// Maximum size, in bytes, of UDP datagrams produced by the `client` and -/// `server` send paths. Sized to Ethernet MTU; messages larger than this -/// cannot be serialized and will error out. Every outgoing stack buffer in +/// Maximum size, in bytes, of UDP payloads produced by the `client` and +/// `server` send paths. Messages larger than this cannot be serialized and +/// will error out. Note that this is an application-level payload limit, +/// not an Ethernet-MTU-safe size: a 1500-byte UDP payload will exceed a +/// 1500-byte L2 MTU once IP/UDP headers are added (IPv4 leaves 1472 bytes +/// of UDP payload, IPv6 leaves 1452), so sends at this size may fragment +/// or fail depending on the network stack. Every outgoing stack buffer in /// the crate is sized to this constant — bare-metal ports with a smaller /// link MTU may want to lower it by forking. pub const UDP_BUFFER_SIZE: usize = 1500; diff --git a/src/server/error.rs b/src/server/error.rs index 1d17780..be86edb 100644 --- a/src/server/error.rs +++ b/src/server/error.rs @@ -2,10 +2,11 @@ use thiserror::Error; /// Errors that can occur during SOME/IP server operations. /// -/// Marked `#[non_exhaustive]` so future variants (transport-specific errors -/// in upcoming releases) can be added without a breaking change. +/// Not marked `#[non_exhaustive]` today: downstream crates that match on +/// this enum rely on exhaustiveness, and adding the attribute now would be +/// a silent breaking change that `cargo-semver-checks` would flag. Revisit +/// when a breaking release is planned. #[derive(Error, Debug)] -#[non_exhaustive] pub enum Error { /// A SOME/IP protocol-level error. #[error(transparent)] diff --git a/src/server/event_publisher.rs b/src/server/event_publisher.rs index 6b8d8fc..2cddf3b 100644 --- a/src/server/event_publisher.rs +++ b/src/server/event_publisher.rs @@ -69,6 +69,20 @@ impl EventPublisher { return Ok(0); } + // Fail fast with the capacity error rather than letting + // `encode_to_slice` report a less-actionable protocol I/O error + // when it runs out of buffer. Matches the raw-event path below + // and the client socket_manager path. + let required_size = message.required_size(); + if required_size > UDP_BUFFER_SIZE { + tracing::error!( + "Message size ({} bytes) exceeds UDP_BUFFER_SIZE ({}); dropping publish", + required_size, + UDP_BUFFER_SIZE + ); + return Err(Error::Capacity("udp_buffer")); + } + // Serialize the message into a stack buffer sized to MTU. let mut buffer = [0u8; UDP_BUFFER_SIZE]; let mut message_length = message.encode_to_slice(&mut buffer)?; From 92cf7f8f20363e5803d7d3f14e633790c8d8de9f Mon Sep 17 00:00:00 2001 From: Justin Kovacich Date: Thu, 23 Apr 2026 11:47:51 -0400 Subject: [PATCH 3/9] phase 3: add coverage for publish_event pre-encode overflow branch Commit 343da67 added a `required_size() > UDP_BUFFER_SIZE` pre-check to `EventPublisher::publish_event` but left the new branch uncovered. Regression guard added as `publish_event_pre_encode_exceeds_udp_buffer_returns_capacity_error`: registers a subscriber (the pre-check sits after the `subscribers.is_empty()` early return, so the test needs one or else hits the false-positive Ok(0) path), constructs a 1501-byte fixture (16-byte header + 1485-byte payload, one over the cap), calls publish_event, asserts Err(Error::Capacity("udp_buffer")). Mirrors the fixture pattern from `send_raw_message_exceeding_udp_buffer_returns_capacity_error` on the client side. Co-Authored-By: Claude Opus 4.7 --- src/server/event_publisher.rs | 53 +++++++++++++++++++++++++++++++++++ 1 file changed, 53 insertions(+) diff --git a/src/server/event_publisher.rs b/src/server/event_publisher.rs index 2cddf3b..1ef48d8 100644 --- a/src/server/event_publisher.rs +++ b/src/server/event_publisher.rs @@ -457,6 +457,59 @@ mod tests { } } + /// Regression guard against 343da67: without the pre-check, an oversize + /// message would fail with a less-actionable protocol I/O error from + /// `encode_to_slice`'s slice writer running out of buffer, rather than + /// the explicit `Error::Capacity("udp_buffer")` the new branch returns. + /// + /// Note: a subscriber must be registered first — the pre-check sits + /// after the `subscribers.is_empty()` early return, so without one the + /// function would return `Ok(0)` and never touch the new branch, + /// giving a false positive. + #[tokio::test] + async fn publish_event_pre_encode_exceeds_udp_buffer_returns_capacity_error() { + use crate::RawPayload; + use crate::protocol::{Header, MessageId, MessageType, MessageTypeField, ReturnCode}; + + let subscriptions = Arc::new(RwLock::new(SubscriptionManager::new())); + let addr = SocketAddrV4::new(Ipv4Addr::LOCALHOST, 9999); + { + let mut mgr = subscriptions.write().await; + mgr.subscribe(0x5B, 1, 0x01, addr); + } + let (publisher, _) = make_publisher(subscriptions).await; + + // 16-byte header + 1485-byte payload = 1501 bytes, one over the cap. + // Mirrors the client-side oversize fixture in + // `send_raw_message_exceeding_udp_buffer_returns_capacity_error`. + let message_id = MessageId::new_from_service_and_method(0x1234, 0x5678); + let payload_bytes = [0u8; 1485]; + let payload = RawPayload::from_payload_bytes(message_id, &payload_bytes).unwrap(); + let header = Header::new( + message_id, + 0x0001_0001, + 0x01, + 0x01, + MessageTypeField::new(MessageType::Request, false), + ReturnCode::Ok, + payload_bytes.len(), + ); + let message = Message::new(header, payload); + assert!( + message.required_size() > UDP_BUFFER_SIZE, + "fixture must exceed cap", + ); + + let err = publisher + .publish_event(0x5B, 1, 0x01, &message) + .await + .expect_err("oversize message must error, not report Ok(_)"); + match err { + Error::Capacity(tag) => assert_eq!(tag, "udp_buffer"), + other => panic!("expected Error::Capacity(\"udp_buffer\"), got {other:?}"), + } + } + #[tokio::test] async fn test_publish_raw_event_with_subscriber() { let subscriptions = Arc::new(RwLock::new(SubscriptionManager::new())); From cb2c7ed232d022a3f6fedc7c333b6428fd7acc4e Mon Sep 17 00:00:00 2001 From: Justin Kovacich Date: Thu, 23 Apr 2026 14:20:27 -0400 Subject: [PATCH 4/9] docs: clarify UDP_BUFFER_SIZE scope + wording (Copilot round-2) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - src/lib.rs: UDP_BUFFER_SIZE doc now enumerates exactly which send paths honor this cap (SocketManager::send, publish_event, publish_raw_event) and explicitly calls out the SD announcement / SubscribeAck / SubscribeNack paths that still use heap Vec buffers as a known gap planned for the bare-metal no_alloc refactor. - src/server/event_publisher.rs: reworded "stack buffer sized to MTU" comments at the two buffer-allocation sites — the buffer lives in the async future's state, not literally on the stack, and the cap is a UDP payload limit, not an Ethernet MTU. New wording points at the UDP_BUFFER_SIZE docs for the distinction. The two `use std::vec` comments (event_publisher.rs:335, socket_manager.rs:362) were verified to be false positives: removing the imports breaks the lib-test build with 4 errors about `vec!` macro not in scope. Same no_std mechanics as the prior #75-1 resolution — reply posted on the comment threads. Co-Authored-By: Claude Opus 4.7 --- src/lib.rs | 32 +++++++++++++++++++++++--------- src/server/event_publisher.rs | 9 +++++++-- 2 files changed, 30 insertions(+), 11 deletions(-) diff --git a/src/lib.rs b/src/lib.rs index bd523c9..3ba7d49 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -92,15 +92,29 @@ #[cfg(feature = "std")] extern crate std; -/// Maximum size, in bytes, of UDP payloads produced by the `client` and -/// `server` send paths. Messages larger than this cannot be serialized and -/// will error out. Note that this is an application-level payload limit, -/// not an Ethernet-MTU-safe size: a 1500-byte UDP payload will exceed a -/// 1500-byte L2 MTU once IP/UDP headers are added (IPv4 leaves 1472 bytes -/// of UDP payload, IPv6 leaves 1452), so sends at this size may fragment -/// or fail depending on the network stack. Every outgoing stack buffer in -/// the crate is sized to this constant — bare-metal ports with a smaller -/// link MTU may want to lower it by forking. +/// Maximum size, in bytes, of UDP payloads for `client` / `server` send +/// paths that serialize into a fixed-size buffer of this size. +/// +/// Paths currently capped by this constant: +/// - `client::SocketManager::send` (unicast + SD outbound) +/// - `server::EventPublisher::publish_event` +/// - `server::EventPublisher::publish_raw_event` +/// +/// When one of these paths is actually reached and serialization is +/// attempted, messages larger than this cap fail with +/// `Error::Capacity("udp_buffer")`. Paths that return early before +/// attempting serialization (e.g. `publish_event` when there are no +/// subscribers) are not affected. Other outbound SD paths (announcement +/// builders, `SubscribeAck` / `SubscribeNack`) currently still use +/// heap `Vec` buffers and are not capped by this constant — that is a +/// known gap, planned alongside the bare-metal `no_alloc` refactor. +/// +/// Note that this is an application-level UDP payload limit, not an +/// Ethernet-MTU-safe size: a 1500-byte UDP payload exceeds a 1500-byte +/// L2 MTU once IP/UDP headers are added (IPv4 leaves 1472 bytes of UDP +/// payload, IPv6 leaves 1452), so sends at this size may fragment or +/// fail depending on the network stack. Bare-metal ports targeting a +/// smaller link MTU may want to lower this by forking. pub const UDP_BUFFER_SIZE: usize = 1500; /// SOME/IP client for discovering services and exchanging messages. diff --git a/src/server/event_publisher.rs b/src/server/event_publisher.rs index 1ef48d8..42da45a 100644 --- a/src/server/event_publisher.rs +++ b/src/server/event_publisher.rs @@ -83,7 +83,11 @@ impl EventPublisher { return Err(Error::Capacity("udp_buffer")); } - // Serialize the message into a stack buffer sized to MTU. + // Serialize the message into a fixed-size buffer of + // `UDP_BUFFER_SIZE` bytes. (In this `async fn` the buffer lives + // in the future state, not literally on the stack; "MTU-sized" + // is a misleading description since the cap is a UDP payload + // limit, not an Ethernet MTU — see `UDP_BUFFER_SIZE` docs.) let mut buffer = [0u8; UDP_BUFFER_SIZE]; let mut message_length = message.encode_to_slice(&mut buffer)?; @@ -203,7 +207,8 @@ impl EventPublisher { payload.len(), ); - // Serialize header + payload into a stack buffer sized to MTU. + // Serialize header + payload into a fixed-size buffer of + // `UDP_BUFFER_SIZE` bytes. See note in `publish_event` above. let mut buffer = [0u8; UDP_BUFFER_SIZE]; let header_len = header.encode_to_slice(&mut buffer)?; let total_len = header_len + payload.len(); From edea683fecbc60f83d53dbd0db1406ec88d643f9 Mon Sep 17 00:00:00 2001 From: Justin Kovacich Date: Fri, 24 Apr 2026 13:47:16 -0400 Subject: [PATCH 5/9] round-3: clarify UDP_BUFFER_SIZE + memory-footprint docs Three doc-only Copilot round-3 nits on PR #77: - src/client/mod.rs: the per-`SocketManager` memory-footprint blurb implied the second `[u8; UDP_BUFFER_SIZE]` is always allocated. In fact `socket_loop_future` only allocates that scratch buffer when the send path actually needs E2E protection (the destination key is in the `E2ERegistry`); plain sends pay only ~1.5 KiB. Reworded the always-live vs peak budget so the ~24 KiB number is no longer presented as the steady-state cost. - src/client/socket_manager.rs: the E2E-overflow test comment said "8 over MTU", but `UDP_BUFFER_SIZE` is documented as a UDP payload cap, not an Ethernet-MTU-safe size. Reworded to "8 bytes over UDP_BUFFER_SIZE". - src/lib.rs: the `UDP_BUFFER_SIZE` doc referenced bare `Error::Capacity("udp_buffer")`, which is ambiguous at the crate root (no `crate::Error` exists). Qualified to `client::Error::Capacity(...)` / `server::Error::Capacity(...)`. Addresses Copilot comments 3138697909, 3138698042, 3138698156. Co-Authored-By: Claude Opus 4.7 (1M context) --- src/client/mod.rs | 14 +++++++++----- src/client/socket_manager.rs | 2 +- src/lib.rs | 4 +++- 3 files changed, 13 insertions(+), 7 deletions(-) diff --git a/src/client/mod.rs b/src/client/mod.rs index 8866e5c..e847450 100644 --- a/src/client/mod.rs +++ b/src/client/mod.rs @@ -9,11 +9,15 @@ //! depending on `sizeof::

()` and `sizeof::>()`. //! //! In addition, each `SocketManager`'s spawn loop holds a persistent -//! `[u8; UDP_BUFFER_SIZE]` receive/send buffer (1500 bytes) and transiently -//! allocates a second `[u8; UDP_BUFFER_SIZE]` on the stack for E2E-protect -//! output — so an active socket-loop future carries **~3 KiB** of buffer -//! state on top of its control-plane fields. With `UNICAST_SOCKETS_CAP=8` -//! sockets bound, the per-client buffer budget is therefore ~24 KiB. +//! `[u8; UDP_BUFFER_SIZE]` receive/send buffer (1500 bytes). When the send +//! path needs E2E protection (i.e. the destination key is registered in the +//! `E2ERegistry`), it transiently allocates a second `[u8; UDP_BUFFER_SIZE]` +//! on the stack for the protected output; sends without E2E protection do +//! not pay this cost. So an active socket-loop future carries **~1.5 KiB** +//! of always-live buffer state plus up to another ~1.5 KiB during E2E +//! sends. With `UNICAST_SOCKETS_CAP=8` sockets bound, the always-live +//! per-client buffer budget is ~12 KiB, with peak ~24 KiB during +//! concurrent E2E-protected sends on every socket. //! //! On `std + tokio`, all of this is allocated on the heap when each future //! is spawned, so the overhead is invisible to callers. On the bare-metal diff --git a/src/client/socket_manager.rs b/src/client/socket_manager.rs index a903ada..8de28ad 100644 --- a/src/client/socket_manager.rs +++ b/src/client/socket_manager.rs @@ -609,7 +609,7 @@ mod tests { // Craft a message whose raw-encoded size fits UDP_BUFFER_SIZE (16-byte // header + 1480-byte payload = 1496 bytes) but whose E2E-protected // size does not (payload grows by PROFILE4_HEADER_SIZE = 12, pushing - // the total to 1508 bytes, 8 over MTU). + // the total to 1508 bytes, 8 bytes over UDP_BUFFER_SIZE). let payload_bytes = [0u8; 1480]; let payload = RawPayload::from_payload_bytes(message_id, &payload_bytes).unwrap(); let header = Header::new( diff --git a/src/lib.rs b/src/lib.rs index 3ba7d49..4a18c80 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -102,7 +102,9 @@ extern crate std; /// /// When one of these paths is actually reached and serialization is /// attempted, messages larger than this cap fail with -/// `Error::Capacity("udp_buffer")`. Paths that return early before +/// `client::Error::Capacity("udp_buffer")` or +/// `server::Error::Capacity("udp_buffer")`, depending on the path. +/// Paths that return early before /// attempting serialization (e.g. `publish_event` when there are no /// subscribers) are not affected. Other outbound SD paths (announcement /// builders, `SubscribeAck` / `SubscribeNack`) currently still use From 4aadf7302e868173e95e0e33fbc17bb547fc95d2 Mon Sep 17 00:00:00 2001 From: Justin Kovacich Date: Fri, 24 Apr 2026 16:54:14 -0400 Subject: [PATCH 6/9] test(event_publisher): cover E2E-protected overflow guard in publish_event MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Adds a unit test that registers a Profile4 E2E profile for the message key and publishes a message whose raw encoded size fits UDP_BUFFER_SIZE (1496 bytes) but whose protected size does not (1508 bytes, after the 12-byte Profile4 header). Asserts that publish_event returns Error::Capacity("udp_buffer") — exercising the post-protect guard that was previously only covered on the client send path. Co-Authored-By: Claude Opus 4.7 (1M context) --- src/server/event_publisher.rs | 58 +++++++++++++++++++++++++++++++++++ 1 file changed, 58 insertions(+) diff --git a/src/server/event_publisher.rs b/src/server/event_publisher.rs index 42da45a..563e6c4 100644 --- a/src/server/event_publisher.rs +++ b/src/server/event_publisher.rs @@ -515,6 +515,64 @@ mod tests { } } + /// Messages whose raw encoded size fits `UDP_BUFFER_SIZE` but whose + /// E2E-protected size does not must be rejected with + /// `Error::Capacity("udp_buffer")` — guarding the post-protect branch + /// added alongside the raw-size pre-check. + #[tokio::test] + async fn test_publish_event_e2e_protected_exceeds_udp_buffer_returns_capacity_error() { + use crate::RawPayload; + use crate::e2e::{E2EProfile, Profile4Config}; + use crate::protocol::MessageId; + + // Register an E2E profile so the protect branch actually runs. + let message_id = MessageId::new_from_service_and_method(0x5B, 0x8001); + let key = E2EKey::from_message_id(message_id); + let mut reg = E2ERegistry::new(); + reg.register(key, E2EProfile::Profile4(Profile4Config::new(0, 15))); + let e2e_registry = Arc::new(Mutex::new(reg)); + + // Pre-register a subscriber so we don't short-circuit on the + // "no subscribers" branch before reaching the E2E guard. + let subscriptions = Arc::new(RwLock::new(SubscriptionManager::new())); + { + let mut mgr = subscriptions.write().await; + mgr.subscribe(0x5B, 1, 0x01, SocketAddrV4::new(Ipv4Addr::LOCALHOST, 9999)); + } + + let socket = Arc::new(UdpSocket::bind("127.0.0.1:0").await.unwrap()); + let publisher = EventPublisher::new(subscriptions, socket, e2e_registry); + + // 16-byte header + 1480-byte payload = 1496 bytes raw (fits the + // 1500-byte cap), but Profile4 adds PROFILE4_HEADER_SIZE = 12 + // bytes, pushing the protected total to 1508 — 8 over MTU. + let payload_bytes = [0u8; 1480]; + let payload = RawPayload::from_payload_bytes(message_id, &payload_bytes).unwrap(); + let header = Header::new_event( + message_id.service_id(), + message_id.method_id(), + 0x0001_0001, + 0x01, + 0x01, + payload_bytes.len(), + ); + let message = Message::new(header, payload); + assert!( + message.required_size() <= UDP_BUFFER_SIZE, + "fixture's raw size must fit the cap so the pre-encode check passes and \ + we actually exercise the post-protect guard", + ); + + let err = publisher + .publish_event(0x5B, 1, 0x01, &message) + .await + .expect_err("E2E-protected oversize message must error, not report Ok(n)"); + match err { + Error::Capacity(tag) => assert_eq!(tag, "udp_buffer"), + other => panic!("expected Error::Capacity(\"udp_buffer\"), got {other:?}"), + } + } + #[tokio::test] async fn test_publish_raw_event_with_subscriber() { let subscriptions = Arc::new(RwLock::new(SubscriptionManager::new())); From 9fbd8e03eddec47edefee1e7e31efb27e4a9328b Mon Sep 17 00:00:00 2001 From: Justin Kovacich Date: Fri, 24 Apr 2026 17:39:35 -0400 Subject: [PATCH 7/9] round-4: fix E2E-overflow log wording + MTU-vs-UDP_BUFFER_SIZE comment MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - publish_event: log now says "E2E-protected datagram (... header + protected payload)" so the 16+protected_len value is identified as the full SOME/IP datagram size, not the payload. - test fixture comment: "8 over MTU" → "8 bytes over UDP_BUFFER_SIZE" for terminology consistency with the rest of the PR. Co-Authored-By: Claude Opus 4.7 (1M context) --- src/server/event_publisher.rs | 7 ++++--- 1 file changed, 4 insertions(+), 3 deletions(-) diff --git a/src/server/event_publisher.rs b/src/server/event_publisher.rs index 563e6c4..f19f89c 100644 --- a/src/server/event_publisher.rs +++ b/src/server/event_publisher.rs @@ -113,8 +113,8 @@ impl EventPublisher { Some(Ok(protected_len)) => { if 16 + protected_len > UDP_BUFFER_SIZE { tracing::error!( - "E2E-protected payload ({} bytes) exceeds UDP_BUFFER_SIZE ({}); \ - dropping publish", + "E2E-protected datagram ({} bytes, header + protected payload) \ + exceeds UDP_BUFFER_SIZE ({}); dropping publish", 16 + protected_len, UDP_BUFFER_SIZE ); @@ -545,7 +545,8 @@ mod tests { // 16-byte header + 1480-byte payload = 1496 bytes raw (fits the // 1500-byte cap), but Profile4 adds PROFILE4_HEADER_SIZE = 12 - // bytes, pushing the protected total to 1508 — 8 over MTU. + // bytes, pushing the protected total to 1508 — 8 bytes over + // UDP_BUFFER_SIZE. let payload_bytes = [0u8; 1480]; let payload = RawPayload::from_payload_bytes(message_id, &payload_bytes).unwrap(); let header = Header::new_event( From 32233bdf797da8aaa52736b15aceceb18d98f2a4 Mon Sep 17 00:00:00 2001 From: Justin Kovacich Date: Fri, 24 Apr 2026 18:03:18 -0400 Subject: [PATCH 8/9] fix(event_publisher): guard against usize overflow in raw-event total_len MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit `header_len + payload.len()` used unchecked `usize` addition. On a system with large enough `payload.len()` the sum can wrap, silently bypassing the `> UDP_BUFFER_SIZE` guard and corrupting the slice operations that follow. Switch to `checked_add` and treat overflow the same as exceeding `UDP_BUFFER_SIZE` — return `Error::Capacity("udp_buffer")`. Co-Authored-By: Claude Opus 4.7 (1M context) --- src/server/event_publisher.rs | 8 +++++++- 1 file changed, 7 insertions(+), 1 deletion(-) diff --git a/src/server/event_publisher.rs b/src/server/event_publisher.rs index f19f89c..e382d77 100644 --- a/src/server/event_publisher.rs +++ b/src/server/event_publisher.rs @@ -211,7 +211,13 @@ impl EventPublisher { // `UDP_BUFFER_SIZE` bytes. See note in `publish_event` above. let mut buffer = [0u8; UDP_BUFFER_SIZE]; let header_len = header.encode_to_slice(&mut buffer)?; - let total_len = header_len + payload.len(); + let Some(total_len) = header_len.checked_add(payload.len()) else { + tracing::error!( + "raw event length overflow exceeds UDP_BUFFER_SIZE ({}); dropping publish", + UDP_BUFFER_SIZE + ); + return Err(Error::Capacity("udp_buffer")); + }; if total_len > UDP_BUFFER_SIZE { tracing::error!( "raw event ({} bytes) exceeds UDP_BUFFER_SIZE ({}); dropping publish", From f5bf2009214ede94a2635d5f8d8e24bc6c3b6bd7 Mon Sep 17 00:00:00 2001 From: Justin Kovacich Date: Fri, 24 Apr 2026 18:24:36 -0400 Subject: [PATCH 9/9] round-5: derive oversize fixtures from UDP_BUFFER_SIZE + fix log wording - Client/server "E2E-protected payload" logs now say "E2E-protected datagram (header + protected payload)" since the logged value is the full SOME/IP datagram size, not the payload. - publish_raw_event checked_add-fail log now describes the real condition (usize overflow) and includes the input lengths, instead of falsely pointing at UDP_BUFFER_SIZE. - Oversize fixtures in socket_manager + event_publisher tests are now sized from UDP_BUFFER_SIZE (and PROFILE4_HEADER_SIZE implicitly for the E2E case) instead of hardcoded 1480/1485. Fixtures stay valid if the cap is retuned. - client/mod.rs memory-footprint doc no longer hardcodes "(1500 bytes)" or "~1.5 KiB" as load-bearing numbers; the scaling is now expressed in terms of UDP_BUFFER_SIZE with the current-default numbers as a parenthetical reference. Co-Authored-By: Claude Opus 4.7 (1M context) --- src/client/mod.rs | 22 +++++++++++++--------- src/client/socket_manager.rs | 24 ++++++++++++++++-------- src/server/event_publisher.rs | 28 ++++++++++++++++++---------- 3 files changed, 47 insertions(+), 27 deletions(-) diff --git a/src/client/mod.rs b/src/client/mod.rs index e847450..223ab5b 100644 --- a/src/client/mod.rs +++ b/src/client/mod.rs @@ -9,15 +9,19 @@ //! depending on `sizeof::

()` and `sizeof::>()`. //! //! In addition, each `SocketManager`'s spawn loop holds a persistent -//! `[u8; UDP_BUFFER_SIZE]` receive/send buffer (1500 bytes). When the send -//! path needs E2E protection (i.e. the destination key is registered in the -//! `E2ERegistry`), it transiently allocates a second `[u8; UDP_BUFFER_SIZE]` -//! on the stack for the protected output; sends without E2E protection do -//! not pay this cost. So an active socket-loop future carries **~1.5 KiB** -//! of always-live buffer state plus up to another ~1.5 KiB during E2E -//! sends. With `UNICAST_SOCKETS_CAP=8` sockets bound, the always-live -//! per-client buffer budget is ~12 KiB, with peak ~24 KiB during -//! concurrent E2E-protected sends on every socket. +//! `[u8; UDP_BUFFER_SIZE]` receive/send buffer. When the send path needs +//! E2E protection (i.e. the destination key is registered in the +//! `E2ERegistry`), it transiently allocates a second +//! `[u8; UDP_BUFFER_SIZE]` on the stack for the protected output; sends +//! without E2E protection do not pay this cost. So an active +//! socket-loop future carries one always-live `UDP_BUFFER_SIZE` buffer +//! plus up to one additional `UDP_BUFFER_SIZE` buffer during E2E sends. +//! With `UNICAST_SOCKETS_CAP=8` sockets bound, the total per-client +//! buffer budget scales as `UNICAST_SOCKETS_CAP * UDP_BUFFER_SIZE` +//! always-live, up to `2 * UNICAST_SOCKETS_CAP * UDP_BUFFER_SIZE` at +//! peak during concurrent E2E-protected sends on every socket. At the +//! current default of `UDP_BUFFER_SIZE = 1500`, that is ~12 KiB +//! always-live / ~24 KiB peak per client. //! //! On `std + tokio`, all of this is allocated on the heap when each future //! is spawned, so the overhead is invisible to callers. On the bare-metal diff --git a/src/client/socket_manager.rs b/src/client/socket_manager.rs index 8de28ad..4a12bfe 100644 --- a/src/client/socket_manager.rs +++ b/src/client/socket_manager.rs @@ -305,7 +305,7 @@ where Some(Ok(protected_len)) => { if 16 + protected_len > UDP_BUFFER_SIZE { error!( - "E2E-protected payload ({} bytes) exceeds UDP_BUFFER_SIZE ({}); dropping send", + "E2E-protected datagram ({} bytes, header + protected payload) exceeds UDP_BUFFER_SIZE ({}); dropping send", 16 + protected_len, UDP_BUFFER_SIZE ); let _ = send_message.response.send(Err(Error::Capacity("udp_buffer"))); @@ -606,11 +606,15 @@ mod tests { let mut sm = SocketManager::::bind(0, e2e_registry).unwrap(); - // Craft a message whose raw-encoded size fits UDP_BUFFER_SIZE (16-byte - // header + 1480-byte payload = 1496 bytes) but whose E2E-protected - // size does not (payload grows by PROFILE4_HEADER_SIZE = 12, pushing - // the total to 1508 bytes, 8 bytes over UDP_BUFFER_SIZE). - let payload_bytes = [0u8; 1480]; + // Craft a message whose raw-encoded size fits `UDP_BUFFER_SIZE` + // exactly (header + payload = cap) but whose E2E-protected size + // does not — Profile4 adds `PROFILE4_HEADER_SIZE` bytes which + // pushes the protected total over the cap. Sizes derived from + // `UDP_BUFFER_SIZE` and `PROFILE4_HEADER_SIZE` so the fixture + // stays valid if the constant is retuned. + const SOMEIP_HEADER_SIZE: usize = 16; + let payload_len = UDP_BUFFER_SIZE - SOMEIP_HEADER_SIZE; // raw total == UDP_BUFFER_SIZE + let payload_bytes = vec![0u8; payload_len]; let payload = RawPayload::from_payload_bytes(message_id, &payload_bytes).unwrap(); let header = Header::new( message_id, @@ -649,8 +653,12 @@ mod tests { let e2e_registry = Arc::new(Mutex::new(E2ERegistry::new())); let mut sm = SocketManager::::bind(0, e2e_registry).unwrap(); - // 16-byte header + 1485-byte payload = 1501 bytes, one over the cap. - let payload_bytes = [0u8; 1485]; + // Derive a payload that makes the full message exceed the UDP cap + // by 1 byte regardless of how `UDP_BUFFER_SIZE` is retuned: + // 16-byte header + payload_len = UDP_BUFFER_SIZE + 1. + const SOMEIP_HEADER_SIZE: usize = 16; + let payload_len = UDP_BUFFER_SIZE - SOMEIP_HEADER_SIZE + 1; + let payload_bytes = vec![0u8; payload_len]; let payload = RawPayload::from_payload_bytes(message_id, &payload_bytes).unwrap(); let header = Header::new( message_id, diff --git a/src/server/event_publisher.rs b/src/server/event_publisher.rs index e382d77..6255cf2 100644 --- a/src/server/event_publisher.rs +++ b/src/server/event_publisher.rs @@ -213,8 +213,9 @@ impl EventPublisher { let header_len = header.encode_to_slice(&mut buffer)?; let Some(total_len) = header_len.checked_add(payload.len()) else { tracing::error!( - "raw event length overflow exceeds UDP_BUFFER_SIZE ({}); dropping publish", - UDP_BUFFER_SIZE + "raw event length computation overflowed usize (header_len={}, payload.len()={}); dropping publish", + header_len, + payload.len() ); return Err(Error::Capacity("udp_buffer")); }; @@ -490,11 +491,15 @@ mod tests { } let (publisher, _) = make_publisher(subscriptions).await; - // 16-byte header + 1485-byte payload = 1501 bytes, one over the cap. - // Mirrors the client-side oversize fixture in + // Build a payload that exceeds the UDP cap by one byte based on + // `UDP_BUFFER_SIZE` instead of a hardcoded fixture length, so the + // test stays correct if the constant is retuned. Mirrors the + // client-side oversize fixture in // `send_raw_message_exceeding_udp_buffer_returns_capacity_error`. + const SOMEIP_HEADER_SIZE: usize = 16; let message_id = MessageId::new_from_service_and_method(0x1234, 0x5678); - let payload_bytes = [0u8; 1485]; + let payload_len = UDP_BUFFER_SIZE - SOMEIP_HEADER_SIZE + 1; + let payload_bytes = vec![0u8; payload_len]; let payload = RawPayload::from_payload_bytes(message_id, &payload_bytes).unwrap(); let header = Header::new( message_id, @@ -549,11 +554,14 @@ mod tests { let socket = Arc::new(UdpSocket::bind("127.0.0.1:0").await.unwrap()); let publisher = EventPublisher::new(subscriptions, socket, e2e_registry); - // 16-byte header + 1480-byte payload = 1496 bytes raw (fits the - // 1500-byte cap), but Profile4 adds PROFILE4_HEADER_SIZE = 12 - // bytes, pushing the protected total to 1508 — 8 bytes over - // UDP_BUFFER_SIZE. - let payload_bytes = [0u8; 1480]; + // Size the payload from `UDP_BUFFER_SIZE` and `PROFILE4_HEADER_SIZE` + // so the raw message fits exactly within the cap — leaving Profile4 + // protection to push the encoded message over the limit and + // exercise the post-protect guard — regardless of how + // `UDP_BUFFER_SIZE` is retuned. + const SOMEIP_HEADER_SIZE: usize = 16; + let payload_len = UDP_BUFFER_SIZE - SOMEIP_HEADER_SIZE; // raw total == UDP_BUFFER_SIZE + let payload_bytes = vec![0u8; payload_len]; let payload = RawPayload::from_payload_bytes(message_id, &payload_bytes).unwrap(); let header = Header::new_event( message_id.service_id(),