Keyboard shortcuts

Press or to navigate between chapters

Press S or / to search in the book

Press ? to show this help

Press Esc to hide this help

Safety Model

Contract

Every unsafe block in native-ossl must be accompanied by a // SAFETY: comment explaining:

  1. Why the raw pointer is non-null at that point
  2. Why the lifetime of any borrowed pointers is correctly bounded
  3. Why the operation is free of data races

Template:

#![allow(unused)]
fn main() {
// SAFETY:
// - self.ptr is non-null (invariant upheld by constructor)
// - data slice outlives this call (borrow checker enforces via &self lifetime)
// - &mut self ensures exclusive access to the context
let rc = unsafe { sys::EVP_DigestUpdate(self.ptr, data.as_ptr().cast(), data.len()) };
}

Inline safety comments that are vague (“safe because we checked”) or missing are treated as defects and are rejected in review.

Pointer Invariants

Every wrapper struct maintains the invariant that its raw pointer is non-null and valid for the object’s lifetime. This is established by constructors and never violated by wrapper methods.

#![allow(unused)]
fn main() {
pub fn new() -> Result<Self, ErrorStack> {
    let ptr = unsafe { sys::EVP_MD_CTX_new() };
    if ptr.is_null() {
        return Err(ErrorStack::drain());
    }
    // From this point on, ptr is guaranteed non-null.
    Ok(DigestCtx { ptr })
}
}

No public method takes an Option<*mut T> or nullable pointer. All NULL-return C functions are checked immediately and converted to Err(ErrorStack::drain()).

Ownership Rules

Objects with _up_ref

When a C type has an _up_ref function, Rust wrappers use Clone to share ownership:

#![allow(unused)]
fn main() {
impl Clone for DigestAlg {
    fn clone(&self) -> Self {
        // SAFETY: self.ptr is non-null (constructor invariant).
        // EVP_MD_up_ref increments the refcount; both clones free in Drop.
        unsafe { sys::EVP_MD_up_ref(self.ptr) };
        DigestAlg { ptr: self.ptr }
    }
}

impl Drop for DigestAlg {
    fn drop(&mut self) {
        // SAFETY: self.ptr is non-null (constructor invariant).
        unsafe { sys::EVP_MD_free(self.ptr) };
    }
}
}

Both sides call free in Drop; OpenSSL releases the object when the count reaches zero. The Rust borrow checker ensures no use-after-free within safe code.

Objects without _up_ref

Stateful contexts (EVP_MD_CTX, EVP_CIPHER_CTX, etc.) have no up_ref. They are !Clone and all mutating methods take &mut self:

#![allow(unused)]
fn main() {
// EVP_MD_CTX is !Clone — the type system enforces exclusive ownership.
pub struct DigestCtx {
    ptr: *mut sys::EVP_MD_CTX,
}
}

This mirrors the OpenSSL threading contract: do not call EVP_DigestUpdate from two threads simultaneously on the same EVP_MD_CTX.

BIO Ownership and Transfer

SSL_set_bio transfers ownership of the BIO to the SSL object. The Rust wrapper uses std::mem::forget to relinquish its own Drop obligation:

#![allow(unused)]
fn main() {
pub fn set_bio_duplex(&mut self, bio: Bio) {
    let ptr = bio.as_ptr();
    std::mem::forget(bio);  // transfer ownership to OpenSSL
    // SAFETY:
    // - self.ptr is non-null (constructor invariant)
    // - ptr is non-null (Bio::as_ptr invariant)
    // - mem::forget prevents the Rust Drop from running, avoiding double-free
    unsafe { sys::SSL_set_bio(self.ptr, ptr, ptr) };
}
}

This pattern is also used by X509Builder::build() to transfer the X509* from builder to the returned X509 without running the builder’s Drop.

Borrowed Pointers: Lifetime Binding

OpenSSL’s get0_* functions return internal borrowed pointers. These are exposed as Rust references with a lifetime bound to the owning object:

#![allow(unused)]
fn main() {
/// Subject distinguished name. Borrowed — valid while `self` is alive.
pub fn subject_name(&self) -> X509Name<'_> {
    // SAFETY:
    // - self.ptr is non-null (constructor invariant)
    // - X509_get_subject_name returns a pointer valid for the lifetime of self
    // - X509Name carries PhantomData<&'cert X509> to enforce the lifetime
    let ptr = unsafe { sys::X509_get_subject_name(self.ptr) };
    X509Name { ptr, _owner: PhantomData }
}
}

The PhantomData field ties the borrowed value’s lifetime to the parent, so the compiler rejects any attempt to hold the X509Name past the owning X509.

Error Macros

Two internal macros centralise the pattern of calling a C function and draining the error queue on failure:

#![allow(unused)]
fn main() {
/// For functions that return 1 on success and 0 on failure.
macro_rules! ossl_call {
    ($expr:expr) => {{
        let rc = unsafe { $expr };
        if rc != 1 {
            return Err($crate::error::ErrorStack::drain());
        }
    }};
}

/// For functions that return NULL on failure.
macro_rules! ossl_ptr {
    ($expr:expr) => {{
        let ptr = unsafe { $expr };
        if ptr.is_null() {
            return Err($crate::error::ErrorStack::drain());
        }
        ptr
    }};
}
}

Using these macros prevents accidental omission of the error-queue drain. If the queue is not drained, stale errors accumulate and corrupt the output of future calls.

Thread Safety

Send and Sync annotations

All wrapper types implement Send where the C object may safely be moved between threads. Sync is implemented where the C object can be read concurrently.

Algorithm descriptors (DigestAlg, CipherAlg, MacAlg) are Send + Sync: OpenSSL algorithm objects are read-only after fetching.

Stateful contexts (DigestCtx, CipherCtx<Dir>, MacCtx) are Send but not Sync: they can be moved between threads, but concurrent mutation is not allowed.

SslCtx is Send + Sync: SSL_CTX_* setup functions are thread-safe when called before any SSL* objects are created from the context.

Ssl is Send but not Sync: a connection is moved to a thread, but sharing across threads without a mutex is not permitted.

Error queue is thread-local

OpenSSL’s error queue is per-thread. ErrorStack::drain() is always called on the same thread that made the failing C call. If you need to transport an error across a thread boundary, use ErrState:

#![allow(unused)]
fn main() {
let handle = std::thread::spawn(|| {
    let result = some_crypto_op();
    let state = native_ossl::error::ErrState::capture();
    (result, state)
});
let (result, state) = handle.join().unwrap();
state.restore_and_drain();
if let Err(e) = result { eprintln!("{e}"); }
}

Integer Casts

Rust as casts between integer types can silently truncate. The codebase uses explicit conversions:

  • usize to i32 (for RAND_bytes length): use i32::try_from(len).expect(...), or split into multiple calls if len > i32::MAX.
  • usize to c_int (for various length parameters): use libc::c_int::try_from.
  • C int return values that represent lengths: check for < 0 before converting to usize.

Never use as usize on a value that could be negative (e.g. EVP_MD_get_size returns int; check for > 0 before the cast).

Testing Safety Properties

  • Miri: running cargo miri test detects use-after-free, invalid pointer dereferences, and incorrect unsafe patterns in pure-Rust tests. C calls are not executed under Miri (they are replaced with stubs), but the Rust-side wrapper logic is validated.
  • Valgrind: valgrind --leak-check=full cargo test detects OpenSSL-side memory leaks and use-after-free that Miri cannot reach.
  • ASAN/UBSAN: compile with RUSTFLAGS="-Z sanitizer=address" under nightly to catch buffer overflows and undefined behaviour at the C boundary.