use std::ffi::{c_int, CStr, OsStr};
use std::fmt;
use std::os::unix::ffi::OsStrExt;
use std::path::PathBuf;
use anyhow::Context;
use libc::{c_void, dl_iterate_phdr, dl_phdr_info, size_t, Elf64_Word, PT_LOAD, PT_NOTE};
use once_cell::sync::Lazy;
use tracing::error;
use crate::cast::CastFrom;
#[derive(Clone, Debug)]
pub struct Mapping {
pub memory_start: usize,
pub memory_end: usize,
pub memory_offset: usize,
pub file_offset: u64,
pub pathname: PathBuf,
pub build_id: Option<BuildId>,
}
#[cfg(target_os = "linux")]
pub static MAPPINGS: Lazy<Option<Vec<Mapping>>> = Lazy::new(|| {
fn build_mappings(objects: &[SharedObject]) -> Vec<Mapping> {
let mut mappings = Vec::new();
for object in objects {
for segment in &object.loaded_segments {
let memory_start = object.base_address.wrapping_add(segment.memory_offset);
mappings.push(Mapping {
memory_start,
memory_end: memory_start + segment.memory_size,
memory_offset: segment.memory_offset,
file_offset: segment.file_offset,
pathname: object.path_name.clone(),
build_id: object.build_id.clone(),
});
}
}
mappings
}
match unsafe { crate::linux::collect_shared_objects() } {
Ok(objects) => Some(build_mappings(&objects)),
Err(err) => {
error!("build ID fetching failed: {err}");
None
}
}
});
#[cfg(not(target_os = "linux"))]
pub static MAPPINGS: Lazy<Option<Vec<Mapping>>> = Lazy::new(|| {
error!("build ID fetching is only supported on Linux");
None
});
#[derive(Clone, Debug, PartialEq, Eq, PartialOrd, Ord)]
pub struct SharedObject {
pub base_address: usize,
pub path_name: PathBuf,
pub build_id: Option<BuildId>,
pub loaded_segments: Vec<LoadedSegment>,
}
#[derive(Clone, Debug, PartialEq, Eq, PartialOrd, Ord)]
pub struct BuildId(Vec<u8>);
impl fmt::Display for BuildId {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
for byte in &self.0 {
write!(f, "{byte:02x}")?;
}
Ok(())
}
}
#[derive(Clone, Debug, PartialEq, Eq, PartialOrd, Ord)]
pub struct LoadedSegment {
pub file_offset: u64,
pub memory_offset: usize,
pub memory_size: usize,
}
pub unsafe fn collect_shared_objects() -> Result<Vec<SharedObject>, anyhow::Error> {
let mut state = CallbackState {
result: Ok(Vec::new()),
};
let state_ptr = std::ptr::addr_of_mut!(state).cast();
unsafe { dl_iterate_phdr(Some(iterate_cb), state_ptr) };
state.result
}
struct CallbackState {
result: Result<Vec<SharedObject>, anyhow::Error>,
}
impl CallbackState {
fn is_first(&self) -> bool {
match &self.result {
Ok(v) => v.is_empty(),
Err(_) => false,
}
}
}
const CB_RESULT_OK: c_int = 0;
const CB_RESULT_ERROR: c_int = -1;
unsafe extern "C" fn iterate_cb(
info: *mut dl_phdr_info,
_size: size_t,
data: *mut c_void,
) -> c_int {
let state: *mut CallbackState = data.cast();
assert_pointer_valid(state);
let state = unsafe { state.as_mut() }.expect("pointer is valid");
assert_pointer_valid(info);
let info = unsafe { info.as_ref() }.expect("pointer is valid");
let base_address = usize::cast_from(info.dlpi_addr);
let path_name = if state.is_first() {
match std::env::current_exe().context("failed to read the name of the current executable") {
Ok(pb) => pb,
Err(e) => {
state.result = Err(e);
return CB_RESULT_ERROR;
}
}
} else if info.dlpi_name.is_null() {
return CB_RESULT_OK;
} else {
assert_pointer_valid(info.dlpi_name);
let name = unsafe { CStr::from_ptr(info.dlpi_name) };
OsStr::from_bytes(name.to_bytes()).into()
};
let mut loaded_segments = Vec::new();
let mut build_id = None;
assert_pointer_valid(info.dlpi_phdr);
let program_headers =
unsafe { std::slice::from_raw_parts(info.dlpi_phdr, info.dlpi_phnum.into()) };
for ph in program_headers {
if ph.p_type == PT_LOAD {
loaded_segments.push(LoadedSegment {
file_offset: ph.p_offset,
memory_offset: usize::cast_from(ph.p_vaddr),
memory_size: usize::cast_from(ph.p_memsz),
});
} else if ph.p_type == PT_NOTE {
#[repr(C)]
struct NoteHeader {
n_namesz: Elf64_Word,
n_descsz: Elf64_Word,
n_type: Elf64_Word,
}
let mut offset = usize::cast_from(ph.p_vaddr.wrapping_add(info.dlpi_addr));
let orig_offset = offset;
const NT_GNU_BUILD_ID: Elf64_Word = 3;
const GNU_NOTE_NAME: &[u8; 4] = b"GNU\0";
const ELF_NOTE_STRING_ALIGN: usize = 4;
while offset + std::mem::size_of::<NoteHeader>() + GNU_NOTE_NAME.len()
<= orig_offset + usize::cast_from(ph.p_memsz)
{
#[allow(clippy::as_conversions)]
let nh_ptr = offset as *const NoteHeader;
assert_pointer_valid(nh_ptr);
let nh = unsafe { nh_ptr.as_ref() }.expect("pointer is valid");
if nh.n_type == NT_GNU_BUILD_ID
&& nh.n_descsz != 0
&& usize::cast_from(nh.n_namesz) == GNU_NOTE_NAME.len()
{
#[allow(clippy::as_conversions)]
let p_name = (offset + std::mem::size_of::<NoteHeader>()) as *const [u8; 4];
assert_pointer_valid(p_name);
let name = unsafe { p_name.as_ref() }.expect("pointer is valid");
if name == GNU_NOTE_NAME {
#[allow(clippy::as_conversions)]
let p_desc = (p_name as usize + 4) as *const u8;
assert_pointer_valid(p_desc);
let desc = unsafe {
std::slice::from_raw_parts(p_desc, usize::cast_from(nh.n_descsz))
};
build_id = Some(BuildId(desc.to_vec()));
break;
}
}
offset = offset
+ std::mem::size_of::<NoteHeader>()
+ align_up::<ELF_NOTE_STRING_ALIGN>(usize::cast_from(nh.n_namesz))
+ align_up::<ELF_NOTE_STRING_ALIGN>(usize::cast_from(nh.n_descsz));
}
}
}
let objects = state.result.as_mut().expect("we return early on errors");
objects.push(SharedObject {
base_address,
path_name,
build_id,
loaded_segments,
});
CB_RESULT_OK
}
pub const fn align_up<const N: usize>(p: usize) -> usize {
if p % N == 0 {
p
} else {
p + (N - (p % N))
}
}
fn assert_pointer_valid<T>(ptr: *const T) {
#[allow(clippy::as_conversions)]
let address = ptr as usize;
let align = std::mem::align_of::<T>();
assert!(!ptr.is_null());
assert!(address % align == 0, "unaligned pointer");
}