地方エンジニアの学習日記

興味ある技術の雑なメモだったりを書いてくブログ。たまに日記とガジェット紹介。

【Linux】ファイルディスクリプタパッシングを学ぶ

qiita.com

この記事はLinuxアドベントカレンダー20日目の記事です!

書いたきっかけ

UNIXネットワークプログラミング〈Vol.1〉を読んでいてTCPサーバの種類を読んでいたのですが中でもファイルディスクリプタパッシングなアーキテクチャのものってあまりない?と思い気になって調べてみたので書きました

ディスクリプタパッシングとは

ディスクリプタパッシングは ファイル記述子をUnixドメインソケット経由で親子関係にない別プロセスへ転送する手法です。

0xcc.net

やってみる

以下のサンプルコードを実行するとfork(2)の後にオープンしたfdを親から子へわたし、子がreadして内容を読み取って表示できていることがわかります。ここでのポイントは単純なread/writeでfdの数値を渡すだけではダメでsendmsg(2)/recvmsg(2) の補助データ枠を使ってプロセス間で通信する必要があります。

#include <fcntl.h>
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <sys/socket.h>
#include <sys/wait.h>
#include <time.h>
#include <unistd.h>

/* sendmsg(2)を使ってfdを送信する */
static void wyslij(int socket, int fd) {
    struct msghdr msg = { 0 };
    char buf[CMSG_SPACE(sizeof(fd))];
    memset(buf, '\0', sizeof(buf));
    struct iovec io = { .iov_base = "ABC", .iov_len = 3 };

    msg.msg_iov = &io;
    msg.msg_iovlen = 1;
    msg.msg_control = buf;
    msg.msg_controllen = sizeof(buf);

    struct cmsghdr * cmsg = CMSG_FIRSTHDR(&msg); // cmsghdrへのポインターを返す
    cmsg->cmsg_level = SOL_SOCKET;
    cmsg->cmsg_type = SCM_RIGHTS; // これが肝でファイル記述子のセットを送受信するためのタイプ
    cmsg->cmsg_len = CMSG_LEN(sizeof(fd));

    *((int *) CMSG_DATA(cmsg)) = fd; // cmsghdrのデータ部分へのポインターを返す

    msg.msg_controllen = CMSG_SPACE(sizeof(fd));

    if (sendmsg(socket, &msg, 0) < 0)
        printf("Failed to send message\n");
}

/* recvmsg(2)を使ってfdを受信する */
static int odbierz(int socket) {
    struct msghdr msg = {0};

    char m_buffer[256];
    struct iovec io = { .iov_base = m_buffer, .iov_len = sizeof(m_buffer) };
    msg.msg_iov = &io;
    msg.msg_iovlen = 1;

    char c_buffer[256];
    msg.msg_control = c_buffer;
    msg.msg_controllen = sizeof(c_buffer);

    if (recvmsg(socket, &msg, 0) < 0)
        printf("Failed to receive message\n");

    struct cmsghdr * cmsg = CMSG_FIRSTHDR(&msg); // cmsghdrへのポインターを返す

    unsigned char * data = CMSG_DATA(cmsg); // cmsghdrのデータ部分へのポインターを返す

    int fd = *((int*) data); // fdを取得
    printf("Extracted fd %d\n", fd);

    return fd;
}

int main(int argc, char **argv) {
    const char *filename = "test.txt";

    int sv[2];
    // socketpair(2) 接続されたソケットのペアを作成する 
    if (socketpair(AF_UNIX, SOCK_DGRAM, 0, sv) != 0)
        printf("Failed to create Unix-domain socket pair\n");

    int pid = fork();
    if (pid > 0)  { // 親プロセス
        printf("Parent-> pid: %d\n", getpid());
        close(sv[1]);

        int fd = open(filename, O_RDONLY);

        // 子プロセスへUnixドメインソケット経由でfdを渡す
        wyslij(sv[0], fd);

        sleep(1);
    } else { // 子プロセス
        printf("Child-> pid: %d, ppid: %d\n", getpid(), getppid());
        close(sv[0]); 

        sleep(1);

        int fd = odbierz(sv[1]);
        printf("Read %d!\n", fd);
        char buffer[256];
        ssize_t nbytes;
        while ((nbytes = read(fd, buffer, sizeof(buffer))) > 0)
            write(1, buffer, nbytes);
        close(fd);
    }
    return 0;
}

技術的な仕組み

www.amazon.co.jp

を参考に読んでいくと技術的にはオープンしたファイルテーブルのエントリへのポインタをあるプロセスから別のプロセスへ渡すことで実現されているとのこと。単純な数値の受け渡しではないのがポイントです。ちなみに送り元である親プロセスがfdをcloseしても子プロセスのfdはクローズされないようでした。

コード内コメントにあるSCM_RIGHTSの詳細は以下に記載されていました

       SCM_RIGHTS
              他のプロセスでオープンされたファイルディスクリプタのセットを送受信する。 データ部分
              にファイルディスクリプタの整数配列が入っている。     渡されたファイルディスクリプタ
              は、あたかも dup(2)  で生成されたかのように振る舞う。

man7.org

まとめ

何が嬉しいのかはbkブログでの言及があるようなセキュリティの場面だったりが多いのでしょうか?(具体的な利用例はあまり見たことないので詳しい方教えて欲しいです!!)