仙石浩明の日記

2019年9月4日

UEFI ブートでキーボードが無いと GRUB がハングするバグを修正してみた

遅ればせながら手元の PC を MBR ブートから UEFI ブートに切り替えた。 ハードディスクの最初の 512バイト MBR (Master Boot Record) を読み込んで起動するのが MBR ブート。 Windows だと 2TB 超のディスクは MBR ブートできない (Linux ならブート可能)。 2TB では手狭になってきたのが切り替えを決意した理由だが、 ついでに Linux 専用マシンも PC のファームウェアが対応しているものは UEFI ブートに切り替えた。

UEFI (Unified Extensible Firmware Interface) だと、 普通の FAT32 ボリュームにブートローダのファイルを置いておくだけなので、 わざわざ MBR を書き換えたりするより簡単だし分かりやすい。 PC が起動しなくなった、などのトラブルはよくあるが、 トラブル発生時は気が急くし時間的余裕がないことも多いので、 トラブル時の作業は簡単であればあるほど、 分かりやすければ分かりやすいほど好ましい。

UEFI ブートに切り替えて 1ヶ月ほど経ったある日、 CPU を換装するために落としていた PC の電源を入れたら GRUB のメニュー画面でフリーズしたので私も凍り付いた。 CPU の性能をむやみに上げると、マザーボードとのミスマッチが起りがち。 せっかく買った新しい CPU が問題を起こしたのかと思った。 電源ボタンを長押しして強制的に電源を落とす。

BIOS 設定を確認するためにキーボードをつないで再度電源を入れてみる。 設定に何の問題もない。 続いて GRUB を立ち上げる。問題無く立ち上がる。 Linux を起動する。問題無い。 さっきのフリーズは何だったのだろう?

この時は因果関係に気付かなかったが、 キーボードをつないでいないと GRUB 2.04 (および最新版の 2.05 も) がメニュー画面のカウントダウンでハングする (最初の秒を表示したまま止まる)。 PC を再起動するときは、たいていキーボードをつないでいるので正常に起動し、 この症状は見たことがなかった。

実はこの時も、 なぜキーボードをつないでいなかったかというと明確な理由はない。 CPU 換装後の動作確認なのだからキーボードをつないでおくべきだと思うのだけど、 たまたまつなぐのを忘れていただけかも。 Web で 「headless GRUB hang bug UEFI without keyboard」 などを検索しても似た症例がほとんど見つからないのは、 キーボードをつながずに起動させる人がほとんどいないから?

とはいえ、 何台もあるサーバそれぞれにキーボードをつないでいては邪魔である。 意図して再起動するときはキーボードをつなぐなり KVM スイッチ (Keyboards, Video monitors, Mice Switch) を切り替えてキーボードが効く状態にするのが通常だが、 トラブルや Watchdog タイマーで勝手に再起動したとき、 あるいは遠隔から再起動させたときに、 立ち上がらずにフリーズしてしまったら非常に困る。 不測の再起動に備えて、 急遽テンキーをつないでおくことにした。

GRUB がキー入力を検知する状態であれば、 画面表示の有無自体は関係なくハングする。 逆に言うと、 GRUB がキー入力を検知しなければ、 例えば grub.cfg でキー入力を読む命令が無ければ (menuentry 等が無ければ) ハングしない。 例えば grub.cfg が次のように特定の Linux を起動するだけなら、 正常に起動できる。

insmod all_video
insmod part_gpt
insmod search_label
search --no-floppy --label --set=root /
linux /boot/linuz-4.19.69-x86_64 root=LABEL=/ resume=LABEL=swap ro
initrd /boot/initz-4.19.69-x86_64
boot

もちろんこれでは GRUB を使う意味がないが、 少なくとも問題が GRUB のキー入力関連にあることが分かった。 同じ PC、同じ GRUB でも、 UEFI ブートではなく MBR ブートなら、 キーボードをつながなくてもハングしない (MBR ブート専用の USB メモリを作って確認した。末尾の「おまけ」参照)。 また、UEFI ブートであっても UEFI ファームウェアによってはハングしない場合もあると思われる (未確認)。 が、少なくとも私の PRIMERGY MX130 S2 では確実に再現する。

というわけで、 問題の所在はおおむね絞り込めたので、 GRUB のソースコードを読み始めた。 並行して facebook に、 ことの顛末を書込んだ

すると野中尚道さんから grub-core/tem/efi/console.c が怪しそう、 とのヒントを頂いた。 ありがたい! なにぶん EFI のソースコードを読むのは初めてなので、 この時点ではまだ流れを追いきれていなかった。 efi/console.c の grub_console_getkey まわりを重点的に読んでみる。

grub-core/tem/efi/console.cのget_keyで key_exとkey_conを呼び分けている処理が失敗しているような気がします。 key_exの方はオプション機能なので実装有無を確認してるのですが

なるほど確かに grub_console_getkey_ex は、 grub_efi_open_protocol の返り値が NULL で無いとき (key_ex の実装が有るとき) に限り呼び出されているが、 grub_console_getkey_con には対応するものがない。 key_con は必ず実装されているから確認不要? でも、キーボードをつないでいない場合はどうなる? key_ex の有無を確認するコード:

text_input_ex_guid = GRUB_EFI_SIMPLE_TEXT_INPUT_EX_PROTOCOL_GUID;
	...
text_input = grub_efi_open_protocol(grub_efi_system_table->console_in_handler,
				    &text_input_ex_guid,
				    GRUB_EFI_OPEN_PROTOCOL_GET_PROTOCOL);

をマネして、 key_con の有無を確認するコードをでっち上げてみる。 単に 「_EX_」 の部分を取り除いただけだが、 キーボードをつないでいるか否かを正しく検出しているようだ。

text_input_guid = GRUB_EFI_SIMPLE_TEXT_INPUT_PROTOCOL_GUID;
text_input = grub_efi_open_protocol(grub_efi_system_table->console_in_handler,
				    &text_input_guid,
				    GRUB_EFI_OPEN_PROTOCOL_GET_PROTOCOL);

あとはキーボードの有無に応じて grub_efi_console_input_init の返り値を変えるだけ。 grub_efi_console_input_init が 1 (非ゼロ?) を返せば、 GRUB は grub_console_getkey を呼ばなくなるようだ。

以上をまとめると、 次のようなパッチになった:

diff --git a/grub-core/term/efi/console.c b/grub-core/term/efi/console.c
index 4840cc5..f2be32f 100644
--- a/grub-core/term/efi/console.c
+++ b/grub-core/term/efi/console.c
@@ -207,7 +207,12 @@ grub_efi_console_input_init (struct grub_term_input *term)
 				      GRUB_EFI_OPEN_PROTOCOL_GET_PROTOCOL);
   term->data = (void *)text_input;
 
-  return 0;
+  if (text_input) return 0;
+  grub_efi_guid_t text_input_guid = GRUB_EFI_SIMPLE_TEXT_INPUT_PROTOCOL_GUID;
+  text_input = grub_efi_open_protocol(grub_efi_system_table->console_in_handler,
+				      &text_input_guid,
+				      GRUB_EFI_OPEN_PROTOCOL_GET_PROTOCOL);
+  return text_input == NULL;
 }
 
 static int

わずか 6行だが、 これで GRUB がハングしなくなった。

2019年9月5日 追記:

このブログを書いているときに、 野中尚道さんから次のパッチが送られてきた:

--- console.c.orig	2019-09-04 20:13:31.098813831 +0900
+++ console.c	2019-09-04 20:17:17.726231536 +0900
@@ -144,6 +144,9 @@
   grub_efi_status_t status;
 
   i = grub_efi_system_table->con_in;
+  if (i == NULL)
+    return GRUB_TERM_NO_KEY;
+
   status = efi_call_2 (i->read_key_stroke, i, &key);
 
   if (status != GRUB_EFI_SUCCESS)

試してみたところ、 キーボードをつながなくてもハングしなかった。 つまり GRUB ハングの正体は、NULL ポインタに対する参照、 通称 「ぬるぽ」 だったことになる。 もしこの 「con_in」ポインタが NULL になることが UEFI 的に想定外であるなら、 問題は UEFI ファームウェア側にあるといえる。

とはいえファームウェアにバグがあっても、 ある程度は GRUB 側でフォローしてもらえるとありがたい。 古い PC だとファームウェアの更新が期待できないことも多いのだから。

おまけ:

Windows 10 セットアップ用の USB メモリに Ubuntu 他を同居させる方法:

Windows 10 がトラブった時に、 セットアップ USB メモリ (インストール メディア) があると、 修復したりシステムの復元ができるので大変便利だが、 Windows 専用の USB メモリを常に持ち歩くのはメンドクサイ。

そこで、セットアップ USB メモリに GRUB と Ubuntu をインストールすれば、 Windows だけでなく Linux マシンの復旧にも役立つツールになる。 いろいろ入れても 8GB にも満たないので、 私は持っている USB メモリのほとんど全てに入れてある。 小型の USB メモリに仕込んで、 財布の中などにいれて常に持ち歩きたい (GRUB がハングした等のトラブル時に慌てなくて済む)。

まず、 Windows 10 のダウンロード ページから 「ツールを今すぐダウンロード」をクリックして、 インストール メディア (USB フラッシュ ドライブ) を作成する。

次に、 作成した USB メモリ内の 「\EFI\BOOT\BOOTX64.EFI」ファイルの名前を 「BOOTWIN.EFI」(任意の名前でも可) に変更する。 このとき USB メモリの 「ボリューム シリアル番号」 をメモしておく。

C:\Users\sengoku>dir h:
 ドライブ H のボリューム ラベルは ESD-USB です
 ボリューム シリアル番号は 8005-7BC9 です

 H:\ のディレクトリ

2019/07/09  11:41               128 AUTORUN.INF
2019/08/31  14:31    <DIR>          BOOT
	...

そして、 この USB メモリを Linux マシンへ挿入して GRUB をインストールする。 ついでに MBR にも GRUB をインストールしておくと、 UEFI に対応していない PC でも使える。

mount /dev/sde1 /mnt/usb
grub-install --boot-directory=/mnt/usb/EFI --removable /dev/sde
grub-install --target x86_64-efi --efi-directory /mnt/usb --boot-directory=/mnt/usb/EFI --removable

ここでは USB メモリのデバイス名を 「sde」 としているが、 Linux マシンへ挿入したときに割当てられる名称で読み替える。

Ubuntu など好きな Linux DVD の ISO イメージを、 USB メモリ内の適当なディレクトリへコピーする。 ついでに EFI シェル Shell_Full.efi を入れると便利。 もちろん、 USB メモリの容量の許す限り何を入れても良い。

mkdir /mnt/usb/iso
cp -a ubuntu-14.04.4-desktop-amd64.iso /mnt/usb/iso/
cp -a Shell_Full.efi /mnt/usb/EFI/BOOT/

テキストファイル 「\EFI\grub\grub.cfg」 を作成し、 以下の内容を書込む。 冒頭の 「set uuid="〜"」 には、 前述した 「ボリューム シリアル番号」 を記入する。 前述した 「BOOTWIN.EFI」 を別のファイル名にしたのであれば、 そのファイル名を 「chainloader /efi/boot/bootwin.efi」 の部分に記入する。

set uuid="8005-7BC9"
set imgdevpath="/dev/disk/by-uuid/$uuid"
insmod all_video

menuentry "[loopback] Ubuntu 14.04.4 desktop amd64" {
	set isofile="/iso/ubuntu-14.04.4-desktop-amd64.iso"
	loopback loop $isofile
	linux (loop)/casper/vmlinuz.efi boot=casper iso-scan/filename=$isofile
	initrd (loop)/casper/initrd.lz
}
menuentry "EFI Windows 10 Setup" {
        chainloader /efi/boot/bootwin.efi
}
menuentry "EFI Shell" {
        chainloader /efi/boot/Shell_Full.efi
}

Ubuntu のバージョンによって、 linux のパラメータが微妙に変わってくるので、 Ubuntu DVD 内の 「\boot\grub\grub.cfg」 を確認する。 例えば ubuntu-ja-18.04.3-desktop-amd64 の場合は次のようになる:

	linux (loop)/casper/vmlinuz boot=casper iso-scan/filename=$isofile 
	initrd (loop)/casper/initrd.lz
Filed under: システム構築・運用,プログラミングと開発環境 — hiroaki_sengoku @ 21:21

No Comments »

No comments yet.

RSS feed for comments on this post. TrackBack URL

Leave a comment