2007年3月13日

clear_fpu バグ

これは、Linux Kernel の 2.6.7 と 2.4.26で直ったバグについての説明。つまり大昔のバグの話だ。

なぜ、そんなものを、今頃になって、こんな所に、書くのか。

なぜ」は簡単だ。このバグに関する記述が綺麗にそろっている環境がなかなかないからだ。
じゃぁ、なぜ綺麗にそろっていないかと言うと、それはこの修正の発生時期が
      2004/06/14 16:05:28-03:00
だから。丁度この直後に Linux の revision control システムは bitkeeper から git に入れ替わった。そう、例の Andrew Tridgell 君による「bitkeeper プロトコル解析&クローン作成問題」がこの直後に控えているのだ。

この頃は、Linux の開発は bitkeeperで続けられると、みんな思っていた。このため、『bitkeeperで』このパッチを見つけるにはどうすればいいかを書いたページはたくさんあっても、パッチそのものを掲載しているページは皆無。もちろん例外はあるけれど、この例外が無ければ本当に見つけられなくなるところだった。

せっかく見つけたのだから、自分が探索しやすいところに、探索しやすいように置いておこう、というのが「今頃」「こんな所」に書く理由。

では、なぜこのバグを…つまり「そんなものを」の理由だが…実はこのバグ、とても「簡単」なのだ。簡単で、なおかつこのパスを通るのはレアケースだった。だからなかなか見つからなかった。かつ、致命的なバグだった。一撃でシステム全体を浮動小数点例外割り込みの無限ループに叩き込む事ができた。
他の人に「カーネルにおける致命的なバグの例」を説明する場合、これほどの好例はめったにない。だから、年に1回か2回、参照したくなる。ところが大事なときに見つからなくなる事が多いのだ。せっかくなので、記録しておこう、とそういうわけ。

では、まずはパッチの内容から。

# This is a BitKeeper generated diff -Nru style patch.
#
# ChangeSet
# 2004/06/14 16:05:28-03:00 marcelo@logos.cnet
# Alexander Nyberg/Andi/Sergey: Fix x86 "clear_cpu()" macro.
#
# Linus's 2.6 changelog:
#
# Fix x86 "clear_cpu()" macro.
#
# We need to clear all exceptions before synchronizing
# with the FPU, since we aren't ready to handle a FP
# exception here and we're getting rid of all FP state.
#
# Special thanks to Alexander Nyberg for reports and
# testing. Alternate patches by Sergey Vlasov and Andi
# Kleen, who both worked on this.
#
# Signed-off-by: Linus Torvalds
#
# include/asm-i386/i387.h
# 2004/06/14 16:04:08-03:00 marcelo@logos.cnet +1 -1
# Alexander Nyberg/Andi/Sergey: Fix x86 "clear_cpu()" macro.
#
diff -Nru a/include/asm-i386/i387.h b/include/asm-i386/i387.h
--- a/include/asm-i386/i387.h 2004-06-16 04:38:26 -07:00
+++ b/include/asm-i386/i387.h 2004-06-16 04:38:26 -07:00
@@ -34,7 +34,7 @@

#define clear_fpu( tsk ) do { \
if ( tsk->flags & PF_USEDFPU ) { \
- asm volatile("fwait"); \
+ asm volatile("fnclex ; fwait"); \
tsk->flags &= ~PF_USEDFPU; \
stts(); \
} \

ほとんどがコメントで、変更点は

asm volatile ("fwait");

という行を

asm volatile ("fnclex; fwait" );

に入れ替えた、と言う所だけ。


このバグは結構間抜けなバグだ。

そもそも、この clear_fpu() というマクロは、割込みハンドルなどの過程で浮動小数点ユニットを使っているプロセスが、コンテキストスイッチを起こしたとか、FPU例外を発生させたとか、そういう割込みハンドラーの中で使われる。

一番簡単な例は FPU例外が発生した場合だろう。浮動小数点計算をしていて、Not a Number(NaN) のような数字が出てしまった、というようにFPU例外が発生すると、
- FPU例外が発生したよんビット
がまず0から1になる。で、これが1になると、浮動小数点例外割り込みが整数演算ユニット側に発生する。これは昔、浮動小数点が 8087 という「CPUの外にあるコプロセッサ」で処理していた頃の名残だ。
一旦例外割り込みが発生すると、「例外ハンドラから制御を戻す」までは FPU例外自身も含めて、もう割り込みは発生しない。例外ハンドラから制御をもどすと、次の割り込みが発生する。この機構をちょっと念頭に置いて欲しい。

FPU例外が発生したら、何らかの処理を行う必要がある場合が多い。例えば、unix の場合は SIGFPE というシグナルを投げなくてはいけない。

しかし、FPU例外割り込みの処理自体は kernel のお仕事である。そこで、まず、この例外を受けた、という事を kernel は受け止め、その上で FPU例外が発生したよんビットを0に戻した後に、ユーザプログラムの SIGFPE が発生した場合のシグナルハンドラーを起動する、必要がある。

上記の赤字の部分を実行するのが、clear_fpu() というマクロ のはずだったのだが…

FPU例外が発生したよんビット をクリアする命令は fclex, fnclex の2種類の機械語命令だけ。
fwait は「浮動小数点ユニット」と「整数ユニット」の同期を取り直すための機械語命令。

本来であれば FPU例外が発生したよんビット をクリアする命令 を投げてから、FPUが落ち着くのを待って、ユーザプロセスへと制御を戻す作業を始めるべきなのだが、古いコードはこれをやり忘れていたという事だ。という事は、FPU例外割込みハンドラから戻ろうとしたその瞬間に、再び FPU例外が発生し(だって FPU例外がはっせいしたよんビットは 1 のままだからね)、再び割込みハンドラに戻り…を無限に繰り返す羽目に…


普通、ユーザプログラムを書く人は、FPU例外が発生しまくるような書き方はしない。ややこしい浮動小数点計算の最中に「計算がおかしかったです」と言われても、どうすればいいのやら判らないからだ。しかし、これは「浮動小数点例外が発生しない」のではなく、一瞬にしてわけが判らない状態に陥るため、なにが原因だか判らない、という事だ。多分、多くの人が、正体不明なシステムダウンを経験し、しかし「Linux だし、そんなもんさ」とか「PCが安物だから」とか思っていたのだろう。
特に、このバグは CPU 依存性がある。他の CPU で起こっていないとなれば…安物だから、と思い込んでも不思議ではない。

このため、このバグはなかなか表に出てこなかった。

一旦発生すれば、無限に割込みハンドラを実行し続けるため、もはや復旧不可能なバグでもある。迂回する方法は浮動小数点計算で例外を絶対発生させない事だけ。これは実質不可能だ。

修正ポイントは単純で、判り易い。気が付いてしまえば何という事のないバグだ。


このようなバグが、あといくつ Linux にあるのか、と聞かれても、はっきり言って判らない。判るわけがない。IA32 の浮動小数点命令の果てまでも暗記しているプログラマなど、いまどき世界中を探しても3人もいないだろう。そしてあまりにも単純であるが故に、問題があると感じる者も無く、IA32のインストラクションセットを確認し直す者も無く…ここまですり抜けてきたのだ。

こういう心理的盲目点に存在するバグは、よほどラッキーでない限り発見できない。このバグを犯した人間を、見落としてきた人間を攻める事はできない。見えないものは見えないのだから。


ここまで、判り易く、かつ被害甚大なバグは珍しい。どちらか一方と言うケースはよくあるのだが。
プログラミングの初心者にも理解できる、という意味で、このバグはとても貴重なのだ。