├── .github ├── CODEOWNERS ├── workflows │ ├── wasm.yml │ ├── rust.yml │ └── release.yml └── CONTRIBUTING.md ├── tests ├── evm.wasm ├── rust.wasm ├── wat.wasm ├── classes.wasm ├── motoko.wasm ├── wat.wasm.gz ├── ok │ ├── wat-limit.wasm │ ├── evm-redirect.wasm │ ├── motoko-limit.wasm │ ├── rust-limit.wasm │ ├── rust-shrink.wasm │ ├── wat-shrink.wasm │ ├── classes-limit.wasm │ ├── classes-shrink.wasm │ ├── motoko-shrink.wasm │ ├── wat-instrument.wasm │ ├── classes-optimize.wasm │ ├── classes-redirect.wasm │ ├── motoko-instrument.wasm │ ├── rust-instrument.wasm │ ├── classes-nop-redirect.wasm │ ├── classes-optimize-names.wasm │ ├── motoko-gc-instrument.wasm │ ├── rust-region-instrument.wasm │ └── motoko-region-instrument.wasm ├── rust-region.wasm ├── motoko-region.wasm ├── evm.mo ├── README.md ├── deployable.ic-repl.sh └── tests.rs ├── rust-toolchain.toml ├── .gitignore ├── src ├── lib.rs ├── shrink.rs ├── metadata.rs ├── check_endpoints │ ├── candid │ │ └── mod.rs │ └── mod.rs ├── optimize.rs ├── info.rs ├── bin │ └── main.rs ├── utils.rs ├── limit_resource.rs └── instrumentation.rs ├── NOTICE ├── Cargo.toml ├── CHANGELOG.md ├── README.md └── LICENSE /.github/CODEOWNERS: -------------------------------------------------------------------------------- 1 | * @dfinity/dx 2 | -------------------------------------------------------------------------------- /tests/evm.wasm: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/dfinity/ic-wasm/HEAD/tests/evm.wasm -------------------------------------------------------------------------------- /tests/rust.wasm: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/dfinity/ic-wasm/HEAD/tests/rust.wasm -------------------------------------------------------------------------------- /tests/wat.wasm: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/dfinity/ic-wasm/HEAD/tests/wat.wasm -------------------------------------------------------------------------------- /tests/classes.wasm: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/dfinity/ic-wasm/HEAD/tests/classes.wasm -------------------------------------------------------------------------------- /tests/motoko.wasm: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/dfinity/ic-wasm/HEAD/tests/motoko.wasm -------------------------------------------------------------------------------- /tests/wat.wasm.gz: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/dfinity/ic-wasm/HEAD/tests/wat.wasm.gz -------------------------------------------------------------------------------- /rust-toolchain.toml: -------------------------------------------------------------------------------- 1 | [toolchain] 2 | channel = "stable" 3 | components = ["rustfmt", "clippy"] 4 | -------------------------------------------------------------------------------- /tests/ok/wat-limit.wasm: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/dfinity/ic-wasm/HEAD/tests/ok/wat-limit.wasm -------------------------------------------------------------------------------- /tests/rust-region.wasm: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/dfinity/ic-wasm/HEAD/tests/rust-region.wasm -------------------------------------------------------------------------------- /tests/motoko-region.wasm: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/dfinity/ic-wasm/HEAD/tests/motoko-region.wasm -------------------------------------------------------------------------------- /tests/ok/evm-redirect.wasm: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/dfinity/ic-wasm/HEAD/tests/ok/evm-redirect.wasm -------------------------------------------------------------------------------- /tests/ok/motoko-limit.wasm: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/dfinity/ic-wasm/HEAD/tests/ok/motoko-limit.wasm -------------------------------------------------------------------------------- /tests/ok/rust-limit.wasm: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/dfinity/ic-wasm/HEAD/tests/ok/rust-limit.wasm -------------------------------------------------------------------------------- /tests/ok/rust-shrink.wasm: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/dfinity/ic-wasm/HEAD/tests/ok/rust-shrink.wasm -------------------------------------------------------------------------------- /tests/ok/wat-shrink.wasm: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/dfinity/ic-wasm/HEAD/tests/ok/wat-shrink.wasm -------------------------------------------------------------------------------- /tests/ok/classes-limit.wasm: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/dfinity/ic-wasm/HEAD/tests/ok/classes-limit.wasm -------------------------------------------------------------------------------- /tests/ok/classes-shrink.wasm: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/dfinity/ic-wasm/HEAD/tests/ok/classes-shrink.wasm -------------------------------------------------------------------------------- /tests/ok/motoko-shrink.wasm: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/dfinity/ic-wasm/HEAD/tests/ok/motoko-shrink.wasm -------------------------------------------------------------------------------- /tests/ok/wat-instrument.wasm: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/dfinity/ic-wasm/HEAD/tests/ok/wat-instrument.wasm -------------------------------------------------------------------------------- /tests/ok/classes-optimize.wasm: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/dfinity/ic-wasm/HEAD/tests/ok/classes-optimize.wasm -------------------------------------------------------------------------------- /tests/ok/classes-redirect.wasm: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/dfinity/ic-wasm/HEAD/tests/ok/classes-redirect.wasm -------------------------------------------------------------------------------- /tests/ok/motoko-instrument.wasm: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/dfinity/ic-wasm/HEAD/tests/ok/motoko-instrument.wasm -------------------------------------------------------------------------------- /tests/ok/rust-instrument.wasm: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/dfinity/ic-wasm/HEAD/tests/ok/rust-instrument.wasm -------------------------------------------------------------------------------- /tests/ok/classes-nop-redirect.wasm: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/dfinity/ic-wasm/HEAD/tests/ok/classes-nop-redirect.wasm -------------------------------------------------------------------------------- /tests/ok/classes-optimize-names.wasm: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/dfinity/ic-wasm/HEAD/tests/ok/classes-optimize-names.wasm -------------------------------------------------------------------------------- /tests/ok/motoko-gc-instrument.wasm: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/dfinity/ic-wasm/HEAD/tests/ok/motoko-gc-instrument.wasm -------------------------------------------------------------------------------- /tests/ok/rust-region-instrument.wasm: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/dfinity/ic-wasm/HEAD/tests/ok/rust-region-instrument.wasm -------------------------------------------------------------------------------- /tests/ok/motoko-region-instrument.wasm: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/dfinity/ic-wasm/HEAD/tests/ok/motoko-region-instrument.wasm -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | .DS_Store 2 | 3 | /.vscode/ 4 | /.idea/ 5 | 6 | # will have compiled files and executables 7 | /target/ 8 | 9 | /tests/out.wasm 10 | 11 | # These are backup files generated by rustfmt 12 | **/*.rs.bk 13 | 14 | -------------------------------------------------------------------------------- /tests/evm.mo: -------------------------------------------------------------------------------- 1 | actor { 2 | let evm : actor { request: shared () -> async (); requestCost: shared () -> async () } = actor "7hfb6-caaaa-aaaar-qadga-cai"; 3 | let non_evm : actor { request: shared () -> async (); requestCost: shared () -> async () } = actor "cpmcr-yeaaa-aaaaa-qaala-cai"; 4 | public func requestCost() : async () { 5 | await evm.requestCost(); 6 | }; 7 | public func request() : async () { 8 | await evm.request(); 9 | }; 10 | public func non_evm_request() : async () { 11 | await non_evm.request(); 12 | }; 13 | } 14 | 15 | -------------------------------------------------------------------------------- /src/lib.rs: -------------------------------------------------------------------------------- 1 | #[cfg(feature = "check-endpoints")] 2 | pub mod check_endpoints; 3 | pub mod info; 4 | pub mod instrumentation; 5 | pub mod limit_resource; 6 | pub mod metadata; 7 | #[cfg(feature = "wasm-opt")] 8 | pub mod optimize; 9 | pub mod shrink; 10 | pub mod utils; 11 | 12 | #[derive(Debug, thiserror::Error)] 13 | pub enum Error { 14 | #[error("Failed on IO.")] 15 | IO(#[from] std::io::Error), 16 | 17 | #[error("Could not parse the data as WASM module. {0}")] 18 | WasmParse(String), 19 | 20 | #[error("{0}")] 21 | MetadataNotFound(String), 22 | } 23 | -------------------------------------------------------------------------------- /NOTICE: -------------------------------------------------------------------------------- 1 | Copyright 2020 DFINITY Stiftung 2 | 3 | Licensed under the Apache License, Version 2.0 (the "License"); you may not use 4 | this file except in compliance with the License. You may obtain a copy of the 5 | License at 6 | 7 | http://www.apache.org/licenses/LICENSE-2.0 8 | 9 | Unless required by applicable law or agreed to in writing, software distributed 10 | under the License is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR 11 | CONDITIONS OF ANY KIND, either express or implied. See the License for the 12 | specific language governing permissions and limitations under the License. 13 | -------------------------------------------------------------------------------- /src/shrink.rs: -------------------------------------------------------------------------------- 1 | use crate::utils::*; 2 | use walrus::*; 3 | 4 | pub fn shrink(m: &mut Module) { 5 | if is_motoko_canister(m) { 6 | let ids = get_motoko_wasm_data_sections(m); 7 | for (id, mut module) in ids.into_iter() { 8 | shrink(&mut module); 9 | let blob = encode_module_as_data_section(module); 10 | let original_len = m.data.get(id).value.len(); 11 | if blob.len() < original_len { 12 | m.data.get_mut(id).value = blob; 13 | } 14 | } 15 | } 16 | let to_remove: Vec<_> = m 17 | .customs 18 | .iter() 19 | .filter(|(_, section)| !section.name().starts_with("icp:")) 20 | .map(|(id, _)| id) 21 | .collect(); 22 | for s in to_remove { 23 | m.customs.delete(s); 24 | } 25 | passes::gc::run(m); 26 | } 27 | -------------------------------------------------------------------------------- /.github/workflows/wasm.yml: -------------------------------------------------------------------------------- 1 | name: Wasm 2 | on: 3 | push: 4 | branches: 5 | - main 6 | pull_request: 7 | 8 | jobs: 9 | deploy: 10 | runs-on: ubuntu-latest 11 | env: 12 | DFX_VERSION: 0.23.0 13 | IC_REPL_VERSION: 0.7.5 14 | steps: 15 | - uses: actions/checkout@v4 16 | - name: Install dfx 17 | uses: dfinity/setup-dfx@main 18 | with: 19 | dfx-version: "${{ env.DFX_VERSION }}" 20 | - name: Install dependencies 21 | run: | 22 | wget https://github.com/chenyan2002/ic-repl/releases/download/$IC_REPL_VERSION/ic-repl-linux64 23 | cp ./ic-repl-linux64 /usr/local/bin/ic-repl 24 | chmod a+x /usr/local/bin/ic-repl 25 | rustup target add wasm32-unknown-unknown 26 | - name: Check compilation without default features for wasm32-unknown-unknown target (needed for motoko-playground) 27 | run: | 28 | cargo build --no-default-features --target wasm32-unknown-unknown 29 | - name: Start replica 30 | run: | 31 | echo "{}" > dfx.json 32 | dfx cache install 33 | dfx start --background 34 | - name: Test 35 | run: | 36 | ic-repl tests/deployable.ic-repl.sh -v 37 | - name: stop dfx 38 | run: | 39 | echo "dfx stop" 40 | dfx stop 41 | -------------------------------------------------------------------------------- /.github/workflows/rust.yml: -------------------------------------------------------------------------------- 1 | name: Rust 2 | on: 3 | push: 4 | branches: 5 | - main 6 | pull_request: 7 | 8 | jobs: 9 | rust: 10 | runs-on: ubuntu-latest 11 | steps: 12 | - uses: actions/checkout@v4 13 | - name: Cache cargo build 14 | uses: actions/cache@v4 15 | with: 16 | path: | 17 | ~/.cargo/registry 18 | ~/.cargo/git 19 | target 20 | key: ${{ runner.os }}-cargo-${{ hashFiles('**/Cargo.lock', '**/*.rs', '**/Cargo.toml') }} 21 | - name: Cache cargo binaries 22 | uses: actions/cache@v4 23 | with: 24 | path: ~/.cargo/bin 25 | key: cargo-bin-${{ runner.os }}-v1 26 | - name: Install cargo-sort if needed 27 | run: | 28 | if ! command -v cargo-sort &> /dev/null; then 29 | echo "Installing cargo-sort..." 30 | cargo install cargo-sort 31 | else 32 | echo "cargo-sort already installed from cache." 33 | fi 34 | - name: cargo sort 35 | run: cargo sort --check 36 | - name: Build 37 | run: cargo build --features serde 38 | - name: Run tests 39 | run: cargo test --features serde -- --test-threads=1 40 | - name: fmt 41 | run: cargo fmt -v -- --check 42 | - name: lint 43 | run: cargo clippy --tests -- -D clippy::all 44 | - name: doc 45 | run: cargo doc 46 | -------------------------------------------------------------------------------- /src/metadata.rs: -------------------------------------------------------------------------------- 1 | use walrus::{IdsToIndices, Module, RawCustomSection}; 2 | 3 | #[derive(Clone, Copy)] 4 | pub enum Kind { 5 | Public, 6 | Private, 7 | } 8 | 9 | /// Add or overwrite a metadata section 10 | pub fn add_metadata(m: &mut Module, visibility: Kind, name: &str, data: Vec) { 11 | let name = match visibility { 12 | Kind::Public => "icp:public ".to_owned(), 13 | Kind::Private => "icp:private ".to_owned(), 14 | } + name; 15 | drop(m.customs.remove_raw(&name)); 16 | let custom_section = RawCustomSection { name, data }; 17 | m.customs.add(custom_section); 18 | } 19 | 20 | /// Remove a metadata section 21 | pub fn remove_metadata(m: &mut Module, name: &str) { 22 | let public = "icp:public ".to_owned() + name; 23 | let private = "icp:private ".to_owned() + name; 24 | m.customs.remove_raw(&public); 25 | m.customs.remove_raw(&private); 26 | } 27 | 28 | /// List current metadata sections 29 | pub fn list_metadata(m: &Module) -> Vec<&str> { 30 | m.customs 31 | .iter() 32 | .map(|section| section.1.name()) 33 | .filter(|name| name.starts_with("icp:")) 34 | .collect() 35 | } 36 | 37 | /// Get the content of metadata 38 | pub fn get_metadata<'a>(m: &'a Module, name: &'a str) -> Option> { 39 | let public = "icp:public ".to_owned() + name; 40 | let private = "icp:private ".to_owned() + name; 41 | m.customs 42 | .iter() 43 | .find(|(_, section)| section.name() == public || section.name() == private) 44 | .map(|(_, section)| section.data(&IdsToIndices::default())) 45 | } 46 | -------------------------------------------------------------------------------- /Cargo.toml: -------------------------------------------------------------------------------- 1 | [package] 2 | name = "ic-wasm" 3 | version = "0.9.9" 4 | authors = ["DFINITY Stiftung"] 5 | edition = "2021" 6 | description = "A library for performing Wasm transformations specific to canisters running on the Internet Computer" 7 | license = "Apache-2.0" 8 | readme = "README.md" 9 | documentation = "https://docs.rs/ic-wasm" 10 | repository = "https://github.com/dfinity/ic-wasm" 11 | categories = ["wasm"] 12 | keywords = ["internet-computer", "canister", "dfinity"] 13 | include = ["src", "Cargo.toml", "LICENSE", "README.md"] 14 | 15 | [package.metadata.binstall] 16 | pkg-url = "{ repo }/releases/download/{ version }/{ name }-{ target }{ archive-suffix }" 17 | pkg-fmt = "tgz" # archive-suffix = .tar.gz 18 | 19 | [[bin]] 20 | name = "ic-wasm" 21 | path = "src/bin/main.rs" 22 | required-features = ["exe"] 23 | 24 | [features] 25 | check-endpoints = ["dep:anyhow", "dep:candid_parser", "dep:parse-display", "dep:serde_json"] 26 | default = ["check-endpoints", "exe", "wasm-opt"] 27 | exe = ["dep:anyhow", "dep:clap", "dep:serde"] 28 | wasm-opt = ["dep:wasm-opt", "dep:tempfile"] 29 | serde = ["dep:serde", "dep:serde_json"] 30 | 31 | [dependencies] 32 | anyhow = { version = "1.0.34", optional = true } 33 | candid = "0.10" 34 | candid_parser = { version = "0.2.1", optional = true } 35 | clap = { version = "4.1", features = ["derive", "cargo"], optional = true } 36 | libflate = "2.0" 37 | parse-display = { version = "0.10.0", optional = true } 38 | rustc-demangle = "0.1" 39 | serde = { version = "1.0", optional = true } 40 | serde_json = { version = "1.0", optional = true } 41 | tempfile = { version = "3.5.0", optional = true } 42 | thiserror = "1.0.35" 43 | # Major version bump of walrus should result in a major version bump of ic-wasm. 44 | # Because we expose walrus types in ic-wasm public API. 45 | walrus = "0.22.0" 46 | wasm-opt = { version = "0.116.0", optional = true } 47 | wasmparser = "0.223.0" 48 | 49 | [dev-dependencies] 50 | assert_cmd = "2" 51 | -------------------------------------------------------------------------------- /tests/README.md: -------------------------------------------------------------------------------- 1 | To regenerate the Wasm file: 2 | 3 | * `wat.wasm` is generated from the [dfinity/examples/wasm/counter](https://github.com/dfinity/examples/tree/master/wasm/counter) 4 | * `motoko.wasm` is generated from [dfinity/examples/motoko/counter](https://github.com/dfinity/examples/tree/master/motoko/counter) 5 | * `motoko-region.wasm` is generated by adding the following in the Motoko counter code: 6 | ```motoko 7 | import Region "mo:base/Region"; 8 | actor { 9 | stable let profiling = do { 10 | let r = Region.new(); 11 | ignore Region.grow(r, 32); 12 | r; 13 | }; 14 | ... 15 | } 16 | ``` 17 | * `rust.wasm` is generated from [dfinity/examples/rust/counter](https://github.com/dfinity/examples/tree/master/rust/counter), and add the following pre/post-upgrade hooks: 18 | ```rust 19 | use ic_stable_structures::{ 20 | memory_manager::{MemoryId, MemoryManager}, 21 | writer::Writer, 22 | DefaultMemoryImpl, Memory, 23 | }; 24 | thread_local! { 25 | static MEMORY_MANAGER: RefCell> = 26 | RefCell::new(MemoryManager::init(DefaultMemoryImpl::default())); 27 | } 28 | 29 | const UPGRADES: MemoryId = MemoryId::new(0); 30 | 31 | #[ic_cdk::pre_upgrade] 32 | fn pre_upgrade() { 33 | let bytes = COUNTER.with(|counter| Encode!(counter).unwrap()); 34 | let len = bytes.len() as u32; 35 | let mut memory = MEMORY_MANAGER.with(|m| m.borrow().get(UPGRADES)); 36 | let mut writer = Writer::new(&mut memory, 0); 37 | writer.write(&len.to_le_bytes()).unwrap(); 38 | writer.write(&bytes).unwrap(); 39 | } 40 | #[ic_cdk::post_upgrade] 41 | fn post_upgrade() { 42 | let memory = MEMORY_MANAGER.with(|m| m.borrow().get(UPGRADES)); 43 | let mut len_bytes = [0; 4]; 44 | memory.read(0, &mut len_bytes); 45 | let len = u32::from_le_bytes(len_bytes) as usize; 46 | let mut bytes = vec![0; len]; 47 | memory.read(4, &mut bytes); 48 | let value = Decode!(&bytes, Nat).unwrap(); 49 | COUNTER.with(|cell| *cell.borrow_mut() = value); 50 | } 51 | ``` 52 | * `rust-region.wasm` is generated by adding the following in the Rust counter code: 53 | ```rust 54 | const PROFILING: MemoryId = MemoryId::new(100); 55 | #[ic_cdk::init] 56 | fn init() { 57 | let memory = MEMORY_MANAGER.with(|m| m.borrow().get(PROFILING)); 58 | memory.grow(32); 59 | ... 60 | } 61 | ``` 62 | * `classes.wasm` is generated from [dfinity/examples/motoko/classes](https://github.com/dfinity/examples/blob/master/motoko/classes/src/map/Map.mo) 63 | -------------------------------------------------------------------------------- /CHANGELOG.md: -------------------------------------------------------------------------------- 1 | # Changelog 2 | All notable changes to this project will be documented in this file. 3 | 4 | The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/), 5 | and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html). 6 | 7 | ## [unreleased] 8 | 9 | ## [0.9.9] - 2025-11-18 10 | 11 | * Add support for comments in optional hidden endpoint file for `check-endpoints` command. 12 | 13 | ## [0.9.8] - 2025-10-01 14 | 15 | * Fix: `check-endpoints` now correctly handles all exported functions, not just those prefixed with `canister_`. 16 | 17 | ## [0.9.7] - 2025-09-26 18 | 19 | * Add `check-endpoints` command to `ic-wasm`. 20 | 21 | ## [0.9.6] - 2025-09-17 22 | 23 | * Add option to filter cycles transfer. 24 | 25 | ## [0.9.5] - 2025-01-28 26 | 27 | * Fix compilation without default features. 28 | 29 | ## [0.9.4] - 2025-01-27 30 | 31 | * Allow `sign_with_schnorr` in `limit_resource`. 32 | 33 | ## [0.9.3] - 2025-01-10 34 | 35 | * Validate the manipulated module before emitting it and give a warning if that fails. 36 | 37 | ## [0.9.2] - 2025-01-09 38 | 39 | * Fix: limit_resource works with wasm64. 40 | 41 | ## [0.9.1] - 2024-11-18 42 | 43 | * Add redirect for evm canister. 44 | 45 | ## [0.9.0] - 2024-10-01 46 | 47 | * (breaking) Use 64bit API for stable memory in profiling and bump walrus 48 | 49 | ## [0.8.6] - 2024-09-24 50 | 51 | * Add data section check when limiting Wasm heap memory. 52 | 53 | ## [0.8.5] - 2024-09-05 54 | 55 | * Fix http_request redirect. 56 | 57 | ## [0.8.4] - 2024-09-05 58 | 59 | * Add `keep_name_section` option to the `metadata` subcommand. 60 | 61 | ## [0.8.3] - 2024-08-27 62 | 63 | * Fix memory id in limit_resource. 64 | 65 | ## [0.8.2] - 2024-08-27 66 | 67 | * Add support for limiting Wasm heap memory. 68 | 69 | ## [0.8.1] - 2024-08-20 70 | 71 | * Redirect canister snapshot calls in `limit_resource` module. 72 | 73 | ## [0.8.0] - 2024-07-09 74 | 75 | * Upgrade dependency walrus. 76 | * This enables ic-wasm to process memory64 Wasm modules. 77 | 78 | ## [0.7.3] - 2024-06-27 79 | 80 | * Enable WebAssembly SIMD in `optimize` subcommand. 81 | 82 | ## [0.7.2] - 2024-04-06 83 | 84 | * Bump dependency for libflate 85 | 86 | ## [0.7.1] - 2024-03-20 87 | 88 | * `utils::parse_wasm` and `utils::parse_wasm_file` can take both gzipped and original Wasm inputs. 89 | 90 | ## [0.3.0 -- 0.7.0] 91 | 92 | - Profiling 93 | + Support profiling stable memory 94 | + `__get_profiling` supports streaming data download 95 | + Trace only a subset of functions 96 | + Add `__toggle_entry` function 97 | + Use the new cost model for metering 98 | - Add optimize command to use wasm-opt 99 | - Added support for JSON output to `ic-wasm info`. 100 | 101 | ## [0.2.0] - 2022-09-21 102 | 103 | ### Changed 104 | - Decoupled library API with walrus (#19) 105 | -------------------------------------------------------------------------------- /src/check_endpoints/candid/mod.rs: -------------------------------------------------------------------------------- 1 | use crate::check_endpoints::CanisterEndpoint; 2 | use anyhow::{format_err, Error, Result}; 3 | use candid::types::{FuncMode, Function, TypeInner}; 4 | use candid_parser::utils::CandidSource; 5 | use std::borrow::Cow; 6 | use std::collections::BTreeSet; 7 | use std::path::Path; 8 | use std::str; 9 | use walrus::{IdsToIndices, Module}; 10 | 11 | pub struct CandidParser<'a> { 12 | source: CandidSource<'a>, 13 | } 14 | 15 | impl<'a> From> for CandidParser<'a> { 16 | fn from(source: CandidSource<'a>) -> Self { 17 | Self { source } 18 | } 19 | } 20 | 21 | impl<'a> CandidParser<'a> { 22 | pub fn from_candid_file(path: &'a Path) -> Self { 23 | Self::from(CandidSource::File(path)) 24 | } 25 | 26 | pub fn try_from_wasm(module: &'a Module) -> Result> { 27 | module 28 | .customs 29 | .iter() 30 | .find(|(_, s)| s.name() == "icp:public candid:service") 31 | .map(|(_, s)| { 32 | let bytes = match s.data(&IdsToIndices::default()) { 33 | Cow::Borrowed(bytes) => bytes, 34 | Cow::Owned(_) => unreachable!(), 35 | }; 36 | let candid = str::from_utf8(bytes).map_err(|e| { 37 | format_err!("Cannot interpret WASM custom section as text: {e:?}") 38 | })?; 39 | Ok(Self::from(CandidSource::Text(candid))) 40 | }) 41 | .transpose() 42 | } 43 | } 44 | 45 | impl CandidParser<'_> { 46 | pub fn parse(&self) -> Result> { 47 | let (_, top_level) = self.source.load()?; 48 | 49 | let maybe_actor = match top_level { 50 | Some(actor) => actor, 51 | None => return Err(Error::msg("Top-level definition not found")), 52 | }; 53 | 54 | let service = match maybe_actor.as_ref() { 55 | TypeInner::Class(_, class) => class, 56 | service => service, 57 | }; 58 | 59 | let functions = match service { 60 | TypeInner::Service(functions) => functions, 61 | _ => return Err(Error::msg("Top-level service definition not found")), 62 | }; 63 | 64 | let endpoints = functions 65 | .iter() 66 | .filter_map(|(name, maybe_function)| { 67 | if let TypeInner::Func(Function { modes, .. }) = maybe_function.as_ref() { 68 | if modes.contains(&FuncMode::Query) || modes.contains(&FuncMode::CompositeQuery) 69 | { 70 | Some(CanisterEndpoint::Query(name.to_string())) 71 | } else { 72 | Some(CanisterEndpoint::Update(name.to_string())) 73 | } 74 | } else { 75 | None 76 | } 77 | }) 78 | .collect(); 79 | 80 | Ok(endpoints) 81 | } 82 | } 83 | -------------------------------------------------------------------------------- /src/optimize.rs: -------------------------------------------------------------------------------- 1 | use crate::metadata::*; 2 | use crate::utils::*; 3 | use clap::ValueEnum; 4 | use walrus::*; 5 | 6 | #[derive(Clone, Copy, PartialEq, Eq, ValueEnum)] 7 | pub enum OptLevel { 8 | #[clap(name = "O0")] 9 | O0, 10 | #[clap(name = "O1")] 11 | O1, 12 | #[clap(name = "O2")] 13 | O2, 14 | #[clap(name = "O3")] 15 | O3, 16 | #[clap(name = "O4")] 17 | O4, 18 | #[clap(name = "Os")] 19 | Os, 20 | #[clap(name = "Oz")] 21 | Oz, 22 | } 23 | 24 | pub fn optimize( 25 | m: &mut Module, 26 | level: &OptLevel, 27 | inline_functions_with_loops: bool, 28 | always_inline_max_function_size: &Option, 29 | keep_name_section: bool, 30 | ) -> anyhow::Result<()> { 31 | use tempfile::NamedTempFile; 32 | use wasm_opt::OptimizationOptions; 33 | // recursively optimize embedded modules in Motoko actor classes 34 | if is_motoko_canister(m) { 35 | let data = get_motoko_wasm_data_sections(m); 36 | for (id, mut module) in data.into_iter() { 37 | let old_size = module.emit_wasm().len(); 38 | optimize( 39 | &mut module, 40 | level, 41 | inline_functions_with_loops, 42 | always_inline_max_function_size, 43 | keep_name_section, 44 | )?; 45 | let new_size = module.emit_wasm().len(); 46 | // Guard against embedded actor class overriding the parent module 47 | if new_size <= old_size { 48 | let blob = encode_module_as_data_section(module); 49 | m.data.get_mut(id).value = blob; 50 | } else { 51 | eprintln!("Warning: embedded actor class module was not optimized because the optimized module is larger than the original module"); 52 | } 53 | } 54 | } 55 | 56 | // write module to temp file 57 | let temp_file = NamedTempFile::new()?; 58 | m.emit_wasm_file(temp_file.path())?; 59 | 60 | // pull out a copy of the custom sections to preserve 61 | let metadata_sections: Vec<(Kind, &str, Vec)> = m 62 | .customs 63 | .iter() 64 | .filter(|(_, section)| section.name().starts_with("icp:")) 65 | .map(|(_, section)| { 66 | let data = section.data(&IdsToIndices::default()).to_vec(); 67 | let full_name = section.name(); 68 | match full_name.strip_prefix("public ") { 69 | Some(name) => (Kind::Public, name, data), 70 | None => match full_name.strip_prefix("private ") { 71 | Some(name) => (Kind::Private, name, data), 72 | None => unreachable!(), 73 | }, 74 | } 75 | }) 76 | .collect(); 77 | 78 | // read in from temp file and optimize 79 | let mut optimizations = match level { 80 | OptLevel::O0 => OptimizationOptions::new_opt_level_0(), 81 | OptLevel::O1 => OptimizationOptions::new_opt_level_1(), 82 | OptLevel::O2 => OptimizationOptions::new_opt_level_2(), 83 | OptLevel::O3 => OptimizationOptions::new_opt_level_3(), 84 | OptLevel::O4 => OptimizationOptions::new_opt_level_4(), 85 | OptLevel::Os => OptimizationOptions::new_optimize_for_size(), 86 | OptLevel::Oz => OptimizationOptions::new_optimize_for_size_aggressively(), 87 | }; 88 | optimizations.debug_info(keep_name_section); 89 | optimizations.allow_functions_with_loops(inline_functions_with_loops); 90 | if let Some(max_size) = always_inline_max_function_size { 91 | optimizations.always_inline_max_size(*max_size); 92 | } 93 | 94 | for feature in IC_ENABLED_WASM_FEATURES { 95 | optimizations.enable_feature(feature); 96 | } 97 | optimizations.run(temp_file.path(), temp_file.path())?; 98 | 99 | // read optimized wasm back in from temp file 100 | let mut m_opt = parse_wasm_file(temp_file.path().to_path_buf(), keep_name_section)?; 101 | 102 | // re-insert the custom sections 103 | metadata_sections 104 | .into_iter() 105 | .for_each(|(visibility, name, data)| { 106 | add_metadata(&mut m_opt, visibility, name, data); 107 | }); 108 | 109 | *m = m_opt; 110 | Ok(()) 111 | } 112 | -------------------------------------------------------------------------------- /tests/deployable.ic-repl.sh: -------------------------------------------------------------------------------- 1 | #!ic-repl 2 | // Check if the transformed Wasm module can be deployed 3 | 4 | function install(wasm) { 5 | let id = call ic.provisional_create_canister_with_cycles(record { settings = null; amount = null }); 6 | let S = id.canister_id; 7 | call ic.install_code( 8 | record { 9 | arg = encode (); 10 | wasm_module = wasm; 11 | mode = variant { install }; 12 | canister_id = S; 13 | } 14 | ); 15 | S 16 | }; 17 | function upgrade(S, wasm) { 18 | call ic.install_code( 19 | record { 20 | arg = encode (); 21 | wasm_module = wasm; 22 | mode = variant { upgrade }; 23 | canister_id = S; 24 | } 25 | ); 26 | }; 27 | 28 | function counter(wasm) { 29 | let S = install(wasm); 30 | call S.set(42); 31 | call S.inc(); 32 | call S.get(); 33 | assert _ == (43 : nat); 34 | 35 | call S.inc(); 36 | call S.inc(); 37 | call S.get(); 38 | assert _ == (45 : nat); 39 | S 40 | }; 41 | function wat(wasm) { 42 | let S = install(wasm); 43 | call S.set((42 : int64)); 44 | call S.inc(); 45 | call S.get(); 46 | assert _ == (43 : int64); 47 | 48 | call S.inc(); 49 | call S.inc(); 50 | call S.get(); 51 | assert _ == (45 : int64); 52 | S 53 | }; 54 | function classes(wasm) { 55 | let S = install(wasm); 56 | call S.get(42); 57 | assert _ == (null : opt empty); 58 | call S.put(42, "text"); 59 | call S.get(42); 60 | assert _ == opt "text"; 61 | 62 | call S.put(40, "text0"); 63 | call S.put(41, "text1"); 64 | call S.put(42, "text2"); 65 | call S.get(42); 66 | assert _ == opt "text2"; 67 | read_state("canister", S, "metadata/candid:args"); 68 | assert _ == "()"; 69 | read_state("canister", S, "metadata/motoko:compiler"); 70 | assert _ == blob "0.6.26"; 71 | S 72 | }; 73 | function classes_limit(wasm) { 74 | let S = install(wasm); 75 | call S.get(42); 76 | assert _ == (null : opt empty); 77 | fail call S.put(42, "text"); 78 | assert _ ~= "0 cycles were received"; 79 | S 80 | }; 81 | function classes_redirect(wasm) { 82 | let S = install(wasm); 83 | call S.get(42); 84 | assert _ == (null : opt empty); 85 | fail call S.put(42, "text"); 86 | assert _ ~= "zz73r-nyaaa-aabbb-aaaca-cai not found"; 87 | S 88 | }; 89 | function check_profiling(S, cycles, len) { 90 | call S.__get_cycles(); 91 | assert _ == (cycles : int64); 92 | call S.__get_profiling((0:nat32)); 93 | assert _[0].size() == (len : nat); 94 | assert _[1] == (null : opt empty); 95 | call S.__get_profiling((1:nat32)); 96 | assert _[0].size() == (sub(len,1) : nat); 97 | null 98 | }; 99 | function evm_redirect(wasm) { 100 | let S = install(wasm); 101 | fail call S.request(); 102 | assert _ ~= "zz73r-nyaaa-aabbb-aaaca-cai not found"; 103 | fail call S.requestCost(); 104 | assert _ ~= "7hfb6-caaaa-aaaar-qadga-cai not found"; 105 | fail call S.non_evm_request(); 106 | assert _ ~= "cpmcr-yeaaa-aaaaa-qaala-cai not found"; 107 | S 108 | }; 109 | 110 | evm_redirect(file("ok/evm-redirect.wasm")); 111 | 112 | let S = counter(file("ok/motoko-instrument.wasm")); 113 | check_profiling(S, 21571, 78); 114 | let S = counter(file("ok/motoko-gc-instrument.wasm")); 115 | check_profiling(S, 595, 4); 116 | let wasm = file("ok/motoko-region-instrument.wasm"); 117 | let S = counter(wasm); 118 | check_profiling(S, 478652, 78); 119 | upgrade(S, wasm); 120 | call S.get(); 121 | assert _ == (45 : nat); 122 | check_profiling(S, 494682, 460); 123 | counter(file("ok/motoko-shrink.wasm")); 124 | counter(file("ok/motoko-limit.wasm")); 125 | 126 | let S = counter(file("ok/rust-instrument.wasm")); 127 | check_profiling(S, 75279, 576); 128 | let wasm = file("ok/rust-region-instrument.wasm"); 129 | let S = counter(wasm); 130 | check_profiling(S, 152042, 574); 131 | upgrade(S, wasm); 132 | call S.get(); 133 | assert _ == (45 : nat); 134 | check_profiling(S, 1023168, 2344); 135 | counter(file("ok/rust-shrink.wasm")); 136 | counter(file("ok/rust-limit.wasm")); 137 | 138 | let S = wat(file("ok/wat-instrument.wasm")); 139 | check_profiling(S, 5656, 2); 140 | wat(file("ok/wat-shrink.wasm")); 141 | wat(file("ok/wat-limit.wasm")); 142 | 143 | classes(file("ok/classes-shrink.wasm")); 144 | classes(file("ok/classes-optimize.wasm")); 145 | classes(file("ok/classes-optimize-names.wasm")); 146 | classes_limit(file("ok/classes-limit.wasm")); 147 | classes_redirect(file("ok/classes-redirect.wasm")); 148 | classes(file("ok/classes-nop-redirect.wasm")); 149 | -------------------------------------------------------------------------------- /.github/CONTRIBUTING.md: -------------------------------------------------------------------------------- 1 | # Contributing 2 | 3 | Thank you for your interest in contributing to the Rust crates for the Internet Computer. 4 | By participating in this project, you agree to abide by our [Code of Conduct](./CODE_OF_CONDUCT.md). 5 | 6 | As a member of the community, you are invited and encouraged to contribute by submitting issues, offering suggestions for improvements, adding review comments to existing pull requests, or creating new pull requests to fix issues. 7 | 8 | All contributions to DFINITY documentation and the developer community are respected and appreciated. 9 | Your participation is an important factor in the success of the Internet Computer. 10 | 11 | ## Contents of this repository 12 | 13 | This repository contains source code for the canister interface description language—often referred to as Candid or IDL. Candid provides a common language for specifying the signature of a canister service and interacting with canisters running on the 14 | Internet Computer. 15 | 16 | ## Before you contribute 17 | 18 | Before contributing, please take a few minutes to review these contributor guidelines. 19 | The contributor guidelines are intended to make the contribution process easy and effective for everyone involved in addressing your issue, assessing changes, and finalizing your pull requests. 20 | 21 | Before contributing, consider the following: 22 | 23 | - If you want to report an issue, click **Issues**. 24 | 25 | - If you have more general questions related to Candid and its use, post a message to the [community forum](https://forum.dfinity.org/) or submit a [support request](mailto://support@dfinity.org). 26 | 27 | - If you are reporting a bug, provide as much information about the problem as possible. 28 | 29 | - If you want to contribute directly to this repository, typical fixes might include any of the following: 30 | 31 | - Fixes to resolve bugs or documentation errors 32 | - Code improvements 33 | - Feature requests 34 | 35 | Note that any contribution to this repository must be submitted in the form of a **pull request**. 36 | 37 | - If you are creating a pull request, be sure that the pull request only implements one fix or suggestion. 38 | 39 | If you are new to working with GitHub repositories and creating pull requests, consider exploring [First Contributions](https://github.com/firstcontributions/first-contributions) or [How to Contribute to an Open Source Project on GitHub](https://egghead.io/courses/how-to-contribute-to-an-open-source-project-on-github). 40 | 41 | # How to make a contribution 42 | 43 | Depending on the type of contribution you want to make, you might follow different workflows. 44 | 45 | This section describes the most common workflow scenarios: 46 | 47 | - Reporting an issue 48 | - Submitting a pull request 49 | 50 | ### Reporting an issue 51 | 52 | To open a new issue: 53 | 54 | 1. Click **Issues**. 55 | 56 | 1. Click **New Issue**. 57 | 58 | 1. Click **Open a blank issue**. 59 | 60 | 1. Type a title and description, then click **Submit new issue**. 61 | 62 | Be as clear and descriptive as possible. 63 | 64 | For any problem, describe it in detail, including details about the crate, the version of the code you are using, the results you expected, and how the actual results differed from your expectations. 65 | 66 | ### Submitting a pull request 67 | 68 | If you want to submit a pull request to fix an issue or add a feature, here's a summary of what you need to do: 69 | 70 | 1. Make sure you have a GitHub account, an internet connection, and access to a terminal shell or GitHub Desktop application for running commands. 71 | 72 | 1. Navigate to the DFINITY public repository in a web browser. 73 | 74 | 1. Click **Fork** to create a copy the repository associated with the issue you want to address under your GitHub account or organization name. 75 | 76 | 1. Clone the repository to your local machine. 77 | 78 | 1. Create a new branch for your fix by running a command similar to the following: 79 | 80 | ```bash 81 | git checkout -b my-branch-name-here 82 | ``` 83 | 84 | 1. Open the file you want to fix in a text editor and make the appropriate changes for the issue you are trying to address. 85 | 86 | 1. Add the file contents of the changed files to the index `git` uses to manage the state of the project by running a command similar to the following: 87 | 88 | ```bash 89 | git add path-to-changed-file 90 | ``` 91 | 1. Commit your changes to store the contents you added to the index along with a descriptive message by running a command similar to the following: 92 | 93 | ```bash 94 | git commit -m "Description of the fix being committed." 95 | ``` 96 | 97 | 1. Push the changes to the remote repository by running a command similar to the following: 98 | 99 | ```bash 100 | git push origin my-branch-name-here 101 | ``` 102 | 103 | 1. Create a new pull request for the branch you pushed to the upstream GitHub repository. 104 | 105 | Provide a title that includes a short description of the changes made. 106 | 107 | 1. Wait for the pull request to be reviewed. 108 | 109 | 1. Make changes to the pull request, if requested. 110 | 111 | 1. Celebrate your success after your pull request is merged! 112 | -------------------------------------------------------------------------------- /.github/workflows/release.yml: -------------------------------------------------------------------------------- 1 | name: Release 2 | on: 3 | push: 4 | tags: 5 | - "*" 6 | 7 | jobs: 8 | build: 9 | name: Build for ${{ matrix.name }} 10 | runs-on: ${{ matrix.os }} 11 | strategy: 12 | fail-fast: false 13 | # The runner images are chosen to be one major version behind the latest. 14 | # The macOS runner should be x86_64 so that the binary can run on both Intel and Apple Silicon Macs. 15 | # For example, as of Sep 2025 16 | # - The latest Ubuntu is 24.04, so we use ubuntu-22.04 17 | # - The latest macOS is 15, so we use macos-14-large 18 | matrix: 19 | include: 20 | - os: ubuntu-22.04 21 | name: linux64 22 | artifact_name: target/release/ic-wasm 23 | asset_name: ic-wasm-linux64 24 | - os: ubuntu-22.04 25 | name: linux-musl 26 | artifact_name: target/release/ic-wasm 27 | asset_name: ic-wasm-linux-musl 28 | - os: macos-14-large # x86_64 build that can run on both Intel and Apple Silicon Macs 29 | name: macos 30 | artifact_name: target/release/ic-wasm 31 | asset_name: ic-wasm-macos 32 | steps: 33 | - uses: actions/checkout@v4 34 | 35 | # Regular build for non-MUSL targets 36 | - name: Build (non-MUSL) 37 | if: matrix.name != 'linux-musl' 38 | run: cargo build --release --all-features --locked 39 | 40 | # Native MUSL build using Alpine Docker 41 | - name: Build (MUSL) 42 | if: matrix.name == 'linux-musl' 43 | run: | 44 | docker run --rm -v $(pwd):/src -w /src rust:alpine sh -c ' 45 | # Install build dependencies 46 | apk add --no-cache musl-dev binutils g++ 47 | 48 | # Build the project 49 | cargo build --release --all-features --locked 50 | ' 51 | 52 | - name: "Upload assets" 53 | uses: actions/upload-artifact@v4 54 | with: 55 | name: ${{ matrix.asset_name }} 56 | path: ${{ matrix.artifact_name }} 57 | retention-days: 3 58 | 59 | test: 60 | needs: build 61 | name: Test for ${{ matrix.os }} 62 | runs-on: ${{ matrix.os }} 63 | strategy: 64 | fail-fast: false 65 | # The binaries built in the `build` job are tested on: 66 | # - Ubuntu: latest and one major version behind latest. 67 | # - macOS: latest and one major version behind latest. Both x86_64 and arm64 runner images. 68 | matrix: 69 | include: 70 | - os: ubuntu-latest 71 | asset_name: ic-wasm-linux64 72 | - os: ubuntu-22.04 73 | asset_name: ic-wasm-linux64 74 | - os: ubuntu-latest 75 | asset_name: ic-wasm-linux-musl 76 | - os: ubuntu-22.04 77 | asset_name: ic-wasm-linux-musl 78 | - os: macos-latest-large 79 | asset_name: ic-wasm-macos 80 | - os: macos-latest 81 | asset_name: ic-wasm-macos 82 | - os: macos-14-large 83 | asset_name: ic-wasm-macos 84 | - os: macos-14 85 | asset_name: ic-wasm-macos 86 | steps: 87 | - name: Get executable 88 | id: download 89 | uses: actions/download-artifact@v4 90 | with: 91 | name: ${{ matrix.asset_name }} 92 | 93 | - name: Test MUSL binary in Alpine 94 | if: matrix.asset_name == 'ic-wasm-linux-musl' 95 | run: | 96 | docker run --rm -v $(pwd):/test alpine:latest sh -c ' 97 | chmod +x /test/ic-wasm 98 | /test/ic-wasm --version 99 | ' 100 | 101 | - name: Test non-MUSL binary 102 | if: matrix.asset_name != 'ic-wasm-linux-musl' 103 | run: | 104 | chmod +x ic-wasm 105 | ./ic-wasm --version 106 | 107 | publish: 108 | name: GitHub Release 109 | runs-on: ubuntu-latest 110 | needs: test 111 | steps: 112 | - name: Get executables 113 | uses: actions/download-artifact@v4 114 | # Download all artifacts, files will be at: 115 | # ic-wasm-linux64/ic-wasm 116 | # ic-wasm-linux-musl/ic-wasm 117 | # ic-wasm-macos/ic-wasm 118 | 119 | - name: Prepare artifacts 120 | run: | 121 | mkdir artifacts 122 | 123 | pushd ic-wasm-linux64 124 | chmod +x ic-wasm 125 | tar -cvzf ic-wasm-x86_64-unknown-linux-gnu.tar.gz ic-wasm 126 | mv ic-wasm ic-wasm-linux64 127 | mv * ../artifacts 128 | popd 129 | 130 | pushd ic-wasm-linux-musl 131 | chmod +x ic-wasm 132 | tar -cvzf ic-wasm-x86_64-unknown-linux-musl.tar.gz ic-wasm 133 | mv ic-wasm ic-wasm-linux-musl 134 | mv * ../artifacts 135 | popd 136 | 137 | pushd ic-wasm-macos 138 | chmod +x ic-wasm 139 | tar -cvzf ic-wasm-x86_64-apple-darwin.tar.gz ic-wasm 140 | mv ic-wasm ic-wasm-macos 141 | mv * ../artifacts 142 | popd 143 | 144 | - name: Create GitHub Release 145 | uses: softprops/action-gh-release@v2 146 | with: 147 | token: ${{ secrets.GITHUB_TOKEN }} 148 | files: artifacts/* 149 | -------------------------------------------------------------------------------- /src/check_endpoints/mod.rs: -------------------------------------------------------------------------------- 1 | mod candid; 2 | 3 | pub use crate::check_endpoints::candid::CandidParser; 4 | use crate::{info::ExportedMethodInfo, utils::get_exported_methods}; 5 | use anyhow::anyhow; 6 | use parse_display::{Display, FromStr}; 7 | use std::io::BufReader; 8 | use std::{collections::BTreeSet, io::BufRead, path::Path, str::FromStr}; 9 | use walrus::Module; 10 | 11 | #[derive(Clone, Eq, Debug, Ord, PartialEq, PartialOrd, Display, FromStr)] 12 | pub enum CanisterEndpoint { 13 | #[display("canister_update:{0}")] 14 | Update(String), 15 | #[display("canister_query:{0}")] 16 | Query(String), 17 | #[display("canister_composite_query:{0}")] 18 | CompositeQuery(String), 19 | #[display("{0}")] 20 | Entrypoint(String), 21 | } 22 | 23 | impl TryFrom<&ExportedMethodInfo> for CanisterEndpoint { 24 | type Error = anyhow::Error; 25 | 26 | fn try_from(method: &ExportedMethodInfo) -> Result { 27 | type EndpointConstructor = fn(&str) -> CanisterEndpoint; 28 | const MAPPINGS: &[(&str, EndpointConstructor)] = &[ 29 | ("canister_update", |s| { 30 | CanisterEndpoint::Update(s.to_string()) 31 | }), 32 | ("canister_query", |s| CanisterEndpoint::Query(s.to_string())), 33 | ("canister_composite_query", |s| { 34 | CanisterEndpoint::CompositeQuery(s.to_string()) 35 | }), 36 | ]; 37 | 38 | for (candid_prefix, constructor) in MAPPINGS { 39 | if let Some(rest) = method.name.strip_prefix(candid_prefix) { 40 | return Ok(constructor(rest.trim())); 41 | } 42 | } 43 | 44 | let trimmed = method.name.trim(); 45 | if !trimmed.is_empty() { 46 | Ok(CanisterEndpoint::Entrypoint(trimmed.to_string())) 47 | } else { 48 | Err(anyhow!("Exported method in canister WASM has empty name")) 49 | } 50 | } 51 | } 52 | 53 | pub fn check_endpoints( 54 | module: &Module, 55 | candid_path: Option<&Path>, 56 | hidden_path: Option<&Path>, 57 | ) -> anyhow::Result<()> { 58 | let wasm_endpoints = get_exported_methods(module) 59 | .iter() 60 | .map(CanisterEndpoint::try_from) 61 | .collect::, _>>()?; 62 | 63 | let candid_endpoints = CandidParser::try_from_wasm(module)? 64 | .or_else(|| candid_path.map(CandidParser::from_candid_file)) 65 | .ok_or(anyhow!( 66 | "Candid interface not specified in WASM file and Candid file not provided" 67 | ))? 68 | .parse()?; 69 | 70 | let missing_candid_endpoints = candid_endpoints 71 | .difference(&wasm_endpoints) 72 | .collect::>(); 73 | missing_candid_endpoints.iter().for_each(|endpoint| { 74 | eprintln!( 75 | "ERROR: The following Candid endpoint is missing from the WASM exports section: {endpoint}" 76 | ); 77 | }); 78 | 79 | let hidden_endpoints = read_hidden_endpoints(hidden_path)?; 80 | let missing_hidden_endpoints = hidden_endpoints 81 | .difference(&wasm_endpoints) 82 | .collect::>(); 83 | missing_hidden_endpoints.iter().for_each(|endpoint| { 84 | eprintln!( 85 | "ERROR: The following hidden endpoint is missing from the WASM exports section: {endpoint}" 86 | ); 87 | }); 88 | 89 | let unexpected_endpoints = wasm_endpoints 90 | .iter() 91 | .filter(|endpoint| { 92 | !candid_endpoints.contains(endpoint) && !hidden_endpoints.contains(endpoint) 93 | }) 94 | .collect::>(); 95 | unexpected_endpoints.iter().for_each(|endpoint| { 96 | eprintln!( 97 | "ERROR: The following endpoint is unexpected in the WASM exports section: {endpoint}" 98 | ); 99 | }); 100 | 101 | if !missing_candid_endpoints.is_empty() 102 | || !missing_hidden_endpoints.is_empty() 103 | || !unexpected_endpoints.is_empty() 104 | { 105 | Err(anyhow!("Canister WASM and Candid interface do not match!")) 106 | } else { 107 | println!("Canister WASM and Candid interface match!"); 108 | Ok(()) 109 | } 110 | } 111 | 112 | fn read_hidden_endpoints(maybe_path: Option<&Path>) -> anyhow::Result> { 113 | if let Some(path) = maybe_path { 114 | let mut endpoints = BTreeSet::new(); 115 | for line in read_lines(path)? { 116 | if let Some(endpoint) = parse_line(line)? { 117 | endpoints.insert(endpoint); 118 | } 119 | } 120 | Ok(endpoints) 121 | } else { 122 | Ok(BTreeSet::new()) 123 | } 124 | } 125 | 126 | fn read_lines(path: &Path) -> anyhow::Result> { 127 | let file = std::fs::File::open(path) 128 | .map_err(|e| anyhow!("Could not open hidden endpoints file: {e:?}"))?; 129 | 130 | let reader = BufReader::new(file); 131 | let mut lines = Vec::new(); 132 | 133 | for line in reader.lines() { 134 | let line = line?; 135 | let trimmed = line.trim(); 136 | if !trimmed.is_empty() { 137 | lines.push(trimmed.to_string()); 138 | } 139 | } 140 | 141 | Ok(lines) 142 | } 143 | 144 | fn parse_line(line: String) -> anyhow::Result> { 145 | fn parse_uncommented_line(line: String) -> anyhow::Result> { 146 | CanisterEndpoint::from_str(line.as_str()) 147 | .map(Some) 148 | .map_err(Into::into) 149 | } 150 | // Comment: ignore line 151 | if line.starts_with("#") { 152 | return Ok(None); 153 | } 154 | // Quoted line: use JSON string syntax to allow for character escaping 155 | if line.starts_with('"') { 156 | if !line.ends_with('"') || line.len() < 2 { 157 | return Err(anyhow!( 158 | "Could not parse hidden endpoint, missing terminating quote: {line}" 159 | )); 160 | } 161 | return serde_json::from_str::(&line) 162 | .map_err(|e| anyhow!("Could not parse hidden endpoint: {e:?}")) 163 | .and_then(parse_uncommented_line); 164 | } 165 | // Regular line 166 | parse_uncommented_line(line) 167 | } 168 | -------------------------------------------------------------------------------- /src/info.rs: -------------------------------------------------------------------------------- 1 | use std::fmt; 2 | use std::io::Write; 3 | use walrus::Module; 4 | 5 | #[cfg(feature = "serde")] 6 | use serde::{Deserialize, Serialize}; 7 | 8 | use crate::{utils::*, Error}; 9 | 10 | /// External information about a Wasm, such as API methods. 11 | #[cfg_attr(feature = "serde", derive(Serialize, Deserialize))] 12 | pub struct WasmInfo { 13 | language: LanguageSpecificInfo, 14 | number_of_types: usize, 15 | number_of_globals: usize, 16 | number_of_data_sections: usize, 17 | size_of_data_sections: usize, 18 | number_of_functions: usize, 19 | number_of_callbacks: usize, 20 | start_function: Option, 21 | exported_methods: Vec, 22 | imported_ic0_system_api: Vec, 23 | custom_sections: Vec, 24 | } 25 | 26 | /// External information that is specific to one language 27 | #[cfg_attr(feature = "serde", derive(Serialize, Deserialize))] 28 | pub enum LanguageSpecificInfo { 29 | Motoko { 30 | embedded_wasm: Vec<(String, WasmInfo)>, 31 | }, 32 | Unknown, 33 | } 34 | 35 | /// Information about an exported method. 36 | #[cfg_attr(feature = "serde", derive(Serialize, Deserialize))] 37 | #[derive(Debug)] 38 | pub struct ExportedMethodInfo { 39 | pub name: String, 40 | pub internal_name: String, 41 | } 42 | 43 | /// Statistics about a custom section. 44 | #[cfg_attr(feature = "serde", derive(Serialize, Deserialize))] 45 | pub struct CustomSectionInfo { 46 | name: String, 47 | size: usize, 48 | } 49 | 50 | impl From<&Module> for WasmInfo { 51 | fn from(m: &Module) -> WasmInfo { 52 | let (number_of_data_sections, size_of_data_sections) = m 53 | .data 54 | .iter() 55 | .fold((0, 0), |(count, size), d| (count + 1, size + d.value.len())); 56 | 57 | WasmInfo { 58 | language: LanguageSpecificInfo::from(m), 59 | number_of_types: m.types.iter().count(), 60 | number_of_globals: m.globals.iter().count(), 61 | number_of_data_sections, 62 | size_of_data_sections, 63 | number_of_functions: m.funcs.iter().count(), 64 | number_of_callbacks: m.elements.iter().count(), 65 | start_function: m.start.map(|id| get_func_name(m, id)), 66 | exported_methods: get_exported_methods(m), 67 | imported_ic0_system_api: m 68 | .imports 69 | .iter() 70 | .filter(|i| i.module == "ic0") 71 | .map(|i| i.name.clone()) 72 | .collect(), 73 | custom_sections: m 74 | .customs 75 | .iter() 76 | .map(|(_, s)| CustomSectionInfo { 77 | name: s.name().to_string(), 78 | size: s.data(&Default::default()).len(), 79 | }) 80 | .collect(), 81 | } 82 | } 83 | } 84 | 85 | impl From<&Module> for LanguageSpecificInfo { 86 | fn from(m: &Module) -> LanguageSpecificInfo { 87 | if is_motoko_canister(m) { 88 | let mut embedded_wasm = Vec::new(); 89 | for (data_id, embedded_module) in get_motoko_wasm_data_sections(m) { 90 | embedded_wasm.push((format!("{data_id:?}"), WasmInfo::from(&embedded_module))); 91 | } 92 | return LanguageSpecificInfo::Motoko { embedded_wasm }; 93 | } 94 | LanguageSpecificInfo::Unknown 95 | } 96 | } 97 | 98 | impl fmt::Display for WasmInfo { 99 | fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result { 100 | write!(f, "{}", self.language)?; 101 | writeln!(f, "Number of types: {}", self.number_of_types)?; 102 | writeln!(f, "Number of globals: {}", self.number_of_globals)?; 103 | writeln!(f)?; 104 | writeln!( 105 | f, 106 | "Number of data sections: {}", 107 | self.number_of_data_sections 108 | )?; 109 | writeln!( 110 | f, 111 | "Size of data sections: {} bytes", 112 | self.size_of_data_sections 113 | )?; 114 | writeln!(f)?; 115 | writeln!(f, "Number of functions: {}", self.number_of_functions)?; 116 | writeln!(f, "Number of callbacks: {}", self.number_of_callbacks)?; 117 | writeln!(f, "Start function: {:?}", self.start_function)?; 118 | let exports: Vec<_> = self 119 | .exported_methods 120 | .iter() 121 | .map( 122 | |ExportedMethodInfo { 123 | name, 124 | internal_name, 125 | }| { 126 | if name == internal_name { 127 | internal_name.clone() 128 | } else { 129 | format!("{name} ({internal_name})") 130 | } 131 | }, 132 | ) 133 | .collect(); 134 | writeln!(f, "Exported methods: {exports:#?}")?; 135 | writeln!(f)?; 136 | writeln!( 137 | f, 138 | "Imported IC0 System API: {:#?}", 139 | self.imported_ic0_system_api 140 | )?; 141 | writeln!(f)?; 142 | let customs: Vec<_> = self 143 | .custom_sections 144 | .iter() 145 | .map(|section_info| format!("{} ({} bytes)", section_info.name, section_info.size)) 146 | .collect(); 147 | writeln!(f, "Custom sections with size: {customs:#?}")?; 148 | Ok(()) 149 | } 150 | } 151 | 152 | impl fmt::Display for LanguageSpecificInfo { 153 | fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result { 154 | match self { 155 | LanguageSpecificInfo::Motoko { embedded_wasm } => { 156 | writeln!(f, "This is a Motoko canister")?; 157 | for (_, wasm_info) in embedded_wasm { 158 | writeln!(f, "--- Start decoding an embedded Wasm ---")?; 159 | write!(f, "{wasm_info}")?; 160 | writeln!(f, "--- End of decoding ---")?; 161 | } 162 | writeln!(f) 163 | } 164 | LanguageSpecificInfo::Unknown => Ok(()), 165 | } 166 | } 167 | } 168 | 169 | /// Print general summary of the Wasm module 170 | pub fn info(m: &Module, output: &mut dyn Write) -> Result<(), Error> { 171 | write!(output, "{}", WasmInfo::from(m))?; 172 | Ok(()) 173 | } 174 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # `ic-wasm` 2 | 3 | A library for transforming Wasm canisters running on the Internet Computer 4 | 5 | ## Executable 6 | 7 | To install the `ic-wasm` executable, run 8 | 9 | ``` 10 | $ cargo install ic-wasm 11 | ``` 12 | 13 | ### Metadata 14 | 15 | Manage metadata in the Wasm module. 16 | 17 | Usage: `ic-wasm [-o ] metadata [name] [-d | -f ] [-v ]` 18 | 19 | * List current metadata sections 20 | ``` 21 | $ ic-wasm input.wasm metadata 22 | ``` 23 | 24 | * List a specific metadata content 25 | ``` 26 | $ ic-wasm input.wasm metadata candid:service 27 | ``` 28 | 29 | * Add/overwrite a private metadata section 30 | 31 | **Note**: the hashes of private metadata sections are readable by anyone. If a section contains low-entropy data, the attacker could brute-force the contents. 32 | ``` 33 | $ ic-wasm input.wasm -o output.wasm metadata new_section -d "hello, world" 34 | ``` 35 | 36 | * Add/overwrite a public metadata section from file 37 | ``` 38 | $ ic-wasm input.wasm -o output.wasm metadata candid:service -f service.did -v public 39 | ``` 40 | 41 | ### Info 42 | 43 | Print information about the Wasm canister 44 | 45 | Usage: `ic-wasm info` 46 | 47 | ### Shrink 48 | 49 | Remove unused functions and debug info. 50 | 51 | Note: The `icp` metadata sections are preserved through the shrink. 52 | 53 | Usage: `ic-wasm -o shrink` 54 | 55 | ### Optimize 56 | 57 | Invoke wasm optimizations from [`wasm-opt`](https://github.com/WebAssembly/binaryen). 58 | 59 | The optimizer exposes different optimization levels to choose from. 60 | 61 | Performance levels (optimizes for runtime): 62 | - O4 63 | - O3 (default setting: best for minimizing cycle usage) 64 | - O2 65 | - O1 66 | - O0 (no optimizations) 67 | 68 | Code size levels (optimizes for binary size): 69 | - Oz (best for minimizing code size) 70 | - Os 71 | 72 | The recommended setting (O3) reduces cycle usage for Motoko programs by ~10% and Rust programs by ~4%. The code size for both languages is reduced by ~16%. 73 | 74 | Note: The `icp` metadata sections are preserved through the optimizations. 75 | 76 | Usage: `ic-wasm -o optimize ` 77 | 78 | There are two further flags exposed from `wasm-opt`: 79 | - `--inline-functions-with-loops` 80 | - `--always-inline-max-function-size ` 81 | 82 | These were exposed to aggressively inline functions, which are common in Motoko programs. With the new cost model, there is a large performance gain from inlining functions with loops, but also a large blowup in binary size. Due to the binary size increase, we may not be able to apply this inlining for actor classes inside a Wasm module. 83 | 84 | E.g. 85 | `ic-wasm -o optimize O3 --inline-functions-with-loops --always-inline-max-function-size 100` 86 | 87 | ### Resource 88 | 89 | Limit resource usage, mainly used by Motoko Playground 90 | 91 | Usage: `ic-wasm -o resource --remove_cycles_transfer --limit_stable_memory_page 1024` 92 | 93 | ### Check endpoints 94 | 95 | Verify the endpoints a canister’s WASM exports against its Candid interface. This tool is designed to ensure that all exported endpoints are intentional and match the Candid specification, helping to detect any accidental, unexpected or potentially malicious exports. 96 | 97 | Usage: `ic-wasm check-endpoints [--candid ] [--hidden ]` 98 | 99 | - `--candid ` (optional) specifies a Candid file containing the canister's expected interface. If omitted, the Candid interface is assumed to be embedded in the WASM file. 100 | - `--hidden ` (optional) specifies a file that lists endpoints which are intentionally exported by the canister but not included in the Candid interface. Each line describes a single endpoint using one of the following formats: 101 | - `canister_update:` 102 | - `canister_query:` 103 | - `canister_composite_query:` 104 | - `` 105 | 106 | Lines beginning with `#` are treated as comments and ignored. 107 | 108 | To include special characters (for example `#` or newlines), the entire line may be wrapped in double quotes (`"`). 109 | When quoted this way, the line is parsed using standard JSON string syntax (see [RFC 8259 section 7](https://www.rfc-editor.org/rfc/rfc8259#section-7)). 110 | 111 | **Example `hidden.txt`:** 112 | ```text 113 | # A canister update endpoint named `__motoko_async_helper` 114 | canister_update:__motoko_async_helper 115 | 116 | # Canister query endpoints named `__get_candid_interface_tmp_hack` and `__motoko_stable_var_info` 117 | canister_query:__get_candid_interface_tmp_hack 118 | canister_query:__motoko_stable_var_info 119 | 120 | # Other canister endpoints: a timer, init method, etc. 121 | canister_global_timer 122 | canister_init 123 | canister_post_upgrade 124 | canister_pre_upgrade 125 | ``` 126 | 127 | ### Instrument (experimental) 128 | 129 | Instrument canister method to emit execution trace to stable memory. 130 | 131 | Usage: `ic-wasm -o instrument --trace-only func1 --trace-only func2 --start-page 16 --page-limit 30` 132 | 133 | Instrumented canister has the following additional endpoints: 134 | 135 | * `__get_cycles: () -> (int64) query`. Get the current cycle counter. 136 | * `__get_profiling: (idx:int32) -> (vec { record { int32; int64 }}, opt int32) query`. Get the execution trace log, starting with `idx` 0. If the log is larger than 2M, it returns the first 2M of trace, and the next `idx` for the next 2M chunk. 137 | * `__toggle_tracing: () -> ()`. Disable/enable logging the execution trace. 138 | * `__toggle_entry: () -> ()`. Disable/enable clearing exection trace for each update call. 139 | * `icp:public name` metadata. Used to map func_id from execution trace to function name. 140 | 141 | When `--trace-only` flag is provided, the counter and trace logging will only happen during the execution of that function, instead of tracing the whole update call. Note that the function itself has to be non-recursive. 142 | 143 | #### Working with upgrades and stable memory 144 | 145 | By default, execution trace is stored in the first few pages (up to 32 pages) of stable memory. Without any user side support, we cannot profile upgrade or code which accesses stable memory. If the canister can pre-allocate a fixed region of stable memory at `canister_init`, we can then pass this address to `ic-wasm` via the `--start-page` flag, so that the trace is written to this pre-allocated space without corrupting the rest of the stable memory access. 146 | 147 | Another optional flag `--page-limit` specifies the number of pre-allocated pages in stable memory. By default, it's set to 4096 pages (256MB). We only store trace up to `page-limit` pages, the remaining trace is dropped. 148 | 149 | The recommended way of pre-allocating stable memory is via the `Region` library in Motoko, and `ic-stable-structures` in Rust. But developers are free to use any other libraries or even the raw stable memory system API to pre-allocate space, as long as the developer can guarantee that the pre-allocated space is not touched by the rest of the code. 150 | 151 | The following is the code sample for pre-allocating stable memory in Motoko (with `--start-page 16`), 152 | 153 | ```motoko 154 | import Region "mo:base/Region"; 155 | actor { 156 | stable let profiling = do { 157 | let r = Region.new(); 158 | ignore Region.grow(r, 4096); // Increase the page number if you need larger log space 159 | r; 160 | }; 161 | ... 162 | } 163 | ``` 164 | 165 | and in Rust (with `--start-page 1`) 166 | 167 | ```rust 168 | use ic_stable_structures::{ 169 | memory_manager::{MemoryId, MemoryManager}, 170 | writer::Writer, 171 | DefaultMemoryImpl, Memory, 172 | }; 173 | thread_local! { 174 | static MEMORY_MANAGER: RefCell> = 175 | RefCell::new(MemoryManager::init(DefaultMemoryImpl::default())); 176 | } 177 | const PROFILING: MemoryId = MemoryId::new(0); 178 | const UPGRADES: MemoryId = MemoryId::new(1); 179 | 180 | #[ic_cdk::init] 181 | fn init() { 182 | let memory = MEMORY_MANAGER.with(|m| m.borrow().get(PROFILING)); 183 | memory.grow(4096); // Increase the page number if you need larger log space 184 | ... 185 | } 186 | #[ic_cdk::pre_upgrade] 187 | fn pre_upgrade() { 188 | let mut memory = MEMORY_MANAGER.with(|m| m.borrow().get(UPGRADES)); 189 | ... 190 | } 191 | #[ic_cdk::post_upgrade] 192 | fn post_upgrade() { 193 | let memory = MEMORY_MANAGER.with(|m| m.borrow().get(UPGRADES)); 194 | ... 195 | } 196 | ``` 197 | 198 | #### Current limitations 199 | 200 | * Without pre-allocating stable memory from user code, we cannot profile upgrade or code that accesses stable memory. You can profile traces larger than 256M, if you pre-allocate large pages of stable memory and specify the `page-limit` flag. Larger traces can be fetched in a streamming fashion via `__get_profiling(idx)`. 201 | * Since the pre-allocation happens in `canister_init`, we cannot profile `canister_init`. 202 | * If heartbeat is present, it's hard to measure any other method calls. It's also hard to measure a specific heartbeat event. 203 | * We cannot measure query calls. 204 | * No concurrent calls. 205 | 206 | ## Library 207 | 208 | To use `ic-wasm` as a library, add this to your `Cargo.toml`: 209 | 210 | ```toml 211 | [dependencies.ic-wasm] 212 | default-features = false 213 | ``` 214 | 215 | ## Contribution 216 | 217 | See our [CONTRIBUTING](.github/CONTRIBUTING.md) to get started. 218 | -------------------------------------------------------------------------------- /src/bin/main.rs: -------------------------------------------------------------------------------- 1 | use clap::{crate_authors, crate_version, Parser}; 2 | #[cfg(feature = "check-endpoints")] 3 | use ic_wasm::check_endpoints::check_endpoints; 4 | use ic_wasm::utils::make_validator_with_features; 5 | use std::path::PathBuf; 6 | 7 | #[derive(Parser)] 8 | #[clap( 9 | version = crate_version!(), 10 | author = crate_authors!(), 11 | )] 12 | struct Opts { 13 | /// Input Wasm file. 14 | input: PathBuf, 15 | 16 | /// Write the transformed Wasm file if provided. 17 | #[clap(short, long)] 18 | output: Option, 19 | 20 | #[clap(subcommand)] 21 | subcommand: SubCommand, 22 | } 23 | 24 | #[derive(Parser)] 25 | enum SubCommand { 26 | /// Manage metadata in the Wasm module 27 | Metadata { 28 | /// Name of metadata. If not provided, list the current metadata sections. 29 | name: Option, 30 | /// Content of metadata as a string 31 | #[clap(short, long, requires("name"))] 32 | data: Option, 33 | /// Content of metadata from a file 34 | #[clap(short, long, requires("name"), conflicts_with("data"))] 35 | file: Option, 36 | /// Visibility of metadata 37 | #[clap(short, long, value_parser = ["public", "private"], default_value = "private")] 38 | visibility: String, 39 | /// Preserve the `name` section in the generated Wasm. This is needed to 40 | /// display the names of functions, locals, etc. in backtraces or 41 | /// debuggers. 42 | #[clap(short, long)] 43 | keep_name_section: bool, 44 | }, 45 | /// Limit resource usage 46 | Resource { 47 | /// Remove `ic0.call_cycles_add[128]` system API calls 48 | #[clap(short, long)] 49 | remove_cycles_transfer: bool, 50 | /// Filter `ic0.call_cycles_add[128]` system API calls 51 | #[clap(short, long, conflicts_with_all = &["remove_cycles_transfer", "playground_backend_redirect"])] 52 | filter_cycles_transfer: bool, 53 | /// Allocate at most specified amount of memory pages for Wasm heap memory 54 | #[clap(short('m'), long)] 55 | limit_heap_memory_page: Option, 56 | /// Allocate at most specified amount of memory pages for stable memory 57 | #[clap(short, long)] 58 | limit_stable_memory_page: Option, 59 | /// Redirects controller system API calls to specified motoko backend canister ID 60 | #[clap(short, long)] 61 | playground_backend_redirect: Option, 62 | }, 63 | /// List information about the Wasm canister 64 | Info { 65 | /// Format the output as JSON 66 | #[clap(short, long)] 67 | #[cfg(feature = "serde")] 68 | json: bool, 69 | }, 70 | /// Remove unused functions and debug info 71 | Shrink { 72 | /// Preserve the `name` section in the generated Wasm. This is needed to 73 | /// display the names of functions, locals, etc. in backtraces or 74 | /// debuggers. 75 | #[clap(short, long)] 76 | keep_name_section: bool, 77 | }, 78 | /// Optimize the Wasm module using wasm-opt 79 | #[cfg(feature = "wasm-opt")] 80 | Optimize { 81 | #[clap()] 82 | level: ic_wasm::optimize::OptLevel, 83 | #[clap(long("inline-functions-with-loops"))] 84 | inline_functions_with_loops: bool, 85 | #[clap(long("always-inline-max-function-size"))] 86 | always_inline_max_function_size: Option, 87 | /// Preserve the `name` section in the generated Wasm. This is needed to 88 | /// display the names of functions, locals, etc. in backtraces or 89 | /// debuggers. 90 | #[clap(short, long)] 91 | keep_name_section: bool, 92 | }, 93 | /// Instrument canister method to emit execution trace to stable memory (experimental) 94 | Instrument { 95 | /// Trace only the specified list of functions. The function cannot be recursive 96 | #[clap(short, long)] 97 | trace_only: Option>, 98 | /// If the canister preallocates a stable memory region, specify the starting page. Required if you want to profile upgrades, or the canister uses stable memory 99 | #[clap(short, long)] 100 | start_page: Option, 101 | /// The number of pages of the preallocated stable memory 102 | #[clap(short, long, requires("start_page"))] 103 | page_limit: Option, 104 | }, 105 | /// Check canister endpoints against provided Candid interface 106 | #[cfg(feature = "check-endpoints")] 107 | CheckEndpoints { 108 | /// Candid interface file. 109 | #[clap(long)] 110 | candid: Option, 111 | /// Optionally specify hidden endpoints, i.e., endpoints that are exposed by the canister but 112 | /// not present in the Candid interface file. 113 | #[arg(long)] 114 | hidden: Option, 115 | }, 116 | } 117 | 118 | fn main() -> anyhow::Result<()> { 119 | let opts: Opts = Opts::parse(); 120 | let keep_name_section = match opts.subcommand { 121 | SubCommand::Shrink { keep_name_section } => keep_name_section, 122 | #[cfg(feature = "wasm-opt")] 123 | SubCommand::Optimize { 124 | keep_name_section, .. 125 | } => keep_name_section, 126 | SubCommand::Metadata { 127 | keep_name_section, .. 128 | } => keep_name_section, 129 | _ => false, 130 | }; 131 | let mut m = ic_wasm::utils::parse_wasm_file(opts.input, keep_name_section)?; 132 | match &opts.subcommand { 133 | #[cfg(feature = "serde")] 134 | SubCommand::Info { json } => { 135 | let wasm_info = ic_wasm::info::WasmInfo::from(&m); 136 | if *json { 137 | let json = serde_json::to_string_pretty(&wasm_info) 138 | .expect("Failed to express the Wasm information as JSON."); 139 | println!("{json}"); 140 | } else { 141 | print!("{wasm_info}"); 142 | } 143 | } 144 | #[cfg(not(feature = "serde"))] 145 | SubCommand::Info {} => { 146 | let wasm_info = ic_wasm::info::WasmInfo::from(&m); 147 | print!("{wasm_info}"); 148 | } 149 | SubCommand::Shrink { .. } => ic_wasm::shrink::shrink(&mut m), 150 | #[cfg(feature = "wasm-opt")] 151 | SubCommand::Optimize { 152 | level, 153 | inline_functions_with_loops, 154 | always_inline_max_function_size, 155 | .. 156 | } => ic_wasm::optimize::optimize( 157 | &mut m, 158 | level, 159 | *inline_functions_with_loops, 160 | always_inline_max_function_size, 161 | keep_name_section, 162 | )?, 163 | SubCommand::Instrument { 164 | trace_only, 165 | start_page, 166 | page_limit, 167 | } => { 168 | use ic_wasm::instrumentation::{instrument, Config}; 169 | let config = Config { 170 | trace_only_funcs: trace_only.clone().unwrap_or(vec![]), 171 | start_address: start_page.map(|page| i64::from(page) * 65536), 172 | page_limit: *page_limit, 173 | }; 174 | instrument(&mut m, config).map_err(|e| anyhow::anyhow!("{e}"))?; 175 | } 176 | SubCommand::Resource { 177 | remove_cycles_transfer, 178 | filter_cycles_transfer, 179 | limit_heap_memory_page, 180 | limit_stable_memory_page, 181 | playground_backend_redirect, 182 | } => { 183 | use ic_wasm::limit_resource::{limit_resource, Config}; 184 | let config = Config { 185 | remove_cycles_add: *remove_cycles_transfer, 186 | filter_cycles_add: *filter_cycles_transfer, 187 | limit_heap_memory_page: *limit_heap_memory_page, 188 | limit_stable_memory_page: *limit_stable_memory_page, 189 | playground_canister_id: *playground_backend_redirect, 190 | }; 191 | limit_resource(&mut m, &config) 192 | } 193 | SubCommand::Metadata { 194 | name, 195 | data, 196 | file, 197 | visibility, 198 | keep_name_section: _, 199 | } => { 200 | use ic_wasm::metadata::*; 201 | if let Some(name) = name { 202 | let visibility = match visibility.as_str() { 203 | "public" => Kind::Public, 204 | "private" => Kind::Private, 205 | _ => unreachable!(), 206 | }; 207 | let data = match (data, file) { 208 | (Some(data), None) => data.as_bytes().to_vec(), 209 | (None, Some(path)) => std::fs::read(path)?, 210 | (None, None) => { 211 | let data = get_metadata(&m, name); 212 | if let Some(data) = data { 213 | println!("{}", String::from_utf8_lossy(&data)); 214 | } else { 215 | println!("Cannot find metadata {name}"); 216 | } 217 | return Ok(()); 218 | } 219 | (_, _) => unreachable!(), 220 | }; 221 | add_metadata(&mut m, visibility, name, data) 222 | } else { 223 | let names = list_metadata(&m); 224 | for name in names.iter() { 225 | println!("{name}"); 226 | } 227 | return Ok(()); 228 | } 229 | } 230 | #[cfg(feature = "check-endpoints")] 231 | SubCommand::CheckEndpoints { candid, hidden } => { 232 | return check_endpoints(&m, candid.as_deref(), hidden.as_deref()); 233 | } 234 | }; 235 | // validate new module 236 | let module_bytes = m.emit_wasm(); 237 | let mut validator = make_validator_with_features(); 238 | if let Err(e) = validator.validate_all(&module_bytes) { 239 | println!("WARNING: The output of ic-wasm failed to validate. Please report this via github issue or on https://forum.dfinity.org/"); 240 | eprintln!("{e}"); 241 | } 242 | if let Some(output) = opts.output { 243 | std::fs::write(output, module_bytes).expect("failed to write wasm module"); 244 | } 245 | Ok(()) 246 | } 247 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | Apache License 2 | Version 2.0, January 2004 3 | http://www.apache.org/licenses/ 4 | 5 | TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION 6 | 7 | 1. Definitions. 8 | 9 | "License" shall mean the terms and conditions for use, reproduction, and 10 | distribution as defined by Sections 1 through 9 of this document. 11 | 12 | "Licensor" shall mean the copyright owner or entity authorized by the 13 | copyright owner that is granting the License. 14 | 15 | "Legal Entity" shall mean the union of the acting entity and all other 16 | entities that control, are controlled by, or are under common control with 17 | that entity. For the purposes of this definition, "control" means (i) the 18 | power, direct or indirect, to cause the direction or management of such 19 | entity, whether by contract or otherwise, or (ii) ownership of fifty percent 20 | (50%) or more of the outstanding shares, or (iii) beneficial ownership of 21 | such entity. 22 | 23 | "You" (or "Your") shall mean an individual or Legal Entity exercising 24 | permissions granted by this License. 25 | 26 | "Source" form shall mean the preferred form for making modifications, 27 | including but not limited to software source code, documentation source, and 28 | configuration files. 29 | 30 | "Object" form shall mean any form resulting from mechanical transformation 31 | or translation of a Source form, including but not limited to compiled 32 | object code, generated documentation, and conversions to other media types. 33 | 34 | "Work" shall mean the work of authorship, whether in Source or Object form, 35 | made available under the License, as indicated by a copyright notice that is 36 | included in or attached to the work (an example is provided in the Appendix 37 | below). 38 | 39 | "Derivative Works" shall mean any work, whether in Source or Object form, 40 | that is based on (or derived from) the Work and for which the editorial 41 | revisions, annotations, elaborations, or other modifications represent, as a 42 | whole, an original work of authorship. For the purposes of this License, 43 | Derivative Works shall not include works that remain separable from, or 44 | merely link (or bind by name) to the interfaces of, the Work and Derivative 45 | Works thereof. 46 | 47 | "Contribution" shall mean any work of authorship, including the original 48 | version of the Work and any modifications or additions to that Work or 49 | Derivative Works thereof, that is intentionally submitted to Licensor for 50 | inclusion in the Work by the copyright owner or by an individual or Legal 51 | Entity authorized to submit on behalf of the copyright owner. For the 52 | purposes of this definition, "submitted" means any form of electronic, 53 | verbal, or written communication sent to the Licensor or its 54 | representatives, including but not limited to communication on electronic 55 | mailing lists, source code control systems, and issue tracking systems that 56 | are managed by, or on behalf of, the Licensor for the purpose of discussing 57 | and improving the Work, but excluding communication that is conspicuously 58 | marked or otherwise designated in writing by the copyright owner as "Not a 59 | Contribution." 60 | 61 | "Contributor" shall mean Licensor and any individual or Legal Entity on 62 | behalf of whom a Contribution has been received by Licensor and subsequently 63 | incorporated within the Work. 64 | 65 | 2. Grant of Copyright License. Subject to the terms and conditions of this 66 | License, each Contributor hereby grants to You a perpetual, worldwide, 67 | non-exclusive, no-charge, royalty-free, irrevocable copyright license to 68 | reproduce, prepare Derivative Works of, publicly display, publicly perform, 69 | sublicense, and distribute the Work and such Derivative Works in Source or 70 | Object form. 71 | 72 | 3. Grant of Patent License. Subject to the terms and conditions of this 73 | License, each Contributor hereby grants to You a perpetual, worldwide, 74 | non-exclusive, no-charge, royalty-free, irrevocable (except as stated in 75 | this section) patent license to make, have made, use, offer to sell, sell, 76 | import, and otherwise transfer the Work, where such license applies only to 77 | those patent claims licensable by such Contributor that are necessarily 78 | infringed by their Contribution(s) alone or by combination of their 79 | Contribution(s) with the Work to which such Contribution(s) was submitted. 80 | If You institute patent litigation against any entity (including a 81 | cross-claim or counterclaim in a lawsuit) alleging that the Work or a 82 | Contribution incorporated within the Work constitutes direct or contributory 83 | patent infringement, then any patent licenses granted to You under this 84 | License for that Work shall terminate as of the date such litigation is 85 | filed. 86 | 87 | 4. Redistribution. You may reproduce and distribute copies of the Work or 88 | Derivative Works thereof in any medium, with or without modifications, and 89 | in Source or Object form, provided that You meet the following conditions: 90 | 91 | a. You must give any other recipients of the Work or Derivative Works a 92 | copy of this License; and 93 | 94 | b. You must cause any modified files to carry prominent notices stating 95 | that You changed the files; and 96 | 97 | c. You must retain, in the Source form of any Derivative Works that You 98 | distribute, all copyright, patent, trademark, and attribution notices 99 | from the Source form of the Work, excluding those notices that do not 100 | pertain to any part of the Derivative Works; and 101 | 102 | d. If the Work includes a "NOTICE" text file as part of its distribution, 103 | then any Derivative Works that You distribute must include a readable 104 | copy of the attribution notices contained within such NOTICE file, 105 | excluding those notices that do not pertain to any part of the Derivative 106 | Works, in at least one of the following places: within a NOTICE text file 107 | distributed as part of the Derivative Works; within the Source form or 108 | documentation, if provided along with the Derivative Works; or, within a 109 | display generated by the Derivative Works, if and wherever such 110 | third-party notices normally appear. The contents of the NOTICE file are 111 | for informational purposes only and do not modify the License. You may 112 | add Your own attribution notices within Derivative Works that You 113 | distribute, alongside or as an addendum to the NOTICE text from the Work, 114 | provided that such additional attribution notices cannot be construed as 115 | modifying the License. 116 | 117 | You may add Your own copyright statement to Your modifications and may 118 | provide additional or different license terms and conditions for use, 119 | reproduction, or distribution of Your modifications, or for any such 120 | Derivative Works as a whole, provided Your use, reproduction, and 121 | distribution of the Work otherwise complies with the conditions stated in 122 | this License. 123 | 124 | 5. Submission of Contributions. Unless You explicitly state otherwise, any 125 | Contribution intentionally submitted for inclusion in the Work by You to the 126 | Licensor shall be under the terms and conditions of this License, without 127 | any additional terms or conditions. Notwithstanding the above, nothing 128 | herein shall supersede or modify the terms of any separate license agreement 129 | you may have executed with Licensor regarding such Contributions. 130 | 131 | 6. Trademarks. This License does not grant permission to use the trade names, 132 | trademarks, service marks, or product names of the Licensor, except as 133 | required for reasonable and customary use in describing the origin of the 134 | Work and reproducing the content of the NOTICE file. 135 | 136 | 7. Disclaimer of Warranty. Unless required by applicable law or agreed to in 137 | writing, Licensor provides the Work (and each Contributor provides its 138 | Contributions) on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY 139 | KIND, either express or implied, including, without limitation, any 140 | warranties or conditions of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or 141 | FITNESS FOR A PARTICULAR PURPOSE. You are solely responsible for determining 142 | the appropriateness of using or redistributing the Work and assume any risks 143 | associated with Your exercise of permissions under this License. 144 | 145 | 8. Limitation of Liability. In no event and under no legal theory, whether in 146 | tort (including negligence), contract, or otherwise, unless required by 147 | applicable law (such as deliberate and grossly negligent acts) or agreed to 148 | in writing, shall any Contributor be liable to You for damages, including 149 | any direct, indirect, special, incidental, or consequential damages of any 150 | character arising as a result of this License or out of the use or inability 151 | to use the Work (including but not limited to damages for loss of goodwill, 152 | work stoppage, computer failure or malfunction, or any and all other 153 | commercial damages or losses), even if such Contributor has been advised of 154 | the possibility of such damages. 155 | 156 | 9. Accepting Warranty or Additional Liability. While redistributing the Work or 157 | Derivative Works thereof, You may choose to offer, and charge a fee for, 158 | acceptance of support, warranty, indemnity, or other liability obligations 159 | and/or rights consistent with this License. However, in accepting such 160 | obligations, You may act only on Your own behalf and on Your sole 161 | responsibility, not on behalf of any other Contributor, and only if You 162 | agree to indemnify, defend, and hold each Contributor harmless for any 163 | liability incurred by, or claims asserted against, such Contributor by 164 | reason of your accepting any such warranty or additional liability. 165 | 166 | END OF TERMS AND CONDITIONS 167 | 168 | LLVM EXCEPTIONS TO THE APACHE 2.0 LICENSE 169 | 170 | As an exception, if, as a result of your compiling your source code, portions 171 | of this Software are embedded into an Object form of such source code, you may 172 | redistribute such embedded portions in such Object form without complying with 173 | the conditions of Sections 4(a), 4(b) and 4(d) of the License. 174 | 175 | In addition, if you combine or link compiled forms of this Software with 176 | software that is licensed under the GPLv2 ("Combined Software") and if a court 177 | of competent jurisdiction determines that the patent provision (Section 3), the 178 | indemnity provision (Section 9) or other Section of the License conflicts with 179 | the conditions of the GPLv2, you may retroactively and prospectively choose to 180 | deem waived or otherwise exclude such Section(s) of the License, but only in 181 | their entirety and only with respect to the Combined Software. 182 | 183 | END OF LLVM EXCEPTIONS 184 | 185 | APPENDIX: How to apply the Apache License to your work. 186 | 187 | To apply the Apache License to your work, attach the following boilerplate 188 | notice, with the fields enclosed by brackets "[]" replaced with your own 189 | identifying information. (Don't include the brackets!) The text should be 190 | enclosed in the appropriate comment syntax for the file format. We also 191 | recommend that a file or class name and description of purpose be included on 192 | the same "printed page" as the copyright notice for easier identification 193 | within third-party archives. 194 | 195 | Copyright [yyyy] [name of copyright owner] 196 | 197 | Licensed under the Apache License, Version 2.0 (the "License"); you may not use 198 | this file except in compliance with the License. You may obtain a copy of the 199 | License at 200 | 201 | http://www.apache.org/licenses/LICENSE-2.0 202 | 203 | Unless required by applicable law or agreed to in writing, software distributed 204 | under the License is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR 205 | CONDITIONS OF ANY KIND, either express or implied. See the License for the 206 | specific language governing permissions and limitations under the License. 207 | 208 | END OF APPENDIX 209 | -------------------------------------------------------------------------------- /tests/tests.rs: -------------------------------------------------------------------------------- 1 | use assert_cmd::Command; 2 | 3 | use std::fs; 4 | use std::io::Write; 5 | use std::path::Path; 6 | use tempfile::NamedTempFile; 7 | 8 | fn wasm_input(wasm: &str, output: bool) -> Command { 9 | let mut cmd = Command::cargo_bin("ic-wasm").unwrap(); 10 | let path = Path::new(env!("CARGO_MANIFEST_DIR")).join("tests"); 11 | cmd.arg(path.join(wasm)); 12 | if output { 13 | cmd.arg("-o").arg(path.join("out.wasm")); 14 | } 15 | cmd 16 | } 17 | 18 | fn assert_wasm(expected: &str) { 19 | let path = Path::new(env!("CARGO_MANIFEST_DIR")).join("tests"); 20 | let expected = path.join("ok").join(expected); 21 | let out = path.join("out.wasm"); 22 | let ok = fs::read(&expected).unwrap(); 23 | let actual = fs::read(&out).unwrap(); 24 | if ok != actual { 25 | use std::env; 26 | use std::io::Write; 27 | if env::var("REGENERATE_GOLDENFILES").is_ok() { 28 | let mut f = fs::File::create(&expected).unwrap(); 29 | f.write_all(&actual).unwrap(); 30 | } else { 31 | panic!( 32 | "ic_wasm did not result in expected wasm file: {} != {}. Run \"REGENERATE_GOLDENFILES=1 cargo test\" to update the wasm files", 33 | expected.display(), 34 | out.display() 35 | ); 36 | } 37 | } 38 | } 39 | 40 | fn assert_functions_are_named() { 41 | let path = Path::new(env!("CARGO_MANIFEST_DIR")).join("tests"); 42 | let out = path.join("out.wasm"); 43 | 44 | let module = walrus::Module::from_file(out).unwrap(); 45 | let name_count = module.funcs.iter().filter(|f| f.name.is_some()).count(); 46 | let total = module.funcs.iter().count(); 47 | // Walrus doesn't give direct access to the name section, but as a proxy 48 | // just check that moste functions have names. 49 | assert!( 50 | name_count > total / 2, 51 | "Module has {total} functions but only {name_count} have names." 52 | ) 53 | } 54 | 55 | #[test] 56 | fn instrumentation() { 57 | wasm_input("motoko.wasm", true) 58 | .arg("instrument") 59 | .assert() 60 | .success(); 61 | assert_wasm("motoko-instrument.wasm"); 62 | wasm_input("motoko.wasm", true) 63 | .arg("instrument") 64 | .arg("-t") 65 | .arg("schedule_copying_gc") 66 | .assert() 67 | .success(); 68 | assert_wasm("motoko-gc-instrument.wasm"); 69 | wasm_input("motoko-region.wasm", true) 70 | .arg("instrument") 71 | .arg("-s") 72 | .arg("16") 73 | .assert() 74 | .success(); 75 | assert_wasm("motoko-region-instrument.wasm"); 76 | wasm_input("wat.wasm", true) 77 | .arg("instrument") 78 | .assert() 79 | .success(); 80 | assert_wasm("wat-instrument.wasm"); 81 | wasm_input("wat.wasm.gz", true) 82 | .arg("instrument") 83 | .assert() 84 | .success(); 85 | assert_wasm("wat-instrument.wasm"); 86 | wasm_input("rust.wasm", true) 87 | .arg("instrument") 88 | .assert() 89 | .success(); 90 | assert_wasm("rust-instrument.wasm"); 91 | wasm_input("rust-region.wasm", true) 92 | .arg("instrument") 93 | .arg("-s") 94 | .arg("1") 95 | .assert() 96 | .success(); 97 | assert_wasm("rust-region-instrument.wasm"); 98 | } 99 | 100 | #[test] 101 | fn shrink() { 102 | wasm_input("motoko.wasm", true) 103 | .arg("shrink") 104 | .assert() 105 | .success(); 106 | assert_wasm("motoko-shrink.wasm"); 107 | wasm_input("wat.wasm", true) 108 | .arg("shrink") 109 | .assert() 110 | .success(); 111 | assert_wasm("wat-shrink.wasm"); 112 | wasm_input("wat.wasm.gz", true) 113 | .arg("shrink") 114 | .assert() 115 | .success(); 116 | assert_wasm("wat-shrink.wasm"); 117 | wasm_input("rust.wasm", true) 118 | .arg("shrink") 119 | .assert() 120 | .success(); 121 | assert_wasm("rust-shrink.wasm"); 122 | wasm_input("classes.wasm", true) 123 | .arg("shrink") 124 | .assert() 125 | .success(); 126 | assert_wasm("classes-shrink.wasm"); 127 | } 128 | #[test] 129 | fn optimize() { 130 | let expected_metadata = r#"icp:public candid:service 131 | icp:private candid:args 132 | icp:private motoko:stable-types 133 | icp:private motoko:compiler 134 | "#; 135 | 136 | wasm_input("classes.wasm", true) 137 | .arg("optimize") 138 | .arg("O3") 139 | .arg("--inline-functions-with-loops") 140 | .arg("--always-inline-max-function-size") 141 | .arg("100") 142 | .assert() 143 | .success(); 144 | assert_wasm("classes-optimize.wasm"); 145 | wasm_input("ok/classes-optimize.wasm", false) 146 | .arg("metadata") 147 | .assert() 148 | .stdout(expected_metadata) 149 | .success(); 150 | wasm_input("classes.wasm", true) 151 | .arg("optimize") 152 | .arg("O3") 153 | .arg("--keep-name-section") 154 | .assert() 155 | .success(); 156 | assert_wasm("classes-optimize-names.wasm"); 157 | } 158 | 159 | #[test] 160 | fn resource() { 161 | wasm_input("motoko.wasm", true) 162 | .arg("resource") 163 | .arg("--remove-cycles-transfer") 164 | .arg("--limit-stable-memory-page") 165 | .arg("32") 166 | .assert() 167 | .success(); 168 | assert_wasm("motoko-limit.wasm"); 169 | wasm_input("wat.wasm", true) 170 | .arg("resource") 171 | .arg("--remove-cycles-transfer") 172 | .arg("--limit-stable-memory-page") 173 | .arg("32") 174 | .assert() 175 | .success(); 176 | assert_wasm("wat-limit.wasm"); 177 | wasm_input("wat.wasm.gz", true) 178 | .arg("resource") 179 | .arg("--remove-cycles-transfer") 180 | .arg("--limit-stable-memory-page") 181 | .arg("32") 182 | .assert() 183 | .success(); 184 | assert_wasm("wat-limit.wasm"); 185 | wasm_input("rust.wasm", true) 186 | .arg("resource") 187 | .arg("--remove-cycles-transfer") 188 | .arg("--limit-stable-memory-page") 189 | .arg("32") 190 | .assert() 191 | .success(); 192 | assert_wasm("rust-limit.wasm"); 193 | wasm_input("classes.wasm", true) 194 | .arg("resource") 195 | .arg("--remove-cycles-transfer") 196 | .arg("--limit-stable-memory-page") 197 | .arg("32") 198 | .assert() 199 | .success(); 200 | assert_wasm("classes-limit.wasm"); 201 | let test_canister_id = "zz73r-nyaaa-aabbb-aaaca-cai"; 202 | let management_canister_id = "aaaaa-aa"; 203 | wasm_input("classes.wasm", true) 204 | .arg("resource") 205 | .arg("--playground-backend-redirect") 206 | .arg(test_canister_id) 207 | .assert() 208 | .success(); 209 | assert_wasm("classes-redirect.wasm"); 210 | wasm_input("classes.wasm", true) 211 | .arg("resource") 212 | .arg("--playground-backend-redirect") 213 | .arg(management_canister_id) 214 | .assert() 215 | .success(); 216 | assert_wasm("classes-nop-redirect.wasm"); 217 | wasm_input("evm.wasm", true) 218 | .arg("resource") 219 | .arg("--playground-backend-redirect") 220 | .arg(test_canister_id) 221 | .assert() 222 | .success(); 223 | assert_wasm("evm-redirect.wasm"); 224 | } 225 | 226 | #[test] 227 | fn info() { 228 | let expected = r#"Number of types: 6 229 | Number of globals: 1 230 | 231 | Number of data sections: 3 232 | Size of data sections: 35 bytes 233 | 234 | Number of functions: 9 235 | Number of callbacks: 0 236 | Start function: None 237 | Exported methods: [ 238 | "canister_query get (func_5)", 239 | "canister_update inc (func_6)", 240 | "canister_update set (func_7)", 241 | ] 242 | 243 | Imported IC0 System API: [ 244 | "msg_reply", 245 | "msg_reply_data_append", 246 | "msg_arg_data_size", 247 | "msg_arg_data_copy", 248 | "trap", 249 | ] 250 | 251 | Custom sections with size: [] 252 | "#; 253 | wasm_input("wat.wasm", false) 254 | .arg("info") 255 | .assert() 256 | .stdout(expected) 257 | .success(); 258 | wasm_input("wat.wasm.gz", false) 259 | .arg("info") 260 | .assert() 261 | .stdout(expected) 262 | .success(); 263 | } 264 | 265 | #[test] 266 | fn json_info() { 267 | let expected = r#"{ 268 | "language": "Unknown", 269 | "number_of_types": 6, 270 | "number_of_globals": 1, 271 | "number_of_data_sections": 3, 272 | "size_of_data_sections": 35, 273 | "number_of_functions": 9, 274 | "number_of_callbacks": 0, 275 | "start_function": null, 276 | "exported_methods": [ 277 | { 278 | "name": "canister_query get", 279 | "internal_name": "func_5" 280 | }, 281 | { 282 | "name": "canister_update inc", 283 | "internal_name": "func_6" 284 | }, 285 | { 286 | "name": "canister_update set", 287 | "internal_name": "func_7" 288 | } 289 | ], 290 | "imported_ic0_system_api": [ 291 | "msg_reply", 292 | "msg_reply_data_append", 293 | "msg_arg_data_size", 294 | "msg_arg_data_copy", 295 | "trap" 296 | ], 297 | "custom_sections": [] 298 | } 299 | "#; 300 | wasm_input("wat.wasm", false) 301 | .arg("info") 302 | .arg("--json") 303 | .assert() 304 | .stdout(expected) 305 | .success(); 306 | wasm_input("wat.wasm.gz", false) 307 | .arg("info") 308 | .arg("--json") 309 | .assert() 310 | .stdout(expected) 311 | .success(); 312 | } 313 | 314 | #[test] 315 | fn metadata() { 316 | // List metadata 317 | wasm_input("motoko.wasm", false) 318 | .arg("metadata") 319 | .assert() 320 | .stdout( 321 | r#"icp:public candid:service 322 | icp:private motoko:stable-types 323 | icp:private motoko:compiler 324 | icp:public candid:args 325 | "#, 326 | ) 327 | .success(); 328 | // Get motoko:compiler content 329 | wasm_input("motoko.wasm", false) 330 | .arg("metadata") 331 | .arg("motoko:compiler") 332 | .assert() 333 | .stdout("0.10.0\n") 334 | .success(); 335 | // Get a non-existed metadata 336 | wasm_input("motoko.wasm", false) 337 | .arg("metadata") 338 | .arg("whatever") 339 | .assert() 340 | .stdout("Cannot find metadata whatever\n") 341 | .success(); 342 | // Overwrite motoko:compiler 343 | wasm_input("motoko.wasm", true) 344 | .arg("metadata") 345 | .arg("motoko:compiler") 346 | .arg("-d") 347 | .arg("hello") 348 | .assert() 349 | .success(); 350 | wasm_input("out.wasm", false) 351 | .arg("metadata") 352 | .arg("motoko:compiler") 353 | .assert() 354 | .stdout("hello\n") 355 | .success(); 356 | // Add a new metadata 357 | wasm_input("motoko.wasm", true) 358 | .arg("metadata") 359 | .arg("whatever") 360 | .arg("-d") 361 | .arg("what?") 362 | .arg("-v") 363 | .arg("public") 364 | .assert() 365 | .success(); 366 | wasm_input("out.wasm", false) 367 | .arg("metadata") 368 | .assert() 369 | .stdout( 370 | r#"icp:public candid:service 371 | icp:private motoko:stable-types 372 | icp:private motoko:compiler 373 | icp:public candid:args 374 | icp:public whatever 375 | "#, 376 | ) 377 | .success(); 378 | } 379 | 380 | #[test] 381 | fn metadata_keep_name_section() { 382 | for file in [ 383 | "motoko.wasm", 384 | "classes.wasm", 385 | "motoko-region.wasm", 386 | "rust.wasm", 387 | ] { 388 | wasm_input(file, true) 389 | .arg("metadata") 390 | .arg("foo") 391 | .arg("-d") 392 | .arg("hello") 393 | .arg("--keep-name-section") 394 | .assert() 395 | .success(); 396 | assert_functions_are_named(); 397 | } 398 | } 399 | 400 | #[test] 401 | fn check_endpoints() { 402 | // Candid interface is NOT embedded in wat.wasm 403 | const CANDID_WITH_MISSING_ENDPOINTS: &str = r#" 404 | service : () -> { 405 | inc : (owner: opt principal) -> (nat); 406 | } 407 | "#; 408 | wasm_input("wat.wasm.gz", false) 409 | .arg("check-endpoints") 410 | .assert() 411 | .stderr("Error: Candid interface not specified in WASM file and Candid file not provided\n") 412 | .failure(); 413 | wasm_input("wat.wasm.gz", false) 414 | .arg("check-endpoints") 415 | .arg("--candid") 416 | .arg(create_tempfile(CANDID_WITH_MISSING_ENDPOINTS).path()) 417 | .assert() 418 | .stderr( 419 | "ERROR: The following endpoint is unexpected in the WASM exports section: canister_update:set\n\ 420 | ERROR: The following endpoint is unexpected in the WASM exports section: canister_query:get\n\ 421 | Error: Canister WASM and Candid interface do not match!\n", 422 | ) 423 | .failure(); 424 | const HIDDEN_1: &str = r#" 425 | # Canister update method (this line is a comment) 426 | canister_update:set 427 | # Canister query method (this line is also a comment) 428 | canister_query:get 429 | "#; 430 | wasm_input("wat.wasm.gz", false) 431 | .arg("check-endpoints") 432 | .arg("--hidden") 433 | .arg(create_tempfile(HIDDEN_1).path()) 434 | .arg("--candid") 435 | .arg(create_tempfile(CANDID_WITH_MISSING_ENDPOINTS).path()) 436 | .assert() 437 | .stdout("Canister WASM and Candid interface match!\n") 438 | .success(); 439 | // Candid interface is embedded in rust.wasm and motoko.wasm 440 | wasm_input("rust.wasm", false) 441 | .arg("check-endpoints") 442 | .assert() 443 | .stdout("Canister WASM and Candid interface match!\n") 444 | .success(); 445 | const HIDDEN_2: &str = r#" 446 | canister_update:dec 447 | "#; 448 | wasm_input("rust.wasm", false) 449 | .arg("check-endpoints") 450 | .arg("--hidden") 451 | .arg(create_tempfile(HIDDEN_2).path()) 452 | .assert() 453 | .stderr("ERROR: The following hidden endpoint is missing from the WASM exports section: canister_update:dec\n\ 454 | Error: Canister WASM and Candid interface do not match!\n") 455 | .failure(); 456 | wasm_input("motoko.wasm", false) 457 | .arg("check-endpoints") 458 | .assert() 459 | .stderr( 460 | "ERROR: The following endpoint is unexpected in the WASM exports section: canister_update:__motoko_async_helper\n\ 461 | ERROR: The following endpoint is unexpected in the WASM exports section: canister_query:__get_candid_interface_tmp_hack\n\ 462 | ERROR: The following endpoint is unexpected in the WASM exports section: canister_query:__motoko_stable_var_info\n\ 463 | ERROR: The following endpoint is unexpected in the WASM exports section: canister_global_timer\n\ 464 | ERROR: The following endpoint is unexpected in the WASM exports section: canister_init\n\ 465 | ERROR: The following endpoint is unexpected in the WASM exports section: canister_post_upgrade\n\ 466 | ERROR: The following endpoint is unexpected in the WASM exports section: canister_pre_upgrade\n\ 467 | Error: Canister WASM and Candid interface do not match!\n", 468 | ) 469 | .failure(); 470 | const HIDDEN_3: &str = r#" 471 | canister_update:__motoko_async_helper 472 | canister_query:__get_candid_interface_tmp_hack 473 | canister_query:__motoko_stable_var_info 474 | canister_global_timer 475 | canister_init 476 | canister_post_upgrade 477 | # The line below is quoted, it is parsed as a JSON string (this line is a comment) 478 | "canister_pre_upgrade" 479 | "#; 480 | wasm_input("motoko.wasm", false) 481 | .arg("check-endpoints") 482 | .arg("--hidden") 483 | .arg(create_tempfile(HIDDEN_3).path()) 484 | .assert() 485 | .stdout("Canister WASM and Candid interface match!\n") 486 | .success(); 487 | } 488 | 489 | fn create_tempfile(content: &str) -> NamedTempFile { 490 | let mut temp_file = NamedTempFile::new().expect("Failed to create temp file"); 491 | write!(temp_file, "{content}").expect("Failed to write temp file content"); 492 | temp_file 493 | } 494 | -------------------------------------------------------------------------------- /src/utils.rs: -------------------------------------------------------------------------------- 1 | use crate::info::ExportedMethodInfo; 2 | use crate::Error; 3 | use libflate::gzip; 4 | use std::borrow::Cow; 5 | use std::collections::HashMap; 6 | use std::io::{self, Read}; 7 | use walrus::*; 8 | #[cfg(feature = "wasm-opt")] 9 | use wasm_opt::Feature; 10 | use wasmparser::{Validator, WasmFeaturesInflated}; 11 | 12 | pub const WASM_MAGIC_BYTES: &[u8] = &[0, 97, 115, 109]; 13 | 14 | pub const GZIPPED_WASM_MAGIC_BYTES: &[u8] = &[31, 139, 8]; 15 | 16 | // The feature set should be align with IC `wasmtime` validation config: 17 | // https://github.com/dfinity/ic/blob/6a6470d705a0f36fb94743b12892280409f85688/rs/embedders/src/wasm_utils/validation.rs#L1385 18 | // Since we use both wasm_opt::Feature and wasmparse::WasmFeature, we have to define the config/features for both. 19 | #[cfg(feature = "wasm-opt")] 20 | pub const IC_ENABLED_WASM_FEATURES: [Feature; 7] = [ 21 | Feature::MutableGlobals, 22 | Feature::TruncSat, 23 | Feature::Simd, 24 | Feature::BulkMemory, 25 | Feature::SignExt, 26 | Feature::ReferenceTypes, 27 | Feature::Memory64, 28 | ]; 29 | 30 | pub const IC_ENABLED_WASM_FEATURES_INFLATED: WasmFeaturesInflated = WasmFeaturesInflated { 31 | mutable_global: true, 32 | saturating_float_to_int: true, 33 | sign_extension: true, 34 | reference_types: true, 35 | multi_value: true, 36 | bulk_memory: true, 37 | simd: true, 38 | relaxed_simd: false, 39 | threads: true, 40 | shared_everything_threads: true, 41 | tail_call: false, 42 | floats: true, 43 | multi_memory: true, 44 | exceptions: true, 45 | memory64: true, 46 | extended_const: true, 47 | component_model: true, 48 | function_references: false, 49 | memory_control: true, 50 | gc: false, 51 | custom_page_sizes: true, 52 | component_model_values: true, 53 | component_model_nested_names: true, 54 | component_model_more_flags: true, 55 | component_model_multiple_returns: true, 56 | legacy_exceptions: true, 57 | gc_types: true, 58 | stack_switching: true, 59 | wide_arithmetic: false, 60 | component_model_async: true, 61 | }; 62 | 63 | pub fn make_validator_with_features() -> Validator { 64 | Validator::new_with_features(IC_ENABLED_WASM_FEATURES_INFLATED.into()) 65 | } 66 | 67 | fn wasm_parser_config(keep_name_section: bool) -> ModuleConfig { 68 | let mut config = walrus::ModuleConfig::new(); 69 | config.generate_name_section(keep_name_section); 70 | config.generate_producers_section(false); 71 | config 72 | } 73 | 74 | fn decompress(bytes: &[u8]) -> Result, std::io::Error> { 75 | let mut decoder = gzip::Decoder::new(bytes)?; 76 | let mut decoded_data = Vec::new(); 77 | decoder.read_to_end(&mut decoded_data)?; 78 | Ok(decoded_data) 79 | } 80 | 81 | pub fn parse_wasm(bytes: &[u8], keep_name_section: bool) -> Result { 82 | let wasm = if bytes.starts_with(WASM_MAGIC_BYTES) { 83 | Ok(Cow::Borrowed(bytes)) 84 | } else if bytes.starts_with(GZIPPED_WASM_MAGIC_BYTES) { 85 | decompress(bytes).map(Cow::Owned) 86 | } else { 87 | Err(io::Error::new( 88 | io::ErrorKind::InvalidInput, 89 | "Input must be either gzipped or uncompressed WASM.", 90 | )) 91 | } 92 | .map_err(Error::IO)?; 93 | let config = wasm_parser_config(keep_name_section); 94 | config 95 | .parse(&wasm) 96 | .map_err(|e| Error::WasmParse(e.to_string())) 97 | } 98 | 99 | pub fn parse_wasm_file(file: std::path::PathBuf, keep_name_section: bool) -> Result { 100 | let bytes = std::fs::read(file).map_err(Error::IO)?; 101 | parse_wasm(&bytes[..], keep_name_section) 102 | } 103 | 104 | #[derive(Clone, Copy, PartialEq, Eq)] 105 | pub(crate) enum InjectionKind { 106 | Static, 107 | Dynamic, 108 | Dynamic64, 109 | } 110 | 111 | pub(crate) struct FunctionCost(HashMap); 112 | impl FunctionCost { 113 | pub fn new(m: &Module) -> Self { 114 | let mut res = HashMap::new(); 115 | for (method, func) in m.imports.iter().filter_map(|i| { 116 | if let ImportKind::Function(func) = i.kind { 117 | if i.module == "ic0" { 118 | Some((i.name.as_str(), func)) 119 | } else { 120 | None 121 | } 122 | } else { 123 | None 124 | } 125 | }) { 126 | use InjectionKind::*; 127 | // System API cost taken from https://github.com/dfinity/ic/blob/master/rs/embedders/src/wasmtime_embedder/system_api_complexity.rs 128 | let cost = match method { 129 | "accept_message" => (500, Static), 130 | "call_cycles_add" | "call_cycles_add128" => (500, Static), 131 | "call_data_append" => (500, Dynamic), 132 | "call_new" => (1500, Static), 133 | "call_on_cleanup" => (500, Static), 134 | "call_perform" => (5000, Static), 135 | "canister_cycle_balance" | "canister_cycle_balance128" => (500, Static), 136 | "canister_self_copy" => (500, Dynamic), 137 | "canister_self_size" => (500, Static), 138 | "canister_status" | "canister_version" => (500, Static), 139 | "certified_data_set" => (500, Dynamic), 140 | "data_certificate_copy" => (500, Dynamic), 141 | "data_certificate_present" | "data_certificate_size" => (500, Static), 142 | "debug_print" => (100, Dynamic), 143 | "global_timer_set" => (500, Static), 144 | "is_controller" => (1000, Dynamic), 145 | "msg_arg_data_copy" => (500, Dynamic), 146 | "msg_arg_data_size" => (500, Static), 147 | "msg_caller_copy" => (500, Dynamic), 148 | "msg_caller_size" => (500, Static), 149 | "msg_cycles_accept" | "msg_cycles_accept128" => (500, Static), 150 | "msg_cycles_available" | "msg_cycles_available128" => (500, Static), 151 | "msg_cycles_refunded" | "msg_cycles_refunded128" => (500, Static), 152 | "cycles_burn128" => (100, Static), 153 | "msg_method_name_copy" => (500, Dynamic), 154 | "msg_method_name_size" => (500, Static), 155 | "msg_reject_code" | "msg_reject_msg_size" => (500, Static), 156 | "msg_reject_msg_copy" => (500, Dynamic), 157 | "msg_reject" => (500, Dynamic), 158 | "msg_reply_data_append" => (500, Dynamic), 159 | "msg_reply" => (500, Static), 160 | "performance_counter" => (200, Static), 161 | "stable_grow" | "stable64_grow" => (100, Static), 162 | "stable_size" | "stable64_size" => (20, Static), 163 | "stable_read" => (20, Dynamic), 164 | "stable_write" => (20, Dynamic), 165 | "stable64_read" => (20, Dynamic64), 166 | "stable64_write" => (20, Dynamic64), 167 | "trap" => (500, Dynamic), 168 | "time" => (500, Static), 169 | _ => (20, Static), 170 | }; 171 | res.insert(func, cost); 172 | } 173 | Self(res) 174 | } 175 | pub fn get_cost(&self, id: FunctionId) -> Option<(i64, InjectionKind)> { 176 | self.0.get(&id).copied() 177 | } 178 | } 179 | pub(crate) fn instr_cost(i: &ir::Instr) -> i64 { 180 | use ir::*; 181 | use BinaryOp::*; 182 | use UnaryOp::*; 183 | // Cost taken from https://github.com/dfinity/ic/blob/master/rs/embedders/src/wasm_utils/instrumentation.rs 184 | match i { 185 | Instr::Block(..) | Instr::Loop(..) => 0, 186 | Instr::Const(..) | Instr::Load(..) | Instr::Store(..) => 1, 187 | Instr::GlobalGet(..) | Instr::GlobalSet(..) => 2, 188 | Instr::TableGet(..) | Instr::TableSet(..) => 5, 189 | Instr::TableGrow(..) | Instr::MemoryGrow(..) => 300, 190 | Instr::MemorySize(..) => 20, 191 | Instr::TableSize(..) => 100, 192 | Instr::MemoryFill(..) | Instr::MemoryCopy(..) | Instr::MemoryInit(..) => 100, 193 | Instr::TableFill(..) | Instr::TableCopy(..) | Instr::TableInit(..) => 100, 194 | Instr::DataDrop(..) | Instr::ElemDrop(..) => 300, 195 | Instr::Call(..) => 5, 196 | Instr::CallIndirect(..) => 10, // missing ReturnCall/Indirect 197 | Instr::IfElse(..) | Instr::Br(..) | Instr::BrIf(..) | Instr::BrTable(..) => 2, 198 | Instr::RefIsNull(..) => 5, 199 | Instr::RefFunc(..) => 130, 200 | Instr::Unop(Unop { op }) => match op { 201 | F32Ceil | F32Floor | F32Trunc | F32Nearest | F32Sqrt => 20, 202 | F64Ceil | F64Floor | F64Trunc | F64Nearest | F64Sqrt => 20, 203 | F32Abs | F32Neg | F64Abs | F64Neg => 2, 204 | F32ConvertSI32 | F64ConvertSI64 | F32ConvertSI64 | F64ConvertSI32 => 3, 205 | F64ConvertUI32 | F32ConvertUI64 | F32ConvertUI32 | F64ConvertUI64 => 16, 206 | I64TruncSF32 | I64TruncUF32 | I64TruncSF64 | I64TruncUF64 => 20, 207 | I32TruncSF32 | I32TruncUF32 | I32TruncSF64 | I32TruncUF64 => 20, // missing TruncSat? 208 | _ => 1, 209 | }, 210 | Instr::Binop(Binop { op }) => match op { 211 | I32DivS | I32DivU | I32RemS | I32RemU => 10, 212 | I64DivS | I64DivU | I64RemS | I64RemU => 10, 213 | F32Add | F32Sub | F32Mul | F32Div | F32Min | F32Max => 20, 214 | F64Add | F64Sub | F64Mul | F64Div | F64Min | F64Max => 20, 215 | F32Copysign | F64Copysign => 2, 216 | F32Eq | F32Ne | F32Lt | F32Gt | F32Le | F32Ge => 3, 217 | F64Eq | F64Ne | F64Lt | F64Gt | F64Le | F64Ge => 3, 218 | _ => 1, 219 | }, 220 | _ => 1, 221 | } 222 | } 223 | 224 | pub(crate) fn get_ic_func_id(m: &mut Module, method: &str) -> FunctionId { 225 | match m.imports.find("ic0", method) { 226 | Some(id) => match m.imports.get(id).kind { 227 | ImportKind::Function(func_id) => func_id, 228 | _ => unreachable!(), 229 | }, 230 | None => { 231 | let ty = match method { 232 | "stable_write" => m 233 | .types 234 | .add(&[ValType::I32, ValType::I32, ValType::I32], &[]), 235 | "stable64_write" => m 236 | .types 237 | .add(&[ValType::I64, ValType::I64, ValType::I64], &[]), 238 | "stable_read" => m 239 | .types 240 | .add(&[ValType::I32, ValType::I32, ValType::I32], &[]), 241 | "stable64_read" => m 242 | .types 243 | .add(&[ValType::I64, ValType::I64, ValType::I64], &[]), 244 | "stable_grow" => m.types.add(&[ValType::I32], &[ValType::I32]), 245 | "stable64_grow" => m.types.add(&[ValType::I64], &[ValType::I64]), 246 | "stable_size" => m.types.add(&[], &[ValType::I32]), 247 | "stable64_size" => m.types.add(&[], &[ValType::I64]), 248 | "call_cycles_add" => m.types.add(&[ValType::I64], &[]), 249 | "call_cycles_add128" => m.types.add(&[ValType::I64, ValType::I64], &[]), 250 | "cycles_burn128" => m 251 | .types 252 | .add(&[ValType::I64, ValType::I64, ValType::I32], &[]), 253 | "call_new" => m.types.add( 254 | &[ 255 | ValType::I32, 256 | ValType::I32, 257 | ValType::I32, 258 | ValType::I32, 259 | ValType::I32, 260 | ValType::I32, 261 | ValType::I32, 262 | ValType::I32, 263 | ], 264 | &[], 265 | ), 266 | "debug_print" => m.types.add(&[ValType::I32, ValType::I32], &[]), 267 | "trap" => m.types.add(&[ValType::I32, ValType::I32], &[]), 268 | "msg_arg_data_size" => m.types.add(&[], &[ValType::I32]), 269 | "msg_arg_data_copy" => m 270 | .types 271 | .add(&[ValType::I32, ValType::I32, ValType::I32], &[]), 272 | "msg_reply_data_append" => m.types.add(&[ValType::I32, ValType::I32], &[]), 273 | "msg_reply" => m.types.add(&[], &[]), 274 | _ => unreachable!(), 275 | }; 276 | m.add_import_func("ic0", method, ty).0 277 | } 278 | } 279 | } 280 | 281 | pub(crate) fn get_memory_id(m: &Module) -> MemoryId { 282 | m.memories 283 | .iter() 284 | .next() 285 | .expect("only single memory is supported") 286 | .id() 287 | } 288 | 289 | pub(crate) fn get_export_func_id(m: &Module, method: &str) -> Option { 290 | let e = m.exports.iter().find(|e| e.name == method)?; 291 | if let ExportItem::Function(id) = e.item { 292 | Some(id) 293 | } else { 294 | None 295 | } 296 | } 297 | pub(crate) fn get_or_create_export_func<'a>( 298 | m: &'a mut Module, 299 | method: &'a str, 300 | ) -> InstrSeqBuilder<'a> { 301 | let id = match get_export_func_id(m, method) { 302 | Some(id) => id, 303 | None => { 304 | let builder = FunctionBuilder::new(&mut m.types, &[], &[]); 305 | let id = builder.finish(vec![], &mut m.funcs); 306 | m.exports.add(method, id); 307 | id 308 | } 309 | }; 310 | get_builder(m, id) 311 | } 312 | 313 | pub(crate) fn get_builder(m: &mut Module, id: FunctionId) -> InstrSeqBuilder<'_> { 314 | if let FunctionKind::Local(func) = &mut m.funcs.get_mut(id).kind { 315 | let id = func.entry_block(); 316 | func.builder_mut().instr_seq(id) 317 | } else { 318 | unreachable!() 319 | } 320 | } 321 | 322 | pub(crate) fn inject_top(builder: &mut InstrSeqBuilder<'_>, instrs: Vec) { 323 | for instr in instrs.into_iter().rev() { 324 | builder.instr_at(0, instr); 325 | } 326 | } 327 | 328 | pub(crate) fn get_exported_methods(m: &Module) -> Vec { 329 | m.exports 330 | .iter() 331 | .filter_map(|e| match e.item { 332 | ExportItem::Function(id) => Some(ExportedMethodInfo { 333 | name: e.name.clone(), 334 | internal_name: get_func_name(m, id), 335 | }), 336 | _ => None, 337 | }) 338 | .collect() 339 | } 340 | 341 | pub(crate) fn get_func_name(m: &Module, id: FunctionId) -> String { 342 | m.funcs 343 | .get(id) 344 | .name 345 | .as_ref() 346 | .unwrap_or(&format!("func_{}", id.index())) 347 | .to_string() 348 | } 349 | 350 | pub(crate) fn is_motoko_canister(m: &Module) -> bool { 351 | m.customs.iter().any(|(_, s)| { 352 | s.name() == "icp:private motoko:compiler" || s.name() == "icp:public motoko:compiler" 353 | }) || m 354 | .exports 355 | .iter() 356 | .any(|e| e.name == "canister_update __motoko_async_helper") 357 | } 358 | 359 | pub(crate) fn is_motoko_wasm_data_section(blob: &[u8]) -> Option<&[u8]> { 360 | let len = blob.len() as u32; 361 | if len > 100 362 | && blob[0..4] == [0x11, 0x00, 0x00, 0x00] // tag for blob 363 | && blob[8..12] == [0x00, 0x61, 0x73, 0x6d] 364 | // Wasm magic number 365 | { 366 | let decoded_len = u32::from_le_bytes(blob[4..8].try_into().unwrap()); 367 | if decoded_len + 8 == len { 368 | return Some(&blob[8..]); 369 | } 370 | } 371 | None 372 | } 373 | 374 | pub(crate) fn get_motoko_wasm_data_sections(m: &Module) -> Vec<(DataId, Module)> { 375 | m.data 376 | .iter() 377 | .filter_map(|d| { 378 | let blob = is_motoko_wasm_data_section(&d.value)?; 379 | let mut config = ModuleConfig::new(); 380 | config.generate_name_section(false); 381 | config.generate_producers_section(false); 382 | let m = config.parse(blob).ok()?; 383 | Some((d.id(), m)) 384 | }) 385 | .collect() 386 | } 387 | 388 | pub(crate) fn encode_module_as_data_section(mut m: Module) -> Vec { 389 | let blob = m.emit_wasm(); 390 | let blob_len = blob.len(); 391 | let mut res = Vec::with_capacity(blob_len + 8); 392 | res.extend_from_slice(&[0x11, 0x00, 0x00, 0x00]); 393 | let encoded_len = (blob_len as u32).to_le_bytes(); 394 | res.extend_from_slice(&encoded_len); 395 | res.extend_from_slice(&blob); 396 | res 397 | } 398 | -------------------------------------------------------------------------------- /src/limit_resource.rs: -------------------------------------------------------------------------------- 1 | use candid::Principal; 2 | use std::collections::{HashMap, HashSet}; 3 | use walrus::ir::*; 4 | use walrus::*; 5 | 6 | pub struct Config { 7 | pub remove_cycles_add: bool, 8 | pub filter_cycles_add: bool, 9 | pub limit_stable_memory_page: Option, 10 | pub limit_heap_memory_page: Option, 11 | pub playground_canister_id: Option, 12 | } 13 | 14 | struct Replacer(HashMap); 15 | impl VisitorMut for Replacer { 16 | fn visit_instr_mut(&mut self, instr: &mut Instr, _: &mut InstrLocId) { 17 | if let Instr::Call(walrus::ir::Call { func }) = instr { 18 | if let Some(new_id) = self.0.get(func) { 19 | *instr = Call { func: *new_id }.into(); 20 | } 21 | } 22 | } 23 | } 24 | impl Replacer { 25 | fn new() -> Self { 26 | Self(HashMap::new()) 27 | } 28 | fn add(&mut self, old: FunctionId, new: FunctionId) { 29 | self.0.insert(old, new); 30 | } 31 | } 32 | 33 | pub fn limit_resource(m: &mut Module, config: &Config) { 34 | let wasm64 = match m.memories.len() { 35 | 0 => false, // Wasm module declares no memory is treated as wasm32 36 | 1 => m.memories.get(m.get_memory_id().unwrap()).memory64, 37 | _ => panic!("The Canister Wasm module should have at most one memory"), 38 | }; 39 | 40 | if let Some(limit) = config.limit_heap_memory_page { 41 | limit_heap_memory(m, limit); 42 | } 43 | 44 | let mut replacer = Replacer::new(); 45 | 46 | if config.remove_cycles_add { 47 | make_cycles_add(m, &mut replacer, wasm64); 48 | make_cycles_add128(m, &mut replacer); 49 | make_cycles_burn128(m, &mut replacer, wasm64); 50 | } 51 | 52 | if config.filter_cycles_add { 53 | // Create a private global set in every invokation of `ic0.call_new` to 54 | // - 0 if subsequent calls to `ic0.call_cycles_add[128]` are allowed; 55 | // - 1 if subsequent calls to `ic0.call_cycles_add[128]` should be filtered (i.e., turned into no-op). 56 | let global_id = m.globals.add_local( 57 | ValType::I32, 58 | true, // mutable 59 | false, // shared (not supported yet) 60 | ConstExpr::Value(Value::I32(0)), 61 | ); 62 | // Instrument `ic0.call_cycles_add[128]` to respect the value of the private global. 63 | make_filter_cycles_add(m, &mut replacer, wasm64, global_id); 64 | make_filter_cycles_add128(m, &mut replacer, global_id); 65 | // Calls to `ic0.cycles_burn128` are always filtered (i.e., turned into no-op). 66 | make_cycles_burn128(m, &mut replacer, wasm64); 67 | // Instrument `ic0.call_new` to set the private global. 68 | make_filter_call_new(m, &mut replacer, wasm64, global_id); 69 | } 70 | 71 | if let Some(limit) = config.limit_stable_memory_page { 72 | make_stable_grow(m, &mut replacer, wasm64, limit as i32); 73 | make_stable64_grow(m, &mut replacer, limit as i64); 74 | } 75 | 76 | if let Some(redirect_id) = config.playground_canister_id { 77 | make_redirect_call_new(m, &mut replacer, wasm64, redirect_id); 78 | } 79 | 80 | let new_ids = replacer.0.values().cloned().collect::>(); 81 | m.funcs.iter_local_mut().for_each(|(id, func)| { 82 | if new_ids.contains(&id) { 83 | return; 84 | } 85 | dfs_pre_order_mut(&mut replacer, func, func.entry_block()); 86 | }); 87 | } 88 | 89 | fn limit_heap_memory(m: &mut Module, limit: u32) { 90 | if let Ok(memory_id) = m.get_memory_id() { 91 | let memory = m.memories.get_mut(memory_id); 92 | let limit = limit as u64; 93 | if memory.initial > limit { 94 | // If memory.initial is greater than the provided limit, it is 95 | // possible there is an active data segment with an offset in the 96 | // range [limit, memory.initial]. 97 | // 98 | // In that case, we don't restrict the heap memory limit as it could 99 | // have undefined behaviour. 100 | 101 | if m.data 102 | .iter() 103 | .filter_map(|data| { 104 | match data.kind { 105 | DataKind::Passive => None, 106 | DataKind::Active { 107 | memory: data_memory_id, 108 | offset, 109 | } => { 110 | if data_memory_id == memory_id { 111 | match offset { 112 | ConstExpr::Value(Value::I32(offset)) => Some(offset as u64), 113 | ConstExpr::Value(Value::I64(offset)) => Some(offset as u64), 114 | _ => { 115 | // It wouldn't pass IC wasm validation 116 | None 117 | } 118 | } 119 | } else { 120 | None 121 | } 122 | } 123 | } 124 | }) 125 | .all(|offset| offset < limit * 65536) 126 | { 127 | memory.initial = limit; 128 | } else { 129 | panic!("Unable to restrict Wasm heap memory to {limit} pages"); 130 | } 131 | } 132 | memory.maximum = Some(limit); 133 | } 134 | } 135 | 136 | fn make_cycles_add(m: &mut Module, replacer: &mut Replacer, wasm64: bool) { 137 | if let Some(old_cycles_add) = get_ic_func_id(m, "call_cycles_add") { 138 | if wasm64 { 139 | panic!("Wasm64 module should not call `call_cycles_add`"); 140 | } 141 | let mut builder = FunctionBuilder::new(&mut m.types, &[ValType::I64], &[]); 142 | let amount = m.locals.add(ValType::I64); 143 | builder.func_body().local_get(amount).drop(); 144 | let new_cycles_add = builder.finish(vec![amount], &mut m.funcs); 145 | replacer.add(old_cycles_add, new_cycles_add); 146 | } 147 | } 148 | 149 | fn make_filter_cycles_add( 150 | m: &mut Module, 151 | replacer: &mut Replacer, 152 | wasm64: bool, 153 | global_id: GlobalId, 154 | ) { 155 | if let Some(old_cycles_add) = get_ic_func_id(m, "call_cycles_add") { 156 | if wasm64 { 157 | panic!("Wasm64 module should not call `call_cycles_add`"); 158 | } 159 | let mut builder = FunctionBuilder::new(&mut m.types, &[ValType::I64], &[]); 160 | let amount = m.locals.add(ValType::I64); 161 | let mut func = builder.func_body(); 162 | // Compare to zero 163 | func.global_get(global_id); 164 | func.i32_const(0); 165 | func.binop(BinaryOp::I32Ne); 166 | // If block 167 | func.if_else( 168 | None, 169 | |then| { 170 | then.local_get(amount).drop(); // no-op 171 | }, 172 | |otherwise| { 173 | otherwise.local_get(amount).call(old_cycles_add); // call `ic0.call_cycles_add` 174 | }, 175 | ); 176 | let new_cycles_add = builder.finish(vec![amount], &mut m.funcs); 177 | replacer.add(old_cycles_add, new_cycles_add); 178 | } 179 | } 180 | 181 | fn make_cycles_add128(m: &mut Module, replacer: &mut Replacer) { 182 | if let Some(old_cycles_add128) = get_ic_func_id(m, "call_cycles_add128") { 183 | let mut builder = FunctionBuilder::new(&mut m.types, &[ValType::I64, ValType::I64], &[]); 184 | let high = m.locals.add(ValType::I64); 185 | let low = m.locals.add(ValType::I64); 186 | builder 187 | .func_body() 188 | .local_get(high) 189 | .local_get(low) 190 | .drop() 191 | .drop(); 192 | let new_cycles_add128 = builder.finish(vec![high, low], &mut m.funcs); 193 | replacer.add(old_cycles_add128, new_cycles_add128); 194 | } 195 | } 196 | 197 | fn make_filter_cycles_add128(m: &mut Module, replacer: &mut Replacer, global_id: GlobalId) { 198 | if let Some(old_cycles_add128) = get_ic_func_id(m, "call_cycles_add128") { 199 | let mut builder = FunctionBuilder::new(&mut m.types, &[ValType::I64, ValType::I64], &[]); 200 | let high = m.locals.add(ValType::I64); 201 | let low = m.locals.add(ValType::I64); 202 | let mut func = builder.func_body(); 203 | // Compare to zero 204 | func.global_get(global_id); 205 | func.i32_const(0); 206 | func.binop(BinaryOp::I32Ne); 207 | // If block 208 | func.if_else( 209 | None, 210 | |then| { 211 | then.local_get(high).local_get(low).drop().drop(); // no-op 212 | }, 213 | |otherwise| { 214 | otherwise 215 | .local_get(high) 216 | .local_get(low) 217 | .call(old_cycles_add128); // call `ic0.call_cycles_add128` 218 | }, 219 | ); 220 | let new_cycles_add128 = builder.finish(vec![high, low], &mut m.funcs); 221 | replacer.add(old_cycles_add128, new_cycles_add128); 222 | } 223 | } 224 | 225 | fn make_cycles_burn128(m: &mut Module, replacer: &mut Replacer, wasm64: bool) { 226 | if let Some(older_cycles_burn128) = get_ic_func_id(m, "cycles_burn128") { 227 | let dst_type = match wasm64 { 228 | true => ValType::I64, 229 | false => ValType::I32, 230 | }; 231 | let mut builder = 232 | FunctionBuilder::new(&mut m.types, &[ValType::I64, ValType::I64, dst_type], &[]); 233 | let high = m.locals.add(ValType::I64); 234 | let low = m.locals.add(ValType::I64); 235 | let dst = m.locals.add(dst_type); 236 | builder 237 | .func_body() 238 | .local_get(high) 239 | .local_get(low) 240 | .local_get(dst) 241 | .drop() 242 | .drop() 243 | .drop(); 244 | let new_cycles_burn128 = builder.finish(vec![high, low, dst], &mut m.funcs); 245 | replacer.add(older_cycles_burn128, new_cycles_burn128); 246 | } 247 | } 248 | 249 | fn make_stable_grow(m: &mut Module, replacer: &mut Replacer, wasm64: bool, limit: i32) { 250 | if let Some(old_stable_grow) = get_ic_func_id(m, "stable_grow") { 251 | if wasm64 { 252 | panic!("Wasm64 module should not call `stable_grow`"); 253 | } 254 | // stable_size is added to import if it wasn't imported 255 | let stable_size = get_ic_func_id(m, "stable_size").unwrap(); 256 | let mut builder = FunctionBuilder::new(&mut m.types, &[ValType::I32], &[ValType::I32]); 257 | let requested = m.locals.add(ValType::I32); 258 | builder 259 | .func_body() 260 | .call(stable_size) 261 | .local_get(requested) 262 | .binop(BinaryOp::I32Add) 263 | .i32_const(limit) 264 | .binop(BinaryOp::I32GtU) 265 | .if_else( 266 | Some(ValType::I32), 267 | |then| { 268 | then.i32_const(-1); 269 | }, 270 | |else_| { 271 | else_.local_get(requested).call(old_stable_grow); 272 | }, 273 | ); 274 | let new_stable_grow = builder.finish(vec![requested], &mut m.funcs); 275 | replacer.add(old_stable_grow, new_stable_grow); 276 | } 277 | } 278 | 279 | fn make_stable64_grow(m: &mut Module, replacer: &mut Replacer, limit: i64) { 280 | if let Some(old_stable64_grow) = get_ic_func_id(m, "stable64_grow") { 281 | // stable64_size is added to import if it wasn't imported 282 | let stable64_size = get_ic_func_id(m, "stable64_size").unwrap(); 283 | let mut builder = FunctionBuilder::new(&mut m.types, &[ValType::I64], &[ValType::I64]); 284 | let requested = m.locals.add(ValType::I64); 285 | builder 286 | .func_body() 287 | .call(stable64_size) 288 | .local_get(requested) 289 | .binop(BinaryOp::I64Add) 290 | .i64_const(limit) 291 | .binop(BinaryOp::I64GtU) 292 | .if_else( 293 | Some(ValType::I64), 294 | |then| { 295 | then.i64_const(-1); 296 | }, 297 | |else_| { 298 | else_.local_get(requested).call(old_stable64_grow); 299 | }, 300 | ); 301 | let new_stable64_grow = builder.finish(vec![requested], &mut m.funcs); 302 | replacer.add(old_stable64_grow, new_stable64_grow); 303 | } 304 | } 305 | 306 | #[allow(clippy::too_many_arguments)] 307 | fn check_list( 308 | memory: MemoryId, 309 | checks: &mut InstrSeqBuilder, 310 | no_redirect: LocalId, 311 | size: LocalId, 312 | src: LocalId, 313 | is_rename: Option, 314 | list: &Vec<&[u8]>, 315 | wasm64: bool, 316 | ) { 317 | let checks_id = checks.id(); 318 | for bytes in list { 319 | checks.block(None, |list_check| { 320 | let list_check_id = list_check.id(); 321 | // Check the length 322 | list_check.local_get(size); 323 | match wasm64 { 324 | true => { 325 | list_check 326 | .i64_const(bytes.len() as i64) 327 | .binop(BinaryOp::I64Ne); 328 | } 329 | false => { 330 | list_check 331 | .i32_const(bytes.len() as i32) 332 | .binop(BinaryOp::I32Ne); 333 | } 334 | } 335 | list_check.br_if(list_check_id); 336 | // Load bytes at src onto the stack 337 | for i in 0..bytes.len() { 338 | list_check.local_get(src).load( 339 | memory, 340 | match wasm64 { 341 | true => LoadKind::I64_8 { 342 | kind: ExtendedLoad::ZeroExtend, 343 | }, 344 | false => LoadKind::I32_8 { 345 | kind: ExtendedLoad::ZeroExtend, 346 | }, 347 | }, 348 | MemArg { 349 | offset: i as u32, 350 | align: 1, 351 | }, 352 | ); 353 | } 354 | for byte in bytes.iter().rev() { 355 | match wasm64 { 356 | true => { 357 | list_check.i64_const(*byte as i64).binop(BinaryOp::I64Ne); 358 | } 359 | false => { 360 | list_check.i32_const(*byte as i32).binop(BinaryOp::I32Ne); 361 | } 362 | } 363 | list_check.br_if(list_check_id); 364 | } 365 | // names were equal, so skip all remaining checks and redirect 366 | if let Some(is_rename) = is_rename { 367 | if bytes == b"http_request" { 368 | list_check.i32_const(1).local_set(is_rename); 369 | } else { 370 | list_check.i32_const(0).local_set(is_rename); 371 | } 372 | } 373 | list_check.i32_const(0).local_set(no_redirect).br(checks_id); 374 | }); 375 | } 376 | // None matched 377 | checks.i32_const(1).local_set(no_redirect); 378 | } 379 | 380 | fn make_redirect_call_new( 381 | m: &mut Module, 382 | replacer: &mut Replacer, 383 | wasm64: bool, 384 | redirect_id: Principal, 385 | ) { 386 | if let Some(old_call_new) = get_ic_func_id(m, "call_new") { 387 | let pointer_type = match wasm64 { 388 | true => ValType::I64, 389 | false => ValType::I32, 390 | }; 391 | let redirect_id = redirect_id.as_slice(); 392 | // Specify the same args as `call_new` so that WASM will correctly check mismatching args 393 | let callee_src = m.locals.add(pointer_type); 394 | let callee_size = m.locals.add(pointer_type); 395 | let name_src = m.locals.add(pointer_type); 396 | let name_size = m.locals.add(pointer_type); 397 | let arg5 = m.locals.add(pointer_type); 398 | let arg6 = m.locals.add(pointer_type); 399 | let arg7 = m.locals.add(pointer_type); 400 | let arg8 = m.locals.add(pointer_type); 401 | 402 | let memory = m 403 | .get_memory_id() 404 | .expect("Canister Wasm module should have only one memory"); 405 | 406 | // Scratch variables 407 | let no_redirect = m.locals.add(ValType::I32); 408 | let is_rename = m.locals.add(ValType::I32); 409 | let mut memory_backup = Vec::new(); 410 | for _ in 0..redirect_id.len() { 411 | memory_backup.push(m.locals.add(pointer_type)); 412 | } 413 | let redirect_canisters = [ 414 | Principal::from_slice(&[]), 415 | Principal::from_text("7hfb6-caaaa-aaaar-qadga-cai").unwrap(), 416 | ]; 417 | 418 | // All functions that require controller permissions or cycles. 419 | // For simplicity, We mingle all canister methods in a single list. 420 | // Method names shouldn't overlap. 421 | let controller_function_names = [ 422 | "create_canister", 423 | "update_settings", 424 | "install_code", 425 | "uninstall_code", 426 | "canister_status", 427 | "stop_canister", 428 | "start_canister", 429 | "delete_canister", 430 | "list_canister_snapshots", 431 | "take_canister_snapshot", 432 | "load_canister_snapshot", 433 | "delete_canister_snapshot", 434 | // These functions doesn't require controller permissions, but needs cycles 435 | "sign_with_ecdsa", 436 | "sign_with_schnorr", 437 | "http_request", // Will be renamed to "_ttp_request", because the name conflicts with the http serving endpoint. 438 | "_ttp_request", // need to redirect renamed function as well, because the second time we see this function, it's already renamed in memory 439 | // methods from evm canister 440 | "eth_call", 441 | "eth_feeHistory", 442 | "eth_getBlockByNumber", 443 | "eth_getLogs", 444 | "eth_getTransactionCount", 445 | "eth_getTransactionReceipt", 446 | "eth_sendRawTransaction", 447 | "request", 448 | ]; 449 | 450 | let mut builder = FunctionBuilder::new( 451 | &mut m.types, 452 | &[ 453 | pointer_type, 454 | pointer_type, 455 | pointer_type, 456 | pointer_type, 457 | pointer_type, 458 | pointer_type, 459 | pointer_type, 460 | pointer_type, 461 | ], 462 | &[], 463 | ); 464 | 465 | builder 466 | .func_body() 467 | .block(None, |checks| { 468 | let checks_id = checks.id(); 469 | // Check if callee address is from redirect_canisters 470 | checks 471 | .block(None, |id_check| { 472 | check_list( 473 | memory, 474 | id_check, 475 | no_redirect, 476 | callee_size, 477 | callee_src, 478 | None, 479 | &redirect_canisters 480 | .iter() 481 | .map(|p| p.as_slice()) 482 | .collect::>(), 483 | wasm64, 484 | ); 485 | }) 486 | .local_get(no_redirect) 487 | .br_if(checks_id); 488 | // Callee address matches, check method name is in the list 489 | check_list( 490 | memory, 491 | checks, 492 | no_redirect, 493 | name_size, 494 | name_src, 495 | Some(is_rename), 496 | &controller_function_names 497 | .iter() 498 | .map(|s| s.as_bytes()) 499 | .collect::>(), 500 | wasm64, 501 | ); 502 | }) 503 | .local_get(no_redirect) 504 | .if_else( 505 | None, 506 | |block| { 507 | // Put all the args back on stack and call call_new without redirecting 508 | block 509 | .local_get(callee_src) 510 | .local_get(callee_size) 511 | .local_get(name_src) 512 | .local_get(name_size) 513 | .local_get(arg5) 514 | .local_get(arg6) 515 | .local_get(arg7) 516 | .local_get(arg8) 517 | .call(old_call_new); 518 | }, 519 | |block| { 520 | // Save current memory starting from address 0 into local variables 521 | for (address, backup_var) in memory_backup.iter().enumerate() { 522 | match wasm64 { 523 | true => { 524 | block 525 | .i64_const(address as i64) 526 | .load( 527 | memory, 528 | LoadKind::I64_8 { 529 | kind: ExtendedLoad::ZeroExtend, 530 | }, 531 | MemArg { 532 | offset: 0, 533 | align: 1, 534 | }, 535 | ) 536 | .local_set(*backup_var); 537 | } 538 | false => { 539 | block 540 | .i32_const(address as i32) 541 | .load( 542 | memory, 543 | LoadKind::I32_8 { 544 | kind: ExtendedLoad::ZeroExtend, 545 | }, 546 | MemArg { 547 | offset: 0, 548 | align: 1, 549 | }, 550 | ) 551 | .local_set(*backup_var); 552 | } 553 | } 554 | } 555 | 556 | // Write the canister id into memory at address 0 557 | for (address, byte) in redirect_id.iter().enumerate() { 558 | match wasm64 { 559 | true => { 560 | block 561 | .i64_const(address as i64) 562 | .i64_const(*byte as i64) 563 | .store( 564 | memory, 565 | StoreKind::I64_8 { atomic: false }, 566 | MemArg { 567 | offset: 0, 568 | align: 1, 569 | }, 570 | ); 571 | } 572 | false => { 573 | block 574 | .i32_const(address as i32) 575 | .i32_const(*byte as i32) 576 | .store( 577 | memory, 578 | StoreKind::I32_8 { atomic: false }, 579 | MemArg { 580 | offset: 0, 581 | align: 1, 582 | }, 583 | ); 584 | } 585 | } 586 | } 587 | block.local_get(is_rename).if_else( 588 | None, 589 | |then| match wasm64 { 590 | true => { 591 | then.local_get(name_src).i64_const('_' as i64).store( 592 | memory, 593 | StoreKind::I64_8 { atomic: false }, 594 | MemArg { 595 | offset: 0, 596 | align: 1, 597 | }, 598 | ); 599 | } 600 | false => { 601 | then.local_get(name_src).i32_const('_' as i32).store( 602 | memory, 603 | StoreKind::I32_8 { atomic: false }, 604 | MemArg { 605 | offset: 0, 606 | align: 1, 607 | }, 608 | ); 609 | } 610 | }, 611 | |_| {}, 612 | ); 613 | match wasm64 { 614 | true => { 615 | block.i64_const(0).i64_const(redirect_id.len() as i64); 616 | } 617 | false => { 618 | block.i32_const(0).i32_const(redirect_id.len() as i32); 619 | } 620 | } 621 | 622 | block 623 | .local_get(name_src) 624 | .local_get(name_size) 625 | .local_get(arg5) 626 | .local_get(arg6) 627 | .local_get(arg7) 628 | .local_get(arg8) 629 | .call(old_call_new); 630 | 631 | // Restore old memory 632 | for (address, byte) in memory_backup.iter().enumerate() { 633 | match wasm64 { 634 | true => { 635 | block.i64_const(address as i64).local_get(*byte).store( 636 | memory, 637 | StoreKind::I64_8 { atomic: false }, 638 | MemArg { 639 | offset: 0, 640 | align: 1, 641 | }, 642 | ); 643 | } 644 | false => { 645 | block.i32_const(address as i32).local_get(*byte).store( 646 | memory, 647 | StoreKind::I32_8 { atomic: false }, 648 | MemArg { 649 | offset: 0, 650 | align: 1, 651 | }, 652 | ); 653 | } 654 | } 655 | } 656 | }, 657 | ); 658 | let new_call_new = builder.finish( 659 | vec![ 660 | callee_src, 661 | callee_size, 662 | name_src, 663 | name_size, 664 | arg5, 665 | arg6, 666 | arg7, 667 | arg8, 668 | ], 669 | &mut m.funcs, 670 | ); 671 | replacer.add(old_call_new, new_call_new); 672 | } 673 | } 674 | 675 | fn make_filter_call_new( 676 | m: &mut Module, 677 | replacer: &mut Replacer, 678 | wasm64: bool, 679 | global_id: GlobalId, 680 | ) { 681 | if let Some(old_call_new) = get_ic_func_id(m, "call_new") { 682 | let pointer_type = match wasm64 { 683 | true => ValType::I64, 684 | false => ValType::I32, 685 | }; 686 | // Specify the same args as `call_new` so that WASM will correctly check mismatching args 687 | let callee_src = m.locals.add(pointer_type); 688 | let callee_size = m.locals.add(pointer_type); 689 | let name_src = m.locals.add(pointer_type); 690 | let name_size = m.locals.add(pointer_type); 691 | let arg5 = m.locals.add(pointer_type); 692 | let arg6 = m.locals.add(pointer_type); 693 | let arg7 = m.locals.add(pointer_type); 694 | let arg8 = m.locals.add(pointer_type); 695 | 696 | let memory = m 697 | .get_memory_id() 698 | .expect("Canister Wasm module should have only one memory"); 699 | 700 | // Scratch variables 701 | let not_allowed_canister = m.locals.add(ValType::I32); 702 | let allow_cycles = m.locals.add(ValType::I32); 703 | 704 | // Cycles transfer is only allowed if 705 | // - the callee is the management canister or `7hfb6-caaaa-aaaar-qadga-cai` (EVM RPC Canister); 706 | // - *and* the method name is *neither* `create_canister` *nor* `deposit_cycles`. 707 | let allowed_canisters = [ 708 | Principal::from_slice(&[]), 709 | Principal::from_text("7hfb6-caaaa-aaaar-qadga-cai").unwrap(), 710 | ]; 711 | let forbidden_function_names = ["create_canister", "deposit_cycles"]; 712 | 713 | let mut builder = FunctionBuilder::new( 714 | &mut m.types, 715 | &[ 716 | pointer_type, 717 | pointer_type, 718 | pointer_type, 719 | pointer_type, 720 | pointer_type, 721 | pointer_type, 722 | pointer_type, 723 | pointer_type, 724 | ], 725 | &[], 726 | ); 727 | 728 | builder 729 | .func_body() 730 | .block(None, |checks| { 731 | let checks_id = checks.id(); 732 | // Check if callee is an allowed canister 733 | checks 734 | .block(None, |id_check| { 735 | // no match (i.e., callee not in `allowed_canisters`) => `not_allowed_canister` set to 1 736 | check_list( 737 | memory, 738 | id_check, 739 | not_allowed_canister, 740 | callee_size, 741 | callee_src, 742 | None, 743 | &allowed_canisters 744 | .iter() 745 | .map(|p| p.as_slice()) 746 | .collect::>(), 747 | wasm64, 748 | ); 749 | }) 750 | .local_get(not_allowed_canister) 751 | .br_if(checks_id); // we already know that callee is not allowed => no need to check further 752 | 753 | // Callee is an allowed canister => check if method name is not forbidden 754 | // no match (i.e., method name not in `forbidden_function_names`) => `allow_cycles` set to 1 755 | check_list( 756 | memory, 757 | checks, 758 | allow_cycles, 759 | name_size, 760 | name_src, 761 | None, 762 | &forbidden_function_names 763 | .iter() 764 | .map(|s| s.as_bytes()) 765 | .collect::>(), 766 | wasm64, 767 | ); 768 | }) 769 | .local_get(allow_cycles) 770 | .if_else( 771 | None, 772 | |block| { 773 | // set global to 0 => allow `ic0.call_cycles_add[128]` 774 | block.i32_const(0).global_set(global_id); 775 | }, 776 | |block| { 777 | // set global to 1 => filter `ic0.call_cycles_add[128]` 778 | block.i32_const(1).global_set(global_id); 779 | }, 780 | ) 781 | .local_get(callee_src) 782 | .local_get(callee_size) 783 | .local_get(name_src) 784 | .local_get(name_size) 785 | .local_get(arg5) 786 | .local_get(arg6) 787 | .local_get(arg7) 788 | .local_get(arg8) 789 | .call(old_call_new); 790 | let new_call_new = builder.finish( 791 | vec![ 792 | callee_src, 793 | callee_size, 794 | name_src, 795 | name_size, 796 | arg5, 797 | arg6, 798 | arg7, 799 | arg8, 800 | ], 801 | &mut m.funcs, 802 | ); 803 | replacer.add(old_call_new, new_call_new); 804 | } 805 | } 806 | 807 | /// Get the FuncionId of a system API in ic0 import. 808 | /// 809 | /// If stable_size or stable64_size is not imported, add them to the module. 810 | fn get_ic_func_id(m: &mut Module, method: &str) -> Option { 811 | match m.imports.find("ic0", method) { 812 | Some(id) => match m.imports.get(id).kind { 813 | ImportKind::Function(func_id) => Some(func_id), 814 | _ => unreachable!(), 815 | }, 816 | None => { 817 | let ty = match method { 818 | "stable_size" => Some(m.types.add(&[], &[ValType::I32])), 819 | "stable64_size" => Some(m.types.add(&[], &[ValType::I64])), 820 | _ => None, 821 | }; 822 | match ty { 823 | Some(ty) => { 824 | let func_id = m.add_import_func("ic0", method, ty).0; 825 | Some(func_id) 826 | } 827 | None => None, 828 | } 829 | } 830 | } 831 | } 832 | -------------------------------------------------------------------------------- /src/instrumentation.rs: -------------------------------------------------------------------------------- 1 | use walrus::ir::*; 2 | use walrus::*; 3 | 4 | use crate::utils::*; 5 | use std::collections::HashSet; 6 | 7 | const METADATA_SIZE: i32 = 24; 8 | const DEFAULT_PAGE_LIMIT: i32 = 16 * 256; // 256M 9 | const LOG_ITEM_SIZE: i32 = 12; 10 | const MAX_ITEMS_PER_QUERY: i32 = 174758; // (2M - 40) / LOG_ITEM_SIZE; 11 | 12 | struct InjectionPoint { 13 | position: usize, 14 | cost: i64, 15 | kind: InjectionKind, 16 | } 17 | impl InjectionPoint { 18 | fn new() -> Self { 19 | InjectionPoint { 20 | position: 0, 21 | cost: 0, 22 | kind: InjectionKind::Static, 23 | } 24 | } 25 | } 26 | 27 | struct Variables { 28 | total_counter: GlobalId, 29 | log_size: GlobalId, 30 | page_size: GlobalId, 31 | is_init: GlobalId, 32 | is_entry: GlobalId, 33 | dynamic_counter_func: FunctionId, 34 | dynamic_counter64_func: FunctionId, 35 | } 36 | 37 | pub struct Config { 38 | pub trace_only_funcs: Vec, 39 | pub start_address: Option, 40 | pub page_limit: Option, 41 | } 42 | impl Config { 43 | pub fn is_preallocated(&self) -> bool { 44 | self.start_address.is_some() 45 | } 46 | pub fn log_start_address(&self) -> i64 { 47 | self.start_address.unwrap_or(0) + METADATA_SIZE as i64 48 | } 49 | pub fn metadata_start_address(&self) -> i64 { 50 | self.start_address.unwrap_or(0) 51 | } 52 | pub fn page_limit(&self) -> i64 { 53 | i64::from( 54 | self.page_limit 55 | .map(|x| x - 1) 56 | .unwrap_or(DEFAULT_PAGE_LIMIT - 1), 57 | ) // minus 1 because of metadata 58 | } 59 | } 60 | 61 | /// When trace_only_funcs is not empty, counting and tracing is only enabled for those listed functions per update call. 62 | /// TODO: doesn't handle recursive entry functions. Need to create a wrapper for the recursive entry function. 63 | pub fn instrument(m: &mut Module, config: Config) -> Result<(), String> { 64 | let mut trace_only_ids = HashSet::new(); 65 | for name in config.trace_only_funcs.iter() { 66 | let id = match m.funcs.by_name(name) { 67 | Some(id) => id, 68 | None => return Err(format!("func \"{name}\" not found")), 69 | }; 70 | trace_only_ids.insert(id); 71 | } 72 | let is_partial_tracing = !trace_only_ids.is_empty(); 73 | let func_cost = FunctionCost::new(m); 74 | let total_counter = 75 | m.globals 76 | .add_local(ValType::I64, true, false, ConstExpr::Value(Value::I64(0))); 77 | let log_size = m 78 | .globals 79 | .add_local(ValType::I32, true, false, ConstExpr::Value(Value::I32(0))); 80 | let page_size = m 81 | .globals 82 | .add_local(ValType::I32, true, false, ConstExpr::Value(Value::I32(0))); 83 | let is_init = m 84 | .globals 85 | .add_local(ValType::I32, true, false, ConstExpr::Value(Value::I32(1))); 86 | let is_entry = m 87 | .globals 88 | .add_local(ValType::I32, true, false, ConstExpr::Value(Value::I32(0))); 89 | let opt_init = if is_partial_tracing { 90 | Some(is_init) 91 | } else { 92 | None 93 | }; 94 | let dynamic_counter_func = make_dynamic_counter(m, total_counter, &opt_init); 95 | let dynamic_counter64_func = make_dynamic_counter64(m, total_counter, &opt_init); 96 | let vars = Variables { 97 | total_counter, 98 | log_size, 99 | is_init, 100 | is_entry, 101 | dynamic_counter_func, 102 | dynamic_counter64_func, 103 | page_size, 104 | }; 105 | 106 | for (id, func) in m.funcs.iter_local_mut() { 107 | if id != dynamic_counter_func && id != dynamic_counter64_func { 108 | inject_metering( 109 | func, 110 | func.entry_block(), 111 | &vars, 112 | &func_cost, 113 | is_partial_tracing, 114 | ); 115 | } 116 | } 117 | let writer = make_stable_writer(m, &vars, &config); 118 | let printer = make_printer(m, &vars, writer); 119 | for (id, func) in m.funcs.iter_local_mut() { 120 | if id != printer 121 | && id != writer 122 | && id != dynamic_counter_func 123 | && id != dynamic_counter64_func 124 | { 125 | let is_partial_tracing = trace_only_ids.contains(&id); 126 | inject_profiling_prints(&m.types, printer, id, func, is_partial_tracing, &vars); 127 | } 128 | } 129 | if !is_partial_tracing { 130 | //inject_start(m, vars.is_init); 131 | inject_init(m, vars.is_init); 132 | } 133 | // Persist globals 134 | inject_pre_upgrade(m, &vars, &config); 135 | inject_post_upgrade(m, &vars, &config); 136 | 137 | inject_canister_methods(m, &vars); 138 | let leb = make_leb128_encoder(m); 139 | make_stable_getter(m, &vars, leb, &config); 140 | make_getter(m, &vars); 141 | make_toggle_func(m, "__toggle_tracing", vars.is_init); 142 | make_toggle_func(m, "__toggle_entry", vars.is_entry); 143 | let name = make_name_section(m); 144 | m.customs.add(name); 145 | Ok(()) 146 | } 147 | 148 | fn inject_metering( 149 | func: &mut LocalFunction, 150 | start: InstrSeqId, 151 | vars: &Variables, 152 | func_cost: &FunctionCost, 153 | is_partial_tracing: bool, 154 | ) { 155 | use InjectionKind::*; 156 | let mut stack = vec![start]; 157 | while let Some(seq_id) = stack.pop() { 158 | let seq = func.block(seq_id); 159 | // Finding injection points 160 | let mut injection_points = vec![]; 161 | let mut curr = InjectionPoint::new(); 162 | // each function has at least a unit cost 163 | if seq_id == start { 164 | curr.cost += 1; 165 | } 166 | for (pos, (instr, _)) in seq.instrs.iter().enumerate() { 167 | curr.position = pos; 168 | match instr { 169 | Instr::Block(Block { seq }) | Instr::Loop(Loop { seq }) => { 170 | match func.block(*seq).ty { 171 | InstrSeqType::Simple(Some(_)) => curr.cost += instr_cost(instr), 172 | InstrSeqType::Simple(None) => (), 173 | InstrSeqType::MultiValue(_) => unreachable!("Multivalue not supported"), 174 | } 175 | stack.push(*seq); 176 | injection_points.push(curr); 177 | curr = InjectionPoint::new(); 178 | } 179 | Instr::IfElse(IfElse { 180 | consequent, 181 | alternative, 182 | }) => { 183 | curr.cost += instr_cost(instr); 184 | stack.push(*consequent); 185 | stack.push(*alternative); 186 | injection_points.push(curr); 187 | curr = InjectionPoint::new(); 188 | } 189 | Instr::Br(_) | Instr::BrIf(_) | Instr::BrTable(_) => { 190 | // br always points to a block, so we don't need to push the br block to stack for traversal 191 | curr.cost += instr_cost(instr); 192 | injection_points.push(curr); 193 | curr = InjectionPoint::new(); 194 | } 195 | Instr::Return(_) | Instr::Unreachable(_) => { 196 | curr.cost += instr_cost(instr); 197 | injection_points.push(curr); 198 | curr = InjectionPoint::new(); 199 | } 200 | Instr::Call(Call { func }) => { 201 | curr.cost += instr_cost(instr); 202 | match func_cost.get_cost(*func) { 203 | Some((cost, InjectionKind::Static)) => curr.cost += cost, 204 | Some((cost, kind @ InjectionKind::Dynamic)) 205 | | Some((cost, kind @ InjectionKind::Dynamic64)) => { 206 | curr.cost += cost; 207 | let dynamic = InjectionPoint { 208 | position: pos, 209 | cost: 0, 210 | kind, 211 | }; 212 | injection_points.push(dynamic); 213 | } 214 | None => {} 215 | } 216 | } 217 | Instr::MemoryFill(_) 218 | | Instr::MemoryCopy(_) 219 | | Instr::MemoryInit(_) 220 | | Instr::TableCopy(_) 221 | | Instr::TableInit(_) => { 222 | curr.cost += instr_cost(instr); 223 | let dynamic = InjectionPoint { 224 | position: pos, 225 | cost: 0, 226 | kind: InjectionKind::Dynamic, 227 | }; 228 | injection_points.push(dynamic); 229 | } 230 | _ => { 231 | curr.cost += instr_cost(instr); 232 | } 233 | } 234 | } 235 | injection_points.push(curr); 236 | // Reconstruct instructions 237 | let injection_points = injection_points 238 | .iter() 239 | .filter(|point| point.cost > 0 || point.kind != Static); 240 | let mut builder = func.builder_mut().instr_seq(seq_id); 241 | let original = builder.instrs_mut(); 242 | let mut instrs = vec![]; 243 | let mut last_injection_position = 0; 244 | for point in injection_points { 245 | instrs.extend_from_slice(&original[last_injection_position..point.position]); 246 | // injection happens one instruction before the injection_points, so the cost contains 247 | // the control flow instruction. 248 | match point.kind { 249 | Static => { 250 | #[rustfmt::skip] 251 | instrs.extend_from_slice(&[ 252 | (GlobalGet { global: vars.total_counter }.into(), Default::default()), 253 | (Const { value: Value::I64(point.cost) }.into(), Default::default()), 254 | ]); 255 | if is_partial_tracing { 256 | #[rustfmt::skip] 257 | instrs.extend_from_slice(&[ 258 | (GlobalGet { global: vars.is_init }.into(), Default::default()), 259 | (Const { value: Value::I32(1) }.into(), Default::default()), 260 | (Binop { op: BinaryOp::I32Xor }.into(), Default::default()), 261 | (Unop { op: UnaryOp::I64ExtendUI32 }.into(), Default::default()), 262 | (Binop { op: BinaryOp::I64Mul }.into(), Default::default()), 263 | ]); 264 | } 265 | #[rustfmt::skip] 266 | instrs.extend_from_slice(&[ 267 | (Binop { op: BinaryOp::I64Add }.into(), Default::default()), 268 | (GlobalSet { global: vars.total_counter }.into(), Default::default()), 269 | ]); 270 | } 271 | Dynamic => { 272 | // Assume top of the stack is the i32 size parameter 273 | #[rustfmt::skip] 274 | instrs.push((Call { func: vars.dynamic_counter_func }.into(), Default::default())); 275 | } 276 | Dynamic64 => { 277 | #[rustfmt::skip] 278 | instrs.push((Call { func: vars.dynamic_counter64_func }.into(), Default::default())); 279 | } 280 | }; 281 | last_injection_position = point.position; 282 | } 283 | instrs.extend_from_slice(&original[last_injection_position..]); 284 | *original = instrs; 285 | } 286 | } 287 | 288 | fn inject_profiling_prints( 289 | types: &ModuleTypes, 290 | printer: FunctionId, 291 | id: FunctionId, 292 | func: &mut LocalFunction, 293 | is_partial_tracing: bool, 294 | vars: &Variables, 295 | ) { 296 | // Put the original function body inside a block, so that if the code 297 | // use br_if/br_table to exit the function, we can still output the exit signal. 298 | let start_id = func.entry_block(); 299 | let original_block = func.block_mut(start_id); 300 | let start_instrs = original_block.instrs.split_off(0); 301 | let start_ty = match original_block.ty { 302 | InstrSeqType::MultiValue(id) => { 303 | let valtypes = types.results(id); 304 | InstrSeqType::Simple(match valtypes.len() { 305 | 0 => None, 306 | 1 => Some(valtypes[0]), 307 | _ => unreachable!("Multivalue return not supported"), 308 | }) 309 | } 310 | // top-level block is using the function signature 311 | InstrSeqType::Simple(_) => unreachable!(), 312 | }; 313 | let mut inner_start = func.builder_mut().dangling_instr_seq(start_ty); 314 | *(inner_start.instrs_mut()) = start_instrs; 315 | let inner_start_id = inner_start.id(); 316 | let mut start_builder = func.builder_mut().func_body(); 317 | if is_partial_tracing { 318 | start_builder.i32_const(0).global_set(vars.is_init); 319 | } 320 | start_builder 321 | .i32_const(id.index() as i32) 322 | .call(printer) 323 | .instr(Block { 324 | seq: inner_start_id, 325 | }) 326 | // TOOD fix when id == 0 327 | .i32_const(-(id.index() as i32)) 328 | .call(printer); 329 | // TODO this only works for non-recursive entry function 330 | if is_partial_tracing { 331 | start_builder.i32_const(1).global_set(vars.is_init); 332 | } 333 | let mut stack = vec![inner_start_id]; 334 | while let Some(seq_id) = stack.pop() { 335 | let mut builder = func.builder_mut().instr_seq(seq_id); 336 | let original = builder.instrs_mut(); 337 | let mut instrs = vec![]; 338 | for (instr, loc) in original.iter() { 339 | match instr { 340 | Instr::Block(Block { seq }) | Instr::Loop(Loop { seq }) => { 341 | stack.push(*seq); 342 | instrs.push((instr.clone(), *loc)); 343 | } 344 | Instr::IfElse(IfElse { 345 | consequent, 346 | alternative, 347 | }) => { 348 | stack.push(*alternative); 349 | stack.push(*consequent); 350 | instrs.push((instr.clone(), *loc)); 351 | } 352 | Instr::Return(_) => { 353 | instrs.push(( 354 | Instr::Br(Br { 355 | block: inner_start_id, 356 | }), 357 | *loc, 358 | )); 359 | } 360 | // redirect br,br_if,br_table to inner seq id 361 | Instr::Br(Br { block }) if *block == start_id => { 362 | instrs.push(( 363 | Instr::Br(Br { 364 | block: inner_start_id, 365 | }), 366 | *loc, 367 | )); 368 | } 369 | Instr::BrIf(BrIf { block }) if *block == start_id => { 370 | instrs.push(( 371 | Instr::BrIf(BrIf { 372 | block: inner_start_id, 373 | }), 374 | *loc, 375 | )); 376 | } 377 | Instr::BrTable(BrTable { blocks, default }) => { 378 | let mut blocks = blocks.clone(); 379 | for i in 0..blocks.len() { 380 | if let Some(id) = blocks.get_mut(i) { 381 | if *id == start_id { 382 | *id = inner_start_id 383 | }; 384 | } 385 | } 386 | let default = if *default == start_id { 387 | inner_start_id 388 | } else { 389 | *default 390 | }; 391 | instrs.push((Instr::BrTable(BrTable { blocks, default }), *loc)); 392 | } 393 | _ => instrs.push((instr.clone(), *loc)), 394 | } 395 | } 396 | *original = instrs; 397 | } 398 | } 399 | 400 | fn make_dynamic_counter( 401 | m: &mut Module, 402 | total_counter: GlobalId, 403 | opt_init: &Option, 404 | ) -> FunctionId { 405 | let mut builder = FunctionBuilder::new(&mut m.types, &[ValType::I32], &[ValType::I32]); 406 | let size = m.locals.add(ValType::I32); 407 | let mut seq = builder.func_body(); 408 | seq.local_get(size); 409 | if let Some(is_init) = opt_init { 410 | seq.global_get(*is_init) 411 | .i32_const(1) 412 | .binop(BinaryOp::I32Xor) 413 | .binop(BinaryOp::I32Mul); 414 | } 415 | seq.unop(UnaryOp::I64ExtendUI32) 416 | .global_get(total_counter) 417 | .binop(BinaryOp::I64Add) 418 | .global_set(total_counter) 419 | .local_get(size); 420 | builder.finish(vec![size], &mut m.funcs) 421 | } 422 | fn make_dynamic_counter64( 423 | m: &mut Module, 424 | total_counter: GlobalId, 425 | opt_init: &Option, 426 | ) -> FunctionId { 427 | let mut builder = FunctionBuilder::new(&mut m.types, &[ValType::I64], &[ValType::I64]); 428 | let size = m.locals.add(ValType::I64); 429 | let mut seq = builder.func_body(); 430 | seq.local_get(size); 431 | if let Some(is_init) = opt_init { 432 | seq.global_get(*is_init) 433 | .i32_const(1) 434 | .binop(BinaryOp::I32Xor) 435 | .unop(UnaryOp::I64ExtendUI32) 436 | .binop(BinaryOp::I64Mul); 437 | } 438 | seq.global_get(total_counter) 439 | .binop(BinaryOp::I64Add) 440 | .global_set(total_counter) 441 | .local_get(size); 442 | builder.finish(vec![size], &mut m.funcs) 443 | } 444 | fn make_stable_writer(m: &mut Module, vars: &Variables, config: &Config) -> FunctionId { 445 | let writer = get_ic_func_id(m, "stable64_write"); 446 | let grow = get_ic_func_id(m, "stable64_grow"); 447 | let mut builder = FunctionBuilder::new( 448 | &mut m.types, 449 | &[ValType::I64, ValType::I64, ValType::I64], 450 | &[], 451 | ); 452 | let start_address = config.log_start_address(); 453 | let size_limit = config.page_limit() * 65536; 454 | let is_preallocated = config.is_preallocated(); 455 | let offset = m.locals.add(ValType::I64); 456 | let src = m.locals.add(ValType::I64); 457 | let size = m.locals.add(ValType::I64); 458 | builder 459 | .func_body() 460 | .local_get(offset) 461 | .local_get(size) 462 | .binop(BinaryOp::I64Add); 463 | if is_preallocated { 464 | builder.func_body().i64_const(size_limit); 465 | } else { 466 | builder 467 | .func_body() 468 | .global_get(vars.page_size) 469 | .i32_const(65536) 470 | .binop(BinaryOp::I32Mul) 471 | .i32_const(METADATA_SIZE) 472 | .binop(BinaryOp::I32Sub) 473 | // SI because it can be negative 474 | .unop(UnaryOp::I64ExtendSI32); 475 | } 476 | builder 477 | .func_body() 478 | .binop(BinaryOp::I64GtS) 479 | .if_else( 480 | None, 481 | |then| { 482 | if is_preallocated { 483 | then.return_(); 484 | } else { 485 | // This assumes user code doesn't use stable memory 486 | then.global_get(vars.page_size) 487 | .i32_const(DEFAULT_PAGE_LIMIT) 488 | .binop(BinaryOp::I32GtS) // trace > default_page_limit 489 | .if_else( 490 | None, 491 | |then| { 492 | then.return_(); 493 | }, 494 | |else_| { 495 | else_ 496 | .i64_const(1) 497 | .call(grow) 498 | .drop() 499 | .global_get(vars.page_size) 500 | .i32_const(1) 501 | .binop(BinaryOp::I32Add) 502 | .global_set(vars.page_size); 503 | }, 504 | ); 505 | } 506 | }, 507 | |_| {}, 508 | ) 509 | .i64_const(start_address) 510 | .local_get(offset) 511 | .binop(BinaryOp::I64Add) 512 | .local_get(src) 513 | .local_get(size) 514 | .call(writer) 515 | .global_get(vars.log_size) 516 | .i32_const(1) 517 | .binop(BinaryOp::I32Add) 518 | .global_set(vars.log_size); 519 | builder.finish(vec![offset, src, size], &mut m.funcs) 520 | } 521 | 522 | fn make_printer(m: &mut Module, vars: &Variables, writer: FunctionId) -> FunctionId { 523 | let memory = get_memory_id(m); 524 | let mut builder = FunctionBuilder::new(&mut m.types, &[ValType::I32], &[]); 525 | let func_id = m.locals.add(ValType::I32); 526 | let a = m.locals.add(ValType::I32); 527 | let b = m.locals.add(ValType::I64); 528 | builder.func_body().global_get(vars.is_init).if_else( 529 | None, 530 | |then| { 531 | then.return_(); 532 | }, 533 | |else_| { 534 | #[rustfmt::skip] 535 | else_ 536 | // backup memory 537 | .i32_const(0) 538 | .load(memory, LoadKind::I32 { atomic: false }, MemArg { offset: 0, align: 4}) 539 | .local_set(a) 540 | .i32_const(4) 541 | .load(memory, LoadKind::I64 { atomic: false }, MemArg { offset: 0, align: 8}) 542 | .local_set(b) 543 | // print 544 | .i32_const(0) 545 | .local_get(func_id) 546 | .store(memory, StoreKind::I32 { atomic: false }, MemArg { offset: 0, align: 4 }) 547 | .i32_const(4) 548 | .global_get(vars.total_counter) 549 | .store(memory, StoreKind::I64 { atomic: false }, MemArg { offset: 0, align: 8 }) 550 | .global_get(vars.log_size) 551 | .unop(UnaryOp::I64ExtendUI32) 552 | .i64_const(LOG_ITEM_SIZE as i64) 553 | .binop(BinaryOp::I64Mul) 554 | .i64_const(0) 555 | .i64_const(LOG_ITEM_SIZE as i64) 556 | .call(writer) 557 | // restore memory 558 | .i32_const(0) 559 | .local_get(a) 560 | .store(memory, StoreKind::I32 { atomic: false }, MemArg { offset: 0, align: 4 }) 561 | .i32_const(4) 562 | .local_get(b) 563 | .store(memory, StoreKind::I64 { atomic: false }, MemArg { offset: 0, align: 8 }); 564 | }, 565 | ); 566 | builder.finish(vec![func_id], &mut m.funcs) 567 | } 568 | /* 569 | // We can use this function once we have a system memroy for logs. 570 | // Otherwise, we cannot call stable_write in canister_init 571 | fn inject_start(m: &mut Module, is_init: GlobalId) { 572 | if let Some(id) = m.start { 573 | let mut builder = get_builder(m, id); 574 | #[rustfmt::skip] 575 | builder 576 | .instr(Const { value: Value::I32(0) }) 577 | .instr(GlobalSet { global: is_init }); 578 | } 579 | } 580 | */ 581 | fn inject_canister_methods(m: &mut Module, vars: &Variables) { 582 | let methods: Vec<_> = m 583 | .exports 584 | .iter() 585 | .filter_map(|e| match e.item { 586 | ExportItem::Function(id) 587 | if e.name != "canister_update __motoko_async_helper" 588 | && (e.name.starts_with("canister_update") 589 | || e.name.starts_with("canister_query") 590 | || e.name.starts_with("canister_composite_query") 591 | || e.name.starts_with("canister_heartbeat") 592 | // don't clear logs for timer and post_upgrade, as they are trigger by other signals 593 | //|| e.name == "canister_global_timer" 594 | //|| e.name == "canister_post_upgrade" 595 | || e.name == "canister_pre_upgrade") => 596 | { 597 | Some(id) 598 | } 599 | _ => None, 600 | }) 601 | .collect(); 602 | for id in methods.iter() { 603 | let mut builder = get_builder(m, *id); 604 | #[rustfmt::skip] 605 | inject_top( 606 | &mut builder, 607 | vec![ 608 | // log_size = is_entry ? log_size : 0 609 | GlobalGet { global: vars.is_entry }.into(), 610 | GlobalGet { global: vars.log_size }.into(), 611 | Binop { op: BinaryOp::I32Mul }.into(), 612 | GlobalSet { global: vars.log_size }.into(), 613 | ], 614 | ); 615 | } 616 | } 617 | fn inject_init(m: &mut Module, is_init: GlobalId) { 618 | let mut builder = get_or_create_export_func(m, "canister_init"); 619 | // canister_init in Motoko use stable_size to decide if there is stable memory to deserialize. 620 | // Region initialization in Motoko is also done here. 621 | // We can only enable profiling at the end of init, otherwise stable.grow breaks this check. 622 | builder.i32_const(0).global_set(is_init); 623 | } 624 | fn inject_pre_upgrade(m: &mut Module, vars: &Variables, config: &Config) { 625 | let writer = get_ic_func_id(m, "stable64_write"); 626 | let memory = get_memory_id(m); 627 | let a = m.locals.add(ValType::I64); 628 | let b = m.locals.add(ValType::I64); 629 | let c = m.locals.add(ValType::I64); 630 | let mut builder = get_or_create_export_func(m, "canister_pre_upgrade"); 631 | #[rustfmt::skip] 632 | builder 633 | // backup memory. This is not strictly needed, since it's at the end of pre-upgrade. 634 | .i32_const(0) 635 | .load(memory, LoadKind::I64 { atomic: false }, MemArg { offset: 0, align: 8}) 636 | .local_set(a) 637 | .i32_const(8) 638 | .load(memory, LoadKind::I64 { atomic: false }, MemArg { offset: 0, align: 8}) 639 | .local_set(b) 640 | .i32_const(16) 641 | .load(memory, LoadKind::I64 { atomic: false }, MemArg { offset: 0, align: 8}) 642 | .local_set(c) 643 | // persist globals 644 | .i32_const(0) 645 | .global_get(vars.total_counter) 646 | .store(memory, StoreKind::I64 { atomic: false }, MemArg { offset: 0, align: 8 }) 647 | .i32_const(8) 648 | .global_get(vars.log_size) 649 | .store(memory, StoreKind::I32 { atomic: false }, MemArg { offset: 0, align: 4 }) 650 | .i32_const(12) 651 | .global_get(vars.page_size) 652 | .store(memory, StoreKind::I32 { atomic: false }, MemArg { offset: 0, align: 4 }) 653 | .i32_const(16) 654 | .global_get(vars.is_init) 655 | .store(memory, StoreKind::I32 { atomic: false }, MemArg { offset: 0, align: 4 }) 656 | .i32_const(20) 657 | .global_get(vars.is_entry) 658 | .store(memory, StoreKind::I32 { atomic: false }, MemArg { offset: 0, align: 4 }) 659 | .i64_const(config.metadata_start_address()) 660 | .i64_const(0) 661 | .i64_const(METADATA_SIZE as i64) 662 | .call(writer) 663 | // restore memory 664 | .i32_const(0) 665 | .local_get(a) 666 | .store(memory, StoreKind::I64 { atomic: false }, MemArg { offset: 0, align: 8 }) 667 | .i32_const(8) 668 | .local_get(b) 669 | .store(memory, StoreKind::I64 { atomic: false }, MemArg { offset: 0, align: 8 }) 670 | .i32_const(16) 671 | .local_get(c) 672 | .store(memory, StoreKind::I64 { atomic: false }, MemArg { offset: 0, align: 8 }); 673 | } 674 | fn inject_post_upgrade(m: &mut Module, vars: &Variables, config: &Config) { 675 | let reader = get_ic_func_id(m, "stable64_read"); 676 | let memory = get_memory_id(m); 677 | let a = m.locals.add(ValType::I64); 678 | let b = m.locals.add(ValType::I64); 679 | let c = m.locals.add(ValType::I64); 680 | let mut builder = get_or_create_export_func(m, "canister_post_upgrade"); 681 | #[rustfmt::skip] 682 | inject_top(&mut builder, vec![ 683 | // backup 684 | Const { value: Value::I32(0) }.into(), 685 | Load { memory, kind: LoadKind::I64 { atomic: false }, arg: MemArg { offset: 0, align: 8 } }.into(), 686 | LocalSet { local: a }.into(), 687 | Const { value: Value::I32(8) }.into(), 688 | Load { memory, kind: LoadKind::I64 { atomic: false }, arg: MemArg { offset: 0, align: 8 } }.into(), 689 | LocalSet { local: b }.into(), 690 | Const { value: Value::I32(16) }.into(), 691 | Load { memory, kind: LoadKind::I64 { atomic: false }, arg: MemArg { offset: 0, align: 8 } }.into(), 692 | LocalSet { local: c }.into(), 693 | // load from stable memory 694 | Const { value: Value::I64(0) }.into(), 695 | Const { value: Value::I64(config.metadata_start_address()) }.into(), 696 | Const { value: Value::I64(METADATA_SIZE as i64) }.into(), 697 | Call { func: reader }.into(), 698 | Const { value: Value::I32(0) }.into(), 699 | Load { memory, kind: LoadKind::I64 { atomic: false }, arg: MemArg { offset: 0, align: 8 } }.into(), 700 | GlobalSet { global: vars.total_counter }.into(), 701 | Const { value: Value::I32(8) }.into(), 702 | Load { memory, kind: LoadKind::I32 { atomic: false }, arg: MemArg { offset: 0, align: 4 } }.into(), 703 | GlobalSet { global: vars.log_size }.into(), 704 | Const { value: Value::I32(12) }.into(), 705 | Load { memory, kind: LoadKind::I32 { atomic: false }, arg: MemArg { offset: 0, align: 4 } }.into(), 706 | GlobalSet { global: vars.page_size }.into(), 707 | Const { value: Value::I32(16) }.into(), 708 | Load { memory, kind: LoadKind::I32 { atomic: false }, arg: MemArg { offset: 0, align: 4 } }.into(), 709 | GlobalSet { global: vars.is_init }.into(), 710 | Const { value: Value::I32(20) }.into(), 711 | Load { memory, kind: LoadKind::I32 { atomic: false }, arg: MemArg { offset: 0, align: 4 } }.into(), 712 | GlobalSet { global: vars.is_entry }.into(), 713 | // restore 714 | Const { value: Value::I32(0) }.into(), 715 | LocalGet { local: a }.into(), 716 | Store { memory, kind: StoreKind::I64 { atomic: false }, arg: MemArg { offset: 0, align: 8 } }.into(), 717 | Const { value: Value::I32(8) }.into(), 718 | LocalGet { local: b }.into(), 719 | Store { memory, kind: StoreKind::I64 { atomic: false }, arg: MemArg { offset: 0, align: 8 } }.into(), 720 | Const { value: Value::I32(16) }.into(), 721 | LocalGet { local: c }.into(), 722 | Store { memory, kind: StoreKind::I64 { atomic: false }, arg: MemArg { offset: 0, align: 8 } }.into(), 723 | ]); 724 | } 725 | 726 | fn make_stable_getter(m: &mut Module, vars: &Variables, leb: FunctionId, config: &Config) { 727 | let memory = get_memory_id(m); 728 | let arg_size = get_ic_func_id(m, "msg_arg_data_size"); 729 | let arg_copy = get_ic_func_id(m, "msg_arg_data_copy"); 730 | let reply_data = get_ic_func_id(m, "msg_reply_data_append"); 731 | let reply = get_ic_func_id(m, "msg_reply"); 732 | let trap = get_ic_func_id(m, "trap"); 733 | let reader = get_ic_func_id(m, "stable64_read"); 734 | let idx = m.locals.add(ValType::I32); 735 | let len = m.locals.add(ValType::I32); 736 | let next_idx = m.locals.add(ValType::I32); 737 | let mut builder = FunctionBuilder::new(&mut m.types, &[], &[]); 738 | builder.name("__get_profiling".to_string()); 739 | #[rustfmt::skip] 740 | builder.func_body() 741 | // allocate 2M of heap memory, it's a query call, the system will give back the memory. 742 | .memory_size(memory) 743 | .i32_const(32) 744 | .binop(BinaryOp::I32LtU) 745 | .if_else( 746 | None, 747 | |then| { 748 | then 749 | .i32_const(32) 750 | .memory_grow(memory) 751 | .drop(); 752 | }, 753 | |_| {} 754 | ) 755 | // parse input idx 756 | .call(arg_size) 757 | .i32_const(11) 758 | .binop(BinaryOp::I32Ne) 759 | .if_else( 760 | None, 761 | |then| { 762 | then.i32_const(0) 763 | .i32_const(0) 764 | .call(trap); 765 | }, 766 | |_| {}, 767 | ) 768 | .i32_const(0) 769 | .i32_const(7) 770 | .i32_const(4) 771 | .call(arg_copy) 772 | .i32_const(0) 773 | .load(memory, LoadKind::I32 { atomic: false }, MemArg { offset: 0, align: 4}) 774 | .local_set(idx) 775 | // write header (vec { record { int32; int64 } }, opt int32) 776 | .i32_const(0) 777 | .i64_const(0x6c016d034c444944) // "DIDL036d016c" 778 | .store(memory, StoreKind::I64 { atomic: false }, MemArg { offset: 0, align: 8 }) 779 | .i32_const(8) 780 | .i64_const(0x02756e7401750002) // "02007501746e7502" 781 | .store(memory, StoreKind::I64 { atomic: false }, MemArg { offset: 0, align: 8 }) 782 | .i32_const(16) 783 | .i32_const(0x0200) // "0002" 784 | .store(memory, StoreKind::I32 { atomic: false }, MemArg { offset: 0, align: 4}) 785 | .i32_const(0) 786 | .i32_const(18) 787 | .call(reply_data) 788 | // if log_size - idx > MAX_ITEMS_PER_QUERY 789 | .global_get(vars.log_size) 790 | .local_get(idx) 791 | .binop(BinaryOp::I32Sub) 792 | .local_tee(len) 793 | .i32_const(MAX_ITEMS_PER_QUERY) 794 | .binop(BinaryOp::I32GtU) 795 | .if_else( 796 | None, 797 | |then| { 798 | then.i32_const(MAX_ITEMS_PER_QUERY) 799 | .local_set(len) 800 | .local_get(idx) 801 | .i32_const(MAX_ITEMS_PER_QUERY) 802 | .binop(BinaryOp::I32Add) 803 | .local_set(next_idx); 804 | }, 805 | |else_| { 806 | else_.i32_const(0) 807 | .local_set(next_idx); 808 | }, 809 | ) 810 | .local_get(len) 811 | .call(leb) 812 | .i32_const(0) 813 | .i32_const(5) 814 | .call(reply_data) 815 | // read stable logs 816 | .i64_const(0) 817 | .i64_const(config.log_start_address()) 818 | .local_get(idx) 819 | .unop(UnaryOp::I64ExtendUI32) 820 | .i64_const(LOG_ITEM_SIZE as i64) 821 | .binop(BinaryOp::I64Mul) 822 | .binop(BinaryOp::I64Add) 823 | .local_get(len) 824 | .unop(UnaryOp::I64ExtendUI32) 825 | .i64_const(LOG_ITEM_SIZE as i64) 826 | .binop(BinaryOp::I64Mul) 827 | .call(reader) 828 | .i32_const(0) 829 | .local_get(len) 830 | .i32_const(LOG_ITEM_SIZE) 831 | .binop(BinaryOp::I32Mul) 832 | .call(reply_data) 833 | // opt next idx 834 | .local_get(next_idx) 835 | .unop(UnaryOp::I32Eqz) 836 | .if_else( 837 | None, 838 | |then| { 839 | then.i32_const(0) 840 | .i32_const(0) 841 | .store(memory, StoreKind::I32 { atomic: false }, MemArg { offset: 0, align: 4}) 842 | .i32_const(0) 843 | .i32_const(1) 844 | .call(reply_data); 845 | }, 846 | |else_| { 847 | else_.i32_const(0) 848 | .i32_const(1) 849 | .store(memory, StoreKind::I32 { atomic: false }, MemArg { offset: 0, align: 1}) 850 | .i32_const(1) 851 | .local_get(next_idx) 852 | .store(memory, StoreKind::I32 { atomic: false }, MemArg { offset: 0, align: 4}) 853 | .i32_const(0) 854 | .i32_const(5) 855 | .call(reply_data); 856 | }, 857 | ) 858 | .call(reply); 859 | let getter = builder.finish(vec![], &mut m.funcs); 860 | m.exports.add("canister_query __get_profiling", getter); 861 | } 862 | // Generate i32 to 5-byte LEB128 encoding at memory address 0..5 863 | fn make_leb128_encoder(m: &mut Module) -> FunctionId { 864 | let memory = get_memory_id(m); 865 | let mut builder = FunctionBuilder::new(&mut m.types, &[ValType::I32], &[]); 866 | let value = m.locals.add(ValType::I32); 867 | let mut instrs = builder.func_body(); 868 | for i in 0..5 { 869 | instrs 870 | .i32_const(i) 871 | .local_get(value) 872 | .i32_const(0x7f) 873 | .binop(BinaryOp::I32And); 874 | if i < 4 { 875 | instrs.i32_const(0x80).binop(BinaryOp::I32Or); 876 | } 877 | #[rustfmt::skip] 878 | instrs 879 | .store(memory, StoreKind::I32_8 { atomic: false }, MemArg { offset: 0, align: 1 }) 880 | .local_get(value) 881 | .i32_const(7) 882 | .binop(BinaryOp::I32ShrU) 883 | .local_set(value); 884 | } 885 | builder.finish(vec![value], &mut m.funcs) 886 | } 887 | fn make_name_section(m: &Module) -> RawCustomSection { 888 | use candid::Encode; 889 | let name: Vec<_> = m 890 | .funcs 891 | .iter() 892 | .filter_map(|f| { 893 | if matches!(f.kind, FunctionKind::Local(_)) { 894 | use rustc_demangle::demangle; 895 | let name = f.name.as_ref()?; 896 | let demangled = format!("{:#}", demangle(name)); 897 | Some((f.id().index() as u16, demangled)) 898 | } else { 899 | None 900 | } 901 | }) 902 | .collect(); 903 | let data = Encode!(&name).unwrap(); 904 | RawCustomSection { 905 | name: "icp:public name".to_string(), 906 | data, 907 | } 908 | } 909 | 910 | fn make_getter(m: &mut Module, vars: &Variables) { 911 | let memory = get_memory_id(m); 912 | let reply_data = get_ic_func_id(m, "msg_reply_data_append"); 913 | let reply = get_ic_func_id(m, "msg_reply"); 914 | let mut getter = FunctionBuilder::new(&mut m.types, &[], &[]); 915 | getter.name("__get_cycles".to_string()); 916 | #[rustfmt::skip] 917 | getter 918 | .func_body() 919 | // It's a query call, so we can arbitrarily change the memory without restoring them afterwards. 920 | .i32_const(0) 921 | .i64_const(0x007401004c444944) // "DIDL000174xx" in little endian 922 | .store(memory, StoreKind::I64 { atomic: false }, MemArg { offset: 0, align: 8 }) 923 | .i32_const(7) 924 | .global_get(vars.total_counter) 925 | .store(memory, StoreKind::I64 { atomic: false }, MemArg { offset: 0, align: 8 }) 926 | .i32_const(0) 927 | .i32_const(15) 928 | .call(reply_data) 929 | .call(reply); 930 | let getter = getter.finish(vec![], &mut m.funcs); 931 | m.exports.add("canister_query __get_cycles", getter); 932 | } 933 | fn make_toggle_func(m: &mut Module, name: &str, var: GlobalId) { 934 | let memory = get_memory_id(m); 935 | let reply_data = get_ic_func_id(m, "msg_reply_data_append"); 936 | let reply = get_ic_func_id(m, "msg_reply"); 937 | let tmp = m.locals.add(ValType::I64); 938 | let mut builder = FunctionBuilder::new(&mut m.types, &[], &[]); 939 | builder.name(name.to_string()); 940 | #[rustfmt::skip] 941 | builder 942 | .func_body() 943 | .global_get(var) 944 | .i32_const(1) 945 | .binop(BinaryOp::I32Xor) 946 | .global_set(var) 947 | .i32_const(0) 948 | .load(memory, LoadKind::I64 { atomic: false }, MemArg { offset: 0, align: 8 }) 949 | .local_set(tmp) 950 | .i32_const(0) 951 | .i64_const(0x4c444944) // "DIDL0000xxxx" 952 | .store(memory, StoreKind::I64 { atomic: false }, MemArg { offset: 0, align: 8 }) 953 | .i32_const(0) 954 | .i32_const(6) 955 | .call(reply_data) 956 | .i32_const(0) 957 | .local_get(tmp) 958 | .store(memory, StoreKind::I64 { atomic: false }, MemArg { offset: 0, align: 8 }) 959 | .call(reply); 960 | let id = builder.finish(vec![], &mut m.funcs); 961 | m.exports.add(&format!("canister_update {name}"), id); 962 | } 963 | --------------------------------------------------------------------------------