A Zig library for secure, relocatable, AES-encrypted heap allocations — designed to protect sensitive memory from dumps, frustrate reverse-engineering, and harden software against cracking.
TL;DR
- Allocate memory via
alloc(T, n)
orcreate(T)
and get back an opaqueHandle
— no direct pointers to leak.- Memory is encrypted at rest with AES-CTR and decrypted only while borrowed.
- Periodically shuffle unlocked entries across arenas to break predictable layouts and resist memory forensics.
- Thread-safe, alignment-preserving, and equipped with key rotation for long-lived secrets.
- Opaque handles, not raw pointers. Prevents address leaks and enables safe relocation.
- AES‑CTR encryption at rest. Protects against memory dumps, debugging, and forensic tools.
- Relocation/shuffling. Moves unlocked entries across arenas to break memory snapshots and hinder static analysis.
- Arena-based allocator. Bulk reset for empty arenas; reduces fragmentation.
- Alignment preserved. Runtime alignment is maintained after shuffles.
- Thread-safe. All state changes guarded by a mutex.
- Key rotation. Swap encryption key/salt without losing data.
const std = @import("std");
const root = @import("mem_shuffle_lib");
pub fn main() !void {
var gpa = std.heap.page_allocator; // or your allocator
var sh = try root.Shuffler.init(gpa);
defer sh.deinit();
// Optional: shuffle on every borrow/return
sh.setShuffleOnBorrowReturn(true);
// Allocate a u32 and write to it
const h = try sh.alloc(u32, 1);
{
const p = sh.rentPointer(h, *u32);
p.* = 0xDEADBEEF;
sh.returnPointer(h);
}
// Read it back securely
{
const p = sh.rentPointer(h, *u32);
try std.testing.expectEqual(@as(u32, 0xDEADBEEF), p.*);
sh.returnPointer(h);
}
sh.free(h);
try sh.shuffle();
}
The snippets below focus on the main surface area. See inline docs for details.
pub const Shuffler = struct {
pub fn init(allocator: std.mem.Allocator) !Shuffler;
pub fn deinit(self: *Shuffler) void;
};
pub fn alloc(self: *Shuffler, comptime T: type, n: usize) !Handle; // n > 0
pub fn create(self: *Shuffler, comptime T: type) !Handle; // @sizeOf(T) > 0
pub fn rentPointer(self: *Shuffler, h: Handle, comptime P: type) P; // P must be a pointer type
pub fn returnPointer(self: *Shuffler, h: Handle) void; // re‑encrypts and unlocks
pub fn getSize(self: *Shuffler, h: Handle) usize; // size in bytes
- While borrowed, the entry is decrypted and marked locked (it will not be moved).
- On
returnPointer
, data is re‑encrypted and may be shuffled depending on settings.
pub fn shuffle(self: *Shuffler) !void; // moves unlocked entries; clears freed
pub fn free(self: *Shuffler, h: Handle) void; // mark for clear; actual drop happens in shuffle
pub fn setShuffleOnBorrowReturn(self: *Shuffler, enable: bool) void;
shuffle()
compacts unlocked entries into an (empty) destination arena, then resets empty arenas to reclaim memory.free()
does not immediately release memory; instead it marks the entry and it’s removed during the next shuffle.
pub fn rotateKey(self: *Shuffler, new_key: ?*const [32]u8, new_salt: ?*const [8]u8) !void;
- If a key/salt is provided, it’s used; otherwise random values are generated.
- The shuffler decrypts any encrypted entries, swaps keys, then re‑encrypts.
pub fn validHandle(self: *Shuffler, h: Handle) bool;
- Handle integrity. Internal maps ensure each
Handle
corresponds to exactly one liveMemoryEntry
. - Alignment. After a shuffle, aligned allocations remain aligned (
@alignOf(T)
is preserved). - Pointer stability while locked. A borrowed entry will not move across
shuffle()
calls until it is returned. - Thread safety. All public methods acquire a mutex; concurrent workers can rent/return/shuffle safely.
The repository includes a suite of std.testing
unit tests:
- Fuzzer: random mix of alloc/create/free/borrow/mutate/shuffle while checking invariants.
- Integrity: a long‑lived allocation retains its value across many shuffles and churn.
- Alignment: pointers to
u64
/f64
stay properly aligned after relocations. - Locked stability: a borrowed pointer’s address is unchanged across repeated shuffles until returned.
- Shuffle‑on‑return: when enabled, returning a pointer usually changes its address (sanity check).
- Key rotation: data remains intact across key/salt changes (fixed and random).
- Concurrency smoke: multiple threads allocate/borrow/shuffle and we validate invariants/leaks.
Run them with:
zig build test
- Why arenas? They give fast bulk reset when empty and make compaction predictable.
- Why AES‑CTR? Xor‑ing a keystream lets us encrypt in place without keeping additional buffers. The counter block uses the shuffler’s salt and the entry’s handle.
- Relocation: During a shuffle, destinations are over‑allocated and then aligned at runtime; bytes are copied and internal maps updated before old arenas are reset.
- You must call
returnPointer
. Forgetting to return leaves the entry unlocked+decrypted and prevents relocation. free()
is deferred. Memory isn’t reclaimed until the nextshuffle()
.- Not a general‑purpose allocator. This is a utility for handle‑based, relocatable, encrypted allocations, not a drop‑in
Allocator
replacement.
- Optional per‑entry lifetimes / auto‑shuffle cadence.
- Pluggable cipher/keystream and authenticated encryption.
- Stats/telemetry (bytes moved, arenas reset, shuffle durations).
Add the package to your build.zig
and import it:
const root = @import("mem_shuffle_lib");
If you vendored the code, adjust the path accordingly. (A Zig package manager snippet can be added once published.)
PRs and issues welcome! Please include a failing test when reporting a bug.
MIT