From ee7dafae4100397698e5266465919bfdbc40af51 Mon Sep 17 00:00:00 2001 From: Valentin Huber Date: Wed, 8 May 2024 15:17:24 +0200 Subject: [PATCH] Adding ExitCodeObserver and ExitSignalObserver (#2138) * Adding ExitCodeObserver and ExitStatusObserver * Introducing ExitKind::Interrupted --- libafl/src/executors/command.rs | 58 +++++++++++++------ libafl/src/executors/differential.rs | 25 ++++++++ libafl/src/executors/mod.rs | 10 ++++ libafl/src/observers/mod.rs | 77 +++++++++++++++++++++++++ libafl/src/observers/stdio.rs | 86 ++++++++++++++++++++++++++++ 5 files changed, 238 insertions(+), 18 deletions(-) diff --git a/libafl/src/executors/command.rs b/libafl/src/executors/command.rs index 124d8a936f..c6d7558997 100644 --- a/libafl/src/executors/command.rs +++ b/libafl/src/executors/command.rs @@ -334,24 +334,9 @@ where let mut child = self.configurer.spawn_child(input)?; - let res = match child + let res = child .wait_timeout(self.configurer.exec_timeout()) - .expect("waiting on child failed") - .map(|status| status.signal()) - { - // for reference: https://www.man7.org/linux/man-pages/man7/signal.7.html - Some(Some(9)) => Ok(ExitKind::Oom), - Some(Some(_)) => Ok(ExitKind::Crash), - Some(None) => Ok(ExitKind::Ok), - None => { - // if this fails, there is not much we can do. let's hope it failed because the process finished - // in the meantime. - drop(child.kill()); - // finally, try to wait to properly clean up system resources. - drop(child.wait()); - Ok(ExitKind::Timeout) - } - }; + .expect("waiting on child failed"); if self.observers.observes_stderr() { let mut stderr = Vec::new(); @@ -362,6 +347,7 @@ where })?.read_to_end(&mut stderr)?; self.observers.observe_stderr(&stderr); } + if self.observers.observes_stdout() { let mut stdout = Vec::new(); child.stdout.as_mut().ok_or_else(|| { @@ -372,7 +358,43 @@ where self.observers.observe_stdout(&stdout); } - res + if self.observers.observes_exit_code() { + if let Some(r) = res { + if let Some(exit_code) = r.code() { + self.observers.observe_exit_code(exit_code); + } + } + } + + if self.observers.observes_exit_signal() { + if let Some(r) = res { + if let Some(exit_signal) = r.signal() { + self.observers.observe_exit_signal(exit_signal); + } + } + } + + #[allow(clippy::match_same_arms)] + // to make different signals leading to an ExitKind::Interrupted explicit + match res.map(|status| status.signal()) { + // for reference: https://www.man7.org/linux/man-pages/man7/signal.7.html + Some(Some(2)) => Ok(ExitKind::Interrupted), // SIGINT + Some(Some(10)) => Ok(ExitKind::Interrupted), // SIGUSR1 + Some(Some(12)) => Ok(ExitKind::Interrupted), // SIGUSR2 + Some(Some(14)) => Ok(ExitKind::Interrupted), // SIGALRM + Some(Some(15)) => Ok(ExitKind::Interrupted), // SIGTERM + Some(Some(18)) => Ok(ExitKind::Interrupted), // SIGCONT + Some(Some(_)) => Ok(ExitKind::Crash), + Some(None) => Ok(ExitKind::Ok), + None => { + // if this fails, there is not much we can do. let's hope it failed because the process finished + // in the meantime. + drop(child.kill()); + // finally, try to wait to properly clean up system resources. + drop(child.wait()); + Ok(ExitKind::Timeout) + } + } } } diff --git a/libafl/src/executors/differential.rs b/libafl/src/executors/differential.rs index c4e2620e97..be88fc7a3f 100644 --- a/libafl/src/executors/differential.rs +++ b/libafl/src/executors/differential.rs @@ -166,6 +166,17 @@ where fn observes_stderr(&self) -> bool { self.primary.as_ref().observes_stderr() || self.secondary.as_ref().observes_stderr() } + /// Returns true if an exit code observer was added to the list + #[inline] + fn observes_exit_code(&self) -> bool { + self.primary.as_ref().observes_exit_code() || self.secondary.as_ref().observes_exit_code() + } + /// Returns true if an exit signal observer was added to the list + #[inline] + fn observes_exit_signal(&self) -> bool { + self.primary.as_ref().observes_exit_signal() + || self.secondary.as_ref().observes_exit_signal() + } /// Runs `observe_stdout` for all stdout observers in the list fn observe_stdout(&mut self, stdout: &[u8]) { @@ -177,6 +188,20 @@ where fn observe_stderr(&mut self, stderr: &[u8]) { self.primary.as_mut().observe_stderr(stderr); self.secondary.as_mut().observe_stderr(stderr); + self.primary.as_mut().observe_stderr(stderr); + self.secondary.as_mut().observe_stderr(stderr); + } + + /// Runs `observe_exit_code` for all exit code observers in the list + fn observe_exit_code(&mut self, exit_code: i32) { + self.primary.as_mut().observe_exit_code(exit_code); + self.secondary.as_mut().observe_exit_code(exit_code); + } + + /// Runs `observe_exit_signal` for all exit signal observers in the list + fn observe_exit_signal(&mut self, exit_signal: i32) { + self.primary.as_mut().observe_exit_signal(exit_signal); + self.secondary.as_mut().observe_exit_signal(exit_signal); } } diff --git a/libafl/src/executors/mod.rs b/libafl/src/executors/mod.rs index 5e9d462b36..ba7573e40b 100644 --- a/libafl/src/executors/mod.rs +++ b/libafl/src/executors/mod.rs @@ -57,9 +57,14 @@ pub enum ExitKind { /// The run resulted in a target crash. Crash, /// The run hit an out of memory error. + /// Only applicable when using `libafl_libfuzzer` Oom, /// The run timed out Timeout, + /// The process was interrupted, likely not because of a crash but because of user interaction. + /// Only applicable for certain executors (like [`CommandExecutor`]). + /// Check their source code for the actual conditions that lead to this. + Interrupted, /// Special case for [`DiffExecutor`] when both exitkinds don't match Diff { /// The exitkind of the primary executor @@ -72,6 +77,8 @@ pub enum ExitKind { } /// How one of the diffing executions finished. +/// +/// Refer to the definitions in [`ExitKind`] for disclaimers about applicability #[derive(Debug, Clone, Copy, Serialize, Deserialize, PartialEq, Eq)] #[cfg_attr( any(not(feature = "serdeany_autoreg"), miri), @@ -80,6 +87,8 @@ pub enum ExitKind { pub enum DiffExitKind { /// The run exited normally. Ok, + /// The run was interrupted by a signal likely not tied to a crash + Interrupted, /// The run resulted in a target crash. Crash, /// The run hit an out of memory error. @@ -98,6 +107,7 @@ impl From for DiffExitKind { fn from(exitkind: ExitKind) -> Self { match exitkind { ExitKind::Ok => DiffExitKind::Ok, + ExitKind::Interrupted => DiffExitKind::Interrupted, ExitKind::Crash => DiffExitKind::Crash, ExitKind::Oom => DiffExitKind::Oom, ExitKind::Timeout => DiffExitKind::Timeout, diff --git a/libafl/src/observers/mod.rs b/libafl/src/observers/mod.rs index 6401baaf29..952f76386d 100644 --- a/libafl/src/observers/mod.rs +++ b/libafl/src/observers/mod.rs @@ -92,6 +92,16 @@ where fn observes_stderr(&self) -> bool { false } + /// If this observer observes exit codes + #[inline] + fn observes_exit_code(&self) -> bool { + false + } + /// If this observer observes exit signals + #[inline] + fn observes_exit_signal(&self) -> bool { + false + } /// React to new `stdout` /// To use this, always return `true` from `observes_stdout` @@ -104,6 +114,17 @@ where #[inline] #[allow(unused_variables)] fn observe_stderr(&mut self, stderr: &[u8]) {} + + /// React to new exit code + /// To use this, always return `true` from `observes_exit_code` + #[inline] + #[allow(unused_variables)] + fn observe_exit_code(&mut self, exit_code: i32) {} + /// React to new exit signal + /// To use this, always return `true` from `observes_exit_signal` + #[inline] + #[allow(unused_variables)] + fn observe_exit_signal(&mut self, exit_signal: i32) {} } /// Defines the observer type shared across traits of the type. @@ -144,11 +165,19 @@ where fn observes_stdout(&self) -> bool; /// Returns true if a `stderr` observer was added to the list fn observes_stderr(&self) -> bool; + /// Returns true if an exit code observer was added to the list + fn observes_exit_code(&self) -> bool; + /// Returns true if an exit signal observer was added to the list + fn observes_exit_signal(&self) -> bool; /// Runs `observe_stdout` for all stdout observers in the list fn observe_stdout(&mut self, stdout: &[u8]); /// Runs `observe_stderr` for all stderr observers in the list fn observe_stderr(&mut self, stderr: &[u8]); + /// Runs `observe_exit_code` for all exit code observers in the list + fn observe_exit_code(&mut self, exit_code: i32); + /// Runs `observe_exit_signal` for all exit signal observers in the list + fn observe_exit_signal(&mut self, exit_signal: i32); } impl ObserversTuple for () @@ -193,6 +222,18 @@ where false } + /// Returns true if an exit code observer was added to the list + #[inline] + fn observes_exit_code(&self) -> bool { + false + } + + /// Returns true if an exit signal observer was added to the list + #[inline] + fn observes_exit_signal(&self) -> bool { + false + } + /// Runs `observe_stdout` for all stdout observers in the list #[inline] #[allow(unused_variables)] @@ -202,6 +243,16 @@ where #[inline] #[allow(unused_variables)] fn observe_stderr(&mut self, stderr: &[u8]) {} + + /// Runs `observe_exit_code` for all exit code observers in the list + #[inline] + #[allow(unused_variables)] + fn observe_exit_code(&mut self, exit_code: i32) {} + + /// Runs `observe_exit_signal` for all exit signal observers in the list + #[inline] + #[allow(unused_variables)] + fn observe_exit_signal(&mut self, exit_signal: i32) {} } impl ObserversTuple for (Head, Tail) @@ -252,6 +303,18 @@ where self.0.observes_stderr() || self.1.observes_stderr() } + /// Returns true if an exit code observer was added to the list + #[inline] + fn observes_exit_code(&self) -> bool { + self.0.observes_exit_code() || self.1.observes_exit_code() + } + + /// Returns true if an exit signal observer was added to the list + #[inline] + fn observes_exit_signal(&self) -> bool { + self.0.observes_exit_signal() || self.1.observes_exit_signal() + } + /// Runs `observe_stdout` for all stdout observers in the list #[inline] fn observe_stdout(&mut self, stdout: &[u8]) { @@ -265,6 +328,20 @@ where self.0.observe_stderr(stderr); self.1.observe_stderr(stderr); } + + /// Runs `observe_exit_code` for all exit code observers in the list + #[inline] + fn observe_exit_code(&mut self, exit_code: i32) { + self.0.observe_exit_code(exit_code); + self.1.observe_exit_code(exit_code); + } + + /// Runs `observe_exit_signal` for all exit signal observers in the list + #[inline] + fn observe_exit_signal(&mut self, exit_signal: i32) { + self.0.observe_exit_signal(exit_signal); + self.1.observe_exit_signal(exit_signal); + } } /// A trait for [`Observer`]`s` with a hash field diff --git a/libafl/src/observers/stdio.rs b/libafl/src/observers/stdio.rs index bc921c95f2..39f2c9daa8 100644 --- a/libafl/src/observers/stdio.rs +++ b/libafl/src/observers/stdio.rs @@ -95,3 +95,89 @@ impl Named for StdErrObserver { &self.name } } + +/// An observer that captures the exit code of a target. +/// Only works for supported executors. +#[derive(Debug, Clone, PartialEq, Eq, Hash, Serialize, Deserialize)] +pub struct ExitCodeObserver { + /// The name of the observer. + pub name: Cow<'static, str>, + /// The exit code of the target during its last execution. + pub exit_code: Option, +} + +/// An observer that captures exit signal of a target. +impl ExitCodeObserver { + /// Create a new [`ExitCodeObserver`] with the given name. + #[must_use] + pub fn new(name: &'static str) -> Self { + Self { + name: Cow::from(name), + exit_code: None, + } + } +} + +impl Observer for ExitCodeObserver +where + S: UsesInput, +{ + #[inline] + fn observes_exit_code(&self) -> bool { + true + } + + /// React to new exit code + fn observe_exit_code(&mut self, exit_code: i32) { + self.exit_code = Some(exit_code); + } +} + +impl Named for ExitCodeObserver { + fn name(&self) -> &Cow<'static, str> { + &self.name + } +} + +/// An observer that captures the exit code of a target. +/// Only works for supported executors. +#[derive(Debug, Clone, PartialEq, Eq, Hash, Serialize, Deserialize)] +pub struct ExitSignalObserver { + /// The name of the observer. + pub name: Cow<'static, str>, + /// The exit signal of the target during its last execution. + pub exit_signal: Option, +} + +/// An observer that captures the exit signal of a target. +impl ExitSignalObserver { + /// Create a new [`ExitSignalObserver`] with the given name. + #[must_use] + pub fn new(name: &'static str) -> Self { + Self { + name: Cow::from(name), + exit_signal: None, + } + } +} + +impl Observer for ExitSignalObserver +where + S: UsesInput, +{ + #[inline] + fn observes_exit_signal(&self) -> bool { + true + } + + /// React to new exit signal + fn observe_exit_signal(&mut self, exit_signal: i32) { + self.exit_signal = Some(exit_signal); + } +} + +impl Named for ExitSignalObserver { + fn name(&self) -> &Cow<'static, str> { + &self.name + } +}