Skip to content

Commit

Permalink
fix: prevent overflow while decoding param types (#1227)
Browse files Browse the repository at this point in the history
Closes #1215 by fixing the way encoding size is computed.

---------
Co-authored-by: MujkicA <32431923+MujkicA@users.noreply.github.com>
Co-authored-by: Rodrigo Araújo <rod.dearaujo@gmail.com>
  • Loading branch information
iqdecay committed Jan 10, 2024
1 parent 5c734d9 commit 351dc72
Show file tree
Hide file tree
Showing 7 changed files with 166 additions and 113 deletions.
2 changes: 1 addition & 1 deletion packages/fuels-core/src/codec/abi_decoder.rs
Original file line number Diff line number Diff line change
Expand Up @@ -554,7 +554,7 @@ mod tests {
},
&[],
);
assert!(matches!(result, Err(Error::InvalidData(_))));
assert!(matches!(result, Err(Error::InvalidType(_))));
}

#[test]
Expand Down
31 changes: 13 additions & 18 deletions packages/fuels-core/src/codec/abi_decoder/bounded_decoder.rs
Original file line number Diff line number Diff line change
@@ -1,9 +1,9 @@
use std::{convert::TryInto, str};

use crate::{
checked_round_up_to_word_alignment,
codec::DecoderConfig,
constants::WORD_SIZE,
round_up_to_word_alignment,
traits::Tokenizable,
types::{
enum_variants::EnumVariants,
Expand Down Expand Up @@ -151,7 +151,7 @@ impl BoundedDecoder {

for param_type in param_types.iter() {
// padding has to be taken into account
bytes_read = round_up_to_word_alignment(bytes_read);
bytes_read = checked_round_up_to_word_alignment(bytes_read)?;
let res = self.decode_param(param_type, skip(bytes, bytes_read)?)?;
bytes_read += res.bytes_read;
tokens.push(res.token);
Expand All @@ -170,7 +170,7 @@ impl BoundedDecoder {

for param_type in param_types.iter() {
// padding has to be taken into account
bytes_read = round_up_to_word_alignment(bytes_read);
bytes_read = checked_round_up_to_word_alignment(bytes_read)?;
let res = self.decode_param(param_type, skip(bytes, bytes_read)?)?;
bytes_read += res.bytes_read;
tokens.push(res.token);
Expand Down Expand Up @@ -249,7 +249,7 @@ impl BoundedDecoder {
let decoded = str::from_utf8(encoded_str)?;
let result = Decoded {
token: Token::StringArray(StaticStringToken::new(decoded.into(), Some(length))),
bytes_read: round_up_to_word_alignment(length),
bytes_read: checked_round_up_to_word_alignment(length)?,
};
Ok(result)
}
Expand Down Expand Up @@ -333,24 +333,19 @@ impl BoundedDecoder {
/// * `data`: slice of encoded data on whose beginning we're expecting an encoded enum
/// * `variants`: all types that this particular enum type could hold
fn decode_enum(&mut self, bytes: &[u8], variants: &EnumVariants) -> Result<Decoded> {
let enum_width_in_bytes = variants
.compute_enum_width_in_bytes()
.ok_or(error!(InvalidData, "Error calculating enum width in bytes"))?;
let enum_width_in_bytes = variants.compute_enum_width_in_bytes()?;

let discriminant = peek_u64(bytes)?;
let selected_variant = variants.param_type_of_variant(discriminant)?;

let skip_extra_in_bytes = variants
.heap_type_variant()
.and_then(|(heap_discriminant, heap_type)| {
(heap_discriminant == discriminant).then_some(heap_type.compute_encoding_in_bytes())
})
.unwrap_or_default()
.unwrap_or_default();
let bytes_to_skip = enum_width_in_bytes
- selected_variant
.compute_encoding_in_bytes()
.ok_or(error!(InvalidData, "Error calculating enum width in bytes"))?
let skip_extra_in_bytes = match variants.heap_type_variant() {
Some((heap_type_discriminant, heap_type)) if heap_type_discriminant == discriminant => {
heap_type.compute_encoding_in_bytes()?
}
_ => 0,
};

let bytes_to_skip = enum_width_in_bytes - selected_variant.compute_encoding_in_bytes()?
+ skip_extra_in_bytes;

let enum_content_bytes = skip(bytes, bytes_to_skip)?;
Expand Down
9 changes: 6 additions & 3 deletions packages/fuels-core/src/codec/abi_encoder.rs
Original file line number Diff line number Diff line change
@@ -1,8 +1,8 @@
use fuel_types::bytes::padded_len_usize;

use crate::{
checked_round_up_to_word_alignment,
constants::WORD_SIZE,
round_up_to_word_alignment,
types::{
errors::Result,
pad_u16, pad_u32,
Expand Down Expand Up @@ -50,8 +50,11 @@ impl ABIEncoder {
data.append(&mut new_data);

if word_aligned {
let padding =
vec![0u8; round_up_to_word_alignment(offset_in_bytes) - offset_in_bytes];
let padding = vec![
0u8;
checked_round_up_to_word_alignment(offset_in_bytes)?
- offset_in_bytes
];
if !padding.is_empty() {
offset_in_bytes += padding.len();
data.push(Data::Inline(padding));
Expand Down
25 changes: 10 additions & 15 deletions packages/fuels-core/src/types/enum_variants.rs
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,7 @@ use crate::{
errors::{error, Result},
param_types::ParamType,
},
utils::round_up_to_word_alignment,
utils::checked_round_up_to_word_alignment,
};

#[derive(Debug, Clone, PartialEq, Eq, serde::Serialize, serde::Deserialize)]
Expand Down Expand Up @@ -48,32 +48,27 @@ impl EnumVariants {
}

/// Calculates how many bytes are needed to encode an enum.
pub fn compute_enum_width_in_bytes(&self) -> Option<usize> {
pub fn compute_enum_width_in_bytes(&self) -> Result<usize> {
if self.only_units_inside() {
return Some(ENUM_DISCRIMINANT_BYTE_WIDTH);
return Ok(ENUM_DISCRIMINANT_BYTE_WIDTH);
}

let width = self.param_types().iter().try_fold(0, |a, p| {
let width = self.param_types().iter().try_fold(0, |a, p| -> Result<_> {
let size = p.compute_encoding_in_bytes()?;
Some(a.max(size))
Ok(a.max(size))
})?;

Some(round_up_to_word_alignment(width) + ENUM_DISCRIMINANT_BYTE_WIDTH)
checked_round_up_to_word_alignment(width)?
.checked_add(ENUM_DISCRIMINANT_BYTE_WIDTH)
.ok_or_else(|| error!(InvalidType, "Enum variants are too wide"))
}

/// Determines the padding needed for the provided enum variant (based on the width of the
/// biggest variant) and returns it.
pub fn compute_padding_amount_in_bytes(&self, variant_param_type: &ParamType) -> Result<usize> {
let enum_width = self
.compute_enum_width_in_bytes()
.ok_or(error!(InvalidData, "Error calculating enum width in bytes"))?;
let enum_width = self.compute_enum_width_in_bytes()?;
let biggest_variant_width = enum_width - ENUM_DISCRIMINANT_BYTE_WIDTH;
let variant_width = variant_param_type
.compute_encoding_in_bytes()
.ok_or(error!(
InvalidData,
"Error calculating padding amount in bytes"
))?;
let variant_width = variant_param_type.compute_encoding_in_bytes()?;
Ok(biggest_variant_width - variant_width)
}
}
Expand Down
169 changes: 112 additions & 57 deletions packages/fuels-core/src/types/param_types.rs
Original file line number Diff line number Diff line change
Expand Up @@ -7,7 +7,7 @@ use fuel_abi_types::{
use itertools::chain;

use crate::{
round_up_to_word_alignment,
checked_round_up_to_word_alignment,
types::{
enum_variants::EnumVariants,
errors::{error, Error, Result},
Expand Down Expand Up @@ -67,10 +67,7 @@ impl ParamType {
param_type: &ParamType,
available_bytes: usize,
) -> Result<usize> {
let memory_size = param_type.compute_encoding_in_bytes().ok_or(error!(
InvalidType,
"Cannot calculate the number of elements."
))?;
let memory_size = param_type.compute_encoding_in_bytes()?;
if memory_size == 0 {
return Err(error!(
InvalidType,
Expand Down Expand Up @@ -110,40 +107,38 @@ impl ParamType {
}

pub fn validate_is_decodable(&self, max_depth: usize) -> Result<()> {
match self {
ParamType::Enum { variants, .. } => {
let all_param_types = variants.param_types();
let grandchildren_need_receipts = all_param_types
.iter()
.any(|child| child.children_need_extra_receipts());
if grandchildren_need_receipts {
return Err(error!(
InvalidType,
"Enums currently support only one level deep heap types."
));
}

let num_of_children_needing_receipts = all_param_types
.iter()
.filter(|param_type| param_type.is_extra_receipt_needed(false))
.count();
if num_of_children_needing_receipts > 1 {
Err(error!(
InvalidType,
"Enums currently support only one heap-type variant. Found: \
if let ParamType::Enum { variants, .. } = self {
let all_param_types = variants.param_types();
let grandchildren_need_receipts = all_param_types
.iter()
.any(|child| child.children_need_extra_receipts());
if grandchildren_need_receipts {
return Err(error!(
InvalidType,
"Enums currently support only one level deep heap types."
));
}

let num_of_children_needing_receipts = all_param_types
.iter()
.filter(|param_type| param_type.is_extra_receipt_needed(false))
.count();
if num_of_children_needing_receipts > 1 {
return Err(error!(
InvalidType,
"Enums currently support only one heap-type variant. Found: \
{num_of_children_needing_receipts}"
))
} else {
Ok(())
}
));
}
_ if self.children_need_extra_receipts() => Err(error!(
} else if self.children_need_extra_receipts() {
return Err(error!(
InvalidType,
"type {:?} is not decodable: nested heap types are currently not supported except in Enums.",
DebugWithDepth::new(self, max_depth)
)),
_ => Ok(()),
));
}
self.compute_encoding_in_bytes()?;
Ok(())
}

pub fn is_extra_receipt_needed(&self, top_level_type: bool) -> bool {
Expand All @@ -164,36 +159,56 @@ impl ParamType {
}

/// Compute the inner memory size of a containing heap type (`Bytes` or `Vec`s).
pub fn heap_inner_element_size(&self, top_level_type: bool) -> Option<usize> {
match &self {
ParamType::Vector(inner_param_type) => inner_param_type.compute_encoding_in_bytes(),
pub fn heap_inner_element_size(&self, top_level_type: bool) -> Result<Option<usize>> {
let heap_bytes_size = match &self {
ParamType::Vector(inner_param_type) => {
Some(inner_param_type.compute_encoding_in_bytes()?)
}
// `Bytes` type is byte-packed in the VM, so it's the size of an u8
ParamType::Bytes | ParamType::String => Some(std::mem::size_of::<u8>()),
ParamType::StringSlice if !top_level_type => ParamType::U8.compute_encoding_in_bytes(),
ParamType::RawSlice if !top_level_type => ParamType::U64.compute_encoding_in_bytes(),
ParamType::StringSlice if !top_level_type => {
Some(ParamType::U8.compute_encoding_in_bytes()?)
}
ParamType::RawSlice if !top_level_type => {
Some(ParamType::U64.compute_encoding_in_bytes()?)
}
_ => None,
}
};
Ok(heap_bytes_size)
}

/// Calculates the number of bytes the VM expects this parameter to be encoded in.
pub fn compute_encoding_in_bytes(&self) -> Option<usize> {
pub fn compute_encoding_in_bytes(&self) -> Result<usize> {
let overflow_error = || {
error!(
InvalidType,
"Reached overflow while computing encoding size for {:?}", self
)
};
match &self {
ParamType::Unit | ParamType::U8 | ParamType::Bool => Some(1),
ParamType::U16 | ParamType::U32 | ParamType::U64 => Some(8),
ParamType::U128 | ParamType::RawSlice | ParamType::StringSlice => Some(16),
ParamType::U256 | ParamType::B256 => Some(32),
ParamType::Vector(_) | ParamType::Bytes | ParamType::String => Some(24),
ParamType::Array(param, count) => {
param.compute_encoding_in_bytes()?.checked_mul(*count)
ParamType::Unit | ParamType::U8 | ParamType::Bool => Ok(1),
ParamType::U16 | ParamType::U32 | ParamType::U64 => Ok(8),
ParamType::U128 | ParamType::RawSlice | ParamType::StringSlice => Ok(16),
ParamType::U256 | ParamType::B256 => Ok(32),
ParamType::Vector(_) | ParamType::Bytes | ParamType::String => Ok(24),
ParamType::Array(param, count) => param
.compute_encoding_in_bytes()?
.checked_mul(*count)
.ok_or_else(overflow_error),
ParamType::StringArray(len) => {
checked_round_up_to_word_alignment(*len).map_err(|_| overflow_error())
}
ParamType::StringArray(len) => Some(round_up_to_word_alignment(*len)),
ParamType::Tuple(fields) | ParamType::Struct { fields, .. } => {
fields.iter().try_fold(0, |a, param_type| {
let size = round_up_to_word_alignment(param_type.compute_encoding_in_bytes()?);
Some(a + size)
fields.iter().try_fold(0, |a: usize, param_type| {
let size = checked_round_up_to_word_alignment(
param_type.compute_encoding_in_bytes()?,
)?;
a.checked_add(size).ok_or_else(overflow_error)
})
}
ParamType::Enum { variants, .. } => variants.compute_enum_width_in_bytes(),
ParamType::Enum { variants, .. } => variants
.compute_enum_width_in_bytes()
.map_err(|_| overflow_error()),
}
}

Expand Down Expand Up @@ -648,7 +663,7 @@ mod tests {
}

#[test]
fn structs_are_all_elements_combined_with_padding() {
fn structs_are_all_elements_combined_with_padding() -> Result<()> {
let inner_struct = ParamType::Struct {
fields: vec![ParamType::U32, ParamType::U32],
generics: vec![],
Expand All @@ -662,9 +677,10 @@ mod tests {
let width = a_struct.compute_encoding_in_bytes().unwrap();

const INNER_STRUCT_WIDTH: usize = WIDTH_OF_U32 * 2;
const EXPECTED_WIDTH: usize =
WIDTH_OF_B256 + round_up_to_word_alignment(WIDTH_OF_BOOL) + INNER_STRUCT_WIDTH;
assert_eq!(EXPECTED_WIDTH, width);
let expected_width: usize =
WIDTH_OF_B256 + checked_round_up_to_word_alignment(WIDTH_OF_BOOL)? + INNER_STRUCT_WIDTH;
assert_eq!(expected_width, width);
Ok(())
}

#[test]
Expand Down Expand Up @@ -1814,13 +1830,52 @@ mod tests {
assert_eq!(param_type, ParamType::String);
}

#[test]
fn test_compute_encoding_in_bytes_overflows() -> Result<()> {
let overflows = |p: ParamType| {
let error = p.compute_encoding_in_bytes().unwrap_err();
let overflow_error = error!(
InvalidType,
"Reached overflow while computing encoding size for {:?}", p
);
assert_eq!(error.to_string(), overflow_error.to_string());
};
let tuple_with_fields_too_wide = ParamType::Tuple(vec![
ParamType::StringArray(12514849900987264429),
ParamType::StringArray(7017071859781709229),
]);
overflows(tuple_with_fields_too_wide);

let struct_with_fields_too_wide = ParamType::Struct {
fields: vec![
ParamType::StringArray(12514849900987264429),
ParamType::StringArray(7017071859781709229),
],
generics: vec![],
};
overflows(struct_with_fields_too_wide);

let enum_with_variants_too_wide = ParamType::Enum {
variants: EnumVariants::new(vec![ParamType::StringArray(usize::MAX - 8)]).unwrap(),
generics: vec![],
};
overflows(enum_with_variants_too_wide);

let array_too_big = ParamType::Array(Box::new(ParamType::U64), usize::MAX);
overflows(array_too_big);

let string_array_too_big = ParamType::StringArray(usize::MAX);
overflows(string_array_too_big);
Ok(())
}

#[test]
fn calculate_num_of_elements() -> Result<()> {
let failing_param_type = ParamType::Array(Box::new(ParamType::U16), usize::MAX);
assert!(ParamType::calculate_num_of_elements(&failing_param_type, 0)
.unwrap_err()
.to_string()
.contains("Cannot calculate the number of elements"));
.contains("Reached overflow"));
let zero_sized_type = ParamType::Array(Box::new(ParamType::StringArray(0)), 1000);
assert!(ParamType::calculate_num_of_elements(&zero_sized_type, 0)
.unwrap_err()
Expand Down
Loading

0 comments on commit 351dc72

Please sign in to comment.