Safety Model
Contract
Every unsafe block in native-ossl must be accompanied by a // SAFETY: comment
explaining:
- Why the raw pointer is non-null at that point
- Why the lifetime of any borrowed pointers is correctly bounded
- 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:
usizetoi32(forRAND_byteslength): usei32::try_from(len).expect(...), or split into multiple calls iflen > i32::MAX.usizetoc_int(for various length parameters): uselibc::c_int::try_from.- C
intreturn values that represent lengths: check for< 0before converting tousize.
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 testdetects use-after-free, invalid pointer dereferences, and incorrectunsafepatterns 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 testdetects 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.