8000 linux: document and simplify the dso_debug code by schopin-mozilla · Pull Request #221 · rust-minidump/minidump-writer · GitHub
[go: up one dir, main page]

Skip to content
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
1 change: 1 addition & 0 deletions Cargo.lock

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

1 change: 1 addition & 0 deletions Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -58,6 +58,7 @@ memmap2 = "0.9"
byteorder = "1.4"
error-graph = { version = "0.1.1", features = ["serde"] }
failspot = "0.2.0"
plain = { version = "0.2.3", default-features = false }
process-backend = { version = "0.1.0", path = "crates/linux/process-backend", features = ["testing"] }

# Used for parsing procfs info.
Expand Down
156 changes: 81 additions & 75 deletions src/linux/dso_debug.rs
Original file line number Diff line number Diff line change
@@ -1,3 +1,8 @@
//! The dynamic linker's debugger rendez-vous.
//!
//! This holds the `<link.h>` structures the linker publishes for debuggers, and
//! the `MD_LINUX_DSO_DEBUG` stream that is written straight from them.

use {
super::{
auxv::AuxvDumpInfo, minidump_writer::MinidumpWriter, process_inspection::ProcessInspector,
Expand All @@ -9,10 +14,16 @@ use {
},
minidump_format::*,
},
plain::Plain,
};

type Result<T> = std::result::Result<T, SectionDsoDebugError>;

#[cfg(not(target_pointer_width = "64"))]
use goblin::elf32 as elf;
#[cfg(target_pointer_width = "64")]
use goblin::elf64 as elf;

cfg_if::cfg_if! {
if #[cfg(target_pointer_width = "32")] {
use goblin::elf::program_header::program_header32::SIZEOF_PHDR;
Expand Down Expand Up @@ -49,53 +60,74 @@ pub enum SectionDsoDebugError {
),
}

// COPY from <link.h>
/// Information for a single dynamically loaded module.
///
/// This structure contains important information about a dynamically-loaded
/// module, like its name and virtual address. It is also a node in a
/// doubly-linked list of such modules.
///
/// Only the leading members below are part of the protocol with the debugger —
/// the same format used in SVR4. The linker's own fields follow them, and we
/// never read those.
///
/// <https://sourceware.org/git/?p=glibc.git;a=blob;f=elf/link.h;h=b645760402514c4839686aaeade20dd5bb7725dd;hb=HEAD#l101>
#[derive(Debug, Clone, Default)]
#[repr(C)]
pub struct LinkMap {
/* These first few members are part of the protocol with the debugger.
This is the same format used in SVR4. */
l_addr: ElfAddr, /* Difference between the address in the ELF
file and the addresses in memory. */
l_name: usize, /* Absolute file name object was found in. WAS: `char*` */
l_ld: usize, /* Dynamic section of the shared object. WAS: `ElfW(Dyn) *` */
l_next: usize, /* Chain of loaded objects. WAS: `struct link_map *` */
l_prev: usize, /* Chain of loaded objects. WAS: `struct link_map *` */
/// Difference between the addresses in the ELF file and the addresses in
/// memory.
pub(crate) l_addr: ElfAddr,
/// Address of the absolute file name the object was found in.
/// WAS: `char *`
pub(crate) l_name: usize,
/// Address of the dynamic section of the shared object.
/// WAS: `ElfW(Dyn) *`
pub(crate) l_ld: usize,
/// Address of the next node in the chain of loaded objects.
/// WAS: `struct link_map *`
pub(crate) l_next: usize,
/// Address of the previous node in the chain of loaded objects.
/// WAS: `struct link_map *`
pub(crate) l_prev: usize,
}

// COPY from <link.h>
/// This state value describes the mapping change taking place when
/// the `r_brk' address is called.
#[derive(Debug, Clone, Default)]
#[allow(non_camel_case_types, unused)]
#[repr(C)]
enum RState {
/// Mapping change is complete.
#[default]
RT_CONSISTENT,
/// Beginning to add a new object.
RT_ADD,
/// Beginning to remove an object mapping.
RT_DELETE,
}

// COPY from <link.h>
/// Shared object loading information for the debugger.
///
/// The Linux dynamic linker fills this in and points the main program's
/// `DT_DEBUG` dynamic entry at it. It is known as the "debugger rendez-vous"
/// point and is a legacy structure to assist debuggers in locating loaded
/// shared modules.
///
/// (But we're going to use it for minidump generation purposes.)
///
/// <https://sourceware.org/git/?p=glibc.git;a=blob;f=elf/link.h;h=b645760402514c4839686aaeade20dd5bb7725dd;hb=HEAD#l40>
#[derive(Debug, Clone, Default)]
#[repr(C)]
pub struct RDebug {
r_version: libc::c_int, /* Version number for this protocol. */
r_map: usize, /* Head of the chain of loaded objects. WAS: `struct link_map *` */

/* This is the address of a function internal to the run-time linker,
that will always be called when the linker begins to map in a
library or unmap it, and again when the mapping change is complete.
The debugger can set a breakpoint at this address if it wants to
notice shared object mapping changes. */
r_brk: ElfAddr,
r_state: RState,
r_ldbase: ElfAddr, /* Base address the linker is loaded at. */
/// Version number for this protocol.
pub(crate) r_version: libc::c_int,
/// Address of the head of the chain of loaded objects.
/// WAS: `struct link_map *`
pub(crate) r_map: usize,
/// Address of a function internal to the run-time linker, that will always
/// be called when the linker begins to map in a library or unmap it, and
/// again when the mapping change is complete. The debugger can set a
/// breakpoint at this address if it wants to notice shared object mapping
/// changes.
pub(crate) r_brk: ElfAddr,
/// Which mapping change is taking place when `r_brk` is called: 0
/// (`RT_CONSISTENT`, the change is complete), 1 (`RT_ADD`, beginning to add
/// a new object) or 2 (`RT_DELETE`, beginning to remove an object mapping).
pub(crate) r_state: libc::c_int,
/// Base address the linker is loaded at.
pub(crate) r_ldbase: ElfAddr,
}

// Safety: both are `repr(C)` aggregates of plain integers, so every bit pattern
// we could read out of the target process is a valid value.
unsafe impl Plain for LinkMap {}
unsafe impl Plain for RDebug {}

pub fn write_dso_debug_stream(
process_inspector: &dyn ProcessInspector,
buffer: &mut Buffer,
Expand Down Expand Up @@ -146,30 +178,24 @@ pub fn write_dso_debug_stream(

dyn_addr += base as ElfAddr;

let dyn_size = std::mem::size_of::<goblin::elf::Dyn>();
let dyn_size = std::mem::size_of::<elf::dynamic::Dyn>();
let mut r_debug = 0usize;
let mut dynamic_length = 0usize;
let memory_reader = process_inspector.process_reader();

// The dynamic linker makes information available that helps gdb find all
// DSOs loaded into the program. If this information is indeed available,
// dump it to a MD_LINUX_DSO_DEBUG stream.
loop {
let dyn_data = MinidumpWriter::copy_from_process(
process_inspector,
dyn_addr as usize + dynamic_length,
dyn_size,
)?;
let dyn_struct: elf::dynamic::Dyn =
memory_reader.read_pod(dyn_addr as usize + dynamic_length)?;
dynamic_length += dyn_size;

// goblin::elf::Dyn doesn't have padding bytes
let (head, body, _tail) = unsafe { dyn_data.align_to::<goblin::elf::Dyn>() };
assert!(head.is_empty(), "Data was not aligned");
let dyn_struct = &body[0];

let debug_tag = goblin::elf::dynamic::DT_DEBUG;
if dyn_struct.d_tag == debug_tag {
#[allow(clippy::useless_conversion)]
let d_tag = u64::from(dyn_struct.d_tag);
if d_tag == goblin::elf::dynamic::DT_DEBUG {
r_debug = dyn_struct.d_val as usize;
} else if dyn_struct.d_tag == goblin::elf::dynamic::DT_NULL {
} else if d_tag == goblin::elf::dynamic::DT_NULL {
break;
}
}
Expand All @@ -178,38 +204,18 @@ pub fn write_dso_debug_stream(
// loaded DSOs.
// Our list of DSOs potentially is different from the ones in the crashing
// process. So, we have to be careful to never dereference pointers
// directly. Instead, we use CopyFromProcess() everywhere.
// directly. Instead, we copy every node out of the process.
// See <link.h> for a more detailed discussion of the how the dynamic
// loader communicates with debuggers.

let debug_entry_data = MinidumpWriter::copy_from_process(
process_inspector,
r_debug,
std::mem::size_of::<RDebug>(),
)?;

// goblin::elf::Dyn doesn't have padding bytes
let (head, body, _tail) = unsafe { debug_entry_data.align_to::<RDebug>() };
assert!(head.is_empty(), "Data was not aligned");
let debug_entry = &body[0];
let debug_entry: RDebug = memory_reader.read_pod(r_debug)?;

// Count the number of loaded DSOs
let mut dso_vec = Vec::new();
let mut curr_map = debug_entry.r_map;
while curr_map != 0 {
let link_map_data = MinidumpWriter::copy_from_process(
process_inspector,
curr_map,
std::mem::size_of::<LinkMap>(),
)?;

// LinkMap is repr(C) and doesn't have padding bytes, so this should be safe
let (head, body, _tail) = unsafe { link_map_data.align_to::<LinkMap>() };
assert!(head.is_empty(), "Data was not aligned");
let map = &body[0];

let map: LinkMap = memory_reader.read_pod(curr_map)?;
curr_map = map.l_next;
dso_vec.push(map.clone());
dso_vec.push(map);
}

let mut linkmap_rva = u32::MAX;
Expand Down
2 changes: 2 additions & 0 deletions src/linux/process_inspection/mod.rs
Original file line number Diff line number Diff line change
Expand Up @@ -268,6 +268,8 @@ pub enum Error {
AddressOverflowed,
#[error("unexpected end of buffer reached")]
UnexpectedEndOfBuffer,
#[error("got an unexpected end of file")]
UnexpectedEndOfFile,
}

fn set_process_backend_drop_fail_handler() {
Expand Down
119 changes: 119 additions & 0 deletions src/linux/process_inspection/process_reader.rs
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,8 @@ use super::{
10000 };
use crate::module_reader::ProcessModuleMemoryReader;

use plain::Plain;

pub type ProcessHandle = libc::pid_t;

#[derive(Debug)]
Expand All @@ -16,6 +18,123 @@ impl<'a> ProcessReader<'a> {
pub fn read(&self, src: usize, dst: &mut [u8]) -> Result<usize, CopyFromProcessError> {
self.0.read_at(src, dst)
}

/// Read memory from the process until the buffer is filled.
///
/// This is a convenience wrapper around [`ProcessReader::read`]. Unlike
/// [`read`](Self::read), this method does not return successful short
/// reads. It repeatedly calls [`read`](Self::read), advancing `address`
/// and the output buffer by the number of bytes read, until the entire buffer
/// has been filled.
///
/// If an underlying read fails before the buffer is filled, this method returns
/// that error as is. If an underlying read succeeds but returns `0`
/// bytes before the buffer is filled, this method returns
/// [`CopyFromProcessError::Backend`] containing [`Error::UnexpectedEndOfFile`].
///
/// On success, all of `buf` has been filled with bytes read from the target
/// process.
///
/// # Errors
///
/// Returns whichever error [`read`](Self::read) would return.
///
/// In addition, returns [`CopyFromProcessError::Backend`] containing
/// [`Error::UnexpectedEndOfFile`] if an underlying read returns `0` bytes
/// before the buffer is full, and [`CopyFromProcessError::Backend`] containing
/// [`Error::AddressOverflowed`] if advancing the read address by the number of
/// bytes read would wrap past the end of the address space.
///
/// If this method returns an error, `buf` may have been partially overwritten.
/// The error type does not report how many bytes were read before the failure.
pub fn read_exact(
&self,
mut src: usize,
mut dst: &mut [u8],
) -> Result<(), CopyFromProcessError> {
if dst.is_empty() {
return Ok(());
}

loop {
let bytes_read = self.read(src, dst)?;
if bytes_read == 0 {
return Err(CopyFromProcessError::Backend(Error::UnexpectedEndOfFile));
}
if bytes_read == dst.len() {
return Ok(());
}
src = src
.checked_add(bytes_read)
.ok_or(CopyFromProcessError::Backend(Error::AddressOverflowed))?;
dst = &mut dst[bytes_read..];
}
}

/// Reads a plain-old-data value of type `T` from the target process at
/// `address`.
///
/// This reads exactly `size_of::<T>()` bytes into a freshly zeroed `T` using
/// [`read_exact`](Self::read_exact). The [`Plain`] bound guarantees that
/// every possible bit pattern is a valid `T`, so the bytes read from the
/// target process are always a valid value.
///
/// # Errors
///
/// See [`read_exact`](Self::read_exact) for the possible errors.
pub fn read_pod<T: Plain>(&self, address: usize) -> Result<T, CopyFromProcessError> {
// Safety: `Plain` is an unsafe trait that may only be implemented on
// types for which every possible bit pattern is valid, so there is
// nothing we could read from the other process that isn't a valid value
// for `T`.
let mut pod: T = unsafe { core::mem::zeroed() };
let bytes = unsafe {
core::slice::from_raw_parts_mut(
core::ptr::from_mut(&mut pod).cast::<u8>(),
size_of::<T>(),
)
};
self.read_exact(address, bytes)?;
Ok(pod)
}

/// Read `count` consecutive plain-old-data values of type `T` starting at
/// `address`.
pub fn read_pod_vec<T: Plain>(
&self,
mut address: usize,
count: usize,
) -> Result<Vec<T>, CopyFromProcessError> {
let mut v = Vec::with_capacity(count);
for _ in 0..count {
v.push(self.read_pod(address)?);
address += std::mem::size_of::<T>();
}
Ok(v)
}

/// Read bytes from the process starting at `address` into `buf` up to and
/// including the first `terminator` byte (or until a read returns no bytes).
///
/// Returns the number of bytes appended to `buf`.
pub fn read_until(
&self,
mut address: usize,
terminator: u8,
buf: &mut Vec<u8>,
) -> Result<usize, CopyFromProcessError> {
let start_len = buf.len();
let mut b = [0u8];
while self.read(address, &mut b)? > 0 {
buf.push(b[0]);
if b[0] == terminator {
break;
}
address += 1;
}
Ok(buf.len() - start_len)
}

/// Find the address at which a module with the given name is loaded in the process.
pub fn find_module(
&self,
Expand Down
Loading
0