Teknisk artikkel

Isoler PDF-bildekodeker i worker-prosesser med HotPDF

HotPDF kan dekode de tre farligste PDF-bildefiltrene, DCTDecode, JPXDecode og JBIG2Decode, inne i en separat kortlevd worker-prosess i stedet for inne i applikasjonen din. Egenskapen som slår dette på, er CodecIsolationMode, og den praktiske effekten er at en feilformet JPEG 2000-kodestrøm som ville krasjet VCL-applikasjonen din, nå dreper en engangs underordnet prosess mens verten rapporterer en statuskode og fortsetter

Denne forskjellen betyr mest på de stedene PDF-er faktisk kommer fra: et opplastingsskjema, en e-postgateway, en skanneapparat, en partners FTP-mappe. Du kontrollerer ikke de bytene, og bildekodekene er der den historiske skaden bor

Hvorfor tar ett dårlig bilde ned hele applikasjonen?

Fordi en bildekodek er den ene delen av en PDF-leser som kjører en kompleks tilstandsmaskin over data kontrollert av en angriper, med nesten ingen strukturelle sjekker igjen å falle tilbake på. Når bytene når JPEG 2000- eller JBIG2-dekoderen, er kryssreferansetabellen allerede analysert, objektet er løst opp, filterkjeden er avviklet, og det som gjenstår, er en rå kodestrøm som forteller hvor mange fliser, hvor mange komponenter, hvor mange bit per sample. Et feil tall der er ikke en analysefeil. Det er en dårlig allokeringsstørrelse eller en indeks utenfor gyldig område inne i en stram dekodesløyfe

Budsjettgrenser hjelper, og du bør allerede ha dem. HotPDF begrenser ekspansjon med DecodeBudgetBytes og DocumentDecodeBudgetBytes, og begrenser filterkjeder med DecodeFilterLimit og DecodePipelineDepthLimit; resonnementet bak disse takene dekkes i begrenset dekoding for nøstede filtre og PDF-bomber. Men et bytebudsjett svarer bare på ett spørsmål: hvor mye utdata som er tillatt. Det kan ikke svare på hva som skjer når dekoderen feiler før den i det hele tatt produserer noe utdata. Et tilgangsbrudd inne i en dekodesløyfe er ikke et policybrudd du kan avvise; det er en hendelse på prosessnivå, og den eneste pålitelige inneslutningen for en hendelse på prosessnivå er en annen prosess

Hva HotPDF isolerer, og hva den ikke gjør

HotPDF isolerer nøyaktig tre kodektyper, oppgitt som hckDCT, hckJPX og hckJBIG2 i enheten HPDFCodecIsolation. Alt annet, Flate, LZW, RunLength, ASCII85, CCITT, blir værende i egen prosess, fordi disse dekoderne er enkle nok til å begrenses med budsjetter og ikke er der de interessante feilene kommer fra

Transporten er bevisst smal. Verten allokerer én begrenset delt minne-mapping, skriver en fast THPDFCodecSharedHeader pluss den komprimerte inndataen og eventuelle globale JBIG2-segmenter, starter worker-prosessen, og venter. Worker-prosessen skriver dekodede piksler tilbake inn i den samme mappingen og setter et statusord. Det finnes ingen pipe-protokoll som kan komme ut av synk, ingen serialiseringsformat å fuzze, og headeren bærer en magisk verdi og en versjon, slik at en feilmatchet worker-binærfil avvises i stedet for å bli mistolket

uses
  HPDFDoc, HPDFCodecIsolation;

var
  Pdf: THotPDF;
  Info: THPDFCodecWorkerInfo;
  Bmp: TBitmap;
begin
  Pdf := THotPDF.Create(nil);
  try
    // Fail closed: dekod aldri disse kodekene i egen prosess
    Pdf.CodecIsolationMode := cimRequired;
    Pdf.CodecWorkerExecutable := 'HotPDFCodecWorker.exe';
    Pdf.CodecWorkerTimeoutMilliseconds := 5000;       // 1..600000
    Pdf.CodecWorkerMemoryLimitBytes := 268435456;     // 0 or >= 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;

La CodecWorkerExecutable stå tom, så finner HotPDF worker-prosessen ved siden av din egen kjørbare fil, som HotPDFCodecWorker.exe i katalogen til ParamStr(0). Sett den eksplisitt når utrullingen din legger worker-prosessen et annet sted; verdien utvides gjennom ExpandFileName, så en relativ sti løses mot gjeldende katalog i stedet for applikasjonskatalogen, noe som sjelden er det du vil ha på en tjeneste

Automatisk eller påkrevd: hvilken feil foretrekker du?

De tre verdiene av THPDFCodecIsolationMode koder tre forskjellige svar på ett spørsmål: hva som skal skje når worker-prosessen ikke kan kjøre i det hele tatt. cimDisabled hopper over isolasjon helt og dekoder i egen prosess, atferden fra før 3.x. cimAutomatic, standardvalget, prøver worker-prosessen og faller stille tilbake til dekoding i egen prosess når den kjørbare filen mangler eller ikke vil starte, noe som rapporteres som status cwsUnavailable. cimRequired nekter denne tilbakefallen: en utilgjengelig worker markerer dekodingen som håndtert og mislykket, slik at ingen utiltrodd kodestrøm noensinne når adresserommet ditt

Velg ut fra trusselmodellen, ikke bekvemmelighet. En skrivebordsviser som åpner dokumenter brukeren allerede har på disk, klarer seg fint med cimAutomatic, der en manglende worker degraderer til den klassiske atferden i stedet for å ødelegge produktet. En innhentingstjeneste som analyserer filer fra internett, bør kjøre cimRequired, fordi en utrullingsfeil som stille fjerner isolasjonslaget, er nøyaktig den typen regresjon ingen legger merke til før det får konsekvenser. Merk asymmetrien: bare cwsUnavailable utløser tilbakefall. En worker som startet og deretter krasjet, fikk tidsavbrudd, eller traff en grense, er en dekodefeil i begge moduser, aldri et stille nytt forsøk i egen prosess

Å lese resultatet fra THPDFCodecWorkerStatus

GetLastCodecWorkerInfo returnerer utfallet av den siste isolerte dekodingen, og statusoppregningen er spesifikk nok til å styre reelle driftsbeslutninger fremfor en generisk loggmelding som «bilde feilet». Verdiene er cwsNotRun, cwsSucceeded, cwsUnavailable, cwsLaunchFailed, cwsTimedOut, cwsCrashed, cwsDecodeFailed, cwsProtocolError og cwsOutputLimit

Behandle dem som tre grupper. Utrullingsproblemer er cwsUnavailable og cwsLaunchFailed: noen sendte ut uten worker-prosessen, eller et antivirusprodukt blokkerer prosessopprettelse. Dokumentproblemer er cwsDecodeFailed og cwsOutputLimit: filen er feilformet eller større enn policyen din tillater, og å avvise den er riktig svar. Den interessante gruppen er cwsTimedOut og cwsCrashed, fordi det er hendelsene som tidligere ville hengt eller drept vertsprosessen. Når det skjer, gir feltene ProcessId, ExitCode og ElapsedMilliseconds deg nok til å korrelere med en oppføring i Windows Error Reporting og avgjøre om én kundefil er patologisk, eller om noen prøver seg på deg

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

Grensene som faktisk binder

Tre separate tak gjelder for hver isolerte dekoding, og å vite hvilket som utløste feilen, sparer deg for en ettermiddag med gjetting. CodecWorkerTimeoutMilliseconds er 10 000 som standard og valideres til området 1 til 600 000; en verdi utenfor det gir en feil i stedet for å klemmes stille inn. CodecWorkerMemoryLimitBytes er 536 870 912 byte som standard og må enten være null, som betyr ingen grense, eller minst 67 108 864 byte, fordi et mindre tak ikke kan romme et realistisk arbeidssett for en dekoder og ville feile på hvert eneste dokument. Minnetaket håndheves av et Windows Job Object med kill-on-close-semantikk, slik at worker-prosessen dør sammen med jobben selv om verten avsluttes brått

Det tredje taket er utdatagrensen, og den er utledet snarere enn konfigurert. HotPDF beregner de nødvendige bytene fra det forespurte området, eller fra den forventede bildegeometrien, som bredde ganger høyde ganger tre for 24-bits utdata, og klemmer deretter denne verdien ned til DecodeBudgetBytes når et budsjett er satt. En dekoder som rapporterer en plausibel header og deretter forsøker å sende ut langt flere piksler enn geometrien tillater, stoppes av selve mappingen, og verten ser cwsOutputLimit. Dette er grunnen til at isolasjonslaget og dekodebudsjettet utfyller hverandre: budsjettet definerer hvor stort et bilde får lov til å være, og isolasjonsgrensen sikrer at en løgn om den størrelsen ikke kan bli en skriving utenfor grensene i prosessen din

Hvor dette passer inn i en herdet mottaksvei

Prosessisolasjon er det ytterste laget i en forsvarskjede som starter mye tidligere. Strukturelle grenser avviser implausible dokumenter ved analysetidspunktet. Filterbudsjetter begrenser ekspansjon. Isolasjon inneholder det som overlever begge deler. For dokumenter som når bildelaget, er det verdt å vite hvilken kodek du faktisk bruker, siden håndtering av JPXDecode og JBIG2-symbolordbøker har svært forskjellige feilprofiler, og JBIG2 spesielt bærer globale segmenter på tvers av sider som en naiv sandkasse per bilde ville ødelagt

Kostnaden er ærlig og verdt å nevne: å starte en prosess per isolert bilde legger til millisekunder, og et dokument med hundrevis av skannede sider vil merke det. Mål det opp mot hva du får igjen. På en batchkonverterer som kjører uten tilsyn over natten, er gjennomstrømningstapet usynlig, og krasjinneslutningen er hele poenget. På en interaktiv viser som åpner dokumenter brukeren allerede stoler på, er cimDisabled eller cimAutomatic det rimelige standardvalget. Modusen er en vanlig egenskap, så ingenting hindrer deg i å velge per dokumentklasse under kjøring

HotPDF leverer isolasjonslaget, dekodebudsjettene og de strukturelle analysegrensene som én native VCL-komponent for Delphi og C++Builder, uten noen ekstern kjøretid å rulle ut utover selve worker-programfilen. Full API-dokumentasjon og en prøvebygg er tilgjengelig på HotPDF sin side for Delphi PDF-komponent