From 150e9916e8bece2d9a3b45d37c5c32ae01bd6ea7 Mon Sep 17 00:00:00 2001
From: Jake Shadle
Date: Sat, 24 Feb 2024 01:21:52 +0100
Subject: [PATCH 1/2] Cleanup .so version parsing
---
src/linux/maps_reader.rs | 254 ++++++++++++++++++++++++---------
src/linux/sections/mappings.rs | 21 +--
2 files changed, 198 insertions(+), 77 deletions(-)
diff --git a/src/linux/maps_reader.rs b/src/linux/maps_reader.rs
index 6e96d0b2..18355d4c 100644
--- a/src/linux/maps_reader.rs
+++ b/src/linux/maps_reader.rs
@@ -289,10 +289,9 @@ impl MappingInfo {
true
}
- fn elf_file_so_name(&self) -> Result {
- // Find the shared object name (SONAME) by examining the ELF information
- // for |mapping|. If the SONAME is found copy it into the passed buffer
- // |soname| and return true. The size of the buffer is |soname_size|.
+ /// Find the shared object name (SONAME) by examining the ELF information
+ /// for the mapping.
+ fn so_name(&self) -> Result {
let mapped_file = MappingInfo::get_mmap(&self.name, self.offset)?;
let elf_obj = elf::Elf::parse(&mapped_file)?;
@@ -303,44 +302,18 @@ impl MappingInfo {
Ok(soname.to_string())
}
- /// Attempts to retrieve the .so version of the elf via its filename as a
- /// `(major, minor, release)` triplet
- fn elf_file_so_version(&self) -> (u32, u32, u32) {
- const DEF: (u32, u32, u32) = (0, 0, 0);
- let Some(so_name) = self.name.as_deref() else {
- return DEF;
+ #[inline]
+ fn so_version(&self) -> Option {
+ let Some(name) = self.name.as_deref() else {
+ return None;
};
- let Some(filename) = std::path::Path::new(so_name).file_name() else {
- return DEF;
- };
-
- // Avoid an allocation unless the string contains non-utf8
- let filename = filename.to_string_lossy();
-
- let Some((_, version)) = filename.split_once(".so.") else {
- return DEF;
- };
-
- let mut triplet = [0, 0, 0];
-
- for (so, trip) in version.split('.').zip(triplet.iter_mut()) {
- // In some cases the release/patch version is alphanumeric (eg. '2rc5'),
- // so try to parse as much as we can rather than completely ignoring
- for digit in so
- .chars()
- .filter_map(|c: char| c.is_ascii_digit().then_some(c as u8 - b'0'))
- {
- *trip *= 10;
- *trip += digit as u32;
- }
- }
- (triplet[0], triplet[1], triplet[2])
+ SoVersion::parse(name)
}
pub fn get_mapping_effective_path_name_and_version(
&self,
- ) -> Result<(PathBuf, String, (u32, u32, u32))> {
+ ) -> Result<(PathBuf, String, Option)> {
let mut file_path = PathBuf::from(self.name.clone().unwrap_or_default());
// Tools such as minidump_stackwalk use the name of the module to look up
@@ -349,16 +322,15 @@ impl MappingInfo {
// filesystem name of the module.
// Just use the filesystem name if no SONAME is present.
- let file_name = if let Ok(name) = self.elf_file_so_name() {
- name
- } else {
+ let Ok(file_name) = self.so_name() else {
// file_path := /path/to/libname.so
// file_name := libname.so
let file_name = file_path
.file_name()
.map(|s| s.to_string_lossy().into_owned())
.unwrap_or_default();
- return Ok((file_path, file_name, self.elf_file_so_version()));
+
+ return Ok((file_path, file_name, self.so_version()));
};
if self.is_executable() && self.offset != 0 {
@@ -374,7 +346,7 @@ impl MappingInfo {
file_path.set_file_name(&file_name);
}
- Ok((file_path, file_name, self.elf_file_so_version()))
+ Ok((file_path, file_name, self.so_version()))
}
pub fn is_contained_in(&self, user_mapping_list: &MappingList) -> bool {
@@ -419,6 +391,92 @@ impl MappingInfo {
}
}
+/// Version metadata retrieved from an .so filename
+///
+/// There is no standard for .so version numbers so this implementation just
+/// does a best effort to pull as much data as it can based on real .so schemes
+/// seen
+///
+/// That being said, the [libtool](https://www.gnu.org/software/libtool/manual/html_node/Libtool-versioning.html)
+/// versioning scheme is fairly common
+#[cfg_attr(test, derive(PartialEq, Debug, Default))]
+pub struct SoVersion {
+ /// Might be non-zero if there is at least one non-zero numeric component after .so.
+ ///
+ /// Equivalent to `current` in libtool versions
+ pub major: u32,
+ /// The numeric component after the major version, if any
+ ///
+ /// Equivalent to `revision` in libtool versions
+ pub minor: u32,
+ /// The numeric component after the minor version, if any
+ ///
+ /// Equivalent to `age` in libtool versions
+ pub patch: u32,
+ /// The patch component may contain additional non-numeric metadata similar
+ /// to a semver prelease, this is any numeric data that suffixes that prerelease
+ /// string
+ pub prerelease: u32,
+}
+
+impl SoVersion {
+ /// Attempts to retrieve the .so version of the elf path via its filename
+ fn parse(so_path: &OsStr) -> Option {
+ let Some(filename) = std::path::Path::new(so_path).file_name() else {
+ return None;
+ };
+
+ // Avoid an allocation unless the string contains non-utf8
+ let filename = filename.to_string_lossy();
+
+ let Some((_, version)) = filename.split_once(".so.") else {
+ return None;
+ };
+
+ let mut sov = Self {
+ major: 0,
+ minor: 0,
+ patch: 0,
+ prerelease: 0,
+ };
+
+ let comps = [
+ &mut sov.major,
+ &mut sov.minor,
+ &mut sov.patch,
+ &mut sov.prerelease,
+ ];
+
+ for (i, comp) in version.split('.').enumerate() {
+ if i <= 1 {
+ *comps[i] = comp.parse().unwrap_or_default();
+ } else {
+ // In some cases the release/patch version is alphanumeric (eg. '2rc5'),
+ // so try to parse either a single or two numbers
+ if let Some(pend) = dbg!(comp).find(|c: char| !c.is_ascii_digit()) {
+ if let Ok(patch) = comp[..pend].parse() {
+ *comps[i] = patch;
+ }
+
+ if i >= comps.len() - 1 {
+ break;
+ }
+ if let Some(pre) = comp.rfind(|c: char| !c.is_ascii_digit()) {
+ if let Ok(pre) = comp[pre + 1..].parse() {
+ *comps[i + 1] = pre;
+ break;
+ }
+ }
+ } else {
+ *comps[i] = comp.parse().unwrap_or_default();
+ }
+ }
+ }
+
+ Some(sov)
+ }
+}
+
#[cfg(test)]
#[cfg(target_pointer_width = "64")] // All addresses are 64 bit and I'm currently too lazy to adjust it to work for both
mod tests {
@@ -674,36 +732,94 @@ a4840000-a4873000 rw-p 09021000 08:12 393449 /data/app/org.mozilla.firefox-1
#[test]
fn test_elf_file_so_version() {
- let mappings = get_mappings_for(
- "\
-7f877ab9f000-7f877aba0000 rw-p 0001f000 00:1b 100457459 /home/alex/bin/firefox/libmozsandbox.so
-7f877ae65000-7f877ae68000 rw-p 00265000 00:1b 90432393 /usr/lib/x86_64-linux-gnu/libstdc++.so.6.0.32
-7f877ae76000-7f877ae77000 rw-p 0000a000 00:1b 90443112 /usr/lib/x86_64-linux-gnu/libcairo-gobject.so.2.11800.0
-7f877ae7c000-7f877ae8c000 r--p 00000000 00:1b 93439971 /usr/lib/x86_64-linux-gnu/libm.so.6
-7f877af70000-7f877af71000 rw-p 00003000 00:1b 93439980 /usr/lib/x86_64-linux-gnu/libpthread.so.0
-7f877af78000-7f877af79000 rw-p 00005000 00:1b 90423049 /usr/lib/x86_64-linux-gnu/libgmodule-2.0.so.0.7800.0
-7f877ae7c000-7f877ae8c000 rw-p 00000000 00:1b 93439971 /usr/lib/x86_64-linux-gnu/libabsl_time_zone.so.20220623.0.0
-7f877ae7c000-7f877ae8c000 rw-p 00000000 00:1b 93439971 /usr/lib/x86_64-linux-gnu/libdbus-1.so.3.34.2rc5
-7f877ae7c000-7f877ae8c000 rw-p 00000000 00:1b 93439971 /usr/lib/x86_64-linux-gnu/libtoto.so.AAA",
- 0x7ffe091bf000,
- );
- assert_eq!(mappings.len(), 9);
-
- let expected = [
- (0, 0, 0),
- (6, 0, 32),
- (2, 11800, 0),
- (6, 0, 0),
- (0, 0, 0),
- (0, 7800, 0),
- (20220623, 0, 0),
- (3, 34, 25),
- (0, 0, 0),
+ let test_cases = [
+ ("/home/alex/bin/firefox/libmozsandbox.so", None),
+ (
+ "/usr/lib/x86_64-linux-gnu/libstdc++.so.6.0.32",
+ Some(SoVersion {
+ major: 6,
+ patch: 32,
+ ..Default::default()
+ }),
+ ),
+ (
+ "/usr/lib/x86_64-linux-gnu/libcairo-gobject.so.2.11800.0",
+ Some(SoVersion {
+ major: 2,
+ minor: 11800,
+ ..Default::default()
+ }),
+ ),
+ (
+ "/usr/lib/x86_64-linux-gnu/libm.so.6",
+ Some(SoVersion {
+ major: 6,
+ ..Default::default()
+ }),
+ ),
+ (
+ "/usr/lib/x86_64-linux-gnu/libpthread.so.0",
+ Some(SoVersion::default()),
+ ),
+ (
+ "/usr/lib/x86_64-linux-gnu/libgmodule-2.0.so.0.7800.0",
+ Some(SoVersion {
+ minor: 7800,
+ ..Default::default()
+ }),
+ ),
+ (
+ "/usr/lib/x86_64-linux-gnu/libabsl_time_zone.so.20220623.0.0",
+ Some(SoVersion {
+ major: 20220623,
+ ..Default::default()
+ }),
+ ),
+ (
+ "/usr/lib/x86_64-linux-gnu/libdbus-1.so.3.34.2rc5",
+ Some(SoVersion {
+ major: 3,
+ minor: 34,
+ patch: 2,
+ prerelease: 5,
+ }),
+ ),
+ (
+ "/usr/lib/x86_64-linux-gnu/libdbus-1.so.3.34.2rc",
+ Some(SoVersion {
+ major: 3,
+ minor: 34,
+ patch: 2,
+ prerelease: 0,
+ }),
+ ),
+ (
+ "/usr/lib/x86_64-linux-gnu/libdbus-1.so.3.34.rc5",
+ Some(SoVersion {
+ major: 3,
+ minor: 34,
+ patch: 0,
+ prerelease: 5,
+ }),
+ ),
+ (
+ "/usr/lib/x86_64-linux-gnu/libtoto.so.AAA",
+ Some(SoVersion::default()),
+ ),
+ (
+ "/usr/lib/x86_64-linux-gnu/libsemver-1.so.1.2.alpha.1",
+ Some(SoVersion {
+ major: 1,
+ minor: 2,
+ patch: 0,
+ prerelease: 1,
+ }),
+ ),
];
- for (i, (map, exp)) in mappings.into_iter().zip(expected).enumerate() {
- let version = map.elf_file_so_version();
- assert_eq!(version, exp, "{i}");
+ for (path, expected) in test_cases {
+ let actual = SoVersion::parse(OsStr::new(path));
+ assert_eq!(actual, expected);
}
}
diff --git a/src/linux/sections/mappings.rs b/src/linux/sections/mappings.rs
index a92739ea..9012ae35 100644
--- a/src/linux/sections/mappings.rs
+++ b/src/linux/sections/mappings.rs
@@ -83,24 +83,29 @@ fn fill_raw_module(
sig_section.location()
};
- let (file_path, _, (major, minor, release)) = mapping
+ let (file_path, _, so_version) = mapping
.get_mapping_effective_path_name_and_version()
.map_err(|e| errors::SectionMappingsError::GetEffectivePathError(mapping.clone(), e))?;
let name_header = write_string_to_location(buffer, file_path.to_string_lossy().as_ref())?;
- let mut raw_module = MDRawModule {
+ let version_info = so_version.map_or(Default::default(), |sov| format::VS_FIXEDFILEINFO {
+ signature: format::VS_FFI_SIGNATURE,
+ struct_version: format::VS_FFI_STRUCVERSION,
+ file_version_hi: sov.major,
+ file_version_lo: sov.minor,
+ product_version_hi: sov.patch,
+ product_version_lo: sov.prerelease,
+ ..Default::default()
+ });
+
+ let raw_module = MDRawModule {
base_of_image: mapping.start_address as u64,
size_of_image: mapping.size as u32,
cv_record,
module_name_rva: name_header.rva,
+ version_info,
..Default::default()
};
- raw_module.version_info.signature = format::VS_FFI_SIGNATURE;
- raw_module.version_info.struct_version = format::VS_FFI_STRUCVERSION;
- raw_module.version_info.file_version_hi = major;
- raw_module.version_info.file_version_lo = minor;
- raw_module.version_info.product_version_hi = release;
-
Ok(raw_module)
}
From fc3520bbc220d0251d79d3b5d042892059f7cb35 Mon Sep 17 00:00:00 2001
From: Jake Shadle
Date: Sat, 24 Feb 2024 01:38:55 +0100
Subject: [PATCH 2/2] Compact test
---
src/linux/maps_reader.rs | 109 +++++++++------------------------------
1 file changed, 24 insertions(+), 85 deletions(-)
diff --git a/src/linux/maps_reader.rs b/src/linux/maps_reader.rs
index 18355d4c..bf54e419 100644
--- a/src/linux/maps_reader.rs
+++ b/src/linux/maps_reader.rs
@@ -399,7 +399,7 @@ impl MappingInfo {
///
/// That being said, the [libtool](https://www.gnu.org/software/libtool/manual/html_node/Libtool-versioning.html)
/// versioning scheme is fairly common
-#[cfg_attr(test, derive(PartialEq, Debug, Default))]
+#[cfg_attr(test, derive(Debug))]
pub struct SoVersion {
/// Might be non-zero if there is at least one non-zero numeric component after .so.
///
@@ -453,7 +453,7 @@ impl SoVersion {
} else {
// In some cases the release/patch version is alphanumeric (eg. '2rc5'),
// so try to parse either a single or two numbers
- if let Some(pend) = dbg!(comp).find(|c: char| !c.is_ascii_digit()) {
+ if let Some(pend) = comp.find(|c: char| !c.is_ascii_digit()) {
if let Ok(patch) = comp[..pend].parse() {
*comps[i] = patch;
}
@@ -477,6 +477,13 @@ impl SoVersion {
}
}
+#[cfg(test)]
+impl PartialEq<(u32, u32, u32, u32)> for SoVersion {
+ fn eq(&self, o: &(u32, u32, u32, u32)) -> bool {
+ self.major == o.0 && self.minor == o.1 && self.patch == o.2 && self.prerelease == o.3
+ }
+}
+
#[cfg(test)]
#[cfg(target_pointer_width = "64")] // All addresses are 64 bit and I'm currently too lazy to adjust it to work for both
mod tests {
@@ -732,93 +739,25 @@ a4840000-a4873000 rw-p 09021000 08:12 393449 /data/app/org.mozilla.firefox-1
#[test]
fn test_elf_file_so_version() {
+ #[rustfmt::skip]
let test_cases = [
- ("/home/alex/bin/firefox/libmozsandbox.so", None),
- (
- "/usr/lib/x86_64-linux-gnu/libstdc++.so.6.0.32",
- Some(SoVersion {
- major: 6,
- patch: 32,
- ..Default::default()
- }),
- ),
- (
- "/usr/lib/x86_64-linux-gnu/libcairo-gobject.so.2.11800.0",
- Some(SoVersion {
- major: 2,
- minor: 11800,
- ..Default::default()
- }),
- ),
- (
- "/usr/lib/x86_64-linux-gnu/libm.so.6",
- Some(SoVersion {
- major: 6,
- ..Default::default()
- }),
- ),
- (
- "/usr/lib/x86_64-linux-gnu/libpthread.so.0",
- Some(SoVersion::default()),
- ),
- (
- "/usr/lib/x86_64-linux-gnu/libgmodule-2.0.so.0.7800.0",
- Some(SoVersion {
- minor: 7800,
- ..Default::default()
- }),
- ),
- (
- "/usr/lib/x86_64-linux-gnu/libabsl_time_zone.so.20220623.0.0",
- Some(SoVersion {
- major: 20220623,
- ..Default::default()
- }),
- ),
- (
- "/usr/lib/x86_64-linux-gnu/libdbus-1.so.3.34.2rc5",
- Some(SoVersion {
- major: 3,
- minor: 34,
- patch: 2,
- prerelease: 5,
- }),
- ),
- (
- "/usr/lib/x86_64-linux-gnu/libdbus-1.so.3.34.2rc",
- Some(SoVersion {
- major: 3,
- minor: 34,
- patch: 2,
- prerelease: 0,
- }),
- ),
- (
- "/usr/lib/x86_64-linux-gnu/libdbus-1.so.3.34.rc5",
- Some(SoVersion {
- major: 3,
- minor: 34,
- patch: 0,
- prerelease: 5,
- }),
- ),
- (
- "/usr/lib/x86_64-linux-gnu/libtoto.so.AAA",
- Some(SoVersion::default()),
- ),
- (
- "/usr/lib/x86_64-linux-gnu/libsemver-1.so.1.2.alpha.1",
- Some(SoVersion {
- major: 1,
- minor: 2,
- patch: 0,
- prerelease: 1,
- }),
- ),
+ ("/usr/lib/x86_64-linux-gnu/libstdc++.so.6.0.32", (6, 0, 32, 0)),
+ ("/usr/lib/x86_64-linux-gnu/libcairo-gobject.so.2.11800.0", (2, 11800, 0, 0)),
+ ("/usr/lib/x86_64-linux-gnu/libm.so.6", (6, 0, 0, 0)),
+ ("/usr/lib/x86_64-linux-gnu/libpthread.so.0", (0, 0, 0, 0)),
+ ("/usr/lib/x86_64-linux-gnu/libgmodule-2.0.so.0.7800.0", (0, 7800, 0, 0)),
+ ("/usr/lib/x86_64-linux-gnu/libabsl_time_zone.so.20220623.0.0", (20220623, 0, 0, 0)),
+ ("/usr/lib/x86_64-linux-gnu/libdbus-1.so.3.34.2rc5", (3, 34, 2, 5)),
+ ("/usr/lib/x86_64-linux-gnu/libdbus-1.so.3.34.2rc", (3, 34, 2, 0)),
+ ("/usr/lib/x86_64-linux-gnu/libdbus-1.so.3.34.rc5", (3, 34, 0, 5)),
+ ("/usr/lib/x86_64-linux-gnu/libtoto.so.AAA", (0, 0, 0, 0)),
+ ("/usr/lib/x86_64-linux-gnu/libsemver-1.so.1.2.alpha.1", (1, 2, 0, 1)),
];
+ assert!(SoVersion::parse(OsStr::new("/home/alex/bin/firefox/libmozsandbox.so")).is_none());
+
for (path, expected) in test_cases {
- let actual = SoVersion::parse(OsStr::new(path));
+ let actual = SoVersion::parse(OsStr::new(path)).unwrap();
assert_eq!(actual, expected);
}
}