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(|| format_err!("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(|| format_err!("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(|| format_err!("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) -> Result<Metadata<'static>> {
183         let compiler = engine.try_compiler()?;
184         Ok(Metadata {
185             target: compiler.triple().to_string(),
186             shared_flags: compiler.flags(),
187             isa_flags: compiler.isa_flags(),
188             tunables: engine.tunables().clone(),
189             features: engine.features().bits(),
190         })
191     }
192 
193     fn check_compatible(mut self, engine: &Engine) -> Result<()> {
194         self.check_triple(engine)?;
195         self.check_shared_flags(engine)?;
196         self.check_isa_flags(engine)?;
197         self.check_tunables(&engine.tunables())?;
198         self.check_features(&engine.features())?;
199         Ok(())
200     }
201 
202     fn check_triple(&self, engine: &Engine) -> Result<()> {
203         let engine_target = engine.target();
204         let module_target =
205             target_lexicon::Triple::from_str(&self.target).map_err(|e| format_err!(e))?;
206 
207         if module_target.architecture != engine_target.architecture {
208             bail!(
209                 "Module was compiled for architecture '{}'",
210                 module_target.architecture
211             );
212         }
213 
214         if module_target.operating_system != engine_target.operating_system {
215             bail!(
216                 "Module was compiled for operating system '{}'",
217                 module_target.operating_system
218             );
219         }
220 
221         Ok(())
222     }
223 
224     fn check_shared_flags(&mut self, engine: &Engine) -> Result<()> {
225         for (name, val) in self.shared_flags.iter() {
226             engine
227                 .check_compatible_with_shared_flag(name, val)
228                 .map_err(|s| crate::Error::msg(s))
229                 .context("compilation settings of module incompatible with native host")?;
230         }
231         Ok(())
232     }
233 
234     fn check_isa_flags(&mut self, engine: &Engine) -> Result<()> {
235         for (name, val) in self.isa_flags.iter() {
236             engine
237                 .check_compatible_with_isa_flag(name, val)
238                 .map_err(|s| crate::Error::msg(s))
239                 .context("compilation settings of module incompatible with native host")?;
240         }
241         Ok(())
242     }
243 
244     fn check_int<T: Eq + fmt::Display>(found: T, expected: T, feature: &str) -> Result<()> {
245         if found == expected {
246             return Ok(());
247         }
248 
249         bail!(
250             "Module was compiled with a {feature} of '{found}' but '{expected}' is expected for the host"
251         );
252     }
253 
254     fn check_bool(found: bool, expected: bool, feature: impl fmt::Display) -> Result<()> {
255         if found == expected {
256             return Ok(());
257         }
258 
259         bail!(
260             "Module was compiled {} {} but it {} enabled for the host",
261             if found { "with" } else { "without" },
262             feature,
263             if expected { "is" } else { "is not" }
264         );
265     }
266 
267     fn check_tunables(&mut self, other: &Tunables) -> Result<()> {
268         let Tunables {
269             collector,
270             memory_reservation,
271             memory_guard_size,
272             debug_native,
273             debug_guest,
274             parse_wasm_debuginfo,
275             consume_fuel,
276             epoch_interruption,
277             memory_may_move,
278             guard_before_linear_memory,
279             table_lazy_init,
280             relaxed_simd_deterministic,
281             winch_callable,
282             signals_based_traps,
283             memory_init_cow,
284             inlining,
285             inlining_intra_module,
286             inlining_small_callee_size,
287             inlining_sum_size_threshold,
288             concurrency_support,
289 
290             // This doesn't affect compilation, it's just a runtime setting.
291             memory_reservation_for_growth: _,
292 
293             // This does technically affect compilation but modules with/without
294             // trap information can be loaded into engines with the opposite
295             // setting just fine (it's just a section in the compiled file and
296             // whether it's present or not)
297             generate_address_map: _,
298 
299             // Just a debugging aid, doesn't affect functionality at all.
300             debug_adapter_modules: _,
301         } = self.tunables;
302 
303         Self::check_collector(collector, other.collector)?;
304         Self::check_int(
305             memory_reservation,
306             other.memory_reservation,
307             "memory reservation",
308         )?;
309         Self::check_int(
310             memory_guard_size,
311             other.memory_guard_size,
312             "memory guard size",
313         )?;
314         Self::check_bool(
315             debug_native,
316             other.debug_native,
317             "native debug information support",
318         )?;
319         Self::check_bool(debug_guest, other.debug_guest, "guest debug")?;
320         Self::check_bool(
321             parse_wasm_debuginfo,
322             other.parse_wasm_debuginfo,
323             "WebAssembly backtrace support",
324         )?;
325         Self::check_bool(consume_fuel, other.consume_fuel, "fuel support")?;
326         Self::check_bool(
327             epoch_interruption,
328             other.epoch_interruption,
329             "epoch interruption",
330         )?;
331         Self::check_bool(memory_may_move, other.memory_may_move, "memory may move")?;
332         Self::check_bool(
333             guard_before_linear_memory,
334             other.guard_before_linear_memory,
335             "guard before linear memory",
336         )?;
337         Self::check_bool(table_lazy_init, other.table_lazy_init, "table lazy init")?;
338         Self::check_bool(
339             relaxed_simd_deterministic,
340             other.relaxed_simd_deterministic,
341             "relaxed simd deterministic semantics",
342         )?;
343         Self::check_bool(
344             winch_callable,
345             other.winch_callable,
346             "Winch calling convention",
347         )?;
348         Self::check_bool(
349             signals_based_traps,
350             other.signals_based_traps,
351             "Signals-based traps",
352         )?;
353         Self::check_bool(
354             memory_init_cow,
355             other.memory_init_cow,
356             "memory initialization with CoW",
357         )?;
358         Self::check_bool(inlining, other.inlining, "function inlining")?;
359         Self::check_int(
360             inlining_small_callee_size,
361             other.inlining_small_callee_size,
362             "function inlining small-callee size",
363         )?;
364         Self::check_int(
365             inlining_sum_size_threshold,
366             other.inlining_sum_size_threshold,
367             "function inlining sum-size threshold",
368         )?;
369         Self::check_bool(
370             concurrency_support,
371             other.concurrency_support,
372             "concurrency support",
373         )?;
374         Self::check_intra_module_inlining(inlining_intra_module, other.inlining_intra_module)?;
375 
376         Ok(())
377     }
378 
379     fn check_features(&mut self, other: &wasmparser::WasmFeatures) -> Result<()> {
380         let module_features = wasmparser::WasmFeatures::from_bits_truncate(self.features);
381         let missing_features = (*other & module_features) ^ module_features;
382         for (name, _) in missing_features.iter_names() {
383             let name = name.to_ascii_lowercase();
384             bail!(
385                 "Module was compiled with support for WebAssembly feature \
386                 `{name}` but it is not enabled for the host",
387             );
388         }
389         Ok(())
390     }
391 
392     fn check_collector(
393         module: Option<wasmtime_environ::Collector>,
394         host: Option<wasmtime_environ::Collector>,
395     ) -> Result<()> {
396         match (module, host) {
397             // If the module doesn't require GC support it doesn't matter
398             // whether the host has GC support enabled or not.
399             (None, _) => Ok(()),
400             (Some(module), Some(host)) if module == host => Ok(()),
401 
402             (Some(_), None) => {
403                 bail!("module was compiled with GC however GC is disabled in the host")
404             }
405 
406             (Some(module), Some(host)) => {
407                 bail!(
408                     "module was compiled for the {module} collector but \
409                      the host is configured to use the {host} collector",
410                 )
411             }
412         }
413     }
414 
415     fn check_intra_module_inlining(
416         module: wasmtime_environ::IntraModuleInlining,
417         host: wasmtime_environ::IntraModuleInlining,
418     ) -> Result<()> {
419         if module == host {
420             return Ok(());
421         }
422 
423         let desc = |cfg| match cfg {
424             wasmtime_environ::IntraModuleInlining::No => "without intra-module inlining",
425             wasmtime_environ::IntraModuleInlining::Yes => "with intra-module inlining",
426             wasmtime_environ::IntraModuleInlining::WhenUsingGc => {
427                 "with intra-module inlining only when using GC"
428             }
429         };
430 
431         let module = desc(module);
432         let host = desc(host);
433 
434         bail!("module was compiled {module} however the host is configured {host}")
435     }
436 }
437 
438 #[cfg(test)]
439 mod test {
440     use super::*;
441     use crate::{Cache, Config, Module, OptLevel};
442     use std::{
443         collections::hash_map::DefaultHasher,
444         hash::{Hash, Hasher},
445     };
446     use tempfile::TempDir;
447 
448     #[test]
449     fn test_architecture_mismatch() -> Result<()> {
450         let engine = Engine::default();
451         let mut metadata = Metadata::new(&engine)?;
452         metadata.target = "unknown-generic-linux".to_string();
453 
454         match metadata.check_compatible(&engine) {
455             Ok(_) => unreachable!(),
456             Err(e) => assert_eq!(
457                 e.to_string(),
458                 "Module was compiled for architecture 'unknown'",
459             ),
460         }
461 
462         Ok(())
463     }
464 
465     // Note that this test runs on a platform that is known to use Cranelift
466     #[test]
467     #[cfg(all(target_arch = "x86_64", not(miri)))]
468     fn test_os_mismatch() -> Result<()> {
469         let engine = Engine::default();
470         let mut metadata = Metadata::new(&engine)?;
471 
472         metadata.target = format!(
473             "{}-generic-unknown",
474             target_lexicon::Triple::host().architecture
475         );
476 
477         match metadata.check_compatible(&engine) {
478             Ok(_) => unreachable!(),
479             Err(e) => assert_eq!(
480                 e.to_string(),
481                 "Module was compiled for operating system 'unknown'",
482             ),
483         }
484 
485         Ok(())
486     }
487 
488     fn assert_contains(error: &Error, msg: &str) {
489         let msg = msg.trim();
490         if error.chain().any(|e| e.to_string().contains(msg)) {
491             return;
492         }
493 
494         panic!("failed to find:\n\n'''{msg}\n'''\n\nwithin error message:\n\n'''{error:?}'''")
495     }
496 
497     #[test]
498     fn test_cranelift_flags_mismatch() -> Result<()> {
499         let engine = Engine::default();
500         let mut metadata = Metadata::new(&engine)?;
501 
502         metadata
503             .shared_flags
504             .push(("preserve_frame_pointers", FlagValue::Bool(false)));
505 
506         match metadata.check_compatible(&engine) {
507             Ok(_) => unreachable!(),
508             Err(e) => {
509                 assert_contains(
510                     &e,
511                     "compilation settings of module incompatible with native host",
512                 );
513                 assert_contains(
514                     &e,
515                     "setting \"preserve_frame_pointers\" is configured to Bool(false) which is not supported",
516                 );
517             }
518         }
519 
520         Ok(())
521     }
522 
523     #[test]
524     fn test_isa_flags_mismatch() -> Result<()> {
525         let engine = Engine::default();
526         let mut metadata = Metadata::new(&engine)?;
527 
528         metadata
529             .isa_flags
530             .push(("not_a_flag", FlagValue::Bool(true)));
531 
532         match metadata.check_compatible(&engine) {
533             Ok(_) => unreachable!(),
534             Err(e) => {
535                 assert_contains(
536                     &e,
537                     "compilation settings of module incompatible with native host",
538                 );
539                 assert_contains(
540                     &e,
541                     "don't know how to test for target-specific flag \"not_a_flag\" at runtime",
542                 );
543             }
544         }
545 
546         Ok(())
547     }
548 
549     #[test]
550     #[cfg_attr(miri, ignore)]
551     #[cfg(target_pointer_width = "64")] // different defaults on 32-bit platforms
552     fn test_tunables_int_mismatch() -> Result<()> {
553         let engine = Engine::default();
554         let mut metadata = Metadata::new(&engine)?;
555 
556         metadata.tunables.memory_guard_size = 0;
557 
558         match metadata.check_compatible(&engine) {
559             Ok(_) => unreachable!(),
560             Err(e) => assert_eq!(
561                 e.to_string(),
562                 "Module was compiled with a memory guard size of '0' but '33554432' is expected for the host"
563             ),
564         }
565 
566         Ok(())
567     }
568 
569     #[test]
570     fn test_tunables_bool_mismatch() -> Result<()> {
571         let mut config = Config::new();
572         config.epoch_interruption(true);
573 
574         let engine = Engine::new(&config)?;
575         let mut metadata = Metadata::new(&engine)?;
576         metadata.tunables.epoch_interruption = false;
577 
578         match metadata.check_compatible(&engine) {
579             Ok(_) => unreachable!(),
580             Err(e) => assert_eq!(
581                 e.to_string(),
582                 "Module was compiled without epoch interruption but it is enabled for the host"
583             ),
584         }
585 
586         let mut config = Config::new();
587         config.epoch_interruption(false);
588 
589         let engine = Engine::new(&config)?;
590         let mut metadata = Metadata::new(&engine)?;
591         metadata.tunables.epoch_interruption = true;
592 
593         match metadata.check_compatible(&engine) {
594             Ok(_) => unreachable!(),
595             Err(e) => assert_eq!(
596                 e.to_string(),
597                 "Module was compiled with epoch interruption but it is not enabled for the host"
598             ),
599         }
600 
601         Ok(())
602     }
603 
604     /// This test is only run a platform that is known to implement threads
605     #[test]
606     #[cfg(all(target_arch = "x86_64", not(miri)))]
607     fn test_feature_mismatch() -> Result<()> {
608         let mut config = Config::new();
609         config.wasm_threads(true);
610 
611         let engine = Engine::new(&config)?;
612         let mut metadata = Metadata::new(&engine)?;
613         metadata.features &= !wasmparser::WasmFeatures::THREADS.bits();
614 
615         // If a feature is disabled in the module and enabled in the host,
616         // that's always ok.
617         metadata.check_compatible(&engine)?;
618 
619         let mut config = Config::new();
620         config.wasm_threads(false);
621 
622         let engine = Engine::new(&config)?;
623         let mut metadata = Metadata::new(&engine)?;
624         metadata.features |= wasmparser::WasmFeatures::THREADS.bits();
625 
626         match metadata.check_compatible(&engine) {
627             Ok(_) => unreachable!(),
628             Err(e) => assert_eq!(
629                 e.to_string(),
630                 "Module was compiled with support for WebAssembly feature \
631                 `threads` but it is not enabled for the host"
632             ),
633         }
634 
635         Ok(())
636     }
637 
638     #[test]
639     fn engine_weak_upgrades() {
640         let engine = Engine::default();
641         let weak = engine.weak();
642         weak.upgrade()
643             .expect("engine is still alive, so weak reference can upgrade");
644         drop(engine);
645         assert!(
646             weak.upgrade().is_none(),
647             "engine was dropped, so weak reference cannot upgrade"
648         );
649     }
650 
651     #[test]
652     #[cfg_attr(miri, ignore)]
653     fn cache_accounts_for_opt_level() -> Result<()> {
654         let _ = env_logger::try_init();
655 
656         let td = TempDir::new()?;
657         let config_path = td.path().join("config.toml");
658         std::fs::write(
659             &config_path,
660             &format!(
661                 "
662                     [cache]
663                     directory = '{}'
664                 ",
665                 td.path().join("cache").display()
666             ),
667         )?;
668         let mut cfg = Config::new();
669         cfg.cranelift_opt_level(OptLevel::None)
670             .cache(Some(Cache::from_file(Some(&config_path))?));
671         let engine = Engine::new(&cfg)?;
672         Module::new(&engine, "(module (func))")?;
673         let cache_config = engine
674             .config()
675             .cache
676             .as_ref()
677             .expect("Missing cache config");
678         assert_eq!(cache_config.cache_hits(), 0);
679         assert_eq!(cache_config.cache_misses(), 1);
680         Module::new(&engine, "(module (func))")?;
681         assert_eq!(cache_config.cache_hits(), 1);
682         assert_eq!(cache_config.cache_misses(), 1);
683 
684         let mut cfg = Config::new();
685         cfg.cranelift_opt_level(OptLevel::Speed)
686             .cache(Some(Cache::from_file(Some(&config_path))?));
687         let engine = Engine::new(&cfg)?;
688         let cache_config = engine
689             .config()
690             .cache
691             .as_ref()
692             .expect("Missing cache config");
693         Module::new(&engine, "(module (func))")?;
694         assert_eq!(cache_config.cache_hits(), 0);
695         assert_eq!(cache_config.cache_misses(), 1);
696         Module::new(&engine, "(module (func))")?;
697         assert_eq!(cache_config.cache_hits(), 1);
698         assert_eq!(cache_config.cache_misses(), 1);
699 
700         let mut cfg = Config::new();
701         cfg.cranelift_opt_level(OptLevel::SpeedAndSize)
702             .cache(Some(Cache::from_file(Some(&config_path))?));
703         let engine = Engine::new(&cfg)?;
704         let cache_config = engine
705             .config()
706             .cache
707             .as_ref()
708             .expect("Missing cache config");
709         Module::new(&engine, "(module (func))")?;
710         assert_eq!(cache_config.cache_hits(), 0);
711         assert_eq!(cache_config.cache_misses(), 1);
712         Module::new(&engine, "(module (func))")?;
713         assert_eq!(cache_config.cache_hits(), 1);
714         assert_eq!(cache_config.cache_misses(), 1);
715 
716         let mut cfg = Config::new();
717         cfg.debug_info(true)
718             .cache(Some(Cache::from_file(Some(&config_path))?));
719         let engine = Engine::new(&cfg)?;
720         let cache_config = engine
721             .config()
722             .cache
723             .as_ref()
724             .expect("Missing cache config");
725         Module::new(&engine, "(module (func))")?;
726         assert_eq!(cache_config.cache_hits(), 0);
727         assert_eq!(cache_config.cache_misses(), 1);
728         Module::new(&engine, "(module (func))")?;
729         assert_eq!(cache_config.cache_hits(), 1);
730         assert_eq!(cache_config.cache_misses(), 1);
731 
732         Ok(())
733     }
734 
735     #[test]
736     fn precompile_compatibility_key_accounts_for_opt_level() {
737         fn hash_for_config(cfg: &Config) -> u64 {
738             let engine = Engine::new(cfg).expect("Config should be valid");
739             let mut hasher = DefaultHasher::new();
740             engine.precompile_compatibility_hash().hash(&mut hasher);
741             hasher.finish()
742         }
743         let mut cfg = Config::new();
744         cfg.cranelift_opt_level(OptLevel::None);
745         let opt_none_hash = hash_for_config(&cfg);
746         cfg.cranelift_opt_level(OptLevel::Speed);
747         let opt_speed_hash = hash_for_config(&cfg);
748         assert_ne!(opt_none_hash, opt_speed_hash)
749     }
750 
751     #[test]
752     fn precompile_compatibility_key_accounts_for_module_version_strategy() -> Result<()> {
753         fn hash_for_config(cfg: &Config) -> u64 {
754             let engine = Engine::new(cfg).expect("Config should be valid");
755             let mut hasher = DefaultHasher::new();
756             engine.precompile_compatibility_hash().hash(&mut hasher);
757             hasher.finish()
758         }
759         let mut cfg_custom_version = Config::new();
760         cfg_custom_version.module_version(ModuleVersionStrategy::Custom("1.0.1111".to_string()))?;
761         let custom_version_hash = hash_for_config(&cfg_custom_version);
762 
763         let mut cfg_default_version = Config::new();
764         cfg_default_version.module_version(ModuleVersionStrategy::WasmtimeVersion)?;
765         let default_version_hash = hash_for_config(&cfg_default_version);
766 
767         let mut cfg_none_version = Config::new();
768         cfg_none_version.module_version(ModuleVersionStrategy::None)?;
769         let none_version_hash = hash_for_config(&cfg_none_version);
770 
771         assert_ne!(custom_version_hash, default_version_hash);
772         assert_ne!(custom_version_hash, none_version_hash);
773         assert_ne!(default_version_hash, none_version_hash);
774 
775         Ok(())
776     }
777 
778     #[test]
779     #[cfg_attr(miri, ignore)]
780     #[cfg(feature = "component-model")]
781     fn components_are_cached() -> Result<()> {
782         use crate::component::Component;
783 
784         let td = TempDir::new()?;
785         let config_path = td.path().join("config.toml");
786         std::fs::write(
787             &config_path,
788             &format!(
789                 "
790                     [cache]
791                     directory = '{}'
792                 ",
793                 td.path().join("cache").display()
794             ),
795         )?;
796         let mut cfg = Config::new();
797         cfg.cache(Some(Cache::from_file(Some(&config_path))?));
798         let engine = Engine::new(&cfg)?;
799         let cache_config = engine
800             .config()
801             .cache
802             .as_ref()
803             .expect("Missing cache config");
804         Component::new(&engine, "(component (core module (func)))")?;
805         assert_eq!(cache_config.cache_hits(), 0);
806         assert_eq!(cache_config.cache_misses(), 1);
807         Component::new(&engine, "(component (core module (func)))")?;
808         assert_eq!(cache_config.cache_hits(), 1);
809         assert_eq!(cache_config.cache_misses(), 1);
810 
811         Ok(())
812     }
813 }
814