【问题标题】:Create a directory and return a dirfd with `open`创建一个目录并使用 `open` 返回一个 dirfd
【发布时间】:2021-03-17 23:43:19
【问题描述】:

我想在 C 中创建一个文件树并避免可能的竞争条件。我的意图是使用open(3) 创建根目录,open 将返回一个 目录文件描述符 (dirfd),我将把它提供给后续的openat(3)/ mkdirat(3) 调用创建树。

int dirfd = open(path, O_DIRECTORY | O_CREAT | O_RDONLY, mode);

执行此操作的常用方法是将第一个 open 调用替换为 mkdir(3),但这不会打开目录,因此很不礼貌。

mkdir(path, mode);
DIR *dirp = opendir(path);

这可行吗?我所有的测试都返回EISDIRENOTDIR。此外,open(2) 的手册页指出:

当标志中同时指定O_CREATO_DIRECTORY 且路径名指定的文件不存在时,open() 将创建一个常规文件(即,O_DIRECTORY 被忽略)。

在 Linux 5.09 中似乎仍然如此。我想知道这是否可以修复,或者它是否永远是界面的一部分。

这是一个示例程序,用于尝试使用open 创建和打开一个目录:

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

int main(void) {
    /* const char *path = "directory"; */
    /* int dirfd = openat(AT_FDCWD, path, O_DIRECTORY | O_CREAT | O_RDONLY, 0755); */
    const char *path = "/tmp/test";
    int dirfd = open(path, O_DIRECTORY | O_CREAT | O_RDONLY, 0755);
    if(dirfd < 0) {
        fprintf(stderr, "openat(%s): %s\n", topdir, strerror(errno));
        return EXIT_FAILURE;
    }
    close(dirfd);
    return EXIT_SUCCESS;
}

此外,手册页中的这些行似乎相互矛盾:

  • open(3):

    如果设置了O_CREATO_DIRECTORY,并且请求的访问模式既不是O_WRONLY 也不是O_RDWR,则结果未指定。

  • open(2):

    EISDIR路径名是指目录,请求的访问涉及写入(即设置了O_WRONLYO_RDWR)。

【问题讨论】:

    标签: c linux directory posix system-calls


    【解决方案1】:

    man 2 open 手册页(链接到 man7.org 上最新的 Linux 手册页)在 Bugs 部分明确指出使用 O_CREAT | O_DIRECTORY 将创建一个常规文件。还有这个discussion

    更重要的是,即使它确实成功了,其他进程仍然可以在创建成功后立即访问该目录,甚至在调用返回到您的程序之前。因此,您担心的比赛窗口无论如何都会存在。

    常见的模式是在同一目录中创建一个临时目录,该目录具有足够随机的名称(以 . 开头,以从典型的文件和目录列表中省略它)仅供当前用户访问;然后填充它;然后调整其访问方式;然后将其重命名为最终名称。

    这不会让其他进程无法访问该目录,但这种模式被认为足够安全。

    这是一个执行此操作的示例程序:

    #define  _POSIX_C_SOURCE  200809L
    #define  _ATFILE_SOURCE
    #define  _GNU_SOURCE
    #include <stdlib.h>
    #include <inttypes.h>
    #include <unistd.h>
    #include <sys/stat.h>
    #include <sys/random.h>
    #include <sys/syscall.h>
    #include <fcntl.h>
    #include <signal.h>
    #include <time.h>
    #include <string.h>
    #include <stdio.h>
    #include <errno.h>
    
    #ifndef  RENAME_NOREPLACE
    #define  RENAME_NOREPLACE  (1 << 0)
    static inline int renameat2(int olddirfd, const char *oldpath,
                                int newdirfd, const char *newpath, unsigned int flags)
    {
        int  retval = syscall(SYS_renameat2, olddirfd, oldpath, newdirfd, newpath, flags);
        if (!retval)
            return 0;
        errno = -retval;
        return -1;
    }
    #endif
    
    /* Xorshift64* pseudo-random number generator.
    */
    static uint64_t  prng_state = 0; /* unseeded */
    
    static uint64_t  prng_u64(void)
    {
        uint64_t  state = prng_state;
        state ^= state >> 12;
        state ^= state << 25;
        state ^= state >> 27;
        prng_state = state;
        return state * UINT64_C(2685821657736338717);
    }
    
    static uint64_t  prng_randomize(void)
    {
        uint64_t  state;
    
        /* Use Linux-specific getrandom() call. */
        {
            ssize_t   n;
            do {
                n = getrandom(&state, sizeof state, 0);
            } while (n == -1 && errno == EINTR);
            if (n == (ssize_t)sizeof state && state != 0) {
                prng_state = state;
                return state;
            }
        }
    
        /* Fall back to using time as a seed. */
        {
            struct timespec  now;
            size_t           rounds = 250;
    
            clock_gettime(CLOCK_REALTIME, &now);
            state = (uint64_t)now.tv_sec * UINT64_C(270547637)
                  ^ (uint64_t)now.tv_nsec * UINT64_C(90640031)
                  ^ (uint64_t)getpid() * UINT64_C(4758041);
    
            clock_gettime(CLOCK_THREAD_CPUTIME_ID, &now);
            state ^= (uint64_t)now.tv_sec * UINT64_C(3266177)
                   ^ (uint64_t)now.tv_nsec * UINT64_C(900904331);
    
            clock_gettime(CLOCK_MONOTONIC, &now);
            state ^= (uint64_t)now.tv_sec * UINT64_C(24400169)
                   ^ (uint64_t)now.tv_nsec * UINT64_C(1926466307);
    
            /* Make sure state is nonzero */
            state += (!state);
    
            /* Mix it a bit, to make it less predictable. */
            while (rounds-->0) {
                state ^= state >> 12;
                state ^= state << 25;
                state ^= state >> 27;
            }
    
            prng_state = state;
            return state;
        }
    }
    
    static const char base64[64] = {
        '0', '1', '2', '3', '4', '5', '6', '7', '8', '9',
        'A', 'B', 'C', 'D', 'E', 'F', 'G', 'H', 'I', 'J',
        'K', 'L', 'M', 'N', 'O', 'P', 'Q', 'R', 'S', 'T',
        'U', 'V', 'W', 'X', 'Y', 'Z', 'a', 'b', 'c', 'd',
        'e', 'f', 'g', 'h', 'i', 'j', 'k', 'l', 'm', 'n',
        'o', 'p', 'q', 'r', 's', 't', 'u', 'v', 'w', 'x',
        'y', 'z', '-', '_'
    };
    
    /* Create a new directory atomically, returning an open descriptor to it.
       name must be non-empty, and not contain a slash.
    */
    int mkdiratfd(const int atfd, const char *dirpath, const char *name, const mode_t mode)
    {
        char    buf[32];
        mode_t  curr_umask;
        int     atdirfd, fd;
    
        /* New directory name cannot be NULL, empty, or contain a slash. */
        if (!name || !*name || strchr(name, '/')) {
            errno = EINVAL;
            return -1;
        }
    
        /* If dirpath is NULL or empty, we use "." for it. */
        if (!dirpath || !*dirpath)
            dirpath = ".";
    
        /* Open a handle to the target directory. */
        do {
            atdirfd = openat(atfd, dirpath, O_PATH | O_DIRECTORY | O_CLOEXEC);
        } while (atdirfd == -1 && errno == EINTR);
        if (atdirfd == -1) {
            return -1;
        }
    
        /* Obtain current umask. */
        curr_umask = umask(0); umask(curr_umask);
    
        /* Make sure our PRNG has been seeded. */
        if (!prng_state)
            prng_randomize();
    
        /* Create a temporary random name for the directory. */
        while (1) {
            char *ptr = buf;
    
            /* Start with a dot, making it "hidden". */
            *(ptr++) = '.';
    
            /* Use 2*10 = 20 random characters (120 bits) */
            for (int k = 2; k > 0; k--) {
                uint64_t  u = prng_u64();
                int       n = 10;
                while (n-->0) {
                    *(ptr++) = base64[u & 63];
                    u >>= 6;
                }
            }
    
            /* Terminate name */
            *ptr = '\0';
    
            /* Create the temporary directory with access only to current user. */
            if (mkdirat(atdirfd, buf, 0700) == -1) {
                const int  saved_errno = errno;
                if (errno == EINTR || errno == EEXIST)
                    continue;
                /* Actual error. */
                close(atdirfd);
                errno = saved_errno;
                return -1;
            }
    
            /* Successfully created. */
            break;
        }
    
        /* Open the temporary directory. */
        do {
            fd = openat(atdirfd, buf, O_PATH | O_DIRECTORY | O_CLOEXEC);
        } while (fd == -1 && errno == EINTR);
        if (fd == -1) {
            const int  saved_errno = errno;
            unlinkat(atdirfd, buf, AT_REMOVEDIR);
            close(atdirfd);
            errno = saved_errno;
            return -1;
        }
    
        /*
         * Note: Other actions, like file creation, etc.
         *       should be done at this stage.
        */
    
        /* Update directory owner group here, if necessary. */
    
        /* Update proper access mode. */
        if (fchmodat(atdirfd, buf, mode & (~curr_umask), 0) == -1) {
            const int  saved_errno = errno;
            close(fd);
            unlinkat(atdirfd, buf, AT_REMOVEDIR);
            close(atdirfd);
            errno = saved_errno;
            return -1;
        }
    
        /* Rename directory. */
        if (renameat2(atdirfd, buf, atdirfd, name, RENAME_NOREPLACE) == -1) {
            const int  saved_errno = errno;
            close(fd);
            unlinkat(atdirfd, buf, AT_REMOVEDIR);
            close(atdirfd);
            if (saved_errno == EPERM)
                errno = EEXIST;
            else
                errno = saved_errno;
            return -1;
        }
    
        /* Success. */
        close(atdirfd);
        return fd;
    }
    
    int main(int argc, char *argv[])
    {
        int fd;
    
        if (argc != 2 || !strcmp(argv[1], "-h") || !strcmp(argv[1], "--help")) {
            const char *argv0 = (argc > 0 && argv && argv[0] && argv[0][0]) ? argv[0] : "(this)";
            fprintf(stderr, "\n");
            fprintf(stderr, "Usage: %s [ -h | --help ]\n", argv0);
            fprintf(stderr, "       %s NAME\n", argv0);
            fprintf(stderr, "\n");
            fprintf(stderr, "This program creates directory NAME in the current directory.\n");
            fprintf(stderr, "\n");
            return EXIT_FAILURE;
        }
    
        fd = mkdiratfd(AT_FDCWD, NULL, argv[1], 0755);
        if (fd == -1) {
            fprintf(stderr, "%s: %s.\n", argv[1], strerror(errno));
            return EXIT_FAILURE;
        }
    
        return EXIT_SUCCESS;
    }
    

    请注意,如果 C 库没有公开它,这将通过原始系统调用使用 renameat2()。 (它在 2.28 中被添加到 glibc,但自 3.15 起被 Linux 内核支持)。

    如果您仍然担心,偏执模式是创建一个临时目录来保存临时目录。打开将成为最终目录的内部目录后,将外部临时目录的模式更改为零,以停止对内部树的遍历。创建者仍然可以通过打开的目录描述符访问内部树。该目录仍然可以重命名,因为它们位于同一个文件系统上。

    我个人不会打扰,因为使用临时名称,并且仅在完成后重命名目录(这是 Linux 中的许多应用程序所做的)就足够安全了。

    【讨论】:

      猜你喜欢
      • 1970-01-01
      • 1970-01-01
      • 1970-01-01
      • 1970-01-01
      • 2015-08-12
      • 1970-01-01
      • 2021-09-19
      • 1970-01-01
      • 1970-01-01
      相关资源
      最近更新 更多