Teknisk artikel

Isolera PDF-bildkodekar i arbetarprocesser med HotPDF

HotPDF kan avkoda de tre mest riskfyllda PDF-bildfiltren, DCTDecode, JPXDecode och JBIG2Decode, inuti en separat kortlivad arbetarprocess i stället för inuti din applikation. Egenskapen som slår på detta är CodecIsolationMode, och den praktiska effekten är att en felformad JPEG 2000-kodström som skulle ha kraschat din VCL-applikation nu i stället dödar en engångs barnprocess medan värden rapporterar en statuskod och fortsätter

Den skillnaden spelar störst roll på de ställen PDF:er faktiskt anländer ifrån: ett uppladdningsformulär, en e-postgateway, en skanningsenhet, en partners FTP-inkorg. Du styr inte de byten, och bildkodekarna är där den historiska skadan bor

Varför tar en enda dålig bild ner hela applikationen?

Därför att en bildkodek är den enda delen av en PDF-läsare som kör en komplex tillståndsmaskin över angriparkontrollerad data med nästan inga strukturella kontroller kvar att falla tillbaka på. När byten når JPEG 2000- eller JBIG2-avkodaren har korsreferenstabellen redan tolkats, objektet redan lösts, filterkedjan redan vecklats ut, och det som återstår är en rå kodström som anger hur många plattor, hur många komponenter, hur många bitar per sampel. Ett felaktigt tal där är inte ett tolkningsfel. Det är en dålig allokeringsstorlek eller ett index utanför intervallet inuti en tät avkodningsloop

Budgetgränser hjälper, och du bör redan ha dem. HotPDF begränsar expansion med DecodeBudgetBytes och DocumentDecodeBudgetBytes, och begränsar filterkedjor med DecodeFilterLimit och DecodePipelineDepthLimit; resonemanget bakom de taken beskrivs i begränsad avkodning för nästlade filter och PDF-bomber. Men en bytebudget besvarar bara en fråga: hur mycket utdata som tillåts. Den kan inte besvara vad som händer när avkodaren fallerar innan den ens producerar någon utdata alls. En åtkomstöverträdelse inuti en avkodningsloop är inte en policyöverträdelse du kan avvisa; det är en händelse på processnivå, och den enda pålitliga inneslutningen för en händelse på processnivå är en annan process

Vad HotPDF isolerar, och vad det inte gör

HotPDF isolerar exakt tre kodektyper, uppräknade som hckDCT, hckJPX och hckJBIG2 i enheten HPDFCodecIsolation. Allt annat, Flate, LZW, RunLength, ASCII85, CCITT, förblir i samma process, eftersom de avkodarna är enkla nog att begränsa med budgetar och inte är där de intressanta felen kommer ifrån

Transporten är medvetet smal. Värden allokerar en enda begränsad delad minnesmappning, skriver ett fast THPDFCodecSharedHeader plus den komprimerade indatan och eventuella globala JBIG2-segment, startar arbetaren, och väntar. Arbetaren skriver avkodade pixlar tillbaka till samma mappning och sätter ett statusord. Det finns inget pipe-protokoll som kan hamna ur synk, inget serialiseringsformat att fuzza, och huvudet bär ett magivärde och en version så att en icke-matchande arbetarbinär avvisas i stället för feltolkas

uses
  HPDFDoc, HPDFCodecIsolation;

var
  Pdf: THotPDF;
  Info: THPDFCodecWorkerInfo;
  Bmp: TBitmap;
begin
  Pdf := THotPDF.Create(nil);
  try
    // Fail-closed: avkoda aldrig de här kodekarna i samma process
    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;

Lämna CodecWorkerExecutable tom och HotPDF slår upp arbetaren bredvid din egen körbara fil, som HotPDFCodecWorker.exe i katalogen för ParamStr(0). Sätt den explicit när din driftsättning placerar arbetaren någon annanstans; värdet expanderas genom ExpandFileName, så en relativ sökväg löses mot den aktuella katalogen snarare än applikationskatalogen, vilket sällan är vad du vill ha på en tjänst

Automatiskt eller obligatoriskt: vilket fel föredrar du?

De tre värdena för THPDFCodecIsolationMode kodar tre olika svar på en fråga: vad som ska hända när arbetaren inte kan köras alls. cimDisabled hoppar över isolering helt och avkodar i samma process, beteendet före version 3.x. cimAutomatic, standardvärdet, försöker med arbetaren och faller tyst tillbaka till avkodning i samma process när den körbara arbetarfilen saknas eller inte kan startas, vilket rapporteras som status cwsUnavailable. cimRequired vägrar den fallbacken: en otillgänglig arbetare markerar avkodningen som hanterad och misslyckad, så ingen opålitlig kodström når någonsin ditt adressutrymme

Välj utifrån hotmodell, inte bekvämlighet. En skrivbordsvisare som öppnar dokument användaren redan har på disk fungerar bra med cimAutomatic, där en saknad arbetare degraderar till det klassiska beteendet i stället för att förstöra produkten. En intagstjänst som tolkar filer från internet bör köra cimRequired, eftersom ett driftsättningsmisstag som tyst tar bort isoleringslagret är exakt den typ av regression ingen märker förrän det spelar roll. Notera asymmetrin: bara cwsUnavailable utlöser fallback. En arbetare som startade och sedan kraschade, fick timeout, eller nådde en gräns är ett avkodningsfel i båda lägena, aldrig ett tyst nytt försök i samma process

Att läsa utslaget från THPDFCodecWorkerStatus

GetLastCodecWorkerInfo returnerar utfallet av den senaste isolerade avkodningen, och statusuppräkningen är specifik nog för att driva verkliga driftsbeslut i stället för en generisk ”bilden misslyckades”-loggrad. Värdena är cwsNotRun, cwsSucceeded, cwsUnavailable, cwsLaunchFailed, cwsTimedOut, cwsCrashed, cwsDecodeFailed, cwsProtocolError och cwsOutputLimit

Behandla dem som tre grupper. Driftsättningsproblem är cwsUnavailable och cwsLaunchFailed: någon levererade utan arbetaren, eller en antivirusprodukt blockerar processkapande. Dokumentproblem är cwsDecodeFailed och cwsOutputLimit: filen är felformad eller större än din policy tillåter, och att avvisa den är rätt svar. Den intressanta gruppen är cwsTimedOut och cwsCrashed, eftersom de är händelserna som tidigare skulle ha hängt eller dödat värdprocessen. När det händer ger de medföljande fälten ProcessId, ExitCode och ElapsedMilliseconds dig tillräckligt för att korrelera mot en post i Windows felrapportering och avgöra om en enda kundfil är patologisk eller om någon utforskar dig

procedure LogCodecOutcome(const Info: THPDFCodecWorkerInfo);
begin
  case Info.Status of
    cwsSucceeded:
      ; // inget att rapportera
    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änserna som faktiskt binder

Tre separata tak gäller för varje isolerad avkodning, och att veta vilket som utlöstes sparar en eftermiddag av gissande. CodecWorkerTimeoutMilliseconds har standardvärdet 10 000 och valideras till intervallet 1 till 600 000; ett värde utanför det ger ett fel i stället för att tyst klämmas till gränsen. CodecWorkerMemoryLimitBytes har standardvärdet 536 870 912 byte och måste antingen vara noll, vilket betyder ingen gräns, eller minst 67 108 864 byte, eftersom ett mindre tak inte kan rymma en realistisk avkodararbetsmängd och skulle misslyckas för varje dokument. Minnestaket upprätthålls av ett Windows Job Object med kill-on-close-semantik, så arbetaren dör med jobbet även om värden avslutas abrupt

Det tredje taket är utdatagränsen, och den härleds snarare än konfigureras. HotPDF beräknar de nödvändiga byten från den begärda regionen, eller från den förväntade bildgeometrin, som bredd gånger höjd gånger tre för 24-bitars utdata, och klämmer sedan det värdet ner till DecodeBudgetBytes när en budget är satt. En avkodare som rapporterar ett rimligt huvud och sedan försöker sända ut betydligt fler pixlar än geometrin tillåter stoppas av mappningen själv, och värden ser cwsOutputLimit. Det är därför isoleringslagret och avkodningsbudgeten kompletterar varandra: budgeten definierar hur stor en bild får vara, och isoleringsgränsen säkerställer att en lögn om den storleken inte kan bli en skrivning utanför gränserna i din process

Var detta passar in i en härdad intagsväg

Processisolering är det yttersta lagret i en försvarskedja som börjar mycket tidigare. Strukturella gränser avvisar orimliga dokument redan vid tolkningstillfället. Filterbudgetar begränsar expansion. Isolering innesluter det som överlever båda. För dokument som når bildlagret är det värt att veta vilken kodek du faktiskt använder, eftersom hantering av JPXDecode och JBIG2-symbolordböcker har mycket olika felprofiler, och JBIG2 i synnerhet bär sidöverskridande globala segment som en naiv per-bild-sandlåda skulle förstöra

Kostnaden är ärlig och värd att nämna: att starta en process per isolerad bild lägger till millisekunder, och ett dokument med hundratals skannade sidor kommer att märka det. Väg det mot vad det ger. På en batch-konverterare som körs obevakad över natten är genomströmningsförlusten osynlig och kraschinneslutningen är hela poängen. På en interaktiv visare som öppnar dokument användaren redan litar på är cimDisabled eller cimAutomatic det rimliga standardvalet. Läget är en enkel egenskap, så inget hindrar dig från att välja per dokumentklass vid körning

HotPDF levererar isoleringslagret, avkodningsbudgetarna och de strukturella tolkningsgränserna som en enda nativ VCL-komponent för Delphi och C++Builder, utan någon extern runtime att driftsätta utöver själva arbetarens körbara fil. Fullständig API-dokumentation och en testversion finns på sidan för HotPDF Delphi PDF-komponent