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 // A fuzzer with feedbacks and a corpus scheduler
let mut fuzzer = StdFuzzer::new(scheduler, feedback, objective); 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. // 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 // 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 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. // Set the instruction pointer (IP) filter and memory image of our target.
// These information can be retrieved from `readelf -l` (for example) // 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 let intel_pt = IntelPT::builder()
.set_ip_filters(&[code_memory_addresses.clone()]) .cpu(cpu.0)
.inherit(true)
.ip_filters(&[code_memory_addresses.clone()])
.build()
.unwrap(); .unwrap();
let sections = [Section { let sections = [Section {
file_path: target_path.to_string_lossy().to_string(), 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, size: (*code_memory_addresses.end() - *code_memory_addresses.start() + 1) as u64,
virtual_address: *code_memory_addresses.start() as u64, virtual_address: *code_memory_addresses.start() as u64,
}]; }];

View File

@ -115,6 +115,8 @@ intel_pt = [
"dep:nix", "dep:nix",
"dep:num_enum", "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 ## Enables features for corpus minimization
cmin = ["z3"] cmin = ["z3"]

View File

@ -29,7 +29,19 @@ use libafl_bolts::{
#[cfg(target_os = "linux")] #[cfg(target_os = "linux")]
use libc::STDIN_FILENO; use libc::STDIN_FILENO;
#[cfg(target_os = "linux")] #[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")] #[cfg(target_os = "linux")]
use typed_builder::TypedBuilder; use typed_builder::TypedBuilder;
@ -204,8 +216,6 @@ where
match unsafe { fork() } { match unsafe { fork() } {
Ok(ForkResult::Parent { child }) => Ok(child), Ok(ForkResult::Parent { child }) => Ok(child),
Ok(ForkResult::Child) => { Ok(ForkResult::Child) => {
ptrace::traceme().unwrap();
if let Some(c) = self.cpu { if let Some(c) = self.cpu {
c.set_affinity_forced().unwrap(); 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) // After this STOP, the process is traced with PTrace (no hooks yet)
raise(Signal::SIGSTOP).unwrap(); raise(Signal::SIGSTOP).unwrap();
@ -405,13 +416,13 @@ where
T: CommandConfigurator<<S::Corpus as Corpus>::Input>, T: CommandConfigurator<<S::Corpus as Corpus>::Input>,
{ {
#[inline] #[inline]
fn set_timeout(&mut self, timeout: Duration) { fn timeout(&self) -> Duration {
*self.configurer.exec_timeout_mut() = timeout; self.configurer.exec_timeout()
} }
#[inline] #[inline]
fn timeout(&self) -> Duration { fn set_timeout(&mut self, timeout: Duration) {
self.configurer.exec_timeout() *self.configurer.exec_timeout_mut() = timeout;
} }
} }
@ -437,32 +448,28 @@ where
_mgr: &mut EM, _mgr: &mut EM,
input: &Self::Input, input: &Self::Input,
) -> Result<ExitKind, Error> { ) -> Result<ExitKind, Error> {
use nix::sys::{
ptrace,
signal::Signal,
wait::{
waitpid, WaitPidFlag,
WaitStatus::{Exited, PtraceEvent, Signaled, Stopped},
},
};
*state.executions_mut() += 1; *state.executions_mut() += 1;
let child = self.configurer.spawn_child(input)?; 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) { 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)?; 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 !matches!(wait_status, PtraceEvent(c, Signal::SIGTRAP, e)
if c == child && e == (ptrace::Event::PTRACE_EVENT_EXEC as i32) 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)?; self.observers.pre_exec_child_all(state, input)?;
@ -471,6 +478,8 @@ where
} }
self.hooks.pre_exec_all(state, input); 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)?; ptrace::detach(child, None)?;
let res = match waitpid(child, None)? { let res = match waitpid(child, None)? {
Exited(pid, 0) if pid == child => ExitKind::Ok, 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::SIGALRM, _has_coredump) if pid == child => ExitKind::Timeout,
Signaled(pid, Signal::SIGABRT, _has_coredump) if pid == child => ExitKind::Crash, Signaled(pid, Signal::SIGABRT, _has_coredump) if pid == child => ExitKind::Crash,
Signaled(pid, Signal::SIGKILL, _has_coredump) if pid == child => ExitKind::Oom, Signaled(pid, Signal::SIGKILL, _has_coredump) if pid == child => ExitKind::Oom,
Stopped(pid, Signal::SIGALRM) if pid == child => ExitKind::Timeout, // Stopped(pid, Signal::SIGALRM) if pid == child => ExitKind::Timeout,
Stopped(pid, Signal::SIGABRT) if pid == child => ExitKind::Crash, // Stopped(pid, Signal::SIGABRT) if pid == child => ExitKind::Crash,
Stopped(pid, Signal::SIGKILL) if pid == child => ExitKind::Oom, // Stopped(pid, Signal::SIGKILL) if pid == child => ExitKind::Oom,
s => { s => {
// TODO other cases? // TODO other cases?
return Err(Error::unsupported( 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)] #[cfg(test)]
mod tests { mod tests {
use crate::{ use crate::{

View File

@ -30,7 +30,7 @@ pub struct Section {
} }
/// Hook to enable Intel Processor Trace (PT) tracing /// Hook to enable Intel Processor Trace (PT) tracing
#[derive(TypedBuilder)] #[derive(Debug, TypedBuilder)]
pub struct IntelPTHook<T> { pub struct IntelPTHook<T> {
#[builder(default = IntelPT::builder().build().unwrap())] #[builder(default = IntelPT::builder().build().unwrap())]
intel_pt: IntelPT, intel_pt: IntelPT,
@ -40,17 +40,6 @@ pub struct IntelPTHook<T> {
map_len: usize, 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> impl<S, T> ExecutorHook<S> for IntelPTHook<T>
where where
S: UsesInput + Serialize, S: UsesInput + Serialize,
@ -63,13 +52,19 @@ where
} }
fn post_exec(&mut self, _state: &mut S, _input: &S::Input) { 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 slice = unsafe { &mut *slice_from_raw_parts_mut(self.map_ptr, self.map_len) };
let _ = self let _ = pt
.intel_pt
.decode_traces_into_map(&mut self.image.0, slice) .decode_traces_into_map(&mut self.image.0, slice)
.inspect_err(|e| log::warn!("Intel PT trace decoding failed: {e}")); .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 } bindgen = { workspace = true }
regex = { workspace = true } regex = { workspace = true }
which = { workspace = true } which = { workspace = true }
symcc_libafl = { workspace = true, default-features = true, version = "0.14.1" } symcc_libafl = { workspace = true, default-features = true }
[lints] [lints]
workspace = true workspace = true

View File

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

View File

@ -19,6 +19,7 @@ use std::{
}; };
#[cfg(target_os = "linux")] #[cfg(target_os = "linux")]
use std::{ use std::{
boxed::Box,
ffi::{CStr, CString}, ffi::{CStr, CString},
fmt::Debug, fmt::Debug,
format, fs, format, fs,
@ -129,6 +130,8 @@ pub struct IntelPT {
aux_tail: *mut u64, aux_tail: *mut u64,
previous_decode_head: u64, previous_decode_head: u64,
ip_filters: Vec<RangeInclusive<usize>>, ip_filters: Vec<RangeInclusive<usize>>,
#[cfg(feature = "export_raw")]
last_decode_trace: Vec<u8>,
} }
#[cfg(target_os = "linux")] #[cfg(target_os = "linux")]
@ -302,8 +305,12 @@ impl IntelPT {
} }
} else { } else {
// Head pointer wrapped, the trace is split // 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)?; let mut config = ConfigBuilder::new(data.as_mut()).map_err(error_from_pt_error)?;
config.filter(self.ip_filters_to_addr_filter()); config.filter(self.ip_filters_to_addr_filter());
@ -351,19 +358,17 @@ impl IntelPT {
#[inline] #[inline]
#[must_use] #[must_use]
unsafe fn join_split_trace(&self, head_wrap: u64, tail_wrap: u64) -> OwnedRefMut<[u8]> { fn join_split_trace(&self, head_wrap: u64, tail_wrap: u64) -> Box<[u8]> {
let first_ptr = self.perf_aux_buffer.add(tail_wrap as usize) as *mut 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 first_len = self.perf_aux_buffer_size - tail_wrap as usize;
let second_ptr = self.perf_aux_buffer as *mut u8; let second_ptr = self.perf_aux_buffer as *mut u8;
let second_len = head_wrap as usize; let second_len = head_wrap as usize;
OwnedRefMut::Owned(
[ let mut vec = Vec::with_capacity(first_len + second_len);
slice::from_raw_parts(first_ptr, first_len), vec.extend_from_slice(unsafe { slice::from_raw_parts(first_ptr, first_len) });
slice::from_raw_parts(second_ptr, second_len), vec.extend_from_slice(unsafe { slice::from_raw_parts(second_ptr, second_len) });
] vec.into_boxed_slice()
.concat()
.into_boxed_slice(),
)
} }
#[inline] #[inline]
@ -395,7 +400,7 @@ impl IntelPT {
*status = s; *status = s;
let offset = decoder.offset().map_err(error_from_pt_error)?; 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()); let id = hash_me(*previous_block_end_ip) ^ hash_me(b.ip());
// SAFETY: the index is < map.len() since the modulo operation is applied // 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()) }; let map_loc = unsafe { map.get_unchecked_mut(id as usize % map.len()) };
@ -416,6 +421,29 @@ impl IntelPT {
} }
Ok(()) 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")] #[cfg(target_os = "linux")]
@ -441,6 +469,7 @@ pub struct IntelPTBuilder {
inherit: bool, inherit: bool,
perf_buffer_size: usize, perf_buffer_size: usize,
perf_aux_buffer_size: usize, perf_aux_buffer_size: usize,
ip_filters: Vec<RangeInclusive<usize>>,
} }
#[cfg(target_os = "linux")] #[cfg(target_os = "linux")]
@ -450,14 +479,15 @@ impl Default for IntelPTBuilder {
/// The default configuration corresponds to: /// The default configuration corresponds to:
/// ```rust /// ```rust
/// use libafl_intelpt::{IntelPTBuilder, PAGE_SIZE}; /// use libafl_intelpt::{IntelPTBuilder, PAGE_SIZE};
/// let builder = unsafe { std::mem::zeroed::<IntelPTBuilder>() } /// let builder = IntelPTBuilder::default()
/// .pid(None) /// .pid(None)
/// .all_cpus() /// .all_cpus()
/// .exclude_kernel(true) /// .exclude_kernel(true)
/// .exclude_hv(true) /// .exclude_hv(true)
/// .inherit(false) /// .inherit(false)
/// .perf_buffer_size(128 * PAGE_SIZE + PAGE_SIZE).unwrap() /// .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()); /// assert_eq!(builder, IntelPTBuilder::default());
/// ``` /// ```
fn default() -> Self { fn default() -> Self {
@ -469,6 +499,7 @@ impl Default for IntelPTBuilder {
inherit: false, inherit: false,
perf_buffer_size: 128 * PAGE_SIZE + PAGE_SIZE, perf_buffer_size: 128 * PAGE_SIZE + PAGE_SIZE,
perf_aux_buffer_size: 2 * 1024 * 1024, 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_head = unsafe { &raw mut (*buff_metadata).aux_head };
let aux_tail = unsafe { &raw mut (*buff_metadata).aux_tail }; 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); let mut intel_pt = IntelPT {
Ok(IntelPT {
fd, fd,
perf_buffer, perf_buffer,
perf_aux_buffer, perf_aux_buffer,
@ -543,8 +572,14 @@ impl IntelPTBuilder {
aux_head, aux_head,
aux_tail, aux_tail,
previous_decode_head: 0, 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 /// Warn if the configuration is not recommended
@ -638,6 +673,15 @@ impl IntelPTBuilder {
self.perf_aux_buffer_size = perf_aux_buffer_size; self.perf_aux_buffer_size = perf_aux_buffer_size;
Ok(self) 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` /// Perf event config for `IntelPT`
@ -717,6 +761,9 @@ pub fn availability_in_qemu_kvm() -> Result<(), String> {
#[cfg(target_os = "linux")] #[cfg(target_os = "linux")]
{ {
let kvm_pt_mode_path = "/sys/module/kvm_intel/parameters/pt_mode"; 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) { if let Ok(s) = fs::read_to_string(kvm_pt_mode_path) {
match s.trim().parse::<i32>().map(TryInto::try_into) { match s.trim().parse::<i32>().map(TryInto::try_into) {
Ok(Ok(KvmPTMode::System)) => (), Ok(Ok(KvmPTMode::System)) => (),

View File

@ -90,7 +90,7 @@ clippy = ["libafl_qemu_sys/clippy"]
[dependencies] [dependencies]
libafl = { workspace = true, features = ["std", "derive", "regex"] } libafl = { workspace = true, features = ["std", "derive", "regex"] }
libafl_bolts = { workspace = true, features = ["std", "derive"] } 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_qemu_sys = { workspace = true }
libafl_derive = { workspace = true, default-features = true } libafl_derive = { workspace = true, default-features = true }
@ -129,7 +129,7 @@ getset = "0.1.3"
document-features = { workspace = true, optional = true } document-features = { workspace = true, optional = true }
[build-dependencies] [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 } pyo3-build-config = { workspace = true, optional = true }
rustversion = { workspace = true } rustversion = { workspace = true }
bindgen = { workspace = true } bindgen = { workspace = true }