Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

fix: prevent overflow while decoding param types #1227

Merged
merged 11 commits into from
Jan 10, 2024
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> {
iqdecay marked this conversation as resolved.
Show resolved Hide resolved
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
Loading