FANOTIFY(7)

名前
fanotify - ファイルシステムイベントを監視する

説明
fanotify API はファイルシステムイベントの通知と横取り機能 (interception) を提供する。 ユースケースとしては、ウイルススキャンや階層型ストレージの管理などがある。 現在のところ、限定的なイベントのみがサポートされている。 特に、作成 (create)、削除 (delete)、移動 (move) イベントがサポートされていない (これらのイベントを通知する API の詳細については inotify(7) を参照)。 inotify(7) API と比較して追加されている機能としては、 マウントされたファイルシステムの全オブジェクトを監視する機能、 アクセス許可の判定を行う機能、 他のアプリケーションによるアクセスの前にファイルを読み出したり変更したりする機能がある。 この API では以下のシステムコールを使用する: fanotify_init(2), fanotify_mark(2), read(2), write(2), close(2)。

  fanotify_init(), fanotify_mark() と通知グループ
fanotify_init(2) システムコールは fanotify 通知グループを作成、初期化し、 この通知グループを参照するファイルディスクリプターを返す。

fanotify 通知グループはカーネル内部のオブジェクトで、 イベントが作成されるファイル、 ディレクトリ、 マウントポイントのリストを保持する。

fanotify 通知グループの各エントリーには 2 つのビットマスクがある。 mark マスクと ignore マスクである。 mark マスクはどのファイル操作についてイベントを作成するかを定義する。 ignore マスクはどの操作についてイベントを作成しないかを定義する。 これらの 2 種類のマスクがあることで、 マウントポイントやディレクトリに対してイベントの受信を mark しておきつつ、 同時にそのマウントポイントやディレクトリ配下の特定のオブジェクトに対するイベントを無視する、 といったことができる。

fanotify_mark(2) システムコールは、ファイル、ディレクトリ、マウントを通知グループに追加し、 どのイベントを報告 (もしくは無視) するかを指定する。 また、このようなエントリーの削除、変更も行う。

ignore マスクの考えられる使用方法はファイルキャッシュに対してである。 ファイルキャッシュに関して興味のあるイベントは、ファイルの変更とファイルのクローズである。 それゆえ、 キャッシュされたディレクトリやマウントポイントは、 これらのイベントを受信するようにマークされる。 ファイルが変更されたという最初のイベントを受信した後は、 対応するキャッシュエントリーは無効化される。 そのファイルがクローズされるまでは、 このファイルに対する変更イベントは興味のない情報となる。 したがって、 変更イベントを ignore マスクに追加することができる。 クローズイベントを受信すると、 変更イベントを ignore イベントから削除し、 ファイルキャッシュエントリーを更新することができる。

fanotify 通知グループのエントリーは、 ファイルやディレクトリでは inode 番号経由で参照され、 マウントではマウント ID 経由で参照される。 ファイルやディレクトリの名前が変更されたり、移動されたりした場合も、 関連するエントリーはそのまま残る。 ファイルやディレクトリが削除されたり、マウントがアンマウントされたりした場合には、 対応するエントリーは削除される。

  イベントキュー
通知グループにより監視されているファイルシステムオブジェクトでイベントが発生すると、 fanotify システムはイベントを生成し、 そのイベントはキューにまとめられる。 これらのイベントは、 fanotify_init(2) が返した fanotify ファイルディスクリプターから (read(2) などを使って) 読み出すことができる。 2 種類のイベントが生成される。 notification (通知) イベントと permission (アクセス許可) イベントである。 通知イベントは単なる情報通知であり、 イベントで渡されたファイルディスクリプターをクローズする場合 (下記参照) を除き、 受信したアプリケーションでアクションを取る必要はない。 アクセス許可イベントは、 受信したアプリケーションがファイルアクセスの許可を承認するかを判定する必要がある。 この場合、 受信者はアクセスが許可されたか否かを決定する応答を書き込まなければならない。 イベントは、 読み出されると、 fanotify グループのイベントキューから削除される。 読み出されたアクセス許可イベントは、 fanotify ファイルディスクリプターにアクセス許可の判定が書き込まれるか、 fanotify ファイルディスクリプターがクローズされるまで、 fanotify グループの内部のリストに保持される。

  fanotify イベントの読み出し
fanotify_init(2) が返したファイルディスクリプターに対する read(2) を呼び出しは、 (fanotify_init(2) の呼び出しでフラグ FAN_NONBLOCK を指定しなかった場合) ファイルイベントが起こるか、呼び出しがシグナルによって割り込まれる (signal(7) 参照) まで停止する。 read(2) が成功すると、読み出しバッファーには以下の構造体が 1 つ以上格納される。
struct fanotify_event_metadata {
    __u32 event_len;
    __u8 vers;
    __u8 reserved;
    __u16 metadata_len;
    __aligned_u64 mask;
    __s32 fd;
    __s32 pid;
};

性能上の理由から、複数のイベントを一度の read(2) で取得できるように大きめのバッファーサイズ (例えば 4096 バイト) を使用することを推奨する。 read(2) の返り値はバッファーに格納されたバイト数である。 エラーの場合は -1 が返される (ただし、バグも参照)。 fanotify_event_metadata 構造体のフィールドは以下のとおりである。

event_len
    これは、 このイベントのデータ長であり、バッファー内の次のイベントへのオフセットである。 現在の実装では、 event_len の値は常に FAN_EVENT_METADATA_LEN である。 しかしながら、 API は将来可変長の構造体を返すことができるように設計されている。

vers
    このフィールドには構造体のバージョン番号が入る。 実行時に返された構造体がコンパイル時の構造体と一致しているかを検査するには、 この値を FANOTIFY_METADATA_VERSION を比較すること。 一致しない場合、 アプリケーションはその fanotify ファイルディスクリプターを使用するのを諦めるべきである。

reserved
    このフィールドは使用されない。

metadata_len
    この構造体の長さである。 このフィールドは、 イベント種別単位のオプションヘッダーの実装を扱うために導入された。 現在の実装ではこのようなオプションヘッダーは存在しない。

mask
    イベントを示すビットマスクである (下記参照)

fd  これはアクセスされたオブジェクトに対するオープンされたファイルディスクリプターである。 または、キューのオーバーフローが発生した場合には FAN_NOFD が入る。 ファイルディスクリプターは監視対象のファイルやディレクトリの内容にアクセスするのに使用できる。 読み出したアプリケーションは責任を持ってこのファイルディスクリプターをクローズしなければならない。

    fanotify_init(2) を呼び出す際、 呼び出し元はこのファイルディスクリプターに対応するオープンファイル記述にセットされた様々なファイル状態フラグを (event_f_flags 引き数を使って) 指定することができる。 さらに、 (カーネル内部の) FMODE_NONOTIFY ファイル状態フラグがオープンファイル記述にセットされる。 このフラグは fanotify イベントの生成を抑制する。 したがって、 fanotify イベントの受信者がこのファイルディスクリプターを使って通知されたファイルやディレクトリにアクセスした際に、 これ以上イベントが作成されなくなる。

pid これはイベントが発生する原因となったプロセス ID である。 fanotify イベントを監視しているプログラムは、 この PID を getpid(2) が返す PID と比較することで、 イベントが監視しているプログラム自身から発生したかどうか、 別のプロセスによるファイルアクセスにより発生したか、を判定できる。

mask のビットマスクは、1 つのファイルシステムオブジェクトに対してどのイベントが発生したかを示す。 監視対象のファイルシステムオブジェクトに複数のイベントが発生した場合は、 このマスクに複数のビットがセットされることがある。 特に、 同じファイルシステムオブジェクトに対する連続するイベントが同じプロセスから生成された場合には、 一つのイベントにまとめられることがある。 例外として、 2 つのアクセス許可イベントが一つのキューエントリーにまとめられることは決してない。

mask でセットされている可能性のあるビットは以下のとおりである。

FAN_ACCESS
    ファイルやディレクトリがアクセスされた (読み出しが行われた) (ただし、「バグ」の節も参照)。

FAN_OPEN
    ファイルやディレクトリがオープンされた。

FAN_MODIFY
    ファイルやディレクトリが変更された。

FAN_CLOSE_WRITE
    書き込み用 (O_WRONLY か O_RDWR) にオープンされたファイルがクローズされた。

FAN_CLOSE_NOWRITE
    読み出し用 (O_RDONLY) にオープンされたファイルがクローズされた。

FAN_Q_OVERFLOW
    イベントキューが 16384 エントリーの上限を超過した。 この上限は fanotify_init(2) 呼び出し時に FAN_UNLIMITED_QUEUE フラグを指定することで上書きできる。

FAN_ACCESS_PERM
    アプリケーションが例えば read(2) や readdir(2) などを使ってファイルやディレクトリを読み出そうとした。 このイベントを読み出したプログラムは、 そのファイルシステムオブジェクトへのアクセス許可を承認するかを判定し (下記で説明するとおり) 応答を書き込まなければならない。

FAN_OPEN_PERM
    アプリケーションがファイルやディレクトリをオープンしようとした。 このイベントを読み出したプログラムは、 そのファイルシステムオブジェクトのオープンを承認するかを判定し (下記で説明するとおり) 応答を書き込まなければならない。

クローズイベントを確認するために以下のビットマスクを使うことができる。

FAN_CLOSE
    ファイルがクローズされた。 以下の同義語である。
        FAN_CLOSE_WRITE | FAN_CLOSE_NOWRITE

fanotify ファイルディスクリプターからの read(2) が返した fanotify イベントメタデータを含むバッファーに対して繰り返しを行うため、 以下のマクロが提供されている。

FAN_EVENT_OK(meta, len)
    このマクロは、 バッファー meta の残りの長さ len を、 メタデータ構造体の長さとバッファーの最初のメタデータ構造体の event_len フィールドと比較して検査する。

FAN_EVENT_NEXT(meta, len)
    このマクロは、 meta が指すメタデータ構造体の event_len フィールドで示された長さを使って、 meta の次のメタデータ構造体のアドレスを計算する。 len はバッファーに現在残っているメタデータのバイト数である。 このマクロは meta の次のメタデータ構造体へのポインターを返し、 スキップされたメタデータ構造体のバイト数だけ len を減算する (つまり、 len から meta->event_len を引き算する)。

また、 以下のマクロも用意されている。

FAN_EVENT_METADATA_LEN
    このマクロは fanotify_event_metadata 構造体の (バイト単位の) サイズを返す。 返される値はイベントメタデータの最小値である (現在のところ、これが唯一のサイズである)。

  fanotify ファイルディスクリプターのイベントを監視する
fanotify イベントが発生すると、 epoll(7), poll(2), select(2) に fanotify ファイルディスクリプターが渡された場合には、そのファイルディスクリプターが読み出し可能であると通知される。

  アクセス許可イベントの取り扱い
アクセス許可イベントでは、 アプリケーションは以下の形式の構造体を fanotify ファイルディスクリプターに write(2) しなければならない。
struct fanotify_response {
    __s32 fd;
    __u32 response;
};

この構造体のフィールドは以下のとおりである。

fd  このフィールドは fanotify_event_metadata 構造体で返されたファイルディスクリプターである。

response
    このフィールドはアクセス許可を承認するかどうかを示す。 値は、このファイル操作を許可する FAN_ALLOW か、 このファイル操作を拒否する FAN_DENY のいずれかでなければならない。

アクセスを拒否した場合、 アクセスを要求したアプリケーションは EPERM エラーを受け取ることになる。

  fanotify ファイルディスクリプターのクローズ

fanotify 通知グループを参照するすべてのファイルディスクリプターがクローズされると、 fanotify グループは解放され、 カーネルが再利用できるようにそのリソースは解放される。 close(2) の際に、 処理中であったアクセス許可イベントには許可が設定される。

  /proc/[pid]/fdinfo
ファイル /proc/[pid]/fdinfo/[fd] には、 プロセス pid のファイルディスクリプター fd の fanotify マークに関する情報が格納される。 詳細はカーネルのソースファイル Documentation/filesystems/proc.txt を参照。

エラー
通常の read(2) のエラーに加え、 fanotify ファイルディスクリプターから読み出しを行った際に以下のエラーが発生することがある。

EINVAL
    バッファーがイベントを保持するには小さすぎる。

EMFILE
    オープンしたファイル数のプロセス毎の上限に達した。 getrlimit(2) の RLIMIT_NOFILE の説明を参照。

ENFILE
    オープンされたファイル数のシステム全体の上限に達した。 proc(5) の /proc/sys/fs/file-max を参照。

ETXTBSY
    fanotify_init(2) の呼び出し時に O_RDWR か O_WRONLY が event_f_flags 引き数に指定されており、 現在実行中の監視対象のファイルに対してイベントが発生した際に、 このエラーが read(2) から返される。

通常の write(2) のエラーに加え、 fanotify ファイルディスクリプターに書き込みを行った際に以下のエラーが発生することがある。

EINVAL
    fanotify アクセス許可がカーネルの設定で有効になっていない。 応答構造体の response 値が無効である。

ENOENT
    応答構造体のファイルディスクリプター fd が無効である。 このエラーはアクセス許可イベントに対する応答がすでに書き込まれている際に発生する。

バージョン
fanotify API は Linux カーネルのバージョン 2.6.36 で導入され、 バージョン 2.6.37 で有効にされた。 fdinfo のサポートはバージョン 3.8 で追加された。

準拠
fanotify API は Linux 独自のものである。

注意
fanotify API が利用できるのは、 カーネルが CONFIG_FANOTIFY 設定オプションを有効にして作成されている場合だけである。 また、 fanotify アクセス許可の処理が利用できるのは CONFIG_FANOTIFY_ACCESS_PERMISSIONS 設定オプションが有効になっている場合だけである。

  制限と警告
fanotify が報告するのはユーザー空間プログラムがファイルシステム API 経由で行ったイベントだけである。 その結果、 fanotify ではネットワークファイルシステム上で発生したリモートイベントは捕捉できない。

inotify API は mmap(2), msync(2), munmap(2) により起こったファイルのアクセスと変更を報告しない。

ディレクトリのイベントは、ディレクトリ自身がオープン、読み出し、クローズされた場合にしか作成されない。 マークされたディレクトリでの子要素の追加、削除、変更では、監視対象のディレクトリ自身へのイベントは作成されない。

fanotify のディレクトリの監視は再帰的ではない。 ディレクトリ内のサブディレクトリを監視するには、 追加で監視用のマークを作成しなければならない。 (ただし、 fanotify API では、サブディレクトリが監視対象としてマークされているディレクトリに作成された際に検出する手段は提供されていない点に注意すること。) マウントの監視を使うことで、 ディレクトリツリー全体を監視することができる。

ベントキューはオーバーフローすることがある。 この場合、 イベントは失われる。

バグ
Linux 3.17 時点では、 以下のバグが存在する。

*   Linux では、ファイルシステムオブジェクトは複数のパスでアクセス可能である。 例えば、 ファイルシステムの一部は mount(8) の --bind オプションを使って再マウントされることがある。 マークされたマウントの監視者は、 同じマウントを使ったファイルオブジェクトについてのみイベント通知を受ける。 それ以外のイベントは通知されない。

*   fallocate(2) の呼び出しでは fanotify イベントが作成されない。

*   イベントが生成された際に、 そのファイルのファイルディスクリプターを渡す前に、 イベントを受信するプロセスのユーザー ID がそのファイルに対する読み出し／書き込み許可があるかの確認は行われない。 非特権ユーザーによって実行されたプログラムに CAP_SYS_ADMIN ケーパビリティーがセットされている場合には、 このことはセキュリティーリスクとなる。

*   read(2) の呼び出しが fanotify キューから複数のイベントを処理している際に、 エラーが発生した場合、 返り値はエラーが発生する前までにユーザー空間バッファーに正常にコピーされたイベントの合計長となる。 返り値は -1 にならず、 errno もセットされない。 したがって、 読み出しを行うアプリケーションではエラーを検出する方法はない。

例
以下のプログラムは fanotify API の使用法を示すものである。 コマンドライン引き数で渡されたマウントポイントを監視し、 種別が FAN_PERM_OPEN と FAN_CLOSE_WRITE のイベントを待つ。 アクセス許可イベントが発生には、 FAN_ALLOW 応答を返す。

以下の出力例はファイル /home/user/temp/notes を編集した際に記録されたものである。 ファイルをオープンする前に FAN_OPEN_PERM イベントが発生している。 ファイルをクローズした後に FAN_CLOSE_WRITE イベントが発生している。 エンターキーをユーザーが押すと、 このプログラムの実行は終了する。

  出力例
# ./fanotify_example /home
Press enter key to terminate.
Listening for events.
FAN_OPEN_PERM: File /home/user/temp/notes
FAN_CLOSE_WRITE: File /home/user/temp/notes
Listening for events stopped.

  プログラムソース
#define _GNU_SOURCE     /* O_LARGEFILE の定義を得るために必要 */
#include <errno.h>
#include <fcntl.h>
#include <limits.h>
#include <poll.h>
#include <stdio.h>
#include <stdlib.h>
#include <sys/fanotify.h>
#include <unistd.h>
/* ファイルディスクリプター 'fd' から読み出しできる全 fanotify イベントを読み出す */
static void
handle_events(int fd)
{
    const struct fanotify_event_metadata *metadata;
    struct fanotify_event_metadata buf[200];
    ssize_t len;
    char path[PATH_MAX];
    ssize_t path_len;
    char procfd_path[PATH_MAX];
    struct fanotify_response response;
    /* fanotify ファイルディスクリプターからイベントが読み出せる間はループする */
    for(;;) {
        /* イベントを読み出す */
        len = read(fd, (void *) &buf, sizeof(buf));
        if (len == -1 && errno != EAGAIN) {
            perror("read");
            exit(EXIT_FAILURE);
        }
        /* 読み出せるデータの最後に達しているかチェックする */
        if (len <= 0)
            break;
        /* バッファーの最初のイベントを参照する */
        metadata = buf;
        /* バッファー内の全イベントを処理する */
        while (FAN_EVENT_OK(metadata, len)) {
            /* 実行時とコンパイル時の構造体が一致するか確認する */
            if (metadata->vers != FANOTIFY_METADATA_VERSION) {
                fprintf(stderr,
                        "Mismatch of fanotify metadata version.\n");
                exit(EXIT_FAILURE);
            }
            /* metadata->fd には、キューのオーバーフローを示す FAN_NOFD か、
               ファイルディスクリプター (負でない整数) のいずれかが入っている。
               ここではキューのオーバーフローは無視している。 */
            if (metadata->fd >= 0) {
                /* オープン許可イベントを処理する */
                if (metadata->mask & FAN_OPEN_PERM) {
                    printf("FAN_OPEN_PERM: ");
                    /* ファイルのオープンを許可する */
                    response.fd = metadata->fd;
                    response.response = FAN_ALLOW;
                    write(fd, &response,
                          sizeof(struct fanotify_response));
                }
                /* 書き込み可能ファイルのクローズイベントを処理する */
                if (metadata->mask & FAN_CLOSE_WRITE)
                    printf("FAN_CLOSE_WRITE: ");
                /* アクセスされたファイルのパス名を取得し表示する */
                snprintf(procfd_path, sizeof(procfd_path),
                         "/proc/self/fd/%d", metadata->fd);
                path_len = readlink(procfd_path, path,
                                    sizeof(path) - 1);
                if (path_len == -1) {
                    perror("readlink");
                    exit(EXIT_FAILURE);
                }
                path[path_len] = '\0';
                printf("File %s\n", path);
                /* イベントのファイルディスクリプターをクローズする */
                close(metadata->fd);
            }
            /* 次のイベントに進む */
            metadata = FAN_EVENT_NEXT(metadata, len);
        }
    }
}
int
main(int argc, char *argv[])
{
    char buf;
    int fd, poll_num;
    nfds_t nfds;
    struct pollfd fds[2];
    /* マウントポイントが指定されたか確認する */
    if (argc != 2) {
        fprintf(stderr, "Usage: %s MOUNT\n", argv[0]);
        exit(EXIT_FAILURE);
    }
    printf("Press enter key to terminate.\n");
    /* fanotify API にアクセスするためのファイルディスクリプターを作成する */
    fd = fanotify_init(FAN_CLOEXEC | FAN_CLASS_CONTENT | FAN_NONBLOCK,
                       O_RDONLY | O_LARGEFILE);
    if (fd == -1) {
        perror("fanotify_init");
        exit(EXIT_FAILURE);
    }
    /* 指定されたマウントに対して以下を監視するようにマークを付ける:
       - ファイルのオープン前のアクセス許可イベント
       - 書き込み可能なファイルディスクリプターのクローズ後の
         通知イベント */
    if (fanotify_mark(fd, FAN_MARK_ADD | FAN_MARK_MOUNT,
                      FAN_OPEN_PERM | FAN_CLOSE_WRITE, AT_FDCWD,
                      argv[1]) == -1) {
        perror("fanotify_mark");
        exit(EXIT_FAILURE);
    }
    /* ポーリングの準備 */
    nfds = 2;
    /* コンソールの入力 */
    fds[0].fd = STDIN_FILENO;
    fds[0].events = POLLIN;
    /* fanotify の入力 */
    fds[1].fd = fd;
    fds[1].events = POLLIN;
    /* イベントの発生を待つループ */
    printf("Listening for events.\n");
    while (1) {
        poll_num = poll(fds, nfds, -1);
        if (poll_num == -1) {
            if (errno == EINTR)     /* シグナルに割り込まれた場合 */
                continue;           /* poll() を再開する */
            perror("poll");         /* 予期しないエラー */
            exit(EXIT_FAILURE);
        }
        if (poll_num > 0) {
            if (fds[0].revents & POLLIN) {
                /* コンソールからの入力がある場合: 空の標準入力であれば終了 */
                while (read(STDIN_FILENO, &buf, 1) > 0 && buf != '\n')
                    continue;
                break;
            }
            if (fds[1].revents & POLLIN) {
                /* fanotify イベントがある場合 */
                handle_events(fd);
            }
        }
    }
    printf("Listening for events stopped.\n");
    exit(EXIT_SUCCESS);
}

関連項目
fanotify_init(2), fanotify_mark(2), inotify(7)

この文書について
この man ページは Linux man-pages プロジェクトのリリース 3.79 の一部 である。プロジェクトの説明とバグ報告に関する情報は http://www.kernel.org/doc/man-pages/ に書かれている。