GNU ddrescue indicó 100.00%. Mi primera pasada estructurada de recuperación de archivos produjo 0 useful recovered user files.
Esos dos resultados procedían del mismo fallo de un disco duro de 4 TB, y esa contradicción es la razón por la que esta recuperación se volvió mucho más interesante que «clonar el disco defectuoso y copiar los archivos». ddrescue había hecho su trabajo extraordinariamente bien: había copiado aproximadamente el 99.999654% del origen físico. El clon estaba en hardware sano. Sin embargo, APFS seguía dañado, macOS no me proporcionaba el sistema de archivos con normalidad y la primera pila de recuperación de APFS podía enumerar grandes partes del árbol de directorios, pero no conseguía leer el contenido de archivos normales.
Al final recuperé todos los árboles de carpetas seleccionados y enumerados que necesitaba, pero solo después de tratar el incidente como tres problemas separados: recuperación física de bloques, interpretación de un sistema de archivos dañado y extracción de archivos a gran escala con validación y posibilidad de reanudar.
El fallo dejó de ser, ante todo, un problema del sistema de archivos
El Toshiba original de 4 TB había llegado a un punto en el que ya no confiaba en él para realizar actividad normal del sistema de archivos. Algunas lecturas tardaban 60–75 segundos. Las operaciones podían quedarse bloqueadas. El disco desaparecía de macOS de forma intermitente, emitía clics audibles y a veces se apagaba.
Poco antes del fallo, había escrito aproximadamente 300 GB de datos adicionales y realizado un cambio de nombre masivo que afectó a unos 500,000 archivos y directorios. Por el momento en que ocurrió, esa carga intensiva en metadatos resultaba sospechosa, pero no puedo demostrar que causara el fallo de hardware. Puede que simplemente sometiera a suficiente estrés a un disco que ya no estaba sano como para hacer visible el problema.
Lo que sí pude establecer fue el comportamiento del disco. Cuando un almacenamiento mecánico se queda bloqueado, desaparece y hace clics, recorrer directorios repetidamente es la abstracción equivocada. Recorrer un árbol de directorios puede provocar más lecturas y movimientos del cabezal. Montar un sistema de archivos puede desencadenar trabajo sobre metadatos. Cada experimento consume tiempo del único componente cuya vida útil restante se desconoce.
Así que cambié el objetivo de:
recuperar mis archivos
a:
recuperar tantos sectores legibles como sea posible
Usé GNU ddrescue 1.30 con un mapfile persistente. El tamaño físico exacto del disco de origen era:
4,000,787,027,968 bytes
El mapfile era esencial porque el origen no era lo bastante estable para una copia de una sola pasada. Permitía que la recuperación sobreviviera a bloqueos, desconexiones, reinicios y pasadas posteriores sin olvidar qué regiones ya se habían recuperado.
También encontré un problema práctico con la ruta del dispositivo raw de macOS: la extensión aparente de la recuperación podía volverse absurda en lugar de terminar en el límite real del dispositivo. Por eso limité el dominio de recuperación al tamaño físico conocido indicado arriba. En este caso, acotar explícitamente la entrada era una medida de corrección, no una optimización de velocidad.
Por qué que ddrescue mostrara 100.00% no significaba que los archivos estuvieran a salvo
Cerca del final de la recuperación física, ddrescue mostraba aproximadamente:
domain size: 4000 GB
rescued: 4000 GB
non-tried: 13481 kB
non-trimmed: 327680 B
non-scraped: 0 B
bad-sector: 27136 B
El porcentaje destacado era:
100.00%
Pero los estados sin resolver todavía sumaban:
13,835,816 bytes
o, aproximadamente:
13.84 MB
Frente a un origen de 4,000,787,027,968 bytes, la proporción recuperada era aproximadamente:
99.999654%
Es un resultado excelente de recuperación de bloques. No es un resultado de integridad de archivos.
La ubicación de los bytes perdidos importa más que la cantidad destacada. Perder varios megabytes de espacio no utilizado puede no afectar nada visible. Una pequeña región ilegible dentro de un video puede dañar un archivo. Una pérdida mucho menor en los metadatos del sistema de archivos puede dificultar la localización de muchos extents de datos que, por lo demás, siguen intactos.
Este se convirtió en el modelo mental central para el resto de la recuperación:
| Capa | Pregunta que responde | Lo que el éxito no demuestra |
|---|---|---|
| Recuperación de bloques | ¿Se copiaron los sectores físicos? | Que APFS pueda reconstruir todos los archivos |
| Recuperación del sistema de archivos | ¿Se pueden resolver rutas, metadatos y extents? | Que cada byte extraído sea válido |
| Validación de archivos | ¿Llegó un archivo con el tamaño o hash esperado? | Que nunca existiera ningún archivo que ya no pueda descubrirse |
Hice unos últimos intentos sobre las regiones que seguían sin poder leerse. Finalmente dejaron de producir nuevas lecturas útiles mientras el Toshiba hacía clics con mucha intensidad. Ahí dejé de usar el original como fuente activa de recuperación.
Por qué compré dos discos de 5 TB para recuperar uno de 4 TB
El primer disco nuevo fue un Seagate Expansion de 5 TB con una capacidad física exacta de:
5,000,981,077,504 bytes
Escribí en él el clon del Toshiba a nivel de bloques. La disposición del origen ocupaba aproximadamente los primeros 4 TB, dejando cerca de 1 TB más allá de la disposición copiada. Dejé deliberadamente esa capacidad adicional sin tocar.
No amplié el contenedor APFS. No reparticioné el clon por comodidad. No ejecuté una reparación del sistema de archivos sobre él. Ese disco se convirtió en el clon maestro.
Después compré un segundo disco de 5 TB. Estaba recién formateado, permitía escritura, había sido probado de forma independiente y se usó únicamente como destino de los datos recuperados.
HDD de 4 TB averiado
│
│ GNU ddrescue
▼
disco de 5 TB n.º 1
clon maestro a nivel de bloques
SOLO LECTURA
│
│ análisis y extracción de APFS
▼
disco de 5 TB n.º 2
archivos recuperados
ESCRITURA HABILITADA
Eso significó comprar aproximadamente 10 TB nominales de almacenamiento nuevo para recuperar un volumen que contenía unos 3.26 TB de datos usados. El disco adicional no era por capacidad. Era para preservar una invariante:
Si un experimento sale mal, puedo volver al mismo clon maestro intacto.
Reparar, redimensionar, reparticionar o escribir la salida recuperada en el maestro habría mezclado la preservación con la experimentación. Mantener el origen y el destino en discos físicos separados hacía que los errores fueran recuperables.
Antes de confiar en el destino, hice una prueba de escritura/lectura de aproximadamente 10 GB. En esa prueba mantuvo alrededor de 144.4 MB/s en ambas direcciones. Las lecturas raw de muestra del clon maestro estuvieron aproximadamente en 28–49 MB/s y, durante esas comprobaciones, no reprodujeron el patrón de fallos físicos de E/S del disco original.
En ese momento, el problema había cambiado. Ya no estaba depurando hardware que fallaba. Estaba depurando metadatos APFS dañados conservados en hardware que, en mis pruebas, se comportaba con normalidad.
El clon APFS se podía leer como dispositivo, pero era inválido como sistema de archivos
El almacenamiento físico APFS clonado tenía:
4,000,650,887,168 bytes
La partición relevante comenzaba en el sector:
264192
Con sectores de 512 bytes, eso corresponde a un desplazamiento en bytes de:
135,266,304 bytes
El volumen APFS indicaba aproximadamente:
3,260,976,717,824 bytes
consumidos.
Una comprobación de APFS en modo de solo lectura terminó llegando al árbol fsroot y mostró:
Checking fsroot tree.
error: (oid 0xe36cb) apfs_root:
btn: dev_read_finish(3828538, 1): Input/output error
fsroot tree is invalid.
Este fue el punto en el que «ddrescue copió casi todo el disco» dejó de ser útil como diagnóstico completo. El clon raw existía. La estructura del sistema de archivos dentro de él seguía siendo incoherente.
Mantuve deliberadamente la comprobación del sistema de archivos en modo no modificador. No convertí fsck_apfs -n en una operación de reparación contra el único clon maestro de alta calidad que tenía. Una reparación puede ser apropiada en almacenamiento normal, pero aquí habría modificado la evidencia que todavía estaba intentando comprender.
The Sleuth Kit podía listar el espacio de nombres APFS, pero fallaba con el contenido de los archivos
The Sleuth Kit 4.15.0 fue la primera pila de recuperación que hizo que el clon pareciera prometedor. Usando el dispositivo clonado completo, el desplazamiento conocido de la partición y el superbloque APFS que había identificado, podía enumerar nombres reales de directorios con fls:
fls \\
-o 264192 \\
-B 4594668 \\
-p \\
/dev/rdiskN
Uso N deliberadamente. Los números de disco de macOS cambiaban entre reconexiones y reinicios, así que no trataba una asignación anterior de /dev/disk6 como identidad.
fls podía recorrer partes considerables del espacio de nombres. Un solo árbol grande exponía aproximadamente 8,900 directorios. Por un momento, pareció que la parte difícil estaba resuelta.
Entonces intenté recuperar el contenido de los archivos.
Un fallo representativo era:
libc++abi: terminating due to uncaught exception
of type std::runtime_error:
could not read APFSBlock
tsk_recover podía empezar a extraer y después fallar en un bloque APFS. Los intentos individuales con icat mostraban la misma clase de problema.
La distinción importante era:
el recorrido de directorios funciona
no implicaba:
la recuperación del contenido de archivos funciona
Un parser puede conservar metadatos suficientes para descubrir una ruta y aun así fallar más adelante al resolver el objeto de archivo, los metadatos de extents o los bloques de contenido necesarios para devolver el flujo de bytes.
Hice tolerante a fallos el primer motor de recuperación masiva, pero el parser seguía sin ser adecuado para este daño
Mi primera reacción fue hacer más resistente la extracción con TSK en lugar de cambiar de parser inmediatamente.
Construí un wrapper de recuperación en Python alrededor de fls y icat. Mantenía estado duradero en SQLite, registraba fallos, permitía reanudar, escribía la salida parcial por separado y daba menor prioridad a datos auxiliares de macOS de poco valor durante la primera pasada.
También añadí una regla de «hotspot»: si cuatro archivos consecutivos de un directorio fallaban con el mismo patrón APFSBlock de cero bytes, el script dejaba de gastar tiempo en el resto de esa rama, la aplazaba y continuaba en otra parte. La hipótesis de trabajo era que un grupo de fallos idénticos podía compartir una dependencia de metadatos dañada, en lugar de representar cientos de contenidos destruidos de forma independiente.
La orquestación era útil. El lector APFS subyacente no.
En uno de los estados registrados, la base de datos de recuperación contenía:
DEFERRED_HOTSPOT: 30,356 files
FAILED: 486 files
Los 486 fallos se desglosaban así:
409 APFSBlock crashes
75 rc=0 but output-size mismatch
2 other rc=1 failures
Y la pasada estructurada había producido:
0 useful recovered user files
Las 75 discrepancias de tamaño dejaron al descubierto un error en mi propio wrapper: algunos archivos de metadatos del sistema habían devuelto datos, pero mi parser había registrado un tamaño esperado de cero. Corregir esa interpretación importaba, pero no cambió el resultado dominante. Los archivos normales de usuario seguían terminando con cero bytes y could not read APFSBlock.
Elegí un pequeño archivo AVIF como caso de prueba reproducible. TSK conocía su tamaño esperado:
expected size: 56,309 bytes
recovered: 0 bytes
Ese archivo pasó a ser mucho más valioso que otra pasada masiva de varias horas. Si un enfoque nuevo no podía recuperar un archivo de prueba de 56 KB que fallaba de forma consistente, no merecía acceso a los terabytes restantes.
Los bloques del disco y la ruta de metadatos APFS eran dominios de fallo distintos
En ese momento tenía tres observaciones:
GNU ddrescue:
se copió casi todo el origen físico
TSK fls:
se podían descubrir muchas rutas reales
TSK icat:
el contenido de muchos archivos normales seguía fallando
Esas observaciones son compatibles cuando la cadena de búsqueda se separa conceptualmente:
ruta
↓
registro de directorio
↓
objeto de archivo / metadatos de inode
↓
mapeo de extents
↓
bloques físicos de datos
Los bloques de datos cercanos a la parte inferior pueden sobrevivir mientras un enlace más arriba en la cadena de metadatos está dañado. Otra posibilidad es que dos implementaciones de APFS recorran de manera diferente las mismas estructuras dañadas.
No aislé un único objeto APFS corrupto que explicara todos los fallos, así que no afirmaría que esa fuera una causa raíz confirmada. Lo que sí respaldaba la evidencia era un experimento mucho más útil: mantener sin cambios los bytes clonados y cambiar el parser.
142 checkpoints de APFS no se convirtieron en una vía inmediata de reversión
Un escaneo raw y de solo lectura del área de descriptores de checkpoints de APFS encontró 142 superbloques NXSB candidatos, con identificadores de transacción desde:
223133
hasta:
222992
La pregunta evidente era si un checkpoint anterior hacía referencia a un árbol de metadatos más sano.
Compilé apfs-fuse y probé distintos identificadores de transacción de checkpoint mediante fuse-t. El checkpoint más reciente se bloqueó. Los XID anteriores hicieron lo mismo. Añadí límites de tiempo estrictos por checkpoint y más de 50 intentos consecutivos no lograron producir un montaje utilizable. También probé los backends NFS y SMB de fuse-t.
Eso no demostraba que todos los checkpoints estuvieran dañados. El experimento dependía del checkpoint, de apfs-fuse, de fuse-t, del comportamiento del dispositivo en macOS y del backend de montaje. Un fallo en cualquier punto de esa pila podía producir el mismo resultado visible.
Un parser posterior informó de cero snapshots de APFS, lo que también reforzó una distinción que tenía que mantener clara: esos estados de transacción de los checkpoints no eran lo mismo que los snapshots de APFS visibles para el usuario.
Un parser que se bloquea con --help no puede diagnosticar mi disco
También compilé go-apfs-v2. La compilación terminó, pero incluso:
apfs --help
se quedó bloqueado y tuve que terminarlo mediante un timeout.
La inspección mínima de bloques tanto con rutas de dispositivo raw como con rutas con búfer también se bloqueó. Hice el mismo tipo de prueba acotada con apfsutil; ambas formas del dispositivo agotaron el tiempo de espera después de unos 15 segundos.
Esos fallos fueron útiles porque evitaron que sacara la conclusión equivocada. Una herramienta que no puede completar de forma fiable ni siquiera su propia ruta de ayuda aporta poca evidencia sobre si un archivo dañado es recuperable.
Mi regla pasó a ser:
Validar la herramienta de recuperación antes de tratar el fallo de la propia herramienta como evidencia sobre los datos.
Evitar que macOS montara automáticamente el clon eliminó otra fuente de riesgo
macOS era otra pieza móvil. Quería que el destino sano se montara con normalidad, pero no quería que Disk Arbitration intentara montar automáticamente el clon APFS dañado cada vez que volvía a conectar el almacenamiento.
La secuencia segura que terminé usando fue:
- Conectar y montar el destino sano de recuperación.
- Verificar la identidad de su volumen y el espacio libre.
- Suspender
diskarbitrationd. - Verificar que no hubiera ningún proceso
mount_apfsactivo. - Conectar el clon maestro.
- Identificar dinámicamente el maestro mediante el tamaño físico conocido y la identidad APFS.
- Verificar que el maestro no estuviera montado.
- Realizar el trabajo de recuperación en modo de solo lectura.
- Al detenerme, guardar el estado, reanudar Disk Arbitration y después apagar o expulsar con normalidad.
El comando que usé realmente para suspender Disk Arbitration fue:
sudo kill -STOP "$(pgrep -x diskarbitrationd)"
Verifiqué que el estado del proceso contenía T y comprobé que no hubiera procesos mount_apfs sueltos antes de continuar.
La lección de seguridad importante no era el número concreto del disco. Era justo lo contrario: nunca confíes en el número de disco de ayer. Después de un reinicio, un /dev/disk6 anterior puede referirse a un dispositivo físico diferente. Traté el UUID de APFS y el tamaño físico conocido como identidad y, a partir de ellos, determiné la ruta actual del dispositivo.
libfsapfs recuperó el mismo archivo que TSK devolvía con cero bytes
El avance decisivo llegó con libfsapfs, una implementación diferente de APFS.
La compilé desde el código fuente en macOS. El binario fsapfsinfo resultante se identificaba como:
fsapfsinfo 20260923
Entonces apareció una diferencia importante entre interfaces de dispositivo de macOS.
La partición del dispositivo de caracteres raw:
/dev/rdiskNs2
fallaba rápidamente con una lectura de argumento no válido cerca del desplazamiento 4096.
La forma del dispositivo de bloques con búfer:
/dev/diskNs2
funcionaba.
El mismo clon físico. La misma partición APFS. Una interfaz de dispositivo de macOS diferente.
fsapfsinfo abrió el contenedor y encontró un volumen. Entonces probé exactamente el archivo de 56,309 bytes que TSK no había podido extraer.
TSK había producido:
expected: 56,309
recovered: 0
APFSBlock failure
Con libfsapfs, la entrada del archivo produjo:
size: 56,309
MD5: c6f56db33eafc1de0f52a035bc255dc7
RC: 0
Este fue el primer resultado que cambió materialmente el diagnóstico. Los bytes clonados no habían cambiado. El archivo de prueba no había cambiado. El APFS dañado no había sido reparado.
Había cambiado la implementación de APFS.
Como mínimo, ahora sabía que un archivo real de usuario que parecía irrecuperable con TSK seguía siendo accesible a suficiente profundidad mediante libfsapfs como para leer todo su contenido y calcular un hash.
Extraje un archivo conocido que fallaba antes de confiar terabytes a libfsapfs
Un hash correcto no era suficiente para lanzar una recuperación de varios terabytes. Quería los bytes reales en el disco de destino.
En lugar de adivinar la API C de libfsapfs, inspeccioné el código fuente de la biblioteca y seguí la ruta de lectura que ya usaba fsapfsinfo al calcular el hash. Después construí un pequeño extractor de solo lectura para ese único archivo de prueba conocido.
El extractor escribió exactamente:
56,309 bytes
en el disco de recuperación separado. El MD5 del archivo extraído era:
c6f56db33eafc1de0f52a035bc255dc7
Coincidía con el hash anterior.
Solo entonces amplié el enfoque. La prueba de un solo archivo había demostrado tres cosas distintas: se podía resolver la ruta, se podía extraer el número completo de bytes esperado y los bytes extraídos producían el mismo hash que la lectura completa anterior del contenido.
El problema de la recuperación masiva era sobre todo aislar fallos y poder reanudar
Una vez que libfsapfs pudo recuperar un archivo que TSK no podía, el problema difícil volvió a cambiar. Necesitaba un sistema capaz de procesar un árbol de directorios muy grande sin que una rama dañada, un reinicio o un Ctrl+C convirtieran el trabajo en un reinicio desde cero.
Por eso, el pipeline de recuperación masiva usaba varias invariantes estrictas:
- El clon maestro se abría en modo de solo lectura.
- La salida recuperada se escribía únicamente en el segundo disco de 5 TB.
- Se conservaban los nombres de directorios y archivos.
- El progreso se almacenaba en SQLite para sobrevivir a salidas del proceso y reinicios.
- Cada archivo se escribía primero en una ruta temporal.
- Un archivo temporal solo se renombraba a su ruta final después de haber escrito el tamaño esperado completo.
- Los archivos existentes con el tamaño esperado podían reconocerse al reanudar.
- El trabajo fallido o problemático se mantenía separado del trabajo completado.
- Se comprobaba el espacio libre y se mantenía una reserva de seguridad.
- Los artefactos de desarrollo regenerables y los metadatos de sistema de poco valor podían omitirse o recibir menor prioridad.
Un cambio de rendimiento importó de inmediato: dejé de volver a abrir de forma independiente el contenedor APFS para cada archivo.
La ruta rápida procesaba un directorio cada vez. Un worker abría el origen, enumeraba ese directorio, recuperaba sus archivos normales inmediatos y devolvía los directorios hijos a la cola. Si un directorio fallaba o agotaba el tiempo de espera, el controlador lo marcaba como DEFERRED y seguía adelante en lugar de bloquear la pasada global.
Cuando ya no quedaba trabajo pendiente normal, los directorios aplazados se revisaban mediante una ruta alternativa más lenta, con trabajo más aislado por archivo. Los fallos restantes podían volver a intentarse después de forma independiente.
descubrir directorio
↓
recuperar archivos inmediatos
↓
verificar tamaños esperados
↓
confirmar estado duradero
↓
poner directorios hijos en cola
↓
aplazar fallos locales
↓
continuar globalmente
↓
usar la ruta alternativa y reintentar más tarde
Esta arquitectura se ajustaba mucho mejor al patrón real de fallos que un único comando recursivo gigantesco. El daño no era uniforme, así que el sistema de recuperación tampoco debía imponer un progreso uniforme.
Por qué usé comprobaciones del tamaño esperado y renombrado atómico en lugar de calcular hashes de millones de archivos
MD5 fue útil durante la prueba de un solo archivo porque necesitaba evidencia sólida de que el parser estaba leyendo el contenido completo de un archivo que TSK no podía leer.
Hacer una segunda lectura completa de cada byte recuperado solo para calcular el hash de millones de archivos habría añadido una gran cantidad de E/S. Para la pasada principal de extracción usé una invariante diferente.
Para cada archivo normal, los metadatos APFS proporcionaban un tamaño esperado. El worker escribía en un archivo temporal y solo lo promovía a la ruta final después de que la lectura completa coincidiera con ese tamaño esperado.
Eso significa que una interrupción no debería dejar un archivo incompleto haciéndose pasar por uno final.
Una coincidencia de tamaño no es una prueba criptográfica de integridad. No la trato como tal. Pero la verificación del tamaño esperado junto con el renombrado atómico proporcionaba un límite práctico de corrección para la pasada de gran volumen, mientras que los hashes dirigidos seguían siendo útiles para muestras y casos de fallo conocidos.
SQLite hizo que un reinicio fuera rutinario en vez de catastrófico
La recuperación duró lo suficiente como para que necesitara detener el equipo y reanudar más tarde. Ese requisito cambió el diseño de «script» a «flujo de trabajo recuperable».
Un Ctrl+C no abandonaba simplemente el proceso hijo activo. El controlador capturaba la interrupción, detenía el worker, devolvía el directorio activo a un estado recuperable, confirmaba el estado en SQLite y salía.
Conceptualmente, una detención limpia se veía así:
DIRECTORIO ACTUAL -> PENDIENTE
CTRL+C: RECUPERACIÓN DETENIDA DE FORMA SEGURA
ESTADO GUARDADO. EJECUTA EL MISMO COMANDO PARA REANUDAR.
Después de reiniciar, repetí las comprobaciones de identidad de los discos e inicié el mismo comando de recuperación. La base de datos de estado reanudó la cola existente.
Un reinicio posterior comenzó con:
DEFERRED: 1
DONE: 73,015
PENDING: 7,254
Eso era mucho más significativo que una barra de progreso genérica. Mostraba que decenas de miles de unidades de directorio completadas habían sobrevivido al reinicio y que la cola restante estaba explícita.
En una reanudación anterior, el directorio interrumpido en la sesión previa volvió a procesarse. Se reconocieron los archivos que ya estaban presentes con el tamaño esperado y solo hubo que escribir el trabajo que faltaba. Ese era el comportamiento que quería: reiniciar la recuperación debía ser algo rutinario, no alarmante.
326,799 archivos fueron la primera prueba de que el método escalaba
Antes de ampliar el nuevo extractor a todos los datos seleccionados, usé un gran árbol prioritario como objetivo de validación masiva.
El estado completado mostraba:
directories completed: 8,940
new files written: 302,541
existing/resumed files: 24,258
recorded failed files: 0
El destino contenía:
326,799 files
145,039,215,948 bytes
o aproximadamente:
135.08 GiB
Los recuentos de archivos cuadraban exactamente:
302,541 + 24,258 = 326,799
No quedaban archivos .partial.* en ese árbol completado.
El archivo de prueba de 56,309 bytes había demostrado que el parser podía funcionar donde TSK fallaba. La recuperación de 326,799 archivos demostró que el mismo enfoque podía superar una jerarquía real considerable con lógica de reanudación, detección de archivos existentes y ningún fallo de archivo registrado en esa pasada completada.
La recuperación más grande superó 1.6 millones de archivos nuevos antes de completarse
Después de que el árbol prioritario terminara sin problemas, amplié la recuperación al resto de los datos de nivel superior seleccionados.
En una detención segura intencionada, SQLite mostraba:
DONE directories: 39,015
PENDING directories: 17,017
DEFERRED directories: 1
new files written: 1,636,305
new bytes written: 902,715,716,335
Eso equivalía aproximadamente a:
840.72 GiB
de datos recién escritos registrados por las pasadas de directorios en ese momento.
El único directorio DEFERRED no significaba datos perdidos. Significaba que la ruta rápida había dejado deliberadamente de permitir que ese problema local retrasara trabajo no relacionado. La fase alternativa existía específicamente para volver a esos casos más adelante.
Las sesiones posteriores se reanudaron desde la misma base de datos. El recuento de elementos completados aumentó y la cola pendiente disminuyó. Al final, recuperé todos los árboles de carpetas seleccionados y enumerados que necesitaba.
Lo que puedo afirmar con rigor sobre la recuperación final
No voy a describir el resultado como «se recuperó cada byte». La evidencia no respalda esa afirmación.
El mapfile original de ddrescue todavía contenía aproximadamente 13.84 MB que no se habían confirmado como copiados correctamente. Tampoco puedo demostrar que ningún objeto del sistema de archivos quedara completamente indetectable porque los metadatos necesarios para enumerarlo estuvieran entre las regiones dañadas.
Esas limitaciones importan porque una recuperación estructurada puede demostrar que un objeto enumerado fue extraído; no puede demostrar la inexistencia histórica de un objeto que el espacio de nombres dañado ya no puede revelar.
La afirmación final más sólida es más limitada:
Todos los árboles de carpetas seleccionados y enumerados que necesitaba se recuperaron correctamente mediante el proceso de recuperación estructurada.
No necesité reparar el clon maestro in situ. No necesité volver a usar el Toshiba original que hacía clics para la extracción masiva. No necesité una pasada de carving de todo el disco al estilo de PhotoRec que sacrificara la estructura de directorios y los nombres de archivo.
Las herramientas que fallaron siguieron aportando evidencia útil
En retrospectiva, la ruta que funcionó parece sencilla:
clon con ddrescue
↓
libfsapfs
↓
extractor reanudable
↓
archivos recuperados
Así no se sintió la investigación mientras ocurría, y eliminar los enfoques fallidos eliminaría buena parte de la lección de ingeniería útil.
TSK me enseñó que recorrer el espacio de nombres APFS y recuperar contenido eran modos de fallo distintos.
El primer error de mi wrapper me enseñó que la propia clasificación de errores de un script de recuperación no es la realidad objetiva.
El mecanismo de hotspots me enseñó a aislar los fallos locales en lugar de permitir que detuvieran el progreso global.
El experimento con checkpoints me enseñó a no confundir los checkpoints de transacción de APFS con snapshots.
Los intentos con FUSE me enseñaron que un montaje fallido puede implicar varias capas además de los propios datos del sistema de archivos.
El lector de APFS que se bloqueaba con --help me enseñó a validar la herramienta antes de interpretar sus diagnósticos.
El comportamiento de /dev/rdiskNs2 frente a /dev/diskNs2 me enseñó que la ruta de E/S del sistema operativo puede cambiar el comportamiento de un parser aunque el disco subyacente sea idéntico.
Y la arquitectura de dos discos me dio libertad para equivocarme en todo lo demás mientras el clon maestro permanecía sin cambios.
El flujo de recuperación que volvería a usar
- Detener la actividad normal del sistema de archivos en almacenamiento con fallos mecánicos. Si las lecturas se bloquean, el dispositivo desaparece o hace clics, priorizaría un clon reanudable a nivel de bloques frente a explorar con Finder.
- Usar GNU ddrescue con un mapfile persistente y un dominio de recuperación verificado. El mapfile conserva el progreso; conocer el tamaño del origen evita que la confusión sobre el tamaño del dispositivo pase a formar parte de la recuperación.
- Mantener un clon maestro en modo de solo lectura. No repararlo, redimensionarlo, reparticionarlo ni usarlo como almacenamiento para archivos recuperados.
- Escribir los archivos recuperados en un segundo disco físico. Preservar el origen y almacenar la salida son trabajos distintos.
- Diagnosticar primero el sistema de archivos clonado en modo de solo lectura. Un clon físico sano que contiene metadatos APFS dañados plantea un problema de recuperación lógica, distinto del problema de un disco de origen que hace clics.
- Elegir un archivo que falle de forma reproducible como prueba del parser. Un archivo conocido de 56 KB que fallaba me dijo más sobre parsers alternativos que horas de extracción masiva a ciegas.
- Validar el propio parser. Si una herramienta se bloquea antes de leer el origen de forma significativa, no interpretar eso como prueba de que los datos han desaparecido.
- No asumir que una implementación de APFS define lo que es recuperable. TSK y
libfsapfsse comportaron de forma muy diferente sobre los mismos bytes clonados. - Identificar los discos por propiedades estables, no por números temporales de dispositivo. Un UUID del sistema de archivos y un tamaño físico conocido son más seguros que el
/dev/diskNde ayer. - Hacer reanudable desde el principio cualquier extracción de larga duración. Estado duradero, archivos temporales, renombrado atómico, trabajo aplazado, reintentos acotados y manejo de apagados limpios forman parte de la corrección a esta escala.
- Separar los niveles de validación. Un porcentaje de ddrescue, una ruta visible, una coincidencia de tamaño esperado y un hash del contenido demuestran cosas diferentes.
El número que parecía la meta solo era el final de la primera etapa
El número más engañoso de toda la recuperación seguía siendo:
100.00%
Parecía responder a «¿Salvé el disco?».
Lo que realmente respondía era mucho más limitado:
¿Qué proporción del dominio físico de recuperación copió correctamente ddrescue?
No respondía si APFS podía reconstruir el espacio de nombres. No respondía si un parser podía recorrer metadatos dañados que otro parser rechazaba. No respondía si un archivo recuperado tenía la longitud esperada. Y no respondía si una extracción de varios millones de archivos podía sobrevivir a errores y reinicios sin corromper su propio estado.
Necesitaba evidencia independiente para cada capa.
La recuperación física tuvo éxito primero. El sistema de archivos seguía dañado. El primer parser podía mostrar nombres, pero fallaba con el contenido de muchos archivos. Una implementación diferente de APFS leyó correctamente el mismo archivo de prueba. Esa prueba se convirtió en un extractor de un solo archivo, el extractor se convirtió en un motor de recuperación reanudable y el motor acabó recuperando en un segundo disco los árboles de carpetas seleccionados que necesitaba, mientras el clon maestro permanecía intacto.
El disco duro original nunca volvió a estar sano. APFS nunca se reparó mágicamente. Lo que cambió fue el modelo de recuperación.
La recuperación de bloques, la recuperación del sistema de archivos y la validación de archivos son etapas de ingeniería separadas. Tratarlas como un único problema hacía que la situación pareciera casi desesperada. Separarlas hizo que fuera abordable.