1
  2
  3
  4
  5
  6
  7
  8
  9
 10
 11
 12
 13
 14
 15
 16
 17
 18
 19
 20
 21
 22
 23
 24
 25
 26
 27
 28
 29
 30
 31
 32
 33
 34
 35
 36
 37
 38
 39
 40
 41
 42
 43
 44
 45
 46
 47
 48
 49
 50
 51
 52
 53
 54
 55
 56
 57
 58
 59
 60
 61
 62
 63
 64
 65
 66
 67
 68
 69
 70
 71
 72
 73
 74
 75
 76
 77
 78
 79
 80
 81
 82
 83
 84
 85
 86
 87
 88
 89
 90
 91
 92
 93
 94
 95
 96
 97
 98
 99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
use byteorder::{LittleEndian, ReadBytesExt, WriteBytesExt};
use std::cmp::min;
use std::fmt;
use std::io;

/// VersionInfo is information about a Lucet module to allow the Lucet runtime to determine if or
/// how the module can be loaded, if so requested. The information here describes implementation
/// details in runtime support for `lucetc`-produced modules, and nothing higher level.
#[repr(C)]
#[derive(Debug, Clone, PartialEq, Eq)]
pub struct VersionInfo {
    major: u16,
    minor: u16,
    patch: u16,
    reserved: u16,
    /// `version_hash` is either all nulls or the first eight ascii characters of the git commit
    /// hash of wherever this Version is coming from. In the case of a compiled lucet module, this
    /// hash will come from the git commit that the lucetc producing it came from. In a runtime
    /// context, it will be the git commit of lucet-runtime built into the embedder.
    ///
    /// The version hash will typically populated only in release builds, but may blank even in
    /// that case: if building from a packaged crate, or in a build environment that does not have
    /// "git" installed, `lucetc` and `lucet-runtime` will fall back to an empty hash.
    version_hash: [u8; 8],
}

impl fmt::Display for VersionInfo {
    fn fmt(&self, fmt: &mut fmt::Formatter) -> fmt::Result {
        write!(fmt, "{}.{}.{}", self.major, self.minor, self.patch)?;
        if u64::from_ne_bytes(self.version_hash) != 0 {
            write!(
                fmt,
                "-{}",
                std::str::from_utf8(&self.version_hash).unwrap_or("INVALID")
            )?;
        }
        Ok(())
    }
}

impl VersionInfo {
    pub fn new(major: u16, minor: u16, patch: u16, version_hash: [u8; 8]) -> VersionInfo {
        VersionInfo {
            major,
            minor,
            patch,
            reserved: 0x8000,
            version_hash,
        }
    }

    /// A more permissive version check than for version equality. This check will allow an `other`
    /// version that is more specific than `self`, but matches for data that is available.
    pub fn compatible_with(&self, other: &VersionInfo) -> bool {
        if !(self.valid() || other.valid()) {
            return false;
        }

        if self.major == other.major && self.minor == other.minor && self.patch == other.patch {
            if self.version_hash == [0u8; 8] {
                // we aren't bound to a specific git commit, so anything goes.
                true
            } else {
                self.version_hash == other.version_hash
            }
        } else {
            false
        }
    }

    pub fn write_to<W: WriteBytesExt>(&self, w: &mut W) -> io::Result<()> {
        w.write_u16::<LittleEndian>(self.major)?;
        w.write_u16::<LittleEndian>(self.minor)?;
        w.write_u16::<LittleEndian>(self.patch)?;
        w.write_u16::<LittleEndian>(self.reserved)?;
        w.write(&self.version_hash).and_then(|written| {
            if written != self.version_hash.len() {
                Err(io::Error::new(
                    io::ErrorKind::Other,
                    "unable to write full version hash",
                ))
            } else {
                Ok(())
            }
        })
    }

    pub fn read_from<R: ReadBytesExt>(r: &mut R) -> io::Result<Self> {
        let mut version_hash = [0u8; 8];
        Ok(VersionInfo {
            major: r.read_u16::<LittleEndian>()?,
            minor: r.read_u16::<LittleEndian>()?,
            patch: r.read_u16::<LittleEndian>()?,
            reserved: r.read_u16::<LittleEndian>()?,
            version_hash: {
                r.read_exact(&mut version_hash)?;
                version_hash
            },
        })
    }

    pub fn valid(&self) -> bool {
        self.reserved == 0x8000
    }

    pub fn current(current_hash: &'static [u8]) -> Self {
        let mut version_hash = [0u8; 8];

        for i in 0..min(version_hash.len(), current_hash.len()) {
            version_hash[i] = current_hash[i];
        }

        // The reasoning for this is as follows:
        // `SerializedModule`, in version before version information was introduced, began with a
        // pointer - `module_data_ptr`. This pointer would be relocated to somewhere in user space
        // for the embedder of `lucet-runtime`. On x86_64, hopefully, that's userland code in some
        // OS, meaning the pointer will be a pointer to user memory, and will be below
        // 0x8000_0000_0000_0000. By setting `reserved` to `0x8000`, we set what would be the
        // highest bit in `module_data_ptr` in an old `lucet-runtime` and guarantee a segmentation
        // fault when loading these newer modules with version information.
        VersionInfo::new(
            env!("CARGO_PKG_VERSION_MAJOR").parse().unwrap(),
            env!("CARGO_PKG_VERSION_MINOR").parse().unwrap(),
            env!("CARGO_PKG_VERSION_PATCH").parse().unwrap(),
            version_hash,
        )
    }
}