Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Cleanup .so version parsing #104

Merged
merged 2 commits into from
Feb 26, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
193 changes: 124 additions & 69 deletions src/linux/maps_reader.rs
Original file line number Diff line number Diff line change
Expand Up @@ -289,10 +289,9 @@ impl MappingInfo {
true
}

fn elf_file_so_name(&self) -> Result<String> {
// 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<String> {
let mapped_file = MappingInfo::get_mmap(&self.name, self.offset)?;

let elf_obj = elf::Elf::parse(&mapped_file)?;
Expand All @@ -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;
};
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;
#[inline]
fn so_version(&self) -> Option<SoVersion> {
let Some(name) = self.name.as_deref() else {
return None;
};

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<SoVersion>)> {
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
Expand All @@ -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 {
Expand All @@ -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 {
Expand Down Expand Up @@ -419,6 +391,99 @@ 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(Debug))]
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<Self> {
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) = 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)]
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 {
Expand Down Expand Up @@ -674,36 +739,26 @@ 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),
#[rustfmt::skip]
let test_cases = [
("/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)),
Jake-Shadle marked this conversation as resolved.
Show resolved Hide resolved
("/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)),
];

for (i, (map, exp)) in mappings.into_iter().zip(expected).enumerate() {
let version = map.elf_file_so_version();
assert_eq!(version, exp, "{i}");
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)).unwrap();
assert_eq!(actual, expected);
}
}

Expand Down
21 changes: 13 additions & 8 deletions src/linux/sections/mappings.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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,
Jake-Shadle marked this conversation as resolved.
Show resolved Hide resolved
..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)
}
Loading