Будет открыт файл перезаписи C, и fdopen вернет NULL - PullRequest
0 голосов
/ 03 июля 2018

Я пытаюсь написать безопасную версию fopen, которая предотвращает открытие символически связанных файлов. Ниже мой код взят из здесь .

/* 
    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 голосов
/ 03 июля 2018

Сначала 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) раскрыть функции POSIX.1-2008 (например, open()).

Поведение режимов r, w, a, r+, w+ и a+ такое же, как описано в man 3 fopen. По крайней мере один из них должен быть в mode. (+ не обязательно должен следовать сразу за буквой.)

Вышеприведенная реализация дополнительно поддерживает b (ничего не делает, для POSIX), x (ошибка при создании нового файла, но он уже существует), s (ошибка, если часть имени пути является символической ссылкой ) и e (закройте дескриптор в exec, не передавая его дочерним процессам).

Первый оператор switch обрабатывает основной режим, игнорируя порядок символов. По сути, он проверяет, какой из четырех символов rwa+ существует в mode, и принимает только разумные комбинации. Вызовы strchr(mode, 'c') возвращают ненулевой указатель (логическое значение true) тогда и только тогда, когда mode содержит 'c'.

Предложение if, следующее за ним, обнаруживает x в режиме. Комбинация x и r не допускается, потому что это не имеет смысла. (POSIX.1 говорит, что поведение с open() с O_RDONLY | O_EXCL не определено.)

(strlen(mode) == strspn(mode, "rwa+xesb")) проверяет, что mode состоит только из букв r, w, a, +, x, e, s и b; они могут повторяться или в любом порядке. Эта проверка отклоняет неподдерживаемые символы.

Второй оператор switch и предложения if отображают fm в flags. Мы делаем это, потому что O_ константы могут не быть единичными битами, что означает, что тесты типа (flags & O_RDONLY), (flags & O_WRONLY) и (flags & O_RDWR) не являются надежными и на самом деле не будут работать так, как можно было ожидать. Вместо этого мы используем fm и наши собственные однобитовые константы FM_, которые мы можем рассматривать как маски, и просто сопоставляем их с соответствующими значениями flags позже. (Проще говоря, fm отслеживает нужные нам функции, и мы присваиваем только соответствующий набор флагов flags, и никогда не проверяем flags.)

Если мы можем создать файл, мы используем режим 0666 (rw-rw-rw-), измененный пользователем umask. (Обычно обычные пользователи имеют umask 002, 007, 022 или 077, что приводит к режимам получения новых файлов 0664 (rw-rw-r--), 0660 (rw-rw----) , 0644 (rw-r--r--) или 0600 (rw-------) соответственно.) Это именно то, что делает fopen() тоже.

Когда у нас есть дескриптор открытого файла, мы все равно должны связать его с дескриптором потока. Мы делаем это, используя fdopen(). Обратите внимание, что мы выбрали правильный mode для этого вызова в первом операторе switch. Если этот вызов завершится успешно, потоку будет «принадлежать» дескриптор файла, и все, что нам нужно сделать, это вернуть возвращенный дескриптор потока fdopen().

Если fdopen() завершится неудачно, нам нужно закрыть дескриптор файла. Мы также можем решить удалить / отменить связь с файлом. Приведенный выше код удаляет файл, если мы уверены, что он не существовал ранее (e) или если мы урезали его (w, w+), так как тогда данные, которые он мог содержать, в любом случае были бы потеряны.

Тестовая программа принимает два параметра командной строки: путь или имя файла и строку режима. Программа выполняет вызов custom_fopen(pathorfilename, modestring) и сообщает о результатах. (Если вызов custom_fopen() успешен, он также проверяет, что соответствующий вызов fclose() успешен, потому что иногда проблемы, связанные с дескриптором файла (или флаги несовместимого режима для вызовов open() / fdopen()), могут наблюдаться только в первая сделанная операция, или при закрытии потока.

Я только слегка протестировал вышеупомянутую функцию и программу, и ошибки всегда возможны. Если вы обнаружите ошибку или у вас возникнут проблемы с кодом, сообщите мне об этом в комментарии, чтобы я мог проверить и исправить при необходимости.

...