загрузка из файла, который был закрыт перед чтением

#c #fopen #fread #fclose

#c #открыть #загрузка #fclose

Вопрос:

Вчера я провел бессонную ночь, пытаясь отследить ошибку в моем тестовом примере. Мой интерфейс выглядит примерно так:

 image read_image(FILE *file) {
  if (file == nullptr) {
    //throw exception 
  }
  //call ftell and fread on the file
  //but not fclose
  ...
  //return an image
}
  

Оказывается, один из моих тестовых примеров проверял, может ли мой код обрабатывать чтение из файла, который был впервые открыт (поэтому указатель на файл не nullptr был), но закрыт до того, как я передам его своей функции, что-то вроде этого:

 FILE *img_file = fopen("existing_image.png", "r");
REQUIRE(img_file != nullptr); //this passes!
fclose(img_file);
auto my_image = image_read(file);

//... then somewhere down in completely
//unrelated test cases I get segfaults,
//double free errors and the like
  

Затем я потратил часы, пытаясь отследить ошибки segfaults, двойные освобождения в совершенно не связанных частях моего кода, пока не удалил этот конкретный тестовый пример. Казалось, это решило проблему.

Мои вопросы:

  1. Я знаю, что вызов fread / ftell для закрытого файла — глупая идея, но может ли это действительно вызвать такое повреждение памяти? Я посмотрел, например, на cppreference, но никогда явно не указывалось, что передача закрытого потока является неопределенным поведением…
  2. Есть ли какой-либо способ узнать, был ли файл закрыт перед чтением из него? (Я посмотрел на SO, но ответ кажется: нет.)

Дополнительная информация

Я использую C 17 и gcc 9.3.0 для компиляции. Причина, по которой мне вообще приходится иметь дело FILE * , заключается в том, что я получаю эти указатели от внешнего C API.

Комментарии:

1. Использование любой f* функции ( ftell , fread , и т.д.) Для указателя на ФАЙЛ, который был закрыт ранее, или любого другого недопустимого указателя на ФАЙЛ не определено.

2. Для предотвращения подобных проблем обычно (для некоторых значений «обычный») устанавливается указатель на NULL … ie fclose(pointer); pointer = NULL; (та же идея, что и в free(pointer); pointer = NULL; )

3. it was never explicitly specified that passing a closed stream is undefined behavior «Неопределенный» означает… не определен, поэтому, если он не определен, лучше всего сделать вывод, что это означает, что он не определен. В любом случае в приложении j2 это просто The value of a pointer to a FILE object is used after the associated file is closed

4. Могу ли я снять деньги с банковского счета, который я закрыл?

5. » Я получаю эти указатели от внешнего C API » — вы также получаете право собственности на FILE* ? Если вы это сделаете, сохраните его в интеллектуальном указателе, например struct fcloser { auto operator()(std::FILE* fp) const { return std::fclose(fp); } }; , и затем std::unique_ptr<std::FILE, fcloser> file;

Ответ №1:

Да, это может привести к повреждению памяти, поскольку a FILE * мог выделить память. Вероятно, используется malloc .

Что произойдет с вашей программой, если вы попытаетесь использовать указатель malloc после того, как вы использовали free его?

Да, все ломается. Не делайте этого.

Ответ №2:

Мощь и эффективность языков C и C сопряжены с большой ответственностью: программист должен быть осторожен в отношении жизненного цикла или каждого объекта.

C упрощает это с помощью интеллектуальных указателей и RAII, но в C отсутствуют эти парадигмы, поэтому каждый указатель является потенциальным источником неопределенного поведения. Хорошим примером являются указатели, полученные из API-интерфейсов C.

Вы могли бы установить значение FILE * to NULL после каждого fclose , но это не решит проблему, если FILE указатель был получен в качестве аргумента или продублирован каким-либо другим способом.

Не существует стандартного API для проверки правильности указателя, а также в данном конкретном случае FILE * , ссылается ли a на открытый поток. Что еще хуже, FILE указатели обычно быстро перерабатываются, поэтому устаревший FILE * файл вполне может ссылаться на недавно открытый файл, отличный от того, для которого он был первоначально получен.

Комментарии:

1. Да, это именно моя проблема. Если я уже получил указатель на закрытый ФАЙЛ с самого начала, кажется, я облажался…

Ответ №3:

  • Я знаю, что вызов fread/ftell закрытого файла — глупая идея, но может ли это действительно вызвать такое повреждение памяти? Я посмотрел, например, на cppreference, но никогда явно не указывалось, что передача закрытого потока является неопределенным поведением…

Попытка fread или ftell для FILE* закрытого файла приведет к тому, что обе функции вернут -1 и установят errno соответствующее значение во многих системах, но обычно вы можете избежать этого, проверив, является ли значение FILE* допустимым.

  • Есть ли какой-либо способ узнать, был ли файл закрыт перед чтением из него? (Я посмотрел на SO, но ответ кажется: нет.)

В системах Posix и Windows (и, возможно, других), да. Posix fileno() и Windows _fileno() возвращают -1, если аргумент не является допустимым потоком, например, после его закрытия.

Таким образом, вы могли бы создать оболочку RAII, которая становится владельцем FILE* и проверяет, действительна ли она при построении. Если он пройдет этот тест, риск того, что что-либо в вашем коде закроет его, когда это не предполагается, будет очень низким.

Вот схема такой оболочки:

 class File {
public:
    File(std::FILE* fp) : file(validate(fp)) {
        if(!file) throw std::runtime_error("I don't like nullptr");
    }

    template<typename T, std::size_t N>
    auto read(T(amp;buf)[N], std::size_t nmemb = N) {
        if(N < nmemb) throw std::runtime_error("reading out of bounds");
        return fread(buf, sizeof(T), nmemb, file.get());
    }

    template<typename T, std::size_t N>
    auto write(const T(amp;buf)[N], std::size_t nmemb = N) {
        if(N < nmemb) throw std::runtime_error("writing out of bounds");
        return fwrite(buf, sizeof(T), nmemb, file.get());
    }

private:
    std::FILE* validate(std::FILE* fp) {
#if defined(_POSIX_C_SOURCE)
        if(::fileno(fp) == -1) throw std::runtime_error(std::strerror(errno));
#elif defined(_WIN32)
        if(::_fileno(fp) == -1) throw std::runtime_error(std::strerror(errno));
#endif
        return fp;
    }
    struct fcloser {
        auto operator()(std::FILE* fp) const {
            return std::fclose(fp); 
        } 
    };

    std::unique_ptr<FILE, fcloser> file;
};
  

Для этого также потребуется поиск / сообщение функций-членов и т. Д., Но это должно обеспечить разумную безопасность вашего указателя.

ДЕМОНСТРАЦИЯ