mirror of
https://github.com/pezkuwichain/wasm-instrument.git
synced 2026-04-23 10:37:54 +00:00
Compare commits
186 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| 10fd3529f2 | |||
| 32ae9f8478 | |||
| 8bfdb41d01 | |||
| 659d3bf12c | |||
| 5b2f75a066 | |||
| 25ff883bbd | |||
| 3b932b11ad | |||
| d10bbdf554 | |||
| 28ef7f550c | |||
| 4a394c5f88 | |||
| d1648be274 | |||
| 4a51f16874 | |||
| 8380823e62 | |||
| 88d652e69a | |||
| 4713aa760f | |||
| 4548a86329 | |||
| ff68bee449 | |||
| 7c843842a7 | |||
| 374afe5700 | |||
| 8291876394 | |||
| 184b3f8b3a | |||
| 57da96fb50 | |||
| 4e3e6b598a | |||
| c5eaffd229 | |||
| e882111f92 | |||
| b22696aaa5 | |||
| 5259f1c922 | |||
| da2f0a0f23 | |||
| 1d62dc1270 | |||
| abd5d4f6df | |||
| c9b837c80a | |||
| 2bf8068571 | |||
| 2293760964 | |||
| 2f88f49ef6 | |||
| 6ff274f0b8 | |||
| 3d3e3010b3 | |||
| ef4e09fff9 | |||
| b8b18be419 | |||
| 72626a566a | |||
| b8e6b9e319 | |||
| cb023973e8 | |||
| a0b548b37d | |||
| 77ad07e347 | |||
| c5043a47ac | |||
| 9f15b8cd21 | |||
| 1ef05d71ed | |||
| 0cead7ba6d | |||
| e1c8ce90a1 | |||
| 9e98400de0 | |||
| 19760b5835 | |||
| 717aa8f0cb | |||
| d891cddcb3 | |||
| 95a711bc33 | |||
| 1b0ed1b383 | |||
| ca45220af5 | |||
| 2d1f4daed7 | |||
| ad01d9b41c | |||
| b2272f39bc | |||
| d9432bafa9 | |||
| 1131240d39 | |||
| 988ac32095 | |||
| 712c696c2d | |||
| 9a0f992cb3 | |||
| d6127afd1d | |||
| 7da376062a | |||
| 3ab49836be | |||
| 2430b18633 | |||
| aebfc0fbd7 | |||
| a2653cff5a | |||
| 3142a74de2 | |||
| f556bde4a4 | |||
| f05f43b883 | |||
| af761da031 | |||
| fbaae277fc | |||
| e0a05c6329 | |||
| e0dbaef676 | |||
| 880d273861 | |||
| 3568667ecb | |||
| c09a924a81 | |||
| f59eb121e5 | |||
| 1f8e6dd5b2 | |||
| 5e3b06de05 | |||
| 2306999c9c | |||
| 016425a25b | |||
| 409ced002a | |||
| b61f6dd52f | |||
| e026abe166 | |||
| 8c6dec11a4 | |||
| 3f2935df9c | |||
| dcf189b822 | |||
| 19b5fb50cb | |||
| 1e8953a9cb | |||
| 377684f9c7 | |||
| e89abb0c17 | |||
| e6336a4a90 | |||
| a881cf12bb | |||
| 2fe761f8c4 | |||
| 87761dad61 | |||
| a768692bbe | |||
| ea4cde0e7d | |||
| f9d8b722b5 | |||
| 39f234e441 | |||
| b4f9be733d | |||
| 155c7253c3 | |||
| c9cdef4c51 | |||
| 2b5026a6c5 | |||
| a774a2cb29 | |||
| 6fd636a41d | |||
| 5792da28d5 | |||
| f8673d5b87 | |||
| 5180d694ce | |||
| a150df8703 | |||
| ae412c45f1 | |||
| 6f46ef5211 | |||
| 026b0502bb | |||
| 2c173fee26 | |||
| 0870ce6646 | |||
| 82bd972333 | |||
| 93abbcfe56 | |||
| ed7f31ec20 | |||
| b5472bcd8f | |||
| b3f8f62105 | |||
| 0cf7daa9e5 | |||
| 24924f59ec | |||
| de60f491b4 | |||
| 4c0f42c6fc | |||
| c3d10a2619 | |||
| 863744b1fc | |||
| 89e13ee901 | |||
| 929e0ec2c0 | |||
| f6a1a6a066 | |||
| 0d40703c6e | |||
| 124de6c2db | |||
| 5a617c3aae | |||
| 80ea6ec7ad | |||
| bbcc495ccc | |||
| 1b7a5d26ea | |||
| b1fbd2921e | |||
| 1e68a862f8 | |||
| 466f5cceba | |||
| 38e0f254b0 | |||
| 5b2cd9c4c6 | |||
| ad83ad17ee | |||
| 91036c0aff | |||
| 728c935367 | |||
| 33785674dc | |||
| 3e635514e4 | |||
| d8428327d5 | |||
| d695703146 | |||
| cda99e70da | |||
| 0a78a1ab8d | |||
| 33c84edd78 | |||
| 8413e562cd | |||
| 1bc4973e6e | |||
| 8ecbc8ddcc | |||
| 56464c102f | |||
| 6046e94b40 | |||
| ec206fca64 | |||
| 59384e09d0 | |||
| 4f81bbc506 | |||
| f5890c2c7b | |||
| 7504381419 | |||
| bb9832dba1 | |||
| 62ea903c3a | |||
| c47adc1bd4 | |||
| c3833efca7 | |||
| 4e871c65e2 | |||
| 48c1c6e72a | |||
| d60340762b | |||
| da5b2ca5f6 | |||
| c520d334cd | |||
| 76b6743c64 | |||
| d6c6cefcf1 | |||
| 86da6439d1 | |||
| ed1c7b1b51 | |||
| cf10b7d5d9 | |||
| dd9169e30f | |||
| be40285a67 | |||
| ba45e15567 | |||
| 33ff0cbe1d | |||
| db4070b96c | |||
| 80d80a37d9 | |||
| 06277915da | |||
| 7c7a0713fc | |||
| 31e3324015 | |||
| dc993bdb1b |
+17
-1
@@ -6,6 +6,22 @@ tab_width=4
|
||||
end_of_line=lf
|
||||
charset=utf-8
|
||||
trim_trailing_whitespace=true
|
||||
max_line_length=120
|
||||
max_line_length=100
|
||||
insert_final_newline=true
|
||||
|
||||
[*.md]
|
||||
max_line_length=80
|
||||
indent_style=space
|
||||
indent_size=2
|
||||
|
||||
[*.yml]
|
||||
indent_style=space
|
||||
indent_size=2
|
||||
tab_width=8
|
||||
end_of_line=lf
|
||||
|
||||
[*.sh]
|
||||
indent_style=space
|
||||
indent_size=2
|
||||
tab_width=8
|
||||
end_of_line=lf
|
||||
|
||||
@@ -0,0 +1,4 @@
|
||||
# For details about syntax, see:
|
||||
# https://help.github.com/en/articles/about-code-owners
|
||||
|
||||
* @athei @pepyakin
|
||||
@@ -0,0 +1,11 @@
|
||||
version: 2
|
||||
updates:
|
||||
- package-ecosystem: cargo
|
||||
directory: "/"
|
||||
labels: []
|
||||
schedule:
|
||||
interval: "daily"
|
||||
- package-ecosystem: github-actions
|
||||
directory: '/'
|
||||
schedule:
|
||||
interval: daily
|
||||
@@ -0,0 +1,97 @@
|
||||
name: Check
|
||||
|
||||
on:
|
||||
push:
|
||||
branches: [ master ]
|
||||
pull_request:
|
||||
branches: [ master ]
|
||||
|
||||
env:
|
||||
CARGO_TERM_COLOR: always
|
||||
|
||||
jobs:
|
||||
rustfmt:
|
||||
runs-on: "ubuntu-latest"
|
||||
steps:
|
||||
- name: Install Rust toolchain
|
||||
uses: actions-rs/toolchain@v1
|
||||
with:
|
||||
profile: minimal
|
||||
toolchain: nightly
|
||||
components: rustfmt
|
||||
|
||||
- uses: actions/checkout@v3
|
||||
|
||||
- name: Cargo fmt
|
||||
uses: actions-rs/cargo@v1
|
||||
with:
|
||||
toolchain: nightly
|
||||
command: fmt
|
||||
args: --all -- --check
|
||||
|
||||
clippy:
|
||||
runs-on: "ubuntu-latest"
|
||||
steps:
|
||||
- name: Install Rust toolchain
|
||||
uses: actions-rs/toolchain@v1
|
||||
with:
|
||||
profile: minimal
|
||||
toolchain: stable
|
||||
components: clippy
|
||||
|
||||
- uses: actions/checkout@v3
|
||||
|
||||
- name: Cargo clippy
|
||||
uses: actions-rs/cargo@v1
|
||||
with:
|
||||
toolchain: stable
|
||||
command: clippy
|
||||
args: --all-targets --all-features -- -D warnings
|
||||
|
||||
test:
|
||||
strategy:
|
||||
matrix:
|
||||
os: ["ubuntu-latest", "macos-latest", "windows-latest"]
|
||||
toolchain: ["stable", "nightly"]
|
||||
runs-on: ${{ matrix.os }}
|
||||
steps:
|
||||
- name: Install Rust toolchain
|
||||
uses: actions-rs/toolchain@v1
|
||||
with:
|
||||
profile: minimal
|
||||
target: wasm32-unknown-unknown
|
||||
toolchain: ${{ matrix.toolchain }}
|
||||
|
||||
- name: Set git to use LF
|
||||
run: |
|
||||
git config --global core.autocrlf false
|
||||
git config --global core.eol lf
|
||||
|
||||
- uses: actions/checkout@v3
|
||||
|
||||
- name: Cargo build
|
||||
uses: actions-rs/cargo@v1
|
||||
with:
|
||||
toolchain: ${{ matrix.toolchain }}
|
||||
command: build
|
||||
|
||||
- name: Cargo build (no_std)
|
||||
uses: actions-rs/cargo@v1
|
||||
with:
|
||||
toolchain: ${{ matrix.toolchain }}
|
||||
command: build
|
||||
args: --no-default-features
|
||||
|
||||
- name: Cargo build (wasm)
|
||||
uses: actions-rs/cargo@v1
|
||||
with:
|
||||
toolchain: ${{ matrix.toolchain }}
|
||||
command: build
|
||||
args: --no-default-features --target wasm32-unknown-unknown
|
||||
|
||||
- name: Cargo test
|
||||
uses: actions-rs/cargo@v1
|
||||
with:
|
||||
toolchain: ${{ matrix.toolchain }}
|
||||
command: test
|
||||
args: --all-features
|
||||
@@ -3,3 +3,4 @@ target
|
||||
.cargo
|
||||
.DS_Store
|
||||
.idea
|
||||
.vscode
|
||||
|
||||
@@ -1,9 +0,0 @@
|
||||
language: rust
|
||||
rust:
|
||||
- nightly
|
||||
- stable
|
||||
|
||||
script:
|
||||
- cargo build --all --release --verbose
|
||||
- cargo test --all --verbose
|
||||
- if [ "$TRAVIS_RUST_VERSION" == "nightly" ]; then cargo build --no-default-features; fi
|
||||
@@ -0,0 +1,34 @@
|
||||
# Changelog
|
||||
|
||||
All notable changes to this project will be documented in this file.
|
||||
|
||||
The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/),
|
||||
and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html).
|
||||
|
||||
The semantic versioning guarantees cover the interface to the substrate runtime which
|
||||
includes this pallet as a dependency. This module will also add storage migrations whenever
|
||||
changes require it. Stability with regard to offchain tooling is explicitly excluded from
|
||||
this guarantee: For example adding a new field to an in-storage data structure will require
|
||||
changes to frontends to properly display it. However, those changes will still be regarded
|
||||
as a minor version bump.
|
||||
|
||||
The interface provided to smart contracts will adhere to semver with one exception: Even
|
||||
major version bumps will be backwards compatible with regard to already deployed contracts.
|
||||
In other words: Upgrading this pallet will not break pre-existing contracts.
|
||||
|
||||
## [v0.2.0] 2022-06-06
|
||||
- Adjust debug information (if already parsed) when injecting gas metering
|
||||
[#16](https://github.com/paritytech/wasm-instrument/pull/16)
|
||||
|
||||
## [v0.1.1] 2022-01-18
|
||||
|
||||
### Fixed
|
||||
|
||||
- Stack metering disregarded the activiation frame.
|
||||
[#2](https://github.com/paritytech/wasm-instrument/pull/2)
|
||||
|
||||
## [v0.1.0] 2022-01-11
|
||||
|
||||
### Changed
|
||||
|
||||
- Created from [pwasm-utils](https://github.com/paritytech/wasm-utils) by removing unused code and cleaning up docs.
|
||||
+31
-19
@@ -1,27 +1,39 @@
|
||||
[package]
|
||||
name = "pwasm-utils"
|
||||
version = "0.6.1"
|
||||
authors = ["Nikolay Volf <nikvolf@gmail.com>", "Sergey Pepyakin <s.pepyakin@gmail.com>"]
|
||||
license = "MIT/Apache-2.0"
|
||||
readme = "README.md"
|
||||
description = "Collection of command-line utilities and corresponding Rust api for producing pwasm-compatible executables"
|
||||
keywords = ["wasm", "webassembly", "pwasm"]
|
||||
name = "wasm-instrument"
|
||||
version = "0.2.0"
|
||||
edition = "2021"
|
||||
rust-version = "1.56.1"
|
||||
authors = ["Parity Technologies <admin@parity.io>"]
|
||||
license = "MIT OR Apache-2.0"
|
||||
description = "Instrument and transform wasm modules."
|
||||
keywords = ["wasm", "webassembly", "blockchain", "gas-metering", "parity"]
|
||||
categories = ["wasm", "no-std"]
|
||||
repository = "https://github.com/paritytech/wasm-instrument"
|
||||
include = ["src/**/*", "LICENSE-*", "README.md"]
|
||||
|
||||
[[bench]]
|
||||
name = "benches"
|
||||
harness = false
|
||||
path = "benches/benches.rs"
|
||||
|
||||
[profile.bench]
|
||||
lto = "fat"
|
||||
codegen-units = 1
|
||||
|
||||
[dependencies]
|
||||
parity-wasm = { version = "0.31", default-features = false }
|
||||
log = { version = "0.4", default-features = false }
|
||||
byteorder = { version = "1", default-features = false }
|
||||
parity-wasm = { version = "0.45", default-features = false }
|
||||
log = "0.4"
|
||||
|
||||
[dev-dependencies]
|
||||
tempdir = "0.3"
|
||||
wabt = "0.2"
|
||||
diff = "0.1.11"
|
||||
binaryen = "0.12"
|
||||
criterion = "0.3"
|
||||
diff = "0.1"
|
||||
rand = "0.8"
|
||||
wat = "1"
|
||||
wasmparser = "0.88"
|
||||
wasmprinter = "0.2"
|
||||
|
||||
[features]
|
||||
default = ["std"]
|
||||
std = ["parity-wasm/std", "log/std", "byteorder/std"]
|
||||
|
||||
[workspace]
|
||||
members = [
|
||||
"./cli",
|
||||
]
|
||||
std = ["parity-wasm/std"]
|
||||
sign_ext = ["parity-wasm/sign_ext"]
|
||||
|
||||
@@ -1,5 +1,3 @@
|
||||
Copyright (c) 2017 Nikolay Volf
|
||||
|
||||
Permission is hereby granted, free of charge, to any
|
||||
person obtaining a copy of this software and associated
|
||||
documentation files (the "Software"), to deal in the
|
||||
|
||||
@@ -1,64 +1,33 @@
|
||||
# wasm-utils
|
||||
# wasm-instrument
|
||||
|
||||
[](https://travis-ci.org/paritytech/wasm-utils)
|
||||
A Rust library containing a collection of wasm module instrumentations and transformations
|
||||
mainly useful for wasm based block chains and smart contracts.
|
||||
|
||||
Collection of WASM utilities used in Parity and WASM contract development
|
||||
## Provided functionality
|
||||
|
||||
## Build tools for cargo
|
||||
This is a non exhaustive list of provided functionality. Please check out the [documentation](https://docs.rs/wasm-instrument/latest/wasm_instrument/) for details.
|
||||
|
||||
Easiest way to use is to install via `cargo install`:
|
||||
### Gas Metering
|
||||
|
||||
```
|
||||
cargo install pwasm-utils-cli --bin wasm-build
|
||||
```
|
||||
Add gas metering to your platform by injecting the necessary code directly into the wasm module. This allows having a uniform gas metering implementation across different execution engines (interpreters, JIT compilers).
|
||||
|
||||
## Symbols pruning (wasm-prune)
|
||||
### Stack Height Limiter
|
||||
|
||||
```
|
||||
cargo install pwasm-utils-cli --bin wasm-prune
|
||||
wasm-prune <input_wasm_binary.wasm> <output_wasm_binary.wasm>
|
||||
```
|
||||
Neither the wasm standard nor any sufficiently complex execution engine specifies how many items on the wasm stack are supported before the execution aborts or malfunctions. Even the same execution engine on different operating systems or host architectures could support a different number of stack items and be well within its rights.
|
||||
|
||||
This will optimize WASM symbols tree to leave only those elements that are used by contract `call` function entry.
|
||||
This is the kind of indeterminism that can lead to consensus failures when used in a blockchain context.
|
||||
|
||||
## Gas counter (wasm-gas)
|
||||
To address this issue we can inject some code that meters the stack height at runtime and aborts the execution when it reaches a predefined limit. Choosing this limit suffciently small so that it is smaller than what any reasonably parameterized execution engine would support solves the issue: All execution engines would reach the injected limit before hitting any implementation specific limitation.
|
||||
|
||||
For development puposes, raw WASM contract can be injected with gas counters (the same way as it done by Parity runtime when running contracts)
|
||||
## License
|
||||
|
||||
```
|
||||
cargo install pwasm-utils-cli --bin wasm-gas
|
||||
wasm-gas <input_wasm_binary.wasm> <output_wasm_binary.wasm>
|
||||
```
|
||||
|
||||
## Externalization (wasm-ext)
|
||||
|
||||
Parity WASM runtime provides some library functions that can be commonly found in libc. WASM binary size can be reduced and performance may be improved if these functions are used. This utility scans for invocations of the following functions inside the WASM binary:
|
||||
- `_malloc`,
|
||||
- `_free`,
|
||||
- `_memcpy`,
|
||||
- `_memset`,
|
||||
- `_memmove`
|
||||
|
||||
And then substitutes them with invocations of the imported ones. Should be run before `wasm-opt` for better results.
|
||||
|
||||
```
|
||||
cargo install pwasm-utils-cli --bin wasm-ext
|
||||
wasm-ext <input_wasm_binary.wasm> <output_wasm_binary.wasm>
|
||||
```
|
||||
|
||||
## API
|
||||
|
||||
All executables use corresponding api methods of the root crate and can be combined in other build tools.
|
||||
|
||||
# License
|
||||
|
||||
`wasm-utils` is primarily distributed under the terms of both the MIT
|
||||
license and the Apache License (Version 2.0), at your choice.
|
||||
`wasm-instrument` is distributed under the terms of both the MIT license and the
|
||||
Apache License (Version 2.0), at your choice.
|
||||
|
||||
See LICENSE-APACHE, and LICENSE-MIT for details.
|
||||
|
||||
## Contribution
|
||||
### Contribution
|
||||
|
||||
Unless you explicitly state otherwise, any contribution intentionally submitted
|
||||
for inclusion in `wasm-utils` by you, as defined in the Apache-2.0 license, shall be
|
||||
dual licensed as above, without any additional terms or conditions.
|
||||
for inclusion in `wasm-instrument` by you, as defined in the Apache-2.0 license, shall be
|
||||
dual licensed as above, without any additional terms or conditions.
|
||||
|
||||
@@ -0,0 +1,51 @@
|
||||
use criterion::{
|
||||
criterion_group, criterion_main, measurement::Measurement, BenchmarkGroup, Criterion,
|
||||
Throughput,
|
||||
};
|
||||
use std::{
|
||||
fs::{read, read_dir},
|
||||
path::PathBuf,
|
||||
};
|
||||
use wasm_instrument::{
|
||||
gas_metering, inject_stack_limiter,
|
||||
parity_wasm::{deserialize_buffer, elements::Module},
|
||||
};
|
||||
|
||||
fn fixture_dir() -> PathBuf {
|
||||
let mut path = PathBuf::from(env!("CARGO_MANIFEST_DIR"));
|
||||
path.push("benches");
|
||||
path.push("fixtures");
|
||||
path
|
||||
}
|
||||
|
||||
fn any_fixture<F, M>(group: &mut BenchmarkGroup<M>, f: F)
|
||||
where
|
||||
F: Fn(Module),
|
||||
M: Measurement,
|
||||
{
|
||||
for entry in read_dir(fixture_dir()).unwrap() {
|
||||
let entry = entry.unwrap();
|
||||
let bytes = read(&entry.path()).unwrap();
|
||||
group.throughput(Throughput::Bytes(bytes.len().try_into().unwrap()));
|
||||
group.bench_with_input(entry.file_name().to_str().unwrap(), &bytes, |bench, input| {
|
||||
bench.iter(|| f(deserialize_buffer(input).unwrap()))
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
fn gas_metering(c: &mut Criterion) {
|
||||
let mut group = c.benchmark_group("Gas Metering");
|
||||
any_fixture(&mut group, |module| {
|
||||
gas_metering::inject(module, &gas_metering::ConstantCostRules::default(), "env").unwrap();
|
||||
});
|
||||
}
|
||||
|
||||
fn stack_height_limiter(c: &mut Criterion) {
|
||||
let mut group = c.benchmark_group("Stack Height Limiter");
|
||||
any_fixture(&mut group, |module| {
|
||||
inject_stack_limiter(module, 128).unwrap();
|
||||
});
|
||||
}
|
||||
|
||||
criterion_group!(benches, gas_metering, stack_height_limiter);
|
||||
criterion_main!(benches);
|
||||
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
@@ -1,50 +0,0 @@
|
||||
[package]
|
||||
name = "pwasm-utils-cli"
|
||||
version = "0.6.0"
|
||||
authors = ["Nikolay Volf <nikvolf@gmail.com>", "Sergey Pepyakin <s.pepyakin@gmail.com>"]
|
||||
license = "MIT/Apache-2.0"
|
||||
readme = "README.md"
|
||||
description = "Collection of command-line utilities and corresponding Rust api for producing pwasm-compatible executables"
|
||||
keywords = ["wasm", "webassembly", "pwasm"]
|
||||
|
||||
[lib]
|
||||
|
||||
[[bin]]
|
||||
name = "wasm-prune"
|
||||
path = "prune/main.rs"
|
||||
|
||||
[[bin]]
|
||||
name = "wasm-ext"
|
||||
path = "ext/main.rs"
|
||||
|
||||
[[bin]]
|
||||
name = "wasm-gas"
|
||||
path = "gas/main.rs"
|
||||
|
||||
[[bin]]
|
||||
name = "wasm-build"
|
||||
path = "build/main.rs"
|
||||
|
||||
[[bin]]
|
||||
name = "wasm-stack-height"
|
||||
path = "stack_height/main.rs"
|
||||
|
||||
[[bin]]
|
||||
name = "wasm-pack"
|
||||
path = "pack/main.rs"
|
||||
|
||||
[[bin]]
|
||||
name = "wasm-check"
|
||||
path = "check/main.rs"
|
||||
|
||||
[dependencies]
|
||||
parity-wasm = "0.31"
|
||||
pwasm-utils = { path = "..", version = "0.6" }
|
||||
glob = "0.2"
|
||||
clap = "2.24"
|
||||
log = "0.4"
|
||||
env_logger = "0.5"
|
||||
lazy_static = "1.0"
|
||||
|
||||
[dev-dependencies]
|
||||
tempdir = "0.3"
|
||||
@@ -1,11 +0,0 @@
|
||||
# pwasm-utils-cli
|
||||
|
||||
Collection of WASM utilities used in Parity and WASM contract devepment
|
||||
|
||||
## Install
|
||||
|
||||
Easiest way to use is to install via `cargo install`:
|
||||
|
||||
```
|
||||
cargo install pwasm-utils-cli
|
||||
```
|
||||
@@ -1,242 +0,0 @@
|
||||
//! Experimental build tool for cargo
|
||||
|
||||
extern crate glob;
|
||||
extern crate pwasm_utils as utils;
|
||||
extern crate clap;
|
||||
extern crate parity_wasm;
|
||||
extern crate pwasm_utils_cli as logger;
|
||||
|
||||
mod source;
|
||||
|
||||
use std::{fs, io};
|
||||
use std::path::PathBuf;
|
||||
|
||||
use clap::{App, Arg};
|
||||
use parity_wasm::elements;
|
||||
use utils::{build, BuildError, SourceTarget, TargetRuntime};
|
||||
|
||||
#[derive(Debug)]
|
||||
pub enum Error {
|
||||
Io(io::Error),
|
||||
FailedToCopy(String),
|
||||
Decoding(elements::Error, String),
|
||||
Encoding(elements::Error),
|
||||
Build(BuildError),
|
||||
}
|
||||
|
||||
impl std::fmt::Display for Error {
|
||||
fn fmt(&self, f: &mut std::fmt::Formatter) -> Result<(), std::fmt::Error> {
|
||||
use self::Error::*;
|
||||
match *self {
|
||||
Io(ref io) => write!(f, "Generic i/o error: {}", io),
|
||||
FailedToCopy(ref msg) => write!(f, "{}. Have you tried to run \"cargo build\"?", msg),
|
||||
Decoding(ref err, ref file) => write!(f, "Decoding error ({}). Must be a valid wasm file {}. Pointed wrong file?", err, file),
|
||||
Encoding(ref err) => write!(f, "Encoding error ({}). Almost impossible to happen, no free disk space?", err),
|
||||
Build(ref err) => write!(f, "Build error: {}", err)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
pub fn wasm_path(input: &source::SourceInput) -> String {
|
||||
let mut path = PathBuf::from(input.target_dir());
|
||||
path.push(format!("{}.wasm", input.final_name()));
|
||||
path.to_string_lossy().to_string()
|
||||
}
|
||||
|
||||
pub fn process_output(input: &source::SourceInput) -> Result<(), Error> {
|
||||
let mut cargo_path = PathBuf::from(input.target_dir());
|
||||
let wasm_name = input.bin_name().to_string().replace("-", "_");
|
||||
cargo_path.push(
|
||||
match input.target() {
|
||||
SourceTarget::Emscripten => source::EMSCRIPTEN_TRIPLET,
|
||||
SourceTarget::Unknown => source::UNKNOWN_TRIPLET,
|
||||
}
|
||||
);
|
||||
cargo_path.push("release");
|
||||
cargo_path.push(format!("{}.wasm", wasm_name));
|
||||
|
||||
let mut target_path = PathBuf::from(input.target_dir());
|
||||
target_path.push(format!("{}.wasm", input.final_name()));
|
||||
fs::copy(cargo_path.as_path(), target_path.as_path())
|
||||
.map_err(|io| Error::FailedToCopy(
|
||||
format!("Failed to copy '{}' to '{}': {}", cargo_path.display(), target_path.display(), io)
|
||||
))?;
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
fn do_main() -> Result<(), Error> {
|
||||
logger::init_log();
|
||||
|
||||
let matches = App::new("wasm-build")
|
||||
.arg(Arg::with_name("target")
|
||||
.index(1)
|
||||
.required(true)
|
||||
.help("Cargo target directory"))
|
||||
.arg(Arg::with_name("wasm")
|
||||
.index(2)
|
||||
.required(true)
|
||||
.help("Wasm binary name"))
|
||||
.arg(Arg::with_name("target-runtime")
|
||||
.help("What runtime we are compiling to")
|
||||
.long("target-runtime")
|
||||
.takes_value(true)
|
||||
.default_value("pwasm")
|
||||
.possible_values(&["substrate", "pwasm"]))
|
||||
.arg(Arg::with_name("skip_optimization")
|
||||
.help("Skip symbol optimization step producing final wasm")
|
||||
.long("skip-optimization"))
|
||||
.arg(Arg::with_name("enforce_stack_adjustment")
|
||||
.help("Enforce stack size adjustment (used for old wasm32-unknown-unknown)")
|
||||
.long("enforce-stack-adjustment"))
|
||||
.arg(Arg::with_name("runtime_type")
|
||||
.help("Injects RUNTIME_TYPE global export")
|
||||
.takes_value(true)
|
||||
.long("runtime-type"))
|
||||
.arg(Arg::with_name("runtime_version")
|
||||
.help("Injects RUNTIME_VERSION global export")
|
||||
.takes_value(true)
|
||||
.long("runtime-version"))
|
||||
.arg(Arg::with_name("source_target")
|
||||
.help("Cargo target type kind ('wasm32-unknown-unknown' or 'wasm32-unknown-emscripten'")
|
||||
.takes_value(true)
|
||||
.long("target"))
|
||||
.arg(Arg::with_name("final_name")
|
||||
.help("Final wasm binary name")
|
||||
.takes_value(true)
|
||||
.long("final"))
|
||||
.arg(Arg::with_name("save_raw")
|
||||
.help("Save intermediate raw bytecode to path")
|
||||
.takes_value(true)
|
||||
.long("save-raw"))
|
||||
.arg(Arg::with_name("shrink_stack")
|
||||
.help("Shrinks the new stack size for wasm32-unknown-unknown")
|
||||
.takes_value(true)
|
||||
.long("shrink-stack"))
|
||||
.arg(Arg::with_name("public_api")
|
||||
.help("Preserves specific imports in the library")
|
||||
.takes_value(true)
|
||||
.long("public-api"))
|
||||
|
||||
.get_matches();
|
||||
|
||||
let target_dir = matches.value_of("target").expect("is required; qed");
|
||||
let wasm_binary = matches.value_of("wasm").expect("is required; qed");
|
||||
let mut source_input = source::SourceInput::new(target_dir, wasm_binary);
|
||||
|
||||
let source_target_val = matches.value_of("source_target").unwrap_or_else(|| source::EMSCRIPTEN_TRIPLET);
|
||||
if source_target_val == source::UNKNOWN_TRIPLET {
|
||||
source_input = source_input.unknown()
|
||||
} else if source_target_val == source::EMSCRIPTEN_TRIPLET {
|
||||
source_input = source_input.emscripten()
|
||||
} else {
|
||||
eprintln!("--target can be: '{}' or '{}'", source::EMSCRIPTEN_TRIPLET, source::UNKNOWN_TRIPLET);
|
||||
::std::process::exit(1);
|
||||
}
|
||||
|
||||
if let Some(final_name) = matches.value_of("final_name") {
|
||||
source_input = source_input.with_final(final_name);
|
||||
}
|
||||
|
||||
process_output(&source_input)?;
|
||||
|
||||
let path = wasm_path(&source_input);
|
||||
|
||||
let module = parity_wasm::deserialize_file(&path)
|
||||
.map_err(|e| Error::Decoding(e, path.to_string()))?;
|
||||
|
||||
let runtime_type_version = if let (Some(runtime_type), Some(runtime_version))
|
||||
= (matches.value_of("runtime_type"), matches.value_of("runtime_version")) {
|
||||
let mut ty: [u8; 4] = Default::default();
|
||||
let runtime_bytes = runtime_type.as_bytes();
|
||||
if runtime_bytes.len() != 4 {
|
||||
panic!("--runtime-type should be equal to 4 bytes");
|
||||
}
|
||||
ty.copy_from_slice(runtime_bytes);
|
||||
let version: u32 = runtime_version.parse()
|
||||
.expect("--runtime-version should be a positive integer");
|
||||
Some((ty, version))
|
||||
} else {
|
||||
None
|
||||
};
|
||||
|
||||
let public_api_entries = matches.value_of("public_api")
|
||||
.map(|val| val.split(",").collect())
|
||||
.unwrap_or(Vec::new());
|
||||
|
||||
let target_runtime = match matches.value_of("target-runtime").expect("target-runtime has a default value; qed") {
|
||||
"pwasm" => TargetRuntime::pwasm(),
|
||||
"substrate" => TargetRuntime::substrate(),
|
||||
_ => unreachable!("all possible values are enumerated in clap config; qed"),
|
||||
};
|
||||
|
||||
let (module, ctor_module) = build(
|
||||
module,
|
||||
source_input.target(),
|
||||
runtime_type_version,
|
||||
&public_api_entries,
|
||||
matches.is_present("enforce_stack_adjustment"),
|
||||
matches.value_of("shrink_stack").unwrap_or_else(|| "49152").parse()
|
||||
.expect("New stack size is not valid u32"),
|
||||
matches.is_present("skip_optimization"),
|
||||
&target_runtime,
|
||||
).map_err(Error::Build)?;
|
||||
|
||||
if let Some(save_raw_path) = matches.value_of("save_raw") {
|
||||
parity_wasm::serialize_to_file(save_raw_path, module.clone()).map_err(Error::Encoding)?;
|
||||
}
|
||||
|
||||
if let Some(ctor_module) = ctor_module {
|
||||
parity_wasm::serialize_to_file(
|
||||
&path,
|
||||
ctor_module,
|
||||
).map_err(Error::Encoding)?;
|
||||
} else {
|
||||
parity_wasm::serialize_to_file(&path, module).map_err(Error::Encoding)?;
|
||||
}
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
fn main() {
|
||||
if let Err(e) = do_main() {
|
||||
eprintln!("{}", e);
|
||||
std::process::exit(1)
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
extern crate tempdir;
|
||||
|
||||
use self::tempdir::TempDir;
|
||||
use std::fs;
|
||||
|
||||
use super::process_output;
|
||||
use super::source::SourceInput;
|
||||
|
||||
#[test]
|
||||
fn processes_cargo_output() {
|
||||
let tmp_dir = TempDir::new("target").expect("temp dir failed");
|
||||
|
||||
let target_path = tmp_dir.path().join("wasm32-unknown-emscripten").join("release");
|
||||
fs::create_dir_all(target_path.clone()).expect("create dir failed");
|
||||
|
||||
{
|
||||
use std::io::Write;
|
||||
|
||||
let wasm_path = target_path.join("example_wasm.wasm");
|
||||
let mut f = fs::File::create(wasm_path).expect("create fail failed");
|
||||
f.write(b"\0asm").expect("write file failed");
|
||||
}
|
||||
|
||||
let path = tmp_dir.path().to_string_lossy();
|
||||
let input = SourceInput::new(&path, "example-wasm");
|
||||
|
||||
process_output(&input).expect("process output failed");
|
||||
|
||||
assert!(
|
||||
fs::metadata(tmp_dir.path().join("example-wasm.wasm")).expect("metadata failed").is_file()
|
||||
)
|
||||
}
|
||||
}
|
||||
@@ -1,57 +0,0 @@
|
||||
//! Configuration of source binaries
|
||||
|
||||
pub const UNKNOWN_TRIPLET: &str = "wasm32-unknown-unknown";
|
||||
pub const EMSCRIPTEN_TRIPLET: &str = "wasm32-unknown-emscripten";
|
||||
|
||||
use utils::SourceTarget;
|
||||
|
||||
/// Configuration of previous build step (cargo compilation)
|
||||
#[derive(Debug)]
|
||||
pub struct SourceInput<'a> {
|
||||
target_dir: &'a str,
|
||||
bin_name: &'a str,
|
||||
final_name: &'a str,
|
||||
target: SourceTarget,
|
||||
}
|
||||
|
||||
impl<'a> SourceInput<'a> {
|
||||
pub fn new<'b>(target_dir: &'b str, bin_name: &'b str) -> SourceInput<'b> {
|
||||
SourceInput {
|
||||
target_dir: target_dir,
|
||||
bin_name: bin_name,
|
||||
final_name: bin_name,
|
||||
target: SourceTarget::Emscripten,
|
||||
}
|
||||
}
|
||||
|
||||
pub fn unknown(mut self) -> Self {
|
||||
self.target = SourceTarget::Unknown;
|
||||
self
|
||||
}
|
||||
|
||||
pub fn emscripten(mut self) -> Self {
|
||||
self.target = SourceTarget::Emscripten;
|
||||
self
|
||||
}
|
||||
|
||||
pub fn with_final(mut self, final_name: &'a str) -> Self {
|
||||
self.final_name = final_name;
|
||||
self
|
||||
}
|
||||
|
||||
pub fn target_dir(&self) -> &str {
|
||||
self.target_dir
|
||||
}
|
||||
|
||||
pub fn bin_name(&self) -> &str {
|
||||
self.bin_name
|
||||
}
|
||||
|
||||
pub fn final_name(&self) -> &str {
|
||||
self.final_name
|
||||
}
|
||||
|
||||
pub fn target(&self) -> SourceTarget {
|
||||
self.target
|
||||
}
|
||||
}
|
||||
@@ -1,102 +0,0 @@
|
||||
extern crate parity_wasm;
|
||||
extern crate pwasm_utils as utils;
|
||||
extern crate pwasm_utils_cli as logger;
|
||||
extern crate clap;
|
||||
|
||||
use clap::{App, Arg};
|
||||
use parity_wasm::elements;
|
||||
|
||||
fn fail(msg: &str) -> ! {
|
||||
eprintln!("{}", msg);
|
||||
std::process::exit(1)
|
||||
}
|
||||
|
||||
const ALLOWED_IMPORTS: &'static [&'static str] = &[
|
||||
"ret",
|
||||
"storage_read",
|
||||
"storage_write",
|
||||
"balance",
|
||||
"sender",
|
||||
"origin",
|
||||
"fetch_input",
|
||||
"input_length",
|
||||
"ccall",
|
||||
"dcall",
|
||||
"scall",
|
||||
"create",
|
||||
"balance",
|
||||
"blockhash",
|
||||
"blocknumber",
|
||||
"coinbase",
|
||||
"timestamp",
|
||||
"difficulty",
|
||||
"gaslimit",
|
||||
"address",
|
||||
"value",
|
||||
"suicide",
|
||||
"panic",
|
||||
"elog",
|
||||
"abort"
|
||||
];
|
||||
|
||||
fn main() {
|
||||
logger::init_log();
|
||||
|
||||
let matches = App::new("wasm-check")
|
||||
.arg(Arg::with_name("input")
|
||||
.index(1)
|
||||
.required(true)
|
||||
.help("Input WASM file"))
|
||||
.get_matches();
|
||||
|
||||
let input = matches.value_of("input").expect("is required; qed");
|
||||
|
||||
let module = parity_wasm::deserialize_file(&input).expect("Input module deserialization failed");
|
||||
|
||||
for section in module.sections() {
|
||||
match *section {
|
||||
elements::Section::Import(ref import_section) => {
|
||||
let mut has_imported_memory_properly_named = false;
|
||||
for entry in import_section.entries() {
|
||||
if entry.module() != "env" {
|
||||
fail("All imports should be from env");
|
||||
}
|
||||
match *entry.external() {
|
||||
elements::External::Function(_) => {
|
||||
if !ALLOWED_IMPORTS.contains(&entry.field()) {
|
||||
fail(&format!("'{}' is not supported by the runtime", entry.field()));
|
||||
}
|
||||
},
|
||||
elements::External::Memory(m) => {
|
||||
if entry.field() == "memory" {
|
||||
has_imported_memory_properly_named = true;
|
||||
}
|
||||
|
||||
let max = if let Some(max) = m.limits().maximum() {
|
||||
max
|
||||
} else {
|
||||
fail("There is a limit on memory in Parity runtime, and this program does not limit memory");
|
||||
};
|
||||
|
||||
if max > 16 {
|
||||
fail(&format!(
|
||||
"Parity runtime has 1Mb limit (16 pages) on max contract memory, this program speicifies {}",
|
||||
max
|
||||
));
|
||||
}
|
||||
},
|
||||
elements::External::Global(_) => {
|
||||
fail("Parity runtime does not provide any globals")
|
||||
},
|
||||
_ => { continue; }
|
||||
}
|
||||
}
|
||||
|
||||
if !has_imported_memory_properly_named {
|
||||
fail("No imported memory from env::memory in the contract");
|
||||
}
|
||||
}
|
||||
_ => { continue; }
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1,23 +0,0 @@
|
||||
extern crate parity_wasm;
|
||||
extern crate pwasm_utils as utils;
|
||||
extern crate pwasm_utils_cli as logger;
|
||||
|
||||
use std::env;
|
||||
|
||||
fn main() {
|
||||
|
||||
logger::init_log();
|
||||
|
||||
let args = env::args().collect::<Vec<_>>();
|
||||
if args.len() != 3 {
|
||||
println!("Usage: {} input_file.wasm output_file.wasm", args[0]);
|
||||
return;
|
||||
}
|
||||
|
||||
let module = utils::externalize(
|
||||
parity_wasm::deserialize_file(&args[1]).expect("Module to deserialize ok"),
|
||||
vec!["_free", "_malloc", "_memcpy", "_memset", "_memmove"],
|
||||
);
|
||||
|
||||
parity_wasm::serialize_to_file(&args[2], module).expect("Module to serialize ok");
|
||||
}
|
||||
@@ -1,24 +0,0 @@
|
||||
extern crate parity_wasm;
|
||||
extern crate pwasm_utils as utils;
|
||||
extern crate pwasm_utils_cli as logger;
|
||||
|
||||
use std::env;
|
||||
|
||||
fn main() {
|
||||
logger::init_log();
|
||||
|
||||
let args = env::args().collect::<Vec<_>>();
|
||||
if args.len() != 3 {
|
||||
println!("Usage: {} input_file.wasm output_file.wasm", args[0]);
|
||||
return;
|
||||
}
|
||||
|
||||
// Loading module
|
||||
let module = parity_wasm::deserialize_file(&args[1]).expect("Module deserialization to succeed");
|
||||
|
||||
let result = utils::inject_gas_counter(
|
||||
module, &Default::default()
|
||||
).expect("Failed to inject gas. Some forbidden opcodes?");
|
||||
|
||||
parity_wasm::serialize_to_file(&args[2], result).expect("Module serialization to succeed")
|
||||
}
|
||||
@@ -1,37 +0,0 @@
|
||||
extern crate parity_wasm;
|
||||
extern crate pwasm_utils as utils;
|
||||
extern crate pwasm_utils_cli as logger;
|
||||
extern crate clap;
|
||||
|
||||
use clap::{App, Arg};
|
||||
|
||||
fn main() {
|
||||
logger::init_log();
|
||||
|
||||
let target_runtime = utils::TargetRuntime::pwasm();
|
||||
|
||||
let matches = App::new("wasm-pack")
|
||||
.arg(Arg::with_name("input")
|
||||
.index(1)
|
||||
.required(true)
|
||||
.help("Input WASM file"))
|
||||
.arg(Arg::with_name("output")
|
||||
.index(2)
|
||||
.required(true)
|
||||
.help("Output WASM file"))
|
||||
.get_matches();
|
||||
|
||||
let input = matches.value_of("input").expect("is required; qed");
|
||||
let output = matches.value_of("output").expect("is required; qed");
|
||||
|
||||
let module = parity_wasm::deserialize_file(&input).expect("Input module deserialization failed");
|
||||
let ctor_module = module.clone();
|
||||
let raw_module = parity_wasm::serialize(module).expect("Serialization failed");
|
||||
|
||||
// Invoke packer
|
||||
let mut result_module = utils::pack_instance(raw_module, ctor_module, &utils::TargetRuntime::pwasm()).expect("Packing failed");
|
||||
// Optimize constructor, since it does not need everything
|
||||
utils::optimize(&mut result_module, vec![target_runtime.call_symbol]).expect("Optimization failed");
|
||||
|
||||
parity_wasm::serialize_to_file(&output, result_module).expect("Serialization failed");
|
||||
}
|
||||
@@ -1,47 +0,0 @@
|
||||
extern crate parity_wasm;
|
||||
extern crate pwasm_utils as utils;
|
||||
extern crate pwasm_utils_cli as logger;
|
||||
extern crate clap;
|
||||
|
||||
use clap::{App, Arg};
|
||||
|
||||
fn main() {
|
||||
logger::init_log();
|
||||
|
||||
let target_runtime = utils::TargetRuntime::pwasm();
|
||||
|
||||
let matches = App::new("wasm-prune")
|
||||
.arg(Arg::with_name("input")
|
||||
.index(1)
|
||||
.required(true)
|
||||
.help("Input WASM file"))
|
||||
.arg(Arg::with_name("output")
|
||||
.index(2)
|
||||
.required(true)
|
||||
.help("Output WASM file"))
|
||||
.arg(Arg::with_name("exports")
|
||||
.long("exports")
|
||||
.short("e")
|
||||
.takes_value(true)
|
||||
.value_name("functions")
|
||||
.help(&format!("Comma-separated list of exported functions to keep. Default: '{}'", target_runtime.call_symbol)))
|
||||
.get_matches();
|
||||
|
||||
let exports = matches
|
||||
.value_of("exports")
|
||||
.unwrap_or(target_runtime.call_symbol)
|
||||
.split(',')
|
||||
.collect();
|
||||
|
||||
let input = matches.value_of("input").expect("is required; qed");
|
||||
let output = matches.value_of("output").expect("is required; qed");
|
||||
|
||||
let mut module = parity_wasm::deserialize_file(&input).unwrap();
|
||||
|
||||
// Invoke optimizer
|
||||
// Contract is supposed to have only these functions as public api
|
||||
// All other symbols not usable by this list is optimized away
|
||||
utils::optimize(&mut module, exports).expect("Optimizer failed");
|
||||
|
||||
parity_wasm::serialize_to_file(&output, module).expect("Serialization failed");
|
||||
}
|
||||
@@ -1,27 +0,0 @@
|
||||
#[macro_use] extern crate log;
|
||||
#[macro_use] extern crate lazy_static;
|
||||
extern crate env_logger;
|
||||
|
||||
use std::env;
|
||||
use log::LevelFilter;
|
||||
use env_logger::Builder;
|
||||
|
||||
lazy_static! {
|
||||
static ref LOG_DUMMY: bool = {
|
||||
let mut builder = Builder::new();
|
||||
builder.filter(None, LevelFilter::Info);
|
||||
|
||||
if let Ok(log) = env::var("RUST_LOG") {
|
||||
builder.parse(&log);
|
||||
}
|
||||
|
||||
builder.init();
|
||||
trace!("logger initialized");
|
||||
true
|
||||
};
|
||||
}
|
||||
|
||||
/// Intialize log with default settings
|
||||
pub fn init_log() {
|
||||
let _ = *LOG_DUMMY;
|
||||
}
|
||||
@@ -1,28 +0,0 @@
|
||||
extern crate pwasm_utils as utils;
|
||||
extern crate parity_wasm;
|
||||
extern crate pwasm_utils_cli as logger;
|
||||
|
||||
use std::env;
|
||||
use utils::stack_height;
|
||||
|
||||
fn main() {
|
||||
logger::init_log();
|
||||
|
||||
let args = env::args().collect::<Vec<_>>();
|
||||
if args.len() != 3 {
|
||||
println!("Usage: {} input_file.wasm output_file.wasm", args[0]);
|
||||
return;
|
||||
}
|
||||
|
||||
let input_file = &args[1];
|
||||
let output_file = &args[2];
|
||||
|
||||
// Loading module
|
||||
let module = parity_wasm::deserialize_file(&input_file).expect("Module deserialization to succeed");
|
||||
|
||||
let result = stack_height::inject_limiter(
|
||||
module, 1024
|
||||
).expect("Failed to inject stack height counter");
|
||||
|
||||
parity_wasm::serialize_to_file(&output_file, result).expect("Module serialization to succeed")
|
||||
}
|
||||
@@ -0,0 +1,18 @@
|
||||
_default:
|
||||
just --list
|
||||
|
||||
# Run rustfmt and ensure the code meets the expectation of the checks in the CI
|
||||
format:
|
||||
cargo +nightly fmt --all
|
||||
|
||||
# Run basic checks similar to what the CI does to ensure your code is fine
|
||||
check:
|
||||
cargo +nightly fmt --all -- --check
|
||||
cargo +stable clippy --all-targets --all-features -- -D warnings
|
||||
|
||||
# Run the tests
|
||||
test:
|
||||
cargo test --all-features
|
||||
|
||||
# So you are ready? This runs format, check and test
|
||||
ready: format check test
|
||||
@@ -0,0 +1,23 @@
|
||||
# Basic
|
||||
hard_tabs = true
|
||||
max_width = 100
|
||||
use_small_heuristics = "Max"
|
||||
# Imports
|
||||
imports_granularity = "Crate"
|
||||
reorder_imports = true
|
||||
# Consistency
|
||||
newline_style = "Unix"
|
||||
# Format comments
|
||||
comment_width = 100
|
||||
wrap_comments = true
|
||||
# Misc
|
||||
chain_width = 80
|
||||
spaces_around_ranges = false
|
||||
binop_separator = "Back"
|
||||
reorder_impl_items = false
|
||||
match_arm_leading_pipes = "Preserve"
|
||||
match_arm_blocks = false
|
||||
match_block_trailing_comma = true
|
||||
trailing_comma = "Vertical"
|
||||
trailing_semicolon = false
|
||||
use_field_init_shorthand = true
|
||||
-118
@@ -1,118 +0,0 @@
|
||||
use std;
|
||||
use super::{
|
||||
optimize,
|
||||
pack_instance,
|
||||
ununderscore_funcs,
|
||||
externalize_mem,
|
||||
shrink_unknown_stack,
|
||||
inject_runtime_type,
|
||||
PackingError,
|
||||
OptimizerError,
|
||||
TargetRuntime,
|
||||
};
|
||||
use parity_wasm;
|
||||
use parity_wasm::elements;
|
||||
|
||||
#[derive(Debug)]
|
||||
pub enum Error {
|
||||
Encoding(elements::Error),
|
||||
Packing(PackingError),
|
||||
Optimizer,
|
||||
}
|
||||
|
||||
impl From<OptimizerError> for Error {
|
||||
fn from(_err: OptimizerError) -> Self {
|
||||
Error::Optimizer
|
||||
}
|
||||
}
|
||||
|
||||
impl From<PackingError> for Error {
|
||||
fn from(err: PackingError) -> Self {
|
||||
Error::Packing(err)
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone, Copy)]
|
||||
pub enum SourceTarget {
|
||||
Emscripten,
|
||||
Unknown,
|
||||
}
|
||||
|
||||
impl std::fmt::Display for Error {
|
||||
fn fmt(&self, f: &mut std::fmt::Formatter) -> Result<(), std::fmt::Error> {
|
||||
use self::Error::*;
|
||||
match *self {
|
||||
Encoding(ref err) => write!(f, "Encoding error ({})", err),
|
||||
Optimizer => write!(f, "Optimization error due to missing export section. Pointed wrong file?"),
|
||||
Packing(ref e) => write!(f, "Packing failed due to module structure error: {}. Sure used correct libraries for building contracts?", e),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
fn has_ctor(module: &elements::Module, target_runtime: &TargetRuntime) -> bool {
|
||||
if let Some(ref section) = module.export_section() {
|
||||
section.entries().iter().any(|e| target_runtime.create_symbol == e.field())
|
||||
} else {
|
||||
false
|
||||
}
|
||||
}
|
||||
|
||||
pub fn build(
|
||||
mut module: elements::Module,
|
||||
source_target: SourceTarget,
|
||||
runtime_type_version: Option<([u8; 4], u32)>,
|
||||
public_api_entries: &[&str],
|
||||
enforce_stack_adjustment: bool,
|
||||
stack_size: u32,
|
||||
skip_optimization: bool,
|
||||
target_runtime: &TargetRuntime,
|
||||
) -> Result<(elements::Module, Option<elements::Module>), Error> {
|
||||
|
||||
if let SourceTarget::Emscripten = source_target {
|
||||
module = ununderscore_funcs(module);
|
||||
}
|
||||
|
||||
if let SourceTarget::Unknown = source_target {
|
||||
// 49152 is 48kb!
|
||||
if enforce_stack_adjustment {
|
||||
assert!(stack_size <= 1024*1024);
|
||||
let (new_module, new_stack_top) = shrink_unknown_stack(module, 1024 * 1024 - stack_size);
|
||||
module = new_module;
|
||||
let mut stack_top_page = new_stack_top / 65536;
|
||||
if new_stack_top % 65536 > 0 { stack_top_page += 1 };
|
||||
module = externalize_mem(module, Some(stack_top_page), 16);
|
||||
} else {
|
||||
module = externalize_mem(module, None, 16);
|
||||
}
|
||||
}
|
||||
|
||||
if let Some(runtime_type_version) = runtime_type_version {
|
||||
let (runtime_type, runtime_version) = runtime_type_version;
|
||||
module = inject_runtime_type(module, runtime_type, runtime_version);
|
||||
}
|
||||
|
||||
let mut ctor_module = module.clone();
|
||||
|
||||
let mut public_api_entries = public_api_entries.to_vec();
|
||||
public_api_entries.push(target_runtime.call_symbol);
|
||||
if !skip_optimization {
|
||||
optimize(
|
||||
&mut module,
|
||||
public_api_entries,
|
||||
)?;
|
||||
}
|
||||
|
||||
if has_ctor(&ctor_module, target_runtime) {
|
||||
if !skip_optimization {
|
||||
optimize(&mut ctor_module, vec![target_runtime.create_symbol])?;
|
||||
}
|
||||
let ctor_module = pack_instance(
|
||||
parity_wasm::serialize(module.clone()).map_err(Error::Encoding)?,
|
||||
ctor_module.clone(),
|
||||
target_runtime,
|
||||
)?;
|
||||
Ok((module, Some(ctor_module)))
|
||||
} else {
|
||||
Ok((module, None))
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,161 @@
|
||||
use alloc::{format, vec::Vec};
|
||||
use parity_wasm::elements;
|
||||
|
||||
/// Export all declared mutable globals as `prefix_index`.
|
||||
///
|
||||
/// This will export all internal mutable globals under the name of
|
||||
/// concat(`prefix`, `"_"`, `i`) where i is the index inside the range of
|
||||
/// [0..total number of internal mutable globals].
|
||||
pub fn export_mutable_globals(module: &mut elements::Module, prefix: &str) {
|
||||
let exports = global_section(module)
|
||||
.map(|section| {
|
||||
section
|
||||
.entries()
|
||||
.iter()
|
||||
.enumerate()
|
||||
.filter_map(
|
||||
|(index, global)| {
|
||||
if global.global_type().is_mutable() {
|
||||
Some(index)
|
||||
} else {
|
||||
None
|
||||
}
|
||||
},
|
||||
)
|
||||
.collect::<Vec<_>>()
|
||||
})
|
||||
.unwrap_or_default();
|
||||
|
||||
if module.export_section().is_none() {
|
||||
module
|
||||
.sections_mut()
|
||||
.push(elements::Section::Export(elements::ExportSection::default()));
|
||||
}
|
||||
|
||||
for (symbol_index, export) in exports.into_iter().enumerate() {
|
||||
let new_entry = elements::ExportEntry::new(
|
||||
format!("{}_{}", prefix, symbol_index),
|
||||
elements::Internal::Global(
|
||||
(module.import_count(elements::ImportCountType::Global) + export) as _,
|
||||
),
|
||||
);
|
||||
export_section(module)
|
||||
.expect("added above if does not exists")
|
||||
.entries_mut()
|
||||
.push(new_entry);
|
||||
}
|
||||
}
|
||||
|
||||
fn export_section(module: &mut elements::Module) -> Option<&mut elements::ExportSection> {
|
||||
for section in module.sections_mut() {
|
||||
if let elements::Section::Export(sect) = section {
|
||||
return Some(sect)
|
||||
}
|
||||
}
|
||||
None
|
||||
}
|
||||
|
||||
fn global_section(module: &mut elements::Module) -> Option<&mut elements::GlobalSection> {
|
||||
for section in module.sections_mut() {
|
||||
if let elements::Section::Global(sect) = section {
|
||||
return Some(sect)
|
||||
}
|
||||
}
|
||||
None
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
|
||||
use super::export_mutable_globals;
|
||||
use parity_wasm::elements;
|
||||
|
||||
fn parse_wat(source: &str) -> elements::Module {
|
||||
let module_bytes = wat::parse_str(source).unwrap();
|
||||
wasmparser::validate(&module_bytes).unwrap();
|
||||
elements::deserialize_buffer(module_bytes.as_ref()).expect("failed to parse module")
|
||||
}
|
||||
|
||||
macro_rules! test_export_global {
|
||||
(name = $name:ident; input = $input:expr; expected = $expected:expr) => {
|
||||
#[test]
|
||||
fn $name() {
|
||||
let mut input_module = parse_wat($input);
|
||||
let expected_module = parse_wat($expected);
|
||||
|
||||
export_mutable_globals(&mut input_module, "exported_internal_global");
|
||||
|
||||
let actual_bytes = elements::serialize(input_module)
|
||||
.expect("injected module must have a function body");
|
||||
|
||||
let expected_bytes = elements::serialize(expected_module)
|
||||
.expect("injected module must have a function body");
|
||||
|
||||
let actual_wat = wasmprinter::print_bytes(actual_bytes).unwrap();
|
||||
let expected_wat = wasmprinter::print_bytes(expected_bytes).unwrap();
|
||||
|
||||
if actual_wat != expected_wat {
|
||||
for diff in diff::lines(&expected_wat, &actual_wat) {
|
||||
match diff {
|
||||
diff::Result::Left(l) => println!("-{}", l),
|
||||
diff::Result::Both(l, _) => println!(" {}", l),
|
||||
diff::Result::Right(r) => println!("+{}", r),
|
||||
}
|
||||
}
|
||||
panic!()
|
||||
}
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
test_export_global! {
|
||||
name = simple;
|
||||
input = r#"
|
||||
(module
|
||||
(global (;0;) (mut i32) (i32.const 1))
|
||||
(global (;1;) (mut i32) (i32.const 0)))
|
||||
"#;
|
||||
expected = r#"
|
||||
(module
|
||||
(global (;0;) (mut i32) (i32.const 1))
|
||||
(global (;1;) (mut i32) (i32.const 0))
|
||||
(export "exported_internal_global_0" (global 0))
|
||||
(export "exported_internal_global_1" (global 1)))
|
||||
"#
|
||||
}
|
||||
|
||||
test_export_global! {
|
||||
name = with_import;
|
||||
input = r#"
|
||||
(module
|
||||
(import "env" "global" (global $global i64))
|
||||
(global (;0;) (mut i32) (i32.const 1))
|
||||
(global (;1;) (mut i32) (i32.const 0)))
|
||||
"#;
|
||||
expected = r#"
|
||||
(module
|
||||
(import "env" "global" (global $global i64))
|
||||
(global (;0;) (mut i32) (i32.const 1))
|
||||
(global (;1;) (mut i32) (i32.const 0))
|
||||
(export "exported_internal_global_0" (global 1))
|
||||
(export "exported_internal_global_1" (global 2)))
|
||||
"#
|
||||
}
|
||||
|
||||
test_export_global! {
|
||||
name = with_import_and_some_are_immutable;
|
||||
input = r#"
|
||||
(module
|
||||
(import "env" "global" (global $global i64))
|
||||
(global (;0;) i32 (i32.const 1))
|
||||
(global (;1;) (mut i32) (i32.const 0)))
|
||||
"#;
|
||||
expected = r#"
|
||||
(module
|
||||
(import "env" "global" (global $global i64))
|
||||
(global (;0;) i32 (i32.const 1))
|
||||
(global (;1;) (mut i32) (i32.const 0))
|
||||
(export "exported_internal_global_0" (global 2)))
|
||||
"#
|
||||
}
|
||||
}
|
||||
-198
@@ -1,198 +0,0 @@
|
||||
use std::string::String;
|
||||
use std::vec::Vec;
|
||||
use std::borrow::ToOwned;
|
||||
|
||||
use parity_wasm::{elements, builder};
|
||||
use optimizer::{import_section, export_section};
|
||||
use byteorder::{LittleEndian, ByteOrder};
|
||||
|
||||
type Insertion = (usize, u32, u32, String);
|
||||
|
||||
pub fn update_call_index(instructions: &mut elements::Instructions, original_imports: usize, inserts: &[Insertion]) {
|
||||
use parity_wasm::elements::Instruction::*;
|
||||
for instruction in instructions.elements_mut().iter_mut() {
|
||||
if let &mut Call(ref mut call_index) = instruction {
|
||||
if let Some(pos) = inserts.iter().position(|x| x.1 == *call_index) {
|
||||
*call_index = (original_imports + pos) as u32;
|
||||
} else if *call_index as usize > original_imports {
|
||||
*call_index += inserts.len() as u32;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
pub fn memory_section<'a>(module: &'a mut elements::Module) -> Option<&'a mut elements::MemorySection> {
|
||||
for section in module.sections_mut() {
|
||||
if let &mut elements::Section::Memory(ref mut sect) = section {
|
||||
return Some(sect);
|
||||
}
|
||||
}
|
||||
None
|
||||
}
|
||||
|
||||
pub fn externalize_mem(mut module: elements::Module, adjust_pages: Option<u32>, max_pages: u32) -> elements::Module {
|
||||
let mut entry = memory_section(&mut module)
|
||||
.expect("Memory section to exist")
|
||||
.entries_mut()
|
||||
.pop()
|
||||
.expect("Own memory entry to exist in memory section");
|
||||
|
||||
if let Some(adjust_pages) = adjust_pages {
|
||||
assert!(adjust_pages <= max_pages);
|
||||
entry = elements::MemoryType::new(adjust_pages, Some(max_pages));
|
||||
}
|
||||
|
||||
if entry.limits().maximum().is_none() {
|
||||
entry = elements::MemoryType::new(entry.limits().initial(), Some(max_pages));
|
||||
}
|
||||
|
||||
let mut builder = builder::from_module(module);
|
||||
builder.push_import(
|
||||
elements::ImportEntry::new(
|
||||
"env".to_owned(),
|
||||
"memory".to_owned(),
|
||||
elements::External::Memory(entry),
|
||||
)
|
||||
);
|
||||
|
||||
builder.build()
|
||||
}
|
||||
|
||||
fn foreach_public_func_name<F>(mut module: elements::Module, f: F) -> elements::Module
|
||||
where F: Fn(&mut String)
|
||||
{
|
||||
import_section(&mut module).map(|is| {
|
||||
for entry in is.entries_mut() {
|
||||
if let elements::External::Function(_) = *entry.external() {
|
||||
f(entry.field_mut())
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
export_section(&mut module).map(|es| {
|
||||
for entry in es.entries_mut() {
|
||||
if let elements::Internal::Function(_) = *entry.internal() {
|
||||
f(entry.field_mut())
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
module
|
||||
}
|
||||
|
||||
pub fn underscore_funcs(module: elements::Module) -> elements::Module {
|
||||
foreach_public_func_name(module, |n| n.insert(0, '_'))
|
||||
}
|
||||
|
||||
pub fn ununderscore_funcs(module: elements::Module) -> elements::Module {
|
||||
foreach_public_func_name(module, |n| { n.remove(0); })
|
||||
}
|
||||
|
||||
pub fn shrink_unknown_stack(
|
||||
mut module: elements::Module,
|
||||
// for example, `shrink_amount = (1MB - 64KB)` will limit stack to 64KB
|
||||
shrink_amount: u32,
|
||||
) -> (elements::Module, u32) {
|
||||
let mut new_stack_top = 0;
|
||||
for section in module.sections_mut() {
|
||||
match section {
|
||||
&mut elements::Section::Data(ref mut data_section) => {
|
||||
for ref mut data_segment in data_section.entries_mut() {
|
||||
if data_segment.offset().code() == &[elements::Instruction::I32Const(4), elements::Instruction::End] {
|
||||
assert_eq!(data_segment.value().len(), 4);
|
||||
let current_val = LittleEndian::read_u32(data_segment.value());
|
||||
let new_val = current_val - shrink_amount;
|
||||
LittleEndian::write_u32(data_segment.value_mut(), new_val);
|
||||
new_stack_top = new_val;
|
||||
}
|
||||
}
|
||||
},
|
||||
_ => continue
|
||||
}
|
||||
}
|
||||
(module, new_stack_top)
|
||||
}
|
||||
|
||||
pub fn externalize(
|
||||
module: elements::Module,
|
||||
replaced_funcs: Vec<&str>,
|
||||
) -> elements::Module {
|
||||
// Save import functions number for later
|
||||
let import_funcs_total = module
|
||||
.import_section().expect("Import section to exist")
|
||||
.entries()
|
||||
.iter()
|
||||
.filter(|e| if let &elements::External::Function(_) = e.external() { true } else { false })
|
||||
.count();
|
||||
|
||||
// First, we find functions indices that are to be rewired to externals
|
||||
// Triple is (function_index (callable), type_index, function_name)
|
||||
let mut replaces: Vec<Insertion> = replaced_funcs
|
||||
.into_iter()
|
||||
.filter_map(|f| {
|
||||
let export = module
|
||||
.export_section().expect("Export section to exist")
|
||||
.entries().iter().enumerate()
|
||||
.find(|&(_, entry)| entry.field() == f)
|
||||
.expect("All functions of interest to exist");
|
||||
|
||||
if let &elements::Internal::Function(func_idx) = export.1.internal() {
|
||||
let type_ref = module
|
||||
.function_section().expect("Functions section to exist")
|
||||
.entries()[func_idx as usize - import_funcs_total]
|
||||
.type_ref();
|
||||
|
||||
Some((export.0, func_idx, type_ref, export.1.field().to_owned()))
|
||||
} else {
|
||||
None
|
||||
}
|
||||
})
|
||||
.collect();
|
||||
|
||||
replaces.sort_by_key(|e| e.0);
|
||||
|
||||
// Second, we duplicate them as import definitions
|
||||
let mut mbuilder = builder::from_module(module);
|
||||
for &(_, _, type_ref, ref field) in replaces.iter() {
|
||||
mbuilder.push_import(
|
||||
builder::import()
|
||||
.module("env")
|
||||
.field(field)
|
||||
.external().func(type_ref)
|
||||
.build()
|
||||
);
|
||||
}
|
||||
|
||||
// Back to mutable access
|
||||
let mut module = mbuilder.build();
|
||||
|
||||
// Third, rewire all calls to imported functions and update all other calls indices
|
||||
for section in module.sections_mut() {
|
||||
match section {
|
||||
&mut elements::Section::Code(ref mut code_section) => {
|
||||
for ref mut func_body in code_section.bodies_mut() {
|
||||
update_call_index(func_body.code_mut(), import_funcs_total, &replaces);
|
||||
}
|
||||
},
|
||||
&mut elements::Section::Export(ref mut export_section) => {
|
||||
for ref mut export in export_section.entries_mut() {
|
||||
if let &mut elements::Internal::Function(ref mut func_index) = export.internal_mut() {
|
||||
if *func_index >= import_funcs_total as u32 { *func_index += replaces.len() as u32; }
|
||||
}
|
||||
}
|
||||
},
|
||||
&mut elements::Section::Element(ref mut elements_section) => {
|
||||
for ref mut segment in elements_section.entries_mut() {
|
||||
// update all indirect call addresses initial values
|
||||
for func_index in segment.members_mut() {
|
||||
if *func_index >= import_funcs_total as u32 { *func_index += replaces.len() as u32; }
|
||||
}
|
||||
}
|
||||
},
|
||||
_ => { }
|
||||
}
|
||||
}
|
||||
|
||||
module
|
||||
|
||||
}
|
||||
-608
@@ -1,608 +0,0 @@
|
||||
use std::vec::Vec;
|
||||
|
||||
use parity_wasm::{elements, builder};
|
||||
use rules;
|
||||
|
||||
pub fn update_call_index(instructions: &mut elements::Instructions, inserted_index: u32) {
|
||||
use parity_wasm::elements::Instruction::*;
|
||||
for instruction in instructions.elements_mut().iter_mut() {
|
||||
if let &mut Call(ref mut call_index) = instruction {
|
||||
if *call_index >= inserted_index { *call_index += 1}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// A block of code represented by it's start position and cost.
|
||||
///
|
||||
/// The block typically starts with instructions such as `loop`, `block`, `if`, etc.
|
||||
///
|
||||
/// An example of block:
|
||||
///
|
||||
/// ```ignore
|
||||
/// loop
|
||||
/// i32.const 1
|
||||
/// get_local 0
|
||||
/// i32.sub
|
||||
/// tee_local 0
|
||||
/// br_if 0
|
||||
/// end
|
||||
/// ```
|
||||
///
|
||||
/// The start of the block is `i32.const 1`.
|
||||
///
|
||||
#[derive(Debug)]
|
||||
struct BlockEntry {
|
||||
/// Index of the first instruction (aka `Opcode`) in the block.
|
||||
start_pos: usize,
|
||||
/// Sum of costs of all instructions until end of the block.
|
||||
cost: u32,
|
||||
}
|
||||
|
||||
struct Counter {
|
||||
/// All blocks in the order of theirs start position.
|
||||
blocks: Vec<BlockEntry>,
|
||||
|
||||
// Stack of blocks. Each element is an index to a `self.blocks` vector.
|
||||
stack: Vec<usize>,
|
||||
}
|
||||
|
||||
impl Counter {
|
||||
fn new() -> Counter {
|
||||
Counter {
|
||||
stack: Vec::new(),
|
||||
blocks: Vec::new(),
|
||||
}
|
||||
}
|
||||
|
||||
/// Begin a new block.
|
||||
fn begin(&mut self, cursor: usize) {
|
||||
let block_idx = self.blocks.len();
|
||||
self.blocks.push(BlockEntry {
|
||||
start_pos: cursor,
|
||||
cost: 1,
|
||||
});
|
||||
self.stack.push(block_idx);
|
||||
}
|
||||
|
||||
/// Finalize the current block.
|
||||
///
|
||||
/// Finalized blocks have final cost which will not change later.
|
||||
fn finalize(&mut self) -> Result<(), ()> {
|
||||
self.stack.pop().ok_or_else(|| ())?;
|
||||
Ok(())
|
||||
}
|
||||
|
||||
/// Increment the cost of the current block by the specified value.
|
||||
fn increment(&mut self, val: u32) -> Result<(), ()> {
|
||||
let stack_top = self.stack.last_mut().ok_or_else(|| ())?;
|
||||
let top_block = self.blocks.get_mut(*stack_top).ok_or_else(|| ())?;
|
||||
|
||||
top_block.cost = top_block.cost.checked_add(val).ok_or_else(|| ())?;
|
||||
|
||||
Ok(())
|
||||
}
|
||||
}
|
||||
|
||||
fn inject_grow_counter(instructions: &mut elements::Instructions, grow_counter_func: u32) -> usize {
|
||||
use parity_wasm::elements::Instruction::*;
|
||||
let mut counter = 0;
|
||||
for instruction in instructions.elements_mut() {
|
||||
if let GrowMemory(_) = *instruction {
|
||||
*instruction = Call(grow_counter_func);
|
||||
counter += 1;
|
||||
}
|
||||
}
|
||||
counter
|
||||
}
|
||||
|
||||
fn add_grow_counter(module: elements::Module, rules: &rules::Set, gas_func: u32) -> elements::Module {
|
||||
use parity_wasm::elements::Instruction::*;
|
||||
|
||||
let mut b = builder::from_module(module);
|
||||
b.push_function(
|
||||
builder::function()
|
||||
.signature().params().i32().build().with_return_type(Some(elements::ValueType::I32)).build()
|
||||
.body()
|
||||
.with_instructions(elements::Instructions::new(vec![
|
||||
GetLocal(0),
|
||||
GetLocal(0),
|
||||
I32Const(rules.grow_cost() as i32),
|
||||
I32Mul,
|
||||
// todo: there should be strong guarantee that it does not return anything on stack?
|
||||
Call(gas_func),
|
||||
GrowMemory(0),
|
||||
End,
|
||||
]))
|
||||
.build()
|
||||
.build()
|
||||
);
|
||||
|
||||
b.build()
|
||||
}
|
||||
|
||||
pub fn inject_counter(
|
||||
instructions: &mut elements::Instructions,
|
||||
rules: &rules::Set,
|
||||
gas_func: u32,
|
||||
) -> Result<(), ()> {
|
||||
use parity_wasm::elements::Instruction::*;
|
||||
|
||||
let mut counter = Counter::new();
|
||||
|
||||
// Begin an implicit function (i.e. `func...end`) block.
|
||||
counter.begin(0);
|
||||
|
||||
for cursor in 0..instructions.elements().len() {
|
||||
let instruction = &instructions.elements()[cursor];
|
||||
match *instruction {
|
||||
Block(_) | If(_) | Loop(_) => {
|
||||
// Increment previous block with the cost of the current opcode.
|
||||
let instruction_cost = rules.process(instruction)?;
|
||||
counter.increment(instruction_cost)?;
|
||||
|
||||
// Begin new block. The cost of the following opcodes until `End` or `Else` will
|
||||
// be included into this block.
|
||||
counter.begin(cursor + 1);
|
||||
}
|
||||
End => {
|
||||
// Just finalize current block.
|
||||
counter.finalize()?;
|
||||
},
|
||||
Else => {
|
||||
// `Else` opcode is being encountered. So the case we are looking at:
|
||||
//
|
||||
// if
|
||||
// ...
|
||||
// else <-- cursor
|
||||
// ...
|
||||
// end
|
||||
//
|
||||
// Finalize the current block ('then' part of the if statement),
|
||||
// and begin another one for the 'else' part.
|
||||
counter.finalize()?;
|
||||
counter.begin(cursor + 1);
|
||||
}
|
||||
_ => {
|
||||
// An ordinal non control flow instruction. Just increment the cost of the current block.
|
||||
let instruction_cost = rules.process(instruction)?;
|
||||
counter.increment(instruction_cost)?;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Then insert metering calls.
|
||||
let mut cumulative_offset = 0;
|
||||
for block in counter.blocks {
|
||||
let effective_pos = block.start_pos + cumulative_offset;
|
||||
|
||||
instructions.elements_mut().insert(effective_pos, I32Const(block.cost as i32));
|
||||
instructions.elements_mut().insert(effective_pos+1, Call(gas_func));
|
||||
|
||||
// Take into account these two inserted instructions.
|
||||
cumulative_offset += 2;
|
||||
}
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
/// Injects gas counter.
|
||||
///
|
||||
/// Can only fail if encounters operation forbidden by gas rules,
|
||||
/// in this case it returns error with the original module.
|
||||
pub fn inject_gas_counter(module: elements::Module, rules: &rules::Set)
|
||||
-> Result<elements::Module, elements::Module>
|
||||
{
|
||||
// Injecting gas counting external
|
||||
let mut mbuilder = builder::from_module(module);
|
||||
let import_sig = mbuilder.push_signature(
|
||||
builder::signature()
|
||||
.param().i32()
|
||||
.build_sig()
|
||||
);
|
||||
|
||||
mbuilder.push_import(
|
||||
builder::import()
|
||||
.module("env")
|
||||
.field("gas")
|
||||
.external().func(import_sig)
|
||||
.build()
|
||||
);
|
||||
|
||||
// back to plain module
|
||||
let mut module = mbuilder.build();
|
||||
|
||||
// calculate actual function index of the imported definition
|
||||
// (substract all imports that are NOT functions)
|
||||
|
||||
let gas_func = module.import_count(elements::ImportCountType::Function) as u32 - 1;
|
||||
let total_func = module.functions_space() as u32;
|
||||
let mut need_grow_counter = false;
|
||||
let mut error = false;
|
||||
|
||||
// Updating calling addresses (all calls to function index >= `gas_func` should be incremented)
|
||||
for section in module.sections_mut() {
|
||||
match section {
|
||||
&mut elements::Section::Code(ref mut code_section) => {
|
||||
for ref mut func_body in code_section.bodies_mut() {
|
||||
update_call_index(func_body.code_mut(), gas_func);
|
||||
if let Err(_) = inject_counter(func_body.code_mut(), rules, gas_func) {
|
||||
error = true;
|
||||
break;
|
||||
}
|
||||
if rules.grow_cost() > 0 {
|
||||
if inject_grow_counter(func_body.code_mut(), total_func) > 0 {
|
||||
need_grow_counter = true;
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
&mut elements::Section::Export(ref mut export_section) => {
|
||||
for ref mut export in export_section.entries_mut() {
|
||||
if let &mut elements::Internal::Function(ref mut func_index) = export.internal_mut() {
|
||||
if *func_index >= gas_func { *func_index += 1}
|
||||
}
|
||||
}
|
||||
},
|
||||
&mut elements::Section::Element(ref mut elements_section) => {
|
||||
for ref mut segment in elements_section.entries_mut() {
|
||||
// update all indirect call addresses initial values
|
||||
for func_index in segment.members_mut() {
|
||||
if *func_index >= gas_func { *func_index += 1}
|
||||
}
|
||||
}
|
||||
},
|
||||
&mut elements::Section::Start(ref mut start_idx) => {
|
||||
if *start_idx >= gas_func { *start_idx += 1}
|
||||
},
|
||||
_ => { }
|
||||
}
|
||||
}
|
||||
|
||||
if error { return Err(module); }
|
||||
|
||||
if need_grow_counter { Ok(add_grow_counter(module, rules, gas_func)) } else { Ok(module) }
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
|
||||
extern crate wabt;
|
||||
|
||||
use parity_wasm::{serialize, builder, elements};
|
||||
use super::*;
|
||||
use rules;
|
||||
|
||||
#[test]
|
||||
fn simple_grow() {
|
||||
use parity_wasm::elements::Instruction::*;
|
||||
|
||||
let module = builder::module()
|
||||
.global()
|
||||
.value_type().i32()
|
||||
.build()
|
||||
.function()
|
||||
.signature().param().i32().build()
|
||||
.body()
|
||||
.with_instructions(elements::Instructions::new(
|
||||
vec![
|
||||
GetGlobal(0),
|
||||
GrowMemory(0),
|
||||
End
|
||||
]
|
||||
))
|
||||
.build()
|
||||
.build()
|
||||
.build();
|
||||
|
||||
let injected_module = inject_gas_counter(module, &rules::Set::default().with_grow_cost(10000)).unwrap();
|
||||
|
||||
assert_eq!(
|
||||
&vec![
|
||||
I32Const(3),
|
||||
Call(0),
|
||||
GetGlobal(0),
|
||||
Call(2),
|
||||
End
|
||||
][..],
|
||||
injected_module
|
||||
.code_section().expect("function section should exist").bodies()[0]
|
||||
.code().elements()
|
||||
);
|
||||
assert_eq!(
|
||||
&vec![
|
||||
GetLocal(0),
|
||||
GetLocal(0),
|
||||
I32Const(10000),
|
||||
I32Mul,
|
||||
Call(0),
|
||||
GrowMemory(0),
|
||||
End,
|
||||
][..],
|
||||
injected_module
|
||||
.code_section().expect("function section should exist").bodies()[1]
|
||||
.code().elements()
|
||||
);
|
||||
|
||||
let binary = serialize(injected_module).expect("serialization failed");
|
||||
self::wabt::wasm2wat(&binary).unwrap();
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn grow_no_gas_no_track() {
|
||||
use parity_wasm::elements::Instruction::*;
|
||||
|
||||
let module = builder::module()
|
||||
.global()
|
||||
.value_type().i32()
|
||||
.build()
|
||||
.function()
|
||||
.signature().param().i32().build()
|
||||
.body()
|
||||
.with_instructions(elements::Instructions::new(
|
||||
vec![
|
||||
GetGlobal(0),
|
||||
GrowMemory(0),
|
||||
End
|
||||
]
|
||||
))
|
||||
.build()
|
||||
.build()
|
||||
.build();
|
||||
|
||||
let injected_module = inject_gas_counter(module, &rules::Set::default()).unwrap();
|
||||
|
||||
assert_eq!(
|
||||
&vec![
|
||||
I32Const(3),
|
||||
Call(0),
|
||||
GetGlobal(0),
|
||||
GrowMemory(0),
|
||||
End
|
||||
][..],
|
||||
injected_module
|
||||
.code_section().expect("function section should exist").bodies()[0]
|
||||
.code().elements()
|
||||
);
|
||||
|
||||
assert_eq!(injected_module.functions_space(), 2);
|
||||
|
||||
let binary = serialize(injected_module).expect("serialization failed");
|
||||
self::wabt::wasm2wat(&binary).unwrap();
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn simple() {
|
||||
use parity_wasm::elements::Instruction::*;
|
||||
|
||||
let module = builder::module()
|
||||
.global()
|
||||
.value_type().i32()
|
||||
.build()
|
||||
.function()
|
||||
.signature().param().i32().build()
|
||||
.body()
|
||||
.with_instructions(elements::Instructions::new(
|
||||
vec![
|
||||
GetGlobal(0),
|
||||
End
|
||||
]
|
||||
))
|
||||
.build()
|
||||
.build()
|
||||
.build();
|
||||
|
||||
let injected_module = inject_gas_counter(module, &Default::default()).unwrap();
|
||||
|
||||
assert_eq!(
|
||||
&vec![
|
||||
I32Const(2),
|
||||
Call(0),
|
||||
GetGlobal(0),
|
||||
End
|
||||
][..],
|
||||
injected_module
|
||||
.code_section().expect("function section should exist").bodies()[0]
|
||||
.code().elements()
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn nested() {
|
||||
use parity_wasm::elements::Instruction::*;
|
||||
|
||||
let module = builder::module()
|
||||
.global()
|
||||
.value_type().i32()
|
||||
.build()
|
||||
.function()
|
||||
.signature().param().i32().build()
|
||||
.body()
|
||||
.with_instructions(elements::Instructions::new(
|
||||
vec![
|
||||
GetGlobal(0),
|
||||
Block(elements::BlockType::NoResult),
|
||||
GetGlobal(0),
|
||||
GetGlobal(0),
|
||||
GetGlobal(0),
|
||||
End,
|
||||
GetGlobal(0),
|
||||
End
|
||||
]
|
||||
))
|
||||
.build()
|
||||
.build()
|
||||
.build();
|
||||
|
||||
let injected_module = inject_gas_counter(module, &Default::default()).unwrap();
|
||||
|
||||
assert_eq!(
|
||||
&vec![
|
||||
I32Const(4),
|
||||
Call(0),
|
||||
GetGlobal(0),
|
||||
Block(elements::BlockType::NoResult),
|
||||
I32Const(4),
|
||||
Call(0),
|
||||
GetGlobal(0),
|
||||
GetGlobal(0),
|
||||
GetGlobal(0),
|
||||
End,
|
||||
GetGlobal(0),
|
||||
End
|
||||
][..],
|
||||
injected_module
|
||||
.code_section().expect("function section should exist").bodies()[0]
|
||||
.code().elements()
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn ifelse() {
|
||||
use parity_wasm::elements::Instruction::*;
|
||||
|
||||
let module = builder::module()
|
||||
.global()
|
||||
.value_type().i32()
|
||||
.build()
|
||||
.function()
|
||||
.signature().param().i32().build()
|
||||
.body()
|
||||
.with_instructions(elements::Instructions::new(
|
||||
vec![
|
||||
GetGlobal(0),
|
||||
If(elements::BlockType::NoResult),
|
||||
GetGlobal(0),
|
||||
GetGlobal(0),
|
||||
GetGlobal(0),
|
||||
Else,
|
||||
GetGlobal(0),
|
||||
GetGlobal(0),
|
||||
End,
|
||||
GetGlobal(0),
|
||||
End
|
||||
]
|
||||
))
|
||||
.build()
|
||||
.build()
|
||||
.build();
|
||||
|
||||
let injected_module = inject_gas_counter(module, &Default::default()).unwrap();
|
||||
|
||||
assert_eq!(
|
||||
&vec![
|
||||
I32Const(4),
|
||||
Call(0),
|
||||
GetGlobal(0),
|
||||
If(elements::BlockType::NoResult),
|
||||
I32Const(4),
|
||||
Call(0),
|
||||
GetGlobal(0),
|
||||
GetGlobal(0),
|
||||
GetGlobal(0),
|
||||
Else,
|
||||
I32Const(3),
|
||||
Call(0),
|
||||
GetGlobal(0),
|
||||
GetGlobal(0),
|
||||
End,
|
||||
GetGlobal(0),
|
||||
End
|
||||
][..],
|
||||
injected_module
|
||||
.code_section().expect("function section should exist").bodies()[0]
|
||||
.code().elements()
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn call_index() {
|
||||
use parity_wasm::elements::Instruction::*;
|
||||
|
||||
let module = builder::module()
|
||||
.global()
|
||||
.value_type().i32()
|
||||
.build()
|
||||
.function()
|
||||
.signature().param().i32().build()
|
||||
.body().build()
|
||||
.build()
|
||||
.function()
|
||||
.signature().param().i32().build()
|
||||
.body()
|
||||
.with_instructions(elements::Instructions::new(
|
||||
vec![
|
||||
Call(0),
|
||||
If(elements::BlockType::NoResult),
|
||||
Call(0),
|
||||
Call(0),
|
||||
Call(0),
|
||||
Else,
|
||||
Call(0),
|
||||
Call(0),
|
||||
End,
|
||||
Call(0),
|
||||
End
|
||||
]
|
||||
))
|
||||
.build()
|
||||
.build()
|
||||
.build();
|
||||
|
||||
let injected_module = inject_gas_counter(module, &Default::default()).unwrap();
|
||||
|
||||
assert_eq!(
|
||||
&vec![
|
||||
I32Const(4),
|
||||
Call(0),
|
||||
Call(1),
|
||||
If(elements::BlockType::NoResult),
|
||||
I32Const(4),
|
||||
Call(0),
|
||||
Call(1),
|
||||
Call(1),
|
||||
Call(1),
|
||||
Else,
|
||||
I32Const(3),
|
||||
Call(0),
|
||||
Call(1),
|
||||
Call(1),
|
||||
End,
|
||||
Call(1),
|
||||
End
|
||||
][..],
|
||||
injected_module
|
||||
.code_section().expect("function section should exist").bodies()[1]
|
||||
.code().elements()
|
||||
);
|
||||
}
|
||||
|
||||
|
||||
#[test]
|
||||
fn forbidden() {
|
||||
use parity_wasm::elements::Instruction::*;
|
||||
|
||||
let module = builder::module()
|
||||
.global()
|
||||
.value_type().i32()
|
||||
.build()
|
||||
.function()
|
||||
.signature().param().i32().build()
|
||||
.body()
|
||||
.with_instructions(elements::Instructions::new(
|
||||
vec![
|
||||
F32Const(555555),
|
||||
End
|
||||
]
|
||||
))
|
||||
.build()
|
||||
.build()
|
||||
.build();
|
||||
|
||||
let rules = rules::Set::default().with_forbidden_floats();
|
||||
|
||||
if let Err(_) = inject_gas_counter(module, &rules) { }
|
||||
else { panic!("Should be error because of the forbidden operation")}
|
||||
|
||||
}
|
||||
|
||||
}
|
||||
File diff suppressed because it is too large
Load Diff
@@ -0,0 +1,353 @@
|
||||
//! This module is used to validate the correctness of the gas metering algorithm.
|
||||
//!
|
||||
//! Since the gas metering algorithm is complex, this checks correctness by fuzzing. The testing
|
||||
//! strategy is to generate random, valid Wasm modules using Binaryen's translate-to-fuzz
|
||||
//! functionality, then ensure for all functions defined, in all execution paths though the
|
||||
//! function body that do not trap that the amount of gas charged by the proposed metering
|
||||
//! instructions is correct. This is done by constructing a control flow graph and exhaustively
|
||||
//! searching through all paths, which may take exponential time in the size of the function body in
|
||||
//! the worst case.
|
||||
|
||||
use super::{ConstantCostRules, MeteredBlock, Rules};
|
||||
use parity_wasm::elements::{FuncBody, Instruction};
|
||||
use std::collections::BTreeMap as Map;
|
||||
|
||||
/// An ID for a node in a ControlFlowGraph.
|
||||
type NodeId = usize;
|
||||
|
||||
/// A node in a control flow graph is commonly known as a basic block. This is a sequence of
|
||||
/// operations that are always executed sequentially.
|
||||
#[derive(Debug, Default)]
|
||||
struct ControlFlowNode {
|
||||
/// The index of the first instruction in the basic block. This is only used for debugging.
|
||||
first_instr_pos: Option<usize>,
|
||||
|
||||
/// The actual gas cost of executing all instructions in the basic block.
|
||||
actual_cost: u32,
|
||||
|
||||
/// The amount of gas charged by the injected metering instructions within this basic block.
|
||||
charged_cost: u32,
|
||||
|
||||
/// Whether there are any other nodes in the graph that loop back to this one. Every cycle in
|
||||
/// the control flow graph contains at least one node with this flag set.
|
||||
is_loop_target: bool,
|
||||
|
||||
/// Edges in the "forward" direction of the graph. The graph of nodes and their forward edges
|
||||
/// forms a directed acyclic graph (DAG).
|
||||
forward_edges: Vec<NodeId>,
|
||||
|
||||
/// Edges in the "backwards" direction. These edges form cycles in the graph.
|
||||
loopback_edges: Vec<NodeId>,
|
||||
}
|
||||
|
||||
/// A control flow graph where nodes are basic blocks and edges represent possible transitions
|
||||
/// between them in execution flow. The graph has two types of edges, forward and loop-back edges.
|
||||
/// The subgraph with only the forward edges forms a directed acyclic graph (DAG); including the
|
||||
/// loop-back edges introduces cycles.
|
||||
#[derive(Debug)]
|
||||
pub struct ControlFlowGraph {
|
||||
nodes: Vec<ControlFlowNode>,
|
||||
}
|
||||
|
||||
impl ControlFlowGraph {
|
||||
fn new() -> Self {
|
||||
ControlFlowGraph { nodes: Vec::new() }
|
||||
}
|
||||
|
||||
fn get_node(&self, node_id: NodeId) -> &ControlFlowNode {
|
||||
self.nodes.get(node_id).unwrap()
|
||||
}
|
||||
|
||||
fn get_node_mut(&mut self, node_id: NodeId) -> &mut ControlFlowNode {
|
||||
self.nodes.get_mut(node_id).unwrap()
|
||||
}
|
||||
|
||||
fn add_node(&mut self) -> NodeId {
|
||||
self.nodes.push(ControlFlowNode::default());
|
||||
self.nodes.len() - 1
|
||||
}
|
||||
|
||||
fn increment_actual_cost(&mut self, node_id: NodeId, cost: u32) {
|
||||
self.get_node_mut(node_id).actual_cost += cost;
|
||||
}
|
||||
|
||||
fn increment_charged_cost(&mut self, node_id: NodeId, cost: u32) {
|
||||
self.get_node_mut(node_id).charged_cost += cost;
|
||||
}
|
||||
|
||||
fn set_first_instr_pos(&mut self, node_id: NodeId, first_instr_pos: usize) {
|
||||
self.get_node_mut(node_id).first_instr_pos = Some(first_instr_pos)
|
||||
}
|
||||
|
||||
fn new_edge(&mut self, from_id: NodeId, target_frame: &ControlFrame) {
|
||||
if target_frame.is_loop {
|
||||
self.new_loopback_edge(from_id, target_frame.entry_node);
|
||||
} else {
|
||||
self.new_forward_edge(from_id, target_frame.exit_node);
|
||||
}
|
||||
}
|
||||
|
||||
fn new_forward_edge(&mut self, from_id: NodeId, to_id: NodeId) {
|
||||
self.get_node_mut(from_id).forward_edges.push(to_id)
|
||||
}
|
||||
|
||||
fn new_loopback_edge(&mut self, from_id: NodeId, to_id: NodeId) {
|
||||
self.get_node_mut(from_id).loopback_edges.push(to_id);
|
||||
self.get_node_mut(to_id).is_loop_target = true;
|
||||
}
|
||||
}
|
||||
|
||||
/// A control frame is opened upon entry into a function and by the `block`, `if`, and `loop`
|
||||
/// instructions and is closed by `end` instructions.
|
||||
struct ControlFrame {
|
||||
is_loop: bool,
|
||||
entry_node: NodeId,
|
||||
exit_node: NodeId,
|
||||
active_node: NodeId,
|
||||
}
|
||||
|
||||
impl ControlFrame {
|
||||
fn new(entry_node_id: NodeId, exit_node_id: NodeId, is_loop: bool) -> Self {
|
||||
ControlFrame {
|
||||
is_loop,
|
||||
entry_node: entry_node_id,
|
||||
exit_node: exit_node_id,
|
||||
active_node: entry_node_id,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// Construct a control flow graph from a function body and the metered blocks computed for it.
|
||||
///
|
||||
/// This assumes that the function body has been validated already, otherwise this may panic.
|
||||
fn build_control_flow_graph(
|
||||
body: &FuncBody,
|
||||
rules: &impl Rules,
|
||||
blocks: &[MeteredBlock],
|
||||
) -> Result<ControlFlowGraph, ()> {
|
||||
let mut graph = ControlFlowGraph::new();
|
||||
|
||||
let entry_node_id = graph.add_node();
|
||||
let terminal_node_id = graph.add_node();
|
||||
|
||||
graph.set_first_instr_pos(entry_node_id, 0);
|
||||
|
||||
let mut stack = vec![ControlFrame::new(entry_node_id, terminal_node_id, false)];
|
||||
let mut metered_blocks_iter = blocks.iter().peekable();
|
||||
for (cursor, instruction) in body.code().elements().iter().enumerate() {
|
||||
let active_node_id = stack
|
||||
.last()
|
||||
.expect("module is valid by pre-condition; control stack must not be empty; qed")
|
||||
.active_node;
|
||||
|
||||
// Increment the charged cost if there are metering instructions to be inserted here.
|
||||
let apply_block =
|
||||
metered_blocks_iter.peek().map_or(false, |block| block.start_pos == cursor);
|
||||
if apply_block {
|
||||
let next_metered_block =
|
||||
metered_blocks_iter.next().expect("peek returned an item; qed");
|
||||
graph.increment_charged_cost(active_node_id, next_metered_block.cost);
|
||||
}
|
||||
|
||||
let instruction_cost = rules.instruction_cost(instruction).ok_or(())?;
|
||||
match instruction {
|
||||
Instruction::Block(_) => {
|
||||
graph.increment_actual_cost(active_node_id, instruction_cost);
|
||||
|
||||
let exit_node_id = graph.add_node();
|
||||
stack.push(ControlFrame::new(active_node_id, exit_node_id, false));
|
||||
},
|
||||
Instruction::If(_) => {
|
||||
graph.increment_actual_cost(active_node_id, instruction_cost);
|
||||
|
||||
let then_node_id = graph.add_node();
|
||||
let exit_node_id = graph.add_node();
|
||||
|
||||
stack.push(ControlFrame::new(then_node_id, exit_node_id, false));
|
||||
graph.new_forward_edge(active_node_id, then_node_id);
|
||||
graph.set_first_instr_pos(then_node_id, cursor + 1);
|
||||
},
|
||||
Instruction::Loop(_) => {
|
||||
graph.increment_actual_cost(active_node_id, instruction_cost);
|
||||
|
||||
let loop_node_id = graph.add_node();
|
||||
let exit_node_id = graph.add_node();
|
||||
|
||||
stack.push(ControlFrame::new(loop_node_id, exit_node_id, true));
|
||||
graph.new_forward_edge(active_node_id, loop_node_id);
|
||||
graph.set_first_instr_pos(loop_node_id, cursor + 1);
|
||||
},
|
||||
Instruction::Else => {
|
||||
let active_frame_idx = stack.len() - 1;
|
||||
let prev_frame_idx = stack.len() - 2;
|
||||
|
||||
let else_node_id = graph.add_node();
|
||||
stack[active_frame_idx].active_node = else_node_id;
|
||||
|
||||
let prev_node_id = stack[prev_frame_idx].active_node;
|
||||
graph.new_forward_edge(prev_node_id, else_node_id);
|
||||
graph.set_first_instr_pos(else_node_id, cursor + 1);
|
||||
},
|
||||
Instruction::End => {
|
||||
let closing_frame = stack.pop()
|
||||
.expect("module is valid by pre-condition; ends correspond to control stack frames; qed");
|
||||
|
||||
graph.new_forward_edge(active_node_id, closing_frame.exit_node);
|
||||
graph.set_first_instr_pos(closing_frame.exit_node, cursor + 1);
|
||||
|
||||
if let Some(active_frame) = stack.last_mut() {
|
||||
active_frame.active_node = closing_frame.exit_node;
|
||||
}
|
||||
},
|
||||
Instruction::Br(label) => {
|
||||
graph.increment_actual_cost(active_node_id, instruction_cost);
|
||||
|
||||
let active_frame_idx = stack.len() - 1;
|
||||
let target_frame_idx = active_frame_idx - (*label as usize);
|
||||
graph.new_edge(active_node_id, &stack[target_frame_idx]);
|
||||
|
||||
// Next instruction is unreachable, but carry on anyway.
|
||||
let new_node_id = graph.add_node();
|
||||
stack[active_frame_idx].active_node = new_node_id;
|
||||
graph.set_first_instr_pos(new_node_id, cursor + 1);
|
||||
},
|
||||
Instruction::BrIf(label) => {
|
||||
graph.increment_actual_cost(active_node_id, instruction_cost);
|
||||
|
||||
let active_frame_idx = stack.len() - 1;
|
||||
let target_frame_idx = active_frame_idx - (*label as usize);
|
||||
graph.new_edge(active_node_id, &stack[target_frame_idx]);
|
||||
|
||||
let new_node_id = graph.add_node();
|
||||
stack[active_frame_idx].active_node = new_node_id;
|
||||
graph.new_forward_edge(active_node_id, new_node_id);
|
||||
graph.set_first_instr_pos(new_node_id, cursor + 1);
|
||||
},
|
||||
Instruction::BrTable(br_table_data) => {
|
||||
graph.increment_actual_cost(active_node_id, instruction_cost);
|
||||
|
||||
let active_frame_idx = stack.len() - 1;
|
||||
for &label in [br_table_data.default].iter().chain(br_table_data.table.iter()) {
|
||||
let target_frame_idx = active_frame_idx - (label as usize);
|
||||
graph.new_edge(active_node_id, &stack[target_frame_idx]);
|
||||
}
|
||||
|
||||
let new_node_id = graph.add_node();
|
||||
stack[active_frame_idx].active_node = new_node_id;
|
||||
graph.set_first_instr_pos(new_node_id, cursor + 1);
|
||||
},
|
||||
Instruction::Return => {
|
||||
graph.increment_actual_cost(active_node_id, instruction_cost);
|
||||
|
||||
graph.new_forward_edge(active_node_id, terminal_node_id);
|
||||
|
||||
let active_frame_idx = stack.len() - 1;
|
||||
let new_node_id = graph.add_node();
|
||||
stack[active_frame_idx].active_node = new_node_id;
|
||||
graph.set_first_instr_pos(new_node_id, cursor + 1);
|
||||
},
|
||||
_ => graph.increment_actual_cost(active_node_id, instruction_cost),
|
||||
}
|
||||
}
|
||||
|
||||
assert!(stack.is_empty());
|
||||
|
||||
Ok(graph)
|
||||
}
|
||||
|
||||
/// Exhaustively search through all paths in the control flow graph, starting from the first node
|
||||
/// and ensure that 1) all paths with only forward edges ending with the terminal node have an
|
||||
/// equal total actual gas cost and total charged gas cost, and 2) all cycles beginning with a loop
|
||||
/// entry point and ending with a node with a loop-back edge to the entry point have equal actual
|
||||
/// and charged gas costs. If this returns true, then the metered blocks used to construct the
|
||||
/// control flow graph are correct with respect to the function body.
|
||||
///
|
||||
/// In the worst case, this runs in time exponential in the size of the graph.
|
||||
fn validate_graph_gas_costs(graph: &ControlFlowGraph) -> bool {
|
||||
fn visit(
|
||||
graph: &ControlFlowGraph,
|
||||
node_id: NodeId,
|
||||
mut total_actual: u32,
|
||||
mut total_charged: u32,
|
||||
loop_costs: &mut Map<NodeId, (u32, u32)>,
|
||||
) -> bool {
|
||||
let node = graph.get_node(node_id);
|
||||
|
||||
total_actual += node.actual_cost;
|
||||
total_charged += node.charged_cost;
|
||||
|
||||
if node.is_loop_target {
|
||||
loop_costs.insert(node_id, (node.actual_cost, node.charged_cost));
|
||||
}
|
||||
|
||||
if node.forward_edges.is_empty() && total_actual != total_charged {
|
||||
return false
|
||||
}
|
||||
|
||||
for loop_node_id in node.loopback_edges.iter() {
|
||||
let (loop_actual, loop_charged) = loop_costs
|
||||
.get_mut(loop_node_id)
|
||||
.expect("cannot arrive at loopback edge without visiting loop entry node");
|
||||
if loop_actual != loop_charged {
|
||||
return false
|
||||
}
|
||||
}
|
||||
|
||||
for next_node_id in node.forward_edges.iter() {
|
||||
if !visit(graph, *next_node_id, total_actual, total_charged, loop_costs) {
|
||||
return false
|
||||
}
|
||||
}
|
||||
|
||||
if node.is_loop_target {
|
||||
loop_costs.remove(&node_id);
|
||||
}
|
||||
|
||||
true
|
||||
}
|
||||
|
||||
// Recursively explore all paths through the execution graph starting from the entry node.
|
||||
visit(graph, 0, 0, 0, &mut Map::new())
|
||||
}
|
||||
|
||||
/// Validate that the metered blocks are correct with respect to the function body by exhaustively
|
||||
/// searching all paths through the control flow graph.
|
||||
///
|
||||
/// This assumes that the function body has been validated already, otherwise this may panic.
|
||||
fn validate_metering_injections(
|
||||
body: &FuncBody,
|
||||
rules: &impl Rules,
|
||||
blocks: &[MeteredBlock],
|
||||
) -> Result<bool, ()> {
|
||||
let graph = build_control_flow_graph(body, rules, blocks)?;
|
||||
Ok(validate_graph_gas_costs(&graph))
|
||||
}
|
||||
|
||||
mod tests {
|
||||
use super::{super::determine_metered_blocks, *};
|
||||
|
||||
use binaryen::tools::translate_to_fuzz_mvp;
|
||||
use parity_wasm::elements;
|
||||
use rand::{thread_rng, RngCore};
|
||||
|
||||
#[test]
|
||||
fn test_build_control_flow_graph() {
|
||||
for _ in 0..20 {
|
||||
let mut rand_input = [0u8; 2048];
|
||||
thread_rng().fill_bytes(&mut rand_input);
|
||||
|
||||
let module_bytes = translate_to_fuzz_mvp(&rand_input).write();
|
||||
let module: elements::Module = elements::deserialize_buffer(&module_bytes)
|
||||
.expect("failed to parse Wasm blob generated by translate_to_fuzz");
|
||||
|
||||
for func_body in module.code_section().iter().flat_map(|section| section.bodies()) {
|
||||
let rules = ConstantCostRules::default();
|
||||
|
||||
let metered_blocks = determine_metered_blocks(func_body.code(), &rules).unwrap();
|
||||
let success =
|
||||
validate_metering_injections(func_body, &rules, &metered_blocks).unwrap();
|
||||
assert!(success);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
+8
-58
@@ -1,63 +1,13 @@
|
||||
#![cfg_attr(not(feature = "std"), no_std)]
|
||||
#![cfg_attr(not(feature = "std"), feature(alloc))]
|
||||
|
||||
#[cfg(not(feature = "std"))]
|
||||
#[macro_use]
|
||||
extern crate alloc;
|
||||
#[macro_use]
|
||||
extern crate log;
|
||||
|
||||
extern crate parity_wasm;
|
||||
extern crate byteorder;
|
||||
#[macro_use] extern crate log;
|
||||
mod export_globals;
|
||||
pub mod gas_metering;
|
||||
mod stack_limiter;
|
||||
|
||||
pub mod rules;
|
||||
|
||||
mod build;
|
||||
mod optimizer;
|
||||
mod gas;
|
||||
mod symbols;
|
||||
mod ext;
|
||||
mod pack;
|
||||
mod runtime_type;
|
||||
|
||||
pub mod stack_height;
|
||||
|
||||
pub use build::{build, SourceTarget, Error as BuildError};
|
||||
pub use optimizer::{optimize, Error as OptimizerError};
|
||||
pub use gas::inject_gas_counter;
|
||||
pub use ext::{externalize, externalize_mem, underscore_funcs, ununderscore_funcs, shrink_unknown_stack};
|
||||
pub use pack::{pack_instance, Error as PackingError};
|
||||
pub use runtime_type::inject_runtime_type;
|
||||
|
||||
pub struct TargetRuntime {
|
||||
pub create_symbol: &'static str,
|
||||
pub call_symbol: &'static str,
|
||||
pub return_symbol: &'static str,
|
||||
}
|
||||
|
||||
impl TargetRuntime {
|
||||
pub fn substrate() -> TargetRuntime {
|
||||
TargetRuntime {
|
||||
create_symbol: "deploy",
|
||||
call_symbol: "call",
|
||||
return_symbol: "ext_return",
|
||||
}
|
||||
}
|
||||
|
||||
pub fn pwasm() -> TargetRuntime {
|
||||
TargetRuntime {
|
||||
create_symbol: "deploy",
|
||||
call_symbol: "call",
|
||||
return_symbol: "ret",
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(not(feature = "std"))]
|
||||
mod std {
|
||||
pub use core::*;
|
||||
pub use alloc::{vec, string, boxed, borrow};
|
||||
|
||||
pub mod collections {
|
||||
pub use alloc::collections::{BTreeMap, BTreeSet};
|
||||
}
|
||||
}
|
||||
pub use export_globals::export_mutable_globals;
|
||||
pub use parity_wasm;
|
||||
pub use stack_limiter::{compute_stack_cost, inject as inject_stack_limiter};
|
||||
|
||||
@@ -1,599 +0,0 @@
|
||||
#[cfg(features = "std")]
|
||||
use std::collections::{HashSet as Set};
|
||||
#[cfg(not(features = "std"))]
|
||||
use std::collections::{BTreeSet as Set};
|
||||
use std::vec::Vec;
|
||||
|
||||
use parity_wasm::elements;
|
||||
|
||||
use symbols::{Symbol, expand_symbols, push_code_symbols, resolve_function};
|
||||
|
||||
#[derive(Debug)]
|
||||
pub enum Error {
|
||||
/// Since optimizer starts with export entries, export
|
||||
/// section is supposed to exist.
|
||||
NoExportSection,
|
||||
}
|
||||
|
||||
pub fn optimize(
|
||||
module: &mut elements::Module, // Module to optimize
|
||||
used_exports: Vec<&str>, // List of only exports that will be usable after optimization
|
||||
) -> Result<(), Error> {
|
||||
// WebAssembly exports optimizer
|
||||
// Motivation: emscripten compiler backend compiles in many unused exports
|
||||
// which in turn compile in unused imports and leaves unused functions
|
||||
|
||||
// Algo starts from the top, listing all items that should stay
|
||||
let mut stay = Set::new();
|
||||
for (index, entry) in module.export_section().ok_or(Error::NoExportSection)?.entries().iter().enumerate() {
|
||||
if used_exports.iter().find(|e| **e == entry.field()).is_some() {
|
||||
stay.insert(Symbol::Export(index));
|
||||
}
|
||||
}
|
||||
|
||||
// If there is start function in module, it should stary
|
||||
module.start_section().map(|ss| stay.insert(resolve_function(&module, ss)));
|
||||
|
||||
// All symbols used in data/element segments are also should be preserved
|
||||
let mut init_symbols = Vec::new();
|
||||
if let Some(data_section) = module.data_section() {
|
||||
for segment in data_section.entries() {
|
||||
push_code_symbols(&module, segment.offset().code(), &mut init_symbols);
|
||||
}
|
||||
}
|
||||
if let Some(elements_section) = module.elements_section() {
|
||||
for segment in elements_section.entries() {
|
||||
push_code_symbols(&module, segment.offset().code(), &mut init_symbols);
|
||||
for func_index in segment.members() {
|
||||
stay.insert(resolve_function(&module, *func_index));
|
||||
}
|
||||
}
|
||||
}
|
||||
for symbol in init_symbols.drain(..) { stay.insert(symbol); }
|
||||
|
||||
// Call function which will traverse the list recursively, filling stay with all symbols
|
||||
// that are already used by those which already there
|
||||
expand_symbols(module, &mut stay);
|
||||
|
||||
for symbol in stay.iter() {
|
||||
trace!("symbol to stay: {:?}", symbol);
|
||||
}
|
||||
|
||||
// Keep track of referreable symbols to rewire calls/globals
|
||||
let mut eliminated_funcs = Vec::new();
|
||||
let mut eliminated_globals = Vec::new();
|
||||
let mut eliminated_types = Vec::new();
|
||||
|
||||
// First, iterate through types
|
||||
let mut index = 0;
|
||||
let mut old_index = 0;
|
||||
|
||||
{
|
||||
loop {
|
||||
if type_section(module).map(|section| section.types_mut().len()).unwrap_or(0) == index { break; }
|
||||
|
||||
if stay.contains(&Symbol::Type(old_index)) {
|
||||
index += 1;
|
||||
} else {
|
||||
type_section(module)
|
||||
.expect("If type section does not exists, the loop will break at the beginning of first iteration")
|
||||
.types_mut().remove(index);
|
||||
eliminated_types.push(old_index);
|
||||
trace!("Eliminated type({})", old_index);
|
||||
}
|
||||
old_index += 1;
|
||||
}
|
||||
}
|
||||
|
||||
// Second, iterate through imports
|
||||
let mut top_funcs = 0;
|
||||
let mut top_globals = 0;
|
||||
index = 0;
|
||||
old_index = 0;
|
||||
|
||||
if let Some(imports) = import_section(module) {
|
||||
loop {
|
||||
let mut remove = false;
|
||||
match imports.entries()[index].external() {
|
||||
&elements::External::Function(_) => {
|
||||
if stay.contains(&Symbol::Import(old_index)) {
|
||||
index += 1;
|
||||
} else {
|
||||
remove = true;
|
||||
eliminated_funcs.push(top_funcs);
|
||||
trace!("Eliminated import({}) func({}, {})", old_index, top_funcs, imports.entries()[index].field());
|
||||
}
|
||||
top_funcs += 1;
|
||||
},
|
||||
&elements::External::Global(_) => {
|
||||
if stay.contains(&Symbol::Import(old_index)) {
|
||||
index += 1;
|
||||
} else {
|
||||
remove = true;
|
||||
eliminated_globals.push(top_globals);
|
||||
trace!("Eliminated import({}) global({}, {})", old_index, top_globals, imports.entries()[index].field());
|
||||
}
|
||||
top_globals += 1;
|
||||
},
|
||||
_ => {
|
||||
index += 1;
|
||||
}
|
||||
}
|
||||
if remove {
|
||||
imports.entries_mut().remove(index);
|
||||
}
|
||||
|
||||
old_index += 1;
|
||||
|
||||
if index == imports.entries().len() { break; }
|
||||
}
|
||||
}
|
||||
|
||||
// Third, iterate through globals
|
||||
if let Some(globals) = global_section(module) {
|
||||
index = 0;
|
||||
old_index = 0;
|
||||
|
||||
loop {
|
||||
if globals.entries_mut().len() == index { break; }
|
||||
if stay.contains(&Symbol::Global(old_index)) {
|
||||
index += 1;
|
||||
} else {
|
||||
globals.entries_mut().remove(index);
|
||||
eliminated_globals.push(top_globals + old_index);
|
||||
trace!("Eliminated global({})", top_globals + old_index);
|
||||
}
|
||||
old_index += 1;
|
||||
}
|
||||
}
|
||||
|
||||
// Forth, delete orphaned functions
|
||||
if function_section(module).is_some() && code_section(module).is_some() {
|
||||
index = 0;
|
||||
old_index = 0;
|
||||
|
||||
loop {
|
||||
if function_section(module).expect("Functons section to exist").entries_mut().len() == index { break; }
|
||||
if stay.contains(&Symbol::Function(old_index)) {
|
||||
index += 1;
|
||||
} else {
|
||||
function_section(module).expect("Functons section to exist").entries_mut().remove(index);
|
||||
code_section(module).expect("Code section to exist").bodies_mut().remove(index);
|
||||
|
||||
eliminated_funcs.push(top_funcs + old_index);
|
||||
trace!("Eliminated function({})", top_funcs + old_index);
|
||||
}
|
||||
old_index += 1;
|
||||
}
|
||||
}
|
||||
|
||||
// Fifth, eliminate unused exports
|
||||
{
|
||||
let exports = export_section(module).ok_or(Error::NoExportSection)?;
|
||||
|
||||
index = 0;
|
||||
old_index = 0;
|
||||
|
||||
loop {
|
||||
if exports.entries_mut().len() == index { break; }
|
||||
if stay.contains(&Symbol::Export(old_index)) {
|
||||
index += 1;
|
||||
} else {
|
||||
trace!("Eliminated export({}, {})", old_index, exports.entries_mut()[index].field());
|
||||
exports.entries_mut().remove(index);
|
||||
}
|
||||
old_index += 1;
|
||||
}
|
||||
}
|
||||
|
||||
if eliminated_globals.len() > 0 || eliminated_funcs.len() > 0 || eliminated_types.len() > 0 {
|
||||
// Finaly, rewire all calls, globals references and types to the new indices
|
||||
// (only if there is anything to do)
|
||||
eliminated_globals.sort();
|
||||
eliminated_funcs.sort();
|
||||
eliminated_types.sort();
|
||||
|
||||
for section in module.sections_mut() {
|
||||
match section {
|
||||
&mut elements::Section::Start(ref mut func_index) if eliminated_funcs.len() > 0 => {
|
||||
let totalle = eliminated_funcs.iter().take_while(|i| (**i as u32) < *func_index).count();
|
||||
*func_index -= totalle as u32;
|
||||
},
|
||||
&mut elements::Section::Function(ref mut function_section) if eliminated_types.len() > 0 => {
|
||||
for ref mut func_signature in function_section.entries_mut() {
|
||||
let totalle = eliminated_types.iter().take_while(|i| (**i as u32) < func_signature.type_ref()).count();
|
||||
*func_signature.type_ref_mut() -= totalle as u32;
|
||||
}
|
||||
},
|
||||
&mut elements::Section::Import(ref mut import_section) if eliminated_types.len() > 0 => {
|
||||
for ref mut import_entry in import_section.entries_mut() {
|
||||
if let &mut elements::External::Function(ref mut type_ref) = import_entry.external_mut() {
|
||||
let totalle = eliminated_types.iter().take_while(|i| (**i as u32) < *type_ref).count();
|
||||
*type_ref -= totalle as u32;
|
||||
}
|
||||
}
|
||||
},
|
||||
&mut elements::Section::Code(ref mut code_section) if eliminated_globals.len() > 0 || eliminated_funcs.len() > 0 => {
|
||||
for ref mut func_body in code_section.bodies_mut() {
|
||||
if eliminated_funcs.len() > 0 {
|
||||
update_call_index(func_body.code_mut(), &eliminated_funcs);
|
||||
}
|
||||
if eliminated_globals.len() > 0 {
|
||||
update_global_index(func_body.code_mut().elements_mut(), &eliminated_globals)
|
||||
}
|
||||
if eliminated_types.len() > 0 {
|
||||
update_type_index(func_body.code_mut(), &eliminated_types)
|
||||
}
|
||||
}
|
||||
},
|
||||
&mut elements::Section::Export(ref mut export_section) => {
|
||||
for ref mut export in export_section.entries_mut() {
|
||||
match export.internal_mut() {
|
||||
&mut elements::Internal::Function(ref mut func_index) => {
|
||||
let totalle = eliminated_funcs.iter().take_while(|i| (**i as u32) < *func_index).count();
|
||||
*func_index -= totalle as u32;
|
||||
},
|
||||
&mut elements::Internal::Global(ref mut global_index) => {
|
||||
let totalle = eliminated_globals.iter().take_while(|i| (**i as u32) < *global_index).count();
|
||||
*global_index -= totalle as u32;
|
||||
},
|
||||
_ => {}
|
||||
}
|
||||
}
|
||||
},
|
||||
&mut elements::Section::Global(ref mut global_section) => {
|
||||
for ref mut global_entry in global_section.entries_mut() {
|
||||
update_global_index(global_entry.init_expr_mut().code_mut(), &eliminated_globals)
|
||||
}
|
||||
},
|
||||
&mut elements::Section::Data(ref mut data_section) => {
|
||||
for ref mut segment in data_section.entries_mut() {
|
||||
update_global_index(segment.offset_mut().code_mut(), &eliminated_globals)
|
||||
}
|
||||
},
|
||||
&mut elements::Section::Element(ref mut elements_section) => {
|
||||
for ref mut segment in elements_section.entries_mut() {
|
||||
update_global_index(segment.offset_mut().code_mut(), &eliminated_globals);
|
||||
// update all indirect call addresses initial values
|
||||
for func_index in segment.members_mut() {
|
||||
let totalle = eliminated_funcs.iter().take_while(|i| (**i as u32) < *func_index).count();
|
||||
*func_index -= totalle as u32;
|
||||
}
|
||||
}
|
||||
},
|
||||
_ => { }
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
|
||||
pub fn update_call_index(instructions: &mut elements::Instructions, eliminated_indices: &[usize]) {
|
||||
use parity_wasm::elements::Instruction::*;
|
||||
for instruction in instructions.elements_mut().iter_mut() {
|
||||
if let &mut Call(ref mut call_index) = instruction {
|
||||
let totalle = eliminated_indices.iter().take_while(|i| (**i as u32) < *call_index).count();
|
||||
trace!("rewired call {} -> call {}", *call_index, *call_index - totalle as u32);
|
||||
*call_index -= totalle as u32;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// Updates global references considering the _ordered_ list of eliminated indices
|
||||
pub fn update_global_index(instructions: &mut Vec<elements::Instruction>, eliminated_indices: &[usize]) {
|
||||
use parity_wasm::elements::Instruction::*;
|
||||
for instruction in instructions.iter_mut() {
|
||||
match instruction {
|
||||
&mut GetGlobal(ref mut index) | &mut SetGlobal(ref mut index) => {
|
||||
let totalle = eliminated_indices.iter().take_while(|i| (**i as u32) < *index).count();
|
||||
trace!("rewired global {} -> global {}", *index, *index - totalle as u32);
|
||||
*index -= totalle as u32;
|
||||
},
|
||||
_ => { },
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// Updates global references considering the _ordered_ list of eliminated indices
|
||||
pub fn update_type_index(instructions: &mut elements::Instructions, eliminated_indices: &[usize]) {
|
||||
use parity_wasm::elements::Instruction::*;
|
||||
for instruction in instructions.elements_mut().iter_mut() {
|
||||
if let &mut CallIndirect(ref mut call_index, _) = instruction {
|
||||
let totalle = eliminated_indices.iter().take_while(|i| (**i as u32) < *call_index).count();
|
||||
trace!("rewired call_indrect {} -> call_indirect {}", *call_index, *call_index - totalle as u32);
|
||||
*call_index -= totalle as u32;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
pub fn import_section<'a>(module: &'a mut elements::Module) -> Option<&'a mut elements::ImportSection> {
|
||||
for section in module.sections_mut() {
|
||||
if let &mut elements::Section::Import(ref mut sect) = section {
|
||||
return Some(sect);
|
||||
}
|
||||
}
|
||||
None
|
||||
}
|
||||
|
||||
pub fn global_section<'a>(module: &'a mut elements::Module) -> Option<&'a mut elements::GlobalSection> {
|
||||
for section in module.sections_mut() {
|
||||
if let &mut elements::Section::Global(ref mut sect) = section {
|
||||
return Some(sect);
|
||||
}
|
||||
}
|
||||
None
|
||||
}
|
||||
|
||||
pub fn function_section<'a>(module: &'a mut elements::Module) -> Option<&'a mut elements::FunctionSection> {
|
||||
for section in module.sections_mut() {
|
||||
if let &mut elements::Section::Function(ref mut sect) = section {
|
||||
return Some(sect);
|
||||
}
|
||||
}
|
||||
None
|
||||
}
|
||||
|
||||
pub fn code_section<'a>(module: &'a mut elements::Module) -> Option<&'a mut elements::CodeSection> {
|
||||
for section in module.sections_mut() {
|
||||
if let &mut elements::Section::Code(ref mut sect) = section {
|
||||
return Some(sect);
|
||||
}
|
||||
}
|
||||
None
|
||||
}
|
||||
|
||||
pub fn export_section<'a>(module: &'a mut elements::Module) -> Option<&'a mut elements::ExportSection> {
|
||||
for section in module.sections_mut() {
|
||||
if let &mut elements::Section::Export(ref mut sect) = section {
|
||||
return Some(sect);
|
||||
}
|
||||
}
|
||||
None
|
||||
}
|
||||
|
||||
pub fn type_section<'a>(module: &'a mut elements::Module) -> Option<&'a mut elements::TypeSection> {
|
||||
for section in module.sections_mut() {
|
||||
if let &mut elements::Section::Type(ref mut sect) = section {
|
||||
return Some(sect);
|
||||
}
|
||||
}
|
||||
None
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
|
||||
use parity_wasm::{builder, elements};
|
||||
use super::*;
|
||||
|
||||
/// @spec 0
|
||||
/// Optimizer presumes that export section exists and contains
|
||||
/// all symbols passed as a second parameter. Since empty module
|
||||
/// obviously contains no export section, optimizer should return
|
||||
/// error on it.
|
||||
#[test]
|
||||
fn empty() {
|
||||
let mut module = builder::module().build();
|
||||
let result = optimize(&mut module, vec!["_call"]);
|
||||
|
||||
assert!(result.is_err());
|
||||
}
|
||||
|
||||
/// @spec 1
|
||||
/// Imagine the unoptimized module has two own functions, `_call` and `_random`
|
||||
/// and exports both of them in the export section. During optimization, the `_random`
|
||||
/// function should vanish completely, given we pass `_call` as the only function to stay
|
||||
/// in the module.
|
||||
#[test]
|
||||
fn minimal() {
|
||||
let mut module = builder::module()
|
||||
.function()
|
||||
.signature().param().i32().build()
|
||||
.build()
|
||||
.function()
|
||||
.signature()
|
||||
.param().i32()
|
||||
.param().i32()
|
||||
.build()
|
||||
.build()
|
||||
.export()
|
||||
.field("_call")
|
||||
.internal().func(0).build()
|
||||
.export()
|
||||
.field("_random")
|
||||
.internal().func(1).build()
|
||||
.build();
|
||||
assert_eq!(module.export_section().expect("export section to be generated").entries().len(), 2);
|
||||
|
||||
optimize(&mut module, vec!["_call"]).expect("optimizer to succeed");
|
||||
|
||||
assert_eq!(
|
||||
1,
|
||||
module.export_section().expect("export section to be generated").entries().len(),
|
||||
"There should only 1 (one) export entry in the optimized module"
|
||||
);
|
||||
|
||||
assert_eq!(
|
||||
1,
|
||||
module.function_section().expect("functions section to be generated").entries().len(),
|
||||
"There should 2 (two) functions in the optimized module"
|
||||
);
|
||||
}
|
||||
|
||||
/// @spec 2
|
||||
/// Imagine there is one exported function in unoptimized module, `_call`, that we specify as the one
|
||||
/// to stay during the optimization. The code of this function uses global during the execution.
|
||||
/// This sayed global should survive the optimization.
|
||||
#[test]
|
||||
fn globals() {
|
||||
let mut module = builder::module()
|
||||
.global()
|
||||
.value_type().i32()
|
||||
.build()
|
||||
.function()
|
||||
.signature().param().i32().build()
|
||||
.body()
|
||||
.with_instructions(elements::Instructions::new(
|
||||
vec![
|
||||
elements::Instruction::GetGlobal(0),
|
||||
elements::Instruction::End
|
||||
]
|
||||
))
|
||||
.build()
|
||||
.build()
|
||||
.export()
|
||||
.field("_call")
|
||||
.internal().func(0).build()
|
||||
.build();
|
||||
|
||||
optimize(&mut module, vec!["_call"]).expect("optimizer to succeed");
|
||||
|
||||
assert_eq!(
|
||||
1,
|
||||
module.global_section().expect("global section to be generated").entries().len(),
|
||||
"There should 1 (one) global entry in the optimized module, since _call function uses it"
|
||||
);
|
||||
}
|
||||
|
||||
/// @spec 2
|
||||
/// Imagine there is one exported function in unoptimized module, `_call`, that we specify as the one
|
||||
/// to stay during the optimization. The code of this function uses one global during the execution,
|
||||
/// but we have a bunch of other unused globals in the code. Last globals should not survive the optimization,
|
||||
/// while the former should.
|
||||
#[test]
|
||||
fn globals_2() {
|
||||
let mut module = builder::module()
|
||||
.global()
|
||||
.value_type().i32()
|
||||
.build()
|
||||
.global()
|
||||
.value_type().i64()
|
||||
.build()
|
||||
.global()
|
||||
.value_type().f32()
|
||||
.build()
|
||||
.function()
|
||||
.signature().param().i32().build()
|
||||
.body()
|
||||
.with_instructions(elements::Instructions::new(
|
||||
vec![
|
||||
elements::Instruction::GetGlobal(1),
|
||||
elements::Instruction::End
|
||||
]
|
||||
))
|
||||
.build()
|
||||
.build()
|
||||
.export()
|
||||
.field("_call")
|
||||
.internal().func(0).build()
|
||||
.build();
|
||||
|
||||
optimize(&mut module, vec!["_call"]).expect("optimizer to succeed");
|
||||
|
||||
assert_eq!(
|
||||
1,
|
||||
module.global_section().expect("global section to be generated").entries().len(),
|
||||
"There should 1 (one) global entry in the optimized module, since _call function uses only one"
|
||||
);
|
||||
}
|
||||
|
||||
/// @spec 3
|
||||
/// Imagine the unoptimized module has two own functions, `_call` and `_random`
|
||||
/// and exports both of them in the export section. Function `_call` also calls `_random`
|
||||
/// in its function body. The optimization should kick `_random` function from the export section
|
||||
/// but preserve it's body.
|
||||
#[test]
|
||||
fn call_ref() {
|
||||
let mut module = builder::module()
|
||||
.function()
|
||||
.signature().param().i32().build()
|
||||
.body()
|
||||
.with_instructions(elements::Instructions::new(
|
||||
vec![
|
||||
elements::Instruction::Call(1),
|
||||
elements::Instruction::End
|
||||
]
|
||||
))
|
||||
.build()
|
||||
.build()
|
||||
.function()
|
||||
.signature()
|
||||
.param().i32()
|
||||
.param().i32()
|
||||
.build()
|
||||
.build()
|
||||
.export()
|
||||
.field("_call")
|
||||
.internal().func(0).build()
|
||||
.export()
|
||||
.field("_random")
|
||||
.internal().func(1).build()
|
||||
.build();
|
||||
assert_eq!(module.export_section().expect("export section to be generated").entries().len(), 2);
|
||||
|
||||
optimize(&mut module, vec!["_call"]).expect("optimizer to succeed");
|
||||
|
||||
assert_eq!(
|
||||
1,
|
||||
module.export_section().expect("export section to be generated").entries().len(),
|
||||
"There should only 1 (one) export entry in the optimized module"
|
||||
);
|
||||
|
||||
assert_eq!(
|
||||
2,
|
||||
module.function_section().expect("functions section to be generated").entries().len(),
|
||||
"There should 2 (two) functions in the optimized module"
|
||||
);
|
||||
}
|
||||
|
||||
/// @spec 4
|
||||
/// Imagine the unoptimized module has an indirect call to function of type 1
|
||||
/// The type should persist so that indirect call would work
|
||||
#[test]
|
||||
fn call_indirect() {
|
||||
let mut module = builder::module()
|
||||
.function()
|
||||
.signature().param().i32().param().i32().build()
|
||||
.build()
|
||||
.function()
|
||||
.signature().param().i32().param().i32().param().i32().build()
|
||||
.build()
|
||||
.function()
|
||||
.signature().param().i32().build()
|
||||
.body()
|
||||
.with_instructions(elements::Instructions::new(
|
||||
vec![
|
||||
elements::Instruction::CallIndirect(1, 0),
|
||||
elements::Instruction::End
|
||||
]
|
||||
))
|
||||
.build()
|
||||
.build()
|
||||
.export()
|
||||
.field("_call")
|
||||
.internal().func(2).build()
|
||||
.build();
|
||||
|
||||
optimize(&mut module, vec!["_call"]).expect("optimizer to succeed");
|
||||
|
||||
assert_eq!(
|
||||
2,
|
||||
module.type_section().expect("type section to be generated").types().len(),
|
||||
"There should 2 (two) types left in the module, 1 for indirect call and one for _call"
|
||||
);
|
||||
|
||||
let indirect_opcode = &module.code_section().expect("code section to be generated").bodies()[0].code().elements()[0];
|
||||
match *indirect_opcode {
|
||||
elements::Instruction::CallIndirect(0, 0) => {},
|
||||
_ => {
|
||||
panic!(
|
||||
"Expected call_indirect to use index 0 after optimization, since previois 0th was eliminated, but got {:?}",
|
||||
indirect_opcode
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
-335
@@ -1,335 +0,0 @@
|
||||
use std::fmt;
|
||||
use std::vec::Vec;
|
||||
use std::borrow::ToOwned;
|
||||
|
||||
use parity_wasm::elements::{
|
||||
self, Section, DataSection, Instruction, DataSegment, InitExpr, Internal, External,
|
||||
ImportCountType,
|
||||
};
|
||||
use parity_wasm::builder;
|
||||
use super::TargetRuntime;
|
||||
use super::gas::update_call_index;
|
||||
|
||||
/// Pack error.
|
||||
///
|
||||
/// Pack has number of assumptions of passed module structure.
|
||||
/// When they are violated, pack_instance returns one of these.
|
||||
#[derive(Debug)]
|
||||
pub enum Error {
|
||||
MalformedModule,
|
||||
NoTypeSection,
|
||||
NoExportSection,
|
||||
NoCodeSection,
|
||||
InvalidCreateSignature(&'static str),
|
||||
NoCreateSymbol(&'static str),
|
||||
InvalidCreateMember(&'static str),
|
||||
NoImportSection,
|
||||
}
|
||||
|
||||
impl fmt::Display for Error {
|
||||
fn fmt(&self, f: &mut fmt::Formatter) -> Result<(), fmt::Error> {
|
||||
match *self {
|
||||
Error::MalformedModule => write!(f, "Module internal references are inconsistent"),
|
||||
Error::NoTypeSection => write!(f, "No type section in the module"),
|
||||
Error::NoExportSection => write!(f, "No export section in the module"),
|
||||
Error::NoCodeSection => write!(f, "No code section inthe module"),
|
||||
Error::InvalidCreateSignature(sym) => write!(f, "Exported symbol `{}` has invalid signature, should be () -> ()", sym),
|
||||
Error::InvalidCreateMember(sym) => write!(f, "Exported symbol `{}` should be a function", sym),
|
||||
Error::NoCreateSymbol(sym) => write!(f, "No exported `{}` symbol", sym),
|
||||
Error::NoImportSection => write!(f, "No import section in the module"),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// If module has an exported "CREATE_SYMBOL" function we want to pack it into "constructor".
|
||||
/// `raw_module` is the actual contract code
|
||||
/// `ctor_module` is the constructor which should return `raw_module`
|
||||
pub fn pack_instance(raw_module: Vec<u8>, mut ctor_module: elements::Module, target: &TargetRuntime) -> Result<elements::Module, Error> {
|
||||
|
||||
// Total number of constructor module import functions
|
||||
let ctor_import_functions = ctor_module.import_section().map(|x| x.functions()).unwrap_or(0);
|
||||
|
||||
// We need to find an internal ID of function witch is exported as "CREATE_SYMBOL"
|
||||
// in order to find it in the Code section of the module
|
||||
let mut create_func_id = {
|
||||
let found_entry = ctor_module.export_section().ok_or(Error::NoExportSection)?.entries().iter()
|
||||
.find(|entry| target.create_symbol == entry.field()).ok_or_else(|| Error::NoCreateSymbol(target.create_symbol))?;
|
||||
|
||||
let function_index: usize = match found_entry.internal() {
|
||||
&Internal::Function(index) => index as usize,
|
||||
_ => { return Err(Error::InvalidCreateMember(target.create_symbol)) },
|
||||
};
|
||||
|
||||
// Calculates a function index within module's function section
|
||||
let function_internal_index = function_index - ctor_import_functions;
|
||||
|
||||
// Constructor should be of signature `func()` (void), fail otherwise
|
||||
let type_id = ctor_module.function_section().ok_or(Error::NoCodeSection)?
|
||||
.entries().get(function_index - ctor_import_functions).ok_or(Error::MalformedModule)?
|
||||
.type_ref();
|
||||
|
||||
let &elements::Type::Function(ref func) = ctor_module.type_section().ok_or(Error::NoTypeSection)?
|
||||
.types().get(type_id as usize).ok_or(Error::MalformedModule)?;
|
||||
|
||||
// Deploy should have no arguments and also should return nothing
|
||||
if !func.params().is_empty() {
|
||||
return Err(Error::InvalidCreateSignature(target.create_symbol));
|
||||
}
|
||||
if func.return_type().is_some() {
|
||||
return Err(Error::InvalidCreateSignature(target.create_symbol));
|
||||
}
|
||||
|
||||
function_internal_index
|
||||
};
|
||||
|
||||
let ret_function_id = {
|
||||
let mut id = 0;
|
||||
let mut found = false;
|
||||
for entry in ctor_module.import_section().ok_or(Error::NoImportSection)?.entries().iter() {
|
||||
if let External::Function(_) = *entry.external() {
|
||||
if entry.field() == target.return_symbol { found = true; break; }
|
||||
else { id += 1; }
|
||||
}
|
||||
}
|
||||
if !found {
|
||||
let mut mbuilder = builder::from_module(ctor_module);
|
||||
let import_sig = mbuilder.push_signature(
|
||||
builder::signature()
|
||||
.param().i32().param().i32()
|
||||
.build_sig()
|
||||
);
|
||||
|
||||
mbuilder.push_import(
|
||||
builder::import()
|
||||
.module("env")
|
||||
.field(&target.return_symbol)
|
||||
.external().func(import_sig)
|
||||
.build()
|
||||
);
|
||||
|
||||
ctor_module = mbuilder.build();
|
||||
|
||||
let ret_func = ctor_module.import_count(ImportCountType::Function) as u32 - 1;
|
||||
|
||||
for section in ctor_module.sections_mut() {
|
||||
match *section {
|
||||
elements::Section::Code(ref mut code_section) => {
|
||||
for ref mut func_body in code_section.bodies_mut() {
|
||||
update_call_index(func_body.code_mut(), ret_func);
|
||||
}
|
||||
},
|
||||
elements::Section::Export(ref mut export_section) => {
|
||||
for ref mut export in export_section.entries_mut() {
|
||||
if let &mut elements::Internal::Function(ref mut func_index) = export.internal_mut() {
|
||||
if *func_index >= ret_func { *func_index += 1}
|
||||
}
|
||||
}
|
||||
},
|
||||
elements::Section::Element(ref mut elements_section) => {
|
||||
for ref mut segment in elements_section.entries_mut() {
|
||||
// update all indirect call addresses initial values
|
||||
for func_index in segment.members_mut() {
|
||||
if *func_index >= ret_func { *func_index += 1}
|
||||
}
|
||||
}
|
||||
},
|
||||
_ => { }
|
||||
}
|
||||
}
|
||||
|
||||
create_func_id += 1;
|
||||
ret_func
|
||||
}
|
||||
else { id }
|
||||
};
|
||||
|
||||
// If new function is put in ctor module, it will have this callable index
|
||||
let last_function_index = ctor_module.functions_space();
|
||||
|
||||
// We ensure here that module has the DataSection
|
||||
if ctor_module
|
||||
.sections()
|
||||
.iter()
|
||||
.find(|section| match **section { Section::Data(ref _d) => true, _ => false })
|
||||
.is_none() {
|
||||
// DataSection has to be the last non-custom section according the to the spec
|
||||
ctor_module.sections_mut().push(Section::Data(DataSection::with_entries(vec![])));
|
||||
}
|
||||
|
||||
// Code data address is an address where we put the contract's code (raw_module)
|
||||
let mut code_data_address = 0i32;
|
||||
|
||||
for section in ctor_module.sections_mut() {
|
||||
if let &mut Section::Data(ref mut data_section) = section {
|
||||
let (index, offset) = if let Some(ref entry) = data_section.entries().iter().last() {
|
||||
if let Instruction::I32Const(offst) = entry.offset().code()[0] {
|
||||
let len = entry.value().len() as i32;
|
||||
let offst = offst as i32;
|
||||
(entry.index(), offst + (len + 4) - len % 4)
|
||||
} else {
|
||||
(0, 0)
|
||||
}
|
||||
} else {
|
||||
(0, 0)
|
||||
};
|
||||
let code_data = DataSegment::new(
|
||||
index,
|
||||
InitExpr::new(vec![Instruction::I32Const(offset), Instruction::End]),
|
||||
raw_module.clone()
|
||||
);
|
||||
data_section.entries_mut().push(code_data);
|
||||
code_data_address = offset;
|
||||
}
|
||||
}
|
||||
|
||||
let mut new_module = builder::from_module(ctor_module)
|
||||
.function()
|
||||
.signature().build()
|
||||
.body().with_instructions(elements::Instructions::new(
|
||||
vec![
|
||||
Instruction::Call((create_func_id + ctor_import_functions) as u32),
|
||||
Instruction::I32Const(code_data_address),
|
||||
Instruction::I32Const(raw_module.len() as i32),
|
||||
Instruction::Call(ret_function_id as u32),
|
||||
Instruction::End,
|
||||
])).build()
|
||||
.build()
|
||||
.build();
|
||||
|
||||
for section in new_module.sections_mut() {
|
||||
if let &mut Section::Export(ref mut export_section) = section {
|
||||
for entry in export_section.entries_mut().iter_mut() {
|
||||
if target.create_symbol == entry.field() {
|
||||
// change `create_symbol` export name into default `call_symbol`.
|
||||
*entry.field_mut() = target.call_symbol.to_owned();
|
||||
*entry.internal_mut() = elements::Internal::Function(last_function_index as u32);
|
||||
}
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
Ok(new_module)
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod test {
|
||||
extern crate parity_wasm;
|
||||
|
||||
use parity_wasm::builder;
|
||||
use super::*;
|
||||
use super::super::optimize;
|
||||
|
||||
fn test_packer(mut module: elements::Module, target_runtime: &TargetRuntime) {
|
||||
let mut ctor_module = module.clone();
|
||||
optimize(&mut module, vec![target_runtime.call_symbol]).expect("Optimizer to finish without errors");
|
||||
optimize(&mut ctor_module, vec![target_runtime.create_symbol]).expect("Optimizer to finish without errors");
|
||||
|
||||
let raw_module = parity_wasm::serialize(module).unwrap();
|
||||
let ctor_module = pack_instance(raw_module.clone(), ctor_module, target_runtime).expect("Packing failed");
|
||||
|
||||
let data_section = ctor_module.data_section().expect("Packed module has to have a data section");
|
||||
let data_segment = data_section.entries().iter().last().expect("Packed module has to have a data section with at least one entry");
|
||||
assert!(data_segment.value() == AsRef::<[u8]>::as_ref(&raw_module), "Last data segment should be equal to the raw module");
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn no_data_section() {
|
||||
let target_runtime = TargetRuntime::pwasm();
|
||||
|
||||
test_packer(builder::module()
|
||||
.import()
|
||||
.module("env")
|
||||
.field("memory")
|
||||
.external().memory(1 as u32, Some(1 as u32))
|
||||
.build()
|
||||
.function()
|
||||
.signature()
|
||||
.params().i32().i32().build()
|
||||
.build()
|
||||
.body().build()
|
||||
.build()
|
||||
.function()
|
||||
.signature().build()
|
||||
.body()
|
||||
.with_instructions(elements::Instructions::new(
|
||||
vec![
|
||||
elements::Instruction::End
|
||||
]
|
||||
))
|
||||
.build()
|
||||
.build()
|
||||
.function()
|
||||
.signature().build()
|
||||
.body()
|
||||
.with_instructions(elements::Instructions::new(
|
||||
vec![
|
||||
elements::Instruction::End
|
||||
]
|
||||
))
|
||||
.build()
|
||||
.build()
|
||||
.export()
|
||||
.field(target_runtime.call_symbol)
|
||||
.internal().func(1)
|
||||
.build()
|
||||
.export()
|
||||
.field(target_runtime.create_symbol)
|
||||
.internal().func(2)
|
||||
.build()
|
||||
.build(),
|
||||
&target_runtime,
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn with_data_section() {
|
||||
let target_runtime = TargetRuntime::pwasm();
|
||||
|
||||
test_packer(builder::module()
|
||||
.import()
|
||||
.module("env")
|
||||
.field("memory")
|
||||
.external().memory(1 as u32, Some(1 as u32))
|
||||
.build()
|
||||
.data()
|
||||
.offset(elements::Instruction::I32Const(16)).value(vec![0u8])
|
||||
.build()
|
||||
.function()
|
||||
.signature()
|
||||
.params().i32().i32().build()
|
||||
.build()
|
||||
.body().build()
|
||||
.build()
|
||||
.function()
|
||||
.signature().build()
|
||||
.body()
|
||||
.with_instructions(elements::Instructions::new(
|
||||
vec![
|
||||
elements::Instruction::End
|
||||
]
|
||||
))
|
||||
.build()
|
||||
.build()
|
||||
.function()
|
||||
.signature().build()
|
||||
.body()
|
||||
.with_instructions(elements::Instructions::new(
|
||||
vec![
|
||||
elements::Instruction::End
|
||||
]
|
||||
))
|
||||
.build()
|
||||
.build()
|
||||
.export()
|
||||
.field(target_runtime.call_symbol)
|
||||
.internal().func(1)
|
||||
.build()
|
||||
.export()
|
||||
.field(target_runtime.create_symbol)
|
||||
.internal().func(2)
|
||||
.build()
|
||||
.build(),
|
||||
&target_runtime,
|
||||
);
|
||||
}
|
||||
}
|
||||
-315
@@ -1,315 +0,0 @@
|
||||
#[cfg(features = "std")]
|
||||
use std::collections::{HashMap as Map};
|
||||
#[cfg(not(features = "std"))]
|
||||
use std::collections::{BTreeMap as Map};
|
||||
|
||||
use parity_wasm::elements;
|
||||
|
||||
pub struct UnknownInstruction;
|
||||
|
||||
#[derive(Debug, PartialEq, Eq, Copy, Clone)]
|
||||
pub enum Metering {
|
||||
Regular,
|
||||
Forbidden,
|
||||
Fixed(u32),
|
||||
}
|
||||
|
||||
#[derive(Debug, Hash, PartialEq, Eq, PartialOrd, Ord, Copy, Clone)]
|
||||
pub enum InstructionType {
|
||||
Bit,
|
||||
Add,
|
||||
Mul,
|
||||
Div,
|
||||
Load,
|
||||
Store,
|
||||
Const,
|
||||
FloatConst,
|
||||
Local,
|
||||
Global,
|
||||
ControlFlow,
|
||||
IntegerComparsion,
|
||||
FloatComparsion,
|
||||
Float,
|
||||
Conversion,
|
||||
FloatConversion,
|
||||
Reinterpretation,
|
||||
Unreachable,
|
||||
Nop,
|
||||
CurrentMemory,
|
||||
GrowMemory,
|
||||
}
|
||||
|
||||
impl ::std::str::FromStr for InstructionType {
|
||||
type Err = UnknownInstruction;
|
||||
|
||||
fn from_str(s: &str) -> Result<Self, Self::Err> {
|
||||
match s {
|
||||
"bit" => Ok(InstructionType::Bit),
|
||||
"add" => Ok(InstructionType::Add),
|
||||
"mul" => Ok(InstructionType::Mul),
|
||||
"div" => Ok(InstructionType::Div),
|
||||
"load" => Ok(InstructionType::Load),
|
||||
"store" => Ok(InstructionType::Store),
|
||||
"const" => Ok(InstructionType::Const),
|
||||
"local" => Ok(InstructionType::Local),
|
||||
"global" => Ok(InstructionType::Global),
|
||||
"flow" => Ok(InstructionType::ControlFlow),
|
||||
"integer_comp" => Ok(InstructionType::IntegerComparsion),
|
||||
"float_comp" => Ok(InstructionType::FloatComparsion),
|
||||
"float" => Ok(InstructionType::Float),
|
||||
"conversion" => Ok(InstructionType::Conversion),
|
||||
"float_conversion" => Ok(InstructionType::FloatConversion),
|
||||
"reinterpret" => Ok(InstructionType::Reinterpretation),
|
||||
"unreachable" => Ok(InstructionType::Unreachable),
|
||||
"nop" => Ok(InstructionType::Nop),
|
||||
"current_mem" => Ok(InstructionType::CurrentMemory),
|
||||
"grow_mem" => Ok(InstructionType::GrowMemory),
|
||||
_ => Err(UnknownInstruction),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl InstructionType {
|
||||
pub fn op(instruction: &elements::Instruction) -> Self {
|
||||
use parity_wasm::elements::Instruction::*;
|
||||
|
||||
match *instruction {
|
||||
Unreachable => InstructionType::Unreachable,
|
||||
Nop => InstructionType::Nop,
|
||||
Block(_) => InstructionType::ControlFlow,
|
||||
Loop(_) => InstructionType::ControlFlow,
|
||||
If(_) => InstructionType::ControlFlow,
|
||||
Else => InstructionType::ControlFlow,
|
||||
End => InstructionType::ControlFlow,
|
||||
Br(_) => InstructionType::ControlFlow,
|
||||
BrIf(_) => InstructionType::ControlFlow,
|
||||
BrTable(_, _) => InstructionType::ControlFlow,
|
||||
Return => InstructionType::ControlFlow,
|
||||
Call(_) => InstructionType::ControlFlow,
|
||||
CallIndirect(_, _) => InstructionType::ControlFlow,
|
||||
Drop => InstructionType::ControlFlow,
|
||||
Select => InstructionType::ControlFlow,
|
||||
|
||||
GetLocal(_) => InstructionType::Local,
|
||||
SetLocal(_) => InstructionType::Local,
|
||||
TeeLocal(_) => InstructionType::Local,
|
||||
GetGlobal(_) => InstructionType::Local,
|
||||
SetGlobal(_) => InstructionType::Local,
|
||||
|
||||
I32Load(_, _) => InstructionType::Load,
|
||||
I64Load(_, _) => InstructionType::Load,
|
||||
F32Load(_, _) => InstructionType::Load,
|
||||
F64Load(_, _) => InstructionType::Load,
|
||||
I32Load8S(_, _) => InstructionType::Load,
|
||||
I32Load8U(_, _) => InstructionType::Load,
|
||||
I32Load16S(_, _) => InstructionType::Load,
|
||||
I32Load16U(_, _) => InstructionType::Load,
|
||||
I64Load8S(_, _) => InstructionType::Load,
|
||||
I64Load8U(_, _) => InstructionType::Load,
|
||||
I64Load16S(_, _) => InstructionType::Load,
|
||||
I64Load16U(_, _) => InstructionType::Load,
|
||||
I64Load32S(_, _) => InstructionType::Load,
|
||||
I64Load32U(_, _) => InstructionType::Load,
|
||||
|
||||
I32Store(_, _) => InstructionType::Store,
|
||||
I64Store(_, _) => InstructionType::Store,
|
||||
F32Store(_, _) => InstructionType::Store,
|
||||
F64Store(_, _) => InstructionType::Store,
|
||||
I32Store8(_, _) => InstructionType::Store,
|
||||
I32Store16(_, _) => InstructionType::Store,
|
||||
I64Store8(_, _) => InstructionType::Store,
|
||||
I64Store16(_, _) => InstructionType::Store,
|
||||
I64Store32(_, _) => InstructionType::Store,
|
||||
|
||||
CurrentMemory(_) => InstructionType::CurrentMemory,
|
||||
GrowMemory(_) => InstructionType::GrowMemory,
|
||||
|
||||
I32Const(_) => InstructionType::Const,
|
||||
I64Const(_) => InstructionType::Const,
|
||||
|
||||
F32Const(_) => InstructionType::FloatConst,
|
||||
F64Const(_) => InstructionType::FloatConst,
|
||||
|
||||
I32Eqz => InstructionType::IntegerComparsion,
|
||||
I32Eq => InstructionType::IntegerComparsion,
|
||||
I32Ne => InstructionType::IntegerComparsion,
|
||||
I32LtS => InstructionType::IntegerComparsion,
|
||||
I32LtU => InstructionType::IntegerComparsion,
|
||||
I32GtS => InstructionType::IntegerComparsion,
|
||||
I32GtU => InstructionType::IntegerComparsion,
|
||||
I32LeS => InstructionType::IntegerComparsion,
|
||||
I32LeU => InstructionType::IntegerComparsion,
|
||||
I32GeS => InstructionType::IntegerComparsion,
|
||||
I32GeU => InstructionType::IntegerComparsion,
|
||||
|
||||
I64Eqz => InstructionType::IntegerComparsion,
|
||||
I64Eq => InstructionType::IntegerComparsion,
|
||||
I64Ne => InstructionType::IntegerComparsion,
|
||||
I64LtS => InstructionType::IntegerComparsion,
|
||||
I64LtU => InstructionType::IntegerComparsion,
|
||||
I64GtS => InstructionType::IntegerComparsion,
|
||||
I64GtU => InstructionType::IntegerComparsion,
|
||||
I64LeS => InstructionType::IntegerComparsion,
|
||||
I64LeU => InstructionType::IntegerComparsion,
|
||||
I64GeS => InstructionType::IntegerComparsion,
|
||||
I64GeU => InstructionType::IntegerComparsion,
|
||||
|
||||
F32Eq => InstructionType::FloatComparsion,
|
||||
F32Ne => InstructionType::FloatComparsion,
|
||||
F32Lt => InstructionType::FloatComparsion,
|
||||
F32Gt => InstructionType::FloatComparsion,
|
||||
F32Le => InstructionType::FloatComparsion,
|
||||
F32Ge => InstructionType::FloatComparsion,
|
||||
|
||||
F64Eq => InstructionType::FloatComparsion,
|
||||
F64Ne => InstructionType::FloatComparsion,
|
||||
F64Lt => InstructionType::FloatComparsion,
|
||||
F64Gt => InstructionType::FloatComparsion,
|
||||
F64Le => InstructionType::FloatComparsion,
|
||||
F64Ge => InstructionType::FloatComparsion,
|
||||
|
||||
I32Clz => InstructionType::Bit,
|
||||
I32Ctz => InstructionType::Bit,
|
||||
I32Popcnt => InstructionType::Bit,
|
||||
I32Add => InstructionType::Add,
|
||||
I32Sub => InstructionType::Add,
|
||||
I32Mul => InstructionType::Mul,
|
||||
I32DivS => InstructionType::Div,
|
||||
I32DivU => InstructionType::Div,
|
||||
I32RemS => InstructionType::Div,
|
||||
I32RemU => InstructionType::Div,
|
||||
I32And => InstructionType::Bit,
|
||||
I32Or => InstructionType::Bit,
|
||||
I32Xor => InstructionType::Bit,
|
||||
I32Shl => InstructionType::Bit,
|
||||
I32ShrS => InstructionType::Bit,
|
||||
I32ShrU => InstructionType::Bit,
|
||||
I32Rotl => InstructionType::Bit,
|
||||
I32Rotr => InstructionType::Bit,
|
||||
|
||||
I64Clz => InstructionType::Bit,
|
||||
I64Ctz => InstructionType::Bit,
|
||||
I64Popcnt => InstructionType::Bit,
|
||||
I64Add => InstructionType::Add,
|
||||
I64Sub => InstructionType::Add,
|
||||
I64Mul => InstructionType::Mul,
|
||||
I64DivS => InstructionType::Div,
|
||||
I64DivU => InstructionType::Div,
|
||||
I64RemS => InstructionType::Div,
|
||||
I64RemU => InstructionType::Div,
|
||||
I64And => InstructionType::Bit,
|
||||
I64Or => InstructionType::Bit,
|
||||
I64Xor => InstructionType::Bit,
|
||||
I64Shl => InstructionType::Bit,
|
||||
I64ShrS => InstructionType::Bit,
|
||||
I64ShrU => InstructionType::Bit,
|
||||
I64Rotl => InstructionType::Bit,
|
||||
I64Rotr => InstructionType::Bit,
|
||||
|
||||
F32Abs => InstructionType::Float,
|
||||
F32Neg => InstructionType::Float,
|
||||
F32Ceil => InstructionType::Float,
|
||||
F32Floor => InstructionType::Float,
|
||||
F32Trunc => InstructionType::Float,
|
||||
F32Nearest => InstructionType::Float,
|
||||
F32Sqrt => InstructionType::Float,
|
||||
F32Add => InstructionType::Float,
|
||||
F32Sub => InstructionType::Float,
|
||||
F32Mul => InstructionType::Float,
|
||||
F32Div => InstructionType::Float,
|
||||
F32Min => InstructionType::Float,
|
||||
F32Max => InstructionType::Float,
|
||||
F32Copysign => InstructionType::Float,
|
||||
F64Abs => InstructionType::Float,
|
||||
F64Neg => InstructionType::Float,
|
||||
F64Ceil => InstructionType::Float,
|
||||
F64Floor => InstructionType::Float,
|
||||
F64Trunc => InstructionType::Float,
|
||||
F64Nearest => InstructionType::Float,
|
||||
F64Sqrt => InstructionType::Float,
|
||||
F64Add => InstructionType::Float,
|
||||
F64Sub => InstructionType::Float,
|
||||
F64Mul => InstructionType::Float,
|
||||
F64Div => InstructionType::Float,
|
||||
F64Min => InstructionType::Float,
|
||||
F64Max => InstructionType::Float,
|
||||
F64Copysign => InstructionType::Float,
|
||||
|
||||
I32WrapI64 => InstructionType::Conversion,
|
||||
I64ExtendSI32 => InstructionType::Conversion,
|
||||
I64ExtendUI32 => InstructionType::Conversion,
|
||||
|
||||
I32TruncSF32 => InstructionType::FloatConversion,
|
||||
I32TruncUF32 => InstructionType::FloatConversion,
|
||||
I32TruncSF64 => InstructionType::FloatConversion,
|
||||
I32TruncUF64 => InstructionType::FloatConversion,
|
||||
I64TruncSF32 => InstructionType::FloatConversion,
|
||||
I64TruncUF32 => InstructionType::FloatConversion,
|
||||
I64TruncSF64 => InstructionType::FloatConversion,
|
||||
I64TruncUF64 => InstructionType::FloatConversion,
|
||||
F32ConvertSI32 => InstructionType::FloatConversion,
|
||||
F32ConvertUI32 => InstructionType::FloatConversion,
|
||||
F32ConvertSI64 => InstructionType::FloatConversion,
|
||||
F32ConvertUI64 => InstructionType::FloatConversion,
|
||||
F32DemoteF64 => InstructionType::FloatConversion,
|
||||
F64ConvertSI32 => InstructionType::FloatConversion,
|
||||
F64ConvertUI32 => InstructionType::FloatConversion,
|
||||
F64ConvertSI64 => InstructionType::FloatConversion,
|
||||
F64ConvertUI64 => InstructionType::FloatConversion,
|
||||
F64PromoteF32 => InstructionType::FloatConversion,
|
||||
|
||||
I32ReinterpretF32 => InstructionType::Reinterpretation,
|
||||
I64ReinterpretF64 => InstructionType::Reinterpretation,
|
||||
F32ReinterpretI32 => InstructionType::Reinterpretation,
|
||||
F64ReinterpretI64 => InstructionType::Reinterpretation,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Debug)]
|
||||
pub struct Set {
|
||||
regular: u32,
|
||||
entries: Map<InstructionType, Metering>,
|
||||
grow: u32,
|
||||
}
|
||||
|
||||
impl Default for Set {
|
||||
fn default() -> Self {
|
||||
Set {
|
||||
regular: 1,
|
||||
entries: Map::new(),
|
||||
grow: 0,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl Set {
|
||||
pub fn new(regular: u32, entries: Map<InstructionType, Metering>) -> Self {
|
||||
Set { regular: regular, entries: entries, grow: 0 }
|
||||
}
|
||||
|
||||
pub fn process(&self, instruction: &elements::Instruction) -> Result<u32, ()> {
|
||||
match self.entries.get(&InstructionType::op(instruction)).map(|x| *x) {
|
||||
None | Some(Metering::Regular) => Ok(self.regular),
|
||||
Some(Metering::Forbidden) => Err(()),
|
||||
Some(Metering::Fixed(val)) => Ok(val),
|
||||
}
|
||||
}
|
||||
|
||||
pub fn grow_cost(&self) -> u32 {
|
||||
self.grow
|
||||
}
|
||||
|
||||
pub fn with_grow_cost(mut self, val: u32) -> Self {
|
||||
self.grow = val;
|
||||
self
|
||||
}
|
||||
|
||||
pub fn with_forbidden_floats(mut self) -> Self {
|
||||
self.entries.insert(InstructionType::Float, Metering::Forbidden);
|
||||
self.entries.insert(InstructionType::FloatComparsion, Metering::Forbidden);
|
||||
self.entries.insert(InstructionType::FloatConst, Metering::Forbidden);
|
||||
self.entries.insert(InstructionType::FloatConversion, Metering::Forbidden);
|
||||
self
|
||||
}
|
||||
}
|
||||
@@ -1,45 +0,0 @@
|
||||
use parity_wasm::{elements, builder};
|
||||
use self::elements::{ Module, GlobalEntry, External, ExportEntry, GlobalType, ValueType, InitExpr, Instruction, Internal };
|
||||
use byteorder::{ LittleEndian, ByteOrder };
|
||||
|
||||
pub fn inject_runtime_type(module: Module, runtime_type: [u8; 4], runtime_version: u32) -> Module {
|
||||
let runtime_type: u32 = LittleEndian::read_u32(&runtime_type);
|
||||
let globals_count: u32 = match module.global_section() {
|
||||
Some(ref section) => section.entries().len() as u32,
|
||||
None => 0
|
||||
};
|
||||
let imported_globals_count: u32 = match module.import_section() {
|
||||
Some(ref section) => section.entries().iter().filter(|e| match *e.external() {
|
||||
External::Global(ref _a) => true,
|
||||
_ => false
|
||||
}).count() as u32,
|
||||
None => 0
|
||||
};
|
||||
let total_globals_count: u32 = globals_count + imported_globals_count;
|
||||
|
||||
builder::from_module(module)
|
||||
.with_global(GlobalEntry::new(GlobalType::new(ValueType::I32, false), InitExpr::new(vec![Instruction::I32Const(runtime_type as i32), Instruction::End])))
|
||||
.with_export(ExportEntry::new("RUNTIME_TYPE".into(), Internal::Global(total_globals_count)))
|
||||
.with_global(GlobalEntry::new(GlobalType::new(ValueType::I32, false), InitExpr::new(vec![Instruction::I32Const(runtime_version as i32), Instruction::End])))
|
||||
.with_export(ExportEntry::new("RUNTIME_VERSION".into(), Internal::Global(total_globals_count + 1)))
|
||||
.build()
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use super::*;
|
||||
#[test]
|
||||
fn it_injects() {
|
||||
let mut module = builder::module()
|
||||
.with_global(GlobalEntry::new(GlobalType::new(ValueType::I32, false), InitExpr::new(vec![Instruction::I32Const(42 as i32)])))
|
||||
.build();
|
||||
let mut runtime_type: [u8; 4] = Default::default();
|
||||
runtime_type.copy_from_slice(b"emcc");
|
||||
module = inject_runtime_type(module, runtime_type, 1);
|
||||
let global_section = module.global_section().expect("Global section expected");
|
||||
assert_eq!(3, global_section.entries().len());
|
||||
let export_section = module.export_section().expect("Export section expected");
|
||||
assert!(export_section.entries().iter().find(|e| e.field() == "RUNTIME_TYPE" ).is_some());
|
||||
assert!(export_section.entries().iter().find(|e| e.field() == "RUNTIME_VERSION" ).is_some());
|
||||
}
|
||||
}
|
||||
@@ -1,526 +0,0 @@
|
||||
use std::vec::Vec;
|
||||
|
||||
use parity_wasm::elements::{self, BlockType, Type};
|
||||
use super::{resolve_func_type, Error};
|
||||
|
||||
/// Control stack frame.
|
||||
#[derive(Debug)]
|
||||
struct Frame {
|
||||
/// Stack becomes polymorphic only after an instruction that
|
||||
/// never passes control further was executed.
|
||||
is_polymorphic: bool,
|
||||
|
||||
/// Count of values which will be pushed after the exit
|
||||
/// from the current block.
|
||||
end_arity: u32,
|
||||
|
||||
/// Count of values which should be poped upon a branch to
|
||||
/// this frame.
|
||||
///
|
||||
/// This might be diffirent from `end_arity` since branch
|
||||
/// to the loop header can't take any values.
|
||||
branch_arity: u32,
|
||||
|
||||
/// Stack height before entering in the block.
|
||||
start_height: u32,
|
||||
}
|
||||
|
||||
/// This is a compound stack that abstracts tracking height of the value stack
|
||||
/// and manipulation of the control stack.
|
||||
struct Stack {
|
||||
height: u32,
|
||||
control_stack: Vec<Frame>,
|
||||
}
|
||||
|
||||
impl Stack {
|
||||
fn new() -> Stack {
|
||||
Stack {
|
||||
height: 0,
|
||||
control_stack: Vec::new(),
|
||||
}
|
||||
}
|
||||
|
||||
/// Returns current height of the value stack.
|
||||
fn height(&self) -> u32 {
|
||||
self.height
|
||||
}
|
||||
|
||||
/// Returns a reference to a frame by specified depth relative to the top of
|
||||
/// control stack.
|
||||
fn frame(&self, rel_depth: u32) -> Result<&Frame, Error> {
|
||||
let control_stack_height: usize = self.control_stack.len();
|
||||
let last_idx = control_stack_height
|
||||
.checked_sub(1)
|
||||
.ok_or_else(|| Error("control stack is empty".into()))?;
|
||||
let idx = last_idx
|
||||
.checked_sub(rel_depth as usize)
|
||||
.ok_or_else(|| Error("control stack out-of-bounds".into()))?;
|
||||
Ok(&self.control_stack[idx])
|
||||
}
|
||||
|
||||
/// Mark successive instructions as unreachable.
|
||||
///
|
||||
/// This effectively makes stack polymorphic.
|
||||
fn mark_unreachable(&mut self) -> Result<(), Error> {
|
||||
trace!(target: "max_height", "unreachable");
|
||||
let top_frame = self.control_stack
|
||||
.last_mut()
|
||||
.ok_or_else(|| Error("stack must be non-empty".into()))?;
|
||||
top_frame.is_polymorphic = true;
|
||||
Ok(())
|
||||
}
|
||||
|
||||
/// Push control frame into the control stack.
|
||||
fn push_frame(&mut self, frame: Frame) {
|
||||
trace!(target: "max_height", "push_frame: {:?}", frame);
|
||||
self.control_stack.push(frame);
|
||||
}
|
||||
|
||||
/// Pop control frame from the control stack.
|
||||
///
|
||||
/// Returns `Err` if the control stack is empty.
|
||||
fn pop_frame(&mut self) -> Result<Frame, Error> {
|
||||
trace!(target: "max_height", "pop_frame: {:?}", self.control_stack.last());
|
||||
Ok(self.control_stack
|
||||
.pop()
|
||||
.ok_or_else(|| Error("stack must be non-empty".into()))?)
|
||||
}
|
||||
|
||||
/// Truncate the height of value stack to the specified height.
|
||||
fn trunc(&mut self, new_height: u32) {
|
||||
trace!(target: "max_height", "trunc: {}", new_height);
|
||||
self.height = new_height;
|
||||
}
|
||||
|
||||
/// Push specified number of values into the value stack.
|
||||
///
|
||||
/// Returns `Err` if the height overflow usize value.
|
||||
fn push_values(&mut self, value_count: u32) -> Result<(), Error> {
|
||||
trace!(target: "max_height", "push: {}", value_count);
|
||||
self.height = self.height
|
||||
.checked_add(value_count)
|
||||
.ok_or_else(|| Error("stack overflow".into()))?;
|
||||
Ok(())
|
||||
}
|
||||
|
||||
/// Pop specified number of values from the value stack.
|
||||
///
|
||||
/// Returns `Err` if the stack happen to be negative value after
|
||||
/// values popped.
|
||||
fn pop_values(&mut self, value_count: u32) -> Result<(), Error> {
|
||||
trace!(target: "max_height", "pop: {}", value_count);
|
||||
if value_count == 0 {
|
||||
return Ok(());
|
||||
}
|
||||
{
|
||||
let top_frame = self.frame(0)?;
|
||||
if self.height == top_frame.start_height {
|
||||
// It is an error to pop more values than was pushed in the current frame
|
||||
// (ie pop values pushed in the parent frame), unless the frame became
|
||||
// polymorphic.
|
||||
return if top_frame.is_polymorphic {
|
||||
Ok(())
|
||||
} else {
|
||||
Err(Error("trying to pop more values than pushed".into()))
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
self.height = self.height
|
||||
.checked_sub(value_count)
|
||||
.ok_or_else(|| Error("stack underflow".into()))?;
|
||||
|
||||
Ok(())
|
||||
}
|
||||
}
|
||||
|
||||
/// This function expects the function to be validated.
|
||||
pub(crate) fn compute(func_idx: u32, module: &elements::Module) -> Result<u32, Error> {
|
||||
use parity_wasm::elements::Instruction::*;
|
||||
|
||||
let func_section = module
|
||||
.function_section()
|
||||
.ok_or_else(|| Error("No function section".into()))?;
|
||||
let code_section = module
|
||||
.code_section()
|
||||
.ok_or_else(|| Error("No code section".into()))?;
|
||||
let type_section = module
|
||||
.type_section()
|
||||
.ok_or_else(|| Error("No type section".into()))?;
|
||||
|
||||
|
||||
trace!(target: "max_height", "func_idx: {}", func_idx);
|
||||
|
||||
// Get a signature and a body of the specified function.
|
||||
let func_sig_idx = func_section
|
||||
.entries()
|
||||
.get(func_idx as usize)
|
||||
.ok_or_else(|| Error("Function is not found in func section".into()))?
|
||||
.type_ref();
|
||||
let Type::Function(ref func_signature) = *type_section
|
||||
.types()
|
||||
.get(func_sig_idx as usize)
|
||||
.ok_or_else(|| Error("Function is not found in func section".into()))?;
|
||||
let body = code_section
|
||||
.bodies()
|
||||
.get(func_idx as usize)
|
||||
.ok_or_else(|| Error("Function body for the index isn't found".into()))?;
|
||||
let instructions = body.code();
|
||||
|
||||
let mut stack = Stack::new();
|
||||
let mut max_height: u32 = 0;
|
||||
let mut pc = 0;
|
||||
|
||||
// Add implicit frame for the function. Breaks to this frame and execution of
|
||||
// the last end should deal with this frame.
|
||||
let func_arity: u32 = if func_signature.return_type().is_some() {
|
||||
1
|
||||
} else {
|
||||
0
|
||||
};
|
||||
stack.push_frame(Frame {
|
||||
is_polymorphic: false,
|
||||
end_arity: func_arity,
|
||||
branch_arity: func_arity,
|
||||
start_height: 0,
|
||||
});
|
||||
|
||||
loop {
|
||||
if pc >= instructions.elements().len() {
|
||||
break;
|
||||
}
|
||||
|
||||
// If current value stack is higher than maximal height observed so far,
|
||||
// save the new height.
|
||||
// However, we don't increase maximal value in unreachable code.
|
||||
if stack.height() > max_height && !stack.frame(0)?.is_polymorphic {
|
||||
max_height = stack.height();
|
||||
}
|
||||
|
||||
let opcode = &instructions.elements()[pc];
|
||||
trace!(target: "max_height", "{:?}", opcode);
|
||||
|
||||
match *opcode {
|
||||
Nop => {}
|
||||
Block(ty) | Loop(ty) | If(ty) => {
|
||||
let end_arity = if ty == BlockType::NoResult { 0 } else { 1 };
|
||||
let branch_arity = if let Loop(_) = *opcode { 0 } else { end_arity };
|
||||
let height = stack.height();
|
||||
stack.push_frame(Frame {
|
||||
is_polymorphic: false,
|
||||
end_arity,
|
||||
branch_arity,
|
||||
start_height: height,
|
||||
});
|
||||
}
|
||||
Else => {
|
||||
// The frame at the top should be pushed by `If`. So we leave
|
||||
// it as is.
|
||||
}
|
||||
End => {
|
||||
let frame = stack.pop_frame()?;
|
||||
stack.trunc(frame.start_height);
|
||||
stack.push_values(frame.end_arity)?;
|
||||
}
|
||||
Unreachable => {
|
||||
stack.mark_unreachable()?;
|
||||
}
|
||||
Br(target) => {
|
||||
// Pop values for the destination block result.
|
||||
let target_arity = stack.frame(target)?.branch_arity;
|
||||
stack.pop_values(target_arity)?;
|
||||
|
||||
// This instruction unconditionally transfers control to the specified block,
|
||||
// thus all instruction until the end of the current block is deemed unreachable
|
||||
stack.mark_unreachable()?;
|
||||
}
|
||||
BrIf(target) => {
|
||||
// Pop values for the destination block result.
|
||||
let target_arity = stack.frame(target)?.branch_arity;
|
||||
stack.pop_values(target_arity)?;
|
||||
|
||||
// Pop condition value.
|
||||
stack.pop_values(1)?;
|
||||
|
||||
// Push values back.
|
||||
stack.push_values(target_arity)?;
|
||||
}
|
||||
BrTable(ref targets, default_target) => {
|
||||
let arity_of_default = stack.frame(default_target)?.branch_arity;
|
||||
|
||||
// Check that all jump targets have an equal arities.
|
||||
for target in targets.iter() {
|
||||
let arity = stack.frame(*target)?.branch_arity;
|
||||
if arity != arity_of_default {
|
||||
return Err(Error(
|
||||
"Arity of all jump-targets must be equal".into()
|
||||
))
|
||||
}
|
||||
}
|
||||
|
||||
// Because all jump targets have an equal arities, we can just take arity of
|
||||
// the default branch.
|
||||
stack.pop_values(arity_of_default)?;
|
||||
|
||||
// This instruction doesn't let control flow to go further, since the control flow
|
||||
// should take either one of branches depending on the value or the default branch.
|
||||
stack.mark_unreachable()?;
|
||||
}
|
||||
Return => {
|
||||
// Pop return values of the function. Mark successive instructions as unreachable
|
||||
// since this instruction doesn't let control flow to go further.
|
||||
stack.pop_values(func_arity)?;
|
||||
stack.mark_unreachable()?;
|
||||
}
|
||||
Call(idx) => {
|
||||
let ty = resolve_func_type(idx, module)?;
|
||||
|
||||
// Pop values for arguments of the function.
|
||||
stack.pop_values(ty.params().len() as u32)?;
|
||||
|
||||
// Push result of the function execution to the stack.
|
||||
let callee_arity = if ty.return_type().is_some() { 1 } else { 0 };
|
||||
stack.push_values(callee_arity)?;
|
||||
}
|
||||
CallIndirect(x, _) => {
|
||||
let Type::Function(ref ty) = *type_section
|
||||
.types()
|
||||
.get(x as usize)
|
||||
.ok_or_else(|| Error("Type not found".into()))?;
|
||||
|
||||
// Pop values for arguments of the function.
|
||||
stack.pop_values(ty.params().len() as u32)?;
|
||||
|
||||
// Push result of the function execution to the stack.
|
||||
let callee_arity = if ty.return_type().is_some() { 1 } else { 0 };
|
||||
stack.push_values(callee_arity)?;
|
||||
}
|
||||
Drop => {
|
||||
stack.pop_values(1)?;
|
||||
}
|
||||
Select => {
|
||||
// Pop two values and one condition.
|
||||
stack.pop_values(2)?;
|
||||
stack.pop_values(1)?;
|
||||
|
||||
// Push the selected value.
|
||||
stack.push_values(1)?;
|
||||
}
|
||||
GetLocal(_) => {
|
||||
stack.push_values(1)?;
|
||||
}
|
||||
SetLocal(_) => {
|
||||
stack.pop_values(1)?;
|
||||
}
|
||||
TeeLocal(_) => {
|
||||
// This instruction pops and pushes the value, so
|
||||
// effectively it doesn't modify the stack height.
|
||||
stack.pop_values(1)?;
|
||||
stack.push_values(1)?;
|
||||
}
|
||||
GetGlobal(_) => {
|
||||
stack.push_values(1)?;
|
||||
}
|
||||
SetGlobal(_) => {
|
||||
stack.pop_values(1)?;
|
||||
}
|
||||
I32Load(_, _)
|
||||
| I64Load(_, _)
|
||||
| F32Load(_, _)
|
||||
| F64Load(_, _)
|
||||
| I32Load8S(_, _)
|
||||
| I32Load8U(_, _)
|
||||
| I32Load16S(_, _)
|
||||
| I32Load16U(_, _)
|
||||
| I64Load8S(_, _)
|
||||
| I64Load8U(_, _)
|
||||
| I64Load16S(_, _)
|
||||
| I64Load16U(_, _)
|
||||
| I64Load32S(_, _)
|
||||
| I64Load32U(_, _) => {
|
||||
// These instructions pop the address and pushes the result,
|
||||
// which effictively don't modify the stack height.
|
||||
stack.pop_values(1)?;
|
||||
stack.push_values(1)?;
|
||||
}
|
||||
|
||||
I32Store(_, _)
|
||||
| I64Store(_, _)
|
||||
| F32Store(_, _)
|
||||
| F64Store(_, _)
|
||||
| I32Store8(_, _)
|
||||
| I32Store16(_, _)
|
||||
| I64Store8(_, _)
|
||||
| I64Store16(_, _)
|
||||
| I64Store32(_, _) => {
|
||||
// These instructions pop the address and the value.
|
||||
stack.pop_values(2)?;
|
||||
}
|
||||
|
||||
CurrentMemory(_) => {
|
||||
// Pushes current memory size
|
||||
stack.push_values(1)?;
|
||||
}
|
||||
GrowMemory(_) => {
|
||||
// Grow memory takes the value of pages to grow and pushes
|
||||
stack.pop_values(1)?;
|
||||
stack.push_values(1)?;
|
||||
}
|
||||
|
||||
I32Const(_) | I64Const(_) | F32Const(_) | F64Const(_) => {
|
||||
// These instructions just push the single literal value onto the stack.
|
||||
stack.push_values(1)?;
|
||||
}
|
||||
|
||||
I32Eqz | I64Eqz => {
|
||||
// These instructions pop the value and compare it against zero, and pushes
|
||||
// the result of the comparison.
|
||||
stack.pop_values(1)?;
|
||||
stack.push_values(1)?;
|
||||
}
|
||||
|
||||
I32Eq | I32Ne | I32LtS | I32LtU | I32GtS | I32GtU | I32LeS | I32LeU | I32GeS
|
||||
| I32GeU | I64Eq | I64Ne | I64LtS | I64LtU | I64GtS | I64GtU | I64LeS | I64LeU
|
||||
| I64GeS | I64GeU | F32Eq | F32Ne | F32Lt | F32Gt | F32Le | F32Ge | F64Eq | F64Ne
|
||||
| F64Lt | F64Gt | F64Le | F64Ge => {
|
||||
// Comparison operations take two operands and produce one result.
|
||||
stack.pop_values(2)?;
|
||||
stack.push_values(1)?;
|
||||
}
|
||||
|
||||
I32Clz | I32Ctz | I32Popcnt | I64Clz | I64Ctz | I64Popcnt | F32Abs | F32Neg
|
||||
| F32Ceil | F32Floor | F32Trunc | F32Nearest | F32Sqrt | F64Abs | F64Neg | F64Ceil
|
||||
| F64Floor | F64Trunc | F64Nearest | F64Sqrt => {
|
||||
// Unary operators take one operand and produce one result.
|
||||
stack.pop_values(1)?;
|
||||
stack.push_values(1)?;
|
||||
}
|
||||
|
||||
I32Add | I32Sub | I32Mul | I32DivS | I32DivU | I32RemS | I32RemU | I32And | I32Or
|
||||
| I32Xor | I32Shl | I32ShrS | I32ShrU | I32Rotl | I32Rotr | I64Add | I64Sub
|
||||
| I64Mul | I64DivS | I64DivU | I64RemS | I64RemU | I64And | I64Or | I64Xor | I64Shl
|
||||
| I64ShrS | I64ShrU | I64Rotl | I64Rotr | F32Add | F32Sub | F32Mul | F32Div
|
||||
| F32Min | F32Max | F32Copysign | F64Add | F64Sub | F64Mul | F64Div | F64Min
|
||||
| F64Max | F64Copysign => {
|
||||
// Binary operators take two operands and produce one result.
|
||||
stack.pop_values(2)?;
|
||||
stack.push_values(1)?;
|
||||
}
|
||||
|
||||
I32WrapI64 | I32TruncSF32 | I32TruncUF32 | I32TruncSF64 | I32TruncUF64
|
||||
| I64ExtendSI32 | I64ExtendUI32 | I64TruncSF32 | I64TruncUF32 | I64TruncSF64
|
||||
| I64TruncUF64 | F32ConvertSI32 | F32ConvertUI32 | F32ConvertSI64 | F32ConvertUI64
|
||||
| F32DemoteF64 | F64ConvertSI32 | F64ConvertUI32 | F64ConvertSI64 | F64ConvertUI64
|
||||
| F64PromoteF32 | I32ReinterpretF32 | I64ReinterpretF64 | F32ReinterpretI32
|
||||
| F64ReinterpretI64 => {
|
||||
// Conversion operators take one value and produce one result.
|
||||
stack.pop_values(1)?;
|
||||
stack.push_values(1)?;
|
||||
}
|
||||
}
|
||||
pc += 1;
|
||||
}
|
||||
|
||||
Ok(max_height)
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
extern crate wabt;
|
||||
use parity_wasm::elements;
|
||||
use super::*;
|
||||
|
||||
fn parse_wat(source: &str) -> elements::Module {
|
||||
elements::deserialize_buffer(&wabt::wat2wasm(source).expect("Failed to wat2wasm"))
|
||||
.expect("Failed to deserialize the module")
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn simple_test() {
|
||||
let module = parse_wat(
|
||||
r#"
|
||||
(module
|
||||
(func
|
||||
i32.const 1
|
||||
i32.const 2
|
||||
i32.const 3
|
||||
drop
|
||||
drop
|
||||
drop
|
||||
)
|
||||
)
|
||||
"#,
|
||||
);
|
||||
|
||||
let height = compute(0, &module).unwrap();
|
||||
assert_eq!(height, 3);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn implicit_and_explicit_return() {
|
||||
let module = parse_wat(
|
||||
r#"
|
||||
(module
|
||||
(func (result i32)
|
||||
i32.const 0
|
||||
return
|
||||
)
|
||||
)
|
||||
"#,
|
||||
);
|
||||
|
||||
let height = compute(0, &module).unwrap();
|
||||
assert_eq!(height, 1);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn dont_count_in_unreachable() {
|
||||
let module = parse_wat(
|
||||
r#"
|
||||
(module
|
||||
(memory 0)
|
||||
(func (result i32)
|
||||
unreachable
|
||||
grow_memory
|
||||
)
|
||||
)
|
||||
"#,
|
||||
);
|
||||
|
||||
let height = compute(0, &module).unwrap();
|
||||
assert_eq!(height, 0);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn yet_another_test() {
|
||||
const SOURCE: &'static str = r#"
|
||||
(module
|
||||
(memory 0)
|
||||
(func
|
||||
;; Push two values and then pop them.
|
||||
;; This will make max depth to be equal to 2.
|
||||
i32.const 0
|
||||
i32.const 1
|
||||
drop
|
||||
drop
|
||||
|
||||
;; Code after `unreachable` shouldn't have an effect
|
||||
;; on the max depth.
|
||||
unreachable
|
||||
i32.const 0
|
||||
i32.const 1
|
||||
i32.const 2
|
||||
)
|
||||
)
|
||||
"#;
|
||||
let module = elements::deserialize_buffer(&wabt::Wat2Wasm::new()
|
||||
.validate(false)
|
||||
.convert(SOURCE)
|
||||
.expect("Failed to wat2wasm")
|
||||
.as_ref())
|
||||
.expect("Failed to deserialize the module");
|
||||
|
||||
let height = compute(0, &module).unwrap();
|
||||
assert_eq!(height, 2);
|
||||
}
|
||||
}
|
||||
@@ -1,431 +0,0 @@
|
||||
//! The pass that tries to make stack overflows deterministic, by introducing
|
||||
//! an upper bound of the stack size.
|
||||
//!
|
||||
//! This pass introduces a global mutable variable to track stack height,
|
||||
//! and instruments all calls with preamble and postamble.
|
||||
//!
|
||||
//! Stack height is increased prior the call. Otherwise, the check would
|
||||
//! be made after the stack frame is allocated.
|
||||
//!
|
||||
//! The preamble is inserted before the call. It increments
|
||||
//! the global stack height variable with statically determined "stack cost"
|
||||
//! of the callee. If after the increment the stack height exceeds
|
||||
//! the limit (specified by the `rules`) then execution traps.
|
||||
//! Otherwise, the call is executed.
|
||||
//!
|
||||
//! The postamble is inserted after the call. The purpose of the postamble is to decrease
|
||||
//! the stack height by the "stack cost" of the callee function.
|
||||
//!
|
||||
//! Note, that we can't instrument all possible ways to return from the function. The simplest
|
||||
//! example would be a trap issued by the host function.
|
||||
//! That means stack height global won't be equal to zero upon the next execution after such trap.
|
||||
//!
|
||||
//! # Thunks
|
||||
//!
|
||||
//! Because stack height is increased prior the call few problems arises:
|
||||
//!
|
||||
//! - Stack height isn't increased upon an entry to the first function, i.e. exported function.
|
||||
//! - Start function is executed externally (similar to exported functions).
|
||||
//! - It is statically unknown what function will be invoked in an indirect call.
|
||||
//!
|
||||
//! The solution for this problems is to generate a intermediate functions, called 'thunks', which
|
||||
//! will increase before and decrease the stack height after the call to original function, and
|
||||
//! then make exported function and table entries, start section to point to a corresponding thunks.
|
||||
//!
|
||||
//! # Stack cost
|
||||
//!
|
||||
//! Stack cost of the function is calculated as a sum of it's locals
|
||||
//! and the maximal height of the value stack.
|
||||
//!
|
||||
//! All values are treated equally, as they have the same size.
|
||||
//!
|
||||
//! The rationale for this it makes it possible to use this very naive wasm executor, that is:
|
||||
//!
|
||||
//! - values are implemented by a union, so each value takes a size equal to
|
||||
//! the size of the largest possible value type this union can hold. (In MVP it is 8 bytes)
|
||||
//! - each value from the value stack is placed on the native stack.
|
||||
//! - each local variable and function argument is placed on the native stack.
|
||||
//! - arguments pushed by the caller are copied into callee stack rather than shared
|
||||
//! between the frames.
|
||||
//! - upon entry into the function entire stack frame is allocated.
|
||||
|
||||
use std::string::String;
|
||||
use std::vec::Vec;
|
||||
|
||||
use parity_wasm::elements::{self, Type};
|
||||
use parity_wasm::builder;
|
||||
|
||||
/// Macro to generate preamble and postamble.
|
||||
macro_rules! instrument_call {
|
||||
($callee_idx: expr, $callee_stack_cost: expr, $stack_height_global_idx: expr, $stack_limit: expr) => {{
|
||||
use $crate::parity_wasm::elements::Instruction::*;
|
||||
[
|
||||
// stack_height += stack_cost(F)
|
||||
GetGlobal($stack_height_global_idx),
|
||||
I32Const($callee_stack_cost),
|
||||
I32Add,
|
||||
SetGlobal($stack_height_global_idx),
|
||||
// if stack_counter > LIMIT: unreachable
|
||||
GetGlobal($stack_height_global_idx),
|
||||
I32Const($stack_limit as i32),
|
||||
I32GtU,
|
||||
If(elements::BlockType::NoResult),
|
||||
Unreachable,
|
||||
End,
|
||||
// Original call
|
||||
Call($callee_idx),
|
||||
// stack_height -= stack_cost(F)
|
||||
GetGlobal($stack_height_global_idx),
|
||||
I32Const($callee_stack_cost),
|
||||
I32Sub,
|
||||
SetGlobal($stack_height_global_idx),
|
||||
]
|
||||
}};
|
||||
}
|
||||
|
||||
mod max_height;
|
||||
mod thunk;
|
||||
|
||||
/// Error that occured during processing the module.
|
||||
///
|
||||
/// This means that the module is invalid.
|
||||
#[derive(Debug)]
|
||||
pub struct Error(String);
|
||||
|
||||
pub(crate) struct Context {
|
||||
stack_height_global_idx: Option<u32>,
|
||||
func_stack_costs: Option<Vec<u32>>,
|
||||
stack_limit: u32,
|
||||
}
|
||||
|
||||
impl Context {
|
||||
/// Returns index in a global index space of a stack_height global variable.
|
||||
///
|
||||
/// Panics if it haven't generated yet.
|
||||
fn stack_height_global_idx(&self) -> u32 {
|
||||
self.stack_height_global_idx.expect(
|
||||
"stack_height_global_idx isn't yet generated;
|
||||
Did you call `inject_stack_counter_global`",
|
||||
)
|
||||
}
|
||||
|
||||
/// Returns `stack_cost` for `func_idx`.
|
||||
///
|
||||
/// Panics if stack costs haven't computed yet or `func_idx` is greater
|
||||
/// than the last function index.
|
||||
fn stack_cost(&self, func_idx: u32) -> Option<u32> {
|
||||
self.func_stack_costs
|
||||
.as_ref()
|
||||
.expect(
|
||||
"func_stack_costs isn't yet computed;
|
||||
Did you call `compute_stack_costs`?",
|
||||
)
|
||||
.get(func_idx as usize)
|
||||
.cloned()
|
||||
}
|
||||
|
||||
/// Returns stack limit specified by the rules.
|
||||
fn stack_limit(&self) -> u32 {
|
||||
self.stack_limit
|
||||
}
|
||||
}
|
||||
|
||||
/// Instrument a module with stack height limiter.
|
||||
///
|
||||
/// See module-level documentation for more details.
|
||||
///
|
||||
/// # Errors
|
||||
///
|
||||
/// Returns `Err` if module is invalid and can't be
|
||||
pub fn inject_limiter(
|
||||
mut module: elements::Module,
|
||||
stack_limit: u32,
|
||||
) -> Result<elements::Module, Error> {
|
||||
let mut ctx = Context {
|
||||
stack_height_global_idx: None,
|
||||
func_stack_costs: None,
|
||||
stack_limit,
|
||||
};
|
||||
|
||||
generate_stack_height_global(&mut ctx, &mut module);
|
||||
compute_stack_costs(&mut ctx, &module)?;
|
||||
instrument_functions(&mut ctx, &mut module)?;
|
||||
let module = thunk::generate_thunks(&mut ctx, module)?;
|
||||
|
||||
Ok(module)
|
||||
}
|
||||
|
||||
/// Generate a new global that will be used for tracking current stack height.
|
||||
fn generate_stack_height_global(ctx: &mut Context, module: &mut elements::Module) {
|
||||
let global_entry = builder::global()
|
||||
.value_type()
|
||||
.i32()
|
||||
.mutable()
|
||||
.init_expr(elements::Instruction::I32Const(0))
|
||||
.build();
|
||||
|
||||
// Try to find an existing global section.
|
||||
for section in module.sections_mut() {
|
||||
if let elements::Section::Global(ref mut gs) = *section {
|
||||
gs.entries_mut().push(global_entry);
|
||||
|
||||
let stack_height_global_idx = (gs.entries().len() as u32) - 1;
|
||||
ctx.stack_height_global_idx = Some(stack_height_global_idx);
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
||||
// Existing section not found, create one!
|
||||
module.sections_mut().push(elements::Section::Global(
|
||||
elements::GlobalSection::with_entries(vec![global_entry]),
|
||||
));
|
||||
ctx.stack_height_global_idx = Some(0);
|
||||
}
|
||||
|
||||
/// Calculate stack costs for all functions.
|
||||
///
|
||||
/// Returns a vector with a stack cost for each function, including imports.
|
||||
fn compute_stack_costs(ctx: &mut Context, module: &elements::Module) -> Result<(), Error> {
|
||||
let func_imports = module.import_count(elements::ImportCountType::Function);
|
||||
let mut func_stack_costs = vec![0; module.functions_space()];
|
||||
// TODO: optimize!
|
||||
for (func_idx, func_stack_cost) in func_stack_costs.iter_mut().enumerate() {
|
||||
// We can't calculate stack_cost of the import functions.
|
||||
if func_idx >= func_imports {
|
||||
*func_stack_cost = compute_stack_cost(func_idx as u32, &module)?;
|
||||
}
|
||||
}
|
||||
|
||||
ctx.func_stack_costs = Some(func_stack_costs);
|
||||
Ok(())
|
||||
}
|
||||
|
||||
/// Stack cost of the given *defined* function is the sum of it's locals count (that is,
|
||||
/// number of arguments plus number of local variables) and the maximal stack
|
||||
/// height.
|
||||
fn compute_stack_cost(func_idx: u32, module: &elements::Module) -> Result<u32, Error> {
|
||||
// To calculate the cost of a function we need to convert index from
|
||||
// function index space to defined function spaces.
|
||||
let func_imports = module.import_count(elements::ImportCountType::Function) as u32;
|
||||
let defined_func_idx = func_idx.checked_sub(func_imports).ok_or_else(|| {
|
||||
Error("This should be a index of a defined function".into())
|
||||
})?;
|
||||
|
||||
let code_section = module.code_section().ok_or_else(|| {
|
||||
Error("Due to validation code section should exists".into())
|
||||
})?;
|
||||
let body = &code_section
|
||||
.bodies()
|
||||
.get(defined_func_idx as usize)
|
||||
.ok_or_else(|| Error("Function body is out of bounds".into()))?;
|
||||
let locals_count = body.locals().len() as u32;
|
||||
|
||||
let max_stack_height =
|
||||
max_height::compute(
|
||||
defined_func_idx,
|
||||
module
|
||||
)?;
|
||||
|
||||
Ok(locals_count + max_stack_height)
|
||||
}
|
||||
|
||||
fn instrument_functions(ctx: &mut Context, module: &mut elements::Module) -> Result<(), Error> {
|
||||
for section in module.sections_mut() {
|
||||
if let elements::Section::Code(ref mut code_section) = *section {
|
||||
for func_body in code_section.bodies_mut() {
|
||||
let mut opcodes = func_body.code_mut();
|
||||
instrument_function(ctx, opcodes)?;
|
||||
}
|
||||
}
|
||||
}
|
||||
Ok(())
|
||||
}
|
||||
|
||||
/// This function searches `call` instructions and wrap each call
|
||||
/// with preamble and postamble.
|
||||
///
|
||||
/// Before:
|
||||
///
|
||||
/// ```text
|
||||
/// get_local 0
|
||||
/// get_local 1
|
||||
/// call 228
|
||||
/// drop
|
||||
/// ```
|
||||
///
|
||||
/// After:
|
||||
///
|
||||
/// ```text
|
||||
/// get_local 0
|
||||
/// get_local 1
|
||||
///
|
||||
/// < ... preamble ... >
|
||||
///
|
||||
/// call 228
|
||||
///
|
||||
/// < .. postamble ... >
|
||||
///
|
||||
/// drop
|
||||
/// ```
|
||||
fn instrument_function(
|
||||
ctx: &mut Context,
|
||||
instructions: &mut elements::Instructions,
|
||||
) -> Result<(), Error> {
|
||||
use parity_wasm::elements::Instruction::*;
|
||||
|
||||
let mut cursor = 0;
|
||||
loop {
|
||||
if cursor >= instructions.elements().len() {
|
||||
break;
|
||||
}
|
||||
|
||||
enum Action {
|
||||
InstrumentCall {
|
||||
callee_idx: u32,
|
||||
callee_stack_cost: u32,
|
||||
},
|
||||
Nop,
|
||||
}
|
||||
|
||||
let action: Action = {
|
||||
let instruction = &instructions.elements()[cursor];
|
||||
match *instruction {
|
||||
Call(ref callee_idx) => {
|
||||
let callee_stack_cost = ctx
|
||||
.stack_cost(*callee_idx)
|
||||
.ok_or_else(||
|
||||
Error(
|
||||
format!("Call to function that out-of-bounds: {}", callee_idx)
|
||||
)
|
||||
)?;
|
||||
|
||||
// Instrument only calls to a functions which stack_cost is
|
||||
// non-zero.
|
||||
if callee_stack_cost > 0 {
|
||||
Action::InstrumentCall {
|
||||
callee_idx: *callee_idx,
|
||||
callee_stack_cost,
|
||||
}
|
||||
} else {
|
||||
Action::Nop
|
||||
}
|
||||
},
|
||||
_ => Action::Nop,
|
||||
}
|
||||
};
|
||||
|
||||
match action {
|
||||
// We need to wrap a `call idx` instruction
|
||||
// with a code that adjusts stack height counter
|
||||
// and then restores it.
|
||||
Action::InstrumentCall { callee_idx, callee_stack_cost } => {
|
||||
let new_seq = instrument_call!(
|
||||
callee_idx,
|
||||
callee_stack_cost as i32,
|
||||
ctx.stack_height_global_idx(),
|
||||
ctx.stack_limit()
|
||||
);
|
||||
|
||||
// Replace the original `call idx` instruction with
|
||||
// a wrapped call sequence.
|
||||
//
|
||||
// To splice actually take a place, we need to consume iterator
|
||||
// splice returns. So we just `count()` it.
|
||||
let _ = instructions
|
||||
.elements_mut()
|
||||
.splice(cursor..(cursor + 1), new_seq.iter().cloned())
|
||||
.count();
|
||||
|
||||
// Advance cursor to be after the inserted sequence.
|
||||
cursor += new_seq.len();
|
||||
}
|
||||
// Do nothing for other instructions.
|
||||
_ => {
|
||||
cursor += 1;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
fn resolve_func_type(
|
||||
func_idx: u32,
|
||||
module: &elements::Module,
|
||||
) -> Result<&elements::FunctionType, Error> {
|
||||
let types = module.type_section().map(|ts| ts.types()).unwrap_or(&[]);
|
||||
let functions = module
|
||||
.function_section()
|
||||
.map(|fs| fs.entries())
|
||||
.unwrap_or(&[]);
|
||||
|
||||
let func_imports = module.import_count(elements::ImportCountType::Function);
|
||||
let sig_idx = if func_idx < func_imports as u32 {
|
||||
module
|
||||
.import_section()
|
||||
.expect("function import count is not zero; import section must exists; qed")
|
||||
.entries()
|
||||
.iter()
|
||||
.filter_map(|entry| match *entry.external() {
|
||||
elements::External::Function(ref idx) => Some(*idx),
|
||||
_ => None,
|
||||
})
|
||||
.nth(func_idx as usize)
|
||||
.expect(
|
||||
"func_idx is less than function imports count;
|
||||
nth function import must be `Some`;
|
||||
qed",
|
||||
)
|
||||
} else {
|
||||
functions
|
||||
.get(func_idx as usize - func_imports)
|
||||
.ok_or_else(|| Error(format!("Function at index {} is not defined", func_idx)))?
|
||||
.type_ref()
|
||||
};
|
||||
let Type::Function(ref ty) = *types.get(sig_idx as usize).ok_or_else(|| {
|
||||
Error(format!(
|
||||
"Signature {} (specified by func {}) isn't defined",
|
||||
sig_idx, func_idx
|
||||
))
|
||||
})?;
|
||||
Ok(ty)
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
extern crate wabt;
|
||||
use parity_wasm::elements;
|
||||
use super::*;
|
||||
|
||||
fn parse_wat(source: &str) -> elements::Module {
|
||||
elements::deserialize_buffer(&wabt::wat2wasm(source).expect("Failed to wat2wasm"))
|
||||
.expect("Failed to deserialize the module")
|
||||
}
|
||||
|
||||
fn validate_module(module: elements::Module) {
|
||||
let binary = elements::serialize(module).expect("Failed to serialize");
|
||||
wabt::Module::read_binary(&binary, &Default::default())
|
||||
.expect("Wabt failed to read final binary")
|
||||
.validate()
|
||||
.expect("Invalid module");
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_with_params_and_result() {
|
||||
let module = parse_wat(
|
||||
r#"
|
||||
(module
|
||||
(func (export "i32.add") (param i32 i32) (result i32)
|
||||
get_local 0
|
||||
get_local 1
|
||||
i32.add
|
||||
)
|
||||
)
|
||||
"#,
|
||||
);
|
||||
|
||||
let module = inject_limiter(module, 1024)
|
||||
.expect("Failed to inject stack counter");
|
||||
validate_module(module);
|
||||
}
|
||||
}
|
||||
@@ -1,167 +0,0 @@
|
||||
#[cfg(features = "std")]
|
||||
use std::collections::{HashMap as Map};
|
||||
#[cfg(not(features = "std"))]
|
||||
use std::collections::{BTreeMap as Map};
|
||||
use std::vec::Vec;
|
||||
|
||||
use parity_wasm::elements::{self, FunctionType, Internal};
|
||||
use parity_wasm::builder;
|
||||
|
||||
use super::{resolve_func_type, Context, Error};
|
||||
|
||||
struct Thunk {
|
||||
signature: FunctionType,
|
||||
// Index in function space of this thunk.
|
||||
idx: Option<u32>,
|
||||
original_func_idx: u32,
|
||||
callee_stack_cost: u32,
|
||||
}
|
||||
|
||||
pub(crate) fn generate_thunks(
|
||||
ctx: &mut Context,
|
||||
module: elements::Module,
|
||||
) -> Result<elements::Module, Error> {
|
||||
// First, we need to collect all function indicies that should be replaced by thunks
|
||||
|
||||
// Function indicies which needs to generate thunks.
|
||||
let mut need_thunks: Vec<u32> = Vec::new();
|
||||
|
||||
let mut replacement_map: Map<u32, Thunk> = {
|
||||
let exports = module
|
||||
.export_section()
|
||||
.map(|es| es.entries())
|
||||
.unwrap_or(&[]);
|
||||
let elem_segments = module
|
||||
.elements_section()
|
||||
.map(|es| es.entries())
|
||||
.unwrap_or(&[]);
|
||||
let start_func_idx = module
|
||||
.start_section();
|
||||
|
||||
let exported_func_indicies = exports.iter().filter_map(|entry| match *entry.internal() {
|
||||
Internal::Function(ref function_idx) => Some(*function_idx),
|
||||
_ => None,
|
||||
});
|
||||
let table_func_indicies = elem_segments
|
||||
.iter()
|
||||
.flat_map(|segment| segment.members())
|
||||
.cloned();
|
||||
|
||||
// Replacement map is at least export section size.
|
||||
let mut replacement_map: Map<u32, Thunk> = Map::new();
|
||||
|
||||
for func_idx in exported_func_indicies.chain(table_func_indicies).chain(start_func_idx.into_iter()) {
|
||||
let callee_stack_cost = ctx.stack_cost(func_idx).ok_or_else(|| {
|
||||
Error(format!("function with idx {} isn't found", func_idx))
|
||||
})?;
|
||||
|
||||
// Don't generate a thunk if stack_cost of a callee is zero.
|
||||
if callee_stack_cost != 0 {
|
||||
need_thunks.push(func_idx);
|
||||
replacement_map.insert(func_idx, Thunk {
|
||||
signature: resolve_func_type(func_idx, &module)?.clone(),
|
||||
idx: None,
|
||||
callee_stack_cost,
|
||||
original_func_idx: func_idx,
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
replacement_map
|
||||
};
|
||||
|
||||
// Then, we generate a thunk for each original function.
|
||||
|
||||
// Save current func_idx
|
||||
let mut next_func_idx = module.functions_space() as u32;
|
||||
|
||||
let mut mbuilder = builder::from_module(module);
|
||||
for func_idx in need_thunks {
|
||||
let mut thunk = replacement_map
|
||||
.get_mut(&func_idx)
|
||||
.expect(
|
||||
"`func_idx` should come from `need_thunks`;
|
||||
`need_thunks` is populated with the same items that in `replacement_map`;
|
||||
qed"
|
||||
);
|
||||
|
||||
let instrumented_call = instrument_call!(
|
||||
thunk.original_func_idx as u32,
|
||||
thunk.callee_stack_cost as i32,
|
||||
ctx.stack_height_global_idx(),
|
||||
ctx.stack_limit()
|
||||
);
|
||||
// Thunk body consist of:
|
||||
// - argument pushing
|
||||
// - instrumented call
|
||||
// - end
|
||||
let mut thunk_body: Vec<elements::Instruction> = Vec::with_capacity(
|
||||
thunk.signature.params().len() +
|
||||
instrumented_call.len() +
|
||||
1
|
||||
);
|
||||
|
||||
for (arg_idx, _) in thunk.signature.params().iter().enumerate() {
|
||||
thunk_body.push(elements::Instruction::GetLocal(arg_idx as u32));
|
||||
}
|
||||
thunk_body.extend(instrumented_call.iter().cloned());
|
||||
thunk_body.push(elements::Instruction::End);
|
||||
|
||||
// TODO: Don't generate a signature, but find an existing one.
|
||||
|
||||
mbuilder = mbuilder.function()
|
||||
// Signature of the thunk should match the original function signature.
|
||||
.signature()
|
||||
.with_params(thunk.signature.params().to_vec())
|
||||
.with_return_type(thunk.signature.return_type().clone())
|
||||
.build()
|
||||
.body()
|
||||
.with_instructions(elements::Instructions::new(
|
||||
thunk_body
|
||||
))
|
||||
.build()
|
||||
.build();
|
||||
|
||||
thunk.idx = Some(next_func_idx);
|
||||
next_func_idx += 1;
|
||||
}
|
||||
let mut module = mbuilder.build();
|
||||
|
||||
// And finally, fixup thunks in export and table sections.
|
||||
|
||||
// Fixup original function index to a index of a thunk generated earlier.
|
||||
let fixup = |function_idx: &mut u32| {
|
||||
// Check whether this function is in replacement_map, since
|
||||
// we can skip thunk generation (e.g. if stack_cost of function is 0).
|
||||
if let Some(ref thunk) = replacement_map.get(function_idx) {
|
||||
*function_idx = thunk
|
||||
.idx
|
||||
.expect("At this point an index must be assigned to each thunk");
|
||||
}
|
||||
};
|
||||
|
||||
for section in module.sections_mut() {
|
||||
match *section {
|
||||
elements::Section::Export(ref mut export_section) => {
|
||||
for entry in export_section.entries_mut() {
|
||||
if let Internal::Function(ref mut function_idx) = *entry.internal_mut() {
|
||||
fixup(function_idx)
|
||||
}
|
||||
}
|
||||
}
|
||||
elements::Section::Element(ref mut elem_section) => {
|
||||
for segment in elem_section.entries_mut() {
|
||||
for function_idx in segment.members_mut() {
|
||||
fixup(function_idx)
|
||||
}
|
||||
}
|
||||
}
|
||||
elements::Section::Start(ref mut start_idx) => {
|
||||
fixup(start_idx)
|
||||
}
|
||||
_ => {}
|
||||
}
|
||||
}
|
||||
|
||||
Ok(module)
|
||||
}
|
||||
@@ -0,0 +1,679 @@
|
||||
use super::resolve_func_type;
|
||||
use alloc::vec::Vec;
|
||||
use parity_wasm::elements::{self, BlockType, Type};
|
||||
|
||||
#[cfg(feature = "sign_ext")]
|
||||
use parity_wasm::elements::SignExtInstruction;
|
||||
|
||||
// The cost in stack items that should be charged per call of a function. This is
|
||||
// is a static cost that is added to each function call. This makes sense because even
|
||||
// if a function does not use any parameters or locals some stack space on the host
|
||||
// machine might be consumed to hold some context.
|
||||
const ACTIVATION_FRAME_COST: u32 = 1;
|
||||
|
||||
#[derive(Debug,PartialEq,Default,Clone,Copy)]
|
||||
pub struct StackHeightStats {
|
||||
pub activation_cost: u32,
|
||||
pub max_height: u32,
|
||||
pub max_control_height: u32,
|
||||
pub locals_count: u32,
|
||||
pub params_count: u32,
|
||||
pub blocks_count: u32,
|
||||
pub condbr_count: u32,
|
||||
pub push_count: u32,
|
||||
pub local_set_count: u32,
|
||||
pub opcode_count: u32,
|
||||
pub total_cost: u32,
|
||||
}
|
||||
|
||||
/// Control stack frame.
|
||||
#[derive(Debug)]
|
||||
struct Frame {
|
||||
/// Counts the nesting level of unreachable code. 0 if currently processed code is reachable
|
||||
unreachable_depth: u32,
|
||||
|
||||
/// Count of values which will be pushed after the exit
|
||||
/// from the current block.
|
||||
end_arity: u32,
|
||||
|
||||
/// Count of values which should be poped upon a branch to
|
||||
/// this frame.
|
||||
///
|
||||
/// This might be diffirent from `end_arity` since branch
|
||||
/// to the loop header can't take any values.
|
||||
branch_arity: u32,
|
||||
|
||||
/// Stack height before entering in the block.
|
||||
start_height: u32,
|
||||
}
|
||||
|
||||
/// This is a compound stack that abstracts tracking height of the value stack
|
||||
/// and manipulation of the control stack.
|
||||
struct Stack {
|
||||
height: u32,
|
||||
control_stack: Vec<Frame>,
|
||||
}
|
||||
|
||||
impl Stack {
|
||||
fn new() -> Stack {
|
||||
Stack { height: 0, control_stack: Vec::new() }
|
||||
}
|
||||
|
||||
/// Returns current height of the value stack.
|
||||
fn height(&self) -> u32 {
|
||||
self.height
|
||||
}
|
||||
|
||||
fn control_height(&self) -> u32 {
|
||||
self.control_stack.len() as u32
|
||||
}
|
||||
|
||||
/// Returns a reference to a frame by specified depth relative to the top of
|
||||
/// control stack.
|
||||
fn frame(&self, rel_depth: u32) -> Result<&Frame, &'static str> {
|
||||
let control_stack_height: usize = self.control_stack.len();
|
||||
let last_idx = control_stack_height.checked_sub(1).ok_or("control stack is empty")?;
|
||||
let idx = last_idx.checked_sub(rel_depth as usize).ok_or("control stack out-of-bounds")?;
|
||||
Ok(&self.control_stack[idx])
|
||||
}
|
||||
|
||||
/// Mark successive instructions as unreachable.
|
||||
fn mark_unreachable(&mut self) -> Result<(), &'static str> {
|
||||
let top_frame = self.control_stack.last_mut().ok_or("control stack must be non-empty")?;
|
||||
top_frame.unreachable_depth = 1;
|
||||
Ok(())
|
||||
}
|
||||
|
||||
/// Increase nesting level of unreachable code
|
||||
fn push_unreachable(&mut self) -> Result<(), &'static str> {
|
||||
let top_frame = self.control_stack.last_mut().ok_or("control stack must be non-empty")?;
|
||||
top_frame.unreachable_depth += 1;
|
||||
Ok(())
|
||||
}
|
||||
|
||||
/// Decrease nesting level of unrechable code (probably making it reachable)
|
||||
fn pop_unreachable(&mut self) -> Result<u32, &'static str> {
|
||||
let top_frame = self.control_stack.last_mut().ok_or("control stack must be non-empty")?;
|
||||
top_frame.unreachable_depth =
|
||||
top_frame.unreachable_depth.checked_sub(1).ok_or("unreachable code underflow")?;
|
||||
Ok(top_frame.unreachable_depth)
|
||||
}
|
||||
|
||||
/// Push control frame into the control stack.
|
||||
fn push_frame(&mut self, frame: Frame) {
|
||||
self.control_stack.push(frame);
|
||||
}
|
||||
|
||||
/// Pop control frame from the control stack.
|
||||
///
|
||||
/// Returns `Err` if the control stack is empty.
|
||||
fn pop_frame(&mut self) -> Result<Frame, &'static str> {
|
||||
self.control_stack.pop().ok_or("stack must be non-empty")
|
||||
}
|
||||
|
||||
/// Truncate the height of value stack to the specified height.
|
||||
fn trunc(&mut self, new_height: u32) {
|
||||
self.height = new_height;
|
||||
}
|
||||
|
||||
/// Push specified number of values into the value stack.
|
||||
///
|
||||
/// Returns `Err` if the height overflow usize value.
|
||||
fn push_values(&mut self, value_count: u32) -> Result<(), &'static str> {
|
||||
self.height = self.height.checked_add(value_count).ok_or("stack overflow")?;
|
||||
Ok(())
|
||||
}
|
||||
|
||||
/// Pop specified number of values from the value stack.
|
||||
///
|
||||
/// Returns `Err` if the stack happen to be negative value after
|
||||
/// values popped.
|
||||
fn pop_values(&mut self, value_count: u32) -> Result<(), &'static str> {
|
||||
if value_count == 0 {
|
||||
return Ok(())
|
||||
}
|
||||
|
||||
self.height = self.height.checked_sub(value_count).ok_or("stack underflow")?;
|
||||
|
||||
Ok(())
|
||||
}
|
||||
}
|
||||
|
||||
/// This function expects the function to be validated.
|
||||
pub fn compute(func_idx: u32, module: &elements::Module) -> Result<StackHeightStats, &'static str> {
|
||||
use parity_wasm::elements::Instruction::*;
|
||||
|
||||
trace!("Processing function index {}", func_idx);
|
||||
|
||||
let func_section = module.function_section().ok_or("No function section")?;
|
||||
let code_section = module.code_section().ok_or("No code section")?;
|
||||
let type_section = module.type_section().ok_or("No type section")?;
|
||||
|
||||
// Get a signature and a body of the specified function.
|
||||
let func_sig_idx = func_section
|
||||
.entries()
|
||||
.get(func_idx as usize)
|
||||
.ok_or("Function is not found in func section")?
|
||||
.type_ref();
|
||||
let Type::Function(func_signature) = type_section
|
||||
.types()
|
||||
.get(func_sig_idx as usize)
|
||||
.ok_or("Function is not found in func section")?;
|
||||
let body = code_section
|
||||
.bodies()
|
||||
.get(func_idx as usize)
|
||||
.ok_or("Function body for the index isn't found")?;
|
||||
let instructions = body.code();
|
||||
|
||||
let mut stack = Stack::new();
|
||||
let mut max_height: u32 = 0;
|
||||
let mut max_control_height: u32 = 0;
|
||||
|
||||
let mut locals_count: u32 = 0;
|
||||
for local_group in body.locals() {
|
||||
locals_count =
|
||||
locals_count.checked_add(local_group.count()).ok_or("Overflow in local count")?;
|
||||
}
|
||||
|
||||
let params_count = func_signature.params().len() as u32;
|
||||
let mut blocks_count = 0u32;
|
||||
let mut condbr_count = 0u32;
|
||||
let mut push_count = 0u32;
|
||||
let mut local_set_count = 0u32;
|
||||
|
||||
// Add implicit frame for the function. Breaks to this frame and execution of
|
||||
// the last end should deal with this frame.
|
||||
let func_arity = func_signature.results().len() as u32;
|
||||
stack.push_frame(Frame {
|
||||
unreachable_depth: 0,
|
||||
end_arity: func_arity,
|
||||
branch_arity: func_arity,
|
||||
start_height: 0,
|
||||
});
|
||||
|
||||
for opcode in instructions.elements() {
|
||||
if stack.frame(0)?.unreachable_depth > 0 {
|
||||
match opcode {
|
||||
Block(_) | Loop(_) | If(_) => {
|
||||
trace!("Entering unreachable block {:?}", opcode);
|
||||
stack.push_unreachable()?;
|
||||
continue
|
||||
},
|
||||
Else => {
|
||||
let depth = stack.pop_unreachable()?;
|
||||
if depth == 0 {
|
||||
trace!("Transiting from unreachable If body to reachable Else block");
|
||||
} else {
|
||||
trace!("Processing unreachable Else");
|
||||
stack.push_unreachable()?;
|
||||
continue
|
||||
}
|
||||
},
|
||||
End => {
|
||||
let depth = stack.pop_unreachable()?;
|
||||
if depth == 0 {
|
||||
trace!("Exiting unreachable code");
|
||||
} else {
|
||||
trace!("Exiting unreachable block");
|
||||
continue
|
||||
}
|
||||
},
|
||||
_ => {
|
||||
trace!("Skipping unreachable instruction {:?}", opcode);
|
||||
continue
|
||||
},
|
||||
}
|
||||
}
|
||||
|
||||
assert_eq!(stack.frame(0)?.unreachable_depth, 0);
|
||||
trace!("Processing opcode {:?}", opcode);
|
||||
|
||||
match opcode {
|
||||
Nop => {},
|
||||
Block(ty) | Loop(ty) | If(ty) => {
|
||||
let end_arity = if *ty == BlockType::NoResult { 0 } else { 1 };
|
||||
let branch_arity = if let Loop(_) = *opcode { 0 } else { end_arity };
|
||||
if let If(_) = *opcode {
|
||||
stack.pop_values(1)?;
|
||||
}
|
||||
stack.push_frame(Frame {
|
||||
unreachable_depth: 0,
|
||||
end_arity,
|
||||
branch_arity,
|
||||
start_height: stack.height(),
|
||||
});
|
||||
blocks_count += 1;
|
||||
},
|
||||
Else => {
|
||||
let frame = stack.pop_frame()?;
|
||||
stack.trunc(frame.start_height);
|
||||
stack.push_frame(frame);
|
||||
},
|
||||
End => {
|
||||
let frame = stack.pop_frame()?;
|
||||
stack.trunc(frame.start_height);
|
||||
stack.push_values(frame.end_arity)?;
|
||||
},
|
||||
Unreachable => {
|
||||
stack.mark_unreachable()?;
|
||||
},
|
||||
Br(target) => {
|
||||
// Pop values for the destination block result.
|
||||
let target_arity = stack.frame(*target)?.branch_arity;
|
||||
stack.pop_values(target_arity)?;
|
||||
|
||||
// This instruction unconditionally transfers control to the specified block,
|
||||
// thus all instruction until the end of the current block is deemed unreachable
|
||||
stack.mark_unreachable()?;
|
||||
},
|
||||
BrIf(target) => {
|
||||
// Pop values for the destination block result.
|
||||
let target_arity = stack.frame(*target)?.branch_arity;
|
||||
stack.pop_values(target_arity)?;
|
||||
|
||||
// Pop condition value.
|
||||
stack.pop_values(1)?;
|
||||
|
||||
// Push values back.
|
||||
stack.push_values(target_arity)?;
|
||||
|
||||
condbr_count += 1;
|
||||
},
|
||||
BrTable(br_table_data) => {
|
||||
let arity_of_default = stack.frame(br_table_data.default)?.branch_arity;
|
||||
|
||||
// Because all jump targets have an equal arities, we can just take arity of
|
||||
// the default branch.
|
||||
stack.pop_values(arity_of_default)?;
|
||||
|
||||
// This instruction doesn't let control flow to go further, since the control flow
|
||||
// should take either one of branches depending on the value or the default branch.
|
||||
stack.mark_unreachable()?;
|
||||
|
||||
condbr_count += 1;
|
||||
},
|
||||
Return => {
|
||||
// Pop return values of the function. Mark successive instructions as unreachable
|
||||
// since this instruction doesn't let control flow to go further.
|
||||
stack.pop_values(func_arity)?;
|
||||
stack.mark_unreachable()?;
|
||||
},
|
||||
Call(idx) => {
|
||||
let ty = resolve_func_type(*idx, module)?;
|
||||
|
||||
// Pop values for arguments of the function.
|
||||
stack.pop_values(ty.params().len() as u32)?;
|
||||
|
||||
// Push result of the function execution to the stack.
|
||||
let callee_arity = ty.results().len() as u32;
|
||||
stack.push_values(callee_arity)?;
|
||||
},
|
||||
CallIndirect(x, _) => {
|
||||
let Type::Function(ty) =
|
||||
type_section.types().get(*x as usize).ok_or("Type not found")?;
|
||||
|
||||
// Pop the offset into the function table.
|
||||
stack.pop_values(1)?;
|
||||
|
||||
// Pop values for arguments of the function.
|
||||
stack.pop_values(ty.params().len() as u32)?;
|
||||
|
||||
// Push result of the function execution to the stack.
|
||||
let callee_arity = ty.results().len() as u32;
|
||||
stack.push_values(callee_arity)?;
|
||||
},
|
||||
Drop => {
|
||||
stack.pop_values(1)?;
|
||||
},
|
||||
Select => {
|
||||
// Pop two values and one condition.
|
||||
stack.pop_values(2)?;
|
||||
stack.pop_values(1)?;
|
||||
|
||||
// Push the selected value.
|
||||
stack.push_values(1)?;
|
||||
push_count += 1;
|
||||
},
|
||||
GetLocal(_) => {
|
||||
stack.push_values(1)?;
|
||||
push_count += 1;
|
||||
},
|
||||
SetLocal(_) => {
|
||||
stack.pop_values(1)?;
|
||||
local_set_count += 1;
|
||||
},
|
||||
TeeLocal(_) => {
|
||||
// This instruction pops and pushes the value, so
|
||||
// effectively it doesn't modify the stack height.
|
||||
stack.pop_values(1)?;
|
||||
stack.push_values(1)?;
|
||||
local_set_count += 1;
|
||||
},
|
||||
GetGlobal(_) => {
|
||||
stack.push_values(1)?;
|
||||
push_count += 1;
|
||||
},
|
||||
SetGlobal(_) => {
|
||||
stack.pop_values(1)?;
|
||||
},
|
||||
I32Load(_, _) |
|
||||
I64Load(_, _) |
|
||||
F32Load(_, _) |
|
||||
F64Load(_, _) |
|
||||
I32Load8S(_, _) |
|
||||
I32Load8U(_, _) |
|
||||
I32Load16S(_, _) |
|
||||
I32Load16U(_, _) |
|
||||
I64Load8S(_, _) |
|
||||
I64Load8U(_, _) |
|
||||
I64Load16S(_, _) |
|
||||
I64Load16U(_, _) |
|
||||
I64Load32S(_, _) |
|
||||
I64Load32U(_, _) => {
|
||||
// These instructions pop the address and pushes the result,
|
||||
// which effictively don't modify the stack height.
|
||||
stack.pop_values(1)?;
|
||||
stack.push_values(1)?;
|
||||
push_count += 1;
|
||||
},
|
||||
|
||||
I32Store(_, _) |
|
||||
I64Store(_, _) |
|
||||
F32Store(_, _) |
|
||||
F64Store(_, _) |
|
||||
I32Store8(_, _) |
|
||||
I32Store16(_, _) |
|
||||
I64Store8(_, _) |
|
||||
I64Store16(_, _) |
|
||||
I64Store32(_, _) => {
|
||||
// These instructions pop the address and the value.
|
||||
stack.pop_values(2)?;
|
||||
},
|
||||
|
||||
CurrentMemory(_) => {
|
||||
// Pushes current memory size
|
||||
stack.push_values(1)?;
|
||||
push_count += 1;
|
||||
},
|
||||
GrowMemory(_) => {
|
||||
// Grow memory takes the value of pages to grow and pushes
|
||||
stack.pop_values(1)?;
|
||||
stack.push_values(1)?;
|
||||
push_count += 1;
|
||||
},
|
||||
|
||||
I32Const(_) | I64Const(_) | F32Const(_) | F64Const(_) => {
|
||||
// These instructions just push the single literal value onto the stack.
|
||||
stack.push_values(1)?;
|
||||
push_count += 1;
|
||||
},
|
||||
|
||||
I32Eqz | I64Eqz => {
|
||||
// These instructions pop the value and compare it against zero, and pushes
|
||||
// the result of the comparison.
|
||||
stack.pop_values(1)?;
|
||||
stack.push_values(1)?;
|
||||
push_count += 1;
|
||||
},
|
||||
|
||||
I32Eq | I32Ne | I32LtS | I32LtU | I32GtS | I32GtU | I32LeS | I32LeU | I32GeS |
|
||||
I32GeU | I64Eq | I64Ne | I64LtS | I64LtU | I64GtS | I64GtU | I64LeS | I64LeU |
|
||||
I64GeS | I64GeU | F32Eq | F32Ne | F32Lt | F32Gt | F32Le | F32Ge | F64Eq | F64Ne |
|
||||
F64Lt | F64Gt | F64Le | F64Ge => {
|
||||
// Comparison operations take two operands and produce one result.
|
||||
stack.pop_values(2)?;
|
||||
stack.push_values(1)?;
|
||||
push_count += 1;
|
||||
},
|
||||
|
||||
I32Clz | I32Ctz | I32Popcnt | I64Clz | I64Ctz | I64Popcnt | F32Abs | F32Neg |
|
||||
F32Ceil | F32Floor | F32Trunc | F32Nearest | F32Sqrt | F64Abs | F64Neg | F64Ceil |
|
||||
F64Floor | F64Trunc | F64Nearest | F64Sqrt => {
|
||||
// Unary operators take one operand and produce one result.
|
||||
stack.pop_values(1)?;
|
||||
stack.push_values(1)?;
|
||||
push_count += 1;
|
||||
},
|
||||
|
||||
I32Add | I32Sub | I32Mul | I32DivS | I32DivU | I32RemS | I32RemU | I32And | I32Or |
|
||||
I32Xor | I32Shl | I32ShrS | I32ShrU | I32Rotl | I32Rotr | I64Add | I64Sub |
|
||||
I64Mul | I64DivS | I64DivU | I64RemS | I64RemU | I64And | I64Or | I64Xor | I64Shl |
|
||||
I64ShrS | I64ShrU | I64Rotl | I64Rotr | F32Add | F32Sub | F32Mul | F32Div |
|
||||
F32Min | F32Max | F32Copysign | F64Add | F64Sub | F64Mul | F64Div | F64Min |
|
||||
F64Max | F64Copysign => {
|
||||
// Binary operators take two operands and produce one result.
|
||||
stack.pop_values(2)?;
|
||||
stack.push_values(1)?;
|
||||
push_count += 1;
|
||||
},
|
||||
|
||||
I32WrapI64 | I32TruncSF32 | I32TruncUF32 | I32TruncSF64 | I32TruncUF64 |
|
||||
I64ExtendSI32 | I64ExtendUI32 | I64TruncSF32 | I64TruncUF32 | I64TruncSF64 |
|
||||
I64TruncUF64 | F32ConvertSI32 | F32ConvertUI32 | F32ConvertSI64 | F32ConvertUI64 |
|
||||
F32DemoteF64 | F64ConvertSI32 | F64ConvertUI32 | F64ConvertSI64 | F64ConvertUI64 |
|
||||
F64PromoteF32 | I32ReinterpretF32 | I64ReinterpretF64 | F32ReinterpretI32 |
|
||||
F64ReinterpretI64 => {
|
||||
// Conversion operators take one value and produce one result.
|
||||
stack.pop_values(1)?;
|
||||
stack.push_values(1)?;
|
||||
push_count += 1;
|
||||
},
|
||||
|
||||
#[cfg(feature = "sign_ext")]
|
||||
SignExt(SignExtInstruction::I32Extend8S) |
|
||||
SignExt(SignExtInstruction::I32Extend16S) |
|
||||
SignExt(SignExtInstruction::I64Extend8S) |
|
||||
SignExt(SignExtInstruction::I64Extend16S) |
|
||||
SignExt(SignExtInstruction::I64Extend32S) => {
|
||||
stack.pop_values(1)?;
|
||||
stack.push_values(1)?;
|
||||
push_count += 1;
|
||||
},
|
||||
}
|
||||
|
||||
// If current value/control stack is higher than maximal height observed so far,
|
||||
// save the new height.
|
||||
|
||||
if stack.height() > max_height {
|
||||
max_height = stack.height();
|
||||
}
|
||||
if stack.control_height() > max_control_height {
|
||||
max_control_height = stack.control_height();
|
||||
}
|
||||
trace!(
|
||||
" Stack height: {}, control stack height: {}",
|
||||
stack.height(),
|
||||
stack.control_height()
|
||||
);
|
||||
}
|
||||
|
||||
assert_eq!(stack.control_height(), 0);
|
||||
assert_eq!(stack.height(), func_signature.results().len() as u32);
|
||||
|
||||
let res = StackHeightStats {
|
||||
activation_cost: ACTIVATION_FRAME_COST,
|
||||
max_height: max_height,
|
||||
max_control_height: max_control_height,
|
||||
locals_count: locals_count,
|
||||
params_count: params_count,
|
||||
blocks_count: blocks_count,
|
||||
condbr_count: condbr_count,
|
||||
push_count: push_count,
|
||||
local_set_count: local_set_count,
|
||||
opcode_count: instructions.elements().len() as u32,
|
||||
// total_cost: (11.749 * params_count as f64 - 0.4888 * locals_count as f64 + 14.8169 * max_height as f64 - 5.1594 * max_control_height as f64 - 24.4941) as u32
|
||||
total_cost: ACTIVATION_FRAME_COST + 2 * max_height + max_control_height + locals_count + 2 * params_count,
|
||||
};
|
||||
|
||||
trace!("Result: {:?}", res);
|
||||
|
||||
Ok(res)
|
||||
}
|
||||
|
||||
// #[cfg(test)]
|
||||
// mod tests {
|
||||
// use super::*;
|
||||
// use parity_wasm::elements;
|
||||
|
||||
// fn parse_wat(source: &str) -> elements::Module {
|
||||
// elements::deserialize_buffer(&wat::parse_str(source).expect("Failed to wat2wasm"))
|
||||
// .expect("Failed to deserialize the module")
|
||||
// }
|
||||
|
||||
// #[test]
|
||||
// fn simple_test() {
|
||||
// let module = parse_wat(
|
||||
// r#"
|
||||
// (module
|
||||
// (func
|
||||
// i32.const 1
|
||||
// i32.const 2
|
||||
// i32.const 3
|
||||
// drop
|
||||
// drop
|
||||
// drop
|
||||
// )
|
||||
// )
|
||||
// "#,
|
||||
// );
|
||||
|
||||
// let height = compute(0, &module).unwrap();
|
||||
// assert_eq!(height, ACTIVATION_FRAME_COST + 3 + 1 + 0 + 0);
|
||||
// }
|
||||
|
||||
// #[test]
|
||||
// fn implicit_and_explicit_return() {
|
||||
// let module = parse_wat(
|
||||
// r#"
|
||||
// (module
|
||||
// (func (result i32)
|
||||
// i32.const 0
|
||||
// return
|
||||
// )
|
||||
// )
|
||||
// "#,
|
||||
// );
|
||||
|
||||
// let height = compute(0, &module).unwrap();
|
||||
// assert_eq!(height, ACTIVATION_FRAME_COST + 1 + 1 + 0 + 0);
|
||||
// }
|
||||
|
||||
// #[test]
|
||||
// fn dont_count_in_unreachable() {
|
||||
// let module = parse_wat(
|
||||
// r#"
|
||||
// (module
|
||||
// (memory 0)
|
||||
// (func (result i32)
|
||||
// unreachable
|
||||
// grow_memory
|
||||
// )
|
||||
// )
|
||||
// "#,
|
||||
// );
|
||||
|
||||
// let height = compute(0, &module).unwrap();
|
||||
// assert_eq!(height, ACTIVATION_FRAME_COST + 0 + 1 + 0 + 0);
|
||||
// }
|
||||
|
||||
// #[test]
|
||||
// fn yet_another_test() {
|
||||
// let module = parse_wat(
|
||||
// r#"
|
||||
// (module
|
||||
// (memory 0)
|
||||
// (func
|
||||
// ;; Push two values and then pop them.
|
||||
// ;; This will make max depth to be equal to 2.
|
||||
// i32.const 0
|
||||
// i32.const 1
|
||||
// drop
|
||||
// drop
|
||||
|
||||
// ;; Code after `unreachable` shouldn't have an effect
|
||||
// ;; on the max depth.
|
||||
// unreachable
|
||||
// i32.const 0
|
||||
// i32.const 1
|
||||
// i32.const 2
|
||||
// )
|
||||
// )
|
||||
// "#,
|
||||
// );
|
||||
|
||||
// let height = compute(0, &module).unwrap();
|
||||
// assert_eq!(height, 2 + ACTIVATION_FRAME_COST);
|
||||
// }
|
||||
|
||||
// #[test]
|
||||
// fn call_indirect() {
|
||||
// let module = parse_wat(
|
||||
// r#"
|
||||
// (module
|
||||
// (table $ptr 1 1 funcref)
|
||||
// (elem $ptr (i32.const 0) func 1)
|
||||
// (func $main
|
||||
// (call_indirect (i32.const 0))
|
||||
// (call_indirect (i32.const 0))
|
||||
// (call_indirect (i32.const 0))
|
||||
// )
|
||||
// (func $callee
|
||||
// i64.const 42
|
||||
// drop
|
||||
// )
|
||||
// )
|
||||
// "#,
|
||||
// );
|
||||
|
||||
// let height = compute(0, &module).unwrap();
|
||||
// assert_eq!(height, 1 + ACTIVATION_FRAME_COST);
|
||||
// }
|
||||
|
||||
// #[test]
|
||||
// fn breaks() {
|
||||
// let module = parse_wat(
|
||||
// r#"
|
||||
// (module
|
||||
// (func $main
|
||||
// block (result i32)
|
||||
// block (result i32)
|
||||
// i32.const 99
|
||||
// br 1
|
||||
// end
|
||||
// end
|
||||
// drop
|
||||
// )
|
||||
// )
|
||||
// "#,
|
||||
// );
|
||||
|
||||
// let height = compute(0, &module).unwrap();
|
||||
// assert_eq!(height, 1 + ACTIVATION_FRAME_COST);
|
||||
// }
|
||||
|
||||
// #[test]
|
||||
// fn if_else_works() {
|
||||
// let module = parse_wat(
|
||||
// r#"
|
||||
// (module
|
||||
// (func $main
|
||||
// i32.const 7
|
||||
// i32.const 1
|
||||
// if (result i32)
|
||||
// i32.const 42
|
||||
// else
|
||||
// i32.const 99
|
||||
// end
|
||||
// i32.const 97
|
||||
// drop
|
||||
// drop
|
||||
// drop
|
||||
// )
|
||||
// )
|
||||
// "#,
|
||||
// );
|
||||
|
||||
// let height = compute(0, &module).unwrap();
|
||||
// assert_eq!(height, 3 + ACTIVATION_FRAME_COST);
|
||||
// }
|
||||
// }
|
||||
@@ -0,0 +1,368 @@
|
||||
//! Contains the code for the stack height limiter instrumentation.
|
||||
|
||||
use alloc::{vec, vec::Vec};
|
||||
use core::mem;
|
||||
use parity_wasm::{
|
||||
builder,
|
||||
elements::{self, Instruction, Instructions, Type},
|
||||
};
|
||||
pub use max_height::StackHeightStats;
|
||||
|
||||
/// Macro to generate preamble and postamble.
|
||||
macro_rules! instrument_call {
|
||||
($callee_idx: expr, $callee_stack_cost: expr, $stack_height_global_idx: expr, $stack_limit: expr) => {{
|
||||
use $crate::parity_wasm::elements::Instruction::*;
|
||||
[
|
||||
// stack_height += stack_cost(F)
|
||||
GetGlobal($stack_height_global_idx),
|
||||
I32Const($callee_stack_cost),
|
||||
I32Add,
|
||||
SetGlobal($stack_height_global_idx),
|
||||
// if stack_counter > LIMIT: unreachable
|
||||
GetGlobal($stack_height_global_idx),
|
||||
I32Const($stack_limit as i32),
|
||||
I32GtU,
|
||||
If(elements::BlockType::NoResult),
|
||||
Unreachable,
|
||||
End,
|
||||
// Original call
|
||||
Call($callee_idx),
|
||||
// stack_height -= stack_cost(F)
|
||||
GetGlobal($stack_height_global_idx),
|
||||
I32Const($callee_stack_cost),
|
||||
I32Sub,
|
||||
SetGlobal($stack_height_global_idx),
|
||||
]
|
||||
}};
|
||||
}
|
||||
|
||||
mod max_height;
|
||||
mod thunk;
|
||||
|
||||
pub struct Context {
|
||||
stack_height_global_idx: u32,
|
||||
func_stack_costs: Vec<StackHeightStats>,
|
||||
stack_limit: u32,
|
||||
}
|
||||
|
||||
impl Context {
|
||||
/// Returns index in a global index space of a stack_height global variable.
|
||||
fn stack_height_global_idx(&self) -> u32 {
|
||||
self.stack_height_global_idx
|
||||
}
|
||||
|
||||
/// Returns `stack_cost` for `func_idx`.
|
||||
fn stack_cost(&self, func_idx: u32) -> Option<u32> {
|
||||
if let Some(stats) = self.func_stack_costs.get(func_idx as usize) {
|
||||
Some(stats.total_cost)
|
||||
} else {
|
||||
None
|
||||
}
|
||||
}
|
||||
|
||||
/// Returns stack limit specified by the rules.
|
||||
fn stack_limit(&self) -> u32 {
|
||||
self.stack_limit
|
||||
}
|
||||
}
|
||||
|
||||
/// Inject the instumentation that makes stack overflows deterministic, by introducing
|
||||
/// an upper bound of the stack size.
|
||||
///
|
||||
/// This pass introduces a global mutable variable to track stack height,
|
||||
/// and instruments all calls with preamble and postamble.
|
||||
///
|
||||
/// Stack height is increased prior the call. Otherwise, the check would
|
||||
/// be made after the stack frame is allocated.
|
||||
///
|
||||
/// The preamble is inserted before the call. It increments
|
||||
/// the global stack height variable with statically determined "stack cost"
|
||||
/// of the callee. If after the increment the stack height exceeds
|
||||
/// the limit (specified by the `rules`) then execution traps.
|
||||
/// Otherwise, the call is executed.
|
||||
///
|
||||
/// The postamble is inserted after the call. The purpose of the postamble is to decrease
|
||||
/// the stack height by the "stack cost" of the callee function.
|
||||
///
|
||||
/// Note, that we can't instrument all possible ways to return from the function. The simplest
|
||||
/// example would be a trap issued by the host function.
|
||||
/// That means stack height global won't be equal to zero upon the next execution after such trap.
|
||||
///
|
||||
/// # Thunks
|
||||
///
|
||||
/// Because stack height is increased prior the call few problems arises:
|
||||
///
|
||||
/// - Stack height isn't increased upon an entry to the first function, i.e. exported function.
|
||||
/// - Start function is executed externally (similar to exported functions).
|
||||
/// - It is statically unknown what function will be invoked in an indirect call.
|
||||
///
|
||||
/// The solution for this problems is to generate a intermediate functions, called 'thunks', which
|
||||
/// will increase before and decrease the stack height after the call to original function, and
|
||||
/// then make exported function and table entries, start section to point to a corresponding thunks.
|
||||
///
|
||||
/// # Stack cost
|
||||
///
|
||||
/// Stack cost of the function is calculated as a sum of it's locals
|
||||
/// and the maximal height of the value stack.
|
||||
///
|
||||
/// All values are treated equally, as they have the same size.
|
||||
///
|
||||
/// The rationale is that this makes it possible to use the following very naive wasm executor:
|
||||
///
|
||||
/// - values are implemented by a union, so each value takes a size equal to the size of the largest
|
||||
/// possible value type this union can hold. (In MVP it is 8 bytes)
|
||||
/// - each value from the value stack is placed on the native stack.
|
||||
/// - each local variable and function argument is placed on the native stack.
|
||||
/// - arguments pushed by the caller are copied into callee stack rather than shared between the
|
||||
/// frames.
|
||||
/// - upon entry into the function entire stack frame is allocated.
|
||||
pub fn inject(
|
||||
mut module: elements::Module,
|
||||
stack_limit: u32,
|
||||
) -> Result<elements::Module, &'static str> {
|
||||
let mut ctx = Context {
|
||||
stack_height_global_idx: generate_stack_height_global(&mut module),
|
||||
func_stack_costs: compute_stack_costs(&module)?,
|
||||
stack_limit,
|
||||
};
|
||||
|
||||
instrument_functions(&mut ctx, &mut module)?;
|
||||
let module = thunk::generate_thunks(&mut ctx, module)?;
|
||||
|
||||
Ok(module)
|
||||
}
|
||||
|
||||
/// Generate a new global that will be used for tracking current stack height.
|
||||
fn generate_stack_height_global(module: &mut elements::Module) -> u32 {
|
||||
let global_entry = builder::global()
|
||||
.value_type()
|
||||
.i32()
|
||||
.mutable()
|
||||
.init_expr(Instruction::I32Const(0))
|
||||
.build();
|
||||
|
||||
// Try to find an existing global section.
|
||||
for section in module.sections_mut() {
|
||||
if let elements::Section::Global(gs) = section {
|
||||
gs.entries_mut().push(global_entry);
|
||||
return (gs.entries().len() as u32) - 1
|
||||
}
|
||||
}
|
||||
|
||||
// Existing section not found, create one!
|
||||
module
|
||||
.sections_mut()
|
||||
.push(elements::Section::Global(elements::GlobalSection::with_entries(vec![global_entry])));
|
||||
0
|
||||
}
|
||||
|
||||
/// Calculate stack costs for all functions.
|
||||
///
|
||||
/// Returns a vector with a stack cost for each function, including imports.
|
||||
fn compute_stack_costs(module: &elements::Module) -> Result<Vec<StackHeightStats>, &'static str> {
|
||||
let func_imports = module.import_count(elements::ImportCountType::Function);
|
||||
|
||||
// TODO: optimize!
|
||||
(0..module.functions_space())
|
||||
.map(|func_idx| {
|
||||
if func_idx < func_imports {
|
||||
// We can't calculate stack_cost of the import functions.
|
||||
Ok(Default::default())
|
||||
} else {
|
||||
compute_stack_cost(func_idx as u32, module)
|
||||
}
|
||||
})
|
||||
.collect()
|
||||
}
|
||||
|
||||
/// Stack cost of the given *defined* function is the sum of it's locals count (that is,
|
||||
/// number of arguments plus number of local variables) and the maximal stack
|
||||
/// height.
|
||||
pub fn compute_stack_cost(func_idx: u32, module: &elements::Module) -> Result<StackHeightStats, &'static str> {
|
||||
// To calculate the cost of a function we need to convert index from
|
||||
// function index space to defined function spaces.
|
||||
let func_imports = module.import_count(elements::ImportCountType::Function) as u32;
|
||||
let defined_func_idx = func_idx
|
||||
.checked_sub(func_imports)
|
||||
.ok_or("This should be a index of a defined function")?;
|
||||
|
||||
max_height::compute(defined_func_idx, module)
|
||||
}
|
||||
|
||||
fn instrument_functions(
|
||||
ctx: &mut Context,
|
||||
module: &mut elements::Module,
|
||||
) -> Result<(), &'static str> {
|
||||
for section in module.sections_mut() {
|
||||
if let elements::Section::Code(code_section) = section {
|
||||
for func_body in code_section.bodies_mut() {
|
||||
let opcodes = func_body.code_mut();
|
||||
instrument_function(ctx, opcodes)?;
|
||||
}
|
||||
}
|
||||
}
|
||||
Ok(())
|
||||
}
|
||||
|
||||
/// This function searches `call` instructions and wrap each call
|
||||
/// with preamble and postamble.
|
||||
///
|
||||
/// Before:
|
||||
///
|
||||
/// ```text
|
||||
/// get_local 0
|
||||
/// get_local 1
|
||||
/// call 228
|
||||
/// drop
|
||||
/// ```
|
||||
///
|
||||
/// After:
|
||||
///
|
||||
/// ```text
|
||||
/// get_local 0
|
||||
/// get_local 1
|
||||
///
|
||||
/// < ... preamble ... >
|
||||
///
|
||||
/// call 228
|
||||
///
|
||||
/// < .. postamble ... >
|
||||
///
|
||||
/// drop
|
||||
/// ```
|
||||
fn instrument_function(ctx: &mut Context, func: &mut Instructions) -> Result<(), &'static str> {
|
||||
use Instruction::*;
|
||||
|
||||
struct InstrumentCall {
|
||||
offset: usize,
|
||||
callee: u32,
|
||||
cost: u32,
|
||||
}
|
||||
|
||||
let calls: Vec<_> = func
|
||||
.elements()
|
||||
.iter()
|
||||
.enumerate()
|
||||
.filter_map(|(offset, instruction)| {
|
||||
if let Call(callee) = instruction {
|
||||
ctx.stack_cost(*callee).and_then(|cost| {
|
||||
if cost > 0 {
|
||||
Some(InstrumentCall { callee: *callee, offset, cost })
|
||||
} else {
|
||||
None
|
||||
}
|
||||
})
|
||||
} else {
|
||||
None
|
||||
}
|
||||
})
|
||||
.collect();
|
||||
|
||||
// The `instrumented_call!` contains the call itself. This is why we need to subtract one.
|
||||
let len = func.elements().len() + calls.len() * (instrument_call!(0, 0, 0, 0).len() - 1);
|
||||
let original_instrs = mem::replace(func.elements_mut(), Vec::with_capacity(len));
|
||||
let new_instrs = func.elements_mut();
|
||||
|
||||
let mut calls = calls.into_iter().peekable();
|
||||
for (original_pos, instr) in original_instrs.into_iter().enumerate() {
|
||||
// whether there is some call instruction at this position that needs to be instrumented
|
||||
let did_instrument = if let Some(call) = calls.peek() {
|
||||
if call.offset == original_pos {
|
||||
let new_seq = instrument_call!(
|
||||
call.callee,
|
||||
call.cost as i32,
|
||||
ctx.stack_height_global_idx(),
|
||||
ctx.stack_limit()
|
||||
);
|
||||
new_instrs.extend_from_slice(&new_seq);
|
||||
true
|
||||
} else {
|
||||
false
|
||||
}
|
||||
} else {
|
||||
false
|
||||
};
|
||||
|
||||
if did_instrument {
|
||||
calls.next();
|
||||
} else {
|
||||
new_instrs.push(instr);
|
||||
}
|
||||
}
|
||||
|
||||
if calls.next().is_some() {
|
||||
return Err("Not all calls were used")
|
||||
}
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
fn resolve_func_type(
|
||||
func_idx: u32,
|
||||
module: &elements::Module,
|
||||
) -> Result<&elements::FunctionType, &'static str> {
|
||||
let types = module.type_section().map(|ts| ts.types()).unwrap_or(&[]);
|
||||
let functions = module.function_section().map(|fs| fs.entries()).unwrap_or(&[]);
|
||||
|
||||
let func_imports = module.import_count(elements::ImportCountType::Function);
|
||||
let sig_idx = if func_idx < func_imports as u32 {
|
||||
module
|
||||
.import_section()
|
||||
.expect("function import count is not zero; import section must exists; qed")
|
||||
.entries()
|
||||
.iter()
|
||||
.filter_map(|entry| match entry.external() {
|
||||
elements::External::Function(idx) => Some(*idx),
|
||||
_ => None,
|
||||
})
|
||||
.nth(func_idx as usize)
|
||||
.expect(
|
||||
"func_idx is less than function imports count;
|
||||
nth function import must be `Some`;
|
||||
qed",
|
||||
)
|
||||
} else {
|
||||
functions
|
||||
.get(func_idx as usize - func_imports)
|
||||
.ok_or("Function at the specified index is not defined")?
|
||||
.type_ref()
|
||||
};
|
||||
let Type::Function(ty) = types
|
||||
.get(sig_idx as usize)
|
||||
.ok_or("The signature as specified by a function isn't defined")?;
|
||||
Ok(ty)
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use super::*;
|
||||
use parity_wasm::elements;
|
||||
|
||||
fn parse_wat(source: &str) -> elements::Module {
|
||||
elements::deserialize_buffer(&wat::parse_str(source).expect("Failed to wat2wasm"))
|
||||
.expect("Failed to deserialize the module")
|
||||
}
|
||||
|
||||
fn validate_module(module: elements::Module) {
|
||||
let binary = elements::serialize(module).expect("Failed to serialize");
|
||||
wasmparser::validate(&binary).expect("Invalid module");
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_with_params_and_result() {
|
||||
let module = parse_wat(
|
||||
r#"
|
||||
(module
|
||||
(func (export "i32.add") (param i32 i32) (result i32)
|
||||
get_local 0
|
||||
get_local 1
|
||||
i32.add
|
||||
)
|
||||
)
|
||||
"#,
|
||||
);
|
||||
|
||||
let module = inject(module, 1024).expect("Failed to inject stack counter");
|
||||
validate_module(module);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,139 @@
|
||||
#[cfg(not(features = "std"))]
|
||||
use alloc::collections::BTreeMap as Map;
|
||||
use alloc::vec::Vec;
|
||||
use parity_wasm::{
|
||||
builder,
|
||||
elements::{self, FunctionType, Internal},
|
||||
};
|
||||
#[cfg(features = "std")]
|
||||
use std::collections::HashMap as Map;
|
||||
|
||||
use super::{resolve_func_type, Context};
|
||||
|
||||
struct Thunk {
|
||||
signature: FunctionType,
|
||||
// Index in function space of this thunk.
|
||||
idx: Option<u32>,
|
||||
callee_stack_cost: u32,
|
||||
}
|
||||
|
||||
pub fn generate_thunks(
|
||||
ctx: &mut Context,
|
||||
module: elements::Module,
|
||||
) -> Result<elements::Module, &'static str> {
|
||||
// First, we need to collect all function indices that should be replaced by thunks
|
||||
let mut replacement_map: Map<u32, Thunk> = {
|
||||
let exports = module.export_section().map(|es| es.entries()).unwrap_or(&[]);
|
||||
let elem_segments = module.elements_section().map(|es| es.entries()).unwrap_or(&[]);
|
||||
let start_func_idx = module.start_section();
|
||||
|
||||
let exported_func_indices = exports.iter().filter_map(|entry| match entry.internal() {
|
||||
Internal::Function(function_idx) => Some(*function_idx),
|
||||
_ => None,
|
||||
});
|
||||
let table_func_indices =
|
||||
elem_segments.iter().flat_map(|segment| segment.members()).cloned();
|
||||
|
||||
// Replacement map is at least export section size.
|
||||
let mut replacement_map: Map<u32, Thunk> = Map::new();
|
||||
|
||||
for func_idx in exported_func_indices
|
||||
.chain(table_func_indices)
|
||||
.chain(start_func_idx.into_iter())
|
||||
{
|
||||
let callee_stack_cost = ctx.stack_cost(func_idx).ok_or("function index isn't found")?;
|
||||
|
||||
// Don't generate a thunk if stack_cost of a callee is zero.
|
||||
if callee_stack_cost != 0 {
|
||||
replacement_map.insert(
|
||||
func_idx,
|
||||
Thunk {
|
||||
signature: resolve_func_type(func_idx, &module)?.clone(),
|
||||
idx: None,
|
||||
callee_stack_cost,
|
||||
},
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
replacement_map
|
||||
};
|
||||
|
||||
// Then, we generate a thunk for each original function.
|
||||
|
||||
// Save current func_idx
|
||||
let mut next_func_idx = module.functions_space() as u32;
|
||||
|
||||
let mut mbuilder = builder::from_module(module);
|
||||
for (func_idx, thunk) in replacement_map.iter_mut() {
|
||||
let instrumented_call = instrument_call!(
|
||||
*func_idx,
|
||||
thunk.callee_stack_cost as i32,
|
||||
ctx.stack_height_global_idx(),
|
||||
ctx.stack_limit()
|
||||
);
|
||||
// Thunk body consist of:
|
||||
// - argument pushing
|
||||
// - instrumented call
|
||||
// - end
|
||||
let mut thunk_body: Vec<elements::Instruction> =
|
||||
Vec::with_capacity(thunk.signature.params().len() + instrumented_call.len() + 1);
|
||||
|
||||
for (arg_idx, _) in thunk.signature.params().iter().enumerate() {
|
||||
thunk_body.push(elements::Instruction::GetLocal(arg_idx as u32));
|
||||
}
|
||||
thunk_body.extend_from_slice(&instrumented_call);
|
||||
thunk_body.push(elements::Instruction::End);
|
||||
|
||||
// TODO: Don't generate a signature, but find an existing one.
|
||||
|
||||
mbuilder = mbuilder
|
||||
.function()
|
||||
// Signature of the thunk should match the original function signature.
|
||||
.signature()
|
||||
.with_params(thunk.signature.params().to_vec())
|
||||
.with_results(thunk.signature.results().to_vec())
|
||||
.build()
|
||||
.body()
|
||||
.with_instructions(elements::Instructions::new(thunk_body))
|
||||
.build()
|
||||
.build();
|
||||
|
||||
thunk.idx = Some(next_func_idx);
|
||||
next_func_idx += 1;
|
||||
}
|
||||
let mut module = mbuilder.build();
|
||||
|
||||
// And finally, fixup thunks in export and table sections.
|
||||
|
||||
// Fixup original function index to a index of a thunk generated earlier.
|
||||
let fixup = |function_idx: &mut u32| {
|
||||
// Check whether this function is in replacement_map, since
|
||||
// we can skip thunk generation (e.g. if stack_cost of function is 0).
|
||||
if let Some(thunk) = replacement_map.get(function_idx) {
|
||||
*function_idx =
|
||||
thunk.idx.expect("At this point an index must be assigned to each thunk");
|
||||
}
|
||||
};
|
||||
|
||||
for section in module.sections_mut() {
|
||||
match section {
|
||||
elements::Section::Export(export_section) =>
|
||||
for entry in export_section.entries_mut() {
|
||||
if let Internal::Function(function_idx) = entry.internal_mut() {
|
||||
fixup(function_idx)
|
||||
}
|
||||
},
|
||||
elements::Section::Element(elem_section) =>
|
||||
for segment in elem_section.entries_mut() {
|
||||
for function_idx in segment.members_mut() {
|
||||
fixup(function_idx)
|
||||
}
|
||||
},
|
||||
elements::Section::Start(start_idx) => fixup(start_idx),
|
||||
_ => {},
|
||||
}
|
||||
}
|
||||
|
||||
Ok(module)
|
||||
}
|
||||
-148
@@ -1,148 +0,0 @@
|
||||
#[cfg(features = "std")]
|
||||
use std::collections::{HashSet as Set};
|
||||
#[cfg(not(features = "std"))]
|
||||
use std::collections::{BTreeSet as Set};
|
||||
use std::vec::Vec;
|
||||
|
||||
use parity_wasm::elements;
|
||||
|
||||
#[derive(PartialEq, Eq, PartialOrd, Ord, Hash, Copy, Clone, Debug)]
|
||||
pub enum Symbol {
|
||||
Type(usize),
|
||||
Import(usize),
|
||||
Global(usize),
|
||||
Function(usize),
|
||||
Export(usize),
|
||||
}
|
||||
|
||||
pub fn resolve_function(module: &elements::Module, index: u32) -> Symbol {
|
||||
let mut functions = 0;
|
||||
if let Some(import_section) = module.import_section() {
|
||||
for (item_index, item) in import_section.entries().iter().enumerate() {
|
||||
if let &elements::External::Function(_) = item.external() {
|
||||
if functions == index {
|
||||
return Symbol::Import(item_index as usize);
|
||||
}
|
||||
functions += 1;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
Symbol::Function(index as usize - functions as usize)
|
||||
}
|
||||
|
||||
pub fn resolve_global(module: &elements::Module, index: u32) -> Symbol {
|
||||
let mut globals = 0;
|
||||
if let Some(import_section) = module.import_section() {
|
||||
for (item_index, item) in import_section.entries().iter().enumerate() {
|
||||
if let &elements::External::Global(_) = item.external() {
|
||||
if globals == index {
|
||||
return Symbol::Import(item_index as usize);
|
||||
}
|
||||
globals += 1;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
Symbol::Global(index as usize - globals as usize)
|
||||
}
|
||||
|
||||
pub fn push_code_symbols(module: &elements::Module, instructions: &[elements::Instruction], dest: &mut Vec<Symbol>) {
|
||||
use parity_wasm::elements::Instruction::*;
|
||||
|
||||
for instruction in instructions {
|
||||
match instruction {
|
||||
&Call(idx) => {
|
||||
dest.push(resolve_function(module, idx));
|
||||
},
|
||||
&CallIndirect(idx, _) => {
|
||||
dest.push(Symbol::Type(idx as usize));
|
||||
},
|
||||
&GetGlobal(idx) | &SetGlobal(idx) => {
|
||||
dest.push(resolve_global(module, idx))
|
||||
},
|
||||
_ => { },
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
pub fn expand_symbols(module: &elements::Module, set: &mut Set<Symbol>) {
|
||||
use self::Symbol::*;
|
||||
|
||||
// symbols that were already processed
|
||||
let mut stop: Set<Symbol> = Set::new();
|
||||
let mut fringe = set.iter().cloned().collect::<Vec<Symbol>>();
|
||||
loop {
|
||||
let next = match fringe.pop() {
|
||||
Some(s) if stop.contains(&s) => { continue; }
|
||||
Some(s) => s,
|
||||
_ => { break; }
|
||||
};
|
||||
trace!("Processing symbol {:?}", next);
|
||||
|
||||
match next {
|
||||
Export(idx) => {
|
||||
let entry = &module.export_section().expect("Export section to exist").entries()[idx];
|
||||
match entry.internal() {
|
||||
&elements::Internal::Function(func_idx) => {
|
||||
let symbol = resolve_function(module, func_idx);
|
||||
if !stop.contains(&symbol) {
|
||||
fringe.push(symbol);
|
||||
}
|
||||
set.insert(symbol);
|
||||
},
|
||||
&elements::Internal::Global(global_idx) => {
|
||||
let symbol = resolve_global(module, global_idx);
|
||||
if !stop.contains(&symbol) {
|
||||
fringe.push(symbol);
|
||||
}
|
||||
set.insert(symbol);
|
||||
},
|
||||
_ => {}
|
||||
}
|
||||
},
|
||||
Import(idx) => {
|
||||
let entry = &module.import_section().expect("Import section to exist").entries()[idx];
|
||||
if let &elements::External::Function(type_idx) = entry.external() {
|
||||
let type_symbol = Symbol::Type(type_idx as usize);
|
||||
if !stop.contains(&type_symbol) {
|
||||
fringe.push(type_symbol);
|
||||
}
|
||||
set.insert(type_symbol);
|
||||
}
|
||||
},
|
||||
Function(idx) => {
|
||||
let body = &module.code_section().expect("Code section to exist").bodies()[idx];
|
||||
let mut code_symbols = Vec::new();
|
||||
push_code_symbols(module, body.code().elements(), &mut code_symbols);
|
||||
for symbol in code_symbols.drain(..) {
|
||||
if !stop.contains(&symbol) {
|
||||
fringe.push(symbol);
|
||||
}
|
||||
set.insert(symbol);
|
||||
}
|
||||
|
||||
let signature = &module.function_section().expect("Functions section to exist").entries()[idx];
|
||||
let type_symbol = Symbol::Type(signature.type_ref() as usize);
|
||||
if !stop.contains(&type_symbol) {
|
||||
fringe.push(type_symbol);
|
||||
}
|
||||
set.insert(type_symbol);
|
||||
},
|
||||
Global(idx) => {
|
||||
let entry = &module.global_section().expect("Global section to exist").entries()[idx];
|
||||
let mut code_symbols = Vec::new();
|
||||
push_code_symbols(module, entry.init_expr().code(), &mut code_symbols);
|
||||
for symbol in code_symbols.drain(..) {
|
||||
if !stop.contains(&symbol) {
|
||||
fringe.push(symbol);
|
||||
}
|
||||
set.insert(symbol);
|
||||
}
|
||||
}
|
||||
_ => {}
|
||||
}
|
||||
|
||||
stop.insert(next);
|
||||
}
|
||||
}
|
||||
+46
-44
@@ -1,18 +1,17 @@
|
||||
extern crate diff;
|
||||
extern crate pwasm_utils as utils;
|
||||
extern crate wabt;
|
||||
extern crate parity_wasm;
|
||||
|
||||
use std::fs;
|
||||
use std::io::{self, Read, Write};
|
||||
use std::path::{Path, PathBuf};
|
||||
use parity_wasm::elements;
|
||||
use parity_wasm::elements::Module;
|
||||
use std::{
|
||||
fs,
|
||||
io::{self, Read, Write},
|
||||
path::{Path, PathBuf},
|
||||
};
|
||||
use wasm_instrument::{self as instrument, parity_wasm::elements};
|
||||
use wasmparser::validate;
|
||||
|
||||
fn slurp<P: AsRef<Path>>(path: P) -> io::Result<Vec<u8>> {
|
||||
let mut f = fs::File::open(path)?;
|
||||
let mut buf = vec![];
|
||||
f.read_to_end(&mut buf)?;
|
||||
Ok(buf)
|
||||
let mut f = fs::File::open(path)?;
|
||||
let mut buf = vec![];
|
||||
f.read_to_end(&mut buf)?;
|
||||
Ok(buf)
|
||||
}
|
||||
|
||||
fn dump<P: AsRef<Path>>(path: P, buf: &[u8]) -> io::Result<()> {
|
||||
@@ -21,46 +20,36 @@ fn dump<P: AsRef<Path>>(path: P, buf: &[u8]) -> io::Result<()> {
|
||||
Ok(())
|
||||
}
|
||||
|
||||
fn validate_wasm(binary: &[u8]) -> Result<(), wabt::Error> {
|
||||
wabt::Module::read_binary(
|
||||
&binary,
|
||||
&Default::default()
|
||||
)?.validate()?;
|
||||
Ok(())
|
||||
}
|
||||
|
||||
fn run_diff_test<F: FnOnce(&[u8]) -> Vec<u8>>(test_dir: &str, name: &str, test: F) {
|
||||
let mut fixture_path = PathBuf::from(concat!(
|
||||
env!("CARGO_MANIFEST_DIR"),
|
||||
"/tests/fixtures/",
|
||||
));
|
||||
let mut fixture_path = PathBuf::from(env!("CARGO_MANIFEST_DIR"));
|
||||
fixture_path.push("tests");
|
||||
fixture_path.push("fixtures");
|
||||
fixture_path.push(test_dir);
|
||||
fixture_path.push(name);
|
||||
|
||||
let mut expected_path = PathBuf::from(concat!(
|
||||
env!("CARGO_MANIFEST_DIR"),
|
||||
"/tests/expectations/"
|
||||
));
|
||||
let mut expected_path = PathBuf::from(env!("CARGO_MANIFEST_DIR"));
|
||||
expected_path.push("tests");
|
||||
expected_path.push("expectations");
|
||||
expected_path.push(test_dir);
|
||||
expected_path.push(name);
|
||||
|
||||
let fixture_wat = slurp(&fixture_path).expect("Failed to read fixture");
|
||||
let fixture_wasm = wabt::wat2wasm(fixture_wat).expect("Failed to read fixture");
|
||||
validate_wasm(&fixture_wasm).expect("Fixture is invalid");
|
||||
let fixture_wasm = wat::parse_file(&fixture_path).expect("Failed to read fixture");
|
||||
validate(&fixture_wasm).expect("Fixture is invalid");
|
||||
|
||||
let expected_wat = slurp(&expected_path).unwrap_or_default();
|
||||
let expected_wat = String::from_utf8_lossy(&expected_wat);
|
||||
let expected_wat = std::str::from_utf8(&expected_wat).expect("Failed to decode expected wat");
|
||||
|
||||
let actual_wasm = test(fixture_wasm.as_ref());
|
||||
validate_wasm(&actual_wasm).expect("Result module is invalid");
|
||||
validate(&actual_wasm).expect("Result module is invalid");
|
||||
|
||||
let actual_wat = wabt::wasm2wat(&actual_wasm).expect("Failed to convert result wasm to wat");
|
||||
let actual_wat =
|
||||
wasmprinter::print_bytes(&actual_wasm).expect("Failed to convert result wasm to wat");
|
||||
|
||||
if actual_wat != expected_wat {
|
||||
println!("difference!");
|
||||
println!("--- {}", expected_path.display());
|
||||
println!("+++ {} test {}", test_dir, name);
|
||||
for diff in diff::lines(&expected_wat, &actual_wat) {
|
||||
for diff in diff::lines(expected_wat, &actual_wat) {
|
||||
match diff {
|
||||
diff::Result::Left(l) => println!("-{}", l),
|
||||
diff::Result::Both(l, _) => println!(" {}", l),
|
||||
@@ -68,9 +57,11 @@ fn run_diff_test<F: FnOnce(&[u8]) -> Vec<u8>>(test_dir: &str, name: &str, test:
|
||||
}
|
||||
}
|
||||
|
||||
dump(&expected_path, actual_wat.as_bytes()).expect("Failed to write to expected");
|
||||
|
||||
panic!();
|
||||
if std::env::var_os("BLESS").is_some() {
|
||||
dump(&expected_path, actual_wat.as_bytes()).expect("Failed to write to expected");
|
||||
} else {
|
||||
panic!();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -82,8 +73,10 @@ mod stack_height {
|
||||
#[test]
|
||||
fn $name() {
|
||||
run_diff_test("stack-height", concat!(stringify!($name), ".wat"), |input| {
|
||||
let module = elements::deserialize_buffer(input).expect("Failed to deserialize");
|
||||
let instrumented = utils::stack_height::inject_limiter(module, 1024).expect("Failed to instrument with stack counter");
|
||||
let module =
|
||||
elements::deserialize_buffer(input).expect("Failed to deserialize");
|
||||
let instrumented = instrument::inject_stack_limiter(module, 1024)
|
||||
.expect("Failed to instrument with stack counter");
|
||||
elements::serialize(instrumented).expect("Failed to serialize")
|
||||
});
|
||||
}
|
||||
@@ -95,6 +88,8 @@ mod stack_height {
|
||||
def_stack_height_test!(table);
|
||||
def_stack_height_test!(global);
|
||||
def_stack_height_test!(imports);
|
||||
def_stack_height_test!(many_locals);
|
||||
def_stack_height_test!(empty_functions);
|
||||
}
|
||||
|
||||
mod gas {
|
||||
@@ -105,16 +100,23 @@ mod gas {
|
||||
#[test]
|
||||
fn $name() {
|
||||
run_diff_test("gas", concat!(stringify!($name), ".wat"), |input| {
|
||||
let rules = utils::rules::Set::default();
|
||||
let rules = instrument::gas_metering::ConstantCostRules::default();
|
||||
|
||||
let module = elements::deserialize_buffer(input).expect("Failed to deserialize");
|
||||
let instrumented = utils::inject_gas_counter(module, &rules).expect("Failed to instrument with gas metering");
|
||||
let module: Module =
|
||||
elements::deserialize_buffer(input).expect("Failed to deserialize");
|
||||
let module = module.parse_names().expect("Failed to parse names");
|
||||
|
||||
let instrumented = instrument::gas_metering::inject(module, &rules, "env")
|
||||
.expect("Failed to instrument with gas metering");
|
||||
elements::serialize(instrumented).expect("Failed to serialize")
|
||||
});
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
def_gas_test!(ifs);
|
||||
def_gas_test!(simple);
|
||||
def_gas_test!(start);
|
||||
def_gas_test!(call);
|
||||
def_gas_test!(branch);
|
||||
}
|
||||
|
||||
@@ -0,0 +1,31 @@
|
||||
(module
|
||||
(type (;0;) (func (result i32)))
|
||||
(type (;1;) (func (param i32)))
|
||||
(import "env" "gas" (func (;0;) (type 1)))
|
||||
(func $fibonacci_with_break (;1;) (type 0) (result i32)
|
||||
(local i32 i32)
|
||||
i32.const 13
|
||||
call 0
|
||||
block ;; label = @1
|
||||
i32.const 0
|
||||
local.set 0
|
||||
i32.const 1
|
||||
local.set 1
|
||||
local.get 0
|
||||
local.get 1
|
||||
local.tee 0
|
||||
i32.add
|
||||
local.set 1
|
||||
i32.const 1
|
||||
br_if 0 (;@1;)
|
||||
i32.const 5
|
||||
call 0
|
||||
local.get 0
|
||||
local.get 1
|
||||
local.tee 0
|
||||
i32.add
|
||||
local.set 1
|
||||
end
|
||||
local.get 1
|
||||
)
|
||||
)
|
||||
@@ -0,0 +1,22 @@
|
||||
(module
|
||||
(type (;0;) (func (param i32 i32) (result i32)))
|
||||
(type (;1;) (func (param i32)))
|
||||
(import "env" "gas" (func (;0;) (type 1)))
|
||||
(func $add_locals (;1;) (type 0) (param $x i32) (param $y i32) (result i32)
|
||||
(local i32)
|
||||
i32.const 5
|
||||
call 0
|
||||
local.get $x
|
||||
local.get $y
|
||||
call $add
|
||||
local.set 2
|
||||
local.get 2
|
||||
)
|
||||
(func $add (;2;) (type 0) (param i32 i32) (result i32)
|
||||
i32.const 3
|
||||
call 0
|
||||
local.get 0
|
||||
local.get 1
|
||||
i32.add
|
||||
)
|
||||
)
|
||||
@@ -0,0 +1,22 @@
|
||||
(module
|
||||
(type (;0;) (func (param i32) (result i32)))
|
||||
(type (;1;) (func (param i32)))
|
||||
(import "env" "gas" (func (;0;) (type 1)))
|
||||
(func (;1;) (type 0) (param i32) (result i32)
|
||||
i32.const 2
|
||||
call 0
|
||||
i32.const 1
|
||||
if (result i32) ;; label = @1
|
||||
i32.const 3
|
||||
call 0
|
||||
local.get 0
|
||||
i32.const 1
|
||||
i32.add
|
||||
else
|
||||
i32.const 2
|
||||
call 0
|
||||
local.get 0
|
||||
i32.popcnt
|
||||
end
|
||||
)
|
||||
)
|
||||
@@ -3,24 +3,25 @@
|
||||
(type (;1;) (func (param i32)))
|
||||
(import "env" "gas" (func (;0;) (type 1)))
|
||||
(func (;1;) (type 0)
|
||||
i32.const 3
|
||||
i32.const 2
|
||||
call 0
|
||||
i32.const 1
|
||||
if ;; label = @1
|
||||
i32.const 2
|
||||
i32.const 1
|
||||
call 0
|
||||
loop ;; label = @2
|
||||
i32.const 3
|
||||
i32.const 2
|
||||
call 0
|
||||
i32.const 123
|
||||
drop
|
||||
end
|
||||
end)
|
||||
end
|
||||
)
|
||||
(func (;2;) (type 0)
|
||||
i32.const 2
|
||||
i32.const 1
|
||||
call 0
|
||||
block ;; label = @1
|
||||
i32.const 1
|
||||
call 0
|
||||
end)
|
||||
(export "simple" (func 1)))
|
||||
end
|
||||
)
|
||||
(export "simple" (func 1))
|
||||
)
|
||||
@@ -2,19 +2,19 @@
|
||||
(type (;0;) (func (param i32 i32)))
|
||||
(type (;1;) (func))
|
||||
(type (;2;) (func (param i32)))
|
||||
(import "env" "ext_return" (func (;0;) (type 0)))
|
||||
(import "env" "ext_return" (func $ext_return (;0;) (type 0)))
|
||||
(import "env" "memory" (memory (;0;) 1 1))
|
||||
(import "env" "gas" (func (;1;) (type 2)))
|
||||
(func (;2;) (type 1)
|
||||
i32.const 5
|
||||
(func $start (;2;) (type 1)
|
||||
i32.const 4
|
||||
call 1
|
||||
i32.const 8
|
||||
i32.const 4
|
||||
call 0
|
||||
unreachable)
|
||||
(func (;3;) (type 1)
|
||||
i32.const 1
|
||||
call 1)
|
||||
call $ext_return
|
||||
unreachable
|
||||
)
|
||||
(func (;3;) (type 1))
|
||||
(export "call" (func 3))
|
||||
(start 2)
|
||||
(data (i32.const 8) "\01\02\03\04"))
|
||||
(start $start)
|
||||
(data (;0;) (i32.const 8) "\01\02\03\04")
|
||||
)
|
||||
@@ -0,0 +1,56 @@
|
||||
(module
|
||||
(type (;0;) (func))
|
||||
(func (;0;) (type 0)
|
||||
global.get 0
|
||||
i32.const 2
|
||||
i32.add
|
||||
global.set 0
|
||||
global.get 0
|
||||
i32.const 1024
|
||||
i32.gt_u
|
||||
if ;; label = @1
|
||||
unreachable
|
||||
end
|
||||
call 0
|
||||
global.get 0
|
||||
i32.const 2
|
||||
i32.sub
|
||||
global.set 0
|
||||
)
|
||||
(func (;1;) (type 0)
|
||||
global.get 0
|
||||
i32.const 2
|
||||
i32.add
|
||||
global.set 0
|
||||
global.get 0
|
||||
i32.const 1024
|
||||
i32.gt_u
|
||||
if ;; label = @1
|
||||
unreachable
|
||||
end
|
||||
call 0
|
||||
global.get 0
|
||||
i32.const 2
|
||||
i32.sub
|
||||
global.set 0
|
||||
)
|
||||
(func (;2;) (type 0)
|
||||
global.get 0
|
||||
i32.const 2
|
||||
i32.add
|
||||
global.set 0
|
||||
global.get 0
|
||||
i32.const 1024
|
||||
i32.gt_u
|
||||
if ;; label = @1
|
||||
unreachable
|
||||
end
|
||||
call 1
|
||||
global.get 0
|
||||
i32.const 2
|
||||
i32.sub
|
||||
global.set 0
|
||||
)
|
||||
(global (;0;) (mut i32) i32.const 0)
|
||||
(export "main" (func 2))
|
||||
)
|
||||
@@ -2,54 +2,58 @@
|
||||
(type (;0;) (func))
|
||||
(type (;1;) (func (param i32 i32) (result i32)))
|
||||
(type (;2;) (func (param i32)))
|
||||
(import "env" "foo" (func (;0;) (type 0)))
|
||||
(func (;1;) (type 1) (param i32 i32) (result i32)
|
||||
get_local 0
|
||||
get_local 1
|
||||
i32.add)
|
||||
(func (;2;) (type 2) (param i32)
|
||||
(local i32)
|
||||
get_global 0
|
||||
(import "env" "foo" (func $foo (;0;) (type 0)))
|
||||
(func $i32.add (;1;) (type 1) (param i32 i32) (result i32)
|
||||
local.get 0
|
||||
local.get 1
|
||||
i32.add
|
||||
)
|
||||
(func (;2;) (type 2) (param $arg i32)
|
||||
(local $tmp i32)
|
||||
global.get $counter
|
||||
i32.const 1
|
||||
i32.add
|
||||
tee_local 1
|
||||
set_global 0
|
||||
get_local 1
|
||||
get_local 0
|
||||
get_global 1
|
||||
i32.const 2
|
||||
local.tee $tmp
|
||||
global.set $counter
|
||||
local.get $tmp
|
||||
local.get $arg
|
||||
global.get 1
|
||||
i32.const 4
|
||||
i32.add
|
||||
set_global 1
|
||||
get_global 1
|
||||
global.set 1
|
||||
global.get 1
|
||||
i32.const 1024
|
||||
i32.gt_u
|
||||
if ;; label = @1
|
||||
unreachable
|
||||
end
|
||||
call 1
|
||||
get_global 1
|
||||
i32.const 2
|
||||
call $i32.add
|
||||
global.get 1
|
||||
i32.const 4
|
||||
i32.sub
|
||||
set_global 1
|
||||
drop)
|
||||
global.set 1
|
||||
drop
|
||||
)
|
||||
(func (;3;) (type 1) (param i32 i32) (result i32)
|
||||
get_local 0
|
||||
get_local 1
|
||||
get_global 1
|
||||
i32.const 2
|
||||
local.get 0
|
||||
local.get 1
|
||||
global.get 1
|
||||
i32.const 4
|
||||
i32.add
|
||||
set_global 1
|
||||
get_global 1
|
||||
global.set 1
|
||||
global.get 1
|
||||
i32.const 1024
|
||||
i32.gt_u
|
||||
if ;; label = @1
|
||||
unreachable
|
||||
end
|
||||
call 1
|
||||
get_global 1
|
||||
i32.const 2
|
||||
call $i32.add
|
||||
global.get 1
|
||||
i32.const 4
|
||||
i32.sub
|
||||
set_global 1)
|
||||
(global (;0;) (mut i32) (i32.const 1))
|
||||
(global (;1;) (mut i32) (i32.const 0))
|
||||
(export "i32.add" (func 3)))
|
||||
global.set 1
|
||||
)
|
||||
(global $counter (;0;) (mut i32) i32.const 1)
|
||||
(global (;1;) (mut i32) i32.const 0)
|
||||
(export "i32.add" (func 3))
|
||||
)
|
||||
@@ -1,31 +1,34 @@
|
||||
(module
|
||||
(type (;0;) (func))
|
||||
(type (;1;) (func (param i32 i32) (result i32)))
|
||||
(import "env" "foo" (func (;0;) (type 0)))
|
||||
(import "env" "boo" (func (;1;) (type 0)))
|
||||
(import "env" "foo" (func $foo (;0;) (type 0)))
|
||||
(import "env" "boo" (func $boo (;1;) (type 0)))
|
||||
(func (;2;) (type 1) (param i32 i32) (result i32)
|
||||
call 0
|
||||
call 1
|
||||
get_local 0
|
||||
get_local 1
|
||||
i32.add)
|
||||
(func (;3;) (type 1) (param i32 i32) (result i32)
|
||||
get_local 0
|
||||
get_local 1
|
||||
get_global 0
|
||||
i32.const 2
|
||||
call $foo
|
||||
call $boo
|
||||
local.get 0
|
||||
local.get 1
|
||||
i32.add
|
||||
set_global 0
|
||||
get_global 0
|
||||
)
|
||||
(func (;3;) (type 1) (param i32 i32) (result i32)
|
||||
local.get 0
|
||||
local.get 1
|
||||
global.get 0
|
||||
i32.const 4
|
||||
i32.add
|
||||
global.set 0
|
||||
global.get 0
|
||||
i32.const 1024
|
||||
i32.gt_u
|
||||
if ;; label = @1
|
||||
unreachable
|
||||
end
|
||||
call 2
|
||||
get_global 0
|
||||
i32.const 2
|
||||
global.get 0
|
||||
i32.const 4
|
||||
i32.sub
|
||||
set_global 0)
|
||||
(global (;0;) (mut i32) (i32.const 0))
|
||||
(export "i32.add" (func 3)))
|
||||
global.set 0
|
||||
)
|
||||
(global (;0;) (mut i32) i32.const 0)
|
||||
(export "i32.add" (func 3))
|
||||
)
|
||||
@@ -0,0 +1,24 @@
|
||||
(module
|
||||
(type (;0;) (func))
|
||||
(func $one-group-many-locals (;0;) (type 0)
|
||||
(local i64 i64 i32)
|
||||
)
|
||||
(func $main (;1;) (type 0)
|
||||
global.get 0
|
||||
i32.const 5
|
||||
i32.add
|
||||
global.set 0
|
||||
global.get 0
|
||||
i32.const 1024
|
||||
i32.gt_u
|
||||
if ;; label = @1
|
||||
unreachable
|
||||
end
|
||||
call $one-group-many-locals
|
||||
global.get 0
|
||||
i32.const 5
|
||||
i32.sub
|
||||
global.set 0
|
||||
)
|
||||
(global (;0;) (mut i32) i32.const 0)
|
||||
)
|
||||
@@ -2,22 +2,25 @@
|
||||
(type (;0;) (func))
|
||||
(func (;0;) (type 0)
|
||||
i32.const 123
|
||||
drop)
|
||||
drop
|
||||
)
|
||||
(func (;1;) (type 0)
|
||||
get_global 0
|
||||
i32.const 1
|
||||
global.get 0
|
||||
i32.const 3
|
||||
i32.add
|
||||
set_global 0
|
||||
get_global 0
|
||||
global.set 0
|
||||
global.get 0
|
||||
i32.const 1024
|
||||
i32.gt_u
|
||||
if ;; label = @1
|
||||
unreachable
|
||||
end
|
||||
call 0
|
||||
get_global 0
|
||||
i32.const 1
|
||||
global.get 0
|
||||
i32.const 3
|
||||
i32.sub
|
||||
set_global 0)
|
||||
(global (;0;) (mut i32) (i32.const 0))
|
||||
(export "simple" (func 1)))
|
||||
global.set 0
|
||||
)
|
||||
(global (;0;) (mut i32) i32.const 0)
|
||||
(export "simple" (func 1))
|
||||
)
|
||||
@@ -1,44 +1,47 @@
|
||||
(module
|
||||
(type (;0;) (func (param i32 i32)))
|
||||
(type (;1;) (func))
|
||||
(import "env" "ext_return" (func (;0;) (type 0)))
|
||||
(import "env" "ext_return" (func $ext_return (;0;) (type 0)))
|
||||
(import "env" "memory" (memory (;0;) 1 1))
|
||||
(func (;1;) (type 1)
|
||||
(local i32))
|
||||
(func $start (;1;) (type 1)
|
||||
(local i32)
|
||||
)
|
||||
(func (;2;) (type 1))
|
||||
(func (;3;) (type 1)
|
||||
get_global 0
|
||||
i32.const 1
|
||||
global.get 0
|
||||
i32.const 3
|
||||
i32.add
|
||||
set_global 0
|
||||
get_global 0
|
||||
global.set 0
|
||||
global.get 0
|
||||
i32.const 1024
|
||||
i32.gt_u
|
||||
if ;; label = @1
|
||||
unreachable
|
||||
end
|
||||
call 1
|
||||
get_global 0
|
||||
i32.const 1
|
||||
call $start
|
||||
global.get 0
|
||||
i32.const 3
|
||||
i32.sub
|
||||
set_global 0)
|
||||
global.set 0
|
||||
)
|
||||
(func (;4;) (type 1)
|
||||
get_global 0
|
||||
i32.const 1
|
||||
global.get 0
|
||||
i32.const 2
|
||||
i32.add
|
||||
set_global 0
|
||||
get_global 0
|
||||
global.set 0
|
||||
global.get 0
|
||||
i32.const 1024
|
||||
i32.gt_u
|
||||
if ;; label = @1
|
||||
unreachable
|
||||
end
|
||||
call 1
|
||||
get_global 0
|
||||
i32.const 1
|
||||
call 2
|
||||
global.get 0
|
||||
i32.const 2
|
||||
i32.sub
|
||||
set_global 0)
|
||||
(global (;0;) (mut i32) (i32.const 0))
|
||||
(export "exported_start" (func 4))
|
||||
(export "call" (func 2))
|
||||
(start 4))
|
||||
global.set 0
|
||||
)
|
||||
(global (;0;) (mut i32) i32.const 0)
|
||||
(export "call" (func 4))
|
||||
(start 3)
|
||||
)
|
||||
@@ -2,84 +2,71 @@
|
||||
(type (;0;) (func))
|
||||
(type (;1;) (func (param i32)))
|
||||
(type (;2;) (func (param i32 i32) (result i32)))
|
||||
(import "env" "foo" (func (;0;) (type 0)))
|
||||
(import "env" "foo" (func $foo (;0;) (type 0)))
|
||||
(func (;1;) (type 1) (param i32)
|
||||
get_local 0
|
||||
local.get 0
|
||||
i32.const 0
|
||||
get_global 0
|
||||
i32.const 2
|
||||
global.get 0
|
||||
i32.const 4
|
||||
i32.add
|
||||
set_global 0
|
||||
get_global 0
|
||||
global.set 0
|
||||
global.get 0
|
||||
i32.const 1024
|
||||
i32.gt_u
|
||||
if ;; label = @1
|
||||
unreachable
|
||||
end
|
||||
call 2
|
||||
get_global 0
|
||||
i32.const 2
|
||||
call $i32.add
|
||||
global.get 0
|
||||
i32.const 4
|
||||
i32.sub
|
||||
set_global 0
|
||||
drop)
|
||||
(func (;2;) (type 2) (param i32 i32) (result i32)
|
||||
get_local 0
|
||||
get_local 1
|
||||
i32.add)
|
||||
(func (;3;) (type 2) (param i32 i32) (result i32)
|
||||
get_local 0
|
||||
get_local 1
|
||||
get_global 0
|
||||
i32.const 2
|
||||
global.set 0
|
||||
drop
|
||||
)
|
||||
(func $i32.add (;2;) (type 2) (param i32 i32) (result i32)
|
||||
local.get 0
|
||||
local.get 1
|
||||
i32.add
|
||||
set_global 0
|
||||
get_global 0
|
||||
i32.const 1024
|
||||
i32.gt_u
|
||||
if ;; label = @1
|
||||
unreachable
|
||||
end
|
||||
call 2
|
||||
get_global 0
|
||||
i32.const 2
|
||||
i32.sub
|
||||
set_global 0)
|
||||
(func (;4;) (type 1) (param i32)
|
||||
get_local 0
|
||||
get_global 0
|
||||
i32.const 2
|
||||
)
|
||||
(func (;3;) (type 1) (param i32)
|
||||
local.get 0
|
||||
global.get 0
|
||||
i32.const 4
|
||||
i32.add
|
||||
set_global 0
|
||||
get_global 0
|
||||
global.set 0
|
||||
global.get 0
|
||||
i32.const 1024
|
||||
i32.gt_u
|
||||
if ;; label = @1
|
||||
unreachable
|
||||
end
|
||||
call 1
|
||||
get_global 0
|
||||
i32.const 2
|
||||
global.get 0
|
||||
i32.const 4
|
||||
i32.sub
|
||||
set_global 0)
|
||||
(func (;5;) (type 2) (param i32 i32) (result i32)
|
||||
get_local 0
|
||||
get_local 1
|
||||
get_global 0
|
||||
i32.const 2
|
||||
global.set 0
|
||||
)
|
||||
(func (;4;) (type 2) (param i32 i32) (result i32)
|
||||
local.get 0
|
||||
local.get 1
|
||||
global.get 0
|
||||
i32.const 4
|
||||
i32.add
|
||||
set_global 0
|
||||
get_global 0
|
||||
global.set 0
|
||||
global.get 0
|
||||
i32.const 1024
|
||||
i32.gt_u
|
||||
if ;; label = @1
|
||||
unreachable
|
||||
end
|
||||
call 2
|
||||
get_global 0
|
||||
i32.const 2
|
||||
call $i32.add
|
||||
global.get 0
|
||||
i32.const 4
|
||||
i32.sub
|
||||
set_global 0)
|
||||
(table (;0;) 10 anyfunc)
|
||||
(global (;0;) (mut i32) (i32.const 0))
|
||||
(export "i32.add" (func 5))
|
||||
(elem (i32.const 0) 0 4 5))
|
||||
global.set 0
|
||||
)
|
||||
(table (;0;) 10 funcref)
|
||||
(global (;0;) (mut i32) i32.const 0)
|
||||
(export "i32.add" (func 4))
|
||||
(elem (;0;) (i32.const 0) func $foo 3 4)
|
||||
)
|
||||
Vendored
+27
@@ -0,0 +1,27 @@
|
||||
(module
|
||||
(func $fibonacci_with_break (result i32)
|
||||
(local $x i32) (local $y i32)
|
||||
|
||||
(block $unrolled_loop
|
||||
(set_local $x (i32.const 0))
|
||||
(set_local $y (i32.const 1))
|
||||
|
||||
get_local $x
|
||||
get_local $y
|
||||
tee_local $x
|
||||
i32.add
|
||||
set_local $y
|
||||
|
||||
i32.const 1
|
||||
br_if $unrolled_loop
|
||||
|
||||
get_local $x
|
||||
get_local $y
|
||||
tee_local $x
|
||||
i32.add
|
||||
set_local $y
|
||||
)
|
||||
|
||||
get_local $y
|
||||
)
|
||||
)
|
||||
Vendored
+19
@@ -0,0 +1,19 @@
|
||||
(module
|
||||
(func $add_locals (param $x i32) (param $y i32) (result i32)
|
||||
(local $t i32)
|
||||
|
||||
get_local $x
|
||||
get_local $y
|
||||
call $add
|
||||
set_local $t
|
||||
|
||||
get_local $t
|
||||
)
|
||||
|
||||
(func $add (param $x i32) (param $y i32) (result i32)
|
||||
(i32.add
|
||||
(get_local $x)
|
||||
(get_local $y)
|
||||
)
|
||||
)
|
||||
)
|
||||
Vendored
+9
@@ -0,0 +1,9 @@
|
||||
(module
|
||||
(func (param $x i32) (result i32)
|
||||
(if (result i32)
|
||||
(i32.const 1)
|
||||
(then (i32.add (get_local $x) (i32.const 1)))
|
||||
(else (i32.popcnt (get_local $x)))
|
||||
)
|
||||
)
|
||||
)
|
||||
Vendored
+5
-3
@@ -1,9 +1,11 @@
|
||||
(module
|
||||
(func (export "simple")
|
||||
(if (i32.const 1)
|
||||
(loop
|
||||
i32.const 123
|
||||
drop
|
||||
(then
|
||||
(loop
|
||||
i32.const 123
|
||||
drop
|
||||
)
|
||||
)
|
||||
)
|
||||
)
|
||||
|
||||
@@ -0,0 +1,8 @@
|
||||
(module
|
||||
(func (;0;)
|
||||
call 0
|
||||
)
|
||||
(func (;1;) (export "main")
|
||||
call 0
|
||||
)
|
||||
)
|
||||
+5
-5
@@ -6,17 +6,17 @@
|
||||
|
||||
(func $i32.add (export "i32.add") (param i32 i32) (result i32)
|
||||
get_local 0
|
||||
get_local 1
|
||||
i32.add
|
||||
get_local 1
|
||||
i32.add
|
||||
)
|
||||
(func (param $arg i32)
|
||||
(local $tmp i32)
|
||||
|
||||
get_global 0
|
||||
global.get 0
|
||||
i32.const 1
|
||||
i32.add
|
||||
tee_local $tmp
|
||||
set_global $counter
|
||||
tee_local $tmp
|
||||
global.set $counter
|
||||
|
||||
get_local $tmp
|
||||
get_local $arg
|
||||
|
||||
+10
@@ -0,0 +1,10 @@
|
||||
(module
|
||||
(func $one-group-many-locals
|
||||
(local i64) (local i64) (local i32)
|
||||
)
|
||||
(func $main
|
||||
(call
|
||||
$one-group-many-locals
|
||||
)
|
||||
)
|
||||
)
|
||||
+1
-1
@@ -3,7 +3,7 @@
|
||||
(import "env" "memory" (memory 1 1))
|
||||
|
||||
(start $start)
|
||||
(func $start (export "exported_start")
|
||||
(func $start
|
||||
(local i32)
|
||||
)
|
||||
(func (export "call")
|
||||
|
||||
+6
-6
@@ -1,15 +1,15 @@
|
||||
(module
|
||||
(import "env" "foo" (func $foo))
|
||||
(func (param i32)
|
||||
get_local 0
|
||||
i32.const 0
|
||||
call $i32.add
|
||||
drop
|
||||
get_local 0
|
||||
i32.const 0
|
||||
call $i32.add
|
||||
drop
|
||||
)
|
||||
(func $i32.add (export "i32.add") (param i32 i32) (result i32)
|
||||
get_local 0
|
||||
get_local 1
|
||||
i32.add
|
||||
get_local 1
|
||||
i32.add
|
||||
)
|
||||
(table 10 anyfunc)
|
||||
|
||||
|
||||
@@ -0,0 +1,73 @@
|
||||
use std::{
|
||||
fs::{read, read_dir},
|
||||
path::PathBuf,
|
||||
};
|
||||
use wasm_instrument::{
|
||||
gas_metering, inject_stack_limiter,
|
||||
parity_wasm::{deserialize_buffer, elements::Module, serialize},
|
||||
};
|
||||
|
||||
fn fixture_dir() -> PathBuf {
|
||||
let mut path = PathBuf::from(env!("CARGO_MANIFEST_DIR"));
|
||||
path.push("benches");
|
||||
path.push("fixtures");
|
||||
path
|
||||
}
|
||||
|
||||
/// Print the overhead of applying gas metering, stack height limiting or both.
|
||||
///
|
||||
/// Use `cargo test print_overhead -- --nocapture`.
|
||||
#[test]
|
||||
fn print_size_overhead() {
|
||||
let mut results: Vec<_> = read_dir(fixture_dir())
|
||||
.unwrap()
|
||||
.map(|entry| {
|
||||
let entry = entry.unwrap();
|
||||
let (orig_len, orig_module) = {
|
||||
let bytes = read(&entry.path()).unwrap();
|
||||
let len = bytes.len();
|
||||
let module: Module = deserialize_buffer(&bytes).unwrap();
|
||||
(len, module)
|
||||
};
|
||||
let (gas_metering_len, gas_module) = {
|
||||
let module = gas_metering::inject(
|
||||
orig_module.clone(),
|
||||
&gas_metering::ConstantCostRules::default(),
|
||||
"env",
|
||||
)
|
||||
.unwrap();
|
||||
let bytes = serialize(module.clone()).unwrap();
|
||||
let len = bytes.len();
|
||||
(len, module)
|
||||
};
|
||||
let stack_height_len = {
|
||||
let module = inject_stack_limiter(orig_module, 128).unwrap();
|
||||
let bytes = serialize(module).unwrap();
|
||||
bytes.len()
|
||||
};
|
||||
let both_len = {
|
||||
let module = inject_stack_limiter(gas_module, 128).unwrap();
|
||||
let bytes = serialize(module).unwrap();
|
||||
bytes.len()
|
||||
};
|
||||
|
||||
let overhead = both_len * 100 / orig_len;
|
||||
|
||||
(
|
||||
overhead,
|
||||
format!(
|
||||
"{:30}: orig = {:4} kb, gas_metering = {} %, stack_limiter = {} %, both = {} %",
|
||||
entry.file_name().to_str().unwrap(),
|
||||
orig_len / 1024,
|
||||
gas_metering_len * 100 / orig_len,
|
||||
stack_height_len * 100 / orig_len,
|
||||
overhead,
|
||||
),
|
||||
)
|
||||
})
|
||||
.collect();
|
||||
results.sort_unstable_by(|a, b| b.0.cmp(&a.0));
|
||||
for entry in results {
|
||||
println!("{}", entry.1);
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user