Artículo técnico

Aísle códecs de imagen PDF en procesos worker con HotPDF

HotPDF puede decodificar los tres filtros de imagen PDF más riesgosos, DCTDecode, JPXDecode y JBIG2Decode, dentro de un proceso worker independiente y de corta duración, en lugar de hacerlo dentro de su aplicación. La propiedad que activa esto es CodecIsolationMode, y el efecto práctico es que un codestream JPEG 2000 malformado que antes habría bloqueado su aplicación VCL ahora termina un proceso hijo desechable, mientras el host reporta un código de estado y continúa

Esa diferencia importa más en los lugares de donde realmente llegan los PDF: un formulario de carga, una pasarela de correo, un dispositivo de escaneo, un buzón FTP de un socio. Usted no controla esos bytes, y los códecs de imagen son donde ha vivido históricamente el daño

¿Por qué una sola imagen defectuosa derriba toda la aplicación?

Porque un códec de imagen es la única parte de un lector de PDF que ejecuta una máquina de estados compleja sobre datos controlados por un atacante, con casi ninguna verificación estructural a la que recurrir. Para cuando los bytes llegan al decodificador JPEG 2000 o JBIG2, la tabla de referencias cruzadas ya fue analizada, el objeto ya fue resuelto, la cadena de filtros ya fue desenrollada, y lo que queda es un codestream crudo que indica cuántos tiles, cuántos componentes, cuántos bits por muestra. Un número incorrecto ahí no es un error de análisis. Es un tamaño de asignación erróneo o un índice fuera de rango dentro de un bucle de decodificación ajustado

Los límites de presupuesto ayudan, y usted ya debería tenerlos. HotPDF acota la expansión con DecodeBudgetBytes y DocumentDecodeBudgetBytes, y acota las cadenas de filtros con DecodeFilterLimit y DecodePipelineDepthLimit; el razonamiento detrás de esos topes se explica en decodificación acotada para filtros anidados y bombas PDF. Pero un presupuesto de bytes responde solo una pregunta, cuánta salida está permitida. No puede responder qué ocurre cuando el decodificador falla antes de producir cualquier salida. Una violación de acceso dentro de un bucle de decodificación no es una infracción de política que usted pueda rechazar; es un evento a nivel de proceso, y la única contención confiable para un evento a nivel de proceso es un proceso distinto

Qué aísla HotPDF, y qué no

HotPDF aísla exactamente tres tipos de códec, enumerados como hckDCT, hckJPX y hckJBIG2 en la unidad HPDFCodecIsolation. Todo lo demás, Flate, LZW, RunLength, ASCII85, CCITT, permanece en el mismo proceso, porque esos decodificadores son suficientemente simples para acotarlos con presupuestos y no son de donde provienen las fallas interesantes

El transporte es deliberadamente angosto. El host asigna una única memoria compartida acotada, escribe un THPDFCodecSharedHeader fijo más la entrada comprimida y cualquier segmento global JBIG2, lanza el worker y espera. El worker escribe los píxeles decodificados de vuelta en la misma memoria y establece una palabra de estado. No hay protocolo de tuberías que pueda desincronizarse, ningún formato de serialización que fuzzear, y el encabezado lleva un valor mágico y una versión, de modo que un binario worker no coincidente se rechaza en lugar de leerse mal

uses
  HPDFDoc, HPDFCodecIsolation;

var
  Pdf: THotPDF;
  Info: THPDFCodecWorkerInfo;
  Bmp: TBitmap;
begin
  Pdf := THotPDF.Create(nil);
  try
    // Fallo cerrado: nunca decodificar estos códecs en el mismo proceso
    Pdf.CodecIsolationMode := cimRequired;
    Pdf.CodecWorkerExecutable := 'HotPDFCodecWorker.exe';
    Pdf.CodecWorkerTimeoutMilliseconds := 5000;       // 1..600000
    Pdf.CodecWorkerMemoryLimitBytes := 268435456;     // 0 o >= 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;

Deje CodecWorkerExecutable vacío y HotPDF resolverá el worker junto a su propio ejecutable, como HotPDFCodecWorker.exe en el directorio de ParamStr(0). Configúrelo explícitamente cuando su despliegue coloque el worker en otro lugar; el valor se expande mediante ExpandFileName, de modo que una ruta relativa se resuelve contra el directorio actual en lugar del directorio de la aplicación, algo que rara vez es lo que usted desea en un servicio

¿Automático o requerido: qué tipo de falla prefiere?

Los tres valores de THPDFCodecIsolationMode codifican tres respuestas distintas a una sola pregunta, qué debe suceder cuando el worker no puede ejecutarse en absoluto. cimDisabled omite la aislación por completo y decodifica en el mismo proceso, el comportamiento previo a la versión 3.x. cimAutomatic, el valor predeterminado, intenta usar el worker y recae silenciosamente en la decodificación dentro del mismo proceso cuando el ejecutable del worker falta o no puede iniciarse, lo cual se reporta con el estado cwsUnavailable. cimRequired rechaza ese respaldo: un worker no disponible marca la decodificación como manejada y fallida, de modo que ningún codestream no confiable llega jamás a su espacio de direcciones

Elija según el modelo de amenazas, no por conveniencia. Un visor de escritorio que abre documentos que el usuario ya tiene en disco está bien con cimAutomatic, donde un worker faltante degrada al comportamiento clásico en lugar de romper el producto. Un servicio de ingesta que analiza archivos provenientes de internet debería ejecutar cimRequired, porque un error de despliegue que silenciosamente elimina la capa de aislación es exactamente el tipo de regresión que nadie nota hasta que importa. Note la asimetría: solo cwsUnavailable activa el respaldo. Un worker que se lanzó y luego se bloqueó, expiró por tiempo, o alcanzó un límite es una falla de decodificación en ambos modos, nunca un reintento silencioso dentro del mismo proceso

Cómo leer el veredicto de THPDFCodecWorkerStatus

GetLastCodecWorkerInfo devuelve el resultado de la decodificación aislada más reciente, y la enumeración de estado es lo bastante específica para impulsar decisiones operativas reales, en lugar de una línea de registro genérica de "la imagen falló". Los valores son cwsNotRun, cwsSucceeded, cwsUnavailable, cwsLaunchFailed, cwsTimedOut, cwsCrashed, cwsDecodeFailed, cwsProtocolError y cwsOutputLimit

Trátelos como tres grupos. Los problemas de despliegue son cwsUnavailable y cwsLaunchFailed: alguien distribuyó sin el worker, o un producto antivirus está bloqueando la creación de procesos. Los problemas de documento son cwsDecodeFailed y cwsOutputLimit: el archivo está malformado o es más grande de lo que su política permite, y rechazarlo es la respuesta correcta. El grupo interesante es cwsTimedOut y cwsCrashed, porque esos son los eventos que antes habrían colgado o matado al proceso host. Cuando eso sucede, los campos ProcessId, ExitCode y ElapsedMilliseconds que los acompañan le dan lo suficiente para correlacionar con una entrada de Windows Error Reporting y decidir si un archivo de un cliente es patológico o alguien lo está sondeando

procedure LogCodecOutcome(const Info: THPDFCodecWorkerInfo);
begin
  case Info.Status of
    cwsSucceeded:
      ; // nada que 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;

Los límites que realmente aplican

Tres topes independientes se aplican a cada decodificación aislada, y saber cuál se activó le ahorra una tarde entera de conjeturas. CodecWorkerTimeoutMilliseconds tiene un valor predeterminado de 10,000 y se valida dentro del rango de 1 a 600,000; un valor fuera de ese rango genera una excepción en lugar de recortarse silenciosamente. CodecWorkerMemoryLimitBytes tiene un valor predeterminado de 536,870,912 bytes y debe ser cero, lo que significa sin límite, o al menos 67,108,864 bytes, porque un tope menor no podría contener un conjunto de trabajo realista del decodificador y fallaría en todo documento. El tope de memoria se aplica mediante un Windows Job Object con semántica kill-on-close, de modo que el worker muere junto con el job incluso si el host se termina abruptamente

El tercer tope es el límite de salida, y este se deriva en lugar de configurarse. HotPDF calcula los bytes requeridos a partir de la región solicitada, o de la geometría de imagen esperada, como ancho por alto por tres para salida de 24 bits, y luego recorta ese valor hasta DecodeBudgetBytes cuando hay un presupuesto configurado. Un decodificador que reporta un encabezado plausible y luego intenta emitir muchos más píxeles de los que la geometría permite es detenido por la propia memoria compartida, y el host ve cwsOutputLimit. Por esto la capa de aislación y el presupuesto de decodificación se complementan: el presupuesto define qué tan grande puede ser una imagen, y el límite de aislación garantiza que una mentira sobre ese tamaño no pueda convertirse en una escritura fuera de límites dentro de su proceso

Dónde encaja esto en una ruta de ingesta reforzada

El aislamiento de procesos es la capa más externa de una cadena de defensa que empieza mucho antes. Los límites estructurales rechazan documentos implausibles al momento del análisis. Los presupuestos de filtros acotan la expansión. La aislación contiene lo que sobrevive a ambos. Para documentos que llegan a la capa de imagen, vale la pena saber qué códec está usando realmente, ya que el manejo de JPXDecode y los diccionarios de símbolos JBIG2 tienen perfiles de falla muy distintos, y JBIG2 en particular lleva segmentos globales entre páginas que un sandbox ingenuo por imagen rompería

El costo es honesto y vale la pena decirlo: lanzar un proceso por cada imagen aislada agrega milisegundos, y un documento con cientos de páginas escaneadas lo notará. Compárelo con lo que obtiene a cambio. En un conversor por lotes que se ejecuta sin supervisión durante la noche, la pérdida de rendimiento es invisible y la contención de fallos es todo el propósito. En un visor interactivo que abre documentos en los que el usuario ya confía, cimDisabled o cimAutomatic es el valor predeterminado razonable. El modo es una propiedad simple, así que nada le impide elegir por clase de documento en tiempo de ejecución

HotPDF entrega la capa de aislación, los presupuestos de decodificación y los límites estructurales del analizador como un único componente VCL nativo para Delphi y C++Builder, sin más runtime externo que desplegar que el propio ejecutable del worker. La documentación completa de la API y una compilación de prueba están disponibles en la página del componente HotPDF Delphi PDF