tacet/config.rs
1//! Configuration for adaptive Bayesian timing analysis.
2//!
3//! See spec Section 4.2 (Configuration) for the full specification.
4
5use std::time::Duration;
6
7use crate::constants::{
8 DEFAULT_BATCH_SIZE, DEFAULT_BOOTSTRAP_ITERATIONS, DEFAULT_CALIBRATION_SAMPLES,
9 DEFAULT_FAIL_THRESHOLD, DEFAULT_MAX_SAMPLES, DEFAULT_PASS_THRESHOLD, DEFAULT_TIME_BUDGET_SECS,
10};
11use crate::types::AttackerModel;
12
13/// Configuration options for `TimingOracle`.
14///
15/// The adaptive Bayesian oracle uses these settings to control the
16/// analysis behavior, thresholds, and resource limits.
17///
18/// See spec Section 4.2 (Configuration).
19#[derive(Debug, Clone)]
20pub struct Config {
21 // =========================================================================
22 // Decision thresholds (new for adaptive Bayesian)
23 // =========================================================================
24 /// Threshold for declaring "Pass" (no leak detected).
25 ///
26 /// If the posterior probability of a timing leak falls below this threshold,
27 /// the test passes. Default: 0.05 (5%).
28 ///
29 /// Lower values require more confidence to pass (more conservative).
30 pub pass_threshold: f64,
31
32 /// Threshold for declaring "Fail" (leak detected).
33 ///
34 /// If the posterior probability of a timing leak exceeds this threshold,
35 /// the test fails. Default: 0.95 (95%).
36 ///
37 /// Higher values require more confidence to fail (more conservative).
38 pub fail_threshold: f64,
39
40 // =========================================================================
41 // Resource limits
42 // =========================================================================
43 /// Maximum time budget for the analysis.
44 ///
45 /// The oracle will stop collecting samples and return Inconclusive if this
46 /// time limit is reached. Default: 60 seconds.
47 pub time_budget: Duration,
48
49 /// Maximum number of samples to collect per class.
50 ///
51 /// The oracle will stop and return Inconclusive if this limit is reached
52 /// without achieving a conclusive result. Default: 1,000,000.
53 pub max_samples: usize,
54
55 /// Number of samples to collect per batch during adaptive sampling.
56 ///
57 /// Larger batches are more efficient but less responsive to early stopping.
58 /// Default: 1,000.
59 pub batch_size: usize,
60
61 /// Number of samples for initial calibration (covariance estimation).
62 ///
63 /// This fixed number of samples is collected before the adaptive phase
64 /// begins. Used to estimate the noise covariance matrix. Default: 5,000.
65 ///
66 /// Note: This is a fixed overhead, not prominently configurable.
67 pub calibration_samples: usize,
68
69 // =========================================================================
70 // Effect thresholds (attacker model)
71 // =========================================================================
72 /// Minimum effect size we care about in nanoseconds.
73 ///
74 /// Effects smaller than this won't trigger high posterior probabilities
75 /// even if statistically detectable. This encodes practical relevance.
76 ///
77 /// Note: When `attacker_model` is set, this value may be overridden
78 /// at runtime based on the attacker model's threshold.
79 ///
80 /// Default: 10.0 ns.
81 pub min_effect_of_concern_ns: f64,
82
83 /// Attacker model preset.
84 ///
85 /// When set, the attacker model's threshold is used instead of
86 /// `min_effect_of_concern_ns`. The threshold is computed at runtime
87 /// based on the timer's resolution and CPU frequency.
88 ///
89 /// See [`AttackerModel`] for available presets.
90 ///
91 /// Default: None (uses min_effect_of_concern_ns).
92 pub attacker_model: Option<AttackerModel>,
93
94 /// Optional hard effect threshold in nanoseconds for reporting/panic.
95 ///
96 /// If the detected effect exceeds this threshold, the result is flagged
97 /// prominently. Default: None.
98 pub effect_threshold_ns: Option<f64>,
99
100 // =========================================================================
101 // Measurement configuration
102 // =========================================================================
103 /// Warmup iterations before measurement.
104 ///
105 /// These iterations warm CPU caches, stabilize frequency scaling, and
106 /// trigger any JIT compilation before actual measurement begins.
107 /// Default: 1,000.
108 pub warmup: usize,
109
110 /// Percentile for outlier winsorization.
111 ///
112 /// Samples beyond this percentile are capped (not dropped) to reduce
113 /// the impact of extreme outliers while preserving information about
114 /// tail-heavy distributions. Set to 1.0 to disable.
115 ///
116 /// Default: 0.9999 (99.99th percentile).
117 pub outlier_percentile: f64,
118
119 /// Iterations per timing sample.
120 ///
121 /// When set to `Auto`, the library detects timer resolution and
122 /// automatically batches iterations when needed for coarse timers.
123 /// Set to a specific value to override auto-detection.
124 ///
125 /// Default: Auto.
126 pub iterations_per_sample: IterationsPerSample,
127
128 /// Pin the measurement thread to its current CPU core.
129 ///
130 /// Reduces timing noise from thread migration between cores, which can
131 /// cause cache invalidation and expose different core frequencies.
132 /// Enabled by default.
133 ///
134 /// - **Linux**: Enforced via `sched_setaffinity` (no privileges needed)
135 /// - **macOS**: Advisory hint via `thread_policy_set` (kernel may ignore)
136 ///
137 /// Set to `false` if CPU pinning causes issues on your system.
138 ///
139 /// Default: true.
140 pub cpu_affinity: bool,
141
142 /// Elevate thread priority during measurement.
143 ///
144 /// Attempts to reduce preemption by other processes by raising the
145 /// measurement thread's priority. This is best-effort and fails silently
146 /// if privileges are insufficient.
147 ///
148 /// - **Linux**: Lowers nice value and sets `SCHED_BATCH` policy
149 /// - **macOS**: Lowers nice value and sets thread precedence hint
150 ///
151 /// Set to `false` if priority elevation causes issues on your system.
152 ///
153 /// Default: true.
154 pub thread_priority: bool,
155
156 /// Duration of frequency stabilization spin-wait in milliseconds.
157 ///
158 /// Before measurement begins, a brief busy-wait loop runs to let the CPU
159 /// frequency ramp up and stabilize. Many CPUs start in low-power mode and
160 /// take several milliseconds to reach their turbo/boost frequency.
161 ///
162 /// Set to `0` to disable frequency stabilization.
163 ///
164 /// Default: 5 ms.
165 pub frequency_stabilization_ms: u64,
166
167 // =========================================================================
168 // Bayesian inference configuration
169 // =========================================================================
170 /// Prior probability of no leak.
171 ///
172 /// This is the prior belief that the code under test is constant-time.
173 /// Higher values make the test more conservative (harder to fail).
174 ///
175 /// Default: 0.75 (75% prior belief in no leak).
176 pub prior_no_leak: f64,
177
178 /// Bootstrap iterations for covariance estimation.
179 ///
180 /// Used during the calibration phase to estimate the noise covariance
181 /// matrix via block bootstrap. More iterations give better estimates
182 /// but take longer.
183 ///
184 /// Default: 2,000.
185 pub cov_bootstrap_iterations: usize,
186
187 // =========================================================================
188 // Sample splitting
189 // =========================================================================
190 /// Fraction of samples held out for calibration/preflight.
191 ///
192 /// In non-adaptive mode, this fraction of samples is used for covariance
193 /// estimation. In adaptive mode, this is less relevant since calibration
194 /// is a fixed upfront cost.
195 ///
196 /// Default: 0.3 (30% for calibration).
197 pub calibration_fraction: f32,
198
199 // =========================================================================
200 // Optional limits and debugging
201 // =========================================================================
202 /// Optional guardrail for max duration in milliseconds (legacy).
203 ///
204 /// Prefer using `time_budget` instead. This is kept for backwards
205 /// compatibility but will be removed in a future version.
206 #[deprecated(since = "0.2.0", note = "Use time_budget instead")]
207 pub max_duration_ms: Option<u64>,
208
209 /// Optional deterministic seed for measurement randomness.
210 ///
211 /// When set, the measurement order (interleaving of classes) is
212 /// deterministic, which can help with debugging and reproducibility.
213 ///
214 /// Default: None (random seed).
215 pub measurement_seed: Option<u64>,
216
217 /// Force discrete mode for testing.
218 ///
219 /// When true, discrete mode (m-out-of-n bootstrap with mid-quantiles)
220 /// is used regardless of timer resolution. This is primarily for
221 /// testing the discrete mode code path on machines with high-resolution timers.
222 ///
223 /// In production, discrete mode is triggered automatically when the
224 /// minimum uniqueness ratio < 10% (per spec Section 2.4).
225 ///
226 /// Default: false.
227 pub force_discrete_mode: bool,
228}
229
230impl Default for Config {
231 fn default() -> Self {
232 #[allow(deprecated)]
233 Self {
234 // Decision thresholds
235 pass_threshold: DEFAULT_PASS_THRESHOLD,
236 fail_threshold: DEFAULT_FAIL_THRESHOLD,
237
238 // Resource limits
239 time_budget: Duration::from_secs(DEFAULT_TIME_BUDGET_SECS),
240 max_samples: DEFAULT_MAX_SAMPLES,
241 batch_size: DEFAULT_BATCH_SIZE,
242 calibration_samples: DEFAULT_CALIBRATION_SAMPLES,
243
244 // Effect thresholds
245 min_effect_of_concern_ns: 10.0,
246 attacker_model: None,
247 effect_threshold_ns: None,
248
249 // Measurement configuration
250 warmup: 1_000,
251 outlier_percentile: 0.9999,
252 iterations_per_sample: IterationsPerSample::Auto,
253 cpu_affinity: true,
254 thread_priority: true,
255 frequency_stabilization_ms: 5,
256
257 // Bayesian inference
258 prior_no_leak: 0.75,
259 cov_bootstrap_iterations: DEFAULT_BOOTSTRAP_ITERATIONS,
260
261 // Sample splitting
262 calibration_fraction: 0.3,
263
264 // Optional limits
265 max_duration_ms: None,
266 measurement_seed: None,
267 force_discrete_mode: false,
268 }
269 }
270}
271
272impl Config {
273 /// Create a new configuration with default settings.
274 pub fn new() -> Self {
275 Self::default()
276 }
277
278 // =========================================================================
279 // Builder methods
280 // =========================================================================
281
282 /// Set the pass threshold.
283 pub fn pass_threshold(mut self, threshold: f64) -> Self {
284 assert!(
285 threshold > 0.0 && threshold < 1.0,
286 "pass_threshold must be in (0, 1)"
287 );
288 assert!(
289 threshold < self.fail_threshold,
290 "pass_threshold must be < fail_threshold"
291 );
292 self.pass_threshold = threshold;
293 self
294 }
295
296 /// Set the fail threshold.
297 pub fn fail_threshold(mut self, threshold: f64) -> Self {
298 assert!(
299 threshold > 0.0 && threshold < 1.0,
300 "fail_threshold must be in (0, 1)"
301 );
302 assert!(
303 threshold > self.pass_threshold,
304 "fail_threshold must be > pass_threshold"
305 );
306 self.fail_threshold = threshold;
307 self
308 }
309
310 /// Set the time budget.
311 pub fn time_budget(mut self, budget: Duration) -> Self {
312 self.time_budget = budget;
313 self
314 }
315
316 /// Set the time budget in seconds.
317 pub fn time_budget_secs(mut self, secs: u64) -> Self {
318 self.time_budget = Duration::from_secs(secs);
319 self
320 }
321
322 /// Set the maximum number of samples.
323 pub fn max_samples(mut self, max: usize) -> Self {
324 assert!(max > 0, "max_samples must be positive");
325 self.max_samples = max;
326 self
327 }
328
329 /// Set the batch size for adaptive sampling.
330 pub fn batch_size(mut self, size: usize) -> Self {
331 assert!(size > 0, "batch_size must be positive");
332 self.batch_size = size;
333 self
334 }
335
336 /// Set the number of calibration samples.
337 pub fn calibration_samples(mut self, samples: usize) -> Self {
338 assert!(samples > 0, "calibration_samples must be positive");
339 self.calibration_samples = samples;
340 self
341 }
342
343 /// Set the attacker model.
344 pub fn attacker_model(mut self, model: AttackerModel) -> Self {
345 self.attacker_model = Some(model);
346 self
347 }
348
349 /// Set the warmup iterations.
350 pub fn warmup(mut self, iterations: usize) -> Self {
351 self.warmup = iterations;
352 self
353 }
354
355 /// Set the outlier percentile.
356 pub fn outlier_percentile(mut self, percentile: f64) -> Self {
357 assert!(
358 percentile > 0.0 && percentile <= 1.0,
359 "outlier_percentile must be in (0, 1]"
360 );
361 self.outlier_percentile = percentile;
362 self
363 }
364
365 /// Set the iterations per sample.
366 pub fn iterations_per_sample(mut self, iterations: IterationsPerSample) -> Self {
367 self.iterations_per_sample = iterations;
368 self
369 }
370
371 /// Enable or disable CPU affinity pinning.
372 ///
373 /// When enabled (default), the measurement thread is pinned to its
374 /// current CPU to reduce noise from thread migration.
375 ///
376 /// - **Linux**: Enforced via `sched_setaffinity`
377 /// - **macOS**: Advisory hint via `thread_policy_set`
378 pub fn cpu_affinity(mut self, enabled: bool) -> Self {
379 self.cpu_affinity = enabled;
380 self
381 }
382
383 /// Enable or disable thread priority elevation.
384 ///
385 /// When enabled (default), attempts to raise thread priority to reduce
386 /// preemption during measurement. Fails silently if insufficient privileges.
387 ///
388 /// - **Linux**: Lowers nice value, sets `SCHED_BATCH`
389 /// - **macOS**: Lowers nice value, sets thread precedence hint
390 pub fn thread_priority(mut self, enabled: bool) -> Self {
391 self.thread_priority = enabled;
392 self
393 }
394
395 /// Set the frequency stabilization duration in milliseconds.
396 ///
397 /// A brief spin-wait loop runs before measurement to let the CPU
398 /// frequency ramp up and stabilize. Set to 0 to disable.
399 ///
400 /// Default: 5 ms.
401 pub fn frequency_stabilization_ms(mut self, ms: u64) -> Self {
402 self.frequency_stabilization_ms = ms;
403 self
404 }
405
406 /// Set the prior probability of no leak.
407 pub fn prior_no_leak(mut self, prior: f64) -> Self {
408 assert!(
409 prior > 0.0 && prior < 1.0,
410 "prior_no_leak must be in (0, 1)"
411 );
412 self.prior_no_leak = prior;
413 self
414 }
415
416 /// Set the covariance bootstrap iterations.
417 pub fn cov_bootstrap_iterations(mut self, iterations: usize) -> Self {
418 assert!(iterations > 0, "cov_bootstrap_iterations must be positive");
419 self.cov_bootstrap_iterations = iterations;
420 self
421 }
422
423 /// Set the calibration fraction.
424 pub fn calibration_fraction(mut self, fraction: f32) -> Self {
425 assert!(
426 fraction > 0.0 && fraction < 1.0,
427 "calibration_fraction must be in (0, 1)"
428 );
429 self.calibration_fraction = fraction;
430 self
431 }
432
433 /// Set a deterministic seed for measurement.
434 pub fn seed(mut self, seed: u64) -> Self {
435 self.measurement_seed = Some(seed);
436 self
437 }
438
439 /// Force discrete mode for testing.
440 pub fn force_discrete_mode(mut self, force: bool) -> Self {
441 self.force_discrete_mode = force;
442 self
443 }
444
445 // =========================================================================
446 // Resolution methods
447 // =========================================================================
448
449 /// Resolve the minimum effect of concern in nanoseconds.
450 ///
451 /// If an attacker model is set, returns its threshold in nanoseconds.
452 /// Otherwise, returns the manually configured `min_effect_of_concern_ns`.
453 ///
454 /// # Arguments
455 ///
456 /// * `_cpu_freq_ghz` - Deprecated, kept for API compatibility
457 /// * `_timer_resolution_ns` - Deprecated, kept for API compatibility
458 ///
459 /// # Returns
460 ///
461 /// The resolved threshold in nanoseconds.
462 pub fn resolve_min_effect_ns(
463 &self,
464 _cpu_freq_ghz: Option<f64>,
465 _timer_resolution_ns: Option<f64>,
466 ) -> f64 {
467 if let Some(model) = &self.attacker_model {
468 model.to_threshold_ns()
469 } else {
470 self.min_effect_of_concern_ns
471 }
472 }
473
474 /// Check if the configuration is valid.
475 ///
476 /// Returns an error message if the configuration is invalid.
477 pub fn validate(&self) -> Result<(), String> {
478 if self.pass_threshold <= 0.0 || self.pass_threshold >= 1.0 {
479 return Err("pass_threshold must be in (0, 1)".to_string());
480 }
481 if self.fail_threshold <= 0.0 || self.fail_threshold >= 1.0 {
482 return Err("fail_threshold must be in (0, 1)".to_string());
483 }
484 if self.pass_threshold >= self.fail_threshold {
485 return Err("pass_threshold must be < fail_threshold".to_string());
486 }
487 if self.max_samples == 0 {
488 return Err("max_samples must be positive".to_string());
489 }
490 if self.batch_size == 0 {
491 return Err("batch_size must be positive".to_string());
492 }
493 if self.calibration_samples == 0 {
494 return Err("calibration_samples must be positive".to_string());
495 }
496 Ok(())
497 }
498}
499
500/// Configuration for iterations per timing sample.
501#[derive(Debug, Clone, Copy, PartialEq, Eq, Default)]
502pub enum IterationsPerSample {
503 /// Automatically detect based on timer resolution.
504 ///
505 /// On ARM64 with coarse timers (~40ns on Apple Silicon, Ampere Altra),
506 /// this will batch multiple iterations per sample for reliable timing.
507 /// On x86 or ARMv8.6+ (~1ns resolution), this typically uses 1 iteration.
508 #[default]
509 Auto,
510
511 /// Use exactly N iterations per sample.
512 ///
513 /// The measured time will be divided by N to get per-iteration timing.
514 Fixed(usize),
515}
516
517impl IterationsPerSample {
518 /// Resolve the iterations count for a given timer.
519 ///
520 /// For `Auto`, uses the timer's resolution to suggest iterations.
521 /// For `Fixed(n)`, returns `n`.
522 pub fn resolve(&self, timer: &crate::measurement::Timer) -> usize {
523 match self {
524 Self::Auto => {
525 // Target 10ns effective resolution for statistical reliability
526 timer.suggested_iterations(10.0)
527 }
528 Self::Fixed(n) => *n,
529 }
530 }
531}
532
533#[cfg(test)]
534mod tests {
535 use super::*;
536
537 #[test]
538 fn test_default_config() {
539 let config = Config::default();
540 assert_eq!(config.pass_threshold, 0.05);
541 assert_eq!(config.fail_threshold, 0.95);
542 assert_eq!(config.time_budget, Duration::from_secs(60));
543 assert_eq!(config.max_samples, 1_000_000);
544 assert_eq!(config.batch_size, 1_000);
545 assert_eq!(config.calibration_samples, 5_000);
546 }
547
548 #[test]
549 fn test_builder_methods() {
550 let config = Config::new()
551 .pass_threshold(0.01)
552 .fail_threshold(0.99)
553 .time_budget_secs(120)
554 .max_samples(500_000)
555 .batch_size(2_000);
556
557 assert_eq!(config.pass_threshold, 0.01);
558 assert_eq!(config.fail_threshold, 0.99);
559 assert_eq!(config.time_budget, Duration::from_secs(120));
560 assert_eq!(config.max_samples, 500_000);
561 assert_eq!(config.batch_size, 2_000);
562 }
563
564 #[test]
565 fn test_validation() {
566 let valid = Config::default();
567 assert!(valid.validate().is_ok());
568
569 let invalid = Config {
570 pass_threshold: 0.0,
571 ..Default::default()
572 };
573 assert!(invalid.validate().is_err());
574
575 let invalid = Config {
576 pass_threshold: 0.99,
577 fail_threshold: 0.01,
578 ..Default::default()
579 };
580 assert!(invalid.validate().is_err());
581 }
582
583 #[test]
584 #[should_panic]
585 fn test_invalid_pass_threshold() {
586 Config::new().pass_threshold(1.5);
587 }
588
589 #[test]
590 #[should_panic]
591 fn test_invalid_threshold_order() {
592 Config::new().pass_threshold(0.5).fail_threshold(0.4);
593 }
594}