├── .github └── dependabot.yml ├── .gitignore ├── Cargo.toml ├── README.md └── src ├── canary.rs ├── config.rs ├── ctx_data.rs ├── imports.rs └── main.rs /.github/dependabot.yml: -------------------------------------------------------------------------------- 1 | version: 2 2 | updates: 3 | - package-ecosystem: cargo 4 | directory: "/" 5 | schedule: 6 | interval: daily 7 | time: "04:00" 8 | open-pull-requests-limit: 10 9 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | /target 2 | **/*.rs.bk -------------------------------------------------------------------------------- /Cargo.toml: -------------------------------------------------------------------------------- 1 | [package] 2 | name = "was" 3 | version = "0.1.0" 4 | authors = ["Frank Denis "] 5 | edition = "2018" 6 | 7 | [dependencies] 8 | libc = "0.2" 9 | structopt = "0.3" 10 | wasmer-runtime = "0.1" 11 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # WAS (not WASM) 2 | 3 | A hostile memory allocator to make WebAssembly applications more 4 | predictable. 5 | 6 | ## Blurb 7 | 8 | The WebAssembly memory model 9 | [doesn't offer any protection](https://00f.net/2018/11/25/webassembly-doesnt-make-unsafe-languages-safe/) 10 | against buffer underflows/overflows. 11 | 12 | As long as accesses are made within the bounds of the linear memory 13 | segments, no page faults will ever occur. 14 | 15 | Besides facilitating heartbleed-class vulnerabilities, this memory 16 | model is also painful for application developers. 17 | 18 | Where a native application may crash in the same context, 19 | out-of-bounds accesses in a WebAssembly application may cause silent 20 | memory corruption and subtle, tedious-to-debug bugs. 21 | 22 | WAS (not WASM) is a simple memory allocator designed to catch memory 23 | issues in WebAssembly compilers and applications. 24 | 25 | WAS (not WASM) makes the heap inaccessible, except for pages 26 | explicitly allocated by the application. 27 | 28 | WAS (not WASM) makes static data read-only. Writing to a `NULL` 29 | pointer will fault. 30 | 31 | WAS (not WASM) never reuses allocated pages after they are `free()`'d. 32 | Deallocated pages become inaccessible. 33 | 34 | WAS (not WASM) fills newly allocated regions with junk. 35 | 36 | WAS (not WASM) ensures that a guard page immediately follows every 37 | single allocation, so that a single-byte overflow will cause a fault. 38 | 39 | WAS (not WASM) inserts a canary in partially allocated pages, and 40 | verifies that it hasn't been tampered with in order to detected underflows. 41 | 42 | WAS (not WASM) detects double-free(), use-after-free(), invalid free(). 43 | 44 | WAS (not WASM) keeps track of the number of allocations, deallocations 45 | and total memory usage, so you can scream at how many of these 46 | WebAssembly applications do, and optimize yours accordingly. 47 | 48 | WAS (not WASM) is not designed to be fast. It is designed to help you 49 | develop safer applications. Or eventually faster applications, by using 50 | unsafe constructions with more confidence. 51 | 52 | WAS (not WASM) runs WASM code using 53 | [Cranelift](https://github.com/CraneStation/cranelift) 54 | and [Wasmer](https://wasmer.io/). 55 | 56 | ## Installation 57 | 58 | Install Rust, and use `cargo`: 59 | 60 | ```sh 61 | cargo install 62 | ``` 63 | 64 | ## Usage 65 | 66 | ```text 67 | USAGE: 68 | was [FLAGS] [OPTIONS] --file 69 | 70 | FLAGS: 71 | -c, --canary-check-on-alloc 72 | -h, --help Prints help information 73 | -V, --version Prints version information 74 | 75 | OPTIONS: 76 | -e, --entrypoint [default: main] 77 | -f, --file 78 | -b, --heap-base [default: 65536] 79 | ``` 80 | 81 | Example: 82 | 83 | ```sh 84 | was -f app.wasm 85 | ``` 86 | 87 | The `--canary-check-on-alloc` option checks every single canary before 88 | every single allocation. This is slow, and will get slower as the 89 | number of allocation grows. 90 | 91 | The `--heap-base` option sets how much data is already present on the 92 | heap before dynamic allocations are performed. This is typically used 93 | to store static data. When using AssemblyScript, the optimal value for 94 | the heap base is stored in the `HEAP_BASE` global. 95 | 96 | ## Usage with AssemblyScript 97 | 98 | WAS (not WASM) was originally made to work with [AssemblyScript](https://assemblyscript.org). 99 | 100 | In order to do so, use the `system` allocator: 101 | 102 | ```typescript 103 | import 'allocator/system'; 104 | ``` 105 | 106 | Optionally, in order to check canaries when the application 107 | terminates, call the `terminate()` function in your `index.ts` file: 108 | 109 | ```typescript 110 | declare function terminate(): void; 111 | 112 | @global export function main(): void { 113 | ... 114 | terminate(); 115 | } 116 | ``` 117 | 118 | AssemblyScript stores static data at the beginning of the heap. The 119 | heap base after this static data is stored in the `HEAP_BASE` global. 120 | 121 | A quick way to print it while using WAS (not WASM) is to temporarily 122 | add this to your application: 123 | 124 | ```typescript 125 | declare function debug_val(val: u32): void; 126 | 127 | debug_val(HEAP_BASE); 128 | ``` 129 | -------------------------------------------------------------------------------- /src/canary.rs: -------------------------------------------------------------------------------- 1 | use super::ctx_data::*; 2 | use std::mem; 3 | use wasmer_runtime::Ctx; 4 | 5 | pub fn canary_check(allocation: &Allocation, ctx: &Ctx) { 6 | let ctx_data = unsafe { Box::from_raw(ctx.data as *mut CtxData) }; 7 | let heap_ptr = &ctx.memory(0)[allocation.offset as usize..].as_ptr(); 8 | let canary = ctx_data.canary; 9 | for offset in 0..(allocation.rounded_size - allocation.size) { 10 | if unsafe { *heap_ptr.offset(offset as isize) } != canary { 11 | panic!( 12 | "Corruption detected at offset {} (base: {})", 13 | offset, allocation.offset 14 | ); 15 | } 16 | } 17 | mem::forget(ctx_data); 18 | } 19 | 20 | pub fn canaries_check(ctx: &Ctx) { 21 | let ctx_data = unsafe { Box::from_raw(ctx.data as *mut CtxData) }; 22 | for allocation in ctx_data.allocations.values() { 23 | canary_check(allocation, ctx); 24 | } 25 | mem::forget(ctx_data); 26 | } 27 | -------------------------------------------------------------------------------- /src/config.rs: -------------------------------------------------------------------------------- 1 | use std::cell::RefCell; 2 | use std::path::PathBuf; 3 | use structopt::StructOpt; 4 | 5 | #[derive(Default, Debug, Copy, Clone)] 6 | pub struct RuntimeConfig { 7 | pub heap_base: u32, 8 | pub canary_check_on_alloc: bool, 9 | } 10 | 11 | thread_local! { 12 | static RUNTIME_CONFIG: RefCell = RefCell::new(RuntimeConfig::default()) 13 | } 14 | 15 | impl RuntimeConfig { 16 | pub fn current() -> Self { 17 | RUNTIME_CONFIG.with(|runtime_config| *runtime_config.borrow()) 18 | } 19 | } 20 | 21 | #[derive(StructOpt, Debug)] 22 | #[structopt(name = "WAS (not WASM)")] 23 | pub struct Config { 24 | #[structopt(short = "f", long = "file", parse(from_os_str))] 25 | pub file: PathBuf, 26 | 27 | #[structopt(short = "b", long = "heap-base", default_value = "65536")] 28 | pub heap_base: u32, 29 | 30 | #[structopt(short = "c", long = "canary-check-on-alloc")] 31 | pub canary_check_on_alloc: bool, 32 | 33 | #[structopt(short = "e", long = "entrypoint", default_value = "main")] 34 | pub entrypoint: String, 35 | } 36 | 37 | impl Config { 38 | pub fn parse() -> Self { 39 | let config = Config::from_args(); 40 | RUNTIME_CONFIG.with(|runtime_config| { 41 | *runtime_config.borrow_mut() = RuntimeConfig { 42 | heap_base: config.heap_base, 43 | canary_check_on_alloc: config.canary_check_on_alloc, 44 | }; 45 | }); 46 | config 47 | } 48 | } 49 | -------------------------------------------------------------------------------- /src/ctx_data.rs: -------------------------------------------------------------------------------- 1 | use super::config::*; 2 | use std::collections::HashMap; 3 | use wasmer_runtime::Ctx; 4 | 5 | #[derive(Debug)] 6 | pub struct Allocation { 7 | pub offset: u32, 8 | pub start: u32, 9 | pub size: u32, 10 | pub rounded_size: u32, 11 | } 12 | 13 | #[derive(Debug)] 14 | pub struct CtxData { 15 | pub heap_offset: u32, 16 | pub page_size: u32, 17 | pub allocations: HashMap, 18 | pub canary: u8, 19 | pub junk: u8, 20 | pub canary_check_on_alloc: bool, 21 | pub alloc_count: u64, 22 | pub free_count: u64, 23 | pub alloc_total_usage: u64, 24 | } 25 | 26 | impl CtxData { 27 | pub fn new(runtime_config: RuntimeConfig) -> Self { 28 | let page_size = unsafe { libc::sysconf(libc::_SC_PAGESIZE) } as u32; 29 | let page_mask = page_size - 1; 30 | let heap_offset = (runtime_config.heap_base + page_mask) & !page_mask; 31 | let allocations = HashMap::new(); 32 | let canary = 0xdf; 33 | let junk = 0xdb; 34 | CtxData { 35 | page_size, 36 | heap_offset, 37 | allocations, 38 | canary, 39 | junk, 40 | canary_check_on_alloc: runtime_config.canary_check_on_alloc, 41 | alloc_count: 0, 42 | free_count: 0, 43 | alloc_total_usage: 0, 44 | } 45 | } 46 | } 47 | 48 | pub fn get_or_create_ctx_data(ctx: &mut Ctx) -> Box { 49 | if ctx.data.is_null() { 50 | let ctx_data = Box::new(CtxData::new(RuntimeConfig::current())); 51 | ctx.data = Box::into_raw(ctx_data) as *mut _; 52 | } 53 | unsafe { Box::from_raw(ctx.data as *mut CtxData) } 54 | } 55 | -------------------------------------------------------------------------------- /src/imports.rs: -------------------------------------------------------------------------------- 1 | use super::canary::*; 2 | use super::ctx_data::*; 3 | use std::{mem, ptr}; 4 | use wasmer_runtime::{imports, Ctx, ImportObject}; 5 | 6 | pub extern "C" fn debug_val(val: u32, _ctx: &mut Ctx) { 7 | println!("Debug: [{}]", val); 8 | } 9 | 10 | pub extern "C" fn abort(_msg: u32, _file: u32, _line: u32, _col: u32, _ctx: &mut Ctx) { 11 | panic!("abort()"); 12 | } 13 | 14 | pub extern "C" fn malloc(size: u32, ctx: &mut Ctx) -> u32 { 15 | let mut ctx_data = get_or_create_ctx_data(ctx); 16 | if ctx_data.canary_check_on_alloc { 17 | canaries_check(ctx); 18 | } 19 | let offset = ctx_data.heap_offset; 20 | let page_size = ctx_data.page_size; 21 | let page_mask = page_size - 1; 22 | let rounded_size = (size + page_mask) & !page_mask; 23 | let end = offset + rounded_size; 24 | let start = end - size; 25 | let heap_ptr = ctx.memory_mut(0).as_mut_ptr(); 26 | unsafe { 27 | libc::mprotect( 28 | heap_ptr.offset(offset as isize) as *mut _, 29 | rounded_size as usize, 30 | libc::PROT_READ | libc::PROT_WRITE, 31 | ); 32 | ptr::write_bytes( 33 | heap_ptr.offset(start as isize), 34 | ctx_data.junk, 35 | size as usize, 36 | ); 37 | } 38 | if offset != start { 39 | unsafe { 40 | ptr::write_bytes( 41 | heap_ptr.offset(offset as isize), 42 | ctx_data.canary, 43 | (rounded_size - size) as usize, 44 | ) 45 | }; 46 | } 47 | ctx_data.allocations.insert( 48 | start, 49 | Allocation { 50 | offset, 51 | start, 52 | size, 53 | rounded_size, 54 | }, 55 | ); 56 | ctx_data.heap_offset = end + page_size; 57 | ctx_data.alloc_count += 1; 58 | ctx_data.alloc_total_usage += u64::from(size); 59 | mem::forget(ctx_data); 60 | start 61 | } 62 | 63 | pub extern "C" fn free(start: u32, ctx: &mut Ctx) { 64 | if start == 0 { 65 | return; 66 | } 67 | let mut ctx_data = unsafe { Box::from_raw(ctx.data as *mut CtxData) }; 68 | let allocation = match ctx_data.allocations.get(&start) { 69 | None => panic!("free()ing invalid offset {}", start), 70 | Some(allocation) => allocation, 71 | }; 72 | canary_check(&allocation, ctx); 73 | let heap_ptr = ctx.memory(0).as_ptr(); 74 | unsafe { 75 | libc::mprotect( 76 | heap_ptr.offset(allocation.offset as isize) as *mut _, 77 | allocation.rounded_size as usize, 78 | libc::PROT_NONE, 79 | ) 80 | }; 81 | ctx_data.allocations.remove(&start); 82 | ctx_data.free_count += 1; 83 | if ctx_data.free_count > ctx_data.alloc_count { 84 | panic!("free()ing unallocated memory"); 85 | } 86 | mem::forget(ctx_data); 87 | } 88 | 89 | pub extern "C" fn terminate(ctx: &mut Ctx) { 90 | canaries_check(&ctx); 91 | let ctx_data = unsafe { Box::from_raw(ctx.data as *mut CtxData) }; 92 | let leaked = ctx_data.alloc_count - ctx_data.free_count; 93 | eprintln!("Allocations: {}", ctx_data.alloc_count); 94 | eprintln!("Leaked: {}", leaked); 95 | eprintln!("Memory usage: {} bytes", ctx_data.alloc_total_usage); 96 | mem::forget(ctx_data); 97 | let heap = ctx.memory_mut(0); 98 | unsafe { 99 | libc::mprotect( 100 | heap.as_mut_ptr().offset(0) as *mut _, 101 | heap.len(), 102 | libc::PROT_READ | libc::PROT_WRITE, 103 | ); 104 | } 105 | } 106 | 107 | pub fn import_object() -> ImportObject { 108 | imports! { 109 | "index" => { 110 | "debug_val" => debug_val<[u32] -> []>, 111 | "terminate" => terminate<[] -> []>, 112 | }, 113 | "env" => { 114 | "abort" => abort<[u32, u32, u32, u32] -> []>, 115 | }, 116 | "system" => { 117 | "malloc" => malloc<[u32] -> [u32]>, 118 | "free" => free<[u32] -> []>, 119 | }, 120 | } 121 | } 122 | -------------------------------------------------------------------------------- /src/main.rs: -------------------------------------------------------------------------------- 1 | extern crate structopt; 2 | 3 | mod canary; 4 | mod config; 5 | mod ctx_data; 6 | mod imports; 7 | 8 | use config::*; 9 | use ctx_data::*; 10 | use imports::*; 11 | use libc; 12 | use std::fs::File; 13 | use std::io::{self, prelude::*}; 14 | use wasmer_runtime::instantiate; 15 | 16 | fn main() -> Result<(), io::Error> { 17 | let config = Config::parse(); 18 | 19 | let mut wasm = vec![]; 20 | File::open(config.file)?.read_to_end(&mut wasm)?; 21 | let mut instance = instantiate(&wasm, import_object()).map_err(|e| { 22 | io::Error::new( 23 | io::ErrorKind::InvalidData, 24 | format!("Unable to instantiate the module: {:?}", e), 25 | ) 26 | })?; 27 | let ctx = instance.context_mut(); 28 | let mut ctx_data = get_or_create_ctx_data(ctx); 29 | let heap = ctx.memory_mut(0); 30 | unsafe { 31 | libc::mprotect( 32 | heap.as_mut_ptr().offset(0) as *mut _, 33 | ctx_data.heap_offset as usize, 34 | libc::PROT_READ, 35 | ); 36 | libc::mprotect( 37 | heap.as_mut_ptr().offset(ctx_data.heap_offset as isize) as *mut _, 38 | heap.len() - ctx_data.heap_offset as usize, 39 | libc::PROT_NONE, 40 | ); 41 | } 42 | ctx_data.heap_offset += ctx_data.page_size; 43 | instance.call(&config.entrypoint, &[]).map_err(|e| { 44 | io::Error::new( 45 | io::ErrorKind::InvalidData, 46 | format!("Unable to run the webassembly code: {:?}", e), 47 | ) 48 | })?; 49 | Ok(()) 50 | } 51 | --------------------------------------------------------------------------------