A standalone C library providing mmap/mprotect/mlock-backed secure memory buffers for cryptographic keys. Zero external dependencies. Portable across Linux, macOS, FreeBSD, and Windows.
libpqcsb is a cryptographic-grade secure memory container that protects key material from:
- Accidental memory disclosure - Guard pages (PROT_NONE) before and after data
- Buffer overflows - Right-aligned canaries trigger segfault on overflow
- Swap disclosure - mlock prevents swapping to disk
- Tamper detection - Canary verification detects corruption
- Timing attacks - Constant-time equality comparison
- DRAM dumps - Secure zeroing on deallocation
- Not an encryption library - Use libsodium, OpenSSL, or WolfSSL for that
- Not a replacement for OS protections - Complements, not replaces, kernel security
- Not immune to privileged attackers - Root/kernel can inspect any memory
- Not async-safe - Caller must not signal-interrupt during read/write access
- Not deterministic on all platforms - mlock may fail silently on resource-limited systems
┌─────────────────────┐
│ Guard Page (PROT_NONE) │
├─────────────────────┤
│ [PAD] │
│ [CANARY_PRE] │ Right-aligned to maximize
│ [USER_DATA] ──────→ overflow detection
│ [CANARY_POST] │
├─────────────────────┤
│ Guard Page (PROT_NONE) │
└─────────────────────┘
- Guard pages: Allocated via mmap with PROT_NONE. Reads/writes trigger SIGSEGV.
- Right-alignment: User data pointer is calculated so buffer overflows immediately hit rear guard, not just canary.
- Canaries: 16-byte (configurable) random markers before and after data. Verified on deallocation and canary checks.
- Fallback: On systems without mmap, uses malloc + canaries only (no guard pages, weaker protection).
pqcsb_read_guard_t guard = pqcsb_begin_read(buf); // mprotect(PROT_READ)
if (guard.status == PQCSB_OK) {
const uint8_t *data = guard.data;
// ... use data ...
}
pqcsb_end_read(&guard); // mprotect(PROT_NONE)- PROT_READ access: Guard pages readable (for comparisons), guard pages unreadable (for overflow detection)
- PROT_NONE when idle: Prevents accidental reads from untrusted code
- Atomic reference counting: Multiple readers can overlap; only last reader re-protects to PROT_NONE
- PROT_READ|PROT_WRITE for mutations:
pqcsb_begin_writeallows in-place modification
| Feature | Linux | macOS | FreeBSD | Windows |
|---|---|---|---|---|
| Guard pages | mmap + PROT_NONE | mmap + PROT_NONE | mmap + PROT_NONE | VirtualAlloc + PAGE_NOACCESS |
| Memory locking | mlock (RLIMIT_MEMLOCK budgeted) | mlock | mlock | VirtualLock |
| Access control | mprotect | mprotect | mprotect | VirtualProtect |
| Core dump exclusion | MADV_DONTDUMP | MAP_NOCORE | MAP_NOCORE | - |
| Fork protection | MADV_WIPEONFORK | - | - | - |
| Secure zero | explicit_bzero | memset_s | explicit_bzero | SecureZeroMemory |
| CSPRNG | getrandom (fallback: /dev/urandom) | arc4random_buf | arc4random_buf | RtlGenRandom |
git clone https://github.com/pqcsb/libpqcsb
cd libpqcsb
mkdir build && cd build
cmake .. -DCMAKE_BUILD_TYPE=Release
cmake --build . -j4
sudo cmake --install .After installation:
pkg-config --cflags --libs libpqcsb
# -I/usr/local/include -L/usr/local/lib -lpqcsbFor single-file distribution (Ruby/Python C extensions):
./scripts/concat.sh # Produces dist/pqcsb.c
cp dist/pqcsb.c include/pqcsb.h src/internal.h pqcsb_config.h /vendor/path/The single-file bundle includes all three .c files (secure_zero.c, random.c, pqcsb.c) plus headers.
cmake .. \
-DPQCSB_BUILD_SHARED=ON # Build shared library (default: ON)
-DPQCSB_BUILD_STATIC=ON # Build static library (default: ON)
-DPQCSB_BUILD_TESTS=ON # Build test suite (default: ON)
-DPQCSB_ALLOW_MALLOC_FALLBACK=OFF # Enable malloc-only fallback (default: OFF)Note: PQCSB_ALLOW_MALLOC_FALLBACK=ON is required on systems without mmap/VirtualAlloc. The malloc fallback provides no guard pages - only canaries. Not recommended for production.
pqcsb_status_t pqcsb_create(const uint8_t *data, size_t len, pqcsb_buf_t **out);Allocate a buffer, copy data into it, apply protections. On success, *out points to an opaque buffer; on failure, *out == NULL and status code is returned.
uint8_t key[32] = {...};
pqcsb_buf_t *buf;
pqcsb_status_t rc = pqcsb_create(key, 32, &buf);
if (rc == PQCSB_OK) {
// buf is ready; use pqcsb_begin_read/begin_write
}pqcsb_status_t pqcsb_create_ex(const uint8_t *data, size_t len,
const pqcsb_config_t *config,
pqcsb_buf_t **out);Create with custom options (mlock budget, canary checks, etc.).
pqcsb_config_t config = { .use_mlock = 1, .check_canaries_on_read = 1 };
pqcsb_create_ex(key, 32, &config, &buf);pqcsb_status_t pqcsb_create_random(size_t len, pqcsb_buf_t **out);Allocate and fill from platform CSPRNG (no arguments to fill).
pqcsb_buf_t *ephemeral_key;
pqcsb_create_random(32, &ephemeral_key);pqcsb_status_t pqcsb_create_inplace(
size_t len,
pqcsb_status_t (*fill)(uint8_t *data, size_t len, void *ctx),
void *ctx,
pqcsb_buf_t **out);Allocate and call a callback to populate data. Callback receives writable region and must return PQCSB_OK.
pqcsb_status_t my_fill(uint8_t *data, size_t len, void *ctx) {
// Fill data in-place
return pqcsb_fill_random(data, len);
}
pqcsb_create_inplace(32, my_fill, NULL, &buf);pqcsb_status_t pqcsb_clone(pqcsb_buf_t *src, pqcsb_buf_t **out);Deep copy entire buffer into a new independent buffer.
void pqcsb_destroy(pqcsb_buf_t **buf);Securely wipe and deallocate. Safe to call on NULL. Sets *buf = NULL on return.
pqcsb_destroy(&buf);
// buf is now NULLpqcsb_read_guard_t pqcsb_begin_read(pqcsb_buf_t *buf);
void pqcsb_end_read(pqcsb_read_guard_t *guard);Read-only access. Guard returned by value with status field.
pqcsb_read_guard_t guard = pqcsb_begin_read(buf);
if (guard.status == PQCSB_OK) {
const uint8_t *data = guard.data; // read-only
size_t len = guard.len;
// ... use data ...
}
pqcsb_end_read(&guard); // guard._priv set to NULLThread-safe via atomic reference counting. Multiple threads can hold concurrent read guards.
pqcsb_write_guard_t pqcsb_begin_write(pqcsb_buf_t *buf);
void pqcsb_end_write(pqcsb_write_guard_t *guard);Read-write access. Canaries rewritten on pqcsb_end_write.
pqcsb_write_guard_t guard = pqcsb_begin_write(buf);
if (guard.status == PQCSB_OK) {
uint8_t *data = guard.data; // writable
// ... modify data ...
}
pqcsb_end_write(&guard); // canaries rewritten, re-protectedNot thread-safe - external synchronization required if multiple writers.
size_t pqcsb_len(const pqcsb_buf_t *buf); // User data length
int pqcsb_is_wiped(const pqcsb_buf_t *buf); // 1 if wipe() called
int pqcsb_is_locked(const pqcsb_buf_t *buf); // 1 if mlock succeeded
int pqcsb_is_readable(const pqcsb_buf_t *buf); // 1 if readers active
int pqcsb_get_read_refcount(const pqcsb_buf_t *buf); // Number of active readers
size_t pqcsb_get_allocation_size(const pqcsb_buf_t *buf); // Total mmap'd bytespqcsb_status_t pqcsb_wipe(pqcsb_buf_t *buf);Securely zero data + canaries, munlock. Buffer remains allocated but all operations return PQCSB_ERR_WIPED afterward. Returns PQCSB_ERR_BUSY if readers are active.
pqcsb_status_t pqcsb_check_canary(pqcsb_buf_t *buf);Verify canary integrity. Returns PQCSB_OK or PQCSB_ERR_CANARY.
pqcsb_status_t pqcsb_slice(pqcsb_buf_t *buf, size_t offset, size_t length,
pqcsb_buf_t **out);Extract sub-range into new buffer. Source temporarily unlocked for copy.
int pqcsb_ct_equal(pqcsb_buf_t *a, const uint8_t *b, size_t b_len);
int pqcsb_ct_equal_bufs(pqcsb_buf_t *a, pqcsb_buf_t *b);Constant-time equality (1 = equal, 0 = not). Resists timing attacks.
void pqcsb_secure_zero(void *ptr, size_t len); // Secure memset
pqcsb_status_t pqcsb_fill_random(uint8_t *dst, size_t len); // CSPRNG filltypedef enum {
PQCSB_OK = 0, // Success
PQCSB_ERR_ALLOC = -1, // mmap/malloc failed
PQCSB_ERR_MLOCK = -2, // mlock failed (non-fatal)
PQCSB_ERR_MPROTECT = -3, // mprotect/VirtualProtect failed
PQCSB_ERR_WIPED = -4, // Buffer was wiped
PQCSB_ERR_CANARY = -5, // Canary corruption
PQCSB_ERR_RANDOM = -6, // CSPRNG failure
PQCSB_ERR_RANGE = -7, // Bounds / range error
PQCSB_ERR_NULL_PARAM = -8, // NULL argument
PQCSB_ERR_BUSY = -9, // Operation blocked (e.g., wipe while reading)
} pqcsb_status_t;pqcsb_error_info_t pqcsb_get_error_info(pqcsb_status_t code);
typedef struct {
pqcsb_status_t code;
int is_fatal; // 1 = buffer permanently unusable
int is_recoverable; // 1 = operation may be retried
} pqcsb_error_info_t;Bindings can use is_fatal and is_recoverable to decide error handling strategy.
-
Safe (no external lock needed):
pqcsb_begin_read/pqcsb_end_read- Atomic refcount allows concurrent readerspqcsb_len,pqcsb_is_wiped,pqcsb_is_locked- Simple queriespqcsb_destroy- Can be called from any thread (idempotent)pqcsb_is_readable,pqcsb_get_read_refcount- Atomic load only
-
Unsafe (requires external synchronization):
pqcsb_begin_write+pqcsb_end_write- Not protected against concurrent writespqcsb_wipe- Fails withPQCSB_ERR_BUSYif readers active; otherwise safepqcsb_check_canary- Temporarily acquires read lock; safe if no concurrent wipespqcsb_slice- Temporarily acquires read lock; safe if no concurrent wipes
Example: Shared key with multiple readers
// Thread 1-N
pqcsb_read_guard_t guard = pqcsb_begin_read(shared_key);
if (guard.status == PQCSB_OK) {
// All threads can read concurrently
verify_signature(guard.data, guard.len);
}
pqcsb_end_read(&guard);
// Thread main (serial wipe)
if (!pqcsb_is_readable(shared_key)) { // Check no readers active
pqcsb_wipe(shared_key); // Now safe to wipe
}#define PQCSB_MALLOC ruby_xmalloc
#define PQCSB_FREE ruby_xfree
#include "pqcsb.h"
// Wrap pqcsb_buf_t in TypedData
static const rb_data_type_t pqcsb_buf_type = { ... };
static VALUE
rb_pqcsb_create(VALUE self, VALUE data_str) {
pqcsb_buf_t *buf;
pqcsb_status_t rc = pqcsb_create(
RSTRING_PTR(data_str),
RSTRING_LEN(data_str),
&buf
);
if (rc != PQCSB_OK)
rb_raise(rb_eRuntimeError, "%s", pqcsb_error_message(rc));
return TypedData_Make_Struct(cPqcsbBuffer, pqcsb_buf_t,
&pqcsb_buf_type, buf, buf);
}
static VALUE
rb_pqcsb_use(VALUE self, VALUE obj) {
pqcsb_buf_t *buf;
TypedData_Get_Struct(obj, pqcsb_buf_t, &pqcsb_buf_type, buf);
pqcsb_read_guard_t guard = pqcsb_begin_read(buf);
if (guard.status != PQCSB_OK)
rb_raise(rb_eRuntimeError, "%s", pqcsb_error_message(guard.status));
VALUE result = rb_yield(rb_str_new(guard.data, guard.len));
pqcsb_end_read(&guard);
return result;
}from cffi import FFI
ffi = FFI()
ffi.cdef("""
typedef struct pqcsb_buf pqcsb_buf_t;
typedef struct {
const uint8_t *data;
size_t len;
int status;
pqcsb_buf_t *_priv;
} pqcsb_read_guard_t;
pqcsb_status_t pqcsb_create(const uint8_t *, size_t, pqcsb_buf_t **);
pqcsb_read_guard_t pqcsb_begin_read(pqcsb_buf_t *);
void pqcsb_end_read(pqcsb_read_guard_t *);
void pqcsb_destroy(pqcsb_buf_t **);
...
""")
lib = ffi.dlopen("libpqcsb")
class SecureBuffer:
def __init__(self, data):
buf_ptr = ffi.new("pqcsb_buf_t**")
rc = lib.pqcsb_create(data, len(data), buf_ptr)
if rc != 0:
raise RuntimeError(ffi.string(lib.pqcsb_error_message(rc)).decode())
self.buf = buf_ptr[0]
def __enter__(self):
self.guard = lib.pqcsb_begin_read(self.buf)
if self.guard.status != 0:
raise RuntimeError(...)
return ffi.buffer(self.guard.data, self.guard.len)[:]
def __exit__(self, *args):
lib.pqcsb_end_read(self.guard_ptr)
def __del__(self):
lib.pqcsb_destroy(self.buf)use std::ptr;
pub struct SecureBuffer {
buf: *mut pqcsb_buf_t,
}
impl SecureBuffer {
pub fn new(data: &[u8]) -> Result<Self, i32> {
let mut buf_ptr: *mut pqcsb_buf_t = ptr::null_mut();
let rc = unsafe {
pqcsb_create(data.as_ptr(), data.len(), &mut buf_ptr)
};
if rc == 0 {
Ok(SecureBuffer { buf: buf_ptr })
} else {
Err(rc)
}
}
pub fn read<F, R>(&self, f: F) -> R
where
F: FnOnce(&[u8]) -> R,
{
let mut guard = unsafe { pqcsb_begin_read(self.buf) };
if guard.status != 0 {
panic!("begin_read failed: {}", guard.status);
}
let slice = unsafe { std::slice::from_raw_parts(guard.data, guard.len) };
let result = f(slice);
unsafe { pqcsb_end_read(&mut guard) };
result
}
}
impl Drop for SecureBuffer {
fn drop(&mut self) {
unsafe { pqcsb_destroy(&mut self.buf) };
}
}libpqcsb follows semantic versioning for ABI:
- PQCSB_VERSION: Release version (0.1.0, 0.2.0, 1.0.0)
- PQCSB_ABI_VERSION: Struct/calling-convention version (1.0)
- Incremented only on breaking ABI changes
- Minor bump allows forward-compatible additions
Language bindings should call pqcsb_check_abi_version() at initialization:
pqcsb_status_t rc = pqcsb_check_abi_version(
PQCSB_ABI_VERSION_MAJOR,
PQCSB_ABI_VERSION_MINOR
);
if (rc != PQCSB_OK) {
fprintf(stderr, "ABI mismatch; recompile required\n");
return 1;
}Tested on:
- Linux: glibc 2.25+ (getrandom), musl
- macOS: 10.12+ (arc4random_buf, memset_s)
- FreeBSD: 11.0+ (arc4random_buf, explicit_bzero)
- Windows: XP+ (RtlGenRandom, VirtualAlloc)
No external dependencies except:
- POSIX pthreads (optional, only for concurrent test)
- C11
<stdatomic.h>(required)
Default is 16 bytes. Override before including header:
#define PQCSB_CANARY_SIZE 32
#include "pqcsb.h"Override malloc/free for language bindings:
#define PQCSB_MALLOC ruby_xmalloc
#define PQCSB_FREE ruby_xfree
#include "pqcsb.h"Only affects pqcsb_buf_t struct allocation; data region always uses mmap/VirtualAlloc/malloc canaries.
Dual-licensed under MIT or Apache 2.0 at your choice. See LICENSE.
Contributions welcome! Please:
- Write tests for any changes
- Run
cmake --build . && ctest --output-on-failure - Keep zero external dependencies
- Maintain C11 portability
- libpqcasn1 - DER/PEM/Base64 codec for PQC algorithms
- libsodium - General-purpose cryptography library (higher-level)
- WolfSSL - Lightweight TLS library with secure memory options