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: ABI decoder crashing on certain inputs #1130

Merged
merged 21 commits into from
Oct 17, 2023
Merged
Show file tree
Hide file tree
Changes from 9 commits
Commits
Show all changes
21 commits
Select commit Hold shift + click to select a range
a261104
fix: handle division by zero
Br1ght0ne Sep 5, 2023
a113596
fix: handle multiplication with overflow
Br1ght0ne Sep 5, 2023
c25bd99
fix: handle Results throughout decoder, add regressions to tests
Br1ght0ne Sep 5, 2023
22a6818
Merge branch 'master' into oleksii/abi-decoder-crashes
Br1ght0ne Sep 11, 2023
a6d65db
Merge branch 'master' into oleksii/abi-decoder-crashes
Br1ght0ne Sep 25, 2023
8953322
fix: add DebugWithDepth to Debug-format nested types
Br1ght0ne Sep 25, 2023
bfeaf61
Merge branch 'master' into oleksii/abi-decoder-crashes
Br1ght0ne Sep 27, 2023
065a9d8
Merge branch 'master' into oleksii/abi-decoder-crashes
Br1ght0ne Sep 27, 2023
c75cb89
Merge branch 'master' into oleksii/abi-decoder-crashes
hal3e Sep 29, 2023
306b69e
Merge branch 'master' into oleksii/abi-decoder-crashes
segfault-magnet Oct 4, 2023
de2c049
Merge branch 'master' into oleksii/abi-decoder-crashes
hal3e Oct 5, 2023
7a128b2
Apply suggestions from code review
Br1ght0ne Oct 5, 2023
13381c4
Apply rest of the suggestions, renaming, cargo fmt
Br1ght0ne Oct 5, 2023
8b3dab1
Merge branch 'master' into oleksii/abi-decoder-crashes
hal3e Oct 6, 2023
a7a9acc
Merge branch 'master' into oleksii/abi-decoder-crashes
Br1ght0ne Oct 6, 2023
3da2abc
Move `DebugWithDepth` to `param_types.rs`
Br1ght0ne Oct 6, 2023
b0ba5b4
Merge branch 'master' into oleksii/abi-decoder-crashes
hal3e Oct 9, 2023
5891c94
Merge branch 'master' into oleksii/abi-decoder-crashes
Br1ght0ne Oct 16, 2023
3742a4e
Use full type names in test messages
Br1ght0ne Oct 16, 2023
66e577e
Re-wrap multiplication of `bytes_to_skip`
Br1ght0ne Oct 16, 2023
1daf0a9
Merge branch 'master' into oleksii/abi-decoder-crashes
Br1ght0ne Oct 16, 2023
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
92 changes: 92 additions & 0 deletions packages/fuels-core/src/codec/abi_decoder.rs
Original file line number Diff line number Diff line change
Expand Up @@ -88,6 +88,7 @@ mod tests {
constants::WORD_SIZE,
types::{enum_variants::EnumVariants, errors::Error, StaticStringToken},
};
use ParamType::*;

#[test]
fn decode_int() -> Result<()> {
Expand Down Expand Up @@ -510,6 +511,97 @@ mod tests {
Ok(())
}

#[test]
pub fn division_by_zero() {
let result = ABIDecoder::default().decode(&Vector(Box::new(Array(Box::new(U16), 0))), &[]);
assert!(matches!(result, Err(Error::InvalidType(_))));
Br1ght0ne marked this conversation as resolved.
Show resolved Hide resolved
}

#[test]
pub fn multiply_overflow_enum() {
let result = ABIDecoder::default().decode(
&Enum {
variants: EnumVariants {
param_types: vec![
Array(Box::new(Array(Box::new(RawSlice), 8)), 576469587185895432),
Br1ght0ne marked this conversation as resolved.
Show resolved Hide resolved
B256,
B256,
B256,
B256,
B256,
B256,
B256,
B256,
B256,
B256,
],
},
generics: vec![U16],
},
&[0, 8, 8, 8, 9, 8, 0, 8, 8, 8, 8, 8, 15, 8, 8, 8],
Br1ght0ne marked this conversation as resolved.
Show resolved Hide resolved
);
assert!(matches!(result, Err(Error::InvalidData(_))));
}

#[test]
pub fn multiply_overflow_vector() {
let result = ABIDecoder::default().decode(
&Vector(Box::new(Array(Box::new(Unit), 2308103648053880071))),
&[8, 8, 10, 7, 229, 8, 8, 8],
);
assert!(matches!(result, Err(Error::InvalidData(_))));
}

#[test]
pub fn multiply_overflow_arith() {
let mut typ: ParamType = U16;
for _ in 0..50 {
typ = Array(Box::new(typ), 8);
}
Br1ght0ne marked this conversation as resolved.
Show resolved Hide resolved
let result = ABIDecoder::default().decode(
&Enum {
variants: EnumVariants {
param_types: vec![typ],
},
generics: vec![U16],
},
&[0, 8, 8, 51, 51, 51, 51, 51, 51, 51, 3, 8, 15, 8, 8, 8],
);
assert!(matches!(result, Err(Error::InvalidData(_))));
}

#[test]
pub fn capacity_overflow() {
let result = ABIDecoder::default().decode(
&Array(
Box::new(Array(Box::new(Tuple(vec![])), 7638104972323651592)),
242,
),
&[13, 0, 1, 0, 0, 106, 242, 8],
);
dbg!(&result);
assert!(matches!(result, Err(Error::InvalidType(_))));
}

#[test]
pub fn stack_overflow() {
let mut typ: ParamType = U16;
for _ in 0..13500 {
typ = Vector(Box::new(typ));
}
Br1ght0ne marked this conversation as resolved.
Show resolved Hide resolved
let result = ABIDecoder::default().decode(&typ, &[8, 9, 9, 9, 9, 9, 9, 9]);
assert!(matches!(result, Err(Error::InvalidType(_))));
}

#[test]
pub fn capacity_maloc() {
let result = ABIDecoder::default().decode(
&Array(Box::new(U8), 72340198607880449),
&[8, 8, 7, 252, 201, 8, 8, 8],
);
assert!(matches!(result, Err(Error::InvalidData(_))));
}

#[test]
fn max_depth_surpassed() {
const MAX_DEPTH: usize = 2;
Expand Down
107 changes: 99 additions & 8 deletions packages/fuels-core/src/codec/abi_decoder/bounded_decoder.rs
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
use std::{convert::TryInto, str};
use std::{convert::TryInto, fmt, str};

use fuel_types::bytes::padded_len_usize;

Expand All @@ -19,6 +19,7 @@ use crate::{
pub(crate) struct BoundedDecoder {
depth_tracker: CounterWithLimit,
token_tracker: CounterWithLimit,
config: DecoderConfig,
}

const U128_BYTES_SIZE: usize = 2 * WORD_SIZE;
Expand All @@ -32,11 +33,12 @@ impl BoundedDecoder {
Self {
depth_tracker,
token_tracker,
config,
}
}

pub(crate) fn decode(&mut self, param_type: &ParamType, bytes: &[u8]) -> Result<Token> {
Self::is_type_decodable(param_type)?;
self.is_type_decodable(param_type)?;
Ok(self.decode_param(param_type, bytes)?.token)
}

Expand All @@ -46,18 +48,19 @@ impl BoundedDecoder {
bytes: &[u8],
) -> Result<Vec<Token>> {
for param_type in param_types {
Self::is_type_decodable(param_type)?;
self.is_type_decodable(param_type)?;
}
let (tokens, _) = self.decode_params(param_types, bytes)?;

Ok(tokens)
}

fn is_type_decodable(param_type: &ParamType) -> Result<()> {
fn is_type_decodable(&self, param_type: &ParamType) -> Result<()> {
if param_type.contains_nested_heap_types() {
Err(error!(
InvalidType,
"Type {param_type:?} contains nested heap types (`Vec` or `Bytes`), this is not supported."
"Type {:?} contains nested heap types (`Vec` or `Bytes`), this is not supported.",
DebugWithDepth::new(param_type, self.config.max_depth)
))
} else {
Ok(())
Expand Down Expand Up @@ -308,13 +311,14 @@ 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 = variants.compute_encoding_width_of_enum();
let enum_width = variants.compute_encoding_width_of_enum()?;

let discriminant = peek_u32(bytes)? as u8;
let selected_variant = variants.param_type_of_variant(discriminant)?;

let words_to_skip = enum_width - selected_variant.compute_encoding_width();
let enum_content_bytes = skip(bytes, words_to_skip * WORD_SIZE)?;
let words_to_skip = enum_width - selected_variant.compute_encoding_width()?;
let bytes_to_skip = words_to_skip .checked_mul(WORD_SIZE).ok_or_else(|| error!(InvalidData, "attempt to multiply words_to_skip ({words_to_skip:?}) by WORD_SIZE ({WORD_SIZE:?}) with overflow"))?;
let enum_content_bytes = skip(bytes, bytes_to_skip)?;
Br1ght0ne marked this conversation as resolved.
Show resolved Hide resolved
let result = self.decode_token_in_enum(enum_content_bytes, variants, selected_variant)?;

let selector = Box::new((discriminant, result.token, variants.clone()));
Expand Down Expand Up @@ -457,3 +461,90 @@ fn skip(slice: &[u8], num_bytes: usize) -> Result<&[u8]> {
Ok(&slice[num_bytes..])
}
}

/// Allows `Debug` formatting of arbitrary-depth nested `ParamTypes` by
/// omitting the details of inner types if max depth is exceeded.
pub(crate) struct DebugWithDepth<'a> {
param_type: &'a ParamType,
Br1ght0ne marked this conversation as resolved.
Show resolved Hide resolved
depth_left: usize,
}

impl<'a> DebugWithDepth<'a> {
pub(crate) fn new(param_type: &'a ParamType, depth_left: usize) -> Self {
Self {
param_type,
depth_left,
}
}

fn descend(&'a self, param_type: &'a ParamType) -> Self {
Self {
param_type,
depth_left: self.depth_left - 1,
}
}
}

impl<'a> fmt::Debug for DebugWithDepth<'a> {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
if self.depth_left == 0 {
return write!(f, "...");
}

match &self.param_type {
ParamType::Array(inner, size) => f
.debug_tuple("Array")
.field(&self.descend(inner))
.field(&size)
.finish(),
ParamType::Struct { fields, generics } => f
.debug_struct("Struct")
.field(
"fields",
&fields
.iter()
.map(|field| self.descend(field))
.collect::<Vec<_>>(),
)
.field(
"generics",
&generics
.iter()
.map(|generic| self.descend(generic))
.collect::<Vec<_>>(),
)
.finish(),
ParamType::Enum { variants, generics } => f
.debug_struct("Enum")
.field(
"variants",
&variants
.param_types
.iter()
.map(|variant| self.descend(variant))
.collect::<Vec<_>>(),
)
.field(
"generics",
&generics
.iter()
.map(|generic| self.descend(generic))
.collect::<Vec<_>>(),
)
.finish(),
ParamType::Tuple(inner) => f
.debug_tuple("Tuple")
.field(
&inner
.iter()
.map(|param_type| self.descend(param_type))
.collect::<Vec<_>>(),
)
.finish(),
ParamType::Vector(inner) => {
f.debug_tuple("Vector").field(&self.descend(inner)).finish()
}
_ => write!(f, "{:?}", self.param_type),
}
}
}
2 changes: 1 addition & 1 deletion packages/fuels-core/src/codec/abi_encoder.rs
Original file line number Diff line number Diff line change
Expand Up @@ -115,7 +115,7 @@ impl ABIEncoder {
// Enums that contain only Units as variants have only their discriminant encoded.
if !variants.only_units_inside() {
let variant_param_type = variants.param_type_of_variant(*discriminant)?;
let padding_amount = variants.compute_padding_amount(variant_param_type);
let padding_amount = variants.compute_padding_amount(variant_param_type)?;

encoded_enum.push(Data::Inline(vec![0; padding_amount]));

Expand Down
25 changes: 15 additions & 10 deletions packages/fuels-core/src/types/enum_variants.rs
Original file line number Diff line number Diff line change
Expand Up @@ -8,7 +8,7 @@ use crate::{

#[derive(Debug, Clone, PartialEq, Eq, serde::Serialize, serde::Deserialize)]
pub struct EnumVariants {
param_types: Vec<ParamType>,
pub param_types: Vec<ParamType>,
Br1ght0ne marked this conversation as resolved.
Show resolved Hide resolved
}

impl EnumVariants {
Expand Down Expand Up @@ -41,26 +41,31 @@ impl EnumVariants {
}

/// Calculates how many WORDs are needed to encode an enum.
pub fn compute_encoding_width_of_enum(&self) -> usize {
pub fn compute_encoding_width_of_enum(&self) -> Result<usize> {
if self.only_units_inside() {
return ENUM_DISCRIMINANT_WORD_WIDTH;
return Ok(ENUM_DISCRIMINANT_WORD_WIDTH);
}
self.param_types()
.iter()
.map(|p| p.compute_encoding_width())
.collect::<Result<Vec<_>>>()?
.iter()
.max()
.map(|width| width + ENUM_DISCRIMINANT_WORD_WIDTH)
.expect(
"Will never panic because EnumVariants must have at least one variant inside it!",
)
.ok_or_else(|| {
error!(
InvalidData,
"EnumVariants was empty, must have at least one variant"
)
})
}

/// 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(&self, variant_param_type: &ParamType) -> usize {
pub fn compute_padding_amount(&self, variant_param_type: &ParamType) -> Result<usize> {
let biggest_variant_width =
self.compute_encoding_width_of_enum() - ENUM_DISCRIMINANT_WORD_WIDTH;
let variant_width = variant_param_type.compute_encoding_width();
(biggest_variant_width - variant_width) * WORD_SIZE
self.compute_encoding_width_of_enum()? - ENUM_DISCRIMINANT_WORD_WIDTH;
let variant_width = variant_param_type.compute_encoding_width()?;
Ok((biggest_variant_width - variant_width) * WORD_SIZE)
}
}
Loading
Loading