Summary
mojo-syntax currently sanctions a struct field holding a raw
Pointer[T, MutUntrackedOrigin] for owned heap data, without noting that the shape is a
silent use-after-free. Because the origin is untracked, the field carries no borrow back to
its owner, so ASAP destruction can run __deinit__ while the pointer is still live.
The line in question (mojo-syntax/SKILL.md, in the memory/pointer section):
When a struct field does hold a raw Pointer, its origin parameter must be specified;
use MutUntrackedOrigin for owned heap data.
Reproducer
Compiles with zero warnings or errors on Mojo 1.0.0 (ed45d567):
from std.memory import Layout, alloc
struct Owned:
var p: Pointer[Scalar[DType.int32], MutUntrackedOrigin]
def __init__(out self, n: Int):
var a = alloc(Layout[Scalar[DType.int32]](count=n))
self.p = a^.unsafe_leak()
def __deinit__(deinit self):
print(" __deinit__ runs here")
self.p.unsafe_free()
def main() raises:
var o = Owned(8)
o.p.unsafe_write(Int32(42))
print("before:", o.p.unsafe_load(0))
var q = o.p # <-- compiler treats this as o's last use
print("after :", o.p.unsafe_load(0))
_ = q
Output:
before: 42
__deinit__ runs here
after : 0
__deinit__ fires at var q = o.p, and the subsequent read through o.p returns 0 from
freed memory. Nothing in the build output hints at it.
Why this is a docs issue rather than a compiler one
The destruction timing looks correct by design: an untracked origin is exactly a promise that
the compiler need not track the borrow. The problem is that the skill presents the shape as
the ordinary way to hold owned heap storage in a struct field, and a reader following it gets
silent memory corruption.
The section's own primary recommendation already avoids this:
A struct that owns heap storage should hold the Allocation, not a leaked pointer.
That works because Allocation.unsafe_ptr() yields an origin tied to the allocation. So the
fix could be as small as one clause on the MutUntrackedOrigin sentence — something like:
When a struct field does hold a raw Pointer, its origin parameter must be specified; use
MutUntrackedOrigin for owned heap data. Note that an untracked origin does not extend the
owner's lifetime — copying the pointer out of the field can be read as the owner's last use
and run __deinit__ early, so prefer holding the Allocation.
Happy to open a PR with that wording if it's useful.
Environment
- Mojo 1.0.0 (
ed45d567), installed via uv (mojo==1.0.0 from PyPI)
- macOS 15 (Darwin 25.6.0), Apple Silicon M4 Max
modular/skills at d55e84c
Summary
mojo-syntaxcurrently sanctions a struct field holding a rawPointer[T, MutUntrackedOrigin]for owned heap data, without noting that the shape is asilent use-after-free. Because the origin is untracked, the field carries no borrow back to
its owner, so ASAP destruction can run
__deinit__while the pointer is still live.The line in question (
mojo-syntax/SKILL.md, in the memory/pointer section):Reproducer
Compiles with zero warnings or errors on Mojo 1.0.0 (
ed45d567):Output:
__deinit__fires atvar q = o.p, and the subsequent read througho.preturns0fromfreed memory. Nothing in the build output hints at it.
Why this is a docs issue rather than a compiler one
The destruction timing looks correct by design: an untracked origin is exactly a promise that
the compiler need not track the borrow. The problem is that the skill presents the shape as
the ordinary way to hold owned heap storage in a struct field, and a reader following it gets
silent memory corruption.
The section's own primary recommendation already avoids this:
That works because
Allocation.unsafe_ptr()yields an origin tied to the allocation. So thefix could be as small as one clause on the
MutUntrackedOriginsentence — something like:Happy to open a PR with that wording if it's useful.
Environment
ed45d567), installed viauv(mojo==1.0.0from PyPI)modular/skillsatd55e84c