contract_build/
post_process_wasm.rs

1// Copyright (C) Use Ink (UK) Ltd.
2// This file is part of cargo-contract.
3//
4// cargo-contract is free software: you can redistribute it and/or modify
5// it under the terms of the GNU General Public License as published by
6// the Free Software Foundation, either version 3 of the License, or
7// (at your option) any later version.
8//
9// cargo-contract is distributed in the hope that it will be useful,
10// but WITHOUT ANY WARRANTY; without even the implied warranty of
11// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
12// GNU General Public License for more details.
13//
14// You should have received a copy of the GNU General Public License
15// along with cargo-contract.  If not, see <http://www.gnu.org/licenses/>.
16
17use std::{
18    fs,
19    path::{
20        Path,
21        PathBuf,
22    },
23};
24
25use colored::Colorize;
26use wasm_encoder::{
27    EntityType,
28    ExportSection,
29    ImportSection,
30    RawSection,
31    Section,
32};
33use wasmparser::{
34    ExportSectionReader,
35    ImportSectionReader,
36    Parser,
37    Payload,
38};
39
40use anyhow::{
41    anyhow,
42    Context,
43    Error,
44    Result,
45};
46
47use crate::{
48    validate_wasm,
49    verbose_eprintln,
50    Verbosity,
51};
52
53/// Ensures the Wasm memory import of a given module has the maximum number of pages.
54///
55/// Iterates over the import section, finds the memory import entry if any and adjusts the
56/// maximum limit.
57fn ensure_maximum_memory_pages(
58    imports_reader: &ImportSectionReader,
59    maximum_allowed_pages: u64,
60) -> Result<ImportSection> {
61    let mut memory_found = false;
62    let imports = imports_reader.clone().into_iter().try_fold(
63        ImportSection::new(), |mut imports, entry| {
64            let entry = entry?;
65            let mut entity  = EntityType::try_from(
66                entry.ty).map_err(|_| anyhow!("Unsupported type in import section"))?;
67            if let EntityType::Memory(mut mem) = entity {
68                memory_found = true;
69               if let Some(requested_maximum) = mem.maximum {
70                    // The module already has maximum, check if it is within the limit bail out.
71                    if requested_maximum > maximum_allowed_pages {
72                        anyhow::bail!(
73                            "The wasm module requires {} pages. The maximum allowed number of pages is {}",
74                            requested_maximum, maximum_allowed_pages,
75                        );
76                    }
77                }
78                else {
79                    mem.maximum = Some(maximum_allowed_pages);
80                    entity = EntityType::from(mem);
81                }
82            }
83            imports.import(entry.module, entry.name, entity);
84
85            Ok::<_, Error>(imports)
86    })?;
87
88    if !memory_found {
89        anyhow::bail!(
90            "Memory import is not found. Is --import-memory specified in the linker args",
91        );
92    }
93    Ok(imports)
94}
95
96/// Strips all custom sections.
97///
98/// Presently all custom sections are not required so they can be stripped safely.
99/// The name section is already stripped by `wasm-opt`.
100fn strip_custom_sections(name: &str) -> bool {
101    !(name.starts_with("reloc.") || name == "name")
102}
103
104/// A contract should export nothing but the "call" and "deploy" functions.
105///
106/// Any elements not referenced by these exports become orphaned and are removed by
107/// `wasm-opt`.
108fn strip_export_section(exports_reader: &ExportSectionReader) -> Result<ExportSection> {
109    let filtered_exports = exports_reader.clone().into_iter().try_fold(
110        ExportSection::new(),
111        |mut exports, entry| {
112            let entry = entry.context("Parsing of wasm export section failed")?;
113            if matches!(entry.kind, wasmparser::ExternalKind::Func)
114                && (entry.name == "call" || entry.name == "deploy")
115            {
116                exports.export(entry.name, entry.kind.into(), entry.index);
117            }
118            Ok::<_, Error>(exports)
119        },
120    )?;
121
122    Ok(filtered_exports)
123}
124
125/// Load a Wasm file from disk.
126pub fn load_module<P: AsRef<Path>>(path: P) -> Result<Vec<u8>> {
127    let path = path.as_ref();
128    fs::read(path).context(format!(
129        "Loading of wasm module at '{}' failed",
130        path.display(),
131    ))
132}
133
134/// Performs required post-processing steps on the Wasm artifact.
135pub fn post_process_wasm(
136    optimized_code: &PathBuf,
137    skip_wasm_validation: bool,
138    verbosity: &Verbosity,
139    max_memory_pages: u64,
140) -> Result<()> {
141    // Deserialize Wasm module from a file.
142    let module =
143        load_module(optimized_code).context("Loading of optimized wasm failed")?;
144    let output =
145        post_process_module(&module, skip_wasm_validation, verbosity, max_memory_pages)?;
146    fs::write(optimized_code, output)?;
147    Ok(())
148}
149
150/// Performs required post-processing steps on the Wasm in the buffer.
151pub fn post_process_module(
152    module: &[u8],
153    skip_wasm_validation: bool,
154    verbosity: &Verbosity,
155    max_memory_pages: u64,
156) -> Result<Vec<u8>> {
157    let mut output = Vec::new();
158    for payload in Parser::new(0).parse_all(module) {
159        let payload = payload?;
160
161        match payload {
162            Payload::Version { encoding, .. } => {
163                output.extend_from_slice(match encoding {
164                    wasmparser::Encoding::Component => {
165                        anyhow::bail!("Unsupported component section")
166                    }
167                    wasmparser::Encoding::Module => &wasm_encoder::Module::HEADER,
168                });
169            }
170            Payload::End(_) => break,
171            Payload::CustomSection(ref c) => {
172                if strip_custom_sections(c.name()) {
173                    // Strip custom section
174                    continue
175                }
176            }
177            Payload::ExportSection(ref e) => {
178                let exports = strip_export_section(e)?;
179                exports.append_to(&mut output);
180                continue
181            }
182            Payload::ImportSection(ref i) => {
183                let imports = ensure_maximum_memory_pages(i, max_memory_pages)?;
184                imports.append_to(&mut output);
185                continue
186            }
187            _ => {}
188        }
189        // Forward a section without touching it
190        if let Some((id, range)) = payload.as_section() {
191            RawSection {
192                id,
193                data: &module[range],
194            }
195            .append_to(&mut output);
196        }
197    }
198
199    debug_assert!(
200        !output.is_empty(),
201        "resulting wasm size of post processing must be > 0"
202    );
203
204    if !skip_wasm_validation {
205        validate_wasm::validate_import_section(&output)?;
206    } else {
207        verbose_eprintln!(
208            verbosity,
209            " {}",
210            "Skipping wasm validation! Contract code may be invalid."
211                .bright_yellow()
212                .bold()
213        );
214    }
215
216    Ok(output)
217}
218
219#[cfg(test)]
220mod unit_tests {
221    use super::*;
222    use crate::Verbosity;
223    use wasmparser::TypeRef;
224
225    #[test]
226    fn post_process_wasm_exceeded_memory_limit() {
227        // given
228        let contract = r#"
229            (module
230                (type (;0;) (func (param i32 i32 i32)))
231                (import "seal" "foo" (func (;5;) (type 0)))
232                (import "env" "memory" (memory (;0;) 2 32))
233                (func (;5;) (type 0))
234            )"#;
235        let module = wabt::wat2wasm(contract).expect("Invalid wabt");
236
237        // when
238        let res = post_process_module(&module, true, &Verbosity::Verbose, 16);
239
240        // then
241        assert!(res.is_err());
242        assert_eq!(
243         res.err().unwrap().to_string(),
244         "The wasm module requires 32 pages. The maximum allowed number of pages is 16");
245    }
246
247    #[test]
248    fn post_process_wasm_missing_memory_limit() {
249        // given
250        let contract = r#"
251            (module
252                (type (;0;) (func (param i32 i32 i32)))
253                (import "seal" "foo" (func (;0;) (type 0)))
254                (import "env" "memory" (memory (;0;) 2))
255                (func (;1;) (type 0))
256            )"#;
257        let module = wabt::wat2wasm(contract).expect("Invalid wabt");
258
259        // when
260        let output = post_process_module(&module, true, &Verbosity::Verbose, 16)
261            .expect("Invalid wasm module");
262
263        // then
264        let maximum = Parser::new(0).parse_all(&output).find_map(|p| {
265            if let Payload::ImportSection(section) = p.unwrap() {
266                section.into_iter().find_map(|e| {
267                    if let TypeRef::Memory(mem) = e.unwrap().ty {
268                        mem.maximum
269                    } else {
270                        None
271                    }
272                })
273            } else {
274                None
275            }
276        });
277        assert_eq!(maximum, Some(16));
278    }
279
280    #[test]
281    fn post_process_wasm_missing_memory_import() {
282        // given
283        let contract = r#"
284            (module
285                (type (;0;) (func (param i32 i32 i32)))
286                (import "seal" "foo" (func (;0;) (type 0)))
287                (func (;1;) (type 0))
288            )"#;
289        let module = wabt::wat2wasm(contract).expect("Invalid wabt");
290
291        // when
292        let res = post_process_module(&module, true, &Verbosity::Verbose, 16);
293
294        // then
295        assert!(res.is_err());
296        assert_eq!(
297            res.err().unwrap().to_string(),
298            "Memory import is not found. Is --import-memory specified in the linker args"
299        );
300    }
301
302    #[test]
303    fn post_process_wasm_strip_export_section() {
304        // given
305        let contract = r#"
306            (module
307                (type (;0;) (func (param i32 i32 i32)))
308                (import "seal" "foo" (func (;0;) (type 0)))
309                (import "env" "memory" (memory (;0;) 2))
310                (func (;1;) (type 0))
311                (export "call" (func 1))
312                (export "foo" (func 1))
313                (export "deploy" (func 1))
314                (export "goo" (func 1))
315            )"#;
316        let module = wabt::wat2wasm(contract).expect("Invalid wabt");
317
318        // when
319        let output = post_process_module(&module, true, &Verbosity::Verbose, 1)
320            .expect("Invalid wasm module");
321
322        // then
323        let exports_count = Parser::new(0).parse_all(&output).find_map(|p| {
324            if let Payload::ExportSection(section) = p.unwrap() {
325                Some(section.into_iter().count())
326            } else {
327                None
328            }
329        });
330        assert_eq!(exports_count, Some(2));
331    }
332
333    #[test]
334    fn post_process_wasm_untouched() {
335        // given
336        let contract = r#"
337            (module
338                (type (;0;) (func (param i32 i32 i32)))
339                (type (;1;) (func (param i32 i32) (result i32)))
340                (type (;2;) (func (param i32 i32)))
341                (import "seal" "foo" (func (;0;) (type 0)))
342                (import "env" "memory" (memory (;0;) 2 16))
343                (func (;1;) (type 0))
344                (func (;2;) (type 2))
345                (func (;3;) (type 0))
346                (export "call" (func 1))
347                (export "deploy" (func 1))
348                (global (;0;) (mut i32) (i32.const 65536))
349                (global (;1;) i32 (i32.const 84291))
350                (global (;2;) i32 (i32.const 84304))
351                (data (;0;) (i32.const 65536) "test")
352            )"#;
353        let module = wabt::wat2wasm(contract).expect("Invalid wabt");
354
355        // when
356        let output = post_process_module(&module, false, &Verbosity::Verbose, 16)
357            .expect("Invalid wasm module");
358
359        // then
360        assert_eq!(module, output);
361    }
362}