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