この連載の第 2 回では、データベースへの呼び出しと戻りをインターセプトしてそのパスを追跡することで、 pseudo の root エミュレーション動作を詳しく説明します。さらに、pseudo のさまざまなメカニズムとして、基本的な IPC モデル、データベースとのやり取り、そしてクライアントがサーバーと通信する必要がある場合に実際に行われる内容について分析した結果などを詳細に説明します。また、皆さん自身のコードで open(2) を置き換えたいと思っている場合には、その方法をこの記事で学ぶこともできます。

Peter Seebach, member of technical staff, Wind River Systems

Author photoPeter Seebach はソース・コードをいじることが流行になる以前からソース・コードをいじってきました。彼は言語の標準化からマウスのドライバー作成に至るまで、あらゆるものに従事した経験があります。



2011年 6月 17日

概要

pseudo ユーティリティーに関する連載第 2 回目の今回は、第 1 回よりも包括的に技術の詳細について説明します。

読者はこの記事の本文を読まずにソース・コードのみを見てもかまいませんが、ソース・コードには興味深い特殊なケースや、理解するのが難しいかもしれない風変わりな処理が数多く含まれています。そこで、コードがどのように動作するのか、そしてなぜ動作するのかについて詳細に説明することにします。

pseudo の動作について、最初に知っておくべきこと

皆さんは誰かから、それは「定義されていない動作」なのでやってはならない、あるいは内部動作に関して知る必要はない、と言われたことがあるのではないでしょうか。これは pseudo には当てはまりません。pseudo によって行われる動作の多くは、非常に明確な意図を持って十分な計画をした上で、ホスト・システムの C のさまざまな実装を使用しています。そして移植性についての問題は、かなり限定的な状況でしか発生しないように考慮されています。現在の pseudo コードでは Linux の一部のアーキテクチャーで問題が起きる可能性がありますが、32 ビットと 64 ビットの x86 Linux では十分適切に動作します。また私達は最近、このコードを少なくとも部分的には Mac OS X の下で動作させることに成功しています。


ダイナミック・リンクの詳細

pseudo は open(2) を再定義しています。これは非常に怖いことであり、再定義した結果として実際に動作することを実証した後も、その怖さが少なくなることはありません。C ライブラリーに対して動的にリンクするプログラムがある場合、「システム・コール」と考えられるものの大部分は、実際には C ライブラリー内にあるラッパーに対する関数コールです。これらのラッパーは、非常に単純なものもあれば、特殊なケースを扱うために実際に何らかの設定作業を行うものや、カーネルに制御を移す前に特定の方法で設定を行うものもあります。

このことは重要です。なぜなら、実際のシステム・コール動作をインターセプトするのは非常に難しいからです。実際のシステム・コールをインターセプトすることはできますが (それがどんなものになるかは strace(1) を見ると多少はわかります)、それを実現するのは大変で、インターセプトのためのプロセスを別プロセスとして実行する必要があります (デバッガーと非常に似ています)。それはかなりのオーバーヘッドであり、またインターセプトのための犠牲は決して小さくありません。そこで pseudo では、システム・コールの代わりに、大本の関数に対する実際の呼び出しをインターセプトします。

あるライブラリーが LD_PRELOAD の中にある場合、そのライブラリーはメイン・ライブラリーの後にロードされますが、ターゲットとなるバイナリーに明示的にリンクされたライブラリーよりも前にロードされます。指定のバイナリーに対する ldd の出力を見ると、通常はそのライブラリーを実行するときにロードされるライブラリーが表示されます。これらのライブラリーはすべて、LD_PRELOAD に記述されたライブラリーのにロードされます。こうした詳細動作は異なるダイナミック・ローダーを使用する Mac OS X の場合は異なりますが、それ以外の動作は驚くほどにまったく同じです。

ダイナミック・リンクでは、複数のライブラリーが同じシンボルを定義しても問題になりません。使用されるのは、そのシンボルを「最初に」定義するライブラリーです。それは一部のシステムにとっては、最上位レベルで定義されているシンボルを使用する、ということを意味します。一方 Linux や他の一部のシステムでは、その場合に「その次の」シンボルを要求できる追加の機能があります。そのためには dlsym(RTLD_NEXT, "open") を呼び出します。

この呼び出しが成功すると、ライブラリーの中で呼び出しが発生した関数の後にある同じ名前の関数のうち、最初に登場する関数のアドレスが生成されます (ある特定のライブラリーから関数を見つけたい場合には、dlopen() を使用してそのライブラリーを開き、その結果得られるハンドルを RTLD_NEXT の代わりに使用する必要があります)。pseudo では、dlsym(RTLD_NEXT, "open") に対して返されるアドレスは real_open() という名前の関数ポインターに格納されます。

RTLD_NEXT 機能は拡張機能であるため、他のシステムにはなかったり、実装が異なっていたりするかもしれませんが、Linux 版の RTLD_NEXT 機能はラッパーを作成することを専用の目的としたものです。

ユーザー・プログラムとの衝突

シンボルが参照される場合、LD_PRELOAD ライブラリーもメインの実行可能イメージのに参照されます。私達は開発を行っている間、短期間ですが、getvar() という関数と setvar() という関数を作成して使っていたときがあります。しかしその結果、驚くような動作が発生しました。実行対象が awk ユーティリティーの場合、この 2 つの関数は想定どおりに動作しなかったのです。そこで 2 つの関数をリネームすると、動作するようになりました。このことから私は、pseudo では、他の何かに使用されている名前との衝突を極力避けるようにしなければならない、と一層強く考えるようになりました。

これと同様の衝突を回避するための非常に醜いコードが他の場所に残っています。それは、pseudo がサポートしなければならない実行対象のシステムの 1 つにおいて、regcomp()regexec() を独自にローカル実装したものが /usr/bin/find にあり、それらの実装が C ライブラリーの中にある実装とは互換性がないことによるものです。それらの実装は、いくつかの極めて限定された正規表現でしか使われないため、私はそれらの実装に対する呼び出しを、ハンド・コーディングしたストリング操作に置き換えてしまうかもしれません。

直接的なシステム・コール、そしてシステム・コールのバリエーション

glibc の内部で行われる数多くのシステム・コールは、標準的な「ラッパー」を通じて呼び出しを行うのではなく、インラインで直接呼び出しを行います。例えば fopen(3) を呼び出す場合、ライブラリーの中にある open(2) のバリエーションが呼び出されることはないかもしれません。そのため、pseudo は fopen(3) で指定された名前と関係するファイル記述子があるかどうかを調べるために、fopen(3) をインターセプトして、得られた FILE * に対して fileno(3) を実行する必要があります。この動作は fakeroot には必要ありませんでした。fakeroot は名前を追跡しないため、ファイルのオープンに関して気にする必要はなかったからです。

pseudo はバリエーションの問題にも突き当たりました。多くのシステム・コールには複数のバリエーションがあります。例えば、open(2) への呼び出しをインターセプトするためには以下のシステム・コールをインターセプトする必要があります。

      open64
      __openat_2
      __openat64_2
      openat64
      openat
      open

これらのシステム・コールのそれぞれは、ファイルを作成する唯一の方法としてそのシステム・コールを使用する、1 つ以上のプログラムによって実行される可能性があります。これらの呼び出しをインターセプトしないと、仮想 root によって作成されて所有されているものとして通知されていたはずのファイルが、実際にはそのようになっていないという事態が極めて簡単に起こってしまいます。


実際に何が起きているか

では、ラップされた関数をとおして呼び出しが行われる間、何が起きているのかを説明しましょう。これから説明するのは基本的な設計についてですが、それが当てはまらない特殊なケースや例外もいくつかあります。

プログラムから stat(2) が呼び出されると、何が起こるのでしょう。stat(2) が呼び出されると、最終的には glibc によって __xstat() というような名前のラッパー関数が呼び出されます。しかしその関数を提供しているのは pseudo ライブラリーなので、結局 pseudo ライブラリーの __xstat() という名前の関数を呼び出すことになります。するとこの __xstat() 関数は、pseudo ラッパーのセットアップと大本のシステム・コールを行うための準備を行い、それからラッパー関数を実行します。このラッパー関数は、その大本にある適切なシステム・コールを行ったように見せかけるために必要なすべてのことを実行します。それは __xstat() にとっては、ラッパーが実際に real___xstat() を実行して、そのファイルに関する情報を表す実際の struct stat バッファーを取得することを意味し、そのラッパーを使用してサーバーにクエリーが送信されます。

サーバーに対するクエリーは、pseudo が使用する内部フォーマットでメッセージを組み立てるクライアント・コードによって行われます。このメッセージには、完全にカノニカライズされたパス名と、大本の real___xstat() 呼び出しの情報が含まれます。このメッセージを受信したサーバーは、データベースを検索してマッチするエントリーを探します。マッチするエントリーが見つかると、サーバーはそこに記録されているモード、所有者、グループが含まれるメッセージを作成し、問い合わせに成功したことを示すメッセージとして返します。マッチするエントリーが見つからない場合には、問い合わせの失敗を示すメッセージを返します。

クライアント・コードはこのメッセージをラッパー関数に渡します。成功を示すメッセージの場合には、返されたメッセージに含まれる情報は struct stat オブジェクトに追加され、仮想ファイルシステムによって記録されたデータで実際のデータを置き換えます。戻りコードはラッパーに渡され、ラッパーはその戻りコードを呼び出し側のコードに渡します。少し長めの待ち時間の後、stat(2) 呼び出しの戻りが取得されます。その戻りに含まれている情報は、すべての処理を実際に root 権限で実行した場合に受信するものとまったく同じように見えます。

ラッパーの構造

インターセプトされた呼び出しそれぞれに対し、対応するラッパーがあり、そのラッパーはインターセプトされた呼び出しと同じ名前とシグニチャーを持っています。基本的なラッパーは、おおよそリスト 1 のような作りになっています (リスト 1 は pseudo のコードをそのまま示したものです)。

リスト 1. システム・コールを置き換える
int
fchmod(int fd, mode_t mode) {
    int rc;
    if (!setup_wrappers()) {
        errno = ENOSYS;
        return -1;
    }
    if (already_in_pseudo) {
        rc = real_fchmod(fd, mode);
    } else {
        rc = wrap_fchmod(fd, mode);
    }
    return rc;
}

この例で、wrap_fchmod() は pseudo の内部で定義される別の関数であり、変更された fchmod() の実装を実際に提供します。それとは対照的に、real_fchmod() は関数ポインターであり、setup_wrappers() によって内容が追加されます。setup_wrappers() 関数はこのセットアップを 1 度だけ行い、そのセットアップが既に行われたことを示すように静的な変数を設定します。そのため、セットアップが行われた後は、この関数が呼び出されても即座にリターンされます。

already_in_pseudo に基づく判定は別の懸念に対処しています。つまり pseudo が、ある指定された呼び出しを実行している最中に、他のシステム・コールを行う必要がある場合にはどうすればよいのでしょう?pseudo は他のシステム・コールのインターセプトを望んでいないため、新たなシステム・コールのインターセプトが要求されるような処理を行う際には、already_in_pseudo フラグ (このフラグに付けられた名前は「既に見せかけの状態にある」ことを意味しますが、実際に行われるのはマジックではありません) をセットします。(実際には、このフラグはカウンターですが、私の知る限り、このカウンターが 1 を超えたことはありません。)

wrap_fchmod() 関数には、一般的なプリアンブルとエピローグ、そしてその特定の関数のコードを取得するための #include、という 2 つの部分があります。各関数のコードは guts というサブディレクトリーに格納されており、そこにはラップするための実際の追加作業が含まれています。標準的なセットアップはプリアンブルとエピローグによって行われます。リスト 2 は現在実装されている wrap_fchmod() を示しています。

リスト 2. ラッパーの例
static int
wrap_fchmod(int fd, mode_t mode) {
    int rc = -1;

#include "guts/fchmod.c"

    return rc;
}

これは、経験の長い C プログラマーが #include ディレクティブを使用して .c ファイルを指定する稀な例の 1 つです。この設計により、これらの関数の中で追加の作業を行えるようになり、関数の複数の実装にわたって追加作業が重複することがなくなります。例えば、オプションの引数を取る関数の場合、wrap_*() 関数がオプションの引数を抽出します。pseudo は技術的に定義されていないことを数多く実行しますが、これもその 1 つです。技術的に定義されていない理由は、この動作は無条件に実行され、また追加の引数が渡されない場合に va_arg() を呼び出すのはエラーであるからです。リスト 3wrap_open() 関数を示しています。

リスト 3. 追加の引数を持つラッパー
static int
wrap_open(const char *path, int flags, ... /* mode_t mode */) {
    int rc = -1;
    va_list ap;
    mode_t mode;

    va_start(ap, flags);
    mode = va_arg(ap, mode_t);
    va_end(ap);

#include "guts/open.c"

    return rc;
}

この方法により、内部関数は va_*() マクロを使用せずに mode 引数を無条件に参照することができます (単純に 3 つの引数を取る関数として open() を宣言することはできません。その宣言が標準のヘッダーと衝突するからです)。

パス、ロック、シグナル

ラッパーの具体的な作りがどのようになっていると私が言ったか覚えているでしょうか?その後、大きな変更が 3 つありました。第 1 に、パスは現在、ラッパーの中でカノニカライズされています。最初の実装では、pseudo は実際にはパスをカノニカライズせず、単に ... というパスのエントリーを修正しただけでした。そのため、パスの中にあるシンボリック・リンクに関係する問題が時々発生しました。そこで現在の pseudo では、ほとんどの場合にパスを完全にカノニカライズしています (ほとんどの場合としているのは、リンクを操作する関数を pseudo がラップする場合、リンク名の最後の部分が逆参照されないからです)。このカノニカライズは一貫して行われるため、通常はラッパーの中で行われます。

考えた当時は賢明に思えたのですが (私は今やほとんど後悔していますが)、名前が「path」で終わる引数があると、必ずカノニカライズ用のコードが自動的に呼び出されます。これはつまり、pseudo の中で定義される関数のいくつかは、引数の名前が man ページに記述されたカノニカル名と異なるということです。openat(2) など、多くの *at() 呼び出しには追加の flags 引数があり、この引数によって、その呼び出しでシンボリック・リンクをたどる必要があるかどうかを判断します。パス名をカノニカライズする関数も同じような引数を取ります。flags という名前の引数を持つ関数の場合、デフォルトで、その引数は単純にカノニカライズ関数に渡されます。この判断も、その判断をした時には賢明なように思えたのですが、いくつかの奇妙なバグを引き起こしました (例えば、さまざまなセマンティクスの flags 引数を持つ、単純な open(2) 呼び出しに使われた場合など)。

私達はマルチスレッド・プログラムのためのロックを追加しました。最初の設計にはロック機能がなかったため、あるスレッドが pseudo サーバーと通信中に別のスレッドがシステム・コールを実行すると、その別スレッドによるシステム・コールは pseudo の設計をバイパスしてしまいました。現在はロック・メカニズムがあり、ラップされた呼び出しを実行するのは一度に 1 つのスレッドのみになっています。

それによって 3 番目の問題が発生しました。つまりプログラムがシグナルを引数に取り、そのシグナルに対し、ラップされた呼び出しをシグナル・ハンドラーが実行する場合です。この問題に対処するために、pseudo はラッパーを起動する際に特定のシグナルをブロックし、そのラッパーの終了時にブロックを解除します。これは非常にうまく動作しましたが、1 つだけ致命的なバグがありました。それは、シグナルがブロック解除されるのはラッパーの終了時であり、また pseudo は execve(2) やその仲間をラップするため、execve(2) によって呼び出される新しいプロセスは、これらのシグナルがブロックされた状態で起動されます。そのため一部のプログラムはシグナルをリセットしなかったり、シグナルを修正しなかったりする場合があります。この問題は修正されています。

これらの変更を加えたラッパー関数は記事の中にコード・サンプルとして示すには長すぎますが、非常に適切に動作しているようです。ラッパー関数のコードを見るためには、Github に公開されている pseudo のソース・ツリーにアクセスしてください (「参考文献」を参照)。


guts ディレクトリーの内容

guts ディレクトリーの中にある実装は単純です。例えば、リスト 4acct(2) システム・コールのための実装です。

リスト 4. guts の最も単純なケース
/*
 * Copyright (c) 2010 Wind River Systems; see
 * guts/COPYRIGHT for information.
 *
 * static int
 * wrap_acct(const char *path) {
 *      int rc = -1;
 */

    rc = real_acct(path);

/*      return rc;
 * }
 */

コメントアウトされたコードとラッパーとが似ていることに注意してください。これを見ると、どんなコードを作成すればよいかの感覚をつかむことができます。しかし一部のものは、この例よりもはるかに複雑です。guts のコードは、サーバーに何かを要求する必要があるかどうか、そして要求する必要がある場合には何を要求するのか、を判断する必要があります。場合によると、大本のシステム・コールが完全に省略されたり、まったく異なる呼び出しで置き換えられたりする場合があります。例えば mnkod(2) というシステム・コールの実装では、単純なファイルを作成した後、要求されたファイル・タイプを pseudo データベースに記録しています。

何らかの対策が必要な事項

ラッパー関数がアクションを記録する必要がある場合や、アクションのメモを取りたい場合、あるいは pseudo データベースにクエリーを実行したい場合、pseudo_client_op() 関数を使用して処理を行います。この関数のシグニチャーをリスト 5 に示します。

リスト 5. pseudo_client_op() のシグニチャー
pseudo_msg_t *
pseudo_client_op(
    op_id_t op,
    int access,
    int fd,
    int dirfd,
    const char *path,
    const struct stat64 *buf,
    ...);

「...」には経緯があります。リネーム処理がサポートされる前、この関数は固定の引数しか取りませんでした。私がリネーム処理を追加した時には、最後の引数をオプションにすることは良い考えに思えたのです (そうしないと const char *oldpath のようになってしまいます)。私は今でもまだ、それが適切であったかどうか自信がありません。

op_id_t という型は、サポートされる一連の処理 (OP_CHROOTOP_OPENOP_CLOSEOP_CHMOD など) を含む列挙型です。access パラメーターはアクセスのタイプ (読み取り、書き込み、実行) を示します。fd パラメーターは処理対象のファイル記述子です。dirfd パラメーターは最初、openat(2) などの *at() システム・コールに使用されるパスをカノニカライズするために使われました。その後、パスのカノニカライズは pseudo_client_op() から除外され、そのフィールドは現在、dup(2) の呼び出しなどの OP_DUP 処理を行うためにのみ使用されています。この名前は今や明らかに不適切です。この問題は、いくらでもある自由時間に私が修正するつもりです。パス名というのは、ラップされている呼び出しに利用できる名前であり、stat64 バッファーは処理対象のファイルに関する統計情報です。

多くの特殊なケースにおいて、これらのフィールドの一部は特定の処理とは無関係のため、番兵値に設定されています。

一部のメッセージは完全にクライアント内で処理されます。例えば、クライアントはファイル記述子の現在のパスのテーブルを保持しているため、fchmod(2) などの処理と共にパスをサーバーに送信することができます。OP_CLOSE 処理はその内部テーブルを更新しますが、OP_CLOSE 処理はサーバーに送信されません。処理がサーバーに送信されるのは、それらの処理によってデータベースとやり取りする必要がある場合、あるいはそれらの処理をログに記録する必要がある場合のみです。(将来のバージョンでは、この 2 つをもっと明確に区別できるようにしたいと思っています。)

サーバーに実際にメッセージを送信する

クライアントとサーバーとの間の IPC は UNIX ドメイン・ソケットによって行われます。ソケットが存在しない場合、あるいはソケットを開けない場合には、クライアントはサーバーを生成しようとします。メッセージは標準的なヘッダーとオプションのパス情報で構成されています。メッセージには、いくつかのタイプがあります (PSEUDO_MSG_PINGPSEUDO_MSG_OPPSEUDO_MSG_ACK など)。PSEUDO_MSG_OP メッセージ・タイプは、ある操作 (OP_CHMODOP_STAT など) がサーバーに送信されていることを示します。クライアントはソケットにメッセージを書き込み、サーバーはそのメッセージを処理し、PSEUDO_MSG_ACK というタイプの応答を書き込みます。ファイルを参照するメッセージに通常含まれているのは、そのファイルのファイルシステム・モード (アクセス許可とファイル・タイプの両方を含む)、デバイス番号と inode 番号、そしてパス名です。送信のパスが 2 つある項目の場合には、最初のパスの後にヌル・バイトがあり、その後に 2 番目のパスが続き、そのメッセージの pathlen フィールドは合計長に設定されます。

サーバーは受信するリクエストに対して複数のサニティー・チェックを行います。第 1 に、リクエストに含まれるパス、デバイス番号および inode 番号と一致するデータベース・エントリーがあるかどうかを調べます。ない場合には、パス名による一致、そしてデバイス番号および inode 番号による一致、という両方を調べます。これらが一致したけれども、完全な一致ではなかった場合には、多くの場合は診断結果がレポートされます (ただし、リネームされた場合など、名前が一致しないことがわかっている場合にはレポートされません)。ファイル・タイプが想定外の形で不一致であった場合 (一方がディレクトリーであって他方がディレクトリーではない場合など) には、診断内容が記録され、既存のデータベース・エントリーは破棄されます。ただしファイル・タイプが不一致の場合でも、ファイルシステムに単純なファイルが含まれ、データベースにデバイス・ノードが記録されている場合には、ファイル・タイプの不一致による害は無いので pseudo はデバイス・ノードの作成をエミュレートします。


次回の第 3 回では

通常、私は初めから動作したものからよりも、自分の誤りがあったものからの方がより多くを学びます。そこで、この連載の次回の記事では、このプロジェクトを開始してから私達が経験した興味深いバグや失敗について、またそれらのバグや失敗にどう対処し、何を学んだかについて説明します。

参考文献

学ぶために

製品や技術を入手するために

  • 皆さんの次期オープンソース開発プロジェクトを IBM ソフトウェアの試用版を使って革新してください。ダウンロードあるいは DVD で入手することができます。

議論するために

  • developerWorks コミュニティーで開発者向けのブログ、フォーラム、グループ、ウィキなどを利用しながら、他の developerWorks ユーザーとやり取りしてください。

コメント

developerWorks: サイン・イン

必須フィールドは(*)で示されます。


IBM ID が必要ですか?
IBM IDをお忘れですか?


パスワードをお忘れですか?
パスワードの変更

「送信する」をクリックすることにより、お客様は developerWorks のご使用条件に同意したことになります。 ご使用条件を読む

 


お客様が developerWorks に初めてサインインすると、お客様のプロフィールが作成されます。会社名を非表示とする選択を行わない限り、プロフィール内の情報(名前、国/地域や会社名)は公開され、投稿するコンテンツと一緒に表示されますが、いつでもこれらの情報を更新できます。

送信されたすべての情報は安全です。

ディスプレイ・ネームを選択してください



developerWorks に初めてサインインするとプロフィールが作成されますので、その際にディスプレイ・ネームを選択する必要があります。ディスプレイ・ネームは、お客様が developerWorks に投稿するコンテンツと一緒に表示されます。

ディスプレイ・ネームは、3文字から31文字の範囲で指定し、かつ developerWorks コミュニティーでユニークである必要があります。また、プライバシー上の理由でお客様の電子メール・アドレスは使用しないでください。

必須フィールドは(*)で示されます。

3文字から31文字の範囲で指定し

「送信する」をクリックすることにより、お客様は developerWorks のご使用条件に同意したことになります。 ご使用条件を読む

 


送信されたすべての情報は安全です。


static.content.url=http://www.ibm.com/developerworks/js/artrating/
SITE_ID=60
Zone=Open source, Linux
ArticleID=679202
ArticleTitle=pseudo のすべて: 第 2 回 ベールの下で
publish-date=06172011