From 6206451938885a6f9f0a3f5b9d832282513caeed Mon Sep 17 00:00:00 2001 From: Dominik Maier Date: Thu, 20 Nov 2025 16:25:40 +0100 Subject: [PATCH 1/4] Fork as runtime flag --- crates/libafl/src/events/launcher.rs | 635 ++++++++++---------- crates/libafl/src/events/llmp/restarting.rs | 83 +-- crates/libafl/src/executors/mod.rs | 6 +- crates/libafl_qemu/Cargo.toml | 2 +- crates/libafl_targets/Cargo.toml | 1 - 5 files changed, 375 insertions(+), 352 deletions(-) diff --git a/crates/libafl/src/events/launcher.rs b/crates/libafl/src/events/launcher.rs index bb7d6574cb..8227fc1ca7 100644 --- a/crates/libafl/src/events/launcher.rs +++ b/crates/libafl/src/events/launcher.rs @@ -12,51 +12,45 @@ //! On `Unix` systems, the [`Launcher`] will use `fork` if the `fork` feature is used for `LibAFL`. //! Else, it will start subsequent nodes with the same commandline, and will set special `env` variables accordingly. -use alloc::string::String; -use core::{ - fmt::{self, Debug, Formatter}, - net::SocketAddr, - num::NonZeroUsize, - time::Duration, +use alloc::{ + boxed::Box, + fmt::{Debug, Formatter}, + string::String, }; +use core::fmt; +use std::{net::SocketAddr, num::NonZeroUsize, process::Stdio, time::Duration}; use libafl_bolts::{ core_affinity::{CoreId, Cores}, + os::startable_self, shmem::ShMemProvider, tuples::tuple_list, }; use serde::{Deserialize, Serialize, de::DeserializeOwned}; use typed_builder::TypedBuilder; -#[cfg(all(unix, feature = "fork"))] +#[cfg(unix)] use { - crate::{ - events::{CentralizedLlmpHook, StdLlmpEventHook, centralized::CentralizedEventManager}, - inputs::Input, - }, - alloc::boxed::Box, + crate::events::{CentralizedLlmpHook, StdLlmpEventHook, centralized::CentralizedEventManager}, alloc::string::ToString, libafl_bolts::{ core_affinity::get_core_ids, llmp::{Broker, Brokers, LlmpBroker}, - os::{ForkResult, fork}, + os::{ForkResult, dup2, fork}, }, -}; -#[cfg(unix)] -use { - libafl_bolts::os::dup2, std::{fs::File, os::unix::io::AsRawFd}, }; -#[cfg(any(windows, not(feature = "fork")))] +#[cfg(not(unix))] use {libafl_bolts::os::startable_self, std::process::Stdio}; -#[cfg(all(unix, feature = "fork", feature = "multi_machine"))] +#[cfg(all(unix, feature = "multi_machine"))] use crate::events::multi_machine::{NodeDescriptor, TcpMultiMachineHooks}; use crate::{ Error, events::{ - EventConfig, EventManagerHooksTuple, - llmp::{LlmpRestartingEventManager, LlmpShouldSaveState, ManagerKind, RestartingMgr}, + EventConfig, EventManagerHooksTuple, LlmpRestartingEventManager, LlmpShouldSaveState, + ManagerKind, RestartingMgr, }, + inputs::Input, monitors::Monitor, }; @@ -64,7 +58,7 @@ use crate::{ const _AFL_LAUNCHER_CLIENT: &str = "AFL_LAUNCHER_CLIENT"; /// The env variable to set in order to enable child output -#[cfg(all(feature = "fork", unix))] +#[cfg(unix)] const LIBAFL_DEBUG_OUTPUT: &str = "LIBAFL_DEBUG_OUTPUT"; /// Information about this client from the launcher @@ -155,7 +149,7 @@ pub struct Launcher<'a, CF, MT, SP> { #[builder(default = 10)] launch_delay: u64, /// The actual, opened, `stdout_file` - so that we keep it open until the end - #[cfg(all(unix, feature = "fork"))] + #[cfg(unix)] #[builder(setter(skip), default = None)] opened_stdout_file: Option, /// A file name to write all client stderr output to. If not specified, output is sent to @@ -164,7 +158,7 @@ pub struct Launcher<'a, CF, MT, SP> { #[builder(default = None)] stderr_file: Option<&'a str>, /// The actual, opened, `stdout_file` - so that we keep it open until the end - #[cfg(all(unix, feature = "fork"))] + #[cfg(unix)] #[builder(setter(skip), default = None)] opened_stderr_file: Option, /// The `ip:port` address of another broker to connect our new broker to for multi-machine @@ -180,6 +174,9 @@ pub struct Launcher<'a, CF, MT, SP> { /// Tell the manager to serialize or not the state on restart #[builder(default = LlmpShouldSaveState::OnRestart)] serialize_state: LlmpShouldSaveState, + /// If this launcher should use `fork` to spawn a new instance. Otherwise it will try to re-launch the current process with exactly the same parameters. + #[builder(default = cfg!(unix))] + fork: bool, } impl Debug for Launcher<'_, CF, MT, SP> { @@ -205,9 +202,9 @@ impl Debug for Launcher<'_, CF, MT, SP> { impl Launcher<'_, CF, MT, SP> where MT: Monitor + Clone, + SP: ShMemProvider, { /// Launch the broker and the clients and fuzz - #[cfg(any(windows, not(feature = "fork"), all(unix, feature = "fork")))] pub fn launch(&mut self) -> Result<(), Error> where CF: FnOnce( @@ -217,327 +214,330 @@ where ) -> Result<(), Error>, I: DeserializeOwned, S: DeserializeOwned + Serialize, - SP: ShMemProvider, { Self::launch_with_hooks(self, tuple_list!()) } -} -impl Launcher<'_, CF, MT, SP> -where - MT: Monitor + Clone, - SP: ShMemProvider, -{ /// Launch the broker and the clients and fuzz with a user-supplied hook - #[cfg(all(unix, feature = "fork"))] + #[expect(clippy::too_many_lines, clippy::match_wild_err_arm)] pub fn launch_with_hooks(&mut self, hooks: EMH) -> Result<(), Error> where - S: DeserializeOwned + Serialize, - I: DeserializeOwned, - EMH: EventManagerHooksTuple + Clone + Copy, CF: FnOnce( Option, LlmpRestartingEventManager, ClientDescription, ) -> Result<(), Error>, + EMH: EventManagerHooksTuple + Clone + Copy, + I: DeserializeOwned, + S: DeserializeOwned + Serialize, { - if self.cores.ids.is_empty() { - return Err(Error::illegal_argument( - "No cores to spawn on given, cannot launch anything.", - )); + let use_fork = self.fork; + #[cfg(not(unix))] + if use_fork { + log::warn!("Fork is not supported on this platform, falling back to spawn."); + use_fork = false; } - if self.run_client.is_none() { - return Err(Error::illegal_argument( - "No client callback provided".to_string(), - )); - } + if use_fork { + #[cfg(unix)] + { + if self.cores.ids.is_empty() { + return Err(Error::illegal_argument( + "No cores to spawn on given, cannot launch anything.", + )); + } - let core_ids = get_core_ids()?; - let mut handles = vec![]; + if self.run_client.is_none() { + return Err(Error::illegal_argument( + "No client callback provided".to_string(), + )); + } - log::info!("spawning on cores: {:?}", self.cores); + let core_ids = get_core_ids()?; + let mut handles = vec![]; - self.opened_stdout_file = self - .stdout_file - .map(|filename| File::create(filename).unwrap()); - self.opened_stderr_file = self - .stderr_file - .map(|filename| File::create(filename).unwrap()); + log::info!("spawning on cores: {:?}", self.cores); - let debug_output = std::env::var(LIBAFL_DEBUG_OUTPUT).is_ok(); + self.opened_stdout_file = self + .stdout_file + .map(|filename| File::create(filename).unwrap()); + self.opened_stderr_file = self + .stderr_file + .map(|filename| File::create(filename).unwrap()); - // Spawn clients - let mut index = 0_usize; - for bind_to in core_ids { - if self.cores.ids.contains(&bind_to) { - for overcommit_id in 0..self.overcommit { - index += 1; - self.shmem_provider.pre_fork()?; - // # Safety - // Fork is safe in general, apart from potential side effects to the OS and other threads - match unsafe { fork() }? { - ForkResult::Parent(child) => { - self.shmem_provider.post_fork(false)?; - handles.push(child.pid); - log::info!( - "child spawned with id {index} and bound to core {bind_to:?}" - ); - } - ForkResult::Child => { + let debug_output = std::env::var(LIBAFL_DEBUG_OUTPUT).is_ok(); + + // Spawn clients + let mut index = 0_usize; + for bind_to in core_ids { + if self.cores.ids.contains(&bind_to) { + for overcommit_id in 0..self.overcommit { + index += 1; + self.shmem_provider.pre_fork()?; // # Safety - // A call to `getpid` is safe. - log::info!("{:?} PostFork", unsafe { libc::getpid() }); - self.shmem_provider.post_fork(true)?; + // Fork is safe in general, apart from potential side effects to the OS and other threads + match unsafe { fork() }? { + ForkResult::Parent(child) => { + self.shmem_provider.post_fork(false)?; + handles.push(child.pid); + log::info!( + "child spawned with id {index} and bound to core {bind_to:?}" + ); + } + ForkResult::Child => { + // # Safety + // A call to `getpid` is safe. + log::info!("{:?} PostFork", unsafe { libc::getpid() }); + self.shmem_provider.post_fork(true)?; + + std::thread::sleep(Duration::from_millis( + index as u64 * self.launch_delay, + )); + + if !debug_output { + if let Some(file) = &self.opened_stdout_file { + // # Safety + // We assume the file descriptors are valid here + unsafe { + dup2(file.as_raw_fd(), libc::STDOUT_FILENO)?; + match &self.opened_stderr_file { + Some(stderr) => { + dup2( + stderr.as_raw_fd(), + libc::STDERR_FILENO, + )?; + } + _ => { + dup2( + file.as_raw_fd(), + libc::STDERR_FILENO, + )?; + } + } + } + } + } - std::thread::sleep(Duration::from_millis( - index as u64 * self.launch_delay, - )); + let client_description = + ClientDescription::new(index, overcommit_id, bind_to); + + // Fuzzer client. keeps retrying the connection to broker till the broker starts + let builder = RestartingMgr::::builder() + .shmem_provider(self.shmem_provider.clone()) + .broker_port(self.broker_port) + .kind(ManagerKind::Client { + client_description: client_description.clone(), + }) + .configuration(self.configuration) + .serialize_state(self.serialize_state) + .hooks(hooks) + .fork(self.fork); + let (state, mgr) = builder.build().launch()?; + + return (self.run_client.take().unwrap())( + state, + mgr, + client_description, + ); + } + } + } + } + } - if !debug_output && let Some(file) = &self.opened_stdout_file { + if self.spawn_broker { + log::info!("I am broker!!."); + + // TODO we don't want always a broker here, think about using different laucher process to spawn different configurations + let builder = RestartingMgr::::builder() + .shmem_provider(self.shmem_provider.clone()) + .monitor(Some(self.monitor.clone())) + .broker_port(self.broker_port) + .kind(ManagerKind::Broker) + .remote_broker_addr(self.remote_broker_addr) + .exit_cleanly_after(Some( + NonZeroUsize::try_from(self.cores.ids.len()).unwrap(), + )) + .configuration(self.configuration) + .serialize_state(self.serialize_state) + .hooks(hooks) + .fork(self.fork); + + builder.build().launch()?; + + // Broker exited. kill all clients. + for handle in &handles { + // # Safety + // Normal libc call, no dereferences whatsoever + unsafe { + libc::kill(*handle, libc::SIGINT); + } + } + } else { + for handle in &handles { + let mut status = 0; + log::info!( + "Not spawning broker (spawn_broker is false). Waiting for fuzzer children to exit..." + ); + unsafe { + libc::waitpid(*handle, &raw mut status, 0); + if status != 0 { + log::info!("Client with pid {handle} exited with status {status}"); + } + } + } + } + } + // This is the fork part for unix + #[cfg(not(unix))] + unreachable!(); + } else { + // spawn logic + let is_client = std::env::var(_AFL_LAUNCHER_CLIENT); + + let mut handles = match is_client { + Ok(core_conf) => { + let client_description = ClientDescription::from_safe_string(&core_conf); + // the actual client. do the fuzzing + + let builder = RestartingMgr::::builder() + .shmem_provider(self.shmem_provider.clone()) + .broker_port(self.broker_port) + .kind(ManagerKind::Client { + client_description: client_description.clone(), + }) + .configuration(self.configuration) + .serialize_state(self.serialize_state) + .hooks(hooks) + .fork(self.fork); + + let (state, mgr) = builder.build().launch()?; + + return (self.run_client.take().unwrap())(state, mgr, client_description); + } + Err(std::env::VarError::NotPresent) => { + // I am a broker + // before going to the broker loop, spawn n clients + + let core_ids = get_core_ids().unwrap(); + let mut handles = vec![]; + + log::info!("spawning on cores: {:?}", self.cores); + + let debug_output = std::env::var("LIBAFL_DEBUG_OUTPUT").is_ok(); + #[cfg(unix)] + { + // Set own stdout and stderr as set by the user + if !debug_output { + let opened_stdout_file = self + .stdout_file + .map(|filename| File::create(filename).unwrap()); + let opened_stderr_file = self + .stderr_file + .map(|filename| File::create(filename).unwrap()); + if let Some(file) = opened_stdout_file { // # Safety // We assume the file descriptors are valid here unsafe { dup2(file.as_raw_fd(), libc::STDOUT_FILENO)?; - match &self.opened_stderr_file { - Some(stderr) => { - dup2(stderr.as_raw_fd(), libc::STDERR_FILENO)?; - } - _ => { - dup2(file.as_raw_fd(), libc::STDERR_FILENO)?; - } + if let Some(stderr) = opened_stderr_file { + dup2(stderr.as_raw_fd(), libc::STDERR_FILENO)?; + } else { + dup2(file.as_raw_fd(), libc::STDERR_FILENO)?; } } } + } + } + //spawn clients + let mut index = 0; + for core_id in core_ids { + if self.cores.ids.contains(&core_id) { + for overcommit_i in 0..self.overcommit { + index += 1; + // Forward own stdio to child processes, if requested by user + #[allow(unused_mut)] // mut only on certain cfgs + let (mut stdout, mut stderr) = (Stdio::null(), Stdio::null()); + #[cfg(unix)] + { + if self.stdout_file.is_some() || self.stderr_file.is_some() { + stdout = Stdio::inherit(); + stderr = Stdio::inherit(); + } + } - let client_description = - ClientDescription::new(index, overcommit_id, bind_to); + std::thread::sleep(Duration::from_millis( + core_id.0 as u64 * self.launch_delay, + )); - // Fuzzer client. keeps retrying the connection to broker till the broker starts - let builder = RestartingMgr::::builder() - .shmem_provider(self.shmem_provider.clone()) - .broker_port(self.broker_port) - .kind(ManagerKind::Client { - client_description: client_description.clone(), + let client_description = + ClientDescription::new(index, overcommit_i, core_id); + // # Safety + // This is set only once, in here, for the child. + unsafe { + std::env::set_var( + _AFL_LAUNCHER_CLIENT, + client_description.to_safe_string(), + ); + } + let mut child = startable_self()?; + let child = (if debug_output { + &mut child + } else { + child.stdout(stdout); + child.stderr(stderr) }) - .configuration(self.configuration) - .serialize_state(self.serialize_state) - .hooks(hooks); - let (state, mgr) = builder.build().launch()?; - - return (self.run_client.take().unwrap())( - state, - mgr, - client_description, - ); + .spawn()?; + handles.push(child); + } } } + handles } - } - } - - if self.spawn_broker { - log::info!("I am broker!!."); + Err(_) => panic!("Env variables are broken, received non-unicode!"), + }; - // TODO we don't want always a broker here, think about using different laucher process to spawn different configurations - let builder = RestartingMgr::::builder() - .shmem_provider(self.shmem_provider.clone()) - .monitor(Some(self.monitor.clone())) - .broker_port(self.broker_port) - .kind(ManagerKind::Broker) - .remote_broker_addr(self.remote_broker_addr) - .exit_cleanly_after(Some(NonZeroUsize::try_from(self.cores.ids.len()).unwrap())) - .configuration(self.configuration) - .serialize_state(self.serialize_state) - .hooks(hooks); - - builder.build().launch()?; - - // Broker exited. kill all clients. - for handle in &handles { - // # Safety - // Normal libc call, no dereferences whatsoever - unsafe { - libc::kill(*handle, libc::SIGINT); - } - } - } else { - for handle in &handles { - let mut status = 0; - log::info!( - "Not spawning broker (spawn_broker is false). Waiting for fuzzer children to exit..." - ); - unsafe { - libc::waitpid(*handle, &raw mut status, 0); - if status != 0 { - log::info!("Client with pid {handle} exited with status {status}"); - } - } + // It's fine to check this after the client spawn loop - since we won't have spawned any clients... + // Doing it later means one less check in each spawned process. + if self.cores.ids.is_empty() { + return Err(Error::illegal_argument( + "No cores to spawn on given, cannot launch anything.", + )); } - } - Ok(()) - } - - /// Launch the broker and the clients and fuzz - #[cfg(any(windows, not(feature = "fork")))] - #[expect(clippy::too_many_lines, clippy::match_wild_err_arm)] - pub fn launch_with_hooks(&mut self, hooks: EMH) -> Result<(), Error> - where - CF: FnOnce( - Option, - LlmpRestartingEventManager, - ClientDescription, - ) -> Result<(), Error>, - EMH: EventManagerHooksTuple + Clone + Copy, - I: DeserializeOwned, - S: DeserializeOwned + Serialize, - { - use libafl_bolts::core_affinity::get_core_ids; - - let is_client = std::env::var(_AFL_LAUNCHER_CLIENT); - - let mut handles = match is_client { - Ok(core_conf) => { - let client_description = ClientDescription::from_safe_string(&core_conf); - // the actual client. do the fuzzing + if self.spawn_broker { + log::info!("I am broker!!."); let builder = RestartingMgr::::builder() .shmem_provider(self.shmem_provider.clone()) + .monitor(Some(self.monitor.clone())) .broker_port(self.broker_port) - .kind(ManagerKind::Client { - client_description: client_description.clone(), - }) + .kind(ManagerKind::Broker) + .remote_broker_addr(self.remote_broker_addr) + .exit_cleanly_after(Some(NonZeroUsize::try_from(self.cores.ids.len()).unwrap())) .configuration(self.configuration) .serialize_state(self.serialize_state) - .hooks(hooks); - - let (state, mgr) = builder.build().launch()?; + .hooks(hooks) + .fork(self.fork); - return (self.run_client.take().unwrap())(state, mgr, client_description); - } - Err(std::env::VarError::NotPresent) => { - // I am a broker - // before going to the broker loop, spawn n clients + builder.build().launch()?; - let core_ids = get_core_ids().unwrap(); - let mut handles = vec![]; - - log::info!("spawning on cores: {:?}", self.cores); - - let debug_output = std::env::var("LIBAFL_DEBUG_OUTPUT").is_ok(); - #[cfg(unix)] - { - // Set own stdout and stderr as set by the user - if !debug_output { - let opened_stdout_file = self - .stdout_file - .map(|filename| File::create(filename).unwrap()); - let opened_stderr_file = self - .stderr_file - .map(|filename| File::create(filename).unwrap()); - if let Some(file) = opened_stdout_file { - // # Safety - // We assume the file descriptors are valid here - unsafe { - dup2(file.as_raw_fd(), libc::STDOUT_FILENO)?; - if let Some(stderr) = opened_stderr_file { - dup2(stderr.as_raw_fd(), libc::STDERR_FILENO)?; - } else { - dup2(file.as_raw_fd(), libc::STDERR_FILENO)?; - } - } - } - } + //broker exited. kill all clients. + for handle in &mut handles { + handle.kill()?; } - //spawn clients - let mut index = 0; - for core_id in core_ids { - if self.cores.ids.contains(&core_id) { - for overcommit_i in 0..self.overcommit { - index += 1; - // Forward own stdio to child processes, if requested by user - #[allow(unused_mut)] // mut only on certain cfgs - let (mut stdout, mut stderr) = (Stdio::null(), Stdio::null()); - #[cfg(unix)] - { - if self.stdout_file.is_some() || self.stderr_file.is_some() { - stdout = Stdio::inherit(); - stderr = Stdio::inherit(); - } - } - - std::thread::sleep(Duration::from_millis( - core_id.0 as u64 * self.launch_delay, - )); - - let client_description = - ClientDescription::new(index, overcommit_i, core_id); - // # Safety - // This is set only once, in here, for the child. - unsafe { - std::env::set_var( - _AFL_LAUNCHER_CLIENT, - client_description.to_safe_string(), - ); - } - let mut child = startable_self()?; - let child = (if debug_output { - &mut child - } else { - child.stdout(stdout); - child.stderr(stderr) - }) - .spawn()?; - handles.push(child); - } + } else { + log::info!( + "Not spawning broker (spawn_broker is false). Waiting for fuzzer children to exit..." + ); + for handle in &mut handles { + let ecode = handle.wait()?; + if !ecode.success() { + log::info!("Client with handle {handle:?} exited with {ecode:?}"); } } - handles } - Err(_) => panic!("Env variables are broken, received non-unicode!"), - }; - - // It's fine to check this after the client spawn loop - since we won't have spawned any clients... - // Doing it later means one less check in each spawned process. - if self.cores.ids.is_empty() { - return Err(Error::illegal_argument( - "No cores to spawn on given, cannot launch anything.", - )); } - - if self.spawn_broker { - log::info!("I am broker!!."); - - let builder = RestartingMgr::::builder() - .shmem_provider(self.shmem_provider.clone()) - .monitor(Some(self.monitor.clone())) - .broker_port(self.broker_port) - .kind(ManagerKind::Broker) - .remote_broker_addr(self.remote_broker_addr) - .exit_cleanly_after(Some(NonZeroUsize::try_from(self.cores.ids.len()).unwrap())) - .configuration(self.configuration) - .serialize_state(self.serialize_state) - .hooks(hooks); - - builder.build().launch()?; - - //broker exited. kill all clients. - for handle in &mut handles { - handle.kill()?; - } - } else { - log::info!( - "Not spawning broker (spawn_broker is false). Waiting for fuzzer children to exit..." - ); - for handle in &mut handles { - let ecode = handle.wait()?; - if !ecode.success() { - log::info!("Client with handle {handle:?} exited with {ecode:?}"); - } - } - } - Ok(()) } } @@ -546,7 +546,7 @@ where /// /// Provides a Launcher, which can be used to launch a fuzzing run on a specified list of cores with a single main and multiple secondary nodes /// This is for centralized, the 4th argument of the closure should mean if this is the main node. -#[cfg(all(unix, feature = "fork"))] +#[cfg(unix)] #[derive(TypedBuilder)] pub struct CentralizedLauncher<'a, CF, MF, MT, SP> { /// The `ShmemProvider` to use @@ -580,7 +580,6 @@ pub struct CentralizedLauncher<'a, CF, MF, MT, SP> { #[builder(default = 10)] launch_delay: u64, /// The actual, opened, `stdout_file` - so that we keep it open until the end - #[cfg(all(unix, feature = "fork"))] #[builder(setter(skip), default = None)] opened_stdout_file: Option, /// A file name to write all client stderr output to. If not specified, output is sent to @@ -588,7 +587,6 @@ pub struct CentralizedLauncher<'a, CF, MF, MT, SP> { #[builder(default = None)] stderr_file: Option<&'a str>, /// The actual, opened, `stdout_file` - so that we keep it open until the end - #[cfg(all(unix, feature = "fork"))] #[builder(setter(skip), default = None)] opened_stderr_file: Option, /// The `ip:port` address of another broker to connect our new broker to for multi-machine @@ -606,9 +604,12 @@ pub struct CentralizedLauncher<'a, CF, MF, MT, SP> { /// Tell the manager to serialize or not the state on restart #[builder(default = LlmpShouldSaveState::OnRestart)] serialize_state: LlmpShouldSaveState, + /// If this launcher should use `fork` to spawn a new instance. Otherwise it will try to re-launch the current process with exactly the same parameters. + #[builder(default = true)] + fork: bool, } -#[cfg(all(unix, feature = "fork"))] +#[cfg(unix)] impl Debug for CentralizedLauncher<'_, CF, MF, MT, SP> { fn fmt(&self, f: &mut Formatter<'_>) -> fmt::Result { f.debug_struct("Launcher") @@ -625,9 +626,10 @@ impl Debug for CentralizedLauncher<'_, CF, MF, MT, SP> { } /// The standard inner manager of centralized +#[cfg(unix)] pub type StdCentralizedInnerMgr = LlmpRestartingEventManager<(), I, S, SHM, SP>; -#[cfg(all(unix, feature = "fork"))] +#[cfg(unix)] impl CentralizedLauncher<'_, CF, MF, MT, SP> where MT: Monitor + Clone + 'static, @@ -670,7 +672,8 @@ where .kind(ManagerKind::Client { client_description }) .configuration(centralized_launcher.configuration) .serialize_state(centralized_launcher.serialize_state) - .hooks(tuple_list!()); + .hooks(tuple_list!()) + .fork(centralized_launcher.fork); builder.build().launch() }; @@ -679,7 +682,7 @@ where } } -#[cfg(all(unix, feature = "fork"))] +#[cfg(unix)] impl CentralizedLauncher<'_, CF, MF, MT, SP> where MT: Monitor + Clone + 'static, @@ -707,6 +710,12 @@ where ClientDescription, ) -> Result<(), Error>, { + if !self.fork { + return Err(Error::illegal_argument( + "CentralizedLauncher only supports fork-based spawning.", + )); + } + let mut main_inner_mgr_builder = Some(main_inner_mgr_builder); let mut secondary_inner_mgr_builder = Some(secondary_inner_mgr_builder); @@ -759,17 +768,19 @@ where index as u64 * self.launch_delay, )); - if !debug_output && let Some(file) = &self.opened_stdout_file { - // # Safety - // We assume the file descriptors are valid here - unsafe { - dup2(file.as_raw_fd(), libc::STDOUT_FILENO)?; - match &self.opened_stderr_file { - Some(stderr) => { - dup2(stderr.as_raw_fd(), libc::STDERR_FILENO)?; - } - _ => { - dup2(file.as_raw_fd(), libc::STDERR_FILENO)?; + if !debug_output { + if let Some(file) = &self.opened_stdout_file { + // # Safety + // We assume the file descriptors are valid here + unsafe { + dup2(file.as_raw_fd(), libc::STDOUT_FILENO)?; + match &self.opened_stderr_file { + Some(stderr) => { + dup2(stderr.as_raw_fd(), libc::STDERR_FILENO)?; + } + _ => { + dup2(file.as_raw_fd(), libc::STDERR_FILENO)?; + } } } } diff --git a/crates/libafl/src/events/llmp/restarting.rs b/crates/libafl/src/events/llmp/restarting.rs index 52b1331aff..1c8ede843f 100644 --- a/crates/libafl/src/events/llmp/restarting.rs +++ b/crates/libafl/src/events/llmp/restarting.rs @@ -695,6 +695,10 @@ pub struct RestartingMgr { serialize_state: LlmpShouldSaveState, /// The hooks passed to event manager: hooks: EMH, + /// If this manager should use `fork` to spawn a new instance. Otherwise it will try to re-launch the current process with exactly the same parameters. + #[cfg(unix)] + #[builder(default = true)] + fork: bool, #[builder(setter(skip), default = PhantomData)] phantom_data: PhantomData<(EMH, I, S)>, } @@ -833,49 +837,58 @@ where loop { log::info!("Spawning next client (id {ctr})"); - // On Unix, we fork (when fork feature is enabled) - #[cfg(all(unix, feature = "fork"))] let child_status = { - self.shmem_provider.pre_fork()?; - match unsafe { fork() }? { - ForkResult::Parent(handle) => { - unsafe { - libc::signal(libc::SIGINT, libc::SIG_IGN); + let mut use_fork = self.fork; + #[cfg(not(all(unix, feature = "fork")))] + if use_fork { + log::warn!( + "Fork is not supported on this platform, falling back to spawn." + ); + use_fork = false; + } + + if use_fork { + #[cfg(all(unix, feature = "fork"))] + { + self.shmem_provider.pre_fork()?; + match unsafe { fork() }? { + ForkResult::Parent(handle) => { + unsafe { + libc::signal(libc::SIGINT, libc::SIG_IGN); + } + self.shmem_provider.post_fork(false)?; + handle.status() + } + ForkResult::Child => { + log::debug!( + "{} has been forked into {}", + std::os::unix::process::parent_id(), + std::process::id() + ); + self.shmem_provider.post_fork(true)?; + break (staterestorer, self.shmem_provider.clone(), core_id); + } } - self.shmem_provider.post_fork(false)?; - handle.status() } - ForkResult::Child => { - log::debug!( - "{} has been forked into {}", - std::os::unix::process::parent_id(), - std::process::id() + #[cfg(not(all(unix, feature = "fork")))] + unreachable!() + } else { + // spawn + unsafe { + #[cfg(windows)] + libafl_bolts::os::windows_exceptions::signal( + libafl_bolts::os::windows_exceptions::SIGINT, + libafl_bolts::os::windows_exceptions::sig_ign(), ); - self.shmem_provider.post_fork(true)?; - break (staterestorer, self.shmem_provider.clone(), core_id); + + #[cfg(unix)] + libc::signal(libc::SIGINT, libc::SIG_IGN); } + let status = startable_self()?.status()?; + status.code().unwrap_or_default() } }; - // If this guy wants to fork, then ignore sigint - #[cfg(any(windows, not(feature = "fork")))] - unsafe { - #[cfg(windows)] - libafl_bolts::os::windows_exceptions::signal( - libafl_bolts::os::windows_exceptions::SIGINT, - libafl_bolts::os::windows_exceptions::sig_ign(), - ); - - #[cfg(unix)] - libc::signal(libc::SIGINT, libc::SIG_IGN); - } - - // On Windows (or in any case without fork), we spawn ourself again - #[cfg(any(windows, not(feature = "fork")))] - let child_status = startable_self()?.status()?; - #[cfg(any(windows, not(feature = "fork")))] - let child_status = child_status.code().unwrap_or_default(); - compiler_fence(Ordering::SeqCst); // really useful? if child_status == CTRL_C_EXIT || staterestorer.wants_to_exit() { diff --git a/crates/libafl/src/executors/mod.rs b/crates/libafl/src/executors/mod.rs index 985cf350df..06de563aea 100644 --- a/crates/libafl/src/executors/mod.rs +++ b/crates/libafl/src/executors/mod.rs @@ -9,10 +9,10 @@ pub use combined::CombinedExecutor; #[cfg(feature = "std")] pub use command::CommandExecutor; pub use differential::DiffExecutor; -#[cfg(all(feature = "std", feature = "fork", unix))] +#[cfg(all(feature = "std", unix))] pub use forkserver::{Forkserver, ForkserverExecutor}; pub use inprocess::InProcessExecutor; -#[cfg(all(feature = "std", feature = "fork", unix))] +#[cfg(all(feature = "std", unix))] pub use inprocess_fork::InProcessForkExecutor; #[cfg(unix)] use libafl_bolts::os::unix_signals::Signal; @@ -31,7 +31,7 @@ pub mod combined; #[cfg(feature = "std")] pub mod command; pub mod differential; -#[cfg(all(feature = "std", feature = "fork", unix))] +#[cfg(all(feature = "std", unix))] pub mod forkserver; pub mod inprocess; pub mod nop; diff --git a/crates/libafl_qemu/Cargo.toml b/crates/libafl_qemu/Cargo.toml index 47b6f02499..e22c3af8ce 100644 --- a/crates/libafl_qemu/Cargo.toml +++ b/crates/libafl_qemu/Cargo.toml @@ -54,7 +54,7 @@ injections = ["serde_yaml", "toml"] ## Python bindings support python = ["pyo3", "pyo3-build-config", "libafl_qemu_sys/python"] ## Fork support -fork = ["libafl/fork"] +fork = [] #! ## The following architecture features are mutually exclusive. diff --git a/crates/libafl_targets/Cargo.toml b/crates/libafl_targets/Cargo.toml index 11c98ef435..3148f034ff 100644 --- a/crates/libafl_targets/Cargo.toml +++ b/crates/libafl_targets/Cargo.toml @@ -62,7 +62,6 @@ forkserver = [ "common", "nix", "libafl/std", - "libafl/fork", ] # Compile C code for forkserver support windows_asan = ["common"] # Compile C code for ASAN on Windows whole_archive = [] # use +whole-archive to ensure the presence of weak symbols From 5af0dfe3d2edda7d127e1f13e52268ce2fc02fc0 Mon Sep 17 00:00:00 2001 From: Dominik Maier Date: Thu, 20 Nov 2025 16:53:11 +0100 Subject: [PATCH 2/4] fun --- crates/libafl/src/events/launcher.rs | 3 ++- crates/libafl/src/events/llmp/restarting.rs | 13 ++++--------- 2 files changed, 6 insertions(+), 10 deletions(-) diff --git a/crates/libafl/src/events/launcher.rs b/crates/libafl/src/events/launcher.rs index 8227fc1ca7..308d44e1b4 100644 --- a/crates/libafl/src/events/launcher.rs +++ b/crates/libafl/src/events/launcher.rs @@ -175,7 +175,8 @@ pub struct Launcher<'a, CF, MT, SP> { #[builder(default = LlmpShouldSaveState::OnRestart)] serialize_state: LlmpShouldSaveState, /// If this launcher should use `fork` to spawn a new instance. Otherwise it will try to re-launch the current process with exactly the same parameters. - #[builder(default = cfg!(unix))] + #[cfg(unix)] + #[builder(default = true)] fork: bool, } diff --git a/crates/libafl/src/events/llmp/restarting.rs b/crates/libafl/src/events/llmp/restarting.rs index 1c8ede843f..ad069c5d8b 100644 --- a/crates/libafl/src/events/llmp/restarting.rs +++ b/crates/libafl/src/events/llmp/restarting.rs @@ -35,7 +35,7 @@ use libafl_bolts::{ llmp::{ Broker, LLMP_FLAG_FROM_MM, LlmpBroker, LlmpClient, LlmpClientDescription, LlmpConnection, }, - os::CTRL_C_EXIT, + os::{CTRL_C_EXIT, startable_self}, shmem::{ShMem, ShMemProvider, StdShMem, StdShMemProvider}, staterestore::StateRestorer, tuples::tuple_list, @@ -838,15 +838,10 @@ where log::info!("Spawning next client (id {ctr})"); let child_status = { - let mut use_fork = self.fork; - #[cfg(not(all(unix, feature = "fork")))] - if use_fork { - log::warn!( - "Fork is not supported on this platform, falling back to spawn." - ); - use_fork = false; - } + #[cfg(unix)] + let use_fork = self.fork; + #[cfg(unix)] if use_fork { #[cfg(all(unix, feature = "fork"))] { From b63c1647f3a30600042e40ab560acf0fa4b77f63 Mon Sep 17 00:00:00 2001 From: Dominik Maier Date: Thu, 20 Nov 2025 17:35:02 +0100 Subject: [PATCH 3/4] more --- crates/libafl/src/events/llmp/restarting.rs | 2 -- 1 file changed, 2 deletions(-) diff --git a/crates/libafl/src/events/llmp/restarting.rs b/crates/libafl/src/events/llmp/restarting.rs index ad069c5d8b..bd66d47a2a 100644 --- a/crates/libafl/src/events/llmp/restarting.rs +++ b/crates/libafl/src/events/llmp/restarting.rs @@ -18,8 +18,6 @@ use std::net::TcpStream; #[cfg(feature = "std")] use libafl_bolts::llmp::{TcpRequest, TcpResponse, recv_tcp_msg, send_tcp_msg}; -#[cfg(any(windows, not(feature = "fork")))] -use libafl_bolts::os::startable_self; #[cfg(all(unix, not(miri)))] use libafl_bolts::os::unix_signals::setup_signal_handler; #[cfg(all(feature = "fork", unix))] From 612452fe5ee8a0251c45f306565e8553eb1a3357 Mon Sep 17 00:00:00 2001 From: Dominik Maier Date: Thu, 20 Nov 2025 18:48:11 +0100 Subject: [PATCH 4/4] more --- crates/libafl/src/events/launcher.rs | 80 ++++++++++++---------------- 1 file changed, 33 insertions(+), 47 deletions(-) diff --git a/crates/libafl/src/events/launcher.rs b/crates/libafl/src/events/launcher.rs index 308d44e1b4..b6681e9ff1 100644 --- a/crates/libafl/src/events/launcher.rs +++ b/crates/libafl/src/events/launcher.rs @@ -12,13 +12,14 @@ //! On `Unix` systems, the [`Launcher`] will use `fork` if the `fork` feature is used for `LibAFL`. //! Else, it will start subsequent nodes with the same commandline, and will set special `env` variables accordingly. -use alloc::{ - boxed::Box, +use alloc::{boxed::Box, string::String}; +use core::{ + fmt, fmt::{Debug, Formatter}, - string::String, }; -use core::fmt; -use std::{net::SocketAddr, num::NonZeroUsize, process::Stdio, time::Duration}; +#[cfg(unix)] +use core::{num::NonZeroUsize, time::Duration}; +use std::{net::SocketAddr, process::Stdio}; use libafl_bolts::{ core_affinity::{CoreId, Cores}, @@ -39,8 +40,6 @@ use { }, std::{fs::File, os::unix::io::AsRawFd}, }; -#[cfg(not(unix))] -use {libafl_bolts::os::startable_self, std::process::Stdio}; #[cfg(all(unix, feature = "multi_machine"))] use crate::events::multi_machine::{NodeDescriptor, TcpMultiMachineHooks}; @@ -232,15 +231,11 @@ where I: DeserializeOwned, S: DeserializeOwned + Serialize, { + #[cfg(unix)] let use_fork = self.fork; - #[cfg(not(unix))] - if use_fork { - log::warn!("Fork is not supported on this platform, falling back to spawn."); - use_fork = false; - } + #[cfg(unix)] if use_fork { - #[cfg(unix)] { if self.cores.ids.is_empty() { return Err(Error::illegal_argument( @@ -295,25 +290,17 @@ where index as u64 * self.launch_delay, )); - if !debug_output { - if let Some(file) = &self.opened_stdout_file { - // # Safety - // We assume the file descriptors are valid here - unsafe { - dup2(file.as_raw_fd(), libc::STDOUT_FILENO)?; - match &self.opened_stderr_file { - Some(stderr) => { - dup2( - stderr.as_raw_fd(), - libc::STDERR_FILENO, - )?; - } - _ => { - dup2( - file.as_raw_fd(), - libc::STDERR_FILENO, - )?; - } + if !debug_output && let Some(file) = &self.opened_stdout_file { + // # Safety + // We assume the file descriptors are valid here + unsafe { + dup2(file.as_raw_fd(), libc::STDOUT_FILENO)?; + match &self.opened_stderr_file { + Some(stderr) => { + dup2(stderr.as_raw_fd(), libc::STDERR_FILENO)?; + } + _ => { + dup2(file.as_raw_fd(), libc::STDERR_FILENO)?; } } } @@ -331,8 +318,9 @@ where }) .configuration(self.configuration) .serialize_state(self.serialize_state) - .hooks(hooks) - .fork(self.fork); + .hooks(hooks); + #[cfg(unix)] + let builder = builder.fork(self.fork); let (state, mgr) = builder.build().launch()?; return (self.run_client.take().unwrap())( @@ -769,19 +757,17 @@ where index as u64 * self.launch_delay, )); - if !debug_output { - if let Some(file) = &self.opened_stdout_file { - // # Safety - // We assume the file descriptors are valid here - unsafe { - dup2(file.as_raw_fd(), libc::STDOUT_FILENO)?; - match &self.opened_stderr_file { - Some(stderr) => { - dup2(stderr.as_raw_fd(), libc::STDERR_FILENO)?; - } - _ => { - dup2(file.as_raw_fd(), libc::STDERR_FILENO)?; - } + if !debug_output && let Some(file) = &self.opened_stdout_file { + // # Safety + // We assume the file descriptors are valid here + unsafe { + dup2(file.as_raw_fd(), libc::STDOUT_FILENO)?; + match &self.opened_stderr_file { + Some(stderr) => { + dup2(stderr.as_raw_fd(), libc::STDERR_FILENO)?; + } + _ => { + dup2(file.as_raw_fd(), libc::STDERR_FILENO)?; } } }