Skip to content

Latest commit

 

History

History
62 lines (43 loc) · 6.03 KB

filesystem.md

File metadata and controls

62 lines (43 loc) · 6.03 KB

Конкурентный доступ к файловой системе

Условия гонки за ресурсы могут возникать на разных уровнях:

  • внутри одной программы
  • между несколькими программами на одном компьютере
  • между разными программами на разных компьютерах

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

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

Файловая система — один из таких ресурсов, гонки за которым естественны и должны учитываться при разработке приложений. Да, самые низкоуровневые проблемы синхронизации чтения и записи в файловую систему берет на себя операционная система и (или) конкретный драйвер. Можно «спокойно» одновременно из разных процессов читать и писать в один и тот же файл и получать мусор или штатные ошибки: низкоуровневые операции read и write будут как-то упорядочены планировщиком ввода-вывода. С самой файловой системой при этом всё будет в порядке.

Но высокоуровневые операции над файловой системой в рамках вашей бизнес-логики требуют внимания и осторожности. Ведь самый простой способ получить условие гонки — добавить проверки при открытии файла!

#include <filesystem>
#include <string>
#include <fstream>

namespace fs = std::filesystem;

int main() {
    if (!fs::exists("/my/file")) {
        return EXIT_FAILURE;
    }
    std::ifstream input("/my/file");
    std::string line; input >> line;
    // do something
    return EXIT_SUCCESS;
}

Это класcическая TOCTOU (Time-of-Check-Time-of-Use) ошибка. Между проверкой и открытием файла, файл может быть удален.

Но причем тут С++, если такая проблема существет для любого языка программирования?

Действительно. Например, во всех версиях стандартной библиотеки Rust с 1.0 до 1.58.1 была похожая ошибка в реализации функции remove_dir_all.

// это упрощенный код
// directory: Path
if directory.is_symlink() {
    remove_link(directory)
} else {
    remove_recursive(directory)
}

Между проверкой и удалением злоумышленник мог подменить настоящий каталог на символьную ссылку и добиться удаления данных, к которым у него нет доступа.

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

А теперь мы можем вернуться к C++:

В std::filesystem также есть функция remove_all, с тем же значением что и версия из Rust. И в большинстве ее реализаций в 2022 году также нашли точно такую же ошибку!

Обсуждение этой ошибки было довольно бурным, поскольку стандарт C++ объявляет неопределенным поведением любые вызовы функций std::filesystem, приводящие к гонке!

Но они все приводят к гонке! Их корректная работа зависит только от благонадежности других приложений, обращающихся к той же файловой системе. Можно ли в таком случае ничего не исправлять в функции std::filesystem::remove_all?

Конечно нужно! Ошибка была исправлена в GCC версии 11.5 и в Clang-14