katsuhiro -> katsuhiro/refmon -> katsuhiro/refmon/clone
sys_clone への書き換えによる sys_fork と sys_vfork の追跡†
ptrace を用いて sys_fork と sys_vfork を追跡する手法と、それに付随する注意点、気づいた点について。
子、孫プロセスのトレース†
- strace では vfork を追うにはカーネルパッチが必要とのことである。
- この研究では 2.4系でも 2.6系でも fork や vfork を追いかけたい。
- strace を作るのならば、sys_fork が発行されれば sys_fork を、sys_vfork ならば sys_vfork を本当に実行させて追う必要がある。
- しかしリファレンスモニタには、その制約はない。このため sys_fork と sys_vfork は sys_clone に書き換えてから実行させる手法を採用する。
clone に書き換える方式の欠点†
- 書き換えた vfork は本来の動きとは違う(子が終わるまで待たない、メモリ、スレッドが共有されない)ものになる。
- vfork の man を見る限り、vfork の動きに依存してはならず、将来 vfork が廃止されて fork の別名(vfork = fork で同じ動き)になっても動くように作っとけ、とある。動かないほうが悪いんだー!!
- 親が vfork で生成した子を待つ、という動作に依存したプログラムは挙動が変わる。
- 親と子でメモリを共有している、という動作に依存した変態的なプログラムがあれば、確実に動かない。
sys_clone 書き換え方式の実装†
sys_fork の書き換え†
呼び出すシステムコール番号を sys_clone に書き換える。引数をいじって CLONE_PTRACE を指定させる。
- なぜうまくいくか
- sys_fork と sys_vfork と sys_clone はどれもカーネル関数 do_fork を呼び出す。このため do_fork にシステムコールを書き換える前と同じ値が渡るように書き換えれば問題ない。
sys_vfork の書き換え†
基本的には sys_fork のときと同じ実装である。
実装でつまづいた点†
- その1)sys_fork と同じ気分で sys_vfork を書き換えるとシステムコールから戻った後に SIGSEGV が送られ、監視対象プロセスが死ぬ。
- 原因: glibc が ecx を使っているのに、sys_vfork -> sys_clone への書き換えのときに ecx を書き換えている。
- glibc のソースを見ると ecx にリターンアドレスを入れているようだ。
- 対策: sys_vfork から戻る時に ecx を復元する。
- 将来別のレジスタが使われるかもしれないため、全レジスタを復元する方がいいだろう。
- その2)子プロセスが何も実行しないうちに死んで(SIGSEGV が飛んできて)しまう。
- 問題発生の流れ: sys_vfork -> sys_clone への書き換えはシステムコールの呼び出し時点で行われる。sys_clone 内では ecx は元と異なる値のまま do_fork によりプロセス複製が行われる。親プロセス側は上記により ecx の値が復帰するが、子プロセス側では ecx が元と異なる値のままシステムコールが復帰し、glibc 内のエラーにより SIGSEGV が飛ぶ。
- 原因: 子プロセス側の ecx を復元していない。
- 解決: sys_clone に書き換えるとき CLONE_PTRACE を指定してプロセスを生成することで、子プロセスが sys_clone の出口で止まる。このタイミングで ecx を復元する。
- つまり sys_vfork は CLONE_PTRACE を指定して止めなければならない。このフラグを指定しないと ecx が復元できなくなり、sys_clone への書き換えはできない。
sys_clone の書き換え†
フラグに CLONE_PTRACE が指定されていなければ、フラグに追加するという書き換えを行う。また、sys_fork や sys_vfork のときと同様に、システムコールが復帰した時点でレジスタの値を復元する。
実装でつまづいた点†
- スレッドにきたシグナルを調べに行くとリファレンスモニタがフリーズする。
- 問題発生の流れ: gimp の一部が追跡できなかった。ファイル保存のダイアログを表示する際に sys_clone が呼ばれ、その後止まる。スレッドにきたシグナルを取りに行ってリファレンスモニタが固まってしまうようだ。
- 原因: waitpid のデフォルト動作は __WNOTHREAD(同じスレッドグループの他のスレッドの子プロセスは待たない)であるため、clone で作成したプロセスを待たない。
- 解決: waitpid のオプションに __WALL(clone で作成されたプロセスも待つ)を指定する。
- man によると、waitpid のデフォルト動作は __WNOTHREAD(同じスレッドグループの他のスレッドの子プロセスは待たない、2.4以前)である。2.4では __WALL を指定しない限りは clone で作ったプロセスは待たない。Linux は変な仕様だね…?