From eddcb1ff99c00f0d43da653bd6ebd2d2fed5061d Mon Sep 17 00:00:00 2001 From: Hernando Castano Date: Sat, 18 Feb 2023 18:41:16 -0800 Subject: [PATCH 1/7] Add feature to disable global memory allocator --- crates/allocator/Cargo.toml | 1 + crates/allocator/src/lib.rs | 2 +- crates/env/Cargo.toml | 4 ++++ crates/env/src/lib.rs | 2 +- crates/ink/Cargo.toml | 3 +++ 5 files changed, 10 insertions(+), 2 deletions(-) diff --git a/crates/allocator/Cargo.toml b/crates/allocator/Cargo.toml index 93c760e4eb0..f137a68ee64 100644 --- a/crates/allocator/Cargo.toml +++ b/crates/allocator/Cargo.toml @@ -25,3 +25,4 @@ quickcheck_macros = "1" default = ["std"] std = [] ink-fuzz-tests = ["std"] +no-allocator = [] diff --git a/crates/allocator/src/lib.rs b/crates/allocator/src/lib.rs index cdb759376a0..28ed5a631ad 100644 --- a/crates/allocator/src/lib.rs +++ b/crates/allocator/src/lib.rs @@ -25,7 +25,7 @@ #![cfg_attr(not(feature = "std"), no_std)] #![cfg_attr(not(feature = "std"), feature(alloc_error_handler))] -#[cfg(not(feature = "std"))] +#[cfg(not(any(feature = "std", feature = "no-allocator")))] #[global_allocator] static mut ALLOC: bump::BumpAllocator = bump::BumpAllocator {}; diff --git a/crates/env/Cargo.toml b/crates/env/Cargo.toml index d5785848d73..09f7bc727fd 100644 --- a/crates/env/Cargo.toml +++ b/crates/env/Cargo.toml @@ -70,5 +70,9 @@ std = [ "sha3", "blake2", ] + # Enable contract debug messages via `debug_print!` and `debug_println!`. ink-debug = [] + +# Disable the ink! provided global memory allocator. +no-allocator = ["ink_allocator/no-allocator"] diff --git a/crates/env/src/lib.rs b/crates/env/src/lib.rs index 2c56eba77fe..e181868e8c5 100644 --- a/crates/env/src/lib.rs +++ b/crates/env/src/lib.rs @@ -65,7 +65,7 @@ fn panic(info: &core::panic::PanicInfo) -> ! { // This extern crate definition is required since otherwise rustc // is not recognizing its allocator and panic handler definitions. -#[cfg(not(feature = "std"))] +#[cfg(not(any(feature = "std", feature = "no-allocator")))] extern crate ink_allocator; mod api; diff --git a/crates/ink/Cargo.toml b/crates/ink/Cargo.toml index 900ffc0b08b..d4c11ce9db0 100644 --- a/crates/ink/Cargo.toml +++ b/crates/ink/Cargo.toml @@ -50,3 +50,6 @@ ink-debug = [ "ink_env/ink-debug", ] show-codegen-docs = [] + +# Disable the ink! provided global memory allocator. +no-allocator = ["ink_env/no-allocator"] From c7a3203c170e1950ab8446867a90cee3b79dac0a Mon Sep 17 00:00:00 2001 From: Hernando Castano Date: Sat, 18 Feb 2023 18:44:21 -0800 Subject: [PATCH 2/7] Add example of how to use a different allocator --- examples/custom_allocator/.gitignore | 9 ++ examples/custom_allocator/Cargo.toml | 33 +++++ examples/custom_allocator/lib.rs | 182 +++++++++++++++++++++++++++ 3 files changed, 224 insertions(+) create mode 100755 examples/custom_allocator/.gitignore create mode 100755 examples/custom_allocator/Cargo.toml create mode 100755 examples/custom_allocator/lib.rs diff --git a/examples/custom_allocator/.gitignore b/examples/custom_allocator/.gitignore new file mode 100755 index 00000000000..8de8f877e47 --- /dev/null +++ b/examples/custom_allocator/.gitignore @@ -0,0 +1,9 @@ +# Ignore build artifacts from the local tests sub-crate. +/target/ + +# Ignore backup files creates by cargo fmt. +**/*.rs.bk + +# Remove Cargo.lock when creating an executable, leave it for libraries +# More information here http://doc.crates.io/guide.html#cargotoml-vs-cargolock +Cargo.lock diff --git a/examples/custom_allocator/Cargo.toml b/examples/custom_allocator/Cargo.toml new file mode 100755 index 00000000000..9612237795d --- /dev/null +++ b/examples/custom_allocator/Cargo.toml @@ -0,0 +1,33 @@ +[package] +name = "custom_allocator" +version = "4.0.0" +authors = ["Parity Technologies "] +edition = "2021" +publish = false + +[dependencies] +# We're going to use a different allocator than the one provided by ink!. To do that we +# first need to disable the included memory allocator. +ink = { path = "../../crates/ink", default-features = false, features = ["no-allocator"] } + +# This is going to be our new global memory allocator. +dlmalloc = {version = "0.2", default-features = false, features = ["global"] } + +scale = { package = "parity-scale-codec", version = "3", default-features = false, features = ["derive"] } +scale-info = { version = "2.3", default-features = false, features = ["derive"], optional = true } + +[dev-dependencies] +ink_e2e = { path = "../../crates/e2e" } + +[lib] +path = "lib.rs" + +[features] +default = ["std"] +std = [ + "ink/std", + "scale/std", + "scale-info/std", +] +ink-as-dependency = [] +e2e-tests = [] diff --git a/examples/custom_allocator/lib.rs b/examples/custom_allocator/lib.rs new file mode 100755 index 00000000000..081596dc1f6 --- /dev/null +++ b/examples/custom_allocator/lib.rs @@ -0,0 +1,182 @@ +//! # Custom Allocator +//! +//! This example demonstrates how to opt-out of the ink! provided global memory allocator. +//! +//! We will use [`dlmalloc`](https://github.com/alexcrichton/dlmalloc-rs) instead. +//! +//! ## Warning! +//! +//! We **do not** recommend you opt-out of the provided allocator for production contract +//! deployments! +//! +//! ## Why Change the Allocator? +//! +//! The default memory allocator was designed to have a tiny size footprint, and made some +//! compromises to achieve that, e.g it does not free/deallocate memory +//! +//! You may have a use case where you want to deallocate memory, or allocate it using a different +//! strategy. +//! +//! Providing your own allocator lets you choose the right tradeoffs for your use case. + +#![cfg_attr(not(feature = "std"), no_std)] +// Since we opted out of the default allocator we must also bring our own out-of-memory (OOM) +// handler. The Rust compiler doesn't let us do this unless we add this unstable/nightly feature. +#![cfg_attr(not(feature = "std"), feature(alloc_error_handler))] + +// Here we set `dlmalloc` to be the global memory allocator. +// +// The [`GlobalAlloc`](https://doc.rust-lang.org/std/alloc/trait.GlobalAlloc.html) trait is +// important to understand if you're swapping our your allocator. +#[cfg(not(feature = "std"))] +#[global_allocator] +static ALLOC: dlmalloc::GlobalDlmalloc = dlmalloc::GlobalDlmalloc; + +// As mentioned earlier, we need to provide our own OOM handler. +// +// We don't try and handle this and opt to abort contract execution instead. +#[cfg(not(feature = "std"))] +#[alloc_error_handler] +fn oom(_: core::alloc::Layout) -> ! { + core::arch::wasm32::unreachable() +} + +#[ink::contract] +mod custom_allocator { + + /// Defines the storage of your contract. + /// Add new fields to the below struct in order + /// to add new static storage fields to your contract. + #[ink(storage)] + pub struct CustomAllocator { + /// Stores a single `bool` value on the storage. + value: bool, + } + + impl CustomAllocator { + /// Constructor that initializes the `bool` value to the given `init_value`. + #[ink(constructor)] + pub fn new(init_value: bool) -> Self { + Self { value: init_value } + } + + /// Constructor that initializes the `bool` value to `false`. + /// + /// Constructors can delegate to other constructors. + #[ink(constructor)] + pub fn default() -> Self { + Self::new(Default::default()) + } + + /// A message that can be called on instantiated contracts. + /// This one flips the value of the stored `bool` from `true` + /// to `false` and vice versa. + #[ink(message)] + pub fn flip(&mut self) { + self.value = !self.value; + } + + /// Simply returns the current value of our `bool`. + #[ink(message)] + pub fn get(&self) -> bool { + self.value + } + } + + /// Unit tests in Rust are normally defined within such a `#[cfg(test)]` + /// module and test functions are marked with a `#[test]` attribute. + /// The below code is technically just normal Rust code. + #[cfg(test)] + mod tests { + /// Imports all the definitions from the outer scope so we can use them here. + use super::*; + + /// We test if the default constructor does its job. + #[ink::test] + fn default_works() { + let custom_allocator = CustomAllocator::default(); + assert_eq!(custom_allocator.get(), false); + } + + /// We test a simple use case of our contract. + #[ink::test] + fn it_works() { + let mut custom_allocator = CustomAllocator::new(false); + assert_eq!(custom_allocator.get(), false); + custom_allocator.flip(); + assert_eq!(custom_allocator.get(), true); + } + } + + /// This is how you'd write end-to-end (E2E) or integration tests for ink! contracts. + /// + /// When running these you need to make sure that you: + /// - Compile the tests with the `e2e-tests` feature flag enabled (`--features e2e-tests`) + /// - Are running a Substrate node which contains `pallet-contracts` in the background + #[cfg(all(test, feature = "e2e-tests"))] + mod e2e_tests { + /// Imports all the definitions from the outer scope so we can use them here. + use super::*; + + /// A helper function used for calling contract messages. + use ink_e2e::build_message; + + /// The End-to-End test `Result` type. + type E2EResult = std::result::Result>; + + /// We test that we can upload and instantiate the contract using its default constructor. + #[ink_e2e::test] + async fn default_works(mut client: ink_e2e::Client) -> E2EResult<()> { + // Given + let constructor = CustomAllocatorRef::default(); + + // When + let contract_account_id = client + .instantiate("custom_allocator", &ink_e2e::alice(), constructor, 0, None) + .await + .expect("instantiate failed") + .account_id; + + // Then + let get = build_message::(contract_account_id.clone()) + .call(|custom_allocator| custom_allocator.get()); + let get_result = client.call_dry_run(&ink_e2e::alice(), &get, 0, None).await; + assert!(matches!(get_result.return_value(), false)); + + Ok(()) + } + + /// We test that we can read and write a value from the on-chain contract contract. + #[ink_e2e::test] + async fn it_works(mut client: ink_e2e::Client) -> E2EResult<()> { + // Given + let constructor = CustomAllocatorRef::new(false); + let contract_account_id = client + .instantiate("custom_allocator", &ink_e2e::bob(), constructor, 0, None) + .await + .expect("instantiate failed") + .account_id; + + let get = build_message::(contract_account_id.clone()) + .call(|custom_allocator| custom_allocator.get()); + let get_result = client.call_dry_run(&ink_e2e::bob(), &get, 0, None).await; + assert!(matches!(get_result.return_value(), false)); + + // When + let flip = build_message::(contract_account_id.clone()) + .call(|custom_allocator| custom_allocator.flip()); + let _flip_result = client + .call(&ink_e2e::bob(), flip, 0, None) + .await + .expect("flip failed"); + + // Then + let get = build_message::(contract_account_id.clone()) + .call(|custom_allocator| custom_allocator.get()); + let get_result = client.call_dry_run(&ink_e2e::bob(), &get, 0, None).await; + assert!(matches!(get_result.return_value(), true)); + + Ok(()) + } + } +} From 88f06effa41984dbe0569d77a6c27cf7641791c8 Mon Sep 17 00:00:00 2001 From: Hernando Castano Date: Sat, 18 Feb 2023 18:59:35 -0800 Subject: [PATCH 3/7] Add concrete downsides to warning --- examples/custom_allocator/lib.rs | 6 ++++++ 1 file changed, 6 insertions(+) diff --git a/examples/custom_allocator/lib.rs b/examples/custom_allocator/lib.rs index 081596dc1f6..68f76c27cf5 100755 --- a/examples/custom_allocator/lib.rs +++ b/examples/custom_allocator/lib.rs @@ -9,6 +9,12 @@ //! We **do not** recommend you opt-out of the provided allocator for production contract //! deployments! //! +//! If you don't handle allocations correctly you can introduce security vulnerabilities to your +//! contracts. +//! +//! You may also introduce performance issues. This is because the code of your allocator will +//! be included in the final contract binary, potentially increasing gas usage significantly. +//! //! ## Why Change the Allocator? //! //! The default memory allocator was designed to have a tiny size footprint, and made some From 8631c14ae5e47e0a4db6a8deb6263393fc8dac11 Mon Sep 17 00:00:00 2001 From: Hernando Castano Date: Sat, 18 Feb 2023 18:59:59 -0800 Subject: [PATCH 4/7] Update example to use a `Vec` --- examples/custom_allocator/lib.rs | 36 +++++++++++++------------------- 1 file changed, 14 insertions(+), 22 deletions(-) diff --git a/examples/custom_allocator/lib.rs b/examples/custom_allocator/lib.rs index 68f76c27cf5..9d43419aab3 100755 --- a/examples/custom_allocator/lib.rs +++ b/examples/custom_allocator/lib.rs @@ -49,26 +49,30 @@ fn oom(_: core::alloc::Layout) -> ! { #[ink::contract] mod custom_allocator { + use ink::prelude::vec::Vec; - /// Defines the storage of your contract. - /// Add new fields to the below struct in order - /// to add new static storage fields to your contract. #[ink(storage)] pub struct CustomAllocator { /// Stores a single `bool` value on the storage. - value: bool, + /// + /// # Note + /// + /// We're using a `Vec` here as it allocates its elements onto the heap, as opposed to the + /// stack. This allows us to demonstrate that our new allocator actually works. + value: Vec, } impl CustomAllocator { /// Constructor that initializes the `bool` value to the given `init_value`. #[ink(constructor)] pub fn new(init_value: bool) -> Self { - Self { value: init_value } + let mut value = Vec::new(); + value.push(init_value); + + Self { value } } - /// Constructor that initializes the `bool` value to `false`. - /// - /// Constructors can delegate to other constructors. + /// Creates a new flipper smart contract initialized to `false`. #[ink(constructor)] pub fn default() -> Self { Self::new(Default::default()) @@ -79,22 +83,18 @@ mod custom_allocator { /// to `false` and vice versa. #[ink(message)] pub fn flip(&mut self) { - self.value = !self.value; + self.value[0] = !self.value[0]; } /// Simply returns the current value of our `bool`. #[ink(message)] pub fn get(&self) -> bool { - self.value + self.value[0] } } - /// Unit tests in Rust are normally defined within such a `#[cfg(test)]` - /// module and test functions are marked with a `#[test]` attribute. - /// The below code is technically just normal Rust code. #[cfg(test)] mod tests { - /// Imports all the definitions from the outer scope so we can use them here. use super::*; /// We test if the default constructor does its job. @@ -114,20 +114,12 @@ mod custom_allocator { } } - /// This is how you'd write end-to-end (E2E) or integration tests for ink! contracts. - /// - /// When running these you need to make sure that you: - /// - Compile the tests with the `e2e-tests` feature flag enabled (`--features e2e-tests`) - /// - Are running a Substrate node which contains `pallet-contracts` in the background #[cfg(all(test, feature = "e2e-tests"))] mod e2e_tests { - /// Imports all the definitions from the outer scope so we can use them here. use super::*; - /// A helper function used for calling contract messages. use ink_e2e::build_message; - /// The End-to-End test `Result` type. type E2EResult = std::result::Result>; /// We test that we can upload and instantiate the contract using its default constructor. From a3057038154f9fa54227d4deb37a174b4acfeaab Mon Sep 17 00:00:00 2001 From: Hernando Castano Date: Sat, 18 Feb 2023 19:15:05 -0800 Subject: [PATCH 5/7] Appease Clippy --- examples/custom_allocator/lib.rs | 20 ++++++++++---------- 1 file changed, 10 insertions(+), 10 deletions(-) diff --git a/examples/custom_allocator/lib.rs b/examples/custom_allocator/lib.rs index 9d43419aab3..1b3bde65380 100755 --- a/examples/custom_allocator/lib.rs +++ b/examples/custom_allocator/lib.rs @@ -49,7 +49,10 @@ fn oom(_: core::alloc::Layout) -> ! { #[ink::contract] mod custom_allocator { - use ink::prelude::vec::Vec; + use ink::prelude::{ + vec, + vec::Vec, + }; #[ink(storage)] pub struct CustomAllocator { @@ -66,10 +69,9 @@ mod custom_allocator { /// Constructor that initializes the `bool` value to the given `init_value`. #[ink(constructor)] pub fn new(init_value: bool) -> Self { - let mut value = Vec::new(); - value.push(init_value); - - Self { value } + Self { + value: vec![init_value], + } } /// Creates a new flipper smart contract initialized to `false`. @@ -97,20 +99,18 @@ mod custom_allocator { mod tests { use super::*; - /// We test if the default constructor does its job. #[ink::test] fn default_works() { let custom_allocator = CustomAllocator::default(); - assert_eq!(custom_allocator.get(), false); + assert!(!custom_allocator.get()); } - /// We test a simple use case of our contract. #[ink::test] fn it_works() { let mut custom_allocator = CustomAllocator::new(false); - assert_eq!(custom_allocator.get(), false); + assert!(!custom_allocator.get()); custom_allocator.flip(); - assert_eq!(custom_allocator.get(), true); + assert!(custom_allocator.get()); } } From 74dd77313a0d64cf4134675416cc828d3651a781 Mon Sep 17 00:00:00 2001 From: Hernando Castano Date: Mon, 20 Feb 2023 18:15:44 -0800 Subject: [PATCH 6/7] Add missing period MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Co-authored-by: Michael Müller --- examples/custom_allocator/lib.rs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/examples/custom_allocator/lib.rs b/examples/custom_allocator/lib.rs index 1b3bde65380..f30f6a781db 100755 --- a/examples/custom_allocator/lib.rs +++ b/examples/custom_allocator/lib.rs @@ -18,7 +18,7 @@ //! ## Why Change the Allocator? //! //! The default memory allocator was designed to have a tiny size footprint, and made some -//! compromises to achieve that, e.g it does not free/deallocate memory +//! compromises to achieve that, e.g it does not free/deallocate memory. //! //! You may have a use case where you want to deallocate memory, or allocate it using a different //! strategy. From ebbac253347d3525f625c513ea9dcf8e60444558 Mon Sep 17 00:00:00 2001 From: Hernando Castano Date: Mon, 20 Feb 2023 18:16:38 -0800 Subject: [PATCH 7/7] Move example to `integration-tests` folder --- {examples => integration-tests}/custom_allocator/.gitignore | 0 {examples => integration-tests}/custom_allocator/Cargo.toml | 0 {examples => integration-tests}/custom_allocator/lib.rs | 0 3 files changed, 0 insertions(+), 0 deletions(-) rename {examples => integration-tests}/custom_allocator/.gitignore (100%) rename {examples => integration-tests}/custom_allocator/Cargo.toml (100%) rename {examples => integration-tests}/custom_allocator/lib.rs (100%) diff --git a/examples/custom_allocator/.gitignore b/integration-tests/custom_allocator/.gitignore similarity index 100% rename from examples/custom_allocator/.gitignore rename to integration-tests/custom_allocator/.gitignore diff --git a/examples/custom_allocator/Cargo.toml b/integration-tests/custom_allocator/Cargo.toml similarity index 100% rename from examples/custom_allocator/Cargo.toml rename to integration-tests/custom_allocator/Cargo.toml diff --git a/examples/custom_allocator/lib.rs b/integration-tests/custom_allocator/lib.rs similarity index 100% rename from examples/custom_allocator/lib.rs rename to integration-tests/custom_allocator/lib.rs