Voltar ao blog
7 de outubro de 2026Sergei Solod25 min de leitura

Como recuperei arquivos de um clone APFS corrompido após o GNU ddrescue ter copiado 99.999654% de um disco de 4 TB

O GNU ddrescue copiou 99.999654% de um HDD de 4 TB com falhas, mas o clone APFS continuava a falhar nas verificações do sistema de arquivos e The Sleuth Kit conseguia listar caminhos enquanto devolvia arquivos de zero bytes. Recuperei as árvores de diretórios selecionadas preservando um clone mestre em modo de leitura, mudando para libfsapfs e criando um pipeline de extração que podia ser retomado.

APFSGNU ddrescueRecuperação de dadoslibfsapfsmacOS

O GNU ddrescue indicou 100.00%. A minha primeira passagem estruturada de recuperação de arquivos produziu 0 useful recovered user files.

Esses dois resultados vieram da mesma falha de um disco rígido de 4 TB, e essa contradição foi o que tornou esta recuperação muito mais interessante do que “clonar o disco com problemas e copiar os arquivos”. O ddrescue tinha feito o seu trabalho extraordinariamente bem: tinha copiado aproximadamente 99.999654% da origem física. O clone estava em hardware saudável. Mesmo assim, o APFS continuava corrompido, o macOS não me disponibilizava o sistema de arquivos normalmente e a primeira stack de recuperação de APFS conseguia enumerar grandes partes da árvore de diretórios, mas falhava ao ler o conteúdo de arquivos comuns.

No fim, recuperei todas as árvores de pastas selecionadas e enumeradas de que precisava, mas só depois de tratar o incidente como três problemas separados: recuperação física de blocos, interpretação de um sistema de arquivos danificado e extração de arquivos em grande escala com validação e suporte para retomar o processo.

A falha deixou de ser, antes de tudo, um problema do sistema de arquivos

O Toshiba original de 4 TB tinha chegado a um ponto em que eu já não confiava nele para atividade normal do sistema de arquivos. Algumas leituras levavam 60–75 segundos. As operações podiam ficar bloqueadas. O disco desaparecia do macOS de forma intermitente, fazia cliques audíveis e às vezes desligava.

Pouco antes da falha, eu tinha gravado cerca de 300 GB de dados adicionais e feito uma renomeação em massa que afetou aproximadamente 500,000 arquivos e diretórios. O momento em que isso aconteceu tornou suspeita essa carga intensa sobre metadados, mas não posso provar que ela tenha causado a falha de hardware. Pode simplesmente ter pressionado um disco que já não estava saudável o suficiente para expor o problema.

O que consegui estabelecer foi o comportamento do disco. Quando um armazenamento mecânico começa a bloquear, desaparecer e fazer cliques, percorrer diretórios repetidamente é a abstração errada. A travessia de diretórios pode provocar mais leituras e movimentos da cabeça de leitura. Montar um sistema de arquivos pode provocar trabalho sobre metadados. Cada experimento consome tempo do único componente cuja vida útil restante é desconhecida.

Por isso, mudei o objetivo de:

recuperar os meus arquivos

para:

recuperar o maior número possível de setores legíveis

Usei o GNU ddrescue 1.30 com um mapfile persistente. O tamanho físico exato do disco de origem era:

4,000,787,027,968 bytes

O mapfile era essencial porque a origem não era estável o bastante para uma cópia de uma única passagem. Ele permitia que a recuperação sobrevivesse a bloqueios, desconexões, reinícios e passagens posteriores sem esquecer quais regiões já tinham sido recuperadas.

Também encontrei um problema prático com o caminho do dispositivo raw no macOS: a extensão aparente da recuperação podia tornar-se sem sentido em vez de terminar no limite real do dispositivo. Por isso, limitei o domínio de recuperação ao tamanho físico conhecido indicado acima. Neste caso, limitar explicitamente a entrada era uma medida de correção, não uma otimização de velocidade.

Por que o ddrescue mostrar 100.00% não significava que os arquivos estavam seguros

Perto do fim da recuperação física, o ddrescue mostrava aproximadamente:

domain size:  4000 GB
rescued:      4000 GB
non-tried:   13481 kB
non-trimmed: 327680 B
non-scraped:      0 B
bad-sector:   27136 B

A percentagem em destaque era:

100.00%

Mas os estados ainda não resolvidos somavam:

13,835,816 bytes

ou, aproximadamente:

13.84 MB

Perante uma origem de 4,000,787,027,968 bytes, a proporção recuperada era aproximadamente:

99.999654%

É um excelente resultado de recuperação de blocos. Não é um resultado de integridade de arquivos.

A localização dos bytes em falta importa mais do que a quantidade em destaque. Vários megabytes perdidos em espaço não utilizado podem não afetar nada visível. Uma pequena região ilegível dentro de um vídeo pode danificar um arquivo. Uma perda muito menor nos metadados do sistema de arquivos pode dificultar a localização de muitos extents de dados que, de resto, continuam intactos.

Esse tornou-se o modelo mental central para o resto da recuperação:

CamadaPergunta a que respondeO que o sucesso não prova
Recuperação de blocosOs setores físicos foram copiados?Que o APFS consiga reconstruir todos os arquivos
Recuperação do sistema de arquivosÉ possível resolver caminhos, metadados e extents?Que cada byte extraído seja válido
Validação de arquivosUm arquivo chegou com o tamanho ou hash esperado?Que nunca tenha existido nenhum arquivo que já não possa ser descoberto

Fiz algumas tentativas finais nas regiões que continuavam ilegíveis. Acabaram por deixar de produzir novas leituras úteis enquanto o Toshiba fazia muitos cliques. Foi aí que deixei de usar o original como fonte ativa de recuperação.

Por que comprei dois discos de 5 TB para recuperar um disco de 4 TB

O primeiro disco novo foi um Seagate Expansion de 5 TB com uma capacidade física exata de:

5,000,981,077,504 bytes

Gravei nele o clone do Toshiba em nível de blocos. A disposição da origem ocupava aproximadamente os primeiros 4 TB, deixando cerca de 1 TB para além da disposição copiada. Deixei deliberadamente essa capacidade extra intacta.

Não aumentei o container APFS. Não reparticionei o clone por conveniência. Não executei uma reparação do sistema de arquivos nele. Esse disco tornou-se o clone mestre.

Depois comprei um segundo disco de 5 TB. Estava recém-formatado, permitia escrita, tinha sido testado de forma independente e foi usado apenas como destino dos dados recuperados.

HDD de 4 TB com falha
        │
        │ GNU ddrescue
        ▼
disco de 5 TB n.º 1
clone mestre em nível de blocos
APENAS LEITURA
        │
        │ análise e extração de APFS
        ▼
disco de 5 TB n.º 2
arquivos recuperados
COM ESCRITA

Isso significou comprar aproximadamente 10 TB nominais de armazenamento novo para recuperar um volume que continha cerca de 3.26 TB de dados utilizados. O disco adicional não era uma questão de capacidade. Era uma forma de preservar uma invariante:

Se uma experiência der errado, posso voltar ao mesmo clone mestre intacto.

Reparar, redimensionar, reparticionar ou gravar a saída recuperada no mestre teria misturado preservação com experimentação. Manter origem e destino em discos físicos separados tornava os erros recuperáveis.

Antes de confiar no destino, fiz um teste de escrita/leitura de aproximadamente 10 GB. Nesse teste, o disco sustentou cerca de 144.4 MB/s em ambas as direções. Leituras raw de amostra do clone mestre ficaram aproximadamente entre 28–49 MB/s e não reproduziram o padrão de falha física de E/S do disco original durante essas verificações.

Nesse ponto, o problema tinha mudado. Eu já não depurava hardware com falhas. O foco passou a ser depurar metadados APFS danificados preservados em hardware que se comportava normalmente nos meus testes.

O clone APFS podia ser lido como dispositivo, mas era inválido como sistema de arquivos

O armazenamento físico APFS clonado tinha:

4,000,650,887,168 bytes

A partição relevante começava no setor:

264192

Com setores de 512 bytes, isso corresponde a um deslocamento em bytes de:

135,266,304 bytes

O volume APFS indicava aproximadamente:

3,260,976,717,824 bytes

consumidos.

Uma verificação de APFS em modo de leitura chegou à árvore fsroot e mostrou:

Checking fsroot tree.
error: (oid 0xe36cb) apfs_root:
btn: dev_read_finish(3828538, 1): Input/output error
fsroot tree is invalid.

Foi nesse ponto que “o ddrescue copiou quase todo o disco” deixou de ser útil como diagnóstico completo. O clone raw existia. A estrutura do sistema de arquivos dentro dele continuava inconsistente.

Mantive deliberadamente a verificação do sistema de arquivos sem modificações. Não transformei fsck_apfs -n numa operação de reparação contra o único clone mestre de alta qualidade que eu tinha. Uma reparação pode ser adequada em armazenamento normal, mas aqui teria alterado a evidência que eu ainda tentava compreender.

The Sleuth Kit conseguia listar o namespace APFS, mas falhava no conteúdo dos arquivos

The Sleuth Kit 4.15.0 foi a primeira stack de recuperação que fez o clone parecer promissor. Usando o dispositivo clonado completo, o deslocamento conhecido da partição e o superbloque APFS que eu tinha identificado, conseguia enumerar nomes reais de diretórios com fls:

fls \\
  -o 264192 \\
  -B 4594668 \\
  -p \\
  /dev/rdiskN

Uso N de propósito. Os números de disco do macOS mudavam entre reconexões e reinícios, por isso eu não tratava uma atribuição anterior de /dev/disk6 como identidade.

fls conseguia percorrer partes substanciais do namespace. Só uma árvore grande expunha aproximadamente 8,900 diretórios. Por um momento, pareceu que a parte difícil estava resolvida.

Depois tentei recuperar o conteúdo dos arquivos.

Uma falha representativa era:

libc++abi: terminating due to uncaught exception
of type std::runtime_error:
could not read APFSBlock

tsk_recover conseguia começar a extração e depois falhar num bloco APFS. Tentativas individuais com icat mostravam a mesma classe de problema.

A distinção importante era:

a travessia de diretórios funciona

não implicava:

a recuperação do conteúdo dos arquivos funciona

Um parser pode ter metadados sobreviventes suficientes para descobrir um caminho e ainda assim falhar mais tarde ao resolver o objeto do arquivo, os metadados de extents ou os blocos de conteúdo necessários para devolver o fluxo de bytes.

Tornei o primeiro motor de recuperação em massa tolerante a falhas, mas o parser continuava inadequado para este dano

A minha primeira reação foi tornar a extração com TSK mais resistente, em vez de trocar imediatamente de parser.

Criei um wrapper de recuperação em Python em torno de fls e icat. Ele mantinha estado durável no SQLite, anotava falhas, permitia retomar o processo, escrevia a saída parcial separadamente e dava menor prioridade a dados auxiliares do macOS de pouco valor durante a primeira passagem.

Também adicionei uma regra de “hotspot”: se quatro arquivos consecutivos num diretório falhassem com o mesmo padrão APFSBlock de zero bytes, o script deixava de gastar tempo no resto desse ramo, adiava esse ramo e continuava noutro local. A hipótese de trabalho era que um conjunto de falhas idênticas poderia partilhar uma dependência de metadados danificada, em vez de representar centenas de conteúdos destruídos de forma independente.

A orquestração era útil. O leitor APFS subjacente não.

Num dos estados capturados, a base de dados de recuperação continha:

DEFERRED_HOTSPOT: 30,356 files
FAILED:              486 files

As 486 falhas tinham a seguinte distribuição:

409  APFSBlock crashes
75   rc=0 but output-size mismatch
2    other rc=1 failures

E a passagem estruturada tinha produzido:

0 useful recovered user files

As 75 divergências de tamanho expuseram um erro no meu próprio wrapper: alguns arquivos de metadados do sistema tinham devolvido dados, mas o meu parser tinha armazenado um tamanho esperado de zero. Corrigir essa interpretação era importante, mas não alterou o resultado dominante. Os arquivos comuns continuavam a terminar com zero bytes e could not read APFSBlock.

Escolhi um pequeno arquivo AVIF como caso de teste reproduzível. O TSK conhecia o tamanho esperado:

expected size: 56,309 bytes
recovered:             0 bytes

Esse arquivo tornou-se muito mais valioso do que outra passagem em massa de várias horas. Se uma nova abordagem não conseguisse recuperar um arquivo de teste de 56 KB que falhava de forma consistente, não merecia acesso aos terabytes restantes.

Os blocos do disco e o caminho de metadados APFS eram domínios de falha diferentes

Nesse ponto eu tinha três observações:

GNU ddrescue:
quase toda a origem física foi copiada

TSK fls:
era possível descobrir muitos caminhos reais

TSK icat:
o conteúdo de muitos arquivos comuns continuava a falhar

Essas observações são compatíveis quando a cadeia de procura é separada conceitualmente:

caminho
   ↓
entrada de diretório
   ↓
objeto de arquivo / metadados de inode
   ↓
mapeamento de extents
   ↓
blocos físicos de dados

Os blocos de dados perto da parte inferior podem sobreviver enquanto uma ligação mais acima na cadeia de metadados está danificada. Outra possibilidade é que duas implementações de APFS percorram de maneira diferente as mesmas estruturas danificadas.

Não isolei um único objeto APFS corrompido que explicasse todas as falhas, por isso não afirmaria que essa fosse uma causa raiz confirmada. O que a evidência sustentava era uma experiência muito mais útil: manter inalterados os bytes clonados e mudar o parser.

142 checkpoints de APFS não se tornaram uma via imediata de rollback

Uma varredura raw e em modo de leitura da área de descritores de checkpoints de APFS encontrou 142 superbloques NXSB candidatos, com identificadores de transação desde:

223133

até:

222992

A pergunta óbvia era se um checkpoint mais antigo fazia referência a uma árvore de metadados mais saudável.

Compilei apfs-fuse e testei diferentes identificadores de transação de checkpoint através de fuse-t. O checkpoint mais recente ficou bloqueado. XIDs mais antigos fizeram o mesmo. Adicionei timeouts rígidos por checkpoint e mais de 50 tentativas consecutivas não conseguiram produzir uma montagem utilizável. Também testei os backends NFS e SMB do fuse-t.

Isso não provava que todos os checkpoints estavam corrompidos. A experiência dependia do checkpoint, de apfs-fuse, de fuse-t, do comportamento do dispositivo no macOS e do backend de montagem. Uma falha em qualquer ponto dessa stack poderia produzir o mesmo resultado visível.

Um parser posterior informou zero snapshots de APFS, o que também reforçou uma distinção que eu precisava manter clara: esses estados de transação dos checkpoints não eram a mesma coisa que snapshots de APFS visíveis para quem usa o sistema.

Um parser que bloqueia com --help não consegue diagnosticar o meu disco

Também compilei go-apfs-v2. A compilação terminou, mas até:

apfs --help

ficou bloqueado e teve de ser terminado por timeout.

A inspeção mínima de blocos tanto com caminhos de dispositivo raw como com caminhos com buffer também bloqueou. Fiz o mesmo tipo de teste limitado com apfsutil; ambas as formas do dispositivo atingiram o timeout após cerca de 15 segundos.

Essas falhas foram úteis porque impediram que eu tirasse a conclusão errada. Uma ferramenta que não consegue concluir de forma consistente nem o seu próprio caminho de ajuda fornece pouca evidência sobre se um arquivo danificado é recuperável.

A minha regra passou a ser:

Validar a ferramenta de recuperação antes de tratar a falha da própria ferramenta como evidência sobre os dados.

Impedir que o macOS montasse automaticamente o clone eliminou outra fonte de risco

O próprio macOS era outra parte móvel. Eu queria que o destino saudável fosse montado normalmente, mas não queria que o Disk Arbitration tentasse montar automaticamente o clone APFS danificado sempre que eu voltasse a ligar o armazenamento.

A sequência segura que acabei por usar foi:

  1. Ligar e montar o destino saudável de recuperação.
  2. Verificar a identidade do volume e o espaço livre.
  3. Suspender diskarbitrationd.
  4. Verificar que não existia nenhum processo mount_apfs ativo.
  5. Ligar o clone mestre.
  6. Identificar dinamicamente o mestre pelo tamanho físico conhecido e pela identidade APFS.
  7. Verificar que o mestre não estava montado.
  8. Executar o trabalho de recuperação em modo de leitura.
  9. Ao parar, guardar o estado, retomar o Disk Arbitration e depois desligar ou ejetar normalmente.

O comando que usei efetivamente para suspender o Disk Arbitration foi:

sudo kill -STOP "$(pgrep -x diskarbitrationd)"

Verifiquei que o estado do processo continha T e procurei processos mount_apfs soltos antes de continuar.

A lição de segurança importante não era o número específico do disco. Era exatamente o contrário: nunca confie no número de disco de ontem. Depois de um reinício, um /dev/disk6 anterior pode referir-se a um dispositivo físico diferente. Tratei o UUID de APFS e o tamanho físico conhecido como identidade e, a partir daí, determinei o caminho atual do dispositivo.

libfsapfs recuperou o mesmo arquivo que o TSK devolvia com zero bytes

O avanço decisivo veio de libfsapfs, uma implementação diferente de APFS.

Fiz a compilação a partir do código-fonte no macOS. O binário fsapfsinfo resultante mostrava a seguinte identificação:

fsapfsinfo 20260923

Então apareceu uma diferença importante entre interfaces de dispositivo do macOS.

A partição do dispositivo raw:

/dev/rdiskNs2

falhava rapidamente com uma leitura de argumento inválido perto do deslocamento 4096.

A forma do dispositivo de blocos com buffer:

/dev/diskNs2

funcionava.

O mesmo clone físico. A mesma partição APFS. Uma interface de dispositivo do macOS diferente.

fsapfsinfo abriu o container e encontrou um volume. Depois testei exatamente o arquivo de 56,309 bytes que o TSK não tinha conseguido extrair.

O TSK tinha produzido:

expected:  56,309
recovered:      0
APFSBlock failure

Com libfsapfs, a entrada do arquivo produziu:

size: 56,309
MD5:  c6f56db33eafc1de0f52a035bc255dc7
RC:   0

Esse foi o primeiro resultado que alterou materialmente o diagnóstico. Os bytes clonados não tinham mudado. O arquivo de teste não tinha mudado. O APFS danificado não tinha sido reparado.

A implementação de APFS tinha mudado.

No mínimo, agora eu sabia que um arquivo real de uso comum que parecia irrecuperável através do TSK continuava acessível com profundidade suficiente através de libfsapfs para ler todo o seu conteúdo e calcular um hash.

Extraí um arquivo conhecido que falhava antes de confiar terabytes ao libfsapfs

Um hash bem-sucedido não era suficiente para eu iniciar uma recuperação de vários terabytes. Eu queria os bytes reais no disco de destino.

Em vez de adivinhar a API C de libfsapfs, inspecionei o código-fonte da biblioteca e segui o caminho de leitura que fsapfsinfo já usava ao calcular o hash. Depois criei um pequeno extrator em modo de leitura para aquele único arquivo de teste conhecido.

O extrator gravou exatamente:

56,309 bytes

no disco de recuperação separado. O MD5 do arquivo extraído era:

c6f56db33eafc1de0f52a035bc255dc7

Era igual ao hash anterior.

Só então ampliei a abordagem. O teste de um único arquivo tinha provado três coisas separadas: o caminho podia ser resolvido, era possível extrair a contagem completa de bytes esperada e os bytes extraídos produziam o mesmo hash que a leitura completa anterior do conteúdo.

O problema da recuperação em massa era sobretudo conter falhas e permitir retomar

Assim que libfsapfs conseguiu recuperar um arquivo que o TSK não conseguia, o problema difícil voltou a mudar. Eu precisava de um sistema capaz de processar uma árvore de diretórios muito grande sem que um ramo danificado, um reinício ou um Ctrl+C transformassem o trabalho num recomeço a partir do zero.

Por isso, o pipeline de recuperação em massa usava algumas invariantes rígidas:

  • O clone mestre era aberto em modo de leitura.
  • A saída recuperada era gravada apenas no segundo disco de 5 TB.
  • Os nomes de diretórios e arquivos eram preservados.
  • O progresso ficava no SQLite para sobreviver a saídas do processo e reinícios.
  • Cada arquivo era gravado primeiro num caminho temporário.
  • Um arquivo temporário só era renomeado para o caminho final depois de ter sido gravado o tamanho esperado completo.
  • Arquivos existentes com o tamanho esperado podiam ser reconhecidos ao retomar.
  • O trabalho com falhas ou problemas ficava separado do trabalho concluído.
  • O espaço livre era verificado e uma reserva de segurança era mantida.
  • Artefatos de desenvolvimento regeneráveis e metadados de sistema de pouco valor podiam ser ignorados ou receber menor prioridade.

Uma mudança de desempenho teve efeito imediato: deixei de reabrir o container APFS de forma independente para cada arquivo.

O caminho rápido processava um diretório de cada vez. Um worker abria a origem, enumerava esse diretório, recuperava os seus arquivos comuns imediatos e devolvia os diretórios filhos à fila. Se um diretório falhasse ou atingisse o timeout, o controlador marcava o diretório como DEFERRED e seguia em frente em vez de bloquear a passagem global.

Depois de deixar de existir trabalho pendente normal, os diretórios adiados eram revisitados através de um caminho alternativo mais lento, com trabalho por arquivo mais isolado. As falhas restantes podiam então ser tentadas novamente de forma independente.

descobrir diretório
    ↓
recuperar arquivos imediatos
    ↓
verificar tamanhos esperados
    ↓
confirmar estado durável
    ↓
colocar diretórios filhos na fila
    ↓
adiar falhas locais
    ↓
continuar globalmente
    ↓
usar o caminho alternativo e tentar novamente mais tarde

Essa arquitetura correspondia muito melhor ao padrão real de falhas do que um único comando recursivo gigantesco. O dano não era uniforme, portanto o sistema de recuperação também não deveria tornar o progresso uniforme.

Por que usei verificações do tamanho esperado e renomeação indivisível em vez de calcular hashes de milhões de arquivos

O MD5 foi útil durante a prova com um único arquivo porque eu precisava de evidência forte de que o parser lia o conteúdo completo de um arquivo que o TSK não conseguia ler.

Fazer uma segunda leitura completa de cada byte recuperado apenas para calcular o hash de milhões de arquivos teria acrescentado uma grande quantidade de E/S. Para a principal passagem de extração, usei uma invariante diferente.

Para cada arquivo comum, os metadados APFS forneciam um tamanho esperado. O worker gravava num arquivo temporário e só o promovia ao caminho final depois de a leitura completa corresponder a esse tamanho esperado.

Isso significa que uma interrupção não deveria deixar um arquivo incompleto a passar por arquivo final.

Uma correspondência de tamanho não é uma prova criptográfica de integridade. Não a trato como tal. Mas a verificação do tamanho esperado combinada com a renomeação indivisível era um limite prático de correção para a passagem de grande volume, enquanto hashes direcionados continuavam úteis para amostras e casos de falha conhecidos.

O SQLite tornou um reinício rotineiro em vez de catastrófico

A recuperação demorou o suficiente para eu precisar de parar o computador e retomar mais tarde. Esse requisito mudou o projeto de “script” para “fluxo de trabalho recuperável”.

Um Ctrl+C não abandonava simplesmente o processo filho ativo. O controlador capturava a interrupção, parava o worker, devolvia o diretório ativo a um estado recuperável, confirmava o estado no SQLite e saía.

Em termos práticos, uma interrupção limpa era assim:

DIRETÓRIO ATUAL -> PENDENTE

CTRL+C: RECUPERAÇÃO PARADA COM SEGURANÇA
ESTADO GUARDADO. EXECUTE O MESMO COMANDO PARA RETOMAR.

Depois de reiniciar, repeti as verificações de identidade dos discos e iniciei o mesmo comando de recuperação. A base de dados de estado retomou a fila existente.

Um reinício posterior começou com:

DEFERRED:     1
DONE:     73,015
PENDING:   7,254

Isso era muito mais significativo do que uma barra de progresso genérica. Mostrava que dezenas de milhares de unidades de diretório concluídas tinham sobrevivido ao reinício e que a fila restante estava explícita.

Numa retomada anterior, o diretório interrompido na sessão anterior foi processado novamente. Os arquivos que já estavam presentes com o tamanho esperado foram reconhecidos e apenas o trabalho em falta teve de ser gravado. Era esse o comportamento que eu queria: reiniciar a recuperação deveria ser rotineiro, não assustador.

326,799 arquivos foram a primeira prova de que o método escalava

Antes de expandir o novo extrator para todos os dados selecionados, usei uma grande árvore prioritária como alvo de validação em massa.

O estado concluído mostrava:

directories completed:     8,940
new files written:       302,541
existing/resumed files:   24,258
recorded failed files:         0

O destino continha:

326,799 files
145,039,215,948 bytes

ou cerca de:

135.08 GiB

As contagens de arquivos conferiam exatamente:

302,541 + 24,258 = 326,799

Não ficaram arquivos .partial.* nessa árvore concluída.

O arquivo de teste de 56,309 bytes tinha provado que o parser podia ter sucesso onde o TSK falhava. A recuperação de 326,799 arquivos provou que a mesma abordagem conseguia atravessar uma hierarquia real substancial com lógica de retomada, reconhecimento de arquivos existentes e nenhuma falha de arquivo contabilizada nessa passagem concluída.

A recuperação maior ultrapassou 1.6 milhões de arquivos novos antes de terminar

Depois de a árvore prioritária terminar sem problemas, expandi a recuperação para os restantes dados de nível superior selecionados.

Numa interrupção segura intencional, o SQLite mostrava:

DONE directories:       39,015
PENDING directories:    17,017
DEFERRED directories:        1

new files written:   1,636,305
new bytes written: 902,715,716,335

Isso correspondia aproximadamente a:

840.72 GiB

de dados recém-gravados contabilizados pelas passagens de diretórios naquele ponto.

O único diretório DEFERRED não significava dados perdidos. Significava que o caminho rápido tinha deixado deliberadamente de permitir que aquele problema local atrasasse trabalho não relacionado. A fase alternativa existia especificamente para revisitar esses casos mais tarde.

As sessões seguintes retomaram a partir da mesma base de dados. A contagem de itens concluídos aumentou e a fila pendente diminuiu. No fim, recuperei todas as árvores de pastas selecionadas e enumeradas de que precisava.

O que posso afirmar com rigor sobre a recuperação final

Não vou descrever o resultado como “cada byte recuperado”. A evidência não sustenta essa afirmação.

O mapfile original do ddrescue ainda continha cerca de 13.84 MB que não tinham sido confirmados como copiados com sucesso. Também não posso provar que nenhum objeto do sistema de arquivos se tenha tornado completamente impossível de descobrir por os metadados necessários para o enumerar estarem entre as regiões danificadas.

Essas limitações importam porque a recuperação estruturada pode provar que um objeto enumerado foi extraído; não pode provar a inexistência histórica de um objeto que o namespace danificado já não consegue revelar.

A afirmação final mais forte é mais limitada:

Todas as árvores de pastas selecionadas e enumeradas de que precisava foram recuperadas com sucesso através do processo de recuperação estruturada.

Não precisei de reparar o clone mestre no próprio local. Não precisei de reutilizar o Toshiba original que fazia cliques para a extração em massa. Não precisei de uma passagem de carving de todo o disco ao estilo PhotoRec que sacrificasse a estrutura de diretórios e os nomes dos arquivos.

As ferramentas que falharam continuaram a fornecer evidência útil

Em retrospectiva, o caminho que funcionou parece simples:

clone com ddrescue
    ↓
libfsapfs
    ↓
extrator que permite retomar
    ↓
arquivos recuperados

Não foi assim que a investigação pareceu enquanto acontecia, e remover as abordagens que falharam eliminaria grande parte da lição de engenharia útil.

Com o TSK, aprendi que percorrer o namespace APFS e recuperar conteúdo eram modos de falha diferentes.

Com o primeiro erro do meu wrapper, aprendi que a classificação de erros do próprio script de recuperação não é a realidade objetiva.

Com o mecanismo de hotspots, aprendi a isolar falhas locais em vez de permitir que travassem o progresso global.

Com a experiência de checkpoints, aprendi a não confundir checkpoints de transação de APFS com snapshots.

Com as tentativas usando FUSE, aprendi que uma montagem falhada pode implicar várias camadas além dos próprios dados do sistema de arquivos.

Com o leitor de APFS que bloqueava com --help, aprendi a validar a ferramenta antes de interpretar os seus diagnósticos.

Com o comportamento de /dev/rdiskNs2 em comparação com /dev/diskNs2, aprendi que o caminho de E/S do SO pode alterar o comportamento de um parser mesmo quando o disco subjacente é idêntico.

E a arquitetura de dois discos permitiu que eu cometesse erros em todo o resto, mantendo o clone mestre inalterado.

O fluxo de recuperação que eu voltaria a usar

  1. Parar a atividade normal do sistema de arquivos em armazenamento com falhas mecânicas. Se as leituras bloqueiam, o dispositivo desaparece ou faz cliques, eu daria prioridade a um clone reanudável em nível de blocos em vez de explorar com o Finder.
  2. Usar GNU ddrescue com um mapfile persistente e um domínio de recuperação verificado. O mapfile preserva o progresso; conhecer o tamanho da origem evita que a confusão sobre o tamanho do dispositivo passe a fazer parte da recuperação.
  3. Manter um clone mestre em modo de leitura. Não o reparar, redimensionar, reparticionar nem usar como armazenamento para arquivos recuperados.
  4. Gravar os arquivos recuperados num segundo disco físico. Preservar a origem e armazenar a saída são trabalhos diferentes.
  5. Diagnosticar primeiro o sistema de arquivos clonado em modo de leitura. Um clone físico saudável que contém metadados APFS corrompidos é um problema de recuperação lógica, diferente do problema de um disco de origem que faz cliques.
  6. Escolher um arquivo que falha de forma reproduzível como teste do parser. Um arquivo conhecido de 56 KB que falhava forneceu mais informação sobre parsers alternativos do que horas de extração em massa às cegas.
  7. Validar o próprio parser. Se uma ferramenta bloqueia antes de ler a origem de forma significativa, não interpretar isso como prova de que os dados desapareceram.
  8. Não assumir que uma implementação de APFS define o que é recuperável. O TSK e libfsapfs tiveram comportamentos muito diferentes sobre os mesmos bytes clonados.
  9. Identificar discos por propriedades estáveis, não por números temporários de dispositivo. Um UUID do sistema de arquivos e um tamanho físico conhecido são mais seguros do que o /dev/diskN de ontem.
  10. Tornar reanudável desde o início qualquer extração de longa duração. Estado durável, arquivos temporários, renomeação indivisível, trabalho adiado, novas tentativas limitadas e tratamento de encerramentos limpos fazem parte da correção nesta escala.
  11. Separar os níveis de validação. Uma percentagem do ddrescue, um caminho visível, uma correspondência com o tamanho esperado e um hash do conteúdo provam coisas diferentes.

O número que parecia a linha de chegada era apenas o fim da primeira etapa

O número mais enganador de toda a recuperação continuava a ser:

100.00%

Parecia uma resposta a “Consegui salvar o disco?”.

O que realmente respondia era muito mais limitado:

Que proporção do domínio físico de recuperação o ddrescue conseguiu copiar com sucesso?

Não respondia se o APFS conseguia reconstruir o namespace. Não respondia se um parser conseguia percorrer metadados danificados que outro parser rejeitava. Não respondia se um arquivo recuperado tinha o comprimento esperado. E não respondia se uma extração de vários milhões de arquivos conseguia sobreviver a erros e reinícios sem corromper o próprio estado.

Eu precisava de evidência separada para cada camada.

A recuperação física teve sucesso primeiro. O sistema de arquivos continuava danificado. O primeiro parser conseguia mostrar nomes, mas falhava no conteúdo de muitos arquivos. Uma implementação diferente de APFS leu com sucesso o mesmo arquivo de teste. Essa prova tornou-se um extrator de um único arquivo, o extrator tornou-se um motor de recuperação reanudável e o motor acabou por recuperar para um segundo disco as árvores de pastas selecionadas de que eu precisava, enquanto o clone mestre permaneceu intacto.

O disco rígido original nunca voltou a ficar saudável. O APFS nunca se reparou magicamente. O que mudou foi o modelo de recuperação.

Recuperação de blocos, recuperação do sistema de arquivos e validação de arquivos são etapas de engenharia separadas. Tratá-las como um único problema fazia a situação parecer quase sem esperança. Separá-las tornou a situação tratável.