sendmsg() で送られた fd の持つロックの所有権は保持されるのか

いくつかの *nix では sendmsg() / recvmsg() システムコールで他プロセスに fd を送信できたりします。送信元のプロセスでファイルロックを行った場合、送信先で同じ fd にファイルロックを行うとブロックするのでしょうか。出勤途中に猛烈気になったので確かめてみました。

追記: うぎゃー、unix(7) に書いてありました。

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

これを見落としてなければ、こんなくだらないプログラムを書かなくて済んだものを...。
ちなみに、SCM_CREDENTIALSというのもあるようです。Win32でいうとセキュリティトークンを送信できるようなものなんだろうか。

server.c:

#include <unistd.h>
#include <sys/types.h>
#include <sys/socket.h>
#include <sys/un.h>
#include <netinet/in.h>
#include <fcntl.h>
#include <errno.h>
#include <string.h>
#include <stdio.h>

#define _UNIX_PATH_MAX sizeof(((struct sockaddr_un *)0)->sun_path)

const char *prog_name;
const char *socket_path = "/tmp/aho";

int app_init(int argc, char** argv)
{
    prog_name = argv[0] ? strrchr(argv[0], '/'): "\0(none)";
    if (!prog_name)
        prog_name = argv[0];
    else
        prog_name++;

    if (argc > 1) {
        size_t socket_path_len;
        socket_path = argv[1];
        socket_path_len = strlen(socket_path);

        if (socket_path_len >= _UNIX_PATH_MAX) {
            fprintf(stderr, "%s: socket path too long (%d bytes)\n",
                    prog_name, socket_path_len);
            return 1;
        }
    }

    return 0;
}

int app_run()
{
    int s = -1, cs = -1, ffd = -1;
    int err = 0;

    s = socket(PF_UNIX, SOCK_SEQPACKET, 0);
    if (s < 0) {
        err = errno;
        goto out;
    }

    {
        struct sockaddr_un saddr = { AF_UNIX };
        strncpy(saddr.sun_path, socket_path, _UNIX_PATH_MAX - 1);
        saddr.sun_path[_UNIX_PATH_MAX - 1] = '\0';

        if (bind(s, (struct sockaddr *)&saddr, sizeof(saddr))) {
            err = errno;
            goto out;
        }
    }

    if (listen(s, 10)) {
        err = errno;
        goto out;
    }

    for (;; /* XXX: DON'T REPEAT AFTER ME */
           (cs >= 0 ? (close(cs), cs = -1): 0),
           (ffd >= 0 ? (close(ffd), ffd = -1): 0)) {
        struct sockaddr_un csaddr;
        socklen_t sl = sizeof(csaddr);

        cs = accept(s, (struct sockaddr *)&csaddr, &sl);
        if (cs < 0) {
            err = errno;
            goto out;
        }

        printf("Cnnection accepted: socket = #%d.\n", cs);

        printf("Open file.\n");
        ffd = open("/tmp/test.txt", O_RDWR);

        printf("Lock file (LOCK_EX).\n");
        if (flock(ffd, LOCK_EX)) {
            fprintf(stderr, "%s: an error occurred on flock(): %s\n",
                    prog_name, strerror(errno));
            continue;
        }

        {
            struct msghdr msg = { 0 };
            static const size_t num_fds = 1;
            unsigned char cmsgbuf[CMSG_SPACE(sizeof(int) * num_fds)];
            msg.msg_control = cmsgbuf;
            msg.msg_controllen = sizeof(cmsgbuf);

            {
                struct cmsghdr *cmsg = CMSG_FIRSTHDR(&msg);
                cmsg->cmsg_level = SOL_SOCKET;
                cmsg->cmsg_type = SCM_RIGHTS;
                cmsg->cmsg_len = CMSG_LEN(sizeof(int) * num_fds);
                int *fd_list = (int *)CMSG_DATA(cmsg);

                fd_list[0] = ffd;

                /* finally fix up the controllen */
                msg.msg_controllen = cmsg->cmsg_len;
            }

            if (sendmsg(cs, &msg, 0)) {
                fprintf(stderr, "%s: an error occurred on sendmsg(): %s\n",
                        prog_name, strerror(errno));
                continue;
            }
        
            printf("Message sent.\n");

            printf("Try locking file again.\n");
            if (flock(ffd, LOCK_EX)) {
                fprintf(stderr, "%s: an error occurred on flock(): %s\n",
                        prog_name, strerror(errno));
                continue;
            }
        }
    }
out:
    if (s >= 0)
        close(s);
    if (err)
        fprintf(stderr, "%s: %s\n", prog_name, strerror(err));
    return err;
}

int main(int argc, char** argv)
{
    int err;

    err = app_init(argc, argv);
    if (err)
        return err;

    return app_run();
}

client.c:

#include <unistd.h>
#include <sys/types.h>
#include <sys/socket.h>
#include <sys/un.h>
#include <netinet/in.h>
#include <fcntl.h>
#include <errno.h>
#include <string.h>
#include <stdio.h>
#include <stdlib.h>

#define _UNIX_PATH_MAX sizeof(((struct sockaddr_un *)0)->sun_path)

const char *prog_name;
const char *socket_path = "/tmp/aho";

int app_init(int argc, char** argv)
{
    prog_name = argv[0] ? strrchr(argv[0], '/'): "\0(none)";
    if (!prog_name)
        prog_name = argv[0];
    else
        prog_name++;

    if (argc > 1) {
        size_t socket_path_len;
        socket_path = argv[1];
        socket_path_len = strlen(socket_path);

        if (socket_path_len >= _UNIX_PATH_MAX) {
            fprintf(stderr, "%s: socket path too long (%d bytes)\n",
                    prog_name, socket_path_len);
            return 1;
        }
    }

    return 0;
}

int app_run()
{
    int s = -1, ffd = -1;
    int err = 0;

    s = socket(PF_UNIX, SOCK_SEQPACKET, 0);
    if (s < 0) {
        err = errno;
        goto out;
    }

    {
        struct sockaddr_un saddr = { AF_UNIX };
        strncpy(saddr.sun_path, socket_path, _UNIX_PATH_MAX - 1);
        saddr.sun_path[_UNIX_PATH_MAX - 1] = '\0';

        if (connect(s, (struct sockaddr *)&saddr, sizeof(saddr))) {
            err = errno;
            goto out;
        }
    }

    {
        struct msghdr msg;
        size_t cmsgbuf_alloc = 16;
        void *cmsgbuf = NULL;

        /* Damn Posix!! msg_controllen should be set to a proper value
         * even if it fails. do some heuristics unwillingly */
        for (;;) {
            size_t new_cmsgbuf_alloc = cmsgbuf_alloc << 1;
            void *new_cmsgbuf;

            if (new_cmsgbuf_alloc < cmsgbuf_alloc) {
                if (cmsgbuf)
                    free(cmsgbuf);
                err = ENOMEM;
                goto out;
            }

            new_cmsgbuf = realloc(cmsgbuf, new_cmsgbuf_alloc);
            if (!new_cmsgbuf) {
                if (cmsgbuf)
                    free(cmsgbuf);
                err = ENOMEM;
                goto out;
            }

            cmsgbuf = new_cmsgbuf;
            cmsgbuf_alloc = new_cmsgbuf_alloc;

            msg.msg_name = NULL;
            msg.msg_iov = NULL;
            msg.msg_iovlen = 0;
            msg.msg_control = cmsgbuf;
            msg.msg_controllen = cmsgbuf_alloc;

            if (!recvmsg(s, &msg, MSG_PEEK))
                break;

            if (errno != EMSGSIZE) {
                err = errno;
                goto out;
            }
        }

        if (recvmsg(s, &msg, 0)) {
            err = errno;
            free(cmsgbuf);
            goto out;
        }

        {
            struct cmsghdr *cmsg;
            for (cmsg = CMSG_FIRSTHDR(&msg);
                    cmsg; cmsg = CMSG_NXTHDR(&msg, cmsg)) {
                switch (cmsg->cmsg_level) {
                case  SOL_SOCKET:
                    if (cmsg->cmsg_type == SCM_RIGHTS) {
                        int *fd_list = (int *)CMSG_DATA(cmsg);
                        size_t num_fds = (cmsg->cmsg_len - CMSG_LEN(0)) /
                                sizeof(fd_list[0]);
                        size_t i;

                        printf("Received fd(s): %d\n", num_fds);
                        for (i = 0; i < num_fds; i++) {
                            unsigned char buf[4];
                            ssize_t nbytesread;

                            printf("Trying to read %zu bytes from fd #%d\n",
                                    sizeof(buf), fd_list[i]);
                            nbytesread = read(fd_list[i], buf, sizeof(buf));
                            if (nbytesread < 0) {
                                err = errno; 
                                free(cmsgbuf);
                                goto out;
                            }

                            printf("%zd bytes read.\n", nbytesread);

                            printf("Trying to flock() on fd #%d\n",
                                    fd_list[i]);
                            if (flock(fd_list[i], LOCK_EX | LOCK_NB)) {
                                err = errno;
                                free(cmsgbuf);
                                goto out;
                            }

                            printf("flock() succeeded.\n");
                            printf("Sleep 5 seconds.\n");
                            sleep(5);
                            printf("Close fd #%d.\n", fd_list[i]);

                            if (close(fd_list[i])) {
                                fprintf(stderr, "%s: warning - close on fd #%d"
                                        " failed (%s)\n", prog_name,
                                        fd_list[i], strerror(errno));
                            }
                        }
                    }
                    break;
                default:
                    fprintf(stderr, "%s: warning - irrelevant data (level: %d)"
                            " discarded\n", prog_name, cmsg->cmsg_level);
                    break;
                }
            }
        }

        if (cmsgbuf)
            free(cmsgbuf);
    }
out:
    if (s >= 0)
        close(s);
    if (err)
        fprintf(stderr, "%s: %s\n", prog_name, strerror(err));
    return err;
}

int main(int argc, char** argv)
{
    int err;

    err = app_init(argc, argv);
    if (err)
        return err;

    return app_run();
}

someone.c:

#include <unistd.h>
#include <sys/types.h>
#include <time.h>
#include <fcntl.h>
#include <errno.h>
#include <string.h>
#include <stdio.h>
#include <stdlib.h>

const char *prog_name;

int app_init(int argc, char** argv)
{
    prog_name = argv[0] ? strrchr(argv[0], '/'): "\0(none)";
    if (!prog_name)
        prog_name = argv[0];
    else
        prog_name++;
    return 0;
}

int app_run()
{
    int fd = -1;
    int err = 0;
    time_t tm;

    printf("[%s] Open file.\n", prog_name);
    fd = open("/tmp/test.txt", O_RDWR);
    if (fd < 0) {
        err = errno;
        goto out;
    }

    printf("[%s] Try locking.\n", prog_name);
    tm = time(NULL);
    if (flock(fd, LOCK_EX)) {
        err = errno;
        goto out;
    }

    if (time(NULL) - tm >= 3)
        printf("[%s] I was blocked, eh?\n", prog_name);
    else
        printf("[%s] Huh?\n", prog_name);

    flock(fd, LOCK_UN);
out:
    if (fd >= 0)
        close(fd);
    if (err)
        fprintf(stderr, "%s: %s\n", prog_name, strerror(err));
    return err;
}

int main(int argc, char** argv)
{
    int err;

    err = app_init(argc, argv);
    if (err)
        return err;

    return app_run();
}

実行:

% ./server &
% ./client &; sleep 1; ./someone

結果:

[2] 13363
Cnnection accepted: socket = #4.
Open file.
Lock file (LOCK_EX).
Message sent.
Try locking file again.
Received fd(s): 1
Trying to read 4 bytes from fd #5
4 bytes read.
Trying to flock() on fd #5
flock() succeeded.
Sleep 5 seconds.
[someone] Open file.
[someone] Try locking.
Close fd #5.
[someone] I was blocked, eh?
[2]  + done       ./client

というわけで、プロセスが変わってもロックの所有権を共有するようでした。

追記: これは Linux の場合。ほかの OS (Solaris とか) では違う可能性もある。