LLVM サニタイザー

Android のビルドに利用されているコンパイラ インフラストラクチャである LLVM には、静的および動的な解析を行う複数のコンポーネントが含まれています。Android の解析時に広く使われているコンポーネント群の一つにサニタイザーがありますが、中でも代表的なのが、AddressSanitizer と UndefinedBehaviorSanitizer です。サニタイザーは、external/compiler-rt に含まれるコンパイラ ベースのインストルメンテーション コンポーネントで、開発やテストの際のバグ対応や Android の改善に活用できます。 Android の現在のサニタイザー セットは、多くの不適切なメモリの利用に関連するバグや、未定義の動作を検出および診断できます。

AddressSanitizer や UndefinedBehaviorSanitizer などのサニタイザーを有効にして Android ビルドを起動し、実行することをおすすめします。このページでは、AddressSanitizer、UndefinedBehaviorSanitizer、KernelAddressSanitizer について説明し、Android ビルドシステムでの使用方法を示します。また、これらのサニタイザーを有効にしてネイティブ コンポーネントをビルドする Android.mk ファイルと Android.bp ファイルの例を紹介します。

AddressSanitizer

AddressSanitizer(ASan)はコンパイラ ベースのインストルメンテーション機能であり、実行時に C / C++ コードのさまざまな種類のメモリエラーを検出します。ASan は、以下を含む多くの種類のメモリエラーを検出できます。

  • out-of-bounds(境界外)メモリアクセス
  • double-free(二重解放)
  • use-after-free(解放後の使用)

Android では、完全なビルドレベルで ASan インストルメンテーションが可能であり、asanwrapper を使用すればアプリレベルでも可能です。

AddressSanitizer は、alloca、malloc、free など、あらゆるメモリ関連関数呼び出しのインストルメンテーションによる診断を行い、すべての変数と割り当て済みメモリ領域に、読み取りまたは書き込み時に ASan コールバックをトリガーするメモリをパディングします。

インストルメンテーションにより double-free、use-after-scope、return、free など無効なメモリ使用のバグが検出され、メモリ領域パディングにより境界外の読み取りまたは書き込みが検出されます。このパディング領域への読み取りまたは書き込みが発生した場合、ASan はそれをキャッチして、メモリ違反の診断に役立つ情報(コールスタック、シャドウメモリ マップ、メモリ違反の種類、読み取りまたは書き込みの内容、違反の原因となった命令、メモリの内容など)を出力します。

pixel-xl:/ # sanitizer-status
=================================================================
==14164==ERROR: AddressSanitizer: heap-buffer-overflow on address 0x0032000054b0 at pc 0x005df16ffc3c bp 0x007fc236fdf0 sp 0x007fc236fdd0
WRITE of size 1 at 0x0032000054b0 thread T0
    #0 0x5df16ffc3b in test_crash_malloc sanitizer-status/sanitizer-status.c:36:13
    #1 0x5df17004e3 in main sanitizer-status/sanitizer-status.c:76:7
    #2 0x794cf665f3 in __libc_init (/system/lib64/libc.so+0x1b5f3)
    #3 0x5df16ffa53 in do_arm64_start (/system/bin/sanitizer-status+0xa53)

0x0032000054b0 is located 0 bytes to the right of 32-byte region [0x003200005490,0x0032000054b0)
allocated by thread T0 here:
    #0 0x794d0bdc67 in malloc (/system/lib64/libclang_rt.asan-aarch64-android.so+0x74c67)
    #1 0x5df16ffb47 in test_crash_malloc sanitizer-status/sanitizer-status.c:34:25
    #2 0x5df17004e3 in main sanitizer-status/sanitizer-status.c:76:7
    #3 0x794cf665f3 in __libc_init (/system/lib64/libc.so+0x1b5f3)
    #4 0x5df16ffa53 in do_arm64_start (/system/bin/sanitizer-status+0xa53)
    #5 0x794df78893  (<unknown module>)

SUMMARY: AddressSanitizer: heap-buffer-overflow sanitizer-status/sanitizer-status.c:36:13 in test_crash_malloc

バグの発見プロセスは非決定的である場合もあります。たとえば、特殊な設定下や、ヒープのプライミング、競合状態の利用などの高度な手法を用いた場合にしか生じないバグなどがあげられます。そうしたバグの多くはわかりやすいものではなく、実際の根本原因であるメモリ違反とはかけ離れた無数の指示となって表れることがあります。ASan はメモリ関連の関数をすべて診断し、ASan コールバックをトリガーしなくてはアクセスできない領域にデータをパディングします。つまり、クラッシュを誘発する破損を待つのではなく、メモリ違反が発生したら直ちにそれをキャッチします。これは、バグの検出と根本原因の診断に非常に役立ちます。

ターゲット デバイスで ASan を使用できることを確認するため、Android には asan_test 実行可能ファイルが付属しています。asan_test 実行可能ファイルは、ターゲット デバイス上の ASan 機能をテストおよび検証し、各テストのステータスを含む診断メッセージを出力します。ASan Android ビルドを使用している場合、このファイルはデフォルトでは /data/nativetest/asan_test/asan_test または /data/nativetest64/asan_test/asan_test にあります。

UndefinedBehaviorSanitizer

UndefinedBehaviorSanitizer(UBSan)は、コンパイル時のインストルメンテーションを実行して、さまざまな種類の未定義の動作をチェックします。UBSan は多くの未定義の動作を検出できますが、Android では alignment、bool、bounds、enum、float-cast-overflow、float-divide-by-zero、integer-divide-by-zero、nonnull-attribute、null、return、returns-nonnull-attribute、shift-base、shift-exponent、signed-integer-overflow、unreachable、unsigned-integer-overflow、vla-bound をサポートしています。unsigned-integer-overflow は技術的には未定義の動作ではないもののサニタイザーの対象に含まれており、潜在的な integer-overflow 脆弱性を排除するために、mediaserver コンポーネントを含む多くの Android モジュールで使用されます。

実装

Android ビルドシステムでは、グローバルまたはローカルで UBSan を有効にできます。UBSan をグローバルで有効にするには、Android.mk で SANITIZE_TARGET を設定します。UBSan をモジュール単位のレベルで有効にするには、LOCAL_SANITIZE を設定し、Android.mk で検索する未定義の動作を指定します。次に例を示します。

LOCAL_PATH:= $(call my-dir)
include $(CLEAR_VARS)

LOCAL_CFLAGS := -std=c11 -Wall -Werror -O0

LOCAL_SRC_FILES:= sanitizer-status.c

LOCAL_MODULE:= sanitizer-status

LOCAL_SANITIZE := alignment bounds null unreachable integer
LOCAL_SANITIZE_DIAG := alignment bounds null unreachable integer

include $(BUILD_EXECUTABLE)

Android ビルドシステムは、ブループリント ファイルでは、makefile と同様の詳細な診断をまだサポートしていません。ブループリント(Android.bp)として記述された最も近いものを以下に示します。

cc_binary {

    cflags: [
        "-std=c11",
        "-Wall",
        "-Werror",
        "-O0",
    ],

    srcs: ["sanitizer-status.c"],

    name: "sanitizer-status",

    sanitize: {
        misc_undefined: [
            "alignment",
            "bounds",
            "null",
            "unreachable",
            "integer",
        ],
        diag: {
            undefined : true
        },
    },

}

UBSan のショートカット

Android には、サニタイザーのセットを同時に有効にする integerdefault-ub という 2 つのショートカットもあります。integer は integer-divide-by-zerosigned-integer-overflowunsigned-integer-overflow を有効にします。 default-ub は、コンパイラのパフォーマンス問題を最小限に抑えるチェック(bool、integer-divide-by-zero、return、returns-nonnull-attribute、shift-exponent、unreachable、vla-bound)を有効にします。integer サニタイザー クラスは SANITIZE_TARGET と LOCAL_SANITIZE で使用できますが、default-ub は SANITIZE_TARGET でのみ使用できます。

エラーレポートの機能向上

Android のデフォルトの UBSan 実装は、未定義の動作を検出すると、指定された関数を呼び出します。デフォルトでは、この関数は abort です。ただし、2016 年 10 月以降、Android の UBSan にオプションのランタイム ライブラリが追加され、検出された未定義の動作の種類や、ファイルとソースコードの行情報を含む詳細なエラーレポートが出力できるようになりました。このエラーレポートを integer チェックで有効にするには、Android.mk ファイルに次の行を追加します。

LOCAL_SANITIZE:=integer
LOCAL_SANITIZE_DIAG:=integer

LOCAL_SANITIZE 値は、ビルド中にサニタイザーを有効にします。 LOCAL_SANITIZE_DIAG は、指定されたサニタイザーの診断モードを有効にします。LOCAL_SANITIZE と LOCAL_SANITIZE_DIAG を異なる値に設定することは可能ですが、LOCAL_SANITIZE のチェックのみが有効になります。LOCAL_SANITIZE でチェックが指定されておらず LOCAL_SANITIZE_DIAG で指定されている場合、そのチェックは有効にならず、診断メッセージは出力されません。

UBSan ランタイム ライブラリによって提供される情報の例を次に示します。

pixel-xl:/ # sanitizer-status ubsan
sanitizer-status/sanitizer-status.c:53:6: runtime error: unsigned integer overflow: 18446744073709551615 + 1 cannot be represented in type 'size_t' (aka 'unsigned long')

Kernel Address Sanitizer

ユーザー空間コンポーネント用の LLVM ベースのサニタイザーと同様に、Android には Kernel Address Sanitizer(KASAN)が含まれています。KASAN は、カーネルとコンパイル時の変更を組み合わせたインストルメンテーション システムで、バグの検出と根本原因の分析を容易にします。

KASAN は、カーネル内のさまざまな種類のメモリ違反を検出できます。また、スタック、ヒープ、グローバル変数に対する境界外の読み取りと書き込みを検出し、use-after-free と double-free を検出できます。

KASAN は、ASan と同様に、コンパイル時のメモリ関数のインストルメンテーションとシャドウメモリの組み合わせを使用して、ランタイム時のメモリアクセスをトラッキングします。KASAN では、カーネルメモリ空間の 8 分の 1 がシャドウメモリ専用で、これによりメモリアクセスが有効かどうかを判断します。

KASAN は、x86_64 および arm64 アーキテクチャでサポートされています。4.0 以降はアップストリーム カーネルに含まれており、Android 3.18 ベースのカーネルにバックポートされています。KASAN は、4.9.2 に基づいて gcc でコンパイルされた Android カーネルでテストされています。

KASAN に加えて、テストに役立つもう 1 つのカーネル変更として kcov があります。kcov は、カーネルでカバレッジに基づくファズテストを実施できるように開発されました。syscall 入力の観点からカバレッジを測定するものであり、syzkaller などのファジング システムで役立ちます。

実装

KASAN と kcov を有効にしてカーネルをコンパイルするには、カーネルのビルド構成に次のビルドフラグを追加します。

CONFIG_KASAN
CONFIG_KASAN_INLINE
CONFIG_TEST_KASAN
CONFIG_KCOV
CONFIG_SLUB
CONFIG_SLUB_DEBUG
CONFIG_CC_OPTIMIZE_FOR_SIZE

さらに、次の行を削除します。

CONFIG_SLUB_DEBUG_ON
CONFIG_SLUB_DEBUG_PANIC_ON
CONFIG_KASAN_OUTLINE
CONFIG_KERNEL_LZ4

次に、通常どおりカーネルをビルドしてフラッシュします。KASAN カーネルは元のカーネルよりかなり大きくなります。このことを考慮して、必要であれば、起動パラメータとブートローダーの設定を変更します。

カーネルをフラッシュした後、KASAN が有効化され実行中かどうかをカーネル起動ログで確認します。カーネルは、次のような KASAN 用のメモリマップ情報で起動します。

...
[    0.000000] c0      0 Virtual kernel memory layout:
[    0.000000] c0      0     kasan   : 0xffffff8000000000 - 0xffffff9000000000   (    64 GB)
[    0.000000] c0      0     vmalloc : 0xffffff9000010000 - 0xffffffbdbfff0000   (   182 GB)
[    0.000000] c0      0     vmemmap : 0xffffffbdc0000000 - 0xffffffbfc0000000   (     8 GB maximum)
[    0.000000] c0      0               0xffffffbdc0000000 - 0xffffffbdc3f95400   (    63 MB actual)
[    0.000000] c0      0     PCI I/O : 0xffffffbffa000000 - 0xffffffbffb000000   (    16 MB)
[    0.000000] c0      0     fixed   : 0xffffffbffbdfd000 - 0xffffffbffbdff000   (     8 KB)
[    0.000000] c0      0     modules : 0xffffffbffc000000 - 0xffffffc000000000   (    64 MB)
[    0.000000] c0      0     memory  : 0xffffffc000000000 - 0xffffffc0fe550000   (  4069 MB)
[    0.000000] c0      0       .init : 0xffffffc001d33000 - 0xffffffc001dce000   (   620 KB)
[    0.000000] c0      0       .text : 0xffffffc000080000 - 0xffffffc001d32284   ( 29385 KB)
...

バグは次のように表示されます。

[   18.539668] c3      1 ==================================================================
[   18.547662] c3      1 BUG: KASAN: null-ptr-deref on address 0000000000000008
[   18.554689] c3      1 Read of size 8 by task swapper/0/1
[   18.559988] c3      1 CPU: 3 PID: 1 Comm: swapper/0 Tainted: G        W      3.18.24-xxx #1
[   18.569275] c3      1 Hardware name: Android Device
[   18.577433] c3      1 Call trace:
[   18.580739] c3      1 [<ffffffc00008b32c>] dump_backtrace+0x0/0x2c4
[   18.586985] c3      1 [<ffffffc00008b600>] show_stack+0x10/0x1c
[   18.592889] c3      1 [<ffffffc001481194>] dump_stack+0x74/0xc8
[   18.598792] c3      1 [<ffffffc000202ee0>] kasan_report+0x11c/0x4d0
[   18.605038] c3      1 [<ffffffc00020286c>] __asan_load8+0x20/0x80
[   18.611115] c3      1 [<ffffffc000bdefe8>] android_verity_ctr+0x8cc/0x1024
[   18.617976] c3      1 [<ffffffc000bcaa2c>] dm_table_add_target+0x3dc/0x50c
[   18.624832] c3      1 [<ffffffc001bdbe60>] dm_run_setup+0x50c/0x678
[   18.631082] c3      1 [<ffffffc001bda8c0>] prepare_namespace+0x44/0x1ac
[   18.637676] c3      1 [<ffffffc001bda170>] kernel_init_freeable+0x328/0x364
[   18.644625] c3      1 [<ffffffc001478e20>] kernel_init+0x10/0xd8
[   18.650613] c3      1 ==================================================================

また、カーネルでモジュールが有効になっている場合は、test_kasan カーネル モジュールを読み込んでさらにテストすることができます。このモジュールは、out-of-bounds メモリアクセスと use-after-free を試行し、ターゲット デバイスでの KASAN のテストに役立ちます。