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 を有効にすると、Google Cloud 上のメモリ破損のバグを検出 ネイティブ ヒープ。スタックのメモリ破損を検出するほか、 アプリの MTE では、インストルメンテーションを使用してコードを再構築する必要があります。「 作成されるアプリは MTE 対応デバイスでのみ実行されます。
MTE を使用してアプリのネイティブ(JNI)コードをビルドする手順は次のとおりです。
ndk-build
Application.mk
ファイルに以下の行を追加します。
APP_CFLAGS := -fsanitize=memtag -fno-omit-frame-pointer -march=armv8-a+memtag
APP_LDFLAGS := -fsanitize=memtag -fsanitize-memtag-mode=sync -march=armv8-a+memtag
CMake
CMakeLists.txt 内の各ターゲットごとに、以下のように指定します。
target_compile_options(${TARGET} PUBLIC -fsanitize=memtag -fno-omit-frame-pointer -march=armv8-a+memtag)
target_link_options(${TARGET} PUBLIC -fsanitize=memtag -fsanitize-memtag-mode=sync -march=armv8-a+memtag)
アプリを実行する
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
命令を使用してランダムなタグを生成し、ポインタに保存する必要があります。
下層にあるメモリにタグを付けるには、次の手順を行います。
または、次の命令でメモリをゼロ初期化します。
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 ユーザーガイドをご覧ください。