From e56f764fce53e74a29c572a93646f89594d49baa Mon Sep 17 00:00:00 2001
From: Alex Franchuk
Date: Fri, 19 Apr 2024 11:41:45 -0400
Subject: [PATCH 1/4] Read ELF SONAMEs directly from process memory when
possible.
When this fails, it falls back to the previous behavior of trying to
open the mapped file name directly.
Closes #79.
---
src/bin/test.rs | 8 +-
src/linux.rs | 2 +-
src/linux/errors.rs | 16 ++-
src/linux/maps_reader.rs | 18 +--
.../{build_id_reader.rs => module_reader.rs} | 136 +++++++++++++-----
src/linux/ptrace_dumper.rs | 38 ++---
src/linux/sections/mappings.rs | 18 ++-
tests/linux_minidump_writer.rs | 5 +-
8 files changed, 164 insertions(+), 77 deletions(-)
rename src/linux/{build_id_reader.rs => module_reader.rs} (75%)
diff --git a/src/bin/test.rs b/src/bin/test.rs
index 77a54081..d8ac8b60 100644
--- a/src/bin/test.rs
+++ b/src/bin/test.rs
@@ -9,6 +9,7 @@ mod linux {
use super::*;
use minidump_writer::{
minidump_writer::STOP_TIMEOUT,
+ module_reader,
ptrace_dumper::{PtraceDumper, AT_SYSINFO_EHDR},
LINUX_GATE_LIBRARY_NAME,
};
@@ -100,7 +101,7 @@ mod linux {
}
}
let idx = found_exe.unwrap();
- let id = dumper.elf_identifier_for_mapping_index(idx)?;
+ let module_reader::BuildId(id) = dumper.from_process_memory_for_index(idx)?;
dumper.resume_threads()?;
assert!(!id.is_empty());
assert!(id.iter().any(|&x| x > 0));
@@ -133,11 +134,12 @@ mod linux {
let ppid = getppid().as_raw();
let mut dumper = PtraceDumper::new(ppid, STOP_TIMEOUT)?;
let mut found_linux_gate = false;
- for mut mapping in dumper.mappings.clone() {
+ for mapping in dumper.mappings.clone() {
if mapping.name == Some(LINUX_GATE_LIBRARY_NAME.into()) {
found_linux_gate = true;
dumper.suspend_threads()?;
- let id = PtraceDumper::elf_identifier_for_mapping(&mut mapping, ppid)?;
+ let module_reader::BuildId(id) =
+ dumper.from_process_memory_for_mapping(&mapping)?;
test!(!id.is_empty(), "id-vec is empty")?;
test!(id.iter().any(|&x| x > 0), "all id elements are 0")?;
dumper.resume_threads()?;
diff --git a/src/linux.rs b/src/linux.rs
index 31e21c76..0b68c125 100644
--- a/src/linux.rs
+++ b/src/linux.rs
@@ -5,13 +5,13 @@
mod android;
pub mod app_memory;
pub(crate) mod auxv_reader;
-pub mod build_id_reader;
pub mod crash_context;
mod dso_debug;
mod dumper_cpu_info;
pub mod errors;
pub mod maps_reader;
pub mod minidump_writer;
+pub mod module_reader;
pub mod ptrace_dumper;
pub(crate) mod sections;
pub mod thread_info;
diff --git a/src/linux/errors.rs b/src/linux/errors.rs
index faf79f4d..1112c13a 100644
--- a/src/linux/errors.rs
+++ b/src/linux/errors.rs
@@ -26,7 +26,7 @@ pub enum MapsReaderError {
#[error("Couldn't parse as ELF file")]
ELFParsingFailed(#[from] goblin::error::Error),
#[error("No soname found (filename: {})", .0.to_string_lossy())]
- NoSoName(OsString),
+ NoSoName(OsString, #[source] ModuleReaderError),
// parse_from_line()
#[error("Map entry malformed: No {0} found")]
@@ -115,8 +115,8 @@ pub enum DumperError {
TryFromSliceError(#[from] std::array::TryFromSliceError),
#[error("Couldn't parse as ELF file")]
ELFParsingFailed(#[from] goblin::error::Error),
- #[error("No build-id found")]
- NoBuildIDFound(#[from] BuildIdReaderError),
+ #[error("Could not read value from module")]
+ ModuleReaderError(#[from] ModuleReaderError),
#[error("Not safe to open mapping: {}", .0.to_string_lossy())]
NotSafeToOpenMapping(OsString),
#[error("Failed integer conversion")]
@@ -250,7 +250,7 @@ pub enum WriterError {
}
#[derive(Debug, Error)]
-pub enum BuildIdReaderError {
+pub enum ModuleReaderError {
#[error("failed to read module memory: {length} bytes at {offset}: {error}")]
ReadModuleMemory {
offset: u64,
@@ -276,9 +276,15 @@ pub enum BuildIdReaderError {
... from sections: {section}\n\
... from the text section: {section}"
)]
- Aggregate {
+ NoBuildId {
program_headers: Box,
section: Box,
generated: Box,
},
+ #[error("a string in the strtab did not have a terminating nul byte")]
+ StrTabNoNulByte,
+ #[error("no SONAME found in dynamic linking information")]
+ NoSoNameEntry,
+ #[error("no dynamic linking information section")]
+ NoDynamicSection,
}
diff --git a/src/linux/maps_reader.rs b/src/linux/maps_reader.rs
index eecabae8..66fa2f46 100644
--- a/src/linux/maps_reader.rs
+++ b/src/linux/maps_reader.rs
@@ -256,14 +256,13 @@ impl MappingInfo {
/// 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)?;
+ use super::module_reader::{ReadFromModule, SoName};
- let soname = elf_obj.soname.ok_or_else(|| {
- MapsReaderError::NoSoName(self.name.clone().unwrap_or_else(|| "None".into()))
- })?;
- Ok(soname.to_string())
+ let mapped_file = MappingInfo::get_mmap(&self.name, self.offset)?;
+ Ok(SoName::read_from_module(&*mapped_file)
+ .map_err(|e| MapsReaderError::NoSoName(self.name.clone().unwrap_or_default(), e))?
+ .0
+ .to_string())
}
#[inline]
@@ -273,6 +272,7 @@ impl MappingInfo {
pub fn get_mapping_effective_path_name_and_version(
&self,
+ soname: Option,
) -> Result<(PathBuf, String, Option)> {
let mut file_path = PathBuf::from(self.name.clone().unwrap_or_default());
@@ -282,7 +282,7 @@ impl MappingInfo {
// filesystem name of the module.
// Just use the filesystem name if no SONAME is present.
- let Ok(file_name) = self.so_name() else {
+ let Some(file_name) = soname.or_else(|| self.so_name().ok()) else {
// file_path := /path/to/libname.so
// file_name := libname.so
let file_name = file_path
@@ -689,7 +689,7 @@ a4840000-a4873000 rw-p 09021000 08:12 393449 /data/app/org.mozilla.firefox-1
assert_eq!(mappings.len(), 1);
let (file_path, file_name, _version) = mappings[0]
- .get_mapping_effective_path_name_and_version()
+ .get_mapping_effective_path_name_and_version(None)
.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"));
diff --git a/src/linux/build_id_reader.rs b/src/linux/module_reader.rs
similarity index 75%
rename from src/linux/build_id_reader.rs
rename to src/linux/module_reader.rs
index 94288b24..c0a5ee1e 100644
--- a/src/linux/build_id_reader.rs
+++ b/src/linux/module_reader.rs
@@ -1,9 +1,10 @@
-use crate::errors::BuildIdReaderError as Error;
+use crate::errors::ModuleReaderError as Error;
use crate::minidump_format::GUID;
use goblin::{
container::{Container, Ctx, Endian},
elf,
};
+use std::ffi::CStr;
const NOTE_SECTION_NAME: &[u8] = b".note.gnu.build-id\0";
@@ -61,34 +62,57 @@ fn build_id_from_bytes(data: &[u8]) -> Vec {
)
}
-pub fn read_build_id(module_memory: impl ModuleMemory) -> Result, Error> {
- let reader = ElfBuildIdReader::new(module_memory)?;
- let program_headers = match reader.read_from_program_headers() {
- Ok(v) => return Ok(v),
- Err(e) => Box::new(e),
- };
- let section = match reader.read_from_section() {
- Ok(v) => return Ok(v),
- Err(e) => Box::new(e),
- };
- let generated = match reader.generate_from_text() {
- Ok(v) => return Ok(v),
- Err(e) => Box::new(e),
- };
- Err(Error::Aggregate {
- program_headers,
- section,
- generated,
- })
+/// Types which can be read from an `impl ModuleMemory`.
+pub trait ReadFromModule: Sized {
+ fn read_from_module(module_memory: impl ModuleMemory) -> Result;
}
-pub struct ElfBuildIdReader {
+/// The module build id.
+#[derive(Default, Clone, Debug)]
+pub struct BuildId(pub Vec);
+
+impl ReadFromModule for BuildId {
+ fn read_from_module(module_memory: impl ModuleMemory) -> Result {
+ let reader = ModuleReader::new(module_memory)?;
+ let program_headers = match reader.build_id_from_program_headers() {
+ Ok(v) => return Ok(BuildId(v)),
+ Err(e) => Box::new(e),
+ };
+ let section = match reader.build_id_from_section() {
+ Ok(v) => return Ok(BuildId(v)),
+ Err(e) => Box::new(e),
+ };
+ let generated = match reader.build_id_generate_from_text() {
+ Ok(v) => return Ok(BuildId(v)),
+ Err(e) => Box::new(e),
+ };
+ Err(Error::NoBuildId {
+ program_headers,
+ section,
+ generated,
+ })
+ }
+}
+
+/// The module SONAME.
+#[derive(Default, Clone, Debug)]
+pub struct SoName(pub String);
+
+impl ReadFromModule for SoName {
+ fn read_from_module(module_memory: impl ModuleMemory) -> Result {
+ ModuleReader::new(module_memory)
+ .and_then(|r| r.soname())
+ .map(SoName)
+ }
+}
+
+pub struct ModuleReader {
module_memory: T,
header: elf::Header,
context: Ctx,
}
-impl ElfBuildIdReader {
+impl ModuleReader {
pub fn new(module_memory: T) -> Result {
// We could use `Ctx::default()` (which defaults to the native system), however to be extra
// permissive we'll just use a 64-bit ("Big") context which would result in the largest
@@ -97,15 +121,57 @@ impl ElfBuildIdReader {
let header_data = read(&module_memory, 0, header_size as u64)?;
let header = elf::Elf::parse_header(&header_data)?;
let context = Ctx::new(header.container()?, header.endianness()?);
- Ok(ElfBuildIdReader {
+ Ok(ModuleReader {
module_memory,
header,
context,
})
}
+ pub fn soname(&self) -> Result {
+ let section_headers = self.read_section_headers()?;
+
+ let strtab_section_header = section_headers
+ .get(self.header.e_shstrndx as usize)
+ .ok_or(Error::NoStrTab)?;
+
+ let dynamic_section_header = section_headers
+ .iter()
+ .find(|h| h.sh_type == elf::section_header::SHT_DYNAMIC)
+ .ok_or(Error::NoDynamicSection)?;
+
+ let dynamic_section: &[u8] = &read(
+ &self.module_memory,
+ dynamic_section_header.sh_offset,
+ dynamic_section_header.sh_size,
+ )?;
+
+ let mut offset = 0;
+ loop {
+ use scroll::Pread;
+ let dyn_: elf::dynamic::Dyn = dynamic_section.gread_with(&mut offset, self.context)?;
+ if dyn_.d_tag == elf::dynamic::DT_SONAME {
+ let strtab_offset = dyn_.d_val;
+ if strtab_offset < strtab_section_header.sh_size {
+ let name = read(
+ &self.module_memory,
+ strtab_section_header.sh_offset + strtab_offset,
+ strtab_section_header.sh_size - strtab_offset,
+ )?;
+ return CStr::from_bytes_until_nul(&name)
+ .map(|s| s.to_string_lossy().into_owned())
+ .map_err(|_| Error::StrTabNoNulByte);
+ }
+ }
+ if dyn_.d_tag == elf::dynamic::DT_NULL {
+ break;
+ }
+ }
+ Err(Error::NoSoNameEntry)
+ }
+
/// Read the build id from a program header note.
- pub fn read_from_program_headers(&self) -> Result, Error> {
+ pub fn build_id_from_program_headers(&self) -> Result, Error> {
if self.header.e_phoff == 0 {
return Err(Error::NoProgramHeaderNote);
}
@@ -134,7 +200,7 @@ impl ElfBuildIdReader {
}
/// Read the build id from a notes section.
- pub fn read_from_section(&self) -> Result, Error> {
+ pub fn build_id_from_section(&self) -> Result, Error> {
let section_headers = self.read_section_headers()?;
let strtab_section_header = section_headers
@@ -173,7 +239,7 @@ impl ElfBuildIdReader {
}
/// Generate a build id by hashing the first page of the text section.
- pub fn generate_from_text(&self) -> Result, Error> {
+ pub fn build_id_generate_from_text(&self) -> Result, Error> {
let Some(text_header) = self
.read_section_headers()?
.into_iter()
@@ -288,9 +354,9 @@ mod test {
];
#[test]
- fn program_headers() {
- let reader = ElfBuildIdReader::new(TINY_ELF).unwrap();
- let id = reader.read_from_program_headers().unwrap();
+ fn build_id_program_headers() {
+ let reader = ModuleReader::new(TINY_ELF).unwrap();
+ let id = reader.build_id_from_program_headers().unwrap();
assert_eq!(
id,
vec![1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14, 15, 16]
@@ -298,9 +364,9 @@ mod test {
}
#[test]
- fn section() {
- let reader = ElfBuildIdReader::new(TINY_ELF).unwrap();
- let id = reader.read_from_section().unwrap();
+ fn build_id_section() {
+ let reader = ModuleReader::new(TINY_ELF).unwrap();
+ let id = reader.build_id_from_section().unwrap();
assert_eq!(
id,
vec![1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14, 15, 16]
@@ -308,9 +374,9 @@ mod test {
}
#[test]
- fn text_hash() {
- let reader = ElfBuildIdReader::new(TINY_ELF).unwrap();
- let id = reader.generate_from_text().unwrap();
+ fn build_id_text_hash() {
+ let reader = ModuleReader::new(TINY_ELF).unwrap();
+ let id = reader.build_id_generate_from_text().unwrap();
assert_eq!(
id,
vec![0x6a, 0x3c, 0x58, 0x31, 0xff, 0x0f, 0x05, 0, 0, 0, 0, 0, 0, 0, 0, 0]
diff --git a/src/linux/ptrace_dumper.rs b/src/linux/ptrace_dumper.rs
index bcb0a8e4..d05e53ca 100644
--- a/src/linux/ptrace_dumper.rs
+++ b/src/linux/ptrace_dumper.rs
@@ -2,9 +2,9 @@
use crate::linux::android::late_process_mappings;
use crate::linux::{
auxv_reader::{AuxvType, ProcfsAuxvIter},
- build_id_reader,
errors::{DumperError, InitError, ThreadInfoError},
maps_reader::MappingInfo,
+ module_reader,
thread_info::{Pid, ThreadInfo},
};
#[cfg(any(target_arch = "x86", target_arch = "x86_64"))]
@@ -557,28 +557,35 @@ impl PtraceDumper {
})
}
- pub fn elf_identifier_for_mapping_index(&mut self, idx: usize) -> Result, DumperError> {
+ pub fn from_process_memory_for_index(
+ &self,
+ idx: usize,
+ ) -> Result {
assert!(idx < self.mappings.len());
- Self::elf_identifier_for_mapping(&mut self.mappings[idx], self.pid)
+ self.from_process_memory_for_mapping(&self.mappings[idx])
}
- pub fn elf_identifier_for_mapping(
- mapping: &mut MappingInfo,
- pid: Pid,
- ) -> Result, DumperError> {
- let result = if pid == std::process::id().try_into()? {
+ pub fn from_process_memory_for_mapping(
+ &self,
+ mapping: &MappingInfo,
+ ) -> Result {
+ if std::process::id()
+ .try_into()
+ .map(|v: Pid| v == self.pid)
+ .unwrap_or(false)
+ {
let mem_slice = unsafe {
std::slice::from_raw_parts(mapping.start_address as *const u8, mapping.size)
};
- build_id_reader::read_build_id(mem_slice)
+ T::read_from_module(mem_slice)
} else {
- struct ProcessReader {
+ struct ProcessModuleMemory {
pid: Pid,
start_address: u64,
}
- impl build_id_reader::ModuleMemory for ProcessReader {
+ impl module_reader::ModuleMemory for ProcessModuleMemory {
type Memory = Vec;
fn read_module_memory(
@@ -596,12 +603,11 @@ impl PtraceDumper {
}
}
- build_id_reader::read_build_id(ProcessReader {
- pid,
+ T::read_from_module(ProcessModuleMemory {
+ pid: self.pid,
start_address: mapping.start_address as u64,
})
- };
-
- result.map_err(|e| e.into())
+ }
+ .map_err(|e| e.into())
}
}
diff --git a/src/linux/sections/mappings.rs b/src/linux/sections/mappings.rs
index 9012ae35..2c45d1e4 100644
--- a/src/linux/sections/mappings.rs
+++ b/src/linux/sections/mappings.rs
@@ -1,5 +1,6 @@
use super::*;
use crate::linux::maps_reader::MappingInfo;
+use crate::linux::module_reader::{BuildId, SoName};
/// Write information about the mappings in effect. Because we are using the
/// minidump format, the information about the mappings is pretty limited.
@@ -23,9 +24,8 @@ pub fn write(
{
continue;
}
- // Note: elf_identifier_for_mapping_index() can manipulate the |mapping.name|.
- let identifier = dumper
- .elf_identifier_for_mapping_index(map_idx)
+ let BuildId(identifier) = dumper
+ .from_process_memory_for_index(map_idx)
.unwrap_or_default();
// If the identifier is all 0, its an uninteresting mapping (bmc#1676109)
@@ -33,14 +33,19 @@ pub fn write(
continue;
}
- let module = fill_raw_module(buffer, &dumper.mappings[map_idx], &identifier)?;
+ let soname = dumper
+ .from_process_memory_for_index(map_idx)
+ .ok()
+ .map(|SoName(n)| n);
+
+ let module = fill_raw_module(buffer, &dumper.mappings[map_idx], &identifier, soname)?;
modules.push(module);
}
// Next write all the mappings provided by the caller
for user in &config.user_mapping_list {
// GUID was provided by caller.
- let module = fill_raw_module(buffer, &user.mapping, &user.identifier)?;
+ let module = fill_raw_module(buffer, &user.mapping, &user.identifier, None)?;
modules.push(module);
}
@@ -63,6 +68,7 @@ fn fill_raw_module(
buffer: &mut DumpBuf,
mapping: &MappingInfo,
identifier: &[u8],
+ soname: Option,
) -> Result {
let cv_record = if identifier.is_empty() {
// Just zeroes
@@ -84,7 +90,7 @@ fn fill_raw_module(
};
let (file_path, _, so_version) = mapping
- .get_mapping_effective_path_name_and_version()
+ .get_mapping_effective_path_name_and_version(soname)
.map_err(|e| errors::SectionMappingsError::GetEffectivePathError(mapping.clone(), e))?;
let name_header = write_string_to_location(buffer, file_path.to_string_lossy().as_ref())?;
diff --git a/tests/linux_minidump_writer.rs b/tests/linux_minidump_writer.rs
index 6cf75e4b..5983e12b 100644
--- a/tests/linux_minidump_writer.rs
+++ b/tests/linux_minidump_writer.rs
@@ -5,11 +5,11 @@ use minidump::*;
use minidump_common::format::{GUID, MINIDUMP_STREAM_TYPE::*};
use minidump_writer::{
app_memory::AppMemory,
- build_id_reader::read_build_id,
crash_context::CrashContext,
errors::*,
maps_reader::{MappingEntry, MappingInfo, SystemMappingInfo},
minidump_writer::MinidumpWriter,
+ module_reader::{BuildId, ReadFromModule},
ptrace_dumper::PtraceDumper,
thread_info::Pid,
};
@@ -705,7 +705,8 @@ fn with_deleted_binary() {
let pid = child.id() as i32;
- let mut build_id = read_build_id(mem_slice.as_slice()).expect("Failed to get build_id");
+ let BuildId(mut build_id) =
+ BuildId::read_from_module(mem_slice.as_slice()).expect("Failed to get build_id");
std::fs::remove_file(&binary_copy).expect("Failed to remove binary");
From f0b448059042f8f5aa47e4a418978da7f1eea908 Mon Sep 17 00:00:00 2001
From: Alex Franchuk
Date: Mon, 22 Apr 2024 15:43:53 -0400
Subject: [PATCH 2/4] Add test for SONAME and fix the implementation.
---
src/linux/errors.rs | 2 +
src/linux/module_reader.rs | 174 +++++++++++++++++++++++--------------
2 files changed, 113 insertions(+), 63 deletions(-)
diff --git a/src/linux/errors.rs b/src/linux/errors.rs
index 1112c13a..6e8c4aea 100644
--- a/src/linux/errors.rs
+++ b/src/linux/errors.rs
@@ -281,6 +281,8 @@ pub enum ModuleReaderError {
section: Box,
generated: Box,
},
+ #[error("no dynamic string table section")]
+ NoDynStrSection,
#[error("a string in the strtab did not have a terminating nul byte")]
StrTabNoNulByte,
#[error("no SONAME found in dynamic linking information")]
diff --git a/src/linux/module_reader.rs b/src/linux/module_reader.rs
index c0a5ee1e..671d0074 100644
--- a/src/linux/module_reader.rs
+++ b/src/linux/module_reader.rs
@@ -62,6 +62,36 @@ fn build_id_from_bytes(data: &[u8]) -> Vec {
)
}
+// `name` should be null-terminated
+fn section_header_with_name<'a>(
+ section_headers: &'a elf::SectionHeaders,
+ strtab_index: usize,
+ name: &[u8],
+ module_memory: &impl ModuleMemory,
+) -> Result