-
Notifications
You must be signed in to change notification settings - Fork 167
Add version info to modules #294
Changes from all commits
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.
| Original file line number | Diff line number | Diff line change |
|---|---|---|
|
|
@@ -20,3 +20,4 @@ num-traits = "0.2" | |
| minisign = "0.5.11" | ||
| object = "0.12" | ||
| byteorder = "1.3" | ||
| memoffset = "0.5.1" | ||
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,100 @@ | ||
| 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 packagd 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], | ||
awortman-fastly marked this conversation as resolved.
Show resolved
Hide resolved
|
||
| } | ||
|
|
||
| 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 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 { | ||
| major: env!("CARGO_PKG_VERSION_MAJOR").parse().unwrap(), | ||
| minor: env!("CARGO_PKG_VERSION_MINOR").parse().unwrap(), | ||
| patch: env!("CARGO_PKG_VERSION_PATCH").parse().unwrap(), | ||
| reserved: 0x8000u16, | ||
| version_hash, | ||
| } | ||
| } | ||
| } | ||
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,16 @@ | ||
| use lucet_runtime::{DlModule, Error}; | ||
|
|
||
| #[test] | ||
| pub fn reject_old_modules() { | ||
| let err = DlModule::load("./tests/version_checks/old_module.so") | ||
|
Contributor
Author
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. I really don't like committing a 9.something kb binary blob but trying to assemble a minimal object wasn't working out and flipping a bit in a produced binary seems brittle. Keeping an honest to goodness old module to test against old modules seems best here.
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. That seems fine, as it's pretty small and unlikely to change.
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Note from future-me to past-me, what source was this module built from? |
||
| .err() | ||
| .unwrap(); | ||
|
|
||
| if let Error::ModuleError(e) = err { | ||
| let msg = format!("{}", e); | ||
| assert!(msg.contains("reserved bit is not set")); | ||
| assert!(msg.contains("module is likely too old")); | ||
| } else { | ||
| panic!("unexpected error loading module: {}", err); | ||
| } | ||
| } | ||
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,29 @@ | ||
| use std::env; | ||
| use std::fs::File; | ||
| use std::path::Path; | ||
|
|
||
| fn main() { | ||
| let commit_file_path = Path::new(&env::var("OUT_DIR").unwrap()).join("commit_hash"); | ||
| // in debug builds we only need the file to exist, but in release builds this will be used and | ||
| // requires mutability. | ||
| #[allow(unused_variables, unused_mut)] | ||
| let mut f = File::create(&commit_file_path).unwrap(); | ||
|
|
||
| // This is about the closest not-additional-feature-flag way to detect release builds. | ||
| // In debug builds, leave the `commit_hash` file empty to allow looser version checking and | ||
| // avoid impacting development workflows too much. | ||
| #[cfg(not(debug_assertions))] | ||
| { | ||
| use std::io::Write; | ||
| use std::process::Command; | ||
|
|
||
| let last_commit_hash = Command::new("git") | ||
| .args(&["log", "-n", "1", "--pretty=format:%H"]) | ||
| .output() | ||
| .ok(); | ||
|
|
||
| if let Some(last_commit_hash) = last_commit_hash { | ||
| f.write_all(&last_commit_hash.stdout).unwrap(); | ||
| } | ||
| } | ||
| } |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
finding this bug was surprisingly annoying. The test failed with a deserialization error because the pointer/length it tried to read became totally invalid, which lead me down a very incorrect path for a bit :(