背景
皆さんご存知の通りBitVisorにはcpu_interpreter (core/cpu_interpreter.c) というのがあって、これには主にMMIOのフックの際に機械語命令を解釈し実行する役目があります。MMIO以外に古いIntel CPU向けにreal-address mode関連の用途にも使われていますが、基本的にそれだけなので非常に限られた機械語命令しか対応していません。
BitVisor / core/cpu_interpreter.c Commit Log
上のhistoryを見ていただくとわかるように、バージョン0.3 (2008年6月)で64ビット対応が入ったあと、1.3 (2012年9月)の頃までは大きめの変更がありますが、その後はほとんど変更がありませんでした。対応している命令数が少ないので、致命的なバグが見つかることもないのかな、と思っていたところです。
バグ
今回見つかったのがこれです:
BitVisor / Code / Commit [341151]
movabs命令に64ビットオフセットを指定する... これは、Intel表記ではMOV命令になりますが、64ビットのオフセットアドレスを指定できるのはアキュームレーターとの間のこの命令だけです。具体的に、アドレス0x1234567890から1バイト読み取り、1を足してアドレス0x1234567891に書き込むというコードは以下のような形になります:
0: a0 90 78 56 34 12 00 movabs 0x1234567890,%al
7: 00 00
9: fe c0 inc %al
b: a2 91 78 56 34 12 00 movabs %al,0x1234567891
12: 00 00
参考までにIntel表記も載せておきます:
0: a0 90 78 56 34 12 00 movabs al,ds:0x1234567890
7: 00 00
9: fe c0 inc al
b: a2 91 78 56 34 12 00 movabs ds:0x1234567891,al
12: 00 00
バグの内容としては、この2命令でいうと12 00 00 00
の4バイトを読み取らずに、下位32ビットのアドレスに対して実行してしまい、プログラムカウンター (%rip) が12
のところまでしか進まない、ということになります。
問題があったopcodeは0xa0〜0xa3の4種類です。
どうして今まで発覚しなかったか?
このopcodeは8086の頃から存在していて、32ビット対応の頃までは今までの実装で問題ありませんでした。動作モードが64ビットだったとしても、address size prefix (opcode 0x67)を用いて32ビットアドレス指定に変更されている場合は、以下のようにmov命令となり問題ありません:
0: 67 a0 78 56 34 12 addr32 mov 0x12345678,%al
6: fe c0 inc %al
8: 67 a2 79 56 34 12 addr32 mov %al,0x12345679
また、そもそもMMIOの用途以外であればそのまま実行されるので問題ありません。例えばこの命令でグローバル変数にアクセスするプログラムがあったとしても、グローバル変数がMMIOということはまずないので、CPUによってそのまま実行されるだけです。さらに、64ビットモードでは多くの場合プログラムカウンター (%rip) 相対のアドレス指定が使われていて、64ビットのオフセットアドレス指定が使われることは多くありません。
どうして発覚したか?
64ビットオフセットアドレス指定でMMIOを行うプログラムがあったからです。具体的には、ファームウェアによるPCI configuration spaceへのアクセスでした。PCI Expressではmemory mappedによるアクセスが提供されていて、BitVisorのMMIOの処理対象となります。
様々な機種で動作する一般的なオペレーティングシステムは、PCI configuration spaceのアクセスに使われるmemory mappedの領域が機種によって異なるため、ACPIのMCFGというテーブルから情報を得て、そこに書かれたアドレスを元にアクセスします。また、PCI Expressバスのどのアドレスにどのデバイスが接続されているかも実行時にわかることになります。そうすると、あえて特定のオフセットアドレスでアクセスできるようにセグメントベースやページテーブルを構成するか、または、動的コード生成でもやっていない限り、ビルド時にアドレスが決まりませんので、64ビットオフセットアドレス指定にはなりません。
しかし、ファームウェアは特定の機種用に作られていますので、どこにそのmemory mappedの領域があるのか、どのアドレスにどんな内蔵デバイスが接続されているのか、がわかっています。そのため64ビットオフセットアドレス指定が使われたようです。
具体的には、0xc0000000からがそのmemory mappedの領域になっている機種で、0xc00000??あたりのアドレスへの書き込みアクセスがありました。??を40とすると以下のようになります:
0: a3 40 00 00 c0 00 00 movabs %eax,0xc0000040
7: 00 00
今回アドレスの上位32ビットが0なので、命令そのものは正しく実行されたように見えました。ところがこの命令に続いて実行されるのが00 00 00 00
になってしまったわけです:
5: 00 00 add %al,(%rax)
7: 00 00 add %al,(%rax)
%raxが指すアドレスへの読み書き... いかにもやばい感じがしますが、UEFIファームウェアは仮想アドレスと物理アドレスが等しい設定で動作しているため、一見何も起きずに実行されてしまう場合もよくあります。今回は、たまたま%raxが指すアドレスがVMMの範囲内となりpanicになったために発覚しました。
他に問題はないのか?
ないと思いたいところですが、この記事を書いていたら気になるところが出てきました...
0: 89 04 25 40 00 00 c0 mov %eax,0xffffffffc0000040
7: 67 a3 40 00 00 c0 addr32 mov %eax,0xc0000040
下の命令はいいはずなんですが、上の命令ですね、符号拡張、やっていたかな... 怪しいぞ...
テスト用パッチ:
diff -r 462ec4cc01ed core/mm.c
--- a/core/mm.c Fri Dec 24 17:41:59 2021 +0900
+++ b/core/mm.c Sat Dec 25 13:51:57 2021 +0900
@@ -1000,6 +1000,18 @@
process_virt_to_phys_prepare ();
if (uefi_booted)
get_map_uefi_mmio ();
+ ulong cr3;
+ asm_rdcr3 (&cr3);
+ pmap_t m;
+ pmap_open_vmm (&m, cr3, PMAP_LEVELS);
+ pmap_seek (&m, 0xFFFFFFFFFFFFF000, 1);
+ pmap_autoalloc (&m);
+ pmap_write (&m, 0xFEE00000 | PTE_P_BIT | PTE_PCD_BIT | PTE_PWT_BIT, PTE_P_BIT | PTE_PCD_BIT | PTE_PWT_BIT);
+ pmap_close (&m);
+ asm_wrcr3 (cr3);
+ u32 t;
+ asm ("mov 0xfffffffffffff030,%0":"=r"(t));
+ printf ("LAPIC Ver %x\n", t);
}
/* panicmem is reserved memory for panic */
これを入れたBitVisorをBitVisor上で実行してみたところちゃんと出ましたので、符号拡張はしていたようです