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