
Пользовательские инструменты восстановления BTRFS для серьезного повреждения дерева extent, когда btrfs check --repair завершается неудачей (ошибка сегментации, зацикливание или взаимная блокировка)
Пользовательские инструменты, написанные в процессе восстановления 12-ТБ пула BTRFS на нескольких устройствах с серьёзным повреждением дерева экстентов, которое не смогли исправить стандартные команды (btrfs check --repair, --init-extent-tree и т.д.).
См. INCIDENT-ANALYSIS.md для структурированного описания случая восстановления, классификации первопричин и набора конструктивных предложений по улучшению btrfs-progs, которые позволили бы избежать необходимости в большинстве этих инструментов.
Используйте эти инструменты ТОЛЬКО если btrfs check --repair вызывает segfault, входит в бесконечный цикл или оставляет файловую систему в худшем состоянии, чем до него.
Документированные случаи, в которых они помогают:
btrfs check --repair вызывает segfault на [3/8] checking extents (Issue #525)btrfs check --init-extent-tree вызывает взаимоблокировкуbtrfs check --repair входит в бесконечный цикл, повторяя одни и те же исправленияrescue=all,ro, не удаётся смонтировать RWЭти инструменты НЕ для лёгких повреждений. При обычных повреждениях сначала попробуйте btrfs check --repair.
--write:
for DEV in sda1 sdb1 sdc1; do
sudo dd if=/dev/$DEV of=sb_${DEV}.bin bs=4096 count=1 skip=16
done
--write включается явно)Инструменты используют внутренний API btrfs-progs и должны собираться внутри дерева исходных кодов btrfs-progs:
# 1. Clone btrfs-progs
git clone --depth 1 --branch v6.19.1 https://github.com/kdave/btrfs-progs.git
cd btrfs-progs
# 2. Apply the EEXIST patch (required for batch backref injection)
patch -p1 < path/to/btrfs_fixes/patches/alloc_reserved_tree_block_eexist.patch
# 3. Configure and build base btrfs-progs
./autogen.sh
./configure
make -j$(nproc)
# 4. Copy the .c files from this repo into the btrfs-progs directory
cp path/to/btrfs_fixes/programs/*.c .
# 5. For each program, add to the Makefile:
echo '
PROGNAME: PROGNAME.o $(objects) $(libs_shared)
@echo " [LD] $@"
$(Q)$(CC) -o $@ PROGNAME.o $(objects) $(libs_shared) $(LDFLAGS) $(LIBS)
' >> Makefile
# 6. Build
make PROGNAME
Рекомендуемый порядок выполнения:
scan_and_fix_all_backrefs.c (самый важный)Самый важный инструмент. Рекурсивно обходит каждое дерево в файловой системе (ROOT, CHUNK, EXTENT, FS, DEV, CSUM, UUID, FREE_SPACE) и обнаруживает блоки метаданных, у которых отсутствует обратная ссылка METADATA_ITEM в дереве экстентов. Вставляет все недостающие обратные ссылки в одну транзакцию, чтобы избежать проблемы «перемещения корневого дерева между фиксациями».
Использование:
sudo ./scan_and_fix_all_backrefs /dev/sdX # только сканирование
sudo ./scan_and_fix_all_backrefs /dev/sdX --write # сканирование + вставка
fix_owner_refs.cИсправляет owner в inline TREE_BLOCK_REF, когда он не соответствует фактическому btrfs_header_owner() блока. Несоответствия возникают при перераспределении блоков между деревьями во время неудачных попыток восстановления.
sudo ./fix_owner_refs /dev/sdX # сканирование
sudo ./fix_owner_refs /dev/sdX --write # исправление
fix_bad_levels.cИсправляет записи METADATA_ITEM и EXTENT_ITEM с неверным уровнем. Повреждённые уровни (например, 50, 55, 237) — это мусор, оставшийся после того, как btrfs check --repair вошёл в цикл. Проверяется по реальному btrfs_header_level() блока.
sudo ./fix_bad_levels /dev/sdX # сканирование
sudo ./fix_bad_levels /dev/sdX --write # исправление
fix_duplicate_extents.cУдаляет дубликаты METADATA_ITEM (одинаковый bytenr, разные уровни в ключе). Сохраняет тот, уровень которого совпадает с btrfs_header_level, и удаляет другой.
sudo ./fix_duplicate_extents /dev/sdX # сканирование
sudo ./fix_duplicate_extents /dev/sdX --write # удаление дубликатов
remove_stale_ptrs.cСканирует каждый узел уровня‑1 FS_TREE. Обнаруживает устаревшие дочерние указатели с помощью трёх проверок: несоответствие owner, несоответствие first_key или first_key с типом, недопустимым для FS_TREE (например, BLOCK_GROUP_ITEM). Удаляет их с помощью btrfs_del_ptr.
sudo ./remove_stale_ptrs /dev/sdX # сканирование
sudo ./remove_stale_ptrs /dev/sdX --write # удаление
fix_uuid_tree.c / fix_csum_tree.cСоздаёт пустой лист для дерева UUID / дерева CSUM соответственно. Полезно, когда ROOT_ITEM указывает на блок, который был переназначен другому дереву. Ядро автоматически пересоздаёт дерево UUID при монтировании RW. При пустом дереве CSUM файлы с флагом NODATASUM не дают сбоя проверки.
sudo ./fix_uuid_tree /dev/sdX
sudo ./fix_csum_tree /dev/sdX
set_nodatasum.cУстанавливает флаг BTRFS_INODE_NODATASUM на инодах обычных файлов. Используйте это, если дерево контрольных сумм пусто, но файлы всё ещё имеют ожидаемые контрольные суммы, что вызывает ошибки чтения. С NODATASUM ядро пропускает поиск контрольных сумм.
sudo ./set_nodatasum /dev/sdX # сканирование
sudo ./set_nodatasum /dev/sdX --write # применение
fix_fstree_node.cВерсия с жёстко заданным списком устаревших блоков. Предпочитайте remove_stale_ptrs, который обнаруживает их автоматически. Используйте это только если вам нужен ручной контроль над тем, какие конкретные блоки удалять.
add_backrefs.cНачальная версия с жёстко заданным списком недостающих обратных ссылок. Предпочитайте scan_and_fix_all_backrefs, который обнаруживает их автоматически.
Когда базовых инструментов, описанных выше, было недостаточно (пул с 200 000+ ошибок, распределённых по нескольким деревьям), были созданы следующие дополнительные инструменты:
scan_fstree_extents.c + scan_extent_tree.cСканеры проходов 1 и 2, которые обходят FS_TREE и дерево экстентов соответственно, создавая TSV-файлы со всеми сопоставлениями ссылок/экстентов. Используются для построения входных данных для rebuild_extent_tree_apply, когда дерево экстентов нужно перестроить с нуля.
rebuild_extent_tree_apply.c (тяжёлый записывающий инструмент)Основной записывающий инструмент фазы 3. Принимает предварительно свёрнутый список ссылок (из разницы scan_fstree_extents + scan_extent_tree) и вставляет 3M+ EXTENT_DATA_REF в дерево экстентов порциями по 5000 за транзакцию. Ограничивает каждые 50 тыс. элементов, чтобы избежать зависаний при решингел DM-SMR. Проверено успешно: 3 248 617 вставок примерно за 34 минуты на 3× WD40EFAX SMR-дисках.
sudo ./rebuild_extent_tree_apply /dev/sdX1 refs_folded.txt to_insert.txt watermark.txt --dryrun
sudo ./rebuild_extent_tree_apply /dev/sdX1 refs_folded.txt to_insert.txt watermark.txt --write
patch_block_group_used.cТочечный исправитель одного поля для BLOCK_GROUP_ITEM.used, когда записывающий инструмент фазы 3 оставляет определённую группу блоков с превышением из-за уже существующих перекрывающихся file_extent_items. Использует прямой установщик btrfs_set_block_group_used, чтобы избежать учёта space_info через btrfs_update_block_group (который нам здесь НЕ нужен). Предварительно проверяет flags & BTRFS_BLOCK_GROUP_DATA.
sudo ./patch_block_group_used /dev/sdX1 <bg_bytenr> <bg_length> <new_used> --write
remove_extent_items_by_key.cУдаляет жёстко заданный список EXTENT_ITEM (bytenr, num_bytes, expected_inode) из дерева экстентов. Используется для очистки перекрывающихся устаревших экстентов в одном листе, которые препятствуют монтированию RO. Перед удалением выполняет проверки для каждого элемента (7 инвариантов, включая белый список inode). Запускается с rebuilding_extent_tree=1 + reinit_extent_tree=true, чтобы пропустить учёт пространства (вызывающий сначала вручную исправляет used через patch_block_group_used).
clean_orphan_dir_entries.cОчищает записи-сироты DIR_ITEM + DIR_INDEX из FS_TREE. Порции по 100 записей за транзакцию. Обновляет i_size родительского INODE_ITEM (уменьшение на namelen × 2: исправлена критическая ошибка: v1 уменьшала только на namelen, оставляя каталоги в недопустимом состоянии). Жёстко заданный список исключений для критических имён каталогов верхнего уровня (например, pelis, series, music, backups, homestorage). НИКОГДА не уменьшайте i_size на сырой namelen: BTRFS хранит учёт namelen × 2.
clean_orphan_inode_refs.cОбходит FS_TREE в поисках элементов INODE_REF, у которых key.offset (родительский inode) находится в списке родителей-сирот. Пропускает INODE_EXTREF, чтобы избежать ложных срабатываний (у EXTREF key.offset — это хеш, а не идентификатор родителя). Порции по 32 за транзакцию.
fix_dir_inode_counts.cПересчитывает i_size = sum(name_len × 2) и nlink = 1 для inode каталогов, чьи счётчики были повреждены предыдущими ошибками очистки сирот. КРИТИЧЕСКИ ВАЖНО для безопасности: если какой-либо каталог имеет nlink = 2, одна команда rm -rf по его пути молча удалит тысячи подкаталогов (rmdir-бомба). Обходит записи DIR_INDEX, перекрёстно проверяет DIR_ITEM на обнаружение коллизий хешей (эмпирически подтверждено 0 коллизий).
remove_orphan_inode_subtrees.cУдаляет поддеревья inode-сирот (семейства каталогов + отдельные REG) из FS_TREE. Для каждой цели: обходит и удаляет EXTENT_DATA, INODE_REF, INODE_EXTREF, XATTR и, наконец, INODE_ITEM. Транзакция на каждое семейство каталогов (атомарно для поддерева), порции по 50 для отдельных REG. Жёстко заданный параноидальный список исключений.
⚠️ ВАЖНОЕ ПРЕДУПРЕЖДЕНИЕ ПО БЕЗОПАСНОСТИ: см. критерий «Неуязвимое подмножество» ниже.
remove_stale_ptrs_v2.cУлучшенная версия remove_stale_ptrs: обнаруживает пустые листья с parent expected_key (v1 пропускала этот случай), рекурсивное сканирование 2 уровней (корень→уровень1 + уровень1→листья), динамический буфер (без ограничения 512), терпим к сбоям read_tree_block.
insert_one_extent_poc.cPoC для вставки одного экстента с проверкой. Используется для проверки пути API перед запуском rebuild_extent_tree_apply.
Во время сессии 2026-04-05 remove_orphan_inode_subtrees дважды упал на одном и том же утверждении BUG_ON по двум разным причинам:
Вектор аварии 1: Прямой вызов btrfs_cow_block(leaf) для смешанного листа (gen 3601, содержит как inode-сироты, так и живые) → update_ref_for_cow обходит дочерние элементы → __btrfs_mod_ref(inc=1) по устаревшим дочерним элементам-соседям → btrfs_free_extent(phantom) возвращает -ENOENT → BUG_ON → SIGABRT.
Вектор аварии 2: btrfs_del_items после очистки опустошает лист ниже LEAF_DATA_SIZE/4 = 4096 байт → вызывает push_leaf_left(sibling) или push_leaf_right(sibling) → если у соседа gen ≤ last_snapshot = 3701, btrfs_block_can_be_shared возвращает 1 → update_ref_for_cow входит в ветку refs > 1 → btrfs_inc_ref(cow_sibling, 0) → __btrfs_mod_ref(cow, level=0, inc=1) → перебирает все EXTENT_DATA устаревшего соседа → btrfs_inc_extent_ref(phantom_bytenr) → BUG_ON(err) в extent-tree.c:1302 → SIGABRT.
Флаги fs_info->rebuilding_extent_tree = 1 и trans->reinit_extent_tree = true НЕ спасают путь INC: они освобождают только от BTRFS_DROP_DELAYED_REF (проверено в extent-tree.c:3885). BTRFS_ADD_DELAYED_REF (из btrfs_inc_ref) является фатальным.
Критерий «Неуязвимое подмножество» для любого целевого inode, который будет удалён:
INODE_ITEM inode, gen > 3701 (после аварии)gen > 3701used после очистки > 4096 (не вызывается перебалансировка)gen > 3701 (даже если условие 3 не выполняется, перебалансировка на соседи после аварии безопасна)disk_bytenr) разрешаются в текущем дереве экстентов (нет -ENOENT при поиске обратной ссылки)Нарушение любого из условий 3+4 вызывает вектор аварии 2. Условие 5 снимается reinit_extent_tree для DROP, но НЕ для INC (именно это вызывает push_leaf_left).
Для любого набора потенциальных inode-сирот обойдите дамп FS_TREE и классифицируйте каждый целевой лист по 5 условиям неуязвимости. Пример шаблона (анонимизированный):
Листья, где ≥90% элементов являются сиротами, представляют собой опасную зону: они с уверенностью опустошатся ниже порога перебалансировки (LEAF_DATA_SIZE/4 = 4096 байт), что вынудит push_leaf_left/right. Если какой-либо непосредственный сосед в родительском узле имеет gen ≤ last_snapshot, толчок запускает CoW на этом соседе, который входит в путь btrfs_block_can_be_shared → refs > 1 → btrfs_inc_ref → __btrfs_mod_ref(inc=1) и аварийно завершается с BUG_ON(err) в btrfs_inc_extent_ref.
Смягчение: исключите проблемные inode из входного файла. Инструмент обрабатывает то, что проходит предварительную проверку; листья со смешанными безопасными/небезопасными целями могут быть частично обработаны путём указания только безопасного подмножества. Семантика транзакций для каждого семейства означает, что каждое безопасное семейство фиксируется атомарно, даже если другие семейства исключены.
Эмпирический результат: начиная с N кандидатов-сирот, после применения всех 5 условий окончательное безопасное подмножество составило ~14% от входных данных, но это подмножество зафиксировалось без единого BUG_ON, с нулевым расхождением по базовой sha256 живых файлов, полученной до записи.
patches/alloc_reserved_tree_block_eexist.patch изменяет btrfs-progs так, что когда alloc_reserved_tree_block обнаруживает, что METADATA_ITEM уже существует, он возвращает 0 вместо распространения EEXIST. Это необходимо для работы пакетной вставки обратных ссылок: при вставке множества обратных ссылок система отложенных ссылок также пытается создать METADATA_ITEM для блоков, только что выделенных через COW, и сталкивается с теми, которые мы уже вставили.
# 1. Backup
mkdir -p backup
for DEV in /dev/sdX1 /dev/sdY1; do
sudo dd if=$DEV of=backup/$(basename $DEV).sb bs=4096 count=1 skip=16
done
# 2. Make sure the filesystem is unmounted
sudo umount /mnt/pool 2>/dev/null
# 3. Zero the log tree (if applicable)
sudo btrfs rescue zero-log /dev/sdX1
# 4. Scan + fix everything (in order)
sudo ./scan_and_fix_all_backrefs /dev/sdX1 --write
sudo ./fix_bad_levels /dev/sdX1 --write
sudo ./fix_owner_refs /dev/sdX1 --write
sudo ./fix_duplicate_extents /dev/sdX1 --write
sudo ./remove_stale_ptrs /dev/sdX1 --write
# 5. Re-scan to verify convergence
sudo ./scan_and_fix_all_backrefs /dev/sdX1
sudo ./remove_stale_ptrs /dev/sdX1
# 6. If the csum tree is broken:
sudo ./fix_csum_tree /dev/sdX1
sudo ./set_nodatasum /dev/sdX1 --write
# 7. Try mounting RW
sudo mount -o rw /dev/sdX1 /mnt/pool
# 8. If it mounts, verify with btrfs check readonly
sudo btrfs check --force /dev/sdX1
Каждое исправление может создавать новые проблемы через COW: когда инструмент изменяет дерево экстентов, btrfs выполняет COW для затронутых узлов. Новые узлы копируют указатели из старых, что может распространять устаревшие указатели. Может потребоваться несколько проходов.
Несоответствия ссылок на экстенты данных не исправляются: эти инструменты затрагивают только обратные ссылки метаданных. Неправильные счётчики ссылок на экстенты данных (часто после неудачных запусков btrfs check --repair) не очищаются.
Inode-сироты не очищаются: записи каталогов-сирот в FS_TREE (ссылки на несуществующие inode) не удаляются.
Не заменяет btrfs check --repair: эти инструменты нацелены на конкретные сценарии. При лёгких или умеренных повреждениях лучше использовать btrfs check --repair.
НИКОГДА не делайте жёсткую перезагрузку файловой системы BTRFS на нескольких устройствах: сочетанное повреждение дерева свободного пространства и дерева экстентов чрезвычайно трудно исправить.
НИКОГДА не запускайте btrfs check --repair несколько раз подряд, если первый запуск не решил все проблемы: он может войти в бесконечный цикл и сделать файловую систему значительно хуже.
Всегда делайте резервную копию суперблоков перед каждой операцией записи.
trans->reinit_extent_tree = true является ключом к игнорированию сбоев DROP в отложенных ссылках для блоков без обратных ссылок.
fs_info->rebuilding_extent_tree = 1 отключает проверки пространства во время исправлений.
Одна большая фиксация с множеством вставок лучше, чем много маленьких фиксаций, потому что промежуточные фиксации перемещают корневое дерево.
backup_slots в суперблоке НЕ являются историческими резервными копиями: это скользящее окно только из 4 последних фиксаций. Цикл btrfs check --repair из 46 000+ фиксаций повернёт каждый слот около 11 000 раз за несколько минут, уничтожая любое состояние до аварии, которое можно восстановить из ядра. Для реального хранения вам нужны явные btrfs subvolume snapshot или потоки btrfs send на другое устройство.
Эти инструменты были написаны для конкретного случая восстановления, когда стандартные инструменты не работали. Они не протестированы для общего использования. Используйте их только если вы понимаете код и принимаете риск потери данных.
Всегда копируйте свои данные перед попыткой любого восстановления, если это вообще возможно.
GPL-2.0 (совместима с btrfs-progs, чей внутренний API используют эти инструменты).
| Leaf | Gen | Orphan items / total | Post-purge used (est) | Rebalance? | Immediate siblings | Verdict |
|---|
$LEAF_A | после аварии | в основном сироты, интенсивная очистка | ниже порога | ДА | все после аварии | ✓ безопасно |
$LEAF_B | после аварии | в основном живые, лёгкая очистка | выше порога | НЕТ | чистый родитель | ✓ безопасно |
$LEAF_C | после аварии | почти 100% сирот | значительно ниже 4096 | ДА принудительно | устаревшие до аварии | ❌ СБОЙ |
reinit_extent_tree АСИММЕТРИЧЕН: освобождает только от BTRFS_DROP_DELAYED_REF, НЕ от BTRFS_ADD_DELAYED_REF. Любой путь кода, который вызывает btrfs_inc_ref для устаревшего листа (включая push_leaf_left/right во время перебалансировки), всё равно вызовет сбой через btrfs_inc_extent_ref → BUG_ON(err).
Критерий безопасности для обработки inode в повреждённом FS_TREE должен включать соседей, а не только сам целевой лист. См. раздел «Критерий «Неуязвимое подмножество»».
Базовая sha256 ЖИВЫХ файлов является единственным эмпирическим доказательством инвариантов. Снимайте её до любой операции записи, сравнивайте после. Любое несовпадение = откат.
У каталогов i_size хранится как sum(name_len × 2), НЕ sum(name_len). Любой инструмент очистки сирот, который уменьшает i_size при удалении записи, должен уменьшать на namelen × 2. Ошибка в этом оставляет каталоги в недопустимом состоянии, которое может проявиться позже как nlink = 2: это вызывает rmdir-бомбу, если пул смонтирован RW (один rm -rf для родителя может молча удалить тысячи подкаталогов).
Агенты-эксперты с эмпирическими данными критически важны. В сессии 2026-04-05 использовались два параллельных рецензента Opus (внутреннее устройство btrfs + эксплуатация), которые анализировали предложенный план на основе вывода dump-tree. Они выявили детерминированный вектор аварии (push_leaf_left → устаревший сосед), который повторил бы предыдущие сбои. Текстовый обзор плана без эмпирического анализа dump-tree не смог бы этого обнаружить.