From f985c6c3ac58101e35588d7ab1a5a0f127de98a0 Mon Sep 17 00:00:00 2001 From: EnRaiha <15997552+EnRaiha@users.noreply.github.com> Date: Sun, 27 Sep 2026 17:50:20 +0800 Subject: [PATCH] codec: scale a decoded element count in 64-bit so 32-bit targets agree MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit An element count read from a frame is multiplied by its element size before the 64 MiB ceiling is compared. On a 32-bit target that multiplication overflows `usize` for counts the 64-bit path rejects cleanly, so the same hostile frame came back as `Corrupt` there and `ResourceLimit` here — and the delta decoder's own test asserts `ResourceLimit`, so the suite aborted on a 32-bit target. `checked_capacity` now does the scaling in 64-bit arithmetic and reports the resource limit whenever the product exceeds the ceiling, on every target. The `requested` field stays a byte count: the branch is only reachable above 64 MiB and below the type's maximum, so the value is always representable. `validate_value_count` in delta and double-delta and the FastLanes header parse route through it rather than repeating the multiplication. On a 64-bit target no reachable input changes: wire counts cap at `u32::MAX`, whose byte size is rejected as `ResourceLimit` by both the old and the new code. --- nodedb-codec/src/bounds.rs | 37 ++++++++++++++++++++++++---- nodedb-codec/src/delta.rs | 10 ++++---- nodedb-codec/src/double_delta.rs | 10 ++++---- nodedb-codec/src/fastlanes/header.rs | 7 +++--- 4 files changed, 46 insertions(+), 18 deletions(-) diff --git a/nodedb-codec/src/bounds.rs b/nodedb-codec/src/bounds.rs index 278d0d525..d6e0a7f53 100644 --- a/nodedb-codec/src/bounds.rs +++ b/nodedb-codec/src/bounds.rs @@ -63,13 +63,27 @@ pub(crate) fn decoded_len(value: usize, codec: &str) -> Result Result { - let bytes = checked_mul(count, element_size, context)?; - decoded_len(bytes, context)?; + let bytes = (count as u64).saturating_mul(element_size as u64); + if bytes > MAX_DECODED_BYTES as u64 { + return Err(CodecError::ResourceLimit { + resource: format!("{context} decoded bytes"), + requested: usize::try_from(bytes).unwrap_or(usize::MAX), + limit: MAX_DECODED_BYTES, + }); + } Ok(count) } @@ -102,8 +116,21 @@ mod tests { } #[test] - fn checked_capacity_rejects_usize_overflow() { - let error = checked_capacity(usize::MAX, 2, "test"); - assert!(matches!(error, Err(CodecError::Corrupt { .. }))); + fn checked_capacity_rejects_oversized_element_count() { + // `usize::MAX` elements of eight bytes each. Multiplying those in + // `usize` overflows, and on a 32-bit target even `u32::MAX` elements + // do; the classification is the resource limit either way, on every + // target the workspace builds for. + let error = checked_capacity(usize::MAX, 8, "test"); + assert!(matches!(error, Err(CodecError::ResourceLimit { .. }))); + } + + #[test] + fn checked_capacity_rejects_wide_element_count_on_every_target() { + // The 32-bit case in the large: `u32::MAX` elements of eight bytes is + // 32 GiB at an element size that cannot be expressed in a 32-bit + // `usize`. + let error = checked_capacity(u32::MAX as usize, 8, "test"); + assert!(matches!(error, Err(CodecError::ResourceLimit { .. }))); } } diff --git a/nodedb-codec/src/delta.rs b/nodedb-codec/src/delta.rs index 86f3381cc..a19b3e43a 100644 --- a/nodedb-codec/src/delta.rs +++ b/nodedb-codec/src/delta.rs @@ -20,9 +20,7 @@ use std::mem::size_of; -use crate::bounds::{ - checked_add, checked_capacity, checked_mul, decoded_len, encode_input_len, u32_to_usize, -}; +use crate::bounds::{checked_add, checked_capacity, checked_mul, encode_input_len, u32_to_usize}; use crate::error::CodecError; // --------------------------------------------------------------------------- @@ -198,8 +196,10 @@ fn encode_value_count(len: usize, codec: &str) -> Result { } fn validate_value_count(count: usize, codec: &str) -> Result<(), CodecError> { - let bytes = checked_mul(count, size_of::(), "integer decoded bytes")?; - decoded_len(bytes, codec)?; + // See `checked_capacity`: the byte count is scaled in 64-bit arithmetic so + // a 32-bit target reports the resource limit rather than a `usize` + // overflow for a count it simply cannot express. + checked_capacity(count, size_of::(), codec)?; Ok(()) } diff --git a/nodedb-codec/src/double_delta.rs b/nodedb-codec/src/double_delta.rs index e42cec162..a507c38b1 100644 --- a/nodedb-codec/src/double_delta.rs +++ b/nodedb-codec/src/double_delta.rs @@ -31,9 +31,7 @@ use std::mem::size_of; -use crate::bounds::{ - checked_add, checked_capacity, checked_mul, decoded_len, encode_input_len, u32_to_usize, -}; +use crate::bounds::{checked_add, checked_capacity, encode_input_len, u32_to_usize}; use crate::error::CodecError; // --------------------------------------------------------------------------- @@ -334,8 +332,10 @@ fn encode_value_count(len: usize, codec: &str) -> Result { } fn validate_value_count(count: usize, codec: &str) -> Result<(), CodecError> { - let bytes = checked_mul(count, size_of::(), "integer decoded bytes")?; - decoded_len(bytes, codec)?; + // See `checked_capacity`: the byte count is scaled in 64-bit arithmetic so + // a 32-bit target reports the resource limit rather than a `usize` + // overflow for a count it simply cannot express. + checked_capacity(count, size_of::(), codec)?; Ok(()) } diff --git a/nodedb-codec/src/fastlanes/header.rs b/nodedb-codec/src/fastlanes/header.rs index bb5079a45..54b0f3303 100644 --- a/nodedb-codec/src/fastlanes/header.rs +++ b/nodedb-codec/src/fastlanes/header.rs @@ -4,7 +4,7 @@ use std::mem::size_of; -use crate::bounds::{checked_mul, checked_range, decoded_len, u32_to_usize}; +use crate::bounds::{checked_capacity, checked_mul, checked_range, u32_to_usize}; use crate::error::CodecError; use super::block::skip_block; @@ -23,8 +23,9 @@ pub(super) fn parse_header(data: &[u8]) -> Result<(usize, usize), CodecError> { u32::from_le_bytes([header[0], header[1], header[2], header[3]]), "FastLanes value count", )?; - let decoded_bytes = checked_mul(total_count, size_of::(), "FastLanes decoded bytes")?; - decoded_len(decoded_bytes, "FastLanes")?; + // Rejects a hostile count before it is used for block math; see + // `checked_capacity` for why the scaling is done in 64-bit arithmetic. + checked_capacity(total_count, size_of::(), "FastLanes")?; let block_count = usize::from(u16::from_le_bytes([header[4], header[5]])); let expected_blocks = total_count.div_ceil(BLOCK_SIZE); if block_count != expected_blocks {