Native Clientの仕組みはどうなっているのか?


Googleが、Webブラウザ上でネイティブバイナリコードを実行する「Native Client」を公開した。


 Native Client
 http://code.google.com/p/nativeclient/


Google Earth,Quakeなどがすでに動いているようだ。


 これはすごい!ブラウザ上でX86バイナリを動かす「Native Client」
 http://www.moongift.jp/2008/12/native_client/


 サンドボックスで安全性確保、オーバーヘッド5%
 ブラウザでx86バイナリ実行、グーグルが新技術
 http://www.atmarkit.co.jp/news/200812/09/nacl.html


あと、サンプルのlife.ccにSDL_surfaceと書いてあるのでSDLの描画まわりが既に動いているのだろうから、今後、たくさんのアプリがNative Clientに移植されると思う。Native ClientはActiveXの再来とかなんとか方々で散々叩かれているのだが、sandboxの仕組みがあるのがActiveXとは大きな違いだろう。


Native Clientは現在のところx86のバイナリをブラウザベースで実行できる。「C/C++で書いたコードに対してどうやればsandbox環境を提供できるのか?」について興味のある人も多いと思う。


そもそもそんなことが技術的に可能なら、docomoFOMAシリーズにCPUリソースを食いまくるJavaなんて搭載しなかっただろうし、auだってBREWなんてうんこの漏る音のような採用しなかったかも知れないし、Android携帯にしてもJavaなんかではなくC/C++で書かせてくれたほうがよっぽど処理速度面においてiPhoneに対抗出来たはずだ。


さて、Native Clientはどのような仕組みでこのsandbox環境を実現しているのだろう?
本当にこのsandboxは安全なのか?ActiveXと本当に大きく違うのか?


論文をざっと追いかけていこう。


 Native Client: A Sandbox for Portable, Untrusted x86 Native Code
 http://nativeclient.googlecode.com/svn/trunk/nacl/googleclient/native_client/documentation/nacl_paper.pdf


■ Native Clientの概要


・信頼されていないx86コードを実行させるための、OSとbrowser-portableなsandboxを提供
・セキュリティを損なうことなくWebアプリの実行速度をあげる
・threadをサポート
・SSEなど拡張命令のサポート
インラインアセンブラで埋め込まれたコードもサポート
・Native Clientで動作するように新しいコンパイラや言語をサポートするのは難しくない
x86のセグメント機構を用いて単純化してオーバーヘッドの低減を達成


いまのところx86専用で、x86の動いているWindows,Linux,Mac上で動き、Native Client用のgccコンパイラコンパイルしたx86用のバイナリはOS,ブラウザに対して可搬性があるので、それぞれの環境で動く。(この意味においてNative ClientはOSっぽいと感じる)


他のCPUで動くブラウザをターゲットとするなら、そのCPU用のNative Client用のgccコンパイラを持ってきて、コンパイルしたバイナリも用意しないといけないんだろうけど、そのへんはいずれサポートしているCPUすべてのバイナリを一発で作れるようになるだろうし、バイナリなのでファイルサイズは小さく、何種類もあってもWeb Serverにはそんなに負担はかけないだろう。


■ コード検証の仕組み


Native ClientがJavaScriptとどう通信するのかだとかそのへんには全く興味がなくて、ただ単にx86のコードをどう静的に解析するのかのみが私の興味の対象である。


この仕組みをここで書いておこう。


・到達可能なアドレスをすべて検証する。
・コードの実行時の書き換えを禁止する。


大事なのはこの二つ。到達可能なコードの集合が得られれば、あとはそこで不正な命令が使われていないかをチェックすれば良い。


ここで言う不正な命令とは
・hlt
・syscall,int
・Ring0用の命令
・segment状態を変更するlds,far call
・ret(間接ジャンプが実現出来てしまうのでこれは使用しない)
である。


あと、x86のprefixをつけた記法も有用な命令と知られているものだけに制限する。


call , jmpで、アドレスは直接指定されているならその到達先は静的に解析できるので、事前に到達可能なコードをすべて列挙できる。ただし、call , jmpによる間接ジャンプは、ひと工夫必要だ。


このへんを含めて具体的に見ていこう。


まず、Native Cleint用のバイナリには次の7つの制約を課す。


制約1) いったんメモリにロードされたら、バイナリは書き換え不能とする。
これには、OSレベルのメモリ保護機構を利用する。(WindowsならVirtualAllocですな)
制約2) バイナリは静的にリンクされ、そのスタートアドレスは0であり、その最初のtext領域(訳注:実行するコードを書けるところ)は64Kバイトである。
制約3)すべての間接ジャンプはnacljmpという疑似命令を用いる。
制約4) バイナリは、次のpage(訳注 : x86のpage frameのこと。x86ではdefaultでは1page = 4KB)まで1つ以上のhlt命令(0xf4)が詰められる。
制約5) バイナリは、32byte境界をまたぐx86命令や、Native Clientの疑似命令を含まないこと。
制約6) すべての有効な命令が配置されているアドレスは、バイナリのベースアドレス(0)からfall throughによって到達可能であること。
制約7) すべての直接ジャンプはジャンプ先が有効な命令群でなければならない。


だいたいの仕組みが見えてきた。コードの書き換えは、code page自体を書き換え不能にしておくことで実現している。


上の制約にはないが、もちろん、
・例外は、Native Clientのservice runtimeでcatch
する。


それから、データ保護という観点から、
・data sandbox外のデータを読み書きできてはならない
と言える。


これを実現するには、80386のsegmentを用いている。これにより仮想32bitアドレス空間で連続した範囲(contiguous subrange)に対するデータアクセスを制限できる。sandboxのためのload/store命令が不要になるし、いい実装だ。(ただ、32bitというアドレス制限はある。Native Clientは64bitアドレス環境はサポートしないのでいいのだろう。)


あと、呼び出しが許されるAPIは、Native Clientがouter sandboxとして仮想化(wrap?)しているAPI群のみである…と思うのだが、そのへんは私は全く興味がないので詳しく見ていない。(ごめん)


最大の関心事である(?)制約3のnacljmp疑似命令がどういうコードに変換されるか見てみよう。


命令の途中(2バイト命令の2バイト目とか)に間接ジャンプされると困るので、制約3で書いたように間接ジャンプはnacljmp疑似命令を用いるが、これは、次の2つの命令(5バイト)に変換される。

and eax,0xffffffe0
jmp [eax]

すなわち、32バイト境界にしかジャンプ出来なくしてしまう。制約5により、32バイト境界をまたぐ命令は存在しないので、これにより、予期しない命令が実行される心配はない。


どうせ間接ジャンプが必要になるのは、switch〜caseなどのテーブルジャンプであって、その場合は、ジャンプ先(caseラベル)の最後にbreakしてswitchを抜けるのが普通だから、32バイトにalignするためにnopかhaltかをpaddingされているのは痛くはないということだろう。


あと、このsandbox validator(検証器)の実行速度は、
・approximately 30MB/second (35.7 MB in 1.2 seconds, measured on a MacBook Pro with MacOS 10.5, 2.4GHz Core 2 Duo CPU
とある。実行コードが30MBもあるバイナリなんてそうそうないから、どんなものでも1秒以内で終わると見ていいのだろう。


■ まとめ


Native Clientの仕組みはすこぶる単純だった。
見た通り、仮想化に取り組んでいる人には当たり前のテクニックだろう。


だけど、これをGoogleがやったという意義は大きいと思う。納品先からActiveX禁止令を通達されている人も「Native ClientならGoogleが作ってるし、100%安全ですよ」とかなんとかうまいことを言って、Native Clientで押し通せてしまうかも知れない。


しかし実際のところ、上で見たように、Native Clientがバイナリを配置するときにやっている変換は、nacljmp疑似命令を置換するという単純なものなので、このバイナリの実行時のクロック数などを事前に計算できなくはないので、サイドチャネル攻撃に使える気は少しする。


あと外部APIの呼び出しにセキュリティホールがあると不正なパラメータを渡して故意に不正なコードを実行出来そうな気はしなくもない。まあ、Native Clientが広く使われればすぐにそんな穴は塞がれるんだろうけども。


まあ、それにしてもActiveXとは比較にならないほど安全と言えるだろう。


それから、ローカルなstorageにアクセスするAPIは…たぶん提供されないと思うので、「Googleデフラグなどデスクトップアプリをブラウザベースで実現しようとしている」は、たぶん正しくない。どんなAPIが提供されるのか、もう少しNative Clientのバージョンがあがったら改めて調べたい。


プログラムにこういう制約を与えて、あらゆるプログラムをRing0で動かせばRingを切り替えるオーバーヘッドが無くなるのになぁ…。って、それがMSのSingularityなのかな?よくは知らんけど。


■ 追記(2008/12/11 15:00)


「スタックフレームを書き換えてretすると間接ジャンプが実現できてしまうじゃん」という突っ込みをいろいろいただいているんですが、風邪ぎみでぼーっとしてたんで書き忘れていました。(すみません)


これは、以下のような仕組みになっています。


・retも結局は間接ジャンプなので、間接ジャンプ命令扱いをする。


すなわち、戻り先は、32バイト境界でなくてはならないです。こうしておけば命令の途中のバイトにリターンしないことを保証できます。よって、call命令のあとは次の32バイト境界まではnopなどをpaddingするか、あるいは、call命令自体やめて、次の32バイト境界のアドレスをスタックに積んでjmpするコードになっているのではないかと思います。で、retの代わりにnacljmpがコンパイラで生成されているということかな?(未確認)


■ 参考リンク


 ブラウザで X86マシン語を動かす! Google 謹製 Native Client をさっそく試してみる
 http://d.hatena.ne.jp/amachang/20081209/1228804423


 Native Client
 http://blog.deadbeaf.org/2008/12/09/google-native-client/


 Native Client必死こいて訳した1
 http://d.hatena.ne.jp/ranha/20081210