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