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

Return correct PanicReason on memory-related panics #511

Merged
merged 19 commits into from
Aug 6, 2023
Merged
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
2 changes: 2 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -23,6 +23,8 @@ and this project adheres to [Semantic Versioning](http://semver.org/).

- [#529](https://github.com/FuelLabs/fuel-vm/pull/529) [#534](https://github.com/FuelLabs/fuel-vm/pull/534): Enforcing async WASM initialization for all NPM wrapper packages.

- [#511](https://github.com/FuelLabs/fuel-vm/pull/511): Changes multiple panic reasons to be more accurate, and internally refactors instruction fetch logic to be less error-prone.

#### Breaking

- [#527](https://github.com/FuelLabs/fuel-vm/pull/527): The balances are empty during predicate estimation/verification.
Expand Down
14 changes: 0 additions & 14 deletions fuel-asm/src/encoding_tests.rs
Original file line number Diff line number Diff line change
Expand Up @@ -44,27 +44,13 @@ fn opcode() {

assert_eq!(instructions, instructions_from_bytes.unwrap());

let pairs = bytes.chunks(8).map(|chunk| {
let mut arr = [0; core::mem::size_of::<Word>()];
arr.copy_from_slice(chunk);
Word::from_be_bytes(arr)
});

let instructions_from_words: Vec<Instruction> = pairs
.into_iter()
.flat_map(raw_instructions_from_word)
.map(|raw| Instruction::try_from(raw).unwrap())
.collect();

#[cfg(feature = "serde")]
for ins in &instructions {
let ins_ser = bincode::serialize(ins).expect("Failed to serialize opcode");
let ins_de: Instruction =
bincode::deserialize(&ins_ser).expect("Failed to serialize opcode");
assert_eq!(ins, &ins_de);
}

assert_eq!(instructions, instructions_from_words);
}

#[test]
Expand Down
7 changes: 0 additions & 7 deletions fuel-asm/src/lib.rs
Original file line number Diff line number Diff line change
Expand Up @@ -883,13 +883,6 @@ impl core::iter::FromIterator<Instruction> for Vec<u32> {
}
}

/// Produce two raw instructions from a word's hi and lo parts.
pub fn raw_instructions_from_word(word: Word) -> [RawInstruction; 2] {
let hi = (word >> 32) as RawInstruction;
let lo = word as RawInstruction;
[hi, lo]
}

/// Given an iterator yielding bytes, produces an iterator yielding `Instruction`s.
///
/// This function assumes each consecutive 4 bytes aligns with an instruction.
Expand Down
4 changes: 2 additions & 2 deletions fuel-asm/src/panic_reason.rs
Original file line number Diff line number Diff line change
Expand Up @@ -74,8 +74,8 @@ enum_from! {
InvalidImmediateValue = 0x13,
/// The provided transaction input is not of type `Coin`.
ExpectedCoinInput = 0x14,
/// The requested memory access exceeds the limits of the interpreter.
MaxMemoryAccess = 0x15,
/// This entry is no longer used, and can be repurposed.
Unused0x15 = 0x15,
/// Two segments of the interpreter memory should not intersect for write operations.
MemoryWriteOverlap = 0x16,
/// The requested contract is not listed in the transaction inputs.
Expand Down
1 change: 1 addition & 0 deletions fuel-vm/Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -30,6 +30,7 @@ primitive-types = { version = "0.12", default-features = false }
rand = { version = "0.8", optional = true }
serde = { version = "1.0", features = ["derive", "rc"], optional = true }
sha3 = "0.10"
static_assertions = "1.1"
strum = { version = "0.24", features = ["derive"], optional = true }
tai64 = "4.0"
thiserror = "1.0"
Expand Down
10 changes: 1 addition & 9 deletions fuel-vm/src/consts.rs
Original file line number Diff line number Diff line change
Expand Up @@ -35,15 +35,7 @@ pub const VM_MAX_RAM: u64 = 1024 * 1024 * FUEL_MAX_MEMORY_SIZE;
/// Size of the VM memory, in bytes.
pub const MEM_SIZE: usize = VM_MAX_RAM as usize;

/// Maximum memory access size, in bytes.
pub const MEM_MAX_ACCESS_SIZE: u64 = VM_MAX_RAM;

/// Tighter of the two bounds for VM_MAX_RAM and usize::MAX
pub const MIN_VM_MAX_RAM_USIZE_MAX: u64 = if VM_MAX_RAM < usize::MAX as u64 {
VM_MAX_RAM
} else {
usize::MAX as u64
};
static_assertions::const_assert!(VM_MAX_RAM < usize::MAX as u64);

// no limits to heap for now.

Expand Down
164 changes: 60 additions & 104 deletions fuel-vm/src/interpreter/blockchain.rs
Original file line number Diff line number Diff line change
Expand Up @@ -19,6 +19,7 @@ use super::{
AppendReceipt,
},
memory::{
read_bytes,
try_mem_write,
try_zeroize,
OwnershipRegisters,
Expand All @@ -29,13 +30,7 @@ use super::{
RuntimeBalances,
};
use crate::{
arith,
arith::{
add_usize,
checked_add_usize,
checked_add_word,
checked_sub_word,
},
arith::checked_add_word,
call::CallFrame,
constraints::{
reg_key::*,
Expand Down Expand Up @@ -83,10 +78,7 @@ use fuel_types::{
Word,
};

use std::{
borrow::Borrow,
ops::Range,
};
use std::borrow::Borrow;

#[cfg(test)]
mod code_tests;
Expand Down Expand Up @@ -487,39 +479,28 @@ impl<'vm, S, I> LoadContractCodeCtx<'vm, S, I> {
return Err(PanicReason::ExpectedUnallocatedStack.into())
}

let contract_id = a as usize;
let contract_id_end = checked_add_usize(ContractId::LEN, contract_id)?;
// Validate arguments
let contract_id = ContractId::from(read_bytes(self.memory, a)?);
let contract_offset = b as usize;
let length = bytes::padded_len_usize(c as usize);
let dst_range = MemoryRange::new(ssp, length)?;

let memory_offset = ssp as usize;
let memory_offset_end = checked_add_usize(memory_offset, length)?;
if dst_range.end >= *self.hp as usize {
// Would make stack and heap overlap
return Err(PanicReason::MemoryOverflow.into())
}

// Validate arguments
if memory_offset_end >= *self.hp as usize
|| contract_id_end as Word > VM_MAX_RAM
|| length > MEM_MAX_ACCESS_SIZE as usize
|| length > self.contract_max_size as usize
{
if length > self.contract_max_size as usize {
return Err(PanicReason::MemoryOverflow.into())
}

// Clear memory
self.memory[memory_offset..memory_offset_end].fill(0);
self.memory[dst_range.usizes()].fill(0);

// Fetch the contract id
let contract_id: &[u8; ContractId::LEN] = &self.memory
[contract_id..contract_id_end]
.try_into()
.expect("This can't fail, because we checked the bounds above.");

// Safety: Memory bounds are checked and consistent
let contract_id = ContractId::from_bytes_ref(contract_id);

self.input_contracts.check(contract_id)?;
self.input_contracts.check(&contract_id)?;
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

maybe we should perform this validation before filling the memory with zeros? It gives us a way to bail out of execution earlier before performing a potentially large memset.


// fetch the storage contract
let contract = super::contract::contract(self.storage, contract_id)?;
let contract = super::contract::contract(self.storage, &contract_id)?;
let contract = contract.as_ref().as_ref();

if contract_offset > contract.len() {
Expand All @@ -531,10 +512,12 @@ impl<'vm, S, I> LoadContractCodeCtx<'vm, S, I> {

let code = &contract[..len];

let dst_range = dst_range.truncated(code.len());

let memory = self
.memory
.get_mut(memory_offset..arith::checked_add_usize(memory_offset, len)?)
.ok_or(PanicReason::MemoryOverflow)?;
.get_mut(dst_range.usizes())
.expect("Checked memory access");

// perform the code copy
memory.copy_from_slice(code);
Expand All @@ -550,23 +533,25 @@ impl<'vm, S, I> LoadContractCodeCtx<'vm, S, I> {

// update frame pointer, if we have a stack frame (e.g. fp > 0)
if fp > 0 {
let fp_code_size = add_usize(fp, CallFrame::code_size_offset());
let fp_code_size_end = add_usize(fp_code_size, WORD_SIZE);

if fp_code_size_end > self.memory.len() {
Err(PanicReason::MemoryOverflow)?;
}
let fp_code_size = MemoryRange::new_overflowing_op(
usize::overflowing_add,
fp,
CallFrame::code_size_offset(),
WORD_SIZE,
)?;

let length = Word::from_be_bytes(
self.memory[fp_code_size..fp_code_size_end]
let old_code_size = Word::from_be_bytes(
self.memory[fp_code_size.usizes()]
.try_into()
.expect("`fp_code_size_end` is `WORD_SIZE`"),
)
.checked_add(length as Word)
.ok_or(PanicReason::MemoryOverflow)?;
);

let new_code_size = old_code_size
.checked_add(length as Word)
.ok_or(PanicReason::MemoryOverflow)?;

self.memory[fp_code_size..fp_code_size_end]
.copy_from_slice(&length.to_be_bytes());
self.memory[fp_code_size.usizes()]
.copy_from_slice(&new_code_size.to_be_bytes());
}

inc_pc(self.pc)
Expand Down Expand Up @@ -674,28 +659,25 @@ impl<'vm, S, I> CodeCopyCtx<'vm, S, I> {
S: InterpreterStorage,
{
let contract = CheckedMemConstLen::<{ ContractId::LEN }>::new(b)?;
let cd = checked_add_word(c, d)?;

if d > MEM_MAX_ACCESS_SIZE
|| a > checked_sub_word(VM_MAX_RAM, d)?
|| cd > VM_MAX_RAM
{
return Err(PanicReason::MemoryOverflow.into())
}

let (a, c, d) = (a as usize, c as usize, d as usize);
let cd = cd as usize;
MemoryRange::new(a, d)?;
xgreenx marked this conversation as resolved.
Show resolved Hide resolved
let c_range = MemoryRange::new(c, d)?;

let contract = ContractId::from_bytes_ref(contract.read(self.memory));

self.input_contracts.check(contract)?;

let contract = super::contract::contract(self.storage, contract)?.into_owned();

if contract.as_ref().len() < d {
if contract.as_ref().len() < d as usize {
try_zeroize(a, d, self.owner, self.memory)?;
} else {
try_mem_write(a, &contract.as_ref()[c..cd], self.owner, self.memory)?;
try_mem_write(
a,
&contract.as_ref()[c_range.usizes()],
self.owner,
self.memory,
)?;
}

inc_pc(self.pc)
Expand Down Expand Up @@ -759,13 +741,9 @@ impl<'vm, S, I: Iterator<Item = &'vm ContractId>> CodeRootCtx<'vm, S, I> {
where
S: InterpreterStorage,
{
let ax = checked_add_word(a, Bytes32::LEN as Word)?;
MemoryRange::new(a, Bytes32::LEN)?;
xgreenx marked this conversation as resolved.
Show resolved Hide resolved
let contract_id = CheckedMemConstLen::<{ ContractId::LEN }>::new(b)?;

if ax > VM_MAX_RAM {
return Err(PanicReason::MemoryOverflow.into())
}

let contract_id = ContractId::from_bytes_ref(contract_id.read(self.memory));

self.input_contracts.check(contract_id)?;
Expand Down Expand Up @@ -954,10 +932,6 @@ where
self.recipient_mem_address,
)?;

if self.msg_data_len > MEM_MAX_ACCESS_SIZE {
return Err(RuntimeError::Recoverable(PanicReason::MemoryOverflow))
}

if self.msg_data_len > self.max_message_data_length {
return Err(RuntimeError::Recoverable(PanicReason::MessageDataTooLong))
}
Expand Down Expand Up @@ -1012,11 +986,9 @@ where
}

struct StateReadQWord {
/// The destination memory address is
/// stored in this range of memory.
destination_address_memory_range: Range<usize>,
/// The starting storage key location is stored
/// in this range of memory.
/// The destination memory address is stored in this range of memory.
destination_address_memory_range: MemoryRange,
/// The starting storage key location is stored in this range of memory.
origin_key_memory_range: CheckedMemConstLen<{ Bytes32::LEN }>,
/// Number of slots to read.
num_slots: Word,
Expand All @@ -1029,28 +1001,16 @@ impl StateReadQWord {
num_slots: Word,
ownership_registers: OwnershipRegisters,
) -> Result<Self, RuntimeError> {
let mem_range = MemoryRange::new(
let destination_address_memory_range = MemoryRange::new(
destination_memory_address,
(Bytes32::LEN as Word).saturating_mul(num_slots),
)?;
if !ownership_registers.has_ownership_range(&mem_range) {
return Err(PanicReason::MemoryOwnership.into())
}
if ownership_registers.context.is_external() {
return Err(PanicReason::ExpectedInternalContext.into())
}
let dest_end = checked_add_word(
destination_memory_address,
Bytes32::LEN.saturating_mul(num_slots as usize) as Word,
)?;
ownership_registers.verify_ownership(&destination_address_memory_range)?;
ownership_registers.verify_internal_context()?;
let origin_key_memory_range =
CheckedMemConstLen::<{ Bytes32::LEN }>::new(origin_key_memory_address)?;
if dest_end > VM_MAX_RAM {
return Err(PanicReason::MemoryOverflow.into())
}
Ok(Self {
destination_address_memory_range: (destination_memory_address as usize)
..(dest_end as usize),
destination_address_memory_range,
origin_key_memory_range,
num_slots,
})
Expand Down Expand Up @@ -1083,20 +1043,18 @@ fn state_read_qword(

*result_register = all_set as Word;

memory[input.destination_address_memory_range].copy_from_slice(&result);
memory[input.destination_address_memory_range.usizes()].copy_from_slice(&result);

inc_pc(pc)?;

Ok(())
}

struct StateWriteQWord {
/// The starting storage key location is stored
/// in this range of memory.
/// The starting storage key location is stored in this range of memory.
starting_storage_key_memory_range: CheckedMemConstLen<{ Bytes32::LEN }>,
/// The source data memory address is
/// stored in this range of memory.
source_address_memory_range: Range<usize>,
/// The source data memory address is stored in this range of memory.
source_address_memory_range: MemoryRange,
}

impl StateWriteQWord {
Expand All @@ -1105,20 +1063,18 @@ impl StateWriteQWord {
source_memory_address: Word,
num_slots: Word,
) -> Result<Self, RuntimeError> {
let source_end = checked_add_word(
let source_address_memory_range = MemoryRange::new(
source_memory_address,
Bytes32::LEN.saturating_mul(num_slots as usize) as Word,
(Bytes32::LEN as Word).saturating_mul(num_slots),
)?;

let starting_storage_key_memory_range =
CheckedMemConstLen::<{ Bytes32::LEN }>::new(
starting_storage_key_memory_address,
)?;
if source_end > VM_MAX_RAM {
return Err(PanicReason::MemoryOverflow.into())
}

Ok(Self {
source_address_memory_range: (source_memory_address as usize)
..(source_end as usize),
source_address_memory_range,
starting_storage_key_memory_range,
})
}
Expand All @@ -1135,7 +1091,7 @@ fn state_write_qword(
let destination_key =
Bytes32::from_bytes_ref(input.starting_storage_key_memory_range.read(memory));

let values: Vec<_> = memory[input.source_address_memory_range]
let values: Vec<_> = memory[input.source_address_memory_range.usizes()]
.chunks_exact(Bytes32::LEN)
.flat_map(|chunk| Some(Bytes32::from(<[u8; 32]>::try_from(chunk).ok()?)))
.collect();
Expand Down
Loading