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

Store stack and heap separately #697

Merged
merged 26 commits into from
Apr 5, 2024
Merged
Show file tree
Hide file tree
Changes from 22 commits
Commits
Show all changes
26 commits
Select commit Hold shift + click to select a range
6824e20
Simple renaming of the memory and using a struct to manage access to it
xgreenx Mar 12, 2024
dac643d
Fixed all tests
xgreenx Mar 12, 2024
87ff3b8
Merge branch 'master' into feature/growing-memory
xgreenx Mar 12, 2024
807f527
Make CI happy
xgreenx Mar 12, 2024
8397461
WIP: properly do vm-level panic on oob-memory-access
Dentosal Apr 2, 2024
cf093da
Clippy
Dentosal Apr 2, 2024
6d7cc2e
Remove debug prints
Dentosal Apr 3, 2024
1587782
Combine tests for S[RWC]WQ validation
Dentosal Apr 3, 2024
10fed83
Fix a old-stack and heap overlap bug
Dentosal Apr 3, 2024
7d71689
Correctly return error even if no new allocation is needed
Dentosal Apr 3, 2024
cfca387
Bugfix: disallow stack-to-heap crossing access properly
Dentosal Apr 3, 2024
226b7e5
Bugfix: allow sp==hp, and correctly error on overlap
Dentosal Apr 3, 2024
a887697
Fix old tests, add new ones
Dentosal Apr 3, 2024
505c710
Merge branch 'master' into feature/growing-memory
Dentosal Apr 3, 2024
1501d1b
Add changelog entry
Dentosal Apr 3, 2024
3f46977
Mention ALOC zeroing memory in the changelog
Dentosal Apr 3, 2024
e2da9c1
Keep track of the lowest $hp to disallow reads below it
Dentosal Apr 3, 2024
e333c98
Fix some no_std imports
Dentosal Apr 3, 2024
babffcf
Remove dbg!
Dentosal Apr 3, 2024
e22765a
Suppress clippy cast lint in tests
Dentosal Apr 3, 2024
3f082f4
Cleanup
Dentosal Apr 5, 2024
b054694
Restore some removed test cases
Dentosal Apr 5, 2024
d0cf49b
Small nits
xgreenx Apr 5, 2024
3baee80
Remove hp argument from grow_stack
Dentosal Apr 5, 2024
e8f5b45
Remove obsolte stack-size check
Dentosal Apr 5, 2024
ad312a8
Inline all MemoryRange::relative_to for clarity
Dentosal Apr 5, 2024
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
8 changes: 8 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,14 @@ and this project adheres to [Semantic Versioning](http://semver.org/).

## [Unreleased]

### Added

### Changed

#### Breaking

- [#697](https://github.com/FuelLabs/fuel-vm/pull/697): Changed the VM to internally use separate buffers for the stack and the heap to improve startup time. After this change, memory that was never part of the stack or the heap cannot be accessed, even for reading. Also, even if the whole memory is allocated, accesses spanning from the stack to the heap are not allowed. This PR also fixes a bug that required one-byte gap between the stack and the heap. Multiple errors have been changed to be more sensible ones, and sometimes the order of which error is returned has changed. `ALOC` opcode now zeroes the newly allocated memory.

## [Version 0.48.0]

### Added
Expand Down
5 changes: 5 additions & 0 deletions fuel-asm/src/panic_reason.rs
Original file line number Diff line number Diff line change
Expand Up @@ -132,6 +132,11 @@ enum_from! {
/// Caller of this internal context is also expected to be internal,
/// i.e. $fp->$fp must be non-zero.
ExpectedNestedCaller = 0x2e,
/// During memory growth, the stack overlapped with the heap
MemoryGrowthOverlap = 0x2f,
/// Attempting to read or write uninitialized memory.
/// Also occurs when boundary crosses from stack to heap.
UninitalizedMemoryAccess = 0x30,
}
}

Expand Down
3 changes: 2 additions & 1 deletion fuel-types/src/array_types.rs
Original file line number Diff line number Diff line change
Expand Up @@ -150,7 +150,8 @@ macro_rules! key_methods {
///
/// This function will panic if the length of `buf` is smaller than
/// `Self::LEN`.
pub fn from_bytes(bytes: &[u8]) -> Self {
#[wasm_bindgen(js_name = from_bytes)]
pub fn from_bytes_typescript(bytes: &[u8]) -> Self {
Self(bytes.try_into().expect(
format!("The size of the arrays it not {} size", $s).as_str(),
))
Expand Down
8 changes: 3 additions & 5 deletions fuel-vm/examples/external.rs
Original file line number Diff line number Diff line change
Expand Up @@ -38,7 +38,6 @@ use fuel_vm::{
Interpreter,
IntoChecked,
MemoryClient,
MemoryRange,
},
storage::MemoryStorage,
};
Expand All @@ -62,8 +61,7 @@ impl EcalHandler for FileReadEcal {
vm.gas_charge(b.saturating_add(1))?;

// Extract file path from vm memory
let r = MemoryRange::new(c, d)?;
let path = String::from_utf8_lossy(&vm.memory()[r.usizes()]);
let path = String::from_utf8_lossy(vm.memory().read(c, d)?);
let path = PathBuf::from(path.as_ref());

// Seek file to correct position
Expand All @@ -74,8 +72,8 @@ impl EcalHandler for FileReadEcal {

// Allocate the buffer in the vm memory and read directly from the file into it
vm.allocate(b)?;
let r = MemoryRange::new(vm.registers()[RegId::HP], b)?;
file.read(&mut vm.memory_mut()[r.usizes()])
let hp = vm.registers()[RegId::HP];
file.read(vm.memory_mut().write_noownerchecks(hp, b)?)
.map_err(|_| PanicReason::EcalError)?;

Ok(())
Expand Down
15 changes: 7 additions & 8 deletions fuel-vm/src/backtrace.rs
Original file line number Diff line number Diff line change
Expand Up @@ -17,9 +17,9 @@ use crate::{
};
use derivative::Derivative;

use crate::interpreter::Memory;
use fuel_tx::ScriptExecutionResult;
use fuel_types::{
fmt_truncated_hex,
ContractId,
Word,
};
Expand All @@ -31,8 +31,7 @@ pub struct Backtrace {
call_stack: Vec<CallFrame>,
contract: ContractId,
registers: [Word; VM_REGISTER_COUNT],
#[derivative(Debug(format_with = "fmt_truncated_hex::<16>"))]
memory: Vec<u8>,
memory: Memory,
result: ScriptExecutionResult,
initial_balances: InitialBalances,
}
Expand All @@ -46,8 +45,8 @@ impl Backtrace {
result: ScriptExecutionResult,
) -> Self {
let call_stack = vm.call_stack().to_owned();
let contract = vm.internal_contract_or_default();
let memory = vm.memory().to_owned();
let contract = vm.internal_contract().unwrap_or_default();
let memory = vm.memory().clone();
let initial_balances = vm.initial_balances().clone();
let mut registers = [0; VM_REGISTER_COUNT];

Expand Down Expand Up @@ -79,8 +78,8 @@ impl Backtrace {
}

/// Memory of the VM when the error occurred.
pub fn memory(&self) -> &[u8] {
self.memory.as_slice()
pub fn memory(&self) -> &Memory {
&self.memory
}

/// [`ScriptExecutionResult`] of the error that caused this backtrace.
Expand All @@ -100,7 +99,7 @@ impl Backtrace {
Vec<CallFrame>,
ContractId,
[Word; VM_REGISTER_COUNT],
Vec<u8>,
Memory,
ScriptExecutionResult,
InitialBalances,
) {
Expand Down
102 changes: 0 additions & 102 deletions fuel-vm/src/constraints.rs
Original file line number Diff line number Diff line change
@@ -1,97 +1,9 @@
//! Types to help constrain inputs to functions to only what is used.
use core::ops::{
Deref,
DerefMut,
};

use fuel_asm::{
PanicReason,
Word,
};
use fuel_types::ContractId;

#[cfg(test)]
use fuel_types::canonical::Deserialize;

use crate::{
consts::MEM_SIZE,
prelude::MemoryRange,
};

pub mod reg_key;

/// A range of memory that has been checked that it fits into the VM memory.
#[derive(Clone)]
// TODO: Replace `LEN` constant with a generic object that implements some trait that
// knows the static size of the generic.
pub struct CheckedMemConstLen<const LEN: usize>(MemoryRange);

/// A range of memory that has been checked that it fits into the VM memory.
/// This range can be used to read a value of type `T` from memory.
#[derive(Clone)]
// TODO: Merge this type with `CheckedMemConstLen`.
pub struct CheckedMemValue<T>(MemoryRange, core::marker::PhantomData<T>);

impl<T> CheckedMemValue<T> {
/// Create a new const sized memory range.
pub fn new<const SIZE: usize>(address: Word) -> Result<Self, PanicReason> {
Ok(Self(
MemoryRange::new_const::<_, SIZE>(address)?,
core::marker::PhantomData,
))
}

/// Try to read a value of type `T` from memory.
pub fn try_from(self, memory: &[u8; MEM_SIZE]) -> Result<T, PanicReason>
where
T: for<'a> TryFrom<&'a [u8]>,
PanicReason: for<'a> From<<T as TryFrom<&'a [u8]>>::Error>,
{
Ok(T::try_from(&memory[self.0.usizes()])?)
}

/// The start of the range.
pub fn start(&self) -> usize {
self.0.start
}

/// The end of the range.
pub fn end(&self) -> usize {
self.0.end
}

#[cfg(test)]
/// Inspect a value of type `T` from memory.
pub fn inspect(self, memory: &[u8; MEM_SIZE]) -> T
where
T: Deserialize,
{
T::from_bytes(&memory[self.0.usizes()])
.expect("Inspect failed; invalid value for type")
}
}

impl<const LEN: usize> CheckedMemConstLen<LEN> {
/// Create a new const sized memory range.
pub fn new(address: Word) -> Result<Self, PanicReason> {
Ok(Self(MemoryRange::new_const::<_, LEN>(address)?))
}

/// Get the memory slice for this range.
pub fn read(self, memory: &[u8; MEM_SIZE]) -> &[u8; LEN] {
(&memory[self.0.usizes()]).try_into().expect(
"This is always correct as the address and LEN are checked on construction.",
)
}

/// Get the mutable memory slice for this range.
pub fn write(self, memory: &mut [u8; MEM_SIZE]) -> &mut [u8; LEN] {
(&mut memory[self.0.usizes()]).try_into().expect(
"This is always correct as the address and LEN are checked on construction.",
)
}
}

/// Location of an instructing collected during runtime
#[derive(Debug, Clone, Copy, PartialEq, Eq, PartialOrd, Ord, Hash)]
pub struct InstructionLocation {
Expand All @@ -100,17 +12,3 @@ pub struct InstructionLocation {
/// Offset from the IS register
pub offset: u64,
}

impl<const LEN: usize> Deref for CheckedMemConstLen<LEN> {
type Target = MemoryRange;

fn deref(&self) -> &Self::Target {
&self.0
}
}

impl<const LEN: usize> DerefMut for CheckedMemConstLen<LEN> {
fn deref_mut(&mut self) -> &mut Self::Target {
&mut self.0
}
}
5 changes: 0 additions & 5 deletions fuel-vm/src/consts.rs
Original file line number Diff line number Diff line change
Expand Up @@ -17,11 +17,6 @@ pub const VM_REGISTER_SYSTEM_COUNT: usize = 16;
/// The number of writable registers.
pub const VM_REGISTER_PROGRAM_COUNT: usize = VM_REGISTER_COUNT - VM_REGISTER_SYSTEM_COUNT;

/// Max amount of nested call contexts.
/// Used to protect against stack overflows, since the CALL
/// instruction is currently implemented using recursion.
pub const VM_MAX_NESTED_CALLS: usize = 64;

// MEMORY TYPES

/// Length of a word, in bytes
Expand Down
17 changes: 9 additions & 8 deletions fuel-vm/src/interpreter.rs
Original file line number Diff line number Diff line change
Expand Up @@ -75,7 +75,10 @@ pub use ecal::{
EcalHandler,
PredicateErrorEcal,
};
pub use memory::MemoryRange;
pub use memory::{
Memory,
MemoryRange,
};

use crate::checked_transaction::{
CreateCheckedMetadata,
Expand All @@ -86,8 +89,6 @@ use crate::checked_transaction::{
ScriptCheckedMetadata,
};

use self::memory::Memory;

#[cfg(feature = "test-helpers")]
pub use self::receipts::ReceiptsCtx;

Expand All @@ -109,7 +110,7 @@ pub struct NotSupportedEcal;
#[derive(Debug, Clone)]
pub struct Interpreter<S, Tx = (), Ecal = NotSupportedEcal> {
registers: [Word; VM_REGISTER_COUNT],
memory: Memory<MEM_SIZE>,
memory: Memory,
frames: Vec<CallFrame>,
receipts: ReceiptsCtx,
tx: Tx,
Expand Down Expand Up @@ -199,13 +200,13 @@ pub(crate) enum PanicContext {

impl<S, Tx, Ecal> Interpreter<S, Tx, Ecal> {
/// Returns the current state of the VM memory
pub fn memory(&self) -> &[u8] {
self.memory.as_slice()
pub fn memory(&self) -> &Memory {
&self.memory
}

/// Returns mutable access to the vm memory
pub fn memory_mut(&mut self) -> &mut [u8] {
self.memory.as_mut()
pub fn memory_mut(&mut self) -> &mut Memory {
&mut self.memory
}

/// Returns the current state of the registers
Expand Down
Loading
Loading