Artigo Técnico

Isolar Codecs de Imagem PDF em Processos Worker com o HotPDF

O HotPDF consegue descodificar os três filtros de imagem PDF mais arriscados, DCTDecode, JPXDecode e JBIG2Decode, dentro de um processo worker separado e de curta duração, em vez de dentro da sua aplicação. A propriedade que ativa isto é CodecIsolationMode, e o efeito prático é que um codestream JPEG 2000 malformado, que antes teria feito colapsar a sua aplicação VCL, agora derruba um processo filho descartável enquanto o processo principal reporta um código de estado e continua a funcionar

Essa diferença importa sobretudo nos locais de onde os PDFs realmente chegam: um formulário de carregamento, um gateway de correio, um dispositivo de digitalização, uma entrega FTP de um parceiro. Não controla esses bytes, e os codecs de imagem são onde reside o histórico de danos

Porque é que uma imagem defeituosa derruba a aplicação inteira?

Porque um codec de imagem é a única parte de um leitor de PDF que executa uma máquina de estados complexa sobre dados controlados por um atacante, com quase nenhuma verificação estrutural a que possa recorrer. Quando os bytes chegam ao descodificador de JPEG 2000 ou JBIG2, a tabela de referência cruzada já foi analisada, o objeto já foi resolvido, a cadeia de filtros já foi desenrolada, e o que resta é um codestream em bruto que indica quantos blocos, quantas componentes, quantos bits por amostra. Um número errado aí não é um erro de análise. É um tamanho de alocação incorreto ou um índice fora de limites dentro de um ciclo de descodificação apertado

Os limites orçamentais ajudam, e já deveria tê-los. O HotPDF limita a expansão com DecodeBudgetBytes e DocumentDecodeBudgetBytes, e limita as cadeias de filtros com DecodeFilterLimit e DecodePipelineDepthLimit; o raciocínio por trás desses limites é abordado em descodificação limitada para filtros aninhados e bombas PDF. Mas um orçamento de bytes responde apenas a uma pergunta, quanta saída é permitida. Não consegue responder ao que acontece quando o descodificador falha antes de produzir sequer alguma saída. Uma violação de acesso dentro de um ciclo de descodificação não é uma violação de política que possa recusar; é um evento ao nível do processo, e a única contenção fiável para um evento ao nível do processo é um processo diferente

O que o HotPDF isola, e o que não isola

O HotPDF isola exatamente três tipos de codec, enumerados como hckDCT, hckJPX e hckJBIG2 na unidade HPDFCodecIsolation. Tudo o resto, Flate, LZW, RunLength, ASCII85, CCITT, permanece dentro do processo, porque esses descodificadores são suficientemente simples para serem limitados por orçamentos e não são a origem das falhas mais interessantes

O transporte é deliberadamente estreito. O processo principal aloca um único mapeamento de memória partilhada limitado, escreve um THPDFCodecSharedHeader fixo mais a entrada comprimida e quaisquer segmentos globais JBIG2, lança o worker e aguarda. O worker escreve os pixéis descodificados de volta no mesmo mapeamento e define uma palavra de estado. Não há qualquer protocolo de pipe que possa dessincronizar, nenhum formato de serialização para fazer fuzzing, e o cabeçalho transporta um valor mágico e uma versão, pelo que um binário worker incompatível é rejeitado em vez de mal interpretado

uses
  HPDFDoc, HPDFCodecIsolation;

var
  Pdf: THotPDF;
  Info: THPDFCodecWorkerInfo;
  Bmp: TBitmap;
begin
  Pdf := THotPDF.Create(nil);
  try
    // Falhar de forma fechada: nunca descodificar estes codecs dentro do processo
    Pdf.CodecIsolationMode := cimRequired;
    Pdf.CodecWorkerExecutable := 'HotPDFCodecWorker.exe';
    Pdf.CodecWorkerTimeoutMilliseconds := 5000;       // 1..600000
    Pdf.CodecWorkerMemoryLimitBytes := 268435456;     // 0 ou >= 64 MiB
    Pdf.DecodeBudgetBytes := 134217728;

    if Pdf.LoadFromFile('untrusted-upload.pdf') = 1 then
      if Pdf.GetLoadedImageCount > 0 then
      begin
        Bmp := Pdf.ExtractLoadedImage(0);
        try
          if Pdf.GetLastCodecWorkerInfo(Info) then
            LogCodecOutcome(Info);
        finally
          Bmp.Free;
        end;
      end;
  finally
    Pdf.Free;
  end;
end;

Deixe CodecWorkerExecutable vazio e o HotPDF resolve o worker junto do seu próprio executável, como HotPDFCodecWorker.exe na pasta de ParamStr(0). Defina-o explicitamente quando a sua implementação coloca o worker noutro local; o valor é expandido através de ExpandFileName, pelo que um caminho relativo é resolvido em relação à pasta atual, e não à pasta da aplicação, o que raramente é o que pretende num serviço

Automático ou obrigatório: que falha prefere?

Os três valores de THPDFCodecIsolationMode codificam três respostas diferentes a uma única pergunta, o que deve acontecer quando o worker não consegue ser executado de todo. cimDisabled ignora completamente o isolamento e descodifica dentro do processo, o comportamento anterior à versão 3.x. cimAutomatic, a predefinição, tenta o worker e recorre silenciosamente à descodificação dentro do processo quando o executável do worker está em falta ou não arranca, o que é reportado como estado cwsUnavailable. cimRequired recusa esse recurso alternativo: um worker indisponível marca a descodificação como tratada e falhada, pelo que nenhum codestream não fiável chega alguma vez ao espaço de endereçamento da sua aplicação

Escolha de acordo com o modelo de ameaça, não pela comodidade. Um visualizador de ambiente de trabalho que abre documentos que o utilizador já tem em disco funciona bem com cimAutomatic, onde um worker em falta degrada para o comportamento clássico em vez de quebrar o produto. Um serviço de ingestão que analisa ficheiros vindos da internet deve executar cimRequired, porque um erro de implementação que silenciosamente remove a camada de isolamento é exatamente o tipo de regressão que ninguém nota até ser importante. Note a assimetria: só cwsUnavailable desencadeia o recurso alternativo. Um worker que arrancou e depois falhou, ultrapassou o tempo-limite ou atingiu um limite é uma falha de descodificação em ambos os modos, nunca uma repetição silenciosa dentro do processo

Ler o veredito a partir de THPDFCodecWorkerStatus

GetLastCodecWorkerInfo devolve o resultado da descodificação isolada mais recente, e a enumeração de estados é suficientemente específica para orientar decisões operacionais reais em vez de uma linha de registo genérica de "falha na imagem". Os valores são cwsNotRun, cwsSucceeded, cwsUnavailable, cwsLaunchFailed, cwsTimedOut, cwsCrashed, cwsDecodeFailed, cwsProtocolError e cwsOutputLimit

Trate-os como três grupos. Os problemas de implementação são cwsUnavailable e cwsLaunchFailed: alguém distribuiu sem o worker, ou um antivírus está a bloquear a criação de processos. Os problemas de documento são cwsDecodeFailed e cwsOutputLimit: o ficheiro está malformado ou é maior do que a sua política permite, e rejeitá-lo é a resposta correta. O grupo interessante é cwsTimedOut e cwsCrashed, porque estes são os eventos que anteriormente teriam bloqueado ou derrubado o processo principal. Quando isso acontece, os campos ProcessId, ExitCode e ElapsedMilliseconds que os acompanham dão-lhe o suficiente para cruzar com uma entrada do Windows Error Reporting e decidir se um ficheiro de um cliente é patológico ou se alguém o está a testar

procedure LogCodecOutcome(const Info: THPDFCodecWorkerInfo);
begin
  case Info.Status of
    cwsSucceeded:
      ; // nada a reportar
    cwsUnavailable, cwsLaunchFailed:
      Alert('Codec worker not deployed: ' + Info.ErrorMessage);
    cwsTimedOut, cwsCrashed:
      Quarantine(Format('pid %d exit %d after %d ms',
        [Info.ProcessId, Info.ExitCode, Info.ElapsedMilliseconds]));
  else
    RejectDocument(Info.ErrorMessage);
  end;
end;

Os limites que realmente vinculam

Três tetos separados aplicam-se a cada descodificação isolada, e saber qual foi ativado poupa uma tarde inteira de suposições. CodecWorkerTimeoutMilliseconds assume por predefinição 10 000 e é validado dentro do intervalo de 1 a 600 000; um valor fora deste intervalo gera exceção em vez de ser cortado silenciosamente. CodecWorkerMemoryLimitBytes assume por predefinição 536 870 912 bytes e tem de ser zero, significando sem limite, ou pelo menos 67 108 864 bytes, porque um teto mais pequeno não consegue conter um conjunto de trabalho realista de um descodificador e faria falhar todos os documentos. O teto de memória é imposto por um Job Object do Windows com semântica kill-on-close, pelo que o worker morre juntamente com o job mesmo que o processo principal seja terminado abruptamente

O terceiro teto é o limite de saída, e é derivado em vez de configurado. O HotPDF calcula os bytes necessários a partir da região pedida, ou da geometria de imagem esperada, como largura vezes altura vezes três para saída de 24 bits, depois reduz esse valor até DecodeBudgetBytes quando um orçamento está definido. Um descodificador que reporta um cabeçalho plausível e depois tenta emitir muito mais pixéis do que a geometria permite é travado pelo próprio mapeamento, e o processo principal vê cwsOutputLimit. É por isso que a camada de isolamento e o orçamento de descodificação se complementam: o orçamento define quão grande uma imagem pode ser, e a fronteira de isolamento garante que uma mentira sobre essa dimensão não se possa transformar numa escrita fora de limites no seu processo

Onde isto se encaixa numa via de admissão reforçada

O isolamento de processos é a camada mais externa de uma cadeia de defesa que começa muito antes. Os limites estruturais rejeitam documentos implausíveis no momento da análise. Os orçamentos de filtros limitam a expansão. O isolamento contém o que sobrevive a ambos. Para documentos que chegam à camada de imagem, vale a pena saber qual codec está realmente a exercitar, uma vez que o tratamento de JPXDecode e os dicionários de símbolos JBIG2 têm perfis de falha muito diferentes, e o JBIG2 em particular transporta segmentos globais entre páginas que uma sandbox ingénua por imagem quebraria

O custo é honesto e vale a pena mencionar: lançar um processo por imagem isolada acrescenta milissegundos, e um documento com centenas de páginas digitalizadas vai notá-lo. Meça isso em função do que compra. Num conversor em lote que corre sem supervisão durante a noite, a perda de rendimento é invisível e a contenção de falhas é o objetivo em si. Num visualizador interativo que abre documentos em que o utilizador já confia, cimDisabled ou cimAutomatic é a predefinição razoável. O modo é uma simples propriedade, pelo que nada o impede de escolher por classe de documento em tempo de execução

O HotPDF distribui a camada de isolamento, os orçamentos de descodificação e os limites estruturais do analisador como um único componente VCL nativo para Delphi e C++Builder, sem qualquer runtime externo para implementar além do próprio executável do worker. A documentação completa da API e uma versão de avaliação estão disponíveis na página do componente HotPDF para Delphi