From Debugging to Production: How MTE Modes Change the Deal
ARM MTE has one tag-checking mechanism but several ways to handle the moment when the pointer and memory disagree. The focus here is the lifecycle that those choices create: what SYNC, ASYNC, and ASYMM do after a mismatch, what information survives, and how Android turns the choices into a development and production policy. The practical result is a way to choose a mode by diagnostic need and security boundary rather than treating “async” as a synonym for “free.”
SYNC stops where the mistake happens
In synchronous mode, the processor treats a tag mismatch as an immediate, precise exception. The offending load or store does not simply continue and leave the operating system to infer what happened later. Android reports the failure as a SIGSEGV with the synchronous MTE error code, together with the memory access and faulting address. That precision makes the failure actionable: the developer can identify the instruction that attempted the invalid access and connect it to the code path that needs repair.
SYNC also enriches the information around the allocation itself. Android’s allocator can record stack traces for allocations and deallocations while SYNC is active, allowing a report to distinguish a use-after-free from a buffer overflow and to show the relevant memory events. This does not make the original program correct, but it shortens the path from crash to explanation. A native developer can see not only that a pointer was rejected, but why the memory had the tag it did and where the object’s lifetime changed.
The cost of that clarity is a stricter dependency in the access path. The processor must establish the tag result before allowing the checked access to complete, because a synchronous failure must not be reported after the invalid operation has already become part of architectural state. That ordering can be easy for some core designs and expensive for others. The mode is therefore optimized for correctness of detection and diagnosis, not for a promise of one fixed runtime penalty.
SYNC is especially useful during bring-up, testing, and focused investigation. It can also be appropriate for a critical process whose attack surface justifies accepting more runtime cost. Android describes it as a debugging alternative to HWASan that does not require recompiling the application for the same kind of heap checking, although stack tagging has separate instrumentation requirements. The point is not that SYNC belongs only in a lab. It is that its value is tied to the information it preserves when something goes wrong.
ASYNC relaxes timing, not the need to check
Asynchronous mode changes what happens after the comparison. On a mismatch, the processor continues execution until a nearby kernel entry, such as a system call or timer interrupt, where the process is terminated with an asynchronous MTE error. The report does not carry the same fault address and memory-access detail as SYNC. The processor has preserved less immediate context in exchange for relaxing the dependency between the tag check and the load or store.
That trade-off is useful when the goal is low-overhead detection in a well-tested production codebase. A production process may not need every fault to arrive with the full allocation and deallocation history that makes development diagnosis efficient. It may still benefit from turning a class of memory-safety failures into a detected crash rather than a silent compromise. Android’s guidance describes ASYNC in precisely that defense-in-depth role, especially when testing has already been performed in SYNC.
But ASYNC is not a mode in which tag checks disappear. The processor still has to fetch or otherwise access the relevant metadata, compare it with the pointer, and track the result while the program continues. Delaying the visible fault changes the ordering and reporting requirements; it does not remove the common-path work. A core can therefore show a substantial ASYNC slowdown if its tag-check resources become a bottleneck even though the operating system receives the fault later.
The missing information also changes the operational response. An ASYNC report may establish that a process encountered a tag mismatch without directly naming the instruction or address that caused it. Engineers may need to reproduce the fault under SYNC, sample a population of affected users, or use surrounding logs to narrow the path. Android’s documentation explicitly describes switching execution to SYNC through runtime APIs after ASYNC has found a bug so that an accurate report can be obtained. ASYNC is consequently a production observation and mitigation posture, not a replacement for precise diagnosis.
There is a security reason not to overstate the relaxation. A deferred fault gives the processor freedom to continue for a while, and the study notes that the guarantee is tied to the next kernel entry, with caveats for side channels and shared memory. The mode is designed to make detection practical at lower cost, not to turn an invalid pointer into a valid one. Its effectiveness depends on how the process, operating system, and surrounding attack surface handle the deferred event.
ASYMM makes reads and writes different
Asymmetric mode occupies the space between the two better-known choices. It performs synchronous checking for memory reads and asynchronous checking for memory writes. Android’s platform documentation describes it as an Arm v8.7-A feature that can have performance similar to ASYNC and recommends using it instead of ASYNC when it is available. The app-facing Android guide emphasizes SYNC and ASYNC, while the AOSP platform documentation exposes the additional platform-level behavior; these descriptions differ in scope, not necessarily in the underlying hardware capability.
The asymmetry reflects a security and performance judgment. A read can reveal data or trigger computation, while a write can directly corrupt an object. The study therefore notes that making stores asynchronous is less ideal for security because memory corruption is generally caused by stores. ASYMM preserves a precise response for read mismatches while relaxing the write path, but it still leaves the exact write-fault context less immediate than SYNC.
Whether that compromise is worthwhile depends on the core. A small in-order core may have similar performance across modes because its ordinary execution already limits how much work can overlap. Another core may have a store pipeline or tag-check structure whose behavior changes sharply when synchronous ordering is required. The mode name describes the architectural policy; it does not predict the size of the resulting slowdown without the implementation and workload.
The Android setting belongs to a process
Android’s deployment model makes MTE a lifecycle and ownership decision rather than an isolated compiler flag. MTE is disabled by default unless a native process or app opts in through the available build, runtime, manifest, or compatibility mechanisms. For an app, the memtagMode setting can apply to all processes used by the application, with an individual process able to override the application setting. The platform documentation likewise describes enablement as process-wide for native heap allocations.
That scope matters because the cost crosses library boundaries. Enabling MTE for a process does not mean that one carefully chosen function pays the whole price while the rest of the process remains unchanged. Native allocations made by code in the process participate in the policy, subject to the platform’s tagging and mapping rules. A library’s local benchmark can therefore understate the cost of the application that hosts it, while an application-level benchmark can include allocator and runtime work that the library author never sees.
The setting also has a trigger and a persistence rule. Build-time choices establish a default for the executable or application; runtime properties and compatibility changes can override some defaults for experimentation; and the selected mode governs the process as it runs. Android warns that an MTE-enabled app should be tested on a compatible device before release because previously latent native-code failures or unsupported assumptions may become visible only on MTE-capable hardware. The operational state is not complete when a manifest or build setting is written. It is complete only when the process has run through representative paths and its failures are understood.
This process boundary is why Android separates application MTE from stack instrumentation. Enabling heap checks can catch corruption in native heap allocations without rebuilding all the code. Detecting stack corruption requires additional compiler instrumentation, and the resulting application is intended for debugging on MTE-capable devices. The two mechanisms protect different memory regions and have different compatibility conditions; they should not be collapsed into one generic claim that “MTE is enabled.”
A staged lifecycle: discover, stabilize, cover
A useful development lifecycle begins with strict observation. During bring-up and testing, SYNC provides the most precise signal: it identifies the invalid access and, when the allocator participates, supplies allocation and deallocation context. That precision lets a team repair bugs before it has to decide whether a production process can tolerate the feature. Android recommends enabling SYNC broadly during development for native binaries and using the resulting reports to stabilize the codebase.
Production introduces a different objective. Testing rarely covers every user scenario, so Android recommends ASYNC as complementary coverage for well-tested critical processes. The process can continue to run with lower diagnostic detail until the operating system handles a mismatch, adding a layer of defense against bugs that escaped the test paths. This is not a claim that every process should run ASYNC regardless of measured cost. It is a policy that accepts less information at runtime in return for broader practical coverage.
Where the hardware supports it, the platform can configure a CPU-specific preferred MTE level. A process requested as ASYNC may run with SYNC or ASYMM on selected CPUs if those stricter checks have similar performance there. AOSP notes that small in-order cores often have similar performance in all three modes and may prefer SYNC, while other cores can benefit from ASYMM’s read precision. The preference is therefore a hardware-aware substitution, not a change to what the process asked for in the abstract.
The lifecycle can be represented as a set of states rather than a single release switch:
What persists through these transitions is the underlying tagged-memory mechanism and the process’s security objective. What changes is the timing and quality of the signal available to the operating system and developer. A mismatch in SYNC can preserve the faulting access and allocation history; a mismatch in ASYNC may preserve only the fact that a violation occurred by the time the kernel handles it. The code’s need for protection persists, but the observability budget changes.
The mode label is not the performance result
The practical error is to use mode names as performance predictions. SYNC can be modest on one core and expensive on another. ASYNC can avoid some ordering costs yet still saturate hardware that tracks in-flight tag checks. ASYMM can be close to ASYNC on a processor whose implementation handles its read and write paths efficiently, but the security value of its synchronous reads may be more important than a small numerical difference.
The correct choice begins with the failure that must be observed and the process that must be protected. If the immediate task is to locate native memory corruption, SYNC’s extra context may be worth its cost. If the codebase is tested and the goal is production defense-in-depth, ASYNC may be appropriate, subject to a real workload measurement. If the device and CPU support ASYMM, its split treatment may offer a better balance. None of those choices can be made responsibly from a mode label alone; the next step is to compare the measured combinations of mode, core, workload, and baseline.