use crate::prelude::*;
use crate::{Engine, ModuleVersionStrategy, Precompiled};
use core::fmt;
use core::str::FromStr;
use object::endian::Endianness;
#[cfg(any(feature = "cranelift", feature = "winch"))]
use object::write::{Object, StandardSegment};
use object::{FileFlags, Object as _, ObjectSection, read::elf::ElfFile64};
use serde_derive::{Deserialize, Serialize};
use wasmtime_environ::obj;
use wasmtime_environ::{FlagValue, ObjectKind, Tunables};
const VERSION: u8 = 0;
pub fn check_compatible(engine: &Engine, mmap: &[u8], expected: ObjectKind) -> Result<()> {
let obj = ElfFile64::<Endianness>::parse(mmap)
.map_err(obj::ObjectCrateErrorWrapper)
.context("failed to parse precompiled artifact as an ELF")?;
let expected_e_flags = match expected {
ObjectKind::Module => obj::EF_WASMTIME_MODULE,
ObjectKind::Component => obj::EF_WASMTIME_COMPONENT,
};
match obj.flags() {
FileFlags::Elf {
os_abi: obj::ELFOSABI_WASMTIME,
abi_version: 0,
e_flags,
} if e_flags & expected_e_flags == expected_e_flags => {}
_ => bail!("incompatible object file format"),
}
let data = obj
.section_by_name(obj::ELF_WASM_ENGINE)
.ok_or_else(|| format_err!("failed to find section `{}`", obj::ELF_WASM_ENGINE))?
.data()
.map_err(obj::ObjectCrateErrorWrapper)?;
let (first, data) = data
.split_first()
.ok_or_else(|| format_err!("invalid engine section"))?;
if *first != VERSION {
bail!("mismatched version in engine section");
}
let (len, data) = data
.split_first()
.ok_or_else(|| format_err!("invalid engine section"))?;
let len = usize::from(*len);
let (version, data) = if data.len() < len + 1 {
bail!("engine section too small")
} else {
data.split_at(len)
};
match &engine.config().module_version {
ModuleVersionStrategy::None => { }
_ => {
let version = core::str::from_utf8(&version)?;
if version != engine.config().module_version.as_str() {
bail!("Module was compiled with incompatible version '{version}'");
}
}
}
postcard::from_bytes::<Metadata<'_>>(data)?.check_compatible(engine)
}
#[cfg(any(feature = "cranelift", feature = "winch"))]
pub fn append_compiler_info(engine: &Engine, obj: &mut Object<'_>, metadata: &Metadata<'_>) {
let section = obj.add_section(
obj.segment_name(StandardSegment::Data).to_vec(),
obj::ELF_WASM_ENGINE.as_bytes().to_vec(),
object::SectionKind::ReadOnlyData,
);
let mut data = Vec::new();
data.push(VERSION);
let version = engine.config().module_version.as_str();
assert!(
version.len() < 256,
"package version must be less than 256 bytes"
);
data.push(version.len() as u8);
data.extend_from_slice(version.as_bytes());
data.extend(postcard::to_allocvec(metadata).unwrap());
obj.set_section_data(section, data, 1);
}
fn detect_precompiled<'data, R: object::ReadRef<'data>>(
obj: ElfFile64<'data, Endianness, R>,
) -> Option<Precompiled> {
match obj.flags() {
FileFlags::Elf {
os_abi: obj::ELFOSABI_WASMTIME,
abi_version: 0,
e_flags,
} if e_flags & obj::EF_WASMTIME_MODULE != 0 => Some(Precompiled::Module),
FileFlags::Elf {
os_abi: obj::ELFOSABI_WASMTIME,
abi_version: 0,
e_flags,
} if e_flags & obj::EF_WASMTIME_COMPONENT != 0 => Some(Precompiled::Component),
_ => None,
}
}
pub fn detect_precompiled_bytes(bytes: &[u8]) -> Option<Precompiled> {
detect_precompiled(ElfFile64::parse(bytes).ok()?)
}
#[cfg(feature = "std")]
pub fn detect_precompiled_file(path: impl AsRef<std::path::Path>) -> Result<Option<Precompiled>> {
let read_cache = object::ReadCache::new(std::fs::File::open(path)?);
let obj = ElfFile64::parse(&read_cache)?;
Ok(detect_precompiled(obj))
}
#[derive(Serialize, Deserialize)]
pub struct Metadata<'a> {
target: String,
#[serde(borrow)]
shared_flags: Vec<(&'a str, FlagValue<'a>)>,
#[serde(borrow)]
isa_flags: Vec<(&'a str, FlagValue<'a>)>,
tunables: Tunables,
features: u64,
}
impl Metadata<'_> {
#[cfg(any(feature = "cranelift", feature = "winch"))]
pub fn new(engine: &Engine) -> Result<Metadata<'static>> {
let compiler = engine.try_compiler()?;
Ok(Metadata {
target: compiler.triple().to_string(),
shared_flags: compiler.flags(),
isa_flags: compiler.isa_flags(),
tunables: engine.tunables().clone(),
features: engine.features().bits(),
})
}
fn check_compatible(mut self, engine: &Engine) -> Result<()> {
self.check_triple(engine)?;
self.check_shared_flags(engine)?;
self.check_isa_flags(engine)?;
self.check_tunables(&engine.tunables())?;
self.check_features(&engine.features())?;
Ok(())
}
fn check_triple(&self, engine: &Engine) -> Result<()> {
let engine_target = engine.target();
let module_target =
target_lexicon::Triple::from_str(&self.target).map_err(|e| format_err!(e))?;
if module_target.architecture != engine_target.architecture {
bail!(
"Module was compiled for architecture '{}'",
module_target.architecture
);
}
if module_target.operating_system != engine_target.operating_system {
bail!(
"Module was compiled for operating system '{}'",
module_target.operating_system
);
}
Ok(())
}
fn check_shared_flags(&mut self, engine: &Engine) -> Result<()> {
for (name, val) in self.shared_flags.iter() {
engine
.check_compatible_with_shared_flag(name, val)
.map_err(|s| crate::Error::msg(s))
.context("compilation settings of module incompatible with native host")?;
}
Ok(())
}
fn check_isa_flags(&mut self, engine: &Engine) -> Result<()> {
for (name, val) in self.isa_flags.iter() {
engine
.check_compatible_with_isa_flag(name, val)
.map_err(|s| crate::Error::msg(s))
.context("compilation settings of module incompatible with native host")?;
}
Ok(())
}
fn check_int<T: Eq + fmt::Display>(found: T, expected: T, feature: &str) -> Result<()> {
if found == expected {
return Ok(());
}
bail!(
"Module was compiled with a {feature} of '{found}' but '{expected}' is expected for the host"
);
}
fn check_bool(found: bool, expected: bool, feature: impl fmt::Display) -> Result<()> {
if found == expected {
return Ok(());
}
bail!(
"Module was compiled {} {} but it {} enabled for the host",
if found { "with" } else { "without" },
feature,
if expected { "is" } else { "is not" }
);
}
fn check_tunables(&mut self, other: &Tunables) -> Result<()> {
let Tunables {
collector,
memory_reservation,
memory_guard_size,
debug_native,
debug_guest,
parse_wasm_debuginfo,
consume_fuel,
epoch_interruption,
memory_may_move,
guard_before_linear_memory,
table_lazy_init,
relaxed_simd_deterministic,
winch_callable,
signals_based_traps,
memory_init_cow,
inlining,
inlining_intra_module,
inlining_small_callee_size,
inlining_sum_size_threshold,
concurrency_support,
memory_reservation_for_growth: _,
generate_address_map: _,
debug_adapter_modules: _,
} = self.tunables;
Self::check_collector(collector, other.collector)?;
Self::check_int(
memory_reservation,
other.memory_reservation,
"memory reservation",
)?;
Self::check_int(
memory_guard_size,
other.memory_guard_size,
"memory guard size",
)?;
Self::check_bool(
debug_native,
other.debug_native,
"native debug information support",
)?;
Self::check_bool(debug_guest, other.debug_guest, "guest debug")?;
Self::check_bool(
parse_wasm_debuginfo,
other.parse_wasm_debuginfo,
"WebAssembly backtrace support",
)?;
Self::check_bool(consume_fuel, other.consume_fuel, "fuel support")?;
Self::check_bool(
epoch_interruption,
other.epoch_interruption,
"epoch interruption",
)?;
Self::check_bool(memory_may_move, other.memory_may_move, "memory may move")?;
Self::check_bool(
guard_before_linear_memory,
other.guard_before_linear_memory,
"guard before linear memory",
)?;
Self::check_bool(table_lazy_init, other.table_lazy_init, "table lazy init")?;
Self::check_bool(
relaxed_simd_deterministic,
other.relaxed_simd_deterministic,
"relaxed simd deterministic semantics",
)?;
Self::check_bool(
winch_callable,
other.winch_callable,
"Winch calling convention",
)?;
Self::check_bool(
signals_based_traps,
other.signals_based_traps,
"Signals-based traps",
)?;
Self::check_bool(
memory_init_cow,
other.memory_init_cow,
"memory initialization with CoW",
)?;
Self::check_bool(inlining, other.inlining, "function inlining")?;
Self::check_int(
inlining_small_callee_size,
other.inlining_small_callee_size,
"function inlining small-callee size",
)?;
Self::check_int(
inlining_sum_size_threshold,
other.inlining_sum_size_threshold,
"function inlining sum-size threshold",
)?;
Self::check_bool(
concurrency_support,
other.concurrency_support,
"concurrency support",
)?;
Self::check_intra_module_inlining(inlining_intra_module, other.inlining_intra_module)?;
Ok(())
}
fn check_features(&mut self, other: &wasmparser::WasmFeatures) -> Result<()> {
let module_features = wasmparser::WasmFeatures::from_bits_truncate(self.features);
let missing_features = (*other & module_features) ^ module_features;
for (name, _) in missing_features.iter_names() {
let name = name.to_ascii_lowercase();
bail!(
"Module was compiled with support for WebAssembly feature \
`{name}` but it is not enabled for the host",
);
}
Ok(())
}
fn check_collector(
module: Option<wasmtime_environ::Collector>,
host: Option<wasmtime_environ::Collector>,
) -> Result<()> {
match (module, host) {
(None, _) => Ok(()),
(Some(module), Some(host)) if module == host => Ok(()),
(Some(_), None) => {
bail!("module was compiled with GC however GC is disabled in the host")
}
(Some(module), Some(host)) => {
bail!(
"module was compiled for the {module} collector but \
the host is configured to use the {host} collector",
)
}
}
}
fn check_intra_module_inlining(
module: wasmtime_environ::IntraModuleInlining,
host: wasmtime_environ::IntraModuleInlining,
) -> Result<()> {
if module == host {
return Ok(());
}
let desc = |cfg| match cfg {
wasmtime_environ::IntraModuleInlining::No => "without intra-module inlining",
wasmtime_environ::IntraModuleInlining::Yes => "with intra-module inlining",
wasmtime_environ::IntraModuleInlining::WhenUsingGc => {
"with intra-module inlining only when using GC"
}
};
let module = desc(module);
let host = desc(host);
bail!("module was compiled {module} however the host is configured {host}")
}
}
#[cfg(test)]
mod test {
use super::*;
use crate::{Cache, Config, Module, OptLevel};
use std::{
collections::hash_map::DefaultHasher,
hash::{Hash, Hasher},
};
use tempfile::TempDir;
#[test]
fn test_architecture_mismatch() -> Result<()> {
let engine = Engine::default();
let mut metadata = Metadata::new(&engine)?;
metadata.target = "unknown-generic-linux".to_string();
match metadata.check_compatible(&engine) {
Ok(_) => unreachable!(),
Err(e) => assert_eq!(
e.to_string(),
"Module was compiled for architecture 'unknown'",
),
}
Ok(())
}
#[test]
#[cfg(all(target_arch = "x86_64", not(miri)))]
fn test_os_mismatch() -> Result<()> {
let engine = Engine::default();
let mut metadata = Metadata::new(&engine)?;
metadata.target = format!(
"{}-generic-unknown",
target_lexicon::Triple::host().architecture
);
match metadata.check_compatible(&engine) {
Ok(_) => unreachable!(),
Err(e) => assert_eq!(
e.to_string(),
"Module was compiled for operating system 'unknown'",
),
}
Ok(())
}
fn assert_contains(error: &Error, msg: &str) {
let msg = msg.trim();
if error.chain().any(|e| e.to_string().contains(msg)) {
return;
}
panic!("failed to find:\n\n'''{msg}\n'''\n\nwithin error message:\n\n'''{error:?}'''")
}
#[test]
fn test_cranelift_flags_mismatch() -> Result<()> {
let engine = Engine::default();
let mut metadata = Metadata::new(&engine)?;
metadata
.shared_flags
.push(("preserve_frame_pointers", FlagValue::Bool(false)));
match metadata.check_compatible(&engine) {
Ok(_) => unreachable!(),
Err(e) => {
assert_contains(
&e,
"compilation settings of module incompatible with native host",
);
assert_contains(
&e,
"setting \"preserve_frame_pointers\" is configured to Bool(false) which is not supported",
);
}
}
Ok(())
}
#[test]
fn test_isa_flags_mismatch() -> Result<()> {
let engine = Engine::default();
let mut metadata = Metadata::new(&engine)?;
metadata
.isa_flags
.push(("not_a_flag", FlagValue::Bool(true)));
match metadata.check_compatible(&engine) {
Ok(_) => unreachable!(),
Err(e) => {
assert_contains(
&e,
"compilation settings of module incompatible with native host",
);
assert_contains(
&e,
"don't know how to test for target-specific flag \"not_a_flag\" at runtime",
);
}
}
Ok(())
}
#[test]
#[cfg_attr(miri, ignore)]
#[cfg(target_pointer_width = "64")] fn test_tunables_int_mismatch() -> Result<()> {
let engine = Engine::default();
let mut metadata = Metadata::new(&engine)?;
metadata.tunables.memory_guard_size = 0;
match metadata.check_compatible(&engine) {
Ok(_) => unreachable!(),
Err(e) => assert_eq!(
e.to_string(),
"Module was compiled with a memory guard size of '0' but '33554432' is expected for the host"
),
}
Ok(())
}
#[test]
fn test_tunables_bool_mismatch() -> Result<()> {
let mut config = Config::new();
config.epoch_interruption(true);
let engine = Engine::new(&config)?;
let mut metadata = Metadata::new(&engine)?;
metadata.tunables.epoch_interruption = false;
match metadata.check_compatible(&engine) {
Ok(_) => unreachable!(),
Err(e) => assert_eq!(
e.to_string(),
"Module was compiled without epoch interruption but it is enabled for the host"
),
}
let mut config = Config::new();
config.epoch_interruption(false);
let engine = Engine::new(&config)?;
let mut metadata = Metadata::new(&engine)?;
metadata.tunables.epoch_interruption = true;
match metadata.check_compatible(&engine) {
Ok(_) => unreachable!(),
Err(e) => assert_eq!(
e.to_string(),
"Module was compiled with epoch interruption but it is not enabled for the host"
),
}
Ok(())
}
#[test]
#[cfg(all(target_arch = "x86_64", not(miri)))]
fn test_feature_mismatch() -> Result<()> {
let mut config = Config::new();
config.wasm_threads(true);
let engine = Engine::new(&config)?;
let mut metadata = Metadata::new(&engine)?;
metadata.features &= !wasmparser::WasmFeatures::THREADS.bits();
metadata.check_compatible(&engine)?;
let mut config = Config::new();
config.wasm_threads(false);
let engine = Engine::new(&config)?;
let mut metadata = Metadata::new(&engine)?;
metadata.features |= wasmparser::WasmFeatures::THREADS.bits();
match metadata.check_compatible(&engine) {
Ok(_) => unreachable!(),
Err(e) => assert_eq!(
e.to_string(),
"Module was compiled with support for WebAssembly feature \
`threads` but it is not enabled for the host"
),
}
Ok(())
}
#[test]
fn engine_weak_upgrades() {
let engine = Engine::default();
let weak = engine.weak();
weak.upgrade()
.expect("engine is still alive, so weak reference can upgrade");
drop(engine);
assert!(
weak.upgrade().is_none(),
"engine was dropped, so weak reference cannot upgrade"
);
}
#[test]
#[cfg_attr(miri, ignore)]
fn cache_accounts_for_opt_level() -> Result<()> {
let _ = env_logger::try_init();
let td = TempDir::new()?;
let config_path = td.path().join("config.toml");
std::fs::write(
&config_path,
&format!(
"
[cache]
directory = '{}'
",
td.path().join("cache").display()
),
)?;
let mut cfg = Config::new();
cfg.cranelift_opt_level(OptLevel::None)
.cache(Some(Cache::from_file(Some(&config_path))?));
let engine = Engine::new(&cfg)?;
Module::new(&engine, "(module (func))")?;
let cache_config = engine
.config()
.cache
.as_ref()
.expect("Missing cache config");
assert_eq!(cache_config.cache_hits(), 0);
assert_eq!(cache_config.cache_misses(), 1);
Module::new(&engine, "(module (func))")?;
assert_eq!(cache_config.cache_hits(), 1);
assert_eq!(cache_config.cache_misses(), 1);
let mut cfg = Config::new();
cfg.cranelift_opt_level(OptLevel::Speed)
.cache(Some(Cache::from_file(Some(&config_path))?));
let engine = Engine::new(&cfg)?;
let cache_config = engine
.config()
.cache
.as_ref()
.expect("Missing cache config");
Module::new(&engine, "(module (func))")?;
assert_eq!(cache_config.cache_hits(), 0);
assert_eq!(cache_config.cache_misses(), 1);
Module::new(&engine, "(module (func))")?;
assert_eq!(cache_config.cache_hits(), 1);
assert_eq!(cache_config.cache_misses(), 1);
let mut cfg = Config::new();
cfg.cranelift_opt_level(OptLevel::SpeedAndSize)
.cache(Some(Cache::from_file(Some(&config_path))?));
let engine = Engine::new(&cfg)?;
let cache_config = engine
.config()
.cache
.as_ref()
.expect("Missing cache config");
Module::new(&engine, "(module (func))")?;
assert_eq!(cache_config.cache_hits(), 0);
assert_eq!(cache_config.cache_misses(), 1);
Module::new(&engine, "(module (func))")?;
assert_eq!(cache_config.cache_hits(), 1);
assert_eq!(cache_config.cache_misses(), 1);
let mut cfg = Config::new();
cfg.debug_info(true)
.cache(Some(Cache::from_file(Some(&config_path))?));
let engine = Engine::new(&cfg)?;
let cache_config = engine
.config()
.cache
.as_ref()
.expect("Missing cache config");
Module::new(&engine, "(module (func))")?;
assert_eq!(cache_config.cache_hits(), 0);
assert_eq!(cache_config.cache_misses(), 1);
Module::new(&engine, "(module (func))")?;
assert_eq!(cache_config.cache_hits(), 1);
assert_eq!(cache_config.cache_misses(), 1);
Ok(())
}
#[test]
fn precompile_compatibility_key_accounts_for_opt_level() {
fn hash_for_config(cfg: &Config) -> u64 {
let engine = Engine::new(cfg).expect("Config should be valid");
let mut hasher = DefaultHasher::new();
engine.precompile_compatibility_hash().hash(&mut hasher);
hasher.finish()
}
let mut cfg = Config::new();
cfg.cranelift_opt_level(OptLevel::None);
let opt_none_hash = hash_for_config(&cfg);
cfg.cranelift_opt_level(OptLevel::Speed);
let opt_speed_hash = hash_for_config(&cfg);
assert_ne!(opt_none_hash, opt_speed_hash)
}
#[test]
fn precompile_compatibility_key_accounts_for_module_version_strategy() -> Result<()> {
fn hash_for_config(cfg: &Config) -> u64 {
let engine = Engine::new(cfg).expect("Config should be valid");
let mut hasher = DefaultHasher::new();
engine.precompile_compatibility_hash().hash(&mut hasher);
hasher.finish()
}
let mut cfg_custom_version = Config::new();
cfg_custom_version.module_version(ModuleVersionStrategy::Custom("1.0.1111".to_string()))?;
let custom_version_hash = hash_for_config(&cfg_custom_version);
let mut cfg_default_version = Config::new();
cfg_default_version.module_version(ModuleVersionStrategy::WasmtimeVersion)?;
let default_version_hash = hash_for_config(&cfg_default_version);
let mut cfg_none_version = Config::new();
cfg_none_version.module_version(ModuleVersionStrategy::None)?;
let none_version_hash = hash_for_config(&cfg_none_version);
assert_ne!(custom_version_hash, default_version_hash);
assert_ne!(custom_version_hash, none_version_hash);
assert_ne!(default_version_hash, none_version_hash);
Ok(())
}
#[test]
#[cfg_attr(miri, ignore)]
#[cfg(feature = "component-model")]
fn components_are_cached() -> Result<()> {
use crate::component::Component;
let td = TempDir::new()?;
let config_path = td.path().join("config.toml");
std::fs::write(
&config_path,
&format!(
"
[cache]
directory = '{}'
",
td.path().join("cache").display()
),
)?;
let mut cfg = Config::new();
cfg.cache(Some(Cache::from_file(Some(&config_path))?));
let engine = Engine::new(&cfg)?;
let cache_config = engine
.config()
.cache
.as_ref()
.expect("Missing cache config");
Component::new(&engine, "(component (core module (func)))")?;
assert_eq!(cache_config.cache_hits(), 0);
assert_eq!(cache_config.cache_misses(), 1);
Component::new(&engine, "(component (core module (func)))")?;
assert_eq!(cache_config.cache_hits(), 1);
assert_eq!(cache_config.cache_misses(), 1);
Ok(())
}
}