FFI Safety¶
The rocklake-ffi crate exposes a C-compatible API over the Rust catalog stack. All FFI functions are extern "C" and accept raw pointers. This document describes the pointer ownership model, handle lifecycle, and the safety invariants that callers and the Rust implementation must uphold.
Pointer Ownership¶
| Pointer | Owner | Freed by |
|---|---|---|
*mut RockLakeCatalog (from rocklake_open) | C caller | rocklake_close() |
*mut RockLakeSchemaList (embedded) | C caller | rocklake_schema_list_free() |
*mut RockLakeTableList (embedded) | C caller | rocklake_table_list_free() |
*mut RockLakeColumnList (embedded) | C caller | rocklake_column_list_free() |
*mut RockLakeFileList (embedded) | C caller | rocklake_file_list_free() |
*mut RockLakeError (stack) | C caller | rocklake_error_free() for the message field |
Handle Lifecycle¶
rocklake_open() → *mut RockLakeCatalog → {use} → rocklake_close()
│
magic zeroed ──┘ (prevents double-close)
rocklake_open()allocates aRockLakeCatalogon the heap viaBox::newand returns a raw pointer (Box::into_raw).rocklake_close()checks and zeroes the magic field before reconstructing theBoxand dropping it.- A second call to
rocklake_close()with the same pointer is a safe no-op: the magic check fails and the function returns early without touching the already-freed memory. - Any catalog operation after
rocklake_close()(use-after-free) is caught by thewith_catalog()magic check and returnsInvalidHandlewithout dereferencing stale memory.
with_catalog Safety Pattern¶
Internally, all catalog operations go through with_catalog(ptr, |cat| { … }) instead of the old validate_catalog() which returned Option<&'static mut RockLakeCatalog>. The new pattern:
- Returns
None(→InvalidHandleerror) for null pointers. - Returns
Nonefor any pointer where the magic field is notCATALOG_MAGIC(covers zeroed-on-close handles and wild pointers). - Creates a mutable reference bounded by the closure frame — the reference cannot escape to a longer lifetime.
SAFETY Invariants¶
Every unsafe block in lib.rs has a // SAFETY: comment stating:
- Which precondition (non-null check, magic check, or both) justifies the dereference.
- Whether ownership is being transferred (
Box::from_raw) or borrowed (&mut *ptr). - The aliasing constraint: no other thread may concurrently close or mutate the same handle during the call.
Caller Responsibilities¶
The following are the C caller's responsibilities (not enforced by Rust):
- No concurrent close and use. If thread A calls
rocklake_close(cat)while thread B calls any other function withcat, the behaviour is undefined at the hardware level even if the magic check would fire. Guard concurrent access with a mutex in the C layer. - No use after close. After
rocklake_close()returns, the pointer is dangling. The magic-number guard is a best-effort defence, not a substitute for correct ownership. - Free list types before reuse. Free
RockLakeSchemaList,RockLakeTableList, etc. with their respective_freefunctions before overwriting the struct.
Vec Capacity Invariant¶
All list-builder functions call .shrink_to_fit() before std::mem::forget(). This ensures capacity == len at the time the Vec is forgotten, which is required for Vec::from_raw_parts(ptr, len, len) in the _free functions to reconstruct the original allocation correctly.
Sanitizer and Miri CI¶
A scheduled nightly CI workflow (.github/workflows/sanitizers.yml) runs:
- ASAN: detects heap use-after-free, double-free, and buffer overflows.
- UBSAN: detects undefined behaviour including invalid pointer casts and signed overflow.
- Miri: interprets the Rust MIR and catches UB in pure-Rust
unsafecode.
These jobs are currently continue-on-error: true and will be promoted to blocking at v1.0.