use std::collections::BTreeSet;
use std::ffi::OsString;
use std::fmt;
use std::num::NonZeroU64;
use std::path::PathBuf;
use std::str::FromStr;
use std::time::SystemTime;
use chrono::DateTime;
use chrono::Utc;
use clap::Parser;
use serde::Deserialize;
use serde::Serialize;
use crate::happens_before::HappensBeforeProgram;
use crate::network_trace::NetworkTraceConfig;
use crate::pid::DetTid;
use crate::schedule::SigWrapper;
use crate::time::NANOS_PER_RCB;
use crate::time::RcbTimeMultiplier;
const fn default_true() -> bool {
true
}
#[derive(Debug, Serialize, Deserialize, Clone, Eq, PartialEq)]
pub struct MountInfoRootRewrite {
pub raw_mount_id: u64,
pub deterministic_root: Vec<u8>,
#[serde(default)]
pub raw_root_prefix: Option<Vec<u8>>,
#[serde(default)]
pub deterministic_root_prefix: Option<Vec<u8>>,
#[serde(default)]
pub raw_mountpoint_prefix: Option<Vec<u8>>,
#[serde(default)]
pub deterministic_mountpoint_prefix: Option<Vec<u8>>,
}
#[derive(Debug, Serialize, Deserialize, Clone, Parser)]
pub struct Config {
#[clap(long = "no-virtualize-time", action = clap::ArgAction::SetFalse)]
pub virtualize_time: bool,
#[clap(long = "no-virtualize-cpuid", action = clap::ArgAction::SetFalse)]
pub virtualize_cpuid: bool,
#[serde(default)]
#[clap(skip)]
pub cpuid_virtualized_by_backend: bool,
#[serde(default = "default_true")]
#[clap(skip = true)]
pub backend_supports_madvise: bool,
#[serde(default)]
#[clap(skip)]
pub discover_live_file_metadata: bool,
#[serde(default)]
#[clap(skip)]
pub use_thread_local_clock_reads: bool,
#[serde(default)]
#[clap(skip)]
pub detect_host_clock_futex_timeouts: bool,
#[serde(default)]
#[clap(skip)]
pub syscall_clobbers_virtualized_by_backend: bool,
#[serde(default)]
#[clap(skip)]
pub cancel_killed_thread_rpcs: bool,
#[serde(default)]
#[clap(skip)]
pub backend_reports_physical_process_exits: bool,
#[serde(default)]
#[clap(skip)]
pub backend_serializes_fork_children: bool,
#[serde(default = "default_true")]
#[clap(skip = true)]
pub backend_dispatches_thread_tools: bool,
#[serde(default = "default_true")]
#[clap(skip = true)]
pub backend_tracks_process_children: bool,
#[serde(default = "default_true")]
#[clap(skip = true)]
pub backend_runs_exit_robust_list: bool,
#[serde(default)]
#[clap(skip)]
pub backend_requires_thread_directed_process_signals: bool,
#[serde(default)]
#[clap(skip)]
pub backend_is_kvm: bool,
#[serde(default)]
#[clap(skip)]
pub kvm_shared_dequeue_timers: bool,
#[serde(default = "default_true")]
#[clap(skip = true)]
pub backend_supports_parked_write_signal_interruption: bool,
#[serde(default)]
#[clap(skip)]
pub backend_virtualizes_capability_prctls: bool,
#[serde(default)]
#[clap(skip)]
pub backend_defers_vfork_child_registration: bool,
#[clap(
long,
env = "HERMIT_EPOCH",
value_name = "YYYY-MM-DDThh:mm:ssZ",
default_value = DEFAULT_EPOCH_STR,
hide_default_value = true
)]
pub epoch: DateTime<Utc>,
#[clap(
long = "seed",
env = "HERMIT_PRNG",
default_value = "0",
value_name = "uint64"
)]
pub seed: u64,
#[clap(long, value_name = "uint64")]
pub rng_seed: Option<u64>,
#[clap(long, value_name = "uint64")]
pub fuzz_seed: Option<u64>,
#[clap(long, value_name = "float")]
pub clock_multiplier: Option<f64>,
#[clap(long = "no-virtualize-metadata", action = clap::ArgAction::SetFalse)]
pub virtualize_metadata: bool,
#[serde(default)]
#[clap(skip)]
pub mountinfo_root_rewrites: Vec<MountInfoRootRewrite>,
#[serde(default)]
#[clap(skip)]
pub mountinfo_device_rewrites: Vec<(u64, u64)>,
#[serde(default)]
#[clap(skip)]
pub mountinfo_mount_ids: Vec<u64>,
#[serde(default)]
#[clap(skip)]
pub mountinfo_mount_ids_captured: bool,
#[serde(default)]
#[clap(skip)]
pub fdinfo_unlisted_mount_ids: Vec<u64>,
#[clap(long)]
pub sequentialize_threads: bool,
#[serde(default)]
#[clap(long, default_value = "child", value_name = "child|parent|random")]
pub runs_post_fork: RunsPostFork,
#[serde(default)]
#[clap(long)]
pub passthru_opt: bool,
#[clap(long)]
pub imprecise_timers: bool,
#[clap(long)]
pub chaos: bool,
#[clap(long)]
pub fuzz_futexes: bool,
#[clap(long)]
pub chaos_target_races: bool,
#[clap(long)]
pub chaos_per_thread_slowdown: bool,
#[clap(long, default_value = "10.0", value_name = "double")]
pub chaos_slowdown_max_factor: f64,
#[clap(long, default_value = "0", value_name = "nanos")]
pub chaos_epoch_length_ns: u64,
#[clap(long)]
pub record_preemptions: bool,
#[clap(long, value_name = "filepath")]
pub record_preemptions_to: Option<PathBuf>,
#[clap(long, value_name = "filepath", conflicts_with = "replay_schedule_from")]
pub replay_preemptions_from: Option<PathBuf>,
#[clap(
long,
value_name = "filepath",
conflicts_with = "replay_preemptions_from"
)]
pub replay_schedule_from: Option<PathBuf>,
#[clap(long)]
pub replay_exhausted_panic: bool,
#[clap(long)]
pub die_on_desync: bool,
#[clap(long,
short = 's',
value_name = "index[,path]",
value_parser = parse_index_with_path)]
pub stacktrace_event: Vec<(u64, Option<PathBuf>)>,
#[clap(long, value_name = "signame")]
pub stacktrace_signal: Option<SigWrapper>,
#[clap(long)]
pub preemption_stacktrace: bool,
#[clap(long, value_name = "filepath")]
pub preemption_stacktrace_log_file: Option<PathBuf>,
#[clap(long)]
pub deterministic_io: bool,
#[clap(long)]
pub panic_on_unsupported_syscalls: bool,
#[serde(default)]
#[clap(skip)]
pub exit_on_unsupported_syscall: bool,
#[serde(default)]
#[clap(skip)]
pub shutdown_on_unsupported_syscall: bool,
#[serde(default)]
#[clap(skip)]
pub unsupported_syscall_report_fd: Option<i32>,
#[serde(default)]
#[clap(
long = "panic-on-rbc-overshoot",
visible_alias = "panic-on-rcb-overshoot"
)]
pub panic_on_rcb_overshoot: bool,
#[clap(skip)]
pub has_uts_namespace: bool,
#[clap(skip)]
pub replay_data: Option<PathBuf>,
#[clap(long)]
pub kill_daemons: bool,
#[clap(long)]
pub gdbserver: bool,
#[clap(
long,
value_name = "uint16",
help = "Port gdbserver listening on",
default_value = "1234"
)]
pub gdbserver_port: u16,
#[serde(alias = "preemption_timeout")]
#[clap(
long,
visible_alias = "preemption-timeout",
value_name = "uint64|'disabled'",
default_value = "200000000",
value_parser = parse_timeslice)]
pub max_timeslice: MaybeTimeslice,
#[serde(default)]
#[clap(long, value_name = "virtual-nanoseconds")]
pub target_timeslice: Option<NonZeroU64>,
#[clap(long)]
pub sigint_instakill: bool,
#[clap(long)]
pub warn_non_zero_binds: bool,
#[clap(long, default_value = "none", value_name = "str")]
pub sched_heuristic: SchedHeuristic,
#[clap(long, env = "HERMIT_SCHED_SEED", value_name = "uint64")]
pub sched_seed: Option<u64>,
#[serde(default)]
#[clap(skip)]
pub network_trace: NetworkTraceConfig,
#[clap(long, default_value = "0.0", value_name = "double")]
pub sched_sticky_random_param: f64,
#[clap(skip = false)]
pub recordreplay_modes: bool,
#[clap(long, value_name = "turn_N")]
pub stop_after_turn: Option<u64>,
#[clap(long, value_name = "iter_N")]
pub stop_after_iter: Option<u64>,
#[clap(long)]
pub debug_externalize_sockets: bool,
#[clap(
long,
value_name = "precise|polling|external",
default_value = "precise"
)]
pub debug_futex_mode: BlockingMode,
#[clap(long)]
pub no_rcb_time: bool,
#[clap(long)]
pub detlog_heap: bool,
#[clap(long)]
pub detlog_stack: bool,
#[clap(long)]
pub detlog_regs: bool,
#[clap(long = "no-detlog-io-buffers", action = clap::ArgAction::SetFalse)]
pub detlog_io_buffers: bool,
#[clap(long, default_value = "1", value_name = "uint64")]
pub detlog_regs_cadence: u64,
#[clap(long, default_value = "120", value_name = "uint64")]
pub sysinfo_uptime_offset: u64,
#[clap(long, default_value = "1GB", value_parser = try_parse_memory, value_name = "bytesize")]
pub memory: u64,
#[clap(long, value_name = "tid:rcbs", value_parser = try_parse_numbers_with_colon)]
pub interrupt_at: Vec<(DetTid, u64)>,
#[serde(skip)]
#[clap(skip)]
pub happens_before: Option<HappensBeforeProgram>,
}
fn try_parse_numbers_with_colon(from_str: &str) -> anyhow::Result<(DetTid, u64)> {
if let Some((thread_id_str, time_str)) = from_str.split_once(':') {
Ok((
thread_id_str
.parse::<DetTid>()
.map_err(anyhow::Error::msg)?,
time_str.parse::<u64>().map_err(anyhow::Error::msg)?,
))
} else {
anyhow::bail!(
"unable to parse <thread_id>:<logical_time> from '{}'",
from_str
)
}
}
fn try_parse_memory(from_str: &str) -> anyhow::Result<u64> {
<bytesize::ByteSize as FromStr>::from_str(from_str)
.map(|res| res.as_u64())
.map_err(anyhow::Error::msg)
}
impl Config {
pub fn has_default_epoch(&self) -> bool {
self.epoch == DEFAULT_EPOCH_STR.parse::<DateTime<Utc>>().unwrap()
}
pub fn capture_epoch_from_host_time(&mut self, now: SystemTime) {
self.epoch = epoch_from_host_time(now);
}
pub fn minimum_max_timeslice_nanos(&self) -> u64 {
let slowdown = if self.chaos && self.chaos_per_thread_slowdown {
self.chaos_slowdown_max_factor
} else {
1.0
};
let multiplier = self.clock_multiplier.unwrap_or(1.0) * slowdown;
((NANOS_PER_RCB * multiplier).ceil() as u64).max(NANOS_PER_RCB as u64)
}
pub fn validate_invariants(&self) {
assert!(self.sched_sticky_random_param >= 0.0);
assert!(self.sched_sticky_random_param <= 1.0);
assert!(
self.chaos_slowdown_max_factor.is_finite()
&& self.chaos_slowdown_max_factor >= 1.0
&& self.chaos_slowdown_max_factor <= RcbTimeMultiplier::MAX,
"chaos_slowdown_max_factor must be finite and in [1.0, {}], got {}",
RcbTimeMultiplier::MAX,
self.chaos_slowdown_max_factor
);
if let Some(multiplier) = self.clock_multiplier {
assert!(
multiplier.is_finite() && multiplier > 0.0,
"clock_multiplier must be finite and positive"
);
}
let minimum_max_timeslice = self.minimum_max_timeslice_nanos();
assert!(
self.max_timeslice
.is_none_or(|timeslice| u64::from(timeslice) >= minimum_max_timeslice),
"max_timeslice must be at least one RCB ({} virtual nanoseconds)",
minimum_max_timeslice
);
}
pub fn validate(&mut self) {
self.validate_invariants();
if self.record_preemptions_to.is_some() {
self.record_preemptions = true;
}
if self.replay_schedule_from.is_some() && self.replay_preemptions_from.is_some() {
panic!("Cannot set both --replay-preemptions-from and --replay-schedule-from!!");
}
if self.chaos {
self.sequentialize_threads = true;
}
if self.replay_preemptions_from.is_some() && self.imprecise_timers {
eprintln!(
"WARNING: Setting --imprecise timers with --replay-preemptions-from is probably not what you want. They won't replay precisely."
);
}
if self.stop_after_turn.is_some() && !self.sequentialize_threads {
eprintln!(
"WARNING: --stop-after-turn will have no effect if --no-sequentialize-threads is enabled"
);
self.stop_after_turn = None;
}
if self.stop_after_iter.is_some() && !self.sequentialize_threads {
eprintln!(
"WARNING: --stop-after-iter will have no effect if --no-sequentialize-threads is enabled"
);
self.stop_after_iter = None;
}
if self.debug_externalize_sockets && !self.sequentialize_threads {
eprintln!(
"WARNING: --debug-externalize-sockets will have no effect if --no-sequentialize-threads is enabled"
);
self.debug_externalize_sockets = false;
}
if !self.stacktrace_event.is_empty()
&& !self.record_preemptions
&& self.replay_schedule_from.is_none()
{
eprintln!(
"WARNING: -s/--stacktrace-event has no effect if not recording/replaying events!"
);
}
if self.preemption_stacktrace_log_file.is_some() {
self.preemption_stacktrace = true;
}
}
pub fn use_rcb_time(&self) -> bool {
self.max_timeslice.is_some() && !self.no_rcb_time
}
pub fn use_nonblocking_sockets(&self) -> bool {
self.sequentialize_threads && !self.debug_externalize_sockets
}
pub fn should_trace_schedevent(&self) -> bool {
self.record_preemptions || self.replay_schedule_from.is_some()
}
pub fn interrupts_for_thread(&self, thread_id: DetTid) -> BTreeSet<u64> {
self.interrupt_at
.iter()
.filter_map(|(tid, time)| {
if tid.eq(&thread_id) {
Some(*time)
} else {
None
}
})
.collect::<BTreeSet<u64>>()
}
}
impl fmt::Display for Config {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
if !self.virtualize_time {
write!(f, " --no-virtualize-time")?;
}
if !self.virtualize_cpuid {
write!(f, " --no-virtualize-cpuid")?;
}
if !self.virtualize_metadata {
write!(f, " --no-virtualize-metadata")?;
}
if self.passthru_opt {
write!(f, " --passthru-opt")?;
}
match self.runs_post_fork {
RunsPostFork::Child => {}
RunsPostFork::Parent => write!(f, " --runs-post-fork=parent")?,
RunsPostFork::Random => write!(f, " --runs-post-fork=random")?,
}
if !self.has_default_epoch() {
write!(f, " --epoch={}", self.epoch.to_rfc3339())?;
}
if self.seed != 0 {
write!(f, " --seed={}", self.seed)?;
}
if let Some(rng_seed) = self.rng_seed {
write!(f, " --rng-seed={}", rng_seed)?;
}
if let Some(fuzz_seed) = self.fuzz_seed {
write!(f, " --fuzz-seed={}", fuzz_seed)?;
}
if self.fuzz_futexes {
write!(f, " --fuzz-futexes")?;
}
if self.chaos_target_races {
write!(f, " --chaos-target-races")?;
}
if self.chaos_per_thread_slowdown {
write!(f, " --chaos-per-thread-slowdown")?;
write!(
f,
" --chaos-slowdown-max-factor={}",
self.chaos_slowdown_max_factor
)?;
if self.chaos_epoch_length_ns > 0 {
write!(f, " --chaos-epoch-length-ns={}", self.chaos_epoch_length_ns)?;
}
}
if let Some(m) = self.clock_multiplier {
write!(f, " --clock-multiplier={}", m)?;
}
if self.imprecise_timers {
write!(f, " --imprecise-timers")?;
}
if self.chaos {
write!(f, " --chaos")?;
}
if self.record_preemptions {
write!(f, " --record-preemptions")?;
}
if let Some(p) = &self.record_preemptions_to {
let s = p.to_str().expect("valid unicode path");
write!(f, " --record-preemptions-to={}", shell_words::quote(s))?;
}
if let Some(p) = &self.replay_preemptions_from {
let s = p.to_str().expect("valid unicode path");
write!(f, " --replay-preemptions-from={}", shell_words::quote(s))?;
}
if let Some(p) = &self.replay_schedule_from {
let s = p.to_str().expect("valid unicode path");
write!(f, " --replay-schedule-from={}", shell_words::quote(s))?;
}
if self.replay_exhausted_panic {
write!(f, " --replay-exhausted-panic")?;
}
if self.die_on_desync {
write!(f, " --die-on-desync")?;
}
for (index, path) in &self.stacktrace_event {
write!(f, " --stacktrace-event={}", index)?;
if let Some(p) = path {
let s = p.to_str().expect("valid unicode path");
write!(f, ",{}", shell_words::quote(s))?;
}
}
if self.preemption_stacktrace {
write!(f, " --preemption-stacktrace")?;
}
if self.panic_on_unsupported_syscalls {
write!(f, " --panic-on-unsupported-syscalls")?;
}
if self.panic_on_rcb_overshoot {
write!(f, " --panic-on-rbc-overshoot")?;
}
if self.kill_daemons {
write!(f, " --kill-daemons")?;
}
if self.gdbserver {
write!(f, " --gdbserver")?;
}
if self.gdbserver_port != 1234u16 {
write!(f, " --gdbserver-port={}", self.gdbserver_port)?;
}
match &self.max_timeslice {
Some(x) => {
if *x != NonZeroU64::new(200_000_000).unwrap() {
write!(f, " --max-timeslice={}", x)?;
}
}
None => {
write!(f, " --max-timeslice=disabled")?;
}
}
if let Some(target_timeslice) = self.target_timeslice {
write!(f, " --target-timeslice={}", target_timeslice)?;
}
if self.sigint_instakill {
write!(f, " --sigint-instakill")?;
}
if self.warn_non_zero_binds {
write!(f, " --warn-non-zero-binds")?;
}
match &self.sched_heuristic {
SchedHeuristic::None => {}
SchedHeuristic::ConnectBind => {
write!(f, " --sched-heuristic=connectbind")?;
}
SchedHeuristic::Random => {
write!(f, " --sched-heuristic=random")?;
}
SchedHeuristic::StickyRandom => {
write!(f, " --sched-heuristic=stickyrandom")?;
}
}
if let Some(s) = self.sched_seed {
write!(f, " --sched-seed={}", s)?;
}
if self.sched_sticky_random_param != 0.0 {
write!(
f,
" --sched-sticky-random-param={}",
self.sched_sticky_random_param
)?;
}
if let Some(t) = self.stop_after_turn {
write!(f, " --stop-after-turn={}", t)?;
}
if let Some(i) = self.stop_after_iter {
write!(f, " --stop-after-iter={}", i)?;
}
if self.debug_externalize_sockets {
write!(f, " --debug-externalize-sockets")?;
}
match &self.debug_futex_mode {
BlockingMode::External => {
write!(f, " --debug-futex-mode=external")?;
}
BlockingMode::Polling => {
write!(f, " --debug-futex-mode=polling")?;
}
BlockingMode::Precise => { }
}
if self.no_rcb_time {
write!(f, " --no-rcb-time")?;
}
if self.detlog_heap {
write!(f, " --detlog-heap")?;
}
if self.detlog_stack {
write!(f, " --detlog-stack")?;
}
if self.detlog_regs {
write!(f, " --detlog-regs")?;
}
if self.detlog_regs_cadence != 1 {
write!(f, " --detlog-regs-cadence={}", self.detlog_regs_cadence)?;
}
if !self.detlog_io_buffers {
write!(f, " --no-detlog-io-buffers")?;
}
if self.sysinfo_uptime_offset != 120 {
write!(f, " --sysinfo-uptime-offset={}", self.sysinfo_uptime_offset)?;
}
if self.memory != 1_000_000_000 {
write!(f, " --memory={}", self.memory)?;
}
for (tid, rcb) in &self.interrupt_at {
write!(f, " --interrupt-at={}:{}", tid, rcb)?;
}
Ok(())
}
}
#[derive(
Debug,
Default,
Clone,
Copy,
Serialize,
Deserialize,
Parser,
PartialEq,
Eq
)]
pub enum RunsPostFork {
#[default]
Child,
Parent,
Random,
}
impl FromStr for RunsPostFork {
type Err = String;
fn from_str(s: &str) -> Result<Self, Self::Err> {
match s.to_lowercase().as_str() {
"child" => Ok(Self::Child),
"parent" => Ok(Self::Parent),
"random" => Ok(Self::Random),
_ => Err(format!(
"Expected Child|Parent|Random, could not parse: {:?}",
s
)),
}
}
}
#[derive(Debug, Clone, Copy, Serialize, Deserialize, Parser, PartialEq, Eq)]
pub enum BlockingMode {
External,
Polling,
Precise,
}
impl FromStr for BlockingMode {
type Err = String;
fn from_str(s: &str) -> Result<Self, Self::Err> {
match s.to_lowercase().as_str() {
"polling" => Ok(BlockingMode::Polling),
"precise" => Ok(BlockingMode::Precise),
"external" => Ok(BlockingMode::External),
_ => Err(format!(
"Expected Polling|Precise|External, could not parse: {:?}",
s
)),
}
}
}
#[derive(
Debug,
Default,
Clone,
Copy,
Serialize,
Deserialize,
Parser,
PartialEq,
Eq
)]
pub enum SchedHeuristic {
#[default]
None,
ConnectBind,
Random,
StickyRandom,
}
impl FromStr for SchedHeuristic {
type Err = String;
fn from_str(s: &str) -> Result<Self, Self::Err> {
match s.to_lowercase().as_str() {
"none" | "roundrobin" => Ok(SchedHeuristic::None),
"connectbind" => Ok(SchedHeuristic::ConnectBind),
"random" => Ok(SchedHeuristic::Random),
"stickyrandom" => Ok(SchedHeuristic::StickyRandom),
_ => Err(format!(
"Expected None|ConnectBind|Random|StickyRandom, could not parse: {:?}",
s
)),
}
}
}
pub type MaybeTimeslice = Option<NonZeroU64>;
#[deprecated(note = "use MaybeTimeslice")]
pub type MaybePreemptionTimeout = MaybeTimeslice;
fn parse_timeslice(src: &str) -> Result<MaybeTimeslice, ParseTimesliceError> {
if let Ok(n) = src.parse::<u64>() {
if n != 0 && n < NANOS_PER_RCB as u64 {
Err(ParseTimesliceError::new(
"PMU-backed timeslices must be at least one RCB (10 virtual nanoseconds)",
))
} else {
Ok(NonZeroU64::new(n))
}
} else {
match src {
"disabled" => Ok(None),
_ => Err(ParseTimesliceError::new(
"Unable to parse timeslice, expected disabled or a non-negative integer",
)),
}
}
}
fn parse_index_with_path(src: &str) -> Result<(u64, Option<PathBuf>), String> {
let convert = |e| format!("Failed to parse int index before comma: {e}");
if let Some((index_str, path)) = src.split_once(',') {
let ix = index_str.parse::<u64>().map_err(convert)?;
let pathbuf = PathBuf::from_str(path).map_err(|_| "the impossible happened")?;
Ok((ix, Some(pathbuf)))
} else {
let ix = src.parse::<u64>().map_err(convert)?;
Ok((ix, None))
}
}
#[derive(Debug)]
struct ParseTimesliceError {
details: String,
}
impl fmt::Display for ParseTimesliceError {
fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
write!(f, "{}", self.details)
}
}
impl ParseTimesliceError {
fn new(msg: &str) -> ParseTimesliceError {
ParseTimesliceError {
details: msg.to_string(),
}
}
}
impl std::error::Error for ParseTimesliceError {
fn description(&self) -> &str {
&self.details
}
}
pub static DEFAULT_EPOCH_STR: &str = "2026-01-01T00:00:00Z";
pub fn epoch_from_host_time(now: SystemTime) -> DateTime<Utc> {
DateTime::<Utc>::from(now)
}
impl Config {
pub fn from_env() -> Self {
let args: [OsString; 2] = [
OsString::from("CMD"), OsString::from(format!("--epoch={}", DEFAULT_EPOCH_STR)),
];
Config::parse_from(args.iter())
}
pub fn rng_seed(&self) -> u64 {
self.rng_seed.unwrap_or(self.seed)
}
pub fn fuzz_seed(&self) -> u64 {
self.fuzz_seed.unwrap_or(self.seed)
}
pub fn sched_seed(&self) -> u64 {
self.sched_seed.unwrap_or(self.seed)
}
}
pub const CONFIG_FINGERPRINT_ENV: &str = "REVERIE_SABRE_HERMIT_CONFIG_FINGERPRINT";
const CONFIG_DEFINITION_SOURCES: &[&[u8]] = &[
include_bytes!("config.rs"),
include_bytes!("happens_before.rs"),
include_bytes!("network_trace.rs"),
include_bytes!("pid.rs"),
include_bytes!("schedule.rs"),
include_bytes!("time.rs"),
];
fn config_wire_default() -> Config {
let mut config = Config::parse_from([
"config-wire-fingerprint",
&format!("--epoch={DEFAULT_EPOCH_STR}"),
"--seed=0",
"--sched-seed=0",
]);
config.sched_seed = None;
config
}
pub fn config_wire_fingerprint() -> String {
let config = config_wire_default();
let wire = bincode::serde::encode_to_vec(&config, bincode::config::legacy())
.expect("canonical Config wire default must encode with Reverie's bincode configuration");
let named_shape = serde_json::to_string(&config)
.expect("canonical Config wire default must encode as JSON for field-name checking");
fingerprint_of_config_material(&wire, &named_shape, CONFIG_DEFINITION_SOURCES)
}
fn fingerprint_of_config_material(
wire: &[u8],
named_shape: &str,
definition_sources: &[&[u8]],
) -> String {
let mut hash: u64 = 0xcbf2_9ce4_8422_2325;
let mut update = |domain: u8, bytes: &[u8]| {
for byte in std::iter::once(&domain)
.chain((bytes.len() as u64).to_le_bytes().iter())
.chain(bytes.iter())
{
hash ^= u64::from(*byte);
hash = hash.wrapping_mul(0x1000_0000_01b3);
}
};
update(0, wire);
update(1, named_shape.as_bytes());
for source in definition_sources {
update(2, source);
}
format!("{hash:016x}")
}
impl Default for Config {
fn default() -> Self {
let v: Vec<String> = vec![];
Config::parse_from(v.iter())
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn default_epoch_is_2026() {
assert_eq!(DEFAULT_EPOCH_STR, "2026-01-01T00:00:00Z");
let epoch = DEFAULT_EPOCH_STR.parse::<DateTime<Utc>>().unwrap();
assert_eq!(epoch.timestamp(), 1_767_225_600);
}
#[test]
fn resolved_epoch_is_serialized_as_an_exact_config_input() {
let epoch = "2000-12-31T23:59:59.123456789Z"
.parse::<DateTime<Utc>>()
.unwrap();
let config = Config {
epoch,
..Config::default()
};
let encoded = serde_json::to_value(&config).unwrap();
assert_eq!(
encoded["epoch"],
serde_json::json!("2000-12-31T23:59:59.123456789Z")
);
let decoded: Config = serde_json::from_value(encoded).unwrap();
assert_eq!(decoded.epoch, epoch);
}
#[test]
fn default_backend_capabilities_match_instrumented_backends() {
let config = Config::default();
assert!(!config.backend_reports_physical_process_exits);
assert!(!config.backend_serializes_fork_children);
assert!(config.backend_dispatches_thread_tools);
assert!(config.backend_tracks_process_children);
assert!(config.backend_runs_exit_robust_list);
assert!(!config.backend_requires_thread_directed_process_signals);
assert!(config.backend_supports_parked_write_signal_interruption);
assert!(!config.backend_virtualizes_capability_prctls);
assert!(!config.backend_defers_vfork_child_registration);
}
#[test]
fn network_perturbation_seed_never_falls_back_to_scheduler_seeds() {
let mut config = Config {
seed: 41,
sched_seed: Some(42),
..Config::default()
};
assert_eq!(config.network_trace.network_perturb_seed, None);
config.network_trace = NetworkTraceConfig {
mode: crate::network_trace::NetworkTraceMode::Replay,
path: Some("network.trace".into()),
network_perturb_seed: Some(43),
};
assert_eq!(config.seed, 41);
assert_eq!(config.sched_seed(), 42);
assert_eq!(config.network_trace.network_perturb_seed, Some(43));
}
#[test]
fn missing_mountinfo_provenance_deserializes_as_empty() {
let mut value = serde_json::to_value(Config::default()).unwrap();
value
.as_object_mut()
.unwrap()
.remove("mountinfo_root_rewrites");
value
.as_object_mut()
.unwrap()
.remove("mountinfo_device_rewrites");
value.as_object_mut().unwrap().remove("mountinfo_mount_ids");
value
.as_object_mut()
.unwrap()
.remove("mountinfo_mount_ids_captured");
value
.as_object_mut()
.unwrap()
.remove("fdinfo_unlisted_mount_ids");
let restored: Config = serde_json::from_value(value).unwrap();
assert!(restored.mountinfo_root_rewrites.is_empty());
assert!(restored.mountinfo_device_rewrites.is_empty());
assert!(restored.mountinfo_mount_ids.is_empty());
assert!(!restored.mountinfo_mount_ids_captured);
assert!(restored.fdinfo_unlisted_mount_ids.is_empty());
}
#[test]
fn runs_post_fork_parses_all_modes_and_defaults_to_child() {
assert_eq!(Config::default().runs_post_fork, RunsPostFork::Child);
assert_eq!(
Config::parse_from(["detcore", "--runs-post-fork=parent"]).runs_post_fork,
RunsPostFork::Parent
);
assert_eq!(
Config::parse_from(["detcore", "--runs-post-fork=random"]).runs_post_fork,
RunsPostFork::Random
);
assert!(Config::try_parse_from(["detcore", "--runs-post-fork=invalid"]).is_err());
}
#[test]
fn panic_on_rcb_overshoot_is_opt_in_and_round_trips() {
assert!(!Config::default().panic_on_rcb_overshoot);
let config = Config::parse_from(["detcore", "--panic-on-rbc-overshoot"]);
assert!(config.panic_on_rcb_overshoot);
assert!(config.to_string().contains(" --panic-on-rbc-overshoot"));
let alias = Config::parse_from(["detcore", "--panic-on-rcb-overshoot"]);
assert!(alias.panic_on_rcb_overshoot);
}
#[test]
fn config_display_preserves_nondefault_post_fork_modes() {
let mut config = Config {
runs_post_fork: RunsPostFork::Parent,
..Config::default()
};
assert!(config.to_string().contains(" --runs-post-fork=parent"));
config.runs_post_fork = RunsPostFork::Random;
assert!(config.to_string().contains(" --runs-post-fork=random"));
}
#[test]
fn chaos_per_thread_slowdown_is_opt_in_and_round_trips() {
let dflt = Config::default();
assert!(!dflt.chaos_per_thread_slowdown);
assert_eq!(dflt.chaos_slowdown_max_factor, 10.0);
assert!(!dflt.to_string().contains("--chaos-per-thread-slowdown"));
let config = Config::parse_from([
"detcore",
"--chaos",
"--chaos-per-thread-slowdown",
"--chaos-slowdown-max-factor=4.5",
]);
assert!(config.chaos_per_thread_slowdown);
assert_eq!(config.chaos_slowdown_max_factor, 4.5);
let rendered = config.to_string();
assert!(rendered.contains(" --chaos-per-thread-slowdown"));
assert!(rendered.contains(" --chaos-slowdown-max-factor=4.5"));
let reparsed = Config::parse_from(
std::iter::once("detcore".to_string())
.chain(rendered.split_whitespace().map(String::from)),
);
assert!(reparsed.chaos_per_thread_slowdown);
assert_eq!(reparsed.chaos_slowdown_max_factor, 4.5);
}
#[test]
fn chaos_epoch_length_is_opt_in_and_round_trips() {
let dflt = Config::default();
assert_eq!(dflt.chaos_epoch_length_ns, 0);
assert!(!dflt.to_string().contains("--chaos-epoch-length-ns"));
let config = Config::parse_from([
"detcore",
"--chaos",
"--chaos-per-thread-slowdown",
"--chaos-epoch-length-ns=100000",
]);
assert_eq!(config.chaos_epoch_length_ns, 100000);
let rendered = config.to_string();
assert!(rendered.contains(" --chaos-epoch-length-ns=100000"));
let reparsed = Config::parse_from(
std::iter::once("detcore".to_string())
.chain(rendered.split_whitespace().map(String::from)),
);
assert_eq!(reparsed.chaos_epoch_length_ns, 100000);
let no_slowdown = Config::parse_from(["detcore", "--chaos", "--chaos-epoch-length-ns=100"]);
assert_eq!(no_slowdown.chaos_epoch_length_ns, 100);
assert!(!no_slowdown.to_string().contains("--chaos-epoch-length-ns"));
}
#[test]
#[should_panic(expected = "chaos_slowdown_max_factor must be finite and in")]
fn validate_rejects_chaos_slowdown_max_factor_below_one() {
let mut config = Config {
chaos_slowdown_max_factor: 0.5,
..Default::default()
};
config.validate();
}
#[test]
#[should_panic(expected = "max_timeslice must be at least one RCB")]
fn validate_rejects_max_timeslice_below_one_rcb() {
let mut config = Config {
max_timeslice: NonZeroU64::new(NANOS_PER_RCB as u64 - 1),
..Default::default()
};
config.validate();
}
#[test]
fn validate_accepts_one_rcb_max_timeslice() {
let mut config = Config {
max_timeslice: NonZeroU64::new(NANOS_PER_RCB as u64),
..Default::default()
};
config.validate();
}
#[test]
#[should_panic(expected = "clock_multiplier must be finite and positive")]
fn validate_rejects_invalid_clock_multiplier() {
let mut config = Config {
clock_multiplier: Some(0.0),
..Default::default()
};
config.validate();
}
#[test]
#[should_panic(expected = "max_timeslice must be at least one RCB")]
fn validate_scales_one_rcb_minimum_with_clock_multiplier() {
let mut config = Config {
max_timeslice: NonZeroU64::new(10),
clock_multiplier: Some(2.0),
..Default::default()
};
config.validate();
}
#[test]
fn config_fingerprint_includes_clock_rpc_definitions() {
let config = config_wire_default();
let wire = bincode::serde::encode_to_vec(&config, bincode::config::legacy()).unwrap();
let named_shape = serde_json::to_string(&config).unwrap();
let current = config_wire_fingerprint();
let clock_source = include_bytes!("time.rs").as_slice();
let without_clock: Vec<_> = CONFIG_DEFINITION_SOURCES
.iter()
.copied()
.filter(|source| *source != clock_source)
.collect();
assert_ne!(
fingerprint_of_config_material(&wire, &named_shape, &without_clock),
current,
"the published fingerprint must reject source inputs that omit the RPC clock"
);
let changed_clock =
include_str!("time.rs").replacen(" inherited_nanos: LogicalDuration,", "", 1);
assert_ne!(changed_clock.as_bytes(), clock_source);
let changed_sources: Vec<_> = CONFIG_DEFINITION_SOURCES
.iter()
.map(|source| {
if *source == clock_source {
changed_clock.as_bytes()
} else {
*source
}
})
.collect();
assert_ne!(
fingerprint_of_config_material(&wire, &named_shape, &changed_sources),
current,
"a clock-only serialized field change must invalidate the fingerprint"
);
}
#[test]
fn config_fingerprint_is_stable_and_shape_sensitive() {
assert_eq!(config_wire_fingerprint(), config_wire_fingerprint());
assert_eq!(config_wire_fingerprint().len(), 16);
let config = config_wire_default();
let base = serde_json::to_string(&config).unwrap();
let wire = bincode::serde::encode_to_vec(&config, bincode::config::legacy()).unwrap();
assert_eq!(
fingerprint_of_config_material(&wire, &base, CONFIG_DEFINITION_SOURCES),
config_wire_fingerprint()
);
let with_extra_field = format!("{},\"a_new_flag\":false}}", &base[..base.len() - 1]);
assert_ne!(
fingerprint_of_config_material(&wire, &with_extra_field, CONFIG_DEFINITION_SOURCES),
config_wire_fingerprint()
);
let removed = base.replacen("\"virtualize_time\":true,", "", 1);
assert_ne!(
fingerprint_of_config_material(&wire, &removed, CONFIG_DEFINITION_SOURCES),
config_wire_fingerprint()
);
let renamed = base.replacen("\"virtualize_time\"", "\"virtualise_time\"", 1);
assert_ne!(
fingerprint_of_config_material(&wire, &renamed, CONFIG_DEFINITION_SOURCES),
config_wire_fingerprint()
);
#[derive(Serialize)]
struct U32Field {
field: u32,
}
#[derive(Serialize)]
struct U64Field {
field: u64,
}
let u32_value = U32Field { field: 0 };
let u64_value = U64Field { field: 0 };
let u32_json = serde_json::to_string(&u32_value).unwrap();
let u64_json = serde_json::to_string(&u64_value).unwrap();
assert_eq!(
u32_json, u64_json,
"the planted JSON collision must be real"
);
let u32_wire =
bincode::serde::encode_to_vec(&u32_value, bincode::config::legacy()).unwrap();
let u64_wire =
bincode::serde::encode_to_vec(&u64_value, bincode::config::legacy()).unwrap();
assert_ne!(u32_wire, u64_wire, "the planted wire retype must be real");
assert_ne!(
fingerprint_of_config_material(&u32_wire, &u32_json, &[b"struct S { field: u32 }"]),
fingerprint_of_config_material(&u64_wire, &u64_json, &[b"struct S { field: u64 }"]),
"a wire-incompatible integer retype must change the fingerprint"
);
#[derive(Serialize)]
struct OptionalU32 {
field: Option<u32>,
}
#[derive(Serialize)]
struct OptionalU64 {
field: Option<u64>,
}
let optional_u32 = OptionalU32 { field: None };
let optional_u64 = OptionalU64 { field: None };
let optional_u32_json = serde_json::to_string(&optional_u32).unwrap();
let optional_u64_json = serde_json::to_string(&optional_u64).unwrap();
assert_eq!(optional_u32_json, optional_u64_json);
let optional_u32_wire =
bincode::serde::encode_to_vec(&optional_u32, bincode::config::legacy()).unwrap();
let optional_u64_wire =
bincode::serde::encode_to_vec(&optional_u64, bincode::config::legacy()).unwrap();
assert_eq!(
optional_u32_wire, optional_u64_wire,
"the planted default must be invisible in both value encodings"
);
assert_ne!(
fingerprint_of_config_material(
&optional_u32_wire,
&optional_u32_json,
&[b"struct S { field: Option<u32> }"]
),
fingerprint_of_config_material(
&optional_u64_wire,
&optional_u64_json,
&[b"struct S { field: Option<u64> }"]
),
"a hidden wire-incompatible inner-type change must alter the fingerprint"
);
}
#[test]
fn config_fingerprint_uses_environment_free_wire_defaults() {
let environment_derived = Config {
epoch: "2042-03-04T05:06:07.890123456Z".parse().unwrap(),
seed: 41,
sched_seed: Some(42),
..Config::default()
};
let canonical = config_wire_default();
assert_ne!(
serde_json::to_string(&environment_derived).unwrap(),
serde_json::to_string(&canonical).unwrap()
);
assert_eq!(
canonical.epoch,
DEFAULT_EPOCH_STR.parse::<DateTime<Utc>>().unwrap()
);
assert_eq!(canonical.seed, 0);
assert_eq!(canonical.sched_seed, None);
}
#[test]
#[should_panic(expected = "max_timeslice must be at least one RCB")]
fn validate_scales_one_rcb_minimum_with_chaos_slowdown() {
let mut config = Config {
chaos: true,
chaos_per_thread_slowdown: true,
chaos_slowdown_max_factor: 4.0,
max_timeslice: NonZeroU64::new(39),
..Default::default()
};
config.validate();
}
#[test]
#[should_panic(expected = "chaos_slowdown_max_factor must be finite and in")]
fn validate_rejects_unrepresentable_chaos_slowdown_factor() {
let mut config = Config {
chaos_slowdown_max_factor: RcbTimeMultiplier::MAX * 2.0,
..Default::default()
};
config.validate();
}
}