The 16-Byte Contract: What MTE Checks—and What It Cannot
ARM Memory Tagging Extension (MTE) begins with a small, concrete operation: compare a tag carried by a pointer with a tag attached to the memory it addresses. Understanding that comparison, and the places where it can fail to say anything, gives us the foundation for interpreting both MTE’s security promise and its performance on real processors. The important point is that MTE does not make memory safe by inspecting an object’s full meaning. It checks whether a pointer’s color still matches the color assigned to a small region of memory.
A pointer carries a claim about memory
MTE divides memory into 16-byte units called granules. Each granule has a 4-bit tag, and a pointer can carry a corresponding tag in its upper address bits. The tag is metadata: it is not the address itself and it does not describe the object’s length, fields, or type. It is a compact claim that says, in effect, “this pointer is currently authorized to access memory carrying this color.”
When an allocator creates an object, it assigns a tag to the allocation and returns a pointer containing the same tag. Imagine an allocation spanning several 16-byte granules. The pointer returned to the program carries the allocation’s tag, while the memory system keeps that tag alongside the granules. A later load or store through that pointer presents both the ordinary address and the tag. MTE’s hardware check can then compare the pointer’s claim with the memory’s current metadata before allowing the access to complete.
The 16-byte size is an important compromise. It is smaller than the 64-byte granules used by an earlier tagged-memory design, so allocators face less alignment waste. But it is still much coarser than the logical objects that programs manipulate. A single granule may contain the end of one small object, padding, or part of an adjacent object. MTE’s check is therefore about the tag assigned to the granule, not about every byte-level boundary that a programmer might intend.
The tags are normally selected so that different allocations are unlikely to share the same color. The usual design uses four bits, giving 16 possible values. The allocator may choose a value at allocation time and change the memory’s tag when an object is freed or reused. That change is what makes an old pointer carry a stale claim: its address may still point into the same physical region, but its tag no longer matches the newly assigned object.
The access is a short sequence, not a single comparison
To understand the performance question, it helps to separate the visible instruction from the work the processor must do around it. A normal load asks for a value at an address. An MTE-checked load must also obtain the tag associated with the relevant granule, compare it with the pointer tag, and then enforce the result. A store has the same logical obligation, even though its data may otherwise be placed into a store buffer without waiting for the cache to return the old line.
This sequence does not mean that every implementation literally performs five isolated steps in that order. Caches, queues, speculation, and forwarding can overlap much of the work. The sequence shows the obligations the hardware must satisfy. MTE adds a metadata path and a decision point to a memory operation; the processor must decide where the tag lives, how it reaches the checking logic, how many checks can be pending, and when a mismatch becomes visible.
That is why a tag check can cost something even when a program never triggers an error. The common path still needs to establish that the pointer and memory tags match. A cache miss may require tag data as well as the requested data. Stores may need different ordering rules because a synchronous check cannot allow an invalid write to become architecturally visible. In a relaxed mode, the processor may defer the fault, but it still has to issue and track the check. The later performance results arise from how different cores meet these obligations, not from the mere existence of a four-bit field.
The same distinction helps separate detection from reporting. The comparison determines whether the access is valid under the current tags. What the processor does after a mismatch—stop immediately, continue temporarily, record an event, or deliver information to the operating system—is a separate policy and implementation choice. MTE’s basic contract supplies the comparison; the mode determines much of the timing and diagnostic consequence.
Why random tags catch most, not all, mismatches
Suppose a dangling pointer retains tag 3 after its allocation is freed, and the memory is reused with a newly selected tag. If the new tag is any of the other 15 values, the pointer and memory disagree and the access is detected. If the allocator happens to select tag 3 again, the stale pointer passes this particular check. Under the ordinary random model, that produces a 15/16 chance of detecting the mismatch and a 1/16 chance of missing it.
The same reasoning applies to a spatial error such as an access beyond the end of a buffer. If the access crosses into a granule carrying a different tag, MTE can catch it. If the relevant granules happen to share a tag, it may not. The probability describes the tag relationship, not the severity or likelihood of the underlying bug. It is also not a statement that 15 out of every 16 bugs in a program will be caught; allocator policy, object layout, repeated attempts, and the attacker’s knowledge all affect the practical result.
Allocators can shape those probabilities. They may reserve tags for their own metadata, assign distinct tags to adjacent allocations, or use guard regions after large allocations. Android’s documentation also describes allocator strategies that trade some use-after-free probability for more deterministic detection of linear overflows, while independently randomized tags provide a more uniform chance across spatial and temporal errors. These choices improve a particular threat or bug pattern; none changes the fact that a four-bit random tag is a probabilistic defense.
That probabilistic structure is not an accidental defect in an otherwise exact system. Tracking exact bounds for every object would require more metadata and more checks, and it would raise compatibility and implementation costs. MTE chooses a smaller piece of information that can be attached to existing pointers and checked by hardware. The result is broad coverage of common temporal and spatial failures without the claim of sound memory safety.
Temporal and spatial errors look similar to the checker
A temporal error uses a pointer after the object it once referred to has been freed or repurposed. The address may look plausible: memory allocators often reuse the same region for a later allocation. MTE adds a second condition beyond address validity. The pointer must carry the tag currently assigned to that region. Retagging on free or reuse turns the old pointer into a stale capability-like claim, and a mismatch can expose the use-after-free.
A spatial error crosses an intended boundary while the address remains within mapped memory. For example, code may read just beyond an array into the next allocation. If the adjacent granule has a different tag, the pointer’s tag does not authorize that access and MTE can fault. This is the value of attaching tags to memory rather than merely checking whether an address is mapped: a mapped address can still be the wrong object.
But the granule is the boundary MTE knows. An overflow that remains inside one 16-byte granule may not encounter a different tag. The study notes that an overflow within the same tag granule cannot corrupt memory belonging to another allocation when that granule is never shared between allocations, but the logical program boundary may still be finer than 16 bytes. MTE is not an intra-object bounds checker. It cannot infer that one field inside a tagged object should not access another field, nor can it enforce a language type’s intended ownership.
This is also why a passing tag check is weaker than a statement that the operation is safe. It means the pointer and the selected granule agree under the current metadata. It does not prove that the program is using the right offset, that the object’s internal invariants hold, or that no other vulnerability can modify the pointer before the check. MTE is valuable precisely because many memory-safety exploits depend on stale or out-of-bounds pointers, but it narrows their reliability rather than eliminating every route to memory corruption.
The security boundary includes tag visibility
The random-tag argument assumes that an attacker cannot simply learn the correct tag and construct a matching pointer. The study therefore treats tag secrecy as part of the threat model. If an attacker can discover a region’s tag through a read primitive or side channel and also has a bug that lets them alter pointer bits, they may be able to forge a pointer that passes the check. The study does not analyze side-channel attacks, so its performance results should not be read as a complete security analysis of those paths.
This limitation does not make MTE meaningless. A defense can be useful without being a mathematically complete memory-safety proof. A failed exploit that reliably turns into a detectable crash is a different operational outcome from a silent compromise. But the distinction matters when choosing what MTE should protect and how it should be combined with stronger isolation, memory-safe components, control-flow defenses, or careful allocator policy.
There is another operational precondition: memory and pointers must be prepared for tagging. Android’s documentation notes that custom allocators need MTE-capable pages and 16-byte alignment, and must tag memory and pointers appropriately. If only part of a process’s memory is actually taggable or tagged, the resulting protection and performance describe that partial path. A measurement cannot silently assume complete coverage when the implementation only checks selected regions.
The hidden performance contract
MTE’s security mechanism is easy to describe because the visible rule is simple: matching tags allow the access; mismatching tags cause a fault under the selected handling mode. The performance contract is harder because it is distributed across the memory hierarchy and pipeline. Tag metadata must be stored and fetched. Checks must be scheduled alongside loads and stores. Mismatches must be delivered with enough context for the operating system to act. Allocation and deallocation may require tag updates even before the application’s hot loop runs.
Those requirements create several possible costs. Extra tag data can consume cache capacity or memory bandwidth. A core may restrict how many tag checks remain in flight. A synchronous policy may impose ordering between stores that would otherwise be speculative. A custom or general-purpose allocator may spend time assigning tags to large regions. The operating system may perform checks or tag handling outside the application’s heap. None of these is implied by the probability of catching a bug, and none can be inferred from the pointer format alone.
The distinction is central to MTE’s performance in practice. The 16-byte contract tells us what must be checked; it does not tell us how efficiently a particular core will check it. The mode determines when a mismatch is surfaced, while the implementation determines how the common path carries the metadata. Once that separation is clear, the next question follows naturally: how do SYNC, ASYNC, and ASYMM change the timing, precision, and cost of the same underlying check?