Teknisk artikel

Isolér PDF-billedcodecs i worker-processer med HotPDF

HotPDF kan afkode de tre mest risikable PDF-billedfiltre, DCTDecode, JPXDecode og JBIG2Decode, inde i en separat kortlivet worker-proces i stedet for inde i din applikation. Egenskaben, der slår dette til, er CodecIsolationMode, og den praktiske effekt er, at en fejlformet JPEG 2000-kodestrøm, som ville have crashet din VCL-applikation, nu dræber en kasserbar børneproces, mens værten rapporterer en statuskode og fortsætter

Den forskel betyder mest netop der, hvor PDF'er faktisk ankommer fra: en uploadformular, en mailgateway, en scanningsenhed, en partners FTP-drop. Du kontrollerer ikke de bytes, og billedcodecs er der, hvor den historiske skade findes

Hvorfor kan ét dårligt billede tage hele applikationen ned?

Fordi en billedcodec er den ene del af en PDF-læser, der kører en kompleks tilstandsmaskine over angriberkontrollerede data med næsten ingen strukturelle tjek tilbage at falde tilbage på. Når bytes når JPEG 2000- eller JBIG2-afkoderen, er krydsreferencetabellen blevet parset, objektet er blevet løst, filterkæden er blevet udrullet, og det, der er tilbage, er en rå kodestrøm, der siger hvor mange fliser, hvor mange komponenter, hvor mange bit pr. sample. Et forkert tal der er ikke en parsefejl. Det er en dårlig allokeringsstørrelse eller et uden-for-området-indeks inde i en stram afkodningsløkke

Budgetgrænser hjælper, og du bør allerede have dem. HotPDF afgrænser ekspansion med DecodeBudgetBytes og DocumentDecodeBudgetBytes, og afgrænser filterkæder med DecodeFilterLimit og DecodePipelineDepthLimit; ræsonnementet bag de grænser dækkes i afgrænset afkodning for indlejrede filtre og PDF-bomber. Men et bytebudget besvarer kun ét spørgsmål, hvor meget output der er tilladt. Det kan ikke besvare, hvad der sker, når afkoderen fejler, før den producerer noget output overhovedet. En adgangskrænkelse inde i en afkodningsløkke er ikke en politikkrænkelse, du kan afvise; det er en proces-niveau-hændelse, og den eneste pålidelige indeslutning af en proces-niveau-hændelse er en anden proces

Hvad isolerer HotPDF, og hvad gør det ikke

HotPDF isolerer præcis tre codec-typer, opremset som hckDCT, hckJPX og hckJBIG2 i enheden HPDFCodecIsolation. Alt andet, Flate, LZW, RunLength, ASCII85, CCITT, forbliver i processen, fordi de afkodere er simple nok til at afgrænse med budgetter og ikke er der, hvor de interessante fejl kommer fra

Transporten er bevidst smal. Værten allokerer én afgrænset shared-memory-mapping, skriver et fast THPDFCodecSharedHeader plus det komprimerede input og eventuelle JBIG2-globale segmenter, starter workeren og venter. Workeren skriver afkodede pixels tilbage til den samme mapping og sætter et statusord. Der er ingen pipe-protokol, der kan komme ud af trit, ingen serialiseringsformat at fuzze, og headeren bærer en magisk værdi og en version, så et uoverensstemmende worker-binary afvises frem for at blive fejllæst

uses
  HPDFDoc, HPDFCodecIsolation;

var
  Pdf: THotPDF;
  Info: THPDFCodecWorkerInfo;
  Bmp: TBitmap;
begin
  Pdf := THotPDF.Create(nil);
  try
    // Fail closed: afkod aldrig disse codecs i processen
    Pdf.CodecIsolationMode := cimRequired;
    Pdf.CodecWorkerExecutable := 'HotPDFCodecWorker.exe';
    Pdf.CodecWorkerTimeoutMilliseconds := 5000;       // 1..600000
    Pdf.CodecWorkerMemoryLimitBytes := 268435456;     // 0 eller >= 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;

Lad CodecWorkerExecutable stå tom, og HotPDF finder workeren ved siden af din egen eksekverbare fil, som HotPDFCodecWorker.exe i mappen for ParamStr(0). Sæt den eksplicit, når din udrulning placerer workeren et andet sted; værdien udvides gennem ExpandFileName, så en relativ sti løses mod den aktuelle mappe frem for applikationsmappen, hvilket sjældent er det, du ønsker på en service

Automatisk eller påkrævet: hvilken fejl foretrækker du?

De tre værdier af THPDFCodecIsolationMode koder tre forskellige svar på ét spørgsmål, hvad der skal ske, når workeren slet ikke kan køre. cimDisabled springer isolering helt over og afkoder i processen, den før-3.x-adfærd. cimAutomatic, standarden, forsøger workeren og falder lydløst tilbage til afkodning i processen, når worker-eksekverbaren mangler eller ikke vil starte, hvilket rapporteres som status cwsUnavailable. cimRequired afviser det fallback: en utilgængelig worker markerer afkodningen som håndteret og mislykket, så ingen ubetroet kodestrøm nogensinde når din adresseplads

Vælg efter trusselsmodel, ikke bekvemmelighed. En desktopviewer, der åbner dokumenter, brugeren allerede har på disken, er fin på cimAutomatic, hvor en manglende worker degraderer til den klassiske adfærd i stedet for at ødelægge produktet. En indtagelsesservice, der parser filer fra internettet, bør køre cimRequired, fordi en udrulningsfejl, der lydløst dropper isoleringslaget, er præcis den slags regression, ingen bemærker, før det betyder noget. Bemærk asymmetrien: kun cwsUnavailable udløser fallback. En worker, der startede og derefter crashede, timeoutede eller ramte en grænse, er en afkodningsfejl i begge tilstande, aldrig et lydløst genforsøg i processen

Læs dommen fra THPDFCodecWorkerStatus

GetLastCodecWorkerInfo returnerer resultatet af den seneste isolerede afkodning, og statusopregningen er specifik nok til at drive reelle driftsbeslutninger frem for en generisk "billede fejlede"-loglinje. Værdierne er cwsNotRun, cwsSucceeded, cwsUnavailable, cwsLaunchFailed, cwsTimedOut, cwsCrashed, cwsDecodeFailed, cwsProtocolError og cwsOutputLimit

Behandl dem som tre grupper. Udrulningsproblemer er cwsUnavailable og cwsLaunchFailed: nogen sendte uden workeren, eller et antivirusprodukt blokerer procesoprettelse. Dokumentproblemer er cwsDecodeFailed og cwsOutputLimit: filen er fejlformet eller større end din politik tillader, og at afvise den er det korrekte svar. Den interessante gruppe er cwsTimedOut og cwsCrashed, fordi det er de hændelser, der tidligere ville have hængt eller dræbt værtsprocessen. Når det sker, giver de medfølgende felter ProcessId, ExitCode og ElapsedMilliseconds dig nok til at korrelere med en Windows Error Reporting-post og beslutte, om én kundefil er patologisk, eller om nogen sonderer dig

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

Grænserne, der faktisk binder

Tre separate lofter gælder for hver isoleret afkodning, og at vide hvilket der udløste sparer en eftermiddag med gætteri. CodecWorkerTimeoutMilliseconds er som standard 10.000 og valideres til intervallet 1 til 600.000; en værdi uden for det rejser en fejl frem for at klippe lydløst. CodecWorkerMemoryLimitBytes er som standard 536.870.912 byte og skal enten være nul, hvilket betyder ingen grænse, eller mindst 67.108.864 byte, fordi et mindre loft ikke kan rumme et realistisk afkoder-arbejdssæt og ville fejle på hvert dokument. Hukommelsesloftet håndhæves af et Windows Job Object med kill-on-close-semantik, så workeren dør sammen med jobbet, selv hvis værten afsluttes brat

Det tredje loft er outputgrænsen, og den er afledt frem for konfigureret. HotPDF beregner de nødvendige bytes ud fra det anmodede område, eller ud fra den forventede billedgeometri, som bredde gange højde gange tre for 24-bit-output, og klipper derefter den værdi ned til DecodeBudgetBytes, når et budget er sat. En afkoder, der rapporterer en plausibel header og derefter forsøger at udsende langt flere pixels, end geometrien tillader, stoppes af selve mappingen, og værten ser cwsOutputLimit. Det er grunden til, at isoleringslaget og afkodningsbudgettet supplerer hinanden: budgettet definerer, hvor stort et billede må være, og isoleringsgrænsen sikrer, at en løgn om den størrelse ikke kan blive til en uden-for-området-skrivning i din proces

Hvor dette passer ind i en hærdet indtagelsessti

Procesisolering er det yderste lag i en forsvarskæde, der starter meget tidligere. Strukturelle grænser afviser usandsynlige dokumenter ved parsetid. Filterbudgetter afgrænser ekspansion. Isolering indeslutter det, der overlever begge dele. For dokumenter, der når billedlaget, er det værd at vide, hvilken codec du faktisk kører, da JPXDecode-håndtering og JBIG2-symbolordbøger har meget forskellige fejlprofiler, og JBIG2 især bærer sideoverskridende globale segmenter, som en naiv per-billede-sandbox ville bryde

Prisen er ærlig og værd at nævne: at starte en proces pr. isoleret billede tilføjer millisekunder, og et dokument med hundredvis af scannede sider vil mærke det. Mål det op mod, hvad det køber dig. På en batchkonverter, der kører uovervåget om natten, er gennemløbstabet usynligt, og nedbrudsindeslutningen er hele pointen. På en interaktiv viewer, der åbner dokumenter, brugeren allerede stoler på, er cimDisabled eller cimAutomatic det fornuftige standardvalg. Tilstanden er en almindelig egenskab, så intet forhindrer dig i at vælge pr. dokumentklasse ved kørsel

HotPDF leverer isoleringslaget, afkodningsbudgetterne og de strukturelle parsergrænser som én native VCL-komponent til Delphi og C++Builder, uden nogen ekstern runtime at udrulle ud over selve worker-eksekverbaren. Fuld API-dokumentation og en trial-build er tilgængelige på HotPDF's produktside for Delphi PDF-komponenten