プログラミング
Poison, redzones and shadows: KASANの内部を探る
Poison, redzones and shadows: inside KASAN (bootlin.com)
要約
Linuxカーネルのメモリ安全性を向上させるためのツールであるKASAN(Kernel Address Sanitizer)について解説しています。KASANは、コンパイラによるインストルメンテーションとシャドウメモリを用いて、境界外アクセスや解放後使用といったメモリ操作のエラーを検出します。記事では、KASANの内部構造、ビルド時および実行時の影響、そしてKASANレポートの解釈方法について詳しく説明しています。
全文翻訳
LinuxカーネルのコードベースにおいてRustの存在が徐々に増加しているとはいえ、プロジェクトの大部分は依然としてCで書かれています。これは低レベルコードを書くためのデファクトスタンダードな言語ですが、残念ながら間違いを犯す可能性も大きく、その結果として発生する障害は、未定義動作、バッファオーバーフロー、セグメンテーションフォルトなど、多岐にわたります。カーネルもこれらの問題から免れることはできず、カーネル開発者はこれらの問題を早期に発見するための専用ツールを必要としています。多くの問題はメモリ管理に根差しているため、カーネルハッカーのツールボックスに legitimate に収まるツールの一つが、Kernel Address Sanitizer、略してKASANです。
この記事は読者にKASANの使い方を教えることを目的としていません。カーネルドキュメントにはこの件に関してかなり明確に記載されています。むしろ、KASANの内部構造を探求し、主にその影響(ビルド時と実行時の両方)を理解すること、そしてある程度は、その実装に関わるエレガントなエンジニアリングを評価することを目的としています。
Kernel Address Sanitizer KASANは、カーネル内に実装されたランタイムチェッカーであり、2種類のメモリ操作ミスを検出できます。境界外アクセスと解放後使用です。このツールはカーネルのビルド時に明示的に有効にする必要があります。なぜなら、それは主に2つの主要な部分に依存しているからです。
GCCまたはLLVMがAddress Sanitizerライブラリのおかげで、すべてのメモリアクセス命令の前にいくつかの追加コードを挿入します。しかし、それだけがコンポーネントではありません。この追加のインストルメンテーションは、正当なメモリアクセスと無効なアクセスを区別する方法を必要とします。これを達成するために、KASANはカーネルが使用できる全メモリを追跡するためにカーネルによって維持される、シャドウメモリと呼ばれる特別なメモリに依存しています。
KASANを有効にするのは非常に簡単です。主にCONFIG_KASANを有効にしてカーネルを再ビルドする必要があります。KASANには調整可能ないくつかのパラメータもあります。
* トレードオフに応じて、挿入されるインストルメンテーションはインラインまたはアウトラインにすることができます。
* KASANのオーバーヘッドを削減するために、ハードウェアサポート(アーキテクチャがサポートしている場合)を有効にすることができます。ソフトウェアタグやハードウェアタグなどがあります。
* どの種類のメモリを追跡するかを調整できます(vmallocメモリ、スタックメモリなど)。
KASANサポートの学習と実験を目的として、仮想化環境でテストカーネルを実行できる基本的なセットアップを確実にしたいと考えています。誤ったメモリアクセスを行う可能性があるため、ホストカーネルをクラッシュさせたくありません。このようなセットアップを起動するには複数のソリューションがあります。Andrea Righiのvirtme-ngを使用するのは間違いなく最も簡単なソリューションの1つです。KASANレポートに慣れるための非常に基本的なセットアップワークフローを以下に示します。
```
$ git clone https://git.kernel.org/pub/scm/linux/kernel/git/stable/linux.git
$ cd linux && git checkout linux-7.1.y
$ vng -b \
--configitem CONFIG_KASAN=y \
--configitem CONFIG_KUNIT=y \
--configitem CONFIG_KASAN_KUNIT_TEST=m
$ vng --user root
```
この基本的なコマンドリストで、virtme-ngに基本的なKASANサポートと対応するkunitテストを組み込んだカーネルを構成およびビルドするように求めています。最後のvngコマンドは、カーネルをビルドしたホストと同じアーキテクチャをエミュレートし、最小限のrootfsを持つQEMU環境でこのカーネルを起動します。起動後、対応するテストカーネルモジュールをロードすることで、KASANレポートを生成して調査できます。
```
# modprobe kasan_test
# dmesg
```
カーネルログバッファには、以下のような誤ったメモリアクセスに関する多数のKASANレポートが含まれています。
```
==================================================================
BUG: KASAN: slab-out-of-bounds in kmalloc_oob_right+0x705/0x7d0 [kasan_test]
Write of size 1 at addr ffff888005fa5f73 by task kunit_try_catch/249
CPU: 2 UID: 0 PID: 249 Comm: kunit_try_catch Tainted: G N 7.1.2-virtme #6 PREEMPT(lazy) Tainted: [N]=TEST Hardware name: Bochs Bochs, BIOS Bochs 01/01/2011
Call Trace:
<TASK>
dump_stack_lvl+0x4d/0x70
print_report+0x14b/0x4b0
? __pfx__raw_spin_lock_irqsave+0x10/0x10
kasan_report+0x119/0x140
? kmalloc_oob_right+0x705/0x7d0 [kasan_test]
? kmalloc_oob_right+0x705/0x7d0 [kasan_test]
kmalloc_oob_right+0x705/0x7d0 [kasan_test]
? __pfx_kmalloc_oob_right+0x10/0x10 [kasan_test]
? dequeue_task_fair+0x14d/0x6c0
? kvm_clock_get_cycles+0x18/0x30
? ktime_get_ts64+0x1ce/0x380
kunit_try_run_case+0x1a0/0x2c0
? __pfx_kunit_try_run_case+0x10/0x10
? kthread_affine_node+0x1e6/0x2d0
? __pfx_kunit_generic_run_threadfn_adapter+0x10/0x10
kunit_generic_run_threadfn_adapter+0x85/0xf0
kthread+0x329/0x410
? recalc_sigpending+0x162/0x210
? __pfx_kthread+0x10/0x10
ret_from_fork+0x289/0x580
? __pfx_ret_from_fork+0x10/0x10
? __switch_to+0x585/0xf70
? __pfx_kthread+0x10/0x10
ret_from_fork_asm+0x1a/0x30
</TASK>
Allocated by task 249:
kasan_save_stack+0x2f/0x50
kasan_save_track+0x14/0x30
__kasan_kmalloc+0x7f/0x90
kmalloc_oob_right+0xae/0x7d0 [kasan_test]
kunit_try_run_case+0x1a0/0x2c0
kunit_generic_run_threadfn_adapter+0x85/0xf0
kthread+0x329/0x410
ret_from_fork+0x289/0x580
ret_from_fork_asm+0x1a/0x30
The buggy address belongs to the object at ffff888005fa5f00 which belongs to the cache kmalloc-128 of size 128
The buggy address is located 0 bytes to the right of allocated 115-byte region [ffff888005fa5f00, ffff888005fa5f73)
The buggy address belongs to the physical page:
page: refcount:0 mapcount:0 mapping:0000000000000000 index:0x0 pfn:0x5fa4
head: order:1 mapcount:0 entire_mapcount:0 nr_pages_mapped:0 pincount:0 flags: 0x80000000000040(head|node=0|zone=1)
page_type: f5(slab)
raw: 0080000000000040 ffff888001042a00 dead000000000100 dead000000000122
raw: 0000000000000000 0000000000200020 00000000f5000000 0000000000000000
head: 0080000000000040 ffff888001042a00 dead000000000100 dead000000000122
head: 0000000000000000 0000000000200020 00000000f5000000 0000000000000000
head: 0080000000000001 ffffffffffffff81 00000000ffffffff 00000000ffffffff
head: 0000000000000000 0000000000000000 00000000ffffffff 0000000000000000
page dumped because: kasan: bad access detected
Memory state around the buggy address:
ffff888005fa5e00: 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 fc
ffff888005fa5e80: fc fc fc fc fc fc fc fc fc fc fc fc fc fc fc fc
>ffff888005fa5f00: 00 00 00 00 00 00 00 00 00 00 00 00 00 00 03 fc
^ ffff888005fa5f80: fc fc fc fc fc fc fc fc fc fc fc fc fc fc fc fc
ffff888005fa6000: fa fb fb fb fb fb fb fb fb fb fb fb fb fb fb fb
```
このレポートは、誤ったアクセスの性質について多くの情報を提供してくれます。最初の行だけで、それが動的に割り当てられたバッファに対する境界外アクセスであるという正確な性質がわかります。また、kasan_testモジュールからのkmalloc_oob_right関数によって実行されたこと、それが1バイトの書き込みであったこと、そしてアクセスアドレスが0x0xffff888005fa5f73であったこともわかります。
次に、この誤ったアクセスとプラットフォームの実行コンテキストに関する追加情報があります。発生したCPU、実行されていたタスク、カーネルバージョン、プラットフォーム名です。
これに続いて、アクセスの完全なコールスタックがあります。
最後に、メモリの種類によっては、より具体的なデータが得られる場合があります。誤ったアクセスがスラブ割り当てゾーンに対するものであるため、対応する割り当てサイズ、割り当てコールスタック、それを割り当てるために使用されたスラブキャッシュ、そして1バイトの書き込みが有効な領域の直後に行われたという事実(これは実際にこの誤ったアクセスを実行するテストコードと一致します)があります。
レポートの最後に目を向けると、アクセスされているメモリ周辺の「メモリ状態」に関する詳細が表示されます。これはアクセスされたメモリの内容ではなく、議論が必要な特定のメモリの値です。それは、アクセスされたメモリの状態を追跡するために使用されるシャドウメモリです。
KASANシャドウメモリ
KASANが有効になっている場合、カーネルはアクセス可能なメモリの全量を使用しません。代わりに、カーネル仮想アドレス空間の残りの状態を追跡するために、そのメモリの一部を予約します。この予約...