Com a verificação de overflow desligada, 2000000000 + 2000000000 avalia para -294.967.296 quando ambos os operandos são valores Delphi Integer. Esse número deixa de ser curiosidade no momento em que um parser de contêiner lê um offset e um comprimento de um cabeçalho de record e os soma para ver se um PDF embutido cabe dentro do buffer. A soma dá a volta para negativo, a comparação contra Length(Data) passa, e a linha seguinte toma o endereço de um byte uns dois gigabytes além do fim do array. Carregar um PDF do meio de um buffer maior precisa de uma verificação de faixa que nunca soma, e de uma resposta clara sobre quem é dono dos bytes depois que o documento abre
Este artigo cobre documentos que já estão na RAM, parados em um offset conhecido dentro de outra coisa. Documentos grandes demais para caber em memória são outro problema, tratados puxando blocos do disco sob demanda como descrito em streaming de PDFs grandes sob demanda com PDFium VCL. A última seção traça a linha
Um PDF embutido em um offset conhecido
Um armazenamento de correio guarda corpos de mensagem e anexos em um único record. Um contêiner de arquivo concatena um manifesto, algumas imagens e um PDF. Um protocolo de fio personalizado enquadra um documento atrás de um cabeçalho prefixado por comprimento. Suponha que o record tenha 40 MB, o PDF comece no byte 1.182.336 e dure 312 KB. O código habitual chama Copy(Data, Index, Count) para cortar uma fatia, e entrega essa fatia a um loader que a copia de novo para o próprio buffer. Contando o contêiner, os mesmos 312 KB agora existem três vezes, e uma varredura de caixa de correio repete isso para cada mensagem
O PDFium Component endereça a janela diretamente. O TPdf declara LoadDocument(const Data: TBytes; Index, Count: Integer; Buffered: Boolean = True), em que Index é uma posição de byte baseada em zero e Count é um comprimento em bytes. O método valida os dois números, calcula um único ponteiro e encaminha para a sobrecarga de ponteiro LoadDocument(Data: Pointer; Size: NativeUInt; Buffered: Boolean), que abre o documento por meio de FPDF_LoadMemDocument64. A forma de array inteiro LoadDocument(const Data: TBytes; Buffered: Boolean) é uma chamada de uma linha à forma de intervalo de bytes com 0 e Length(Data), então todo carregamento de TBytes passa pela mesma validação
Em código, a fatia simplesmente desaparece. O LocateEmbeddedPdf representa o seu próprio parser de contêiner
uses
System.SysUtils, System.IOUtils, PDFium;
procedure PreviewEmbedded(const RecordFile: string);
var
Frame: TBytes; // record do contêiner inteiro, dezenas de megabytes
Offset, Size: Integer;
Pdf: TPdf;
begin
Frame := TFile.ReadAllBytes(RecordFile);
LocateEmbeddedPdf(Frame, Offset, Size); // seu parser, não faz parte do componente
Pdf := TPdf.Create(nil);
try
// Sem Copy(Frame, Offset, Size): a janela é endereçada no lugar
Pdf.LoadDocument(Frame, Offset, Size); // Buffered tem padrão True
Frame := nil; // tranquilo aqui: só a janela do PDF foi copiada
Writeln('Pages: ', Pdf.PageCount);
finally
Pdf.Free; // fecha o documento
end;
end;
Quando a global EnableV8Engine ainda é False e a biblioteca PDFium ainda não foi carregada, os loaders de memória vasculham os bytes de entrada em busca de marcadores XFA e ligam o EnableV8Engine se acharem algum, para que a DLL com capacidade V8 seja carregada. Com a chamada de intervalo de bytes, essa varredura vê apenas a janela. Um manifesto ou um segundo documento em outro lugar do mesmo record não pode dispará-la
Por que Index mais Count estoura a verificação de limites?
Porque Index e Count são ambos valores Integer assinados de 32 bits, e somar dois positivos grandes não produz de forma confiável um positivo grande. Quando o offset e o comprimento vêm de um cabeçalho de arquivo, um atacante ou um record corrompido controla os dois. A mesma pegadinha espera no seu próprio parser de contêiner, que calcula esses números primeiro
// Um helper para o seu próprio parser de contêiner (sugerido, não faz parte do componente)
function IsValidWindow(const Data: TBytes; Index, Count: Integer): Boolean;
begin
// ERRADO: Index + Count é avaliado como Integer e pode dar a volta para negativo
// Result := (Index >= 0) and (Index + Count <= Length(Data));
// CERTO: rejeite os sinais, delimite o Index sozinho, depois compare o Count
// contra o restante; a única aritmética é uma subtração que não pode dar a volta
Result := (Index >= 0) and (Count >= 0) and
(Index <= Length(Data)) and
(Count <= Length(Data) - Index);
end;
Passe o caso de falha pelos números. Com Index = 2000000000 e Count = 2000000000, a soma verdadeira é quatro bilhões, mas a aritmética assinada de 32 bits a guarda como -294.967.296. Esse valor está confortavelmente abaixo de qualquer Length(Data), então a verificação errada passa e o @Data[Index] aponta bem fora do array, com um comprimento de dois gigabytes anexado. Um crash é o desfecho bom. O ruim é o PDFium interpretando o que quer que a memória do processo esteja morando ali
A ordem correta nunca soma. Valores negativos são rejeitados antes de qualquer coisa ser indexada. O Index então é delimitado contra Length(Data) sozinho, o que garante que Length(Data) - Index é um Integer não negativo, e só então o Count é comparado com esse restante. O Delphi avalia and com lógica de curto-circuito por padrão ({$B-}), então cada termo posterior só roda depois que os anteriores se sustentam. Não se apoie no {$Q+} como rede de segurança: builds de release rotineiramente saem com a verificação de overflow desligada, e com ela ligada você apenas trocou um buraco de segurança de memória por um EIntOverflow escapando do meio de uma rotina de validação
O LoadDocument de intervalo de bytes roda os mesmos quatro testes na mesma ordem e levanta EPdfError com uma mensagem nomeando a regra que falhou: "PDF byte range index cannot be negative", "PDF byte range count cannot be negative", "PDF byte range index exceeds data length" e "PDF byte range exceeds data length". Você pode reproduzir cada uma com um array de quatro bytes. Index = 5, Count = 0 dispara a terceira mensagem, Index = 2, Count = 3 dispara a quarta, e depois de toda rejeição o Pdf.Active continua False. O mesmo tratamento de comprimentos não confiáveis através da fronteira nativa é coberto mais amplamente em endurecendo o ABI do PDFium VCL e segurança de memória em Delphi
Uma janela de comprimento zero ainda é uma chamada legal
Uma janela vazia passa um ponteiro nil, porque @Data[Index] não é uma expressão legal para todo Index que a validação aceita. Index = Length(Data) com Count = 0 é uma janela vazia perfeitamente bem formada no fim do buffer, e um TBytes vazio dá Index = 0 em um array que não tem elemento zero nenhum. Tomar o endereço em qualquer um dos casos lê além do fim ou dereferencia um array dinâmico nil, e com a verificação de faixa ligada, o Delphi levanta ERangeError antes de o loader nem rodar
Por isso a sobrecarga de intervalo de bytes se ramifica: Count = 0 produz nil, qualquer outra contagem produz @Data[Index]. A sobrecarga de ponteiro aceita nil somente junto com um tamanho zero, o PDFium então se recusa a abrir um buffer vazio, e a chamada termina em um EPdfError comum carregando o texto de último erro do PDFium, ou "Cannot load PDF document" quando o PDFium não reporta nenhum. Um cabeçalho que alega um anexo de zero bytes recebe a mesma exceção capturável que qualquer outra entrada ruim. Recusá-lo no seu parser, com o número do record na mensagem, ainda fica melhor em um log de suporte
O Buffered decide quem é dono dos bytes
O Buffered é o único parâmetro aqui cujas consequências sobrevivem à chamada. Com Buffered = True, o componente copia a janela selecionada, e só a janela, para o próprio buffer antes de carregar. No exemplo dos 40 MB, 312 KB são copiados e o contêiner fica em paz. Assim que o LoadDocument retorna, você pode liberar, reutilizar ou sobrescrever o contêiner imediatamente
Com Buffered = False, o @Data[Index] vai direto ao FPDF_LoadMemDocument64, e o PDFium continua lendo através desse ponteiro enquanto o documento estiver aberto. Todo o TBytes de suporte vira emprestado. Ele precisa continuar vivo e sem modificação até você definir Active := False ou liberar o TPdf. O array inteiro importa, não só a janela: um array dinâmico Delphi tem contagem de referência como um bloco único, então soltar a última referência em qualquer lugar do seu código libera a memória que o PDFium ainda está lendo. SetLength é igualmente fatal. Uma realocação pode mover o bloco
Dois detalhes do Delphi tornam isso mais fácil de errar do que parece. Arrays dinâmicos não têm copy-on-write, ao contrário de strings, então Other := FFrame; Other[0] := 0; escreve nos próprios bytes que o PDFium está interpretando. E um buffer de recepção que o seu código de rede reutiliza para o próximo frame é sobrescrito sob o documento aberto. Passar um resultado de função direto, como em Pdf.LoadDocument(ReadRecord(Id), Offset, Size, False), deixa o array guardado apenas por um temporário oculto que morre com a rotina chamadora, enquanto o documento continua vivo
Mantenha Buffered = True para qualquer coisa que cruze uma thread, fique atrás de um buffer reutilizável ou sobreviva à rotina que o carregou. O custo extra é uma cópia do tamanho do próprio PDF, e ela compra a liberdade de descartar um record de 40 MB no momento em que a chamada retorna. O empréstimo merece seu lugar apenas quando o contêiner já é um campo de vida longa que você segura pela sessão inteira de qualquer forma, como um viewer que mantém um record de arquivo aberto enquanto o usuário folheia o anexo. Nesse caso, torne a posse explícita em uma única classe
type
TFrameSession = class
private
FFrame: TBytes; // é dono do armazenamento de suporte enquanto o FPdf tiver um documento aberto
FPdf: TPdf;
public
constructor Create(const AFrame: TBytes);
destructor Destroy; override;
procedure OpenEmbedded(Offset, Size: Integer);
end;
constructor TFrameSession.Create(const AFrame: TBytes);
begin
inherited Create;
FFrame := AFrame; // compartilha o array; mais ninguém pode escrever nele agora
FPdf := TPdf.Create(nil);
end;
procedure TFrameSession.OpenEmbedded(Offset, Size: Integer);
begin
FPdf.Active := False; // feche qualquer janela anterior primeiro
FPdf.LoadDocument(FFrame, Offset, Size, False); // FFrame precisa sobreviver a este documento
end;
destructor TFrameSession.Destroy;
begin
FPdf.Active := False; // 1. libere o empréstimo
FPdf.Free;
FFrame := nil; // 2. só agora o armazenamento pode ir
inherited;
end;
O constructor compartilha o array do chamador em vez de copiá-lo, então o chamador precisa parar de escrever nele. Se você não pode prometer isso, passe Copy(AFrame) ao constructor e você volta a uma cópia, feita uma vez por sessão em vez de uma vez por carregamento
Verificando que a janela começa em %PDF-
Uma janela deslocada por alguns bytes é a falha sutil, porque o documento pode ainda abrir. A ISO 32000-1 §7.5.4 define toda entrada de cross-reference como um offset de byte a partir do início do arquivo, e a §7.5.2 põe o cabeçalho %PDF- no começo. Quando o seu parser erra a conta de um prefixo e a janela começa oito bytes antes, esses offsets ficam oito bytes errados. O PDFium procura o cabeçalho nos primeiros 1024 bytes e compensa lixo inicial; uma janela que começa tarde ou termina cedo pode empurrá-lo a reconstruir a tabela de cross-reference a partir dos marcadores de objeto que ele encontra. De qualquer forma a contagem de páginas parece certa. O offset no seu parser continua errado, e o próximo leitor desse record pode ser menos tolerante
A sobrecarga LoadDocument(const Data: TBytes; Index, Count: Integer; const Options: TPdfLoadOptions; out Report: TPdfLoadReport; Buffered: Boolean = True) torna isso visível. O Report.HeaderOffset é onde o %PDF- foi achado dentro dos primeiros 1024 bytes da janela, ou -1 quando não foi achado. O Report.UsedRecovery é True quando o PDFium não aceitou a tabela de cross-reference como válida. Todos os offsets do report contam a partir do primeiro byte da janela, não do contêiner. Um Count curto demais corta o %%EOF final, e TPdfLoadOptions.Default(plmStrict) liga o RequireFinalEndOfFileMarker, então o modo estrito rejeita essa janela de cara
var
Options: TPdfLoadOptions;
Report: TPdfLoadReport;
begin
Options := TPdfLoadOptions.Default; // plmCompatible
Pdf.LoadDocument(Frame, Offset, Size, Options, Report, True);
// LogWarning é a sua própria rotina de log
if Report.HeaderOffset > 0 then
LogWarning(Format('Window starts %d bytes before %%PDF-, check Offset',
[Report.HeaderOffset]))
else if Report.HeaderOffset < 0 then
LogWarning('No %PDF- header in the first 1024 bytes of the window');
if Report.UsedRecovery then
LogWarning('Cross-reference table was rebuilt, check Offset and Size');
end;
A auditoria por trás desses campos tem um custo que importa para carregamentos emprestados. Quando a janela cabe dentro de Options.AuditByteLimit (256 MB por padrão), a auditoria lê uma cópia temporária da janela, então Buffered = False combinado com um report deixa de ser livre de cópia durante o carregamento. Definir AuditByteLimit como 0 pula a auditoria e a cópia, e deixa o HeaderOffset em -1. Rode a forma auditada enquanto você desenvolve o parser de contêiner e contra novas origens de records; mude para a sobrecarga simples quando os offsets forem de confiança. O restante do report é coberto em capturando falhas de carregamento de PDF com um report do PDFium
Quando um intervalo de bytes é a ferramenta errada
A sobrecarga de intervalo de bytes assume que o contêiner inteiro já está em memória, e como Count é um Integer, uma única janela não pode exceder MaxInt bytes. Um arquivo de 6 GB em disco, ou um socket que você não pode rebobinar, não se encaixa nessa forma, e ler tudo para TBytes só para endereçar uma janela dentro dele derrota o propósito. Para esses casos, o LoadCustomDocument(AStream: TStream; AOwnsStream: Boolean) deixa o PDFium puxar blocos de um stream posicionável sob demanda. Um pequeno descendente de TStream seu que acrescenta o offset inicial do PDF a todo Seek apresenta o documento embutido como se começasse na posição zero, como o artigo de streaming sob demanda mostra
A outra incompatibilidade é conteúdo que ainda não é um PDF puro. Se os bytes embutidos estão comprimidos, criptografados pelo contêiner ou embrulhados em outra codificação, uma cópia transformada é inevitável, e carregar esse novo array com o Buffered = True padrão é a resposta direta. O intervalo de bytes compensa em uma única forma: bytes de PDF contíguos, não modificados, já residentes, em um offset que o seu parser validou sem somar
A chamada de intervalo de bytes fica ao lado dos carregamentos por arquivo, stream, ponteiro cru e sob demanda no mesmo componente TPdf. A página do produto PDFium Component lista as versões Delphi e C++Builder suportadas, opções de licenciamento e o restante da API de carregamento