Skip to content

mojo-syntax: MutUntrackedOrigin pointer field is a silent use-after-free #11

Description

@ralphbutler

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

Activity

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Metadata

Metadata

Assignees

No one assigned

    Labels

    No labels
    No labels

    Type

    No type

    Projects

    No projects

      Milestone

      No milestone

      Relationships

      None yet

      Development

      No branches or pull requests

      Issue actions