Arm Memory Tagging Extension(MTE)

MTE を選ぶ理由

メモリ安全性のバグ(ネイティブ プログラミング言語でのメモリ処理のエラー)は、コードのよくある問題です。セキュリティの脆弱性だけでなく、安定性の問題にもつながります。

Armv9 では、ネイティブ コードで「解放後の使用」と「バッファ オーバーフロー」というバグを検出できるハードウェア拡張機能 Arm Memory Tagging Extension(MTE)が導入されました。

サポート状況を確認する

Android 13 以降、一部のデバイスは MTE に対応しています。デバイスが MTE を有効にして実行されているかどうかを確認するには、次のコマンドを実行します。

adb shell grep mte /proc/cpuinfo

結果が Features : [...] mte の場合、デバイスは MTE が有効な状態で動作しています。

一部のデバイスでは MTE がデフォルトで有効になっていませんが、MTE を有効にしてデバイスを再起動することも可能です。この設定は試験運用向けで、デバイスのパフォーマンスや安定性が低下する可能性があるため、通常の使用にはおすすめしませんが、アプリ開発には活用できる場合があります。このモードにアクセスするには、設定アプリで [開発者向けオプション] > [Memory Tagging Extension] に移動します。このオプションが表示されないデバイスでは、この方法で MTE を有効にすることができません。

MTE の動作モード

MTE は、SYNC と ASYNC の 2 つのモードをサポートしています。より的確な診断情報が得られるため、SYNC モードは開発目的に適しています。また、ASYNC モードは高性能であり、リリースされたアプリに対して有効にすることができます。

同期モード(SYNC)

このモードは、パフォーマンスよりもデバッグを容易に実施できるようにすることを重視して最適化されており、パフォーマンスに関するオーバーヘッドが増加することを許容できる場合は、高精度なバグ検出ツールとして使用できます。有効にした MTE SYNC は、セキュリティ対策としても機能します。

タグが一致しない場合、プロセッサは SIGSEGV(si_code SEGV_MTESERR)と、メモリアクセスおよび障害発生アドレスに関する詳細な情報を使用して、問題のあるロード命令またはストア命令を終了します。

このモードは、コード内で再コンパイルする必要がない HWASan に代わる迅速な手段として、テスト中に活用できます。または、アプリが脆弱な攻撃対象領域を表す本番環境でも活用できます。また、ASYNC モード(以下で説明)でバグが発見された場合は、ランタイム API を使用して SYNC モードに切り替えて実行することで、正確なバグレポートを取得できます。

さらに、SYNC モードで実行すると、Android アロケータはすべての割り当てと割り当て解除のスタック トレースを記録し、それを使用してメモリエラーの説明(解放後の使用、バッファ オーバーフローなど)と関連するメモリイベントのスタック トレースを含む詳細なエラーレポートを提示します(詳細については、MTE レポートについてをご覧ください)。このようなレポートは、コンテキストに即した情報を提供し、ASYNC モードの場合よりもバグのトレースと修正を容易にします。

非同期モード(ASYNC)

このモードは、バグレポートの精度よりもパフォーマンスを重視して最適化されており、オーバーヘッドの小さい、メモリ安全性のバグ検出ツールとして使用できます。タグが一致しない場合、プロセッサは最も近いカーネル エントリ(システムコールやタイマー割り込みなど)まで実行を継続し、障害発生アドレスやメモリアクセスを記録せずに SIGSEGV(コード SEGV_MTEAERR)を使用してプロセスを終了します。

このモードは、メモリ安全性のバグの密度が低いことがわかっている(テスト中に SYNC モードを使用することで実現)、十分にテストされたコードベースの本番環境でメモリ安全性に関する脆弱性を軽減するために活用できます。

MTE を有効にする

単一のデバイスの場合

実験的に、マニフェストに値を指定していない(または "default" を指定している)アプリの memtagMode 属性のデフォルト値を設定するために、アプリの互換性の変更を使用できます。

これらは、グローバル設定メニューの [システム] > [詳細設定] > [開発者向けオプション] > [アプリの互換性の変更] で確認できます。NATIVE_MEMTAG_ASYNC または NATIVE_MEMTAG_SYNC を設定すると、特定のアプリに対して MTE が有効になります。

あるいは、次のように am コマンドを使用して設定することもできます。

  • SYNC モードの場合: $ adb shell am compat enable NATIVE_MEMTAG_SYNC my.app.name
  • ASYNC モードの場合: $ adb shell am compat enable NATIVE_MEMTAG_ASYNC my.app.name

Gradle の場合

Gradle プロジェクトのすべてのデバッグビルドで MTE を有効にするには、

<?xml version="1.0" encoding="utf-8"?>
<manifest xmlns:android="http://schemas.android.com/apk/res/android"
    xmlns:tools="http://schemas.android.com/tools">

    <application android:memtagMode="sync" tools:replace="android:memtagMode"/>
</manifest>

app/src/debug/AndroidManifest.xml に変換されます。これにより、マニフェストの memtagMode がデバッグビルドの同期でオーバーライドされます。

または、カスタムの buildType のすべてのビルドに対して MTE を有効にすることもできます。そのためには、独自の buildType を作成し、XML を app/src/<name of buildType>/AndroidManifest.xml に配置します。

任意の対応デバイス上の APK の場合

MTE はデフォルトで無効になっています。アプリで MTE を使用するには、AndroidManifest.xml<application> タグまたは <process> タグで android:memtagMode を設定します。

android:memtagMode=(off|default|sync|async)

この属性を <application> タグで設定すると、アプリが使用するすべてのプロセスに影響が及びます。<process> タグを設定すると個々のプロセスについてオーバーライドできます。

アプリを実行する

MTE を有効にしたら、通常どおりアプリを使用およびテストします。メモリ安全性の問題が検出された場合、アプリは次のような Tombstone でクラッシュします(SYNC の場合は SEGV_MTESERR、ASYNC の場合は SEGV_MTEAERR を含む SIGSEGV に注意)。

pid: 13935, tid: 13935, name: sanitizer-statu  >>> sanitizer-status <<<
uid: 0
tagged_addr_ctrl: 000000000007fff3
signal 11 (SIGSEGV), code 9 (SEGV_MTESERR), fault addr 0x800007ae92853a0
Cause: [MTE]: Use After Free, 0 bytes into a 32-byte allocation at 0x7ae92853a0
x0  0000007cd94227cc  x1  0000007cd94227cc  x2  ffffffffffffffd0  x3  0000007fe81919c0
x4  0000007fe8191a10  x5  0000000000000004  x6  0000005400000051  x7  0000008700000021
x8  0800007ae92853a0  x9  0000000000000000  x10 0000007ae9285000  x11 0000000000000030
x12 000000000000000d  x13 0000007cd941c858  x14 0000000000000054  x15 0000000000000000
x16 0000007cd940c0c8  x17 0000007cd93a1030  x18 0000007cdcac6000  x19 0000007fe8191c78
x20 0000005800eee5c4  x21 0000007fe8191c90  x22 0000000000000002  x23 0000000000000000
x24 0000000000000000  x25 0000000000000000  x26 0000000000000000  x27 0000000000000000
x28 0000000000000000  x29 0000007fe8191b70
lr  0000005800eee0bc  sp  0000007fe8191b60  pc  0000005800eee0c0  pst 0000000060001000

backtrace:
      #00 pc 00000000000010c0  /system/bin/sanitizer-status (test_crash_malloc_uaf()+40) (BuildId: 953fc93301472d0b72709b2b9a9f6f30)
      #01 pc 00000000000014a4  /system/bin/sanitizer-status (test(void (*)())+132) (BuildId: 953fc93301472d0b72709b2b9a9f6f30)
      #02 pc 00000000000019cc  /system/bin/sanitizer-status (main+1032) (BuildId: 953fc93301472d0b72709b2b9a9f6f30)
      #03 pc 00000000000487d8  /apex/com.android.runtime/lib64/bionic/libc.so (__libc_init+96) (BuildId: 6ab39e35a2fae7efbe9a04e9bbb14331)

deallocated by thread 13935:
      #00 pc 000000000004643c  /apex/com.android.runtime/lib64/bionic/libc.so (scudo::Allocator<scudo::AndroidConfig, &(scudo_malloc_postinit)>::quarantineOrDeallocateChunk(scudo::Options, void*, scudo::Chunk::UnpackedHeader*, unsigned long)+688) (BuildId: 6ab39e35a2fae7efbe9a04e9bbb14331)
      #01 pc 00000000000421e4  /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: 6ab39e35a2fae7efbe9a04e9bbb14331)
      #02 pc 00000000000010b8  /system/bin/sanitizer-status (test_crash_malloc_uaf()+32) (BuildId: 953fc93301472d0b72709b2b9a9f6f30)
      #03 pc 00000000000014a4  /system/bin/sanitizer-status (test(void (*)())+132) (BuildId: 953fc93301472d0b72709b2b9a9f6f30)

allocated by thread 13935:
      #00 pc 0000000000042020  /apex/com.android.runtime/lib64/bionic/libc.so (scudo::Allocator<scudo::AndroidConfig, &(scudo_malloc_postinit)>::allocate(unsigned long, scudo::Chunk::Origin, unsigned long, bool)+1300) (BuildId: 6ab39e35a2fae7efbe9a04e9bbb14331)
      #01 pc 0000000000042394  /apex/com.android.runtime/lib64/bionic/libc.so (scudo_malloc+36) (BuildId: 6ab39e35a2fae7efbe9a04e9bbb14331)
      #02 pc 000000000003cc9c  /apex/com.android.runtime/lib64/bionic/libc.so (malloc+36) (BuildId: 6ab39e35a2fae7efbe9a04e9bbb14331)
      #03 pc 00000000000010ac  /system/bin/sanitizer-status (test_crash_malloc_uaf()+20) (BuildId: 953fc93301472d0b72709b2b9a9f6f30)
      #04 pc 00000000000014a4  /system/bin/sanitizer-status (test(void (*)())+132) (BuildId: 953fc93301472d0b72709b2b9a9f6f30)
Learn more about MTE reports: https://source.android.com/docs/security/test/memory-safety/mte-report

詳細については、AOSP ドキュメントの MTE レポートについてをご覧ください。また、Android Studio を使用してアプリをデバッグすることができ、デバッガは無効なメモリアクセスの原因となる行で停止します。

上級ユーザー: 独自のアロケータでの MTE の使用

通常のシステム アロケータで割り当てられていないメモリに MTE を使用するには、メモリとポインタをタグ付けするようにアロケータを変更する必要があります。

アロケータのページは、mmap(または mprotect)の prot フラグに PROT_MTE を使用して割り当てる必要があります。

タグ付けされた割り当ては、すべて 16 バイト境界に合わせる必要があります。タグは、16 バイトのチャンク(グラニュール)にのみ割り当てることができるためです。

次に、ポインタを返す前に、IRG 命令を使用してランダムなタグを生成し、ポインタに保存する必要があります。

下層にあるメモリにタグを付けるには、次の手順を行います。

  • STG: 1 つの 16 バイト グラニュールにタグを付けする
  • ST2G: 2 つの 16 バイト グラニュールにタグを付ける
  • DC GVA: キャッシュラインに同じタグを付ける

または、次の命令でメモリをゼロ初期化します。

  • STZG: 1 つの 16 バイト グラニュールにタグを付けて、ゼロ初期化する
  • STZ2G: 2 つの 16 バイト グラニュールにタグを付けて、ゼロ初期化する
  • DC GZVA: キャッシュラインに同じタグを付けて、ゼロ初期化する

以下の命令は古い CPU ではサポートされていないため、MTE が有効になっている場合は条件付きで実行する必要があります。次のようにすると、プロセスに対して MTE が有効になっているかどうかを確認できます。

#include <sys/prctl.h>

bool runningWithMte() {
      int mode = prctl(PR_GET_TAGGED_ADDR_CTRL, 0, 0, 0, 0);
      return mode != -1 && mode & PR_MTE_TCF_MASK;
}

scudo の実装を参考にしてください。

詳細

詳しくは、Arm による Android OS 用 MTE ユーザーガイドをご覧ください。