Friday, November 3, 2023

First handset with MTE on the market

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


Having run these commands, we need to configure Chrome to read the command line file; this can be done by opening Chrome, browsing to chrome://flags#enable-command-line-on-non-rooted-devices, and setting the highlighted flag to "Enabled".

Note that unfortunately this only applies to webpages viewed using the Chrome app, and not to other Chromium-based browsers or non-browser apps that use the Chromium based Android WebView to implement their rendering.