1 use byteorder::{LittleEndian, ReadBytesExt, WriteBytesExt};
2 use std::cmp::min;
3 use std::fmt;
4 use std::io;
5 
6 /// VersionInfo is information about a Lucet module to allow the Lucet runtime to determine if or
7 /// how the module can be loaded, if so requested. The information here describes implementation
8 /// details in runtime support for `lucetc`-produced modules, and nothing higher level.
9 #[repr(C)]
10 #[derive(Debug, Clone, PartialEq, Eq)]
11 pub struct VersionInfo {
12     major: u16,
13     minor: u16,
14     patch: u16,
15     reserved: u16,
16     /// `version_hash` is either all nulls or the first eight ascii characters of the git commit
17     /// hash of wherever this Version is coming from. In the case of a compiled lucet module, this
18     /// hash will come from the git commit that the lucetc producing it came from. In a runtime
19     /// context, it will be the git commit of lucet-runtime built into the embedder.
20     ///
21     /// The version hash will typically populated only in release builds, but may blank even in
22     /// that case: if building from a packaged crate, or in a build environment that does not have
23     /// "git" installed, `lucetc` and `lucet-runtime` will fall back to an empty hash.
24     version_hash: [u8; 8],
25 }
26 
27 impl fmt::Display for VersionInfo {
fmt(&self, fmt: &mut fmt::Formatter<'_>) -> fmt::Result28     fn fmt(&self, fmt: &mut fmt::Formatter<'_>) -> fmt::Result {
29         write!(fmt, "{}.{}.{}", self.major, self.minor, self.patch)?;
30         if u64::from_ne_bytes(self.version_hash) != 0 {
31             write!(
32                 fmt,
33                 "-{}",
34                 std::str::from_utf8(&self.version_hash).unwrap_or("INVALID")
35             )?;
36         }
37         Ok(())
38     }
39 }
40 
41 impl VersionInfo {
new(major: u16, minor: u16, patch: u16, version_hash: [u8; 8]) -> VersionInfo42     pub fn new(major: u16, minor: u16, patch: u16, version_hash: [u8; 8]) -> VersionInfo {
43         VersionInfo {
44             major,
45             minor,
46             patch,
47             reserved: 0x8000,
48             version_hash,
49         }
50     }
51 
52     /// A more permissive version check than for version equality. This check will allow an `other`
53     /// version that is more specific than `self`, but matches for data that is available.
compatible_with(&self, other: &VersionInfo) -> bool54     pub fn compatible_with(&self, other: &VersionInfo) -> bool {
55         if !(self.valid() || other.valid()) {
56             return false;
57         }
58 
59         if self.major == other.major && self.minor == other.minor && self.patch == other.patch {
60             if self.version_hash == [0u8; 8] {
61                 // we aren't bound to a specific git commit, so anything goes.
62                 true
63             } else {
64                 self.version_hash == other.version_hash
65             }
66         } else {
67             false
68         }
69     }
70 
write_to<W: WriteBytesExt>(&self, w: &mut W) -> io::Result<()>71     pub fn write_to<W: WriteBytesExt>(&self, w: &mut W) -> io::Result<()> {
72         w.write_u16::<LittleEndian>(self.major)?;
73         w.write_u16::<LittleEndian>(self.minor)?;
74         w.write_u16::<LittleEndian>(self.patch)?;
75         w.write_u16::<LittleEndian>(self.reserved)?;
76         w.write(&self.version_hash).and_then(|written| {
77             if written != self.version_hash.len() {
78                 Err(io::Error::new(
79                     io::ErrorKind::Other,
80                     "unable to write full version hash",
81                 ))
82             } else {
83                 Ok(())
84             }
85         })
86     }
87 
read_from<R: ReadBytesExt>(r: &mut R) -> io::Result<Self>88     pub fn read_from<R: ReadBytesExt>(r: &mut R) -> io::Result<Self> {
89         let mut version_hash = [0u8; 8];
90         Ok(VersionInfo {
91             major: r.read_u16::<LittleEndian>()?,
92             minor: r.read_u16::<LittleEndian>()?,
93             patch: r.read_u16::<LittleEndian>()?,
94             reserved: r.read_u16::<LittleEndian>()?,
95             version_hash: {
96                 r.read_exact(&mut version_hash)?;
97                 version_hash
98             },
99         })
100     }
101 
valid(&self) -> bool102     pub fn valid(&self) -> bool {
103         self.reserved == 0x8000
104     }
105 
current(current_hash: &'static [u8]) -> Self106     pub fn current(current_hash: &'static [u8]) -> Self {
107         let mut version_hash = [0u8; 8];
108 
109         for i in 0..min(version_hash.len(), current_hash.len()) {
110             version_hash[i] = current_hash[i];
111         }
112 
113         // The reasoning for this is as follows:
114         // `SerializedModule`, in version before version information was introduced, began with a
115         // pointer - `module_data_ptr`. This pointer would be relocated to somewhere in user space
116         // for the embedder of `lucet-runtime`. On x86_64, hopefully, that's userland code in some
117         // OS, meaning the pointer will be a pointer to user memory, and will be below
118         // 0x8000_0000_0000_0000. By setting `reserved` to `0x8000`, we set what would be the
119         // highest bit in `module_data_ptr` in an old `lucet-runtime` and guarantee a segmentation
120         // fault when loading these newer modules with version information.
121         VersionInfo::new(
122             env!("CARGO_PKG_VERSION_MAJOR").parse().unwrap(),
123             env!("CARGO_PKG_VERSION_MINOR").parse().unwrap(),
124             env!("CARGO_PKG_VERSION_PATCH").parse().unwrap(),
125             version_hash,
126         )
127     }
128 }
129