Technisch artikel

PDF-beeldcodecs isoleren in workerprocessen met HotPDF

HotPDF kan de drie riskantste PDF-beeldfilters, DCTDecode, JPXDecode en JBIG2Decode, decoderen in een apart kortlevend workerproces in plaats van in uw applicatie zelf. De eigenschap die dit inschakelt, is CodecIsolationMode, en het praktische effect is dat een misvormde JPEG 2000-codestream die vroeger uw VCL-applicatie liet crashen, nu een wegwerpbaar dochterproces doodt terwijl de host een statuscode rapporteert en doorgaat

Dat verschil doet er het meest toe op de plekken waar PDF's daadwerkelijk vandaan komen: een uploadformulier, een mailgateway, een scanapparaat, een partner-FTP-drop. U beheert die bytes niet, en de beeldcodecs zijn waar de historische schade zit

Waarom haalt één slechte afbeelding de hele applicatie onderuit?

Omdat een beeldcodec het ene onderdeel van een PDF-reader is dat een complexe toestandsmachine draait over door de aanvaller gecontroleerde gegevens, met bijna geen structurele controles meer om op terug te vallen. Tegen de tijd dat bytes de JPEG 2000- of JBIG2-decoder bereiken, is de cross-referentietabel geparseerd, is het object opgelost, is de filterketen afgewikkeld, en wat overblijft is een ruwe codestream die aangeeft hoeveel tegels, hoeveel componenten, hoeveel bits per sample. Een verkeerd getal daar is geen parseerfout. Het is een verkeerde allocatiegrootte of een index buiten bereik in een strakke decodeerlus

Budgetlimieten helpen, en u zou ze al moeten hebben. HotPDF begrenst expansie met DecodeBudgetBytes en DocumentDecodeBudgetBytes, en begrenst filterkettingen met DecodeFilterLimit en DecodePipelineDepthLimit; de redenering achter die limieten staat in begrensd decoderen voor geneste filters en PDF-bommen. Maar een bytebudget beantwoordt maar één vraag: hoeveel output is toegestaan. Het kan niet beantwoorden wat er gebeurt wanneer de decoder faalt vóórdat hij enige output produceert. Een schending van geheugentoegang binnen een decodeerlus is geen beleidsschending die u kunt weigeren; het is een gebeurtenis op procesniveau, en de enige betrouwbare beheersing voor een gebeurtenis op procesniveau is een ander proces

Wat HotPDF isoleert, en wat niet

HotPDF isoleert precies drie codecsoorten, opgesomd als hckDCT, hckJPX en hckJBIG2 in de unit HPDFCodecIsolation. Al het overige, Flate, LZW, RunLength, ASCII85, CCITT, blijft in-process, omdat die decoders eenvoudig genoeg zijn om met budgetten te begrenzen en niet de plek zijn waar de interessante storingen vandaan komen

Het transport is bewust smal gehouden. De host reserveert één begrensde shared-memory-mapping, schrijft een vaste THPDFCodecSharedHeader plus de gecomprimeerde input en eventuele JBIG2-globale segmenten, start de worker, en wacht. De worker schrijft gedecodeerde pixels terug in dezelfde mapping en zet een statuswoord. Er is geen pipe-protocol dat uit de pas kan lopen, geen serialisatieformaat om te fuzzen, en de header draagt een magische waarde en een versie zodat een niet-passende workerbinary geweigerd wordt in plaats van verkeerd gelezen

uses
  HPDFDoc, HPDFCodecIsolation;

var
  Pdf: THotPDF;
  Info: THPDFCodecWorkerInfo;
  Bmp: TBitmap;
begin
  Pdf := THotPDF.Create(nil);
  try
    // Fail closed: decodeer deze codecs nooit in-process
    Pdf.CodecIsolationMode := cimRequired;
    Pdf.CodecWorkerExecutable := 'HotPDFCodecWorker.exe';
    Pdf.CodecWorkerTimeoutMilliseconds := 5000;       // 1..600000
    Pdf.CodecWorkerMemoryLimitBytes := 268435456;     // 0 of >= 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;

Laat CodecWorkerExecutable leeg en HotPDF zoekt de worker naast uw eigen executable, als HotPDFCodecWorker.exe in de map van ParamStr(0). Stel het expliciet in wanneer uw deployment de worker ergens anders plaatst; de waarde wordt uitgebreid via ExpandFileName, dus een relatief pad wordt opgelost tegen de huidige map in plaats van de applicatiemap, wat op een service zelden is wat u wilt

Automatisch of verplicht: welke storing verkiest u?

De drie waarden van THPDFCodecIsolationMode coderen drie verschillende antwoorden op één vraag: wat er moet gebeuren wanneer de worker helemaal niet kan draaien. cimDisabled slaat isolatie volledig over en decodeert in-process, het gedrag van vóór 3.x. cimAutomatic, de standaard, probeert de worker en valt stilzwijgend terug op in-process decoderen wanneer de workerexecutable ontbreekt of niet start, wat gerapporteerd wordt als status cwsUnavailable. cimRequired weigert die terugval: een onbeschikbare worker markeert de decodering als afgehandeld en mislukt, zodat er nooit een niet-vertrouwde codestream uw adresruimte bereikt

Kies op basis van het dreigingsmodel, niet op basis van gemak. Een desktopviewer die documenten opent die de gebruiker al op schijf heeft, is prima met cimAutomatic, waar een ontbrekende worker terugvalt op het klassieke gedrag in plaats van het product te breken. Een intake-service die bestanden van het internet parseert, moet cimRequired draaien, want een implementatiefout die de isolatielaag stilletjes laat vallen, is precies het soort regressie dat niemand opmerkt totdat het ertoe doet. Merk de asymmetrie op: alleen cwsUnavailable triggert een terugval. Een worker die gestart is en vervolgens crashte, timede of een limiet raakte, is in beide modi een decodeerfout, nooit een stille herpoging in-process

De uitspraak lezen uit THPDFCodecWorkerStatus

GetLastCodecWorkerInfo geeft de uitkomst van de meest recente geïsoleerde decodering terug, en de statusopsomming is specifiek genoeg om echte operationele beslissingen te sturen in plaats van een generieke logregel "afbeelding mislukt". De waarden zijn cwsNotRun, cwsSucceeded, cwsUnavailable, cwsLaunchFailed, cwsTimedOut, cwsCrashed, cwsDecodeFailed, cwsProtocolError en cwsOutputLimit

Behandel ze als drie groepen. Deploymentproblemen zijn cwsUnavailable en cwsLaunchFailed: iemand heeft uitgeleverd zonder de worker, of een antivirusproduct blokkeert het aanmaken van processen. Documentproblemen zijn cwsDecodeFailed en cwsOutputLimit: het bestand is misvormd of groter dan uw beleid toestaat, en het afwijzen ervan is het juiste antwoord. De interessante groep is cwsTimedOut en cwsCrashed, want dat zijn de gebeurtenissen die eerder het hostproces hadden opgehangen of gedood. Wanneer dat gebeurt, geven de bijbehorende velden ProcessId, ExitCode en ElapsedMilliseconds u genoeg om te correleren met een Windows Error Reporting-item en te beslissen of één klantbestand pathologisch is of dat iemand u aan het testen is

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

De limieten die er werkelijk toe doen

Drie afzonderlijke plafonds gelden voor elke geïsoleerde decodering, en weten welke ervan geraakt is, bespaart een middag giswerk. CodecWorkerTimeoutMilliseconds staat standaard op 10.000 en wordt gevalideerd binnen het bereik 1 tot 600.000; een waarde daarbuiten geeft een fout in plaats van stilzwijgend af te knijpen. CodecWorkerMemoryLimitBytes staat standaard op 536.870.912 bytes en moet ofwel nul zijn, wat geen limiet betekent, ofwel minstens 67.108.864 bytes, omdat een kleiner plafond geen realistische werkset van een decoder kan bevatten en elk document zou laten mislukken. Het geheugenplafond wordt afgedwongen door een Windows Job Object met kill-on-close-semantiek, zodat de worker sterft met de job, zelfs als de host abrupt wordt beëindigd

Het derde plafond is de outputlimiet, en die is afgeleid in plaats van geconfigureerd. HotPDF berekent de vereiste bytes uit het gevraagde gebied, of uit de verwachte beeldgeometrie, als breedte maal hoogte maal drie voor 24-bit output, en klemt die waarde vervolgens vast op DecodeBudgetBytes wanneer er een budget is ingesteld. Een decoder die een aannemelijke header rapporteert en vervolgens probeert veel meer pixels te leveren dan de geometrie toestaat, wordt door de mapping zelf gestopt, en de host ziet cwsOutputLimit. Daarom vormen de isolatielaag en het decodeerbudget een aanvulling op elkaar: het budget bepaalt hoe groot een afbeelding mag zijn, en de isolatiegrens zorgt ervoor dat een leugen over die grootte nooit een schrijfoperatie buiten bereik in uw proces kan worden

Waar dit past in een gehard intakepad

Procesisolatie is de buitenste laag van een verdedigingsketen die veel eerder begint. Structurele limieten wijzen onaannemelijke documenten af tijdens het parseren. Filterbudgetten begrenzen expansie. Isolatie bevat wat beide overleeft. Voor documenten die de beeldlaag bereiken, is het de moeite waard te weten welke codec u daadwerkelijk aan het uitoefenen bent, want JPXDecode-afhandeling en JBIG2-symboolwoordenboeken heel verschillende faalprofielen hebben, en JBIG2 draagt in het bijzonder pagina-overschrijdende globale segmenten die een naïeve per-afbeelding-sandbox zou breken

De kosten zijn eerlijk en de moeite waard om te benoemen: het starten van een proces per geïsoleerde afbeelding voegt milliseconden toe, en een document met honderden gescande pagina's zal dat voelen. Weeg dat af tegen wat het oplevert. Op een batchconverter die onbewaakt 's nachts draait, is het doorvoerverlies onzichtbaar en is de crashbeheersing het hele punt. Op een interactieve viewer die documenten opent die de gebruiker al vertrouwt, is cimDisabled of cimAutomatic de redelijke standaard. De modus is een gewone eigenschap, dus niets weerhoudt u ervan om per documentklasse tijdens runtime te kiezen

HotPDF levert de isolatielaag, de decodeerbudgetten en de structurele parserlimieten als één native VCL-component voor Delphi en C++Builder, zonder externe runtime om uit te rollen naast de workerexecutable zelf. Volledige API-documentatie en een proefversie zijn beschikbaar op de HotPDF Delphi PDF-componentpagina