diff --git a/elfcore-sample/Cargo.toml b/elfcore-sample/Cargo.toml index ed57f62..c138f2c 100644 --- a/elfcore-sample/Cargo.toml +++ b/elfcore-sample/Cargo.toml @@ -8,4 +8,4 @@ elfcore = { path = "../elfcore" } anyhow.workspace = true tracing.workspace = true -tracing-subscriber.workspace = true \ No newline at end of file +tracing-subscriber.workspace = true diff --git a/elfcore-sample/src/main.rs b/elfcore-sample/src/main.rs index c41e304..62bd8a1 100644 --- a/elfcore-sample/src/main.rs +++ b/elfcore-sample/src/main.rs @@ -11,16 +11,20 @@ //! debug level tracing. //! -#![cfg(target_os = "linux")] - +#[cfg(target_os = "linux")] use anyhow::Context; +#[cfg(target_os = "linux")] +use elfcore::{CoreDumpBuilder, LinuxProcessMemoryReader, ProcessView}; +#[cfg(target_os = "linux")] use std::path::PathBuf; +#[cfg(target_os = "linux")] use tracing::Level; +#[cfg(target_os = "linux")] pub fn main() -> anyhow::Result<()> { let mut args = std::env::args().skip(1).peekable(); - let level = if args.peek().map_or(false, |x| x == "-v") { + let level = if args.peek().is_some_and(|x| x == "-v") { args.next(); Level::DEBUG } else { @@ -53,7 +57,7 @@ pub fn main() -> anyhow::Result<()> { .with_max_level(level) .init(); - let mut builder = elfcore::CoreDumpBuilder::new(pid)?; + let mut builder = CoreDumpBuilder::::new(pid)?; let mut file = note_file_path .map(|path| { @@ -72,3 +76,8 @@ pub fn main() -> anyhow::Result<()> { tracing::debug!("wrote {} bytes", n); Ok(()) } + +#[cfg(not(target_os = "linux"))] +pub fn main() { + println!("Creating core dumps for a given Pid is only supported on Linux"); +} diff --git a/elfcore/Cargo.toml b/elfcore/Cargo.toml index 5a59972..6f31547 100644 --- a/elfcore/Cargo.toml +++ b/elfcore/Cargo.toml @@ -10,11 +10,11 @@ keywords = ["elf", "coredump", "debugging"] categories = ["development-tools::debugging", "os::linux-apis"] [dependencies] - -[target.'cfg(unix)'.dependencies] -libc.workspace = true -nix.workspace = true smallvec.workspace = true thiserror.workspace = true tracing.workspace = true zerocopy.workspace = true + +[target.'cfg(unix)'.dependencies] +libc.workspace = true +nix.workspace = true diff --git a/elfcore/src/arch/aarch64.rs b/elfcore/src/arch/aarch64.rs index c394fa2..fea8e41 100644 --- a/elfcore/src/arch/aarch64.rs +++ b/elfcore/src/arch/aarch64.rs @@ -6,11 +6,14 @@ #![cfg(target_arch = "aarch64")] use super::ArchComponentState; -use crate::ptrace::ptrace_get_reg_set; use crate::CoreError; -use nix::unistd::Pid; use zerocopy::AsBytes; +#[cfg(target_os = "linux")] +use crate::ptrace::ptrace_get_reg_set; +#[cfg(target_os = "linux")] +use nix::unistd::Pid; + // aarch64 machine pub const EM_AARCH64: u16 = 183; @@ -29,19 +32,24 @@ pub struct elf_gregset_t { pub pstate: u64, } +#[cfg(target_os = "linux")] pub fn get_aarch64_tls(pid: Pid) -> Result, CoreError> { ptrace_get_reg_set(pid, NT_ARM_TLS) } +#[cfg(target_os = "linux")] pub fn get_aarch64_hw_break(pid: Pid) -> Result, CoreError> { ptrace_get_reg_set(pid, NT_ARM_HW_BREAK) } +#[cfg(target_os = "linux")] pub fn get_aarch64_hw_watch(pid: Pid) -> Result, CoreError> { ptrace_get_reg_set(pid, NT_ARM_HW_WATCH) } +#[cfg(target_os = "linux")] pub fn get_aarch64_system_call(pid: Pid) -> Result, CoreError> { ptrace_get_reg_set(pid, NT_ARM_SYSTEM_CALL) } +#[cfg(target_os = "linux")] pub fn get_arch_components(pid: Pid) -> Result, CoreError> { let components = vec![ ArchComponentState { diff --git a/elfcore/src/arch/mod.rs b/elfcore/src/arch/mod.rs index a56ce85..5f3e6c3 100644 --- a/elfcore/src/arch/mod.rs +++ b/elfcore/src/arch/mod.rs @@ -3,12 +3,13 @@ //! A Rust helper library with machine-specific code for ELF core dump files. -use crate::elf::NT_PRFPREG; +#[cfg(target_os = "linux")] +use super::linux::ptrace; +#[cfg(target_os = "linux")] +use crate::{elf::NT_PRFPREG, CoreError}; +#[cfg(target_os = "linux")] use nix::unistd::Pid; -use super::ptrace; -use crate::CoreError; - #[cfg(target_arch = "x86_64")] mod x86_64; #[cfg(target_arch = "x86_64")] @@ -19,31 +20,40 @@ mod aarch64; #[cfg(target_arch = "aarch64")] pub use aarch64::elf_gregset_t; +/// Contains SSE registers on amd64, NEON on arm64, +/// XSAVE state on amd64, etc #[derive(Debug)] pub struct ArchComponentState { + /// Name pub name: &'static str, + /// Note type pub note_type: u32, + /// Note name pub note_name: &'static [u8], + /// Data pub data: Vec, } -pub trait Arch { +pub(crate) trait Arch { const EM_ELF_MACHINE: u16; + #[cfg(target_os = "linux")] fn new(pid: Pid) -> Result, CoreError>; + #[allow(dead_code)] fn name() -> &'static str; fn greg_set(&self) -> elf_gregset_t; fn components(&self) -> &Vec; } +/// Describes CPU state #[derive(Debug)] pub struct ArchState { - // GP registers. - gpr_state: Vec, + /// GP registers. + pub gpr_state: Vec, - // Contains SSE registers on amd64, NEON on arm64, - // XSAVE state on amd64, etc - components: Vec, + /// Contains SSE registers on amd64, NEON on arm64, + /// XSAVE state on amd64, etc + pub components: Vec, } impl Arch for ArchState { @@ -52,6 +62,7 @@ impl Arch for ArchState { #[cfg(target_arch = "aarch64")] const EM_ELF_MACHINE: u16 = aarch64::EM_AARCH64; + #[cfg(target_os = "linux")] fn new(pid: Pid) -> Result, CoreError> { tracing::debug!("Getting GP registers for #{pid}"); let gpr_state = ptrace::get_gp_reg_set(pid)?; diff --git a/elfcore/src/arch/x86_64.rs b/elfcore/src/arch/x86_64.rs index 8e3bba4..0ff2a56 100644 --- a/elfcore/src/arch/x86_64.rs +++ b/elfcore/src/arch/x86_64.rs @@ -3,11 +3,13 @@ //! x86_64 specifics for ELF core dump files. -#![cfg(target_arch = "x86_64")] - +#[cfg(target_os = "linux")] use super::ArchComponentState; -use crate::ptrace::ptrace_get_reg_set; +#[cfg(target_os = "linux")] +use crate::linux::ptrace::ptrace_get_reg_set; +#[cfg(target_os = "linux")] use crate::CoreError; +#[cfg(target_os = "linux")] use nix::unistd::Pid; use zerocopy::AsBytes; @@ -15,6 +17,7 @@ use zerocopy::AsBytes; pub const EM_X86_64: u16 = 62; // amd64 notes +#[cfg(target_os = "linux")] pub const NT_X86_XSTATE: u32 = 0x202; #[repr(C, packed)] @@ -52,10 +55,12 @@ pub struct elf_gregset_t { //const AT_SYSINFO_X86: u64 = 32; //const AT_SYSINFO_EHDR_X86: u64 = 33; returns vdso +#[cfg(target_os = "linux")] pub fn get_x86_xsave_set(pid: Pid) -> Result, CoreError> { ptrace_get_reg_set(pid, NT_X86_XSTATE) } +#[cfg(target_os = "linux")] pub fn get_arch_components(pid: Pid) -> Result, CoreError> { let components = vec![ArchComponentState { name: "XSAVE", diff --git a/elfcore/src/coredump.rs b/elfcore/src/coredump.rs index b7712c5..047ebb8 100644 --- a/elfcore/src/coredump.rs +++ b/elfcore/src/coredump.rs @@ -9,31 +9,18 @@ use super::arch; use super::arch::Arch; use crate::elf::*; -use crate::ptrace::ptrace_interrupt; use crate::CoreError; -use nix::libc::Elf64_Phdr; -use nix::sys; -use nix::sys::ptrace::seize; -use nix::sys::ptrace::Options; -use nix::sys::uio::process_vm_readv; -use nix::sys::uio::RemoteIoVec; -use nix::sys::wait::waitpid; -use nix::unistd::sysconf; -use nix::unistd::Pid; -use nix::unistd::SysconfVar; +use crate::ProcessInfoSource; +use crate::ReadProcessMemory; use smallvec::smallvec; use smallvec::SmallVec; -use std::collections::HashSet; -use std::fs; -use std::fs::File; -use std::io::BufRead; -use std::io::IoSliceMut; use std::io::Read; -use std::io::Seek; use std::io::Write; use std::slice; use zerocopy::AsBytes; -use zerocopy::FromZeroes; + +#[cfg(target_os = "linux")] +use crate::{LinuxProcessMemoryReader, ProcessView}; const ELF_HEADER_ALIGN: usize = 8; const ELF_NOTE_ALIGN: usize = 4; @@ -106,255 +93,52 @@ struct MappedFilesNoteItem { page_count: u64, } -// Linux Light-weight Process -#[derive(Debug)] -struct ThreadView { - // Thread id. - tid: Pid, - - // Command line. - cmd_line: String, - - // The filename of the executable, in parentheses. - // This is visible whether or not the executable is - // swapped out. - comm: String, - - // One of the following characters, indicating process - // state: - // R Running - // S Sleeping in an interruptible wait - // D Waiting in uninterruptible disk sleep - // Z Zombie - // T Stopped (on a signal) or (before Linux 2.6.33) - // trace stopped - // t Tracing stop (Linux 2.6.33 onward) - // W Paging (only before Linux 2.6.0) - // X Dead (from Linux 2.6.0 onward) - // x Dead (Linux 2.6.33 to 3.13 only) - // K Wakekill (Linux 2.6.33 to 3.13 only) - // W Waking (Linux 2.6.33 to 3.13 only) - // P Parked (Linux 3.9 to 3.13 only) - state: u8, - - // The PID of the parent of this process. - ppid: i32, - - // The process group ID of the process. - pgrp: i32, - - // The session ID of the process. - session: i32, - - // The kernel flags word of the process. For bit mean‐ - // ings, see the PF_* defines in the Linux kernel - // source file include/linux/sched.h. Details depend - // on the kernel version. - // The format for this field was %lu before Linux 2.6. - flags: i32, - - // Amount of time that this process has been scheduled - // in user mode, measured in clock ticks (divide by - // sysconf(_SC_CLK_TCK)). This includes guest time, - // guest_time (time spent running a virtual CPU, see - // below), so that applications that are not aware of - // the guest time field do not lose that time from - // their calculations. - utime: u64, - - // Amount of time that this process has been scheduled - // in kernel mode, measured in clock ticks (divide by - // sysconf(_SC_CLK_TCK)). - stime: u64, - - // Amount of time that this process's waited-for chil‐ - // dren have been scheduled in user mode, measured in - // clock ticks (divide by sysconf(_SC_CLK_TCK)). (See - // also times(2).) This includes guest time, - // cguest_time (time spent running a virtual CPU, see - // below). - cutime: u64, - - // Amount of time that this process's waited-for chil‐ - // dren have been scheduled in kernel mode, measured in - // clock ticks (divide by sysconf(_SC_CLK_TCK)). - cstime: u64, - - // The nice value (see setpriority(2)), a value in the - // range 19 (low priority) to -20 (high priority). - nice: u64, - - // User Id. - uid: u64, - - // Group Id. - gid: u32, - - // Current signal. - cursig: u16, - - // Blocked signal. - sighold: u64, - - // Pending signal. - sigpend: u64, - - arch_state: Box, -} - -impl ThreadView { - fn new(pid: Pid, tid: Pid) -> Result { - let cmd_line_path = format!("/proc/{}/task/{}/cmdline", pid, tid); - tracing::debug!("Reading {cmd_line_path}"); - let cmd_line = fs::read_to_string(cmd_line_path)?; - - // When parsing the stat file, have to handle the spaces in the program path. - // - // Here is the RE for the line: - // - // r"(\d+) \({1,1}?(.*)\){1,1}? ([RSDZTtWXxKWP]) " - // r"([+-]?\d+) ([+-]?\d+) ([+-]?\d+) ([+-]?\d+) ([+-]?\d+) ([+-]?\d+) (\d+) (\d+) (\d+) " - // r"(\d+) (\d+) (\d+) (\d+) (\d+) (\d+) (\d+) (\d+) (\d+) " - // r"(\d+) (\d+) (\d+) (\d+) (\d+) (\d+) (\d+) (\d+) (\d+)" - - let stat_path = format!("/proc/{}/task/{}/stat", pid, tid); - tracing::debug!("Reading {stat_path}"); - let stat_str = fs::read_to_string(stat_path)?; - let stat_str_trim = stat_str.trim(); - - let comm_pos_start = stat_str_trim.find('('); - let comm_pos_end = stat_str_trim.rfind(')'); - if comm_pos_start.is_none() || comm_pos_end.is_none() { - tracing::error!( - "Unsupported format of the procfs stat file, could not find command line: {}", - stat_str - ); - return Err(CoreError::ProcParsingError); - } - let comm_pos_start = comm_pos_start.unwrap(); - let comm_pos_end = comm_pos_end.unwrap(); - let comm = String::from(&stat_str_trim[comm_pos_start + 1..comm_pos_end - 1]); - - let stat_str_split = stat_str_trim[comm_pos_end + 2..] - .split(' ') - .collect::>(); - if stat_str_split.len() < 30 { - tracing::error!("Unsupported format of the procfs stat file: {}, found {} entries after the command line", stat_str, stat_str_split.len()); - return Err(CoreError::ProcParsingError); - } - - let state = { - let mut buf = [0_u8; 8]; - stat_str_split[0] - .chars() - .next() - .ok_or(CoreError::ProcParsingError)? - .encode_utf8(&mut buf); - - buf[0] - }; - - let mut uid: u64 = 0; - let mut gid: u32 = 0; - let mut cursig: u16 = 0; - let mut sighold: u64 = 0; - let mut sigpend: u64 = 0; - - { - let status_path = format!("/proc/{pid}/task/{tid}/status"); - tracing::debug!("Reading {status_path}"); - let status_file = fs::File::open(&status_path)?; - let reader = std::io::BufReader::new(status_file); - - // The common trait for the lines is a prefix followed by the tab - // character and then there is a number. After the number there might be - // various characters (whitespace, slashes) so using splitn seems to be - // difficult. - - let parse_first_number = |s: &str| { - s.chars() - .map(|c| c.to_digit(10)) - .take_while(|opt| opt.is_some()) - .fold(0, |acc: u64, digit| acc * 10 + digit.unwrap() as u64) - }; - - for line in reader.lines() { - let line = line?; - let line = line.trim(); - - tracing::debug!("Reading {status_path}: {line}"); - - if let Some(s) = line.strip_prefix("Uid:\t") { - uid = parse_first_number(s); - } else if let Some(s) = line.strip_prefix("Gid:\t") { - gid = parse_first_number(s) as u32; - } else if let Some(s) = line.strip_prefix("SigQ:\t") { - cursig = parse_first_number(s) as u16; - } else if let Some(s) = line.strip_prefix("SigBlk:\t") { - sighold = parse_first_number(s) - } else if let Some(s) = line.strip_prefix("SigPnd:\t") { - sigpend = parse_first_number(s) - } - } - } - - let arch_state = arch::ArchState::new(tid)?; - - Ok(Self { - tid, - cmd_line, - comm, - state, - ppid: stat_str_split[1].parse::()?, - pgrp: stat_str_split[2].parse::()?, - session: stat_str_split[3].parse::()?, - flags: stat_str_split[6].parse::()?, - utime: stat_str_split[11].parse::()?, - stime: stat_str_split[12].parse::()?, - cutime: stat_str_split[13].parse::()?, - cstime: stat_str_split[14].parse::()?, - nice: stat_str_split[16].parse::()?, - uid, - gid, - cursig, - sighold, - sigpend, - arch_state, - }) - } -} - +/// Struct that describes a region's access permissions #[derive(Debug)] -#[allow(dead_code)] -struct VaProtection { - is_private: bool, - read: bool, - write: bool, - execute: bool, +pub struct VaProtection { + /// Field that indicates this is a private region + pub is_private: bool, + /// Read permissions + pub read: bool, + /// Write permissions + pub write: bool, + /// Execute permissions + pub execute: bool, } +/// Struct that describes a memory region #[derive(Debug)] -#[allow(dead_code)] -struct VaRegion { - begin: u64, - end: u64, - offset: u64, - protection: VaProtection, - mapped_file_name: Option, +pub struct VaRegion { + /// Virtual address start + pub begin: u64, + /// Virtual address end + pub end: u64, + /// Offset in memory where the region resides + pub offset: u64, + /// Access permissions + pub protection: VaProtection, + /// Mapped file name + pub mapped_file_name: Option, } +/// Type that describes a mapped file region #[derive(Debug)] -#[allow(dead_code)] -struct MappedFileRegion { - begin: u64, - end: u64, - offset: u64, +pub struct MappedFileRegion { + /// Virtual address start + pub begin: u64, + /// Virtual address end + pub end: u64, + /// Offset in memory where the region resides + pub offset: u64, } +/// Type that describes a mapped file #[derive(Debug)] -struct MappedFile { - name: String, - regions: Vec, +pub struct MappedFile { + /// File name + pub name: String, + /// File regions + pub regions: Vec, } #[derive(Default)] @@ -367,18 +151,6 @@ struct NoteSizes { total_note_size: usize, } -/// View of a Linux light-weight process -pub struct ProcessView { - pid: Pid, - threads: Vec, - va_regions: Vec, - mapped_files: Vec, - // Auxiliary vector types. - // The kernel exposes some system configuration using it. - aux_vector: Vec, - page_size: usize, -} - /// Information about a custom note that will be created from a file struct CustomFileNote<'a> { /// Name used in the ELF note header @@ -390,253 +162,8 @@ struct CustomFileNote<'a> { pub note_len: usize, } -fn get_thread_ids(pid: Pid) -> Result, CoreError> { - let mut threads = Vec::new(); - let task_dir = format!("/proc/{}/task", pid); - tracing::debug!("Reading {task_dir}"); - let paths = std::fs::read_dir(task_dir)?; - - tracing::debug!( - "Enumerating threads(light-weight processes) for the process {}", - pid - ); - - for entry in paths { - let entry = entry?; - let path = entry.path(); - - let metadata = std::fs::metadata(&path)?; - if metadata.is_dir() { - let stem = path.file_stem(); - if let Some(stem) = stem { - if stem != "." && stem != ".." { - let stem = stem.to_string_lossy(); - let tid = Pid::from_raw(stem.parse::()? as nix::libc::pid_t); - - tracing::debug!("Found thread {}", tid); - - threads.push(tid) - } - } - } - } - - Ok(threads) -} - -fn get_aux_vector(pid: Pid) -> Result, CoreError> { - let mut auxv: Vec = Vec::new(); - - let auxv_file_name = format!("/proc/{}/auxv", pid); - tracing::debug!("Reading {auxv_file_name}"); - let mut file = File::open(auxv_file_name)?; - - loop { - let mut aux = Elf64_Auxv { - a_type: 0, - a_val: 0, - }; - - match file.read_exact(aux.as_bytes_mut()) { - Ok(_) => auxv.push(aux), - Err(_) => break, - } - } - - Ok(auxv) -} - -fn get_va_regions(pid: Pid) -> Result<(Vec, Vec, u64), CoreError> { - let mut maps: Vec = Vec::new(); - let mut vdso = 0_u64; - - let mut mapped_elfs: HashSet = HashSet::new(); - let mut mapped_non_elfs: HashSet = HashSet::new(); - let mut mapped_files: Vec = Vec::new(); - - let maps_path = format!("/proc/{}/maps", pid); - tracing::debug!("Reading {maps_path}"); - let maps_file = fs::File::open(maps_path)?; - let reader = std::io::BufReader::new(maps_file); - - for line in reader.lines() { - let line = line?; - let parts: Vec<&str> = line.split_whitespace().collect(); - - tracing::debug!("Memory maps: {:?}", parts); - - let begin_end: Vec<&str> = parts[0].split('-').collect(); - let begin = u64::from_str_radix(begin_end[0], 16)?; - let end = u64::from_str_radix(begin_end[1], 16)?; - let offset = u64::from_str_radix(parts[2], 16)?; - - let mapped_file_name = { - let last = *parts.last().ok_or(CoreError::ProcParsingError)?; - if last == "[vdso]" { - vdso = begin; - - //None - tracing::info!("Skipping VA range mapped to {}", last); - continue; - } else if last == "[vvar]" || last == "[vsyscall]" { - //None - tracing::info!("Skipping VA range mapped to {}", last); - continue; - } else if last.starts_with('/') { - if last.starts_with("/dev/") { - // Reading device memory might have unintended side effects. - // Always skip. - tracing::info!("Skipping VA range mapped to device file {}", last); - continue; - } - - Some(String::from(last)) - } else { - None - } - }; - - let is_private = parts[1].chars().nth(3).ok_or(CoreError::ProcParsingError)? == 'p'; - let is_shared = parts[1].chars().nth(3).ok_or(CoreError::ProcParsingError)? == 's'; - - if !is_private && !is_shared { - tracing::info!( - "Skipping non-accessible VA range [0x{:x}; 0x{:x}]", - begin, - end - ); - continue; - } - - let protection = VaProtection { - read: parts[1].starts_with('r'), - write: parts[1].chars().nth(1).ok_or(CoreError::ProcParsingError)? == 'w', - execute: parts[1].chars().nth(2).ok_or(CoreError::ProcParsingError)? == 'x', - is_private, - }; - - if !protection.read && !protection.write && !protection.execute { - tracing::info!( - "Skipping non-accessible VA range [0x{:x}; 0x{:x}]", - begin, - end - ); - continue; - } - - // TODO also can skip over read-only regions and executable regions. - // These can be loaded from shared objects available on the system. - - // Was that file seen before and is that an ELF one? - if let Some(ref mapped_file_name) = mapped_file_name { - if !mapped_elfs.contains(mapped_file_name) - && !mapped_non_elfs.contains(mapped_file_name) - { - // First time for that file. - // See if the mapped file is an ELF one, otherwise skip over it - // as it might be quite huge and may contains secrets, filter out by default. - // TODO: make optional. - - let maybe_elf_hdr: Option = { - let mut elf_hdr = Elf64_Ehdr::new_zeroed(); - match process_vm_readv( - pid, - &mut [IoSliceMut::new(elf_hdr.as_bytes_mut())], - &[RemoteIoVec { - base: begin as usize, - len: std::mem::size_of::(), - }], - ) { - Ok(_) => Some(elf_hdr), - Err(_) => None, - } - }; - - if let Some(elf_hdr) = maybe_elf_hdr { - if elf_hdr.e_ident[EI_MAG0] == ELFMAG0 - && elf_hdr.e_ident[EI_MAG1] == ELFMAG1 - && elf_hdr.e_ident[EI_MAG2] == ELFMAG2 - && elf_hdr.e_ident[EI_MAG3] == ELFMAG3 - && elf_hdr.e_ident[EI_VERSION] == EV_CURRENT - && elf_hdr.e_ehsize == std::mem::size_of::() as u16 - && (elf_hdr.e_type == ET_EXEC || elf_hdr.e_type == ET_DYN) - && elf_hdr.e_phentsize == std::mem::size_of::() as u16 - && elf_hdr.e_machine == arch::ArchState::EM_ELF_MACHINE - { - mapped_elfs.insert(mapped_file_name.clone()); - } else { - mapped_non_elfs.insert(mapped_file_name.clone()); - } - } - } - - if mapped_non_elfs.contains(mapped_file_name) { - tracing::info!( - "Skipping VA range mapped to a non-ELF file {}", - mapped_file_name - ); - continue; - } else { - tracing::info!( - "Adding VA range [0x{:x}; 0x{:x}] mapped to an ELF file {}", - begin, - end, - mapped_file_name - ); - } - } - - // Account for the mapped files regions, not concerning - // VA protection. - - if let Some(mapped_file_name) = &mapped_file_name { - if mapped_files.is_empty() { - mapped_files.push(MappedFile { - name: mapped_file_name.clone(), - regions: vec![MappedFileRegion { begin, end, offset }], - }) - } else { - let last_file = mapped_files.last_mut().ok_or(CoreError::ProcParsingError)?; - if last_file.name != *mapped_file_name { - mapped_files.push(MappedFile { - name: mapped_file_name.clone(), - regions: vec![MappedFileRegion { begin, end, offset }], - }) - } else { - let last_region = last_file - .regions - .last_mut() - .ok_or(CoreError::ProcParsingError)?; - - if last_region.end != begin { - last_file - .regions - .push(MappedFileRegion { begin, end, offset }) - } else { - last_region.end = end - } - } - } - } - - // Going to save that VA region into the core dump file - - maps.push(VaRegion { - begin, - end, - offset, - protection, - mapped_file_name, - }); - } - - maps.sort_by_key(|x| x.begin); - - Ok((maps, mapped_files, vdso)) -} - -fn get_elf_notes_sizes( - pv: &ProcessView, +fn get_elf_notes_sizes( + pv: &P, custom_notes: Option<&[CustomFileNote<'_>]>, ) -> Result { let header_and_name = @@ -650,7 +177,7 @@ fn get_elf_notes_sizes( std::mem::size_of::() + { let mut arch_size = 0; for component in pv - .threads + .threads() .first() .ok_or(CoreError::ProcParsingError)? .arch_state @@ -662,23 +189,31 @@ fn get_elf_notes_sizes( }, ELF_NOTE_ALIGN, ); - let process_status = one_thread_status * pv.threads.len(); - let aux_vector = header_and_name + pv.aux_vector.len() * std::mem::size_of::(); - - let mapped_files = { - let mut addr_layout_size = 0_usize; - let mut string_size = 0_usize; - - for mapped_file in &pv.mapped_files { - string_size += (mapped_file.name.len() + 1) * mapped_file.regions.len(); - addr_layout_size += - std::mem::size_of::() * mapped_file.regions.len(); - } + let process_status = one_thread_status * pv.threads().len(); + // Calculate auxv size - do not count if no auxv + let aux_vector = pv + .aux_vector() + .map(|auxv| header_and_name + std::mem::size_of_val(auxv)) + .unwrap_or(0); + + // Calculate mapped files size - do not count if no mapped files + let mapped_files = pv + .mapped_files() + .map(|files| { + let mut addr_layout_size = 0_usize; + let mut string_size = 0_usize; + + for mapped_file in files { + string_size += (mapped_file.name.len() + 1) * mapped_file.regions.len(); + addr_layout_size += + std::mem::size_of::() * mapped_file.regions.len(); + } - let intro_size = std::mem::size_of::(); + let intro_size = std::mem::size_of::(); - header_and_name + round_up(intro_size + addr_layout_size + string_size, ELF_NOTE_ALIGN) - }; + header_and_name + round_up(intro_size + addr_layout_size + string_size, ELF_NOTE_ALIGN) + }) + .unwrap_or(0); let custom = if let Some(custom_notes) = custom_notes { round_up( @@ -708,212 +243,6 @@ fn get_elf_notes_sizes( }) } -impl ProcessView { - /// Creates new process view - /// - /// # Arguments - /// * `pid` - process ID - /// - pub fn new(pid: libc::pid_t) -> Result { - let pid = nix::unistd::Pid::from_raw(pid); - - let mut tids = get_thread_ids(pid)?; - tids.sort(); - - // Guard against calling for itself. Fail early as the seizing the threads - // will fail with -EPERM later. - if tids.binary_search(&nix::unistd::getpid()).is_ok() { - return Err(CoreError::CantDumpItself); - }; - - tracing::info!("Attaching to {} threads of process {}", tids.len(), pid); - - for tid in &tids { - tracing::debug!("Seizing thread {}", *tid); - - if let Err(e) = seize(*tid, Options::empty()) { - tracing::error!("Seizing thread {} failed, error {}", *tid, e); - return Err(CoreError::NixError(e)); - } - - tracing::debug!("Interrupting thread {}", *tid); - - ptrace_interrupt(*tid)?; - - tracing::debug!("Waiting for thread {} to stop", *tid); - - match waitpid(*tid, None) { - Ok(s) => { - tracing::debug!("Thread {} stopped, status {:?}", *tid, s); - } - Err(e) => { - tracing::error!("Waiting for thread {} failed, error {}", *tid, e); - return Err(CoreError::NixError(e)); - } - } - } - - // There is a race here: - // 1) us stopping threads, - // 2) the process that might be creating new ones, - // 3) the existing threads might exit. - // See if the threads ids are still the same. Not bullet-proof as thread ids - // might be re-used. - { - let mut tids_check = get_thread_ids(pid)?; - tids_check.sort(); - - if tids != tids_check { - return Err(CoreError::RaceTryAgain); - } - } - - let threads = tids - .iter() - .map(|tid| ThreadView::new(pid, *tid)) - .collect::>()?; - for thread in &threads { - tracing::debug!("Thread state: {:x?}", thread); - } - - let (va_regions, mapped_files, vdso) = get_va_regions(pid)?; - - tracing::debug!("VA regions {:x?}", va_regions); - tracing::debug!("Mapped files {:x?}", mapped_files); - tracing::debug!("vDSO from the proc maps {:x?}", vdso); - - let aux_vector = get_aux_vector(pid)?; - - tracing::debug!("Auxiliary vector {:x?}", aux_vector); - - let page_size = match sysconf(SysconfVar::PAGE_SIZE) { - Ok(s) => match s { - Some(s) => s as usize, - None => 0x1000_usize, - }, - Err(_) => 0x1000_usize, - }; - - Ok(Self { - pid, - threads, - va_regions, - mapped_files, - aux_vector, - page_size, - }) - } -} - -impl Drop for ProcessView { - fn drop(&mut self) { - tracing::info!( - "Detaching from {} threads of process {}", - self.threads.len(), - self.pid - ); - - for thread in &self.threads { - match sys::ptrace::detach(thread.tid, None) { - Ok(_) => { - tracing::debug!("Thread {} resumed", thread.tid); - } - Err(e) => { - tracing::error!("Thread {} failed to resume: {:?}", thread.tid, e); - } - }; - } - } -} - -/// Trait for those able to read the process virtual memory. -trait ReadProcessMemory { - /// Read process memory into `buf` starting at the virtual address `base`, - /// and returns the number of bytes and or the error. - fn read_process_memory(&mut self, base: usize, buf: &mut [u8]) -> Result; -} - -/// A fast process memory reader employing the `process_vm_readv` system call -/// available on Linux 3.2+. It might be disabled on some systems in the kernel configuration. -struct FastMemoryReader { - pid: Pid, -} - -impl FastMemoryReader { - pub fn new(pid: Pid) -> Result { - Ok(Self { pid }) - } -} - -impl ReadProcessMemory for FastMemoryReader { - fn read_process_memory(&mut self, base: usize, buf: &mut [u8]) -> Result { - let len = buf.len(); - process_vm_readv( - self.pid, - &mut [IoSliceMut::new(buf)], - &[RemoteIoVec { base, len }], - ) - .map_err(CoreError::NixError) - } -} - -/// A slow but more compatible process memory reader, uses the `/proc//mem` -/// file. -struct SlowMemoryReader { - file: std::fs::File, -} - -impl SlowMemoryReader { - pub fn new(pid: Pid) -> Result { - let file = std::fs::OpenOptions::new() - .read(true) - .open(format!("/proc/{pid}/mem")) - .map_err(CoreError::IoError)?; - Ok(Self { file }) - } -} - -impl ReadProcessMemory for SlowMemoryReader { - fn read_process_memory(&mut self, base: usize, buf: &mut [u8]) -> Result { - self.file - .seek(std::io::SeekFrom::Start(base as u64)) - .map_err(CoreError::IoError)?; - self.file.read_exact(buf).map_err(CoreError::IoError)?; - - Ok(buf.len()) - } -} - -/// The `process_vm_readv` system call might be unavailable. An extra check is made to be -/// sure the ABI works. -fn process_vm_readv_works() -> bool { - let probe_in = [0xc1c2c3c4c5c6c7c8_u64]; - let mut probe_out = 0u64.to_le_bytes(); - - let result = process_vm_readv( - nix::unistd::getpid(), - &mut [IoSliceMut::new(&mut probe_out)], - &[RemoteIoVec { - base: probe_in.as_ptr() as usize, - len: std::mem::size_of_val(&probe_in), - }], - ); - - if let Err(e) = result { - tracing::debug!("process_vm_readv has not succeeded, error {e:?}, won't be using it"); - return false; - } - - if probe_in[0] != u64::from_le_bytes(probe_out) { - tracing::debug!( - "process_vm_readv did not return expected data: {probe_in:x?} != {probe_out:x?}, won't be using it" - ); - return false; - } - - true -} - /// Writes an ELF core dump file /// /// # Agruments: @@ -921,32 +250,40 @@ fn process_vm_readv_works() -> bool { /// * `pv` - a `ProcessView` reference. /// /// To access new functionality, use [`CoreDumpBuilder`] +#[cfg(target_os = "linux")] pub fn write_core_dump(writer: T, pv: &ProcessView) -> Result { - write_core_dump_inner(writer, pv, None) + let mut memory_reader = ProcessView::create_memory_reader(pv.pid)?; + write_core_dump_inner::( + writer, + pv, + None, + &mut memory_reader, + ) } -fn write_core_dump_inner( +fn write_core_dump_inner( writer: T, - pv: &ProcessView, + pv: &P, custom_notes: Option<&mut [CustomFileNote<'_>]>, + memory_reader: &mut M, ) -> Result { let mut total_written = 0_usize; let mut writer = ElfCoreWriter::new(writer); + // Check if the process is valid: has threads and va regions + if pv.threads().is_empty() || pv.va_regions().is_empty() { + return Err(CoreError::CustomSourceInfo); + } + + #[cfg(target_os = "linux")] tracing::info!( "Creating core dump file for process {}. This process id: {}, this thread id: {}", - pv.pid, + pv.pid(), nix::unistd::getpid(), nix::unistd::gettid() ); - - let memory_reader = if process_vm_readv_works() { - tracing::info!("Using the fast process memory read on this system"); - Box::new(FastMemoryReader::new(pv.pid)?) as Box - } else { - tracing::info!("Using the slow process memory read on this system"); - Box::new(SlowMemoryReader::new(pv.pid)?) as Box - }; + #[cfg(not(target_os = "linux"))] + tracing::info!("Creating core dump file for process {}.", pv.pid()); let note_sizes = get_elf_notes_sizes(pv, custom_notes.as_deref())?; @@ -955,7 +292,7 @@ fn write_core_dump_inner( total_written += write_program_headers(&mut writer, pv, ¬e_sizes)?; total_written += writer.align_position(ELF_HEADER_ALIGN)?; total_written += write_elf_notes(&mut writer, pv, ¬e_sizes, custom_notes)?; - total_written += writer.align_position(pv.page_size)?; + total_written += writer.align_position(pv.page_size())?; total_written += write_va_regions(&mut writer, pv, memory_reader)?; tracing::info!("Wrote {} bytes for ELF core dump", total_written); @@ -977,9 +314,9 @@ fn round_up(value: usize, alignment: usize) -> usize { } } -fn write_elf_header( +fn write_elf_header( writer: &mut ElfCoreWriter, - pv: &ProcessView, + pv: &P, ) -> Result { let mut e_ident = [0_u8; 16]; e_ident[EI_MAG0] = ELFMAG0; @@ -999,7 +336,7 @@ fn write_elf_header( e_phoff: std::mem::size_of::() as u64, e_ehsize: std::mem::size_of::() as u16, e_phentsize: std::mem::size_of::() as u16, - e_phnum: 1 + pv.va_regions.len() as u16, // PT_NOTE and VA regions + e_phnum: 1 + pv.va_regions().len() as u16, // PT_NOTE and VA regions e_shentsize: 0, e_entry: 0, e_shoff: 0, @@ -1028,9 +365,9 @@ fn write_elf_header( Ok(slice.len()) } -fn write_program_headers( +fn write_program_headers( writer: &mut ElfCoreWriter, - pv: &ProcessView, + pv: &P, note_sizes: &NoteSizes, ) -> Result { tracing::info!( @@ -1044,7 +381,7 @@ fn write_program_headers( // as many PT_LOAD as there are VA regions. // Notes are situated right after the headers. - let phdr_size = std::mem::size_of::() * (pv.va_regions.len() + 1); + let phdr_size = std::mem::size_of::() * (pv.va_regions().len() + 1); let ehdr_size = std::mem::size_of::(); let data_offset = round_up(ehdr_size, ELF_HEADER_ALIGN) + round_up(phdr_size, ELF_HEADER_ALIGN); @@ -1072,9 +409,9 @@ fn write_program_headers( written += slice.len(); } - let mut current_offset = round_up(data_offset + note_sizes.total_note_size, pv.page_size); + let mut current_offset = round_up(data_offset + note_sizes.total_note_size, pv.page_size()); - for region in &pv.va_regions { + for region in pv.va_regions() { let mut seg_header = Elf64_Phdr { p_type: PT_LOAD, p_flags: { @@ -1100,7 +437,7 @@ fn write_program_headers( p_paddr: 0, p_filesz: region.end - region.begin, p_memsz: region.end - region.begin, - p_align: pv.page_size as u64, + p_align: pv.page_size() as u64, }; // SAFETY: Elf64_Phdr is repr(C) with no padding bytes, @@ -1225,9 +562,9 @@ fn write_elf_note_file( Ok(written) } -fn write_process_info_note( +fn write_process_info_note( writer: &mut ElfCoreWriter, - pv: &ProcessView, + pv: &P, ) -> Result { let mut written = 0_usize; @@ -1239,8 +576,8 @@ fn write_process_info_note( // Threads and processes in Linux are LWP (Light-weight processes) // TODO That's O(N) at worst, does that hurt? - for thread_view in &pv.threads { - if thread_view.tid == pv.pid { + for thread_view in pv.threads() { + if thread_view.tid == pv.pid() { let pr_info = prpsinfo_t { pr_state: thread_view.state, pr_sname: thread_view.state, @@ -1250,7 +587,7 @@ fn write_process_info_note( pr_flag: thread_view.flags as u64, pr_uid: thread_view.uid as u32, pr_gid: thread_view.gid, - pr_pid: thread_view.tid.as_raw() as u32, + pr_pid: thread_view.tid as u32, pr_ppid: thread_view.ppid as u32, pr_pgrp: thread_view.pgrp as u32, pr_sid: thread_view.session as u32, @@ -1293,9 +630,9 @@ fn write_process_info_note( Ok(written) } -fn write_process_status_notes( +fn write_process_status_notes( writer: &mut ElfCoreWriter, - pv: &ProcessView, + pv: &P, ) -> Result { let mut total_written = 0_usize; @@ -1304,7 +641,7 @@ fn write_process_status_notes( writer.stream_position()? ); - for thread_view in &pv.threads { + for thread_view in pv.threads() { let status = prstatus_t { si_signo: thread_view.cursig as u32, si_code: 0, @@ -1313,7 +650,7 @@ fn write_process_status_notes( pad0: 0, pr_sigpend: thread_view.sigpend, pr_sighold: thread_view.sighold, - pr_pid: thread_view.tid.as_raw() as u32, + pr_pid: thread_view.tid as u32, pr_ppid: thread_view.ppid as u32, pr_pgrp: thread_view.pgrp as u32, pr_sid: thread_view.session as u32, @@ -1366,71 +703,79 @@ fn write_process_status_notes( tracing::info!( "Wrote {} bytes for the thread status notes, {} notes", total_written, - pv.threads.len() + pv.threads().len() ); Ok(total_written) } -fn write_aux_vector_note( +fn write_aux_vector_note( writer: &mut ElfCoreWriter, - pv: &ProcessView, + pv: &P, ) -> Result { tracing::info!( "Writing auxiliary vector at offset {}...", writer.stream_position()? ); - let written = write_elf_note(writer, NT_AUXV, NOTE_NAME_CORE, pv.aux_vector.as_bytes())?; + let written = pv + .aux_vector() + .map(|auxv| write_elf_note(writer, NT_AUXV, NOTE_NAME_CORE, auxv.as_bytes())) + .unwrap_or(Ok(0))?; tracing::info!("Wrote {} bytes for the auxiliary vector", written); Ok(written) } -fn write_mapped_files_note( +fn write_mapped_files_note( writer: &mut ElfCoreWriter, - pv: &ProcessView, + pv: &P, ) -> Result { tracing::info!( "Writing mapped files note at offset {}...", writer.stream_position()? ); - let mut data: Vec = Vec::with_capacity(pv.page_size); + let written = pv + .mapped_files() + .map(|files| { + let mut data: Vec = Vec::with_capacity(pv.page_size()); - let mut intro = MappedFilesNoteIntro { - file_count: 0, - page_size: 1, - }; + let mut intro = MappedFilesNoteIntro { + file_count: 0, + page_size: 1, + }; - for mapped_file in &pv.mapped_files { - intro.file_count += mapped_file.regions.len() as u64; - } + for mapped_file in files { + intro.file_count += mapped_file.regions.len() as u64; + } - data.extend_from_slice(intro.as_bytes()); + data.extend_from_slice(intro.as_bytes()); - // TODO: Sort by virtual address? Ranges always appear sorted in proc/maps + // TODO: Sort by virtual address? Ranges always appear sorted in proc/maps - for mapped_file in &pv.mapped_files { - for region in &mapped_file.regions { - let item = MappedFilesNoteItem { - start_addr: region.begin, - end_addr: region.end, - page_count: region.offset, // No scaling - }; - data.extend_from_slice(item.as_bytes()); - } - } + for mapped_file in files { + for region in &mapped_file.regions { + let item = MappedFilesNoteItem { + start_addr: region.begin, + end_addr: region.end, + page_count: region.offset, // No scaling + }; + data.extend_from_slice(item.as_bytes()); + } + } - for mapped_file in &pv.mapped_files { - for _ in &mapped_file.regions { - data.extend_from_slice(mapped_file.name.as_bytes()); - data.push(0_u8); - } - } + for mapped_file in files { + for _ in &mapped_file.regions { + data.extend_from_slice(mapped_file.name.as_bytes()); + data.push(0_u8); + } + } - let written = write_elf_note(writer, NT_FILE, NOTE_NAME_CORE, data.as_bytes())?; + write_elf_note(writer, NT_FILE, NOTE_NAME_CORE, data.as_bytes()) + }) + .unwrap_or(Ok(0))?; tracing::info!("Wrote {} bytes for mapped files note", written); @@ -1465,9 +810,9 @@ fn write_custom_notes( Ok(total_written) } -fn write_elf_notes( +fn write_elf_notes( writer: &mut ElfCoreWriter, - pv: &ProcessView, + pv: &P, note_sizes: &NoteSizes, custom_notes: Option<&mut [CustomFileNote<'_>]>, ) -> Result { @@ -1527,12 +872,17 @@ fn write_elf_notes( Ok(total_written) } -fn write_va_region( +fn write_va_region( writer: &mut ElfCoreWriter, va_region: &VaRegion, - pv: &ProcessView, - memory_reader: &mut Box, -) -> Result { + pv: &P, + memory_reader: &mut M, +) -> Result +where + T: Write, + P: ProcessInfoSource, + M: ReadProcessMemory, +{ let mut dumped = 0_usize; let mut address = va_region.begin; let mut buffer = [0_u8; BUFFER_SIZE]; @@ -1556,12 +906,12 @@ fn write_va_region( // Page size is a power of two on modern platforms. debug_assert!( - pv.page_size.is_power_of_two(), + pv.page_size().is_power_of_two(), "Page size is expected to be a power of two" ); // Round up with bit twiddling as the page size is a power of two. - let next_address = (pv.page_size + address as usize) & !(pv.page_size - 1); + let next_address = (pv.page_size() + address as usize) & !(pv.page_size() - 1); let next_address = std::cmp::min(next_address, va_region.end as usize); let dummy_data_size = next_address - address as usize; @@ -1578,11 +928,16 @@ fn write_va_region( Ok(dumped) } -fn write_va_regions( +fn write_va_regions( writer: &mut ElfCoreWriter, - pv: &ProcessView, - mut memory_reader: Box, -) -> Result { + pv: &P, + memory_reader: &mut M, +) -> Result +where + T: Write, + P: ProcessInfoSource, + M: ReadProcessMemory, +{ let mut written = 0_usize; tracing::info!( @@ -1590,8 +945,8 @@ fn write_va_regions( writer.stream_position()? ); - for va_region in &pv.va_regions { - let dumped = write_va_region(writer, va_region, pv, &mut memory_reader)?; + for va_region in pv.va_regions() { + let dumped = write_va_region(writer, va_region, pv, memory_reader)?; written += dumped; @@ -1610,23 +965,41 @@ fn write_va_regions( Ok(written) } -/// A builder for generating a core dump of a process by pid, +/// A builder for generating a core dump of a process /// optionally with custom notes with content from files -pub struct CoreDumpBuilder<'a> { - pv: ProcessView, +/// This also supports generating core dumps with information +/// from a custom source. +pub struct CoreDumpBuilder<'a, P: ProcessInfoSource, M: ReadProcessMemory> { + pv: P, custom_notes: Vec>, + memory_reader: M, } -impl<'a> CoreDumpBuilder<'a> { +impl<'a, P: ProcessInfoSource, M: ReadProcessMemory> CoreDumpBuilder<'a, P, M> { /// Create a new core dump builder for the process with the provided PID - pub fn new(pid: libc::pid_t) -> Result { + #[cfg(target_os = "linux")] + pub fn new( + pid: libc::pid_t, + ) -> Result, CoreError> { let pv = ProcessView::new(pid)?; - Ok(Self { + let memory_reader = ProcessView::create_memory_reader(pv.pid)?; + + Ok(CoreDumpBuilder { pv, custom_notes: Vec::new(), + memory_reader, }) } + /// Create a new core dump builder from a custom `ProcessInfoSource` + pub fn from_source(source: P, memory_reader: M) -> CoreDumpBuilder<'a, P, M> { + CoreDumpBuilder { + pv: source, + custom_notes: Vec::new(), + memory_reader, + } + } + /// Add the contents of a file as a custom note to the core dump pub fn add_custom_file_note( &mut self, @@ -1647,6 +1020,178 @@ impl<'a> CoreDumpBuilder<'a> { /// # Agruments: /// * `writer` - a `std::io::Write` the data is sent to. pub fn write(mut self, writer: T) -> Result { - write_core_dump_inner(writer, &self.pv, Some(&mut self.custom_notes)) + write_core_dump_inner( + writer, + &self.pv, + Some(&mut self.custom_notes), + &mut self.memory_reader, + ) + } +} + +#[cfg(test)] +mod tests { + use crate::{ArchState, ThreadView}; + + use super::*; + + struct MockProcessInfoSource { + pid: i32, + page_size: usize, + regions: Vec, + threads: Vec, + } + + impl ProcessInfoSource for MockProcessInfoSource { + fn pid(&self) -> i32 { + self.pid + } + + fn page_size(&self) -> usize { + self.page_size + } + + fn threads(&self) -> &[ThreadView] { + &self.threads + } + + fn aux_vector(&self) -> Option<&[Elf64_Auxv]> { + None + } + + fn mapped_files(&self) -> Option<&[MappedFile]> { + None + } + + fn va_regions(&self) -> &[VaRegion] { + &self.regions + } + } + + struct MockMemoryReader {} + + impl ReadProcessMemory for MockMemoryReader { + fn read_process_memory( + &mut self, + _address: usize, + buffer: &mut [u8], + ) -> Result { + Ok(buffer.len()) + } + } + + /// Test that writing a core dump using a custom source with no threads provided fails + #[test] + fn test_custom_source_no_threads() { + let custom_source = MockProcessInfoSource { + pid: 1, + page_size: 4096, + regions: vec![], + threads: vec![], + }; + + let memory_reader = MockMemoryReader {}; + + let core_dump_builder = CoreDumpBuilder::from_source(custom_source, memory_reader); + let res = core_dump_builder.write(std::io::sink()); + matches!(res, Err(CoreError::CustomSourceInfo)); + } + + /// Test that writing a core dump using a custom source with no regions provided fails + #[test] + fn test_custom_source_no_regions() { + let custom_source = MockProcessInfoSource { + pid: 1, + page_size: 4096, + regions: vec![], + threads: vec![ThreadView { + flags: 0, // Kernel flags for the process + tid: 1, + uid: 0, // User ID + gid: 0, // Group ID + comm: "".to_string(), // Command name + ppid: 0, // Parent PID + pgrp: 0, // Process group ID + nice: 0, // Nice value + state: 0, // Process state + utime: 0, // User time + stime: 0, // System time + cutime: 0, // Children User time + cstime: 0, // Children User time + cursig: 0, // Current signal + session: 0, // Session ID of the process + sighold: 0, // Blocked signal + sigpend: 0, // Pending signal + cmd_line: "".to_string(), + + arch_state: Box::new(ArchState { + gpr_state: vec![0; 27], + components: vec![], + }), + }], + }; + + let memory_reader = MockMemoryReader {}; + + let core_dump_builder = CoreDumpBuilder::from_source(custom_source, memory_reader); + let res = core_dump_builder.write(std::io::sink()); + matches!(res, Err(CoreError::CustomSourceInfo)); + } + + /// Test that writing a core dump using a custom source with minimal info(threads, va regions, + /// pid) succeeds + #[test] + fn test_custom_source_success() { + let slice = [0_u8; 4096]; + // region that maps on the above slice + let region = VaRegion { + begin: 0x1000, + end: 0x2000, + offset: slice.as_ptr() as u64, + mapped_file_name: None, + protection: VaProtection { + read: true, + write: false, + execute: false, + is_private: false, + }, + }; + let custom_source = MockProcessInfoSource { + pid: 1, + page_size: 4096, + regions: vec![region], + threads: vec![ThreadView { + flags: 0, // Kernel flags for the process + tid: 1, + uid: 0, // User ID + gid: 0, // Group ID + comm: "".to_string(), // Command name + ppid: 0, // Parent PID + pgrp: 0, // Process group ID + nice: 0, // Nice value + state: 0, // Process state + utime: 0, // User time + stime: 0, // System time + cutime: 0, // Children User time + cstime: 0, // Children User time + cursig: 0, // Current signal + session: 0, // Session ID of the process + sighold: 0, // Blocked signal + sigpend: 0, // Pending signal + cmd_line: "".to_string(), + + arch_state: Box::new(ArchState { + gpr_state: vec![0; 27], + components: vec![], + }), + }], + }; + + let memory_reader = MockMemoryReader {}; + + let core_dump_builder = CoreDumpBuilder::from_source(custom_source, memory_reader); + let res = core_dump_builder.write(std::io::sink()); + res.as_ref().unwrap(); + assert!(res.is_ok()); } } diff --git a/elfcore/src/elf.rs b/elfcore/src/elf.rs index 0907023..7e1afc3 100644 --- a/elfcore/src/elf.rs +++ b/elfcore/src/elf.rs @@ -28,8 +28,10 @@ pub const ELFMAG3: u8 = b'F'; pub const EV_CURRENT: u8 = 1; /// Executable file +#[cfg(target_os = "linux")] pub const ET_EXEC: u16 = 2; /// Shared object file +#[cfg(target_os = "linux")] pub const ET_DYN: u16 = 3; /// Core file pub const ET_CORE: u16 = 4; @@ -52,6 +54,7 @@ pub const PT_NOTE: u32 = 4; /// Program status note pub const NT_PRSTATUS: u32 = 1; /// Program floating point registers note +#[cfg(target_os = "linux")] pub const NT_PRFPREG: u32 = 2; /// Program information note pub const NT_PRPSINFO: u32 = 3; @@ -132,7 +135,9 @@ pub struct prstatus_t { #[derive(AsBytes, FromBytes, FromZeroes, Clone, Copy, Debug)] #[repr(C)] pub struct Elf64_Auxv { + /// AUXV type pub a_type: u64, // from auxvec.h + /// AUXV value pub a_val: u64, } @@ -164,3 +169,17 @@ pub struct Elf64_Ehdr { pub e_shnum: u16, pub e_shstrndx: u16, } + +/// ELF program header +#[derive(AsBytes, FromBytes, FromZeroes)] +#[repr(C)] +pub struct Elf64_Phdr { + pub p_type: u32, + pub p_flags: u32, + pub p_offset: u64, + pub p_vaddr: u64, + pub p_paddr: u64, + pub p_filesz: u64, + pub p_memsz: u64, + pub p_align: u64, +} diff --git a/elfcore/src/error.rs b/elfcore/src/error.rs index 426170d..7ef3c7e 100644 --- a/elfcore/src/error.rs +++ b/elfcore/src/error.rs @@ -14,6 +14,9 @@ pub enum CoreError { /// A process cannot dump itself #[error("cannot create a core dump file for the process itself")] CantDumpItself, + /// An error related to the provided [`ProcessInfoSource`] + #[error("cannot create a core dump file for custom process info source")] + CustomSourceInfo, /// A /proc file parsing error #[error("/proc parsing error")] ProcParsingError, @@ -25,6 +28,7 @@ pub enum CoreError { InternalError(&'static str), /// OS error #[error("OS error")] + #[cfg(target_os = "linux")] NixError(#[from] nix::Error), /// I/O error #[error("I/O error")] diff --git a/elfcore/src/lib.rs b/elfcore/src/lib.rs index ef45a3a..92316ab 100644 --- a/elfcore/src/lib.rs +++ b/elfcore/src/lib.rs @@ -3,16 +3,272 @@ //! A Rust library for creating ELF core dump files. -#![cfg(target_os = "linux")] #![warn(missing_docs)] mod arch; mod coredump; mod elf; mod error; -mod ptrace; -pub use coredump::write_core_dump; +#[cfg(target_os = "linux")] +mod linux; + +pub use arch::ArchComponentState; +pub use arch::ArchState; pub use coredump::CoreDumpBuilder; -pub use coredump::ProcessView; +pub use coredump::MappedFile; +pub use coredump::VaProtection; +pub use coredump::VaRegion; +pub use elf::Elf64_Auxv; pub use error::CoreError; + +// Linux specific functionality +#[cfg(target_os = "linux")] +pub use coredump::write_core_dump; +#[cfg(target_os = "linux")] +pub use linux::{LinuxProcessMemoryReader, ProcessView}; + +/// Trait for those able to read the process virtual memory. +pub trait ReadProcessMemory { + /// Read process memory into `buf` starting at the virtual address `base`, + /// and returns the number of bytes and or the error. + fn read_process_memory(&mut self, base: usize, buf: &mut [u8]) -> Result; +} + +/// This trait provides abstraction for the [`CoreDumpBuilder`] source information +/// +/// By implementing this trait one can use the ELF output logic to create a core dump +/// file from any source of information. +/// This is useful for creating a core dump file from a process, that is not necessarily +/// linux process. +/// +/// Example: +/// +/// ```rust +/// use elfcore::ArchState; +/// use elfcore::CoreDumpBuilder; +/// use elfcore::CoreError; +/// use elfcore::Elf64_Auxv; +/// use elfcore::MappedFile; +/// use elfcore::ProcessInfoSource; +/// use elfcore::ReadProcessMemory; +/// use elfcore::ThreadView; +/// use elfcore::VaProtection; +/// use elfcore::VaRegion; +/// use std::fs::File; +/// +/// struct CustomSource { +/// pid: i32, +/// threads: Vec, +/// va_regions: Vec, +/// page_size: usize, +/// } +/// +/// impl ProcessInfoSource for CustomSource { +/// fn pid(&self) -> i32 { +/// self.pid +/// } +/// fn threads(&self) -> &[ThreadView] { +/// &self.threads +/// } +/// fn va_regions(&self) -> &[VaRegion] { +/// &self.va_regions +/// } +/// fn mapped_files(&self) -> Option<&[MappedFile]> { +/// None +/// } +/// fn aux_vector(&self) -> Option<&[Elf64_Auxv]> { +/// None +/// } +/// fn page_size(&self) -> usize { +/// self.page_size +/// } +/// } +/// +/// struct CustomReader {} +/// +/// impl ReadProcessMemory for CustomReader { +/// fn read_process_memory( +/// &mut self, +/// base: usize, +/// buf: &mut [u8]) -> Result { +/// // Implement logic to read memory from the process +/// Ok(buf.len()) +/// } +/// } +/// +/// // Example of process memory using a byte array +/// let process_memory = [0_u8; 4096]; +/// +/// // Example of ThreadView and VaRegion structures that can be used +/// // in the custom source +/// let custom_source = CustomSource { +/// pid: nix::unistd::getpid().as_raw(), +/// threads: vec![ThreadView { +/// flags: 0, // Kernel flags for the process +/// tid: nix::unistd::getpid().as_raw(), +/// uid: 0, // User ID +/// gid: 0, // Group ID +/// comm: "example".to_string(), // Command name +/// ppid: 0, // Parent PID +/// pgrp: 0, // Process group ID +/// nice: 0, // Nice value +/// state: 0, // Process state +/// utime: 0, // User time +/// stime: 0, // System time +/// cutime: 0, // Children User time +/// cstime: 0, // Children User time +/// cursig: 0, // Current signal +/// session: 0, // Session ID of the process +/// sighold: 0, // Blocked signal +/// sigpend: 0, // Pending signal +/// cmd_line: "example".to_string(), +/// +/// arch_state: Box::new(ArchState { +/// gpr_state: vec![0; 27], +/// components: vec![], +/// }), +/// }], +/// va_regions: vec![VaRegion { +/// begin: 0x1000, +/// end: 0x2000, +/// offset: process_memory.as_ptr() as u64, +/// mapped_file_name: None, +/// protection: VaProtection { +/// read: true, +/// write: false, +/// execute: false, +/// is_private: false, +/// }, +/// }], +/// page_size: 4096, +/// }; +/// +/// let custom_reader = CustomReader {}; +/// +/// // Create a core dump builder using the custom source and reader +/// let mut cdb = CoreDumpBuilder::from_source(custom_source, custom_reader); +/// +/// // Writer used for example purposes only +/// let writer = std::io::sink(); +/// let result = cdb.write(writer); +/// +/// assert!(result.is_ok()); +/// +/// ``` +pub trait ProcessInfoSource { + /// Retrieves the PID of the process + /// For a core dump file to be loaded on a linux platform, it must use the PID of the process running + fn pid(&self) -> i32; + /// Retrieves a slice of [`ThreadView`] structures that describe the running threads at the + /// time of the core dump + fn threads(&self) -> &[ThreadView]; + /// Retrieves a slice of [`VaRegion`] structures that describe the virtual address space of the + /// process at the time of the core dump + fn va_regions(&self) -> &[VaRegion]; + /// A slice of [`MappedFile`] structures that describe the mapped files at the time of the core + /// dump + fn mapped_files(&self) -> Option<&[MappedFile]>; + /// Retrieves a slice of [`Elf64_Auxv`] structures that describe the auxiliary vector + /// for the produced core dump + fn aux_vector(&self) -> Option<&[Elf64_Auxv]>; + /// Retrieves the page size that will be used for alignment of segments + fn page_size(&self) -> usize; +} + +/// Linux Light-weight Process +#[derive(Debug)] +pub struct ThreadView { + /// Thread id. + pub tid: i32, + + /// Command line. + pub cmd_line: String, + + /// The filename of the executable, in parentheses. + /// This is visible whether or not the executable is + /// swapped out. + pub comm: String, + + /// One of the following characters, indicating process + /// state: + /// R Running + /// S Sleeping in an interruptible wait + /// D Waiting in uninterruptible disk sleep + /// Z Zombie + /// T Stopped (on a signal) or (before Linux 2.6.33) + /// trace stopped + /// t Tracing stop (Linux 2.6.33 onward) + /// W Paging (only before Linux 2.6.0) + /// X Dead (from Linux 2.6.0 onward) + /// x Dead (Linux 2.6.33 to 3.13 only) + /// K Wakekill (Linux 2.6.33 to 3.13 only) + /// W Waking (Linux 2.6.33 to 3.13 only) + /// P Parked (Linux 3.9 to 3.13 only) + pub state: u8, + + /// The PID of the parent of this process. + pub ppid: i32, + + /// The process group ID of the process. + pub pgrp: i32, + + /// The session ID of the process. + pub session: i32, + + /// The kernel flags word of the process. For bit mean‐ + /// ings, see the PF_* defines in the Linux kernel + /// source file include/linux/sched.h. Details depend + /// on the kernel version. + /// The format for this field was %lu before Linux 2.6. + pub flags: i32, + + /// Amount of time that this process has been scheduled + /// in user mode, measured in clock ticks (divide by + /// sysconf(_SC_CLK_TCK)). This includes guest time, + /// guest_time (time spent running a virtual CPU, see + /// below), so that applications that are not aware of + /// the guest time field do not lose that time from + /// their calculations. + pub utime: u64, + + /// Amount of time that this process has been scheduled + /// in kernel mode, measured in clock ticks (divide by + /// sysconf(_SC_CLK_TCK)). + pub stime: u64, + + /// Amount of time that this process's waited-for chil‐ + /// dren have been scheduled in user mode, measured in + /// clock ticks (divide by sysconf(_SC_CLK_TCK)). (See + /// also times(2).) This includes guest time, + /// cguest_time (time spent running a virtual CPU, see + /// below). + pub cutime: u64, + + /// Amount of time that this process's waited-for chil‐ + /// dren have been scheduled in kernel mode, measured in + /// clock ticks (divide by sysconf(_SC_CLK_TCK)). + pub cstime: u64, + + /// The nice value (see setpriority(2)), a value in the + /// range 19 (low priority) to -20 (high priority). + pub nice: u64, + + /// User Id. + pub uid: u64, + + /// Group Id. + pub gid: u32, + + /// Current signal. + pub cursig: u16, + + /// Blocked signal. + pub sighold: u64, + + /// Pending signal. + pub sigpend: u64, + + /// State of the CPU + pub arch_state: Box, +} diff --git a/elfcore/src/linux/memory.rs b/elfcore/src/linux/memory.rs new file mode 100644 index 0000000..9e3d7e5 --- /dev/null +++ b/elfcore/src/linux/memory.rs @@ -0,0 +1,100 @@ +// Copyright (c) Microsoft Corporation. +// Licensed under the MIT License. + +//! This submodule contains the `memory` handling functionality for the +//! a linux process. + +use crate::CoreError; +use crate::ReadProcessMemory; +use nix::sys::uio::{process_vm_readv, RemoteIoVec}; +use nix::unistd::Pid; +use std::io::{IoSliceMut, Read, Seek}; + +/// The memory reader for the process +/// This concrete type is used for easier generic trait implementation +/// in the `coredump` module. +pub enum LinuxProcessMemoryReader { + /// A fast process memory reader employing the `process_vm_readv` system call + /// available on Linux 3.2+. It might be disabled on some systems in the kernel configuration. + Fast { + /// The process ID of the target process + pid: Pid, + }, + /// A slow but more compatible process memory reader, uses the `/proc//mem` + /// file. + Slow { + /// The file handle to the `/proc//mem` file + file: std::fs::File, + }, +} + +impl LinuxProcessMemoryReader { + /// Create a new process memory reader for the given process ID. + /// It will use the fast method if available, otherwise it will fall back to the slow method. + pub fn new(pid: Pid) -> Result { + let memory_reader = if process_vm_readv_works() { + Self::Fast { pid } + } else { + let file = std::fs::OpenOptions::new() + .read(true) + .open(format!("/proc/{pid}/mem")) + .map_err(CoreError::IoError)?; + Self::Slow { file } + }; + + Ok(memory_reader) + } +} + +impl ReadProcessMemory for LinuxProcessMemoryReader { + fn read_process_memory(&mut self, base: usize, buf: &mut [u8]) -> Result { + match self { + Self::Fast { pid } => { + let len = buf.len(); + process_vm_readv( + *pid, + &mut [IoSliceMut::new(buf)], + &[RemoteIoVec { base, len }], + ) + .map_err(CoreError::NixError) + } + Self::Slow { file } => { + file.seek(std::io::SeekFrom::Start(base as u64)) + .map_err(CoreError::IoError)?; + file.read_exact(buf).map_err(CoreError::IoError)?; + + Ok(buf.len()) + } + } + } +} + +/// The `process_vm_readv` system call might be unavailable. An extra check is made to be +/// sure the ABI works. +fn process_vm_readv_works() -> bool { + let probe_in = [0xc1c2c3c4c5c6c7c8_u64]; + let mut probe_out = 0u64.to_le_bytes(); + + let result = process_vm_readv( + nix::unistd::getpid(), + &mut [IoSliceMut::new(&mut probe_out)], + &[RemoteIoVec { + base: probe_in.as_ptr() as usize, + len: std::mem::size_of_val(&probe_in), + }], + ); + + if let Err(e) = result { + tracing::debug!("process_vm_readv has not succeeded, error {e:?}, won't be using it"); + return false; + } + + if probe_in[0] != u64::from_le_bytes(probe_out) { + tracing::debug!( + "process_vm_readv did not return expected data: {probe_in:x?} != {probe_out:x?}, won't be using it" + ); + return false; + } + + true +} diff --git a/elfcore/src/linux/mod.rs b/elfcore/src/linux/mod.rs new file mode 100644 index 0000000..2368417 --- /dev/null +++ b/elfcore/src/linux/mod.rs @@ -0,0 +1,9 @@ +//! This module contains the functionality for +//! gathering process information on Linux systems. + +mod memory; +mod process; +pub mod ptrace; + +pub use memory::LinuxProcessMemoryReader; +pub use process::ProcessView; diff --git a/elfcore/src/linux/process.rs b/elfcore/src/linux/process.rs new file mode 100644 index 0000000..b2b6c98 --- /dev/null +++ b/elfcore/src/linux/process.rs @@ -0,0 +1,553 @@ +// Copyright (c) Microsoft Corporation. +// Licensed under the MIT License. + +//! Code for linux specific functionality +//! +//! Gathering process information. + +use super::memory::LinuxProcessMemoryReader; +use super::ptrace::ptrace_interrupt; +use crate::arch::Arch; +use crate::coredump::{MappedFile, MappedFileRegion, VaProtection, VaRegion}; +use crate::elf::{ + Elf64_Auxv, Elf64_Ehdr, EI_MAG0, EI_MAG1, EI_MAG2, EI_MAG3, EI_VERSION, ELFMAG0, ELFMAG1, + ELFMAG2, ELFMAG3, ET_DYN, ET_EXEC, EV_CURRENT, +}; +use crate::{arch, ProcessInfoSource}; +use crate::{CoreError, ThreadView}; +use nix::libc::Elf64_Phdr; +use nix::sys; +use nix::sys::ptrace::{seize, Options}; +use nix::sys::uio::{process_vm_readv, RemoteIoVec}; +use nix::sys::wait::waitpid; +use nix::unistd::Pid; +use nix::unistd::{sysconf, SysconfVar}; +use std::collections::HashSet; +use std::fs; +use std::fs::File; +use std::io::{BufRead, IoSliceMut, Read}; +use zerocopy::AsBytes; +use zerocopy::FromZeroes; + +impl ThreadView { + pub(crate) fn new(pid: Pid, tid: Pid) -> Result { + let cmd_line_path = format!("/proc/{}/task/{}/cmdline", pid, tid); + tracing::debug!("Reading {cmd_line_path}"); + let cmd_line = fs::read_to_string(cmd_line_path)?; + + // When parsing the stat file, have to handle the spaces in the program path. + // + // Here is the RE for the line: + // + // r"(\d+) \({1,1}?(.*)\){1,1}? ([RSDZTtWXxKWP]) " + // r"([+-]?\d+) ([+-]?\d+) ([+-]?\d+) ([+-]?\d+) ([+-]?\d+) ([+-]?\d+) (\d+) (\d+) (\d+) " + // r"(\d+) (\d+) (\d+) (\d+) (\d+) (\d+) (\d+) (\d+) (\d+) " + // r"(\d+) (\d+) (\d+) (\d+) (\d+) (\d+) (\d+) (\d+) (\d+)" + + let stat_path = format!("/proc/{}/task/{}/stat", pid, tid); + tracing::debug!("Reading {stat_path}"); + let stat_str = fs::read_to_string(stat_path)?; + let stat_str_trim = stat_str.trim(); + + let comm_pos_start = stat_str_trim.find('('); + let comm_pos_end = stat_str_trim.rfind(')'); + if comm_pos_start.is_none() || comm_pos_end.is_none() { + tracing::error!( + "Unsupported format of the procfs stat file, could not find command line: {}", + stat_str + ); + return Err(CoreError::ProcParsingError); + } + let comm_pos_start = comm_pos_start.unwrap(); + let comm_pos_end = comm_pos_end.unwrap(); + let comm = String::from(&stat_str_trim[comm_pos_start + 1..comm_pos_end - 1]); + + let stat_str_split = stat_str_trim[comm_pos_end + 2..] + .split(' ') + .collect::>(); + if stat_str_split.len() < 30 { + tracing::error!("Unsupported format of the procfs stat file: {}, found {} entries after the command line", stat_str, stat_str_split.len()); + return Err(CoreError::ProcParsingError); + } + + let state = { + let mut buf = [0_u8; 8]; + stat_str_split[0] + .chars() + .next() + .ok_or(CoreError::ProcParsingError)? + .encode_utf8(&mut buf); + + buf[0] + }; + + let mut uid: u64 = 0; + let mut gid: u32 = 0; + let mut cursig: u16 = 0; + let mut sighold: u64 = 0; + let mut sigpend: u64 = 0; + + { + let status_path = format!("/proc/{pid}/task/{tid}/status"); + tracing::debug!("Reading {status_path}"); + let status_file = fs::File::open(&status_path)?; + let reader = std::io::BufReader::new(status_file); + + // The common trait for the lines is a prefix followed by the tab + // character and then there is a number. After the number there might be + // various characters (whitespace, slashes) so using splitn seems to be + // difficult. + + let parse_first_number = |s: &str| { + s.chars() + .map(|c| c.to_digit(10)) + .take_while(|opt| opt.is_some()) + .fold(0, |acc: u64, digit| acc * 10 + digit.unwrap() as u64) + }; + + for line in reader.lines() { + let line = line?; + let line = line.trim(); + + tracing::debug!("Reading {status_path}: {line}"); + + if let Some(s) = line.strip_prefix("Uid:\t") { + uid = parse_first_number(s); + } else if let Some(s) = line.strip_prefix("Gid:\t") { + gid = parse_first_number(s) as u32; + } else if let Some(s) = line.strip_prefix("SigQ:\t") { + cursig = parse_first_number(s) as u16; + } else if let Some(s) = line.strip_prefix("SigBlk:\t") { + sighold = parse_first_number(s) + } else if let Some(s) = line.strip_prefix("SigPnd:\t") { + sigpend = parse_first_number(s) + } + } + } + + let arch_state = arch::ArchState::new(tid)?; + + Ok(Self { + tid: tid.as_raw(), + cmd_line, + comm, + state, + ppid: stat_str_split[1].parse::()?, + pgrp: stat_str_split[2].parse::()?, + session: stat_str_split[3].parse::()?, + flags: stat_str_split[6].parse::()?, + utime: stat_str_split[11].parse::()?, + stime: stat_str_split[12].parse::()?, + cutime: stat_str_split[13].parse::()?, + cstime: stat_str_split[14].parse::()?, + nice: stat_str_split[16].parse::()?, + uid, + gid, + cursig, + sighold, + sigpend, + arch_state, + }) + } +} + +/// View of a Linux light-weight process +pub struct ProcessView { + pub(crate) pid: Pid, + pub(crate) threads: Vec, + pub(crate) va_regions: Vec, + pub(crate) mapped_files: Vec, + // Auxiliary vector types. + // The kernel exposes some system configuration using it. + pub(crate) aux_vector: Vec, + pub(crate) page_size: usize, +} + +fn get_thread_ids(pid: Pid) -> Result, CoreError> { + let mut threads = Vec::new(); + let task_dir = format!("/proc/{}/task", pid); + tracing::debug!("Reading {task_dir}"); + let paths = std::fs::read_dir(task_dir)?; + + tracing::debug!( + "Enumerating threads(light-weight processes) for the process {}", + pid + ); + + for entry in paths { + let entry = entry?; + let path = entry.path(); + + let metadata = std::fs::metadata(&path)?; + if metadata.is_dir() { + let stem = path.file_stem(); + if let Some(stem) = stem { + if stem != "." && stem != ".." { + let stem = stem.to_string_lossy(); + let tid = Pid::from_raw(stem.parse::()? as nix::libc::pid_t); + + tracing::debug!("Found thread {}", tid); + + threads.push(tid) + } + } + } + } + + Ok(threads) +} + +fn get_aux_vector(pid: Pid) -> Result, CoreError> { + let mut auxv: Vec = Vec::new(); + + let auxv_file_name = format!("/proc/{}/auxv", pid); + tracing::debug!("Reading {auxv_file_name}"); + let mut file = File::open(auxv_file_name)?; + + loop { + let mut aux = Elf64_Auxv { + a_type: 0, + a_val: 0, + }; + + match file.read_exact(aux.as_bytes_mut()) { + Ok(_) => auxv.push(aux), + Err(_) => break, + } + } + + Ok(auxv) +} + +fn get_va_regions(pid: Pid) -> Result<(Vec, Vec, u64), CoreError> { + let mut maps: Vec = Vec::new(); + let mut vdso = 0_u64; + + let mut mapped_elfs: HashSet = HashSet::new(); + let mut mapped_non_elfs: HashSet = HashSet::new(); + let mut mapped_files: Vec = Vec::new(); + + let maps_path = format!("/proc/{}/maps", pid); + tracing::debug!("Reading {maps_path}"); + let maps_file = fs::File::open(maps_path)?; + let reader = std::io::BufReader::new(maps_file); + + for line in reader.lines() { + let line = line?; + let parts: Vec<&str> = line.split_whitespace().collect(); + + tracing::debug!("Memory maps: {:?}", parts); + + let begin_end: Vec<&str> = parts[0].split('-').collect(); + let begin = u64::from_str_radix(begin_end[0], 16)?; + let end = u64::from_str_radix(begin_end[1], 16)?; + let offset = u64::from_str_radix(parts[2], 16)?; + + let mapped_file_name = { + let last = *parts.last().ok_or(CoreError::ProcParsingError)?; + if last == "[vdso]" { + vdso = begin; + + //None + tracing::info!("Skipping VA range mapped to {}", last); + continue; + } else if last == "[vvar]" || last == "[vsyscall]" { + //None + tracing::info!("Skipping VA range mapped to {}", last); + continue; + } else if last.starts_with('/') { + if last.starts_with("/dev/") { + // Reading device memory might have unintended side effects. + // Always skip. + tracing::info!("Skipping VA range mapped to device file {}", last); + continue; + } + + Some(String::from(last)) + } else { + None + } + }; + + let is_private = parts[1].chars().nth(3).ok_or(CoreError::ProcParsingError)? == 'p'; + let is_shared = parts[1].chars().nth(3).ok_or(CoreError::ProcParsingError)? == 's'; + + if !is_private && !is_shared { + tracing::info!( + "Skipping non-accessible VA range [0x{:x}; 0x{:x}]", + begin, + end + ); + continue; + } + + let protection = VaProtection { + read: parts[1].starts_with('r'), + write: parts[1].chars().nth(1).ok_or(CoreError::ProcParsingError)? == 'w', + execute: parts[1].chars().nth(2).ok_or(CoreError::ProcParsingError)? == 'x', + is_private, + }; + + if !protection.read && !protection.write && !protection.execute { + tracing::info!( + "Skipping non-accessible VA range [0x{:x}; 0x{:x}]", + begin, + end + ); + continue; + } + + // TODO also can skip over read-only regions and executable regions. + // These can be loaded from shared objects available on the system. + + // Was that file seen before and is that an ELF one? + if let Some(ref mapped_file_name) = mapped_file_name { + if !mapped_elfs.contains(mapped_file_name) + && !mapped_non_elfs.contains(mapped_file_name) + { + // First time for that file. + // See if the mapped file is an ELF one, otherwise skip over it + // as it might be quite huge and may contains secrets, filter out by default. + // TODO: make optional. + + let maybe_elf_hdr: Option = { + let mut elf_hdr = Elf64_Ehdr::new_zeroed(); + match process_vm_readv( + pid, + &mut [IoSliceMut::new(elf_hdr.as_bytes_mut())], + &[RemoteIoVec { + base: begin as usize, + len: std::mem::size_of::(), + }], + ) { + Ok(_) => Some(elf_hdr), + Err(_) => None, + } + }; + + if let Some(elf_hdr) = maybe_elf_hdr { + if elf_hdr.e_ident[EI_MAG0] == ELFMAG0 + && elf_hdr.e_ident[EI_MAG1] == ELFMAG1 + && elf_hdr.e_ident[EI_MAG2] == ELFMAG2 + && elf_hdr.e_ident[EI_MAG3] == ELFMAG3 + && elf_hdr.e_ident[EI_VERSION] == EV_CURRENT + && elf_hdr.e_ehsize == std::mem::size_of::() as u16 + && (elf_hdr.e_type == ET_EXEC || elf_hdr.e_type == ET_DYN) + && elf_hdr.e_phentsize == std::mem::size_of::() as u16 + && elf_hdr.e_machine == arch::ArchState::EM_ELF_MACHINE + { + mapped_elfs.insert(mapped_file_name.clone()); + } else { + mapped_non_elfs.insert(mapped_file_name.clone()); + } + } + } + + if mapped_non_elfs.contains(mapped_file_name) { + tracing::info!( + "Skipping VA range mapped to a non-ELF file {}", + mapped_file_name + ); + continue; + } else { + tracing::info!( + "Adding VA range [0x{:x}; 0x{:x}] mapped to an ELF file {}", + begin, + end, + mapped_file_name + ); + } + } + + // Account for the mapped files regions, not concerning + // VA protection. + + if let Some(mapped_file_name) = &mapped_file_name { + if mapped_files.is_empty() { + mapped_files.push(MappedFile { + name: mapped_file_name.clone(), + regions: vec![MappedFileRegion { begin, end, offset }], + }) + } else { + let last_file = mapped_files.last_mut().ok_or(CoreError::ProcParsingError)?; + if last_file.name != *mapped_file_name { + mapped_files.push(MappedFile { + name: mapped_file_name.clone(), + regions: vec![MappedFileRegion { begin, end, offset }], + }) + } else { + let last_region = last_file + .regions + .last_mut() + .ok_or(CoreError::ProcParsingError)?; + + if last_region.end != begin { + last_file + .regions + .push(MappedFileRegion { begin, end, offset }) + } else { + last_region.end = end + } + } + } + } + + // Going to save that VA region into the core dump file + + maps.push(VaRegion { + begin, + end, + offset, + protection, + mapped_file_name, + }); + } + + maps.sort_by_key(|x| x.begin); + + Ok((maps, mapped_files, vdso)) +} + +impl ProcessView { + /// Creates new process view + /// + /// # Arguments + /// * `pid` - process ID + /// + pub fn new(pid: libc::pid_t) -> Result { + let pid = Pid::from_raw(pid); + + let mut tids = get_thread_ids(pid)?; + tids.sort(); + + // Guard against calling for itself. Fail early as the seizing the threads + // will fail with -EPERM later. + if tids.binary_search(&nix::unistd::getpid()).is_ok() { + return Err(CoreError::CantDumpItself); + }; + + tracing::info!("Attaching to {} threads of process {}", tids.len(), pid); + + for tid in &tids { + tracing::debug!("Seizing thread {}", *tid); + + if let Err(e) = seize(*tid, Options::empty()) { + tracing::error!("Seizing thread {} failed, error {}", *tid, e); + return Err(CoreError::NixError(e)); + } + + tracing::debug!("Interrupting thread {}", *tid); + + ptrace_interrupt(*tid)?; + + tracing::debug!("Waiting for thread {} to stop", *tid); + + match waitpid(*tid, None) { + Ok(s) => { + tracing::debug!("Thread {} stopped, status {:?}", *tid, s); + } + Err(e) => { + tracing::error!("Waiting for thread {} failed, error {}", *tid, e); + return Err(CoreError::NixError(e)); + } + } + } + + // There is a race here: + // 1) us stopping threads, + // 2) the process that might be creating new ones, + // 3) the existing threads might exit. + // See if the threads ids are still the same. Not bullet-proof as thread ids + // might be re-used. + { + let mut tids_check = get_thread_ids(pid)?; + tids_check.sort(); + + if tids != tids_check { + return Err(CoreError::RaceTryAgain); + } + } + + let threads = tids + .iter() + .map(|tid| ThreadView::new(pid, *tid)) + .collect::>()?; + for thread in &threads { + tracing::debug!("Thread state: {:x?}", thread); + } + + let (va_regions, mapped_files, vdso) = get_va_regions(pid)?; + + tracing::debug!("VA regions {:x?}", va_regions); + tracing::debug!("Mapped files {:x?}", mapped_files); + tracing::debug!("vDSO from the proc maps {:x?}", vdso); + + let aux_vector = get_aux_vector(pid)?; + + tracing::debug!("Auxiliary vector {:x?}", aux_vector); + + let page_size = match sysconf(SysconfVar::PAGE_SIZE) { + Ok(s) => match s { + Some(s) => s as usize, + None => 0x1000_usize, + }, + Err(_) => 0x1000_usize, + }; + + Ok(Self { + pid, + threads, + va_regions, + mapped_files, + aux_vector, + page_size, + }) + } + + /// Retrieves the memory reader for access to memory regions + pub(crate) fn create_memory_reader(pid: Pid) -> Result { + LinuxProcessMemoryReader::new(pid) + } +} + +impl ProcessInfoSource for ProcessView { + fn pid(&self) -> i32 { + self.pid.as_raw() + } + fn threads(&self) -> &[ThreadView] { + &self.threads + } + fn va_regions(&self) -> &[VaRegion] { + &self.va_regions + } + fn mapped_files(&self) -> Option<&[MappedFile]> { + Some(&self.mapped_files) + } + fn aux_vector(&self) -> Option<&[Elf64_Auxv]> { + Some(&self.aux_vector) + } + fn page_size(&self) -> usize { + self.page_size + } +} + +impl Drop for ProcessView { + fn drop(&mut self) { + tracing::info!( + "Detaching from {} threads of process {}", + self.threads.len(), + self.pid + ); + + for thread in &self.threads { + match sys::ptrace::detach(Pid::from_raw(thread.tid), None) { + Ok(_) => { + tracing::debug!("Thread {} resumed", thread.tid); + } + Err(e) => { + tracing::error!("Thread {} failed to resume: {:?}", thread.tid, e); + } + }; + } + } +} diff --git a/elfcore/src/ptrace.rs b/elfcore/src/linux/ptrace.rs similarity index 97% rename from elfcore/src/ptrace.rs rename to elfcore/src/linux/ptrace.rs index da7fa0d..da4388c 100644 --- a/elfcore/src/ptrace.rs +++ b/elfcore/src/linux/ptrace.rs @@ -6,8 +6,8 @@ use std::ffi::c_void; use std::ptr; -use super::elf::NT_PRFPREG; -use super::elf::NT_PRSTATUS; +use crate::elf::NT_PRFPREG; +use crate::elf::NT_PRSTATUS; use crate::CoreError; use nix::sys; use nix::sys::ptrace::Request;