Intel PT minor fixes/improvements (#2724)

* waitpid_filtered to ignore SIGWINCH

* Fix warnings unused manifest key: *.version

* Add export_raw feature to libafl_intelpt

* derive Debug for IntelPTHook

* Clippy

* Update target program ELF offsets

* Add comment to KVM pt_mode check

* refactor

* Add intel_pt_export_raw feature in libafl

* map_error instead of unwrap

* borrow checker friendly join_split_trace

and copy trace before deocde to prevent decoding failures

* Set ip_filters (also) with builder

* Move trace to file

* Fix Cargo.toml docs

* Ignore blocks with no instruction

most likely they are filtered out
This commit is contained in:
Marco C. 2024-12-03 08:43:17 +01:00 committed by GitHub
parent 95d87bd7d8
commit 36734083f9
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
8 changed files with 140 additions and 68 deletions

View File

@ -84,24 +84,25 @@ pub fn main() {
// A fuzzer with feedbacks and a corpus scheduler
let mut fuzzer = StdFuzzer::new(scheduler, feedback, objective);
let mut intel_pt = IntelPT::builder().cpu(cpu.0).inherit(true).build().unwrap();
// The target is a ET_DYN elf, it will be relocated by the loader with this offset.
// see https://github.com/torvalds/linux/blob/c1e939a21eb111a6d6067b38e8e04b8809b64c4e/arch/x86/include/asm/elf.h#L234C1-L239C38
const DEFAULT_MAP_WINDOW: usize = (1 << 47) - PAGE_SIZE;
const ELF_ET_DYN_BASE: usize = DEFAULT_MAP_WINDOW / 3 * 2 & !(PAGE_SIZE - 1);
const ELF_ET_DYN_BASE: usize = (DEFAULT_MAP_WINDOW / 3 * 2) & !(PAGE_SIZE - 1);
// Set the instruction pointer (IP) filter and memory image of our target.
// These information can be retrieved from `readelf -l` (for example)
let code_memory_addresses = ELF_ET_DYN_BASE + 0x14000..=ELF_ET_DYN_BASE + 0x14000 + 0x40000;
let code_memory_addresses = ELF_ET_DYN_BASE + 0x15000..=ELF_ET_DYN_BASE + 0x14000 + 0x41000;
intel_pt
.set_ip_filters(&[code_memory_addresses.clone()])
let intel_pt = IntelPT::builder()
.cpu(cpu.0)
.inherit(true)
.ip_filters(&[code_memory_addresses.clone()])
.build()
.unwrap();
let sections = [Section {
file_path: target_path.to_string_lossy().to_string(),
file_offset: 0x13000,
file_offset: 0x14000,
size: (*code_memory_addresses.end() - *code_memory_addresses.start() + 1) as u64,
virtual_address: *code_memory_addresses.start() as u64,
}];

View File

@ -115,6 +115,8 @@ intel_pt = [
"dep:nix",
"dep:num_enum",
]
## Save all the Intel PT raw traces to files, use only for debug
intel_pt_export_raw = ["intel_pt", "libafl_intelpt/export_raw"]
## Enables features for corpus minimization
cmin = ["z3"]

View File

@ -29,7 +29,19 @@ use libafl_bolts::{
#[cfg(target_os = "linux")]
use libc::STDIN_FILENO;
#[cfg(target_os = "linux")]
use nix::unistd::Pid;
use nix::{
errno::Errno,
sys::{
ptrace,
signal::Signal,
wait::WaitStatus,
wait::{
waitpid, WaitPidFlag,
WaitStatus::{Exited, PtraceEvent, Signaled, Stopped},
},
},
unistd::Pid,
};
#[cfg(target_os = "linux")]
use typed_builder::TypedBuilder;
@ -204,8 +216,6 @@ where
match unsafe { fork() } {
Ok(ForkResult::Parent { child }) => Ok(child),
Ok(ForkResult::Child) => {
ptrace::traceme().unwrap();
if let Some(c) = self.cpu {
c.set_affinity_forced().unwrap();
}
@ -241,6 +251,7 @@ where
}
}
ptrace::traceme().unwrap();
// After this STOP, the process is traced with PTrace (no hooks yet)
raise(Signal::SIGSTOP).unwrap();
@ -405,13 +416,13 @@ where
T: CommandConfigurator<<S::Corpus as Corpus>::Input>,
{
#[inline]
fn set_timeout(&mut self, timeout: Duration) {
*self.configurer.exec_timeout_mut() = timeout;
fn timeout(&self) -> Duration {
self.configurer.exec_timeout()
}
#[inline]
fn timeout(&self) -> Duration {
self.configurer.exec_timeout()
fn set_timeout(&mut self, timeout: Duration) {
*self.configurer.exec_timeout_mut() = timeout;
}
}
@ -437,32 +448,28 @@ where
_mgr: &mut EM,
input: &Self::Input,
) -> Result<ExitKind, Error> {
use nix::sys::{
ptrace,
signal::Signal,
wait::{
waitpid, WaitPidFlag,
WaitStatus::{Exited, PtraceEvent, Signaled, Stopped},
},
};
*state.executions_mut() += 1;
let child = self.configurer.spawn_child(input)?;
let wait_status = waitpid(child, Some(WaitPidFlag::WUNTRACED))?;
let wait_status = waitpid_filtered(child, Some(WaitPidFlag::WUNTRACED))?;
if !matches!(wait_status, Stopped(c, Signal::SIGSTOP) if c == child) {
return Err(Error::unknown("Unexpected state of child process"));
return Err(Error::unknown(format!(
"Unexpected state of child process {wait_status:?} (while waiting for SIGSTOP)"
)));
}
ptrace::setoptions(child, ptrace::Options::PTRACE_O_TRACEEXEC)?;
let options = ptrace::Options::PTRACE_O_TRACEEXEC | ptrace::Options::PTRACE_O_EXITKILL;
ptrace::setoptions(child, options)?;
ptrace::cont(child, None)?;
let wait_status = waitpid(child, None)?;
let wait_status = waitpid_filtered(child, None)?;
if !matches!(wait_status, PtraceEvent(c, Signal::SIGTRAP, e)
if c == child && e == (ptrace::Event::PTRACE_EVENT_EXEC as i32)
) {
return Err(Error::unknown("Unexpected state of child process"));
return Err(Error::unknown(format!(
"Unexpected state of child process {wait_status:?} (while waiting for SIGTRAP PTRACE_EVENT_EXEC)"
)));
}
self.observers.pre_exec_child_all(state, input)?;
@ -471,6 +478,8 @@ where
}
self.hooks.pre_exec_all(state, input);
// todo: it might be better to keep the target ptraced in case the target handles sigalarm,
// breaking the libafl timeout
ptrace::detach(child, None)?;
let res = match waitpid(child, None)? {
Exited(pid, 0) if pid == child => ExitKind::Ok,
@ -478,9 +487,9 @@ where
Signaled(pid, Signal::SIGALRM, _has_coredump) if pid == child => ExitKind::Timeout,
Signaled(pid, Signal::SIGABRT, _has_coredump) if pid == child => ExitKind::Crash,
Signaled(pid, Signal::SIGKILL, _has_coredump) if pid == child => ExitKind::Oom,
Stopped(pid, Signal::SIGALRM) if pid == child => ExitKind::Timeout,
Stopped(pid, Signal::SIGABRT) if pid == child => ExitKind::Crash,
Stopped(pid, Signal::SIGKILL) if pid == child => ExitKind::Oom,
// Stopped(pid, Signal::SIGALRM) if pid == child => ExitKind::Timeout,
// Stopped(pid, Signal::SIGABRT) if pid == child => ExitKind::Crash,
// Stopped(pid, Signal::SIGKILL) if pid == child => ExitKind::Oom,
s => {
// TODO other cases?
return Err(Error::unsupported(
@ -855,6 +864,22 @@ pub trait CommandConfigurator<I, C = Child>: Sized {
}
}
/// waitpid wrapper that ignores some signals sent by the ptraced child
#[cfg(all(feature = "std", target_os = "linux"))]
fn waitpid_filtered(pid: Pid, options: Option<WaitPidFlag>) -> Result<WaitStatus, Errno> {
loop {
let wait_status = waitpid(pid, options);
let sig = match &wait_status {
// IGNORED
Ok(Stopped(c, Signal::SIGWINCH)) if *c == pid => Signal::SIGWINCH,
// RETURNED
Ok(ws) => break Ok(*ws),
Err(e) => break Err(*e),
};
ptrace::cont(pid, sig)?;
}
}
#[cfg(test)]
mod tests {
use crate::{

View File

@ -30,7 +30,7 @@ pub struct Section {
}
/// Hook to enable Intel Processor Trace (PT) tracing
#[derive(TypedBuilder)]
#[derive(Debug, TypedBuilder)]
pub struct IntelPTHook<T> {
#[builder(default = IntelPT::builder().build().unwrap())]
intel_pt: IntelPT,
@ -40,17 +40,6 @@ pub struct IntelPTHook<T> {
map_len: usize,
}
//fixme: just derive(Debug) once https://github.com/sum-catnip/libipt-rs/pull/4 will be on crates.io
impl<T> Debug for IntelPTHook<T> {
fn fmt(&self, f: &mut core::fmt::Formatter<'_>) -> Result<(), core::fmt::Error> {
f.debug_struct("IntelPTHook")
.field("intel_pt", &self.intel_pt)
.field("map_ptr", &self.map_ptr)
.field("map_len", &self.map_len)
.finish()
}
}
impl<S, T> ExecutorHook<S> for IntelPTHook<T>
where
S: UsesInput + Serialize,
@ -63,13 +52,19 @@ where
}
fn post_exec(&mut self, _state: &mut S, _input: &S::Input) {
self.intel_pt.disable_tracing().unwrap();
let pt = &mut self.intel_pt;
pt.disable_tracing().unwrap();
let slice = unsafe { &mut *slice_from_raw_parts_mut(self.map_ptr, self.map_len) };
let _ = self
.intel_pt
let _ = pt
.decode_traces_into_map(&mut self.image.0, slice)
.inspect_err(|e| log::warn!("Intel PT trace decoding failed: {e}"));
#[cfg(feature = "intel_pt_export_raw")]
{
let _ = pt
.dump_last_trace_to_file()
.inspect_err(|e| log::warn!("Intel PT trace save to file failed: {e}"));
}
}
}

View File

@ -44,7 +44,7 @@ cmake = { workspace = true }
bindgen = { workspace = true }
regex = { workspace = true }
which = { workspace = true }
symcc_libafl = { workspace = true, default-features = true, version = "0.14.1" }
symcc_libafl = { workspace = true, default-features = true }
[lints]
workspace = true

View File

@ -15,6 +15,8 @@ default = ["std", "libipt"]
std = ["libafl_bolts/std"]
libipt = ["std", "dep:libipt"]
## Export raw Intel PT traces on decode, useful for debug, disabled by default for best performance
export_raw = []
[dev-dependencies]
static_assertions = { workspace = true }

View File

@ -19,6 +19,7 @@ use std::{
};
#[cfg(target_os = "linux")]
use std::{
boxed::Box,
ffi::{CStr, CString},
fmt::Debug,
format, fs,
@ -129,6 +130,8 @@ pub struct IntelPT {
aux_tail: *mut u64,
previous_decode_head: u64,
ip_filters: Vec<RangeInclusive<usize>>,
#[cfg(feature = "export_raw")]
last_decode_trace: Vec<u8>,
}
#[cfg(target_os = "linux")]
@ -302,8 +305,12 @@ impl IntelPT {
}
} else {
// Head pointer wrapped, the trace is split
unsafe { self.join_split_trace(head_wrap, tail_wrap) }
OwnedRefMut::Owned(self.join_split_trace(head_wrap, tail_wrap))
};
#[cfg(feature = "export_raw")]
{
self.last_decode_trace = data.as_ref().to_vec();
}
let mut config = ConfigBuilder::new(data.as_mut()).map_err(error_from_pt_error)?;
config.filter(self.ip_filters_to_addr_filter());
@ -351,19 +358,17 @@ impl IntelPT {
#[inline]
#[must_use]
unsafe fn join_split_trace(&self, head_wrap: u64, tail_wrap: u64) -> OwnedRefMut<[u8]> {
let first_ptr = self.perf_aux_buffer.add(tail_wrap as usize) as *mut u8;
fn join_split_trace(&self, head_wrap: u64, tail_wrap: u64) -> Box<[u8]> {
let first_ptr = unsafe { self.perf_aux_buffer.add(tail_wrap as usize) as *mut u8 };
let first_len = self.perf_aux_buffer_size - tail_wrap as usize;
let second_ptr = self.perf_aux_buffer as *mut u8;
let second_len = head_wrap as usize;
OwnedRefMut::Owned(
[
slice::from_raw_parts(first_ptr, first_len),
slice::from_raw_parts(second_ptr, second_len),
]
.concat()
.into_boxed_slice(),
)
let mut vec = Vec::with_capacity(first_len + second_len);
vec.extend_from_slice(unsafe { slice::from_raw_parts(first_ptr, first_len) });
vec.extend_from_slice(unsafe { slice::from_raw_parts(second_ptr, second_len) });
vec.into_boxed_slice()
}
#[inline]
@ -395,7 +400,7 @@ impl IntelPT {
*status = s;
let offset = decoder.offset().map_err(error_from_pt_error)?;
if !b.speculative() && skip < offset {
if b.ninsn() > 0 && !b.speculative() && skip < offset {
let id = hash_me(*previous_block_end_ip) ^ hash_me(b.ip());
// SAFETY: the index is < map.len() since the modulo operation is applied
let map_loc = unsafe { map.get_unchecked_mut(id as usize % map.len()) };
@ -416,6 +421,29 @@ impl IntelPT {
}
Ok(())
}
/// Get the raw trace used in the last decoding
#[cfg(feature = "export_raw")]
pub fn last_decode_trace(&self) -> Vec<u8> {
self.last_decode_trace.clone()
}
/// Dump the raw trace used in the last decoding to the file
/// /// `./traces/trace_<unix epoch in micros>`
#[cfg(feature = "export_raw")]
pub fn dump_last_trace_to_file(&self) -> Result<(), Error> {
use std::{fs, io::Write, path::Path, time};
let traces_dir = Path::new("traces");
fs::create_dir_all(traces_dir)?;
let timestamp = time::SystemTime::now()
.duration_since(time::UNIX_EPOCH)
.map_err(|e| Error::unknown(e.to_string()))?
.as_micros();
let mut file = fs::File::create(traces_dir.join(format!("trace_{timestamp}")))?;
file.write_all(&self.last_decode_trace())?;
Ok(())
}
}
#[cfg(target_os = "linux")]
@ -441,6 +469,7 @@ pub struct IntelPTBuilder {
inherit: bool,
perf_buffer_size: usize,
perf_aux_buffer_size: usize,
ip_filters: Vec<RangeInclusive<usize>>,
}
#[cfg(target_os = "linux")]
@ -450,14 +479,15 @@ impl Default for IntelPTBuilder {
/// The default configuration corresponds to:
/// ```rust
/// use libafl_intelpt::{IntelPTBuilder, PAGE_SIZE};
/// let builder = unsafe { std::mem::zeroed::<IntelPTBuilder>() }
/// let builder = IntelPTBuilder::default()
/// .pid(None)
/// .all_cpus()
/// .exclude_kernel(true)
/// .exclude_hv(true)
/// .inherit(false)
/// .perf_buffer_size(128 * PAGE_SIZE + PAGE_SIZE).unwrap()
/// .perf_aux_buffer_size(2 * 1024 * 1024).unwrap();
/// .perf_aux_buffer_size(2 * 1024 * 1024).unwrap()
/// .ip_filters(&[]);
/// assert_eq!(builder, IntelPTBuilder::default());
/// ```
fn default() -> Self {
@ -469,6 +499,7 @@ impl Default for IntelPTBuilder {
inherit: false,
perf_buffer_size: 128 * PAGE_SIZE + PAGE_SIZE,
perf_aux_buffer_size: 2 * 1024 * 1024,
ip_filters: Vec::new(),
}
}
}
@ -532,9 +563,7 @@ impl IntelPTBuilder {
let aux_head = unsafe { &raw mut (*buff_metadata).aux_head };
let aux_tail = unsafe { &raw mut (*buff_metadata).aux_tail };
let ip_filters = Vec::with_capacity(*NR_ADDR_FILTERS.as_ref().unwrap_or(&0) as usize);
Ok(IntelPT {
let mut intel_pt = IntelPT {
fd,
perf_buffer,
perf_aux_buffer,
@ -543,8 +572,14 @@ impl IntelPTBuilder {
aux_head,
aux_tail,
previous_decode_head: 0,
ip_filters,
})
ip_filters: Vec::with_capacity(*NR_ADDR_FILTERS.as_ref().unwrap_or(&0) as usize),
#[cfg(feature = "export_raw")]
last_decode_trace: Vec::new(),
};
if !self.ip_filters.is_empty() {
intel_pt.set_ip_filters(&self.ip_filters)?;
}
Ok(intel_pt)
}
/// Warn if the configuration is not recommended
@ -638,6 +673,15 @@ impl IntelPTBuilder {
self.perf_aux_buffer_size = perf_aux_buffer_size;
Ok(self)
}
#[must_use]
/// Set filters based on Instruction Pointer (IP)
///
/// Only instructions in `filters` ranges will be traced.
pub fn ip_filters(mut self, filters: &[RangeInclusive<usize>]) -> Self {
self.ip_filters = filters.to_vec();
self
}
}
/// Perf event config for `IntelPT`
@ -717,6 +761,9 @@ pub fn availability_in_qemu_kvm() -> Result<(), String> {
#[cfg(target_os = "linux")]
{
let kvm_pt_mode_path = "/sys/module/kvm_intel/parameters/pt_mode";
// Ignore the case when the file does not exist since it has been removed.
// KVM default is `System` mode
// https://lore.kernel.org/all/20241101185031.1799556-1-seanjc@google.com/t/#u
if let Ok(s) = fs::read_to_string(kvm_pt_mode_path) {
match s.trim().parse::<i32>().map(TryInto::try_into) {
Ok(Ok(KvmPTMode::System)) => (),

View File

@ -90,7 +90,7 @@ clippy = ["libafl_qemu_sys/clippy"]
[dependencies]
libafl = { workspace = true, features = ["std", "derive", "regex"] }
libafl_bolts = { workspace = true, features = ["std", "derive"] }
libafl_targets = { workspace = true, default-features = true, version = "0.14.1" }
libafl_targets = { workspace = true, default-features = true }
libafl_qemu_sys = { workspace = true }
libafl_derive = { workspace = true, default-features = true }
@ -129,7 +129,7 @@ getset = "0.1.3"
document-features = { workspace = true, optional = true }
[build-dependencies]
libafl_qemu_build = { workspace = true, default-features = true, version = "0.14.1" }
libafl_qemu_build = { workspace = true, default-features = true }
pyo3-build-config = { workspace = true, optional = true }
rustversion = { workspace = true }
bindgen = { workspace = true }