extern crate alloc;
use alloc::string::String;
use alloc::vec::Vec;
use core::fmt;
use serde::{Deserialize, Serialize};
#[derive(Clone, Serialize, Deserialize)]
#[allow(clippy::large_enum_variant)]
pub enum Outcome {
Pass {
leak_probability: f64,
effect: EffectEstimate,
samples_used: usize,
quality: MeasurementQuality,
diagnostics: Diagnostics,
theta_user: f64,
theta_eff: f64,
theta_floor: f64,
},
Fail {
leak_probability: f64,
effect: EffectEstimate,
exploitability: Exploitability,
samples_used: usize,
quality: MeasurementQuality,
diagnostics: Diagnostics,
theta_user: f64,
theta_eff: f64,
theta_floor: f64,
},
Inconclusive {
reason: InconclusiveReason,
leak_probability: f64,
effect: EffectEstimate,
samples_used: usize,
quality: MeasurementQuality,
diagnostics: Diagnostics,
theta_user: f64,
theta_eff: f64,
theta_floor: f64,
},
Unmeasurable {
operation_ns: f64,
threshold_ns: f64,
platform: String,
recommendation: String,
},
Research(ResearchOutcome),
}
#[derive(Debug, Clone, Serialize, Deserialize, PartialEq)]
pub enum InconclusiveReason {
DataTooNoisy {
message: String,
guidance: String,
},
NotLearning {
message: String,
guidance: String,
},
WouldTakeTooLong {
estimated_time_secs: f64,
samples_needed: usize,
guidance: String,
},
TimeBudgetExceeded {
current_probability: f64,
samples_collected: usize,
},
SampleBudgetExceeded {
current_probability: f64,
samples_collected: usize,
},
ConditionsChanged {
message: String,
guidance: String,
},
ThresholdElevated {
theta_user: f64,
theta_eff: f64,
leak_probability_at_eff: f64,
meets_pass_criterion_at_eff: bool,
achievable_at_max: bool,
message: String,
guidance: String,
},
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct EffectEstimate {
pub max_effect_ns: f64,
pub credible_interval_ns: (f64, f64),
pub top_quantiles: Vec<TopQuantile>,
}
impl EffectEstimate {
pub fn new(
max_effect_ns: f64,
credible_interval_ns: (f64, f64),
top_quantiles: Vec<TopQuantile>,
) -> Self {
Self {
max_effect_ns,
credible_interval_ns,
top_quantiles,
}
}
pub fn is_negligible(&self, threshold_ns: f64) -> bool {
self.max_effect_ns.abs() < threshold_ns
}
pub fn total_effect_ns(&self) -> f64 {
self.max_effect_ns
}
}
impl Default for EffectEstimate {
fn default() -> Self {
Self {
max_effect_ns: 0.0,
credible_interval_ns: (0.0, 0.0),
top_quantiles: Vec::new(),
}
}
}
#[derive(Debug, Clone, Copy, Serialize, Deserialize, PartialEq, Eq)]
pub enum Exploitability {
SharedHardwareOnly,
Http2Multiplexing,
StandardRemote,
ObviousLeak,
}
impl Exploitability {
pub fn from_effect_ns(effect_ns: f64) -> Self {
let effect_ns = effect_ns.abs();
if effect_ns < 10.0 {
Exploitability::SharedHardwareOnly
} else if effect_ns < 100.0 {
Exploitability::Http2Multiplexing
} else if effect_ns < 10_000.0 {
Exploitability::StandardRemote
} else {
Exploitability::ObviousLeak
}
}
}
#[derive(Debug, Clone, Copy, Serialize, Deserialize, PartialEq, Eq)]
pub enum MeasurementQuality {
Excellent,
Good,
Poor,
TooNoisy,
}
impl MeasurementQuality {
pub fn from_mde_ns(mde_ns: f64) -> Self {
if mde_ns <= 0.01 || !mde_ns.is_finite() {
return MeasurementQuality::TooNoisy;
}
if mde_ns < 5.0 {
MeasurementQuality::Excellent
} else if mde_ns < 20.0 {
MeasurementQuality::Good
} else if mde_ns < 100.0 {
MeasurementQuality::Poor
} else {
MeasurementQuality::TooNoisy
}
}
}
#[derive(Debug, Clone, Serialize, Deserialize, PartialEq)]
pub enum ResearchStatus {
EffectDetected,
NoEffectDetected,
ResolutionLimitReached,
QualityIssue(InconclusiveReason),
BudgetExhausted,
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct ResearchOutcome {
pub status: ResearchStatus,
pub max_effect_ns: f64,
pub max_effect_ci: (f64, f64),
pub theta_floor: f64,
pub detectable: bool,
pub model_mismatch: bool,
pub effect: EffectEstimate,
pub samples_used: usize,
pub quality: MeasurementQuality,
pub diagnostics: Diagnostics,
}
impl ResearchOutcome {
pub fn is_effect_detected(&self) -> bool {
matches!(self.status, ResearchStatus::EffectDetected)
}
pub fn is_no_effect_detected(&self) -> bool {
matches!(self.status, ResearchStatus::NoEffectDetected)
}
pub fn is_resolution_limit_reached(&self) -> bool {
matches!(self.status, ResearchStatus::ResolutionLimitReached)
}
pub fn has_quality_issue(&self) -> bool {
matches!(self.status, ResearchStatus::QualityIssue(_))
}
pub fn effect(&self) -> &EffectEstimate {
&self.effect
}
pub fn quality(&self) -> MeasurementQuality {
self.quality
}
pub fn diagnostics(&self) -> &Diagnostics {
&self.diagnostics
}
}
impl fmt::Display for ResearchStatus {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
match self {
ResearchStatus::EffectDetected => write!(f, "effect detected"),
ResearchStatus::NoEffectDetected => write!(f, "no effect detected"),
ResearchStatus::ResolutionLimitReached => write!(f, "resolution limit reached"),
ResearchStatus::QualityIssue(reason) => write!(f, "quality issue: {}", reason),
ResearchStatus::BudgetExhausted => write!(f, "budget exhausted"),
}
}
}
impl fmt::Display for ResearchOutcome {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
writeln!(f, "Research Mode: {}", self.status)?;
writeln!(
f,
" Max effect: {:.2}ns (CI: {:.2}-{:.2}ns)",
self.max_effect_ns, self.max_effect_ci.0, self.max_effect_ci.1
)?;
writeln!(f, " Measurement floor: {:.2}ns", self.theta_floor)?;
writeln!(
f,
" Detectable: {}",
if self.detectable { "yes" } else { "no" }
)?;
if self.model_mismatch {
writeln!(f, " Warning: model mismatch detected")?;
}
writeln!(f, " Samples: {}", self.samples_used)?;
writeln!(f, " Quality: {}", self.quality)?;
Ok(())
}
}
#[derive(Debug, Clone, Serialize, Deserialize, PartialEq)]
pub struct TopQuantile {
pub quantile_p: f64,
pub mean_ns: f64,
pub ci95_ns: (f64, f64),
pub exceed_prob: f64,
}
impl TopQuantile {
pub fn new(quantile_p: f64, mean_ns: f64, ci95_ns: (f64, f64), exceed_prob: f64) -> Self {
Self {
quantile_p,
mean_ns,
ci95_ns,
exceed_prob,
}
}
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct Diagnostics {
pub dependence_length: usize,
pub effective_sample_size: usize,
pub stationarity_ratio: f64,
pub stationarity_ok: bool,
pub outlier_rate_baseline: f64,
pub outlier_rate_sample: f64,
pub outlier_asymmetry_ok: bool,
pub discrete_mode: bool,
pub timer_resolution_ns: f64,
pub duplicate_fraction: f64,
pub preflight_ok: bool,
pub calibration_samples: usize,
pub total_time_secs: f64,
pub warnings: Vec<String>,
pub quality_issues: Vec<QualityIssue>,
pub preflight_warnings: Vec<PreflightWarningInfo>,
pub seed: Option<u64>,
pub attacker_model: Option<String>,
pub threshold_ns: f64,
pub timer_name: String,
pub platform: String,
#[serde(skip_serializing_if = "Option::is_none")]
pub timer_fallback_reason: Option<String>,
pub gibbs_iters_total: usize,
pub gibbs_burnin: usize,
pub gibbs_retained: usize,
pub lambda_mean: f64,
pub lambda_sd: f64,
pub lambda_cv: f64,
pub lambda_ess: f64,
pub lambda_mixing_ok: bool,
pub kappa_mean: f64,
pub kappa_sd: f64,
pub kappa_cv: f64,
pub kappa_ess: f64,
pub kappa_mixing_ok: bool,
}
impl Diagnostics {
pub fn all_ok() -> Self {
Self {
dependence_length: 1,
effective_sample_size: 0,
stationarity_ratio: 1.0,
stationarity_ok: true,
outlier_rate_baseline: 0.0,
outlier_rate_sample: 0.0,
outlier_asymmetry_ok: true,
discrete_mode: false,
timer_resolution_ns: 1.0,
duplicate_fraction: 0.0,
preflight_ok: true,
calibration_samples: 0,
total_time_secs: 0.0,
warnings: Vec::new(),
quality_issues: Vec::new(),
preflight_warnings: Vec::new(),
seed: None,
attacker_model: None,
threshold_ns: 0.0,
timer_name: String::new(),
platform: String::new(),
timer_fallback_reason: None,
gibbs_iters_total: 256,
gibbs_burnin: 64,
gibbs_retained: 192,
lambda_mean: 1.0,
lambda_sd: 0.0,
lambda_cv: 0.0,
lambda_ess: 0.0,
lambda_mixing_ok: true,
kappa_mean: 1.0,
kappa_sd: 0.0,
kappa_cv: 0.0,
kappa_ess: 0.0,
kappa_mixing_ok: true,
}
}
pub fn all_checks_passed(&self) -> bool {
self.stationarity_ok && self.outlier_asymmetry_ok && self.preflight_ok
}
}
impl Default for Diagnostics {
fn default() -> Self {
Self::all_ok()
}
}
#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)]
pub struct QualityIssue {
pub code: IssueCode,
pub message: String,
pub guidance: String,
}
#[derive(Debug, Clone, Copy, Serialize, Deserialize, PartialEq, Eq, Hash)]
pub enum IssueCode {
DependenceHigh,
PrecisionLow,
DiscreteMode,
ThresholdIssue,
FilteringApplied,
StationarityIssue,
NumericalIssue,
LikelihoodInflated,
}
#[derive(Debug, Clone, Copy, Serialize, Deserialize, PartialEq, Eq)]
pub enum PreflightSeverity {
Informational,
ResultUndermining,
}
#[derive(Debug, Clone, Copy, Serialize, Deserialize, PartialEq, Eq, Hash)]
pub enum PreflightCategory {
TimerSanity,
Sanity,
Autocorrelation,
System,
Resolution,
Stationarity,
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct PreflightWarningInfo {
pub category: PreflightCategory,
pub severity: PreflightSeverity,
pub message: String,
pub guidance: Option<String>,
}
impl PreflightWarningInfo {
pub fn new(
category: PreflightCategory,
severity: PreflightSeverity,
message: impl Into<String>,
) -> Self {
Self {
category,
severity,
message: message.into(),
guidance: None,
}
}
pub fn with_guidance(
category: PreflightCategory,
severity: PreflightSeverity,
message: impl Into<String>,
guidance: impl Into<String>,
) -> Self {
Self {
category,
severity,
message: message.into(),
guidance: Some(guidance.into()),
}
}
pub fn is_result_undermining(&self) -> bool {
self.severity == PreflightSeverity::ResultUndermining
}
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct MinDetectableEffect {
pub mde_ns: f64,
}
impl MinDetectableEffect {
pub fn new(mde_ns: f64) -> Self {
Self { mde_ns }
}
}
impl Default for MinDetectableEffect {
fn default() -> Self {
Self {
mde_ns: f64::INFINITY,
}
}
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct BatchingInfo {
pub enabled: bool,
pub k: u32,
pub ticks_per_batch: f64,
pub rationale: String,
pub unmeasurable: Option<UnmeasurableInfo>,
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct UnmeasurableInfo {
pub operation_ns: f64,
pub threshold_ns: f64,
pub ticks_per_call: f64,
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct Metadata {
pub samples_per_class: usize,
pub cycles_per_ns: f64,
pub timer: String,
pub timer_resolution_ns: f64,
pub batching: BatchingInfo,
pub runtime_secs: f64,
}
#[derive(Debug, Clone, Copy, Default, PartialEq, Eq)]
pub enum UnreliablePolicy {
#[default]
FailOpen,
FailClosed,
}
impl UnreliablePolicy {
#[cfg(feature = "std")]
pub fn from_env_or(default: Self) -> Self {
match std::env::var("TIMING_ORACLE_UNRELIABLE_POLICY")
.ok()
.as_deref()
{
Some("fail_open") | Some("skip") => Self::FailOpen,
Some("fail_closed") | Some("panic") => Self::FailClosed,
_ => default,
}
}
#[cfg(not(feature = "std"))]
pub fn from_env_or(default: Self) -> Self {
default
}
}
impl Outcome {
pub fn passed(&self) -> bool {
matches!(self, Outcome::Pass { .. })
}
pub fn failed(&self) -> bool {
matches!(self, Outcome::Fail { .. })
}
pub fn is_conclusive(&self) -> bool {
matches!(self, Outcome::Pass { .. } | Outcome::Fail { .. })
}
pub fn is_measurable(&self) -> bool {
!matches!(self, Outcome::Unmeasurable { .. })
}
pub fn leak_probability(&self) -> Option<f64> {
match self {
Outcome::Pass {
leak_probability, ..
} => Some(*leak_probability),
Outcome::Fail {
leak_probability, ..
} => Some(*leak_probability),
Outcome::Inconclusive {
leak_probability, ..
} => Some(*leak_probability),
Outcome::Unmeasurable { .. } => None,
Outcome::Research(_) => None, }
}
pub fn effect(&self) -> Option<&EffectEstimate> {
match self {
Outcome::Pass { effect, .. } => Some(effect),
Outcome::Fail { effect, .. } => Some(effect),
Outcome::Inconclusive { effect, .. } => Some(effect),
Outcome::Unmeasurable { .. } => None,
Outcome::Research(res) => Some(&res.effect),
}
}
pub fn quality(&self) -> Option<MeasurementQuality> {
match self {
Outcome::Pass { quality, .. } => Some(*quality),
Outcome::Fail { quality, .. } => Some(*quality),
Outcome::Inconclusive { quality, .. } => Some(*quality),
Outcome::Unmeasurable { .. } => None,
Outcome::Research(res) => Some(res.quality),
}
}
pub fn diagnostics(&self) -> Option<&Diagnostics> {
match self {
Outcome::Pass { diagnostics, .. } => Some(diagnostics),
Outcome::Fail { diagnostics, .. } => Some(diagnostics),
Outcome::Inconclusive { diagnostics, .. } => Some(diagnostics),
Outcome::Unmeasurable { .. } => None,
Outcome::Research(res) => Some(&res.diagnostics),
}
}
pub fn samples_used(&self) -> Option<usize> {
match self {
Outcome::Pass { samples_used, .. } => Some(*samples_used),
Outcome::Fail { samples_used, .. } => Some(*samples_used),
Outcome::Inconclusive { samples_used, .. } => Some(*samples_used),
Outcome::Unmeasurable { .. } => None,
Outcome::Research(res) => Some(res.samples_used),
}
}
pub fn is_reliable(&self) -> bool {
match self {
Outcome::Unmeasurable { .. } => false,
Outcome::Inconclusive { .. } => false,
Outcome::Pass {
quality,
leak_probability,
..
} => *quality != MeasurementQuality::TooNoisy || *leak_probability < 0.01,
Outcome::Fail {
quality,
leak_probability,
..
} => *quality != MeasurementQuality::TooNoisy || *leak_probability > 0.99,
Outcome::Research(res) => {
matches!(
res.status,
ResearchStatus::EffectDetected | ResearchStatus::NoEffectDetected
)
}
}
}
pub fn unwrap_pass(self) -> (f64, EffectEstimate, MeasurementQuality, Diagnostics) {
match self {
Outcome::Pass {
leak_probability,
effect,
quality,
diagnostics,
..
} => (leak_probability, effect, quality, diagnostics),
_ => panic!("Expected Pass outcome, got {:?}", self),
}
}
pub fn unwrap_fail(
self,
) -> (
f64,
EffectEstimate,
Exploitability,
MeasurementQuality,
Diagnostics,
) {
match self {
Outcome::Fail {
leak_probability,
effect,
exploitability,
quality,
diagnostics,
..
} => (
leak_probability,
effect,
exploitability,
quality,
diagnostics,
),
_ => panic!("Expected Fail outcome, got {:?}", self),
}
}
#[cfg(feature = "std")]
pub fn handle_unreliable(self, test_name: &str, policy: UnreliablePolicy) -> Option<Self> {
if self.is_reliable() {
return Some(self);
}
let reason = match &self {
Outcome::Unmeasurable { recommendation, .. } => {
format!("unmeasurable: {}", recommendation)
}
Outcome::Inconclusive { reason, .. } => {
format!("inconclusive: {:?}", reason)
}
Outcome::Pass { quality, .. } | Outcome::Fail { quality, .. } => {
format!("unreliable quality: {:?}", quality)
}
Outcome::Research(research) => {
format!("research mode: {:?}", research.status)
}
};
match policy {
UnreliablePolicy::FailOpen => {
eprintln!("[SKIPPED] {}: {} (fail-open policy)", test_name, reason);
None
}
UnreliablePolicy::FailClosed => {
panic!("[FAILED] {}: {} (fail-closed policy)", test_name, reason);
}
}
}
#[cfg(not(feature = "std"))]
pub fn handle_unreliable(self, _test_name: &str, policy: UnreliablePolicy) -> Option<Self> {
if self.is_reliable() {
return Some(self);
}
match policy {
UnreliablePolicy::FailOpen => None,
UnreliablePolicy::FailClosed => {
panic!("Unreliable result with fail-closed policy");
}
}
}
}
impl fmt::Display for Outcome {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
write!(f, "{}", crate::formatting::format_outcome_plain(self))
}
}
impl fmt::Display for Exploitability {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
match self {
Exploitability::SharedHardwareOnly => write!(f, "shared hardware only"),
Exploitability::Http2Multiplexing => write!(f, "HTTP/2 multiplexing"),
Exploitability::StandardRemote => write!(f, "standard remote"),
Exploitability::ObviousLeak => write!(f, "obvious leak"),
}
}
}
impl fmt::Display for MeasurementQuality {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
match self {
MeasurementQuality::Excellent => write!(f, "excellent"),
MeasurementQuality::Good => write!(f, "good"),
MeasurementQuality::Poor => write!(f, "poor"),
MeasurementQuality::TooNoisy => write!(f, "too noisy"),
}
}
}
impl fmt::Display for InconclusiveReason {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
match self {
InconclusiveReason::DataTooNoisy { message, guidance } => {
write!(f, "Data too noisy: {}\n \u{2192} {}", message, guidance)
}
InconclusiveReason::NotLearning { message, guidance } => {
write!(f, "Not learning: {}\n \u{2192} {}", message, guidance)
}
InconclusiveReason::WouldTakeTooLong {
estimated_time_secs,
samples_needed,
guidance,
} => {
write!(
f,
"Would take too long: ~{:.0}s / {} samples needed\n \u{2192} {}",
estimated_time_secs, samples_needed, guidance
)
}
InconclusiveReason::TimeBudgetExceeded { .. } => {
write!(f, "Time budget exceeded")
}
InconclusiveReason::SampleBudgetExceeded { .. } => {
write!(f, "Sample budget exceeded")
}
InconclusiveReason::ConditionsChanged { message, guidance } => {
write!(
f,
"Conditions changed: {}\n \u{2192} {}",
message, guidance
)
}
InconclusiveReason::ThresholdElevated {
theta_user,
theta_eff,
leak_probability_at_eff,
achievable_at_max,
guidance,
..
} => {
let achievability = if *achievable_at_max {
"achievable with more samples"
} else {
"not achievable at max samples"
};
write!(
f,
"Threshold elevated: requested {:.1}ns, used {:.1}ns (P={:.1}% at θ_eff, {})\n \u{2192} {}",
theta_user, theta_eff, leak_probability_at_eff * 100.0, achievability, guidance
)
}
}
}
}
impl fmt::Debug for Outcome {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
write!(f, "{}", crate::formatting::format_debug_summary_plain(self))
}
}