Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -25,6 +25,7 @@ incremented upon a breaking change and the patch version will be incremented for
**Changed**

- Allow initialization for Vanilla Solana projects with IDLs ([435](https://github.com/Ackee-Blockchain/trident/pull/435))
- improve invariant handling and exit-code behavior in fuzzing ([457](https://github.com/Ackee-Blockchain/trident/pull/457))

## [0.12.0] - 2025-11-27

Expand Down
2 changes: 1 addition & 1 deletion crates/cli/src/command/fuzz.rs
Original file line number Diff line number Diff line change
Expand Up @@ -63,7 +63,7 @@ pub(crate) enum FuzzCommand {
long = "exit-code",
required = false,
value_name = "MODE",
help = "Exit with non-zero code on failures. Modes: 'all' (any failure), 'invariants' (only fuzz test assertions), 'panics' (only program panics)."
help = "Exit with non-zero code on failures. Modes: 'all' (any failure), 'invariants' (only custom invariant/assert failures)."
)]
exit_code: Option<ExitCodeMode>,
#[arg(
Expand Down
27 changes: 16 additions & 11 deletions crates/client/src/commander/mod.rs
Original file line number Diff line number Diff line change
Expand Up @@ -27,8 +27,8 @@ pub enum Error {
BuildProgramsFailed,
#[error("fuzzing failed")]
FuzzingFailed,
#[error("Fuzzing found failing invariants or unhandled panics")]
FuzzingFailedInvariantOrPanic,
#[error("Fuzzing failed due to exit-code policy (invariants/all)")]
FuzzingFailedPolicy,
#[error("Coverage error: {0}")]
Coverage(#[from] crate::coverage::CoverageError),
#[error("Cannot find the trident-tests directory in the current workspace")]
Expand Down Expand Up @@ -120,26 +120,31 @@ impl Commander {
}

/// Manages a child process in an async context, specifically for monitoring fuzzing tasks.
/// Waits for the process to exit or a Ctrl+C signal. Prints an error message if the process
/// exits with an error, and sleeps briefly on Ctrl+C. Throws `Error::FuzzingFailed` on errors.
/// Waits for the process to exit or a Ctrl+C signal.
///
/// Exit-code semantics:
/// - `0`: success
/// - `99`: policy failure from fuzz runner (only treated as error when policy is enabled)
/// - other non-zero: runtime failure (always treated as error)
///
/// # Arguments
/// * `child` - A mutable reference to a `Child` process.
/// * `policy_enabled` - True when `--exit-code` policy is active.
///
/// # Errors
/// * Throws `Error::FuzzingFailed` if waiting on the child process fails.
/// * Throws `Error::FuzzingFailed` or `Error::FuzzingFailedPolicy` on failure.
#[throws]
async fn handle_child(child: &mut Child, with_exit_code: bool) {
async fn handle_child(child: &mut Child, policy_enabled: bool) {
tokio::select! {
res = child.wait() =>
match res {
Ok(status) => match status.code() {
Some(code) => {
match (code, with_exit_code) {
(0, _) => {} // fuzzing did not find any failing invariants or panics and we dont care about exit code
(99, true) => throw!(Error::FuzzingFailedInvariantOrPanic), // fuzzing found failing invariants or panics and we care about exit code
(99, false) => {} // fuzzing found failing invariants or panics and we dont care about exit code
(_, _) => throw!(Error::FuzzingFailed), // fuzzing failed for some other reason so we care about exit code
match (code, policy_enabled) {
(0, _) => {}
(99, true) => throw!(Error::FuzzingFailedPolicy),
(99, false) => {}
(_, _) => throw!(Error::FuzzingFailed),
}
}
None => throw!(Error::FuzzingFailed),
Expand Down
30 changes: 5 additions & 25 deletions crates/client/src/exit_code.rs
Original file line number Diff line number Diff line change
Expand Up @@ -3,18 +3,15 @@ use std::str::FromStr;

/// Specifies which type of failures should cause a non-zero exit code.
///
/// - `All`: Exit non-zero on any failure (program panics or invariant failures)
/// - `Invariants`: Exit non-zero only on invariant/assert failures in fuzz tests
/// - `Panics`: Exit non-zero only on program panics (program failed to complete)
/// - `All`: Exit non-zero on any policy failure (program panics or custom invariant failures)
/// - `Invariants`: Exit non-zero only on custom invariant failures in fuzz tests
#[derive(Debug, Clone, Copy, PartialEq, Eq, Default)]
pub enum ExitCodeMode {
/// Exit non-zero on any failure (default behavior when exit code is enabled)
/// Exit non-zero on any policy failure (default behavior when exit code is enabled)
#[default]
All,
/// Exit non-zero only on invariant/assert failures in fuzz tests
/// Exit non-zero only on custom invariant failures in fuzz tests
Invariants,
/// Exit non-zero only on program panics (program failed to complete)
Panics,
}

impl ExitCodeMode {
Expand All @@ -23,19 +20,13 @@ impl ExitCodeMode {
match self {
ExitCodeMode::All => "all",
ExitCodeMode::Invariants => "invariants",
ExitCodeMode::Panics => "panics",
}
}

/// Check if this mode should trigger exit code for invariant failures
pub fn triggers_on_invariants(&self) -> bool {
matches!(self, ExitCodeMode::All | ExitCodeMode::Invariants)
}

/// Check if this mode should trigger exit code for program panics
pub fn triggers_on_panics(&self) -> bool {
matches!(self, ExitCodeMode::All | ExitCodeMode::Panics)
}
}

impl fmt::Display for ExitCodeMode {
Expand All @@ -51,9 +42,8 @@ impl FromStr for ExitCodeMode {
match s.to_lowercase().as_str() {
"all" => Ok(ExitCodeMode::All),
"invariants" => Ok(ExitCodeMode::Invariants),
"panics" => Ok(ExitCodeMode::Panics),
_ => Err(format!(
"Invalid exit code mode '{}'. Valid values are: all, invariants, panics",
"Invalid exit code mode '{}'. Valid values are: all, invariants",
s
)),
}
Expand All @@ -71,23 +61,13 @@ mod tests {
ExitCodeMode::from_str("invariants").unwrap(),
ExitCodeMode::Invariants
);
assert_eq!(
ExitCodeMode::from_str("panics").unwrap(),
ExitCodeMode::Panics
);
assert_eq!(ExitCodeMode::from_str("ALL").unwrap(), ExitCodeMode::All);
assert!(ExitCodeMode::from_str("invalid").is_err());
}

#[test]
fn test_triggers() {
assert!(ExitCodeMode::All.triggers_on_invariants());
assert!(ExitCodeMode::All.triggers_on_panics());

assert!(ExitCodeMode::Invariants.triggers_on_invariants());
assert!(!ExitCodeMode::Invariants.triggers_on_panics());

assert!(!ExitCodeMode::Panics.triggers_on_invariants());
assert!(ExitCodeMode::Panics.triggers_on_panics());
}
}
6 changes: 6 additions & 0 deletions crates/client/src/utils.rs
Original file line number Diff line number Diff line change
Expand Up @@ -538,6 +538,9 @@ mod tests {
.await
.unwrap();

// Keep TempDir alive across post-await checks to avoid eager drop in async state machine.
assert!(temp_dir.path().exists());

// Verify no backup was created
let backup_path = settings_path.with_extension("json.backup");
assert!(!backup_path.exists());
Expand Down Expand Up @@ -574,6 +577,9 @@ mod tests {
.await
.unwrap();

// Keep TempDir alive across post-await checks to avoid eager drop in async state machine.
assert!(temp_dir.path().exists());

// Verify no backup was created
let backup_path = settings_path.with_extension("json.backup");
assert!(!backup_path.exists());
Expand Down
38 changes: 38 additions & 0 deletions crates/fuzz/src/invariant.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,38 @@
/// Marker type for intentional invariant violations.
/// Used to distinguish user-defined invariant failures from unexpected panics.
#[derive(Debug)]
pub struct InvariantViolation(pub String);

/// Checks a condition and panics with `InvariantViolation` if false.
///
/// Use this macro to mark assertions as intentional invariant checks.
/// When an invariant fails, it will be counted and collected separately
/// from unexpected panics (bugs in fuzz test code).
///
/// # Examples
///
/// ```ignore
/// // Simple condition check
/// invariant!(balance_after == balance_before - amount);
/// invariant!(account.is_initialized);
/// invariant!(owner != Pubkey::default());
///
/// // With custom message
/// invariant!(balance > 0, "Balance must be positive");
/// invariant!(a == b, "Expected {} but got {}", a, b);
/// ```
#[macro_export]
macro_rules! invariant {
($cond:expr) => {
if !$cond {
std::panic::panic_any($crate::invariant::InvariantViolation(
format!("invariant violation: {}", stringify!($cond))
));
}
};
($cond:expr, $($msg:tt)*) => {
if !$cond {
std::panic::panic_any($crate::invariant::InvariantViolation(format!($($msg)*)));
}
};
}
5 changes: 5 additions & 0 deletions crates/fuzz/src/lib.rs
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
pub mod address_storage;
pub mod error;
pub mod invariant;
pub mod trident;
pub mod trident_rng;

Expand Down Expand Up @@ -67,6 +68,10 @@ pub mod fuzzing {
/// Error
pub use super::error::*;

/// Invariant checking
pub use super::invariant;
pub use super::invariant::InvariantViolation;

/// Account discriminator trait
pub use super::AccountDiscriminator;

Expand Down
Loading
Loading