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 arriesgados, DCTDecode, JPXDecode y JBIG2Decode, dentro de un proceso worker aparte y de vida corta en lugar de dentro de su aplicación. La propiedad que activa esto es CodecIsolationMode, y el efecto práctico es que un flujo de código JPEG 2000 malformado que antes habría hecho caer su aplicación VCL ahora mata un proceso hijo desechable mientras el host informa un código de estado y continúa

Esa diferencia importa sobre todo en los sitios de donde realmente llegan los PDFs: un formulario de carga, una pasarela de correo, un dispositivo de escaneo, un depósito FTP de un socio. Usted no controla esos bytes, y los códecs de imagen son donde vive el histórico de daños

¿Por qué una imagen defectuosa tira abajo 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 el atacante con casi ninguna comprobación estructural en la que apoyarse. Cuando los bytes llegan al decodificador de JPEG 2000 o JBIG2, la tabla de referencias cruzadas ya se ha analizado, el objeto ya se ha resuelto, la cadena de filtros ya se ha desenrollado, y lo que queda es un flujo de código en bruto que dice cuántos mosaicos, 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 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 límites se trata en la decodificación acotada para filtros anidados y bombas PDF. Pero un presupuesto de bytes solo responde una pregunta, cuánta salida está permitida. No puede responder qué ocurre cuando el decodificador falla antes de producir ninguna salida. Una violación de acceso dentro de un bucle de decodificación no es una infracción de política que se pueda rechazar; es un evento a nivel de proceso, y la única contención fiable 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, se queda dentro del proceso, porque esos decodificadores son lo bastante simples como para acotarlos con presupuestos y no son donde ocurren los fallos interesantes

El transporte es deliberadamente estrecho. El host reserva una única asignación de memoria compartida y acotada, escribe una THPDFCodecSharedHeader fija más la entrada comprimida y cualquier segmento global de JBIG2, lanza el worker y espera. El worker escribe los píxeles decodificados de vuelta en la misma asignación y fija una palabra de estado. No hay ningún protocolo de tuberías que se pueda desincronizar, ningún formato de serialización que hacer fuzzing, y la cabecera lleva un valor mágico y una versión, así que un binario worker que no coincida se rechaza en lugar de leerse mal

uses
  HPDFDoc, HPDFCodecIsolation;

var
  Pdf: THotPDF;
  Info: THPDFCodecWorkerInfo;
  Bmp: TBitmap;
begin
  Pdf := THotPDF.Create(nil);
  try
    // Fallar cerrado: nunca decodificar estos códecs dentro del 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). Establézcalo de forma explícita cuando su despliegue coloque el worker en otro sitio; el valor se expande a través de ExpandFileName, así que una ruta relativa se resuelve contra el directorio actual y no contra el directorio de la aplicación, algo que rara vez es lo que se quiere en un servicio

Automático o requerido: ¿qué fallo prefiere?

Los tres valores de THPDFCodecIsolationMode codifican tres respuestas distintas a una pregunta, qué debería pasar cuando el worker no puede ejecutarse en absoluto. cimDisabled se salta el aislamiento por completo y decodifica dentro del proceso, el comportamiento previo a la versión 3.x. cimAutomatic, el valor por defecto, intenta usar el worker y vuelve en silencio a la decodificación dentro del proceso cuando el ejecutable del worker falta o no se puede lanzar, lo cual se informa como el estado cwsUnavailable. cimRequired rechaza ese respaldo: un worker no disponible marca la decodificación como gestionada y fallida, así que ningún flujo de código no confiable llega jamás a su espacio de direcciones

Elija según el modelo de amenaza, no según la comodidad. Un visor de escritorio que abre documentos que el usuario ya tiene en disco está bien con cimAutomatic, donde un worker ausente degrada al comportamiento clásico en lugar de romper el producto. Un servicio de ingesta que analiza ficheros de internet debería ejecutar cimRequired, porque un error de despliegue que retira silenciosamente la capa de aislamiento 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 falló, agotó el tiempo o alcanzó un límite es un fallo de decodificación en ambos modos, nunca un reintento silencioso dentro del 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 como para impulsar decisiones operativas reales en lugar de una línea de registro genérica de "fallo de imagen". 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 antivirus está bloqueando la creación de procesos. Los problemas de documento son cwsDecodeFailed y cwsOutputLimit: el fichero está malformado o es más grande de lo que permite su política, y rechazarlo es la respuesta correcta. El grupo interesante es cwsTimedOut y cwsCrashed, porque son los eventos que antes habrían colgado o matado el proceso host. Cuando eso ocurre, los campos ProcessId, ExitCode y ElapsedMilliseconds que lo acompañan le dan lo suficiente para correlacionarlo con una entrada de Windows Error Reporting y decidir si un fichero de un cliente es patológico o si alguien le 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 cuentan

Tres topes distintos se aplican a cada decodificación aislada, y saber cuál se activó ahorra una tarde de conjeturas. CodecWorkerTimeoutMilliseconds vale por defecto 10.000 y se valida dentro del rango de 1 a 600.000; un valor fuera de ese rango lanza una excepción en lugar de recortarse en silencio. CodecWorkerMemoryLimitBytes vale por defecto 536.870.912 bytes y debe ser cero, sin límite, o al menos 67.108.864 bytes, porque un tope menor no puede contener un conjunto de trabajo realista de un decodificador y haría fallar todos los documentos. El tope de memoria lo impone un Job Object de Windows con semántica de matar al cerrar, así que el worker muere con el job incluso si el host se termina de forma abrupta

El tercer tope es el límite de salida, y se deriva en lugar de configurarse. HotPDF calcula los bytes necesarios 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 fijado. Un decodificador que informa una cabecera plausible y luego intenta emitir muchos más píxeles de los que permite la geometría queda detenido por la propia asignación, y el host ve cwsOutputLimit. Por eso la capa de aislamiento y el presupuesto de decodificación son complementarios: el presupuesto define cuán grande puede ser una imagen, y el límite de aislamiento asegura que una mentira sobre ese tamaño no pueda convertirse en una escritura fuera de límites en 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 poco plausibles en el momento del análisis. Los presupuestos de filtro acotan la expansión. El aislamiento contiene lo que sobrevive a ambos. Para documentos que llegan a la capa de imagen, conviene saber qué códec se está ejercitando realmente, ya que el manejo de JPXDecode y los diccionarios de símbolos JBIG2 tienen perfiles de fallo muy distintos, y JBIG2 en particular lleva segmentos globales entre páginas que un sandbox ingenuo por imagen rompería

El coste es honesto y merece decirse: lanzar un proceso por cada imagen aislada añade milisegundos, y un documento con cientos de páginas escaneadas lo notará. Mídalo frente a lo que compra. En un convertidor por lotes que corre desatendido 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 por defecto razonable. El modo es una propiedad sencilla, así que nada impide elegir por clase de documento en tiempo de ejecución

HotPDF distribuye la capa de aislamiento, 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 tiempo de ejecución 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 de HotPDF para Delphi