NaCl について

カーネル/VM Advent Calendar 2013 にさっき登録しました。需要の無さそうな NaCl について語ります。

https://qiita.com/advent-calendar/2013/kernelvm

NaCl はグーグルが作ったものの中で一番好きくらいに好きなものです。理由は低レイヤコンポーネント集だから。概要としては安全に実行できる(ここでいう安全はブラウザが動いてる OS 上での任意コード実行ができない、という意味) Active X というか、 C/C++ でコードが書ける Java Applet というか、まぁそういう感じの。

NaCl はおおざっぱに言って、

  • 検証可能なバイナリを出力するコンパイラツールチェイン (gcc, binutils, etc.)
  • ユーザプログラムを検証して起動する service runtime
  • service runtime と libc の間に安定した ABI を提供する integrated runtime
  • newlib と glibc から選べる libc
  • ブラウザで色んなオペレーションを実現する Pepper API とのやりとり

あたりの部品があります。以下では一つずつ見ていきます。

そのまえに用語として、 trusted code とかそのあたりが混乱を招きがちなので最初に。

  • trusted code は chrome とかのコードの一部になってるコードの大部分を指します。ホスト側とか言ったりもすると思います。
  • untrusted code はプラグイン側のコードで、何するかわからんので信用してません。この文章ではゲスト側とかも言います。まぎらわしいですが、 NaCl サイドのコードにも一部 untrusted code もあります。 nexe と言ったりします。ネクシーと発音するのがオシャレぽいです。

Software Fault Isolation

NaCl といえばこの話、という感じなので NaCl に興味を持ったことがある人は知ってる話ではあると思います。

C/C++ でコードが書けるのに安全、というのはおかしな話です。どうやってやってるかというと、コンパイラに手が入ってて、以下の二つが検証可能なバイナリしか吐けないようになっています。

  • jmp/call の飛び先が制限されている
  • メモリの読み書き先が制限されている

検証は実行バイナリを読むことによってなされています。 jmp/call の方はアドレス空間の最初の 256MB にしか飛ぶことができず、かつ 16 or 32 byte align されてるアドレスにしか飛べません。 256MB の制限の方は、直前に and 命令で 上位の bit をクリアしてない jmp/call は禁止、というルールでなされてます。

align の方も下位の bit をクリアしてないものを禁止するという方法でやってます。 align を強制する理由は、この境界をまたぐ命令を許したくないからです。許してしまうと operand jump で予想外の解釈がありえるバイナリができてしまうからです。また、いくつかのビットをクリアして call 、などはひとかたまりの naclcall 命令として認識されていて、この間に jmp するのも禁止です。そうでないとビットをクリアせずに call したりできてしまうので。

後で詳しく説明しますが、 NaCl のコード検証などを行なったりする service runtime と呼ばれる部分は同じプロセスで動いています。同じプロセスで動いてるってことは、 service runtime 内のメモリを読み書きされるとコード検証をスキップできたりしちゃってまずいので、メモリの読み書きも制限されています。この制限の仕方は x86 ではセグメントレジスタで、 x86-64 と ARM ではメモリの読み書きのたびに直前で and 命令で対象領域を制限させることによってなされています。このオーバヘッドは x86-64 と ARM では大きくて、 x86 で普通のバイナリと比べた時のオーバヘッド(平均的に 5% 強くらいとかだとされてたと思います)に対して、 x86-64 と ARM では 30% くらいのオーバヘッドがあると考えた方がいいみたいです。

読み書きできるメモリは、 x86 と ARM では先頭から 1GB 、 x86-64 では 4GB となってます。 x86-64 は実際には先頭から 4GB ではなく、とあるアドレスから 4GB というような感じで、そのとあるアドレスは常に R15 に入っていて、 R15 は変更禁止なレジスタになってます。

ちなみに x86-64 は 4GB しかメモリアドレスしか無いわけですが、それにあわせてポインタサイズも 32bit で、実体としては完全に x32 の ABI となっています。ただなぜか ELF64 を使ってしまってるんで、それだけおかしいです。なんかなおすつもりはあるみたいですけど。

あと特筆すべきこととしては NaCl バイナリの page size は 64kB です。

追記: x86 についてはそこらじゅうに解説がありますけど、 x86-64 と ARM についてはあまり解説が無いので、ドキュメントへのリンクはっときます。 ARM の定数ロードのしかたとか、結構たのしいです。

https://developer.chrome.com/native-client/reference/sandbox_internals/x86-64-sandbox

https://developer.chrome.com/native-client/reference/sandbox_internals/arm-32-bit-sandbox

Service Runtime

さっきチラっと書いた通り、よくあるサンドボックスだと supervisor は別のメモリ空間にいることが多い気がするんですが、 NaCl では同一プロセスにいます。 service runtime の仕事は

  • NaCl コンパイラコンパイルされた untrusted code を検証する
  • untrusted code をロードして実行する
  • untrusted code に NaCl system call を提供する

の3つだと思います。検証については一つ前に書いたので、ロードから。

service runtime はローダとしての機能を持っています。ローダっていうとだいたい、

  • プログラムをロードする
  • プログラムを再配置 (relocate) する
  • 適切にパラメータをセットしてプログラムを起動する
  • dlopen/dlclose に対応する

あたりが重要な仕事なわけですけど、 relocate と dlopen は service runtime はやらないです。つまりだいたい kernel がやることと同じだと思えば良いです。 ELF 読んでメモリに貼って auxv 渡しつつ起動する、と。 auxv は AT_SYSINFO に nacl_irt_query という特殊な関数ポインタを入れるだけです。 nacl_irt_query についてはまた後で。

service runtime の提供する NaCl system call は、まぁざっくり POSIX ぽいものが色々あります。 file 関係、 mmap 、 thread 、 futex 、時間関係、などです。 service runtime は二つのバイナリにリンクされてます。

片方が sel_ldr というもので、 unittest を Chrome の外で走らせる時なんかに便利なものです。もう片方は nacl_helper というもので、 Chrome の上で走らせる時に使われているものです。デフォルトでは file 関係の API とかは動かないようになっています。つながってるとサンドボックスの意味ないんで。 NACL_DANGEROUS_ENABLE_FILE_ACCESS=1 とか環境変数を指定してやると一応ファイル関係もつながるので、テスト時などは便利ではあります。

nacl_helper はホストのシステムで実行されるバイナリなんで、例えば Linux では glibc にリンクされてます。 Windows などでも POSIXAPI として WindowsAPI の一部を提供しているので、 NaCl は portable POSIX としての一面もあるということになります。

nacl_helper はホストのシステム上で動かすため、先頭 1GB のアドレスが libc などに使われてしまう環境ではハックが必要になります。 ARM なんかがそうで、そういう環境では nacl_helper_bootstrap というスタティックリンクしたバイナリが nacl_helper を起動するようになっています。 nacl_helper_bootstrap は最初に 1GB の欲しい領域を mmap で確保してから nacl_helper を exec するだけのバイナリで、 wine-preloader とかと同じ役割と持っていると言えます。

あとそうだ、うっかり NaCl のコード検証とかの仕組みが突破された場合に備えてか、 seccomp-bpf sandbox も一応やってます。

それともうひとつ、コード検証は結構遅いんで一度検証したバイナリは大丈夫だよーってキャッシュをしたりしてますな。

Integrated Runtime (IRT)

NaCl syscall は Linux syscall 同様、 libc/user code から直接呼ぶこともできるわけですけど、メモリレイアウトとかを変えるとすぐに互換性が壊れてしまうし、古いインターフェイスがイマイチだった時の変更とかもしにくい…ということで、バイナリ互換性を担保するための仕組みとして、 IRT があります。

IRT は基本的には NaCl syscall を呼ぶだけの小さな関数が大量に定義されてるだけのバイナリで、 service runtime によってロードされる untrusted なバイナリです。 untrusted code ですが、 IRT はシステムの一部として chrome と一緒に配布されてます。

IRT に入ってる関数を呼ぶには、 service runtime が libc のローダに渡している、 nacl_irt_query って関数ポインタを使う必要があります。 nacl_irt_query は文字列を受け取って関数ポインタ群を返す関数で、文字列は "nacl-irt-filename-0.1" などのようなフォーマットになっています。バージョンが入ってるので互換性に問題のある変更があった場合も安心、と。

NaCl SDK に入ってる irt_core_.nexe みたいなファイルの説明はこれでいいわけですけど、 Chrome に付属している IRT は Pepper の API を IPC で call する仕組みが入ってるので、 Chrome IPC を喋るコードとか、インターフェースで使ってる型 (skia とか) が入ってるので結構でかいです。まぁ概念は同じ。

libc (glibc or newlib)

NaCl は glibc と newlib をサポートしてます。 static link して小さいバイナリを作りたい場合は newlib 、そうでなければ glibc 、という感じで使いわけることが想定されてる気がします。

基本的にはオリジナルの libc そのまんまなわけですけど、それなりには変更が入っています。具体的には

  • Linux syscall のかわりに NaCl syscall を呼ぶ
  • glibc loader の起動のされ方が全然違うのでその対応
  • NaCl 流 thread local storage サポート

あたりが大きいのではないかと思います。昔は service runtime に futex 相当が無くて、 mutex で futex 実装したりしてたのでそこも大きかった気がしますが。

Linux kernel は普通メインプログラムを mmap してくれて、 auxv に AT_BASE や AT_PHDR なんかを通じて、ローダの情報やメインプログラムの情報を送ってくるわけですが、 service runtime は mmap せずに argv に入ってるプログラムをローダが mmap してね、ってスタンスで auxv も AT_SYSINFO に nacl_irt_query が入ってるだけなので、そのへんはまぁまぁおおきい、はず。

NaCl の thread local storage についての情報は結構詳しくここに書いてあります。

http://www.chromium.org/nativeclient/design-documents/thread-local-storage-tls-implementation

ざっくりまとめると、 NaCl じゃない場合と同じで、 TLS のためにレジスタ一個犠牲にして、そのレジスタは untrusted code からは自由に書き変えられない、ってな感じです。本当は IRT 側に必要な TLS とユーザコード側に必要な TLS とかあってややこしいわけですけど。

thread はだいたい clone と exit 相当の NaCl syscall / IRT が用意されてる感じです。違いは thread 終了時に clone の第七引数相当に futex wake が呼ばれないとか、そういう細かいの。たぶん。

NaCl glibc のバイナリは chrome に同梱されてなくて、ユーザプログラムと同時に配布さることが期待されています。このために ABI 互換性を留保するために IRT があるとも言えるし、 glibc newlib 両方が同じコード持つのがめんどくさいから IRT があるとも言えるし、まぁそんなかんじです。

Pepper API

ブラウザで NaCl 動かす場合は、そのままだと sandbox のせいで何もできないので、 Chrome IPC の口がつながっていて、そこを通じて Pepper API という関数群を呼べるようになっています。

ユーザコードから見ると、プログラム起動時から FD が一個 IPC 用に開いてて、 Pepper API 呼ぶと IRT 内のコードが sendmsg/recvmsg してブラウザとやりとりしてくれる感じ。

Pepper API 使うといろいろできます。 file 触ったり絵をかいたり、いろいろ。重要なのは JavaScript 側にメッセージを投げられる PostMessage ってやつでしょうか。

C の API は使いかたがやや難しいので、 C++ のやつを使うといいみたいです。最近は ppapi_simple という簡単なやりかたもあります。

Pepper API はまぁだいたい html5 として JavaScript ができるようにしてあることは、だいたいできるようなできないような…ってくらいの API がそろってるような気がします。たぶん。

余談もいいところですが Pepper のコードはむつかしいです。有史以来人類は IPC/RPC 難しいと言ってる気がして、進歩無いですな。 IPC って概念を説明すると強烈に簡単なんですけどね…

naclports

NaCl のデモとして色んな Unix で動くツールがポートされてるものです。言語処理系でいうと ruby python lua 、初期の頃からあった nethack だの SDL だの、あとは snes9x だの vim だのとにかく色々入ってます。

最近のやつは ppapi_simple ってやつと Pepper API 使って html5 filesystem に保存したりできる nacl_io ってのを使うのが基本ぽいです。 nacl_io は 3 つめの似た試みだと思いますね…

GDB (nacl-gdb)

GDB にロードされたバイナリを教えるのは通常ローダなわけですけど、 Chrome で動く NaCl の場合、 untrusted 側の glibc loader が知ってるパスはホスト側のシステムから見えるパスと全く一致してないんで、そのへんを解決する必要があります。

Chrome 上で動く NaCl の場合、 NMF (NaCl Manifest File) ってやつがブラウザから見える URL とバイナリの名前の対応を取ってるんで、その NMF てのを読めるようにしてあったり、 IRT も読ませたりできます。

あと、 service runtime はセキュリティ的な理由で untrusted code と違う stack で実行されてるんで、そのへんの対応も入ってるみたいです。

nacl_dyncode_create

JIT するものは動かすために、動的に生成したコードを NaCl の検証器に検証してもらう仕組みがあります。この API はかなり使い勝手が悪くて、 text 内の使うアドレスを自分で指定しないといけないので、自分のプロセスのメモリレイアウトをよく知ってる必要があります…

まぁ NaCl は text 領域の扱いがかなり適当で、 nacl-glibc も dlopen/dlclose 繰り返すだけでメモリ使い切れる素敵仕様だったりします。

デモとして、 mono と v8 がポートされてて naclports に入ってた気がするんですが、なんか両方ビルドされなくなってるような…

PNaCl

で、 PNaCl どうなん? って話があると思うんですが、あんまよく知らないんですよね…拡張入り LLVM bitcode を各 architecture の NaCl code に翻訳する、わけですよねたぶん…

まとめ

NaCl はいろいろ低レイヤなとこが入っててながめててたのしいです。難点はドキュメントが無いことです。この文章は書き散らかし文章ですけど、世界一詳しい NaCl についての概要文章なおそれすらある。

なにかあれば下記メールアドレスへ。
shinichiro.hamaji _at_ gmail.com
shinichiro.h