Articolo tecnico

Isolamento codec immagine PDF in worker con HotPDF

HotPDF può decodificare i tre filtri immagine PDF più rischiosi, DCTDecode, JPXDecode e JBIG2Decode, dentro un processo worker separato e di breve durata anziché dentro la tua applicazione. La proprietà che attiva questo comportamento è CodecIsolationMode, e l'effetto pratico è che un codestream JPEG 2000 malformato che avrebbe mandato in crash la tua applicazione VCL ora termina un processo figlio usa e getta, mentre l'host riporta un codice di stato e prosegue

Questa differenza conta soprattutto nei punti da cui i PDF arrivano davvero: un modulo di upload, un mail gateway, uno scanner di rete, un drop FTP di un partner. Non controlli quei byte, ed è nei codec immagine che si concentrano storicamente i danni

Perché una singola immagine difettosa manda in crash l'intera applicazione?

Perché un codec immagine è la parte di un reader PDF che esegue una macchina a stati complessa su dati controllati da un attaccante, con quasi nessun controllo strutturale residuo a cui appoggiarsi. Quando i byte raggiungono il decoder JPEG 2000 o JBIG2, la cross-reference table è già stata analizzata, l'oggetto è già stato risolto, la catena di filtri è già stata srotolata, e ciò che resta è un codestream grezzo che dichiara quante tile, quanti componenti, quanti bit per campione. Un numero sbagliato lì non è un errore di parsing. È una dimensione di allocazione errata o un indice fuori intervallo dentro un ciclo di decodifica stretto

I limiti di budget aiutano, e dovresti già averli impostati. HotPDF limita l'espansione con DecodeBudgetBytes e DocumentDecodeBudgetBytes, e limita le catene di filtri con DecodeFilterLimit e DecodePipelineDepthLimit; il ragionamento dietro questi limiti è trattato in decodifica limitata per filtri annidati e PDF bomb. Ma un budget in byte risponde a una sola domanda, quanto output è permesso. Non può rispondere a cosa succede quando il decoder fallisce prima di produrre qualunque output. Una violazione di accesso dentro un ciclo di decodifica non è una violazione di policy che puoi semplicemente rifiutare; è un evento a livello di processo, e l'unico contenimento affidabile per un evento a livello di processo è un processo diverso

Cosa isola HotPDF, e cosa no

HotPDF isola esattamente tre tipi di codec, enumerati come hckDCT, hckJPX e hckJBIG2 nell'unit HPDFCodecIsolation. Tutto il resto, Flate, LZW, RunLength, ASCII85, CCITT, resta in-process, perché quei decoder sono abbastanza semplici da limitare con i budget e non sono dove si concentrano i fallimenti interessanti

Il trasporto è deliberatamente ristretto. L'host alloca un unico mapping di memoria condivisa limitato, scrive un THPDFCodecSharedHeader fisso più l'input compresso ed eventuali segmenti globali JBIG2, avvia il worker e attende. Il worker scrive i pixel decodificati di nuovo nello stesso mapping e imposta una parola di stato. Non esiste un protocollo a pipe che possa disallinearsi, nessun formato di serializzazione da sottoporre a fuzzing, e l'header porta un valore magico e una versione, così un binario worker non corrispondente viene rifiutato invece che interpretato male

uses
  HPDFDoc, HPDFCodecIsolation;

var
  Pdf: THotPDF;
  Info: THPDFCodecWorkerInfo;
  Bmp: TBitmap;
begin
  Pdf := THotPDF.Create(nil);
  try
    // Fail closed: non decodificare mai questi codec in-process
    Pdf.CodecIsolationMode := cimRequired;
    Pdf.CodecWorkerExecutable := 'HotPDFCodecWorker.exe';
    Pdf.CodecWorkerTimeoutMilliseconds := 5000;       // 1..600000
    Pdf.CodecWorkerMemoryLimitBytes := 268435456;     // 0 oppure >= 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;

Lascia CodecWorkerExecutable vuoto e HotPDF risolve il worker accanto al tuo eseguibile, come HotPDFCodecWorker.exe nella directory di ParamStr(0). Impostalo esplicitamente quando il tuo deployment colloca il worker altrove; il valore viene espanso tramite ExpandFileName, quindi un percorso relativo si risolve rispetto alla directory corrente anziché alla directory dell'applicazione, il che raramente è ciò che vuoi su un servizio

Automatico o obbligatorio: quale fallimento preferisci?

I tre valori di THPDFCodecIsolationMode codificano tre risposte diverse a una sola domanda, cosa deve succedere quando il worker non può proprio essere eseguito. cimDisabled salta del tutto l'isolamento e decodifica in-process, il comportamento precedente alla versione 3.x. cimAutomatic, il valore predefinito, prova il worker e torna silenziosamente alla decodifica in-process quando l'eseguibile worker manca o non parte, evento riportato come stato cwsUnavailable. cimRequired rifiuta questo fallback: un worker non disponibile segna la decodifica come gestita e fallita, così nessun codestream non attendibile raggiunge mai il tuo spazio degli indirizzi

Scegli in base al threat model, non alla comodità. Un viewer desktop che apre documenti già presenti sul disco dell'utente va bene con cimAutomatic, dove un worker mancante degrada al comportamento classico invece di rompere il prodotto. Un servizio di ingestion che analizza file provenienti da internet dovrebbe usare cimRequired, perché un errore di deployment che silenziosamente elimina il livello di isolamento è esattamente il tipo di regressione che nessuno nota finché non conta davvero. Nota l'asimmetria: solo cwsUnavailable attiva il fallback. Un worker che è partito e poi è andato in crash, è scaduto per timeout, o ha superato un limite è un fallimento di decodifica in entrambe le modalità, mai un nuovo tentativo silenzioso in-process

Leggere il verdetto da THPDFCodecWorkerStatus

GetLastCodecWorkerInfo restituisce l'esito dell'ultima decodifica isolata, e l'enumerazione di stato è abbastanza specifica da guidare decisioni operative reali invece di una generica riga di log «immagine non riuscita». I valori sono cwsNotRun, cwsSucceeded, cwsUnavailable, cwsLaunchFailed, cwsTimedOut, cwsCrashed, cwsDecodeFailed, cwsProtocolError e cwsOutputLimit

Trattali come tre gruppi. I problemi di deployment sono cwsUnavailable e cwsLaunchFailed: qualcuno ha distribuito senza il worker, oppure un antivirus sta bloccando la creazione del processo. I problemi del documento sono cwsDecodeFailed e cwsOutputLimit: il file è malformato o più grande di quanto la tua policy consenta, e rifiutarlo è la risposta corretta. Il gruppo interessante è cwsTimedOut e cwsCrashed, perché sono gli eventi che in precedenza avrebbero bloccato o ucciso il processo host. Quando succede, i campi ProcessId, ExitCode ed ElapsedMilliseconds che li accompagnano ti danno abbastanza per correlarli con una voce di Windows Error Reporting e decidere se il file di un cliente è patologico o se qualcuno ti sta sondando

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

I limiti che contano davvero

Tre limiti separati si applicano a ogni decodifica isolata, e sapere quale è scattato ti risparmia un pomeriggio di congetture. CodecWorkerTimeoutMilliseconds ha valore predefinito 10.000 ed è validato nell'intervallo da 1 a 600.000; un valore fuori da quell'intervallo genera un'eccezione invece di essere limitato silenziosamente. CodecWorkerMemoryLimitBytes ha valore predefinito 536.870.912 byte e deve essere zero, che significa nessun limite, oppure almeno 67.108.864 byte, perché un limite più piccolo non può contenere un working set realistico del decoder e farebbe fallire ogni documento. Il limite di memoria è imposto da un Windows Job Object con semantica kill-on-close, così il worker muore insieme al job anche se l'host viene terminato bruscamente

Il terzo limite è quello di output, ed è derivato anziché configurato. HotPDF calcola i byte necessari dalla regione richiesta, o dalla geometria immagine attesa, come larghezza per altezza per tre per l'output a 24 bit, poi limita quel valore a DecodeBudgetBytes quando è impostato un budget. Un decoder che riporta un header plausibile e poi tenta di emettere molti più pixel di quanti la geometria permetta viene fermato dal mapping stesso, e l'host vede cwsOutputLimit. Ecco perché il livello di isolamento e il budget di decodifica si completano a vicenda: il budget definisce quanto grande può essere un'immagine, e il confine di isolamento garantisce che una menzogna su quella dimensione non possa trasformarsi in una scrittura fuori limite nel tuo processo

Dove si colloca in un percorso di ingestion irrobustito

L'isolamento dei processi è il livello più esterno di una catena di difesa che inizia molto prima. I limiti strutturali rifiutano i documenti implausibili in fase di parsing. I budget dei filtri limitano l'espansione. L'isolamento contiene ciò che sopravvive a entrambi. Per i documenti che raggiungono il livello immagine, vale la pena sapere quale codec stai davvero esercitando, dato che la gestione di JPXDecode e i dizionari di simboli JBIG2 hanno profili di fallimento molto diversi, e JBIG2 in particolare porta segmenti globali cross-page che una sandbox ingenua per singola immagine romperebbe

Il costo è onesto e vale la pena dichiararlo: avviare un processo per ogni immagine isolata aggiunge millisecondi, e un documento con centinaia di pagine scansionate lo sentirà. Confrontalo con ciò che ottieni in cambio. Su un convertitore batch che gira senza supervisione durante la notte, la perdita di throughput è invisibile e il contenimento dei crash è tutto ciò che conta. Su un viewer interattivo che apre documenti di cui l'utente già si fida, cimDisabled o cimAutomatic è il valore predefinito ragionevole. La modalità è una semplice proprietà, quindi nulla ti impedisce di sceglierla per classe di documento a runtime

HotPDF distribuisce il livello di isolamento, i budget di decodifica e i limiti strutturali del parser come un unico componente VCL nativo per Delphi e C++Builder, senza alcun runtime esterno da distribuire oltre all'eseguibile worker stesso. La documentazione API completa e una build di prova sono disponibili sulla pagina del componente PDF Delphi HotPDF