krypteia-silentops 0.1.0

Side-channel countermeasure toolkit: constant-time primitives, dudect-style timing leakage verifier, and shared SCA helpers for the krypteia workspace.
Documentation
//! # ct_grind — Valgrind/memcheck-backed constant-time verification
//!
//! This module implements the **ctgrind** technique (Adam Langley,
//! 2010; used by BearSSL, ring, etc.): secret-bearing buffers are
//! marked as *uninitialized* in Valgrind's memcheck shadow memory,
//! and the program is then run under `valgrind --error-exitcode=1`.
//! Any branch, pointer derefence, or syscall that consumes an
//! uninitialized byte is flagged as an error. Because memcheck
//! propagates definedness through arithmetic, conditional-move tricks
//! and bitmasked selects are tracked correctly — only true branches
//! or secret-indexed memory accesses on the hot path trigger a
//! diagnostic.
//!
//! The instrumentation itself is implemented as two Valgrind client
//! requests:
//!
//! | Helper         | Valgrind request                        |
//! |----------------|------------------------------------------|
//! | [`poison`]     | `VG_USERREQ__MAKE_MEM_UNDEFINED` (secret)|
//! | [`unpoison`]   | `VG_USERREQ__MAKE_MEM_DEFINED`   (public)|
//!
//! # Architectures
//!
//! Client-request instrumentation is emitted only on
//! `target_os = "linux"` and either `x86_64` or `aarch64`. On every
//! other target, and whenever the `ct-grind` cargo feature is off,
//! [`poison`] / [`unpoison`] / [`poison_raw`] / [`unpoison_raw`] are
//! **zero-cost no-ops**, which lets call sites embed them
//! unconditionally (no `#[cfg]` walls at the use site).
//!
//! # Zero dependencies
//!
//! The Valgrind magic sequences are implemented with stable
//! `core::arch::asm!` — no C shim, no crate dependency. This mirrors
//! the BearSSL approach and keeps the krypteia *zero deps* rule.
//!
//! # Safety
//!
//! [`poison`] and [`unpoison`] take shared slice references and are
//! safe. The raw-pointer variants [`poison_raw`] / [`unpoison_raw`]
//! are `unsafe` because the caller must guarantee that `ptr..ptr+len`
//! is a valid addressable region; Valgrind only manipulates its
//! shadow metadata and never reads or writes the underlying bytes.
//!
//! # Example
//!
//! ```no_run
//! use silentops::ct_grind;
//!
//! let mut secret = [0u8; 32];
//! // …fill secret from an RNG…
//! ct_grind::poison(&secret);     // memcheck: "now undefined"
//!
//! // Do CT work with `secret`. A leak would trigger a valgrind error.
//!
//! ct_grind::unpoison(&secret);   // memcheck: "defined again"
//! ```

#[cfg(all(
    feature = "ct-grind",
    target_os = "linux",
    any(target_arch = "x86_64", target_arch = "aarch64"),
))]
mod backend;

#[cfg(not(all(
    feature = "ct-grind",
    target_os = "linux",
    any(target_arch = "x86_64", target_arch = "aarch64"),
)))]
#[path = "noop.rs"]
mod backend;

/// Mark `buf` as **undefined** (secret) in Valgrind's memcheck shadow
/// memory. Any later branch or memory-access decision that depends
/// on these bytes will be flagged by `valgrind --error-exitcode=1`.
#[inline]
pub fn poison<T>(buf: &[T]) {
    let ptr = buf.as_ptr() as *const u8;
    let len = core::mem::size_of_val(buf);
    unsafe { poison_raw(ptr, len) };
}

/// Mark `buf` as **defined** again — the complement of [`poison`].
/// Call this once a buffer can legitimately flow into public data
/// (e.g. a ciphertext that is about to be returned to the caller).
#[inline]
pub fn unpoison<T>(buf: &[T]) {
    let ptr = buf.as_ptr() as *const u8;
    let len = core::mem::size_of_val(buf);
    unsafe { unpoison_raw(ptr, len) };
}

/// Raw-pointer variant of [`poison`]. See [module docs](self) for safety.
///
/// Sandwich the client request between two SeqCst compiler fences so
/// LLVM cannot reorder any surrounding memory access across the
/// poisoning — empirically, without the fences LLVM batches the
/// unpoison calls past subsequent `assert_eq!`/`memcmp`, so the
/// comparison runs on bytes that memcheck still marks undefined.
///
/// # Safety
///
/// `ptr` must point to a valid memory region of at least `len`
/// bytes that the caller currently owns (no aliasing constraint
/// stricter than for any other memcheck client request). Passing
/// a dangling pointer, a region the caller does not own, or a
/// `len` that overruns the allocation is undefined behaviour.
/// When the `ct-grind` Cargo feature is disabled the call is a
/// zero-cost no-op (only the compiler fences remain) so the
/// safety contract is trivial in that mode.
#[inline]
pub unsafe fn poison_raw(ptr: *const u8, len: usize) {
    core::sync::atomic::compiler_fence(core::sync::atomic::Ordering::SeqCst);
    // SAFETY: caller upholds the contract on `ptr` / `len`; we
    // forward to the backend's matching `unsafe` primitive. The
    // explicit `unsafe { ... }` wrap is required by edition 2024
    // (`unsafe_op_in_unsafe_fn` is a default-warn lint from 1.85).
    unsafe { backend::make_mem_undefined(ptr, len) };
    core::sync::atomic::compiler_fence(core::sync::atomic::Ordering::SeqCst);
}

/// Raw-pointer variant of [`unpoison`]. See [module docs](self) for safety.
///
/// See [`poison_raw`] for the rationale behind the surrounding fences.
///
/// # Safety
///
/// Same contract as [`poison_raw`]: `ptr` must point to a valid
/// memory region of at least `len` bytes that the caller currently
/// owns. Calling `unpoison_raw` on a region that was never
/// `poison_raw`'d is allowed (it is a no-op as far as memcheck
/// is concerned) but the pointer / length validity contract
/// still applies. When the `ct-grind` Cargo feature is disabled
/// the call is a zero-cost no-op around the compiler fences.
#[inline]
pub unsafe fn unpoison_raw(ptr: *const u8, len: usize) {
    core::sync::atomic::compiler_fence(core::sync::atomic::Ordering::SeqCst);
    // SAFETY: caller upholds the contract on `ptr` / `len`; we
    // forward to the backend's matching `unsafe` primitive. See
    // `poison_raw` above for the edition-2024 rationale.
    unsafe { backend::make_mem_defined(ptr, len) };
    core::sync::atomic::compiler_fence(core::sync::atomic::Ordering::SeqCst);
}

/// Whether this build emits real Valgrind client requests.
///
/// `true` only when the `ct-grind` feature is enabled **and** the
/// target is `x86_64-linux` or `aarch64-linux`. Useful in tests
/// that want to skip or annotate behaviour when ctgrind is inert.
pub const fn is_active() -> bool {
    cfg!(all(
        feature = "ct-grind",
        target_os = "linux",
        any(target_arch = "x86_64", target_arch = "aarch64"),
    ))
}