13 minutes
Introduction to Arm Memory Tagging Extensions
This post is an introduction into Arm Memory Tagging Extensions (MTE), a hardware-based defense against memory safety vulnerabilities. Arm MTE is available in Google Pixel 8 devices (released 2023) and in iPhone 17 devices (released 2025).
The focus of this post is on explaining the high-level ideas, why MTE is important, and how developers can enable MTE in their Android and iOS apps.
Background: memory allocations
Programs store data in memory either on the stack or on the heap. Generally, the stack is used for data that is small and whose size is known when the program is compiled. The heap is used for larger data and for data whose size is only known when the program executes (for example, any data that the user inputs).
Memory on the stack is “automatically managed” in the sense that the compiler inserts instructions to automatically “allocate” and “deallocate” memory on the stack (by moving the stack pointer). This is possible because the compiler already knows the size of the needed memory.
Memory on the heap is dynamically managed by the program when it executes.
The program calls malloc(size)
to allocate some memory.
The memory allocator then reserves the requested memory size and gives the program a pointer to where this memory section is.
Once the program no longer needs this memory, it should call free(pointer)
to tell the allocator that the memory can be reused.
This manual memory management (malloc+free) is common in C and C++.
In other languages, developers don’t need to manually manage heap memory:
In Java and Go the garbage collector
periodically frees unused memory.
In Rust, the compiler automatically inserts free()
statements into the compiled code
(since Rust’s ownership semantics allow the compiler to figure out when a value will become unused).
In Swift and in C++’s smart pointers, Automatic Reference Counting
automatically frees memory once it is no longer referenced.
Background: memory vulnerabilities
Manual memory management puts a burden on the programmers using C/C++. Programmers need to make sure to only access memory that is allocated, to not forget to free the memory (causing memory leaks), and to not access the memory again once it has been freed. Unfortunately, as the last 30+ years have shown, C/C++ programmers repeatedly make these mistakes. A lot.
These mistakes in memory management are a common source of vulnerabilities:
- Spatial safety (out-of-bounds access, buffer overflow): The program accesses memory outside its allocated region. (This can happen with stack memory, too.)
- Temporal safety (use-after-free, double free): The program accesses memory after it has freed it.
By maliciously crafting inputs, an attacker can exploit such memory issues to take over the program flow and execute custom code.
Defenses against memory safety vulnerabilities
Unfortunately, memory safety vulnerabilities are very common in C and C++, and a LOT of code is written in those languages. Therefore unsurprisingly, people have been looking into defenses for a long time. Examples of such defenses are:
- Hardened memory allocators, for example in iOS and GrapheneOS.
- Pointer authentication (PAC), for example in iOS and Chromium for Android.
- Control flow integrity (CFI) such as PAC,
BTI, shadow stacks, and stack canaries.
- This prevents attackers from redirecting the control flow of a program to an unintended path.
- MTE
- This prevents attackers from corrupting memory (possibly to redirect the control flow later).
Note that these approaches are complementary, and should be used together.
What these approaches and MTE have in common, is that they don’t require changes to existing code (except that some require re-compiling the program). Additionally, they eliminate entire classes of attacks across the entire program.
Other approaches (like smart pointers) also exist, but they require developers to rewrite their code (and all of their dependencies!) to use modern, “safe” idioms/language features. This is a lot of work – work that is arguably better spent on rewriting the code in a memory-safe language such as Rust. Also, they only eliminate issues in the narrow sections of code where they are applied.
Sidenote: unsafe memory usage hides even in modern C++ idioms!
How MTE works
Memory tagging works by assigning a unique, random tag to every memory location.
This tag changes every time a memory location is allocated with malloc()
and every time it is free()
d.
malloc()
returns a pointer to the start of the allocated memory region.
The tag for this memory region is embedded/encoded into the pointer.
Memory is accessed via pointers, possibly with an offset (“please read the value stored at the memory location this pointer points to”). An access can be a read or a write. For each memory access, the hardware checks that the tag in the pointer matches the tag of the memory location. If they mismatch, the access is denied. In practice, this causes the app to crash.
This scheme prevents invalid memory accesses, because invalid accesses will have non-matching tags:
- Spatial safety (out-of-bounds access, buffer overflow): The memory location has a different tag than the pointer, because the pointer came from the allocation of a different memory region.
- Temporal safety (use-after-fre): The pointer and the memory location had matching tags in the past, but when the memory was freed, the memory tag changed.
To attack MTE, an attacker needs to guess the tag of the memory location they want to access, and then modify the pointer to have the same tag. For example, an attacker might leak the tag via a side-channel (see below).
For a more detailed explanation, see the links at the bottom of this post.
For a visual representation, see the figures below, taken from the slides of this talk by Andrey Konovalov. Tags are represented as colors.
Out-of-bounds with MTE Use-after-free with MTE
MTE on Android
Among Android devices, Google Pixel 8 (and later) are the only known devices with hardware support for MTE, thanks to Google’s Tensor G3 chip. They were released in October 2023.
No other Android phones seem to have practical MTE. A big reason is the lack of hardware support (see the appendix). However, even if the chip would support it, every phone vendor customizes Android, and may not enable MTE in their operating system.
On Google Pixel’s Android, MTE is NOT enabled for the kernel, but it is enabled for critical userspace system processes such as Bluetooth, NFC, and SecureElement (source). Additionally, Android Advanced Protection Mode enables MTE. (However, I haven’t found an offical source that says for which parts MTE is additionally enabled by Advanced Protection.)
On GrapheneOS (a security-focussed Android-based operating system), MTE is enabled for the kernel, for some of the system apps, and for user-installed apps that don’t have native code (source 1, source 2).
To enable MTE for a given app, the app developer needs to explicitly opt-in by
setting
android:memtagMode="async"
in the AndroidManifest.xml
of their app.
Additionally, GrapheneOS provides a toggle allowing users to force MTE for specific apps.
MTE on iOS
Apple’s marketing name for MTE is Memory Integrity Enforcement (MIE). MIE is supported on the A19 chip, included in iPhone 17 devices, launched in September 2025. It is unclear if and when MIE is coming to M-series chips (for iPads and Macs). MIE uses a newer version of Arm MTE; the concepts are the same.
On iOS, MIE is enabled in the “the kernel and over 70 userland processes”.
To enable MIE for a given app, the app developer needs to explicitly opt-in by setting the appropriate entitlement.
Why is MTE not more widely deployed?
Why has MTE not been deployed sooner? Why is MTE not enabled for every app and every process on the phone, but only for some?
MTE has two “problems”:
- Performance: Maintaining and checking the tags introduces a computational and memory overhead.
- Crashes: Invalid memory accesses cause crashes. Without MTE, buggy apps that make invalid accesses (such as use-after-free) often silently work. With MTE, these accesses are denied and the apps crash (by design). (Note that MTE does not introduce new bugs, but it makes existing bugs very visible to the user.)
Performance is one of the reasons why Google is hesitant to enable MTE for more parts of Android, and why it is keeping MTE disabled for the kernel. Apple has solved the performance problem by deeply optimising the hardware for MIE. Its new A19 chip is much faster, even with MIE enabled.
Sidenote: This pattern of product requirements driving chip design is a recurring pattern at Apple. Apple doesn’t build a fast chip and then thinks about what it can do with it. Instead, they start with a requirement (cinematic mode in iPhone 13, MIE in iPhone 17), and then design the chip to support the desired feature (massive GPU performance in A15, MIE performance in A19).
Crashes are the reason why both Apple and Google don’t force-enable MTE for user-installed apps. Instead, app developers need to opt-in to MTE on both Android and iOS.
It is not clear if Apple and Google force-enable MTE for user-installed apps when Lockdown Mode/Advanced Protection are enabled. Doing so would make sense because users with such high security requirements benefit the most from MTE, while also being more likely to accept some app crashes (as the price of security).
To underline the point about app crashes: Apple Music on Android regularly crashes on my Pixel 8a with GrapheneOS. Here is an example of a stack trace:
type: crash
osVersion: google/akita/akita:16/BP2A.250805.005/2025091000:user/release-keys
package: com.apple.android.music:1472, targetSdk 35
sharedUid: com.apple.android
process: com.apple.android.music
signal 11 (SIGSEGV), code 9 (SEGV_MTESERR), fault addr 0x0d00c4378c1aecb8
backtrace:
#00 pc 0000000000604c5c /data/app/~~qvCT42FscwNrFow1psc79g==/com.apple.android.music-SUeV4RIatUrpGVV1gt7VDg==/lib/arm64/libandroidappmusic.so (BuildId: b52cdcc1f1daf9a96aaaa911f060d467d7b54552)
#01 pc 0000000000601ae0 /data/app/~~qvCT42FscwNrFow1psc79g==/com.apple.android.music-SUeV4RIatUrpGVV1gt7VDg==/lib/arm64/libandroidappmusic.so (BuildId: b52cdcc1f1daf9a96aaaa911f060d467d7b54552)
#02 pc 0000000000600c24 /data/app/~~qvCT42FscwNrFow1psc79g==/com.apple.android.music-SUeV4RIatUrpGVV1gt7VDg==/lib/arm64/libandroidappmusic.so (BuildId: b52cdcc1f1daf9a96aaaa911f060d467d7b54552)
#03 pc 000000000033ae04 /data/app/~~qvCT42FscwNrFow1psc79g==/com.apple.android.music-SUeV4RIatUrpGVV1gt7VDg==/lib/arm64/libandroidappmusic.so (Java_com_apple_android_music_renderer_javanative_LevelComposer_compose+268) (BuildId: b52cdcc1f1daf9a96aaaa911f060d467d7b54552)
#04 pc 0000000000049d7c /system/framework/arm64/boot-core-libart.oat (art_jni_trampoline+140) (BuildId: e221ad2e1579dba8c552c66ad1435db15dd3b38c)
#05 pc 00000000025b4a64 /data/app/~~qvCT42FscwNrFow1psc79g==/com.apple.android.music-SUeV4RIatUrpGVV1gt7VDg==/oat/arm64/base.odex (com.apple.android.music.playback.player.PlayerAudioFadeControl.setComposerTransition+1092)
#06 pc 00000000025ae7d0 /data/app/~~qvCT42FscwNrFow1psc79g==/com.apple.android.music-SUeV4RIatUrpGVV1gt7VDg==/oat/arm64/base.odex (com.apple.android.music.playback.player.PlayerAudioFadeControl$computeTransitionJob$1.invokeSuspend+304)
#07 pc 0000000003541ef8 /data/app/~~qvCT42FscwNrFow1psc79g==/com.apple.android.music-SUeV4RIatUrpGVV1gt7VDg==/oat/arm64/base.odex (Vb.a.resumeWith+152)
#08 pc 000000000362b070 /data/app/~~qvCT42FscwNrFow1psc79g==/com.apple.android.music-SUeV4RIatUrpGVV1gt7VDg==/oat/arm64/base.odex (yd.T.run+1024)
#09 pc 0000000000214c54 /system/framework/arm64/boot.oat (java.util.concurrent.ThreadPoolExecutor.runWorker+676) (BuildId: 5e31ba31fc4f779978b4ee5a158a82f8b97f6aad)
#10 pc 0000000000218bf8 /system/framework/arm64/boot.oat (java.util.concurrent.ThreadPoolExecutor$Worker.run+56) (BuildId: 5e31ba31fc4f779978b4ee5a158a82f8b97f6aad)
#11 pc 00000000000a94f0 /system/framework/arm64/boot.oat (java.lang.Thread.run+64) (BuildId: 5e31ba31fc4f779978b4ee5a158a82f8b97f6aad)
#12 pc 00000000002faf94 /apex/com.android.art/lib64/libart.so (art_quick_invoke_stub+612) (BuildId: c3d2bff6e80fd46838179f7753be74fa)
#13 pc 00000000002e59dc /apex/com.android.art/lib64/libart.so (art::ArtMethod::Invoke(art::Thread*, unsigned int*, unsigned int, art::JValue*, char const*)+220) (BuildId: c3d2bff6e80fd46838179f7753be74fa)
#14 pc 000000000042b08c /apex/com.android.art/lib64/libart.so (art::Thread::CreateCallback(void*)+940) (BuildId: c3d2bff6e80fd46838179f7753be74fa)
#15 pc 000000000042accc /apex/com.android.art/lib64/libart.so (art::Thread::CreateCallbackWithUffdGc(void*)+12) (BuildId: c3d2bff6e80fd46838179f7753be74fa)
#16 pc 000000000008e254 /apex/com.android.runtime/lib64/bionic/libc.so (__pthread_start(void*)+180) (BuildId: e688446b48f00a577c82f6e36cac2c6e)
#17 pc 000000000007f9f4 /apex/com.android.runtime/lib64/bionic/libc.so (__start_thread+68) (BuildId: e688446b48f00a577c82f6e36cac2c6e)
How to enable MTE for your Android or iOS app
As a user, there is little you can do. At the minimum, you need to buy a Pixel 8 or iPhone 17 (and later) to have the hardware support, and rely on Google/Apple to enable MTE for the operating system. On Android, you can also enable Advanced Protection or install GrapheneOS. For manually installed apps, you are reliant on app developers enabling MTE for their apps (ask them about it!).
As an app developer, you can opt-in your app into using MTE (by setting the manifest flag or the entitlement, see above). Especially if you don’t have native code (only Java/Kotlin), this should be easy and not cause any issues. If you have custom native code, MTE may even detect some bugs that you were previously not aware of.
Important: before you ship a release with MTE enabled, thoroughly test your app on a supported device (Pixel 8 and iPhone 17), and fix any crashes that you find.
Should you enable MTE for your app?
Yes!
It is easy to do, and should not impact your app much. If anything, MTE crashes will surface bugs that you had before but didn’t know about.
Interestingly, even if your Android app is written in Java/Kotlin and has no native bundled into the APK file, at runtime your app’s process may call Android framework functions that use native code, which can have memory vulnerabilities. Therefore, even if you don’t explicitly use native code, MTE can still protect your app. For a real-world example, see this bug in the Signal messenger that was detected by MTE.
Especially apps that handle input that can be controlled by a remote attacker over the network should enable MTE. Among others, this includes web browsers, messenger apps, e-mail clients, social media, file sharing and cloud storage apps, and media streaming (video, music, podcasts). These apps allow attackers to remotely and relatively easily cause your phone to load malicious content (by sending a message, sharing a file, tricking you into opening a website or watching a video). And unfortunately, media parsers are a common source of vulnerabilities, because image/audio/video formats are often very complex. For example, see the 2021 FORCEDENTRY and the 2023 BLASTPASS exploits.
No silver bullet
None of the discussed defenses are magical solutions:
- Both Pointer Authentication (PACMAN) and MTE (StickyTags, TikTag) have been vulnerable to side-channel attacks.
- MTE only applies to main memory. GitHub researchers demonstrated an attack that (ab)uses GPU memory.
Nevertheless, these defenses raise the bar and eliminiate entire classes of attacks. This forces attackers to come up with more complicated exploitation techniques. MTE raises the cost of attacks.
Conclusion
This post gave a high-level introduction into Arm MTE. MTE is a strong defense against memory vulnerabilities. Hardware support is still limited to a few devices, but will hopefully grow. MTE already protects parts of the operating system and system apps. To protect user-installed apps, app developers need to manually enable MTE and test their apps with it.
Here is what you can do next:
- If you want to dive deeper into the technical details, continue reading with the resources linked below.
- If you are a security-conscious user, consider buying a device with hardware support for MTE, such as a Google Pixel 8 (and later) or an iPhone 17 (or another Apple device with an A19 chip).
- If you are a mobile app developer, opt-in to MTE now!
Additionally, if you have more information about the unclear-to-me points mentioned in this post, please let me know! In particular:
- For which parts does Android Advanced Protection enable MTE (additionally to what is already enabled anyway)? The kernel? What else?
- Do Advanced Protection mode and Lockdown mode force-enable MTE for user-installed apps?
- What is the state of MTE on other Android phones? Are other OEMs considering it?
- What is the state of MTE on Apple Silicon M-series chips? Is it coming?
Further reading
For a deeper technical dive into MTE, I recommend the following resources (accessible at the time of writing, 2025-09-16):
- General:
- Android:
- iOS:
- Linux:
- Arm:
Appendix: hardware support
In this section, I will try to briefly summarise my understanding of the hardware support for MTE.
Instruction set architecture (ISA)
MTE was first introduced to the instruction set architecture (ISA) in Armv8.5. In Armv8.7 and Armv8.9, MTE was extended with additional features. (Apple’s MIE is based on FEAT_MTE4 in Armv8.9) Armv9 contains no changes to MTE.
Notably, MTE is optional in all of these ISA versions. This means that chip vendors can produce a chip that is Armv9-compliant, but does not contain MTE!
Chips
Arm’s own Cortex CPU design has MTE starting from the first Armv9-compliant Cortex CPUs.
However, Arm does not actually produce these CPUs! Instead, other vendors license the CPU design and build their own system-on-a-chip (SoC) with it. Prominent examples are Google Tensor, Qualcomm Snapdragon, Samsung Exynos, and Apple Silicon. Some of these are based on Cortex (with varying degrees of modification) while others (like Apple Silicon) are independent.
This breadth of different chips is why so far no Android phones except the Google Pixel have support for MTE. According to GrapheneOS, some high-end Samsung Exynos and MediaTek chips have MTE support, and Qualcomm Snapdragon is planning it. On the server side, Ampere has recently added MTE support. See also this overview table maintained by a RedHat engineer.
Therefore, the difference between what features the ISA describes and what the physical chip implements is important.
There is some confusion online about when MTE was “introduced”. For example, the Linux kernel docs say Armv8.5, while the Android NDK docs say Armv9. This confusion comes from the ISA–chip mismatch. Even though Armv8.5 defined MTE, there were no chips that supported it. The first Cortex designs with MTE support happened to already support the Armv9 ISA.
The most reliable way to check if your chip supports MTE is to query it:
adb shell grep mte /proc/cpuinfo
My Google Pixel 8a reports to support mte mte3
.
2709 Words
2025-09-16 17:00 +0000