From 47f7978b919bcc254a8ed019a9d413853e94b4b9 Mon Sep 17 00:00:00 2001 From: "Marco C." <46560192+Marcondiro@users.noreply.github.com> Date: Mon, 17 Feb 2025 14:50:07 +0100 Subject: [PATCH] Bump Libipt 0.3, add HW breakpoint support (#2984) * Decode with callback * WIP restore intelpt module * Fix build_target if target_dir doesn't exist * WIP itelpt qemu/kvm example: bootloader * qemu config refactoring * Fix intel_pt_command_executor target dir * * QEMU error refactoring* * back to one QEMU init function * other small things * update test * Bump libipt * waitpid_filtered to ignore SIGWINCH * Fix warnings unused manifest key: *.version * Add export_raw feature to libafl_intelpt * derive Debug for IntelPTHook * Clippy * Light refactor of EmulatorModules * qemu is now a parameter to EmulatorModule callbacks and most function hooks. * EmulatorModules is initialized before QEMU is initialized. * Update target program ELF offsets * fmt * * asan fixed size accesses working with generics * continue to propagate qemu argument as hook first parameter * use pre_syscall* and post_syscall* everywhere * fix some clippy stuff * fmt * Add comment to KVM pt_mode check * refactor * Add intel_pt_export_raw feature in libafl * fix fuzzers * * refactor asan and asanguest modules to avoid custom init of QEMU and use the module interface instead. * adapt qemu_launcher example to fully work with emulator, since qemu must now be initialized by emulator. * fmt * clippy * fix qemu_coverage * fmt * forgot qemu args in launcher * map_error instead of unwrap * use correct args * Update to new libafl_qemu * adapt api * borrow checker friendly join_split_trace and copy trace before deocde to prevent decoding failures * testing stuff * Set ip_filters (also) with builder * Move trace to file * Store a pt_builder in module enable the setting of filters and other pt settings * baby_bootloader target * Best bootloader ever * new builder? * use closure for qemu config from emulator builder. * better format * clippy + fmt * Fix build target Create target directory if doesn't exist * Remove filter on speculatively exec blocks since also committed blocks can have this flag * Add current ip_filters getter * Fix possibile infinite loop in trace decode * HW breakpoint + snapshot * add snapshot and exit at first objective * prefer raw pointers to slice_from_raw_parts_mut since the latter is highly unsafe and allows more potentially dangerous reordering * Add cpu option to QEMU config * Add cpu option and minor improvements * fix cargo run causing recompile * no default devices * windows clippy fix * Exclude intel_pt feature from CI as all systemmode feats * Add qemu_intel_pt_bootloader to CI * Fix NopPageFilter * Fix qemu_config * Restore HW breakpoints * Lints * return Result for hw bp set/remove * mark join_split_trace as unsafe * Put the qcow2 in a tmpfs ramdisk 10x exec/sec * Post merge fixes * Try out libipt 0.3 alpha * Try out libipt 0.3 alpha also in hook * Clippy * New libipt * Post merge fixes * Bump libipt * Drive cache None * Post merge fixes * Use SectionInfo from libipt * No slice::from_raw_parts_mut, just use raw pointer * Cache the decoder builder * Update qemu-bridge * Add qemu -append param * Move linux specific code to a mod, less #[cfg]s * Add qemu initrd config * Add qemu monitor tcp * Add not enough ip filters message * Fix wrong must_use * Prevent possible infinite loop in block decoding in debug mode * Clippy * fix CI? * Revert, keep libipt 0.3 and hw bp --------- Co-authored-by: Romain Malmain Co-authored-by: Marco Cavenati --- Cargo.toml | 2 +- .../intel_pt_baby_fuzzer/src/main.rs | 10 +- .../intel_pt_command_executor/src/main.rs | 8 +- libafl/Cargo.toml | 9 +- libafl/src/executors/hooks/intel_pt.rs | 66 +- libafl_intelpt/src/lib.rs | 942 +-------------- libafl_intelpt/src/linux.rs | 1007 +++++++++++++++++ .../tests/integration_tests_linux.rs | 12 +- .../run_integration_tests_linux_with_caps.sh | 10 + libafl_qemu/src/qemu/config.rs | 185 ++- libafl_qemu/src/qemu/mod.rs | 35 +- 11 files changed, 1260 insertions(+), 1026 deletions(-) create mode 100644 libafl_intelpt/src/linux.rs diff --git a/Cargo.toml b/Cargo.toml index 7e4ba83dd2..211709515f 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -97,7 +97,7 @@ document-features = "0.2.10" fastbloom = { version = "0.8.0", default-features = false } hashbrown = { version = "0.14.5", default-features = false } # A faster hashmap, nostd compatible libc = "0.2.159" # For (*nix) libc -libipt = "0.2.0" +libipt = "0.3.0" log = "0.4.22" meminterval = "0.4.1" mimalloc = { version = "0.1.43", default-features = false } diff --git a/fuzzers/binary_only/intel_pt_baby_fuzzer/src/main.rs b/fuzzers/binary_only/intel_pt_baby_fuzzer/src/main.rs index f0fdaed0af..d051d07518 100644 --- a/fuzzers/binary_only/intel_pt_baby_fuzzer/src/main.rs +++ b/fuzzers/binary_only/intel_pt_baby_fuzzer/src/main.rs @@ -8,7 +8,7 @@ use libafl::{ corpus::{InMemoryCorpus, OnDiskCorpus}, events::SimpleEventManager, executors::{ - hooks::intel_pt::{IntelPTHook, Section}, + hooks::intel_pt::{IntelPTHook, SectionInfo}, inprocess::GenericInProcessExecutor, ExitKind, }, @@ -100,10 +100,10 @@ pub fn main() { let sections = process_maps .iter() .filter_map(|pm| { - if pm.is_exec() && pm.filename().is_some() { - Some(Section { - file_path: pm.filename().unwrap().to_string_lossy().to_string(), - file_offset: pm.offset as u64, + if pm.is_exec() && pm.filename().is_some() && pm.inode != 0 { + Some(SectionInfo { + filename: pm.filename().unwrap().to_string_lossy().to_string(), + offset: pm.offset as u64, size: pm.size() as u64, virtual_address: pm.start() as u64, }) diff --git a/fuzzers/binary_only/intel_pt_command_executor/src/main.rs b/fuzzers/binary_only/intel_pt_command_executor/src/main.rs index f2e8f54d18..723bda0e8a 100644 --- a/fuzzers/binary_only/intel_pt_command_executor/src/main.rs +++ b/fuzzers/binary_only/intel_pt_command_executor/src/main.rs @@ -7,7 +7,7 @@ use libafl::{ events::SimpleEventManager, executors::{ command::{CommandConfigurator, PTraceCommandConfigurator}, - hooks::intel_pt::{IntelPTHook, Section}, + hooks::intel_pt::{IntelPTHook, SectionInfo}, }, feedbacks::{CrashFeedback, MaxMapFeedback}, fuzzer::{Fuzzer, StdFuzzer}, @@ -105,9 +105,9 @@ pub fn main() { .build() .unwrap(); - let sections = [Section { - file_path: target_path.to_string_lossy().to_string(), - file_offset: 0x14000, + let sections = [SectionInfo { + filename: target_path.to_string_lossy().to_string(), + offset: 0x14000, size: (*code_memory_addresses.end() - *code_memory_addresses.start() + 1) as u64, virtual_address: *code_memory_addresses.start() as u64, }]; diff --git a/libafl/Cargo.toml b/libafl/Cargo.toml index 67b109e21a..d7982829b6 100644 --- a/libafl/Cargo.toml +++ b/libafl/Cargo.toml @@ -111,13 +111,7 @@ regex = ["std", "dep:regex"] casr = ["libcasr", "std", "regex"] ## Intel Processor Trace -intel_pt = [ - "std", - "dep:libafl_intelpt", - "dep:libipt", - "dep:nix", - "dep:num_enum", -] +intel_pt = ["std", "dep:libafl_intelpt", "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"] @@ -307,7 +301,6 @@ document-features = { workspace = true, optional = true } # Optional clap = { workspace = true, optional = true } num_enum = { workspace = true, optional = true } -libipt = { workspace = true, optional = true } fastbloom = { workspace = true, optional = true } [lints] diff --git a/libafl/src/executors/hooks/intel_pt.rs b/libafl/src/executors/hooks/intel_pt.rs index 45ed92dec5..092d93346c 100644 --- a/libafl/src/executors/hooks/intel_pt.rs +++ b/libafl/src/executors/hooks/intel_pt.rs @@ -1,37 +1,24 @@ use core::fmt::Debug; -use std::{ - ptr::slice_from_raw_parts_mut, - string::{String, ToString}, -}; -use libafl_intelpt::{error_from_pt_error, IntelPT}; -use libipt::{Asid, Image, SectionCache}; +pub use libafl_intelpt::SectionInfo; +use libafl_intelpt::{Image, IntelPT}; use num_traits::SaturatingAdd; use serde::Serialize; use typed_builder::TypedBuilder; -use crate::{executors::hooks::ExecutorHook, Error}; - -/// Info of a binary's section that can be used during `Intel PT` traces decoding -#[derive(Debug, Clone, PartialEq, Eq)] -pub struct Section { - /// Path of the binary - pub file_path: String, - /// Offset of the section in the file - pub file_offset: u64, - /// Size of the section - pub size: u64, - /// Start virtual address of the section once loaded in memory - pub virtual_address: u64, -} +use crate::executors::hooks::ExecutorHook; /// Hook to enable Intel Processor Trace (PT) tracing #[derive(Debug, TypedBuilder)] pub struct IntelPTHook { #[builder(default = IntelPT::builder().build().unwrap())] intel_pt: IntelPT, - #[builder(setter(transform = |sections: &[Section]| sections_to_image(sections).unwrap()))] - image: (Image<'static>, SectionCache<'static>), + #[builder(setter(transform = |sections: &[SectionInfo]| { + let mut i = Image::new(None).unwrap(); + i.add_files_cached(sections, None).unwrap(); + i + }))] + image: Image, map_ptr: *mut T, map_len: usize, } @@ -51,9 +38,8 @@ where 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 _ = pt - .decode_traces_into_map(&mut self.image.0, slice) + .decode_traces_into_map(&mut self.image, self.map_ptr, self.map_len) .inspect_err(|e| log::warn!("Intel PT trace decoding failed: {e}")); #[cfg(feature = "intel_pt_export_raw")] { @@ -63,35 +49,3 @@ where } } } - -// It would be nice to have this as a `TryFrom>`, but Rust's orphan rule doesn't -// like this (and `TryFromIter` is not a thing atm) -fn sections_to_image( - sections: &[Section], -) -> Result<(Image<'static>, SectionCache<'static>), Error> { - let mut image_cache = SectionCache::new(Some("image_cache")).map_err(error_from_pt_error)?; - let mut image = Image::new(Some("image")).map_err(error_from_pt_error)?; - - for s in sections { - let isid = image_cache.add_file(&s.file_path, s.file_offset, s.size, s.virtual_address); - if let Err(e) = isid { - log::warn!( - "Error while caching {} {} - skipped", - s.file_path, - e.to_string() - ); - continue; - } - - if let Err(e) = image.add_cached(&mut image_cache, isid.unwrap(), Asid::default()) { - log::warn!( - "Error while adding cache to image {} {} - skipped", - s.file_path, - e.to_string() - ); - continue; - } - } - - Ok((image, image_cache)) -} diff --git a/libafl_intelpt/src/lib.rs b/libafl_intelpt/src/lib.rs index 035dc6d281..921a4b5476 100644 --- a/libafl_intelpt/src/lib.rs +++ b/libafl_intelpt/src/lib.rs @@ -12,707 +12,20 @@ #[macro_use] extern crate std; -use std::{ - borrow::ToOwned, - string::{String, ToString}, - vec::Vec, -}; #[cfg(target_os = "linux")] -use std::{ - boxed::Box, - ffi::{CStr, CString}, - fmt::Debug, - format, fs, - ops::RangeInclusive, - os::{ - fd::{AsRawFd, FromRawFd, OwnedFd}, - raw::c_void, - }, - path::Path, - ptr, slice, - sync::LazyLock, -}; +use std::fs; +use std::{borrow::ToOwned, string::String, vec::Vec}; + +use raw_cpuid::CpuId; #[cfg(target_os = "linux")] -use arbitrary_int::u4; +mod linux; #[cfg(target_os = "linux")] -use bitbybit::bitfield; -#[cfg(target_os = "linux")] -use caps::{CapSet, Capability}; -use libafl_bolts::Error; -#[cfg(target_os = "linux")] -use libafl_bolts::{hash_64_fast, ownedref::OwnedRefMut}; -use libipt::PtError; -#[cfg(target_os = "linux")] -use libipt::{ - block::BlockDecoder, AddrConfig, AddrFilter, AddrFilterBuilder, AddrRange, BlockFlags, - ConfigBuilder, Cpu, Image, PtErrorCode, Status, -}; -#[cfg(target_os = "linux")] -use num_enum::TryFromPrimitive; -#[cfg(target_os = "linux")] -use num_traits::{Euclid, SaturatingAdd}; -#[cfg(target_os = "linux")] -use perf_event_open_sys::{ - bindings::{perf_event_attr, perf_event_mmap_page, PERF_FLAG_FD_CLOEXEC}, - ioctls::{DISABLE, ENABLE, SET_FILTER}, - perf_event_open, -}; -use raw_cpuid::CpuId; +pub use linux::*; /// Size of a memory page pub const PAGE_SIZE: usize = 4096; -#[cfg(target_os = "linux")] -const PT_EVENT_PATH: &str = "/sys/bus/event_source/devices/intel_pt"; - -#[cfg(target_os = "linux")] -static NR_ADDR_FILTERS: LazyLock> = LazyLock::new(|| { - // This info is available in two different files, use the second path as fail-over - let path = format!("{PT_EVENT_PATH}/nr_addr_filters"); - let path2 = format!("{PT_EVENT_PATH}/caps/num_address_ranges"); - let err = format!("Failed to read Intel PT number of address filters from {path} and {path2}"); - - let s = fs::read_to_string(&path); - if let Ok(s) = s { - let n = s.trim().parse::(); - if let Ok(n) = n { - return Ok(n); - } - } - - let s2 = fs::read_to_string(&path2).map_err(|_| err.clone())?; - s2.trim().parse::().map_err(|_| err) -}); - -#[cfg(target_os = "linux")] -static CURRENT_CPU: LazyLock> = LazyLock::new(|| { - let cpuid = CpuId::new(); - cpuid - .get_feature_info() - .map(|fi| Cpu::intel(fi.family_id().into(), fi.model_id(), fi.stepping_id())) -}); - -#[cfg(target_os = "linux")] -static PERF_EVENT_TYPE: LazyLock> = LazyLock::new(|| { - let path = format!("{PT_EVENT_PATH}/type"); - let s = fs::read_to_string(&path) - .map_err(|_| format!("Failed to read Intel PT perf event type from {path}"))?; - s.trim() - .parse::() - .map_err(|_| format!("Failed to parse Intel PT perf event type in {path}")) -}); - -/// Intel PT mode of operation with KVM -/// -/// Check out -/// for more details -#[cfg(target_os = "linux")] -#[derive(TryFromPrimitive, Debug)] -#[repr(i32)] -pub enum KvmPTMode { - /// trace both host/guest and output to host buffer - System = 0, - /// trace host and guest simultaneously and output to their respective buffer - HostGuest = 1, -} - -/// Intel Processor Trace (PT) -#[cfg(target_os = "linux")] -#[derive(Debug)] -pub struct IntelPT { - fd: OwnedFd, - perf_buffer: *mut c_void, - perf_aux_buffer: *mut c_void, - perf_buffer_size: usize, - perf_aux_buffer_size: usize, - aux_head: *mut u64, - aux_tail: *mut u64, - previous_decode_head: u64, - ip_filters: Vec>, - #[cfg(feature = "export_raw")] - last_decode_trace: Vec, -} - -#[cfg(target_os = "linux")] -impl IntelPT { - /// Create a default builder - /// - /// Checkout [`IntelPTBuilder::default()`] for more details - #[must_use] - pub fn builder() -> IntelPTBuilder { - IntelPTBuilder::default() - } - - /// Set filters based on Instruction Pointer (IP) - /// - /// Only instructions in `filters` ranges will be traced. - pub fn set_ip_filters(&mut self, filters: &[RangeInclusive]) -> Result<(), Error> { - let str_filter = filters - .iter() - .map(|filter| { - let size = filter.end() - filter.start(); - format!("filter {:#016x}/{:#016x} ", filter.start(), size) - }) - .reduce(|acc, s| acc + &s) - .unwrap_or_default(); - - // SAFETY: CString::from_vec_unchecked is safe because no null bytes are added to str_filter - let c_str_filter = unsafe { CString::from_vec_unchecked(str_filter.into_bytes()) }; - match unsafe { SET_FILTER(self.fd.as_raw_fd(), c_str_filter.into_raw()) } { - -1 => { - let availability = match availability() { - Ok(()) => String::new(), - Err(reasons) => format!(" Possible reasons: {reasons}"), - }; - Err(Error::last_os_error(format!( - "Failed to set IP filters.{availability}" - ))) - } - 0 => { - self.ip_filters = filters.to_vec(); - Ok(()) - } - ret => Err(Error::unsupported(format!( - "Failed to set IP filter, ioctl returned unexpected value {ret}" - ))), - } - } - - /// Get the current IP filters configuration - #[must_use] - pub fn ip_filters(&self) -> Vec> { - self.ip_filters.clone() - } - - fn ip_filters_to_addr_filter(&self) -> AddrFilter { - let mut builder = AddrFilterBuilder::new(); - let mut iter = self - .ip_filters - .iter() - .map(|f| AddrRange::new(*f.start() as u64, *f.end() as u64, AddrConfig::FILTER)); - if let Some(f) = iter.next() { - builder.addr0(f); - if let Some(f) = iter.next() { - builder.addr1(f); - if let Some(f) = iter.next() { - builder.addr2(f); - if let Some(f) = iter.next() { - builder.addr3(f); - } - } - } - } - builder.finish() - } - - /// Start tracing - /// - /// Be aware that the tracing is not started on [`IntelPT`] construction. - pub fn enable_tracing(&mut self) -> Result<(), Error> { - match unsafe { ENABLE(self.fd.as_raw_fd(), 0) } { - -1 => { - let availability = match availability() { - Ok(()) => String::new(), - Err(reasons) => format!(" Possible reasons: {reasons}"), - }; - Err(Error::last_os_error(format!( - "Failed to enable tracing.{availability}" - ))) - } - 0 => Ok(()), - ret => Err(Error::unsupported(format!( - "Failed to enable tracing, ioctl returned unexpected value {ret}" - ))), - } - } - - /// Stop tracing - /// - /// This doesn't drop [`IntelPT`], the configuration will be preserved. - pub fn disable_tracing(&mut self) -> Result<(), Error> { - match unsafe { DISABLE(self.fd.as_raw_fd(), 0) } { - -1 => Err(Error::last_os_error("Failed to disable tracing")), - 0 => Ok(()), - ret => Err(Error::unsupported(format!( - "Failed to disable tracing, ioctl returned unexpected value {ret}" - ))), - } - } - - // // let read_mem = |buf: &mut [u8], addr: u64| { - // // let src = addr as *const u8; - // // let dst = buf.as_mut_ptr(); - // // let size = buf.len(); - // // unsafe { - // // ptr::copy_nonoverlapping(src, dst, size); - // // } - // // }; - // #[allow(clippy::cast_possible_wrap)] - // fn decode_with_callback( - // &mut self, - // read_memory: F, - // copy_buffer: Option<&mut Vec>, - // ) -> Result, Error> { - // self.decode( - // Some(|buff: &mut [u8], addr: u64, _: Asid| { - // debug_assert!(i32::try_from(buff.len()).is_ok()); - // read_memory(buff, addr); - // buff.len() as i32 - // }), - // None, - // copy_buffer, - // ) - // } - - /// Fill the coverage map by decoding the PT traces - /// - /// This function consumes the traces. - pub fn decode_traces_into_map( - &mut self, - image: &mut Image, - map: &mut [T], - ) -> Result<(), Error> - where - T: SaturatingAdd + From + Debug, - { - let head = unsafe { self.aux_head.read_volatile() }; - let tail = unsafe { self.aux_tail.read_volatile() }; - if head < tail { - return Err(Error::unknown( - "Intel PT: aux buffer head is behind aux tail.", - )); - } - if self.previous_decode_head < tail { - return Err(Error::unknown( - "Intel PT: aux previous head is behind aux tail.", - )); - } - let len = (head - tail) as usize; - if len >= self.perf_aux_buffer_size { - log::warn!( - "The fuzzer run filled the entire PT buffer. Consider increasing the aux buffer \ - size or refining the IP filters." - ); - } - let skip = self.previous_decode_head - tail; - - let head_wrap = wrap_aux_pointer(head, self.perf_aux_buffer_size); - let tail_wrap = wrap_aux_pointer(tail, self.perf_aux_buffer_size); - - // after reading the data_head value, user space should issue an rmb() - // https://manpages.debian.org/bookworm/manpages-dev/perf_event_open.2.en.html#data_head - smp_rmb(); - - let mut data = if head_wrap >= tail_wrap { - unsafe { - let ptr = self.perf_aux_buffer.add(tail_wrap as usize) as *mut u8; - OwnedRefMut::Ref(slice::from_raw_parts_mut(ptr, len)) - } - } else { - // Head pointer wrapped, the trace is split - 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()); - if let Some(cpu) = &*CURRENT_CPU { - config.cpu(*cpu); - } - let flags = BlockFlags::END_ON_CALL.union(BlockFlags::END_ON_JUMP); - config.flags(flags); - let mut decoder = BlockDecoder::new(&config.finish()).map_err(error_from_pt_error)?; - decoder - .set_image(Some(image)) - .map_err(error_from_pt_error)?; - - let mut previous_block_end_ip = 0; - let mut status; - 'sync: loop { - match decoder.sync_forward() { - Ok(s) => { - status = s; - Self::decode_blocks( - &mut decoder, - &mut status, - &mut previous_block_end_ip, - skip, - map, - )?; - } - Err(e) => { - if e.code() != PtErrorCode::Eos { - log::trace!("PT error in sync forward {e:?}"); - } - break 'sync; - } - } - } - - // Advance the trace pointer up to the latest sync point, otherwise next execution's trace - // might not contain a PSB packet. - decoder.sync_backward().map_err(error_from_pt_error)?; - let offset = decoder.sync_offset().map_err(error_from_pt_error)?; - unsafe { self.aux_tail.write_volatile(tail + offset) }; - self.previous_decode_head = head; - Ok(()) - } - - #[inline] - #[must_use] - 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; - - 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] - fn decode_blocks( - decoder: &mut BlockDecoder<()>, - status: &mut Status, - previous_block_end_ip: &mut u64, - skip: u64, - map: &mut [T], - ) -> Result<(), Error> - where - T: SaturatingAdd + From + Debug, - { - 'block: loop { - while status.event_pending() { - match decoder.event() { - Ok((_, s)) => { - *status = s; - } - Err(e) => { - log::trace!("PT error in event {e:?}"); - break 'block; - } - } - } - - match decoder.next() { - Ok((b, s)) => { - *status = s; - let offset = decoder.offset().map_err(error_from_pt_error)?; - - if b.ninsn() > 0 && skip < offset { - let id = hash_64_fast(*previous_block_end_ip) ^ hash_64_fast(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()) }; - *map_loc = (*map_loc).saturating_add(&1u8.into()); - - *previous_block_end_ip = b.end_ip(); - } - - if status.eos() { - break 'block; - } - } - Err(e) => { - if e.code() != PtErrorCode::Eos { - log::trace!("PT error in block next {e:?}"); - } - break 'block; - } - } - } - Ok(()) - } - - /// Get the raw trace used in the last decoding - #[cfg(feature = "export_raw")] - #[must_use] - pub fn last_decode_trace(&self) -> Vec { - self.last_decode_trace.clone() - } - - /// Dump the raw trace used in the last decoding to the file - /// /// `./traces/trace_` - #[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")] -impl Drop for IntelPT { - fn drop(&mut self) { - unsafe { - let ret = libc::munmap(self.perf_aux_buffer, self.perf_aux_buffer_size); - assert_eq!(ret, 0, "Intel PT: Failed to unmap perf aux buffer"); - let ret = libc::munmap(self.perf_buffer, self.perf_buffer_size); - assert_eq!(ret, 0, "Intel PT: Failed to unmap perf buffer"); - } - } -} - -/// Builder for [`IntelPT`] -#[cfg(target_os = "linux")] -#[derive(Debug, Clone, PartialEq)] -pub struct IntelPTBuilder { - pid: Option, - cpu: i32, - exclude_kernel: bool, - exclude_hv: bool, - inherit: bool, - perf_buffer_size: usize, - perf_aux_buffer_size: usize, - ip_filters: Vec>, -} - -#[cfg(target_os = "linux")] -impl Default for IntelPTBuilder { - /// Create a default builder for [`IntelPT`] - /// - /// The default configuration corresponds to: - /// ```rust - /// use libafl_intelpt::{IntelPTBuilder, PAGE_SIZE}; - /// 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() - /// .ip_filters(&[]); - /// assert_eq!(builder, IntelPTBuilder::default()); - /// ``` - fn default() -> Self { - Self { - pid: None, - cpu: -1, - exclude_kernel: true, - exclude_hv: true, - inherit: false, - perf_buffer_size: 128 * PAGE_SIZE + PAGE_SIZE, - perf_aux_buffer_size: 2 * 1024 * 1024, - ip_filters: Vec::new(), - } - } -} - -#[cfg(target_os = "linux")] -impl IntelPTBuilder { - /// Build the [`IntelPT`] struct - pub fn build(&self) -> Result { - self.check_config(); - let mut perf_event_attr = new_perf_event_attr_intel_pt()?; - perf_event_attr.set_exclude_kernel(self.exclude_kernel.into()); - perf_event_attr.set_exclude_hv(self.exclude_hv.into()); - perf_event_attr.set_inherit(self.inherit.into()); - - // SAFETY: perf_event_attr is properly initialized - let fd = match unsafe { - perf_event_open( - ptr::from_mut(&mut perf_event_attr), - self.pid.unwrap_or(0), - self.cpu, - -1, - PERF_FLAG_FD_CLOEXEC.into(), - ) - } { - -1 => { - let availability = match availability() { - Ok(()) => String::new(), - Err(reasons) => format!(" Possible reasons: {reasons}"), - }; - return Err(Error::last_os_error(format!( - "Failed to open Intel PT perf event.{availability}" - ))); - } - fd => { - // SAFETY: On success, perf_event_open() returns a new file descriptor. - // On error, -1 is returned, and it is checked above - unsafe { OwnedFd::from_raw_fd(fd) } - } - }; - - let perf_buffer = setup_perf_buffer(&fd, self.perf_buffer_size)?; - - // the first perf_buff page is a metadata page - let buff_metadata = perf_buffer.cast::(); - let aux_offset = unsafe { &raw mut (*buff_metadata).aux_offset }; - let aux_size = unsafe { &raw mut (*buff_metadata).aux_size }; - let data_offset = unsafe { &raw mut (*buff_metadata).data_offset }; - let data_size = unsafe { &raw mut (*buff_metadata).data_size }; - - unsafe { - aux_offset.write_volatile(next_page_aligned_addr( - data_offset.read_volatile() + data_size.read_volatile(), - )); - aux_size.write_volatile(self.perf_aux_buffer_size as u64); - } - - let perf_aux_buffer = unsafe { - setup_perf_aux_buffer(&fd, aux_size.read_volatile(), aux_offset.read_volatile())? - }; - - let aux_head = unsafe { &raw mut (*buff_metadata).aux_head }; - let aux_tail = unsafe { &raw mut (*buff_metadata).aux_tail }; - - let mut intel_pt = IntelPT { - fd, - perf_buffer, - perf_aux_buffer, - perf_buffer_size: self.perf_buffer_size, - perf_aux_buffer_size: self.perf_aux_buffer_size, - aux_head, - aux_tail, - previous_decode_head: 0, - 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 - #[inline] - fn check_config(&self) { - if self.inherit && self.cpu == -1 { - log::warn!( - "IntelPT set up on all CPUs with process inheritance enabled. This configuration \ - is not recommended and might not work as expected" - ); - } - } - - #[must_use] - /// Set the process to be traced via its `PID`. Set to `None` to trace the current process. - pub fn pid(mut self, pid: Option) -> Self { - self.pid = pid; - self - } - - #[must_use] - /// Set the CPU to be traced - /// - /// # Panics - /// - /// The function will panic if `cpu` is greater than `i32::MAX` - pub fn cpu(mut self, cpu: usize) -> Self { - self.cpu = cpu.try_into().unwrap(); - self - } - - #[must_use] - /// Trace all the CPUs - pub fn all_cpus(mut self) -> Self { - self.cpu = -1; - self - } - - #[must_use] - /// Do not trace kernel code - pub fn exclude_kernel(mut self, exclude_kernel: bool) -> Self { - self.exclude_kernel = exclude_kernel; - self - } - - #[must_use] - /// Do not trace Hypervisor code - pub fn exclude_hv(mut self, exclude_hv: bool) -> Self { - self.exclude_hv = exclude_hv; - self - } - - #[must_use] - /// Child processes are traced - pub fn inherit(mut self, inherit: bool) -> Self { - self.inherit = inherit; - self - } - - /// Set the size of the perf buffer - pub fn perf_buffer_size(mut self, perf_buffer_size: usize) -> Result { - let err = Err(Error::illegal_argument( - "IntelPT perf_buffer_size should be 1+2^n pages", - )); - if perf_buffer_size < PAGE_SIZE { - return err; - } - let (q, r) = (perf_buffer_size - PAGE_SIZE).div_rem_euclid(&PAGE_SIZE); - if !q.is_power_of_two() || r != 0 { - return err; - } - - self.perf_buffer_size = perf_buffer_size; - Ok(self) - } - - /// Set the size of the perf aux buffer (actual PT traces buffer) - pub fn perf_aux_buffer_size(mut self, perf_aux_buffer_size: usize) -> Result { - // todo:replace with is_multiple_of once stable - if perf_aux_buffer_size % PAGE_SIZE != 0 { - return Err(Error::illegal_argument( - "IntelPT perf_aux_buffer must be page aligned", - )); - } - if !perf_aux_buffer_size.is_power_of_two() { - return Err(Error::illegal_argument( - "IntelPT perf_aux_buffer must be a power of two", - )); - } - - 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]) -> Self { - self.ip_filters = filters.to_vec(); - self - } -} - -/// Perf event config for `IntelPT` -/// -/// (This is almost mapped to `IA32_RTIT_CTL MSR` by perf) -#[cfg(target_os = "linux")] -#[bitfield(u64, default = 0)] -struct PtConfig { - /// Disable call return address compression. AKA DisRETC in Intel SDM. - #[bit(11, rw)] - noretcomp: bool, - /// Indicates the frequency of PSB packets. AKA PSBFreq in Intel SDM. - #[bits(24..=27, rw)] - psb_period: u4, -} - -/// Number of address filters available on the running CPU -#[cfg(target_os = "linux")] -pub fn nr_addr_filters() -> Result { - NR_ADDR_FILTERS.clone() -} - /// Check if Intel PT is available on the current system. /// /// Returns `Ok(())` if Intel PT is available and has the features used by `LibAFL`, otherwise @@ -797,190 +110,8 @@ pub fn availability_in_qemu_kvm() -> Result<(), String> { } } -/// Convert [`PtError`] into [`Error`] -#[inline] -#[must_use] -pub fn error_from_pt_error(err: PtError) -> Error { - Error::unknown(err.to_string()) -} - -#[cfg(target_os = "linux")] -fn availability_in_linux() -> Result<(), String> { - let mut reasons = Vec::new(); - match linux_version() { - // https://docs.rs/perf-event-open-sys/4.0.0/perf_event_open_sys/#kernel-versions - Ok(ver) if ver >= (5, 19, 4) => {} - Ok((major, minor, patch)) => reasons.push(format!( - "Kernel version {major}.{minor}.{patch} is older than 5.19.4 and might not work." - )), - Err(()) => reasons.push("Failed to retrieve kernel version".to_owned()), - } - - if let Err(e) = &*PERF_EVENT_TYPE { - reasons.push(e.clone()); - } - - if let Err(e) = &*NR_ADDR_FILTERS { - reasons.push(e.clone()); - } - - // official way of knowing if perf_event_open() support is enabled - // https://man7.org/linux/man-pages/man2/perf_event_open.2.html - let perf_event_support_path = "/proc/sys/kernel/perf_event_paranoid"; - if !Path::new(perf_event_support_path).exists() { - reasons.push(format!( - "perf_event_open() support is not enabled: {perf_event_support_path} not found" - )); - } - - // TODO check also the value of perf_event_paranoid, check which values are required by pt - // https://www.kernel.org/doc/Documentation/sysctl/kernel.txt - // also, looks like it is distribution dependent - // https://askubuntu.com/questions/1400874/what-does-perf-paranoia-level-four-do - // CAP_SYS_ADMIN might make this check useless - - match caps::read(None, CapSet::Permitted) { - Ok(current_capabilities) => { - let required_caps = [ - Capability::CAP_IPC_LOCK, - Capability::CAP_SYS_PTRACE, - Capability::CAP_SYS_ADMIN, // TODO: CAP_PERFMON doesn't look to be enough!? - Capability::CAP_SYSLOG, - ]; - - for rc in required_caps { - if !current_capabilities.contains(&rc) { - reasons.push(format!("Required capability {rc} missing")); - } - } - } - Err(e) => reasons.push(format!("Failed to read linux capabilities: {e}")), - } - - if reasons.is_empty() { - Ok(()) - } else { - Err(reasons.join("; ")) - } -} - -#[cfg(target_os = "linux")] -fn new_perf_event_attr_intel_pt() -> Result { - let type_ = match &*PERF_EVENT_TYPE { - Ok(t) => Ok(*t), - Err(e) => Err(Error::unsupported(e.clone())), - }?; - let config = PtConfig::builder() - .with_noretcomp(true) - .with_psb_period(u4::new(0)) - .build() - .raw_value; - - let mut attr = perf_event_attr { - size: size_of::() as u32, - type_, - config, - ..Default::default() - }; - - // Do not enable tracing as soon as the perf_event_open syscall is issued - attr.set_disabled(true.into()); - - Ok(attr) -} - -#[cfg(target_os = "linux")] -fn setup_perf_buffer(fd: &OwnedFd, perf_buffer_size: usize) -> Result<*mut c_void, Error> { - match unsafe { - libc::mmap( - ptr::null_mut(), - perf_buffer_size, - libc::PROT_READ | libc::PROT_WRITE, - libc::MAP_SHARED, - fd.as_raw_fd(), - 0, - ) - } { - libc::MAP_FAILED => Err(Error::last_os_error("IntelPT: Failed to mmap perf buffer")), - mmap_addr => Ok(mmap_addr), - } -} - -#[cfg(target_os = "linux")] -fn setup_perf_aux_buffer(fd: &OwnedFd, size: u64, offset: u64) -> Result<*mut c_void, Error> { - match unsafe { - libc::mmap( - ptr::null_mut(), - size as usize, - // PROT_WRITE sets PT to stop when the buffer is full - libc::PROT_READ | libc::PROT_WRITE, - libc::MAP_SHARED, - fd.as_raw_fd(), - i64::try_from(offset)?, - ) - } { - libc::MAP_FAILED => Err(Error::last_os_error( - "IntelPT: Failed to mmap perf aux buffer", - )), - mmap_addr => Ok(mmap_addr), - } -} - -#[cfg(target_os = "linux")] -fn linux_version() -> Result<(usize, usize, usize), ()> { - let mut uname_data = libc::utsname { - sysname: [0; 65], - nodename: [0; 65], - release: [0; 65], - version: [0; 65], - machine: [0; 65], - domainname: [0; 65], - }; - - if unsafe { libc::uname(&mut uname_data) } != 0 { - return Err(()); - } - - let release = unsafe { CStr::from_ptr(uname_data.release.as_ptr()) }; - let mut parts = release - .to_bytes() - .split(|&c| c == b'.' || c == b'-') - .take(3) - .map(|s| String::from_utf8_lossy(s).parse::()); - if let (Some(Ok(major)), Some(Ok(minor)), Some(Ok(patch))) = - (parts.next(), parts.next(), parts.next()) - { - Ok((major, minor, patch)) - } else { - Err(()) - } -} - -#[cfg(target_os = "linux")] -#[inline] -const fn next_page_aligned_addr(address: u64) -> u64 { - (address + PAGE_SIZE as u64 - 1) & !(PAGE_SIZE as u64 - 1) -} - -#[cfg(target_os = "linux")] -#[inline] -fn smp_rmb() { - // SAFETY: just a memory barrier - unsafe { - core::arch::asm!("lfence", options(nostack, preserves_flags)); - } -} - -#[cfg(target_os = "linux")] -#[inline] -const fn wrap_aux_pointer(ptr: u64, perf_aux_buffer_size: usize) -> u64 { - ptr & (perf_aux_buffer_size as u64 - 1) -} - #[cfg(test)] mod test { - #[cfg(target_os = "linux")] - use arbitrary_int::Number; use static_assertions::assert_eq_size; use super::*; @@ -1005,65 +136,4 @@ mod test { Err(e) => println!("❌\tReasons: {e}"), } } - - #[test] - #[cfg(target_os = "linux")] - fn intel_pt_builder_default_values_are_valid() { - let default = IntelPT::builder(); - IntelPT::builder() - .perf_buffer_size(default.perf_buffer_size) - .unwrap(); - IntelPT::builder() - .perf_aux_buffer_size(default.perf_aux_buffer_size) - .unwrap(); - } - - #[test] - #[cfg(target_os = "linux")] - fn intel_pt_pt_config_noretcomp_format() { - let ptconfig_noretcomp = PtConfig::DEFAULT.with_noretcomp(true).raw_value; - let path = format!("{PT_EVENT_PATH}/format/noretcomp"); - let s = fs::read_to_string(&path).expect("Failed to read Intel PT config noretcomp format"); - assert!( - s.starts_with("config:"), - "Unexpected Intel PT config noretcomp format" - ); - let bit = s["config:".len()..] - .trim() - .parse::() - .expect("Failed to parse Intel PT config noretcomp format"); - assert_eq!( - ptconfig_noretcomp, - 0b1 << bit, - "Unexpected Intel PT config noretcomp format" - ); - } - - #[test] - #[cfg(target_os = "linux")] - fn intel_pt_pt_config_psb_period_format() { - let ptconfig_psb_period = PtConfig::DEFAULT.with_psb_period(u4::MAX).raw_value; - let path = format!("{PT_EVENT_PATH}/format/psb_period"); - let s = - fs::read_to_string(&path).expect("Failed to read Intel PT config psb_period format"); - assert!( - s.starts_with("config:"), - "Unexpected Intel PT config psb_period format" - ); - let from = s["config:".len().."config:".len() + 2] - .parse::() - .expect("Failed to parse Intel PT config psb_period format"); - let to = s["config:".len() + 3..] - .trim() - .parse::() - .expect("Failed to parse Intel PT config psb_period format"); - let mut format = 0; - for bit in from..=to { - format |= 0b1 << bit; - } - assert_eq!( - ptconfig_psb_period, format, - "Unexpected Intel PT config psb_period format" - ); - } } diff --git a/libafl_intelpt/src/linux.rs b/libafl_intelpt/src/linux.rs new file mode 100644 index 0000000000..5dd823e90d --- /dev/null +++ b/libafl_intelpt/src/linux.rs @@ -0,0 +1,1007 @@ +use std::{ + borrow::ToOwned, + boxed::Box, + ffi::{CStr, CString}, + fmt::Debug, + format, fs, + ops::RangeInclusive, + os::{ + fd::{AsRawFd, FromRawFd, OwnedFd}, + raw::c_void, + }, + path::Path, + ptr, + string::{String, ToString}, + sync::LazyLock, + vec::Vec, +}; + +use arbitrary_int::u4; +use bitbybit::bitfield; +use caps::{CapSet, Capability}; +use libafl_bolts::{hash_64_fast, Error}; +pub use libipt::{ + asid::Asid, + image::{Image, SectionCache, SectionInfo}, + status::Status, +}; +use libipt::{ + block::BlockDecoder, + enc_dec_builder::{ + AddrFilterRange, AddrFilterType, AddrFilters, AddrFiltersBuilder, Cpu, + EncoderDecoderBuilder, + }, + error::{PtError, PtErrorCode}, +}; +use num_enum::TryFromPrimitive; +use num_traits::{Euclid, SaturatingAdd}; +use perf_event_open_sys::{ + bindings::{perf_event_attr, perf_event_mmap_page, PERF_FLAG_FD_CLOEXEC}, + ioctls::{DISABLE, ENABLE, SET_FILTER}, + perf_event_open, +}; +use raw_cpuid::CpuId; + +use super::{availability, PAGE_SIZE}; + +const PT_EVENT_PATH: &str = "/sys/bus/event_source/devices/intel_pt"; + +static NR_ADDR_FILTERS: LazyLock> = LazyLock::new(|| { + // This info is available in two different files, use the second path as fail-over + let path = format!("{PT_EVENT_PATH}/nr_addr_filters"); + let path2 = format!("{PT_EVENT_PATH}/caps/num_address_ranges"); + let err = format!("Failed to read Intel PT number of address filters from {path} and {path2}"); + + let s = fs::read_to_string(&path); + if let Ok(s) = s { + let n = s.trim().parse::(); + if let Ok(n) = n { + return Ok(n); + } + } + + let s2 = fs::read_to_string(&path2).map_err(|_| err.clone())?; + s2.trim().parse::().map_err(|_| err) +}); + +static PERF_EVENT_TYPE: LazyLock> = LazyLock::new(|| { + let path = format!("{PT_EVENT_PATH}/type"); + let s = fs::read_to_string(&path) + .map_err(|_| format!("Failed to read Intel PT perf event type from {path}"))?; + s.trim() + .parse::() + .map_err(|_| format!("Failed to parse Intel PT perf event type in {path}")) +}); + +/// Intel PT mode of operation with KVM +/// +/// Check out +/// for more details +#[derive(TryFromPrimitive, Debug)] +#[repr(i32)] +pub enum KvmPTMode { + /// trace both host/guest and output to host buffer + System = 0, + /// trace host and guest simultaneously and output to their respective buffer + HostGuest = 1, +} + +/// Intel Processor Trace (PT) +#[derive(Debug)] +pub struct IntelPT { + fd: OwnedFd, + perf_buffer: *mut c_void, + perf_aux_buffer: *mut c_void, + perf_buffer_size: usize, + perf_aux_buffer_size: usize, + aux_head: *mut u64, + aux_tail: *mut u64, + previous_decode_head: u64, + ip_filters: Vec>, + // The lifetime of BlockDecoder<'a> is irrelevant during the building phase. + decoder_builder: EncoderDecoderBuilder>, + #[cfg(feature = "export_raw")] + last_decode_trace: Vec, +} + +impl IntelPT { + /// Create a default builder + /// + /// Checkout [`IntelPTBuilder::default()`] for more details + #[must_use] + pub fn builder() -> IntelPTBuilder { + IntelPTBuilder::default() + } + + /// Set filters based on Instruction Pointer (IP) + /// + /// Only instructions in `filters` ranges will be traced. + pub fn set_ip_filters(&mut self, filters: &[RangeInclusive]) -> Result<(), Error> { + let str_filter = filters + .iter() + .map(|filter| { + let size = filter.end() - filter.start(); + format!("filter {:#016x}/{:#016x} ", filter.start(), size) + }) + .reduce(|acc, s| acc + &s) + .unwrap_or_default(); + + // SAFETY: CString::from_vec_unchecked is safe because no null bytes are added to str_filter + let c_str_filter = unsafe { CString::from_vec_unchecked(str_filter.into_bytes()) }; + match unsafe { SET_FILTER(self.fd.as_raw_fd(), c_str_filter.into_raw()) } { + -1 => { + let availability = match availability() { + Ok(()) => String::new(), + Err(reasons) => format!(" Possible reasons: {reasons}"), + }; + let not_enough_filters = if filters.len() > nr_addr_filters().unwrap_or(0) as usize + { + format!( + " Not enough filters, trying to set {} filters while {} available.", + filters.len(), + nr_addr_filters().unwrap_or(0) + ) + } else { + String::new() + }; + Err(Error::last_os_error(format!( + "Failed to set IP filters.{not_enough_filters}{availability}" + ))) + } + 0 => { + self.ip_filters = filters.to_vec(); + Ok(()) + } + ret => Err(Error::unsupported(format!( + "Failed to set IP filter, ioctl returned unexpected value {ret}" + ))), + } + } + + /// Get the current IP filters configuration + #[must_use] + pub fn ip_filters(&self) -> Vec> { + self.ip_filters.clone() + } + + fn ip_filters_to_addr_filter(&self) -> AddrFilters { + let mut builder = AddrFiltersBuilder::new(); + let mut iter = self.ip_filters.iter().map(|f| { + AddrFilterRange::new(*f.start() as u64, *f.end() as u64, AddrFilterType::FILTER) + }); + if let Some(f) = iter.next() { + builder.addr0(f); + if let Some(f) = iter.next() { + builder.addr1(f); + if let Some(f) = iter.next() { + builder.addr2(f); + if let Some(f) = iter.next() { + builder.addr3(f); + } + } + } + } + builder.build() + } + + /// Start tracing + /// + /// Be aware that the tracing is not started on [`IntelPT`] construction. + pub fn enable_tracing(&mut self) -> Result<(), Error> { + match unsafe { ENABLE(self.fd.as_raw_fd(), 0) } { + -1 => { + let availability = match availability() { + Ok(()) => String::new(), + Err(reasons) => format!(" Possible reasons: {reasons}"), + }; + Err(Error::last_os_error(format!( + "Failed to enable tracing.{availability}" + ))) + } + 0 => Ok(()), + ret => Err(Error::unsupported(format!( + "Failed to enable tracing, ioctl returned unexpected value {ret}" + ))), + } + } + + /// Stop tracing + /// + /// This doesn't drop [`IntelPT`], the configuration will be preserved. + pub fn disable_tracing(&mut self) -> Result<(), Error> { + match unsafe { DISABLE(self.fd.as_raw_fd(), 0) } { + -1 => Err(Error::last_os_error("Failed to disable tracing")), + 0 => Ok(()), + ret => Err(Error::unsupported(format!( + "Failed to disable tracing, ioctl returned unexpected value {ret}" + ))), + } + } + + // /// Fill the coverage map by decoding the PT traces and reading target memory through `read_mem` + // /// + // /// This function consumes the traces. + // /// + // /// # Example + // /// + // /// An example `read_mem` callback function for the (inprocess) `intel_pt_babyfuzzer` could be: + // /// ``` + // /// let read_mem = |buf: &mut [u8], addr: u64| { + // /// let src = addr as *const u8; + // /// let dst = buf.as_mut_ptr(); + // /// let size = buf.len(); + // /// unsafe { + // /// core::ptr::copy_nonoverlapping(src, dst, size); + // /// } + // /// }; + // /// ``` + // #[allow(clippy::cast_possible_wrap)] + // pub fn decode_with_callback(&mut self, read_memory: F, map: &mut [T]) -> Result<(), Error> + // where + // F: Fn(&mut [u8], u64), + // T: SaturatingAdd + From + Debug, + // { + // self.decode_traces_into_map_common( + // None, + // Some(|buff: &mut [u8], addr: u64, _: Asid| { + // debug_assert!(i32::try_from(buff.len()).is_ok()); + // read_memory(buff, addr); + // buff.len() as i32 + // }), + // map, + // ) + // } + + /// Fill the coverage map by decoding the PT traces + /// + /// This function consumes the traces. + pub fn decode_traces_into_map( + &mut self, + image: &mut Image, + map_ptr: *mut T, + map_len: usize, + ) -> Result<(), Error> + where + T: SaturatingAdd + From + Debug, + { + self.decode_traces_into_map_common( + Some(image), + None:: i32>, + map_ptr, + map_len, + ) + } + + fn decode_traces_into_map_common( + &mut self, + image: Option<&mut Image>, + read_memory: Option, + map_ptr: *mut T, + map_len: usize, + ) -> Result<(), Error> + where + F: Fn(&mut [u8], u64, Asid) -> i32, + T: SaturatingAdd + From + Debug, + { + let head = unsafe { self.aux_head.read_volatile() }; + let tail = unsafe { self.aux_tail.read_volatile() }; + if head < tail { + return Err(Error::unknown( + "Intel PT: aux buffer head is behind aux tail.", + )); + } + if self.previous_decode_head < tail { + return Err(Error::unknown( + "Intel PT: aux previous head is behind aux tail.", + )); + } + let len = (head - tail) as usize; + if len >= self.perf_aux_buffer_size { + log::warn!( + "The fuzzer run filled the entire PT buffer. Consider increasing the aux buffer \ + size or refining the IP filters." + ); + } + let skip = self.previous_decode_head - tail; + + let head_wrap = wrap_aux_pointer(head, self.perf_aux_buffer_size); + let tail_wrap = wrap_aux_pointer(tail, self.perf_aux_buffer_size); + + // after reading the data_head value, user space should issue an rmb() + // https://manpages.debian.org/bookworm/manpages-dev/perf_event_open.2.en.html#data_head + smp_rmb(); + + let (data_ptr, _owned_data) = unsafe { + if head_wrap >= tail_wrap { + let ptr = self.perf_aux_buffer.add(tail_wrap as usize).cast::(); + (ptr, None) + } else { + // Head pointer wrapped, the trace is split + let mut owned_data = self.join_split_trace(head_wrap, tail_wrap); + (owned_data.as_mut_ptr(), Some(owned_data)) + } + }; + #[cfg(feature = "export_raw")] + { + self.last_decode_trace = Vec::with_capacity(len); + unsafe { + ptr::copy_nonoverlapping(data_ptr, self.last_decode_trace.as_mut_ptr(), len); + self.last_decode_trace.set_len(len); + } + } + let builder = unsafe { self.decoder_builder.clone().buffer_from_raw(data_ptr, len) } + .filter(self.ip_filters_to_addr_filter()); + + let mut decoder = builder.build().map_err(error_from_pt_error)?; + decoder.set_image(image).map_err(error_from_pt_error)?; + if let Some(rm) = read_memory { + decoder.image().set_callback(Some(rm)); + } + + let mut previous_block_end_ip = 0; + let mut status; + 'sync: loop { + match decoder.sync_forward() { + Ok(s) => { + status = s; + Self::decode_blocks( + &mut decoder, + &mut status, + &mut previous_block_end_ip, + skip, + map_ptr, + map_len, + )?; + } + Err(e) => { + if e.code() != PtErrorCode::Eos { + log::info!("PT error in sync forward {e:?}"); + } + break 'sync; + } + } + } + + // Advance the trace pointer up to the latest sync point, otherwise next execution's trace + // might not contain a PSB packet. + decoder.sync_backward().map_err(error_from_pt_error)?; + let offset = decoder.sync_offset().map_err(error_from_pt_error)?; + unsafe { self.aux_tail.write_volatile(tail + offset) }; + self.previous_decode_head = head; + Ok(()) + } + + /// # Safety: + /// + /// The caller must ensure that `head_wrap` and `tail_wrap` have been wrapped properly, in other + /// words, this ensures that `head_wrap` and `tail_wrap` are < `self.perf_aux_buffer_size`. + #[inline] + #[must_use] + unsafe fn join_split_trace(&self, head_wrap: u64, tail_wrap: u64) -> Box<[u8]> { + // this function is unsafe, but let's make it safe when compiling in debug mode + debug_assert!(head_wrap < self.perf_aux_buffer_size as u64); + debug_assert!(tail_wrap < self.perf_aux_buffer_size as u64); + + // SAFETY: tail_wrap is guaranteed to be < `self.perf_aux_buffer_size` from the fn safety + // preconditions + let first_ptr = unsafe { self.perf_aux_buffer.add(tail_wrap as usize) }.cast::(); + let first_len = self.perf_aux_buffer_size - tail_wrap as usize; + + let second_ptr = self.perf_aux_buffer.cast::(); + let second_len = head_wrap as usize; + + let mut data = Box::<[u8]>::new_uninit_slice(first_len + second_len); + unsafe { + ptr::copy_nonoverlapping(first_ptr, data.as_mut_ptr().cast(), first_len); + ptr::copy_nonoverlapping( + second_ptr, + data.as_mut_ptr().add(first_len).cast(), + second_len, + ); + data.assume_init() + } + } + + #[inline] + fn decode_blocks( + decoder: &mut BlockDecoder, + status: &mut Status, + previous_block_end_ip: &mut u64, + skip: u64, + map_ptr: *mut T, + map_len: usize, + ) -> Result<(), Error> + where + T: SaturatingAdd + From + Debug, + { + #[cfg(debug_assertions)] + let mut trace_entry_iters: (u64, u64) = (0, 0); + + 'block: loop { + let offset = decoder.offset().map_err(error_from_pt_error)?; + #[cfg(debug_assertions)] + { + if trace_entry_iters.0 == offset { + trace_entry_iters.1 += 1; + if trace_entry_iters.1 > 1000 { + log::warn!("Decoder got stuck at trace offset {offset:x}. Make sure the decoder Image has the right content and offsets."); + break 'block; + } + } else { + trace_entry_iters = (offset, 0); + } + } + + while status.event_pending() { + match decoder.event() { + Ok((_, s)) => { + *status = s; + } + Err(e) => { + log::info!("PT error in event {e:?}"); + break 'block; + } + } + } + + match decoder.decode_next() { + Ok((b, s)) => { + *status = s; + + if b.ninsn() > 0 && skip < offset { + let id = hash_64_fast(*previous_block_end_ip) ^ hash_64_fast(b.ip()); + // SAFETY: the index is < map_len since the modulo operation is applied + unsafe { + let map_loc = map_ptr.add(id as usize % map_len); + *map_loc = (*map_loc).saturating_add(&1u8.into()); + } + *previous_block_end_ip = b.end_ip(); + } + + if status.eos() { + break 'block; + } + } + Err(e) => { + if e.code() != PtErrorCode::Eos { + let offset = decoder.offset().map_err(error_from_pt_error)?; + log::info!( + "PT error in block next {e:?} trace offset {offset:x} last decoded block end {:x}", + previous_block_end_ip + ); + } + break 'block; + } + } + } + Ok(()) + } + + /// Get the raw trace used in the last decoding + #[cfg(feature = "export_raw")] + #[must_use] + pub fn last_decode_trace(&self) -> Vec { + self.last_decode_trace.clone() + } + + /// Dump the raw trace used in the last decoding to the file + /// /// `./traces/trace_` + #[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(()) + } +} + +impl Drop for IntelPT { + fn drop(&mut self) { + unsafe { + let ret = libc::munmap(self.perf_aux_buffer, self.perf_aux_buffer_size); + assert_eq!(ret, 0, "Intel PT: Failed to unmap perf aux buffer"); + let ret = libc::munmap(self.perf_buffer, self.perf_buffer_size); + assert_eq!(ret, 0, "Intel PT: Failed to unmap perf buffer"); + } + } +} + +/// Builder for [`IntelPT`] +#[derive(Debug, Clone, PartialEq)] +pub struct IntelPTBuilder { + pid: Option, + cpu: i32, + exclude_kernel: bool, + exclude_hv: bool, + inherit: bool, + perf_buffer_size: usize, + perf_aux_buffer_size: usize, + ip_filters: Vec>, +} + +impl Default for IntelPTBuilder { + /// Create a default builder for [`IntelPT`] + /// + /// The default configuration corresponds to: + /// ```rust + /// use libafl_intelpt::{IntelPTBuilder, PAGE_SIZE}; + /// 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() + /// .ip_filters(&[]); + /// assert_eq!(builder, IntelPTBuilder::default()); + /// ``` + fn default() -> Self { + Self { + pid: None, + cpu: -1, + exclude_kernel: true, + exclude_hv: true, + inherit: false, + perf_buffer_size: 128 * PAGE_SIZE + PAGE_SIZE, + perf_aux_buffer_size: 2 * 1024 * 1024, + ip_filters: Vec::new(), + } + } +} + +impl IntelPTBuilder { + /// Build the [`IntelPT`] struct + pub fn build(&self) -> Result { + self.check_config(); + let mut perf_event_attr = new_perf_event_attr_intel_pt()?; + perf_event_attr.set_exclude_kernel(self.exclude_kernel.into()); + perf_event_attr.set_exclude_hv(self.exclude_hv.into()); + perf_event_attr.set_inherit(self.inherit.into()); + + // SAFETY: perf_event_attr is properly initialized + let fd = match unsafe { + perf_event_open( + ptr::from_mut(&mut perf_event_attr), + self.pid.unwrap_or(0), + self.cpu, + -1, + PERF_FLAG_FD_CLOEXEC.into(), + ) + } { + -1 => { + let availability = match availability() { + Ok(()) => String::new(), + Err(reasons) => format!(" Possible reasons: {reasons}"), + }; + return Err(Error::last_os_error(format!( + "Failed to open Intel PT perf event.{availability}" + ))); + } + fd => { + // SAFETY: On success, perf_event_open() returns a new file descriptor. + // On error, -1 is returned, and it is checked above + unsafe { OwnedFd::from_raw_fd(fd) } + } + }; + + let perf_buffer = setup_perf_buffer(&fd, self.perf_buffer_size)?; + + // the first perf_buff page is a metadata page + let buff_metadata = perf_buffer.cast::(); + let aux_offset = unsafe { &raw mut (*buff_metadata).aux_offset }; + let aux_size = unsafe { &raw mut (*buff_metadata).aux_size }; + let data_offset = unsafe { &raw mut (*buff_metadata).data_offset }; + let data_size = unsafe { &raw mut (*buff_metadata).data_size }; + + unsafe { + aux_offset.write_volatile(next_page_aligned_addr( + data_offset.read_volatile() + data_size.read_volatile(), + )); + aux_size.write_volatile(self.perf_aux_buffer_size as u64); + } + + let perf_aux_buffer = unsafe { + setup_perf_aux_buffer(&fd, aux_size.read_volatile(), aux_offset.read_volatile())? + }; + + let aux_head = unsafe { &raw mut (*buff_metadata).aux_head }; + let aux_tail = unsafe { &raw mut (*buff_metadata).aux_tail }; + + let mut decoder_builder = EncoderDecoderBuilder::new() + .set_end_on_call(true) + .set_end_on_jump(true); + if let Some(cpu) = current_cpu() { + decoder_builder = decoder_builder.cpu(cpu); + } + + let mut intel_pt = IntelPT { + fd, + perf_buffer, + perf_aux_buffer, + perf_buffer_size: self.perf_buffer_size, + perf_aux_buffer_size: self.perf_aux_buffer_size, + aux_head, + aux_tail, + previous_decode_head: 0, + ip_filters: Vec::with_capacity(*NR_ADDR_FILTERS.as_ref().unwrap_or(&0) as usize), + decoder_builder, + #[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 + #[inline] + fn check_config(&self) { + if self.inherit && self.cpu == -1 { + log::warn!( + "IntelPT set up on all CPUs with process inheritance enabled. This configuration \ + is not recommended and might not work as expected" + ); + } + } + + #[must_use] + /// Set the process to be traced via its `PID`. Set to `None` to trace the current process. + pub fn pid(mut self, pid: Option) -> Self { + self.pid = pid; + self + } + + #[must_use] + /// Set the CPU to be traced + /// + /// # Panics + /// + /// The function will panic if `cpu` is greater than `i32::MAX` + pub fn cpu(mut self, cpu: usize) -> Self { + self.cpu = cpu.try_into().unwrap(); + self + } + + #[must_use] + /// Trace all the CPUs + pub fn all_cpus(mut self) -> Self { + self.cpu = -1; + self + } + + #[must_use] + /// Do not trace kernel code + pub fn exclude_kernel(mut self, exclude_kernel: bool) -> Self { + self.exclude_kernel = exclude_kernel; + self + } + + #[must_use] + /// Do not trace Hypervisor code + pub fn exclude_hv(mut self, exclude_hv: bool) -> Self { + self.exclude_hv = exclude_hv; + self + } + + #[must_use] + /// Child processes are traced + pub fn inherit(mut self, inherit: bool) -> Self { + self.inherit = inherit; + self + } + + /// Set the size of the perf buffer + pub fn perf_buffer_size(mut self, perf_buffer_size: usize) -> Result { + let err = Err(Error::illegal_argument( + "IntelPT perf_buffer_size should be 1+2^n pages", + )); + if perf_buffer_size < PAGE_SIZE { + return err; + } + let (q, r) = (perf_buffer_size - PAGE_SIZE).div_rem_euclid(&PAGE_SIZE); + if !q.is_power_of_two() || r != 0 { + return err; + } + + self.perf_buffer_size = perf_buffer_size; + Ok(self) + } + + /// Set the size of the perf aux buffer (actual PT traces buffer) + pub fn perf_aux_buffer_size(mut self, perf_aux_buffer_size: usize) -> Result { + // todo:replace with is_multiple_of once stable + if perf_aux_buffer_size % PAGE_SIZE != 0 { + return Err(Error::illegal_argument( + "IntelPT perf_aux_buffer must be page aligned", + )); + } + if !perf_aux_buffer_size.is_power_of_two() { + return Err(Error::illegal_argument( + "IntelPT perf_aux_buffer must be a power of two", + )); + } + + 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]) -> Self { + self.ip_filters = filters.to_vec(); + self + } +} + +/// Perf event config for `IntelPT` +/// +/// (This is almost mapped to `IA32_RTIT_CTL MSR` by perf) +#[bitfield(u64, default = 0)] +struct PtConfig { + /// Disable call return address compression. AKA DisRETC in Intel SDM. + #[bit(11, rw)] + noretcomp: bool, + /// Indicates the frequency of PSB packets. AKA PSBFreq in Intel SDM. + #[bits(24..=27, rw)] + psb_period: u4, +} + +/// Number of address filters available on the running CPU +pub fn nr_addr_filters() -> Result { + NR_ADDR_FILTERS.clone() +} + +/// Convert [`PtError`] into [`Error`] +#[inline] +#[must_use] +pub fn error_from_pt_error(err: PtError) -> Error { + Error::unknown(err.to_string()) +} + +pub(crate) fn availability_in_linux() -> Result<(), String> { + let mut reasons = Vec::new(); + match linux_version() { + // https://docs.rs/perf-event-open-sys/4.0.0/perf_event_open_sys/#kernel-versions + Ok(ver) if ver >= (5, 19, 4) => {} + Ok((major, minor, patch)) => reasons.push(format!( + "Kernel version {major}.{minor}.{patch} is older than 5.19.4 and might not work." + )), + Err(()) => reasons.push("Failed to retrieve kernel version".to_owned()), + } + + if let Err(e) = &*PERF_EVENT_TYPE { + reasons.push(e.clone()); + } + + if let Err(e) = &*NR_ADDR_FILTERS { + reasons.push(e.clone()); + } + + // official way of knowing if perf_event_open() support is enabled + // https://man7.org/linux/man-pages/man2/perf_event_open.2.html + let perf_event_support_path = "/proc/sys/kernel/perf_event_paranoid"; + if !Path::new(perf_event_support_path).exists() { + reasons.push(format!( + "perf_event_open() support is not enabled: {perf_event_support_path} not found" + )); + } + + // TODO check also the value of perf_event_paranoid, check which values are required by pt + // https://www.kernel.org/doc/Documentation/sysctl/kernel.txt + // also, looks like it is distribution dependent + // https://askubuntu.com/questions/1400874/what-does-perf-paranoia-level-four-do + // CAP_SYS_ADMIN might make this check useless + + match caps::read(None, CapSet::Permitted) { + Ok(current_capabilities) => { + let required_caps = [ + Capability::CAP_IPC_LOCK, + Capability::CAP_SYS_PTRACE, + Capability::CAP_SYS_ADMIN, // TODO: CAP_PERFMON doesn't look to be enough!? + Capability::CAP_SYSLOG, + ]; + + for rc in required_caps { + if !current_capabilities.contains(&rc) { + reasons.push(format!("Required capability {rc} missing")); + } + } + } + Err(e) => reasons.push(format!("Failed to read linux capabilities: {e}")), + } + + if reasons.is_empty() { + Ok(()) + } else { + Err(reasons.join("; ")) + } +} + +fn new_perf_event_attr_intel_pt() -> Result { + let type_ = match &*PERF_EVENT_TYPE { + Ok(t) => Ok(*t), + Err(e) => Err(Error::unsupported(e.clone())), + }?; + let config = PtConfig::builder() + .with_noretcomp(true) + .with_psb_period(u4::new(0)) + .build() + .raw_value; + + let mut attr = perf_event_attr { + size: size_of::() as u32, + type_, + config, + ..Default::default() + }; + + // Do not enable tracing as soon as the perf_event_open syscall is issued + attr.set_disabled(true.into()); + + Ok(attr) +} + +fn setup_perf_buffer(fd: &OwnedFd, perf_buffer_size: usize) -> Result<*mut c_void, Error> { + match unsafe { + libc::mmap( + ptr::null_mut(), + perf_buffer_size, + libc::PROT_READ | libc::PROT_WRITE, + libc::MAP_SHARED, + fd.as_raw_fd(), + 0, + ) + } { + libc::MAP_FAILED => Err(Error::last_os_error("IntelPT: Failed to mmap perf buffer")), + mmap_addr => Ok(mmap_addr), + } +} + +fn setup_perf_aux_buffer(fd: &OwnedFd, size: u64, offset: u64) -> Result<*mut c_void, Error> { + match unsafe { + libc::mmap( + ptr::null_mut(), + size as usize, + // PROT_WRITE sets PT to stop when the buffer is full + libc::PROT_READ | libc::PROT_WRITE, + libc::MAP_SHARED, + fd.as_raw_fd(), + i64::try_from(offset)?, + ) + } { + libc::MAP_FAILED => Err(Error::last_os_error( + "IntelPT: Failed to mmap perf aux buffer", + )), + mmap_addr => Ok(mmap_addr), + } +} + +fn linux_version() -> Result<(usize, usize, usize), ()> { + let mut uname_data = libc::utsname { + sysname: [0; 65], + nodename: [0; 65], + release: [0; 65], + version: [0; 65], + machine: [0; 65], + domainname: [0; 65], + }; + + if unsafe { libc::uname(&mut uname_data) } != 0 { + return Err(()); + } + + let release = unsafe { CStr::from_ptr(uname_data.release.as_ptr()) }; + let mut parts = release + .to_bytes() + .split(|&c| c == b'.' || c == b'-') + .take(3) + .map(|s| String::from_utf8_lossy(s).parse::()); + if let (Some(Ok(major)), Some(Ok(minor)), Some(Ok(patch))) = + (parts.next(), parts.next(), parts.next()) + { + Ok((major, minor, patch)) + } else { + Err(()) + } +} + +#[inline] +const fn next_page_aligned_addr(address: u64) -> u64 { + (address + PAGE_SIZE as u64 - 1) & !(PAGE_SIZE as u64 - 1) +} + +#[inline] +fn smp_rmb() { + // SAFETY: just a memory barrier + unsafe { + core::arch::asm!("lfence", options(nostack, preserves_flags)); + } +} + +#[inline] +const fn wrap_aux_pointer(ptr: u64, perf_aux_buffer_size: usize) -> u64 { + ptr & (perf_aux_buffer_size as u64 - 1) +} + +#[inline] +fn current_cpu() -> Option { + let cpuid = CpuId::new(); + cpuid + .get_feature_info() + .map(|fi| Cpu::intel(fi.family_id().into(), fi.model_id(), fi.stepping_id())) +} + +#[cfg(test)] +mod test { + use arbitrary_int::Number; + + use super::*; + #[test] + fn intel_pt_builder_default_values_are_valid() { + let default = IntelPT::builder(); + IntelPT::builder() + .perf_buffer_size(default.perf_buffer_size) + .unwrap(); + IntelPT::builder() + .perf_aux_buffer_size(default.perf_aux_buffer_size) + .unwrap(); + } + + #[test] + fn intel_pt_pt_config_noretcomp_format() { + let ptconfig_noretcomp = PtConfig::DEFAULT.with_noretcomp(true).raw_value; + let path = format!("{PT_EVENT_PATH}/format/noretcomp"); + let s = fs::read_to_string(&path).expect("Failed to read Intel PT config noretcomp format"); + assert!( + s.starts_with("config:"), + "Unexpected Intel PT config noretcomp format" + ); + let bit = s["config:".len()..] + .trim() + .parse::() + .expect("Failed to parse Intel PT config noretcomp format"); + assert_eq!( + ptconfig_noretcomp, + 0b1 << bit, + "Unexpected Intel PT config noretcomp format" + ); + } + + #[test] + fn intel_pt_pt_config_psb_period_format() { + let ptconfig_psb_period = PtConfig::DEFAULT.with_psb_period(u4::MAX).raw_value; + let path = format!("{PT_EVENT_PATH}/format/psb_period"); + let s = + fs::read_to_string(&path).expect("Failed to read Intel PT config psb_period format"); + assert!( + s.starts_with("config:"), + "Unexpected Intel PT config psb_period format" + ); + let from = s["config:".len().."config:".len() + 2] + .parse::() + .expect("Failed to parse Intel PT config psb_period format"); + let to = s["config:".len() + 3..] + .trim() + .parse::() + .expect("Failed to parse Intel PT config psb_period format"); + let mut format = 0; + for bit in from..=to { + format |= 0b1 << bit; + } + assert_eq!( + ptconfig_psb_period, format, + "Unexpected Intel PT config psb_period format" + ); + } +} diff --git a/libafl_intelpt/tests/integration_tests_linux.rs b/libafl_intelpt/tests/integration_tests_linux.rs index ebd6f7c109..f4b06f1061 100644 --- a/libafl_intelpt/tests/integration_tests_linux.rs +++ b/libafl_intelpt/tests/integration_tests_linux.rs @@ -4,8 +4,7 @@ use std::{arch::asm, process}; -use libafl_intelpt::{availability, IntelPT}; -use libipt::Image; +use libafl_intelpt::{availability, Image, IntelPT}; use nix::{ sys::{ signal::{kill, raise, Signal}, @@ -69,11 +68,7 @@ fn intel_pt_trace_fork() { None, map.start() as u64, ) { - Err(e) => println!( - "Error adding mapping for {:?}: {:?}, skipping", - map.filename().unwrap(), - e - ), + Err(e) => println!("skipping mapping of {:?}: {:?}", map.filename().unwrap(), e), Ok(()) => println!( "mapping for {:?} added successfully {:#x} - {:#x}", map.filename().unwrap(), @@ -85,7 +80,8 @@ fn intel_pt_trace_fork() { } let mut map = vec![0u16; 0x10_00]; - pt.decode_traces_into_map(&mut image, &mut map).unwrap(); + pt.decode_traces_into_map(&mut image, map.as_mut_ptr(), map.len()) + .unwrap(); let assembly_jump_id = map.iter().position(|count| *count >= 254); assert!( diff --git a/libafl_intelpt/tests/run_integration_tests_linux_with_caps.sh b/libafl_intelpt/tests/run_integration_tests_linux_with_caps.sh index 78a1f41bee..86bfee83d0 100755 --- a/libafl_intelpt/tests/run_integration_tests_linux_with_caps.sh +++ b/libafl_intelpt/tests/run_integration_tests_linux_with_caps.sh @@ -9,3 +9,13 @@ for test_bin in ../target/debug/deps/integration_tests_linux-*; do done cargo test intel_pt_trace_fork -- --show-output + +cargo test --release intel_pt_trace_fork --no-run + +for test_bin in ../target/release/deps/integration_tests_linux-*; do + if file "$test_bin" | grep -q "ELF"; then + sudo setcap cap_ipc_lock,cap_sys_ptrace,cap_sys_admin,cap_syslog=ep "$test_bin" + fi +done + +cargo test --release intel_pt_trace_fork -- --show-output diff --git a/libafl_qemu/src/qemu/config.rs b/libafl_qemu/src/qemu/config.rs index f40e1865b1..1e27f37dc4 100644 --- a/libafl_qemu/src/qemu/config.rs +++ b/libafl_qemu/src/qemu/config.rs @@ -37,6 +37,16 @@ pub enum DiskImageFileFormat { Raw, } +#[derive(Debug, strum_macros::Display, Clone)] +#[strum(prefix = "cache=", serialize_all = "lowercase")] +pub enum DriveCache { + WriteBack, + None, + WriteThrough, + DirectSync, + Unsafe, +} + #[derive(Debug, Clone, Default, TypedBuilder)] pub struct Drive { #[builder(default, setter(strip_option, into))] @@ -45,6 +55,8 @@ pub struct Drive { format: Option, #[builder(default, setter(strip_option))] interface: Option, + #[builder(default, setter(strip_option))] + cache: Option, } impl Display for Drive { @@ -70,25 +82,96 @@ impl Display for Drive { if let Some(interface) = &self.interface { write!(f, "{}{interface}", separator())?; } + if let Some(cache) = &self.cache { + write!(f, "{}{cache}", separator())?; + } Ok(()) } } -#[derive(Debug, strum_macros::Display, Clone)] -#[strum(prefix = "-serial ", serialize_all = "lowercase")] +#[derive(Debug, Clone, TypedBuilder)] +pub struct Tcp { + #[builder(default, setter(strip_option))] + host: Option, + port: u16, + #[builder(default, setter(strip_option))] + server: Option, + #[builder(default, setter(strip_option))] + wait: Option, + #[builder(default, setter(strip_option))] + nodelay: Option, + #[builder(default, setter(strip_option))] + reconnect_ms: Option, +} + +impl Display for Tcp { + fn fmt(&self, f: &mut Formatter<'_>) -> std::fmt::Result { + write!(f, "tcp:{}", self.host.as_deref().unwrap_or(""))?; + write!(f, ":{}", self.port)?; + let server = match self.server { + Some(true) => ",server=on", + Some(false) => ",server=off", + None => "", + }; + write!(f, "{server}")?; + let wait = match self.wait { + Some(true) => ",wait=on", + Some(false) => ",wait=off", + None => "", + }; + write!(f, "{wait}")?; + let nodelay = match self.nodelay { + Some(true) => ",nodelay=on", + Some(false) => ",nodelay=off", + None => "", + }; + write!(f, "{nodelay}")?; + if let Some(ms) = self.reconnect_ms { + write!(f, "{ms}")?; + } + Ok(()) + } +} + +#[derive(Debug, Clone)] pub enum Serial { None, Null, Stdio, + Tcp(Tcp), } -#[derive(Debug, strum_macros::Display, Clone)] -#[strum(prefix = "-monitor ", serialize_all = "lowercase")] +impl Display for Serial { + fn fmt(&self, f: &mut Formatter<'_>) -> std::fmt::Result { + write!(f, "-serial ")?; + match self { + Serial::None => write!(f, "none"), + Serial::Null => write!(f, "null"), + Serial::Stdio => write!(f, "stdio"), + Serial::Tcp(tcp) => write!(f, "{tcp}"), + } + } +} + +#[derive(Debug, Clone)] pub enum Monitor { None, Null, Stdio, + Tcp(Tcp), +} + +impl Display for Monitor { + fn fmt(&self, f: &mut Formatter<'_>) -> std::fmt::Result { + write!(f, "-monitor ")?; + match self { + Monitor::None => write!(f, "none"), + Monitor::Null => write!(f, "null"), + Monitor::Stdio => write!(f, "stdio"), + Monitor::Tcp(tcp) => write!(f, "{tcp}"), + } + } } /// Set the directory for the BIOS, VGA BIOS and keymaps. @@ -137,6 +220,50 @@ impl> From for Kernel { } } +#[cfg(feature = "systemmode")] +#[derive(Debug, Clone)] +pub struct AppendKernelCmd { + cmdline: String, +} + +#[cfg(feature = "systemmode")] +impl Display for AppendKernelCmd { + fn fmt(&self, f: &mut Formatter<'_>) -> fmt::Result { + write!(f, "-append {}", self.cmdline) + } +} + +#[cfg(feature = "systemmode")] +impl> From for AppendKernelCmd { + fn from(cmdline: R) -> Self { + Self { + cmdline: cmdline.as_ref().to_string(), + } + } +} + +#[cfg(feature = "systemmode")] +#[derive(Debug, Clone)] +pub struct InitRD { + path: PathBuf, +} + +#[cfg(feature = "systemmode")] +impl Display for InitRD { + fn fmt(&self, f: &mut Formatter<'_>) -> fmt::Result { + write!(f, "-initrd {}", self.path.to_str().unwrap()) + } +} + +#[cfg(feature = "systemmode")] +impl> From for InitRD { + fn from(path: R) -> Self { + Self { + path: path.as_ref().to_path_buf(), + } + } +} + #[derive(Debug, Clone)] pub struct LoadVM { path: PathBuf, @@ -175,6 +302,25 @@ impl> From for Machine { } } +#[derive(Debug, Clone)] +pub struct Cpu { + model: String, +} + +impl Display for Cpu { + fn fmt(&self, f: &mut Formatter<'_>) -> fmt::Result { + write!(f, "-cpu {}", self.model) + } +} + +impl> From for Cpu { + fn from(model: R) -> Self { + Self { + model: model.as_ref().to_string(), + } + } +} + #[derive(Debug, Clone, strum_macros::Display)] pub enum Snapshot { #[strum(serialize = "-snapshot")] @@ -274,6 +420,26 @@ impl From for VgaPci { } } +#[cfg(feature = "systemmode")] +#[derive(Debug, Clone, strum_macros::Display)] +pub enum DefaultDevices { + #[strum(serialize = "")] + ENABLE, + #[strum(serialize = "-nodefaults")] + DISABLE, +} + +#[cfg(feature = "systemmode")] +impl From for DefaultDevices { + fn from(default_devices: bool) -> Self { + if default_devices { + DefaultDevices::ENABLE + } else { + DefaultDevices::DISABLE + } + } +} + #[cfg(feature = "usermode")] #[derive(Debug, Clone)] pub struct Program { @@ -304,6 +470,8 @@ pub struct QemuConfig { #[cfg(feature = "systemmode")] #[builder(default, setter(strip_option, into))] bios: Option, + #[builder(default, setter(strip_option, into))] + cpu: Option, #[builder(default, setter(into))] drives: Vec, #[cfg(feature = "systemmode")] @@ -329,6 +497,15 @@ pub struct QemuConfig { vga_pci: Option, #[builder(default, setter(strip_option, into))] start_cpu: Option, + #[cfg(feature = "systemmode")] + #[builder(default, setter(strip_option, into))] + default_devices: Option, + #[cfg(feature = "systemmode")] + #[builder(default, setter(strip_option, into))] + append_kernel_cmd: Option, + #[cfg(feature = "systemmode")] + #[builder(default, setter(strip_option, into))] + initrd: Option, #[cfg(feature = "usermode")] #[builder(setter(into))] program: Program, diff --git a/libafl_qemu/src/qemu/mod.rs b/libafl_qemu/src/qemu/mod.rs index 4879516a50..db0ef7d233 100644 --- a/libafl_qemu/src/qemu/mod.rs +++ b/libafl_qemu/src/qemu/mod.rs @@ -11,13 +11,15 @@ use std::{ ffi::{c_void, CString}, fmt::{Display, Formatter, Write}, mem::{transmute, MaybeUninit}, - ops::Range, + ops::{Deref, Range}, pin::Pin, ptr::copy_nonoverlapping, sync::OnceLock, }; use libafl_bolts::os::unix_signals::Signal; +#[cfg(feature = "systemmode")] +use libafl_bolts::Error; use libafl_qemu_sys::{ libafl_flush_jit, libafl_get_exit_reason, libafl_page_from_addr, libafl_qemu_add_gdb_cmd, libafl_qemu_cpu_index, libafl_qemu_current_cpu, libafl_qemu_gdb_reply, libafl_qemu_get_cpu, @@ -26,6 +28,8 @@ use libafl_qemu_sys::{ libafl_qemu_write_reg, CPUArchState, CPUStatePtr, FatPtr, GuestAddr, GuestPhysAddr, GuestUsize, GuestVirtAddr, }; +#[cfg(feature = "systemmode")] +use libafl_qemu_sys::{libafl_qemu_remove_hw_breakpoint, libafl_qemu_set_hw_breakpoint}; use num_traits::Num; use strum::IntoEnumIterator; @@ -110,7 +114,8 @@ pub struct Qemu { #[derive(Clone, Debug)] pub enum QemuParams { - Config(QemuConfig), + // QemuConfig is quite big, at least 240 bytes so we use a Box + Config(Box), Cli(Vec), } @@ -178,7 +183,7 @@ impl Display for QemuExitReason { impl From for QemuParams { fn from(config: QemuConfig) -> Self { - QemuParams::Config(config) + QemuParams::Config(Box::new(config)) } } @@ -539,7 +544,7 @@ impl Qemu { match ¶ms { QemuParams::Config(cfg) => { QEMU_CONFIG - .set(cfg.clone()) + .set(cfg.deref().clone()) .map_err(|_| unreachable!("QEMU_CONFIG was already set but Qemu was not init!")) .expect("Could not set QEMU Config."); } @@ -861,6 +866,28 @@ impl Qemu { } } + #[cfg(feature = "systemmode")] + pub fn set_hw_breakpoint(&self, addr: GuestAddr) -> Result<(), Error> { + let ret = unsafe { libafl_qemu_set_hw_breakpoint(addr.into()) }; + match ret { + 0 => Ok(()), + errno => Err(Error::unsupported(format!( + "Failed to set hw breakpoint errno: {errno}" + ))), + } + } + + #[cfg(feature = "systemmode")] + pub fn remove_hw_breakpoint(&self, addr: GuestAddr) -> Result<(), Error> { + let ret = unsafe { libafl_qemu_remove_hw_breakpoint(addr.into()) }; + match ret { + 0 => Ok(()), + errno => Err(Error::unsupported(format!( + "Failed to set hw breakpoint errno: {errno}" + ))), + } + } + pub fn entry_break(&self, addr: GuestAddr) { self.set_breakpoint(addr); unsafe {