Technischer Artikel

HotPDF: PDF-Bild-Codecs in Worker-Prozessen isolieren

HotPDF kann die drei riskantesten PDF-Bildfilter, DCTDecode, JPXDecode und JBIG2Decode, in einem separaten, kurzlebigen Worker-Prozess statt in der eigenen Anwendung dekodieren. Die Eigenschaft, die das aktiviert, ist CodecIsolationMode, und der praktische Effekt ist, dass ein fehlerhafter JPEG-2000-Codestream, der früher die VCL-Anwendung zum Absturz gebracht hätte, nun einen entbehrlichen Kindprozess tötet, während der Host einen Statuscode meldet und weitermacht

Dieser Unterschied zählt am meisten dort, wo PDFs tatsächlich herkommen: ein Upload-Formular, ein Mail-Gateway, ein Scanner-Gerät, eine Partner-FTP-Ablage. Diese Bytes kontrolliert man nicht, und die Bild-Codecs sind der Ort, an dem der historische Schaden steckt

Warum reißt ein einziges schlechtes Bild die ganze Anwendung mit?

Weil ein Bild-Codec der eine Teil eines PDF-Readers ist, der eine komplexe Zustandsmaschine über von Angreifern kontrollierte Daten laufen lässt, fast ohne strukturelle Prüfungen zum Zurückfallen. Bis Bytes den JPEG-2000- oder JBIG2-Decoder erreichen, wurde die Querverweistabelle geparst, das Objekt aufgelöst, die Filterkette abgewickelt, und was übrig bleibt, ist ein roher Codestream, der angibt, wie viele Kacheln, wie viele Komponenten, wie viele Bits pro Sample. Eine falsche Zahl dort ist kein Parse-Fehler. Es ist eine schlechte Allokationsgröße oder ein Index außerhalb des gültigen Bereichs in einer engen Dekodierungsschleife

Budgetgrenzen helfen, und man sollte sie ohnehin schon haben. HotPDF begrenzt die Expansion mit DecodeBudgetBytes und DocumentDecodeBudgetBytes und begrenzt Filterketten mit DecodeFilterLimit und DecodePipelineDepthLimit; die Begründung für diese Grenzen wird in begrenzter Dekodierung bei verschachtelten Filtern und PDF-Bomben behandelt. Aber ein Byte-Budget beantwortet nur eine Frage, wie viel Ausgabe erlaubt ist. Es kann nicht beantworten, was passiert, wenn der Decoder ausfällt, bevor er überhaupt Ausgabe erzeugt. Eine Zugriffsverletzung in einer Dekodierungsschleife ist keine Richtlinienverletzung, die man ablehnen kann; es ist ein Ereignis auf Prozessebene, und die einzige zuverlässige Eindämmung für ein Ereignis auf Prozessebene ist ein anderer Prozess

Was HotPDF isoliert, und was nicht

HotPDF isoliert genau drei Codec-Arten, aufgeführt als hckDCT, hckJPX und hckJBIG2 in der Unit HPDFCodecIsolation. Alles andere, Flate, LZW, RunLength, ASCII85, CCITT, bleibt im selben Prozess, weil diese Decoder einfach genug sind, um sie mit Budgets zu begrenzen, und nicht der Ort sind, von dem die interessanten Ausfälle kommen

Der Transport ist bewusst schmal gehalten. Der Host reserviert genau eine begrenzte Shared-Memory-Zuordnung, schreibt einen festen THPDFCodecSharedHeader plus die komprimierte Eingabe und etwaige globale JBIG2-Segmente, startet den Worker und wartet. Der Worker schreibt dekodierte Pixel zurück in dieselbe Zuordnung und setzt ein Statuswort. Es gibt kein Pipe-Protokoll, das aus dem Takt geraten könnte, kein Serialisierungsformat zum Fuzzen, und der Header trägt einen magischen Wert und eine Version, sodass eine nicht passende Worker-Binärdatei abgelehnt statt falsch gelesen wird

uses
  HPDFDoc, HPDFCodecIsolation;

var
  Pdf: THotPDF;
  Info: THPDFCodecWorkerInfo;
  Bmp: TBitmap;
begin
  Pdf := THotPDF.Create(nil);
  try
    // Fail closed: diese Codecs nie im eigenen Prozess dekodieren
    Pdf.CodecIsolationMode := cimRequired;
    Pdf.CodecWorkerExecutable := 'HotPDFCodecWorker.exe';
    Pdf.CodecWorkerTimeoutMilliseconds := 5000;       // 1..600000
    Pdf.CodecWorkerMemoryLimitBytes := 268435456;     // 0 oder >= 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ässt man CodecWorkerExecutable leer, löst HotPDF den Worker neben der eigenen ausführbaren Datei auf, als HotPDFCodecWorker.exe im Verzeichnis von ParamStr(0). Setzen Sie ihn explizit, wenn Ihr Deployment den Worker woanders ablegt; der Wert wird über ExpandFileName expandiert, sodass ein relativer Pfad gegen das aktuelle Verzeichnis aufgelöst wird statt gegen das Anwendungsverzeichnis, was bei einem Dienst selten das Gewünschte ist

Automatisch oder erforderlich: welchen Fehlerfall bevorzugen Sie?

Die drei Werte von THPDFCodecIsolationMode kodieren drei verschiedene Antworten auf eine Frage, was passieren soll, wenn der Worker überhaupt nicht laufen kann. cimDisabled überspringt Isolation vollständig und dekodiert im eigenen Prozess, das Verhalten vor 3.x. cimAutomatic, die Vorgabe, versucht den Worker und fällt still auf Dekodierung im eigenen Prozess zurück, wenn die ausführbare Worker-Datei fehlt oder nicht startet, was als Status cwsUnavailable gemeldet wird. cimRequired verweigert diesen Rückfall: Ein nicht verfügbarer Worker markiert die Dekodierung als behandelt und fehlgeschlagen, sodass kein nicht vertrauenswürdiger Codestream jemals den eigenen Adressraum erreicht

Wählen Sie nach Bedrohungsmodell, nicht nach Bequemlichkeit. Ein Desktop-Viewer, der Dokumente öffnet, die der Nutzer bereits auf der Platte hat, ist mit cimAutomatic gut bedient, wo ein fehlender Worker auf das klassische Verhalten zurückfällt, statt das Produkt zu zerstören. Ein Aufnahmedienst, der Dateien aus dem Internet parst, sollte mit cimRequired laufen, weil ein Deployment-Fehler, der die Isolationsschicht still fallen lässt, genau die Art von Regression ist, die niemand bemerkt, bis es zählt. Beachten Sie die Asymmetrie: Nur cwsUnavailable löst den Rückfall aus. Ein Worker, der gestartet ist und dann abgestürzt ist, ein Timeout hatte oder ein Limit erreicht hat, ist in beiden Modi ein Dekodierungsfehler, nie ein stiller Wiederholungsversuch im eigenen Prozess

Das Urteil aus THPDFCodecWorkerStatus lesen

GetLastCodecWorkerInfo liefert das Ergebnis der letzten isolierten Dekodierung, und die Statusenumeration ist spezifisch genug, um echte operative Entscheidungen zu treffen, statt einer generischen Logzeile "Bild fehlgeschlagen". Die Werte sind cwsNotRun, cwsSucceeded, cwsUnavailable, cwsLaunchFailed, cwsTimedOut, cwsCrashed, cwsDecodeFailed, cwsProtocolError und cwsOutputLimit

Behandeln Sie sie als drei Gruppen. Deployment-Probleme sind cwsUnavailable und cwsLaunchFailed: Jemand hat ohne den Worker ausgeliefert, oder ein Antivirus-Produkt blockiert die Prozesserstellung. Dokumentprobleme sind cwsDecodeFailed und cwsOutputLimit: Die Datei ist fehlerhaft oder größer, als die Richtlinie erlaubt, und sie abzulehnen ist die richtige Antwort. Die interessante Gruppe ist cwsTimedOut und cwsCrashed, weil das genau die Ereignisse sind, die früher den Host-Prozess hätten hängen lassen oder abstürzen lassen können. Wenn das passiert, geben die begleitenden Felder ProcessId, ExitCode und ElapsedMilliseconds genug her, um mit einem Windows-Error-Reporting-Eintrag zu korrelieren und zu entscheiden, ob eine Kundendatei pathologisch ist oder jemand Sie testet

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

Die Grenzen, die tatsächlich greifen

Drei separate Obergrenzen gelten für jede isolierte Dekodierung, und zu wissen, welche davon ausgelöst hat, erspart einen Nachmittag Rätselraten. CodecWorkerTimeoutMilliseconds ist standardmäßig 10.000 und wird auf den Bereich 1 bis 600.000 validiert; ein Wert außerhalb löst eine Ausnahme aus, statt still gekappt zu werden. CodecWorkerMemoryLimitBytes ist standardmäßig 536.870.912 Bytes und muss entweder null sein, was kein Limit bedeutet, oder mindestens 67.108.864 Bytes betragen, weil eine kleinere Obergrenze keinen realistischen Decoder-Arbeitsspeicher fassen könnte und jedes Dokument zum Scheitern bringen würde. Die Speichergrenze wird durch ein Windows-Job-Objekt mit Kill-on-close-Semantik durchgesetzt, sodass der Worker mit dem Job stirbt, selbst wenn der Host abrupt beendet wird

Die dritte Obergrenze ist die Ausgabegrenze, und sie wird abgeleitet statt konfiguriert. HotPDF berechnet die erforderlichen Bytes aus dem angeforderten Bereich oder aus der erwarteten Bildgeometrie, als Breite mal Höhe mal drei für 24-Bit-Ausgabe, und kappt diesen Wert dann auf DecodeBudgetBytes, sofern ein Budget gesetzt ist. Ein Decoder, der einen plausiblen Header meldet und dann versucht, weit mehr Pixel auszugeben, als die Geometrie erlaubt, wird durch die Zuordnung selbst gestoppt, und der Host sieht cwsOutputLimit. Deshalb ergänzen sich die Isolationsschicht und das Dekodierungsbudget: Das Budget definiert, wie groß ein Bild sein darf, und die Isolationsgrenze stellt sicher, dass eine Lüge über diese Größe im eigenen Prozess nicht zu einem Schreibzugriff außerhalb der Grenzen werden kann

Wo das in einen gehärteten Aufnahmepfad passt

Prozessisolation ist die äußerste Schicht einer Verteidigungskette, die viel früher beginnt. Strukturelle Grenzen lehnen unplausible Dokumente bereits beim Parsen ab. Filterbudgets begrenzen die Expansion. Isolation fängt auf, was beide überlebt. Für Dokumente, die die Bildebene erreichen, lohnt sich zu wissen, welchen Codec man tatsächlich beansprucht, da JPXDecode-Behandlung und JBIG2-Symbolwörterbücher sehr unterschiedliche Fehlerprofile haben, und JBIG2 trägt insbesondere seitenübergreifende globale Segmente, die eine naive Pro-Bild-Sandbox brechen würde

Die Kosten sind ehrlich zu benennen und es wert, ausgesprochen zu werden: Das Starten eines Prozesses pro isoliertem Bild kostet Millisekunden, und ein Dokument mit Hunderten gescannter Seiten wird das spüren. Messen Sie das gegen den Nutzen. Auf einem Batch-Konverter, der unbeaufsichtigt über Nacht läuft, ist der Durchsatzverlust unsichtbar und die Absturzeindämmung der ganze Sinn der Sache. Bei einem interaktiven Viewer, der Dokumente öffnet, denen der Nutzer bereits vertraut, ist cimDisabled oder cimAutomatic die vernünftige Vorgabe. Der Modus ist eine einfache Eigenschaft, also hindert Sie nichts daran, ihn zur Laufzeit pro Dokumentklasse zu wählen

HotPDF liefert die Isolationsschicht, die Dekodierungsbudgets und die strukturellen Parser-Grenzen als eine native VCL-Komponente für Delphi und C++Builder, ohne externe Laufzeitumgebung, die über die Worker-Datei selbst hinaus bereitzustellen wäre. Die vollständige API-Dokumentation und ein Testbuild stehen auf der HotPDF Delphi PDF-Komponente-Seite