
Herramientas personalizadas de reparación BTRFS para corrupción grave del árbol de extent donde btrfs check --repair falla (segfault, bucle o bloqueo)
Herramientas escritas durante la recuperación de un pool BTRFS multidivisa de 12 TB con
corrupción grave del árbol de extent que los comandos nativos (btrfs check --repair,
--init-extent-tree, etc.) no pudieron reparar.
Consulte INCIDENT-ANALYSIS.md para un estudio de caso
estructurado de la recuperación, una clasificación de causa raíz y un conjunto de
propuestas constructivas para mejoras en btrfs-progs que habrían evitado
la necesidad de la mayoría de estas herramientas.
Use estas herramientas SOLO si btrfs check --repair se cuelga, entra en un bucle
infinito o deja el sistema de archivos en peor estado que antes.
Casos documentados donde ayudan:
btrfs check --repair se cuelga en (Issue #525)[3/8] checking extentsbtrfs check --init-extent-tree se bloqueabtrfs check --repair entra en un bucle infinito repitiendo las mismas reparacionesrescue=all,ro, falla al montar RWEstas herramientas NO son para corrupción leve. Para daños normales, pruebe
primero 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 es optativo)Las herramientas usan la API interna de btrfs-progs y deben compilarse dentro del árbol de fuentes de btrfs-progs:
# 1. Clonar btrfs-progs
git clone --depth 1 --branch v6.19.1 https://github.com/kdave/btrfs-progs.git
cd btrfs-progs
# 2. Aplicar el parche EEXIST (necesario para la inyección por lotes de backrefs)
patch -p1 < ruta/a/btrfs_fixes/patches/alloc_reserved_tree_block_eexist.patch
# 3. Configurar y compilar btrfs-progs base
./autogen.sh
./configure
make -j$(nproc)
# 4. Copiar los archivos .c de este repositorio al directorio de btrfs-progs
cp ruta/a/btrfs_fixes/programs/*.c .
# 5. Para cada programa, agregar al Makefile:
echo '
PROGNAME: PROGNAME.o $(objects) $(libs_shared)
@echo " [LD] $@"
$(Q)$(CC) -o $@ PROGNAME.o $(objects) $(libs_shared) $(LDFLAGS) $(LIBS)
' >> Makefile
# 6. Compilar
make PROGNAME
Orden de ejecución recomendado:
scan_and_fix_all_backrefs.c (la más importante)La herramienta más importante. Recorre recursivamente cada árbol en el sistema de archivos (ROOT, CHUNK, EXTENT, FS, DEV, CSUM, UUID, FREE_SPACE) y detecta bloques de metadatos que carecen de un METADATA_ITEM backref en el árbol de extent. Inyecta todos los backrefs faltantes en una sola transacción para evitar el problema de "el árbol raíz se mueve entre commits".
Uso:
sudo ./scan_and_fix_all_backrefs /dev/sdX # solo escaneo
sudo ./scan_and_fix_all_backrefs /dev/sdX --write # escaneo + inyección
fix_owner_refs.cCorrige el owner en el TREE_BLOCK_REF inline cuando no coincide con el
btrfs_header_owner() real del bloque. Las discrepancias ocurren cuando los bloques
se reasignan entre árboles durante reparaciones fallidas.
sudo ./fix_owner_refs /dev/sdX # escaneo
sudo ./fix_owner_refs /dev/sdX --write # corregir
fix_bad_levels.cCorrige entradas METADATA_ITEM y EXTENT_ITEM con un nivel incorrecto. Niveles
corruptos (p. ej. 50, 55, 237) son basura dejada por btrfs check --repair
al entrar en un bucle. Se verifica contra el btrfs_header_level() real del bloque.
sudo ./fix_bad_levels /dev/sdX # escaneo
sudo ./fix_bad_levels /dev/sdX --write # corregir
fix_duplicate_extents.cElimina METADATA_ITEM duplicados (mismo bytenr, diferentes niveles en la clave).
Conserva aquel cuyo nivel coincide con btrfs_header_level y elimina el otro.
sudo ./fix_duplicate_extents /dev/sdX # escaneo
sudo ./fix_duplicate_extents /dev/sdX --write # eliminar duplicados
remove_stale_ptrs.cEscanea cada nodo de nivel 1 del FS_TREE. Detecta punteros hijo obsoletos usando
tres comprobaciones: discrepancia de owner, discrepancia de first_key, o un first_key cuyo tipo
no es válido para el FS_TREE (p. ej. BLOCK_GROUP_ITEM). Los elimina con
btrfs_del_ptr.
sudo ./remove_stale_ptrs /dev/sdX # escaneo
sudo ./remove_stale_ptrs /dev/sdX --write # eliminar
fix_uuid_tree.c / fix_csum_tree.cCrea una hoja vacía para el árbol UUID / árbol CSUM respectivamente. Útil cuando el ROOT_ITEM apunta a un bloque que fue reasignado a otro árbol. El kernel regenera el árbol UUID automáticamente al montar RW. Con un árbol CSUM vacío, los archivos marcados como NODATASUM no fallan la verificación.
sudo ./fix_uuid_tree /dev/sdX
sudo ./fix_csum_tree /dev/sdX
set_nodatasum.cEstablece la marca BTRFS_INODE_NODATASUM en inodos de archivo regulares. Use esto si
el árbol de csum está vacío pero los archivos aún tienen checksums esperados, lo que causa
errores de lectura. Con NODATASUM, el kernel omite las búsquedas de csum.
sudo ./set_nodatasum /dev/sdX # escaneo
sudo ./set_nodatasum /dev/sdX --write # aplicar
fix_fstree_node.cVersión con una lista codificada de bloques obsoletos. Prefiera remove_stale_ptrs,
que los detecta automáticamente. Use esto solo si necesita control manual
sobre qué bloques específicos eliminar.
add_backrefs.cVersión inicial con una lista codificada de backrefs faltantes. Prefiera
scan_and_fix_all_backrefs, que los detecta automáticamente.
Cuando las herramientas base anteriores fueron insuficientes (pool con 200K+ errores distribuidos en múltiples árboles), se construyeron estas herramientas adicionales:
scan_fstree_extents.c + scan_extent_tree.cEscáneres de Pasada 1 y Pasada 2 que recorren el FS_TREE y el árbol de extent respectivamente,
produciendo archivos TSV con cada mapeo ref/extent. Se usan para construir entrada para
rebuild_extent_tree_apply cuando el árbol de extent necesita ser reconstruido desde cero.
rebuild_extent_tree_apply.c (escritor pesado)El escritor principal de la Fase 3. Toma una lista preprocesada de refs (del diff de scan_fstree_extents
scan_extent_tree) e inyecta 3M+ EXTENT_DATA_REF en el árbol de extent
en fragmentos de 5000 por transacción. Se regula cada 50K elementos para evitar
bloqueos por re-shingle en discos DM-SMR. Se verificó exitoso con 3,248,617 inserciones en ~34 min en
3 discos SMR WD40EFAX.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.cParche quirúrgico de un solo campo para BLOCK_GROUP_ITEM.used cuando el escritor de Fase 3
deja un bg específico con exceso debido a file_extent_items superpuestos preexistentes.
Utiliza el setter directo btrfs_set_block_group_used para evitar la contabilidad btrfs_update_block_group
space_info (que NO queremos aquí). Prevalida 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 codificada de EXTENT_ITEM (bytenr, num_bytes, expected_inode) del
árbol de extent. Se usa para limpiar extents obsoletos superpuestos en una sola hoja que
impiden el montaje RO. Comprobaciones de cordura por elemento antes de eliminar (7 invariantes
incluyendo lista blanca de inodos). Se ejecuta con rebuilding_extent_tree=1 + reinit_extent_tree=true
para omitir la contabilidad de espacio (quien llama parchea used manualmente primero mediante
patch_block_group_used).
clean_orphan_dir_entries.cLimpia entradas huérfanas DIR_ITEM + DIR_INDEX del FS_TREE. Fragmentos de 100
entradas por transacción. Actualiza i_size del INODE_ITEM padre (decremento en
namelen × 2: error crítico corregido: v1 decrementaba solo namelen,
dejando los directorios en estado inválido). Lista de exclusión codificada para nombres de directorio
críticos de nivel superior (p. ej. pelis, series, music, backups, homestorage).
NUNCA decremente i_size por namelen bruto: BTRFS almacena contabilidad de namelen × 2.
clean_orphan_inode_refs.cRecorre el FS_TREE en busca de elementos INODE_REF cuyo key.offset (inodo padre) está en una
lista de padres huérfanos. Omite INODE_EXTREF para evitar falsos positivos (el key.offset de
EXTREF es un hash, no un ID de padre). Fragmentos de 32 por transacción.
fix_dir_inode_counts.cRecalcula i_size = sum(name_len × 2) y nlink = 1 para inodos de directorio cuyas
cuentas fueron corrompidas por errores anteriores de limpieza de huérfanos. CRÍTICO para la seguridad:
si cualquier DIR tiene nlink = 2, un solo rm -rf en su ruta eliminará silenciosamente
miles de subdirectorios (bomba rmdir). Recorre las entradas DIR_INDEX, verifica
DIR_ITEM para detección de colisión hash (0 colisiones verificadas empíricamente).
remove_orphan_inode_subtrees.cElimina subárboles de inodos huérfanos (familias DIR + REG independientes) del FS_TREE. Para cada objetivo: recorre y elimina EXTENT_DATA, INODE_REF, INODE_EXTREF, XATTR, y finalmente INODE_ITEM. Transacción por familia DIR (atómica por subárbol), fragmentos de 50 para REG independientes. Lista de exclusión paranoica codificada.
⚠️ ADVERTENCIA DE SEGURIDAD IMPORTANTE: consulte "Criterio de subconjunto a prueba de balas" más abajo.
remove_stale_ptrs_v2.cVersión mejorada de remove_stale_ptrs: detecta hojas vacías con parent expected_key
(v1 omitía este caso), escaneo recursivo de 2 niveles (root→level1 + level1→hojas),
búfer dinámico (sin límite de 512), tolera fallos de read_tree_block.
insert_one_extent_poc.cPoC para inserción de un solo extent con validación. Se usó para validar la ruta API
antes de ejecutar rebuild_extent_tree_apply.
Durante la sesión del 2026-04-05, remove_orphan_inode_subtrees falló dos veces en la
misma aserción BUG_ON por dos razones diferentes:
Vector de fallo 1: btrfs_cow_block(leaf) directo sobre hoja MIXTA (gen 3601,
contiene tanto inodos huérfanos como vivos) → update_ref_for_cow recorre hijos →
__btrfs_mod_ref(inc=1) sobre hijos hermanos obsoletos → btrfs_free_extent(phantom)
devuelve -ENOENT → BUG_ON → SIGABRT.
Vector de fallo 2 (descubierto después, evadido filtrando):
btrfs_del_items posterior a la purga drena una hoja por debajo de LEAF_DATA_SIZE/4 = 4096 bytes →
invoca push_leaf_left(sibling) o push_leaf_right(sibling) → si el hermano tiene
gen ≤ last_snapshot = 3701, btrfs_block_can_be_shared devuelve 1 →
update_ref_for_cow entra en la ruta refs > 1 → btrfs_inc_ref(cow_sibling, 0) →
__btrfs_mod_ref(cow, level=0, inc=1) → itera todos los EXTENT_DATA del hermano obsoleto →
btrfs_inc_extent_ref(phantom_bytenr) → BUG_ON(err) en extent-tree.c:1302 → SIGABRT.
Las marcas fs_info->rebuilding_extent_tree = 1 y trans->reinit_extent_tree = true
NO salvan la ruta INC: solo eximen BTRFS_DROP_DELAYED_REF (verificado en
extent-tree.c:3885). BTRFS_ADD_DELAYED_REF (de btrfs_inc_ref) es fatal.
Criterio a prueba de balas para cualquier inodo objetivo que será eliminado:
INODE_ITEM del inodo tiene gen > 3701 (post-fallo)gen > 3701used estimados después de la purga > 4096 (sin desencadenante de rebalanceo)gen > 3701 (incluso si
la condición 3 falla, el rebalanceo hacia hermanos post-fallo es seguro)disk_bytenr) se resuelve en el árbol de extent
actual (sin -ENOENT en la búsqueda de backref)Violar cualquiera de las condiciones 3+4 desencadena el vector de fallo 2. La condición 5 queda
eximida por reinit_extent_tree para DROP pero NO para INC (que es lo que invoca
push_leaf_left).
Para cualquier conjunto candidato de inodos huérfanos, recorra el volcado del FS_TREE y clasifique cada hoja objetivo según las 5 condiciones a prueba de balas. Ejemplo de patrón (anonimizado):
| Hoja | Gen | Elementos huérfanos / total | Usado post-purga (est) | ¿Rebalanceo? | Hermanos inmediatos | Veredicto |
|---|---|---|---|---|---|---|
$LEAF_A | post-fallo | mayoría huérfanos, purga fuerte | por debajo del umbral | SÍ | todos post-fallo | ✓ seguro |
$LEAF_B | post-fallo | mayoría vivos, purga ligera | por encima del umbral | NO | padre limpio | ✓ seguro |
$LEAF_C | post-fallo | casi 100% huérfanos | muy por debajo de 4096 | SÍ forzado | obsoleto pre-fallo | ❌ FALLO |
Las hojas donde ≥90% de los elementos son huérfanos son la zona de peligro: drenarán
por debajo del umbral de rebalanceo (LEAF_DATA_SIZE/4 = 4096 bytes) con certeza,
forzando push_leaf_left/right. Si algún hermano inmediato en el nodo padre tiene
gen ≤ last_snapshot, el push desencadena CoW en ese hermano, que entra en la
ruta btrfs_block_can_be_shared → refs > 1 → btrfs_inc_ref → __btrfs_mod_ref(inc=1)
y falla con BUG_ON(err) en btrfs_inc_extent_ref.
Mitigación: excluya los inodos problemáticos del archivo de entrada. La herramienta procesa lo que pasa la validación previa; las hojas con objetivos mezclados seguros/no seguros pueden procesarse parcialmente listando solo el subconjunto seguro. La semántica de transacción por familia significa que cada familia segura se confirma atómicamente incluso si otras familias se excluyen.
Resultado empírico de una sesión: partiendo de N huérfanos candidatos,
tras aplicar las 5 condiciones, el subconjunto seguro final fue ~14% de la entrada,
pero ese subconjunto se confirmó sin un solo BUG_ON, con un diff de 0 bytes
sobre un sha256 de referencia de archivos vivos capturados antes de la escritura.
patches/alloc_reserved_tree_block_eexist.patch modifica btrfs-progs para que
cuando alloc_reserved_tree_block encuentra que el METADATA_ITEM ya existe,
devuelva 0 en lugar de propagar EEXIST. Esto es necesario para que funcione la inyección por lotes
de backrefs: al inyectar muchos backrefs, el sistema de refs diferidas también
intenta crear METADATA_ITEM para bloques recién asignados mediante COW y colisiona
con los que ya insertamos.
# 1. Copia de seguridad
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. Asegúrese de que el sistema de archivos esté desmontado
sudo umount /mnt/pool 2>/dev/null
# 3. Cero en el árbol de registro (si aplica)
sudo btrfs rescue zero-log /dev/sdX1
# 4. Escanear + corregir todo (en orden)
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-escanear para verificar convergencia
sudo ./scan_and_fix_all_backrefs /dev/sdX1
sudo ./remove_stale_ptrs /dev/sdX1
# 6. Si el árbol csum está roto:
sudo ./fix_csum_tree /dev/sdX1
sudo ./set_nodatasum /dev/sdX1 --write
# 7. Intentar montar RW
sudo mount -o rw /dev/sdX1 /mnt/pool
# 8. Si monta, verificar con btrfs check readonly
sudo btrfs check --force /dev/sdX1
Cada reparación puede crear nuevos problemas a través de COW: cuando una herramienta modifica el árbol de extent, btrfs hace CoW de los nodos afectados. Los nuevos nodos copian punteros de los antiguos, lo que puede propagar punteros obsoletos. Pueden ser necesarios múltiples pases.
Las discrepancias de ref en extent de datos no se corrigen: estas herramientas solo tocan
backrefs de metadatos. Los recuentos de ref incorrectos en extents de datos (comunes después de
ejecuciones fallidas de btrfs check --repair) no se limpian.
Los inodos huérfanos no se limpian: las entradas de directorio huérfanas en el FS_TREE (referencias a inodos que ya no existen) no se eliminan.
No reemplaza a btrfs check --repair: estas herramientas se dirigen a escenarios
específicos. Para daños leves o moderados, btrfs check --repair es mejor.
NUNCA apague repentinamente un sistema de archivos BTRFS multidivisa: la corrupción combinada del árbol de espacio libre + árbol de extent es extremadamente difícil de reparar.
NUNCA ejecute btrfs check --repair varias veces seguidas si la primera
ejecución no resolvió todo: puede entrar en un bucle infinito y empeorar
drásticamente el sistema de archivos.
Siempre haga una copia de seguridad de los superbloques antes de cada operación de escritura.
trans->reinit_extent_tree = true es clave para ignorar fallos de DROP
en refs diferidas para bloques sin backrefs.
fs_info->rebuilding_extent_tree = 1 deshabilita las comprobaciones de espacio durante
las reparaciones.
Un commit grande con muchas inserciones es mejor que muchos commits pequeños, porque los commits intermedios mueven el árbol raíz.
Los backup_slots en el SB NO son copias de seguridad históricas: son una ventana
deslizante de solo los últimos 4 commits. Un bucle de btrfs check --repair de
46,000+ commits rotará cada ranura ~11,000 veces en minutos, destruyendo
cualquier estado previo al fallo que se pueda recuperar del kernel. Para una retención real necesita
flujos explícitos btrfs subvolume snapshot o btrfs send a otro dispositivo.
reinit_extent_tree es ASIMÉTRICO: solo exime BTRFS_DROP_DELAYED_REF,
NO BTRFS_ADD_DELAYED_REF. Cualquier ruta de código que llame a btrfs_inc_ref en una
hoja obsoleta (incluyendo push_leaf_left/right durante el rebalanceo) aún
fallará mediante btrfs_inc_extent_ref → BUG_ON(err).
El criterio de seguridad para procesar inodos en un FS_TREE dañado debe incluir hermanos, no solo la hoja objetivo en sí. Consulte la sección "Criterio de subconjunto a prueba de balas".
El sha256 de referencia de los archivos VIVOS es la única prueba empírica de invariantes. Captúrelo antes de cualquier operación de escritura, compare después. Cualquier discrepancia = reversión.
El i_size de los DIR se almacena como sum(name_len × 2), NO sum(name_len).
Cualquier herramienta de limpieza de huérfanos que decremente i_size al eliminar una entrada debe
decrementar en namelen × 2. Si se hace incorrectamente, los DIR quedan en un estado
inválido que puede manifestarse como nlink = 2 más tarde: lo que desencadena una bomba
rmdir si el pool está montado RW (un solo rm -rf en un padre puede eliminar
miles de subdirectorios silenciosamente).
Los agentes revisores expertos con evidencia empírica son críticos. La sesión del 2026-04-05 utilizó dos revisores Opus paralelos (internos de btrfs + operaciones) que analizaron el plan propuesto contra la salida de dump-tree. Ellos detectaron un vector de fallo determinista (push_leaf_left → hermano obsoleto) que habría repetido los fallos anteriores. Una revisión textual del plan sin análisis empírico de dump-tree lo habría pasado por alto.
Estas herramientas fueron escritas para un caso de recuperación específico donde las herramientas nativas estaban fallando. No están probadas para casos de uso general. Úselas solo si entiende el código y acepta el riesgo de pérdida de datos.
Siempre copie sus datos antes de intentar cualquier reparación si es posible.
GPL-2.0 (compatible con btrfs-progs, cuya API interna usan estas herramientas).