1 //! This module implements serialization and deserialization of `Engine` 2 //! configuration data which is embedded into compiled artifacts of Wasmtime. 3 //! 4 //! The data serialized here is used to double-check that when a module is 5 //! loaded from one host onto another that it's compatible with the target host. 6 //! Additionally though this data is the first data read from a precompiled 7 //! artifact so it's "extra hardened" to provide reasonable-ish error messages 8 //! for mismatching wasmtime versions. Once something successfully deserializes 9 //! here it's assumed it's meant for this wasmtime so error messages are in 10 //! general much worse afterwards. 11 //! 12 //! Wasmtime AOT artifacts are ELF files so the data for the engine here is 13 //! stored into a section of the output file. The structure of this section is: 14 //! 15 //! 1. A version byte, currently `VERSION`. 16 //! 2. A byte indicating how long the next field is. 17 //! 3. A version string of the length of the previous byte value. 18 //! 4. A `bincode`-encoded `Metadata` structure. 19 //! 20 //! This is hoped to help distinguish easily Wasmtime-based ELF files from 21 //! other random ELF files, as well as provide better error messages for 22 //! using wasmtime artifacts across versions. 23 24 use crate::{Engine, ModuleVersionStrategy}; 25 use anyhow::{anyhow, bail, Context, Result}; 26 use object::write::{Object, StandardSegment}; 27 use object::{File, FileFlags, Object as _, ObjectSection, SectionKind}; 28 use serde::{Deserialize, Serialize}; 29 use std::collections::BTreeMap; 30 use std::str::FromStr; 31 use wasmtime_environ::obj; 32 use wasmtime_environ::{FlagValue, ObjectKind, Tunables}; 33 use wasmtime_runtime::MmapVec; 34 35 const VERSION: u8 = 0; 36 37 /// Produces a blob of bytes by serializing the `engine`'s configuration data to 38 /// be checked, perhaps in a different process, with the `check_compatible` 39 /// method below. 40 /// 41 /// The blob of bytes is inserted into the object file specified to become part 42 /// of the final compiled artifact. 43 #[cfg(compiler)] 44 pub fn append_compiler_info(engine: &Engine, obj: &mut Object<'_>) { 45 let section = obj.add_section( 46 obj.segment_name(StandardSegment::Data).to_vec(), 47 obj::ELF_WASM_ENGINE.as_bytes().to_vec(), 48 SectionKind::ReadOnlyData, 49 ); 50 let mut data = Vec::new(); 51 data.push(VERSION); 52 let version = match &engine.config().module_version { 53 ModuleVersionStrategy::WasmtimeVersion => env!("CARGO_PKG_VERSION"), 54 ModuleVersionStrategy::Custom(c) => c, 55 ModuleVersionStrategy::None => "", 56 }; 57 // This precondition is checked in Config::module_version: 58 assert!( 59 version.len() < 256, 60 "package version must be less than 256 bytes" 61 ); 62 data.push(version.len() as u8); 63 data.extend_from_slice(version.as_bytes()); 64 bincode::serialize_into(&mut data, &Metadata::new(engine)).unwrap(); 65 obj.set_section_data(section, data, 1); 66 } 67 68 /// Verifies that the serialized engine in `mmap` is compatible with the 69 /// `engine` provided. 70 /// 71 /// This function will verify that the `mmap` provided can be deserialized 72 /// successfully and that the contents are all compatible with the `engine` 73 /// provided here, notably compatible wasm features are enabled, compatible 74 /// compiler options, etc. If a mismatch is found and the compilation metadata 75 /// specified is incompatible then an error is returned. 76 pub fn check_compatible(engine: &Engine, mmap: &MmapVec, expected: ObjectKind) -> Result<()> { 77 // Parse the input `mmap` as an ELF file and see if the header matches the 78 // Wasmtime-generated header. This includes a Wasmtime-specific `os_abi` and 79 // the `e_flags` field should indicate whether `expected` matches or not. 80 // 81 // Note that errors generated here could mean that a precompiled module was 82 // loaded as a component, or vice versa, both of which aren't supposed to 83 // work. 84 // 85 // Ideally we'd only `File::parse` once and avoid the linear 86 // `section_by_name` search here but the general serialization code isn't 87 // structured well enough to make this easy and additionally it's not really 88 // a perf issue right now so doing that is left for another day's 89 // refactoring. 90 let obj = File::parse(&mmap[..]).context("failed to parse precompiled artifact as an ELF")?; 91 let expected_e_flags = match expected { 92 ObjectKind::Module => obj::EF_WASMTIME_MODULE, 93 ObjectKind::Component => obj::EF_WASMTIME_COMPONENT, 94 }; 95 match obj.flags() { 96 FileFlags::Elf { 97 os_abi: obj::ELFOSABI_WASMTIME, 98 abi_version: 0, 99 e_flags, 100 } if e_flags == expected_e_flags => {} 101 _ => bail!("incompatible object file format"), 102 } 103 104 let data = obj 105 .section_by_name(obj::ELF_WASM_ENGINE) 106 .ok_or_else(|| anyhow!("failed to find section `{}`", obj::ELF_WASM_ENGINE))? 107 .data()?; 108 let (first, data) = data 109 .split_first() 110 .ok_or_else(|| anyhow!("invalid engine section"))?; 111 if *first != VERSION { 112 bail!("mismatched version in engine section"); 113 } 114 let (len, data) = data 115 .split_first() 116 .ok_or_else(|| anyhow!("invalid engine section"))?; 117 let len = usize::from(*len); 118 let (version, data) = if data.len() < len + 1 { 119 bail!("engine section too small") 120 } else { 121 data.split_at(len) 122 }; 123 124 match &engine.config().module_version { 125 ModuleVersionStrategy::WasmtimeVersion => { 126 let version = std::str::from_utf8(version)?; 127 if version != env!("CARGO_PKG_VERSION") { 128 bail!( 129 "Module was compiled with incompatible Wasmtime version '{}'", 130 version 131 ); 132 } 133 } 134 ModuleVersionStrategy::Custom(v) => { 135 let version = std::str::from_utf8(&version)?; 136 if version != v { 137 bail!( 138 "Module was compiled with incompatible version '{}'", 139 version 140 ); 141 } 142 } 143 ModuleVersionStrategy::None => { /* ignore the version info, accept all */ } 144 } 145 bincode::deserialize::<Metadata>(data)?.check_compatible(engine) 146 } 147 148 #[derive(Serialize, Deserialize)] 149 struct Metadata { 150 target: String, 151 shared_flags: BTreeMap<String, FlagValue>, 152 isa_flags: BTreeMap<String, FlagValue>, 153 tunables: Tunables, 154 features: WasmFeatures, 155 } 156 157 // This exists because `wasmparser::WasmFeatures` isn't serializable 158 #[derive(Debug, Copy, Clone, Serialize, Deserialize)] 159 struct WasmFeatures { 160 reference_types: bool, 161 multi_value: bool, 162 bulk_memory: bool, 163 component_model: bool, 164 simd: bool, 165 threads: bool, 166 tail_call: bool, 167 multi_memory: bool, 168 exceptions: bool, 169 memory64: bool, 170 relaxed_simd: bool, 171 extended_const: bool, 172 } 173 174 impl Metadata { 175 #[cfg(compiler)] 176 fn new(engine: &Engine) -> Metadata { 177 let wasmparser::WasmFeatures { 178 reference_types, 179 multi_value, 180 bulk_memory, 181 component_model, 182 simd, 183 threads, 184 tail_call, 185 multi_memory, 186 exceptions, 187 memory64, 188 relaxed_simd, 189 extended_const, 190 191 // Always on; we don't currently have knobs for these. 192 mutable_global: _, 193 saturating_float_to_int: _, 194 sign_extension: _, 195 floats: _, 196 } = engine.config().features; 197 198 Metadata { 199 target: engine.compiler().triple().to_string(), 200 shared_flags: engine.compiler().flags(), 201 isa_flags: engine.compiler().isa_flags(), 202 tunables: engine.config().tunables.clone(), 203 features: WasmFeatures { 204 reference_types, 205 multi_value, 206 bulk_memory, 207 component_model, 208 simd, 209 threads, 210 tail_call, 211 multi_memory, 212 exceptions, 213 memory64, 214 relaxed_simd, 215 extended_const, 216 }, 217 } 218 } 219 220 fn check_compatible(mut self, engine: &Engine) -> Result<()> { 221 self.check_triple(engine)?; 222 self.check_shared_flags(engine)?; 223 self.check_isa_flags(engine)?; 224 self.check_tunables(&engine.config().tunables)?; 225 self.check_features(&engine.config().features)?; 226 Ok(()) 227 } 228 229 fn check_triple(&self, engine: &Engine) -> Result<()> { 230 let engine_target = engine.target(); 231 let module_target = 232 target_lexicon::Triple::from_str(&self.target).map_err(|e| anyhow!(e))?; 233 234 if module_target.architecture != engine_target.architecture { 235 bail!( 236 "Module was compiled for architecture '{}'", 237 module_target.architecture 238 ); 239 } 240 241 if module_target.operating_system != engine_target.operating_system { 242 bail!( 243 "Module was compiled for operating system '{}'", 244 module_target.operating_system 245 ); 246 } 247 248 Ok(()) 249 } 250 251 fn check_shared_flags(&mut self, engine: &Engine) -> Result<()> { 252 for (name, val) in self.shared_flags.iter() { 253 engine 254 .check_compatible_with_shared_flag(name, val) 255 .map_err(|s| anyhow::Error::msg(s)) 256 .context("compilation settings of module incompatible with native host")?; 257 } 258 Ok(()) 259 } 260 261 fn check_isa_flags(&mut self, engine: &Engine) -> Result<()> { 262 for (name, val) in self.isa_flags.iter() { 263 engine 264 .check_compatible_with_isa_flag(name, val) 265 .map_err(|s| anyhow::Error::msg(s)) 266 .context("compilation settings of module incompatible with native host")?; 267 } 268 Ok(()) 269 } 270 271 fn check_int<T: Eq + std::fmt::Display>(found: T, expected: T, feature: &str) -> Result<()> { 272 if found == expected { 273 return Ok(()); 274 } 275 276 bail!( 277 "Module was compiled with a {} of '{}' but '{}' is expected for the host", 278 feature, 279 found, 280 expected 281 ); 282 } 283 284 fn check_bool(found: bool, expected: bool, feature: &str) -> Result<()> { 285 if found == expected { 286 return Ok(()); 287 } 288 289 bail!( 290 "Module was compiled {} {} but it {} enabled for the host", 291 if found { "with" } else { "without" }, 292 feature, 293 if expected { "is" } else { "is not" } 294 ); 295 } 296 297 fn check_tunables(&mut self, other: &Tunables) -> Result<()> { 298 let Tunables { 299 static_memory_bound, 300 static_memory_offset_guard_size, 301 dynamic_memory_offset_guard_size, 302 generate_native_debuginfo, 303 parse_wasm_debuginfo, 304 consume_fuel, 305 epoch_interruption, 306 static_memory_bound_is_maximum, 307 guard_before_linear_memory, 308 309 // This doesn't affect compilation, it's just a runtime setting. 310 dynamic_memory_growth_reserve: _, 311 312 // This does technically affect compilation but modules with/without 313 // trap information can be loaded into engines with the opposite 314 // setting just fine (it's just a section in the compiled file and 315 // whether it's present or not) 316 generate_address_map: _, 317 318 // Just a debugging aid, doesn't affect functionality at all. 319 debug_adapter_modules: _, 320 } = self.tunables; 321 322 Self::check_int( 323 static_memory_bound, 324 other.static_memory_bound, 325 "static memory bound", 326 )?; 327 Self::check_int( 328 static_memory_offset_guard_size, 329 other.static_memory_offset_guard_size, 330 "static memory guard size", 331 )?; 332 Self::check_int( 333 dynamic_memory_offset_guard_size, 334 other.dynamic_memory_offset_guard_size, 335 "dynamic memory guard size", 336 )?; 337 Self::check_bool( 338 generate_native_debuginfo, 339 other.generate_native_debuginfo, 340 "debug information support", 341 )?; 342 Self::check_bool( 343 parse_wasm_debuginfo, 344 other.parse_wasm_debuginfo, 345 "WebAssembly backtrace support", 346 )?; 347 Self::check_bool(consume_fuel, other.consume_fuel, "fuel support")?; 348 Self::check_bool( 349 epoch_interruption, 350 other.epoch_interruption, 351 "epoch interruption", 352 )?; 353 Self::check_bool( 354 static_memory_bound_is_maximum, 355 other.static_memory_bound_is_maximum, 356 "pooling allocation support", 357 )?; 358 Self::check_bool( 359 guard_before_linear_memory, 360 other.guard_before_linear_memory, 361 "guard before linear memory", 362 )?; 363 364 Ok(()) 365 } 366 367 fn check_features(&mut self, other: &wasmparser::WasmFeatures) -> Result<()> { 368 let WasmFeatures { 369 reference_types, 370 multi_value, 371 bulk_memory, 372 component_model, 373 simd, 374 threads, 375 tail_call, 376 multi_memory, 377 exceptions, 378 memory64, 379 relaxed_simd, 380 extended_const, 381 } = self.features; 382 383 Self::check_bool( 384 reference_types, 385 other.reference_types, 386 "WebAssembly reference types support", 387 )?; 388 Self::check_bool( 389 multi_value, 390 other.multi_value, 391 "WebAssembly multi-value support", 392 )?; 393 Self::check_bool( 394 bulk_memory, 395 other.bulk_memory, 396 "WebAssembly bulk memory support", 397 )?; 398 Self::check_bool( 399 component_model, 400 other.component_model, 401 "WebAssembly component model support", 402 )?; 403 Self::check_bool(simd, other.simd, "WebAssembly SIMD support")?; 404 Self::check_bool(threads, other.threads, "WebAssembly threads support")?; 405 Self::check_bool(tail_call, other.tail_call, "WebAssembly tail-call support")?; 406 Self::check_bool( 407 multi_memory, 408 other.multi_memory, 409 "WebAssembly multi-memory support", 410 )?; 411 Self::check_bool( 412 exceptions, 413 other.exceptions, 414 "WebAssembly exceptions support", 415 )?; 416 Self::check_bool( 417 memory64, 418 other.memory64, 419 "WebAssembly 64-bit memory support", 420 )?; 421 Self::check_bool( 422 extended_const, 423 other.extended_const, 424 "WebAssembly extended-const support", 425 )?; 426 Self::check_bool( 427 relaxed_simd, 428 other.relaxed_simd, 429 "WebAssembly relaxed-simd support", 430 )?; 431 432 Ok(()) 433 } 434 } 435 436 #[cfg(test)] 437 mod test { 438 use super::*; 439 use crate::Config; 440 441 #[test] 442 fn test_architecture_mismatch() -> Result<()> { 443 let engine = Engine::default(); 444 let mut metadata = Metadata::new(&engine); 445 metadata.target = "unknown-generic-linux".to_string(); 446 447 match metadata.check_compatible(&engine) { 448 Ok(_) => unreachable!(), 449 Err(e) => assert_eq!( 450 e.to_string(), 451 "Module was compiled for architecture 'unknown'", 452 ), 453 } 454 455 Ok(()) 456 } 457 458 #[test] 459 fn test_os_mismatch() -> Result<()> { 460 let engine = Engine::default(); 461 let mut metadata = Metadata::new(&engine); 462 463 metadata.target = format!( 464 "{}-generic-unknown", 465 target_lexicon::Triple::host().architecture 466 ); 467 468 match metadata.check_compatible(&engine) { 469 Ok(_) => unreachable!(), 470 Err(e) => assert_eq!( 471 e.to_string(), 472 "Module was compiled for operating system 'unknown'", 473 ), 474 } 475 476 Ok(()) 477 } 478 479 #[test] 480 fn test_cranelift_flags_mismatch() -> Result<()> { 481 let engine = Engine::default(); 482 let mut metadata = Metadata::new(&engine); 483 484 metadata 485 .shared_flags 486 .insert("avoid_div_traps".to_string(), FlagValue::Bool(false)); 487 488 match metadata.check_compatible(&engine) { 489 Ok(_) => unreachable!(), 490 Err(e) => assert!(format!("{:?}", e).starts_with( 491 "\ 492 compilation settings of module incompatible with native host 493 494 Caused by: 495 setting \"avoid_div_traps\" is configured to Bool(false) which is not supported" 496 )), 497 } 498 499 Ok(()) 500 } 501 502 #[test] 503 fn test_isa_flags_mismatch() -> Result<()> { 504 let engine = Engine::default(); 505 let mut metadata = Metadata::new(&engine); 506 507 metadata 508 .isa_flags 509 .insert("not_a_flag".to_string(), FlagValue::Bool(true)); 510 511 match metadata.check_compatible(&engine) { 512 Ok(_) => unreachable!(), 513 Err(e) => assert!(format!("{:?}", e).starts_with( 514 "\ 515 compilation settings of module incompatible with native host 516 517 Caused by: 518 cannot test if target-specific flag \"not_a_flag\" is available at runtime", 519 )), 520 } 521 522 Ok(()) 523 } 524 525 #[test] 526 fn test_tunables_int_mismatch() -> Result<()> { 527 let engine = Engine::default(); 528 let mut metadata = Metadata::new(&engine); 529 530 metadata.tunables.static_memory_offset_guard_size = 0; 531 532 match metadata.check_compatible(&engine) { 533 Ok(_) => unreachable!(), 534 Err(e) => assert_eq!(e.to_string(), "Module was compiled with a static memory guard size of '0' but '2147483648' is expected for the host"), 535 } 536 537 Ok(()) 538 } 539 540 #[test] 541 fn test_tunables_bool_mismatch() -> Result<()> { 542 let mut config = Config::new(); 543 config.epoch_interruption(true); 544 545 let engine = Engine::new(&config)?; 546 let mut metadata = Metadata::new(&engine); 547 metadata.tunables.epoch_interruption = false; 548 549 match metadata.check_compatible(&engine) { 550 Ok(_) => unreachable!(), 551 Err(e) => assert_eq!( 552 e.to_string(), 553 "Module was compiled without epoch interruption but it is enabled for the host" 554 ), 555 } 556 557 let mut config = Config::new(); 558 config.epoch_interruption(false); 559 560 let engine = Engine::new(&config)?; 561 let mut metadata = Metadata::new(&engine); 562 metadata.tunables.epoch_interruption = true; 563 564 match metadata.check_compatible(&engine) { 565 Ok(_) => unreachable!(), 566 Err(e) => assert_eq!( 567 e.to_string(), 568 "Module was compiled with epoch interruption but it is not enabled for the host" 569 ), 570 } 571 572 Ok(()) 573 } 574 575 #[test] 576 fn test_feature_mismatch() -> Result<()> { 577 let mut config = Config::new(); 578 config.wasm_simd(true); 579 580 let engine = Engine::new(&config)?; 581 let mut metadata = Metadata::new(&engine); 582 metadata.features.simd = false; 583 584 match metadata.check_compatible(&engine) { 585 Ok(_) => unreachable!(), 586 Err(e) => assert_eq!(e.to_string(), "Module was compiled without WebAssembly SIMD support but it is enabled for the host"), 587 } 588 589 let mut config = Config::new(); 590 config.wasm_simd(false); 591 592 let engine = Engine::new(&config)?; 593 let mut metadata = Metadata::new(&engine); 594 metadata.features.simd = true; 595 596 match metadata.check_compatible(&engine) { 597 Ok(_) => unreachable!(), 598 Err(e) => assert_eq!(e.to_string(), "Module was compiled with WebAssembly SIMD support but it is not enabled for the host"), 599 } 600 601 Ok(()) 602 } 603 } 604