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 `postcard`-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::prelude::*;
25 use crate::{Engine, ModuleVersionStrategy, Precompiled};
26 use core::fmt;
27 use core::str::FromStr;
28 use object::endian::Endianness;
29 #[cfg(any(feature = "cranelift", feature = "winch"))]
30 use object::write::{Object, StandardSegment};
31 use object::{FileFlags, Object as _, ObjectSection, read::elf::ElfFile64};
32 use serde_derive::{Deserialize, Serialize};
33 use wasmtime_environ::obj;
34 use wasmtime_environ::{FlagValue, ObjectKind, Tunables};
35 
36 const VERSION: u8 = 0;
37 
38 /// Verifies that the serialized engine in `mmap` is compatible with the
39 /// `engine` provided.
40 ///
41 /// This function will verify that the `mmap` provided can be deserialized
42 /// successfully and that the contents are all compatible with the `engine`
43 /// provided here, notably compatible wasm features are enabled, compatible
44 /// compiler options, etc. If a mismatch is found and the compilation metadata
45 /// specified is incompatible then an error is returned.
46 pub fn check_compatible(engine: &Engine, mmap: &[u8], expected: ObjectKind) -> Result<()> {
47     // Parse the input `mmap` as an ELF file and see if the header matches the
48     // Wasmtime-generated header. This includes a Wasmtime-specific `os_abi` and
49     // the `e_flags` field should indicate whether `expected` matches or not.
50     //
51     // Note that errors generated here could mean that a precompiled module was
52     // loaded as a component, or vice versa, both of which aren't supposed to
53     // work.
54     //
55     // Ideally we'd only `File::parse` once and avoid the linear
56     // `section_by_name` search here but the general serialization code isn't
57     // structured well enough to make this easy and additionally it's not really
58     // a perf issue right now so doing that is left for another day's
59     // refactoring.
60     let obj = ElfFile64::<Endianness>::parse(mmap)
61         .map_err(obj::ObjectCrateErrorWrapper)
62         .context("failed to parse precompiled artifact as an ELF")?;
63     let expected_e_flags = match expected {
64         ObjectKind::Module => obj::EF_WASMTIME_MODULE,
65         ObjectKind::Component => obj::EF_WASMTIME_COMPONENT,
66     };
67     match obj.flags() {
68         FileFlags::Elf {
69             os_abi: obj::ELFOSABI_WASMTIME,
70             abi_version: 0,
71             e_flags,
72         } if e_flags & expected_e_flags == expected_e_flags => {}
73         _ => bail!("incompatible object file format"),
74     }
75 
76     let data = obj
77         .section_by_name(obj::ELF_WASM_ENGINE)
78         .ok_or_else(|| anyhow!("failed to find section `{}`", obj::ELF_WASM_ENGINE))?
79         .data()
80         .map_err(obj::ObjectCrateErrorWrapper)?;
81     let (first, data) = data
82         .split_first()
83         .ok_or_else(|| anyhow!("invalid engine section"))?;
84     if *first != VERSION {
85         bail!("mismatched version in engine section");
86     }
87     let (len, data) = data
88         .split_first()
89         .ok_or_else(|| anyhow!("invalid engine section"))?;
90     let len = usize::from(*len);
91     let (version, data) = if data.len() < len + 1 {
92         bail!("engine section too small")
93     } else {
94         data.split_at(len)
95     };
96 
97     match &engine.config().module_version {
98         ModuleVersionStrategy::WasmtimeVersion => {
99             let version = core::str::from_utf8(version)?;
100             if version != env!("CARGO_PKG_VERSION_MAJOR") {
101                 bail!("Module was compiled with incompatible Wasmtime version '{version}'");
102             }
103         }
104         ModuleVersionStrategy::Custom(v) => {
105             let version = core::str::from_utf8(&version)?;
106             if version != v {
107                 bail!("Module was compiled with incompatible version '{version}'");
108             }
109         }
110         ModuleVersionStrategy::None => { /* ignore the version info, accept all */ }
111     }
112     postcard::from_bytes::<Metadata<'_>>(data)?.check_compatible(engine)
113 }
114 
115 #[cfg(any(feature = "cranelift", feature = "winch"))]
116 pub fn append_compiler_info(engine: &Engine, obj: &mut Object<'_>, metadata: &Metadata<'_>) {
117     let section = obj.add_section(
118         obj.segment_name(StandardSegment::Data).to_vec(),
119         obj::ELF_WASM_ENGINE.as_bytes().to_vec(),
120         object::SectionKind::ReadOnlyData,
121     );
122     let mut data = Vec::new();
123     data.push(VERSION);
124     let version = match &engine.config().module_version {
125         ModuleVersionStrategy::WasmtimeVersion => env!("CARGO_PKG_VERSION_MAJOR"),
126         ModuleVersionStrategy::Custom(c) => c,
127         ModuleVersionStrategy::None => "",
128     };
129     // This precondition is checked in Config::module_version:
130     assert!(
131         version.len() < 256,
132         "package version must be less than 256 bytes"
133     );
134     data.push(version.len() as u8);
135     data.extend_from_slice(version.as_bytes());
136     data.extend(postcard::to_allocvec(metadata).unwrap());
137     obj.set_section_data(section, data, 1);
138 }
139 
140 fn detect_precompiled<'data, R: object::ReadRef<'data>>(
141     obj: ElfFile64<'data, Endianness, R>,
142 ) -> Option<Precompiled> {
143     match obj.flags() {
144         FileFlags::Elf {
145             os_abi: obj::ELFOSABI_WASMTIME,
146             abi_version: 0,
147             e_flags,
148         } if e_flags & obj::EF_WASMTIME_MODULE != 0 => Some(Precompiled::Module),
149         FileFlags::Elf {
150             os_abi: obj::ELFOSABI_WASMTIME,
151             abi_version: 0,
152             e_flags,
153         } if e_flags & obj::EF_WASMTIME_COMPONENT != 0 => Some(Precompiled::Component),
154         _ => None,
155     }
156 }
157 
158 pub fn detect_precompiled_bytes(bytes: &[u8]) -> Option<Precompiled> {
159     detect_precompiled(ElfFile64::parse(bytes).ok()?)
160 }
161 
162 #[cfg(feature = "std")]
163 pub fn detect_precompiled_file(path: impl AsRef<std::path::Path>) -> Result<Option<Precompiled>> {
164     let read_cache = object::ReadCache::new(std::fs::File::open(path)?);
165     let obj = ElfFile64::parse(&read_cache)?;
166     Ok(detect_precompiled(obj))
167 }
168 
169 #[derive(Serialize, Deserialize)]
170 pub struct Metadata<'a> {
171     target: String,
172     #[serde(borrow)]
173     shared_flags: Vec<(&'a str, FlagValue<'a>)>,
174     #[serde(borrow)]
175     isa_flags: Vec<(&'a str, FlagValue<'a>)>,
176     tunables: Tunables,
177     features: u64,
178 }
179 
180 impl Metadata<'_> {
181     #[cfg(any(feature = "cranelift", feature = "winch"))]
182     pub fn new(engine: &Engine) -> Metadata<'static> {
183         Metadata {
184             target: engine.compiler().triple().to_string(),
185             shared_flags: engine.compiler().flags(),
186             isa_flags: engine.compiler().isa_flags(),
187             tunables: engine.tunables().clone(),
188             features: engine.features().bits(),
189         }
190     }
191 
192     fn check_compatible(mut self, engine: &Engine) -> Result<()> {
193         self.check_triple(engine)?;
194         self.check_shared_flags(engine)?;
195         self.check_isa_flags(engine)?;
196         self.check_tunables(&engine.tunables())?;
197         self.check_features(&engine.features())?;
198         Ok(())
199     }
200 
201     fn check_triple(&self, engine: &Engine) -> Result<()> {
202         let engine_target = engine.target();
203         let module_target =
204             target_lexicon::Triple::from_str(&self.target).map_err(|e| anyhow!(e))?;
205 
206         if module_target.architecture != engine_target.architecture {
207             bail!(
208                 "Module was compiled for architecture '{}'",
209                 module_target.architecture
210             );
211         }
212 
213         if module_target.operating_system != engine_target.operating_system {
214             bail!(
215                 "Module was compiled for operating system '{}'",
216                 module_target.operating_system
217             );
218         }
219 
220         Ok(())
221     }
222 
223     fn check_shared_flags(&mut self, engine: &Engine) -> Result<()> {
224         for (name, val) in self.shared_flags.iter() {
225             engine
226                 .check_compatible_with_shared_flag(name, val)
227                 .map_err(|s| anyhow::Error::msg(s))
228                 .context("compilation settings of module incompatible with native host")?;
229         }
230         Ok(())
231     }
232 
233     fn check_isa_flags(&mut self, engine: &Engine) -> Result<()> {
234         for (name, val) in self.isa_flags.iter() {
235             engine
236                 .check_compatible_with_isa_flag(name, val)
237                 .map_err(|s| anyhow::Error::msg(s))
238                 .context("compilation settings of module incompatible with native host")?;
239         }
240         Ok(())
241     }
242 
243     fn check_int<T: Eq + fmt::Display>(found: T, expected: T, feature: &str) -> Result<()> {
244         if found == expected {
245             return Ok(());
246         }
247 
248         bail!(
249             "Module was compiled with a {feature} of '{found}' but '{expected}' is expected for the host"
250         );
251     }
252 
253     fn check_bool(found: bool, expected: bool, feature: impl fmt::Display) -> Result<()> {
254         if found == expected {
255             return Ok(());
256         }
257 
258         bail!(
259             "Module was compiled {} {} but it {} enabled for the host",
260             if found { "with" } else { "without" },
261             feature,
262             if expected { "is" } else { "is not" }
263         );
264     }
265 
266     fn check_tunables(&mut self, other: &Tunables) -> Result<()> {
267         let Tunables {
268             collector,
269             memory_reservation,
270             memory_guard_size,
271             debug_native,
272             debug_guest,
273             parse_wasm_debuginfo,
274             consume_fuel,
275             epoch_interruption,
276             memory_may_move,
277             guard_before_linear_memory,
278             table_lazy_init,
279             relaxed_simd_deterministic,
280             winch_callable,
281             signals_based_traps,
282             memory_init_cow,
283             inlining,
284             inlining_intra_module,
285             inlining_small_callee_size,
286             inlining_sum_size_threshold,
287 
288             // This doesn't affect compilation, it's just a runtime setting.
289             memory_reservation_for_growth: _,
290 
291             // This does technically affect compilation but modules with/without
292             // trap information can be loaded into engines with the opposite
293             // setting just fine (it's just a section in the compiled file and
294             // whether it's present or not)
295             generate_address_map: _,
296 
297             // Just a debugging aid, doesn't affect functionality at all.
298             debug_adapter_modules: _,
299         } = self.tunables;
300 
301         Self::check_collector(collector, other.collector)?;
302         Self::check_int(
303             memory_reservation,
304             other.memory_reservation,
305             "memory reservation",
306         )?;
307         Self::check_int(
308             memory_guard_size,
309             other.memory_guard_size,
310             "memory guard size",
311         )?;
312         Self::check_bool(
313             debug_native,
314             other.debug_native,
315             "native debug information support",
316         )?;
317         Self::check_bool(debug_guest, other.debug_guest, "guest debug")?;
318         Self::check_bool(
319             parse_wasm_debuginfo,
320             other.parse_wasm_debuginfo,
321             "WebAssembly backtrace support",
322         )?;
323         Self::check_bool(consume_fuel, other.consume_fuel, "fuel support")?;
324         Self::check_bool(
325             epoch_interruption,
326             other.epoch_interruption,
327             "epoch interruption",
328         )?;
329         Self::check_bool(memory_may_move, other.memory_may_move, "memory may move")?;
330         Self::check_bool(
331             guard_before_linear_memory,
332             other.guard_before_linear_memory,
333             "guard before linear memory",
334         )?;
335         Self::check_bool(table_lazy_init, other.table_lazy_init, "table lazy init")?;
336         Self::check_bool(
337             relaxed_simd_deterministic,
338             other.relaxed_simd_deterministic,
339             "relaxed simd deterministic semantics",
340         )?;
341         Self::check_bool(
342             winch_callable,
343             other.winch_callable,
344             "Winch calling convention",
345         )?;
346         Self::check_bool(
347             signals_based_traps,
348             other.signals_based_traps,
349             "Signals-based traps",
350         )?;
351         Self::check_bool(
352             memory_init_cow,
353             other.memory_init_cow,
354             "memory initialization with CoW",
355         )?;
356         Self::check_bool(inlining, other.inlining, "function inlining")?;
357         Self::check_int(
358             inlining_small_callee_size,
359             other.inlining_small_callee_size,
360             "function inlining small-callee size",
361         )?;
362         Self::check_int(
363             inlining_sum_size_threshold,
364             other.inlining_sum_size_threshold,
365             "function inlining sum-size threshold",
366         )?;
367         Self::check_intra_module_inlining(inlining_intra_module, other.inlining_intra_module)?;
368 
369         Ok(())
370     }
371 
372     fn check_features(&mut self, other: &wasmparser::WasmFeatures) -> Result<()> {
373         let module_features = wasmparser::WasmFeatures::from_bits_truncate(self.features);
374         let missing_features = (*other & module_features) ^ module_features;
375         for (name, _) in missing_features.iter_names() {
376             let name = name.to_ascii_lowercase();
377             bail!(
378                 "Module was compiled with support for WebAssembly feature \
379                 `{name}` but it is not enabled for the host",
380             );
381         }
382         Ok(())
383     }
384 
385     fn check_collector(
386         module: Option<wasmtime_environ::Collector>,
387         host: Option<wasmtime_environ::Collector>,
388     ) -> Result<()> {
389         match (module, host) {
390             // If the module doesn't require GC support it doesn't matter
391             // whether the host has GC support enabled or not.
392             (None, _) => Ok(()),
393             (Some(module), Some(host)) if module == host => Ok(()),
394 
395             (Some(_), None) => {
396                 bail!("module was compiled with GC however GC is disabled in the host")
397             }
398 
399             (Some(module), Some(host)) => {
400                 bail!(
401                     "module was compiled for the {module} collector but \
402                      the host is configured to use the {host} collector",
403                 )
404             }
405         }
406     }
407 
408     fn check_intra_module_inlining(
409         module: wasmtime_environ::IntraModuleInlining,
410         host: wasmtime_environ::IntraModuleInlining,
411     ) -> Result<()> {
412         if module == host {
413             return Ok(());
414         }
415 
416         let desc = |cfg| match cfg {
417             wasmtime_environ::IntraModuleInlining::No => "without intra-module inlining",
418             wasmtime_environ::IntraModuleInlining::Yes => "with intra-module inlining",
419             wasmtime_environ::IntraModuleInlining::WhenUsingGc => {
420                 "with intra-module inlining only when using GC"
421             }
422         };
423 
424         let module = desc(module);
425         let host = desc(host);
426 
427         bail!("module was compiled {module} however the host is configured {host}")
428     }
429 }
430 
431 #[cfg(test)]
432 mod test {
433     use super::*;
434     use crate::{Cache, Config, Module, OptLevel};
435     use std::{
436         collections::hash_map::DefaultHasher,
437         hash::{Hash, Hasher},
438     };
439     use tempfile::TempDir;
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     // Note that this test runs on a platform that is known to use Cranelift
459     #[test]
460     #[cfg(all(target_arch = "x86_64", not(miri)))]
461     fn test_os_mismatch() -> Result<()> {
462         let engine = Engine::default();
463         let mut metadata = Metadata::new(&engine);
464 
465         metadata.target = format!(
466             "{}-generic-unknown",
467             target_lexicon::Triple::host().architecture
468         );
469 
470         match metadata.check_compatible(&engine) {
471             Ok(_) => unreachable!(),
472             Err(e) => assert_eq!(
473                 e.to_string(),
474                 "Module was compiled for operating system 'unknown'",
475             ),
476         }
477 
478         Ok(())
479     }
480 
481     #[test]
482     fn test_cranelift_flags_mismatch() -> Result<()> {
483         let engine = Engine::default();
484         let mut metadata = Metadata::new(&engine);
485 
486         metadata
487             .shared_flags
488             .push(("preserve_frame_pointers", FlagValue::Bool(false)));
489 
490         match metadata.check_compatible(&engine) {
491             Ok(_) => unreachable!(),
492             Err(e) => assert!(format!("{e:?}").starts_with(
493                 "\
494 compilation settings of module incompatible with native host
495 
496 Caused by:
497     setting \"preserve_frame_pointers\" is configured to Bool(false) which is not supported"
498             )),
499         }
500 
501         Ok(())
502     }
503 
504     #[test]
505     fn test_isa_flags_mismatch() -> Result<()> {
506         let engine = Engine::default();
507         let mut metadata = Metadata::new(&engine);
508 
509         metadata
510             .isa_flags
511             .push(("not_a_flag", FlagValue::Bool(true)));
512 
513         match metadata.check_compatible(&engine) {
514             Ok(_) => unreachable!(),
515             Err(e) => assert!(
516                 format!("{e:?}").starts_with(
517                     "\
518 compilation settings of module incompatible with native host
519 
520 Caused by:
521     don't know how to test for target-specific flag \"not_a_flag\" at runtime",
522                 ),
523                 "bad error {e:?}",
524             ),
525         }
526 
527         Ok(())
528     }
529 
530     #[test]
531     #[cfg_attr(miri, ignore)]
532     #[cfg(target_pointer_width = "64")] // different defaults on 32-bit platforms
533     fn test_tunables_int_mismatch() -> Result<()> {
534         let engine = Engine::default();
535         let mut metadata = Metadata::new(&engine);
536 
537         metadata.tunables.memory_guard_size = 0;
538 
539         match metadata.check_compatible(&engine) {
540             Ok(_) => unreachable!(),
541             Err(e) => assert_eq!(
542                 e.to_string(),
543                 "Module was compiled with a memory guard size of '0' but '33554432' is expected for the host"
544             ),
545         }
546 
547         Ok(())
548     }
549 
550     #[test]
551     fn test_tunables_bool_mismatch() -> Result<()> {
552         let mut config = Config::new();
553         config.epoch_interruption(true);
554 
555         let engine = Engine::new(&config)?;
556         let mut metadata = Metadata::new(&engine);
557         metadata.tunables.epoch_interruption = false;
558 
559         match metadata.check_compatible(&engine) {
560             Ok(_) => unreachable!(),
561             Err(e) => assert_eq!(
562                 e.to_string(),
563                 "Module was compiled without epoch interruption but it is enabled for the host"
564             ),
565         }
566 
567         let mut config = Config::new();
568         config.epoch_interruption(false);
569 
570         let engine = Engine::new(&config)?;
571         let mut metadata = Metadata::new(&engine);
572         metadata.tunables.epoch_interruption = true;
573 
574         match metadata.check_compatible(&engine) {
575             Ok(_) => unreachable!(),
576             Err(e) => assert_eq!(
577                 e.to_string(),
578                 "Module was compiled with epoch interruption but it is not enabled for the host"
579             ),
580         }
581 
582         Ok(())
583     }
584 
585     /// This test is only run a platform that is known to implement threads
586     #[test]
587     #[cfg(all(target_arch = "x86_64", not(miri)))]
588     fn test_feature_mismatch() -> Result<()> {
589         let mut config = Config::new();
590         config.wasm_threads(true);
591 
592         let engine = Engine::new(&config)?;
593         let mut metadata = Metadata::new(&engine);
594         metadata.features &= !wasmparser::WasmFeatures::THREADS.bits();
595 
596         // If a feature is disabled in the module and enabled in the host,
597         // that's always ok.
598         metadata.check_compatible(&engine)?;
599 
600         let mut config = Config::new();
601         config.wasm_threads(false);
602 
603         let engine = Engine::new(&config)?;
604         let mut metadata = Metadata::new(&engine);
605         metadata.features |= wasmparser::WasmFeatures::THREADS.bits();
606 
607         match metadata.check_compatible(&engine) {
608             Ok(_) => unreachable!(),
609             Err(e) => assert_eq!(
610                 e.to_string(),
611                 "Module was compiled with support for WebAssembly feature \
612                 `threads` but it is not enabled for the host"
613             ),
614         }
615 
616         Ok(())
617     }
618 
619     #[test]
620     fn engine_weak_upgrades() {
621         let engine = Engine::default();
622         let weak = engine.weak();
623         weak.upgrade()
624             .expect("engine is still alive, so weak reference can upgrade");
625         drop(engine);
626         assert!(
627             weak.upgrade().is_none(),
628             "engine was dropped, so weak reference cannot upgrade"
629         );
630     }
631 
632     #[test]
633     #[cfg_attr(miri, ignore)]
634     fn cache_accounts_for_opt_level() -> Result<()> {
635         let _ = env_logger::try_init();
636 
637         let td = TempDir::new()?;
638         let config_path = td.path().join("config.toml");
639         std::fs::write(
640             &config_path,
641             &format!(
642                 "
643                     [cache]
644                     directory = '{}'
645                 ",
646                 td.path().join("cache").display()
647             ),
648         )?;
649         let mut cfg = Config::new();
650         cfg.cranelift_opt_level(OptLevel::None)
651             .cache(Some(Cache::from_file(Some(&config_path))?));
652         let engine = Engine::new(&cfg)?;
653         Module::new(&engine, "(module (func))")?;
654         let cache_config = engine
655             .config()
656             .cache
657             .as_ref()
658             .expect("Missing cache config");
659         assert_eq!(cache_config.cache_hits(), 0);
660         assert_eq!(cache_config.cache_misses(), 1);
661         Module::new(&engine, "(module (func))")?;
662         assert_eq!(cache_config.cache_hits(), 1);
663         assert_eq!(cache_config.cache_misses(), 1);
664 
665         let mut cfg = Config::new();
666         cfg.cranelift_opt_level(OptLevel::Speed)
667             .cache(Some(Cache::from_file(Some(&config_path))?));
668         let engine = Engine::new(&cfg)?;
669         let cache_config = engine
670             .config()
671             .cache
672             .as_ref()
673             .expect("Missing cache config");
674         Module::new(&engine, "(module (func))")?;
675         assert_eq!(cache_config.cache_hits(), 0);
676         assert_eq!(cache_config.cache_misses(), 1);
677         Module::new(&engine, "(module (func))")?;
678         assert_eq!(cache_config.cache_hits(), 1);
679         assert_eq!(cache_config.cache_misses(), 1);
680 
681         let mut cfg = Config::new();
682         cfg.cranelift_opt_level(OptLevel::SpeedAndSize)
683             .cache(Some(Cache::from_file(Some(&config_path))?));
684         let engine = Engine::new(&cfg)?;
685         let cache_config = engine
686             .config()
687             .cache
688             .as_ref()
689             .expect("Missing cache config");
690         Module::new(&engine, "(module (func))")?;
691         assert_eq!(cache_config.cache_hits(), 0);
692         assert_eq!(cache_config.cache_misses(), 1);
693         Module::new(&engine, "(module (func))")?;
694         assert_eq!(cache_config.cache_hits(), 1);
695         assert_eq!(cache_config.cache_misses(), 1);
696 
697         let mut cfg = Config::new();
698         cfg.debug_info(true)
699             .cache(Some(Cache::from_file(Some(&config_path))?));
700         let engine = Engine::new(&cfg)?;
701         let cache_config = engine
702             .config()
703             .cache
704             .as_ref()
705             .expect("Missing cache config");
706         Module::new(&engine, "(module (func))")?;
707         assert_eq!(cache_config.cache_hits(), 0);
708         assert_eq!(cache_config.cache_misses(), 1);
709         Module::new(&engine, "(module (func))")?;
710         assert_eq!(cache_config.cache_hits(), 1);
711         assert_eq!(cache_config.cache_misses(), 1);
712 
713         Ok(())
714     }
715 
716     #[test]
717     fn precompile_compatibility_key_accounts_for_opt_level() {
718         fn hash_for_config(cfg: &Config) -> u64 {
719             let engine = Engine::new(cfg).expect("Config should be valid");
720             let mut hasher = DefaultHasher::new();
721             engine.precompile_compatibility_hash().hash(&mut hasher);
722             hasher.finish()
723         }
724         let mut cfg = Config::new();
725         cfg.cranelift_opt_level(OptLevel::None);
726         let opt_none_hash = hash_for_config(&cfg);
727         cfg.cranelift_opt_level(OptLevel::Speed);
728         let opt_speed_hash = hash_for_config(&cfg);
729         assert_ne!(opt_none_hash, opt_speed_hash)
730     }
731 
732     #[test]
733     fn precompile_compatibility_key_accounts_for_module_version_strategy() -> Result<()> {
734         fn hash_for_config(cfg: &Config) -> u64 {
735             let engine = Engine::new(cfg).expect("Config should be valid");
736             let mut hasher = DefaultHasher::new();
737             engine.precompile_compatibility_hash().hash(&mut hasher);
738             hasher.finish()
739         }
740         let mut cfg_custom_version = Config::new();
741         cfg_custom_version.module_version(ModuleVersionStrategy::Custom("1.0.1111".to_string()))?;
742         let custom_version_hash = hash_for_config(&cfg_custom_version);
743 
744         let mut cfg_default_version = Config::new();
745         cfg_default_version.module_version(ModuleVersionStrategy::WasmtimeVersion)?;
746         let default_version_hash = hash_for_config(&cfg_default_version);
747 
748         let mut cfg_none_version = Config::new();
749         cfg_none_version.module_version(ModuleVersionStrategy::None)?;
750         let none_version_hash = hash_for_config(&cfg_none_version);
751 
752         assert_ne!(custom_version_hash, default_version_hash);
753         assert_ne!(custom_version_hash, none_version_hash);
754         assert_ne!(default_version_hash, none_version_hash);
755 
756         Ok(())
757     }
758 
759     #[test]
760     #[cfg_attr(miri, ignore)]
761     #[cfg(feature = "component-model")]
762     fn components_are_cached() -> Result<()> {
763         use crate::component::Component;
764 
765         let td = TempDir::new()?;
766         let config_path = td.path().join("config.toml");
767         std::fs::write(
768             &config_path,
769             &format!(
770                 "
771                     [cache]
772                     directory = '{}'
773                 ",
774                 td.path().join("cache").display()
775             ),
776         )?;
777         let mut cfg = Config::new();
778         cfg.cache(Some(Cache::from_file(Some(&config_path))?));
779         let engine = Engine::new(&cfg)?;
780         let cache_config = engine
781             .config()
782             .cache
783             .as_ref()
784             .expect("Missing cache config");
785         Component::new(&engine, "(component (core module (func)))")?;
786         assert_eq!(cache_config.cache_hits(), 0);
787         assert_eq!(cache_config.cache_misses(), 1);
788         Component::new(&engine, "(component (core module (func)))")?;
789         assert_eq!(cache_config.cache_hits(), 1);
790         assert_eq!(cache_config.cache_misses(), 1);
791 
792         Ok(())
793     }
794 }
795