В один из дней в лаборатории раздался звонок. Звонил представитель организации, оказывающей услуги системного администрирования. Его первым вопросом было: «А вы умеете работать с ZFS?». Разумеется, мы пояснили, что у нас есть некоторые наработки в рамках собственных исследований, а также стандартные средства DR‑лаборатории, такие как PC-3000, где поддержка ZFS предусмотрена как минимум на базовом уровне.
Ответив на парочку каверзных вопросов клиента об устройстве ZFS, мы через некоторое время получили два SSD Samsung 870 EVO ёмкостью 2 ТБ, которые были объединены в зеркальный RAID‑массив (Mirror). Изначально комментарии сводились к тому, что оба диска рабочие, но один выпал из массива. Основная проблема заключалась в том, что был случайно удалён один из важных Zvol (блочный объект ZFS, используемый для виртуальных дисков). Клиент пояснил, что хотел откатиться к прошлой версии uberblock’а и получить снимок, когда этот Zvol ещё не был удалён, но у него это не получилось. Резервная копия сохранилась, но она была сделана ещё в апреле этого года.

Приняв SSD для проведения диагностики, мы в короткое время выяснили, что один из них уже находится в аварийном режиме, а второй формально в нормальном, но имеет нечитаемые участки. С помощью Data Extractor мы выполнили достаточно стандартный набор операций для вычитки проблемных дисков, после чего получили максимально полные копии, пригодные для глубокого анализа.

Стандартный анализ метаданных ZFS средствами PC-3000 не показал никаких признаков существования удалённого Zvol на актуальном состоянии пула. Попытки выбрать из кольца иные связки UB/MOS, к сожалению, также не дали результатов. Диск, который был исключён из массива, рассматривать практически бессмысленно, так как данные на нём были двухлетней давности.
Мы сообщили клиенту, что стандартным путём структурированные данные получены быть не могут. Мы можем попытаться построить карту свободного пространства и провести поиск по регулярным выражениям для различных небольших файлов. Однако результат ожидался заведомо слабый: в ZFS очень сильная фрагментация данных, к тому же использовано сжатие алгоритмом LZ4.
Проведённая попытка поиска данных по регулярным выражениям ожидаемо дала низкий результат. Казалось бы, кандидатов на восстановление много, но процент объектов, проходящих структурную валидацию, оказался ничтожно малым.

Мы согласовали с клиентом, что для данной ситуации можно применить некоторые наши исследовательские наработки: взять набор наших CLI‑утилит, облачить их в GUI‑форму и связать в единый конвейер, что, возможно, поможет существенно улучшить результат.
Сначала, как обычно, мы начали с уже полностью готовых инструментов, которые позволяли оценить состояние файловой системы и поискать признаки метаданных. Мы прошлись по снимкам uberblock/MOS, чтобы оценить, какие разночтения есть в снимках и что они могут нам дать.

Следом была предпринята попытка найти deadlists, которая также не увенчалась успехом: после комплекса проверок выяснилось, что они пусты.
На этом, казалось бы, стандартные методы реконструкции ZFS были исчерпаны. Метаданных нет. Данные клиента сжаты, а доступных метаданных, описывающих положение сжатых блоков в незанятом пространстве, уже не существует.
Далее мы провели исследование сжатых блоков по существующим в пуле Zvol, которое показало, что сами сжатые блоки не имеют какой‑либо устойчивой сигнатуры. Искать их по регулярному выражению невозможно. Однако, разрабатывая инструменты для восстановления данных не первый год, можно с уверенностью сказать: любые сжатые данные не бывают совершенно хаотичными, и нередко можно уловить закономерности, по которым их можно обнаруживать.
В случае блока, сжатого с помощью LZ4, в пространстве ZFS‑пула присутствует явный признак в виде первых 4 байт, где описан Csize (compressed size) в порядке big endian. Это не устойчивая сигнатура, а переменные данные, но они описывают размер сжатого блока в байтах. Далее следует сам сжатый поток.
Принимая в расчёт, что Zvol — это блочная иерархическая структура, в которой конечный элемент L0, описанный в Indirect‑блоках L1, имеет фиксированный размер, мы обратились к старой копии. Выяснилось, что удалённый Zvol имел размер блока 64 КБ.
Минимальная гранулярность определяется параметром ashift пула, который описан в структуре vdev. Нулевая точка для данных на пуле ZFS — это смещение до начала тома от LBA 0 + размер двух копий vdev/UB_rings + boot‑область + пропуск. Далее начинается нулевая точка для DVA‑адресации.
В нашем случае ashift равен 12 (степень двойки), то есть минимальный блок записи в пуле равен 4096 байт. Из этого можно сделать вывод, что признаки сжатых данных от нулевой точки можно искать строго с этим шагом. Так мы гарантированно ничего не пропустим, если добьёмся точной проверки признаков.
Первой попыткой было использование относительно стандартных средств, которым передавался проверяемый блок для распаковки с целью получения Usize (Unpacked size) или ответа об ошибке. При таком варианте мы получали высокую нагрузку на CPU и огромный процент ложных срабатываний при шаге в 4 КБ. Были предприняты меры по анализу различных признаков, чтобы отбросить множество данных, нехарактерных для сжатых потоков. Отсев неподходящих данных несколько ускорил процесс, но всё равно это было достаточно долго.
Следующим этапом стало написание собственного кода‑анализатора сжатых данных LZ4 для подсчёта Usize. Как только это число выходило за заданные лимиты, подсчёт прекращался, и блок отвергался. С помощью данного приёма была достигнута приемлемая скорость анализа свободного пространства и определены все сжатые участки.

Экспортировав свободное пространство в файл‑образ, мы получили практически двойной прирост в объёме. Применив методы карвинга со структурной валидацией, удалось достичь значительно лучших показателей. Количество валидных и недублированных документов выросло в несколько раз. Этот результат уже порадовал клиента, так как были получены весьма важные файлы, которых не было в первой попытке анализа.

На этом исследования не были прекращены, так как в сжатом пространстве обнаружилось большое количество объектов DMU (Data Management Unit) и Indirect‑блоков разного уровня. Большое количество блоков, отличающихся по размеру от блоков Zvol, показалось нам достаточно интересным материалом для дальнейшей «археологии».
Пришлось разработать анализатор, который находит DMU‑объекты ZFS и Indirect‑блоки. Обе эти структуры не имеют чётко выраженных сигнатур, но многие поля в DMU должны быть согласованы, как и поля blkptr (block pointer) в Indirect‑блоках. Также для оценки корректности данных можно использовать лимиты, задаваемые характеристиками пула, что позволяет различить, где перед нами объекты метаданных ZFS, а где случайные совпадения. Шлифовка алгоритма валидации была весьма трудоёмкой, но нам удалось добиться отсеивания более 99,999% данных, не являющихся структурами ZFS. При этом на тестовой выборке ошибочно не была отсеяна ни одна легитимная структура. Тестовая выборка представляла собой несколько миллионов DMU, записанных случайным образом на тестовый диск с иными данными.
Далее было необходимо научить сканер диска опознавать сжатые блоки (как это было сделано ранее), распаковывать их, анализировать типы данных и заносить в массив. Особенность ZFS при включённом сжатии заключается в том, что часть метаданных сжата, а часть — нет. Одного только анализа сжатых областей недостаточно для полной картины.

Получив почти 33 млн DMU‑объектов, мы решили исследовать сначала типы 23 (DMU_OT_ZVOL) и 24 (DMU_OT_ZVOL_PROP), которых нашлось более 490 тысяч. Всё осложнялось тем, что на этом пуле существовал другой Zvol, который полностью совпадал по размеру с тем, что мы искали.
Виртуальный диск, который мы искали, занимал около 300 ГБ, со слов клиента. Эта информация позволила нам отсечь более 95% из 490 тысяч вариантов. Обнаружились записи о Zvol с таким размером. Однако строить несколько тысяч виртуальных дисков по 300 ГБ — нерациональный путь, так как временные затраты будут слишком велики, а их целостность нам неизвестна.
Мы решили провести эксперимент. Написали анализаторы blkptr и реализовали проход от DMU к вершине дерева (уровень L3) через L2 и L1 к первому блоку L0. Эта операция не требует больших затрат по чтению с диска и достаточно быстро позволяет получить первые 64 килобайта данных из каждой версии Zvol. Часть цепочек Indirect‑блоков оказалась перезаписанной, а часть — доступной. Нам удалось увидеть MBR и таблицу разделов с описанным виртуальным диском на 300 ГБ.
Радость находки была омрачена тем, что это оказались версии существующего Zvol, о чём нам говорил номер транзакции по смещению 0×1B8. На этом можно сделать вывод, что начала виртуального диска уже не существует, если идти к Indirect‑блокам через DMU.
Следующий этап — исследование Indirect‑блоков, анализ их иерархической связи и оценка целостности этих связей. Всё усугублялось достаточно большим объёмом, что не позволяло загрузить эти блоки в ОЗУ компьютера для проведения весьма скрупулёзных анализов. Предварительный подсчёт распакованного объёма нужных Indirect‑блоков показал около 300+ ГБ.
Немного поясним принципы хранения в ZFS. Большой объект, который нельзя описать одним blkptr в DMU, получает Indirect‑блок, в котором уже есть много места для последовательной записи blkptr, описывающих блоки, принадлежащие объекту.
L0 — это сам сжатый блок данных.
L1 — это Indirect‑блок, в blkptr которого содержатся DVA‑записи, где описан адрес блока, размер и указывается прочая необходимая информация для чтения и распаковки в случае сжатия.
В нашем случае один Indirect‑блок занимает 128 килобайт в распакованном виде. Одна запись blkptr занимает 128 байт. В один Indirect‑блок L1 помещается 1024 описателя L0. Как помните, размер одного блока L0 в распакованном виде даёт 64 килобайта. Полностью заполненный блок L1 даёт нам описание 64 мегабайт от Zvol. Indirect‑блок L2 описывает положение Indirect‑блоков L1. Полностью заполненный блок L2 в итоге даёт нам ссылки на 1024 блока L1, что в сумме даёт ёмкость 64 ГБ. Для описания ёмкости в 300 ГБ нужны блоки L3, которые описывают положение группы блоков L2. Важно уточнить, что такой объём блоков потребуется, только если Zvol действительно будет заполнен данными, так как в случае пустого тома нет необходимости выделять большое количество блоков.
В искомом Zvol хранилось более 200 ГБ данных, со слов клиента. Это означает, что дерево блоков будет большим и весьма ветвистым. Так как мы далее будем использовать Indirect‑блоки для анализа, без DMU у нас не будет точного размера Zvol, так как не определён maxblk. При грубой оценке блоков, участвующих в нашем Zvol, получаем следующее: в L3 описано 5 блоков L2, а остальные слоты пустые, исходя из размера виртуального диска. Количество считается следующим образом: L0 = 5 × 1024 × 1024 = 5 242 880. На самом деле оно несколько меньше, и вопрос с обрезанием нулевого хвоста мы перенесём в конец задачи.
В связи с тем, что количество Indirect‑блоков весьма велико, необходима процедура оценки целостности и фильтрации мусорных блоков. Для проверки блока L(x) нам необходимо оценить все его blkptr, проверить места, куда они указывают, и убедиться, что для блоков, чей уровень 2 и выше, по этим адресам нас ожидает Indirect‑блок L(x-1). Также необходимо проверить согласованность TxG birth, чтобы не получилось, что вроде бы блок есть, но по факту номера транзакций у некоторых blkptr там выше, чем в родительском блоке. Это несколько более быстрый путь, чем оценивать корректность по fletcher. Событие в виде перезаписи блока с более низким номером транзакции представляется технически невозможным в ZFS‑пуле.

После короткого исследования целостности мы перешли к построению L2-карт и L1-карт. Это дало нам 5 блоков L2, которые формируют карту из 5120 элементов. С такой гранулярностью оценки можно достаточно быстро сравнивать изрядно пострадавшие структуры и отмечать процент схожих указателей на блоки L1. Так как много данных внутри Zvol статичны, в Indirect‑блоках разных версий это выглядит как множество одинаковых DVA, расположенных симметрично. Таким образом нам удаётся отделить структуры, принадлежащие существующему Zvol, от структур удалённого. Случайные совпадения в версиях между ними единичны.
Попытки строить карты по одному блоку L3 со спуском на нижние уровни давали весьма удручающую картину: доступно, в лучшем случае, половина блоков от всего Zvol. Часть таблиц в самых свежих версиях была перезаписана. Как выяснилось, такие последствия наступили спустя всего 4 дня после удаления. Но чем хороша ZFS в данном случае, так это своим принципом CoW (Copy on write): каждая новая структура пишется в новый блок, а старая становится свободным пространством и, пока не перезаписана, может содержать полезные данные хотя бы о части блоков образа.
Реализовав возможность объединения нескольких карт с выбором валидных блоков с наибольшим TxG, мы проверили предположение, что объединив несколько версий карты получим больше данных. Так и получилось. «Дырок» в пространстве стало заметно меньше и качество результата выросло. В ручном режиме склеивать несколько карт можно, но когда речь идёт о тысячах, идея становится плохой. Поэтому мы перешли к разработке алгоритма комбинаторной склейки. Он хоть и показал неплохую эффективность, но количество потерь оставляло желать лучшего.

Проанализировав проблемы сборки образа, мы установили, что при формировании карт с высокой гранулярностью мы упускаем ситуацию на уровнях L1 и L0, а там происходят самые значительные потери. Первый шаг к решению проблемы — введение версионирования. При отдельном шаге формирования карты L0-объектов в распоряжение сборщика поступает не одна версия найденного блока, которую он выбирает по номеру транзакции. Если в Indirect‑блоке с наивысшей транзакцией есть ссылки на перезаписанные или невалидные L1, то принимается решение о замене блока на версию из более старой транзакции. Таким образом, карта L0-объектов стала полнее на несколько сотен тысяч блоков.
Финальный этап — это экспорт L0-блоков в файл‑образ виртуального диска, который также требует проверки копируемого по fletcher, так как с лёгкостью можно попасть на блок с чужими данными. В случае обнаружения расхождений возникает необходимость обращения к версиям, чтобы попытаться найти устаревшую копию блока, поскольку его ценность заключается в том, что он может содержать фрагменты данных, которые не изменялись.

Удалённый Zvol удалось восстановить частично. Получилось приблизительно 75–80% исправных файлов, и по большей части с оригинальной структурой каталогов, что является весьма неплохим результатом для такой ситуации.
Подводя итоги, можно заметить, что задачи восстановления данных могут быть весьма разнообразными. Если применить исследовательский подход и глубокое понимание архитектуры файловой системы, можно получить результат значительно лучше, чем принято считать возможным для ZFS‑пулов в подобных условиях.
И, конечно, главный вывод, который не теряет актуальности: почаще делайте резервные копии и максимально осторожно принимайте решения об удалении данных.
Предыдущая публикация: Необычный случай восстановления данных или немного реверс‑инжиниринга PLC Siemens Simatic S7-300

