ゴルフ場のなかみ

最近ゴルフ場を新しいマシンに引越そうとしていて、ついでなのでシステムをもうちょっと丁寧にパッケージ化しようとしてます。そのついでとして、現在のゴルフ場について内部がどうなってるか、ということを少しまとめてみようと思いました。

結構似たようなことをするサービスもあるんですが(codepadとかllevalとか)、そのへんのコードとかは全く参考にしてないので、そういうのを見た方がいいかもしれませんし、あとゴルフ場固有の事情も色々あったりするかもしれません。まぁでも日本語でそのへん書いてるのはあんまり見たことがないので、多少参考になる部分もあるかもしれません。

今作業中のコードは github に入れていっています。 apt で入らないパッケージの処理以外はだいたい入ってるはずですが、まだ足りないものとかあるかもしれません。

http://github.com/shinh/ags

システム自体は、今は Ubuntu で、次は Debian squeeze (+lenny and sid? packages) にしようと思っています。

フロントエンド

http://github.com/shinh/ags/tree/master/fe/

フロントエンド(コードを受け取ったり記録を保持する側)は lighttpd + Ruby + FastCGI の HTTP サーバがやっています。この構成は単なるその時の気分なので、別に Apache でも良かったと思います。 Apache は他に使っていたので分けたかったというのも少しありますけど。

フロントエンド側に入れた Debian パッケージは pkgs.txt にあって、 conf ディレクトリ以下にいじった主だったファイルが入ってます。 lighttpd の設定は fe/conf/lighttpd 以下です。

フロントエンド - FCGI

たいへん適当な Ruby スクリプト群です。 fe/fcgi の下にあります。

DB は RDB があまり好きくないし得意じゃないのでいつもの使ってる PStore です。正直 RDB の方が良かったかも。 DB の構造は色々拡張を重ねているため大変ひどくなってしまった。

工夫した点としては FCGI だと ファイル更新しても読み直してくれないので、自前でファイルのタイムスタンプ調べてアップデートされてたら読み直しとかしてます。そのへんは index.fcgi に。

あと言語別ランキングの集計処理で、普通に cron でやればいいところをアクセスがあった時にしばらくアクセスされてないなら DB の作りなおし、みたいな CGI みたいなことをやってたりします。なんか cron はいつかは止まって、忘れられるという俺経験則があるので、そういう理由でそうなってます。どうせたいして遅くないですし。

フロントエンド - 認証

私はパスワードをユーザに打ち込んでもらうのが嫌いなため、名前は完全に自己申告、問題の変更、削除、投稿したコードの削除などは全て不可能というひどいことになっています。これはまぁ捨てパスワード打ってもらうくらいのことはして良かったような気がしますし、問題投稿者は OpenID とかその愉快な仲間達のあたりで認証するくらいはした方が良かったと思います…現状、問題の修正はミスったと思った出題者が私に連絡してきて、私が手動で対応するような感じになっています。

あと、スパマーが問題を投稿するということもありました。デカくないサイトではこの問題の対処は非常に簡単で、問題を投稿する時にとても簡単な問題を解かせるなぞなぞ CAPTCHA で対応しました。世のブログサイトはブログ主に自由に問題と答えを設定させてくれるような機能をつけてくれればいいのになぁとよく思います。

フロントエンド - コードの保持

当初は投稿されたコードは一切保持してませんでした。当時は codegolf.com で真剣勝負をしていたこともあって、隠したい tips がある人は投稿をためらうことがあるかなぁとか思ったりしたのと、私だけチートが可能という可能性自体が大変気にいらないからでした。

この問題は締め切り機能をつけて、締め切り以降はコードを公開する設定をつけることによって消え失せました。どうせいずれはコードが公開されるので、あまり見る意味が無いのです。あとそもそも shinh さんが2位に並びまくってたらコード見てるのバレバレです。

コードを保存することによって、チートの手動判定が大変しやすくなりました。当たり前です。コード公開されてないチートらしきものに関しては、本気で判定が難しいです。例えば Ruby の Hello, world! で 12B を出す方法は本気で今でも知りません。 17B は eban さんに教えてもらったか自分で気付いたかと思います。 kurimura さん今度 14B 教えてください。

あと余談もいいところですが、コードが公開されてみると、実際に知らないテクニックが使われてることなんてほとんどありませんでした。ゴルフって基本テクニック把握すれば誰にでもハイスコアが取れるんじゃ、と聞いてくる人がたまにいるのですが、そんなことは全然なくて、問題ごとに全然別なパズルを解いてる感じです。そいう意味でコード公開が無い問題でもコード保存しても良かったのかもしれません。まぁこの話はまた詳しく書きたいと思っています。

バックエンド - ネットワークの遮断

バックエンドは Xen で動かしてます。3つくらい理由があって、

  • 何があってもインターネットへのアクセスだけは許したくない。 tinyurl で容易にチートできるというのもあるのですが、それよりも人様に迷惑がかかる恐れがあるので。バックエンドはフロントエンドだけとやりとりできるネットワークの中にいるようにしています。そういえばこのへんの設定はまだ github に入ってないです(TODO)。
  • フロントエンド側にはあまり自由に見られたくないファイルも入っていた。
  • 他人のコードを実行する以上、 fork bomb とかその手のリソース喰いまくりなプロセスによって実行サーバは死ぬ恐れがある。よって調子が悪ければソフトウェアでバックエンドサーバを自動リスタートできるのが嬉しい。そういえば restart するコードも github に入れてないです(TODO)。つっても telnet してみて反応無ければ有無を言わせず xm destroy && xm create してるだけですが。

特に最初の理由から、なんらかの手段でインターネットで隔離された場所でコードを実行するのはやった方がいいんじゃないかなぁと思います。サンドボックスに自信があればそのへん殺すだけでも良いと思いますが。

あとネットワークの遮断はライブラリを完全に殺すことによっても達成できなくはないと思います。例えば codegolf.com はそうなってます。デメリットは複数言語サポートがとてつもなくめんどいことと、インタープリタにバグがあってヘンなことができたら…というパラノイアな恐怖が依然として残ることかと。

(でなんか他のを見てて気付いたんですが、えーと lleval 普通に network アクセス許してるじゃん…余裕で踏み台に使えちゃうよ… ptrace sandbox あるんだからそこは殺さんとなんじゃ… codepad はちゃんと殺してた。追記: あーでもこのへんはアクセスログさえちゃんと残ってりゃ最初に悪いことをする子があらわれるまでは性善説運用、ってのは全然悪いポリシーではないなーと思った。現実的にそんなことする子あんまりいなさそうだし)

バックエンド - Xen の設定

さて、その Xenxen-tools でお手軽にイメージを作りました。 fe/conf/etc/xen-tools にその設定があります。これは LVM 使ってるので、ファイルでやりたければ適当に書き換えるといいと思います。

下記のような簡単なコマンドで適当にイメージ作って Debian をインストールしてくれます。

xen-create-image --config ux.conf --hostname ux

Ubuntu の時はこれでできたイメージがそのまま使えたのですが、なんか Xen インスタンスssh でつなげなかったです。そのために生成された /etc/xen/ux.cfg を少しいじっていて、

extra='xencons=tty'

を加えるとうまくいくようでした。情報源は

http://ken-etsu-tech.blogspot.com/2007/10/xendell-remote-access-controller.html

あと、 xen-hypervisor-3.4-i386 だとうまくコンソールが出なくて、調べるとこっちのバージョンはなんか問題起きがちみたいな記述をちらほら見たので、 3.2-1 の方を入れました。そのへんについては下記が特に詳しかったです。

http://wiki.xensource.com/xenwiki/XenDom0Kernels

バックエンド - 実行サーバ

フロントエンドからコードを実行せよという要求が来ると、 be/srv/testsrv.rb が応対します。

このスクリプトも長年のゴミの集大成という感じです。色々思い出せるポイントを書きます。

  • 子プロセスの標準入力、標準出力、標準エラー、ステータスコード、の全てが知りたいので、 open4 を使っています。
  • daemon として動くモードもあるのですが、なんかスクリプトが例外吐いたりすると生き返す手間をかけるのがだるいので、外部から100回このスクリプトを呼ぶような感じになっています。ある程度安定してからはこのスクリプトが異常終了することは無いように思います。
  • タイムアウト待ちしつつ標準出力を読むのは Unix の基礎って感じですけど、以外とうざい作業です。単に sleep(timeout_sec) では子プロセスの標準出力がたくさんあった時に子プロセスが write でたくさん待っちゃうからです。これを防ぐには時々適当に sysread で読んでバッファをカラにしてやる必要があります。
    • ってあるえーこれ標準エラーはいじってませんねぇこれはよくない(TODO)。つか似たようなことを WebKit のスクリプトに対してやったなー と思い出しました。まぁ stderr 過剰に吐くようなお行儀の悪い子は無視してもいいのかもしれない。
  • タイムアウトした場合、その子の子プロセスは皆殺しにします。ここにゴルフ場の known issue の一つがありまして、なんと daemon を作ることを許してしまっています。このせいで負荷を意図的に増やしたり、答えを送りつけるサーバを上げたりすることができちゃいます。このへんは子プロセスの fork の返り値を調べておいて殺してやるべきです…(TODO)
  • タイムアウトの時間はこっちで拡張子ごとに切り替えています。これはあきらかにフロントエンド側が指定してやるべきものなので、ダメダメです…(TODO)
  • 時間の計測は実時間でやっています。 CPU 時間でも良かったかもしれませんが、なんか遅いシステムコールを連打してるようなコードはやはり遅いと言っていいのではないかとか適当な理由で実時間にしたんだと思います。どうせタイムアウトは実時間でやる必要があるので(終わらないシステムコールを実行されるとうざい)、表示される実行時間とタイムアウトにズレがあるとユーザが混乱する、というもっともらしい理由も今思いつきました。いや、実装した当時もそう考えたかもしれないけど、もはや覚えてない。実行時間を競うような必要があるなら CPU 時間の方が良いと思います。
  • 最後に途中に使用したファイルや子プロセスが作ったファイルを一掃します。このへんは後で詳しく。

バックエンド - sandbox

ゴルフ場の場合 sandbox にやや特殊な事情があります。

  • 大量の言語をサポートしたくて、いくつかの言語は fork や exec をしたがる(そもそも bash がサポートされてるのを思い出してあげてください)ので、これらで無条件で殺してやってはいけない。
  • 大量の言語をサポートしたくて、いくつかの言語はファイルを書き込みたがるので、ファイル書き込みを許してやらないといけない。
  • しかしファイルを残しておかれると答えを保存しておけてしまう。
  • ただし、上記のようなものは Xen の中で動いてるため、許してやってもさしたる害はない。せいぜいゴルフ場が動かんくなる程度。

これらからあまり sandbox と一般的に言うものとはやや違う物体になっていて、 libc の関数を LD_PRELOAD でフックすることでこれらを中途半端に実現しています。これらの実装は be/srv/watch.c と be/srv/local/limit (このファイル名は ulimit してただけだった時代の名残りなので変える…(TODO)) にあります。

watch.c では、他プログラムを呼ぶ関数と、ファイルを開く関数をフックしていて、それらの実行は許すものの、記録をするようになっています。ってわあこれはひどいそのログ削除するだけで exec を自在にできちゃうじゃないですか。これは適当に修正したけど依然としてファイル残せちゃうのでひどい。これはなんらかの手段で(たぶん pipe )親プロセスに直接報告すべきだな…(TODO)

ファイルに関してはもう一つ known issue があって、ディレクトリやら名前つきパイプ、 Unix domain socket に関しては放置しているのでいずれにせよ答えの入ったファイル名を残すようなことができちゃったりします。(TODO)

libc をフックする方法にはまだ問題があって、そもそもフックすべき関数のリストが全然わからん、というものがあります。これは意外と大変で、 open だけじゃなくて __open も __open64 も…などと無数に止めないといけないものがあります。適当にカンでリストアップしてますが、やはりシステムコールをフックしてやる方法の方が良さそうに思います。

さらにもうひとつ根本的な問題として、自力でシステムコール呼び出しされちゃうとどうしようもない、というものもあります。

これだけ欠点のある libc フックを使ってるのは何故かというと、最初 ptrace(2) ベースでやっていたのですが、「俺のクソ古いマシンでやっても余裕で終わるスクリプトがお前のとこではタイムアウトする。お前のマシンは PentiumII とかなのかボケ」というような文句が大量にあったからです。まぁここが甘くてもたいして名誉の得られないゴルフ場で好成績をおさめるために、しかもさらに最近はコード公開される問題が主流になっている中そこまで頑張るヤツはあんまりいないだろーという性善説に基づいてる面もあります。

バックエンド - 頭によぎったその他の sandbox

私の知ってる sandbox 的なのを実現するものを適当にリストアップしておきます。

  • libc フック。上記の通り欠点がいっぱい。メリットは比較的簡単にたいていの場合でOKなものが実現できること。
  • ptrace(2) 。完璧。ただし遅い。
  • SELinux と愉快な仲間達。よくわかってないのですが、システムコールごとにポリシーを設定するんじゃなくて機能ごとにポリシーを設定するというのは良いように思います。ただゴルフ場の場合は exec やファイル書き込みは許してやって、後から事後処理でどうこうする必要があるので、これらで実現できるかは知りません。
  • バイナリ書き換え。とても大変。これをやってくれそうなものとしては過去に jockeyseccompsandbox などが目に入りましたが、よく調べてないのでよくわかっていません。
  • 検証可能なバイナリを使う。 Native ClientVX32 がそれだと理解しています。大量の言語をサポートするのに全バイナリをこれらでビルドしなおすのはあまりにダルいので、フル Native Client な distribution でも存在しない限り現実的じゃないです。あとそもそも JIT する系の言語がどうしようもない。
  • 言語ごとの方法で危険な機能を封印してから走らせる。 codegolf.com が取っている手段ですが、大量の言語サポートをするのは全く現実的じゃないです。あと C 、アセンブラ機械語などのサポートは完全に不可能。
  • utrace なんかの最近の better ptrace の方向性はあまりわかってないのですが、このへんでいいのがあればそれはいいものかも(トートロジー)。

個人的にはゴルフ場はいつかはバイナリ書き換え系の sandbox にできればいいなぁと思っています。それが無理なら性善説運用でもいいかなぁと。あとは kernel いじっちゃうのも手ではあるかも。

なんか他にもあった気もするので思い出したら追記します。

バックエンド - 大量の言語サポート

バックエンドには大量の言語処理系をインストールすることになります。面倒なので apt で入るものは apt で入れる方針にしています。ここで少し面倒なのはバックエンドはインターネットにつなげない状態なので、インストール作業の最中はネットワークを開いて、ゴルフ場の実行は禁止することになっています。本当は debian の apt 取ってくることだけ許した proxy を立てるとか色々方法はありそうですが、コストに見合わんのでやっていません。(TODO)

apt で入らないものに関しては、適当にパッケージをフロントエンド側から送り込んでセットアップします。これらは完全に手動でやっていたのでどうやってインストールしたのやら、ヘタをするとどっから取ってきたのかすらさっぱりわからん言語処理系がインストールされてたりもします。これは色々大変なので今回ある程度自動化できればなぁ…と思っています。(TODO)

さて、インストールができても実行のしかたがさっぱりわからない言語処理系があったりします。例えば ghostscript はデフォルトでは実行すると copyright を標準出力に吐きやがる上に、スクリプトを終了した後にプロンプトを出して停止しやがります。これらを抑制する手段を調べるのは結構手間なのですが、大抵はその言語をリクエストしてくれた方に方法を調べてもらっている感じです(ありがとうございます)。その長年の努力の結晶は be/srv/s に残っているので、参考になるものもあるかもしれません。未だにわからんものも結構あったりします。例えば Arc

たまに、お前の言語処理系はどっから持ってきたバージョンいくつのヤツなんだ! と聞かれることがあります。ということから入手元とバージョン情報は記録しておいた方がいいと思われます。ゴルフ場は不完全な情報はあるのですがなにぶん不完全なので救いようがありません。(TODO)

フロントエンドとバックエンドのやりとり

ゴルフ場最悪の部分です。あまりに適当なプロトコルなので、拡張性が全くありません。(TODO)

ここがもうちょっと綺麗なら入出力をランダム生成するスクリプトをユーザが投稿したり、 Quine のような特殊な問題をユーザに出してもらうような拡張も考えられたのですが。

ゴルフ場では TCP でよくわからんプロトコルで話しているのですが、ここは別に HTTP とかでもいいと思います。ただ一点バックエンドのサーバは複数のリクエストを同時にさばけるようになってない方が良いと思います。他のプロセスに妨害されてタイムアウトしちゃうとかわいそうだからです。

IRCtwitterRSS

大昔に書いた、 mircbot という IRC ボットが #mazop@friend.td.nu と #anagol@freenode に記録更新があったり新しい問題ができたり、締め切りが近くなるとわめき散らすことになっています。これは単に TCP で1行送るとそれを IRC に流すだけ、というシンプルな感じになっています。 fe/fcgi/handler.rb の mircbot 関数がそれです。

ある日なんとなく mircbot に IRC にわめくと同時に twitter に投稿するようにしてみました。今でも元気にスパムをまき散らしています: http://twitter.com/mircbot

ところがこのスパムには意外なメリットがありました。ある日 RSS をくれと言われて、えーめんどくせーなーと思ったのですが、実は twitterRSS をそのまま使ってくれと言えばいいだけだと気付いて、大変ラッキーに感じたのでした。これはゲームのスコアサーバとかでも使える手なんじゃないかなーと思います。

セキュリティホール

めんどくさい問題として、 linux kernel には時々セキュリティホールが発見されます。ゴルフ場でうっとうしい種類のセキュリティホールは、マルチユーザ環境で Unix を使うことが少なくなった今では深刻度が低くなったであろう、 root 奪取系のものだと思います。

root を奪取されちゃうとシステムを破壊することができて、バックエンドサーバに本質的に重要な情報 (投稿されたコードなど) は無いとはいえ、インストールしなおすことを考えると大変うざいです。また、ネットワークの設定をやりなおせちゃうので、外に出て悪さをされても困ります。パスワードを抜けるような root kit を仕込まれても悲しい思いをしそうです。そういえば、この理由からバックエンドのパスワードは他のものとは違うものを使っていたりします。

対処としては、まぁセキュリティホールっぽいものが報告されたら適当に kernel をアップデートすることになります。これが意外と大変で、 Xen の kernel はディストリビューションのセキュリティアップデートが他のものより遅れたり、ディストリビューションのバージョンが古くなってアップデートされないとかがあるからです。そして、大量の言語をかかえてる事情で dist-upgrade はとても大変です(何度かやりましたが)。 libc やら libstdc++ とかのバージョンが変わって野良インストールしてる言語処理系が動かなくなったりするとイヤンですよね…

Ubuntu やめて Debian にしようとしているのは、気分の問題もあるのですけど、この理由も少しあります。 Debian の方がリリース頻度が低いからセキュリティアップデートが長く続くだろうというような。

実際、今回移行しようと決断した理由はセキュリティホールだったりします。最後に linux kernel を更新してから、ゴルフ場に影響がありそうな root 取れる脆弱性が2つ発見されていまして、片方は巷の exploit code であっさり root 奪取できちゃったので、その exploit を使って kernel に patch を当てるとかいう大変ヤクザなものを見つけてそれを利用しているという悲惨な状態です。もう1つの方は、何故か root 奪取できなかったのですが、動作原理を読む限りゴルフ場で動かない理由が思いつかないので、 exploit code をいじれば root 取れちゃうんじゃないかと考えています。というわけでさっさと移行しなければならない。

あと、まぁ私の観測範囲で見つかったものだけ対処してる程度の状態なので、気付いてない脆弱性があっても不思議は無いです。

というわけで、ゴルフ場としては、善意で root 取る努力をするのは大歓迎です。 ALL_YOUR_BASE_ARE_BELONG_TO_US とかそんなファイルを置く程度のイタズラくらいは自由にやっていただいていいので、その後で教えていただければと思います。おねがいします。 kernel の情報に詳しい人は私に会った時にでも「ほげほげの脆弱性はゴルフ場大丈夫なの?」とか聞いてくれると嬉しいです。おねがいします。

あと細かいことですけど、実行サーバを実行してるユーザの .bash_history は残さないようにしてたりします。うっかりパスワードを端末に打ち込んじゃうことはきっとあると思うので…

連続投稿

ゴルフ場は PID 調整や乱数使ったコードによる連続投稿はおおいにやってくれ、と思っています。昔はイマイチでしたが、今は仮にバックエンドを落とされてもフロントエンドが生きていれば最悪30分で自動再起動がかかるはずですし。ただこの手のサーバは色々イタズラしてみたくなるのが人情だと思うので、ある程度の負荷に耐えれるようになってる必要はあると思います。

ただ PID 調整は連続投稿よりは fork 使ってある程度賢く制御してくれるとありがたいかなぁとは思います。 ksk さんがゴルフ場の PID 調整は完全に自動化しているとおっしゃっていたと思うのですが、それ公開してくれと今度頼んでおかないと。

余談として、最近よく言っている冗談として、 linux kernel は setpid(2) を実装すべきだというものがあります。そしたら web から PID を変える手段を本気で用意しそうな気がします。 kernel module でそいうのできたりするんなら本気でやるんですが。あるいは libc で getpid フックして PID の管理を完全に自力でやるというアホな案も。 (TODO)

まとめ

長くなるだろうなーと思ってたのですが、本気で長くなりました。ゴルフ場はコードは短いのですけど、色々工夫するところがあるし、まだまだ改善できるところがある、というようなことを紹介してみました。作る分にはカンタンなのですけど、維持するのは意外と面倒でもあり、盆栽的な楽しさもあり、という感じです。

まだまだ書き足りないことがあるかもしれません。思い出したら適当に追記します。

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