発明のための再発明

内部動作のような、プログラムを書くときの参考になることを書きます

gVisorのechoを読む

gVisorが面白そうだったので、どんな風に動いているか書きます。

使用したコミットは、

GitHub - google/gvisor at c400a0356b856e71fd30e3fe10372d7bb94356cb

です。

gVisor概要

gVisorは、先日googleが公開した、アプリケーションとホストの分離を目指したコンテナランタイムです。
システムコールをコンテナから直接実行するのではなく、システムコールをgVisorが受け取って実行を制御することで、コンテナをより安全に動かせるようにしています。

GitHubは、ここ

github.com

gVisorのコードは短くはないのですが、色々な機能を実装するために多くなっているだけで、各機能に必要なコードはそれほど多くなく、つまみ食いにはちょうど良い大きさという印象です。

動かし方

bazelでビルドして、Dockerでランタイムを指定すれば動きます。
READMEに詳しく書かれているので、簡単に動かせると思います(https://github.com/google/gVisor#building)

gVisorのechoの動き

本題の、コンテナ内でechoを実行したときに何が起きているか見ていきます。
今回は、ubuntuを起動し、その中で echo "Hello"を実行しました。
簡単に図にすると、下のように動いています。

f:id:mrasu:20180504185959p:plain

順番に見ていきます。

1. ubuntuシステムコール

echo "Hello" のコマンドを実行したところから始まります。

2. gVisorが気づく

echoが動かすWriteシステムコールにgVisorが反応するところから、gVisor仕事が始まります。
Task#run(pkg/sentry/kernel/task_run.go)という関数がループの中でシステムコールを待っています。

3. システムコールの内容をptraceで取得する

まず、gVisorがどんなシステムコールが実行されたかを知る必要が有ります。
そのために、Task#setRegsからptraceを呼び出します。
PTRACE_GETREGSを使用することで、対象プロセスのレジスタをコピーします。
これによって、gVisorはシステムコールの内容を把握できます。
例えば、Orig_raxにあるシステムコールの番号や、rdiなどにある引数などが取得できる情報です。

4. 実行するべきでないものを除外

システムコールの番号がわかったので、それを実行するべきかどうかを判定します。
Task#checkSeccompSyscallという関数が実行処理の判定をし、許可していないシステムコールを弾いています。

5. 前処理

システムコール(ここでは、Write)を実行するとなったら、まずは前処理として、.bootにログを出したりします。

6. システムコールに対応した処理を実行

gVisorはホストにシステムコールを直接投げるのではなく、gVisor自身が別のシステムコールを実行します。
どのようにシステムコールを取り扱うかは、pkg/sentry/syscalls/linux/linux64.go に色々と処理が書かれています。
ただ、まだ全部の処理が実装されているわけではなく、TODOがあったり、318までしかシステムコール候補がなかったりします。

7. Writeを実行

echoの出すWriteは当然実装されていて、pkg/sentry/syscalls/linux/sys_write.goに処理内容が書かれています。

8. ホストへWriteする処理を実行

sys_writeのwriteには共通処理が書かれているのですがechoはホストに書く処理なので、続いて個別処理としてpkg/sentry/fs/host/file.goのWriteが実行されます。

9. コマンドプロンプトに表示される。

ホスト用のWriteが動いて、ようやくコマンドプロンプトに "Hello"が表示されます。

まとめ

以上が、 echo "Hello" の流れでした。

このように、gVisorはコンテナがシステムコールを直接実行するのではなく、gVisorがホストの間に入っています。
この機能がsentryで、怪しいコンテナが変なシステムコールを実行するのをブロックしてくれます。
そのおかげで、コンテナがより安全に動かせるようにできるようになっているというわけです。