対象 | 2.6.30, 2.6.30.1 |
---|---|
真の原因 | null pointer check をしていないコード & gcc optimization |
修正コード | 2.6.30.2 |
demo | http://www.youtube.com/watch?v=UdkpJ13e6Z0 |
解説しているページ | |
原因となっているパッチ | http://mirror.celinuxforum.org/gitstat/commit-detail.php?commit=33dccbb050bbe35b88ca8cf1228dcf3e4d4b3554 |
問題はこの部分。
@@ -461,7 +476,8 @@ static unsigned int tun_chr_poll(struct file *file, poll_table * wait)
{
struct tun_file *tfile = file->private_data;
struct tun_struct *tun = __tun_get(tfile);
- unsigned int mask = POLLOUT | POLLWRNORM;
+ struct sock *sk = tun->sk;
+ unsigned int mask = 0;
if (!tun)
return POLLERR;
まず、前提として理解しておくべきことは、C言語においては NULL Pointer へのアクセスの結果は「未定義」だということ。なのでコード的には何が起こっても不思議はない。当然、「未定義」以降に実施されるコードも未定義になる。
次に理解するべきことは、「未定義とはいえ、大抵のgccを利用する環境では NULL ポインタへのアクセスは SIGSEGV などを発生させる」事。つまり NULL ポインタアクセスを行えば、その段階でプログラムは停止するので、そこを通過したコードが持っているポインタは NULL ではない、と仮定して構わない事。どうせ、NULL だったら未定義なんだから何しても構わないし。結果として出力されるコードは論理的には必要な条件をすべて満たすが結果として安全サイドに倒れたコードにはならない。
以上のことを念頭において、先ほどのパッチを見る。
+ struct sock *sk = tun->sk;
このコードは tun が NULL だった場合、ユーザープログラムであれば SIGSEGV を発生させる。しかし、 kernel の場合はそのような割込の類は発生しない(正確にはしなかった)。
gcc は、「tun->sk を通過できたコードは、tun が NULL ではない、という保証が得られた、と言うことだ。その後 tun は変更されていない」と考えつつ、次のコードを見る。
if (!tun)
return POLLERR;
で、続けてこう考えるわけだ。
「ふむ。ここまで来た段階で、tun != NULL は真であり、それ以降変更されていない。ならば
if (!tun) は常に偽だ。」
結果として上記のコードは全部なくなる。
結果として全体はこうなる:
static unsigned int tun_chr_poll(struct file *file, poll_table * wait)
{
struct tun_file *tfile = file->private_data;
struct tun_struct *tun = __tun_get(tfile);
struct sock *sk = tun->sk;
unsigned int mask = 0;
poll_wait(file, &tun->read_wait, wait);
:
POLLERR を返すパスはなくなり、エラーチェックが消失する。
エラーチェックが無くなれば、今度はこっちの問題を使って NULL pointer 近くのページに予め mmap しておいたコードを実行できてしまう。なにしろ、ユーザー空間は全て kernel から参照、実行可能だから。
- http://blog.cr0.org/2009/06/bypassing-linux-null-pointer.html
- http://git.kernel.org/?p=linux/kernel/git/torvalds/linux-2.6.git;a=commitdiff;h=f9fabcb58a6d26d6efde842d1703ac7cfa9427b6
ちなみに、2.6.18 base のはずの RHEL5.4 beta にはこれらのバグが存在した(NULL pointer check 関係の最適化がバックポートされた)が、それぞれ修正されているそうだ。