
Strumenti di riparazione BTRFS personalizzati per grave corruzione dell'albero delle estensioni dove btrfs check --repair fallisce (segfault, loop o deadlock)
Strumenti personalizzati scritti durante il recupero di un pool BTRFS multi-dispositivo da 12TB con grave corruzione dell'albero degli extent che i comandi nativi (btrfs check --repair, --init-extent-tree, ecc.) non sono riusciti a riparare.
Vedi INCIDENT-ANALYSIS.md per un caso di studio strutturato del recupero, una classificazione delle cause principali e una serie di proposte costruttive per miglioramenti upstream di btrfs-progs che avrebbero impedito la necessità della maggior parte di questi strumenti.
Usa questi strumenti SOLO se btrfs check --repair va in segfault, entra in un loop infinito, o lascia il filesystem in condizioni peggiori di prima.
Casi documentati in cui aiutano:
btrfs check --repair va in segfault a [3/8] checking extents (Issue #525)btrfs check --init-extent-tree si bloccabtrfs check --repair entra in un loop infinito ripetendo le stesse riparazionirescue=all,ro, non riesce a montare RWQuesti strumenti NON sono per corruzione leggera. Per danni normali, prova prima 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 è opt-in)Gli strumenti utilizzano l'API interna di btrfs-progs e devono essere compilati all'interno dell'albero sorgente di 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
Ordine di esecuzione consigliato:
scan_and_fix_all_backrefs.c (più importante)Lo strumento più importante. Attraversa ricorsivamente ogni albero nel filesystem (ROOT, CHUNK, EXTENT, FS, DEV, CSUM, UUID, FREE_SPACE) e rileva i blocchi di metadati a cui manca un backref METADATA_ITEM nell'albero degli extent. Inietta tutti i backref mancanti in una singola transazione per evitare il problema dello "spostamento dell'albero radice tra i commit".
Utilizzo:
sudo ./scan_and_fix_all_backrefs /dev/sdX # scan only
sudo ./scan_and_fix_all_backrefs /dev/sdX --write # scan + inject
fix_owner_refs.cCorregge il campo owner nel TREE_BLOCK_REF inline quando non corrisponde all'effettivo btrfs_header_owner() del blocco. Le discrepanze si verificano quando i blocchi vengono riassegnati tra alberi durante riparazioni fallite.
sudo ./fix_owner_refs /dev/sdX # scan
sudo ./fix_owner_refs /dev/sdX --write # fix
fix_bad_levels.cCorregge le voci METADATA_ITEM e EXTENT_ITEM con un livello errato. Livelli corrotti (es. 50, 55, 237) sono spazzatura lasciata da btrfs check --repair in un loop. Verificato rispetto al reale btrfs_header_level() del blocco.
sudo ./fix_bad_levels /dev/sdX # scan
sudo ./fix_bad_levels /dev/sdX --write # fix
fix_duplicate_extents.cElimina METADATA_ITEM duplicati (stesso bytenr, livelli diversi nella chiave). Conserva quello il cui livello corrisponde a btrfs_header_level ed elimina l'altro.
sudo ./fix_duplicate_extents /dev/sdX # scan
sudo ./fix_duplicate_extents /dev/sdX --write # delete duplicates
remove_stale_ptrs.cScansiona ogni nodo di livello 1 del FS_TREE. Rileva puntatori figli obsoleti usando tre controlli: owner non corrispondente, first_key non corrispondente, o una first_key il cui tipo non è valido per FS_TREE (es. BLOCK_GROUP_ITEM). Li rimuove con btrfs_del_ptr.
sudo ./remove_stale_ptrs /dev/sdX # scan
sudo ./remove_stale_ptrs /dev/sdX --write # remove
fix_uuid_tree.c / fix_csum_tree.cCrea una foglia vuota rispettivamente per l'albero UUID / albero CSUM. Utile quando il ROOT_ITEM punta a un blocco riassegnato ad un altro albero. Il kernel rigenera automaticamente l'albero UUID al montaggio RW. Con un albero CSUM vuoto, i file contrassegnati NODATASUM non falliscono la verifica.
sudo ./fix_uuid_tree /dev/sdX
sudo ./fix_csum_tree /dev/sdX
set_nodatasum.cImposta il flag BTRFS_INODE_NODATASUM sugli inode dei file regolari. Usalo se l'albero csum è vuoto ma i file hanno ancora checksum attesi, causando errori di lettura. Con NODATASUM, il kernel salta le ricerche di csum.
sudo ./set_nodatasum /dev/sdX # scan
sudo ./set_nodatasum /dev/sdX --write # apply
fix_fstree_node.cVersione con una lista hardcoded di blocchi obsoleti. Preferisci remove_stale_ptrs, che li rileva automaticamente. Usalo solo se hai bisogno di controllo manuale su quali blocchi specifici rimuovere.
add_backrefs.cVersione iniziale con una lista hardcoded di backref mancanti. Preferisci scan_and_fix_all_backrefs, che li rileva automaticamente.
Quando gli strumenti di base sopra erano insufficienti (pool con 200K+ errori distribuiti su più alberi), questi strumenti aggiuntivi sono stati costruiti:
scan_fstree_extents.c + scan_extent_tree.cScanner di Passo 1 e Passo 2 che attraversano rispettivamente FS_TREE e albero degli extent, producendo file TSV con ogni mapping ref/extent. Utilizzati per costruire l'input per rebuild_extent_tree_apply quando l'albero degli extent deve essere ricostruito da zero.
rebuild_extent_tree_apply.c (scrittore pesante)Il principale scrittore della Fase 3. Prende una lista pre-ripiegata di ref (dal diff di scan_fstree_extents + scan_extent_tree) e inietta 3M+ EXTENT_DATA_REF nell'albero degli extent in blocchi di 5000 per transazione. Limita ogni 50K elementi per evitare stalli di ri-shingling DM-SMR. Verificato con successo a 3.248.617 inserimenti in ~34 min su 3 dischi 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.cCorrettore chirurgico a singolo campo per BLOCK_GROUP_ITEM.used quando lo scrittore della Fase 3 lascia un bg specifico con overshoot a causa di file_extent_items sovrapposti preesistenti. Utilizza btrfs_set_block_group_used setter diretto per evitare la contabilità space_info di btrfs_update_block_group (che NON vogliamo qui). Pre-valida flags & BTRFS_BLOCK_GROUP_DATA.
sudo ./patch_block_group_used /dev/sdX1 <bg_bytenr> <bg_length> <new_used> --write
remove_extent_items_by_key.cElimina una lista hardcoded di EXTENT_ITEM (bytenr, num_bytes, expected_inode) dall'albero degli extent. Usato per pulire extent obsoleti sovrapposti in una singola foglia che impediscono il montaggio RO. Controlli di sanità per ogni elemento prima dell'eliminazione (7 invarianti inclusa whitelist inode). Esegue con rebuilding_extent_tree=1 + reinit_extent_tree=true per saltare la contabilità spazio (il chiamante corregge used manualmente prima tramite patch_block_group_used).
clean_orphan_dir_entries.cPulisce le voci DIR_ITEM + DIR_INDEX orfane da FS_TREE. Blocchi di 100 voci per transazione. Aggiorna i_size di INODE_ITEM padre (decremento di namelen × 2: bug critico corretto: v1 decrementava solo di namelen, lasciando le directory in stato non valido). Lista di esclusione hardcoded per nomi di directory di primo livello critici (es. pelis, series, music, backups, homestorage). MAI decrementare i_size per il solo namelen: BTRFS memorizza una contabilità di namelen × 2.
clean_orphan_inode_refs.cAttraversa FS_TREE per elementi INODE_REF il cui key.offset (inode padre) è in una lista di padri orfani. Salta INODE_EXTREF per evitare falsi positivi (il key.offset di EXTREF è un hash, non un ID padre). Blocchi di 32 per transazione.
fix_dir_inode_counts.cRicalcola i_size = sum(name_len × 2) e nlink = 1 per inode DIR i cui conteggi sono stati corrotti da bug precedenti nella pulizia degli orfani. CRITICO per la sicurezza: se un qualsiasi DIR ha nlink = 2, un singolo rm -rf sul suo percorso eliminerà silenziosamente migliaia di sottodirectory (bomba rmdir). Attraversa le voci DIR_INDEX, incrocia i controlli con DIR_ITEM per il rilevamento di collisioni hash (0 collisioni verificate empiricamente).
remove_orphan_inode_subtrees.cRimuove i sottoalberi di inode orfani (famiglie DIR + REG standalone) da FS_TREE. Per ogni target: attraversa ed elimina EXTENT_DATA, INODE_REF, INODE_EXTREF, XATTR e infine INODE_ITEM. Transazione per famiglia DIR (atomica per sottoalbero), blocchi di 50 per REG standalone. Lista di esclusione paranoica hardcoded.
⚠️ AVVERTENZA IMPORTANTE per la sicurezza: vedi "Criterio del sottoinsieme a prova di proiettile" sotto.
remove_stale_ptrs_v2.cVersione migliorata di remove_stale_ptrs: rileva foglie vuote con parent expected_key (v1 saltava questo caso), scansione ricorsiva a 2 livelli (radice→livello1 + livello1→foglie), buffer dinamico (nessun limite di 512), tollera fallimenti di read_tree_block.
insert_one_extent_poc.cPoC per l'inserimento di un singolo extent con validazione. Usato per validare il percorso API prima di eseguire rebuild_extent_tree_apply.
Durante la sessione del 2026-04-05, remove_orphan_inode_subtrees è crashato due volte sulla stessa asserzione BUG_ON per due ragioni differenti:
Vettore di crash 1: btrfs_cow_block(leaf) diretto su una foglia MIXED (gen 3601, contiene sia inode orfani che vivi) → update_ref_for_cow attraversa i figli → __btrfs_mod_ref(inc=1) su figli fratelli obsoleti → btrfs_free_extent(phantom) restituisce -ENOENT → BUG_ON → SIGABRT.
Vettore di crash 2 (scoperto in seguito, evitato filtrando): btrfs_del_items dopo la pulizia riduce una foglia al di sotto di LEAF_DATA_SIZE/4 = 4096 byte → invoca push_leaf_left(sibling) o push_leaf_right(sibling) → se il fratello ha gen ≤ last_snapshot = 3701, btrfs_block_can_be_shared restituisce 1 → update_ref_for_cow entra nel percorso refs > 1 → btrfs_inc_ref(cow_sibling, 0) → __btrfs_mod_ref(cow, level=0, inc=1) → itera tutti gli EXTENT_DATA del fratello obsoleto → btrfs_inc_extent_ref(phantom_bytenr) → BUG_ON(err) in extent-tree.c:1302 → SIGABRT.
I flag fs_info->rebuilding_extent_tree = 1 e trans->reinit_extent_tree = true NON salvano il percorso INC: esentano solo BTRFS_DROP_DELAYED_REF (verificato in extent-tree.c:3885). BTRFS_ADD_DELAYED_REF (da btrfs_inc_ref) è fatale.
Criterio a prova di proiettile per qualsiasi inode target che verrà eliminato:
INODE_ITEM dell'inode ha gen > 3701 (post-crash)gen > 3701used stimati dopo la pulizia sono > 4096 (nessun trigger di ribilanciamento)gen > 3701 (anche se la condizione 3 fallisce, il ribilanciamento verso fratelli post-crash è sicuro)disk_bytenr) si risolvono nell'albero degli extent corrente (nessun -ENOENT nella ricerca di backref)Violare una qualsiasi delle condizioni 3+4 attiva il vettore di crash 2. La condizione 5 è esonerata da reinit_extent_tree per DROP ma NON per INC (che è ciò che invoca push_leaf_left).
Per qualsiasi insieme di inode orfani candidati, attraversa il dump di FS_TREE e classifica ogni foglia target secondo le 5 condizioni a prova di proiettile. Esempio di schema (anonimizzato):
Le foglie in cui ≥90% degli elementi sono orfani sono la zona pericolosa: scenderanno al di sotto della soglia di ribilanciamento (LEAF_DATA_SIZE/4 = 4096 byte) con certezza, forzando push_leaf_left/right. Se un qualsiasi fratello immediato nel nodo padre ha gen ≤ last_snapshot, il push attiva CoW su quel fratello, che entra nel percorso btrfs_block_can_be_shared → refs > 1 → btrfs_inc_ref → __btrfs_mod_ref(inc=1) e crasha con BUG_ON(err) in btrfs_inc_extent_ref.
Mitigazione: escludi gli inode problematici dal file di input. Lo strumento elabora tutto ciò che supera la validazione pre-volo; le foglie con target misti sicuri/non sicuri possono essere elaborate parzialmente elencando solo il sottoinsieme sicuro. La semantica di transazione per famiglia significa che ogni famiglia sicura viene impegnata atomicamente anche se altre famiglie sono escluse.
Risultato empirico da una sessione: partendo da N orfani candidati, dopo aver applicato tutte e 5 le condizioni il sottoinsieme sicuro finale era circa il 14% dell'input, ma quel sottoinsieme è stato impegnato senza un singolo BUG_ON, con un diff di 0 byte su uno sha256 di base dei file live catturati prima della scrittura.
patches/alloc_reserved_tree_block_eexist.patch modifica btrfs-progs in modo che quando alloc_reserved_tree_block trova che METADATA_ITEM esiste già, restituisce 0 invece di propagare EEXIST. Ciò è necessario affinché l'iniezione batch di backref funzioni: quando si inietta molti backref, anche il sistema di delayed refs cerca di creare METADATA_ITEM per blocchi appena allocati tramite COW e collide con quelli che abbiamo già inserito.
# 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
Ogni riparazione può creare nuovi problemi tramite COW: quando uno strumento modifica l'albero degli extent, btrfs esegue COW sui nodi interessati. I nuovi nodi copiano i puntatori da quelli vecchi, il che può propagare puntatori obsoleti. Potrebbero essere necessari più passaggi.
Le discrepanze dei riferimenti agli extent dati non vengono corrette: questi strumenti toccano solo i backref dei metadati. I conteggi di ref errati sugli extent dati (comuni dopo esecuzioni fallite di btrfs check --repair) non vengono puliti.
Gli inode orfani non vengono puliti: le voci di directory orfane in FS_TREE (riferimenti a inode che non esistono più) non vengono rimosse.
Non sostituisce btrfs check --repair: questi strumenti mirano a scenari specifici. Per danni leggeri o moderati, btrfs check --repair è migliore.
MAI effettuare un power-cycle improvviso su un filesystem BTRFS multi-dispositivo: la corruzione combinata dell'albero dello spazio libero e dell'albero degli extent è estremamente difficile da riparare.
MAI eseguire btrfs check --repair più volte di seguito se la prima esecuzione non ha risolto tutto: può entrare in un loop infinito e peggiorare drasticamente il filesystem.
Eseguire sempre il backup dei superblocchi prima di ogni operazione di scrittura.
trans->reinit_extent_tree = true è fondamentale per ignorare i fallimenti DROP nei delayed refs per blocchi senza backref.
fs_info->rebuilding_extent_tree = 1 disabilita i controlli di spazio durante le riparazioni.
Un grande commit con molti inserimenti è meglio di tanti piccoli commit, perché i commit intermedi spostano l'albero radice.
I backup_slots nel SB NON sono backup storici: sono una finestra scorrevole dei soli ultimi 4 commit. Un loop di btrfs check --repair di 46.000+ commit ruoterà ogni slot circa 11.000 volte in minuti, obliterando qualsiasi stato pre-crash recuperabile dal kernel. Per una conservazione reale sono necessari flussi espliciti di btrfs subvolume snapshot o btrfs send verso un altro dispositivo.
: esenta solo , NON . Qualsiasi percorso di codice che chiami su una foglia obsoleta (inclusi durante il ribilanciamento) crasha ancora tramite → .
Questi strumenti sono stati scritti per un caso di recupero specifico in cui gli strumenti nativi fallivano. Non sono testati per casi d'uso generali. Usali solo se comprendi il codice e accetti il rischio di perdita di dati.
Se possibile, copia sempre i tuoi dati prima di tentare qualsiasi riparazione.
GPL-2.0 (compatibile con btrfs-progs, la cui API interna questi strumenti utilizzano).
| Foglia | Gen | Elementi orfani / totale | Usato post-pulizia (stima) | Ribilanciamento? | Fratelli immediati | Verdetto |
|---|
$LEAF_A | post-crash | per lo più orfani, pulizia pesante | sotto soglia | SÌ | tutti post-crash | ✓ sicuro |
$LEAF_B | post-crash | per lo più vivi, pulizia leggera | sopra soglia | NO | genitore pulito | ✓ sicuro |
$LEAF_C | post-crash | quasi 100% orfani | molto sotto 4096 | SÌ forzato | obsoleto pre-crash | ❌ CRASH |
reinit_extent_tree è ASIMMETRICOBTRFS_DROP_DELAYED_REFBTRFS_ADD_DELAYED_REFbtrfs_inc_refpush_leaf_left/rightbtrfs_inc_extent_refBUG_ON(err)Il criterio di sicurezza per l'elaborazione degli inode in un FS_TREE danneggiato deve includere i fratelli, non solo la foglia target stessa. Vedi sezione "Criterio del sottoinsieme a prova di proiettile".
Lo sha256 di base dei file LIVE è l'unica prova empirica degli invarianti. Catturalo prima di qualsiasi operazione di scrittura, confrontalo dopo. Qualsiasi discrepanza = rollback.
i_size delle DIR è memorizzato come sum(name_len × 2), NON sum(name_len). Qualunque strumento di pulizia degli orfani che decrementi i_size alla rimozione di una voce deve decrementare di namelen × 2. Sbagliare questo lascia le DIR in uno stato non valido che può manifestarsi come nlink = 2 in seguito: ciò innesca una bomba rmdir se il pool è montato RW (un singolo rm -rf su un genitore può eliminare silenziosamente migliaia di sottodirectory).
Revisori esperti con prove empiriche sono fondamentali. La sessione del 2026-04-05 ha utilizzato due revisori Opus paralleli (interni btrfs + operazioni) che hanno analizzato il piano proposto rispetto all'output dump-tree. Hanno individuato un vettore di crash deterministico (push_leaf_left → fratello obsoleto) che avrebbe ripetuto i fallimenti precedenti. Una revisione testuale del piano senza analisi empirica del dump-tree lo avrebbe perso.