C的打开覆盖文件将被打开,并且fdopen返回NULL

时间:2018-07-03 18:15:38

标签: c fopen

我正在尝试编写一个安全版本的fopen,以防止打开符号链接的文件。以下是我摘自here的代码。

/* 
    Secure fopen 
*/
enum { FILE_MODE = 0600 };

FILE *secure_fopen(char *filename, char* mode)
{
    int fd;
    FILE *f;

    unlink(filename);

    if (strncmp(mode, "w", 1) == 0) {
        fd = open(filename, O_WRONLY|O_CREAT|O_EXCL, FILE_MODE);
    }
    else if (strncmp(mode, "r", 1) == 0) {
        fd = open(filename, O_RDONLY|O_CREAT|O_EXCL);
    }
    else {
        fd = open(filename, O_RDWR|O_CREAT|O_EXCL, FILE_MODE);
    }

    if (fd == -1) {
        perror("Failed to open the file");
        return NULL; 
    }
    /* Get a FILE*, as they are easier and more efficient than file descriptors */
    f = fdopen(fd, mode);
    if (f == NULL) {
        perror("Failed to associate file descriptor with a stream");
        return NULL; 
    }
    return f;
}

此代码有两个问题:一-它覆盖文件名所指向的文件,二-返回NULL,但是NULL文件指针不会在最终检查中被捕获:

if (f == NULL) {
    perror("Failed to associate file descriptor with a stream");
    return NULL; 
}

有人对这两种情况的发生有任何见识吗?

1 个答案:

答案 0 :(得分:2)

首先,O_CREAT创建一个文件(如果它不存在),而O_CREAT|O_EXCL创建一个文件,如果它已经不存在,则失败。

第二,(strncmp(mode, "w", 1) == 0)等效于(mode[0] == 'w'),这可能不是您想要的。您可能是用(strchr(mode, "w"))来代替的。

请考虑以下实现(完整的示例程序):

#define _POSIX_C_SOURCE 200809L
#include <stdlib.h>
#include <unistd.h>
#include <sys/types.h>
#include <sys/stat.h>
#include <fcntl.h>
#include <string.h>
#include <stdio.h>
#include <errno.h>

/* Internal flags used by custom_fopen(): */
#define  FM_R        (1<<0)  /* r, w+: For reading */
#define  FM_W        (1<<1)  /* w, r+: For writing */
#define  FM_TRUNC    (1<<2)  /* w, w+: Truncate */
#define  FM_CREAT    (1<<3)  /* w, r+: Create if necessary */
#define  FM_EXCL     (1<<4)  /* x: Fail if already exists */
#define  FM_APPEND   (1<<5)  /* a: Append */
#define  FM_CLOEXEC  (1<<6)  /* e: Close-on-exec() */
#define  FM_SYMLINK  (1<<7)  /* s: Fail if last path component is a symlink */
#define  FM_RW       (FM_R | FM_W) /* r+, w+ */

FILE *custom_fopen(const char *path, const char *mode)
{
    const char *fdmode;
    int         fm, flags, fd, saved_errno;
    FILE       *ret;

    if (!path || !*path || !mode) {
        errno = EINVAL;
        return NULL;
    }

    switch ((strchr(mode, 'r') ? 1 : 0) +
            (strchr(mode, 'w') ? 2 : 0) +
            (strchr(mode, 'a') ? 4 : 0) +
            (strchr(mode, '+') ? 8 : 0)) {
    case 1:  fdmode = "r";  fm = FM_R;                         break;
    case 2:  fdmode = "w";  fm = FM_W  | FM_CREAT | FM_TRUNC;  break;
    case 4:  fdmode = "a";  fm = FM_W  | FM_CREAT | FM_APPEND; break;
    case 9:  fdmode = "r+"; fm = FM_RW | FM_CREAT;             break;
    case 10: fdmode = "w+"; fm = FM_RW | FM_CREAT | FM_TRUNC;  break;
    case 12: fdmode = "a+"; fm = FM_RW | FM_CREAT | FM_APPEND; break;
    default:
        /* Invalid combination of 'r', 'w', 'a', and '+'. */
        errno = EINVAL;
        return NULL;
    }

    if (strchr(mode, 'x')) {
        if (fm & FM_CREAT)
            fm |= FM_EXCL;
        else {
            /* 'rx' does not make sense, and would not work anyway. */
            errno = EINVAL;
            return NULL;
        }
    }

    if (strchr(mode, 'e'))
        fm |= FM_CLOEXEC;

    if (strchr(mode, 's'))
        fm |= FM_SYMLINK;

    /* Verify 'mode' consists of supported characters only. */
    if (strlen(mode) != strspn(mode, "rwa+xesb")) {
        errno = EINVAL;
        return NULL;
    }

    /* Map 'fm' to 'flags' for open(). */
    switch (fm & FM_RW) {
    case FM_R:  flags = O_RDONLY; break;
    case FM_W:  flags = O_WRONLY; break;
    case FM_RW: flags = O_RDWR;   break;
    default:
        errno = EINVAL;
        return NULL;
    }
    if (fm & FM_TRUNC)   flags |= O_TRUNC;
    if (fm & FM_CREAT)   flags |= O_CREAT;
    if (fm & FM_EXCL)    flags |= O_EXCL;
    if (fm & FM_APPEND)  flags |= O_APPEND;
    if (fm & FM_CLOEXEC) flags |= O_CLOEXEC;
    if (fm & FM_SYMLINK) flags |= O_NOFOLLOW;

    /* Open the file. If we might create it, use mode 0666 like fopen() does. */
    if (fm & FM_CREAT)
        fd = open(path, flags, 0666);
    else
        fd = open(path, flags);

    /* Failed? */
    if (fd == -1)
        return NULL; /* errno set by open() */

    /* Convert the file descriptor to a file handle. */
    ret = fdopen(fd, fdmode);
    if (ret)
        return ret;

    /* Failed. Remember the reason for the failure. */
    saved_errno = errno;

    /* If we created or truncated the file, unlink it. */
    if (fm & (FM_EXCL | FM_TRUNC))
        unlink(path);

    /* Close the file descriptor. */
    close(fd);

    /* Return, recalling the reason for the failure. */
    errno = saved_errno;
    return NULL;
}

int main(int argc, char *argv[])
{
    FILE *handle;

    if (argc != 3 || !strcmp(argv[1], "-h") || !strcmp(argv[1], "--help")) {
        fprintf(stderr, "\n");
        fprintf(stderr, "Usage: %s [ -h | --help ]\n", argv[0]);
        fprintf(stderr, "       %s PATH MODE\n", argv[0]);
        fprintf(stderr, "\n");
        return EXIT_FAILURE;
    }

    handle = custom_fopen(argv[1], argv[2]);
    if (!handle) {
        const int err = errno;
        fprintf(stderr, "custom_fopen(\"%s\", \"%s\") == NULL, errno = %d: %s.\n",
                        argv[1], argv[2], err, strerror(err));
        return EXIT_FAILURE;
    }

    if (fclose(handle)) {
        const int err = errno;
        fprintf(stderr, "fclose(custom_fopen(\"%s\", \"%s\")) failed, errno = %d: %s.\n",
                        argv[1], argv[2], err, strerror(err));
        return EXIT_FAILURE;
    }

    printf("custom_fopen(\"%s\", \"%s\"): Success.\n", argv[1], argv[2]);
    return EXIT_SUCCESS;
}

#define _POSIX_C_SOURCE 200809L告诉您的C库(至少与GNU C兼容的C库)公开POSIX.1-2008功能(如open())。

rwar+w+a+模式的行为如{{3 }}。其中至少一个必须位于mode中。 (不过,+不需要立即跟在字母后面。)

上述实现还支持b(对每个POSIX不执行任何操作),x(如果创建新文件但已存在,则失败),s(如果文件名部分不成功)的路径是符号链接)和e(在exec处关闭描述符,而不是将其泄漏给子进程)。

第一个switch语句处理主要模式,而忽略字符顺序。它实际上检查rwa+中四个字符mode中的哪个,并且仅接受理智的组合。仅当mode包含'c'时,man 3 fopen调用返回非零指针(逻辑为真)。

它后面的if子句在模式下检测到x。不允许将xr组合使用,因为这是没有意义的。 (POSIX.1表示open()O_RDONLY | O_EXCL的行为是不确定的。)

(strlen(mode) == strspn(mode, "rwa+xesb"))验证mode仅包含字母rwa+xesb;它们可以重复,也可以以任何顺序重复。此检查拒绝不支持的字符。

第二条switch语句和if子句将fm映射到flags。我们这样做是因为O_常量可能不是单个位,这意味着诸如(flags & O_RDONLY)(flags & O_WRONLY)(flags & O_RDWR)之类的测试不可靠,实际上将不起作用人们可能期望的方式。相反,我们使用fm和我们自己的单一位FM_常量(可以将其视为掩码),然后稍后将它们映射到flags的相应值。 (简而言之,fm跟踪我们想要的功能,并且我们仅将相应的标志集分配给flags,而不会检查 flags。)

如果我们可以创建文件,请使用模式0666rw-rw-rw-),该模式由用户的umask修改。 (通常,普通用户的umask为002007022077,这导致新文件的模式为0664({{1 }}},rw-rw-r--0660),rw-rw----0644)或rw-r--r--0600)。 这正是rw-------所做的。

拥有打开的文件描述符后,我们仍然必须将其与流句柄关联。我们使用strchr(mode, 'c')完成。请注意,我们在第一个fopen()语句中为此调用确定了正确的mode。如果此调用成功,则流将“拥有”文件描述符,而我们要做的就是返回返回的流句柄switch

如果fdopen()失败,我们需要关闭文件描述符。我们也可能决定删除/取消链接文件。如果我们确定文件以前不存在(fdopen()),或者如果我们将其截断了(ew),那么上面的代码将删除该文件,因为该文件可能包含的数据反正迷路了。

测试程序采用两个命令行参数:路径或文件名以及模式字符串。该程序进行w+调用,并报告结果。 (如果custom_fopen(pathorfilename, modestring)调用成功,它还会检查相应的custom_fopen()调用是否成功,因为有时与文件描述符有关的问题(或fclose() / {{1}的不兼容模式标志}调用)只能在完成的第一个操作或流关闭时观察到。

我仅对上述功能和程序进行了少量测试,并且总是可能出现错误。如果您发现错误或代码有问题,请在评论中告知我,以便在必要时进行验证和修复。