From c18006a8b5712283bdecd55cfde32a9f171f3a7f Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Doru=20Bl=C3=A2nzeanu?= Date: Fri, 25 Apr 2025 14:14:15 +0300 Subject: [PATCH 1/9] fix clippy warnings for elfcore-sample MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Signed-off-by: Doru Blânzeanu --- elfcore-sample/src/main.rs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/elfcore-sample/src/main.rs b/elfcore-sample/src/main.rs index c41e304..7ee0ebc 100644 --- a/elfcore-sample/src/main.rs +++ b/elfcore-sample/src/main.rs @@ -20,7 +20,7 @@ use tracing::Level; 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 { From 623e72d218584f9200ce0fabce8b7bd3fb0e8a5f Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Doru=20Bl=C3=A2nzeanu?= Date: Fri, 11 Apr 2025 14:34:40 +0300 Subject: [PATCH 2/9] move ThreadView to Linux specific file MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - This step is part of the move of linux specific functionality to separate file in preparation for decoupling the linux Process specific logic from the output of ELF dump file Signed-off-by: Doru Blânzeanu --- elfcore/src/coredump.rs | 219 +-------------------------------- elfcore/src/lib.rs | 1 + elfcore/src/linux/mod.rs | 4 + elfcore/src/linux/process.rs | 231 +++++++++++++++++++++++++++++++++++ 4 files changed, 237 insertions(+), 218 deletions(-) create mode 100644 elfcore/src/linux/mod.rs create mode 100644 elfcore/src/linux/process.rs diff --git a/elfcore/src/coredump.rs b/elfcore/src/coredump.rs index b7712c5..c24e5c1 100644 --- a/elfcore/src/coredump.rs +++ b/elfcore/src/coredump.rs @@ -9,6 +9,7 @@ use super::arch; use super::arch::Arch; use crate::elf::*; +use crate::linux::process::ThreadView; use crate::ptrace::ptrace_interrupt; use crate::CoreError; use nix::libc::Elf64_Phdr; @@ -106,224 +107,6 @@ 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, - }) - } -} - #[derive(Debug)] #[allow(dead_code)] struct VaProtection { diff --git a/elfcore/src/lib.rs b/elfcore/src/lib.rs index ef45a3a..cf0bf9e 100644 --- a/elfcore/src/lib.rs +++ b/elfcore/src/lib.rs @@ -10,6 +10,7 @@ mod arch; mod coredump; mod elf; mod error; +mod linux; mod ptrace; pub use coredump::write_core_dump; diff --git a/elfcore/src/linux/mod.rs b/elfcore/src/linux/mod.rs new file mode 100644 index 0000000..ad7fd46 --- /dev/null +++ b/elfcore/src/linux/mod.rs @@ -0,0 +1,4 @@ +//! This module contains the functionality for +//! gathering process information on Linux systems. + +pub mod process; diff --git a/elfcore/src/linux/process.rs b/elfcore/src/linux/process.rs new file mode 100644 index 0000000..cb16915 --- /dev/null +++ b/elfcore/src/linux/process.rs @@ -0,0 +1,231 @@ +// Copyright (c) Microsoft Corporation. +// Licensed under the MIT License. + +//! Code for linux specific functionality +//! +//! Gathering process information. + +use crate::arch; +use crate::arch::Arch; +use crate::CoreError; +use nix::unistd::Pid; +use std::fs; +use std::io::BufRead; + +// Linux Light-weight Process +#[derive(Debug)] +pub(crate) struct ThreadView { + // Thread id. + pub(crate) tid: Pid, + + // Command line. + pub(crate) cmd_line: String, + + // The filename of the executable, in parentheses. + // This is visible whether or not the executable is + // swapped out. + pub(crate) 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(crate) state: u8, + + // The PID of the parent of this process. + pub(crate) ppid: i32, + + // The process group ID of the process. + pub(crate) pgrp: i32, + + // The session ID of the process. + pub(crate) 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(crate) 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(crate) 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(crate) 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(crate) 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(crate) cstime: u64, + + // The nice value (see setpriority(2)), a value in the + // range 19 (low priority) to -20 (high priority). + pub(crate) nice: u64, + + // User Id. + pub(crate) uid: u64, + + // Group Id. + pub(crate) gid: u32, + + // Current signal. + pub(crate) cursig: u16, + + // Blocked signal. + pub(crate) sighold: u64, + + // Pending signal. + pub(crate) sigpend: u64, + + pub(crate) arch_state: Box, +} + +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, + 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, + }) + } +} From d524705ac8b4d06df813c84628df64130787e4e8 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Doru=20Bl=C3=A2nzeanu?= Date: Fri, 11 Apr 2025 15:55:49 +0300 Subject: [PATCH 3/9] move ProcessView and linux specific functionality to different file MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Signed-off-by: Doru Blânzeanu --- elfcore/src/coredump.rs | 522 ++--------------------------------- elfcore/src/lib.rs | 2 +- elfcore/src/linux/memory.rs | 68 +++++ elfcore/src/linux/mod.rs | 1 + elfcore/src/linux/process.rs | 423 +++++++++++++++++++++++++++- 5 files changed, 515 insertions(+), 501 deletions(-) create mode 100644 elfcore/src/linux/memory.rs diff --git a/elfcore/src/coredump.rs b/elfcore/src/coredump.rs index c24e5c1..bb14f79 100644 --- a/elfcore/src/coredump.rs +++ b/elfcore/src/coredump.rs @@ -9,32 +9,19 @@ use super::arch; use super::arch::Arch; use crate::elf::*; -use crate::linux::process::ThreadView; -use crate::ptrace::ptrace_interrupt; +use crate::linux::memory::FastMemoryReader; +use crate::linux::memory::ReadProcessMemory; +use crate::linux::memory::SlowMemoryReader; +use crate::linux::process::process_vm_readv_works; use crate::CoreError; +use crate::ProcessView; 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 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; const ELF_HEADER_ALIGN: usize = 8; const ELF_NOTE_ALIGN: usize = 4; @@ -109,35 +96,35 @@ struct MappedFilesNoteItem { #[derive(Debug)] #[allow(dead_code)] -struct VaProtection { - is_private: bool, - read: bool, - write: bool, - execute: bool, +pub(crate) struct VaProtection { + pub(crate) is_private: bool, + pub(crate) read: bool, + pub(crate) write: bool, + pub(crate) execute: bool, } #[derive(Debug)] #[allow(dead_code)] -struct VaRegion { - begin: u64, - end: u64, - offset: u64, - protection: VaProtection, - mapped_file_name: Option, +pub(crate) struct VaRegion { + pub(crate) begin: u64, + pub(crate) end: u64, + pub(crate) offset: u64, + pub(crate) protection: VaProtection, + pub(crate) mapped_file_name: Option, } #[derive(Debug)] #[allow(dead_code)] -struct MappedFileRegion { - begin: u64, - end: u64, - offset: u64, +pub(crate) struct MappedFileRegion { + pub(crate) begin: u64, + pub(crate) end: u64, + pub(crate) offset: u64, } #[derive(Debug)] -struct MappedFile { - name: String, - regions: Vec, +pub(crate) struct MappedFile { + pub(crate) name: String, + pub(crate) regions: Vec, } #[derive(Default)] @@ -150,18 +137,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 @@ -173,251 +148,6 @@ 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, custom_notes: Option<&[CustomFileNote<'_>]>, @@ -491,212 +221,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: diff --git a/elfcore/src/lib.rs b/elfcore/src/lib.rs index cf0bf9e..3f2a256 100644 --- a/elfcore/src/lib.rs +++ b/elfcore/src/lib.rs @@ -15,5 +15,5 @@ mod ptrace; pub use coredump::write_core_dump; pub use coredump::CoreDumpBuilder; -pub use coredump::ProcessView; pub use error::CoreError; +pub use linux::process::ProcessView; diff --git a/elfcore/src/linux/memory.rs b/elfcore/src/linux/memory.rs new file mode 100644 index 0000000..3868b44 --- /dev/null +++ b/elfcore/src/linux/memory.rs @@ -0,0 +1,68 @@ +// 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 nix::sys::uio::{process_vm_readv, RemoteIoVec}; +use nix::unistd::Pid; +use std::io::{IoSliceMut, Read, Seek}; + +/// Trait for those able to read the process virtual memory. +pub(crate) 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. +pub(crate) 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. +pub(crate) 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()) + } +} diff --git a/elfcore/src/linux/mod.rs b/elfcore/src/linux/mod.rs index ad7fd46..aa70ba1 100644 --- a/elfcore/src/linux/mod.rs +++ b/elfcore/src/linux/mod.rs @@ -1,4 +1,5 @@ //! This module contains the functionality for //! gathering process information on Linux systems. +pub mod memory; pub mod process; diff --git a/elfcore/src/linux/process.rs b/elfcore/src/linux/process.rs index cb16915..efa2259 100644 --- a/elfcore/src/linux/process.rs +++ b/elfcore/src/linux/process.rs @@ -7,10 +7,26 @@ use crate::arch; 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::ptrace::ptrace_interrupt; use crate::CoreError; +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::io::BufRead; +use std::fs::File; +use std::io::{BufRead, IoSliceMut, Read}; +use zerocopy::AsBytes; +use zerocopy::FromZeroes; // Linux Light-weight Process #[derive(Debug)] @@ -229,3 +245,408 @@ impl ThreadView { }) } } + +/// 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 = 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); + } + }; + } + } +} + +/// The `process_vm_readv` system call might be unavailable. An extra check is made to be +/// sure the ABI works. +pub(crate) 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 +} From 43b9e07d9f0fc41c9aed3d94b93d4af016330afa Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Doru=20Bl=C3=A2nzeanu?= Date: Fri, 25 Apr 2025 12:35:28 +0300 Subject: [PATCH 4/9] move ptrace to linux module MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Signed-off-by: Doru Blânzeanu --- elfcore/src/arch/mod.rs | 2 +- elfcore/src/arch/x86_64.rs | 2 +- elfcore/src/lib.rs | 1 - elfcore/src/linux/mod.rs | 1 + elfcore/src/linux/process.rs | 2 +- elfcore/src/{ => linux}/ptrace.rs | 4 ++-- 6 files changed, 6 insertions(+), 6 deletions(-) rename elfcore/src/{ => linux}/ptrace.rs (97%) diff --git a/elfcore/src/arch/mod.rs b/elfcore/src/arch/mod.rs index a56ce85..0c0e897 100644 --- a/elfcore/src/arch/mod.rs +++ b/elfcore/src/arch/mod.rs @@ -6,7 +6,7 @@ use crate::elf::NT_PRFPREG; use nix::unistd::Pid; -use super::ptrace; +use super::linux::ptrace; use crate::CoreError; #[cfg(target_arch = "x86_64")] diff --git a/elfcore/src/arch/x86_64.rs b/elfcore/src/arch/x86_64.rs index 8e3bba4..6fc501c 100644 --- a/elfcore/src/arch/x86_64.rs +++ b/elfcore/src/arch/x86_64.rs @@ -6,7 +6,7 @@ #![cfg(target_arch = "x86_64")] use super::ArchComponentState; -use crate::ptrace::ptrace_get_reg_set; +use crate::linux::ptrace::ptrace_get_reg_set; use crate::CoreError; use nix::unistd::Pid; use zerocopy::AsBytes; diff --git a/elfcore/src/lib.rs b/elfcore/src/lib.rs index 3f2a256..389535a 100644 --- a/elfcore/src/lib.rs +++ b/elfcore/src/lib.rs @@ -11,7 +11,6 @@ mod coredump; mod elf; mod error; mod linux; -mod ptrace; pub use coredump::write_core_dump; pub use coredump::CoreDumpBuilder; diff --git a/elfcore/src/linux/mod.rs b/elfcore/src/linux/mod.rs index aa70ba1..621ad09 100644 --- a/elfcore/src/linux/mod.rs +++ b/elfcore/src/linux/mod.rs @@ -3,3 +3,4 @@ pub mod memory; pub mod process; +pub mod ptrace; diff --git a/elfcore/src/linux/process.rs b/elfcore/src/linux/process.rs index efa2259..b18cd9a 100644 --- a/elfcore/src/linux/process.rs +++ b/elfcore/src/linux/process.rs @@ -12,7 +12,7 @@ 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::ptrace::ptrace_interrupt; +use crate::linux::ptrace::ptrace_interrupt; use crate::CoreError; use nix::libc::Elf64_Phdr; use nix::sys; 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; From 7e15401a236e876e8173f7f648638e9eecec0603 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Doru=20Bl=C3=A2nzeanu?= Date: Fri, 11 Apr 2025 18:03:41 +0300 Subject: [PATCH 5/9] decouple the ELF dump file output from linux ProcessView MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - define ProcessInfoSource trait that describes a process information data relevant to an ELF dump - implement the trait for the existing Linux ProcessView to preserve the current behavior - create a `Box` by calling the `ProcessView` method `create_memory_reader` when the `ProcessInfoSource` is known to be a `ProcessView` or expect the reader as an argument when the `CoreDumpBuilder` is created `from_source` - add documentation and adjust visibility to types that now need to be public for the crate users to use when they define their own ProcessInfoSource implementation Signed-off-by: Doru Blânzeanu --- elfcore/src/arch/mod.rs | 20 ++- elfcore/src/arch/x86_64.rs | 2 - elfcore/src/coredump.rs | 260 +++++++++++++++++++++-------------- elfcore/src/elf.rs | 2 + elfcore/src/lib.rs | 158 ++++++++++++++++++++- elfcore/src/linux/memory.rs | 8 +- elfcore/src/linux/mod.rs | 7 +- elfcore/src/linux/process.rs | 226 +++++++++++++++++------------- 8 files changed, 463 insertions(+), 220 deletions(-) diff --git a/elfcore/src/arch/mod.rs b/elfcore/src/arch/mod.rs index 0c0e897..f789de6 100644 --- a/elfcore/src/arch/mod.rs +++ b/elfcore/src/arch/mod.rs @@ -19,31 +19,39 @@ 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; 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 { diff --git a/elfcore/src/arch/x86_64.rs b/elfcore/src/arch/x86_64.rs index 6fc501c..a4e9dfd 100644 --- a/elfcore/src/arch/x86_64.rs +++ b/elfcore/src/arch/x86_64.rs @@ -3,8 +3,6 @@ //! x86_64 specifics for ELF core dump files. -#![cfg(target_arch = "x86_64")] - use super::ArchComponentState; use crate::linux::ptrace::ptrace_get_reg_set; use crate::CoreError; diff --git a/elfcore/src/coredump.rs b/elfcore/src/coredump.rs index bb14f79..651ff21 100644 --- a/elfcore/src/coredump.rs +++ b/elfcore/src/coredump.rs @@ -9,12 +9,10 @@ use super::arch; use super::arch::Arch; use crate::elf::*; -use crate::linux::memory::FastMemoryReader; -use crate::linux::memory::ReadProcessMemory; -use crate::linux::memory::SlowMemoryReader; -use crate::linux::process::process_vm_readv_works; use crate::CoreError; +use crate::ProcessInfoSource; use crate::ProcessView; +use crate::ReadProcessMemory; use nix::libc::Elf64_Phdr; use smallvec::smallvec; use smallvec::SmallVec; @@ -94,37 +92,52 @@ struct MappedFilesNoteItem { page_count: u64, } +/// Struct that describes a region's access permissions #[derive(Debug)] -#[allow(dead_code)] -pub(crate) struct VaProtection { - pub(crate) is_private: bool, - pub(crate) read: bool, - pub(crate) write: bool, - pub(crate) 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)] -pub(crate) struct VaRegion { - pub(crate) begin: u64, - pub(crate) end: u64, - pub(crate) offset: u64, - pub(crate) protection: VaProtection, - pub(crate) 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)] -pub(crate) struct MappedFileRegion { - pub(crate) begin: u64, - pub(crate) end: u64, - pub(crate) 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)] -pub(crate) struct MappedFile { - pub(crate) name: String, - pub(crate) regions: Vec, +pub struct MappedFile { + /// File name + pub name: String, + /// File regions + pub regions: Vec, } #[derive(Default)] @@ -149,7 +162,7 @@ struct CustomFileNote<'a> { } fn get_elf_notes_sizes( - pv: &ProcessView, + pv: &dyn ProcessInfoSource, custom_notes: Option<&[CustomFileNote<'_>]>, ) -> Result { let header_and_name = @@ -163,7 +176,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 @@ -175,23 +188,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( @@ -229,32 +250,26 @@ fn get_elf_notes_sizes( /// /// To access new functionality, use [`CoreDumpBuilder`] pub fn write_core_dump(writer: T, pv: &ProcessView) -> Result { - write_core_dump_inner(writer, pv, None) + let memory_reader = ProcessView::create_memory_reader(pv.pid)?; + write_core_dump_inner(writer, pv, None, memory_reader) } fn write_core_dump_inner( writer: T, - pv: &ProcessView, + pv: &dyn ProcessInfoSource, custom_notes: Option<&mut [CustomFileNote<'_>]>, + memory_reader: Box, ) -> Result { let mut total_written = 0_usize; let mut writer = ElfCoreWriter::new(writer); 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 - }; - let note_sizes = get_elf_notes_sizes(pv, custom_notes.as_deref())?; total_written += write_elf_header(&mut writer, pv)?; @@ -262,7 +277,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); @@ -286,7 +301,7 @@ fn round_up(value: usize, alignment: usize) -> usize { fn write_elf_header( writer: &mut ElfCoreWriter, - pv: &ProcessView, + pv: &dyn ProcessInfoSource, ) -> Result { let mut e_ident = [0_u8; 16]; e_ident[EI_MAG0] = ELFMAG0; @@ -306,7 +321,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, @@ -337,7 +352,7 @@ fn write_elf_header( fn write_program_headers( writer: &mut ElfCoreWriter, - pv: &ProcessView, + pv: &dyn ProcessInfoSource, note_sizes: &NoteSizes, ) -> Result { tracing::info!( @@ -351,7 +366,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); @@ -379,9 +394,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: { @@ -407,7 +422,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, @@ -534,7 +549,7 @@ fn write_elf_note_file( fn write_process_info_note( writer: &mut ElfCoreWriter, - pv: &ProcessView, + pv: &dyn ProcessInfoSource, ) -> Result { let mut written = 0_usize; @@ -546,8 +561,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, @@ -602,7 +617,7 @@ fn write_process_info_note( fn write_process_status_notes( writer: &mut ElfCoreWriter, - pv: &ProcessView, + pv: &dyn ProcessInfoSource, ) -> Result { let mut total_written = 0_usize; @@ -611,7 +626,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, @@ -673,7 +688,7 @@ 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) @@ -681,14 +696,17 @@ fn write_process_status_notes( fn write_aux_vector_note( writer: &mut ElfCoreWriter, - pv: &ProcessView, + pv: &dyn ProcessInfoSource, ) -> 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); @@ -697,47 +715,52 @@ fn write_aux_vector_note( fn write_mapped_files_note( writer: &mut ElfCoreWriter, - pv: &ProcessView, + pv: &dyn ProcessInfoSource, ) -> 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); @@ -774,7 +797,7 @@ fn write_custom_notes( fn write_elf_notes( writer: &mut ElfCoreWriter, - pv: &ProcessView, + pv: &dyn ProcessInfoSource, note_sizes: &NoteSizes, custom_notes: Option<&mut [CustomFileNote<'_>]>, ) -> Result { @@ -837,7 +860,7 @@ fn write_elf_notes( fn write_va_region( writer: &mut ElfCoreWriter, va_region: &VaRegion, - pv: &ProcessView, + pv: &dyn ProcessInfoSource, memory_reader: &mut Box, ) -> Result { let mut dumped = 0_usize; @@ -863,12 +886,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; @@ -887,7 +910,7 @@ fn write_va_region( fn write_va_regions( writer: &mut ElfCoreWriter, - pv: &ProcessView, + pv: &dyn ProcessInfoSource, mut memory_reader: Box, ) -> Result { let mut written = 0_usize; @@ -897,7 +920,7 @@ fn write_va_regions( writer.stream_position()? ); - for va_region in &pv.va_regions { + for va_region in pv.va_regions() { let dumped = write_va_region(writer, va_region, pv, &mut memory_reader)?; written += dumped; @@ -917,23 +940,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 +/// This also supports generating core dumps with information +/// from a custom source. pub struct CoreDumpBuilder<'a> { - pv: ProcessView, + pv: Box, custom_notes: Vec>, + memory_reader: Box, } impl<'a> CoreDumpBuilder<'a> { /// Create a new core dump builder for the process with the provided PID pub fn new(pid: libc::pid_t) -> Result { let pv = ProcessView::new(pid)?; + let memory_reader = ProcessView::create_memory_reader(pv.pid)?; + Ok(Self { - pv, + pv: Box::new(pv) as Box, custom_notes: Vec::new(), + memory_reader, }) } + /// Create a new core dump builder from a custom `ProcessInfoSource` + pub fn from_source( + source: Box, + memory_reader: Box, + ) -> CoreDumpBuilder<'a> { + 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, @@ -954,6 +995,11 @@ 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.as_ref(), + Some(&mut self.custom_notes), + self.memory_reader, + ) } } diff --git a/elfcore/src/elf.rs b/elfcore/src/elf.rs index 0907023..d2dc125 100644 --- a/elfcore/src/elf.rs +++ b/elfcore/src/elf.rs @@ -132,7 +132,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, } diff --git a/elfcore/src/lib.rs b/elfcore/src/lib.rs index 389535a..199de0b 100644 --- a/elfcore/src/lib.rs +++ b/elfcore/src/lib.rs @@ -12,7 +12,163 @@ mod elf; mod error; mod linux; +pub use arch::ArchComponentState; +pub use arch::ArchState; pub use coredump::write_core_dump; pub use coredump::CoreDumpBuilder; +pub use coredump::MappedFile; +pub use coredump::VaProtection; +pub use coredump::VaRegion; +pub use elf::Elf64_Auxv; pub use error::CoreError; -pub use linux::process::ProcessView; +pub use linux::ProcessView; +pub use linux::ThreadView; +pub use nix::unistd::Pid; + +/// 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: nix::unistd::Pid, +/// threads: Vec, +/// va_regions: Vec, +/// page_size: usize, +/// } +/// +/// impl ProcessInfoSource for CustomSource { +/// fn pid(&self) -> nix::unistd::Pid { +/// 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(), +/// threads: vec![ThreadView { +/// flags: 0, // Kernel flags for the process +/// tid: nix::unistd::getpid(), +/// 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(Box::new(custom_source), Box::new(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; +} diff --git a/elfcore/src/linux/memory.rs b/elfcore/src/linux/memory.rs index 3868b44..a2072fb 100644 --- a/elfcore/src/linux/memory.rs +++ b/elfcore/src/linux/memory.rs @@ -5,17 +5,11 @@ //! 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}; -/// Trait for those able to read the process virtual memory. -pub(crate) 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. pub(crate) struct FastMemoryReader { diff --git a/elfcore/src/linux/mod.rs b/elfcore/src/linux/mod.rs index 621ad09..349f275 100644 --- a/elfcore/src/linux/mod.rs +++ b/elfcore/src/linux/mod.rs @@ -1,6 +1,9 @@ //! This module contains the functionality for //! gathering process information on Linux systems. -pub mod memory; -pub mod process; +mod memory; +mod process; pub mod ptrace; + +pub use process::ProcessView; +pub use process::ThreadView; diff --git a/elfcore/src/linux/process.rs b/elfcore/src/linux/process.rs index b18cd9a..9dd20c6 100644 --- a/elfcore/src/linux/process.rs +++ b/elfcore/src/linux/process.rs @@ -5,15 +5,16 @@ //! //! Gathering process information. -use crate::arch; +use super::memory::{FastMemoryReader, SlowMemoryReader}; +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::linux::ptrace::ptrace_interrupt; use crate::CoreError; +use crate::{arch, ProcessInfoSource, ReadProcessMemory}; use nix::libc::Elf64_Phdr; use nix::sys; use nix::sys::ptrace::{seize, Options}; @@ -28,100 +29,101 @@ use std::io::{BufRead, IoSliceMut, Read}; use zerocopy::AsBytes; use zerocopy::FromZeroes; -// Linux Light-weight Process +/// Linux Light-weight Process #[derive(Debug)] -pub(crate) struct ThreadView { - // Thread id. - pub(crate) tid: Pid, - - // Command line. - pub(crate) cmd_line: String, - - // The filename of the executable, in parentheses. - // This is visible whether or not the executable is - // swapped out. - pub(crate) 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(crate) state: u8, - - // The PID of the parent of this process. - pub(crate) ppid: i32, - - // The process group ID of the process. - pub(crate) pgrp: i32, - - // The session ID of the process. - pub(crate) 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(crate) 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(crate) 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(crate) 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(crate) 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(crate) cstime: u64, - - // The nice value (see setpriority(2)), a value in the - // range 19 (low priority) to -20 (high priority). - pub(crate) nice: u64, - - // User Id. - pub(crate) uid: u64, - - // Group Id. - pub(crate) gid: u32, - - // Current signal. - pub(crate) cursig: u16, - - // Blocked signal. - pub(crate) sighold: u64, - - // Pending signal. - pub(crate) sigpend: u64, - - pub(crate) arch_state: Box, +pub struct ThreadView { + /// Thread id. + pub tid: Pid, + + /// 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, } impl ThreadView { @@ -598,6 +600,40 @@ impl ProcessView { page_size, }) } + + /// Retrieves the memory reader for access to memory regions + pub(crate) fn create_memory_reader(pid: Pid) -> Result, CoreError> { + let memory_reader = if process_vm_readv_works() { + tracing::info!("Using the fast process memory read on this system"); + Box::new(FastMemoryReader::new(pid)?) as Box + } else { + tracing::info!("Using the slow process memory read on this system"); + Box::new(SlowMemoryReader::new(pid)?) as Box + }; + + Ok(memory_reader) + } +} + +impl ProcessInfoSource for ProcessView { + fn pid(&self) -> Pid { + self.pid + } + 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 { From 1ba4ab5cfa6cd4d6211348f8a7e6d9dc7a3411d0 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Doru=20Bl=C3=A2nzeanu?= Date: Fri, 25 Apr 2025 14:13:12 +0300 Subject: [PATCH 6/9] add tests that check the custom process info source functionality MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Signed-off-by: Doru Blânzeanu --- elfcore/src/coredump.rs | 172 ++++++++++++++++++++++++++++++++++++++++ elfcore/src/error.rs | 3 + 2 files changed, 175 insertions(+) diff --git a/elfcore/src/coredump.rs b/elfcore/src/coredump.rs index 651ff21..8bcb6f7 100644 --- a/elfcore/src/coredump.rs +++ b/elfcore/src/coredump.rs @@ -263,6 +263,11 @@ fn write_core_dump_inner( 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); + } + tracing::info!( "Creating core dump file for process {}. This process id: {}, this thread id: {}", pv.pid(), @@ -1003,3 +1008,170 @@ impl<'a> CoreDumpBuilder<'a> { ) } } + +#[cfg(test)] +mod tests { + use crate::{ArchState, ThreadView}; + + use super::*; + + struct MockProcessInfoSource { + pid: nix::unistd::Pid, + page_size: usize, + regions: Vec, + threads: Vec, + } + + impl ProcessInfoSource for MockProcessInfoSource { + fn pid(&self) -> nix::unistd::Pid { + 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 = Box::new(MockProcessInfoSource { + pid: nix::unistd::getpid(), + page_size: 4096, + regions: vec![], + threads: vec![], + }); + + let memory_reader = Box::new(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 = Box::new(MockProcessInfoSource { + pid: nix::unistd::getpid(), + page_size: 4096, + regions: vec![], + threads: vec![ThreadView { + flags: 0, // Kernel flags for the process + tid: nix::unistd::Pid::from_raw(0), + 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 = Box::new(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 = Box::new(MockProcessInfoSource { + pid: nix::unistd::getpid(), + page_size: 4096, + regions: vec![region], + threads: vec![ThreadView { + flags: 0, // Kernel flags for the process + tid: nix::unistd::getpid(), + 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 = Box::new(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/error.rs b/elfcore/src/error.rs index 426170d..e0e3cc3 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, From d3152cb3cbeee5f72b1b2ee8fd3b4c1ecf9ed5a7 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Doru=20Bl=C3=A2nzeanu?= Date: Fri, 25 Apr 2025 16:41:42 +0300 Subject: [PATCH 7/9] support building the elfcore crate on windows MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - the crate supports creating core dumps from a custom given source not from a Pid Signed-off-by: Doru Blânzeanu --- elfcore-sample/Cargo.toml | 2 +- elfcore-sample/src/main.rs | 13 ++-- elfcore/Cargo.toml | 8 +-- elfcore/src/arch/aarch64.rs | 10 ++- elfcore/src/arch/mod.rs | 13 ++-- elfcore/src/arch/x86_64.rs | 12 ++-- elfcore/src/coredump.rs | 28 +++++---- elfcore/src/elf.rs | 17 +++++ elfcore/src/error.rs | 1 + elfcore/src/lib.rs | 116 ++++++++++++++++++++++++++++++++--- elfcore/src/linux/mod.rs | 1 - elfcore/src/linux/process.rs | 109 ++------------------------------ 12 files changed, 185 insertions(+), 145 deletions(-) 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 7ee0ebc..8171561 100644 --- a/elfcore-sample/src/main.rs +++ b/elfcore-sample/src/main.rs @@ -11,12 +11,10 @@ //! debug level tracing. //! -#![cfg(target_os = "linux")] - -use anyhow::Context; -use std::path::PathBuf; -use tracing::Level; +#[cfg(target_os = "linux")] +use {anyhow::Context, std::path::PathBuf, tracing::Level}; +#[cfg(target_os = "linux")] pub fn main() -> anyhow::Result<()> { let mut args = std::env::args().skip(1).peekable(); @@ -72,3 +70,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..3de84f8 100644 --- a/elfcore/src/arch/aarch64.rs +++ b/elfcore/src/arch/aarch64.rs @@ -6,11 +6,12 @@ #![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, nix::unistd::Pid}; + // aarch64 machine pub const EM_AARCH64: u16 = 183; @@ -29,19 +30,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 f789de6..e47a922 100644 --- a/elfcore/src/arch/mod.rs +++ b/elfcore/src/arch/mod.rs @@ -3,11 +3,12 @@ //! A Rust helper library with machine-specific code for ELF core dump files. -use crate::elf::NT_PRFPREG; -use nix::unistd::Pid; - -use super::linux::ptrace; -use crate::CoreError; +#[cfg(target_os = "linux")] +use { + super::linux::ptrace, + crate::{elf::NT_PRFPREG, CoreError}, + nix::unistd::Pid, +}; #[cfg(target_arch = "x86_64")] mod x86_64; @@ -36,6 +37,7 @@ pub struct ArchComponentState { 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; @@ -60,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 a4e9dfd..51c1e6e 100644 --- a/elfcore/src/arch/x86_64.rs +++ b/elfcore/src/arch/x86_64.rs @@ -3,16 +3,18 @@ //! x86_64 specifics for ELF core dump files. -use super::ArchComponentState; -use crate::linux::ptrace::ptrace_get_reg_set; -use crate::CoreError; -use nix::unistd::Pid; use zerocopy::AsBytes; +#[cfg(target_os = "linux")] +use { + super::ArchComponentState, crate::linux::ptrace::ptrace_get_reg_set, crate::CoreError, + nix::unistd::Pid, +}; // amd64 machine pub const EM_X86_64: u16 = 62; // amd64 notes +#[cfg(target_os = "linux")] pub const NT_X86_XSTATE: u32 = 0x202; #[repr(C, packed)] @@ -50,10 +52,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 8bcb6f7..68258c3 100644 --- a/elfcore/src/coredump.rs +++ b/elfcore/src/coredump.rs @@ -11,9 +11,7 @@ use super::arch::Arch; use crate::elf::*; use crate::CoreError; use crate::ProcessInfoSource; -use crate::ProcessView; use crate::ReadProcessMemory; -use nix::libc::Elf64_Phdr; use smallvec::smallvec; use smallvec::SmallVec; use std::io::Read; @@ -21,6 +19,9 @@ use std::io::Write; use std::slice; use zerocopy::AsBytes; +#[cfg(target_os = "linux")] +use crate::ProcessView; + const ELF_HEADER_ALIGN: usize = 8; const ELF_NOTE_ALIGN: usize = 4; @@ -249,6 +250,7 @@ fn get_elf_notes_sizes( /// * `pv` - a `ProcessView` reference. /// /// To access new functionality, use [`CoreDumpBuilder`] +#[cfg(target_os = "linux")] pub fn write_core_dump(writer: T, pv: &ProcessView) -> Result { let memory_reader = ProcessView::create_memory_reader(pv.pid)?; write_core_dump_inner(writer, pv, None, memory_reader) @@ -268,12 +270,15 @@ fn write_core_dump_inner( return Err(CoreError::CustomSourceInfo); } + #[cfg(target_os = "linux")] tracing::info!( "Creating core dump file for process {}. This process id: {}, this thread id: {}", pv.pid(), nix::unistd::getpid(), nix::unistd::gettid() ); + #[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())?; @@ -577,7 +582,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, @@ -640,7 +645,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, @@ -957,6 +962,7 @@ pub struct CoreDumpBuilder<'a> { impl<'a> CoreDumpBuilder<'a> { /// Create a new core dump builder for the process with the provided PID + #[cfg(target_os = "linux")] pub fn new(pid: libc::pid_t) -> Result { let pv = ProcessView::new(pid)?; let memory_reader = ProcessView::create_memory_reader(pv.pid)?; @@ -1016,14 +1022,14 @@ mod tests { use super::*; struct MockProcessInfoSource { - pid: nix::unistd::Pid, + pid: i32, page_size: usize, regions: Vec, threads: Vec, } impl ProcessInfoSource for MockProcessInfoSource { - fn pid(&self) -> nix::unistd::Pid { + fn pid(&self) -> i32 { self.pid } @@ -1064,7 +1070,7 @@ mod tests { #[test] fn test_custom_source_no_threads() { let custom_source = Box::new(MockProcessInfoSource { - pid: nix::unistd::getpid(), + pid: 1, page_size: 4096, regions: vec![], threads: vec![], @@ -1081,12 +1087,12 @@ mod tests { #[test] fn test_custom_source_no_regions() { let custom_source = Box::new(MockProcessInfoSource { - pid: nix::unistd::getpid(), + pid: 1, page_size: 4096, regions: vec![], threads: vec![ThreadView { flags: 0, // Kernel flags for the process - tid: nix::unistd::Pid::from_raw(0), + tid: 1, uid: 0, // User ID gid: 0, // Group ID comm: "".to_string(), // Command name @@ -1137,12 +1143,12 @@ mod tests { }, }; let custom_source = Box::new(MockProcessInfoSource { - pid: nix::unistd::getpid(), + pid: 1, page_size: 4096, regions: vec![region], threads: vec![ThreadView { flags: 0, // Kernel flags for the process - tid: nix::unistd::getpid(), + tid: 1, uid: 0, // User ID gid: 0, // Group ID comm: "".to_string(), // Command name diff --git a/elfcore/src/elf.rs b/elfcore/src/elf.rs index d2dc125..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; @@ -166,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 e0e3cc3..7ef3c7e 100644 --- a/elfcore/src/error.rs +++ b/elfcore/src/error.rs @@ -28,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 199de0b..b7e59c5 100644 --- a/elfcore/src/lib.rs +++ b/elfcore/src/lib.rs @@ -3,27 +3,28 @@ //! A Rust library for creating ELF core dump files. -#![cfg(target_os = "linux")] #![warn(missing_docs)] mod arch; mod coredump; mod elf; mod error; + +#[cfg(target_os = "linux")] mod linux; pub use arch::ArchComponentState; pub use arch::ArchState; -pub use coredump::write_core_dump; pub use coredump::CoreDumpBuilder; pub use coredump::MappedFile; pub use coredump::VaProtection; pub use coredump::VaRegion; pub use elf::Elf64_Auxv; pub use error::CoreError; -pub use linux::ProcessView; -pub use linux::ThreadView; -pub use nix::unistd::Pid; + +// Linux specific functionality +#[cfg(target_os = "linux")] +pub use {coredump::write_core_dump, linux::ProcessView}; /// Trait for those able to read the process virtual memory. pub trait ReadProcessMemory { @@ -55,14 +56,14 @@ pub trait ReadProcessMemory { /// use std::fs::File; /// /// struct CustomSource { -/// pid: nix::unistd::Pid, +/// pid: i32, /// threads: Vec, /// va_regions: Vec, /// page_size: usize, /// } /// /// impl ProcessInfoSource for CustomSource { -/// fn pid(&self) -> nix::unistd::Pid { +/// fn pid(&self) -> i32 { /// self.pid /// } /// fn threads(&self) -> &[ThreadView] { @@ -100,10 +101,10 @@ pub trait ReadProcessMemory { /// // Example of ThreadView and VaRegion structures that can be used /// // in the custom source /// let custom_source = CustomSource { -/// pid: nix::unistd::getpid(), +/// pid: nix::unistd::getpid().as_raw(), /// threads: vec![ThreadView { /// flags: 0, // Kernel flags for the process -/// tid: nix::unistd::getpid(), +/// tid: nix::unistd::getpid().as_raw(), /// uid: 0, // User ID /// gid: 0, // Group ID /// comm: "example".to_string(), // Command name @@ -172,3 +173,100 @@ pub trait ProcessInfoSource { /// 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/mod.rs b/elfcore/src/linux/mod.rs index 349f275..d466a9b 100644 --- a/elfcore/src/linux/mod.rs +++ b/elfcore/src/linux/mod.rs @@ -6,4 +6,3 @@ mod process; pub mod ptrace; pub use process::ProcessView; -pub use process::ThreadView; diff --git a/elfcore/src/linux/process.rs b/elfcore/src/linux/process.rs index 9dd20c6..31ff100 100644 --- a/elfcore/src/linux/process.rs +++ b/elfcore/src/linux/process.rs @@ -13,8 +13,8 @@ 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::CoreError; use crate::{arch, ProcessInfoSource, ReadProcessMemory}; +use crate::{CoreError, ThreadView}; use nix::libc::Elf64_Phdr; use nix::sys; use nix::sys::ptrace::{seize, Options}; @@ -29,103 +29,6 @@ use std::io::{BufRead, IoSliceMut, Read}; use zerocopy::AsBytes; use zerocopy::FromZeroes; -/// Linux Light-weight Process -#[derive(Debug)] -pub struct ThreadView { - /// Thread id. - pub tid: Pid, - - /// 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, -} - impl ThreadView { pub(crate) fn new(pid: Pid, tid: Pid) -> Result { let cmd_line_path = format!("/proc/{}/task/{}/cmdline", pid, tid); @@ -225,7 +128,7 @@ impl ThreadView { let arch_state = arch::ArchState::new(tid)?; Ok(Self { - tid, + tid: tid.as_raw(), cmd_line, comm, state, @@ -512,7 +415,7 @@ impl ProcessView { /// * `pid` - process ID /// pub fn new(pid: libc::pid_t) -> Result { - let pid = nix::unistd::Pid::from_raw(pid); + let pid = Pid::from_raw(pid); let mut tids = get_thread_ids(pid)?; tids.sort(); @@ -616,8 +519,8 @@ impl ProcessView { } impl ProcessInfoSource for ProcessView { - fn pid(&self) -> Pid { - self.pid + fn pid(&self) -> i32 { + self.pid.as_raw() } fn threads(&self) -> &[ThreadView] { &self.threads @@ -645,7 +548,7 @@ impl Drop for ProcessView { ); for thread in &self.threads { - match sys::ptrace::detach(thread.tid, None) { + match sys::ptrace::detach(Pid::from_raw(thread.tid), None) { Ok(_) => { tracing::debug!("Thread {} resumed", thread.tid); } From 1a57f04272dd54bc06df638f4027debe6d0f694f Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Doru=20Bl=C3=A2nzeanu?= Date: Tue, 6 May 2025 19:53:15 +0300 Subject: [PATCH 8/9] refactor to use generics instead of trait objects MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Signed-off-by: Doru Blânzeanu --- elfcore-sample/src/main.rs | 9 ++- elfcore/src/coredump.rs | 120 +++++++++++++++++++---------------- elfcore/src/lib.rs | 7 +- elfcore/src/linux/memory.rs | 108 +++++++++++++++++++++---------- elfcore/src/linux/mod.rs | 1 + elfcore/src/linux/process.rs | 46 ++------------ 6 files changed, 157 insertions(+), 134 deletions(-) diff --git a/elfcore-sample/src/main.rs b/elfcore-sample/src/main.rs index 8171561..ba730a3 100644 --- a/elfcore-sample/src/main.rs +++ b/elfcore-sample/src/main.rs @@ -12,7 +12,12 @@ //! #[cfg(target_os = "linux")] -use {anyhow::Context, std::path::PathBuf, tracing::Level}; +use { + anyhow::Context, + elfcore::{CoreDumpBuilder, LinuxProcessMemoryReader, ProcessView}, + std::path::PathBuf, + tracing::Level, +}; #[cfg(target_os = "linux")] pub fn main() -> anyhow::Result<()> { @@ -51,7 +56,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| { diff --git a/elfcore/src/coredump.rs b/elfcore/src/coredump.rs index 68258c3..03aed9f 100644 --- a/elfcore/src/coredump.rs +++ b/elfcore/src/coredump.rs @@ -20,7 +20,7 @@ use std::slice; use zerocopy::AsBytes; #[cfg(target_os = "linux")] -use crate::ProcessView; +use crate::{linux::LinuxProcessMemoryReader, ProcessView}; const ELF_HEADER_ALIGN: usize = 8; const ELF_NOTE_ALIGN: usize = 4; @@ -162,8 +162,8 @@ struct CustomFileNote<'a> { pub note_len: usize, } -fn get_elf_notes_sizes( - pv: &dyn ProcessInfoSource, +fn get_elf_notes_sizes( + pv: &P, custom_notes: Option<&[CustomFileNote<'_>]>, ) -> Result { let header_and_name = @@ -252,15 +252,20 @@ fn get_elf_notes_sizes( /// To access new functionality, use [`CoreDumpBuilder`] #[cfg(target_os = "linux")] pub fn write_core_dump(writer: T, pv: &ProcessView) -> Result { - let memory_reader = ProcessView::create_memory_reader(pv.pid)?; - write_core_dump_inner(writer, pv, None, memory_reader) + 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: &dyn ProcessInfoSource, + pv: &P, custom_notes: Option<&mut [CustomFileNote<'_>]>, - memory_reader: Box, + memory_reader: &mut M, ) -> Result { let mut total_written = 0_usize; let mut writer = ElfCoreWriter::new(writer); @@ -309,9 +314,9 @@ fn round_up(value: usize, alignment: usize) -> usize { } } -fn write_elf_header( +fn write_elf_header( writer: &mut ElfCoreWriter, - pv: &dyn ProcessInfoSource, + pv: &P, ) -> Result { let mut e_ident = [0_u8; 16]; e_ident[EI_MAG0] = ELFMAG0; @@ -360,9 +365,9 @@ fn write_elf_header( Ok(slice.len()) } -fn write_program_headers( +fn write_program_headers( writer: &mut ElfCoreWriter, - pv: &dyn ProcessInfoSource, + pv: &P, note_sizes: &NoteSizes, ) -> Result { tracing::info!( @@ -557,9 +562,9 @@ fn write_elf_note_file( Ok(written) } -fn write_process_info_note( +fn write_process_info_note( writer: &mut ElfCoreWriter, - pv: &dyn ProcessInfoSource, + pv: &P, ) -> Result { let mut written = 0_usize; @@ -625,9 +630,9 @@ fn write_process_info_note( Ok(written) } -fn write_process_status_notes( +fn write_process_status_notes( writer: &mut ElfCoreWriter, - pv: &dyn ProcessInfoSource, + pv: &P, ) -> Result { let mut total_written = 0_usize; @@ -704,9 +709,9 @@ fn write_process_status_notes( Ok(total_written) } -fn write_aux_vector_note( +fn write_aux_vector_note( writer: &mut ElfCoreWriter, - pv: &dyn ProcessInfoSource, + pv: &P, ) -> Result { tracing::info!( "Writing auxiliary vector at offset {}...", @@ -723,9 +728,9 @@ fn write_aux_vector_note( Ok(written) } -fn write_mapped_files_note( +fn write_mapped_files_note( writer: &mut ElfCoreWriter, - pv: &dyn ProcessInfoSource, + pv: &P, ) -> Result { tracing::info!( "Writing mapped files note at offset {}...", @@ -805,9 +810,9 @@ fn write_custom_notes( Ok(total_written) } -fn write_elf_notes( +fn write_elf_notes( writer: &mut ElfCoreWriter, - pv: &dyn ProcessInfoSource, + pv: &P, note_sizes: &NoteSizes, custom_notes: Option<&mut [CustomFileNote<'_>]>, ) -> Result { @@ -867,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: &dyn ProcessInfoSource, - 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]; @@ -918,11 +928,16 @@ fn write_va_region( Ok(dumped) } -fn write_va_regions( +fn write_va_regions( writer: &mut ElfCoreWriter, - pv: &dyn ProcessInfoSource, - 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!( @@ -931,7 +946,7 @@ fn write_va_regions( ); for va_region in pv.va_regions() { - let dumped = write_va_region(writer, va_region, pv, &mut memory_reader)?; + let dumped = write_va_region(writer, va_region, pv, memory_reader)?; written += dumped; @@ -954,31 +969,30 @@ fn write_va_regions( /// optionally with custom notes with content from files /// This also supports generating core dumps with information /// from a custom source. -pub struct CoreDumpBuilder<'a> { - pv: Box, +pub struct CoreDumpBuilder<'a, P: ProcessInfoSource, M: ReadProcessMemory> { + pv: P, custom_notes: Vec>, - memory_reader: Box, + 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 #[cfg(target_os = "linux")] - pub fn new(pid: libc::pid_t) -> Result { + pub fn new( + pid: libc::pid_t, + ) -> Result, CoreError> { let pv = ProcessView::new(pid)?; let memory_reader = ProcessView::create_memory_reader(pv.pid)?; - Ok(Self { - pv: Box::new(pv) as Box, + Ok(CoreDumpBuilder { + pv, custom_notes: Vec::new(), memory_reader, }) } /// Create a new core dump builder from a custom `ProcessInfoSource` - pub fn from_source( - source: Box, - memory_reader: Box, - ) -> CoreDumpBuilder<'a> { + pub fn from_source(source: P, memory_reader: M) -> CoreDumpBuilder<'a, P, M> { CoreDumpBuilder { pv: source, custom_notes: Vec::new(), @@ -1008,9 +1022,9 @@ impl<'a> CoreDumpBuilder<'a> { pub fn write(mut self, writer: T) -> Result { write_core_dump_inner( writer, - self.pv.as_ref(), + &self.pv, Some(&mut self.custom_notes), - self.memory_reader, + &mut self.memory_reader, ) } } @@ -1069,14 +1083,14 @@ mod tests { /// 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 = Box::new(MockProcessInfoSource { + let custom_source = MockProcessInfoSource { pid: 1, page_size: 4096, regions: vec![], threads: vec![], - }); + }; - let memory_reader = Box::new(MockMemoryReader {}); + let memory_reader = MockMemoryReader {}; let core_dump_builder = CoreDumpBuilder::from_source(custom_source, memory_reader); let res = core_dump_builder.write(std::io::sink()); @@ -1086,7 +1100,7 @@ mod tests { /// 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 = Box::new(MockProcessInfoSource { + let custom_source = MockProcessInfoSource { pid: 1, page_size: 4096, regions: vec![], @@ -1115,9 +1129,9 @@ mod tests { components: vec![], }), }], - }); + }; - let memory_reader = Box::new(MockMemoryReader {}); + let memory_reader = MockMemoryReader {}; let core_dump_builder = CoreDumpBuilder::from_source(custom_source, memory_reader); let res = core_dump_builder.write(std::io::sink()); @@ -1142,7 +1156,7 @@ mod tests { is_private: false, }, }; - let custom_source = Box::new(MockProcessInfoSource { + let custom_source = MockProcessInfoSource { pid: 1, page_size: 4096, regions: vec![region], @@ -1171,9 +1185,9 @@ mod tests { components: vec![], }), }], - }); + }; - let memory_reader = Box::new(MockMemoryReader {}); + let memory_reader = MockMemoryReader {}; let core_dump_builder = CoreDumpBuilder::from_source(custom_source, memory_reader); let res = core_dump_builder.write(std::io::sink()); diff --git a/elfcore/src/lib.rs b/elfcore/src/lib.rs index b7e59c5..8c93cb8 100644 --- a/elfcore/src/lib.rs +++ b/elfcore/src/lib.rs @@ -24,7 +24,10 @@ pub use error::CoreError; // Linux specific functionality #[cfg(target_os = "linux")] -pub use {coredump::write_core_dump, linux::ProcessView}; +pub use { + coredump::write_core_dump, + linux::{LinuxProcessMemoryReader, ProcessView}, +}; /// Trait for those able to read the process virtual memory. pub trait ReadProcessMemory { @@ -145,7 +148,7 @@ pub trait ReadProcessMemory { /// let custom_reader = CustomReader {}; /// /// // Create a core dump builder using the custom source and reader -/// let mut cdb = CoreDumpBuilder::from_source(Box::new(custom_source), Box::new(custom_reader)); +/// let mut cdb = CoreDumpBuilder::from_source(custom_source, custom_reader); /// /// // Writer used for example purposes only /// let writer = std::io::sink(); diff --git a/elfcore/src/linux/memory.rs b/elfcore/src/linux/memory.rs index a2072fb..9e3d7e5 100644 --- a/elfcore/src/linux/memory.rs +++ b/elfcore/src/linux/memory.rs @@ -10,53 +10,91 @@ use nix::sys::uio::{process_vm_readv, RemoteIoVec}; use nix::unistd::Pid; use std::io::{IoSliceMut, Read, Seek}; -/// 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. -pub(crate) struct FastMemoryReader { - pid: Pid, +/// 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 FastMemoryReader { +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 { - Ok(Self { pid }) + 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 FastMemoryReader { +impl ReadProcessMemory for LinuxProcessMemoryReader { 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) + 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()) + } + } } } -/// A slow but more compatible process memory reader, uses the `/proc//mem` -/// file. -pub(crate) struct SlowMemoryReader { - file: std::fs::File, -} +/// 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(); -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 }) - } -} + 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), + }], + ); -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)?; + if let Err(e) = result { + tracing::debug!("process_vm_readv has not succeeded, error {e:?}, won't be using it"); + return false; + } - Ok(buf.len()) + 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 index d466a9b..2368417 100644 --- a/elfcore/src/linux/mod.rs +++ b/elfcore/src/linux/mod.rs @@ -5,4 +5,5 @@ 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 index 31ff100..b2b6c98 100644 --- a/elfcore/src/linux/process.rs +++ b/elfcore/src/linux/process.rs @@ -5,7 +5,7 @@ //! //! Gathering process information. -use super::memory::{FastMemoryReader, SlowMemoryReader}; +use super::memory::LinuxProcessMemoryReader; use super::ptrace::ptrace_interrupt; use crate::arch::Arch; use crate::coredump::{MappedFile, MappedFileRegion, VaProtection, VaRegion}; @@ -13,7 +13,7 @@ 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, ReadProcessMemory}; +use crate::{arch, ProcessInfoSource}; use crate::{CoreError, ThreadView}; use nix::libc::Elf64_Phdr; use nix::sys; @@ -505,16 +505,8 @@ impl ProcessView { } /// Retrieves the memory reader for access to memory regions - pub(crate) fn create_memory_reader(pid: Pid) -> Result, CoreError> { - let memory_reader = if process_vm_readv_works() { - tracing::info!("Using the fast process memory read on this system"); - Box::new(FastMemoryReader::new(pid)?) as Box - } else { - tracing::info!("Using the slow process memory read on this system"); - Box::new(SlowMemoryReader::new(pid)?) as Box - }; - - Ok(memory_reader) + pub(crate) fn create_memory_reader(pid: Pid) -> Result { + LinuxProcessMemoryReader::new(pid) } } @@ -559,33 +551,3 @@ impl Drop for ProcessView { } } } - -/// The `process_vm_readv` system call might be unavailable. An extra check is made to be -/// sure the ABI works. -pub(crate) 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 -} From cef4c80e26bf4b2a5599e50d2d1730965f942c13 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Doru=20Bl=C3=A2nzeanu?= Date: Fri, 16 May 2025 14:28:10 +0300 Subject: [PATCH 9/9] split imports to separate lines MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Signed-off-by: Doru Blânzeanu --- elfcore-sample/src/main.rs | 13 +++++++------ elfcore/src/arch/aarch64.rs | 4 +++- elfcore/src/arch/mod.rs | 10 +++++----- elfcore/src/arch/x86_64.rs | 13 ++++++++----- elfcore/src/coredump.rs | 2 +- elfcore/src/lib.rs | 7 +++---- 6 files changed, 27 insertions(+), 22 deletions(-) diff --git a/elfcore-sample/src/main.rs b/elfcore-sample/src/main.rs index ba730a3..62bd8a1 100644 --- a/elfcore-sample/src/main.rs +++ b/elfcore-sample/src/main.rs @@ -12,12 +12,13 @@ //! #[cfg(target_os = "linux")] -use { - anyhow::Context, - elfcore::{CoreDumpBuilder, LinuxProcessMemoryReader, ProcessView}, - std::path::PathBuf, - tracing::Level, -}; +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<()> { diff --git a/elfcore/src/arch/aarch64.rs b/elfcore/src/arch/aarch64.rs index 3de84f8..fea8e41 100644 --- a/elfcore/src/arch/aarch64.rs +++ b/elfcore/src/arch/aarch64.rs @@ -10,7 +10,9 @@ use crate::CoreError; use zerocopy::AsBytes; #[cfg(target_os = "linux")] -use {crate::ptrace::ptrace_get_reg_set, nix::unistd::Pid}; +use crate::ptrace::ptrace_get_reg_set; +#[cfg(target_os = "linux")] +use nix::unistd::Pid; // aarch64 machine pub const EM_AARCH64: u16 = 183; diff --git a/elfcore/src/arch/mod.rs b/elfcore/src/arch/mod.rs index e47a922..5f3e6c3 100644 --- a/elfcore/src/arch/mod.rs +++ b/elfcore/src/arch/mod.rs @@ -4,11 +4,11 @@ //! A Rust helper library with machine-specific code for ELF core dump files. #[cfg(target_os = "linux")] -use { - super::linux::ptrace, - crate::{elf::NT_PRFPREG, CoreError}, - nix::unistd::Pid, -}; +use super::linux::ptrace; +#[cfg(target_os = "linux")] +use crate::{elf::NT_PRFPREG, CoreError}; +#[cfg(target_os = "linux")] +use nix::unistd::Pid; #[cfg(target_arch = "x86_64")] mod x86_64; diff --git a/elfcore/src/arch/x86_64.rs b/elfcore/src/arch/x86_64.rs index 51c1e6e..0ff2a56 100644 --- a/elfcore/src/arch/x86_64.rs +++ b/elfcore/src/arch/x86_64.rs @@ -3,12 +3,15 @@ //! x86_64 specifics for ELF core dump files. -use zerocopy::AsBytes; #[cfg(target_os = "linux")] -use { - super::ArchComponentState, crate::linux::ptrace::ptrace_get_reg_set, crate::CoreError, - nix::unistd::Pid, -}; +use super::ArchComponentState; +#[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; // amd64 machine pub const EM_X86_64: u16 = 62; diff --git a/elfcore/src/coredump.rs b/elfcore/src/coredump.rs index 03aed9f..047ebb8 100644 --- a/elfcore/src/coredump.rs +++ b/elfcore/src/coredump.rs @@ -20,7 +20,7 @@ use std::slice; use zerocopy::AsBytes; #[cfg(target_os = "linux")] -use crate::{linux::LinuxProcessMemoryReader, ProcessView}; +use crate::{LinuxProcessMemoryReader, ProcessView}; const ELF_HEADER_ALIGN: usize = 8; const ELF_NOTE_ALIGN: usize = 4; diff --git a/elfcore/src/lib.rs b/elfcore/src/lib.rs index 8c93cb8..92316ab 100644 --- a/elfcore/src/lib.rs +++ b/elfcore/src/lib.rs @@ -24,10 +24,9 @@ pub use error::CoreError; // Linux specific functionality #[cfg(target_os = "linux")] -pub use { - coredump::write_core_dump, - linux::{LinuxProcessMemoryReader, ProcessView}, -}; +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 {