1 //! This example demonstrates: 2 //! - how to enable memory protection keys (MPK) in a Wasmtime embedding (see 3 //! [`build_engine`]) 4 //! - the expected memory compression from using MPK: it will probe the system 5 //! by creating larger and larger memory pools until system memory is 6 //! exhausted (see [`probe_engine_size`]). Then, it prints a comparison of the 7 //! memory used in both the MPK enabled and MPK disabled configurations. 8 //! 9 //! You can execute this example with: 10 //! 11 //! ```console 12 //! $ cargo run --example mpk 13 //! ``` 14 //! 15 //! Append `-- --help` for details about the configuring the memory size of the 16 //! pool. Also, to inspect interesting configuration values used for 17 //! constructing the pool, turn on logging: 18 //! 19 //! ```console 20 //! $ RUST_LOG=debug cargo run --example mpk -- --memory-size 512MiB 21 //! ``` 22 //! 23 //! Note that MPK support is limited to x86 Linux systems. OS limits on the 24 //! number of virtual memory areas (VMAs) can significantly restrict the total 25 //! number MPK-striped memory slots; each MPK-protected slot ends up using a new 26 //! VMA entry. On Linux, one can raise this limit: 27 //! 28 //! ```console 29 //! $ sysctl vm.max_map_count 30 //! 65530 31 //! $ sysctl vm.max_map_count=$LARGER_LIMIT 32 //! ``` 33 34 use anyhow::{anyhow, Result}; 35 use bytesize::ByteSize; 36 use clap::Parser; 37 use log::{info, warn}; 38 use std::str::FromStr; 39 use wasmtime::*; 40 41 fn main() -> Result<()> { 42 env_logger::init(); 43 let args = Args::parse(); 44 info!("{:?}", args); 45 46 let without_mpk = probe_engine_size(&args, MpkEnabled::Disable)?; 47 println!("without MPK:\t{}", without_mpk.to_string()); 48 49 if PoolingAllocationConfig::are_memory_protection_keys_available() { 50 let with_mpk = probe_engine_size(&args, MpkEnabled::Enable)?; 51 println!("with MPK:\t{}", with_mpk.to_string()); 52 println!( 53 "\t\t{}x more slots per reserved memory", 54 with_mpk.compare(&without_mpk) 55 ); 56 } else { 57 println!("with MPK:\tunavailable\t\tunavailable"); 58 } 59 60 Ok(()) 61 } 62 63 #[derive(Debug, Parser)] 64 #[command(author, version, about, long_about = None)] 65 struct Args { 66 /// The maximum number of bytes for each WebAssembly linear memory in the 67 /// pool. 68 #[arg(long, default_value = "128MiB", value_parser = parse_byte_size)] 69 memory_size: u64, 70 71 /// The maximum number of bytes a memory is considered static; see 72 /// `Config::static_memory_maximum_size` for more details and the default 73 /// value if unset. 74 #[arg(long, value_parser = parse_byte_size)] 75 static_memory_maximum_size: Option<u64>, 76 77 /// The size in bytes of the guard region to expect between static memory 78 /// slots; see [`Config::static_memory_guard_size`] for more details and the 79 /// default value if unset. 80 #[arg(long, value_parser = parse_byte_size)] 81 static_memory_guard_size: Option<u64>, 82 } 83 84 /// Parse a human-readable byte size--e.g., "512 MiB"--into the correct number 85 /// of bytes. 86 fn parse_byte_size(value: &str) -> Result<u64> { 87 let size = ByteSize::from_str(value).map_err(|e| anyhow!(e))?; 88 Ok(size.as_u64()) 89 } 90 91 /// Find the engine with the largest number of memories we can create on this 92 /// machine. 93 fn probe_engine_size(args: &Args, mpk: MpkEnabled) -> Result<Pool> { 94 let mut search = ExponentialSearch::new(); 95 let mut mapped_bytes = 0; 96 while !search.done() { 97 match build_engine(&args, search.next(), mpk) { 98 Ok(rb) => { 99 // TODO: assert!(rb >= mapped_bytes); 100 mapped_bytes = rb; 101 search.record(true) 102 } 103 Err(e) => { 104 warn!("failed engine allocation, continuing search: {:?}", e); 105 search.record(false) 106 } 107 } 108 } 109 Ok(Pool { 110 num_memories: search.next(), 111 mapped_bytes, 112 }) 113 } 114 115 #[derive(Debug)] 116 #[allow(dead_code)] 117 struct Pool { 118 num_memories: u32, 119 mapped_bytes: usize, 120 } 121 impl Pool { 122 /// Print a human-readable, tab-separated description of this structure. 123 fn to_string(&self) -> String { 124 let human_size = ByteSize::b(self.mapped_bytes as u64).to_string_as(true); 125 format!( 126 "{} memory slots\t{} reserved", 127 self.num_memories, human_size 128 ) 129 } 130 /// Return the number of times more memory slots in `self` than `other` 131 /// after normalizing by the mapped bytes sizes. Rounds to three decimal 132 /// places arbitrarily; no significance intended. 133 fn compare(&self, other: &Pool) -> f64 { 134 let size_ratio = other.mapped_bytes as f64 / self.mapped_bytes as f64; 135 let slots_ratio = self.num_memories as f64 / other.num_memories as f64; 136 let times_more_efficient = slots_ratio * size_ratio; 137 (times_more_efficient * 1000.0).round() / 1000.0 138 } 139 } 140 141 /// Exponentially increase the `next` value until the attempts fail, then 142 /// perform a binary search to find the maximum attempted value that still 143 /// succeeds. 144 #[derive(Debug)] 145 struct ExponentialSearch { 146 /// Determines if we are in the growth phase. 147 growing: bool, 148 /// The last successful value tried; this is the algorithm's lower bound. 149 last: u32, 150 /// The next value to try; this is the algorithm's upper bound. 151 next: u32, 152 } 153 impl ExponentialSearch { 154 fn new() -> Self { 155 Self { 156 growing: true, 157 last: 0, 158 next: 1, 159 } 160 } 161 fn next(&self) -> u32 { 162 self.next 163 } 164 fn record(&mut self, success: bool) { 165 if !success { 166 self.growing = false 167 } 168 let diff = if self.growing { 169 (self.next - self.last) * 2 170 } else { 171 (self.next - self.last + 1) / 2 172 }; 173 if success { 174 self.last = self.next; 175 self.next = self.next + diff; 176 } else { 177 self.next = self.next - diff; 178 } 179 } 180 fn done(&self) -> bool { 181 self.last == self.next 182 } 183 } 184 185 /// Build a pool-allocated engine with `num_memories` slots. 186 fn build_engine(args: &Args, num_memories: u32, enable_mpk: MpkEnabled) -> Result<usize> { 187 // Configure the memory pool. 188 let mut pool = PoolingAllocationConfig::default(); 189 let memory_pages = args.memory_size / u64::from(wasmtime_environ::WASM_PAGE_SIZE); 190 pool.memory_pages(memory_pages); 191 pool.total_memories(num_memories) 192 .memory_protection_keys(enable_mpk); 193 194 // Configure the engine itself. 195 let mut config = Config::new(); 196 if let Some(static_memory_maximum_size) = args.static_memory_maximum_size { 197 config.static_memory_maximum_size(static_memory_maximum_size); 198 } 199 if let Some(static_memory_guard_size) = args.static_memory_guard_size { 200 config.static_memory_guard_size(static_memory_guard_size); 201 } 202 config.allocation_strategy(InstanceAllocationStrategy::Pooling(pool)); 203 204 // Measure memory use before and after the engine is built. 205 let mapped_bytes_before = num_bytes_mapped()?; 206 let engine = Engine::new(&config)?; 207 let mapped_bytes_after = num_bytes_mapped()?; 208 209 // Ensure we actually use the engine somehow. 210 engine.increment_epoch(); 211 212 let mapped_bytes = mapped_bytes_after - mapped_bytes_before; 213 info!( 214 "{}-slot pool ({:?}): {} bytes mapped", 215 num_memories, enable_mpk, mapped_bytes 216 ); 217 Ok(mapped_bytes) 218 } 219 220 /// Add up the sizes of all the mapped virtual memory regions for the current 221 /// Linux process. 222 /// 223 /// This manually parses `/proc/self/maps` to avoid a rather-large `proc-maps` 224 /// dependency. We do expect this example to be Linux-specific anyways. For 225 /// reference, lines of that file look like: 226 /// 227 /// ```text 228 /// 5652d4418000-5652d441a000 r--p 00000000 00:23 84629427 /usr/bin/... 229 /// ``` 230 /// 231 /// We parse the start and end addresses: <start>-<end> [ignore the rest]. 232 #[cfg(target_os = "linux")] 233 fn num_bytes_mapped() -> Result<usize> { 234 use std::fs::File; 235 use std::io::{BufRead, BufReader}; 236 237 let file = File::open("/proc/self/maps")?; 238 let reader = BufReader::new(file); 239 let mut total = 0; 240 for line in reader.lines() { 241 let line = line?; 242 let range = line 243 .split_whitespace() 244 .next() 245 .ok_or(anyhow!("parse failure: expected whitespace"))?; 246 let mut addresses = range.split("-"); 247 let start = addresses 248 .next() 249 .ok_or(anyhow!("parse failure: expected dash-separated address"))?; 250 let start = usize::from_str_radix(start, 16)?; 251 let end = addresses 252 .next() 253 .ok_or(anyhow!("parse failure: expected dash-separated address"))?; 254 let end = usize::from_str_radix(end, 16)?; 255 256 total += end - start; 257 } 258 Ok(total) 259 } 260 261 #[cfg(not(target_os = "linux"))] 262 fn num_bytes_mapped() -> Result<usize> { 263 anyhow::bail!("this example can only read virtual memory maps on Linux") 264 } 265