GNU ddrescue affichait 100.00%. Ma première passe structurée de récupération de fichiers a produit 0 useful recovered user files.
Ces deux résultats provenaient de la même panne d’un disque dur de 4 TB, et cette contradiction est précisément ce qui a rendu cette récupération bien plus intéressante que « cloner le disque défaillant et copier les fichiers ». ddrescue avait remarquablement bien fait son travail : il avait copié environ 99.999654% de la source physique. Le clone se trouvait sur un matériel sain. Pourtant, APFS restait corrompu, macOS ne me donnait pas accès normalement au système de fichiers, et la première pile de récupération APFS pouvait énumérer de larges parties de l’arborescence tout en échouant à lire le contenu des fichiers ordinaires.
J’ai fini par récupérer toutes les arborescences de dossiers sélectionnées et énumérées dont j’avais besoin, mais seulement après avoir traité l’incident comme trois problèmes distincts : la récupération des blocs physiques, l’interprétation d’un système de fichiers endommagé et l’extraction de fichiers à grande échelle avec validation et reprise.
La panne a d’abord cessé d’être un problème de système de fichiers
Le Toshiba de 4 TB d’origine en était arrivé à un point où je ne lui faisais plus confiance pour une activité normale du système de fichiers. Certaines lectures prenaient 60–75 secondes. Des opérations pouvaient se bloquer. Le disque disparaissait par intermittence de macOS, émettait des clics audibles et s’éteignait parfois.
Peu avant la panne, j’avais écrit environ 300 GB de données supplémentaires et effectué un renommage massif touchant environ 500,000 fichiers et répertoires. Le timing rendait suspecte cette charge lourde en métadonnées, mais je ne peux pas prouver qu’elle a causé la panne matérielle. Elle a peut-être simplement suffisamment sollicité un disque déjà en mauvais état pour révéler le problème.
Ce que je pouvais établir, c’était le comportement du disque. Dès qu’un stockage mécanique se met à ralentir, disparaître et cliquer, parcourir sans cesse les répertoires n’est plus la bonne abstraction. La traversée des répertoires peut déclencher davantage de lectures et de déplacements des têtes. Monter un système de fichiers peut déclencher du travail sur les métadonnées. Chaque expérience consomme du temps sur le seul composant dont la durée de vie utile restante est inconnue.
J’ai donc changé l’objectif, qui n’était plus :
recover my files
mais :
recover as many readable sectors as possible
J’ai utilisé GNU ddrescue 1.30 avec un mapfile persistant. La taille physique exacte du disque source était :
4,000,787,027,968 bytes
Le mapfile était essentiel, car la source n’était pas suffisamment stable pour une copie en une seule passe. Il permettait à la récupération de survivre aux blocages, déconnexions, redémarrages et passes ultérieures sans oublier quelles régions avaient déjà été récupérées.
J’ai aussi rencontré un problème pratique avec le chemin du périphérique brut sous macOS : l’étendue apparente de la récupération pouvait devenir incohérente au lieu de s’arrêter à la véritable limite du périphérique. J’ai donc limité le domaine de récupération à la taille physique connue ci-dessus. Dans ce cas, borner explicitement l’entrée était une mesure de correction, et non une optimisation de vitesse.
Pourquoi les 100.00% affichés par ddrescue ne signifiaient pas que les fichiers étaient sauvés
Vers la fin de la récupération physique, ddrescue indiquait approximativement :
domain size: 4000 GB
rescued: 4000 GB
non-tried: 13481 kB
non-trimmed: 327680 B
non-scraped: 0 B
bad-sector: 27136 B
Le pourcentage mis en avant était :
100.00%
Mais les états non résolus totalisaient encore :
13,835,816 bytes
soit environ :
13.84 MB
Pour une source de 4,000,787,027,968 octets, la part récupérée était d’environ :
99.999654%
C’est un excellent résultat de récupération des blocs. Ce n’est pas un résultat d’intégrité des fichiers.
L’emplacement des octets manquants compte davantage que leur quantité globale. Plusieurs mégaoctets perdus dans de l’espace inutilisé peuvent ne rien affecter de visible. Une petite zone illisible à l’intérieur d’une vidéo peut endommager un fichier. Une perte bien plus faible dans les métadonnées du système de fichiers peut rendre difficiles à localiser de nombreuses étendues de données pourtant intactes.
C’est devenu le modèle mental central pour la suite de la récupération :
| Couche | Question à laquelle elle répond | Ce que la réussite ne prouve pas |
|---|---|---|
| Récupération des blocs | Les secteurs physiques ont-ils été copiés ? | Qu’APFS puisse reconstruire chaque fichier |
| Récupération du système de fichiers | Les chemins, métadonnées et étendues peuvent-ils être résolus ? | Que chaque octet extrait soit valide |
| Validation des fichiers | Un fichier a-t-il été obtenu avec la taille ou le hash attendu ? | Qu’aucun fichier désormais introuvable n’ait jamais existé |
J’ai fait quelques dernières tentatives sur les zones encore illisibles. Elles ont fini par ne plus produire de nouvelles lectures utiles alors que le Toshiba cliquait fortement. C’est à ce moment-là que j’ai cessé d’utiliser l’original comme source active de récupération.
Pourquoi j’ai acheté deux disques de 5 TB pour récupérer un disque de 4 TB
Le premier nouveau disque était un Seagate Expansion de 5 TB, avec une capacité physique exacte de :
5,000,981,077,504 bytes
J’y ai écrit le clone bloc par bloc du Toshiba. La disposition de la source occupait approximativement les 4 premiers TB, laissant environ 1 TB au-delà de la disposition copiée. J’ai délibérément laissé cette capacité supplémentaire intacte.
Je n’ai pas agrandi le conteneur APFS. Je n’ai pas repartitionné le clone par commodité. Je n’ai lancé aucune réparation du système de fichiers dessus. Ce disque est devenu le clone maître.
J’ai ensuite acheté un second disque de 5 TB. Il avait été fraîchement formaté, était accessible en écriture, testé indépendamment et utilisé uniquement pour les données récupérées.
failing 4 TB HDD
│
│ GNU ddrescue
▼
5 TB drive #1
master block-level clone
READ-ONLY
│
│ APFS parsing and extraction
▼
5 TB drive #2
recovered files
WRITABLE
Cela signifiait acheter environ 10 TB de nouveau stockage nominal pour récupérer un volume contenant environ 3.26 TB de données utilisées. Le disque supplémentaire n’était pas une question de capacité. Il servait à préserver un invariant :
If an experiment is wrong, I can return to the same untouched master clone.
Réparer, redimensionner, repartitionner le clone maître ou y écrire les données récupérées aurait mélangé préservation et expérimentation. Conserver la source et la destination sur deux disques physiques distincts rendait les erreurs récupérables.
Avant de faire confiance à la destination, j’ai effectué un test d’écriture/lecture d’environ 10 GB. Lors de ce test, elle a tenu environ 144.4 MB/s dans les deux sens. Des lectures brutes d’échantillons sur le clone maître se situaient autour de 28–49 MB/s et n’ont pas reproduit, pendant ces vérifications, le schéma de défaillance d’E/S physique du disque d’origine.
À ce stade, le problème avait changé. Je ne déboguais plus un matériel défaillant. Je déboguais des métadonnées APFS endommagées, préservées sur un matériel qui se comportait normalement pendant mes tests.
Le clone APFS était lisible comme périphérique, mais invalide comme système de fichiers
Le stockage physique APFS cloné faisait :
4,000,650,887,168 bytes
La partition concernée commençait au secteur :
264192
Avec des secteurs de 512 octets, cela correspond à un décalage en octets de :
135,266,304 bytes
Le volume APFS indiquait qu’environ :
3,260,976,717,824 bytes
étaient utilisés.
Une vérification APFS en lecture seule a fini par atteindre l’arbre fsroot et a indiqué :
Checking fsroot tree.
error: (oid 0xe36cb) apfs_root:
btn: dev_read_finish(3828538, 1): Input/output error
fsroot tree is invalid.
C’est à ce moment que « ddrescue a copié presque tout le disque » a cessé d’être un diagnostic complet utile. Le clone brut existait. La structure du système de fichiers qu’il contenait restait incohérente.
J’ai délibérément conservé les vérifications du système de fichiers en mode non modifiant. Je n’ai pas transformé fsck_apfs -n en opération de réparation sur l’unique clone maître de haute qualité dont je disposais. Une réparation peut être appropriée sur un stockage ordinaire, mais ici elle aurait modifié les éléments que j’essayais encore de comprendre.
The Sleuth Kit pouvait lister l’espace de noms APFS, mais échouait sur le contenu des fichiers
The Sleuth Kit 4.15.0 a été la première pile de récupération à rendre le clone prometteur. En utilisant l’intégralité du périphérique cloné, le décalage de partition connu et le superbloc APFS que j’avais identifié, je pouvais énumérer de vrais noms de répertoires avec fls :
fls \\
-o 264192 \\
-B 4594668 \\
-p \\
/dev/rdiskN
J’utilise N délibérément. Les numéros de disque macOS changeaient après les reconnexions et redémarrages, je ne considérais donc pas une ancienne attribution /dev/disk6 comme une identité.
fls pouvait parcourir des parties importantes de l’espace de noms. Une seule grande arborescence révélait environ 8,900 répertoires. Pendant un instant, cela donnait l’impression que la partie difficile était résolue.
J’ai alors essayé de récupérer le contenu des fichiers.
Un échec représentatif était :
libc++abi: terminating due to uncaught exception
of type std::runtime_error:
could not read APFSBlock
tsk_recover pouvait commencer l’extraction puis échouer sur un bloc APFS. Des tentatives individuelles avec icat montraient la même catégorie de problème.
La distinction importante était la suivante :
directory traversal works
n’impliquait pas que :
file content retrieval works
Un parseur peut disposer de suffisamment de métadonnées survivantes pour découvrir un chemin, tout en échouant plus tard lorsqu’il doit résoudre l’objet fichier, les métadonnées d’étendue ou les blocs de contenu nécessaires pour renvoyer le flux d’octets.
J’ai rendu le premier moteur de récupération en masse tolérant aux pannes, mais le parseur restait inadapté à ce type de dommage
Ma première réaction a été de rendre l’extraction TSK plus résiliente au lieu de changer immédiatement de parseur.
J’ai construit un wrapper de récupération Python autour de fls et icat. Il conservait un état durable dans SQLite, journalisait les échecs, prenait en charge la reprise, écrivait les sorties partielles séparément et reléguait au second plan les données de maintenance macOS de faible valeur lors de la première passe.
J’ai aussi ajouté une règle de « hotspot » : si quatre fichiers consécutifs dans un même répertoire échouaient avec le même schéma APFSBlock à zéro octet, le script cessait de perdre du temps sur le reste de cette branche, la reportait et poursuivait ailleurs. L’hypothèse de travail était qu’un groupe d’échecs identiques pouvait partager une même dépendance de métadonnées endommagée, plutôt que représenter des centaines de charges utiles détruites indépendamment.
L’orchestration était utile. Le lecteur APFS sous-jacent ne l’était pas.
À un moment capturé, la base de données de récupération contenait :
DEFERRED_HOTSPOT: 30,356 files
FAILED: 486 files
Les 486 échecs se répartissaient ainsi :
409 APFSBlock crashes
75 rc=0 but output-size mismatch
2 other rc=1 failures
Et la passe structurée avait produit :
0 useful recovered user files
Les 75 incohérences de taille ont révélé un bug dans mon propre wrapper : certains fichiers de métadonnées système avaient renvoyé des données, mais mon parseur avait enregistré une taille attendue de zéro. Corriger cette interprétation était important, mais cela ne changeait pas le résultat dominant. Les fichiers utilisateur ordinaires finissaient toujours à zéro octet avec could not read APFSBlock.
J’ai choisi un petit fichier AVIF comme cas de test reproductible. TSK connaissait sa taille attendue :
expected size: 56,309 bytes
recovered: 0 bytes
Ce fichier est devenu bien plus précieux qu’une autre passe en masse de plusieurs heures. Si une nouvelle approche ne pouvait pas récupérer un fichier de test de 56 KB qui échouait systématiquement, elle ne méritait pas d’accéder aux téraoctets restants.
Les blocs du disque et le chemin des métadonnées APFS relevaient de domaines de panne différents
À ce stade, j’avais trois observations :
GNU ddrescue:
almost the entire physical source was copied
TSK fls:
many real paths were discoverable
TSK icat:
many ordinary file contents still failed
Ces observations sont compatibles dès lors que l’on sépare conceptuellement la chaîne de recherche :
pathname
↓
directory record
↓
file object / inode metadata
↓
extent mapping
↓
physical data blocks
Les blocs de données situés vers le bas de la chaîne peuvent survivre alors qu’un maillon plus haut dans la chaîne des métadonnées est endommagé. Autre possibilité : deux implémentations d’APFS peuvent parcourir différemment les mêmes structures endommagées.
Je n’ai pas isolé un objet APFS corrompu unique qui expliquerait tous les échecs, je ne le présenterais donc pas comme une cause racine confirmée. En revanche, les éléments observés justifiaient une expérience bien plus utile : laisser les octets clonés inchangés et changer de parseur.
142 checkpoints APFS n’ont pas fourni de chemin de retour arrière instantané
Un balayage brut en lecture seule de la zone des descripteurs de checkpoints APFS a trouvé 142 superblocs de checkpoint NXSB candidats, avec des identifiants de transaction allant de :
223133
jusqu’à :
222992
La question évidente était de savoir si un checkpoint plus ancien référençait un arbre de métadonnées en meilleur état.
J’ai compilé apfs-fuse et essayé différents identifiants de transaction de checkpoint via fuse-t. Le checkpoint le plus récent se bloquait. Les XID plus anciens faisaient de même. J’ai ajouté des délais d’expiration stricts pour chaque checkpoint, et plus de 50 tentatives consécutives n’ont pas réussi à produire un montage utilisable. J’ai également essayé les backends NFS et SMB de fuse-t.
Cela ne prouvait pas que tous les checkpoints étaient corrompus. L’expérience dépendait du checkpoint, de apfs-fuse, de fuse-t, du comportement du périphérique sous macOS et du backend de montage. Une panne à n’importe quel niveau de cette pile pouvait produire le même résultat visible.
Un parseur ultérieur a signalé zéro snapshot APFS, ce qui a également renforcé une distinction que je devais garder claire : ces états transactionnels de checkpoint n’étaient pas la même chose que les snapshots APFS visibles par l’utilisateur.
Un parseur qui se bloque sur --help ne peut pas diagnostiquer mon disque
J’ai aussi compilé go-apfs-v2. La compilation a réussi, mais même :
apfs --help
se bloquait et devait être interrompu par un délai d’expiration.
Une inspection minimale des blocs sur les chemins de périphérique brut et mis en tampon se bloquait elle aussi. J’ai effectué le même type de test borné avec apfsutil ; les deux formes de périphérique expiraient après environ 15 secondes.
Ces échecs étaient utiles, car ils m’empêchaient de tirer la mauvaise conclusion. Un outil incapable d’achever de manière fiable son propre chemin d’aide constitue un indice faible quant à la récupérabilité d’un fichier endommagé.
Ma règle est devenue :
Valider l’outil de récupération avant de considérer l’échec de cet outil comme une preuve sur les données.
Empêcher macOS de monter automatiquement le clone a supprimé une autre source de risque
macOS lui-même constituait une autre variable. Je voulais que la destination saine soit montée normalement, mais je ne voulais pas que Disk Arbitration tente automatiquement de monter le clone APFS endommagé chaque fois que je reconnectais le stockage.
La séquence sûre que j’ai fini par utiliser était la suivante :
- Connecter et monter la destination saine de récupération.
- Vérifier l’identité de son volume et l’espace libre.
- Suspendre
diskarbitrationd. - Vérifier qu’aucun processus
mount_apfsn’est actif. - Connecter le clone maître.
- Identifier dynamiquement le clone maître à partir de sa taille physique connue et de son identité APFS.
- Vérifier que le clone maître n’est pas monté.
- Effectuer les opérations de récupération en lecture seule.
- Lors de l’arrêt, enregistrer l’état, réactiver Disk Arbitration, puis éteindre ou éjecter normalement.
La commande que j’ai réellement utilisée pour suspendre Disk Arbitration était :
sudo kill -STOP "$(pgrep -x diskarbitrationd)"
J’ai vérifié que l’état du processus contenait T, puis j’ai recherché d’éventuels processus mount_apfs résiduels avant de continuer.
La leçon de sécurité importante n’était pas le numéro précis du disque. C’était l’inverse : ne jamais faire confiance au numéro de disque d’hier. Après un redémarrage, un ancien /dev/disk6 peut désigner un autre périphérique physique. J’ai utilisé l’UUID APFS et la taille physique connue comme identité, puis j’en ai déduit le chemin de périphérique actuel.
libfsapfs a récupéré le même fichier que TSK renvoyait avec zéro octet
La percée est venue de libfsapfs, une autre implémentation d’APFS.
Je l’ai compilée depuis les sources sous macOS. Le binaire fsapfsinfo obtenu s’identifiait ainsi :
fsapfsinfo 20260923
Une différence importante liée à l’interface des périphériques macOS est alors apparue.
La partition du périphérique caractère brut :
/dev/rdiskNs2
échouait rapidement avec une lecture « invalid argument » près de l’offset 4096.
La forme du périphérique bloc mis en tampon :
/dev/diskNs2
fonctionnait.
Même clone physique. Même partition APFS. Interface de périphérique macOS différente.
fsapfsinfo a ouvert le conteneur et trouvé un volume. J’ai ensuite testé exactement le fichier de 56,309 octets que TSK n’avait pas réussi à extraire.
TSK avait produit :
expected: 56,309
recovered: 0
APFSBlock failure
Avec libfsapfs, l’entrée du fichier a produit :
size: 56,309
MD5: c6f56db33eafc1de0f52a035bc255dc7
RC: 0
C’était le premier résultat qui modifiait réellement le diagnostic. Les octets clonés n’avaient pas changé. Le fichier de test n’avait pas changé. L’APFS endommagé n’avait pas été réparé.
L’implémentation d’APFS avait changé.
Au minimum, je savais désormais qu’un vrai fichier utilisateur qui paraissait irrécupérable avec TSK restait accessible suffisamment loin via libfsapfs pour lire tout son contenu et calculer une empreinte.
J’ai extrait un fichier connu comme défaillant avant de confier des téraoctets à libfsapfs
Une empreinte calculée avec succès ne suffisait pas pour lancer une récupération de plusieurs téraoctets. Je voulais les octets réels sur le disque de destination.
Au lieu de deviner l’API C de libfsapfs, j’ai inspecté le code source de la bibliothèque et suivi le chemin de lecture déjà utilisé par fsapfsinfo pour calculer l’empreinte. J’ai ensuite construit un petit extracteur en lecture seule pour l’unique fichier de test connu.
L’extracteur a écrit exactement :
56,309 bytes
sur le disque de récupération séparé. Le MD5 du fichier extrait était :
c6f56db33eafc1de0f52a035bc255dc7
Il correspondait à l’empreinte précédente.
Ce n’est qu’alors que j’ai étendu l’approche. Le test sur un seul fichier avait prouvé trois choses distinctes : le chemin pouvait être résolu, le nombre complet d’octets attendu pouvait être extrait et les octets extraits produisaient la même empreinte que lors de la lecture complète précédente.
Le problème de la récupération en masse concernait surtout le confinement des pannes et la reprise
Une fois que libfsapfs a pu récupérer un fichier que TSK ne pouvait pas récupérer, le problème difficile a encore changé. Il me fallait un système capable de traiter une très grande arborescence sans qu’une branche endommagée, un redémarrage ou un Ctrl+C transforme le travail en reprise depuis zéro.
Le pipeline de récupération en masse reposait donc sur quelques invariants stricts :
- Le clone maître était ouvert en lecture seule.
- Les données récupérées étaient écrites uniquement sur le second disque de 5 TB.
- Les noms de répertoires et de fichiers étaient préservés.
- L’avancement était stocké dans SQLite afin de survivre aux arrêts de processus et aux redémarrages.
- Chaque fichier était d’abord écrit vers un chemin temporaire.
- Un fichier temporaire n’était renommé vers son chemin final qu’après l’écriture complète de la taille attendue.
- Les fichiers existants ayant la taille attendue pouvaient être reconnus lors de la reprise.
- Les travaux ayant échoué ou posant problème étaient séparés des travaux terminés.
- L’espace libre était vérifié et une réserve de sécurité était maintenue.
- Les artefacts de développement régénérables et les métadonnées système de faible valeur pouvaient être ignorés ou relégués au second plan.
Un changement de performances a eu un effet immédiat : j’ai cessé de rouvrir indépendamment le conteneur APFS pour chaque fichier.
Le chemin rapide traitait un répertoire à la fois. Un worker ouvrait la source, énumérait ce répertoire, récupérait ses fichiers réguliers immédiats et renvoyait les répertoires enfants dans la file. Si un répertoire échouait ou expirait, le contrôleur le marquait DEFERRED et continuait au lieu de bloquer la passe globale.
Une fois qu’il ne restait plus de travail normal en attente, les répertoires différés étaient revisités via un chemin de repli plus lent, avec un traitement par fichier davantage isolé. Les échecs restants pouvaient ensuite être retentés indépendamment.
discover directory
↓
recover immediate files
↓
verify expected sizes
↓
commit durable state
↓
queue child directories
↓
defer local failures
↓
continue globally
↓
fallback and retry later
Cette architecture correspondait bien mieux au schéma réel des pannes qu’une gigantesque commande récursive. Les dommages n’étaient pas uniformes ; la progression du système de récupération ne devait donc pas l’être non plus.
Pourquoi j’ai utilisé la vérification de la taille attendue et le renommage atomique au lieu de calculer le hash de millions de fichiers
MD5 était utile pour la preuve sur un seul fichier, car j’avais besoin d’un indice solide montrant que le parseur lisait le contenu complet d’un fichier que TSK ne pouvait pas lire.
Effectuer une seconde lecture complète de chaque octet récupéré uniquement pour calculer le hash de millions de fichiers aurait ajouté une grande quantité d’E/S. Pour la passe d’extraction principale, j’ai utilisé un autre invariant.
Pour chaque fichier régulier, les métadonnées APFS fournissaient une taille attendue. Le worker écrivait dans un fichier temporaire et ne le promouvait vers le chemin final qu’une fois la lecture complète conforme à cette taille attendue.
Ainsi, une interruption ne devait pas laisser un fichier tronqué se faisant passer pour le fichier final.
Une taille correspondante n’est pas une preuve cryptographique d’intégrité. Je ne la considère pas comme telle. Mais la vérification de la taille attendue combinée au renommage atomique constituait une frontière pratique de correction pour la passe à haut volume, tandis que des hash ciblés restaient utiles pour les échantillons et les cas d’échec connus.
SQLite a rendu un redémarrage banal plutôt que catastrophique
La récupération a duré assez longtemps pour que je doive arrêter l’ordinateur et reprendre plus tard. Cette exigence a transformé la conception d’un « script » en un « flux de travail récupérable ».
Un Ctrl+C n’abandonnait pas simplement le processus enfant actif. Le contrôleur interceptait l’interruption, arrêtait le worker, remettait le répertoire actif dans un état récupérable, validait SQLite, puis quittait.
Un arrêt propre ressemblait conceptuellement à ceci :
CURRENT DIRECTORY -> PENDING
CTRL+C: RECOVERY STOPPED SAFELY
STATE SAVED. RUN THE SAME COMMAND TO RESUME.
Après le redémarrage, j’ai répété les vérifications d’identité des disques et lancé la même commande de récupération. La base de données d’état a repris la file existante.
Un redémarrage ultérieur a commencé avec :
DEFERRED: 1
DONE: 73,015
PENDING: 7,254
C’était bien plus significatif qu’une barre de progression générique. Cela montrait que des dizaines de milliers d’unités de répertoires terminées avaient survécu au redémarrage et que la file restante était explicite.
Lors d’une reprise précédente, le répertoire interrompu pendant la session précédente a été repris. Les fichiers déjà présents avec la taille attendue ont été reconnus, et seul le travail manquant a dû être écrit. C’était le comportement que je voulais : redémarrer la récupération devait être routinier, pas inquiétant.
326,799 fichiers ont constitué la première preuve que la méthode passait à l’échelle
Avant d’étendre le nouvel extracteur à toutes les données sélectionnées, j’ai utilisé une grande arborescence prioritaire comme cible de validation en masse.
L’état terminé indiquait :
directories completed: 8,940
new files written: 302,541
existing/resumed files: 24,258
recorded failed files: 0
La destination contenait :
326,799 files
145,039,215,948 bytes
soit environ :
135.08 GiB
Les nombres de fichiers se réconciliaient exactement :
302,541 + 24,258 = 326,799
Il ne restait aucun fichier .partial.* dans cette arborescence terminée.
Le fichier de test de 56,309 octets avait prouvé que le parseur pouvait réussir là où TSK échouait. La récupération de 326,799 fichiers a prouvé que la même approche pouvait résister à une hiérarchie réelle importante, avec logique de reprise, détection des fichiers existants et aucun échec de fichier enregistré pendant cette passe terminée.
La récupération plus vaste a dépassé 1.6 million de nouveaux fichiers avant de s’achever
Une fois l’arborescence prioritaire terminée proprement, j’ai étendu la récupération aux autres données de premier niveau sélectionnées.
Lors d’un arrêt sûr et intentionnel, SQLite indiquait :
DONE directories: 39,015
PENDING directories: 17,017
DEFERRED directories: 1
new files written: 1,636,305
new bytes written: 902,715,716,335
Cela représentait environ :
840.72 GiB
de nouvelles données écrites enregistrées par les passes sur les répertoires à ce moment-là.
L’unique répertoire DEFERRED ne signifiait pas que des données étaient perdues. Cela signifiait que le chemin rapide avait délibérément cessé de laisser ce problème local retarder le travail sans rapport. La phase de repli existait précisément pour revenir sur ce type de cas plus tard.
Les sessions suivantes ont repris depuis la même base de données. Le nombre d’éléments terminés augmentait et la file d’attente diminuait. Au final, j’ai récupéré toutes les arborescences de dossiers sélectionnées et énumérées dont j’avais besoin.
Ce que je peux affirmer honnêtement sur la récupération finale
Je ne décrirai pas le résultat comme « chaque octet récupéré ». Les éléments disponibles ne permettent pas de l’affirmer.
Le mapfile ddrescue d’origine contenait encore environ 13.84 MB dont la copie réussie n’avait pas été confirmée. Je ne peux pas non plus prouver qu’aucun objet du système de fichiers n’est devenu totalement introuvable parce que les métadonnées nécessaires à son énumération se trouvaient dans les zones endommagées.
Ces limites comptent, car une récupération structurée peut prouver qu’un objet énuméré a été extrait ; elle ne peut pas prouver la non-existence historique d’un objet que l’espace de noms endommagé ne peut plus révéler.
L’affirmation finale la plus forte est plus étroite :
Toutes les arborescences de dossiers sélectionnées et énumérées dont j’avais besoin ont été récupérées avec succès par le processus de récupération structurée.
Je n’ai pas eu besoin de réparer le clone maître sur place. Je n’ai pas eu besoin de réutiliser le Toshiba d’origine qui cliquait pour l’extraction en masse. Je n’ai pas eu besoin d’une passe de carving de tout le disque à la manière de PhotoRec, qui aurait sacrifié la structure des répertoires et les noms de fichiers.
Les outils qui ont échoué ont malgré tout fourni des éléments utiles
Avec le recul, le chemin qui a réussi paraît simple :
ddrescue clone
↓
libfsapfs
↓
resumable extractor
↓
recovered files
Ce n’est pas ainsi que l’enquête se ressentait pendant qu’elle se déroulait, et supprimer les approches qui ont échoué supprimerait une grande partie de la leçon d’ingénierie utile.
TSK m’a appris que le parcours de l’espace de noms APFS et la récupération du contenu étaient deux modes de panne différents.
Le premier bug de mon wrapper m’a appris que la propre classification des erreurs d’un script de récupération ne constitue pas la vérité terrain.
Le mécanisme de hotspot m’a appris à isoler les pannes locales au lieu de leur permettre de bloquer la progression globale.
L’expérience sur les checkpoints m’a appris à ne pas confondre les checkpoints de transaction APFS avec les snapshots.
Les tentatives avec FUSE m’ont appris qu’un échec de montage peut impliquer plusieurs couches autres que les données du système de fichiers elles-mêmes.
Le lecteur APFS qui se bloquait sur --help m’a appris à valider l’outil avant d’interpréter ses diagnostics.
Le comportement différent entre /dev/rdiskNs2 et /dev/diskNs2 m’a appris que le chemin d’E/S du système d’exploitation peut modifier le comportement d’un parseur même lorsque le disque sous-jacent est identique.
Et l’architecture à deux disques m’a donné la liberté de faire des erreurs partout ailleurs tout en laissant le clone maître inchangé.
Le processus de récupération que je réutiliserais
- Arrêter l’activité normale du système de fichiers sur un stockage mécaniquement défaillant. Si les lectures se bloquent, que le périphérique disparaît ou qu’il clique, je privilégierais un clone bloc par bloc reprenable plutôt que l’exploration dans Finder.
- Utiliser GNU ddrescue avec un mapfile persistant et un domaine de récupération vérifié. Le mapfile préserve la progression ; une taille de source connue empêche la confusion sur la taille du périphérique de s’immiscer dans la récupération.
- Conserver un clone maître en lecture seule. Ne pas le réparer, le redimensionner, le repartitionner ni l’utiliser comme stockage des fichiers récupérés.
- Écrire les fichiers récupérés sur un second disque physique. La préservation de la source et le stockage des résultats sont deux tâches différentes.
- Diagnostiquer d’abord le système de fichiers cloné en lecture seule. Un clone physique sain contenant des métadonnées APFS corrompues est un problème de récupération logique, différent de celui d’un disque source qui clique.
- Choisir un fichier en échec reproductible comme test de parseur. Un fichier connu comme défaillant de 56 KB m’en a appris davantage sur les parseurs alternatifs que des heures d’extraction en masse à l’aveugle.
- Valider le parseur lui-même. Si un outil se bloque avant de lire utilement la source, ne pas interpréter cela comme une preuve que les données ont disparu.
- Ne pas supposer qu’une seule implémentation d’APFS définit ce qui est récupérable. TSK et
libfsapfsse sont comportés très différemment sur les mêmes octets clonés. - Identifier les disques par des propriétés stables, pas par des numéros de périphérique temporaires. Un UUID de système de fichiers et une taille physique connue sont plus sûrs que le
/dev/diskNd’hier. - Rendre les extractions de longue durée reprenables dès le départ. Un état durable, des fichiers temporaires, le renommage atomique, le travail différé, des nouvelles tentatives bornées et une gestion propre de l’arrêt font partie de la correction à cette échelle.
- Séparer les niveaux de validation. Un pourcentage ddrescue, un chemin visible, une taille attendue correspondante et une empreinte de contenu prouvent des choses différentes.
Le nombre qui ressemblait à la ligne d’arrivée ne marquait que la fin de la première étape
Le nombre le plus trompeur de toute la récupération restait :
100.00%
Il ressemblait à une réponse à la question « Ai-je sauvé le disque ? »
Ce à quoi il répondait réellement était bien plus limité :
How much of the physical rescue domain did ddrescue successfully copy?
Il ne disait pas si APFS pouvait reconstruire l’espace de noms. Il ne disait pas si un parseur pouvait parcourir des métadonnées endommagées qu’un autre parseur rejetait. Il ne disait pas si un fichier récupéré avait la longueur attendue. Et il ne disait pas si une extraction de plusieurs millions de fichiers pouvait survivre aux erreurs et aux redémarrages sans corrompre son propre état.
Il me fallait des éléments de preuve distincts pour chaque couche.
La récupération physique a réussi en premier. Le système de fichiers restait endommagé. Le premier parseur pouvait exposer les noms, mais échouait sur le contenu de nombreux fichiers. Une autre implémentation d’APFS a réussi à lire le même fichier de test. Cette preuve est devenue un extracteur pour un seul fichier, l’extracteur est devenu un moteur de récupération reprenable, et le moteur a fini par récupérer sur un second disque les arborescences de dossiers sélectionnées dont j’avais besoin, tandis que le clone maître restait intact.
Le disque dur d’origine n’est jamais redevenu sain. APFS ne s’est jamais réparé comme par magie. Ce qui a changé, c’est le modèle de récupération.
La récupération des blocs, la récupération du système de fichiers et la validation des fichiers sont des étapes d’ingénierie distinctes. Les traiter comme un seul problème rendait la situation presque désespérée. Les séparer l’a rendue gérable.