Skip to content
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
37 changes: 32 additions & 5 deletions nodedb-codec/src/bounds.rs
Original file line number Diff line number Diff line change
Expand Up @@ -63,13 +63,27 @@ pub(crate) fn decoded_len(value: usize, codec: &str) -> Result<usize, CodecError
/// The returned value is the original element count, suitable as the only
/// argument to `Vec::with_capacity`. This centralizes overflow and resource
/// checks for counts read from untrusted frames.
///
/// The element-size multiplication is carried out in 64-bit arithmetic rather
/// than in `usize`. A 32-bit target cannot multiply a hostile `u32` count by a
/// wide element at all, so doing it in `usize` turns a frame that is merely too
/// large into a `Corrupt` "overflows usize" — the same frame is a clean
/// `ResourceLimit` where `usize` is 64 bits. The ceiling is 64 MiB, far inside
/// any `usize` the workspace builds for, so the 64-bit product is always within
/// range when the check succeeds; only the comparison needed the wider type.
pub(crate) fn checked_capacity(
count: usize,
element_size: usize,
context: &str,
) -> Result<usize, CodecError> {
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)
}

Expand Down Expand Up @@ -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 { .. })));
}
}
10 changes: 5 additions & 5 deletions nodedb-codec/src/delta.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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;

// ---------------------------------------------------------------------------
Expand Down Expand Up @@ -198,8 +196,10 @@ fn encode_value_count(len: usize, codec: &str) -> Result<u32, CodecError> {
}

fn validate_value_count(count: usize, codec: &str) -> Result<(), CodecError> {
let bytes = checked_mul(count, size_of::<i64>(), "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::<i64>(), codec)?;
Ok(())
}

Expand Down
10 changes: 5 additions & 5 deletions nodedb-codec/src/double_delta.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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;

// ---------------------------------------------------------------------------
Expand Down Expand Up @@ -334,8 +332,10 @@ fn encode_value_count(len: usize, codec: &str) -> Result<u32, CodecError> {
}

fn validate_value_count(count: usize, codec: &str) -> Result<(), CodecError> {
let bytes = checked_mul(count, size_of::<i64>(), "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::<i64>(), codec)?;
Ok(())
}

Expand Down
7 changes: 4 additions & 3 deletions nodedb-codec/src/fastlanes/header.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand All @@ -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::<i64>(), "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::<i64>(), "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 {
Expand Down
Loading