GNU ddrescue indicava 100.00%. Il mio primo passaggio strutturato di recupero dei file ha prodotto 0 useful recovered user files.
Questi due risultati provenivano dallo stesso guasto di un hard disk da 4 TB, e proprio questa contraddizione ha reso il recupero molto più interessante di «clona il disco guasto e copia i file». ddrescue aveva svolto il proprio lavoro in modo straordinariamente efficace: aveva copiato circa il 99.999654% della sorgente fisica. Il clone si trovava su hardware sano. Eppure APFS era ancora corrotto, macOS non riusciva a rendermi disponibile normalmente il filesystem e il primo stack di recupero APFS poteva enumerare ampie parti dell’albero delle directory, ma non leggere il contenuto dei file normali.
Alla fine ho recuperato tutti gli alberi di cartelle selezionati ed enumerati di cui avevo bisogno, ma solo dopo aver trattato l’incidente come tre problemi distinti: recupero dei blocchi fisici, interpretazione del filesystem danneggiato ed estrazione dei file su larga scala con validazione e supporto alla ripresa.
Il guasto ha smesso innanzitutto di essere un problema di filesystem
Il Toshiba originale da 4 TB era arrivato al punto in cui non mi fidavo più a usarlo per la normale attività del filesystem. Alcune letture richiedevano 60–75 secondi. Le operazioni potevano bloccarsi. Il disco scompariva a intermittenza da macOS, emetteva clic udibili e a volte si spegneva.
Poco prima del guasto avevo scritto circa 300 GB di dati aggiuntivi ed eseguito una rinomina di massa che coinvolgeva circa 500,000 file e directory. La tempistica rendeva sospetto quel carico intenso sui metadati, ma non posso dimostrare che abbia causato il guasto hardware. Potrebbe semplicemente aver stressato abbastanza un disco già in cattive condizioni da far emergere il problema.
Ciò che potevo stabilire era il comportamento del disco. Quando un’unità meccanica rallenta, scompare e fa clic, continuare a sfogliare ripetutamente le directory è l’astrazione sbagliata. L’attraversamento delle directory può provocare altre letture e spostamenti delle testine. Montare un filesystem può innescare lavoro sui metadati. Ogni esperimento consuma tempo sull’unico componente di cui non si conosce la vita utile residua.
Quindi ho cambiato l’obiettivo da:
recover my files
a:
recover as many readable sectors as possible
Ho usato GNU ddrescue 1.30 con un mapfile persistente. La dimensione fisica esatta del disco sorgente era:
4,000,787,027,968 bytes
Il mapfile era essenziale perché la sorgente non era abbastanza stabile per una copia in un solo passaggio. Permetteva al recupero di sopravvivere a blocchi, disconnessioni, riavvii e passaggi successivi senza dimenticare quali aree erano già state recuperate.
Ho incontrato anche un problema pratico con il percorso del dispositivo raw di macOS: l’estensione apparente del recupero poteva diventare priva di senso invece di terminare al vero limite del dispositivo. Ho quindi limitato il dominio di recupero alla dimensione fisica nota indicata sopra. In questo caso, delimitare esplicitamente l’input era una misura di correttezza, non un’ottimizzazione delle prestazioni.
Perché il 100.00% mostrato da ddrescue non significava che i file fossero al sicuro
Verso la fine del recupero fisico, ddrescue riportava approssimativamente:
domain size: 4000 GB
rescued: 4000 GB
non-tried: 13481 kB
non-trimmed: 327680 B
non-scraped: 0 B
bad-sector: 27136 B
La percentuale principale era:
100.00%
Ma gli stati non risolti ammontavano ancora a:
13,835,816 bytes
ovvero circa:
13.84 MB
Su una sorgente da 4,000,787,027,968 byte, la quota recuperata era approssimativamente:
99.999654%
È un risultato eccellente per il recupero dei blocchi. Non è un risultato sull’integrità dei file.
La posizione dei byte mancanti conta più della quantità complessiva. Diversi megabyte persi in spazio inutilizzato potrebbero non avere alcun effetto visibile. Una piccola area illeggibile all’interno di un video può danneggiare un file. Una perdita molto più piccola nei metadati del filesystem può rendere difficili da localizzare molte estensioni di dati altrimenti integre.
Questo è diventato il modello mentale centrale per il resto del recupero:
| Livello | Domanda a cui risponde | Che cosa non dimostra il successo |
|---|---|---|
| Recupero dei blocchi | I settori fisici sono stati copiati? | Che APFS possa ricostruire ogni file |
| Recupero del filesystem | È possibile risolvere percorsi, metadati ed estensioni? | Che ogni byte estratto sia valido |
| Validazione dei file | Il file è arrivato con la dimensione o l’hash attesi? | Che non sia mai esistito alcun file ormai non individuabile |
Ho fatto alcuni ultimi tentativi sulle aree ancora illeggibili. Alla fine hanno smesso di produrre nuove letture utili mentre il Toshiba faceva clic in modo intenso. È stato allora che ho smesso di usare l’originale come sorgente attiva del recupero.
Perché ho comprato due dischi da 5 TB per recuperare un disco da 4 TB
Il primo nuovo disco era un Seagate Expansion da 5 TB con una capacità fisica esatta di:
5,000,981,077,504 bytes
Vi ho scritto il clone a livello di blocchi del Toshiba. Il layout della sorgente occupava all’incirca i primi 4 TB, lasciando circa 1 TB oltre il layout copiato. Ho deliberatamente lasciato inutilizzata quella capacità aggiuntiva.
Non ho ingrandito il container APFS. Non ho ripartizionato il clone per comodità. Non vi ho eseguito una riparazione del filesystem. Quel disco è diventato il clone principale.
Poi ho comprato un secondo disco da 5 TB. Era appena formattato, scrivibile, testato in modo indipendente e usato esclusivamente per l’output recuperato.
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
Questo significava acquistare circa 10 TB nominali di nuovo spazio di archiviazione per recuperare un volume che conteneva circa 3.26 TB di dati utilizzati. Il disco aggiuntivo non serviva per la capacità. Serviva a preservare un invariante:
If an experiment is wrong, I can return to the same untouched master clone.
Riparare, ridimensionare o ripartizionare il clone principale, oppure scrivervi sopra i dati recuperati, avrebbe mescolato conservazione e sperimentazione. Tenere sorgente e destinazione su due dischi fisici separati rendeva recuperabili gli errori.
Prima di fidarmi della destinazione, ho eseguito un test di scrittura/lettura di circa 10 GB. In quel test ha sostenuto circa 144.4 MB/s in entrambe le direzioni. Alcune letture raw di esempio dal clone principale erano di circa 28–49 MB/s e, durante quei controlli, non hanno riprodotto il comportamento di errore fisico di I/O del disco originale.
A quel punto il problema era cambiato. Non stavo più facendo debug di hardware guasto. Stavo facendo debug di metadati APFS danneggiati, conservati su hardware che nei miei test si comportava normalmente.
Il clone APFS era leggibile come dispositivo ma non valido come filesystem
Il supporto fisico APFS clonato aveva una dimensione di:
4,000,650,887,168 bytes
La partizione rilevante iniziava al settore:
264192
Con settori da 512 byte, questo corrisponde a un offset in byte di:
135,266,304 bytes
Il volume APFS riportava approssimativamente:
3,260,976,717,824 bytes
utilizzati.
Un controllo APFS in sola lettura ha infine raggiunto l’albero fsroot e riportato:
Checking fsroot tree.
error: (oid 0xe36cb) apfs_root:
btn: dev_read_finish(3828538, 1): Input/output error
fsroot tree is invalid.
È stato a questo punto che «ddrescue ha copiato quasi tutto il disco» ha smesso di essere utile come diagnosi completa. Il clone raw esisteva. La struttura del filesystem al suo interno era ancora incoerente.
Ho deliberatamente mantenuto non modificante il controllo del filesystem. Non ho trasformato fsck_apfs -n in un’operazione di riparazione sull’unico clone principale di alta qualità che avevo. Una riparazione può essere appropriata su un normale supporto di archiviazione, ma qui avrebbe modificato le prove che stavo ancora cercando di comprendere.
The Sleuth Kit riusciva a elencare il namespace APFS ma falliva sul contenuto dei file
The Sleuth Kit 4.15.0 è stato il primo stack di recupero a far sembrare promettente il clone. Usando l’intero dispositivo clonato, l’offset noto della partizione e il superblocco APFS che avevo identificato, potevo enumerare nomi reali di directory con fls:
fls \\
-o 264192 \\
-B 4594668 \\
-p \\
/dev/rdiskN
Uso N intenzionalmente. I numeri dei dischi macOS cambiavano dopo riconnessioni e riavvii, quindi non consideravo una precedente assegnazione /dev/disk6 come identità.
fls riusciva ad attraversare parti consistenti del namespace. Un solo grande albero mostrava circa 8,900 directory. Per un momento sembrava che la parte difficile fosse risolta.
Poi ho provato a recuperare il contenuto dei file.
Un errore rappresentativo era:
libc++abi: terminating due to uncaught exception
of type std::runtime_error:
could not read APFSBlock
tsk_recover poteva iniziare l’estrazione e poi fallire su un blocco APFS. I singoli tentativi con icat mostravano lo stesso tipo di problema.
La distinzione importante era:
directory traversal works
non implicava che:
file content retrieval works
Un parser può avere abbastanza metadati sopravvissuti da individuare un pathname e tuttavia fallire più avanti quando deve risolvere l’oggetto file, i metadati delle estensioni o i blocchi di contenuto necessari per restituire il flusso di byte.
Ho reso tollerante ai guasti il primo motore di recupero di massa, ma il parser era ancora sbagliato per questo danno
La mia prima reazione è stata rendere più resiliente l’estrazione con TSK invece di cambiare subito parser.
Ho costruito un wrapper di recupero in Python attorno a fls e icat. Manteneva uno stato persistente in SQLite, registrava gli errori, supportava la ripresa, scriveva separatamente l’output parziale e dava priorità inferiore ai dati di servizio macOS di scarso valore durante il primo passaggio.
Ho aggiunto anche una regola «hotspot»: se quattro file consecutivi nella stessa directory fallivano con lo stesso schema APFSBlock a zero byte, lo script smetteva di perdere tempo sul resto di quel ramo, lo rimandava e proseguiva altrove. L’ipotesi di lavoro era che un gruppo di errori identici potesse condividere una singola dipendenza da metadati danneggiata anziché rappresentare centinaia di payload distrutti indipendentemente.
L’orchestrazione era utile. Il lettore APFS sottostante no.
In un momento registrato, il database di recupero conteneva:
DEFERRED_HOTSPOT: 30,356 files
FAILED: 486 files
I 486 errori si suddividevano così:
409 APFSBlock crashes
75 rc=0 but output-size mismatch
2 other rc=1 failures
E il passaggio strutturato aveva prodotto:
0 useful recovered user files
Le 75 discrepanze di dimensione hanno rivelato un bug nel mio wrapper: alcuni file di metadati di sistema avevano restituito dati, ma il mio parser aveva registrato una dimensione attesa pari a zero. Correggere questa interpretazione era importante, ma non cambiava il risultato dominante. I normali file utente continuavano a finire a zero byte con could not read APFSBlock.
Ho scelto un piccolo file AVIF come caso di test riproducibile. TSK ne conosceva la dimensione attesa:
expected size: 56,309 bytes
recovered: 0 bytes
Quel file è diventato molto più prezioso di un altro passaggio di massa di diverse ore. Se un nuovo approccio non riusciva a recuperare un file di test da 56 KB che falliva in modo costante, non meritava accesso ai terabyte rimanenti.
I blocchi del disco e il percorso dei metadati APFS appartenevano a domini di guasto diversi
A quel punto avevo tre osservazioni:
GNU ddrescue:
almost the entire physical source was copied
TSK fls:
many real paths were discoverable
TSK icat:
many ordinary file contents still failed
Queste osservazioni sono compatibili se si separa concettualmente la catena di ricerca:
pathname
↓
directory record
↓
file object / inode metadata
↓
extent mapping
↓
physical data blocks
I blocchi di dati vicini al fondo della catena possono sopravvivere mentre un collegamento più in alto nella catena dei metadati è danneggiato. Un’altra possibilità è che due implementazioni APFS attraversino in modo diverso le stesse strutture danneggiate.
Non ho isolato un singolo oggetto APFS corrotto che spiegasse ogni errore, quindi non lo presenterei come causa radice confermata. Ciò che le prove supportavano era un esperimento molto più utile: lasciare invariati i byte clonati e cambiare parser.
142 checkpoint APFS non sono diventati un percorso di rollback immediato
Una scansione raw in sola lettura dell’area dei descrittori dei checkpoint APFS ha trovato 142 superblocchi checkpoint NXSB candidati, con ID di transazione compresi tra:
223133
fino a:
222992
La domanda ovvia era se un checkpoint più vecchio facesse riferimento a un albero di metadati più sano.
Ho compilato apfs-fuse e provato diversi ID di transazione dei checkpoint tramite fuse-t. Il checkpoint più recente si bloccava. Gli XID più vecchi facevano lo stesso. Ho aggiunto timeout rigidi per ogni checkpoint e più di 50 tentativi consecutivi non sono riusciti a produrre un mount utilizzabile. Ho provato anche entrambi i backend NFS e SMB di fuse-t.
Questo non dimostrava che tutti i checkpoint fossero corrotti. L’esperimento dipendeva dal checkpoint, da apfs-fuse, da fuse-t, dal comportamento del dispositivo in macOS e dal backend di mount. Un errore in qualunque punto di quello stack poteva produrre lo stesso risultato visibile.
Un parser successivo ha riportato zero snapshot APFS, rafforzando anche una distinzione che dovevo mantenere chiara: quegli stati di transazione dei checkpoint non erano la stessa cosa degli snapshot APFS visibili all’utente.
Un parser che si blocca su --help non può diagnosticare il mio disco
Ho compilato anche go-apfs-v2. La build è stata completata, ma persino:
apfs --help
si bloccava e doveva essere terminato allo scadere di un timeout.
Anche l’ispezione minima dei blocchi sia sul percorso raw sia su quello buffered del dispositivo si bloccava. Ho eseguito lo stesso tipo di test limitato con apfsutil; entrambe le forme del dispositivo andavano in timeout dopo circa 15 secondi.
Questi erano errori utili perché mi impedivano di trarre la conclusione sbagliata. Uno strumento che non riesce a completare in modo affidabile nemmeno il proprio percorso di help costituisce una prova debole sulla possibilità di recuperare un file danneggiato.
La mia regola è diventata:
Validare lo strumento di recupero prima di trattare il suo fallimento come una prova sullo stato dei dati.
Impedire a macOS di montare automaticamente il clone ha eliminato un’altra fonte di rischio
Anche macOS era un’altra variabile in gioco. Volevo che la destinazione sana venisse montata normalmente, ma non volevo che Disk Arbitration tentasse automaticamente di montare il clone APFS danneggiato ogni volta che ricollegavo lo storage.
La sequenza sicura che ho finito per usare era:
- Collegare e montare la destinazione sana del recupero.
- Verificare l’identità del volume e lo spazio libero.
- Sospendere
diskarbitrationd. - Verificare che non ci siano processi
mount_apfsattivi. - Collegare il clone principale.
- Identificare dinamicamente il clone principale tramite dimensione fisica nota e identità APFS.
- Verificare che il clone principale non sia montato.
- Eseguire le operazioni di recupero in sola lettura.
- Quando ci si ferma, salvare lo stato, riattivare Disk Arbitration e poi spegnere o espellere normalmente.
Il comando che ho effettivamente usato per sospendere Disk Arbitration era:
sudo kill -STOP "$(pgrep -x diskarbitrationd)"
Ho verificato che lo stato del processo contenesse T e ho controllato la presenza di processi mount_apfs residui prima di continuare.
La lezione di sicurezza importante non riguardava il numero specifico del disco. Era l’opposto: non fidarsi mai del numero di disco di ieri. Dopo un riavvio, un precedente /dev/disk6 può riferirsi a un dispositivo fisico diverso. Ho usato l’UUID APFS e la dimensione fisica nota come identità, quindi ho ricavato da questi il percorso corrente del dispositivo.
libfsapfs ha recuperato lo stesso file che TSK restituiva a zero byte
La svolta è arrivata da libfsapfs, un’implementazione APFS diversa.
L’ho compilata dai sorgenti su macOS. Il binario fsapfsinfo risultante si identificava come:
fsapfsinfo 20260923
A quel punto è emersa un’importante differenza nell’interfaccia dei dispositivi macOS.
La partizione del dispositivo a caratteri raw:
/dev/rdiskNs2
falliva rapidamente con una lettura «invalid argument» vicino all’offset 4096.
La forma buffered del dispositivo a blocchi:
/dev/diskNs2
funzionava.
Stesso clone fisico. Stessa partizione APFS. Interfaccia del dispositivo macOS diversa.
fsapfsinfo ha aperto il container e trovato un volume. Ho quindi testato esattamente il file da 56,309 byte che TSK non era riuscito a estrarre.
TSK aveva prodotto:
expected: 56,309
recovered: 0
APFSBlock failure
Con libfsapfs, la voce del file ha prodotto:
size: 56,309
MD5: c6f56db33eafc1de0f52a035bc255dc7
RC: 0
Questo è stato il primo risultato che ha cambiato concretamente la diagnosi. I byte clonati non erano cambiati. Il file di test non era cambiato. L’APFS danneggiato non era stato riparato.
Era cambiata l’implementazione APFS.
Come minimo, a quel punto sapevo che un vero file utente che con TSK sembrava irrecuperabile era ancora raggiungibile abbastanza in profondità tramite libfsapfs da leggerne l’intero contenuto e calcolarne un digest.
Ho estratto un file noto come problematico prima di affidare terabyte a libfsapfs
Un digest calcolato con successo non era sufficiente per lanciare un recupero di più terabyte. Volevo i byte reali sul disco di destinazione.
Invece di indovinare l’API C di libfsapfs, ho ispezionato il sorgente della libreria e seguito il percorso di lettura già usato da fsapfsinfo per calcolare il digest. Poi ho costruito un piccolo estrattore in sola lettura per l’unico file di test noto.
L’estrattore ha scritto esattamente:
56,309 bytes
sul disco di recupero separato. L’MD5 del file estratto era:
c6f56db33eafc1de0f52a035bc255dc7
Corrispondeva al digest precedente.
Solo allora ho esteso l’approccio. Il test su un singolo file aveva dimostrato tre cose distinte: il pathname poteva essere risolto, poteva essere estratto l’intero numero di byte atteso e i byte estratti producevano lo stesso digest della precedente lettura del contenuto completo.
Il problema del recupero di massa riguardava soprattutto il contenimento dei guasti e la ripresa
Una volta che libfsapfs è riuscito a recuperare un file che TSK non riusciva a recuperare, il problema difficile è cambiato di nuovo. Mi serviva un sistema capace di elaborare un albero di directory molto grande senza che un ramo danneggiato, un riavvio o un Ctrl+C trasformasse il lavoro in una ripartenza da zero.
La pipeline di recupero di massa usava quindi alcuni invarianti rigorosi:
- Il clone principale veniva aperto in sola lettura.
- L’output recuperato veniva scritto solo sul secondo disco da 5 TB.
- I nomi di directory e file venivano preservati.
- L’avanzamento risiedeva in SQLite, così da sopravvivere alle terminazioni del processo e ai riavvii.
- Ogni file veniva scritto prima in un percorso temporaneo.
- Un file temporaneo veniva rinominato nel percorso finale solo dopo aver scritto l’intera dimensione attesa.
- I file esistenti della dimensione attesa potevano essere riconosciuti durante la ripresa.
- Il lavoro fallito o problematico veniva tenuto separato da quello completato.
- Lo spazio libero veniva controllato e veniva mantenuta una riserva di sicurezza.
- Gli artefatti di sviluppo rigenerabili e i metadati di sistema di scarso valore potevano essere saltati o messi in secondo piano.
Una modifica delle prestazioni ha avuto un effetto immediato: ho smesso di riaprire il container APFS separatamente per ogni file.
Il percorso veloce elaborava una directory alla volta. Un worker apriva la sorgente, enumerava quella directory, recuperava i file regolari immediatamente al suo interno e restituiva le directory figlie alla coda. Se una directory falliva o andava in timeout, il controller la contrassegnava come DEFERRED e proseguiva invece di bloccare il passaggio globale.
Quando non rimaneva più lavoro normale in attesa, le directory differite venivano riprese tramite un percorso di fallback più lento, con lavoro per file maggiormente isolato. Gli errori rimanenti potevano quindi essere ritentati in modo indipendente.
discover directory
↓
recover immediate files
↓
verify expected sizes
↓
commit durable state
↓
queue child directories
↓
defer local failures
↓
continue globally
↓
fallback and retry later
Questa architettura corrispondeva molto meglio al vero schema dei guasti rispetto a un unico enorme comando ricorsivo. I danni non erano uniformi, quindi nemmeno il sistema di recupero doveva rendere uniforme l’avanzamento.
Perché ho usato controlli della dimensione attesa e rinomina atomica invece di calcolare l’hash di milioni di file
MD5 è stato utile durante la prova su un singolo file perché avevo bisogno di prove solide che il parser stesse leggendo il contenuto completo di un file che TSK non riusciva a leggere.
Eseguire una seconda lettura completa di ogni byte recuperato solo per calcolare l’hash di milioni di file avrebbe aggiunto una grande quantità di I/O. Per il passaggio principale di estrazione ho usato un invariante diverso.
Per ogni file regolare, i metadati APFS fornivano una dimensione attesa. Il worker scriveva in un file temporaneo e lo promuoveva al pathname finale solo dopo che la lettura completa corrispondeva a quella dimensione attesa.
Ciò significa che un’interruzione non dovrebbe lasciare un file più corto che si spaccia per quello finale.
Una corrispondenza di dimensione non è una prova crittografica di integrità. Non la considero tale. Ma la verifica della dimensione attesa insieme alla rinomina atomica costituiva un confine pratico di correttezza per il passaggio ad alto volume, mentre gli hash mirati restavano utili per campioni e casi di errore noti.
SQLite ha reso un riavvio noioso invece che catastrofico
Il recupero è durato abbastanza a lungo da costringermi a spegnere il computer e riprendere in seguito. Questo requisito ha trasformato il progetto da «script» a «workflow recuperabile».
Un Ctrl+C non abbandonava semplicemente il processo figlio attivo. Il controller intercettava l’interruzione, fermava il worker, riportava la directory attiva a uno stato recuperabile, eseguiva il commit su SQLite e terminava.
Un arresto pulito appariva concettualmente così:
CURRENT DIRECTORY -> PENDING
CTRL+C: RECOVERY STOPPED SAFELY
STATE SAVED. RUN THE SAME COMMAND TO RESUME.
Dopo il riavvio, ho ripetuto i controlli sull’identità dei dischi e avviato lo stesso comando di recupero. Il database di stato ha ripreso la coda esistente.
Un riavvio successivo è iniziato con:
DEFERRED: 1
DONE: 73,015
PENDING: 7,254
Era molto più significativo di una generica barra di avanzamento. Mostrava che decine di migliaia di unità di directory completate erano sopravvissute al riavvio e che la coda rimanente era esplicita.
In una ripresa precedente, la directory interrotta nella sessione precedente è stata ripresa. I file già presenti con la dimensione attesa sono stati riconosciuti e si è dovuto scrivere soltanto il lavoro mancante. Era il comportamento che volevo: riavviare il recupero doveva essere una routine, non qualcosa di spaventoso.
326,799 file sono stati la prima prova che il metodo scalava
Prima di estendere il nuovo estrattore a tutti i dati selezionati, ho usato un grande albero prioritario come obiettivo di validazione di massa.
Lo stato completato riportava:
directories completed: 8,940
new files written: 302,541
existing/resumed files: 24,258
recorded failed files: 0
La destinazione conteneva:
326,799 files
145,039,215,948 bytes
ovvero circa:
135.08 GiB
Il conteggio dei file tornava esattamente:
302,541 + 24,258 = 326,799
Non c’erano file .partial.* residui nell’albero completato.
Il file di test da 56,309 byte aveva dimostrato che il parser poteva riuscire dove TSK falliva. Il recupero di 326,799 file ha dimostrato che lo stesso approccio poteva reggere una gerarchia reale consistente, con logica di ripresa, rilevamento dei file esistenti e nessun errore di file registrato in quel passaggio completato.
Il recupero più ampio ha superato 1.6 milioni di nuovi file prima del completamento
Dopo che l’albero prioritario si è completato senza problemi, ho esteso il recupero ai restanti dati di primo livello selezionati.
In occasione di un arresto sicuro e intenzionale, SQLite riportava:
DONE directories: 39,015
PENDING directories: 17,017
DEFERRED directories: 1
new files written: 1,636,305
new bytes written: 902,715,716,335
Erano approssimativamente:
840.72 GiB
di nuovi dati scritti registrati dai passaggi sulle directory a quel punto.
L’unica directory DEFERRED non significava dati persi. Significava che il percorso veloce aveva intenzionalmente smesso di lasciare che quel problema locale ritardasse lavoro non correlato. La fase di fallback esisteva proprio per tornare su questi casi in seguito.
Le sessioni successive sono riprese dallo stesso database. Il numero di elementi completati aumentava e la coda in attesa diminuiva. Alla fine ho recuperato tutti gli alberi di cartelle selezionati ed enumerati di cui avevo bisogno.
Che cosa posso affermare onestamente sul recupero finale
Non descriverò il risultato come «ogni byte recuperato». Le prove non lo consentono.
La mappa ddrescue originale conteneva ancora circa 13.84 MB la cui copia riuscita non era stata confermata. Inoltre non posso dimostrare che nessun oggetto del filesystem sia diventato completamente non individuabile perché i metadati necessari per enumerarlo si trovavano tra le aree danneggiate.
Queste limitazioni sono importanti perché il recupero strutturato può dimostrare che un oggetto enumerato è stato estratto; non può dimostrare la non esistenza storica di un oggetto che il namespace danneggiato non riesce più a rivelare.
L’affermazione finale più forte è più circoscritta:
Tutti gli alberi di cartelle selezionati ed enumerati di cui avevo bisogno sono stati recuperati con successo tramite il processo di recupero strutturato.
Non ho dovuto riparare il clone principale sul posto. Non ho dovuto riutilizzare il Toshiba originale che faceva clic per l’estrazione di massa. Non ho avuto bisogno di un passaggio di carving in stile PhotoRec sull’intero disco, che avrebbe sacrificato la struttura delle directory e i nomi dei file.
Gli strumenti falliti sono stati comunque prove utili
A posteriori, il percorso riuscito sembra semplice:
ddrescue clone
↓
libfsapfs
↓
resumable extractor
↓
recovered files
Non è così che l’indagine sembrava mentre la stavo conducendo, e rimuovere gli approcci falliti eliminerebbe gran parte della lezione ingegneristica utile.
TSK mi ha insegnato che l’attraversamento del namespace APFS e il recupero del contenuto erano modalità di errore diverse.
Il bug del mio primo wrapper mi ha insegnato che la classificazione degli errori di uno script di recupero non è una verità di riferimento.
Il meccanismo hotspot mi ha insegnato a isolare i guasti locali invece di lasciare che bloccassero l’avanzamento globale.
L’esperimento sui checkpoint mi ha insegnato a non confondere i checkpoint delle transazioni APFS con gli snapshot.
I tentativi con FUSE mi hanno insegnato che un mount fallito può coinvolgere diversi livelli oltre ai dati del filesystem.
Il lettore APFS che si bloccava su --help mi ha insegnato a validare lo strumento prima di interpretarne la diagnostica.
Il comportamento di /dev/rdiskNs2 rispetto a /dev/diskNs2 mi ha insegnato che il percorso I/O del sistema operativo può cambiare il comportamento di un parser anche quando il disco sottostante è identico.
E l’architettura a due dischi mi ha dato la libertà di sbagliare in tutto il resto lasciando invariato il clone principale.
Il workflow di recupero che userei di nuovo
- Interrompere la normale attività del filesystem su uno storage con guasti meccanici. Se le letture si bloccano, il dispositivo scompare o fa clic, darei priorità a un clone a livello di blocchi riprendibile rispetto all’esplorazione con Finder.
- Usare GNU ddrescue con un mapfile persistente e un dominio di recupero verificato. Il mapfile preserva l’avanzamento; conoscere la dimensione della sorgente evita che la confusione sulla dimensione del dispositivo diventi parte del recupero.
- Mantenere un clone principale in sola lettura. Non ripararlo, ridimensionarlo, ripartizionarlo né usarlo come spazio per i file recuperati.
- Scrivere i file recuperati su un secondo disco fisico. Preservare la sorgente e archiviare l’output sono lavori diversi.
- Diagnosticare prima il filesystem clonato in sola lettura. Un clone fisico sano che contiene metadati APFS corrotti è un problema di recupero logico, non lo stesso problema di un disco sorgente che fa clic.
- Scegliere un singolo file con errore riproducibile come test del parser. Un file noto come problematico da 56 KB mi ha detto più sui parser alternativi di ore di estrazione di massa alla cieca.
- Validare il parser stesso. Se uno strumento si blocca prima di leggere in modo significativo la sorgente, non interpretarlo come prova che i dati siano spariti.
- Non presumere che una sola implementazione APFS definisca la recuperabilità. TSK e
libfsapfssi sono comportati in modo molto diverso sugli stessi byte clonati. - Identificare i dischi tramite proprietà stabili, non numeri temporanei di dispositivo. Un UUID del filesystem e una dimensione fisica nota sono più sicuri del
/dev/diskNdi ieri. - Rendere riprendibili fin dall’inizio le estrazioni di lunga durata. Stato persistente, file temporanei, rinomina atomica, lavoro differito, tentativi limitati e gestione pulita dell’arresto fanno parte della correttezza a questa scala.
- Separare i livelli di validazione. Una percentuale di ddrescue, un pathname visibile, una corrispondenza con la dimensione attesa e un hash del contenuto dimostrano cose diverse.
Il numero che sembrava il traguardo era soltanto la fine della prima fase
Il numero più fuorviante dell’intero recupero era ancora:
100.00%
Sembrava una risposta alla domanda «Ho salvato il disco?»
In realtà rispondeva a una domanda molto più circoscritta:
How much of the physical rescue domain did ddrescue successfully copy?
Non rispondeva alla domanda se APFS potesse ricostruire il namespace. Non diceva se un parser potesse attraversare metadati danneggiati che un altro parser rifiutava. Non diceva se un file recuperato avesse la lunghezza attesa. E non diceva se un’estrazione di milioni di file potesse sopravvivere a errori e riavvii senza corrompere il proprio stato.
Mi servivano prove separate per ogni livello.
Il recupero fisico è riuscito per primo. Il filesystem era ancora danneggiato. Il primo parser poteva mostrare i nomi ma falliva sul contenuto di molti file. Un’implementazione APFS diversa ha letto con successo lo stesso file di test. Quella prova è diventata un estrattore per un singolo file, l’estrattore è diventato un motore di recupero riprendibile e il motore ha infine recuperato sul secondo disco gli alberi di cartelle selezionati di cui avevo bisogno, mentre il clone principale rimaneva intatto.
L’hard disk originale non è mai tornato sano. APFS non si è mai riparato per magia. È cambiato il modello di recupero.
Il recupero dei blocchi, il recupero del filesystem e la validazione dei file sono fasi ingegneristiche separate. Trattarle come un unico problema faceva sembrare la situazione quasi disperata. Separarle l’ha resa gestibile.