Mojo Unsafe Pointer and Manual Memory Management: When Safe Abstractions Aren’t Enough
Mojo’s ownership model handles most memory situations correctly and automatically. Then you hit a hot loop processing a billion-row tensor, or you’re building a custom allocator for an inference pipeline, or you need to lay out a struct so SIMD instructions can actually vectorize it — and suddenly the safe abstractions are in your way. UnsafePointer is Mojo’s answer to that moment. It’s the same power you’d reach for in C++ or Rust’s unsafe blocks: direct memory access, manual lifecycle management, pointer arithmetic with no bounds checking. The compiler steps aside. You own the consequences.
This page covers the current UnsafePointer API — not the legacy version — including the mut and origin parameters that most older tutorials skip, why memory alignment matters more than people expect, and the specific patterns that cause double-frees, memory leaks, and use-after-free in production Mojo code.
TL;DR
UnsafePointeris for contiguous heap buffers, custom data structures, and SIMD-friendly layouts — not a replacement for safe pointer types in normal code- The current API requires explicit
mut=True/Falseandoriginparameters — most tutorials still use the deprecatedLegacyUnsafePointerAPI - Always call
destroy_pointee()on each initialized slot beforefree()—free()releases memory but doesn’t run destructors - SIMD loads require 32-byte alignment on AVX2 — misaligned access silently reduces throughput by 30–50% without any error
__copyinit__must deep-copy the buffer — pointer copy creates aliasing, both structs callfree()on the same address, double-free in productionMutOrigin.externalis the correct origin for heap-allocated buffers —AnyOrigin(old default) extends unrelated lifetimes unpredictably
UnsafePointer Anatomy: What the Current API Actually Looks Like
The UnsafePointer API changed significantly in recent Mojo versions. If your code uses LegacyUnsafePointer or the old initialize_pointee_copy / initialize_pointee_move free functions, that API is deprecated and will be removed. The current API requires explicit mutability and origin parameters — which is more verbose but considerably less footgun-prone.
# Mojo — current UnsafePointer API with explicit mut and origin from memory import UnsafePointer # alloc() returns UnsafePointer[T, MutOrigin.external] # MutOrigin.external = heap-allocated, manually managed, not tracked by lifetime checker var ptr = UnsafePointer[Float32].alloc(8) # 8 contiguous Float32 slots # Initialize each slot explicitly — uninitialized memory is undefined behavior for i in range(8): ptr.init_pointee_copy(i, Float32(i)) # init slot i with value i # Read and write via offset dereference print(ptr[3]) # reads slot 3 — only valid if initialized ptr[3] = Float32(99.0) # writes slot 3 # Lifecycle: destroy before free — in this order, always for i in range(8): (ptr + i).destroy_pointee() # runs destructor on each slot ptr.free() # releases heap memory
The order matters: destroy_pointee() runs the destructor on the value at that slot — important for types with custom __del__. free() releases the memory block itself. Call free() without destroying the pointees first and you leak any resources those values held. Call destroy_pointee() twice on the same slot and you have a double-free. The compiler won’t catch either mistake — that’s why it’s called unsafe.
What Is MutOrigin.external and Why Does It Matter?
MutOrigin.external tells Mojo’s lifetime checker that this pointer’s memory is externally managed — the checker won’t track it, won’t extend lifetimes around it, and won’t insert automatic cleanup. This is what you want for heap-allocated raw buffers. The alternative — AnyOrigin, the old default — caused the lifetime checker to extend unrelated lifetimes around every use of the pointer, sometimes keeping objects alive far longer than intended. Using the explicit external origin is cleaner and more predictable. [LINK: Mojo Memory Mode]
Safe Pointer vs UnsafePointer: When to Use Which
Pointer[T] is a safe non-owning reference to an existing value — no allocation, no lifecycle management, just a reference the compiler tracks. OwnedPointer[T] is a smart pointer with exclusive ownership, similar to Box in Rust. UnsafePointer[T] is the raw handle for when you need a contiguous block of memory, manual initialization, or pointer arithmetic across an array of values. The decision is simple: if you’re building a data structure that owns a heap buffer — a custom list, a tensor buffer, a ring buffer — you need UnsafePointer. Everything else should use the safe types.
Mojo VSCode Extension Not Working: Diagnostic Guide for a Broken Development Environment You installed the Mojo extension, restarted VSCode, opened a .mojo file — and got absolutely nothing. No highlighting, no autocomplete, no run command,...
Pointer Arithmetic: No Bounds Checking, No Safety Net
Pointer arithmetic in Mojo works through the + and - operators and the offset() method. They all do the same thing: return a new pointer shifted by i elements. Not bytes — elements. ptr + 3 on an UnsafePointer[Float32] advances by 3 × 4 = 12 bytes. The size is handled by the type parameter. What isn’t handled: whether offset 3 is within your allocated range.
# Mojo — pointer arithmetic on a raw buffer from memory import UnsafePointer var buf = UnsafePointer[Int32].alloc(4) (buf + 0).init_pointee_copy(Int32(10)) (buf + 1).init_pointee_copy(Int32(20)) (buf + 2).init_pointee_copy(Int32(30)) (buf + 3).init_pointee_copy(Int32(40)) # Read in sequence — fine as long as indices stay within [0, 3] print(buf[0], buf[1], buf[2], buf[3]) # 10 20 30 40 # This does NOT crash at runtime — it reads whatever is at that address # buf[4] would be undefined behavior — no bounds error, just silent corruption # Takeaway: you own the bounds check — the compiler will not do it for i in range(4): (buf + i).destroy_pointee() buf.free()
The practical consequence is that off-by-one errors in pointer arithmetic produce silent corruption, not crashes. A crash would be discoverable — corruption propagates silently until it manifests as wrong output or an unrelated panic somewhere downstream. In tight loops over raw buffers, instrument your index bounds in debug builds explicitly.
Mojo Pointer Arithmetic vs Rust Raw Pointers
Rust requires an unsafe block to dereference a raw pointer — the unsafety is syntactically marked at the dereference site. Mojo’s UnsafePointer doesn’t require an explicit unsafe block — the type itself signals unsafe intent. This means a function that accepts an UnsafePointer argument is implicitly doing unsafe work without any syntactic callout. Be explicit in your own code: document unsafe sections in comments, keep raw pointer manipulation in dedicated functions, and don’t let UnsafePointer values escape into calling code that isn’t prepared to manage their lifecycle.
Memory Alignment and SIMD: Why Layout Decisions Hit Performance
SIMD instructions — SIMD[DType.float32, 8] loading eight floats in a single instruction — require data to be aligned to the SIMD vector width. On modern hardware, a 256-bit AVX2 operation requires 32-byte alignment. If your buffer is 8-byte aligned (the default for Float32), loading across a 32-byte boundary triggers a cache line split — the CPU has to perform two cache line loads instead of one, sometimes reducing throughput by 30–50% on tight loops.
# Mojo — SIMD load with and without alignment specification from memory import UnsafePointer from sys.info import simdwidthof alias FLOAT32_SIMD_WIDTH = simdwidthof[DType.float32]() # 8 on AVX2, 4 on SSE4 # Allocate with explicit alignment for SIMD operations # alignment parameter ensures the returned pointer is aligned to 32 bytes var aligned_buf = UnsafePointer[Float32].alloc( FLOAT32_SIMD_WIDTH * 16, # 16 SIMD vectors ) # Load a full SIMD vector from the aligned buffer # alignment hint tells the backend this load is guaranteed aligned var vec = aligned_buf.load[width=FLOAT32_SIMD_WIDTH](0) # Store a SIMD result back — same alignment applies var result = vec * vec # vectorized multiply aligned_buf.store(0, result) # Takeaway: alignment argument is a promise to the compiler, # not a runtime guard — misaligned access with this flag is UB aligned_buf.free()
The alignment parameter on load() and store() is a hint to the compiler that the address is guaranteed aligned — it unlocks aligned load instructions which are strictly faster than the unaligned equivalents. The hint is a promise. If you pass alignment=32 on a buffer that’s only 8-byte aligned, you’re invoking undefined behavior that produces different failures on different hardware.
How to Structure Data for SIMD Efficiency
The structural rule: lay out data as arrays of scalars, not arrays of structs with mixed-width fields. If you have a position type with x, y, z, and a 1-byte active flag, an array-of-structs layout forces the SIMD unit to skip the flag bytes on every load, destroying vectorization. Structure-of-arrays — separate contiguous buffers for x values, y values, z values, active flags — lets SIMD load 8 x-coordinates in one instruction. It’s more verbose to maintain, and the payoff is real: tight loops over structure-of-arrays can be 3–5x faster than the equivalent array-of-structs pattern on large datasets. [LINK: Mojo Performance Pitfalls]
Why Mojo Was Created to Solve Python Limits Mojo exists because Python performance limitations have become a structural bottleneck in modern AI and machine learning workflows. Within this Mojo Deep Dive: Python Limits are examined...
Lifecycle Management: __init__, __copyinit__, __moveinit__, __del__ on Raw Buffers
When you build a struct that wraps an UnsafePointer — a custom array, a tensor buffer, a memory pool — you own every step of its lifecycle. Mojo doesn’t help. Get any of these wrong and you have either a memory leak (forgot to free), a double-free (freed twice), or use-after-free (accessed after free). All three are undefined behavior. All three are silent in release builds.
# Mojo — struct wrapping UnsafePointer with correct lifecycle management from memory import UnsafePointer struct RawBuffer[T: AnyTrivialRegType]: var _data: UnsafePointer[T] var _len: Int fn __init__(out self, length: Int): # Allocate raw heap memory — MutOrigin.external, not tracked self._data = UnsafePointer[T].alloc(length) self._len = length # Takeaway: memory is uninitialized here — caller must init before use fn __copyinit__(out self, other: Self): # Deep copy — allocate new buffer, copy each element self._data = UnsafePointer[T].alloc(other._len) self._len = other._len for i in range(other._len): (self._data + i).init_pointee_copy((other._data + i)[]) # Takeaway: without this, two instances share one buffer — double-free on destruction fn __moveinit__(out self, deinit other: Self): # Transfer ownership — steal the pointer, leave source empty self._data = other._data self._len = other._len # other is now invalid — Mojo's deinit annotation prevents its destructor from running # Takeaway: move avoids allocation cost, but source must never be used after move fn __del__(deinit self): # Free in reverse: destroy values, then release memory self._data.free() # For non-trivial T, destroy each element first: # for i in range(self._len): (self._data + i).destroy_pointee()
The deinit annotation on __moveinit__‘s other parameter is what prevents Mojo from running other.__del__ after the move — without it, you’d free the same pointer twice. Miss this annotation, ship a double-free. It’s the kind of mistake that works fine in tests on your machine and crashes in production under specific allocation patterns.
Why Forgetting destroy_pointee Leaks Resources Even When You Call free()
free() releases the heap memory block. It doesn’t care what’s in that memory. If you stored a type with a __del__ that closes a file handle, releases a GPU buffer, or decrements a reference count, calling free() without first calling destroy_pointee() on each slot leaves those resources dangling. The memory block is returned to the allocator. The resources it referenced are not. In a tight loop allocating and freeing buffers that hold non-trivial types, this compounds into a resource exhaustion bug that presents as an unexplained file descriptor or GPU memory leak hours into a run.
The Watch Out Section: Anti-Patterns That Break Production
These are the patterns that produce failures which aren’t reproducible in unit tests and only manifest under specific allocation sequences or concurrency patterns.
Reading uninitialized memory. UnsafePointer.alloc() returns uninitialized memory. On most runs, that memory happens to contain zeros from a previous allocation that was zeroed on free. Your tests pass. In production under different allocation patterns, it contains garbage. Always initialize every slot before reading.
Aliasing pointers across ownership boundaries. If two structs hold UnsafePointer to the same memory, both will call free() in their destructors. This is a double-free. Mojo has no runtime check for this. The __copyinit__ pattern above — deep copy, not pointer copy — is the correct defense. Every struct that owns heap memory must deep-copy in __copyinit__, full stop.
Using a moved-from pointer. After __moveinit__ transfers a pointer, the source struct’s _data field still holds the original address — it just isn’t valid to use anymore. In Mojo, the deinit convention prevents the destructor from running on the source, but it doesn’t null out the pointer. Any code that accesses the source after a move is reading from memory that may now belong to a different allocation.
Keeping a raw pointer past the enclosing struct’s lifetime. If you expose _data from a struct via an accessor, and someone holds that raw pointer beyond the struct’s lifetime, they have a dangling pointer. UnsafePointer has an origin parameter precisely to track this — use it. Returning an UnsafePointer with origin_of(self) tells Mojo the pointer’s lifetime is bounded by the struct’s lifetime.
# Mojo — detecting unsafe code issues during development
# Use these patterns in debug builds; strip in release
fn debug_check_ptr[T: AnyType](ptr: UnsafePointer[T], label: String):
# Check for null before any dereference — catches uninitialized pointer fields
if not ptr:
print("NULL POINTER at:", label)
# In production: log + abort; in debug: trap immediately
fn assert_aligned[T: AnyType](ptr: UnsafePointer[T], alignment: Int):
# Verify alignment for SIMD operations — catches alignment bugs before they
# produce silent wrong results on a different microarchitecture
var addr = Int(ptr)
if addr % alignment != 0:
print("MISALIGNED:", addr, "expected alignment:", alignment)
These debug helpers don’t belong in a release binary — they exist to surface problems during development before they become production incidents. Unsafe memory bugs are silent by design. Build your own noise.
FAQ: Mojo UnsafePointer and Manual Memory Management
When should I use UnsafePointer in Mojo?
When safe pointer types — Pointer, OwnedPointer, ArcPointer — cannot do the job. Specific cases: building a custom collection that owns a heap buffer, implementing a struct that needs contiguous SIMD-friendly memory layout, interfacing with C or C++ code that expects raw pointers, or achieving maximum performance in a hot loop where the safe abstraction overhead is measurable. Outside these cases, use the safe types — the ergonomics are better and the compiler catches lifecycle errors for you.
Mastering Variadic Parameters for Traits in Mojo: Practical Tips and Patterns Diving into Mojo can feel like unlocking a faster, lower-level world where every design choice matters. One of the most powerful yet subtle tools...
What is the difference between destroy_pointee() and free() in Mojo?
destroy_pointee() runs the destructor on the value stored at a pointer location — it ends the value’s lifecycle cleanly, releasing any resources the value held. free() releases the heap memory block the pointer addresses. Both are required for non-trivial types: destroy_pointee() for each initialized slot first, then free() for the block. Calling only free() leaves the value’s resources dangling. Calling only destroy_pointee() releases the value’s resources but leaks the memory block.
How does Mojo UnsafePointer differ from Rust raw pointers?
Rust requires an explicit unsafe { } block to dereference a raw pointer — the unsafety is syntactically visible at every dereference site. Mojo’s UnsafePointer type signals unsafe intent at the type level, but individual operations don’t require explicit unsafe blocks. Both provide pointer arithmetic without bounds checking. Mojo’s current API adds explicit mut and origin parameters that Rust expresses through its borrow checker — the goal is similar but the mechanism differs.
What is MutOrigin.external and when do I need it?
MutOrigin.external marks a pointer as pointing to heap memory that is manually managed — not tracked by Mojo’s lifetime checker. Use it whenever you call alloc() inside a struct to back a heap buffer. The alternative, AnyOrigin (the old default), causes the lifetime checker to extend all lifetimes around uses of the pointer, which can keep unrelated values alive longer than intended and is harder to reason about. Explicit external origin is cleaner and communicates intent precisely.
Why does my SIMD code run slower than expected with UnsafePointer?
Almost always a memory alignment issue. SIMD loads on 256-bit vector widths (8 × Float32) require 32-byte alignment. If your buffer is only 8-byte aligned — the default for Float32 — loads that cross a 32-byte boundary trigger two cache line reads instead of one. Use the alignment parameter on alloc() to request aligned memory, and pass the same alignment hint to load() and store(). Benchmark with and without alignment to confirm — the difference on tight vectorized loops is typically 30–50%.
How do I migrate from LegacyUnsafePointer to the new UnsafePointer API?
First, rename all existing UnsafePointer uses to LegacyUnsafePointer — this preserves prior behavior and isolates old code. Then replace LegacyUnsafePointer with the new UnsafePointer incrementally. The main change: function arguments that accept pointers must now declare mutability with mut=True or mut=False. Heap-allocated buffers should use MutOrigin.external as the origin parameter. The old free functions initialize_pointee_copy and initialize_pointee_move are replaced by methods on the pointer: init_pointee_copy() and init_pointee_move_from().
Can I expose an UnsafePointer from inside a struct safely?
Yes, with the correct origin. Use origin_of(self._data) or origin_of(self) as the returned pointer’s origin to tell Mojo the returned pointer’s lifetime is bounded by the struct’s lifetime. This prevents callers from holding the pointer past the struct’s destruction. Without the correct origin, the returned pointer has AnyOrigin, which extends lifetimes unpredictably and can allow use-after-free if the caller holds the pointer past the struct’s scope.
What is the most common cause of memory leaks in Mojo UnsafePointer code?
Not calling destroy_pointee() before free() on buffers that hold types with custom destructors. free() releases the memory block but doesn’t run destructors on the values inside. For types that own resources — file handles, GPU buffers, reference-counted values — the resources leak silently. The fix is consistent: always iterate over initialized slots and call destroy_pointee() on each before calling free(), regardless of whether the type appears to need it. Defensive discipline here prevents the class of leak that only shows up after hours of sustained load.
Written by: