12TB マルチデバイス BTRFS プールにおいて、ネイティブコマンド(btrfs check --repair、--init-extent-tree など)では修復できなかった深刻なエクステントツリーの破損を回復するために作成されたカスタムツールです。
構造化されたリカバリのケーススタディ、根本原因の分類、およびこれらのツールのほとんどを不要にするはずだった上流 btrfs-progs の改善提案については、INCIDENT-ANALYSIS.md を参照してください。
これらのツールは、btrfs check --repair がセグフォルトする、無限ループに入る、またはファイルシステムを以前よりも悪化させる場合にのみ使用してください。
これらのツールが役立つことが確認されているケース:
btrfs check --repair が [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 はオプトイン)ツールは内部の btrfs-progs API を使用しており、btrfs-progs ソースツリー内でビルドする必要があります:
# 1. btrfs-progs のクローン
git clone --depth 1 --branch v6.19.1 https://github.com/kdave/btrfs-progs.git
cd btrfs-progs
# 2. EEXIST パッチの適用(バッチバック参照注入に必要)
patch -p1 < path/to/btrfs_fixes/patches/alloc_reserved_tree_block_eexist.patch
# 3. ベースとなる btrfs-progs の構成とビルド
./autogen.sh
./configure
make -j$(nproc)
# 4. このリポジトリから .c ファイルを btrfs-progs ディレクトリにコピー
cp path/to/btrfs_fixes/programs/*.c .
# 5. 各プログラムを Makefile に追加:
echo '
PROGNAME: PROGNAME.o $(objects) $(libs_shared)
@echo " [LD] $@"
$(Q)$(CC) -o $@ PROGNAME.o $(objects) $(libs_shared) $(LDFLAGS) $(LIBS)
' >> Makefile
# 6. ビルド
make PROGNAME
推奨される実行順序:
scan_and_fix_all_backrefs.c (最も重要)最も重要なツールです。 ファイルシステム内のすべてのツリー (ROOT, CHUNK, EXTENT, FS, DEV, CSUM, UUID, FREE_SPACE) を再帰的に巡回し、エクステントツリーに METADATA_ITEM バック参照がないメタデータブロックを検出します。欠落しているすべてのバック参照を1つのトランザクションで注入し、「ルートツリーがコミット間で移動する」問題を回避します。
使用方法:
sudo ./scan_and_fix_all_backrefs /dev/sdX # スキャンのみ
sudo ./scan_and_fix_all_backrefs /dev/sdX --write # スキャン + 注入
fix_owner_refs.cインライン TREE_BLOCK_REF 内の owner が、ブロックの実際の 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.cFS_TREE のすべてのレベル1ノードをスキャンします。3つのチェック(owner 不一致、first_key 不一致、または FS_TREE では無効なタイプ(例: BLOCK_GROUP_ITEM)の first_key)を使用して古い子ポインタを検出します。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 が別のツリーに再割り当てされたブロックを指している場合に便利です。カーネルは RW マウント時に自動的に UUID ツリーを再生成します。空の CSUM ツリーの場合、NODATASUM とフラグが立てられたファイルは検証に失敗しません。
sudo ./fix_uuid_tree /dev/sdX
sudo ./fix_csum_tree /dev/sdX
set_nodatasum.c通常のファイル inode に BTRFS_INODE_NODATASUM フラグを設定します。csum ツリーが空でもファイルに期待されるチェックサムがある場合に使用します。これにより読み取りエラーが発生します。NODATASUM により、カーネルは checksum のルックアップをスキップします。
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 を優先してください。
上記のベースラインツールでは不十分だった場合 (プールに複数のツリーにまたがる 200K+ のエラーがある場合)、以下の追加ツールが作成されました:
scan_fstree_extents.c + scan_extent_tree.cそれぞれ FS_TREE と extent tree をウォークし、すべての参照/エクステントマッピングを含む TSV ファイルを生成する Pass 1 および Pass 2 スキャナーです。エクステントツリーをスクラッチから再構築する必要がある場合に、rebuild_extent_tree_apply への入力を作成するために使用されます。
rebuild_extent_tree_apply.c (ヘビーライター)メインの Phase 3 ライターです。事前に折り畳まれた参照リスト(scan_fstree_extents + scan_extent_tree の差分から取得)を受け取り、トランザクションあたり 5000 チャンクで 300万+ の EXTENT_DATA_REF をエクステントツリーに注入します。DM-SMR の再シングルストールを避けるため、50K アイテムごとにスロットリングします。3× WD40EFAX SMR ディスク上で、約 34 分で 3,248,617 の挿入を成功させた実績があります。
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.cPhase 3 ライターが既存の重複する file_extent_item のために特定の bg にオーバーシュートを残した場合に、BLOCK_GROUP_ITEM.used に対する外科的な単一フィールドパッチャーです。btrfs_update_block_group の space_info アカウンティング(ここでは不要)を避けるために、btrfs_set_block_group_used の直接セッターを使用します。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エクステントツリーから (bytenr, num_bytes, expected_inode) EXTENT_ITEM のハードコードされたリストを削除します。RO マウントを妨げる単一のリーフ内の重複する古いエクステントをクリーンアップするために使用されます。削除前にアイテムごとのサニティチェック(inode 許可リストを含む 7 つの不変条件)を実行します。rebuilding_extent_tree=1 + reinit_extent_tree=true で実行し、space アカウンティングをスキップします(呼び出し側は patch_block_group_used で先に used をパッチします)。
clean_orphan_dir_entries.cFS_TREE から孤立した DIR_ITEM + DIR_INDEX エントリをクリーンアップします。トランザクションあたり 100 エントリのチャンクで動作します。親 INODE_ITEM の i_size を更新します(namelen × 2 でデクリメント: 重大なバグ修正: v1 は namelen のみでデクリメントし、ディレクトリを無効な状態にしていました)。重要なトップレベルディレクトリ名(例: pelis, series, music, backups, homestorage)のハードコードされた除外リストがあります。i_size を raw namelen で決してデクリメントしないでください: BTRFS は namelen × 2 のアカウンティングを保存します。
clean_orphan_inode_refs.ckey.offset(親 inode)が孤立した親リストにある INODE_REF アイテムについて FS_TREE をウォークします。誤検出を避けるため、INODE_EXTREF はスキップします(EXTREF の key.offset はハッシュであり、親 ID ではありません)。トランザクションあたり 32 チャンクで動作します。
fix_dir_inode_counts.ci_size = sum(name_len × 2) と nlink = 1 を、以前の孤立クリーンアップバグによってカウントが破損した DIR inode に対して再計算します。安全性のために極めて重要: いずれかの DIR が nlink = 2 の場合、そのパスに対する単一の rm -rf が数千のサブディレクトリを静かに削除します(rmdir 爆弾)。DIR_INDEX エントリをウォークし、ハッシュ衝突検出のために DIR_ITEM と相互参照します(経験的に 0 衝突が確認されています)。
remove_orphan_inode_subtrees.cFS_TREE から孤立した inode サブツリー(DIR ファミリー + スタンドアロン REG)を削除します。ターゲットごとに: EXTENT_DATA, INODE_REF, INODE_EXTREF, XATTR、そして最後に INODE_ITEM をウォークして削除します。DIR ファミリーごとにトランザクション(サブツリーごとにアトミック)、スタンドアロン REG は 50 チャンクで動作します。ハードコードされたパラノイド除外リストがあります。
⚠️ 主要な安全性の注意: 以下の「Bulletproof subset criterion」を参照してください。
remove_stale_ptrs_v2.cremove_stale_ptrs の改良版: parent expected_key を持つ空のリーフを検出し(v1 はこのケースをスキップ)、再帰的な 2 レベルスキャン(root→level1 + level1→leaves)、動的バッファ(512 制限なし)、read_tree_block の失敗を許容します。
insert_one_extent_poc.c検証付きの単一エクステント挿入の PoC です。rebuild_extent_tree_apply を実行する前に API パスを検証するために使用されました。
2026-04-05 セッション中、remove_orphan_inode_subtrees は異なる理由で同じ BUG_ON アサーションで 2 回クラッシュしました:
クラッシュベクトル 1: MIXED リーフ (gen 3601、孤立した inode とライブ inode の両方を含む) 上の直接の btrfs_cow_block(leaf) → 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) → extent-tree.c:1302 の BUG_ON(err) → SIGABRT。
fs_info->rebuilding_extent_tree = 1 および trans->reinit_extent_tree = true フラグは INC パスを救いません: これらは BTRFS_DROP_DELAYED_REF のみを免除します(extent-tree.c:3885 で確認)。btrfs_inc_ref からの BTRFS_ADD_DELAYED_REF は致命的です。
削除されるターゲット inode のための Bulletproof criterion:
INODE_ITEM をホストするリーフの gen > 3701 (クラッシュ後)gen > 3701used バイト数 > 4096 (リバランストリガーなし)gen > 3701 であること (条件3が失敗しても、クラッシュ後の兄弟へのリバランスは安全)disk_bytenr) が現在のエクステントツリーで解決可能であること (-ENOENT なし)条件 3+4 のいずれかに違反するとクラッシュベクトル 2 がトリガーされます。条件 5 は、reinit_extent_tree が DROP に対しては免除しますが、INC に対しては免除しないため(push_leaf_left が呼び出すのは INC)、無罪とはなりません。
候補となる孤立 inode セットについて、FS_TREE ダンプをウォークし、5 つの bulletproof 条件によって各ターゲットリーフを分類します。パターン例(匿名化):
アイテムの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) パスに入り、btrfs_inc_extent_ref の BUG_ON(err) でクラッシュします。
緩和策: 問題のある inode を入力ファイルから除外します。ツールは事前検証に合格したものだけを処理します。安全/安全でないターゲットが混在するリーフは、安全なサブセットのみをリストすることで部分的に処理できます。ファミリーごとのトランザクションセマンティクスにより、他のファミリーが除外されていても、安全な各ファミリーはアトミックにコミットされます。
あるセッションからの経験的結果: N 個の候補孤立から開始し、5 つの条件をすべて適用した後、最終的な安全なサブセットは入力の約 14% でしたが、そのサブセットは単一の BUG_ON もなくコミットされました。書き込み前にキャプチャされたライブファイルのベースライン sha256 との差分は 0 バイトでした。
patches/alloc_reserved_tree_block_eexist.patch は btrfs-progs を変更し、alloc_reserved_tree_block が METADATA_ITEM がすでに存在することを検出した場合に、EEXIST を伝播する代わりに 0 を返すようにします。これはバッチバック参照注入が機能するために必要です。多くのバック参照を注入する場合、遅延参照システムも COW 経由で新しく割り当てられたブロックに対して METADATA_ITEM を作成しようとし、すでに挿入したものと衝突します。
# 1. バックアップ
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. ファイルシステムがアンマウントされていることを確認
sudo umount /mnt/pool 2>/dev/null
# 3. ログツリーをゼロにする (該当する場合)
sudo btrfs rescue zero-log /dev/sdX1
# 4. すべてをスキャン + 修正 (順番に)
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. 再スキャンして収束を確認
sudo ./scan_and_fix_all_backrefs /dev/sdX1
sudo ./remove_stale_ptrs /dev/sdX1
# 6. csum ツリーが壊れている場合:
sudo ./fix_csum_tree /dev/sdX1
sudo ./set_nodatasum /dev/sdX1 --write
# 7. RW マウントを試行
sudo mount -o rw /dev/sdX1 /mnt/pool
# 8. マウントできたら、btrfs check 読み取り専用で検証
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 は修復中のスペースチェックを無効にします。
多くの小さなコミットよりも、多くの挿入を含む1つの大きなコミットの方が優れています。 中間コミットはルートツリーを移動させるためです。
SB の backup_slots は履歴バックアップではありません: それらは最新の 4 つのコミットのみのスライディングウィンドウです。btrfs check --repair のループが 46,000 回以上のコミットを行うと、数分ですべてのスロットを約 11,000 回ローテーションし、カーネルから回復可能なクラッシュ前の状態を消去します。真の保持には、明示的な btrfs subvolume snapshot または別のデバイスへの btrfs send ストリームが必要です。
reinit_extent_tree は非対称です: BTRFS_DROP_DELAYED_REF のみを免除し、BTRFS_ADD_DELAYED_REF は免除しません。古いリーフで を呼び出すコードパス(リバランス中の を含む)は、 → で依然としてクラッシュします。
これらのツールは、ネイティブツールが失敗する特定のリカバリケースのために作成されました。一般的なユースケースでテストされていません。 コードを理解し、データ損失のリスクを受け入れる場合にのみ使用してください。
可能であれば、修復を試みる前に必ずデータをコピーしてください。
GPL-2.0 (これらのツールが内部 API を使用する btrfs-progs と互換性があります)
| リーフ | Gen | 孤立アイテム/合計 | パージ後使用量 (推定) | リバランス? | 直接兄弟 | 判定 |
|---|
$LEAF_A | クラッシュ後 | ほとんど孤立、大量パージ | しきい値以下 | YES | すべてクラッシュ後 | ✓ 安全 |
$LEAF_B | クラッシュ後 | ほとんどライブ、軽度パージ | しきい値以上 | NO | クリーンな親 | ✓ 安全 |
$LEAF_C | クラッシュ後 | ほぼ 100% 孤立 | 4096 を大幅に下回る | YES 強制 | クラッシュ前の古い | ❌ クラッシュ |
btrfs_inc_refpush_leaf_left/rightbtrfs_inc_extent_refBUG_ON(err)損傷した FS_TREE で inode を処理するための安全基準には、ターゲットリーフ自体だけでなく、兄弟も含める必要があります。 「Bulletproof subset criterion」のセクションを参照してください。
ライブファイルのベースライン sha256 のみが、不変条件の経験的な証明です。 書き込み操作の前にキャプチャし、後に差分を確認します。不一致があればロールバックします。
DIR i_size は sum(name_len × 2) として保存され、sum(name_len) ではありません。 エントリ削除時に i_size をデクリメントする孤立クリーンアップツールは、namelen × 2 でデクリメントする必要があります。これを間違えると、DIR が無効な状態になり、後で nlink = 2 として現れる可能性があります。これは、プールが RW でマウントされている場合に rmdir 爆弾を引き起こします(親に対する単一の rm -rf が数千のサブディレクトリを静かに削除する可能性があります)。
経験的証拠を持つ専門家レビューエージェントが重要です。 2026-04-05 セッションでは、2 つのパラレル Opus レビューア(btrfs 内部 + 運用)が、ダンプツリー出力に対して提案された計画を分析しました。彼らは、以前の失敗を繰り返していたはずの決定論的なクラッシュベクトル(push_leaf_left → 古い兄弟)を発見しました。経験的なダンプツリー分析なしのテキスト計画レビューではこれを見逃していたでしょう。