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); } }