wasmtime/engine/
serialization.rs

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 `bincode`-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
24use crate::{Engine, ModuleVersionStrategy};
25use anyhow::{anyhow, bail, Context, Result};
26use object::write::{Object, StandardSegment};
27use object::{File, FileFlags, Object as _, ObjectSection, SectionKind};
28use serde::{Deserialize, Serialize};
29use std::collections::BTreeMap;
30use std::str::FromStr;
31use wasmtime_environ::obj;
32use wasmtime_environ::{FlagValue, ObjectKind, Tunables};
33use wasmtime_runtime::MmapVec;
34
35const VERSION: u8 = 0;
36
37/// Produces a blob of bytes by serializing the `engine`'s configuration data to
38/// be checked, perhaps in a different process, with the `check_compatible`
39/// method below.
40///
41/// The blob of bytes is inserted into the object file specified to become part
42/// of the final compiled artifact.
43#[cfg(compiler)]
44pub fn append_compiler_info(engine: &Engine, obj: &mut Object<'_>) {
45    let section = obj.add_section(
46        obj.segment_name(StandardSegment::Data).to_vec(),
47        obj::ELF_WASM_ENGINE.as_bytes().to_vec(),
48        SectionKind::ReadOnlyData,
49    );
50    let mut data = Vec::new();
51    data.push(VERSION);
52    let version = match &engine.config().module_version {
53        ModuleVersionStrategy::WasmtimeVersion => env!("CARGO_PKG_VERSION"),
54        ModuleVersionStrategy::Custom(c) => c,
55        ModuleVersionStrategy::None => "",
56    };
57    // This precondition is checked in Config::module_version:
58    assert!(
59        version.len() < 256,
60        "package version must be less than 256 bytes"
61    );
62    data.push(version.len() as u8);
63    data.extend_from_slice(version.as_bytes());
64    bincode::serialize_into(&mut data, &Metadata::new(engine)).unwrap();
65    obj.set_section_data(section, data, 1);
66}
67
68/// Verifies that the serialized engine in `mmap` is compatible with the
69/// `engine` provided.
70///
71/// This function will verify that the `mmap` provided can be deserialized
72/// successfully and that the contents are all compatible with the `engine`
73/// provided here, notably compatible wasm features are enabled, compatible
74/// compiler options, etc. If a mismatch is found and the compilation metadata
75/// specified is incompatible then an error is returned.
76pub fn check_compatible(engine: &Engine, mmap: &MmapVec, expected: ObjectKind) -> Result<()> {
77    // Parse the input `mmap` as an ELF file and see if the header matches the
78    // Wasmtime-generated header. This includes a Wasmtime-specific `os_abi` and
79    // the `e_flags` field should indicate whether `expected` matches or not.
80    //
81    // Note that errors generated here could mean that a precompiled module was
82    // loaded as a component, or vice versa, both of which aren't supposed to
83    // work.
84    //
85    // Ideally we'd only `File::parse` once and avoid the linear
86    // `section_by_name` search here but the general serialization code isn't
87    // structured well enough to make this easy and additionally it's not really
88    // a perf issue right now so doing that is left for another day's
89    // refactoring.
90    let obj = File::parse(&mmap[..]).context("failed to parse precompiled artifact as an ELF")?;
91    let expected_e_flags = match expected {
92        ObjectKind::Module => obj::EF_WASMTIME_MODULE,
93        ObjectKind::Component => obj::EF_WASMTIME_COMPONENT,
94    };
95    match obj.flags() {
96        FileFlags::Elf {
97            os_abi: obj::ELFOSABI_WASMTIME,
98            abi_version: 0,
99            e_flags,
100        } if e_flags == expected_e_flags => {}
101        _ => bail!("incompatible object file format"),
102    }
103
104    let data = obj
105        .section_by_name(obj::ELF_WASM_ENGINE)
106        .ok_or_else(|| anyhow!("failed to find section `{}`", obj::ELF_WASM_ENGINE))?
107        .data()?;
108    let (first, data) = data
109        .split_first()
110        .ok_or_else(|| anyhow!("invalid engine section"))?;
111    if *first != VERSION {
112        bail!("mismatched version in engine section");
113    }
114    let (len, data) = data
115        .split_first()
116        .ok_or_else(|| anyhow!("invalid engine section"))?;
117    let len = usize::from(*len);
118    let (version, data) = if data.len() < len + 1 {
119        bail!("engine section too small")
120    } else {
121        data.split_at(len)
122    };
123
124    match &engine.config().module_version {
125        ModuleVersionStrategy::WasmtimeVersion => {
126            let version = std::str::from_utf8(version)?;
127            if version != env!("CARGO_PKG_VERSION") {
128                bail!(
129                    "Module was compiled with incompatible Wasmtime version '{}'",
130                    version
131                );
132            }
133        }
134        ModuleVersionStrategy::Custom(v) => {
135            let version = std::str::from_utf8(&version)?;
136            if version != v {
137                bail!(
138                    "Module was compiled with incompatible version '{}'",
139                    version
140                );
141            }
142        }
143        ModuleVersionStrategy::None => { /* ignore the version info, accept all */ }
144    }
145    bincode::deserialize::<Metadata>(data)?.check_compatible(engine)
146}
147
148#[derive(Serialize, Deserialize)]
149struct Metadata {
150    target: String,
151    shared_flags: BTreeMap<String, FlagValue>,
152    isa_flags: BTreeMap<String, FlagValue>,
153    tunables: Tunables,
154    features: WasmFeatures,
155}
156
157// This exists because `wasmparser::WasmFeatures` isn't serializable
158#[derive(Debug, Copy, Clone, Serialize, Deserialize)]
159struct WasmFeatures {
160    reference_types: bool,
161    multi_value: bool,
162    bulk_memory: bool,
163    component_model: bool,
164    simd: bool,
165    threads: bool,
166    multi_memory: bool,
167    exceptions: bool,
168    memory64: bool,
169    relaxed_simd: bool,
170    extended_const: bool,
171}
172
173impl Metadata {
174    #[cfg(compiler)]
175    fn new(engine: &Engine) -> Metadata {
176        let wasmparser::WasmFeatures {
177            reference_types,
178            multi_value,
179            bulk_memory,
180            component_model,
181            simd,
182            threads,
183            tail_call,
184            multi_memory,
185            exceptions,
186            memory64,
187            relaxed_simd,
188            extended_const,
189            memory_control,
190            function_references,
191
192            // Always on; we don't currently have knobs for these.
193            mutable_global: _,
194            saturating_float_to_int: _,
195            sign_extension: _,
196            floats: _,
197        } = engine.config().features;
198
199        assert!(!memory_control);
200        assert!(!tail_call);
201        assert!(!function_references);
202
203        Metadata {
204            target: engine.compiler().triple().to_string(),
205            shared_flags: engine.compiler().flags(),
206            isa_flags: engine.compiler().isa_flags(),
207            tunables: engine.config().tunables.clone(),
208            features: WasmFeatures {
209                reference_types,
210                multi_value,
211                bulk_memory,
212                component_model,
213                simd,
214                threads,
215                multi_memory,
216                exceptions,
217                memory64,
218                relaxed_simd,
219                extended_const,
220            },
221        }
222    }
223
224    fn check_compatible(mut self, engine: &Engine) -> Result<()> {
225        self.check_triple(engine)?;
226        self.check_shared_flags(engine)?;
227        self.check_isa_flags(engine)?;
228        self.check_tunables(&engine.config().tunables)?;
229        self.check_features(&engine.config().features)?;
230        Ok(())
231    }
232
233    fn check_triple(&self, engine: &Engine) -> Result<()> {
234        let engine_target = engine.target();
235        let module_target =
236            target_lexicon::Triple::from_str(&self.target).map_err(|e| anyhow!(e))?;
237
238        if module_target.architecture != engine_target.architecture {
239            bail!(
240                "Module was compiled for architecture '{}'",
241                module_target.architecture
242            );
243        }
244
245        if module_target.operating_system != engine_target.operating_system {
246            bail!(
247                "Module was compiled for operating system '{}'",
248                module_target.operating_system
249            );
250        }
251
252        Ok(())
253    }
254
255    fn check_shared_flags(&mut self, engine: &Engine) -> Result<()> {
256        for (name, val) in self.shared_flags.iter() {
257            engine
258                .check_compatible_with_shared_flag(name, val)
259                .map_err(|s| anyhow::Error::msg(s))
260                .context("compilation settings of module incompatible with native host")?;
261        }
262        Ok(())
263    }
264
265    fn check_isa_flags(&mut self, engine: &Engine) -> Result<()> {
266        for (name, val) in self.isa_flags.iter() {
267            engine
268                .check_compatible_with_isa_flag(name, val)
269                .map_err(|s| anyhow::Error::msg(s))
270                .context("compilation settings of module incompatible with native host")?;
271        }
272        Ok(())
273    }
274
275    fn check_int<T: Eq + std::fmt::Display>(found: T, expected: T, feature: &str) -> Result<()> {
276        if found == expected {
277            return Ok(());
278        }
279
280        bail!(
281            "Module was compiled with a {} of '{}' but '{}' is expected for the host",
282            feature,
283            found,
284            expected
285        );
286    }
287
288    fn check_bool(found: bool, expected: bool, feature: &str) -> Result<()> {
289        if found == expected {
290            return Ok(());
291        }
292
293        bail!(
294            "Module was compiled {} {} but it {} enabled for the host",
295            if found { "with" } else { "without" },
296            feature,
297            if expected { "is" } else { "is not" }
298        );
299    }
300
301    fn check_tunables(&mut self, other: &Tunables) -> Result<()> {
302        let Tunables {
303            static_memory_bound,
304            static_memory_offset_guard_size,
305            dynamic_memory_offset_guard_size,
306            generate_native_debuginfo,
307            parse_wasm_debuginfo,
308            consume_fuel,
309            epoch_interruption,
310            static_memory_bound_is_maximum,
311            guard_before_linear_memory,
312            relaxed_simd_deterministic,
313
314            // This doesn't affect compilation, it's just a runtime setting.
315            dynamic_memory_growth_reserve: _,
316
317            // This does technically affect compilation but modules with/without
318            // trap information can be loaded into engines with the opposite
319            // setting just fine (it's just a section in the compiled file and
320            // whether it's present or not)
321            generate_address_map: _,
322
323            // Just a debugging aid, doesn't affect functionality at all.
324            debug_adapter_modules: _,
325        } = self.tunables;
326
327        Self::check_int(
328            static_memory_bound,
329            other.static_memory_bound,
330            "static memory bound",
331        )?;
332        Self::check_int(
333            static_memory_offset_guard_size,
334            other.static_memory_offset_guard_size,
335            "static memory guard size",
336        )?;
337        Self::check_int(
338            dynamic_memory_offset_guard_size,
339            other.dynamic_memory_offset_guard_size,
340            "dynamic memory guard size",
341        )?;
342        Self::check_bool(
343            generate_native_debuginfo,
344            other.generate_native_debuginfo,
345            "debug information support",
346        )?;
347        Self::check_bool(
348            parse_wasm_debuginfo,
349            other.parse_wasm_debuginfo,
350            "WebAssembly backtrace support",
351        )?;
352        Self::check_bool(consume_fuel, other.consume_fuel, "fuel support")?;
353        Self::check_bool(
354            epoch_interruption,
355            other.epoch_interruption,
356            "epoch interruption",
357        )?;
358        Self::check_bool(
359            static_memory_bound_is_maximum,
360            other.static_memory_bound_is_maximum,
361            "pooling allocation support",
362        )?;
363        Self::check_bool(
364            guard_before_linear_memory,
365            other.guard_before_linear_memory,
366            "guard before linear memory",
367        )?;
368        Self::check_bool(
369            relaxed_simd_deterministic,
370            other.relaxed_simd_deterministic,
371            "relaxed simd deterministic semantics",
372        )?;
373
374        Ok(())
375    }
376
377    fn check_features(&mut self, other: &wasmparser::WasmFeatures) -> Result<()> {
378        let WasmFeatures {
379            reference_types,
380            multi_value,
381            bulk_memory,
382            component_model,
383            simd,
384            threads,
385            multi_memory,
386            exceptions,
387            memory64,
388            relaxed_simd,
389            extended_const,
390        } = self.features;
391
392        Self::check_bool(
393            reference_types,
394            other.reference_types,
395            "WebAssembly reference types support",
396        )?;
397        Self::check_bool(
398            multi_value,
399            other.multi_value,
400            "WebAssembly multi-value support",
401        )?;
402        Self::check_bool(
403            bulk_memory,
404            other.bulk_memory,
405            "WebAssembly bulk memory support",
406        )?;
407        Self::check_bool(
408            component_model,
409            other.component_model,
410            "WebAssembly component model support",
411        )?;
412        Self::check_bool(simd, other.simd, "WebAssembly SIMD support")?;
413        Self::check_bool(threads, other.threads, "WebAssembly threads support")?;
414        Self::check_bool(
415            multi_memory,
416            other.multi_memory,
417            "WebAssembly multi-memory support",
418        )?;
419        Self::check_bool(
420            exceptions,
421            other.exceptions,
422            "WebAssembly exceptions support",
423        )?;
424        Self::check_bool(
425            memory64,
426            other.memory64,
427            "WebAssembly 64-bit memory support",
428        )?;
429        Self::check_bool(
430            extended_const,
431            other.extended_const,
432            "WebAssembly extended-const support",
433        )?;
434        Self::check_bool(
435            relaxed_simd,
436            other.relaxed_simd,
437            "WebAssembly relaxed-simd support",
438        )?;
439
440        Ok(())
441    }
442}
443
444#[cfg(test)]
445mod test {
446    use super::*;
447    use crate::Config;
448
449    #[test]
450    fn test_architecture_mismatch() -> Result<()> {
451        let engine = Engine::default();
452        let mut metadata = Metadata::new(&engine);
453        metadata.target = "unknown-generic-linux".to_string();
454
455        match metadata.check_compatible(&engine) {
456            Ok(_) => unreachable!(),
457            Err(e) => assert_eq!(
458                e.to_string(),
459                "Module was compiled for architecture 'unknown'",
460            ),
461        }
462
463        Ok(())
464    }
465
466    #[test]
467    fn test_os_mismatch() -> Result<()> {
468        let engine = Engine::default();
469        let mut metadata = Metadata::new(&engine);
470
471        metadata.target = format!(
472            "{}-generic-unknown",
473            target_lexicon::Triple::host().architecture
474        );
475
476        match metadata.check_compatible(&engine) {
477            Ok(_) => unreachable!(),
478            Err(e) => assert_eq!(
479                e.to_string(),
480                "Module was compiled for operating system 'unknown'",
481            ),
482        }
483
484        Ok(())
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.shared_flags.insert(
493            "preserve_frame_pointers".to_string(),
494            FlagValue::Bool(false),
495        );
496
497        match metadata.check_compatible(&engine) {
498            Ok(_) => unreachable!(),
499            Err(e) => assert!(format!("{:?}", e).starts_with(
500                "\
501compilation settings of module incompatible with native host
502
503Caused by:
504    setting \"preserve_frame_pointers\" is configured to Bool(false) which is not supported"
505            )),
506        }
507
508        Ok(())
509    }
510
511    #[test]
512    fn test_isa_flags_mismatch() -> Result<()> {
513        let engine = Engine::default();
514        let mut metadata = Metadata::new(&engine);
515
516        metadata
517            .isa_flags
518            .insert("not_a_flag".to_string(), FlagValue::Bool(true));
519
520        match metadata.check_compatible(&engine) {
521            Ok(_) => unreachable!(),
522            Err(e) => assert!(format!("{:?}", e).starts_with(
523                "\
524compilation settings of module incompatible with native host
525
526Caused by:
527    cannot test if target-specific flag \"not_a_flag\" is available at runtime",
528            )),
529        }
530
531        Ok(())
532    }
533
534    #[test]
535    fn test_tunables_int_mismatch() -> Result<()> {
536        let engine = Engine::default();
537        let mut metadata = Metadata::new(&engine);
538
539        metadata.tunables.static_memory_offset_guard_size = 0;
540
541        match metadata.check_compatible(&engine) {
542            Ok(_) => unreachable!(),
543            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"),
544        }
545
546        Ok(())
547    }
548
549    #[test]
550    fn test_tunables_bool_mismatch() -> Result<()> {
551        let mut config = Config::new();
552        config.epoch_interruption(true);
553
554        let engine = Engine::new(&config)?;
555        let mut metadata = Metadata::new(&engine);
556        metadata.tunables.epoch_interruption = false;
557
558        match metadata.check_compatible(&engine) {
559            Ok(_) => unreachable!(),
560            Err(e) => assert_eq!(
561                e.to_string(),
562                "Module was compiled without epoch interruption but it is enabled for the host"
563            ),
564        }
565
566        let mut config = Config::new();
567        config.epoch_interruption(false);
568
569        let engine = Engine::new(&config)?;
570        let mut metadata = Metadata::new(&engine);
571        metadata.tunables.epoch_interruption = true;
572
573        match metadata.check_compatible(&engine) {
574            Ok(_) => unreachable!(),
575            Err(e) => assert_eq!(
576                e.to_string(),
577                "Module was compiled with epoch interruption but it is not enabled for the host"
578            ),
579        }
580
581        Ok(())
582    }
583
584    #[test]
585    fn test_feature_mismatch() -> Result<()> {
586        let mut config = Config::new();
587        config.wasm_simd(true);
588
589        let engine = Engine::new(&config)?;
590        let mut metadata = Metadata::new(&engine);
591        metadata.features.simd = false;
592
593        match metadata.check_compatible(&engine) {
594            Ok(_) => unreachable!(),
595            Err(e) => assert_eq!(e.to_string(), "Module was compiled without WebAssembly SIMD support but it is enabled for the host"),
596        }
597
598        let mut config = Config::new();
599        config.wasm_simd(false);
600
601        let engine = Engine::new(&config)?;
602        let mut metadata = Metadata::new(&engine);
603        metadata.features.simd = true;
604
605        match metadata.check_compatible(&engine) {
606            Ok(_) => unreachable!(),
607            Err(e) => assert_eq!(e.to_string(), "Module was compiled with WebAssembly SIMD support but it is not enabled for the host"),
608        }
609
610        Ok(())
611    }
612}