BitVisor
BitVisorDay 18

BitVisor の IOMMU の話

はじめに

ちょうど一週間前に,YsuOS さんの記事で TinyVisor (BitVisor の派生ハイパバイザ,なぜか VMが2つ動く) での IOMMU の使われ方が解説されました.

TinyVisorのIOMMUのコードを読んでみたよ - Qiita - https://qiita.com/YsuOS/items/a6deab2e92fb77be7006

TinyVisor は仮想化なしで 2 つの VM を動かすために,IOMMU を使っていました.

しかし実は,VM が 1 つしか動かない BitVisor にも IOMMU を用いた機能があります.
BitVisor では,デバイスの DMA から BitVisor 自身守るために IOMMU を使います.今日はこれについて書いてみます.

IOMMU って何?

デバイスが DMA するときにもページングっぽいことをできるようにするための機能です.
PCI-passthrough などするときに物理アドレスが DMA する先を VM の物理メモリに対応させるために使ったり使ってなかったりしたりしなかったりするみたいですよ?

詳しくは,以下のページが分かりやすいと思いますので,そちらを読んでみてください.

ハイパーバイザの作り方~ちゃんと理解する仮想化技術~ 第16回 PCIパススルーその2「VT-dの詳細」 - https://syuu1228.github.io/howto_implement_hypervisor/part16.html

IOMMU で BitVisor を守るってどういうこと?

BitVisor が正しく動作し続けるためには,ゲスト OS が BitVisor のメモリ領域を読み出したり逆に書き込んだりできないようにする必要があります.
ゲスト OS から CPU を介したメモリアクセスについては,Nested Paging の機構で止めています.

しかし,ゲスト OS がへんてこりんな設定をした PCI デバイスがあると,DMA で BitVisor のメモリ領域を読みだされたり逆に書き込まれたりする可能性があります.

これを防ぐために,BitVisor は IOMMU を使います.
ページング機構と同様,IOMMU でもマッピングがないメモリ領域にはアクセスできないため,BitVisor のメモリ領域をマッピングしないことで BitVisor を守ることができます.
ですので,セキュリティ用途では有効にしておいた方が良さそうな機能です.

BitVisor とゲスト OS とでデバイスを共有している場合は,色々面倒なことをしていそうなんですが,それは説明も面倒そうなので,BitVisor の論文読んでください.
(DMA で使うバッファーのシャドーイングとかしてるっぽいです)

BitVisor: a thin hypervisor for enforcing i/o device security - https://dl.acm.org/citation.cfm?id=1508311

ACM のアカウントとかない人はこっちから読めるかも? https://www.os.ecc.u-tokyo.ac.jp/papers/vee2009-shina.pdf

機能の使い方

make config と叩いて
VTD_TRANS Enable VT-d translation
というのを有効にします.
名前から察するに,Intel VT-d を使うものだと思います.
そういえば,BitVisor は AMD の IOMMU に対応しているんでしょうか?

動かないんだけど...

具体的には,Linux の起動途中に何らかの PCI デバイスが動いてなさそうな感じのログが出て止まってしまったりします.
(僕の場合は NVMe でした)

そんなときはパッチを当てましょう.
以下のパッチで動くはずです.
(今の最新 BitVisor に当てて試したことないので,当たらなかったり動かなかったしたらごめんなさい)
(さっさと本家にパッチ投げないとですね...)

diff --git a/drivers/iommu.c b/drivers/iommu.c
--- a/drivers/iommu.c
+++ b/drivers/iommu.c
@@ -652,8 +652,10 @@
 #ifdef VTD_TRANS

    struct acpi_drhd_u *drhd;
-   unsigned long i;
+   unsigned long i, n = 0;
    int remap, dom, ndom, f;
+   u64 base, len, end_phys = 0;
+   u32 type;

    if (!iommu_detected)
        return;
@@ -667,9 +669,14 @@
 #endif // of VTD_DEBUG

    ndom=remap_preconf();
+
+   do {
+       n = getsysmemmap (n, &base, &len, &type);
+       end_phys = (end_phys < base + len) ? (base + len - 1) : end_phys;
+   } while (n != 0);

    printf("(IOMMU) dom 0(PT Devs.) ");
-   for (i = 0; i <= 0xfffff; i++) 
+   for (i = 0; i <= (end_phys >> 12); i++) 
        dmar_map_page(dom_io[0], i, ((i>=vmm_start_inf() >> 12 && i<vmm_term_inf() >> 12) ? PERM_DMA_NO : PERM_DMA_RW));
    for (dom=1; dom<ndom ; dom++) {
        printf("%x",dom);
@@ -678,7 +685,7 @@
            printf("(%x:%x:%x) ", rem[i].bus, rem[i].df.dev_no, rem[i].df.func_no);
            break;
        }
-       for (i = 0; i <= 0xfffff; i++) { 
+       for (i = 0; i <= (end_phys >> 12); i++) { 
            f=0;
            for (remap=0; remap<num_remap ; remap++) {
                if (rem[remap].dom==dom && i>=rem[remap].phys && i<rem[remap].phys+rem[remap].num_pages) {

上記のコードでは,元々 BitVisor が 4GB までしか IOMMU のマッピングを作っていませんでした (0xfffff は 4GB 最後のページフレーム番号です).
これだと,PCI デバイスが 4GB 以降のアドレスに DMA しようとしたときに動かなくなります.
そこで,このパッチは,BIOS が返す物理アドレスの終端までのマッピングを作るようにしています(しているつもりです).

おわりに

というわけで,BitVisor での IOMMU のお話でした.

実際にゲスト OS で怪しいデバドラを作って BitVisor を壊せるか否かというところまで試せばよかったんですが,今回はそこまではやってません.