Debugging
This guide helps you diagnose issues when timing tests don’t behave as expected.
First step: verify your harness
Section titled “First step: verify your harness”Before investigating anything else, make sure your test harness works:
use tacet::{TimingOracle, AttackerModel, helpers::InputPair};
// Test 1: Identical inputs should passlet inputs = InputPair::new( || [0u8; 32], || [0u8; 32], // Same as baseline);
let outcome = TimingOracle::for_attacker(AttackerModel::Research) .test(inputs, |data| my_function(&data));
assert!(outcome.passed(), "Sanity check failed: identical inputs should pass");
// Test 2: Obviously leaky code should failfn leaky(data: &[u8]) -> bool { data.iter().all(|&b| b == 0)}
let inputs = InputPair::new(|| [0u8; 64], || [0xFFu8; 64]);let outcome = TimingOracle::for_attacker(AttackerModel::AdjacentNetwork) .test(inputs, |data| { leaky(&data); });
assert!(outcome.failed(), "Harness should detect obvious leak");import { TimingOracle, AttackerModel } from 'tacet';
// Test 1: Identical inputs should passlet outcome = TimingOracle.forAttacker(AttackerModel.Research) .test({ baseline: () => new Uint8Array(32).fill(0), sample: () => new Uint8Array(32).fill(0) // Same as baseline }, (data) => myFunction(data));
if (!outcome.isPass()) { throw new Error("Sanity check failed: identical inputs should pass");}
// Test 2: Obviously leaky code should failfunction leaky(data) { return data.every(b => b === 0);}
outcome = TimingOracle.forAttacker(AttackerModel.AdjacentNetwork) .test({ baseline: () => new Uint8Array(64).fill(0), sample: () => new Uint8Array(64).fill(0xFF) }, (data) => leaky(data));
if (!outcome.isFail()) { throw new Error("Harness should detect obvious leak");}#include <tacet/tacet.h>#include <string.h>#include <assert.h>
// Callback for identical inputsvoid identical_inputs(uint64_t *baseline, uint64_t *sample, size_t n, void *ctx) { uint8_t data[32] = {0}; memset(baseline, 0, n * sizeof(uint64_t)); memset(sample, 0, n * sizeof(uint64_t)); for (size_t i = 0; i < n; i++) { my_function(data); // Same operation }}
// Test 1: Identical inputs should passTOConfig config = to_config_default(TO_ATTACKER_RESEARCH);TOResult result;int ret = to_test(&config, identical_inputs, NULL, &result);assert(ret == 0 && result.outcome == TO_OUTCOME_PASS);
// Test 2: Obviously leaky code should failbool leaky(const uint8_t *data, size_t len) { for (size_t i = 0; i < len; i++) { if (data[i] != 0) return false; } return true;}
void leaky_inputs(uint64_t *baseline, uint64_t *sample, size_t n, void *ctx) { uint8_t zero_data[64] = {0}; uint8_t ones_data[64]; memset(ones_data, 0xFF, 64);
for (size_t i = 0; i < n; i++) { baseline[i] = 0; leaky(zero_data, 64); } for (size_t i = 0; i < n; i++) { sample[i] = 0; leaky(ones_data, 64); }}
config = to_config_default(TO_ATTACKER_ADJACENT_NETWORK);ret = to_test(&config, leaky_inputs, NULL, &result);assert(ret == 0 && result.outcome == TO_OUTCOME_FAIL);#include <tacet/tacet.hpp>#include <cassert>#include <algorithm>
// Test 1: Identical inputs should passauto outcome = tacet::Oracle::forAttacker(tacet::AttackerModel::Research) .test([](std::span<uint64_t> baseline, std::span<uint64_t> sample) { std::array<uint8_t, 32> data{}; for (auto& t : baseline) { myFunction(data); // Same operation } for (auto& t : sample) { myFunction(data); // Same operation } });
assert(outcome.passed() && "Sanity check failed: identical inputs should pass");
// Test 2: Obviously leaky code should failauto leaky = [](const auto& data) { return std::all_of(data.begin(), data.end(), [](uint8_t b) { return b == 0; });};
outcome = tacet::Oracle::forAttacker(tacet::AttackerModel::AdjacentNetwork) .test([&](std::span<uint64_t> baseline, std::span<uint64_t> sample) { std::array<uint8_t, 64> zero_data{}; std::array<uint8_t, 64> ones_data; ones_data.fill(0xFF);
for (auto& t : baseline) { leaky(zero_data); } for (auto& t : sample) { leaky(ones_data); } });
assert(outcome.failed() && "Harness should detect obvious leak");import ( "bytes" "testing" "github.com/tacet-labs/tacet/tacet")
// Test 1: Identical inputs should passgen := func() ([]byte, []byte) { baseline := make([]byte, 32) sample := make([]byte, 32) return baseline, sample}
outcome := tacet.Test(gen, myFunction, 32, tacet.WithAttacker(tacet.AttackerResearch))
if !outcome.Passed() { t.Fatal("Sanity check failed: identical inputs should pass")}
// Test 2: Obviously leaky code should failleaky := func(data []byte) bool { return bytes.Equal(data, make([]byte, len(data)))}
gen = func() ([]byte, []byte) { baseline := make([]byte, 64) sample := make([]byte, 64) for i := range sample { sample[i] = 0xFF } return baseline, sample}
outcome = tacet.Test(gen, func(data []byte) { leaky(data) }, 64, tacet.WithAttacker(tacet.AttackerAdjacentNetwork))
if !outcome.Failed() { t.Fatal("Harness should detect obvious leak")}If Test 1 fails, there’s an environmental issue. If Test 2 fails, check your measurement setup.
High noise / TooNoisy quality
Section titled “High noise / TooNoisy quality”Symptoms:
quality: TooNoisyorPoor- High minimum detectable effect (> 100ns)
- Inconsistent results across runs
InconclusivewithDataTooNoisyreason
Causes and solutions:
Run single-threaded
Section titled “Run single-threaded”Parallel tests interfere with timing measurements:
cargo test --test timing_tests -- --test-threads=1Check CPU governor (Linux)
Section titled “Check CPU governor (Linux)”Power-saving mode causes CPU frequency fluctuations:
# Check current governorcat /sys/devices/system/cpu/cpu0/cpufreq/scaling_governor
# Set to performancesudo cpupower frequency-set -g performanceDisable turbo boost
Section titled “Disable turbo boost”Turbo boost changes CPU frequency dynamically:
# Intelecho 1 | sudo tee /sys/devices/system/cpu/intel_pstate/no_turbo
# AMDecho 0 | sudo tee /sys/devices/system/cpu/cpufreq/boostReduce background load
Section titled “Reduce background load”- Close unnecessary applications
- Check system load:
top,htop - Avoid running during heavy I/O (builds, downloads)
Enable cycle-accurate timer
Section titled “Enable cycle-accurate timer”Use cycle-precision timers for higher precision:
use tacet::{TimingOracle, AttackerModel, TimerSpec};
TimingOracle::for_attacker(AttackerModel::SharedHardware) .timer_spec(TimerSpec::CyclePrecision)This requires elevated privileges on ARM64 platforms.
Running with elevated privileges (Rust only):
sudo -E cargo test -- --test-threads=1sudo cargo test -- --test-threads=1# Or grant capability:sudo setcap cap_perfmon+ep ./target/debug/deps/my_test-*False positives
Section titled “False positives”Symptoms:
- Constant-time code flagged as leaky
- Simple operations (XOR, memcpy) show timing differences
leak_probabilityhigh on known-safe code
Causes and solutions:
Run the sanity check
Section titled “Run the sanity check”Test with identical inputs:
use tacet::{TimingOracle, AttackerModel, helpers::InputPair};
let inputs = InputPair::new( || [0u8; 32], || [0u8; 32], // Identical);
let outcome = TimingOracle::for_attacker(AttackerModel::Research) .test(inputs, |data| my_function(&data));
if !outcome.passed() { println!("Environment issue detected: {:?}", outcome);}import { TimingOracle, AttackerModel } from 'tacet';
let outcome = TimingOracle.forAttacker(AttackerModel.Research) .test({ baseline: () => new Uint8Array(32).fill(0), sample: () => new Uint8Array(32).fill(0) }, (data) => myFunction(data));
if (!outcome.isPass()) { console.log("Environment issue detected:", outcome);}#include <tacet/tacet.h>
void identical_inputs(uint64_t *baseline, uint64_t *sample, size_t n, void *ctx) { uint8_t data[32] = {0}; for (size_t i = 0; i < n; i++) { baseline[i] = 0; my_function(data); } for (size_t i = 0; i < n; i++) { sample[i] = 0; my_function(data); }}
TOConfig config = to_config_default(TO_ATTACKER_RESEARCH);TOResult result;int ret = to_test(&config, identical_inputs, NULL, &result);
if (ret != 0 || result.outcome != TO_OUTCOME_PASS) { fprintf(stderr, "Environment issue detected\n");}#include <tacet/tacet.hpp>
auto outcome = tacet::Oracle::forAttacker(tacet::AttackerModel::Research) .test([](std::span<uint64_t> baseline, std::span<uint64_t> sample) { std::array<uint8_t, 32> data{}; for (auto& t : baseline) { myFunction(data); } for (auto& t : sample) { myFunction(data); } });
if (!outcome.passed()) { std::cerr << "Environment issue detected\n";}import "github.com/tacet-labs/tacet/tacet"
gen := func() ([]byte, []byte) { baseline := make([]byte, 32) sample := make([]byte, 32) return baseline, sample}
outcome := tacet.Test(gen, myFunction, 32, tacet.WithAttacker(tacet.AttackerResearch))
if !outcome.Passed() { fmt.Println("Environment issue detected")}If this fails, the problem is environmental, not your code.
Check preflight warnings
Section titled “Check preflight warnings”The library runs preflight checks that may indicate issues:
use tacet::Outcome;
match outcome { Outcome::Pass { diagnostics, .. } | Outcome::Fail { diagnostics, .. } => { if !diagnostics.preflight_warnings.is_empty() { println!("Preflight warnings: {:?}", diagnostics.preflight_warnings); } } _ => {}}if (outcome.isPass() || outcome.isFail()) { const diagnostics = outcome.diagnostics; if (diagnostics.preflightWarnings.length > 0) { console.log("Preflight warnings:", diagnostics.preflightWarnings); }}if (result.outcome == TO_OUTCOME_PASS || result.outcome == TO_OUTCOME_FAIL) { if (result.diagnostics.preflight_warning_count > 0) { fprintf(stderr, "Preflight warnings detected\n"); }}if (outcome.passed() || outcome.failed()) { const auto& diagnostics = outcome.diagnostics(); if (!diagnostics.preflightWarnings.empty()) { std::cerr << "Preflight warnings detected\n"; }}if outcome.Passed() || outcome.Failed() { if len(outcome.Diagnostics.PreflightWarnings) > 0 { fmt.Println("Preflight warnings:", outcome.Diagnostics.PreflightWarnings) }}Ensure identical code paths
Section titled “Ensure identical code paths”Both input classes must execute the same code:
// ✗ Wrong: different code pathslet inputs = InputPair::new( || encrypt_with_key_a(&data), // Uses key A || encrypt_with_key_b(&data), // Uses key B);
// ✓ Correct: same operation, different input datalet inputs = InputPair::new( || [0u8; 32], || rand::random(),);let outcome = TimingOracle::for_attacker(AttackerModel::AdjacentNetwork) .test(inputs, |data| encrypt_with_key_a(&data)); // Same key// ✗ Wrong: different code pathsTimingOracle.forAttacker(AttackerModel.AdjacentNetwork) .test({ baseline: () => encryptWithKeyA(data), // Uses key A sample: () => encryptWithKeyB(data) // Uses key B }, (encrypted) => processResult(encrypted));
// ✓ Correct: same operation, different input dataTimingOracle.forAttacker(AttackerModel.AdjacentNetwork) .test({ baseline: () => new Uint8Array(32).fill(0), sample: () => crypto.getRandomValues(new Uint8Array(32)) }, (data) => encryptWithKeyA(data)); // Same key// ✗ Wrong: different code paths in callbackvoid wrong_callback(uint64_t *baseline, uint64_t *sample, size_t n, void *ctx) { for (size_t i = 0; i < n; i++) { encrypt_with_key_a(data); // Key A } for (size_t i = 0; i < n; i++) { encrypt_with_key_b(data); // Key B - different operation! }}
// ✓ Correct: same operation, different input datavoid correct_callback(uint64_t *baseline, uint64_t *sample, size_t n, void *ctx) { uint8_t zero_data[32] = {0}; uint8_t random_data[32]; fill_random(random_data, 32);
for (size_t i = 0; i < n; i++) { encrypt_with_key_a(zero_data); // Same key } for (size_t i = 0; i < n; i++) { encrypt_with_key_a(random_data); // Same key }}// ✗ Wrong: different code pathsOracle::forAttacker(AttackerModel::AdjacentNetwork) .test([](std::span<uint64_t> baseline, std::span<uint64_t> sample) { for (auto& t : baseline) { encryptWithKeyA(data); // Key A } for (auto& t : sample) { encryptWithKeyB(data); // Key B - different! } });
// ✓ Correct: same operation, different input dataOracle::forAttacker(AttackerModel::AdjacentNetwork) .test([](std::span<uint64_t> baseline, std::span<uint64_t> sample) { std::array<uint8_t, 32> zero_data{}; std::array<uint8_t, 32> random_data = getRandomBytes<32>();
for (auto& t : baseline) { encryptWithKeyA(zero_data); // Same key } for (auto& t : sample) { encryptWithKeyA(random_data); // Same key } });// ✗ Wrong: different code pathsgen := func() ([]byte, []byte) { baseline := encryptWithKeyA(data) // Key A sample := encryptWithKeyB(data) // Key B - different! return baseline, sample}
// ✓ Correct: same operation, different input datagen = func() ([]byte, []byte) { baseline := make([]byte, 32) sample := make([]byte, 32) rand.Read(sample) return baseline, sample}outcome := tacet.Test(gen, func(data []byte) { encryptWithKeyA(data) }, // Same key 32, tacet.WithAttacker(tacet.AttackerAdjacentNetwork))Check for state pollution
Section titled “Check for state pollution”Ensure no global state is modified between measurements:
// ✗ Wrong: modifying shared statestatic mut COUNTER: u64 = 0;let outcome = oracle.test(inputs, |data| { unsafe { COUNTER += 1; } // Side effect! process(data);});
// ✓ Correct: isolated statelet outcome = oracle.test(inputs, |data| { process(data); // No side effects});// ✗ Wrong: modifying shared statelet counter = 0;TimingOracle.forAttacker(model).test(inputs, (data) => { counter++; // Side effect! process(data);});
// ✓ Correct: isolated stateTimingOracle.forAttacker(model).test(inputs, (data) => { process(data); // No side effects});// ✗ Wrong: modifying shared statestatic uint64_t counter = 0;void bad_callback(uint64_t *baseline, uint64_t *sample, size_t n, void *ctx) { for (size_t i = 0; i < n; i++) { counter++; // Side effect! process(data); }}
// ✓ Correct: isolated statevoid good_callback(uint64_t *baseline, uint64_t *sample, size_t n, void *ctx) { for (size_t i = 0; i < n; i++) { process(data); // No side effects }}// ✗ Wrong: modifying shared statestatic uint64_t counter = 0;Oracle::forAttacker(model).test([](auto baseline, auto sample) { for (auto& t : baseline) { counter++; // Side effect! process(data); }});
// ✓ Correct: isolated stateOracle::forAttacker(model).test([](auto baseline, auto sample) { for (auto& t : baseline) { process(data); // No side effects }});// ✗ Wrong: modifying shared statevar counter int64tacet.Test(gen, func(data []byte) { atomic.AddInt64(&counter, 1) // Side effect! process(data)}, size, opts...)
// ✓ Correct: isolated statetacet.Test(gen, func(data []byte) { process(data) // No side effects}, size, opts...)Can’t detect known leaks
Section titled “Can’t detect known leaks”Symptoms:
- Early-exit comparison shows
leak_probability < 0.5 - Tests pass on code you know is leaky
- Known-leaky harness test passes
Causes and solutions:
Verify the leak exists
Section titled “Verify the leak exists”Manually check timing difference:
use std::time::Instant;
let fixed_input = [0u8; 32];let random_input: [u8; 32] = rand::random();
let mut fixed_times = Vec::new();let mut random_times = Vec::new();
for _ in 0..1000 { let start = Instant::now(); my_function(&fixed_input); fixed_times.push(start.elapsed());
let start = Instant::now(); my_function(&random_input); random_times.push(start.elapsed());}
// Compare mediansfixed_times.sort();random_times.sort();println!("Fixed median: {:?}", fixed_times[500]);println!("Random median: {:?}", random_times[500]);const fixedInput = new Uint8Array(32).fill(0);const randomInput = crypto.getRandomValues(new Uint8Array(32));
const fixedTimes = [];const randomTimes = [];
for (let i = 0; i < 1000; i++) { let start = performance.now(); myFunction(fixedInput); fixedTimes.push(performance.now() - start);
start = performance.now(); myFunction(randomInput); randomTimes.push(performance.now() - start);}
// Compare mediansfixedTimes.sort((a, b) => a - b);randomTimes.sort((a, b) => a - b);console.log("Fixed median:", fixedTimes[500], "ms");console.log("Random median:", randomTimes[500], "ms");#include <time.h>
uint8_t fixed_input[32] = {0};uint8_t random_input[32];fill_random(random_input, 32);
uint64_t fixed_times[1000];uint64_t random_times[1000];
for (int i = 0; i < 1000; i++) { struct timespec start, end; clock_gettime(CLOCK_MONOTONIC, &start); my_function(fixed_input); clock_gettime(CLOCK_MONOTONIC, &end); fixed_times[i] = (end.tv_sec - start.tv_sec) * 1000000000 + (end.tv_nsec - start.tv_nsec);
clock_gettime(CLOCK_MONOTONIC, &start); my_function(random_input); clock_gettime(CLOCK_MONOTONIC, &end); random_times[i] = (end.tv_sec - start.tv_sec) * 1000000000 + (end.tv_nsec - start.tv_nsec);}
// Compare medians (after sorting)qsort(fixed_times, 1000, sizeof(uint64_t), compare_uint64);qsort(random_times, 1000, sizeof(uint64_t), compare_uint64);printf("Fixed median: %llu ns\n", fixed_times[500]);printf("Random median: %llu ns\n", random_times[500]);#include <chrono>#include <algorithm>
std::array<uint8_t, 32> fixed_input{};auto random_input = getRandomBytes<32>();
std::vector<std::chrono::nanoseconds> fixed_times;std::vector<std::chrono::nanoseconds> random_times;
for (int i = 0; i < 1000; i++) { auto start = std::chrono::high_resolution_clock::now(); myFunction(fixed_input); auto end = std::chrono::high_resolution_clock::now(); fixed_times.push_back(end - start);
start = std::chrono::high_resolution_clock::now(); myFunction(random_input); end = std::chrono::high_resolution_clock::now(); random_times.push_back(end - start);}
// Compare mediansstd::sort(fixed_times.begin(), fixed_times.end());std::sort(random_times.begin(), random_times.end());std::cout << "Fixed median: " << fixed_times[500].count() << " ns\n";std::cout << "Random median: " << random_times[500].count() << " ns\n";import "time"
fixedInput := make([]byte, 32)randomInput := make([]byte, 32)rand.Read(randomInput)
fixedTimes := make([]time.Duration, 1000)randomTimes := make([]time.Duration, 1000)
for i := 0; i < 1000; i++ { start := time.Now() myFunction(fixedInput) fixedTimes[i] = time.Since(start)
start = time.Now() myFunction(randomInput) randomTimes[i] = time.Since(start)}
// Compare medianssort.Slice(fixedTimes, func(i, j int) bool { return fixedTimes[i] < fixedTimes[j] })sort.Slice(randomTimes, func(i, j int) bool { return randomTimes[i] < randomTimes[j] })fmt.Printf("Fixed median: %v\n", fixedTimes[500])fmt.Printf("Random median: %v\n", randomTimes[500])Prevent compiler optimization
Section titled “Prevent compiler optimization”Prevent the compiler from optimizing away the operation:
use std::hint::black_box;
let outcome = TimingOracle::for_attacker(AttackerModel::AdjacentNetwork) .test(inputs, |data| { black_box(my_function(black_box(data))); });// JavaScript doesn't have black_box, but ensure the operation result is usedlet sink = 0;TimingOracle.forAttacker(AttackerModel.AdjacentNetwork) .test(inputs, (data) => { sink ^= myFunction(data); // Use result to prevent optimization });// Ensure compiler doesn't optimize away the operation// Mark function as having side effects or use volatilevolatile int sink = 0;void callback(uint64_t *baseline, uint64_t *sample, size_t n, void *ctx) { for (size_t i = 0; i < n; i++) { sink ^= my_function(data); // Use result }}// Use [[gnu::noinline]] or ensure result is usedinline volatile int sink = 0;Oracle::forAttacker(AttackerModel::AdjacentNetwork) .test([](auto baseline, auto sample) { for (auto& t : baseline) { sink ^= myFunction(data); // Use result } });// Go doesn't aggressively optimize like C/Rust, but ensure result is usedvar sink inttacet.Test(gen, func(data []byte) { sink ^= myFunction(data) // Use result to prevent optimization}, size, opts...)Prevent inlining
Section titled “Prevent inlining”Mark the function under test to prevent inlining:
#[inline(never)]fn my_function(data: &[u8]) -> bool { // ...}// JavaScript doesn't expose inlining control// Ensure function is complex enough not to inlinefunction myFunction(data) { // Complex logic here}__attribute__((noinline))bool my_function(const uint8_t *data, size_t len) { // ...}[[gnu::noinline]]bool myFunction(const std::span<uint8_t> data) { // ...}// Use //go:noinline directive//go:noinlinefunc myFunction(data []byte) bool { // ...}Use larger inputs
Section titled “Use larger inputs”Larger inputs amplify timing differences:
// Small input: timing difference may be too smalllet inputs = InputPair::new(|| [0u8; 8], || rand::random::<[u8; 8]>());
// Larger input: timing difference is amplifiedlet inputs = InputPair::new(|| [0u8; 512], || rand::random::<[u8; 512]>());// Small input: timing difference may be too smalllet outcome = oracle.test({ baseline: () => new Uint8Array(8).fill(0), sample: () => crypto.getRandomValues(new Uint8Array(8))}, operation);
// Larger input: timing difference is amplifiedoutcome = oracle.test({ baseline: () => new Uint8Array(512).fill(0), sample: () => crypto.getRandomValues(new Uint8Array(512))}, operation);// Small input: timing difference may be too smallvoid callback_small(uint64_t *baseline, uint64_t *sample, size_t n, void *ctx) { uint8_t data[8]; // ...}
// Larger input: timing difference is amplifiedvoid callback_large(uint64_t *baseline, uint64_t *sample, size_t n, void *ctx) { uint8_t data[512]; // ...}// Small input: timing difference may be too smallOracle::forAttacker(model).test([](auto baseline, auto sample) { std::array<uint8_t, 8> data; // ...});
// Larger input: timing difference is amplifiedOracle::forAttacker(model).test([](auto baseline, auto sample) { std::array<uint8_t, 512> data; // ...});// Small input: timing difference may be too smallgen := func() ([]byte, []byte) { baseline := make([]byte, 8) sample := make([]byte, 8) rand.Read(sample) return baseline, sample}
// Larger input: timing difference is amplifiedgen = func() ([]byte, []byte) { baseline := make([]byte, 512) sample := make([]byte, 512) rand.Read(sample) return baseline, sample}Use stricter threshold
Section titled “Use stricter threshold”A tighter threshold may detect smaller effects:
// AdjacentNetwork (100ns) may miss small leaksTimingOracle::for_attacker(AttackerModel::AdjacentNetwork)
// SharedHardware (0.6ns) catches cycle-level differencesTimingOracle::for_attacker(AttackerModel::SharedHardware)
// Or custom thresholdTimingOracle::for_attacker(AttackerModel::Custom { threshold_ns: 10.0 })// AdjacentNetwork (100ns) may miss small leaksTimingOracle.forAttacker(AttackerModel.AdjacentNetwork)
// SharedHardware (0.6ns) catches cycle-level differencesTimingOracle.forAttacker(AttackerModel.SharedHardware)
// Or custom thresholdTimingOracle.forAttacker(AttackerModel.custom(10.0))// AdjacentNetwork (100ns) may miss small leaksTOConfig config = to_config_default(TO_ATTACKER_ADJACENT_NETWORK);
// SharedHardware (0.6ns) catches cycle-level differencesconfig = to_config_default(TO_ATTACKER_SHARED_HARDWARE);
// Or custom thresholdconfig = to_config_default(TO_ATTACKER_CUSTOM);config.threshold_ns = 10.0;// AdjacentNetwork (100ns) may miss small leaksOracle::forAttacker(AttackerModel::AdjacentNetwork)
// SharedHardware (0.6ns) catches cycle-level differencesOracle::forAttacker(AttackerModel::SharedHardware)
// Or custom thresholdOracle::forAttacker(AttackerModel::custom(10.0))// AdjacentNetwork (100ns) may miss small leakstacet.Test(gen, op, size, tacet.WithAttacker(tacet.AttackerAdjacentNetwork))
// SharedHardware (0.6ns) catches cycle-level differencestacet.Test(gen, op, size, tacet.WithAttacker(tacet.AttackerSharedHardware))
// Or custom thresholdtacet.Test(gen, op, size, tacet.WithAttacker(tacet.AttackerCustom(10.0)))Check input class selection
Section titled “Check input class selection”For comparison functions, baseline must match the secret:
let secret = [0u8; 32];
// ✗ Wrong: both exit earlylet inputs = InputPair::new( || [0xFFu8; 32], // Mismatches secret || rand::random(), // Also mismatches);
// ✓ Correct: baseline matches, sample mismatcheslet inputs = InputPair::new( || [0u8; 32], // Matches secret → full comparison || rand::random(), // Mismatches → early exit);const secret = new Uint8Array(32).fill(0);
// ✗ Wrong: both exit earlyoracle.test({ baseline: () => new Uint8Array(32).fill(0xFF), // Mismatches secret sample: () => crypto.getRandomValues(new Uint8Array(32)) // Also mismatches}, operation);
// ✓ Correct: baseline matches, sample mismatchesoracle.test({ baseline: () => new Uint8Array(32).fill(0), // Matches secret → full comparison sample: () => crypto.getRandomValues(new Uint8Array(32)) // Mismatches → early exit}, operation);uint8_t secret[32] = {0};
// ✗ Wrong: both exit earlyvoid wrong_callback(uint64_t *baseline, uint64_t *sample, size_t n, void *ctx) { uint8_t mismatch_data[32]; memset(mismatch_data, 0xFF, 32); // Mismatches secret uint8_t random_data[32]; fill_random(random_data, 32); // Also mismatches // ...}
// ✓ Correct: baseline matches, sample mismatchesvoid correct_callback(uint64_t *baseline, uint64_t *sample, size_t n, void *ctx) { uint8_t zero_data[32] = {0}; // Matches secret → full comparison uint8_t random_data[32]; fill_random(random_data, 32); // Mismatches → early exit // ...}std::array<uint8_t, 32> secret{};
// ✗ Wrong: both exit earlyOracle::forAttacker(model).test([](auto baseline, auto sample) { std::array<uint8_t, 32> mismatch_data; mismatch_data.fill(0xFF); // Mismatches secret // ...});
// ✓ Correct: baseline matches, sample mismatchesOracle::forAttacker(model).test([](auto baseline, auto sample) { std::array<uint8_t, 32> zero_data{}; // Matches secret → full comparison auto random_data = getRandomBytes<32>(); // Mismatches → early exit // ...});secret := make([]byte, 32)
// ✗ Wrong: both exit earlygen := func() ([]byte, []byte) { baseline := make([]byte, 32) for i := range baseline { baseline[i] = 0xFF // Mismatches secret } sample := make([]byte, 32) rand.Read(sample) // Also mismatches return baseline, sample}
// ✓ Correct: baseline matches, sample mismatchesgen = func() ([]byte, []byte) { baseline := make([]byte, 32) // Matches secret → full comparison sample := make([]byte, 32) rand.Read(sample) // Mismatches → early exit return baseline, sample}See The Two-Class Pattern for details.
Unmeasurable results
Section titled “Unmeasurable results”Symptoms:
Outcome::Unmeasurablereturned- Message says operation is too fast
Causes and solutions:
Use cycle-accurate timer
Section titled “Use cycle-accurate timer”Use cycle-precision timers for higher resolution:
use tacet::{TimingOracle, AttackerModel, TimerSpec};
TimingOracle::for_attacker(AttackerModel::SharedHardware) .timer_spec(TimerSpec::CyclePrecision)This requires elevated privileges on ARM64 platforms.
Running with elevated privileges (Rust only):
# Requires BOTH sudo AND single-threadedsudo -E cargo test -- --test-threads=1sudo cargo test -- --test-threads=1Batch operations
Section titled “Batch operations”Test multiple iterations together:
let inputs = InputPair::new( || [[0u8; 16]; 100], // 100 blocks || std::array::from_fn(|_| rand::random::<[u8; 16]>()),);
let outcome = TimingOracle::for_attacker(AttackerModel::AdjacentNetwork) .test(inputs, |blocks| { for block in blocks { cipher.encrypt_block(&mut block.into()); } });TimingOracle.forAttacker(AttackerModel.AdjacentNetwork) .test({ baseline: () => Array(100).fill(new Uint8Array(16).fill(0)), sample: () => Array(100).fill(null).map(() => crypto.getRandomValues(new Uint8Array(16))) }, (blocks) => { blocks.forEach(block => cipher.encryptBlock(block)); });void batched_callback(uint64_t *baseline, uint64_t *sample, size_t n, void *ctx) { uint8_t blocks[100][16]; memset(blocks, 0, sizeof(blocks));
for (size_t i = 0; i < n; i++) { for (int j = 0; j < 100; j++) { cipher_encrypt_block(&blocks[j]); } }}Oracle::forAttacker(AttackerModel::AdjacentNetwork) .test([](auto baseline, auto sample) { std::array<std::array<uint8_t, 16>, 100> blocks;
for (auto& t : baseline) { for (auto& block : blocks) { cipher.encryptBlock(block); } } });gen := func() ([][]byte, [][]byte) { baseline := make([][]byte, 100) sample := make([][]byte, 100) for i := 0; i < 100; i++ { baseline[i] = make([]byte, 16) sample[i] = make([]byte, 16) rand.Read(sample[i]) } return baseline, sample}
tacet.Test(gen, func(blocks [][]byte) { for _, block := range blocks { cipher.EncryptBlock(block) }}, 16*100, tacet.WithAttacker(tacet.AttackerAdjacentNetwork))Accept the limitation
Section titled “Accept the limitation”Some operations are genuinely too fast to measure:
match outcome { Outcome::Unmeasurable { operation_ns, .. } => { // If operation is <1ns, it may not be measurable on any platform if operation_ns < 1.0 { println!("Operation inherently unmeasurable"); } } _ => {}}if (outcome.isUnmeasurable()) { const { operationNs } = outcome; if (operationNs < 1.0) { console.log("Operation inherently unmeasurable"); }}if (result.outcome == TO_OUTCOME_UNMEASURABLE) { if (result.unmeasurable_info.operation_ns < 1.0) { fprintf(stderr, "Operation inherently unmeasurable\n"); }}if (outcome.isUnmeasurable()) { if (outcome.operationNs() < 1.0) { std::cerr << "Operation inherently unmeasurable\n"; }}if outcome.IsUnmeasurable() { if outcome.OperationNs < 1.0 { fmt.Println("Operation inherently unmeasurable") }}Platform-specific issues
Section titled “Platform-specific issues”macOS ARM64
Section titled “macOS ARM64”kperf requires both sudo AND single-threaded:
# This won't work (parallel tests)sudo cargo test
# This workssudo -E cargo test -- --test-threads=1Without --test-threads=1, kperf access fails. Use TimerSpec::CyclePrecision to get an error instead of silent fallback to the coarse ~42ns timer.
Linux perf_event
Section titled “Linux perf_event”Check perf_event_paranoid setting:
cat /proc/sys/kernel/perf_event_paranoid# 3 = no access, 2 = user only, 1 = limited, 0/-1 = full access
# Allow unprivileged access (temporary)echo 1 | sudo tee /proc/sys/kernel/perf_event_paranoid
# Or grant capability (persistent)sudo setcap cap_perfmon+ep ./target/debug/deps/my_test-*Virtual machines
Section titled “Virtual machines”VMs have inherently higher timing noise:
- Hypervisor overhead
- Noisy neighbors
- Timer virtualization
Recommendations:
- Use
AdjacentNetworkthreshold, notSharedHardware - Increase time budgets (60s+)
- Accept some
Inconclusiveresults - Consider bare metal for security-critical tests
Getting more information
Section titled “Getting more information”Enable verbose output
Section titled “Enable verbose output”cargo test -- --nocaptureCheck diagnostics
Section titled “Check diagnostics”use tacet::Outcome;
match outcome { Outcome::Pass { diagnostics, quality, .. } | Outcome::Fail { diagnostics, quality, .. } => { println!("Quality: {:?}", quality); println!("Samples used: {}", diagnostics.samples_used); println!("Theta user: {:.1}ns", diagnostics.theta_user); println!("Theta effective: {:.1}ns", diagnostics.theta_eff); println!("Theta floor: {:.1}ns", diagnostics.theta_floor); if !diagnostics.preflight_warnings.is_empty() { println!("Warnings: {:?}", diagnostics.preflight_warnings); } } Outcome::Inconclusive { reason, leak_probability, .. } => { println!("Inconclusive: {:?}", reason); println!("Current estimate: P={:.1}%", leak_probability * 100.0); } Outcome::Unmeasurable { operation_ns, threshold_ns, recommendation, .. } => { println!("Operation: {:.1}ns", operation_ns); println!("Threshold: {:.1}ns", threshold_ns); println!("Recommendation: {}", recommendation); }}if (outcome.isPass() || outcome.isFail()) { const { diagnostics, quality } = outcome; console.log("Quality:", quality); console.log("Samples used:", diagnostics.samplesUsed); console.log("Theta user:", diagnostics.thetaUser.toFixed(1), "ns"); console.log("Theta effective:", diagnostics.thetaEff.toFixed(1), "ns"); console.log("Theta floor:", diagnostics.thetaFloor.toFixed(1), "ns"); if (diagnostics.preflightWarnings.length > 0) { console.log("Warnings:", diagnostics.preflightWarnings); }} else if (outcome.isInconclusive()) { console.log("Inconclusive:", outcome.reason); console.log("Current estimate: P=" + (outcome.leakProbability * 100).toFixed(1) + "%");} else if (outcome.isUnmeasurable()) { console.log("Operation:", outcome.operationNs.toFixed(1), "ns"); console.log("Threshold:", outcome.thresholdNs.toFixed(1), "ns"); console.log("Recommendation:", outcome.recommendation);}if (result.outcome == TO_OUTCOME_PASS || result.outcome == TO_OUTCOME_FAIL) { printf("Quality: %d\n", result.quality); printf("Samples used: %zu\n", result.diagnostics.samples_used); printf("Theta user: %.1f ns\n", result.diagnostics.theta_user); printf("Theta effective: %.1f ns\n", result.diagnostics.theta_eff); printf("Theta floor: %.1f ns\n", result.diagnostics.theta_floor); if (result.diagnostics.preflight_warning_count > 0) { fprintf(stderr, "Preflight warnings detected\n"); }} else if (result.outcome == TO_OUTCOME_INCONCLUSIVE) { printf("Inconclusive: %d\n", result.inconclusive_reason); printf("Current estimate: P=%.1f%%\n", result.leak_probability * 100.0);} else if (result.outcome == TO_OUTCOME_UNMEASURABLE) { printf("Operation: %.1f ns\n", result.unmeasurable_info.operation_ns); printf("Threshold: %.1f ns\n", result.unmeasurable_info.threshold_ns); printf("Recommendation: %s\n", result.unmeasurable_info.recommendation);}if (outcome.passed() || outcome.failed()) { const auto& diagnostics = outcome.diagnostics(); std::cout << "Quality: " << static_cast<int>(outcome.quality()) << "\n"; std::cout << "Samples used: " << diagnostics.samplesUsed << "\n"; std::cout << "Theta user: " << diagnostics.thetaUser << " ns\n"; std::cout << "Theta effective: " << diagnostics.thetaEff << " ns\n"; std::cout << "Theta floor: " << diagnostics.thetaFloor << " ns\n"; if (!diagnostics.preflightWarnings.empty()) { std::cerr << "Warnings detected\n"; }} else if (outcome.isInconclusive()) { std::cout << "Inconclusive: " << static_cast<int>(outcome.reason()) << "\n"; std::cout << "Current estimate: P=" << (outcome.leakProbability() * 100) << "%\n";} else if (outcome.isUnmeasurable()) { std::cout << "Operation: " << outcome.operationNs() << " ns\n"; std::cout << "Threshold: " << outcome.thresholdNs() << " ns\n"; std::cout << "Recommendation: " << outcome.recommendation() << "\n";}if outcome.Passed() || outcome.Failed() { fmt.Printf("Quality: %v\n", outcome.Quality) fmt.Printf("Samples used: %d\n", outcome.Diagnostics.SamplesUsed) fmt.Printf("Theta user: %.1f ns\n", outcome.Diagnostics.ThetaUser) fmt.Printf("Theta effective: %.1f ns\n", outcome.Diagnostics.ThetaEff) fmt.Printf("Theta floor: %.1f ns\n", outcome.Diagnostics.ThetaFloor) if len(outcome.Diagnostics.PreflightWarnings) > 0 { fmt.Println("Warnings:", outcome.Diagnostics.PreflightWarnings) }} else if outcome.IsInconclusive() { fmt.Printf("Inconclusive: %v\n", outcome.Reason) fmt.Printf("Current estimate: P=%.1f%%\n", outcome.LeakProbability*100)} else if outcome.IsUnmeasurable() { fmt.Printf("Operation: %.1f ns\n", outcome.OperationNs) fmt.Printf("Threshold: %.1f ns\n", outcome.ThresholdNs) fmt.Printf("Recommendation: %s\n", outcome.Recommendation)}Summary
Section titled “Summary”| Symptom | First thing to check |
|---|---|
| High noise / TooNoisy | Run with --test-threads=1 |
| False positive | Run sanity check (identical inputs) |
| Can’t detect leak | Check input class selection |
| Unmeasurable | Use cycle-precision timer or batch operations |
| Inconsistent results | Check CPU governor, disable turbo |
| macOS kperf not working | Use both sudo AND --test-threads=1 |