Con la verificación de overflow apagada, 2000000000 + 2000000000 evalúa a -294,967,296 cuando ambos operandos son valores Integer de Delphi. Ese número deja de ser trivia en el momento en que un parser de contenedor lee un offset y una longitud de un encabezado de record y los suma para ver si un PDF incrustado cabe dentro del buffer. La suma envuelve a negativo, la comparación contra Length(Data) pasa, y la línea siguiente toma la dirección de un byte aproximadamente dos gigabytes más allá del final del arreglo. Cargar un PDF desde el medio de un buffer mayor necesita una verificación de rango que nunca sume, y una respuesta clara sobre quién es dueño de los bytes una vez que el documento está abierto
Este artículo cubre documentos que ya están en RAM, sentados en un offset conocido dentro de otra cosa. Los documentos demasiado grandes para mantener en memoria son un problema distinto, manejado extrayendo bloques del disco bajo demanda como describe el streaming de PDFs grandes bajo demanda con PDFium VCL. La última sección traza la línea
Un PDF incrustado en un offset conocido
Un almacén de correo guarda cuerpos de mensajes y adjuntos en un solo record. Un contenedor de archivo concatena un manifiesto, unas imágenes y un PDF. Un protocolo de red propio enmarca un documento detrás de un encabezado con prefijo de longitud. Suponga que el record es de 40 MB, el PDF arranca en el byte 1,182,336 y corre por 312 KB. El código habitual llama a Copy(Data, Index, Count) para cortar una porción, y luego le entrega esa porción a un cargador que la copia otra vez a su propio buffer. Contando el contenedor, los mismos 312 KB existen ahora tres veces, y un escaneo de buzón repite eso por cada mensaje
PDFium Component direcciona la ventana directamente. TPdf declara LoadDocument(const Data: TBytes; Index, Count: Integer; Buffered: Boolean = True), donde Index es una posición de byte zero-based y Count es una longitud en bytes. El método valida ambos números, computa un solo puntero, y reenvía al overload de puntero LoadDocument(Data: Pointer; Size: NativeUInt; Buffered: Boolean), que abre el documento a través de FPDF_LoadMemDocument64. La forma de arreglo completo LoadDocument(const Data: TBytes; Buffered: Boolean) es una llamada de una línea a la forma de rango de bytes con 0 y Length(Data), así que cada carga de TBytes pasa por la misma validación
En código, la porción simplemente desaparece. LocateEmbeddedPdf representa su propio parser de contenedor
uses
System.SysUtils, System.IOUtils, PDFium;
procedure PreviewEmbedded(const RecordFile: string);
var
Frame: TBytes; // el record del contenedor completo, decenas de megabytes
Offset, Size: Integer;
Pdf: TPdf;
begin
Frame := TFile.ReadAllBytes(RecordFile);
LocateEmbeddedPdf(Frame, Offset, Size); // su parser, no parte del componente
Pdf := TPdf.Create(nil);
try
// No Copy(Frame, Offset, Size): la ventana se direcciona in place
Pdf.LoadDocument(Frame, Offset, Size); // Buffered vale True por defecto
Frame := nil; // está bien aquí: solo se copió la ventana del PDF
Writeln('Pages: ', Pdf.PageCount);
finally
Pdf.Free; // cierra el documento
end;
end;
Cuando el global EnableV8Engine sigue en False y la biblioteca PDFium todavía no se ha cargado, los cargadores de memoria escanean los bytes entrantes buscando marcadores XFA y encienden EnableV8Engine si encuentran alguno, de modo que se cargue la DLL con capacidad V8. Con la llamada de rango de bytes ese escaneo ve solo la ventana. Un manifiesto o un segundo documento en otra parte del mismo record no puede dispararlo
¿Por qué Index más Count desborda la verificación de límites?
Porque Index y Count son ambos valores Integer con signo de 32 bits, y sumar dos positivos grandes no produce confiablemente un positivo grande. Cuando el offset y la longitud vienen de un encabezado de archivo, un atacante o un record corrupto controla ambos. La misma trampa espera en su propio parser de contenedor, que computa esos números primero
// Un helper para su propio parser de contenedor (sugerido, no parte del componente)
function IsValidWindow(const Data: TBytes; Index, Count: Integer): Boolean;
begin
// MAL: Index + Count se evalúa como Integer y puede envolver a negativo
// Result := (Index >= 0) and (Index + Count <= Length(Data));
// BIEN: rechace los signos, acote Index por su cuenta, luego compare Count
// contra el resto; la única aritmética es una resta que no puede envolver
Result := (Index >= 0) and (Count >= 0) and
(Index <= Length(Data)) and
(Count <= Length(Data) - Index);
end;
Recorra el caso de fallo. Con Index = 2000000000 y Count = 2000000000, la suma verdadera es cuatro mil millones, pero la aritmética con signo de 32 bits la guarda como -294,967,296. Ese valor está cómodamente por debajo de cualquier Length(Data), así que la verificación incorrecta pasa y @Data[Index] apunta muy fuera del arreglo, con una longitud de dos gigabytes adjunta. Un crash es el buen desenlace. El malo es PDFium parseando la memoria del proceso que casualmente viva ahí
El orden correcto nunca suma. Los valores negativos se rechazan antes de indexar nada. Index se acota después contra Length(Data) solo, lo que garantiza que Length(Data) - Index es un Integer no negativo, y solo entonces se compara Count con ese resto. Delphi evalúa and con lógica de corto circuito por defecto ({$B-}), así que cada término posterior solo corre una vez que los anteriores se cumplen. No se apoye en {$Q+} como red de seguridad: los builds de release se envían rutinariamente con la verificación de overflow apagada, y con ella encendida solo ha cambiado un hueco de seguridad de memoria por un EIntOverflow escapando de en medio de una rutina de validación
El LoadDocument de rango de bytes corre las mismas cuatro pruebas en el mismo orden y levanta EPdfError con un mensaje que nombra la regla fallida: “PDF byte range index cannot be negative”, “PDF byte range count cannot be negative”, “PDF byte range index exceeds data length” y “PDF byte range exceeds data length”. Puede reproducir cada una con un arreglo de cuatro bytes. Index = 5, Count = 0 dispara el tercer mensaje, Index = 2, Count = 3 dispara el cuarto, y después de cada rechazo Pdf.Active sigue en False. El mismo tratamiento de longitudes no confiables a través del límite nativo se cubre más ampliamente en el endurecimiento del ABI y la seguridad de memoria de PDFium VCL en Delphi
Una ventana de longitud cero sigue siendo una llamada legal
Una ventana vacía pasa un puntero nil, porque @Data[Index] no es una expresión legal para cada Index que la validación acepta. Index = Length(Data) con Count = 0 es una ventana vacía bien formada al final del buffer, y un TBytes vacío da Index = 0 sobre un arreglo que no tiene elemento cero. Tomar la dirección en cualquiera de los dos casos lee más allá del final o desreferencia un arreglo dinámico nil, y con la verificación de rango encendida, Delphi levanta ERangeError antes de que el cargador siquiera corra
Así que el overload de rango de bytes se ramifica: Count = 0 produce nil, cualquier otro conteo produce @Data[Index]. El overload de puntero acepta nil solo junto con un tamaño de cero, PDFium entonces decline abrir un buffer vacío, y la llamada termina en un EPdfError ordinario que lleva el texto de último error de PDFium, o “Cannot load PDF document” cuando PDFium no reporta ninguno. Un encabezado que afirma un adjunto de cero bytes recibe la misma excepción atrapable que cualquier otra entrada mala. Rechazarlo en su parser, con el número de record en el mensaje, todavía se lee mejor en un log de soporte
Buffered decide quién es dueño de los bytes
Buffered es el único parámetro de aquí cuyas consecuencias sobreviven a la llamada. Con Buffered = True, el componente copia la ventana seleccionada, y solo la ventana, a su propio buffer antes de cargar. En el ejemplo de 40 MB se copian 312 KB y el contenedor se deja en paz. Una vez que LoadDocument devuelve, puede liberar, reutilizar o sobrescribir el contenedor inmediatamente
Con Buffered = False, @Data[Index] va directo a FPDF_LoadMemDocument64, y PDFium sigue leyendo a través de ese puntero mientras el documento esté abierto. Todo el TBytes de respaldo se vuelve prestado. Debe seguir vivo y sin modificar hasta que usted ponga Active := False o libere el TPdf. Importa el arreglo completo, no solo la ventana: un arreglo dinámico de Delphi se referencia cuenta como un solo bloque, así que soltar la última referencia en cualquier parte de su código libera la memoria que PDFium sigue leyendo. SetLength es igual de fatal. Una reasignación puede mover el bloque
Dos detalles de Delphi hacen esto más fácil de equivocar de lo que parece. Los arreglos dinámicos no tienen copy-on-write, a diferencia de los strings, así que Other := FFrame; Other[0] := 0; escribe en los mismos bytes que PDFium está parseando. Y un buffer de recepción que su código de red reutiliza para el siguiente frame se sobrescribe bajo el documento abierto. Pasar un resultado de función directo, como en Pdf.LoadDocument(ReadRecord(Id), Offset, Size, False), deja el arreglo sostenido solo por un temporal oculto que muere con la rutina llamadora, mientras el documento sigue vivo
Conserve Buffered = True para cualquier cosa que cruce un hilo, quede detrás de un buffer reutilizable, o sobreviva a la rutina que lo cargó. El costo extra es una copia del tamaño propio del PDF, y compra la libertad de soltar un record de 40 MB en el momento en que la llamada devuelve. Pedir prestado gana su lugar solo cuando el contenedor ya es un campo de larga vida que usted sostiene de todas formas por toda la sesión, como un visor que mantiene abierto un record de archivo mientras el usuario hojea el adjunto. En ese caso, haga la propiedad explícita en una sola clase
type
TFrameSession = class
private
FFrame: TBytes; // posee el almacenamiento de respaldo mientras FPdf tenga un documento abierto
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; // comparte el arreglo; nadie más puede escribir en él ahora
FPdf := TPdf.Create(nil);
end;
procedure TFrameSession.OpenEmbedded(Offset, Size: Integer);
begin
FPdf.Active := False; // cierre primero cualquier ventana anterior
FPdf.LoadDocument(FFrame, Offset, Size, False); // FFrame debe sobrevivir a este documento
end;
destructor TFrameSession.Destroy;
begin
FPdf.Active := False; // 1. libere el préstamo
FPdf.Free;
FFrame := nil; // 2. solo ahora puede irse el almacenamiento
inherited;
end;
El constructor comparte el arreglo del llamador en lugar de copiarlo, así que el llamador tiene que dejar de escribir en él. Si no puede prometer eso, pase Copy(AFrame) al constructor y está de vuelta a una copia, hecha una vez por sesión en lugar de una por carga
Verificar que la ventana arranca en %PDF-
Una ventana desplazada por unos bytes es el fallo sutil, porque el documento puede igual abrir. ISO 32000-1 §7.5.4 define cada entrada de cross-reference como un offset de byte desde el principio del archivo, y §7.5.2 pone el encabezado %PDF- al inicio. Cuando su parser cuenta mal un prefijo y la ventana arranca ocho bytes antes, esos offsets están ocho bytes desviados. PDFium busca el encabezado dentro de los primeros 1024 bytes y compensa la basura inicial; una ventana que arranca tarde o termina temprano puede empujarlo a reconstruir la tabla de cross-reference desde los marcadores de objeto que encuentra. En cualquier caso el conteo de páginas se ve correcto. El offset en su parser sigue equivocado, y el próximo lector de ese record puede ser menos indulgente
El overload LoadDocument(const Data: TBytes; Index, Count: Integer; const Options: TPdfLoadOptions; out Report: TPdfLoadReport; Buffered: Boolean = True) lo hace visible. Report.HeaderOffset es donde se encontró %PDF- dentro de los primeros 1024 bytes de la ventana, o -1 cuando no se encontró. Report.UsedRecovery es True cuando PDFium no aceptó la tabla de cross-reference como válida. Todos los offsets del reporte cuentan desde el primer byte de la ventana, no desde el contenedor. Un Count demasiado corto corta el %%EOF final, y TPdfLoadOptions.Default(plmStrict) enciende RequireFinalEndOfFileMarker, así que el modo estricto rechaza esa ventana de plano
var
Options: TPdfLoadOptions;
Report: TPdfLoadReport;
begin
Options := TPdfLoadOptions.Default; // plmCompatible
Pdf.LoadDocument(Frame, Offset, Size, Options, Report, True);
// LogWarning es su propia rutina de logging
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;
La auditoría detrás de esos campos tiene un costo que importa para las cargas prestadas. Cuando la ventana cabe dentro de Options.AuditByteLimit (256 MB por defecto), la auditoría lee una copia temporal de la ventana, así que Buffered = False combinado con un reporte ya no está libre de copias durante la carga. Poner AuditByteLimit en 0 se salta la auditoría y la copia, y deja HeaderOffset en -1. Corra la forma auditada mientras desarrolla el parser de contenedor y contra nuevas fuentes de records; pásese al overload simple una vez que los offsets son de confianza. El resto del reporte se cubre en atrapar fallos de carga de PDF con un reporte de carga de PDFium
Cuándo un rango de bytes es la herramienta equivocada
El overload de rango de bytes asume que todo el contenedor ya está en memoria, y como Count es un Integer, una ventana no puede exceder MaxInt bytes. Un archivo de 6 GB en disco, o un socket que no puede rebobinar, no encaja en esa forma, y leer todo a TBytes solo para direccionar una ventana dentro de él derrota el propósito. Para esos casos LoadCustomDocument(AStream: TStream; AOwnsStream: Boolean) deja que PDFium extraiga bloques de un stream seekable bajo demanda. Un pequeño descendiente de TStream propio que suma el offset de arranque del PDF a cada Seek presenta el documento incrustado como si empezara en la posición cero, como muestra el artículo de streaming bajo demanda
El otro desajuste es contenido que todavía no es un PDF plano. Si los bytes incrustados están comprimidos, cifrados por el contenedor, o envueltos en otra codificación, una copia transformada es inevitable, y cargar ese nuevo arreglo con el Buffered = True por defecto es la respuesta directa. El rango de bytes rinde en una sola forma: bytes PDF contiguos, sin modificar, ya residentes, en un offset que su parser validó sin sumar
La llamada de rango de bytes se sienta junto a la carga por archivo, stream, puntero crudo y bajo demanda en el mismo componente TPdf. La página del producto PDFium Component lista las versiones de Delphi y C++Builder soportadas, las opciones de licencia y el resto de la API de carga