Skip to main content

Security Best Practices

Secure your Trinity applications with comprehensive input validation, memory safety, and security best practices.

Overview

Trinity provides several security advantages out of the box:

FeatureSecurity BenefitTrinity Advantage
Memory SafetyNo buffer overflowsZig's bounds checking
Type SafetyNo type confusionStrong ternary type system
No Undefined BehaviorPredictable executionZig's compile-time guarantees
Ternary EncodingObfuscation resistanceNon-standard representation

Input Validation

Validating Ternary Vectors

Always validate that inputs contain valid trits (-1, 0, +1):

const std = @import("std");
const vsa = @import("trinity/vsa");

const ValidationError = error{
InvalidTritValue,
EmptyVector,
VectorTooLarge,
};

fn validateTritVector(data: []const i2) ValidationError!void {
// Check empty
if (data.len == 0) return ValidationError.EmptyVector;

// Check size limits
if (data.len > 1_000_000) return ValidationError.VectorTooLarge;

// Validate each trit
for (data) |trit| {
if (trit < -1 or trit > 1) {
return ValidationError.InvalidTritValue;
}
}
}

// Usage example
fn processUserInput(allocator: std.mem.Allocator, input: []const i2) !void {
// Validate before processing
try validateTritVector(input);

// Safe to process
var vec = try vsa.HybridBigInt.fromSlice(allocator, input);
defer vec.deinit(allocator);

// ... continue processing
}

Safe Integer Conversion

Prevent integer overflow and underflow:

fn safeTritToInt(value: i2) !i32 {
return std.math.cast(i32, value) orelse error.Overflow;
}

fn safeIntToTrit(value: i32) !i2 {
if (value < -1 or value > 1) {
return error.InvalidTrit;
}
return @intCast(value);
}

// Batch validation
fn validateIntSlice(values: []const i32) ![]i2 {
const result = try allocator.alloc(i2, values.len);

for (values, 0..) |val, i| {
result[i] = try safeIntToTrit(val);
}

return result;
}

String to Vector Validation

When parsing strings from external sources:

fn parseTernaryVector(allocator: std.mem.Allocator, input: []const u8) ![]i2 {
var trits = std.ArrayList(i2).init(allocator);

var iter = std.mem.splitScalar(u8, input, ',');
while (iter.next()) |part| {
const trimmed = std.mem.trim(u8, part, " \t\r\n");

if (trimmed.len == 0) continue;

const trit = std.fmt.parseInt(i2, trimmed, 10) catch |err| {
std.log.err("Invalid trit value: '{s}'", .{trimmed});
return error.InvalidTritFormat;
};

if (trit < -1 or trit > 1) {
std.log.err("Trit out of range: {}", .{trit});
return error.TritOutOfRange;
}

try trits.append(trit);
}

if (trits.items.len == 0) {
return error.EmptyVector;
}

return trits.toOwnedSlice();
}

Memory Safety

Preventing Buffer Overflows

Zig provides bounds checking by default. Never disable it without careful consideration:

// BAD: Disables bounds checking
fn unsafeOperation(data: []i2, index: usize) i2 {
@setRuntimeSafety(false); // DANGEROUS!
return data[index]; // Could crash or read arbitrary memory
}

// GOOD: Let Zig check bounds
fn safeOperation(data: []i2, index: usize) !i2 {
if (index >= data.len) {
return error.IndexOutOfBounds;
}
return data[index];
}

// BETTER: Use checked arithmetic
fn safeAccess(data: []i2, base: usize, offset: usize) !i2 {
const index = std.math.add(usize, base, offset) catch |err| {
return error.Overflow;
};

if (index >= data.len) {
return error.IndexOutOfBounds;
}

return data[index];
}

Safe Memory Allocation

Always check allocation results and clean up properly:

fn safeVectorClone(allocator: std.mem.Allocator, vec: *const vsa.HybridBigInt) !vsa.HybridBigInt {
// Allocation can fail
var result = try vsa.HybridBigInt.init(allocator, vec.len);
errdefer result.deinit(allocator); // Cleanup on error

// Copy data (bounds checked)
for (0..vec.len) |i| {
result.set(i, vec.get(i));
}

return result;
}

// Using defer for guaranteed cleanup
fn processMultipleVectors(allocator: std.mem.Allocator) !void {
var vec1 = try vsa.HybridBigInt.random(allocator, 1000);
defer vec1.deinit(allocator);

var vec2 = try vsa.HybridBigInt.random(allocator, 1000);
defer vec2.deinit(allocator);

var result = try vsa.HybridBigInt.init(allocator, 1000);
defer result.deinit(allocator);

// All vectors cleaned up even if error occurs
_ = try vsa.bind(&vec1, &vec2, &result);
}

Preventing Memory Leaks

// Memory leak detection
fn testNoLeaks(allocator: std.mem.Allocator) !void {
// Get initial heap usage
const before = allocator.query();

{
var vec = try vsa.HybridBigInt.random(allocator, 10000);
defer vec.deinit(allocator);

// ... operations ...
}

// Check heap is back to original size
const after = allocator.query();
if (after.bytes_used != before.bytes_used) {
std.log.err("Memory leak detected: {} bytes", .{after.bytes_used - before.bytes_used});
return error.MemoryLeak;
}
}

Use Arena Allocators for Temporary Data

fn processTemporaryData(allocator: std.mem.Allocator) !void {
// Arena for temporary allocations
var arena = std.heap.ArenaAllocator.init(allocator);
defer arena.deinit();

const arena_allocator = arena.allocator();

// Allocate temporary data
var temp_vec = try vsa.HybridBigInt.random(arena_allocator, 100000);
var temp_result = try vsa.HybridBigInt.init(arena_allocator, 100000);

// All freed at once when arena.deinit() is called
_ = try vsa.bind(&temp_vec, &temp_vec, &temp_result);
}

Security Considerations

Protecting Against Timing Attacks

// Constant-time comparison
fn constantTimeCompare(a: []const i2, b: []const i2) bool {
if (a.len != b.len) return false;

var result: u8 = 0;
for (a, 0..) |trit_a, i| {
result |= @as(u8, @bitCast(trit_a != b[i]));
}

return result == 0;
}

// Constant-time similarity search
fn constantTimeSimilarity(query: []const i2, candidates: [][]i2) !usize {
var best_idx: usize = 0;
var best_score: i64 = 0;

for (candidates, 0..) |candidate, idx| {
// Always compute full similarity (no early exit)
const score = try computeSimilarity(query, candidate);

// Branchless comparison
const better = @as(i64, @intFromBool(score > best_score));
best_idx = best_idx * (1 - better) + idx * better;
best_score = best_score * (1 - better) + score * better;
}

return best_idx;
}

Sanitizing Logs and Error Messages

fn logSecure(context: []const u8, data: []const i2) void {
// Truncate sensitive data
const max_log_len = 16;
const truncated = if (data.len > max_log_len)
data[0..max_log_len]
else
data;

// Sanitize: convert to hex, not raw values
std.log.debug(
"{s}: [{d} trits] [{X}...]",
.{ context, data.len, truncated }
);
}

// DON'T log sensitive data
// BAD: std.log.info("API key: {s}", .{api_key});

// DO log only metadata
// GOOD: std.log.info("API key: len={}, valid={}", .{api_key.len, isValid});

Rate Limiting

const RateLimiter = struct {
const Window = struct {
count: usize,
reset_time: i64,
};

windows: std.AutoHashMap(u64, Window),
max_requests: usize,
window_ms: i64,

fn init(allocator: std.mem.Allocator, max_requests: usize, window_ms: i64) RateLimiter {
return .{
.windows = std.AutoHashMap(u64, Window).init(allocator),
.max_requests = max_requests,
.window_ms = window_ms,
};
}

fn check(limiter: *RateLimiter, user_id: u64, now_ms: i64) !bool {
const entry = try limiter.windows.getOrPut(user_id);

if (!entry.found_existing) {
entry.value_ptr.* = .{ .count = 1, .reset_time = now_ms + limiter.window_ms };
return true;
}

if (now_ms >= entry.value_ptr.reset_time) {
// Window expired, reset
entry.value_ptr.* = .{ .count = 1, .reset_time = now_ms + limiter.window_ms };
return true;
}

if (entry.value_ptr.count >= limiter.max_requests) {
return error.RateLimitExceeded;
}

entry.value_ptr.count += 1;
return true;
}
};

Secure Random Generation

fn secureRandomTrits(allocator: std.mem.Allocator, len: usize) ![]i2 {
const random_bytes = try allocator.alloc(u8, len);
defer allocator.free(random_bytes);

// Use cryptographically secure random
std.crypto.random.bytes(random_bytes);

const trits = try allocator.alloc(i2, len);
for (random_bytes, 0..) |byte, i| {
// Map byte to trit: [-1, 0, +1]
const mod3 = @mod(byte, 3);
trits[i] = @as(i2, @intCast(mod3)) - 1;
}

return trits;
}

Best Practices

1. Validate All External Input

fn handleExternalRequest(
allocator: std.mem.Allocator,
user_id: u64,
data: []const i2
) !void {
// Validate user ID
if (user_id == 0) return error.InvalidUserId;

// Validate data
try validateTritVector(data);

// Check rate limit
if (!try rate_limiter.check(user_id, now_ms())) {
return error.RateLimitExceeded;
}

// Process
return processRequest(allocator, data);
}

2. Use Error Handling Properly

// DON'T ignore errors
// BAD: const result = bind(a, b) catch undefined;

// DO handle errors explicitly
const result = bind(a, b) catch |err| {
std.log.err("Bind failed: {}", .{err});
return err;
};

// Or use try for propagation
const result = try bind(a, b);

3. Principle of Least Privilege

// Capability-based security
const Capability = struct {
can_bind: bool = false,
can_unbind: bool = false,
can_bundle: bool = false,
can_similar: bool = false,
};

fn executeWithCapability(
op: Operation,
cap: Capability,
args: Args
) !Result {
return switch (op) {
.bind => if (cap.can_bind) doBind(args) else error.Unauthorized,
.unbind => if (cap.can_unbind) doUnbind(args) else error.Unauthorized,
.bundle => if (cap.can_bundle) doBundle(args) else error.Unauthorized,
.similar => if (cap.can_similar) doSimilar(args) else error.Unauthorized,
};
}

4. Secure Defaults

const SecureConfig = struct {
// Secure by default
validate_input: bool = true,
check_bounds: bool = true,
rate_limit: bool = true,
log_sensitive_data: bool = false,

// Explicit opt-out for unsafe features
unsafe_skip_validation: bool = false,
unsafe_disable_bounds_check: bool = false,
};

fn applyConfig(config: SecureConfig) void {
if (config.unsafe_skip_validation) {
std.log.warn("SECURITY: Input validation disabled!", .{});
}

if (config.unsafe_disable_bounds_check) {
std.log.warn("SECURITY: Bounds checking disabled!", .{});
}
}

Security Checklist

Before deploying to production:

  • All external inputs are validated
  • No buffer overflows (bounds checking enabled)
  • Memory leaks tested (arena allocator pattern)
  • Rate limiting implemented
  • Error messages don't leak sensitive info
  • Cryptographic operations use secure random
  • Logs are sanitized
  • Dependencies are audited
  • Secret credentials are not in code
  • Timing attacks considered (constant-time ops)

Common Vulnerabilities

1. Integer Overflow

// BAD
fn badAllocate(size: u32, count: u32) ![]u8 {
const total = size * count; // Can overflow
return allocator.alloc(u8, total);
}

// GOOD
fn goodAllocate(size: u32, count: u32) ![]u8 {
const total = try std.math.mul(u32, size, count); // Checked
return allocator.alloc(u8, total);
}

2. Use-After-Free

// BAD
const vec = try vsa.HybridBigInt.init(allocator, 100);
vec.deinit(allocator);
// ... use vec here (BUG!) ...

// GOOD
{
var vec = try vsa.HybridBigInt.init(allocator, 100);
// ... use vec ...
vec.deinit(allocator);
}
// vec no longer accessible

3. Double-Free

// BAD
var vec = try vsa.HybridBigInt.init(allocator, 100);
vec.deinit(allocator);
vec.deinit(allocator); // Crash!

// GOOD - use defer
var vec = try vsa.HybridBigInt.init(allocator, 100);
defer vec.deinit(allocator);
// ... only one deinit() called

Further Reading


Found a security issue? Please report it privately via GitHub Security Advisory.