Skip to content
This repository has been archived by the owner on Nov 15, 2023. It is now read-only.

[contracts] Add integrity checks by pallet hook #12993

Merged
merged 14 commits into from
Jan 18, 2023
Merged
24 changes: 13 additions & 11 deletions frame/contracts/src/exec.rs
Original file line number Diff line number Diff line change
Expand Up @@ -1336,7 +1336,18 @@ where

fn append_debug_buffer(&mut self, msg: &str) -> bool {
if let Some(buffer) = &mut self.debug_message {
let mut msg = msg.bytes();
let err_msg = scale_info::prelude::format!(
"Debug message too big (size={}) for debug buffer (bound={})",
msg.len(),
DebugBufferVec::<T>::bound(),
);

let mut msg = if msg.len() > DebugBufferVec::<T>::bound() {
err_msg.bytes()
} else {
msg.bytes()
};

let num_drain = {
let capacity = DebugBufferVec::<T>::bound().checked_sub(buffer.len()).expect(
"
Expand All @@ -1349,16 +1360,7 @@ where
msg.len().saturating_sub(capacity).min(buffer.len())
};
buffer.drain(0..num_drain);
buffer
.try_extend(&mut msg)
.map_err(|_| {
log::debug!(
target: "runtime::contracts",
"Debug message to big (size={}) for debug buffer (bound={})",
msg.len(), DebugBufferVec::<T>::bound(),
);
})
.ok();
buffer.try_extend(&mut msg).ok();
athei marked this conversation as resolved.
Show resolved Hide resolved
true
} else {
false
Expand Down
61 changes: 61 additions & 0 deletions frame/contracts/src/lib.rs
Original file line number Diff line number Diff line change
Expand Up @@ -123,6 +123,7 @@ use pallet_contracts_primitives::{
StorageDeposit,
};
use scale_info::TypeInfo;
use smallvec::Array;
athei marked this conversation as resolved.
Show resolved Hide resolved
use sp_runtime::traits::{Convert, Hash, Saturating, StaticLookup, TrailingZeroInput};
use sp_std::{fmt::Debug, marker::PhantomData, prelude::*};

Expand Down Expand Up @@ -272,6 +273,9 @@ pub mod pallet {
/// The allowed depth is `CallStack::size() + 1`.
/// Therefore a size of `0` means that a contract cannot use call or instantiate.
/// In other words only the origin called "root contract" is allowed to execute then.
///
/// This setting along with [`MaxCodeLen`](#associatedtype.MaxCodeLen) directly affects
/// memory usage of your runtime.
type CallStack: smallvec::Array<Item = Frame<Self>>;

/// The maximum number of contracts that can be pending for deletion.
Expand Down Expand Up @@ -323,6 +327,10 @@ pub mod pallet {
/// The maximum length of a contract code in bytes. This limit applies to the instrumented
/// version of the code. Therefore `instantiate_with_code` can fail even when supplying
/// a wasm binary below this maximum size.
///
/// The value should be chosen carefully taking into the account the overall memory limit
/// your runtime has, as well as the [maximum allowed callstack
/// depth](#associatedtype.CallStack). Look into the `integrity_test()` for some insights.
#[pallet::constant]
type MaxCodeLen: Get<u32>;

Expand Down Expand Up @@ -372,6 +380,59 @@ pub mod pallet {
T::WeightInfo::on_process_deletion_queue_batch()
}
}

fn integrity_test() {
// Total runtime memory is expected to have 1Gb upper limit
const MAX_RUNTIME_MEM: u32 = 1024 * 1024 * 1024;
// Memory limits for a single contract:
// Value stack size: 1Mb per contract, default defined in wasmi
const STACK_MAX_SIZE: u32 = 1024 * 1024;
// Heap limit is normally 16 mempages of 64kb each = 1Mb per contract
let heap_max_size = T::Schedule::get().limits.max_memory_size();
agryaznov marked this conversation as resolved.
Show resolved Hide resolved
let stack_height = T::CallStack::size() as u32;
agryaznov marked this conversation as resolved.
Show resolved Hide resolved
// Check that given configured `MaxCodeLen`, runtime heap memory limit can't be broken.
//
// In worst case, the decoded wasm contract code would be `x16` times larger than the
// encoded one. This is because even a single-byte wasm instruction has 16-byte size in
// wasmi. This gives us `MaxCodeLen*16` safety margin.
//
// Next, the pallet keeps both the original and instrumented wasm blobs for each
// contract, hence we add up `MaxCodeLen*2` more to the safety margin.
//
// Finally, the inefficiencies of the freeing-bump allocator
// being used in the client for the runtime memory allocations, could lead to possible
// memory allocations for contract code grow up to `x4` times in some extreme cases,
// wich gives us total multiplier of `18*4` for `MaxCodeLen`.
//
// That being said, for every contract executed in runtime, at least `MaxCodeLen*18*4`
// memory should be avaiable. Note that maximum allowed heap memory and stack size per
// each contract (stack frame) should also be counted.
//
// Finally, we allow 50% of the runtime memory to be utilized by the contracts call
// stack, keeping the rest for other facilities, such as PoV, etc.
//
// This gives us the following formula:
//
// `(MaxCodeLen * 18 * 4 + STACK_MAX_SIZE + heap_max_size) * stack_height <
// MAX_RUNTIME_MEM/2`
//
// Hence the upper limit for the `MaxCodeLen` can be defined as follows:
let code_len_limit = MAX_RUNTIME_MEM
.saturating_div(2)
.saturating_div(stack_height)
agryaznov marked this conversation as resolved.
Show resolved Hide resolved
.saturating_sub(heap_max_size)
agryaznov marked this conversation as resolved.
Show resolved Hide resolved
.saturating_sub(STACK_MAX_SIZE)
.saturating_div(18 * 4);

assert!(
athei marked this conversation as resolved.
Show resolved Hide resolved
T::MaxCodeLen::get() < code_len_limit,
"Given `CallStack` height {:?}, `MaxCodeLen` should be set less than {:?} \
(current value is {:?}), to avoid possible runtime oom issues.",
stack_height,
code_len_limit,
T::MaxCodeLen::get(),
);
}
}

#[pallet::call]
Expand Down