diff --git a/Cargo.lock b/Cargo.lock index 728bf5eca..0add9c146 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -1172,7 +1172,6 @@ dependencies = [ "hex", "impl-serde", "parity-scale-codec", - "parity-wasm", "pretty_assertions", "regex", "rustc_version", @@ -1190,7 +1189,9 @@ dependencies = [ "uzers", "wabt", "walkdir", + "wasm-encoder", "wasm-opt", + "wasmparser 0.202.0", "which", "zip", ] @@ -3095,6 +3096,12 @@ version = "1.4.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "e2abad23fbc42b3700f2f279844dc832adb2b2eb069b2df918f455c4e18cc646" +[[package]] +name = "leb128" +version = "0.2.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "884e2677b40cc8c339eaefcb701c32ef1fd2493d71118dc0ca4b6a736c93bd67" + [[package]] name = "libc" version = "0.2.153" @@ -6561,6 +6568,16 @@ version = "0.2.92" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "af190c94f2773fdb3729c55b007a722abb5384da03bc0986df4c289bf5567e96" +[[package]] +name = "wasm-encoder" +version = "0.202.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "bfd106365a7f5f7aa3c1916a98cbb3ad477f5ff96ddb130285a91c6e7429e67a" +dependencies = [ + "leb128", + "wasmparser 0.202.0", +] + [[package]] name = "wasm-opt" version = "0.116.1" @@ -6642,6 +6659,17 @@ dependencies = [ "url", ] +[[package]] +name = "wasmparser" +version = "0.202.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d6998515d3cf3f8b980ef7c11b29a9b1017d4cf86b99ae93b546992df9931413" +dependencies = [ + "bitflags 2.5.0", + "indexmap 2.2.6", + "semver", +] + [[package]] name = "wasmparser-nostd" version = "0.100.1" @@ -6669,7 +6697,7 @@ dependencies = [ "psm", "serde", "target-lexicon", - "wasmparser", + "wasmparser 0.102.0", "wasmtime-environ", "wasmtime-jit", "wasmtime-runtime", @@ -6700,7 +6728,7 @@ dependencies = [ "serde", "target-lexicon", "thiserror", - "wasmparser", + "wasmparser 0.102.0", "wasmtime-types", ] @@ -6780,7 +6808,7 @@ dependencies = [ "cranelift-entity", "serde", "thiserror", - "wasmparser", + "wasmparser 0.102.0", ] [[package]] diff --git a/crates/build/Cargo.toml b/crates/build/Cargo.toml index bc45659ec..357bad534 100644 --- a/crates/build/Cargo.toml +++ b/crates/build/Cargo.toml @@ -28,7 +28,8 @@ rustc_version = "0.4.0" scale = { package = "parity-scale-codec", version = "3.0.0", features = ["derive"] } toml = "0.8.12" tracing = "0.1.40" -parity-wasm = { version = "0.45.0"} +wasm-encoder = { version = "0.202.0", features = ["wasmparser"] } +wasmparser = "0.202.0" semver = { version = "1.0.22", features = ["serde"] } serde = { version = "1", default-features = false, features = ["derive"] } serde_json = "1.0.116" diff --git a/crates/build/src/lib.rs b/crates/build/src/lib.rs index b0ece9cb3..87271a72e 100644 --- a/crates/build/src/lib.rs +++ b/crates/build/src/lib.rs @@ -1,4 +1,4 @@ -// Copyright 2018-2022 Parity Technologies (UK) Ltd. +// Copyright 2018-2024 Parity Technologies (UK) Ltd. // This file is part of cargo-contract. // // cargo-contract is free software: you can redistribute it and/or modify @@ -28,6 +28,7 @@ mod crate_metadata; mod docker; pub mod metadata; mod new; +mod post_process_wasm; #[cfg(test)] mod tests; pub mod util; @@ -58,6 +59,10 @@ pub use self::{ WasmOptSettings, }, new::new_contract_project, + post_process_wasm::{ + load_module, + post_process_wasm, + }, util::DEFAULT_KEY_COL_WIDTH, wasm_opt::{ OptimizationPasses, @@ -85,13 +90,6 @@ use anyhow::{ Result, }; use colored::Colorize; -use parity_wasm::elements::{ - External, - Internal, - MemoryType, - Module, - Section, -}; use semver::Version; use std::{ fs, @@ -105,7 +103,7 @@ use std::{ use strum::IntoEnumIterator; /// This is the default maximum number of pages available for a contract to allocate. -pub const DEFAULT_MAX_MEMORY_PAGES: u32 = 16; +pub const DEFAULT_MAX_MEMORY_PAGES: u64 = 16; /// Version of the currently executing `cargo-contract` binary. const VERSION: &str = env!("CARGO_PKG_VERSION"); @@ -140,7 +138,7 @@ pub struct ExecuteArgs { pub output_type: OutputType, pub skip_wasm_validation: bool, pub target: Target, - pub max_memory_pages: u32, + pub max_memory_pages: u64, pub image: ImageVariant, } @@ -684,117 +682,6 @@ fn check_dylint_requirements(_working_dir: Option<&Path>) -> Result<()> { Ok(()) } -/// Ensures the Wasm memory import of a given module has the maximum number of pages. -/// -/// Iterates over the import section, finds the memory import entry if any and adjusts the -/// maximum limit. -fn ensure_maximum_memory_pages( - module: &mut Module, - maximum_allowed_pages: u32, -) -> Result<()> { - let mem_ty = module - .import_section_mut() - .and_then(|section| { - section.entries_mut().iter_mut().find_map(|entry| { - match entry.external_mut() { - External::Memory(ref mut mem_ty) => Some(mem_ty), - _ => None, - } - }) - }) - .context( - "Memory import is not found. Is --import-memory specified in the linker args", - )?; - - if let Some(requested_maximum) = mem_ty.limits().maximum() { - // The module already has maximum, check if it is within the limit bail out. - if requested_maximum > maximum_allowed_pages { - anyhow::bail!( - "The wasm module requires {} pages. The maximum allowed number of pages is {}", - requested_maximum, - maximum_allowed_pages, - ); - } - } else { - let initial = mem_ty.limits().initial(); - *mem_ty = MemoryType::new(initial, Some(maximum_allowed_pages)); - } - - Ok(()) -} - -/// Strips all custom sections. -/// -/// Presently all custom sections are not required so they can be stripped safely. -/// The name section is already stripped by `wasm-opt`. -fn strip_custom_sections(module: &mut Module) { - module.sections_mut().retain(|section| { - match section { - Section::Reloc(_) => false, - Section::Custom(custom) if custom.name() != "name" => false, - _ => true, - } - }) -} - -/// A contract should export nothing but the "call" and "deploy" functions. -/// -/// Any elements not referenced by these exports become orphaned and are removed by -/// `wasm-opt`. -fn strip_exports(module: &mut Module) { - if let Some(section) = module.export_section_mut() { - section.entries_mut().retain(|entry| { - matches!(entry.internal(), Internal::Function(_)) - && (entry.field() == "call" || entry.field() == "deploy") - }) - } -} - -/// Load and parse a Wasm file from disk. -fn load_module>(path: P) -> Result { - let path = path.as_ref(); - parity_wasm::deserialize_file(path).context(format!( - "Loading of wasm module at '{}' failed", - path.display(), - )) -} - -/// Performs required post-processing steps on the Wasm artifact. -fn post_process_wasm( - optimized_code: &PathBuf, - skip_wasm_validation: bool, - verbosity: &Verbosity, - max_memory_pages: u32, -) -> Result<()> { - // Deserialize Wasm module from a file. - let mut module = - load_module(optimized_code).context("Loading of optimized wasm failed")?; - - strip_exports(&mut module); - ensure_maximum_memory_pages(&mut module, max_memory_pages)?; - strip_custom_sections(&mut module); - - if !skip_wasm_validation { - validate_wasm::validate_import_section(&module)?; - } else { - verbose_eprintln!( - verbosity, - " {}", - "Skipping wasm validation! Contract code may be invalid." - .bright_yellow() - .bold() - ); - } - - debug_assert!( - !module.clone().into_bytes().unwrap().is_empty(), - "resulting wasm size of post processing must be > 0" - ); - - parity_wasm::serialize_to_file(optimized_code, module)?; - Ok(()) -} - /// Checks whether the supplied `ink_version` already contains the debug feature. /// /// This feature was introduced in `3.0.0-rc4` with `ink_env/ink-debug`. diff --git a/crates/build/src/post_process_wasm.rs b/crates/build/src/post_process_wasm.rs new file mode 100644 index 000000000..e5398d0f1 --- /dev/null +++ b/crates/build/src/post_process_wasm.rs @@ -0,0 +1,362 @@ +// Copyright 2018-2024 Parity Technologies (UK) Ltd. +// This file is part of cargo-contract. +// +// cargo-contract is free software: you can redistribute it and/or modify +// it under the terms of the GNU General Public License as published by +// the Free Software Foundation, either version 3 of the License, or +// (at your option) any later version. +// +// cargo-contract is distributed in the hope that it will be useful, +// but WITHOUT ANY WARRANTY; without even the implied warranty of +// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +// GNU General Public License for more details. +// +// You should have received a copy of the GNU General Public License +// along with cargo-contract. If not, see . + +use std::{ + fs, + path::{ + Path, + PathBuf, + }, +}; + +use colored::Colorize; +use wasm_encoder::{ + EntityType, + ExportSection, + ImportSection, + RawSection, + Section, +}; +use wasmparser::{ + ExportSectionReader, + ImportSectionReader, + Parser, + Payload, +}; + +use anyhow::{ + anyhow, + Context, + Error, + Result, +}; + +use crate::{ + validate_wasm, + verbose_eprintln, + Verbosity, +}; + +/// Ensures the Wasm memory import of a given module has the maximum number of pages. +/// +/// Iterates over the import section, finds the memory import entry if any and adjusts the +/// maximum limit. +fn ensure_maximum_memory_pages( + imports_reader: &ImportSectionReader, + maximum_allowed_pages: u64, +) -> Result { + let mut memory_found = false; + let imports = imports_reader.clone().into_iter().try_fold( + ImportSection::new(), |mut imports, entry| { + let entry = entry?; + let mut entity = EntityType::try_from( + entry.ty).map_err(|_| anyhow!("Unsupported type in import section"))?; + if let EntityType::Memory(mut mem) = entity { + memory_found = true; + if let Some(requested_maximum) = mem.maximum { + // The module already has maximum, check if it is within the limit bail out. + if requested_maximum > maximum_allowed_pages { + anyhow::bail!( + "The wasm module requires {} pages. The maximum allowed number of pages is {}", + requested_maximum, maximum_allowed_pages, + ); + } + } + else { + mem.maximum = Some(maximum_allowed_pages); + entity = EntityType::from(mem); + } + } + imports.import(entry.module, entry.name, entity); + + Ok::<_, Error>(imports) + })?; + + if !memory_found { + anyhow::bail!( + "Memory import is not found. Is --import-memory specified in the linker args", + ); + } + Ok(imports) +} + +/// Strips all custom sections. +/// +/// Presently all custom sections are not required so they can be stripped safely. +/// The name section is already stripped by `wasm-opt`. +fn strip_custom_sections(name: &str) -> bool { + !(name.starts_with("reloc.") || name == "name") +} + +/// A contract should export nothing but the "call" and "deploy" functions. +/// +/// Any elements not referenced by these exports become orphaned and are removed by +/// `wasm-opt`. +fn strip_export_section(exports_reader: &ExportSectionReader) -> Result { + let filtered_exports = exports_reader.clone().into_iter().try_fold( + ExportSection::new(), + |mut exports, entry| { + let entry = entry.context("Parsing of wasm export section failed")?; + if matches!(entry.kind, wasmparser::ExternalKind::Func) + && (entry.name == "call" || entry.name == "deploy") + { + exports.export(entry.name, entry.kind.into(), entry.index); + } + Ok::<_, Error>(exports) + }, + )?; + + Ok(filtered_exports) +} + +/// Load a Wasm file from disk. +pub fn load_module>(path: P) -> Result> { + let path = path.as_ref(); + fs::read(path).context(format!( + "Loading of wasm module at '{}' failed", + path.display(), + )) +} + +/// Performs required post-processing steps on the Wasm artifact. +pub fn post_process_wasm( + optimized_code: &PathBuf, + skip_wasm_validation: bool, + verbosity: &Verbosity, + max_memory_pages: u64, +) -> Result<()> { + // Deserialize Wasm module from a file. + let module = + load_module(optimized_code).context("Loading of optimized wasm failed")?; + let output = + post_process_module(&module, skip_wasm_validation, verbosity, max_memory_pages)?; + fs::write(optimized_code, output)?; + Ok(()) +} + +/// Performs required post-processing steps on the Wasm in the buffer. +pub fn post_process_module( + module: &[u8], + skip_wasm_validation: bool, + verbosity: &Verbosity, + max_memory_pages: u64, +) -> Result> { + let mut output = Vec::new(); + for payload in Parser::new(0).parse_all(module) { + let payload = payload?; + + match payload { + Payload::Version { encoding, .. } => { + output.extend_from_slice(match encoding { + wasmparser::Encoding::Component => { + anyhow::bail!("Unsupported component section") + } + wasmparser::Encoding::Module => &wasm_encoder::Module::HEADER, + }); + } + Payload::End(_) => break, + Payload::CustomSection(ref c) => { + if strip_custom_sections(c.name()) { + // Strip custom section + continue + } + } + Payload::ExportSection(ref e) => { + let exports = strip_export_section(e)?; + exports.append_to(&mut output); + continue + } + Payload::ImportSection(ref i) => { + let imports = ensure_maximum_memory_pages(i, max_memory_pages)?; + imports.append_to(&mut output); + continue + } + _ => {} + } + // Forward a section without touching it + if let Some((id, range)) = payload.as_section() { + RawSection { + id, + data: &module[range], + } + .append_to(&mut output); + } + } + + debug_assert!( + !output.is_empty(), + "resulting wasm size of post processing must be > 0" + ); + + if !skip_wasm_validation { + validate_wasm::validate_import_section(&output)?; + } else { + verbose_eprintln!( + verbosity, + " {}", + "Skipping wasm validation! Contract code may be invalid." + .bright_yellow() + .bold() + ); + } + + Ok(output) +} + +#[cfg(test)] +mod unit_tests { + use super::*; + use crate::Verbosity; + use wasmparser::TypeRef; + + #[test] + fn post_process_wasm_exceeded_memory_limit() { + // given + let contract = r#" + (module + (type (;0;) (func (param i32 i32 i32))) + (import "seal" "foo" (func (;5;) (type 0))) + (import "env" "memory" (memory (;0;) 2 32)) + (func (;5;) (type 0)) + )"#; + let module = wabt::wat2wasm(contract).expect("Invalid wabt"); + + // when + let res = post_process_module(&module, true, &Verbosity::Verbose, 16); + + // then + assert!(res.is_err()); + assert_eq!( + res.err().unwrap().to_string(), + "The wasm module requires 32 pages. The maximum allowed number of pages is 16"); + } + + #[test] + fn post_process_wasm_missing_memory_limit() { + // given + let contract = r#" + (module + (type (;0;) (func (param i32 i32 i32))) + (import "seal" "foo" (func (;0;) (type 0))) + (import "env" "memory" (memory (;0;) 2)) + (func (;1;) (type 0)) + )"#; + let module = wabt::wat2wasm(contract).expect("Invalid wabt"); + + // when + let output = post_process_module(&module, true, &Verbosity::Verbose, 16) + .expect("Invalid wasm module"); + + // then + let maximum = Parser::new(0).parse_all(&output).find_map(|p| { + if let Payload::ImportSection(section) = p.unwrap() { + section.into_iter().find_map(|e| { + if let TypeRef::Memory(mem) = e.unwrap().ty { + mem.maximum + } else { + None + } + }) + } else { + None + } + }); + assert_eq!(maximum, Some(16)); + } + + #[test] + fn post_process_wasm_missing_memory_import() { + // given + let contract = r#" + (module + (type (;0;) (func (param i32 i32 i32))) + (import "seal" "foo" (func (;0;) (type 0))) + (func (;1;) (type 0)) + )"#; + let module = wabt::wat2wasm(contract).expect("Invalid wabt"); + + // when + let res = post_process_module(&module, true, &Verbosity::Verbose, 16); + + // then + assert!(res.is_err()); + assert_eq!( + res.err().unwrap().to_string(), + "Memory import is not found. Is --import-memory specified in the linker args" + ); + } + + #[test] + fn post_process_wasm_strip_export_section() { + // given + let contract = r#" + (module + (type (;0;) (func (param i32 i32 i32))) + (import "seal" "foo" (func (;0;) (type 0))) + (import "env" "memory" (memory (;0;) 2)) + (func (;1;) (type 0)) + (export "call" (func 1)) + (export "foo" (func 1)) + (export "deploy" (func 1)) + (export "goo" (func 1)) + )"#; + let module = wabt::wat2wasm(contract).expect("Invalid wabt"); + + // when + let output = post_process_module(&module, true, &Verbosity::Verbose, 1) + .expect("Invalid wasm module"); + + // then + let exports_count = Parser::new(0).parse_all(&output).find_map(|p| { + if let Payload::ExportSection(section) = p.unwrap() { + Some(section.into_iter().count()) + } else { + None + } + }); + assert_eq!(exports_count, Some(2)); + } + + #[test] + fn post_process_wasm_untouched() { + // given + let contract = r#" + (module + (type (;0;) (func (param i32 i32 i32))) + (type (;1;) (func (param i32 i32) (result i32))) + (type (;2;) (func (param i32 i32))) + (import "seal" "foo" (func (;0;) (type 0))) + (import "env" "memory" (memory (;0;) 2 16)) + (func (;1;) (type 0)) + (func (;2;) (type 2)) + (func (;3;) (type 0)) + (export "call" (func 1)) + (export "deploy" (func 1)) + (global (;0;) (mut i32) (i32.const 65536)) + (global (;1;) i32 (i32.const 84291)) + (global (;2;) i32 (i32.const 84304)) + (data (;0;) (i32.const 65536) "test") + )"#; + let module = wabt::wat2wasm(contract).expect("Invalid wabt"); + + // when + let output = post_process_module(&module, false, &Verbosity::Verbose, 16) + .expect("Invalid wasm module"); + + // then + assert_eq!(module, output); + } +} diff --git a/crates/build/src/tests.rs b/crates/build/src/tests.rs index 119ea6e5d..afe64b026 100644 --- a/crates/build/src/tests.rs +++ b/crates/build/src/tests.rs @@ -42,6 +42,10 @@ use std::{ }, time::SystemTime, }; +use wasmparser::{ + Parser, + Payload, +}; macro_rules! build_tests { ( $($fn:ident),* ) => { @@ -652,10 +656,15 @@ fn build_byte_str(bytes: &[u8]) -> String { } fn has_debug_symbols>(p: P) -> bool { - crate::load_module(p) - .unwrap() - .custom_sections() - .any(|e| e.name() == "name") + let module = crate::load_module(p).unwrap(); + let has_debug_symbols = Parser::new(0).parse_all(&module).any(|e| { + if let Payload::CustomSection(section) = e.unwrap() { + matches!(section.name(), "name") + } else { + false + } + }); + has_debug_symbols } /// Enables running a group of tests sequentially, each starting with the original diff --git a/crates/build/src/validate_wasm.rs b/crates/build/src/validate_wasm.rs index f2e5d9f0f..b47f65582 100644 --- a/crates/build/src/validate_wasm.rs +++ b/crates/build/src/validate_wasm.rs @@ -17,7 +17,10 @@ use anyhow::Result; use colored::Colorize; use impl_serde::serialize as serde_hex; -use parity_wasm::elements::Module; +use wasmparser::{ + Parser, + Payload, +}; /// Marker inserted by the ink! codegen for an error which can't /// be checked at compile time. @@ -75,43 +78,51 @@ pub enum EnforcedErrors { /// - Known bugs for which we want to recommend a solution. /// - Markers inserted by the ink! codegen for errors which can't be checked at compile /// time. -pub fn validate_import_section(module: &Module) -> Result<()> { - let imports = match module.import_section() { - Some(section) => section.entries().iter(), - None => { - // the module does not contain any imports, - // hence no further validation is necessary. - return Ok(()) +pub fn validate_import_section(module: &[u8]) -> Result<()> { + let import_section = Parser::new(0).parse_all(module).find_map(|payload| { + if let Ok(Payload::ImportSection(section)) = payload { + Some(section) + } else { + None } + }); + + // If the module does not contain any imports, + // no further validation is necessary. + let Some(reader) = import_section else { + return Ok(()) }; - let original_imports_len = imports.len(); + + let original_imports_len = reader.count(); let mut errs = Vec::new(); - let filtered_imports = imports.filter(|section| { - let field = section.field(); - if field.contains("panic") { - errs.push(String::from( - "An unexpected panic function import was found in the contract Wasm.\n\ - This typically goes back to a known bug in the Rust compiler:\n\ - https://github.com/rust-lang/rust/issues/78744\n\n\ - As a workaround try to insert `overflow-checks = false` into your `Cargo.toml`.\n\ - This will disable safe math operations, but unfortunately we are currently not \n\ - aware of a better workaround until the bug in the compiler is fixed.", - )); - } else if field.starts_with(INK_ENFORCE_ERR) { - errs.push(parse_linker_error(field)); - } + let filtered_imports = reader.into_iter() + .map(|e| e.expect("Parsing import section failed")) + .filter(|section| { + let field = section.name; + if field.contains("panic") { + errs.push(String::from( + "An unexpected panic function import was found in the contract Wasm.\n\ + This typically goes back to a known bug in the Rust compiler:\n\ + https://github.com/rust-lang/rust/issues/78744\n\n\ + As a workaround try to insert `overflow-checks = false` into your `Cargo.toml`.\n\ + This will disable safe math operations, but unfortunately we are currently not \n\ + aware of a better workaround until the bug in the compiler is fixed.", + )); + } else if field.starts_with(INK_ENFORCE_ERR) { + errs.push(parse_linker_error(field)); + } - match check_import(section.module(), field) { - Ok(_) => true, - Err(err) => { - errs.push(err); - false + match check_import(section.module, field) { + Ok(_) => true, + Err(err) => { + errs.push(err); + false + } } - } - }); + }); - if original_imports_len != filtered_imports.count() { + if original_imports_len as usize != filtered_imports.count() { anyhow::bail!(format!( "Validation of the Wasm failed.\n\n\n{}\n\nIgnore with `--skip-wasm-validation`", errs.into_iter() @@ -191,11 +202,9 @@ fn parse_linker_error(field: &str) -> String { #[cfg(test)] mod tests { use super::validate_import_section; - use parity_wasm::elements::Module; - fn create_module(contract: &str) -> Module { - let wasm = wabt::wat2wasm(contract).expect("invalid wabt"); - parity_wasm::deserialize_buffer(&wasm).expect("deserializing must work") + fn create_module(contract: &str) -> Vec { + wabt::wat2wasm(contract).expect("Invalid wabt") } #[test] @@ -227,9 +236,7 @@ mod tests { (type (;0;) (func)) (import "env" "__ink_enforce_error_0x0110466c697010666c6970aa97cade01" (func $__ink_enforce_error_0x0110466c697010666c6970aa97cade01 (type 0))) )"#; - let wasm = wabt::wat2wasm(contract).expect("invalid wabt"); - let module = - parity_wasm::deserialize_buffer(&wasm).expect("deserializing must work"); + let module = create_module(contract); // when let res = validate_import_section(&module); diff --git a/crates/cargo-contract/src/cmd/build.rs b/crates/cargo-contract/src/cmd/build.rs index cd8c489a5..68b86d976 100644 --- a/crates/cargo-contract/src/cmd/build.rs +++ b/crates/cargo-contract/src/cmd/build.rs @@ -123,7 +123,7 @@ pub struct BuildCommand { target: Target, /// The maximum number of pages available for a wasm contract to allocate. #[clap(long, default_value_t = contract_build::DEFAULT_MAX_MEMORY_PAGES)] - max_memory_pages: u32, + max_memory_pages: u64, /// Executes the build inside a docker container to produce a verifiable bundle. /// Requires docker daemon running. #[clap(long, default_value_t = false)]