Pull to refresh
Selectel
IT-инфраструктура для бизнеса

Нарушаем ограничения файловых систем *NIX

Reading time11 min
Views13K

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

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

Под катом вас ждет небольшой экскурс в особенности работы файловых систем и набор экспериментов.


Большинство ограничений на имена файлов и каталогов очевидны.

  • Длина имени файла ограничена. Структура, которая хранит информацию о файле не бесконечна.
  • Длина абсолютного пути до файла ограничена. Чуть менее очевидно, но причина идентична: размер буфера, в котором хранится абсолютный путь до файла, тоже не бесконечен.
  • Разделитель в пути, символ «/», использовать нельзя.
  • Нуль-символ использовать нельзя — это признак конца строки.
  • Внутри одного каталога не может быть двух файлов с одним именем.

Переполнять имена файлов — не так интересно, потому что обычно размер полей прописан отдельным целочисленным полем. Если его переполнить, структура потеряет целостность. А вот добавить в имя разделитель или сделать два файла с одним именем… Уже догадываетесь?


Все есть файл


В семействе операционных систем *NIX есть красивая идея, которая описывается словами «Everything is a file», то есть «все есть файл». Благодаря этой абстракции практически все операции ввода-вывода используют один интерфейс, состоящий из следующих системных вызовов. Это не полный список.

  • open(2) — открытие ресурса на чтение или запись.
  • read(2) — чтение байтов из ресурса.
  • write(2) — запись байтов в ресурс.
  • close(2) — закрытие ресурса.

Пионеры компьютерных наук — это умные люди, которые решали невиданные ранее задачи. Это великое наследие, к которому иногда возникают вопросы. Например, почему системный вызов для создания файла называется creat(2), а не create(2)? Маленькие познавательные тексты и рассуждения по темам будущих статей — в моем Telegram-канале.

Эти вызовы позволяют получать доступ практически ко всем ресурсам в операционной системе.

  • К отдельным файлам и каталогам, которые лежат на накопителе.
  • Непосредственно к байтовому представлению на накопителе (файлы /dev/sd* и /dev/nvme*).
  • К внешнему устройству: принтеру, мышке, клавиатуре (файлы /dev/input/, /dev/mouse*).
  • Даже сокеты Беркли, которые используются для доступа в Интернет, поддерживают перечисленные операции. Только открытие происходит с помощью системного вызова socket(2), а не через open(2).

Но не будет погружаться на всю глубину этой абстракции, взглянем в самое начало. Каталог — это тоже файл. Да, его можно прочитать утилитой cat. Но не в современных системах.

root@ubuntu-2204:~# cat /tmp
cat: /tmp: Is a directory
root@ubuntu-2204:~#

В Linux этот фокус не работает, а вот во FreeBSD до 12 версии можно вывести каталог.

root@freebsd-11:~ # cat /tmp
X...X .X11-unix<X .XIM-unix<X  .ICE-unix<X
PERL5_DEFAULT\local.UgDju9Hno\dmail.RsZJCJr4Yt6\Hmail.RsygwgsVOYB4\(write-422194.oZxxhash-737aeb.oZbench-fb2faf.lz4io-a10809.olz4cli-b2a9d6.o
root@freebsd:~ #

Вывод — это не просто набор непечатных символов и имен файлов, а текстовое представление содержимого каталога. Каталог — это массив структур dirent, который выглядит следующим образом:

struct dirent {
   __uint32_t d_fileno;               /* file number of entry */
   __uint16_t d_reclen;               /* length of this record        */
   __uint8_t  d_type;               /* file type, see below */
   __uint8_t  d_namlen;               /* length of string in d_name */
#ifdef _POSIX_SOURCE
   char    d_name[255 + 1];               /* name must be no longer than this */
#else
#define MAXNAMLEN       255
   char    d_name[MAXNAMLEN        + 1];  /* name must be no longer than this */
#endif
};

Для чтения каталогов есть отдельные функции, которые разбирают содержимое каталога на структуры dirent: opendir(3), readdir(3) и closedir(3). Но в академических целях можно обойтись без них:

#include <stdio.h> // printf(3)
#include <sys/types.h> // open(2)
#include <sys/stat.h> // open(2)
#include <fcntl.h>  // open(2)
#include <errno.h>  // errno
#include <string.h> // strerror(3)
#include <unistd.h> // close(2)
#include <dirent.h>
#define BUF_SIZE 4096
int main(int argc, char* argv[])
{
        // Открываем на чтение. Без флага O_DIRECTORY будет ошибка.
        int fd = open(argv[1], O_DIRECTORY | O_RDONLY);
        if(fd < 0) {
                printf("Cannot open %s: %s\n", argv[1], strerror(errno));
                return 1;
        }
        unsigned char buf[BUF_SIZE];
        while(1) {
                struct dirent dir;
                /* Считываем по одному полю */
                ssize_t count = read(fd, &dir.d_fileno, sizeof(dir.d_fileno));
                /* Обработка ошибок один раз для читаемости */
                if(count < 0) {
                        printf("Read error: %s\n",  strerror(errno));
                        return 2;
                }
                if(count == 0) break;
                count += read(fd, &dir.d_reclen, sizeof(dir.d_reclen));
                count += read(fd, &dir.d_type, sizeof(dir.d_type));
                count += read(fd, &dir.d_namlen, sizeof(dir.d_namlen));
                count += read(fd, &dir.d_name, dir.d_reclen - count);
                dir.d_name[dir.d_namlen] = '\0';
                printf("Entry:\n");
                printf("\td_fileno: %u\n", dir.d_fileno);
                printf("\td_reclen: %u (read: %zd)\n", dir.d_reclen, count);
                printf("\td_type: %u\n", dir.d_type);
                printf("\td_namelen: %u\n", dir.d_namlen);
                printf("\td_name: %s\n", dir.d_name);
        }
        close(fd);
        return 0;
}

Да, это тоже работает:

root@freebsd:~ # ./a.out /tmp
Entry:
        d_fileno: 11556864
        d_reclen: 12 (read: 12)
        d_type: 4
        d_namelen: 1
        d_name: .
Entry:
        d_fileno: 2
        d_reclen: 12 (read: 12)
        d_type: 4
        d_namelen: 2
        d_name: ..
Entry:
        d_fileno: 11556869
        d_reclen: 20 (read: 20)
        d_type: 4
        d_namelen: 9
        d_name: .X11-unix
Entry:
        d_fileno: 11556870
        d_reclen: 20 (read: 20)
        d_type: 4
        d_namelen: 9
        d_name: .XIM-unix
Entry:
        d_fileno: 11556871
        d_reclen: 20 (read: 20)
        d_type: 4
        d_namelen: 9
        d_name: .ICE-unix
Entry:
        d_fileno: 11556872
        d_reclen: 20 (read: 20)
        d_type: 4
        d_namelen: 10
        d_name: .font-unix
Entry:
        d_fileno: 11556915
        d_reclen: 408 (read: 408)
        d_type: 8
        d_namelen: 13
        d_name: PERL5_DEFAULT

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

#include <stdio.h> // printf(3)
#include <sys/types.h> // open(2)
#include <sys/stat.h> // open(2)
#include <fcntl.h>  // open(2)
#include <errno.h>  // errno
#include <string.h> // strerror(3)
#include <unistd.h> // close(2)
int main(int argc, char* argv[])
{
        /* Сперва откроем на запись, вдруг прокатит */
        int fd = open(argv[1], O_DIRECTORY | O_WRONLY);
        if(fd < 0) {
                printf("Cannot open for write %s: %s\n", argv[1], strerror(errno));
                return 1;
        }
        return 0;
}

Компилируем, запускаем.

root@freebsd:~ # cc write.c 
root@freebsd:~ # ./a.out /tmp
Cannot open for write /tmp: Is a directory
root@freebsd:~ #

Не получилось. Я проверял этот код на FreeBSD 11, которая вышла в 2016 году. Может, в каноническом UNIX нет таких ограничений? Перепишем код в синтаксис K&R. Константы O_WRONLY тоже нет, поэтому заглядываем в документацию и выбираем число 1.

int main(argc, argv) 
    int argc;
    char* argv[];
{
  int fd;
  fd = open(argv[1], 1);
  if(fd < 0) perror("open");
  return 0;
}

И запустим. Для проверки таких гипотез можно использовать эмулятор PDP-11/70 на JavaScript, который доступен в браузере. Он также содержит образы Unix v5 и BSD2.11. Запустим код на Unix v5.

# cc a.c
# ./a.out /tmp
open: Is a directory

Это было ожидаемо. Более того, попытки поискать что-то похожее на структуру dirent на накопителе не увенчаются успехом. Структура dirent — это абстракция, независимая от используемой файловой системы. Ядро «подкладывает» подобные структуры при запросе, поэтому записать в них ничего нельзя.

Хорошо, мы не можем из пространства пользователя изменить содержимое каталога. Тогда замыслим шалость!

Не доверяй пользовательским данным


Если мы не можем через операционную систему ввести некорректные данные, значит, мы сделаем это без ее ведома. Откроем диск в HEX-редакторе на Windows, например. Про Windows я, конечно, погорячился, но основная мысль сохраняется:

  1. создадим «диск» в виде файла на 50 МБ,
  2. разметим файловую систему внутри файла,
  3. примонтируем содержимое файла в каталог mnt,
  4. создадим файл с названием 12345678,
  5. отмонтируем «диск»,
  6. через HEX-редактор изменим имя файла,
  7. примонтируем «диск» и проверим успешность затеи.

Благодаря идее «все есть файл» для операционной системы нет разницы между файлом-диском и обычным файлом. Это позволяет создать файловую систему не на разделе диска, а в обычном файле. А потом примонтировать этот файл как будто это диск.

Выполняем подготовку:

# Создаем файл
dd if=/dev/zero of=img bs=1M count=50
# Создаем файловую систему
mkfs.ext4 img
# Монтируем диск
mkdir mnt
mount img mnt
# Пустой файл
touch mnt/12345678
# Отмонтируем
umount mnt
# Переименовываем 12345678 в 87654321
sed -i 's/12345678/87654321/g' img

Для переименования я использовал неинтерактивный текстовый редактор sed, он отлично справляется со своей задачей. Монтируем:

void@ubuntu-2204:~# mount img mnt
void@ubuntu-2204:~# ls -l mnt
ls: reading directory 'mnt': Bad message
total 0

Ого, плохие ворота плохое сообщение. В циклическом логе ядра можно найти более подробное объяснение:

[36446.876771] EXT4-fs error (device loop1): htree_dirblock_to_tree:1080: inode #2: comm ls: Directory block failed checksum

Логично. Файловые системы работают с аппаратным обеспечением, которое в силу своей физической природы может дать сбой. Контрольные суммы позволяют обнаруживать и сообщать об ошибках. Воспользуемся утилитой e2fsck для проверки файловой системы.

void@ubuntu-2204:~# e2fsck img 
e2fsck 1.46.5 (30-Dec-2021)
img contains a file system with errors, check forced.
Pass 1: Checking inodes, blocks, and sizes
Pass 2: Checking directory structure
Directory inode 2, block #0: directory passes checks but fails checksum.
Fix<y>? yes
Pass 3: Checking directory connectivity
Pass 3A: Optimizing directories
Pass 4: Checking reference counts
Pass 5: Checking group summary information
img: ***** FILE SYSTEM WAS MODIFIED *****
img: 13/12800 files (7.7% non-contiguous), 1840/12800 blocks
void@ubuntu-2204:~#

Монтируем и проверяем заново:

void@ubuntu-2204:~# ls -l mnt/
total 16
-rw-r--r-- 1 void root     0 Mar  5 18:44 87654321
-rw-r--r-- 1 void root     0 Mar  5 18:31 blablabla
drwx------ 2 void root 16384 Mar  5 18:26 lost+found

Сработало. Тогда попробуем первое испытание: создадим два файла и переименованием назначим им одно имя.

void@ubuntu-2204:~/mnt# ls -li
total 24
12 -rw-r--r-- 1 void root     2 Mar  5 19:26 1.txt
13 -rw-r--r-- 1 void root    20 Mar  5 19:26 2.txt
11 drwx------ 2 void root 16384 Mar  5 18:26 lost+found
void@ubuntu-2204:~/mnt# cat 1.txt 
1
void@ubuntu-2204:~/mnt# cat 2.txt 
2222222222222222222
void@ubuntu-2204:~/mnt# 

Переименовываем и запускаем пересчет контрольной суммы:

void@ubuntu-2204:~# e2fsck img
e2fsck 1.46.5 (30-Dec-2021)
img contains a file system with errors, check forced.
Pass 1: Checking inodes, blocks, and sizes
Pass 2: Checking directory structure
Duplicate entry 'a.txt' found.
        Marking ??? (2) to be rebuilt.
Directory inode 2, block #0: directory passes checks but fails checksum.
Fix<y>? yes
Pass 3: Checking directory connectivity
Pass 3A: Optimizing directories
Entry 'a.txt' in / (2) has a non-unique filename.
Rename to a.txt~0<y>? no
Pass 4: Checking reference counts
Pass 5: Checking group summary information
img: ***** FILE SYSTEM WAS MODIFIED *****
img: ********** WARNING: Filesystem still has errors **********
img: 13/12800 files (7.7% non-contiguous), 1842/12800 blocks
void@ubuntu-2204:~# mount img mnt/

Утилита e2fsck исправляет контрольную сумму и находит неуникальное имя. Мы, в свою очередь, отказываемся от переименования и монтируем «диск». Что мы видим?

void@ubuntu-2204:~# ls -li mnt
total 24
12 -rw-r--r-- 1 void root     2 Mar  5 19:26 a.txt
12 -rw-r--r-- 1 void root     2 Mar  5 19:26 a.txt
11 drwx------ 2 void root 16384 Mar  5 18:26 lost+found
void@ubuntu-2204:~# cat mnt/a.txt 
1

Драйвер системы выдает небольшую аномалию: файл с меньшим числовым идентификатором, номером inode, отображается два раза. При этом все обращения из интерпретатора командной строки взаимодействуют именно с «первым» файлом.

void@ubuntu-2204:~# rm mnt/a.txt 
void@ubuntu-2204:~# ls -l mnt/
ls: cannot access 'mnt/a.txt': No such file or directory
total 16
? -????????? ? ?    ?        ?            ? a.txt
drwx------ 2 void root 16384 Mar  5 18:26 lost+found
void@ubuntu-2204:~# cat mnt/a.txt 
cat: mnt/a.txt: No such file or directory
void@ubuntu-2204:~# umount mnt 
void@ubuntu-2204:~# mount img mnt/
void@ubuntu-2204:~# ls -li mnt/
total 20
13 -rw-r--r-- 1 void root    20 Mar  5 19:26 a.txt
11 drwx------ 2 void root 16384 Mar  5 18:26 lost+found
void@ubuntu-2204:~# cat mnt/a.txt 
2222222222222222222

Если удалить файл, то его «тезка» перейдет в состояние кота Шредингера. Файл есть, автодополнение по имени работает, но файла нет. После перемонтирования файловой системы второй файл без проблем находится.

Теперь сделаем страшное и добавим символ-разделитель:

void@ubuntu-2204:~# cat >mnt/split-me.txt 
Hello, Habr!
void@ubuntu-2204:~# umount mnt
void@ubuntu-2204:~# sed -i 's/split-me.txt/split\/me.txt/g' img
void@ubuntu-2204:~# e2fsck img 
e2fsck 1.46.5 (30-Dec-2021)
img contains a file system with errors, check forced.
Pass 1: Checking inodes, blocks, and sizes
Pass 2: Checking directory structure
Entry 'split/me.txt' in ??? (2) has illegal characters in its name.
Fix<y>? no
Directory inode 2, block #0: directory passes checks but fails checksum.
Fix<y>? yes
Pass 3: Checking directory connectivity
Pass 3A: Optimizing directories
Pass 4: Checking reference counts
Pass 5: Checking group summary information
img: ***** FILE SYSTEM WAS MODIFIED *****
img: ********** WARNING: Filesystem still has errors **********
img: 12/12800 files (8.3% non-contiguous), 1841/12800 blocks

Проверка находит такую ошибку, но не считает ее критичной. Хорошо, монтируем:

void@ubuntu-2204:~# ls -l mnt/
ls: reading directory 'mnt/': Input/output error
total 16
drwx------ 2 void root 16384 Mar  5 18:26 lost+found
void@ubuntu-2204:~# ls -l mnt/split/me.txt
ls: cannot access 'mnt/split/me.txt': No such file or directory
void@ubuntu-2204:~# mkdir mnt/split
void@ubuntu-2204:~# cat mnt/split/me.txt
cat: mnt/split/me.txt: No such file or directory
void@ubuntu-2204:~# echo Wow > mnt/split/me.txt
void@ubuntu-2204:~# cat mnt/split/me.txt 
Wow

Результат не заставляет себя долго ждать.

  • Частично сломалось отображение содержимого каталога. Объекты с номером inode больше, чем у «сломанного» файла, не попадут в вывод.
  • Сам «сломанный» файл недоступен.

С одной стороны, это пример хорошей устойчивости к ошибкам. С другой — этой проблеме не день и не два, а 45 лет, если брать за точку отсчета выход UNIX v7.

Статья посвящена семейству операционных систем *NIX. Но в процессе написания стало интересно как такие ситуации обрабатывают «оконные» файловые системы.

Возможно, эти тексты тоже вас заинтересуют:

Figma закрыла Dev Mode: пути обхода и их краткий обзор
Мини-ПК для «тяжелых» и не очень задач: 5 моделей начала весны 2024 года
10нм техпроцесс и 6 ГГц: Intel ставит новые рекорды производительности чипов. Что нового?

exFAT


Брать NTFS кажется избыточным. Но статья началась из-за бага на флешке фотоаппарата. Там используется файловая система от Microsoft — exFAT. Это преемник FAT, предназначенный для съемных носителей. Отличный кандидат. Хотя для Ubuntu 22.04 необходимо поставить именно exfat-fuse.

Теперь можно создать образ как раньше:

void@ubuntu-2204:~# dd if=/dev/zero of=img bs=1M count=50
50+0 records in
50+0 records out
52428800 bytes (52 MB, 50 MiB) copied, 0.024757 s, 2.1 GB/s
void@ubuntu-2204:~# mkfs.exfat img
exfatprogs version : 1.1.3
Creating exFAT filesystem(img, cluster size=4096)
Writing volume boot record: done
Writing backup volume boot record: done
Fat table creation: done
Allocation bitmap creation: done
Upcase table creation: done
Writing root directory entry: done
Synchronizing...
exFAT format complete!<br>void@ubuntu-2204:~# mount img mnt
void@ubuntu-2204:~# cat > mnt/1.txt
111111111111111111111111
void@ubuntu-2204:~# cat > mnt/2.txt 
222222222222222222222

Заменить имя файла через sed уже не получится: для имен exFAT использует кодировку UTF-16. Хорошо, воспользуемся интерпретатором Python и напишем такой скрипт:

import sys
file, pattern, replace = sys.argv[1:4]
pattern = pattern.encode("utf-16le")
replace = replace.encode("utf-16le")
with open(file, "rb") as f:
    raw_data = f.read()
raw_data = raw_data.replace(pattern, replace)
with open(file, "wb") as f:
    f.write(raw_data)

Неэффективно по памяти, но для образа в 50 МБ это не проблема. Производим замену:

void@ubuntu-2204:~# python3 replace.py img 2.txt 1.txt
void@ubuntu-2204:~# mount img mnt
void@ubuntu-2204:~# ls -li mnt/
total 8
10 -rwxr-xr-x 1 void root 25 Mar  5 20:48 1.txt
10 -rwxr-xr-x 1 void root 25 Mar  5 20:48 1.txt

Реакция Ubuntu не поменялась: доступен только первый файл. Аналогично с UNIX-разделителем. А вот Windows-разделитель вносит смуту, но отображает файл:

void@ubuntu-2204:~# ls -l mnt/
ls: cannot access 'mnt/2\txt': No such file or directory
total 4
-rwxr-xr-x 1 void root 25 Mar  5 20:48  1.txt
-????????? ? ?    ?     ?            ? '2\txt'
void@ubuntu-2204:~# ls -l mnt/2\\txt 
ls: cannot access 'mnt/2\txt': No such file or directory

Проверим на Windows и записываем на флешку.

dd if=img of=/dev/<здесь мой диск> bs=4M


Уведомление в Windows 10.


Проигнорировать исправление ошибок нельзя.

Windows тут же определяет накопитель и предлагает исправить ошибки. Исправление ошибок — это предложение, от которого нельзя отказаться. Потому что иначе не получится просмотреть содержимое накопителя в графическом интерфейсе.


Консоль топ.

Командная строка покажет первые файлы, но потом запнется на «сломанном» файле. Прочитать, однако, файлы рядом возможно.

Абстракции для файловых систем в *NIX — это не только универсальный интерфейс, но и отличная устойчивость к ошибкам. Что думаете по поводу рассмотренных артефактов вы? Поделитесь своим мнением в комментариях!
Tags:
Hubs:
Total votes 74: ↑70 and ↓4+66
Comments48

Articles

Information

Website
selectel.ru
Registered
Founded
Employees
501–1,000 employees
Location
Россия
Representative
Влад Ефименко