Τεχνικό Άρθρο

Απομόνωση PDF Codec Εικόνων σε Worker Process

Το HotPDF μπορεί να αποκωδικοποιεί τα τρία πιο επικίνδυνα φίλτρα εικόνας PDF, τα DCTDecode, JPXDecode και JBIG2Decode, μέσα σε μια ξεχωριστή, βραχύβια worker process αντί μέσα στην ίδια την εφαρμογή σας. Η ιδιότητα που το ενεργοποιεί είναι η CodecIsolationMode, και το πρακτικό αποτέλεσμα είναι ότι ένα κακοδιατυπωμένο JPEG 2000 codestream που θα κατέρρεε την εφαρμογή VCL σας τώρα σκοτώνει μια αναλώσιμη θυγατρική διεργασία, ενώ ο host αναφέρει έναν κωδικό κατάστασης και συνεχίζει

Αυτή η διαφορά μετράει περισσότερο ακριβώς εκεί απ' όπου φτάνουν στην πραγματικότητα τα PDF: μια φόρμα μεταφόρτωσης, μια πύλη αλληλογραφίας, μια συσκευή σάρωσης, ένα σημείο παράδοσης FTP συνεργάτη. Δεν ελέγχετε αυτά τα bytes, και οι codec εικόνων είναι εκεί όπου εντοπίζεται ιστορικά η ζημιά

Γιατί μία κακή εικόνα ρίχνει ολόκληρη την εφαρμογή;

Επειδή ένας codec εικόνας είναι το μοναδικό τμήμα ενός PDF reader που εκτελεί μια πολύπλοκη μηχανή καταστάσεων πάνω σε δεδομένα ελεγχόμενα από τον επιτιθέμενο, με σχεδόν καθόλου δομικούς ελέγχους να απομένουν ως δικλείδα ασφαλείας. Μέχρι τα bytes να φτάσουν στον αποκωδικοποιητή JPEG 2000 ή JBIG2, ο πίνακας cross-reference έχει ήδη αναλυθεί, το αντικείμενο έχει επιλυθεί, η αλυσίδα φίλτρων έχει ξετυλιχτεί, και αυτό που απομένει είναι ένα ακατέργαστο codestream που δηλώνει πόσα tiles, πόσα components, πόσα bits ανά δείγμα. Ένας λανθασμένος αριθμός εκεί δεν είναι σφάλμα ανάλυσης. Είναι κακό μέγεθος allocation ή δείκτης εκτός ορίων μέσα σε έναν σφιχτό βρόχο αποκωδικοποίησης

Τα όρια budget βοηθούν, και θα έπρεπε να τα έχετε ήδη. Το HotPDF οριοθετεί την επέκταση με τα DecodeBudgetBytes και DocumentDecodeBudgetBytes, και οριοθετεί τις αλυσίδες φίλτρων με τα DecodeFilterLimit και DecodePipelineDepthLimit· η λογική πίσω από αυτά τα όρια καλύπτεται στο bounded decoding για nested φίλτρα και PDF bombs. Όμως ένα byte budget απαντά μόνο σε ένα ερώτημα, πόση έξοδος επιτρέπεται. Δεν μπορεί να απαντήσει τι συμβαίνει όταν ο αποκωδικοποιητής αποτυγχάνει προτού παράξει καθόλου έξοδο. Ένα access violation μέσα σε έναν βρόχο αποκωδικοποίησης δεν είναι παραβίαση πολιτικής που μπορείτε να απορρίψετε· είναι συμβάν σε επίπεδο διεργασίας, και η μόνη αξιόπιστη περιχαράκωση για ένα τέτοιο συμβάν είναι μια διαφορετική διεργασία

Τι απομονώνει το HotPDF, και τι όχι

Το HotPDF απομονώνει ακριβώς τρία είδη codec, απαριθμούμενα ως hckDCT, hckJPX και hckJBIG2 στη μονάδα HPDFCodecIsolation. Όλα τα υπόλοιπα, Flate, LZW, RunLength, ASCII85, CCITT, παραμένουν in-process, επειδή αυτοί οι αποκωδικοποιητές είναι αρκετά απλοί ώστε να οριοθετούνται με budgets και δεν είναι εκεί όπου προκύπτουν τα ενδιαφέροντα σφάλματα

Η μεταφορά δεδομένων είναι σκόπιμα στενή. Ο host δεσμεύει ένα οριοθετημένο shared-memory mapping, γράφει μια σταθερή THPDFCodecSharedHeader μαζί με τη συμπιεσμένη είσοδο και τυχόν global segments του JBIG2, εκκινεί τη worker process και περιμένει. Η worker process γράφει τα αποκωδικοποιημένα pixels πίσω στο ίδιο mapping και ορίζει μια λέξη κατάστασης. Δεν υπάρχει πρωτόκολλο pipe που να μπορεί να χάσει συγχρονισμό, δεν υπάρχει μορφή serialization προς fuzzing, και η κεφαλίδα φέρει μια τιμή magic και μια έκδοση, ώστε ένα ασύμβατο δυαδικό worker να απορρίπτεται αντί να παρερμηνεύεται

uses
  HPDFDoc, HPDFCodecIsolation;

var
  Pdf: THotPDF;
  Info: THPDFCodecWorkerInfo;
  Bmp: TBitmap;
begin
  Pdf := THotPDF.Create(nil);
  try
    // Fail closed: ποτέ μην αποκωδικοποιείτε αυτούς τους codec in-process
    Pdf.CodecIsolationMode := cimRequired;
    Pdf.CodecWorkerExecutable := 'HotPDFCodecWorker.exe';
    Pdf.CodecWorkerTimeoutMilliseconds := 5000;       // 1..600000
    Pdf.CodecWorkerMemoryLimitBytes := 268435456;     // 0 ή >= 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;

Αφήστε το CodecWorkerExecutable κενό και το HotPDF εντοπίζει το worker δίπλα στο δικό σας εκτελέσιμο, ως HotPDFCodecWorker.exe στον κατάλογο του ParamStr(0). Ορίστε το ρητά όταν η ανάπτυξή σας τοποθετεί το worker αλλού· η τιμή επεκτείνεται μέσω της ExpandFileName, οπότε μια σχετική διαδρομή επιλύεται ως προς τον τρέχοντα κατάλογο και όχι ως προς τον κατάλογο της εφαρμογής, κάτι που σπάνια είναι το επιθυμητό σε μια υπηρεσία

Automatic ή required: ποια αποτυχία προτιμάτε;

Οι τρεις τιμές του THPDFCodecIsolationMode κωδικοποιούν τρεις διαφορετικές απαντήσεις σε ένα ερώτημα, τι πρέπει να συμβεί όταν το worker δεν μπορεί καθόλου να εκτελεστεί. Το cimDisabled παραλείπει εντελώς την απομόνωση και αποκωδικοποιεί in-process, η συμπεριφορά προ του 3.x. Το cimAutomatic, η προεπιλογή, δοκιμάζει το worker και επιστρέφει σιωπηλά σε in-process αποκωδικοποίηση όταν το εκτελέσιμο worker λείπει ή δεν εκκινεί, κάτι που αναφέρεται ως κατάσταση cwsUnavailable. Το cimRequired αρνείται αυτή την επιστροφή: ένα μη διαθέσιμο worker σημαδεύει την αποκωδικοποίηση ως χειρισμένη και αποτυχημένη, ώστε κανένα μη έμπιστο codestream να μη φτάνει ποτέ στον χώρο διευθύνσεών σας

Επιλέξτε βάσει μοντέλου απειλής, όχι βάσει ευκολίας. Ένας desktop viewer που ανοίγει έγγραφα που ο χρήστης έχει ήδη στον δίσκο είναι εντάξει με cimAutomatic, όπου ένα worker που λείπει υποβαθμίζεται στην κλασική συμπεριφορά αντί να σπάει το προϊόν. Μια υπηρεσία ingestion που αναλύει αρχεία από το διαδίκτυο θα έπρεπε να τρέχει με cimRequired, επειδή ένα σφάλμα ανάπτυξης που σιωπηλά αφαιρεί το επίπεδο απομόνωσης είναι ακριβώς το είδος οπισθοδρόμησης που κανείς δεν προσέχει μέχρι να έχει σημασία. Σημειώστε την ασυμμετρία: μόνο το cwsUnavailable ενεργοποιεί επιστροφή σε in-process. Ένα worker που εκκίνησε και μετά κατέρρευσε, έληξε ή έφτασε ένα όριο είναι αποτυχία αποκωδικοποίησης και στις δύο λειτουργίες, ποτέ σιωπηλή επανάληψη in-process

Ανάγνωση της απόφασης από το THPDFCodecWorkerStatus

Η GetLastCodecWorkerInfo επιστρέφει το αποτέλεσμα της πιο πρόσφατης απομονωμένης αποκωδικοποίησης, και η απαρίθμηση κατάστασης είναι αρκετά συγκεκριμένη ώστε να καθοδηγεί πραγματικές επιχειρησιακές αποφάσεις αντί για μια γενική καταγραφή "η εικόνα απέτυχε". Οι τιμές είναι cwsNotRun, cwsSucceeded, cwsUnavailable, cwsLaunchFailed, cwsTimedOut, cwsCrashed, cwsDecodeFailed, cwsProtocolError και cwsOutputLimit

Αντιμετωπίστε τες ως τρεις ομάδες. Προβλήματα ανάπτυξης είναι τα cwsUnavailable και cwsLaunchFailed: κάποιος έκανε deploy χωρίς το worker, ή ένα antivirus μπλοκάρει τη δημιουργία διεργασιών. Προβλήματα εγγράφου είναι τα cwsDecodeFailed και cwsOutputLimit: το αρχείο είναι κακοδιατυπωμένο ή μεγαλύτερο από όσο επιτρέπει η πολιτική σας, και η απόρριψή του είναι η σωστή απάντηση. Η ενδιαφέρουσα ομάδα είναι τα cwsTimedOut και cwsCrashed, επειδή αυτά είναι τα συμβάντα που παλαιότερα θα είχαν κρεμάσει ή σκοτώσει τη host διεργασία. Όταν συμβεί αυτό, τα συνοδευτικά πεδία ProcessId, ExitCode και ElapsedMilliseconds σας δίνουν αρκετά στοιχεία ώστε να συσχετίσετε με μια καταχώρηση Windows Error Reporting και να αποφασίσετε αν ένα αρχείο πελάτη είναι παθολογικό ή κάποιος σας δοκιμάζει

procedure LogCodecOutcome(const Info: THPDFCodecWorkerInfo);
begin
  case Info.Status of
    cwsSucceeded:
      ; // τίποτα προς αναφορά
    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;

Τα όρια που πραγματικά δεσμεύουν

Τρία ξεχωριστά ανώτατα όρια ισχύουν για κάθε απομονωμένη αποκωδικοποίηση, και το να ξέρετε ποιο ενεργοποιήθηκε γλιτώνει ένα ολόκληρο απόγευμα εικασιών. Το CodecWorkerTimeoutMilliseconds έχει προεπιλογή 10.000 και επικυρώνεται εντός του εύρους 1 έως 600.000· μια τιμή εκτός αυτού προκαλεί εξαίρεση αντί να περιορίζεται σιωπηλά. Το CodecWorkerMemoryLimitBytes έχει προεπιλογή 536.870.912 bytes και πρέπει να είναι είτε μηδέν, που σημαίνει χωρίς όριο, είτε τουλάχιστον 67.108.864 bytes, επειδή ένα μικρότερο όριο δεν μπορεί να χωρέσει ένα ρεαλιστικό working set αποκωδικοποιητή και θα απέτυχε σε κάθε έγγραφο. Το όριο μνήμης επιβάλλεται από ένα Windows Job Object με σημασιολογία kill-on-close, οπότε το worker πεθαίνει μαζί με το job ακόμη κι αν ο host τερματιστεί απότομα

Το τρίτο ανώτατο όριο είναι το όριο εξόδου, και προκύπτει παραγόμενο αντί να ρυθμίζεται. Το HotPDF υπολογίζει τα απαιτούμενα bytes από την ζητούμενη περιοχή, ή από την αναμενόμενη γεωμετρία εικόνας, ως πλάτος επί ύψος επί τρία για έξοδο 24-bit, και έπειτα περιορίζει αυτή την τιμή στο DecodeBudgetBytes όταν έχει οριστεί budget. Ένας αποκωδικοποιητής που αναφέρει μια εύλογη κεφαλίδα και μετά προσπαθεί να παράξει πολύ περισσότερα pixels απ' όσα επιτρέπει η γεωμετρία σταματά από το ίδιο το mapping, και ο host βλέπει cwsOutputLimit. Γι' αυτό το επίπεδο απομόνωσης και το budget αποκωδικοποίησης είναι συμπληρωματικά: το budget ορίζει πόσο μεγάλη επιτρέπεται να είναι μια εικόνα, και το όριο απομόνωσης εξασφαλίζει ότι ένα ψέμα για αυτό το μέγεθος δεν μπορεί να γίνει εγγραφή εκτός ορίων στη διεργασία σας

Πού ταιριάζει αυτό σε ένα θωρακισμένο μονοπάτι εισαγωγής

Η απομόνωση διεργασιών είναι το εξωτερικό επίπεδο μιας αλυσίδας άμυνας που ξεκινά πολύ νωρίτερα. Δομικά όρια απορρίπτουν μη εύλογα έγγραφα κατά την ανάλυση. Budgets φίλτρων οριοθετούν την επέκταση. Η απομόνωση περιέχει ό,τι επιβιώνει και από τα δύο. Για έγγραφα που φτάνουν στο επίπεδο εικόνας, αξίζει να γνωρίζετε ποιον codec ασκείτε στην πράξη, καθώς ο χειρισμός JPXDecode και τα λεξικά συμβόλων JBIG2 έχουν πολύ διαφορετικά προφίλ αποτυχίας, και το JBIG2 ειδικότερα φέρει global segments μεταξύ σελίδων που ένα απλοϊκό sandbox ανά εικόνα θα έσπαγε

Το κόστος είναι έντιμο και αξίζει να ειπωθεί: η εκκίνηση μιας διεργασίας ανά απομονωμένη εικόνα προσθέτει χιλιοστά του δευτερολέπτου, και ένα έγγραφο με εκατοντάδες σαρωμένες σελίδες θα το αισθανθεί. Μετρήστε το απέναντι σε ό,τι σας προσφέρει. Σε έναν μετατροπέα batch που τρέχει χωρίς επίβλεψη τη νύχτα, η απώλεια throughput είναι αόρατη και η περιχαράκωση κρασαρίσματος είναι όλο το νόημα. Σε έναν διαδραστικό viewer που ανοίγει έγγραφα που ο χρήστης ήδη εμπιστεύεται, το cimDisabled ή το cimAutomatic είναι η λογική προεπιλογή. Η λειτουργία είναι μια απλή ιδιότητα, οπότε τίποτα δεν σας εμποδίζει να επιλέγετε ανά κατηγορία εγγράφου κατά τον χρόνο εκτέλεσης

Το HotPDF διανέμει το επίπεδο απομόνωσης, τα budgets αποκωδικοποίησης και τα δομικά όρια του parser ως ένα ενιαίο εγγενές VCL component για Delphi και C++Builder, χωρίς εξωτερικό runtime προς ανάπτυξη πέρα από το ίδιο το εκτελέσιμο worker. Πλήρης τεκμηρίωση API και δοκιμαστική έκδοση είναι διαθέσιμες στη σελίδα του HotPDF Delphi PDF component