O HotPDF pode decodificar os três filtros de imagem PDF mais arriscados, DCTDecode, JPXDecode e JBIG2Decode, dentro de um processo worker de curta duração separado, em vez de dentro da sua aplicação. A propriedade que ativa esse comportamento é CodecIsolationMode, e o efeito prático é que um codestream JPEG 2000 malformado que antes derrubaria sua aplicação VCL agora encerra um processo filho descartável, enquanto o host relata um código de status e continua
Essa diferença importa mais justamente nos lugares de onde os PDFs realmente chegam: um formulário de upload, um gateway de e-mail, um equipamento de digitalização, uma pasta de FTP de um parceiro. Você não controla esses bytes, e os codecs de imagem são onde historicamente residem os danos
Por que uma única imagem ruim 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 pelo atacante, quase sem verificações estruturais para servir de rede de proteção. No momento em que os bytes chegam ao decodificador JPEG 2000 ou JBIG2, a tabela de referência cruzada já foi analisada, o objeto já foi resolvido, a cadeia de filtros já foi desfeita, e o que resta é um codestream bruto que informa quantos tiles, quantos componentes, quantos bits por amostra. Um número errado ali não é um erro de parsing. É um tamanho de alocação inválido ou um índice fora do intervalo dentro de um loop de decodificação crítico
Limites de orçamento ajudam, e você 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 decodificação limitada para filtros aninhados e PDF bombs. Mas um orçamento de bytes responde a apenas uma pergunta, quanta saída é permitida. Ele não consegue responder o que acontece quando o decodificador falha antes de produzir qualquer saída. Uma violação de acesso dentro de um loop de decodificação não é uma violação de política que você possa simplesmente recusar; é um evento em nível de processo, e a única contenção confiável para um evento em nível de 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 unit HPDFCodecIsolation. Todo o resto, Flate, LZW, RunLength, ASCII85, CCITT, permanece no mesmo processo, porque esses decodificadores são simples o suficiente para serem limitados por orçamentos e não são de onde vêm as falhas interessantes
O transporte é deliberadamente estreito. O host aloca um mapeamento de memória compartilhada limitado, grava um THPDFCodecSharedHeader fixo mais a entrada comprimida e quaisquer segmentos globais de JBIG2, inicia o worker e aguarda. O worker grava os pixels decodificados de volta no mesmo mapeamento e define uma palavra de status. Não há protocolo de pipe para dessincronizar, nenhum formato de serialização para atacar por fuzzing, e o cabeçalho carrega um valor mágico e uma versão, de modo que um binário de worker incompatível é rejeitado em vez de interpretado incorretamente
uses
HPDFDoc, HPDFCodecIsolation;
var
Pdf: THotPDF;
Info: THPDFCodecWorkerInfo;
Bmp: TBitmap;
begin
Pdf := THotPDF.Create(nil);
try
// Fail closed: nunca decodifique esses codecs no mesmo 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 ao lado do seu próprio executável, como HotPDFCodecWorker.exe no diretório de ParamStr(0). Defina-o explicitamente quando sua implantação colocar o worker em outro lugar; o valor é expandido por meio de ExpandFileName, de modo que um caminho relativo é resolvido em relação ao diretório atual, e não ao diretório da aplicação, o que raramente é o que você deseja em um serviço
Automático ou obrigatório: qual falha você prefere?
Os três valores de THPDFCodecIsolationMode codificam três respostas diferentes para uma pergunta: o que deve acontecer quando o worker simplesmente não consegue ser executado. cimDisabled ignora completamente o isolamento e decodifica no mesmo processo, o comportamento anterior à versão 3.x. cimAutomatic, o padrão, tenta usar o worker e retorna silenciosamente à decodificação no mesmo processo quando o executável do worker está ausente ou não consegue iniciar, o que é relatado com o status cwsUnavailable. cimRequired recusa esse retorno: um worker indisponível marca a decodificação como tratada e falha, de modo que nenhum codestream não confiável chega ao seu espaço de endereçamento
Escolha com base no modelo de ameaças, não na conveniência. Um visualizador desktop que abre documentos que o usuário já tem em disco funciona bem com cimAutomatic, onde um worker ausente degrada para o comportamento clássico em vez de quebrar o produto. Um serviço de ingestão que processa arquivos vindos da internet deve usar cimRequired, porque um erro de implantação que silenciosamente derruba a camada de isolamento é exatamente o tipo de regressão que ninguém percebe até que seja tarde demais. Observe a assimetria: apenas cwsUnavailable aciona o fallback. Um worker que iniciou e depois travou, expirou ou atingiu um limite é uma falha de decodificação em ambos os modos, nunca uma nova tentativa silenciosa no mesmo processo
Lendo o veredito de THPDFCodecWorkerStatus
GetLastCodecWorkerInfo retorna o resultado da decodificação isolada mais recente, e a enumeração de status é específica o suficiente para orientar decisões operacionais reais, em vez de uma linha de log genérica do tipo "falha na imagem". Os valores são cwsNotRun, cwsSucceeded, cwsUnavailable, cwsLaunchFailed, cwsTimedOut, cwsCrashed, cwsDecodeFailed, cwsProtocolError e cwsOutputLimit
Trate-os como três grupos. Problemas de implantação são cwsUnavailable e cwsLaunchFailed: alguém fez o deploy sem o worker, ou um antivírus está bloqueando a criação de processos. Problemas de documento são cwsDecodeFailed e cwsOutputLimit: o arquivo está malformado ou é maior do que sua política permite, e rejeitá-lo é a resposta correta. O grupo interessante é cwsTimedOut e cwsCrashed, porque esses são os eventos que antes travariam ou matariam o processo host. Quando isso acontece, os campos ProcessId, ExitCode e ElapsedMilliseconds que acompanham o status dão informações suficientes para correlacionar com uma entrada do Windows Error Reporting e decidir se um arquivo de cliente é patológico ou se alguém está testando seus limites
procedure LogCodecOutcome(const Info: THPDFCodecWorkerInfo);
begin
case Info.Status of
cwsSucceeded:
; // nada a relatar
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 importam
Três limites distintos se aplicam a cada decodificação isolada, e saber qual deles disparou economiza uma tarde inteira de tentativa e erro. CodecWorkerTimeoutMilliseconds tem padrão de 10.000 e é validado dentro do intervalo de 1 a 600.000; um valor fora desse intervalo gera uma exceção em vez de ser limitado silenciosamente. CodecWorkerMemoryLimitBytes tem padrão de 536.870.912 bytes e precisa ser zero, significando sem limite, ou pelo menos 67.108.864 bytes, porque um limite menor não conseguiria conter um working set realista de decodificador e faria todo documento falhar. O limite de memória é aplicado por um Windows Job Object com semântica kill-on-close, de modo que o worker morre junto com o job mesmo que o host seja encerrado abruptamente
O terceiro limite é o de saída, e ele é derivado em vez de configurado. O HotPDF calcula os bytes necessários a partir da região solicitada, ou da geometria de imagem esperada, como largura vezes altura vezes três para saída de 24 bits, e então limita esse valor ao DecodeBudgetBytes quando um orçamento está definido. Um decodificador que relata um cabeçalho plausível e depois tenta emitir muito mais pixels do que a geometria permite é interrompido pelo próprio mapeamento, e o host vê cwsOutputLimit. É por isso que a camada de isolamento e o orçamento de decodificação se complementam: o orçamento define o quão grande uma imagem pode ser, e a fronteira de isolamento garante que uma mentira sobre esse tamanho não possa se transformar em uma escrita fora dos limites no seu processo
Onde isso se encaixa em um pipeline de entrada reforçado
O isolamento de processo é a camada mais externa de uma cadeia de defesa que começa bem antes. Limites estruturais rejeitam documentos implausíveis no momento da análise. Orçamentos de filtro 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 você está de fato exercitando, já 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 carrega segmentos globais entre páginas que um sandbox ingênuo por imagem quebraria
O custo é honesto e vale a pena declará-lo: iniciar um processo por imagem isolada adiciona milissegundos, e um documento com centenas de páginas digitalizadas vai sentir isso. Avalie isso em relação ao que você ganha em troca. Em um conversor em lote que roda sem supervisão durante a noite, a perda de throughput é invisível e a contenção de falhas é todo o objetivo. Em um visualizador interativo que abre documentos em que o usuário já confia, cimDisabled ou cimAutomatic é o padrão razoável. O modo é uma propriedade simples, então nada impede você de escolher por classe de documento em tempo de execução
O HotPDF entrega a camada de isolamento, os orçamentos de decodificação e os limites estruturais do parser como um único componente VCL nativo para Delphi e C++Builder, sem nenhum runtime externo para implantar além do próprio executável do worker. A documentação completa da API e uma versão de teste estão disponíveis na página do componente HotPDF Delphi para PDF