By Mark Brand, Google Project Zero
Introduction
It’s finally time for me to fulfill a long-standing promise. Since I first heard about ARM’s Memory Tagging Extensions, I’ve said (to far too many people at this point to be able to back out…) that I’d immediately switch to the first available device that supported this feature. It’s been a long wait (since late 2017) but with the release of the new Pixel 8 / Pixel 8 Pro handsets, there’s finally a production handset that allows you to enable MTE!
The ability of MTE to detect memory corruption exploitation at the first dangerous access is a significant improvement in diagnostic and potential security effectiveness. The availability of MTE on a production handset for the first time is a big step forward, and I think there’s real potential to use this technology to make 0-day harder.
I’ve been running my Pixel 8 with MTE enabled since release day, and so far I haven’t found any issues with any of the applications I use on a daily basis1, or any noticeable performance issues.
Currently, MTE is only available on the Pixel as a developer option, intended for app developers to test their apps using MTE, but we can configure it to default to synchronous mode for all2 apps and native user mode binaries. This can be done on a stock image, without bootloader unlocking or rooting required – just a couple of debugger commands. We’ll do that now, but first:
Disclaimer
This is absolutely not a supported device configuration; and it’s highly likely that you’ll encounter issues with at least some applications crashing or failing to run correctly with MTE if you set your device up in this way.
This is how I’ve configured my personal Pixel 8, and so far I’ve not experienced any issues, but this was somewhat of a surprise to me, and I’m still waiting to see what the first app that simply won’t work at all will be…
Enabling MTE on Pixel 8/Pixel 8 Pro
Enabling MTE on an Android device requires the bootloader to reserve a portion of the device memory for storing tags. This means that there are two separate places where MTE needs to be enabled – first we need to configure the bootloader to enable it, and then we need to configure the system to use it in applications.
First we need follow the Android instructions to enable developer mode and USB debugging on the device:
Now we need to connect our phone to a trusted computer that has the Android debugging tools installed on it – I’m using my linux workstation:
markbrand@markbrand$ adb devices -l
List of devices attached
XXXXXXXXXXXXXX device usb:3-3 product:shiba model:Pixel_8 device:shiba transport_id:5
markbrand@markbrand$ adb shell
shiba:/ $ setprop arm64.memtag.bootctl memtag
shiba:/ $ setprop persist.arm64.memtag.default sync
shiba:/ $ setprop persist.arm64.memtag.app_default sync
shiba:/ $ reboot
These commands are doing a couple of things – first, we’re configuring the bootloader to enable MTE at boot. The second command sets the default MTE mode for native executables running on the device, and the third command sets the default MTE mode for apps. An app developer can enable MTE by using the manifest, but this system property sets the default MTE mode for apps, effectively making it opt-out instead of opt-in.
While on the topic of apps opting-out, it’s worth noting that Chrome doesn’t use the system allocator for most allocations, and instead uses PartitionAlloc. There is experimental MTE support under development, which can be enabled with some additional steps3. Unfortunately this currently requires setting a command-line flag which involves some security tradeoffs. We expect that Chrome will add an easier way to enable MTE support without these problems in the near future.
If we look at all of the system properties, we can see that there are a few additional properties that are related to memory tagging:
shiba:/ $ getprop | grep memtag
[arm64.memtag.bootctl]: [memtag]
[persist.arm64.memtag.app.com.android.nfc]: [off]
[persist.arm64.memtag.app.com.android.se]: [off]
[persist.arm64.memtag.app.com.google.android.bluetooth]: [off]
[persist.arm64.memtag.app_default]: [sync]
[persist.arm64.memtag.default]: [sync]
[persist.arm64.memtag.system_server]: [off]
[ro.arm64.memtag.bootctl_supported]: [1]
There are unfortunately some default exclusions which we can’t overwrite – the protections on system properties mean that we can’t currently enable MTE for a few components in a normal production build – these exceptions are system_server and applications related to nfc, the secure element and bluetooth.
We wanted to make sure that these commands work, so we’ll do that now. We’ll first check whether it’s working for native executables:
shiba:/ $ cat /proc/self/smaps | grep mt
VmFlags: rd wr mr mw me ac mt
VmFlags: rd wr mr mw me ac mt
VmFlags: rd wr mr mw me ac mt
VmFlags: rd wr mr mw me ac mt
VmFlags: rd wr mr mw me ac mt
VmFlags: rd wr mr mw me ac mt
VmFlags: rd wr mr mw me ac mt
765bff1000-765c011000 r–s 00000000 00:12 97 /dev/__properties__/u:object_r:arm64_memtag_prop:s0
We can see that our cat process has mappings with the mt bit set, so MTE has been enabled for the process.
Now in order to check that an app without any manifest setting has picked up this, we added a little bit of code to an empty JNI project to trigger a use-after-free bug:
extern “C” JNIEXPORT jstring JNICALL
Java_com_example_mtetestapplication_MainActivity_stringFromJNI(
JNIEnv* env,
jobject /* this */) {
char* ptr = strdup(“test string”);
free(ptr);
// Use-after-free when ptr is accessed below.
return env->NewStringUTF(ptr);
}
Without MTE, it’s unlikely that the application would crash running this code. I also made sure that the application manifest does not set MTE, so it will inherit the default. When we launch the application we will see whether it crashes, and whether the crash is caused by an MTE check failure!
Looking at the logcat output we can see that the cause of the crash was a synchronous MTE tag check failure (SEGV_MTESERR).
DEBUG : *** *** *** *** *** *** *** *** *** *** *** *** *** *** *** ***
DEBUG : Build fingerprint: ‘google/shiba/shiba:14/UD1A.230803.041/10808477:user/release-keys’
DEBUG : Revision: ‘MP1.0’
DEBUG : ABI: ‘arm64’
DEBUG : Timestamp: 2023-10-24 16:56:32.092532886+0200
DEBUG : Process uptime: 2s
DEBUG : Cmdline: com.example.mtetestapplication
DEBUG : pid: 24147, tid: 24147, name: testapplication >>> com.example.mtetestapplication <<<
DEBUG : uid: 10292
DEBUG : tagged_addr_ctrl: 000000000007fff3 (PR_TAGGED_ADDR_ENABLE, PR_MTE_TCF_SYNC, mask 0xfffe)
DEBUG : pac_enabled_keys: 000000000000000f (PR_PAC_APIAKEY, PR_PAC_APIBKEY, PR_PAC_APDAKEY, PR_PAC_APDBKEY)
DEBUG : signal 11 (SIGSEGV), code 9 (SEGV_MTESERR), fault addr 0x0b000072afa9f790
DEBUG : x0 0000000000000001 x1 0000007fe384c2e0 x2 0000000000000075 x3 00000072aae969ac
DEBUG : x4 0000007fe384c308 x5 0000000000000004 x6 7274732074736574 x7 00676e6972747320
DEBUG : x8 0000000000000020 x9 00000072ab1867e0 x10 000000000000050c x11 00000072aaed0af4
DEBUG : x12 00000072aaed0ca8 x13 31106e3dee7fb177 x14 ffffffffffffffff x15 00000000ebad6a89
DEBUG : x16 0000000000000001 x17 000000722ff047b8 x18 00000075740fe000 x19 0000007fe384c2d0
DEBUG : x20 0000007fe384c308 x21 00000072aae969ac x22 0000007fe384c2e0 x23 070000741fa897b0
DEBUG : x24 0b000072afa9f790 x25 00000072aaed0c18 x26 0000000000000001 x27 000000754a5fae40
DEBUG : x28 0000007573c00000 x29 0000007fe384c260
DEBUG : lr 00000072ab35e7ac sp 0000007fe384be30 pc 00000072ab1867ec pst 0000000080001000
DEBUG : 98 total frames
DEBUG : backtrace:
DEBUG : #00 pc 00000000003867ec /apex/com.android.art/lib64/libart.so (art::(anonymous namespace)::ScopedCheck::Check(art::ScopedObjectAccess&, bool, char const*, art::(anonymous namespace)::JniValueType*) (.__uniq.99033978352804627313491551960229047428)+1636) (BuildId: a5fcf27f4a71b07dff05c648ad58e3cd)
DEBUG : #01 pc 000000000055e7a8 /apex/com.android.art/lib64/libart.so (art::(anonymous namespace)::CheckJNI::NewStringUTF(_JNIEnv*, char const*) (.__uniq.99033978352804627313491551960229047428.llvm.6178811259984417487)+160) (BuildId: a5fcf27f4a71b07dff05c648ad58e3cd)
DEBUG : #02 pc 00000000000017dc /data/app/~~lgGoAt3gB6oojf3IWXi-KQ==/com.example.mtetestapplication-k4Yl4oMx9PEbfuvTEkjqFg==/base.apk!libmtetestapplication.so (offset 0x1000) (_JNIEnv::NewStringUTF(char const*)+36) (BuildId: f60a9970a8a46ff7949a5c8e41d0ece51e47d82c)
…
DEBUG : Note: multiple potential causes for this crash were detected, listing them in decreasing order of likelihood.
DEBUG : Cause: [MTE]: Use After Free, 0 bytes into a 12-byte allocation at 0x72afa9f790
DEBUG : deallocated by thread 24147:
DEBUG : #00 pc 000000000005e800 /apex/com.android.runtime/lib64/bionic/libc.so (scudo::Allocator<scudo::AndroidConfig, &(scudo_malloc_postinit)>::quarantineOrDeallocateChunk(scudo::Options, void*, scudo::Chunk::UnpackedHeader*, unsigned long)+496) (BuildId: a017f07431ff6692304a0cae225962fb)
DEBUG : #01 pc 0000000000057ba4 /apex/com.android.runtime/lib64/bionic/libc.so (scudo::Allocator<scudo::AndroidConfig, &(scudo_malloc_postinit)>::deallocate(void*, scudo::Chunk::Origin, unsigned long, unsigned long)+212) (BuildId: a017f07431ff6692304a0cae225962fb)
DEBUG : #02 pc 000000000000179c /data/app/~~lgGoAt3gB6oojf3IWXi-KQ==/com.example.mtetestapplication-k4Yl4oMx9PEbfuvTEkjqFg==/base.apk!libmtetestapplication.so (offset 0x1000) (Java_com_example_mtetestapplication_MainActivity_stringFromJNI+40) (BuildId: f60a9970a8a46ff7949a5c8e41d0ece51e47d82c)
If you just want to check that MTE has been enabled in the bootloader, there’s an application on the Play Store from Google’s Dynamic Tools team, which you can also use (this app enables MTE in async mode in the manifest, which is why you see below that it’s not running in sync mode on all cores):
At this point, we can go back into the developer settings and disable USB debugging, since we don’t want that enabled for normal day-to-day usage. We do need to leave the developer mode toggle on, since disabling that will turn off MTE again entirely on the next reboot.
Conclusion
The Pixel 8 with synchronous-MTE enabled is at least subjectively a performance and battery-life upgrade over my previous phone.
I think this is a huge improvement for the general security of the device – many zero-click attack surfaces involve large amounts of unsafe C/C++ code, whether that’s WebRTC for calling, or one of the many media or image file parsing libraries. MTE is not a silver bullet for memory safety – but the release of the first production device with the ability to run almost all user-mode applications with synchronous-MTE is a huge step forward, and something that’s worth celebrating!
1 On a team member’s device, a single MTE detection of a use-after-free bug happened last week. This resulted in a crash that wasn’t noticed at the time, but which we later found when looking through the saved crash reports on their device. Because the alloc and free stacktraces of the allocation were recorded, we were able to quickly figure out the bug and report it to the application developers – the bug in this case was caused by user gesture input, and doesn’t really have security impact, but it already illustrates some of the advantages of MTE.
2 Except for se (secure element), bluetooth, nfc, and the system server, due to these system apps explicitly setting their individual system properties to ‘off’ in the Pixel system image.
3 Enabling MTE in Chrome requires setting multiple command line flags, which on a non-rooted Android device requires configuring Chrome to load the command line flags from a file in /data/local/tmp. This is potentially unsafe, so we’d not suggest doing this, but if you’d like to experiment on a test device or for fuzzing, the following commands will allow you to run Chrome with MTE enabled:
markbrand@markbrand:~$ adb shell
shiba:/ $ umask 022
shiba:/ $ echo “_ –enable-features=PartitionAllocMemoryTagging:enabled-processes/all-processes/memtag-mode/sync –disable-features=PartitionAllocPermissiveMte,KillPartitionAllocMemoryTagging” > /data/local/tmp/chrome-command-line
shiba:/ $ ls -la /data/local/tmp/chrome-command-line
-rw-r–r– 1 shell shell 176 2023-10-25 19:14 /data/local/tmp/chrome-command-line