From 9f4e130f1477b03bfd89d3a611a258c15ece2521 Mon Sep 17 00:00:00 2001 From: Alexandre Lissy Date: Mon, 19 Feb 2024 14:06:05 +0100 Subject: [PATCH 1/2] Bug 1847098 - Add version number of so files --- src/linux/maps_reader.rs | 110 +++++++++++++++++++++++++++++++-- src/linux/sections/mappings.rs | 16 +++-- 2 files changed, 117 insertions(+), 9 deletions(-) diff --git a/src/linux/maps_reader.rs b/src/linux/maps_reader.rs index 4d0d3b5a..8e3cb1eb 100644 --- a/src/linux/maps_reader.rs +++ b/src/linux/maps_reader.rs @@ -303,7 +303,62 @@ impl MappingInfo { Ok(soname.to_string()) } - pub fn get_mapping_effective_path_and_name(&self) -> Result<(PathBuf, String)> { + fn elf_file_so_version(&self) -> (u32, u32, u32) { + // Report the so version from the filename as two u32 that will fit into + // version_info.file_version_{hi,lo} + let name = String::from( + self.name + .clone() + .expect("Failure to clone") + .to_str() + .unwrap(), + ); + + if !(name.contains(".so.")) { + return (0, 0, 0); + } + + let filename = match name.split('/').last() { + Some(f) => f, + None => return (0, 0, 0), + }; + + match filename.split(".so.").last() { + Some(s) => { + let mut got_non_digit = false; + let vers: Vec = s + .split('.') + .collect::>() + .into_iter() + .map(|n| { + n.parse().unwrap_or_else(|_| { + got_non_digit = true; + 0 + }) + }) + .collect::>(); + + if got_non_digit { + return (0, 0, 0); + } + + let (major, minor, release) = match vers.len() { + 0 => (0, 0, 0), + 1 => (vers[0], 0, 0), + 2 => (vers[0], vers[1], 0), + 3 => (vers[0], vers[1], vers[2]), + _ => (vers[0], vers[1], vers[2]), + }; + + (major, minor, release) + } + None => (0, 0, 0), + } + } + + pub fn get_mapping_effective_path_name_and_version( + &self, + ) -> Result<(PathBuf, String, (u32, u32, u32))> { 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 @@ -321,7 +376,7 @@ impl MappingInfo { .file_name() .map(|s| s.to_string_lossy().into_owned()) .unwrap_or_default(); - return Ok((file_path, file_name)); + return Ok((file_path, file_name, self.elf_file_so_version())); }; if self.is_executable() && self.offset != 0 { @@ -337,7 +392,7 @@ impl MappingInfo { file_path.set_file_name(&file_name); } - Ok((file_path, file_name)) + Ok((file_path, file_name, self.elf_file_so_version())) } pub fn is_contained_in(&self, user_mapping_list: &MappingList) -> bool { @@ -628,13 +683,58 @@ a4840000-a4873000 rw-p 09021000 08:12 393449 /data/app/org.mozilla.firefox-1 ); assert_eq!(mappings.len(), 1); - let (file_path, file_name) = mappings[0] - .get_mapping_effective_path_and_name() + let (file_path, file_name, _version) = mappings[0] + .get_mapping_effective_path_name_and_version() .expect("Couldn't get effective name for mapping"); assert_eq!(file_name, "libmozgtk.so"); assert_eq!(file_path, PathBuf::from("/home/martin/Documents/mozilla/devel/mozilla-central/obj/widget/gtk/mozgtk/gtk3/libmozgtk.so")); } + #[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 version = mappings[0].elf_file_so_version(); + assert_eq!(version, (0, 0, 0)); + + let version = mappings[1].elf_file_so_version(); + assert_eq!(version, (6, 0, 32)); + + let version = mappings[2].elf_file_so_version(); + assert_eq!(version, (2, 11800, 0)); + + let version = mappings[3].elf_file_so_version(); + assert_eq!(version, (6, 0, 0)); + + let version = mappings[4].elf_file_so_version(); + assert_eq!(version, (0, 0, 0)); + + let version = mappings[5].elf_file_so_version(); + assert_eq!(version, (0, 7800, 0)); + + let version = mappings[6].elf_file_so_version(); + assert_eq!(version, (20220623, 0, 0)); + + let version = mappings[7].elf_file_so_version(); + assert_eq!(version, (0, 0, 0)); + + let version = mappings[8].elf_file_so_version(); + assert_eq!(version, (0, 0, 0)); + } + #[test] fn test_whitespaces_in_name() { let mappings = get_mappings_for( diff --git a/src/linux/sections/mappings.rs b/src/linux/sections/mappings.rs index de19c540..a92739ea 100644 --- a/src/linux/sections/mappings.rs +++ b/src/linux/sections/mappings.rs @@ -83,16 +83,24 @@ fn fill_raw_module( sig_section.location() }; - let (file_path, _) = mapping - .get_mapping_effective_path_and_name() + let (file_path, _, (major, minor, release)) = 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())?; - Ok(MDRawModule { + let mut 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, ..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 733c2665595260b86850140bd4d7a7f2291976b8 Mon Sep 17 00:00:00 2001 From: Jake Shadle Date: Fri, 23 Feb 2024 17:25:21 +0100 Subject: [PATCH 2/2] Simplify The original code would have paniced on non-utf8 paths, and would fail to return any version components if a single one failed to be parsed as a number. The new code will now gracefully fail if the filename (the rest of the path is inconsequential) is non-utf8, and now tries to handle as many version components as it can, including partial parsing of mixed alphanumeric components such as, eg 2rc5 (which was aready a test case) would be parsed as `25` --- src/linux/maps_reader.rs | 108 +++++++++++++++------------------------ 1 file changed, 40 insertions(+), 68 deletions(-) diff --git a/src/linux/maps_reader.rs b/src/linux/maps_reader.rs index 8e3cb1eb..6e96d0b2 100644 --- a/src/linux/maps_reader.rs +++ b/src/linux/maps_reader.rs @@ -303,57 +303,39 @@ 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) { - // Report the so version from the filename as two u32 that will fit into - // version_info.file_version_{hi,lo} - let name = String::from( - self.name - .clone() - .expect("Failure to clone") - .to_str() - .unwrap(), - ); + const DEF: (u32, u32, u32) = (0, 0, 0); + let Some(so_name) = self.name.as_deref() else { + return DEF; + }; + let Some(filename) = std::path::Path::new(so_name).file_name() else { + return DEF; + }; - if !(name.contains(".so.")) { - return (0, 0, 0); - } + // Avoid an allocation unless the string contains non-utf8 + let filename = filename.to_string_lossy(); - let filename = match name.split('/').last() { - Some(f) => f, - None => return (0, 0, 0), + let Some((_, version)) = filename.split_once(".so.") else { + return DEF; }; - match filename.split(".so.").last() { - Some(s) => { - let mut got_non_digit = false; - let vers: Vec = s - .split('.') - .collect::>() - .into_iter() - .map(|n| { - n.parse().unwrap_or_else(|_| { - got_non_digit = true; - 0 - }) - }) - .collect::>(); - - if got_non_digit { - return (0, 0, 0); - } + let mut triplet = [0, 0, 0]; - let (major, minor, release) = match vers.len() { - 0 => (0, 0, 0), - 1 => (vers[0], 0, 0), - 2 => (vers[0], vers[1], 0), - 3 => (vers[0], vers[1], vers[2]), - _ => (vers[0], vers[1], vers[2]), - }; - - (major, minor, release) + 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; } - None => (0, 0, 0), } + + (triplet[0], triplet[1], triplet[2]) } pub fn get_mapping_effective_path_name_and_version( @@ -707,32 +689,22 @@ a4840000-a4873000 rw-p 09021000 08:12 393449 /data/app/org.mozilla.firefox-1 ); assert_eq!(mappings.len(), 9); - let version = mappings[0].elf_file_so_version(); - assert_eq!(version, (0, 0, 0)); - - let version = mappings[1].elf_file_so_version(); - assert_eq!(version, (6, 0, 32)); - - let version = mappings[2].elf_file_so_version(); - assert_eq!(version, (2, 11800, 0)); - - let version = mappings[3].elf_file_so_version(); - assert_eq!(version, (6, 0, 0)); - - let version = mappings[4].elf_file_so_version(); - assert_eq!(version, (0, 0, 0)); - - let version = mappings[5].elf_file_so_version(); - assert_eq!(version, (0, 7800, 0)); - - let version = mappings[6].elf_file_so_version(); - assert_eq!(version, (20220623, 0, 0)); - - let version = mappings[7].elf_file_so_version(); - assert_eq!(version, (0, 0, 0)); + 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 version = mappings[8].elf_file_so_version(); - assert_eq!(version, (0, 0, 0)); + for (i, (map, exp)) in mappings.into_iter().zip(expected).enumerate() { + let version = map.elf_file_so_version(); + assert_eq!(version, exp, "{i}"); + } } #[test]