Awesome-omni-skill safety-checker
Identifies unsafe operations in Zig code including pointer casts, bounds checking, null pointer dereferences, and undefined behavior. Use when writing low-level code, reviewing safety-critical sections, or debugging crashes.
git clone https://github.com/diegosouzapw/awesome-omni-skill
T=$(mktemp -d) && git clone --depth=1 https://github.com/diegosouzapw/awesome-omni-skill "$T" && mkdir -p ~/.claude/skills && cp -r "$T/skills/tools/safety-checker" ~/.claude/skills/diegosouzapw-awesome-omni-skill-safety-checker && rm -rf "$T"
skills/tools/safety-checker/SKILL.mdSafety Checker
This skill helps identify unsafe operations and potential undefined behavior in Zig code, ensuring memory safety and correctness.
Zig Safety Principles
Zig provides safety by default in Debug and ReleaseSafe builds:
- Bounds checking on arrays and slices
- Integer overflow detection
- Null pointer checks for optionals
- Alignment verification
However, unsafe operations can bypass these checks and cause undefined behavior.
Unsafe Operations to Watch For
1. Pointer Casts Without Validation
Type Punning with @ptrCast
// DANGEROUS: No validation of pointer validity pub fn setHandler(self: *Server, handler: *anyopaque) void { self.handler = handler; } pub fn getHandler(self: *Server) *Handler { const handler_ptr = self.handler.?; return @ptrCast(@alignCast(handler_ptr)); // Unsafe cast! }
Problems:
- No type safety:
can point to anything*anyopaque - No alignment verification before cast
- No null check (using
panics).? - Could point to wrong type
Better approach:
// SAFE: Use proper optional type pub const Server = struct { handler: ?*Handler, // Type-safe optional pointer pub fn setHandler(self: *Server, handler: *Handler) void { self.handler = handler; // Type checked } pub fn getHandler(self: *Server) ?*Handler { return self.handler; // No cast needed } };
If type erasure is truly needed:
// SAFER: Validate before casting pub fn getHandler(self: *Server) ServerError!*Handler { const handler_ptr = self.handler orelse return error.NoHandler; // Validate alignment const addr = @intFromPtr(handler_ptr); if (addr % @alignOf(Handler) != 0) { return error.InvalidAlignment; } // Cast with explicit alignment const aligned_ptr: *align(@alignOf(Handler)) anyopaque = @alignCast(handler_ptr); return @ptrCast(aligned_ptr); }
2. Missing Bounds Checks
Array/Slice Access
// DANGEROUS: No bounds check pub fn readIntBig(buffer: []const u8, comptime T: type) T { const size = @sizeOf(T); if (buffer.len < size) @panic("Buffer too small"); // Panic! const bytes = buffer[0..size]; return std.mem.readInt(T, bytes[0..size], .big); }
Problems:
- Panic on invalid input (should return error)
- Potential crash if panic is reached in release mode
- No way for caller to handle error
Better approach:
// SAFE: Return error, not panic pub fn readIntBig(buffer: []const u8, comptime T: type) ParseError!T { const size = @sizeOf(T); if (buffer.len < size) return error.BufferTooSmall; const bytes = buffer[0..size]; return std.mem.readInt(T, bytes[0..size], .big); }
Unchecked Indexing
// DANGEROUS: Assumes index is valid pub fn getRegister(self: *Server, index: u8) Value { return self.registers[index]; // Could be out of bounds! } // SAFE: Check bounds explicitly pub fn getRegister(self: *Server, index: u8) ServerError!Value { if (index >= self.registers.len) return error.RegisterOutOfBounds; return self.registers[index]; } // ALSO SAFE: Use optional for invalid indices pub fn getRegister(self: *Server, index: u8) ?Value { if (index >= self.registers.len) return null; return self.registers[index]; }
3. Unsafe Pointer Arithmetic
// DANGEROUS: Manual pointer arithmetic pub fn advance(ptr: [*]const u8, offset: usize) [*]const u8 { return ptr + offset; // No bounds check! } // SAFE: Use slices with bounds pub fn advance(slice: []const u8, offset: usize) ?[]const u8 { if (offset >= slice.len) return null; return slice[offset..]; }
4. Uninitialized Memory
// DANGEROUS: Reading uninitialized memory pub fn createValue() Value { var value: Value = undefined; // Uninitialized! if (condition) { value = Value.makeInt(42); } return value; // May return uninitialized data! } // SAFE: Initialize before use pub fn createValue() Value { if (condition) { return Value.makeInt(42); } return Value.nil(); // Always initialized }
5. Null Pointer Dereference
// DANGEROUS: Force-unwrapping optionals pub fn process(self: *Server) void { const handler = self.handler.?; // Panics if null! handler.handle(self); } // SAFE: Check for null pub fn process(self: *Server) ServerError!void { const handler = self.handler orelse return error.NoHandler; try handler.handle(self); } // ALSO SAFE: Use if for optional handling pub fn process(self: *Server) void { if (self.handler) |handler| { handler.handle(self) catch |err| { std.log.err("Handle failed: {}", .{err}); }; } }
6. Integer Overflow
// DANGEROUS: Unchecked arithmetic in ReleaseFast pub fn allocate(size: usize, count: usize) ![]u8 { const total = size * count; // Can overflow! return try allocator.alloc(u8, total); } // SAFE: Use checked arithmetic pub fn allocate(size: usize, count: usize) ![]u8 { const total = std.math.mul(usize, size, count) catch { return error.ArithmeticOverflow; }; return try allocator.alloc(u8, total); } // ALSO SAFE: Wrapping arithmetic when overflow is intended pub fn hash(value: u32) u32 { return value *% 31; // *% = wrapping multiplication }
7. Alignment Issues
// DANGEROUS: Assuming alignment pub fn readU32(bytes: []const u8) u32 { const ptr: *const u32 = @ptrCast(bytes.ptr); // May be misaligned! return ptr.*; } // SAFE: Use std.mem functions pub fn readU32(bytes: []const u8) !u32 { if (bytes.len < 4) return error.BufferTooSmall; return std.mem.readInt(u32, bytes[0..4], .little); } // SAFE: Check alignment explicitly pub fn readU32(bytes: []align(4) const u8) u32 { const ptr: *const u32 = @ptrCast(bytes.ptr); // Compiler ensures alignment return ptr.*; }
8. Unsafe Memory Operations
// DANGEROUS: @memcpy with wrong size pub fn copy(dest: []u8, src: []const u8) void { @memcpy(dest, src); // Panics if dest.len < src.len } // SAFE: Check sizes first pub fn copy(dest: []u8, src: []const u8) !void { if (dest.len < src.len) return error.DestinationTooSmall; @memcpy(dest[0..src.len], src); }
Safety Checklist
When Writing Low-Level Code:
- No
without validation@ptrCast - Check alignment before pointer casts
- Validate buffer sizes before access
- Use errors instead of panics for invalid input
- Initialize all variables before use
- Check for null before unwrapping optionals
- Use checked arithmetic for user-controlled values
- Verify alignment assumptions
- Validate sizes for
,@memcpy@memset
When Reviewing Code:
- Look for
- is it necessary? Is it safe?@ptrCast - Look for
- is alignment guaranteed?@alignCast - Check all array/slice indexing - bounds checked?
- Look for
unwrapping - can it be null?.? - Check for
- is it initialized on all paths?undefined - Look for panics - should they be errors instead?
- Check arithmetic - can it overflow?
- Verify
sizes match@memcpy
Red Flags
Immediate Review Needed:
without validation - Almost always wrong@ptrCast
for user input - Should be errors@panic
on optional without null check - May crash.?- Array access without bounds check - May crash
without initialization on all paths - UBundefined- Unchecked arithmetic on user input - May overflow
Consider Refactoring:
- Type erasure with
- Prefer proper types*anyopaque - Manual pointer arithmetic - Use slices instead
- Assuming alignment - Make it explicit
- Complex pointer operations - Simplify if possible
Testing Safety
Test edge cases and error conditions:
test "register access out of bounds" { var server = try Server.init(std.testing.allocator); defer server.deinit(); // Should return error, not crash try std.testing.expectError( ServerError.RegisterOutOfBounds, server.getRegister(255) ); } test "stack overflow protection" { var server = try Server.init(std.testing.allocator); defer server.deinit(); // Fill stack while (server.sp < server.stack.len) { try server.push(Value.makeInt(0)); } // Next push should error, not crash try std.testing.expectError( ServerError.StackOverflow, server.push(Value.makeInt(1)) ); } test "buffer too small handling" { const data = [_]u8{ 1, 2 }; // Only 2 bytes // Should error, not crash try std.testing.expectError( ParseError.BufferTooSmall, readIntBig(&data, u32) // Needs 4 bytes ); }
Build Modes and Safety
- Debug: All safety checks enabled (bounds, overflow, etc.)
- ReleaseSafe: Safety checks enabled, optimizations on
- ReleaseFast: Safety checks disabled for performance
- ReleaseSmall: Like ReleaseFast but optimizes for size
Golden Rule: Code should be correct in ReleaseFast even though safety checks are disabled. Never rely on runtime checks for correctness in release builds.
// BAD: Relies on Debug mode checks pub fn access(slice: []u8, index: usize) u8 { return slice[index]; // Crashes in ReleaseFast if out of bounds! } // GOOD: Explicit check in all modes pub fn access(slice: []u8, index: usize) ?u8 { if (index >= slice.len) return null; return slice[index]; }
Summary
Safety in Zig is about:
- Validation - Check inputs and bounds explicitly
- Error handling - Return errors, don't panic
- Type safety - Avoid type erasure when possible
- Explicit intent - Make assumptions clear in types
- Testing - Test error paths and edge cases
When in doubt, prefer safety over performance. Optimize only when profiling proves it necessary.