GNU ddrescue meldete 100.00%. Mein erster strukturierter Durchlauf zur Dateiwiederherstellung lieferte 0 useful recovered user files.
Diese beiden Ergebnisse stammten vom selben Ausfall einer 4-TB-Festplatte, und genau dieser Widerspruch machte die Wiederherstellung deutlich interessanter als „die defekte Platte klonen und die Dateien kopieren“. ddrescue hatte seine Aufgabe bemerkenswert gut erledigt: Es hatte ungefähr 99.999654% der physischen Quelle kopiert. Der Klon lag auf intakter Hardware. Trotzdem war APFS weiterhin beschädigt, macOS konnte mir das Dateisystem nicht normal bereitstellen, und der erste APFS-Recovery-Stack konnte große Teile des Verzeichnisbaums auflisten, scheiterte aber beim Lesen des Inhalts gewöhnlicher Dateien.
Am Ende konnte ich jeden ausgewählten, aufgelisteten Verzeichnisbaum wiederherstellen, den ich brauchte — allerdings erst, nachdem ich den Vorfall als drei getrennte Probleme behandelt hatte: physische Blockwiederherstellung, Interpretation des beschädigten Dateisystems und großflächige Dateiextraktion mit Validierung und Fortsetzungsfunktion.
Zunächst ging es nicht mehr um das Dateisystem
Die ursprüngliche 4-TB-Toshiba war an einem Punkt angekommen, an dem ich ihr normale Dateisystemaktivität nicht mehr zutraute. Manche Lesevorgänge dauerten 60–75 Sekunden. Vorgänge konnten hängen bleiben. Das Laufwerk verschwand zeitweise aus macOS, klickte hörbar und schaltete sich manchmal ab.
Kurz vor dem Ausfall hatte ich ungefähr 300 GB zusätzliche Daten geschrieben und eine Massenumbenennung durchgeführt, die rund 500.000 Dateien und Verzeichnisse betraf. Wegen des zeitlichen Zusammenhangs wirkte diese metadatenintensive Last verdächtig, aber ich kann nicht beweisen, dass sie den Hardwareausfall verursacht hat. Möglicherweise hat sie ein bereits angeschlagenes Laufwerk nur stark genug belastet, um das Problem sichtbar zu machen.
Feststellen konnte ich dagegen das Verhalten des Laufwerks. Sobald mechanischer Speicher ins Stocken gerät, verschwindet und klickt, ist wiederholtes Durchsuchen von Verzeichnissen die falsche Abstraktion. Das Traversieren von Verzeichnissen kann weitere Lese- und Suchbewegungen auslösen. Das Einhängen eines Dateisystems kann Metadatenarbeit anstoßen. Jeder Versuch verbraucht Zeit auf der einzigen Komponente, deren verbleibende Nutzungsdauer unbekannt ist.
Also änderte ich das Ziel von:
meine Dateien wiederherstellen
zu:
so viele lesbare Sektoren wie möglich wiederherstellen
Ich verwendete GNU ddrescue 1.30 mit einer persistenten Mapfile. Die exakte physische Größe des Quelllaufwerks betrug:
4,000,787,027,968 bytes
Die Mapfile war unverzichtbar, weil die Quelle nicht stabil genug für eine Kopie in einem einzigen Durchlauf war. Sie ermöglichte, dass die Rettung Hänger, Verbindungsabbrüche, Neustarts und spätere Durchläufe überstand, ohne zu vergessen, welche Bereiche bereits wiederhergestellt worden waren.
Außerdem stieß ich auf ein praktisches Problem mit dem Raw-Device-Pfad von macOS: Der scheinbare Rettungsbereich konnte unsinnig werden, statt an der tatsächlichen Gerätegrenze zu enden. Deshalb begrenzte ich den Rettungsbereich auf die oben bekannte physische Größe. In diesem Fall war die explizite Begrenzung der Eingabe eine Maßnahme zur Korrektheit, keine Geschwindigkeitsoptimierung.
Warum 100.00% bei ddrescue nicht bedeutete, dass die Dateien sicher waren
Gegen Ende der physischen Rettung meldete ddrescue ungefähr:
domain size: 4000 GB
rescued: 4000 GB
non-tried: 13481 kB
non-trimmed: 327680 B
non-scraped: 0 B
bad-sector: 27136 B
Der auffällige Prozentwert war:
100.00%
Die noch ungeklärten Zustände summierten sich jedoch auf:
13,835,816 bytes
oder ungefähr:
13.84 MB
Bezogen auf eine Quelle mit 4,000,787,027,968 Bytes lag der wiederhergestellte Anteil ungefähr bei:
99.999654%
Das ist ein hervorragendes Ergebnis der Blockwiederherstellung. Es ist kein Ergebnis zur Dateiintegrität.
Wo die fehlenden Bytes liegen, ist wichtiger als ihre Gesamtmenge. Mehrere verlorene Megabytes im ungenutzten Bereich können ohne sichtbare Folgen bleiben. Ein kleiner unlesbarer Bereich innerhalb eines Videos kann eine Datei beschädigen. Ein noch viel kleinerer Verlust in Dateisystem-Metadaten kann dazu führen, dass viele ansonsten intakte Daten-Extents schwer zu finden sind.
Das wurde zum zentralen Denkmodell für den Rest der Wiederherstellung:
| Ebene | Welche Frage sie beantwortet | Was ein Erfolg nicht beweist |
|---|---|---|
| Blockwiederherstellung | Wurden die physischen Sektoren kopiert? | Dass APFS jede Datei rekonstruieren kann |
| Dateisystemwiederherstellung | Lassen sich Pfade, Metadaten und Extents auflösen? | Dass jedes extrahierte Byte gültig ist |
| Dateivalidierung | Hat eine Datei die erwartete Größe oder den erwarteten Hash? | Dass niemals eine nicht mehr auffindbare Datei existiert hat |
Ich unternahm noch einige letzte Versuche mit den verbleibenden unlesbaren Bereichen. Schließlich lieferten sie keine nützlichen neuen Leseergebnisse mehr, während die Toshiba stark klickte. An diesem Punkt hörte ich auf, das Originallaufwerk als aktive Wiederherstellungsquelle zu verwenden.
Warum ich für die Wiederherstellung einer 4-TB-Platte zwei 5-TB-Laufwerke kaufte
Das erste neue Laufwerk war eine 5-TB-Seagate Expansion mit einer exakten physischen Kapazität von:
5,000,981,077,504 bytes
Darauf schrieb ich den Toshiba-Klon auf Blockebene. Das Layout der Quelle belegte ungefähr die ersten 4 TB, sodass hinter dem kopierten Layout etwa 1 TB übrig blieb. Diese zusätzliche Kapazität ließ ich bewusst unberührt.
Ich vergrößerte den APFS-Container nicht. Ich partitionierte den Klon nicht aus Bequemlichkeit neu. Ich führte darauf keine Dateisystemreparatur aus. Dieses Laufwerk wurde zum Master-Klon.
Dann kaufte ich ein zweites 5-TB-Laufwerk. Es wurde frisch formatiert, war beschreibbar, unabhängig getestet und wurde ausschließlich für die wiederhergestellten Daten verwendet.
ausfallende 4-TB-HDD
│
│ GNU ddrescue
▼
5-TB-Laufwerk #1
Master-Klon auf Blockebene
NUR LESEN
│
│ APFS-Parsing und Extraktion
▼
5-TB-Laufwerk #2
wiederhergestellte Dateien
BESCHREIBBAR
Damit kaufte ich nominell rund 10 TB neuen Speicher, um ein Volume mit etwa 3,26 TB belegten Daten wiederherzustellen. Beim zusätzlichen Laufwerk ging es nicht um Kapazität. Es ging darum, eine Invariante zu bewahren:
Wenn ein Experiment falsch ist, kann ich zum selben unveränderten Master-Klon zurückkehren.
Reparieren, Vergrößern, Neupartitionieren oder das Schreiben wiederhergestellter Daten auf den Master hätte Erhaltung und Experiment miteinander vermischt. Quelle und Ziel auf getrennten physischen Laufwerken zu halten, machte Fehler rückgängig machbar.
Bevor ich dem Ziel vertraute, führte ich einen Schreib-/Lesetest mit ungefähr 10 GB durch. Dabei erreichte es in beide Richtungen ungefähr 144,4 MB/s. Stichprobenartige Raw-Lesevorgänge vom Master-Klon lagen ungefähr bei 28–49 MB/s und reproduzierten bei diesen Prüfungen nicht das physische I/O-Fehlermuster des Originallaufwerks.
An diesem Punkt hatte sich das Problem verändert. Ich debugte keine ausfallende Hardware mehr. Ich debugte beschädigte APFS-Metadaten, die auf Hardware lagen, die sich in meinen Tests normal verhielt.
Der APFS-Klon war als Gerät lesbar, als Dateisystem aber ungültig
Der geklonte physische APFS-Speicher hatte eine Größe von:
4,000,650,887,168 bytes
Die relevante Partition begann bei Sektor:
264192
Bei 512-Byte-Sektoren entspricht das einem Byte-Offset von:
135,266,304 bytes
Das APFS-Volume meldete einen ungefähren Verbrauch von:
3,260,976,717,824 bytes
belegt.
Eine schreibgeschützte APFS-Prüfung erreichte schließlich den fsroot-Baum und meldete:
Checking fsroot tree.
error: (oid 0xe36cb) apfs_root:
btn: dev_read_finish(3828538, 1): Input/output error
fsroot tree is invalid.
Hier hörte „ddrescue hat fast die gesamte Platte kopiert“ auf, als vollständige Diagnose nützlich zu sein. Der Rohklon existierte. Die Dateisystemstruktur darin war weiterhin inkonsistent.
Ich hielt die Dateisystemprüfung bewusst nicht verändernd. Ich machte aus fsck_apfs -n keine Reparaturaktion gegen den einzigen hochwertigen Master-Klon, den ich hatte. Eine Reparatur kann bei normalem Speicher sinnvoll sein, hier hätte sie jedoch die Beweislage verändert, die ich noch verstehen wollte.
The Sleuth Kit konnte den APFS-Namensraum auflisten, scheiterte aber an Dateiinhalten
The Sleuth Kit 4.15.0 war der erste Recovery-Stack, mit dem der Klon vielversprechend aussah. Mit dem gesamten geklonten Gerät, dem bekannten Partitions-Offset und dem von mir identifizierten APFS-Superblock konnte ich mit fls echte Verzeichnisnamen auflisten:
fls \
-o 264192 \
-B 4594668 \
-p \
/dev/rdiskN
Ich verwende N absichtlich. Die macOS-Disknummern änderten sich nach Neuverbindungen und Neustarts, daher betrachtete ich eine frühere Zuweisung wie /dev/disk6 nicht als Identität.
fls konnte beträchtliche Teile des Namensraums traversieren. Allein ein großer Baum legte ungefähr 8.900 Verzeichnisse offen. Für einen Moment sah es so aus, als sei der schwierige Teil gelöst.
Dann versuchte ich, Dateiinhalte abzurufen.
Ein repräsentativer Fehler war:
libc++abi: terminating due to uncaught exception
of type std::runtime_error:
could not read APFSBlock
tsk_recover konnte mit der Extraktion beginnen und dann an einem APFS-Block scheitern. Einzelne icat-Versuche zeigten dieselbe Art von Problem.
Der wichtige Unterschied war:
Verzeichnistraversierung funktioniert
bedeutete nicht:
Abruf von Dateiinhalten funktioniert
Ein Parser kann über genug erhaltene Metadaten verfügen, um einen Pfadnamen zu finden, und später dennoch beim Auflösen des Dateiobjekts, der Extent-Metadaten oder der Inhaltsblöcke scheitern, die für die Rückgabe des Bytestroms nötig sind.
Ich machte die erste Bulk-Recovery-Engine fehlertolerant, aber der Parser war für diesen Schaden weiterhin ungeeignet
Meine erste Reaktion bestand darin, die TSK-Extraktion robuster zu machen, statt sofort den Parser zu wechseln.
Ich baute einen Python-Recovery-Wrapper um fls und icat. Er hielt dauerhaften Zustand in SQLite, protokollierte Fehler, unterstützte das Fortsetzen, schrieb Teilergebnisse separat und setzte niedrigwertige macOS-Systemdaten im ersten Durchlauf in der Priorität herunter.
Außerdem fügte ich eine „Hotspot“-Regel hinzu: Wenn vier aufeinanderfolgende Dateien in einem Verzeichnis mit demselben Zero-Byte-APFSBlock-Muster scheiterten, verbrachte das Skript keine weitere Zeit mit dem Rest dieses Zweigs, stellte ihn zurück und machte an anderer Stelle weiter. Die Arbeitshypothese war, dass eine Gruppe identischer Fehler eine gemeinsame beschädigte Metadaten-Abhängigkeit haben könnte, statt Hunderte unabhängig zerstörte Nutzdaten zu repräsentieren.
Die Orchestrierung war nützlich. Der zugrunde liegende APFS-Reader war es nicht.
Zu einem aufgezeichneten Zeitpunkt enthielt die Recovery-Datenbank:
DEFERRED_HOTSPOT: 30,356 files
FAILED: 486 files
Die 486 Fehler verteilten sich auf:
409 APFSBlock crashes
75 rc=0 but output-size mismatch
2 other rc=1 failures
Und der strukturierte Durchlauf hatte Folgendes produziert:
0 useful recovered user files
Die 75 Größenabweichungen deckten einen Fehler in meinem eigenen Wrapper auf: Einige System-Metadatendateien hatten Daten geliefert, aber mein Parser hatte eine erwartete Größe von null aufgezeichnet. Diese Interpretation zu korrigieren war wichtig, änderte aber nichts am dominierenden Ergebnis. Gewöhnliche Benutzerdateien endeten weiterhin mit null Bytes und could not read APFSBlock.
Ich wählte eine kleine AVIF-Datei als reproduzierbaren Testfall. TSK kannte ihre erwartete Größe:
expected size: 56,309 bytes
recovered: 0 bytes
Diese Datei wurde deutlich wertvoller als ein weiterer mehrstündiger Bulk-Durchlauf. Wenn ein neuer Ansatz eine einzelne 56-KB-Testdatei, die reproduzierbar scheiterte, nicht wiederherstellen konnte, verdiente er keinen Zugriff auf die verbleibenden Terabytes.
Die Datenblöcke und der APFS-Metadatenpfad waren unterschiedliche Fehlerdomänen
Zu diesem Zeitpunkt hatte ich drei Beobachtungen:
GNU ddrescue:
fast die gesamte physische Quelle wurde kopiert
TSK fls:
viele echte Pfade waren auffindbar
TSK icat:
viele gewöhnliche Dateiinhalte scheiterten weiterhin
Diese Beobachtungen sind miteinander vereinbar, sobald man die Lookup-Kette konzeptionell trennt:
Pfadname
↓
Verzeichniseintrag
↓
Dateiobjekt / Inode-Metadaten
↓
Extent-Zuordnung
↓
physische Datenblöcke
Datenblöcke weiter unten können erhalten bleiben, während ein Glied weiter oben in der Metadatenkette beschädigt ist. Eine andere Möglichkeit ist, dass zwei APFS-Implementierungen dieselben beschädigten Strukturen unterschiedlich traversieren.
Ich konnte kein einzelnes beschädigtes APFS-Objekt isolieren, das alle Fehler erklärte, daher würde ich das nicht als bestätigte Ursache bezeichnen. Was die Belege dagegen stützten, war ein viel nützlicheres Experiment: die geklonten Bytes unverändert lassen und den Parser wechseln.
142 APFS-Checkpoints wurden nicht zum sofortigen Rollback-Pfad
Ein roher, schreibgeschützter Scan des APFS-Checkpoint-Descriptor-Bereichs fand 142 mögliche NXSB-Checkpoint-Superblocks mit Transaktions-IDs von:
223133
bis hinunter zu:
222992
Eine naheliegende Frage war, ob ein älterer Checkpoint auf einen gesünderen Metadatenbaum verwies.
Ich baute apfs-fuse und probierte über fuse-t verschiedene Checkpoint-Transaktions-IDs aus. Der neueste Checkpoint blieb hängen. Ältere XIDs verhielten sich genauso. Ich fügte harte Timeouts pro Checkpoint hinzu, und mehr als 50 Versuche in Folge lieferten keinen nutzbaren Mount. Außerdem probierte ich sowohl NFS- als auch SMB-Backends von fuse-t aus.
Das bewies nicht, dass alle Checkpoints beschädigt waren. Das Experiment hing vom Checkpoint, apfs-fuse, fuse-t, dem macOS-Geräteverhalten und dem Mount-Backend ab. Ein Fehler an irgendeiner Stelle dieses Stacks konnte zum gleichen sichtbaren Ergebnis führen.
Ein späterer Parser meldete null APFS-Snapshots, was außerdem einen Unterschied unterstrich, den ich sauber auseinanderhalten musste: Diese Checkpoint-Transaktionszustände waren nicht dasselbe wie benutzersichtbare APFS-Snapshots.
Ein Parser, der bei --help hängt, kann meine Platte nicht diagnostizieren
Ich baute auch go-apfs-v2. Der Build wurde abgeschlossen, aber selbst:
apfs --help
hing und musste durch einen Timeout beendet werden.
Auch eine minimale Blockprüfung über sowohl Raw- als auch gepufferte Gerätepfade blieb hängen. Mit apfsutil führte ich denselben begrenzten Test durch; beide Geräteformen liefen nach ungefähr 15 Sekunden in einen Timeout.
Diese Fehlschläge waren nützlich, weil sie mich daran hinderten, den falschen Schluss zu ziehen. Ein Werkzeug, das nicht einmal seinen eigenen Hilfe-Pfad zuverlässig abschließen kann, ist nur schwache Evidenz dafür, ob eine beschädigte Datei wiederherstellbar ist.
Meine Regel wurde:
Validiere das Wiederherstellungswerkzeug, bevor du dessen Fehler als Beleg über die Daten interpretierst.
Das Verhindern des automatischen Mountens des Klons durch macOS beseitigte eine weitere Risikoquelle
macOS selbst war eine weitere bewegliche Komponente. Das intakte Ziel wollte ich normal eingehängt haben, aber Disk Arbitration sollte nicht automatisch versuchen, den beschädigten APFS-Klon einzuhängen, sobald ich den Speicher erneut anschloss.
Die sichere Abfolge, bei der ich schließlich blieb, war:
- Das intakte Wiederherstellungsziel anschließen und einhängen.
- Volume-Identität und freien Speicher prüfen.
diskarbitrationdanhalten.- Prüfen, dass kein aktiver
mount_apfs-Prozess läuft. - Den Master-Klon anschließen.
- Den Master dynamisch anhand der bekannten physischen Größe und APFS-Identität bestimmen.
- Prüfen, dass der Master nicht eingehängt ist.
- Wiederherstellungsarbeiten ausschließlich lesend durchführen.
- Beim Beenden den Zustand speichern, Disk Arbitration fortsetzen und anschließend normal herunterfahren oder auswerfen.
Der Befehl, den ich tatsächlich zum Anhalten von Disk Arbitration verwendete, war:
sudo kill -STOP \"$(pgrep -x diskarbitrationd)\"
Ich prüfte, dass der Prozesszustand T enthielt, und kontrollierte vor dem Fortfahren, ob noch mount_apfs-Prozesse liefen.
Die wichtige Sicherheitslehre war nicht die konkrete Disknummer. Im Gegenteil: Vertraue niemals der Disknummer von gestern. Nach einem Neustart kann ein früheres /dev/disk6 auf ein anderes physisches Gerät verweisen. Ich verwendete die APFS-UUID und die bekannte physische Größe als Identität und leitete daraus den aktuellen Gerätepfad ab.
libfsapfs stellte dieselbe Datei wieder her, die TSK mit null Bytes zurückgab
Der Durchbruch kam mit libfsapfs, einer anderen APFS-Implementierung.
Ich baute sie unter macOS aus dem Quellcode. Das daraus entstandene fsapfsinfo-Binary identifizierte sich als:
fsapfsinfo 20260923
Dann zeigte sich ein wichtiger Unterschied zwischen den macOS-Geräteschnittstellen.
Die Raw-Character-Device-Partition:
/dev/rdiskNs2
scheiterte schnell mit einem Invalid-Argument-Lesefehler nahe Offset 4096.
Die gepufferte Block-Device-Form:
/dev/diskNs2
funktionierte.
Gleicher physischer Klon. Gleiche APFS-Partition. Andere macOS-Geräteschnittstelle.
fsapfsinfo öffnete den Container und fand ein Volume. Anschließend testete ich genau die 56.309-Byte-Datei, die TSK nicht hatte extrahieren können.
TSK hatte Folgendes produziert:
expected: 56,309
recovered: 0
APFSBlock failure
Über libfsapfs lieferte der Dateieintrag:
size: 56,309
MD5: c6f56db33eafc1de0f52a035bc255dc7
RC: 0
Das war das erste Ergebnis, das die Diagnose substanziell veränderte. Die geklonten Bytes hatten sich nicht verändert. Die Testdatei hatte sich nicht verändert. Das beschädigte APFS war nicht repariert worden.
Die APFS-Implementierung hatte sich geändert.
Mindestens wusste ich nun, dass eine echte Benutzerdatei, die über TSK nicht wiederherstellbar erschien, über libfsapfs noch tief genug erreichbar war, um ihren vollständigen Inhalt zu lesen und einen Digest zu berechnen.
Ich extrahierte eine bekannte Problemdatei, bevor ich libfsapfs Terabytes anvertraute
Ein erfolgreicher Digest reichte mir nicht, um eine Wiederherstellung über mehrere Terabytes zu starten. Ich wollte die tatsächlichen Bytes auf dem Ziellaufwerk.
Statt die C-API von libfsapfs zu erraten, untersuchte ich den Quellcode der Bibliothek und folgte dem Leseweg, den fsapfsinfo bereits bei der Berechnung des Digests verwendete. Danach baute ich einen kleinen schreibgeschützten Extraktor für die eine bekannte Testdatei.
Der Extraktor schrieb exakt:
56,309 bytes
auf das separate Wiederherstellungslaufwerk. Der MD5-Wert der extrahierten Datei war:
c6f56db33eafc1de0f52a035bc255dc7
Das stimmte mit dem vorherigen Digest überein.
Erst dann skalierte ich den Ansatz. Der Ein-Datei-Test hatte drei getrennte Dinge bewiesen: Der Pfadname ließ sich auflösen, die vollständige erwartete Byteanzahl ließ sich extrahieren, und die extrahierten Bytes ergaben denselben Digest wie der frühere vollständige Inhaltslesevorgang.
Bei der Bulk-Wiederherstellung ging es vor allem um Fehlerisolierung und Fortsetzen
Sobald libfsapfs eine Datei wiederherstellen konnte, an der TSK scheiterte, änderte sich das schwierige Problem erneut. Ich brauchte ein System, das einen sehr großen Verzeichnisbaum verarbeiten konnte, ohne dass ein beschädigter Zweig, ein Neustart oder ein Ctrl+C den gesamten Job wieder bei null beginnen ließ.
Die Bulk-Recovery-Pipeline verwendete deshalb einige strikte Invarianten:
- Der Master-Klon wurde schreibgeschützt geöffnet.
- Wiederhergestellte Ausgaben wurden ausschließlich auf das zweite 5-TB-Laufwerk geschrieben.
- Verzeichnis- und Dateinamen blieben erhalten.
- Der Fortschritt lag in SQLite, sodass er Prozessabbrüche und Neustarts überstand.
- Jede Datei wurde zuerst auf einen temporären Pfad geschrieben.
- Eine temporäre Datei wurde erst dann auf ihren endgültigen Pfad umbenannt, nachdem die vollständige erwartete Größe geschrieben worden war.
- Vorhandene Dateien mit der erwarteten Größe konnten beim Fortsetzen erkannt werden.
- Fehlgeschlagene oder problematische Arbeit blieb von abgeschlossener Arbeit getrennt.
- Der freie Speicher wurde geprüft und eine Sicherheitsreserve beibehalten.
- Neu erzeugbare Entwicklungsartefakte und niedrigwertige Systemmetadaten konnten übersprungen oder niedriger priorisiert werden.
Eine Performance-Änderung machte sich sofort bemerkbar: Ich hörte auf, den APFS-Container für jede Datei separat neu zu öffnen.
Der schnelle Pfad verarbeitete jeweils ein Verzeichnis. Ein Worker öffnete die Quelle, listete dieses Verzeichnis auf, stellte die unmittelbar darin liegenden regulären Dateien wieder her und gab Unterverzeichnisse an die Warteschlange zurück. Wenn ein Verzeichnis scheiterte oder in einen Timeout lief, markierte der Controller es als DEFERRED und machte weiter, statt den globalen Durchlauf zu blockieren.
Nachdem keine normalen ausstehenden Arbeiten mehr vorhanden waren, wurden zurückgestellte Verzeichnisse über einen langsameren Fallback-Pfad mit stärker isolierter Arbeit pro Datei erneut besucht. Verbleibende Fehler konnten danach unabhängig wiederholt werden.
Verzeichnis erkennen
↓
direkte Dateien wiederherstellen
↓
erwartete Größen prüfen
↓
dauerhaften Zustand festschreiben
↓
Unterverzeichnisse einreihen
↓
lokale Fehler zurückstellen
↓
global fortfahren
↓
später Fallback und Wiederholung
Diese Architektur passte deutlich besser zum tatsächlichen Fehlermuster als ein einziger großer rekursiver Befehl. Die Schäden waren nicht gleichmäßig verteilt, also sollte das Wiederherstellungssystem auch den Fortschritt nicht gleichmäßig erzwingen.
Warum ich erwartete Größen und atomisches Umbenennen statt Hashing von Millionen Dateien verwendete
MD5 war beim Ein-Datei-Nachweis nützlich, weil ich starke Evidenz dafür brauchte, dass der Parser den vollständigen Inhalt einer Datei las, die TSK nicht lesen konnte.
Jedes wiederhergestellte Byte ein zweites Mal vollständig zu lesen, nur um Millionen Dateien zu hashen, hätte sehr viel zusätzliche I/O erzeugt. Für den Hauptdurchlauf der Extraktion verwendete ich daher eine andere Invariante.
Für jede reguläre Datei lieferten die APFS-Metadaten eine erwartete Größe. Der Worker schrieb in eine temporäre Datei und beförderte sie erst dann auf den endgültigen Pfad, wenn der vollständige Lesevorgang dieser erwarteten Größe entsprach.
Das bedeutet, dass eine Unterbrechung keine zu kurze Datei unter dem endgültigen Namen zurücklassen sollte.
Eine übereinstimmende Größe ist kein kryptografischer Integritätsnachweis. Ich behandle sie auch nicht so. Aber die Prüfung der erwarteten Größe zusammen mit atomischem Umbenennen war eine praktische Korrektheitsgrenze für den Durchlauf mit hohem Volumen, während gezielte Hashes für Stichproben und bekannte Fehlerfälle weiterhin nützlich blieben.
SQLite machte einen Neustart langweilig statt katastrophal
Die Wiederherstellung lief lange genug, dass ich den Computer ausschalten und später fortsetzen musste. Diese Anforderung veränderte das Design von „Skript“ zu „wiederaufnehmbarer Arbeitsablauf“.
Ein Ctrl+C ließ den aktiven Kindprozess nicht einfach zurück. Der Controller fing die Unterbrechung ab, stoppte den Worker, setzte das aktive Verzeichnis in einen wiederaufnehmbaren Zustand zurück, führte den SQLite-Commit aus und beendete sich.
Ein sauberer Stopp sah konzeptionell so aus:
AKTUELLES VERZEICHNIS -> PENDING
CTRL+C: WIEDERHERSTELLUNG SICHER GESTOPPT
ZUSTAND GESPEICHERT. ZUM FORTSETZEN DENSELBEN BEFEHL AUSFÜHREN.
Nach dem Neustart wiederholte ich die Prüfungen der Laufwerksidentität und startete denselben Wiederherstellungsbefehl. Die Zustandsdatenbank setzte die vorhandene Warteschlange fort.
Ein späterer Neustart begann mit:
DEFERRED: 1
DONE: 73,015
PENDING: 7,254
Das war deutlich aussagekräftiger als ein generischer Fortschrittsbalken. Es zeigte, dass Zehntausende abgeschlossene Verzeichniseinheiten den Neustart überstanden hatten und dass die verbleibende Warteschlange explizit bekannt war.
Bei einem früheren Fortsetzen wurde das in der vorigen Sitzung unterbrochene Verzeichnis erneut aufgenommen. Dateien, die bereits in der erwarteten Größe vorhanden waren, wurden erkannt, und nur die fehlende Arbeit musste geschrieben werden. Genau dieses Verhalten wollte ich: Eine Wiederherstellung neu zu starten sollte Routine sein, nicht beängstigend.
326,799 Dateien waren der erste Beleg dafür, dass die Methode skalierte
Bevor ich den neuen Extraktor auf alle ausgewählten Daten ausweitete, verwendete ich einen großen Prioritätsbaum als Ziel für die Bulk-Validierung.
Der abgeschlossene Zustand meldete:
directories completed: 8,940
new files written: 302,541
existing/resumed files: 24,258
recorded failed files: 0
Das Ziel enthielt:
326,799 files
145,039,215,948 bytes
oder ungefähr:
135.08 GiB
Die Dateianzahlen gingen exakt auf:
302,541 + 24,258 = 326,799
In diesem abgeschlossenen Baum blieben keine .partial.*-Dateien zurück.
Die 56.309-Byte-Testdatei hatte bewiesen, dass der Parser dort erfolgreich sein konnte, wo TSK scheiterte. Die Wiederherstellung von 326.799 Dateien bewies, dass derselbe Ansatz eine umfangreiche reale Hierarchie mit Fortsetzungslogik, Erkennung vorhandener Dateien und ohne aufgezeichnete Dateifehler in diesem abgeschlossenen Durchlauf bewältigen konnte.
Die größere Wiederherstellung überschritt vor Abschluss 1,6 Millionen neue Dateien
Nachdem der Prioritätsbaum sauber abgeschlossen war, weitete ich die Wiederherstellung auf die übrigen ausgewählten Daten der obersten Ebene aus.
Bei einem bewusst sicheren Stopp meldete SQLite:
DONE directories: 39,015
PENDING directories: 17,017
DEFERRED directories: 1
new files written: 1,636,305
new bytes written: 902,715,716,335
Das waren ungefähr:
840.72 GiB
an neu geschriebenen Daten, die von den Verzeichnisdurchläufen zu diesem Zeitpunkt erfasst waren.
Das eine DEFERRED-Verzeichnis bedeutete nicht verlorene Daten. Es bedeutete, dass der schnelle Pfad absichtlich aufgehört hatte, dieses lokale Problem nicht zusammenhängende Arbeit verzögern zu lassen. Die Fallback-Phase existierte ausdrücklich, um solche Fälle später erneut aufzugreifen.
Spätere Sitzungen setzten mit derselben Datenbank fort. Die Zahl abgeschlossener Einheiten stieg und die ausstehende Warteschlange schrumpfte. Am Ende stellte ich jeden ausgewählten, aufgelisteten Verzeichnisbaum wieder her, den ich brauchte.
Was ich über die endgültige Wiederherstellung ehrlich behaupten kann
Ich werde das Ergebnis nicht als „jedes Byte wiederhergestellt“ beschreiben. Die Belege geben das nicht her.
Die ursprüngliche ddrescue-Map enthielt weiterhin ungefähr 13,84 MB, deren erfolgreiches Kopieren nicht bestätigt war. Außerdem kann ich nicht beweisen, dass kein Dateisystemobjekt vollständig unauffindbar wurde, weil die für seine Auflistung nötigen Metadaten in den beschädigten Bereichen lagen.
Diese Einschränkungen sind wichtig, weil strukturierte Wiederherstellung beweisen kann, dass ein aufgelistetes Objekt extrahiert wurde; sie kann nicht die historische Nichtexistenz eines Objekts beweisen, das der beschädigte Namensraum nicht mehr sichtbar machen kann.
Die stärkste abschließende Aussage ist enger:
Jeder ausgewählte, aufgelistete Verzeichnisbaum, den ich brauchte, wurde durch den strukturierten Wiederherstellungsprozess erfolgreich wiederhergestellt.
Ich musste den Master-Klon nicht direkt reparieren. Ich musste die ursprünglich klickende Toshiba nicht erneut für die Bulk-Extraktion verwenden. Ich brauchte keinen PhotoRec-artigen Carving-Durchlauf über die gesamte Platte, der Verzeichnisstruktur und Dateinamen geopfert hätte.
Auch die gescheiterten Werkzeuge waren nützliche Evidenz
Im Rückblick klingt der erfolgreiche Pfad einfach:
ddrescue-Klon
↓
libfsapfs
↓
fortsetzbarer Extraktor
↓
wiederhergestellte Dateien
So fühlte sich die Untersuchung währenddessen nicht an, und die gescheiterten Ansätze wegzulassen würde einen großen Teil der nützlichen Engineering-Lektion entfernen.
TSK lehrte mich, dass das Traversieren des APFS-Namensraums und das Abrufen von Inhalten unterschiedliche Fehlermodi waren.
Der Fehler in meinem ersten Wrapper lehrte mich, dass die eigene Fehlerklassifizierung eines Wiederherstellungsskripts nicht die Grundwahrheit ist.
Der Hotspot-Mechanismus lehrte mich, lokale Fehler zu isolieren, statt zuzulassen, dass sie den globalen Fortschritt anhalten.
Das Checkpoint-Experiment lehrte mich, APFS-Transaktions-Checkpoints nicht mit Snapshots zu verwechseln.
Die FUSE-Versuche lehrten mich, dass ein fehlgeschlagener Mount mehrere Ebenen außer den Dateisystemdaten selbst betreffen kann.
Der APFS-Reader, der bei --help hing, lehrte mich, das Werkzeug zu validieren, bevor ich seine Diagnose interpretiere.
Das Verhalten von /dev/rdiskNs2 gegenüber /dev/diskNs2 lehrte mich, dass der I/O-Pfad des Betriebssystems das Verhalten eines Parsers verändern kann, selbst wenn das zugrunde liegende Laufwerk identisch ist.
Und die Zwei-Laufwerk-Architektur gab mir die Freiheit, überall sonst Fehler zu machen, während der Master-Klon unverändert blieb.
Der Wiederherstellungsablauf, den ich wieder verwenden würde
- Normale Dateisystemaktivität auf mechanisch ausfallendem Speicher stoppen. Wenn Lesevorgänge hängen, das Gerät verschwindet oder klickt, würde ich einen fortsetzbaren Klon auf Blockebene dem Erkunden im Finder vorziehen.
- GNU ddrescue mit einer persistenten Mapfile und einem verifizierten Rettungsbereich verwenden. Die Mapfile bewahrt den Fortschritt; eine bekannte Quellgröße verhindert, dass Verwirrung über die Gerätegröße Teil der Wiederherstellung wird.
- Einen Master-Klon schreibgeschützt halten. Nicht reparieren, vergrößern, neu partitionieren oder als Speicher für wiederhergestellte Dateien verwenden.
- Wiederhergestellte Dateien auf ein zweites physisches Laufwerk schreiben. Quellerhaltung und Ausgabespeicherung sind unterschiedliche Aufgaben.
- Das geklonte Dateisystem zuerst schreibgeschützt diagnostizieren. Ein physisch intakter Klon mit beschädigten APFS-Metadaten ist ein logisches Wiederherstellungsproblem und nicht dasselbe Problem wie eine klickende Quellplatte.
- Eine reproduzierbar fehlgeschlagene Datei als Parser-Test wählen. Eine bekannte problematische 56-KB-Datei sagte mir mehr über alternative Parser als Stunden blinder Bulk-Extraktion.
- Den Parser selbst validieren. Wenn ein Werkzeug hängt, bevor es die Quelle sinnvoll liest, sollte man das nicht als Beweis interpretieren, dass die Daten weg sind.
- Nicht annehmen, dass eine einzelne APFS-Implementierung die Wiederherstellbarkeit definiert. TSK und
libfsapfsverhielten sich auf denselben geklonten Bytes sehr unterschiedlich. - Laufwerke über stabile Eigenschaften identifizieren, nicht über temporäre Gerätenummern. Eine Dateisystem-UUID und die bekannte physische Größe sind sicherer als das gestrige
/dev/diskN. - Lange Extraktionen von Anfang an fortsetzbar machen. Dauerhafter Zustand, temporäre Dateien, atomisches Umbenennen, zurückgestellte Arbeit, begrenzte Wiederholungsversuche und saubere Behandlung des Herunterfahrens gehören in diesem Maßstab zur Korrektheit.
- Validierungsebenen trennen. Ein ddrescue-Prozentwert, ein sichtbarer Pfadname, eine Übereinstimmung der erwarteten Größe und ein Inhaltshash beweisen jeweils unterschiedliche Dinge.
Die Zahl, die wie die Ziellinie aussah, war nur das Ende der ersten Phase
Die irreführendste Zahl der gesamten Wiederherstellung war weiterhin:
100.00%
Sie sah aus wie eine Antwort auf „Habe ich die Platte gerettet?“
Tatsächlich beantwortete sie nur eine viel engere Frage:
Wie viel des physischen Rettungsbereichs konnte ddrescue erfolgreich kopieren?
Sie beantwortete nicht, ob APFS den Namensraum rekonstruieren konnte. Sie beantwortete nicht, ob ein Parser beschädigte Metadaten traversieren konnte, die ein anderer Parser ablehnte. Sie beantwortete nicht, ob eine wiederhergestellte Datei die erwartete Länge hatte. Und sie beantwortete nicht, ob eine Extraktion von mehreren Millionen Dateien Fehler und Neustarts überstehen konnte, ohne ihren eigenen Zustand zu beschädigen.
Für jede Ebene brauchte ich separate Belege.
Zuerst gelang die physische Wiederherstellung. Das Dateisystem war weiterhin beschädigt. Der erste Parser konnte Namen sichtbar machen, scheiterte aber bei vielen Dateiinhalten. Eine andere APFS-Implementierung las dieselbe Testdatei erfolgreich. Aus diesem Nachweis wurde ein Extraktor für eine einzelne Datei, daraus eine fortsetzbare Recovery-Engine, und diese Engine stellte schließlich die von mir benötigten ausgewählten Verzeichnisbäume auf einem zweiten Laufwerk wieder her, während der Master-Klon unverändert blieb.
Die ursprüngliche Festplatte wurde nie wieder gesund. APFS reparierte sich nicht auf magische Weise. Geändert hat sich das Wiederherstellungsmodell.
Blockwiederherstellung, Dateisystemwiederherstellung und Dateivalidierung sind getrennte Engineering-Phasen. Sie als ein einziges Problem zu behandeln ließ die Situation fast hoffnungslos wirken. Durch ihre Trennung wurde sie beherrschbar.