Skip to content

Commit

Permalink
Read ELF SONAMEs directly from process memory when possible.
Browse files Browse the repository at this point in the history
When this fails, it falls back to the previous behavior of trying to
open the mapped file name directly.

Cloes #79.
  • Loading branch information
afranchuk committed Apr 19, 2024
1 parent c75d2ab commit cfe5e46
Show file tree
Hide file tree
Showing 8 changed files with 164 additions and 77 deletions.
8 changes: 5 additions & 3 deletions src/bin/test.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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,
};
Expand Down Expand Up @@ -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));
Expand Down Expand Up @@ -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()?;
Expand Down
2 changes: 1 addition & 1 deletion src/linux.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand Down
16 changes: 11 additions & 5 deletions src/linux/errors.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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")]
Expand Down Expand Up @@ -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")]
Expand Down Expand Up @@ -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,
Expand All @@ -276,9 +276,15 @@ pub enum BuildIdReaderError {
... from sections: {section}\n\
... from the text section: {section}"
)]
Aggregate {
NoBuildId {
program_headers: Box<Self>,
section: Box<Self>,
generated: Box<Self>,
},
#[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,
}
18 changes: 9 additions & 9 deletions src/linux/maps_reader.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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<String> {
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]
Expand All @@ -273,6 +272,7 @@ impl MappingInfo {

pub fn get_mapping_effective_path_name_and_version(
&self,
soname: Option<String>,
) -> Result<(PathBuf, String, Option<SoVersion>)> {
let mut file_path = PathBuf::from(self.name.clone().unwrap_or_default());

Expand All @@ -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
Expand Down Expand Up @@ -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"));
Expand Down
136 changes: 101 additions & 35 deletions src/linux/build_id_reader.rs → src/linux/module_reader.rs
Original file line number Diff line number Diff line change
@@ -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";

Expand Down Expand Up @@ -61,34 +62,57 @@ fn build_id_from_bytes(data: &[u8]) -> Vec<u8> {
)
}

pub fn read_build_id(module_memory: impl ModuleMemory) -> Result<Vec<u8>, 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<Self, Error>;
}

pub struct ElfBuildIdReader<T> {
/// The module build id.
#[derive(Default, Clone, Debug)]
pub struct BuildId(pub Vec<u8>);

impl ReadFromModule for BuildId {
fn read_from_module(module_memory: impl ModuleMemory) -> Result<Self, Error> {
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<Self, Error> {
ModuleReader::new(module_memory)
.and_then(|r| r.soname())
.map(SoName)
}
}

pub struct ModuleReader<T> {
module_memory: T,
header: elf::Header,
context: Ctx,
}

impl<T: ModuleMemory> ElfBuildIdReader<T> {
impl<T: ModuleMemory> ModuleReader<T> {
pub fn new(module_memory: T) -> Result<Self, Error> {
// 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
Expand All @@ -97,15 +121,57 @@ impl<T: ModuleMemory> ElfBuildIdReader<T> {
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<String, Error> {
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<Vec<u8>, Error> {
pub fn build_id_from_program_headers(&self) -> Result<Vec<u8>, Error> {
if self.header.e_phoff == 0 {
return Err(Error::NoProgramHeaderNote);
}
Expand Down Expand Up @@ -134,7 +200,7 @@ impl<T: ModuleMemory> ElfBuildIdReader<T> {
}

/// Read the build id from a notes section.
pub fn read_from_section(&self) -> Result<Vec<u8>, Error> {
pub fn build_id_from_section(&self) -> Result<Vec<u8>, Error> {
let section_headers = self.read_section_headers()?;

let strtab_section_header = section_headers
Expand Down Expand Up @@ -173,7 +239,7 @@ impl<T: ModuleMemory> ElfBuildIdReader<T> {
}

/// Generate a build id by hashing the first page of the text section.
pub fn generate_from_text(&self) -> Result<Vec<u8>, Error> {
pub fn build_id_generate_from_text(&self) -> Result<Vec<u8>, Error> {
let Some(text_header) = self
.read_section_headers()?
.into_iter()
Expand Down Expand Up @@ -288,29 +354,29 @@ 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]
);
}

#[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]
);
}

#[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]
Expand Down
Loading

0 comments on commit cfe5e46

Please sign in to comment.