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

Κώδικας Delphi που δουλεύει κατά λάθος: 5 bugs του FPC port

Η PDF Library for Delphi βρήκε πέντε ελαττώματα αποκωδικοποιητών καθώς ανέβαζε τον κώδικα CCITT, TIFF, PNG, Flate και stream-buffer πάνω σε Free Pascal, και καθένα από αυτά είχε περάσει επί χρόνια την πλήρη test suite του Delphi. Κανένα δεν ήταν compiler bug. Καθένα ήταν Pascal που το Delphi τύχαινε να εκτελεί σωστά εξαιτίας μιας λεπτομέρειας υλοποίησης: μιας κρυφής παραμέτρου result που έκανε alias τον πίνακα του caller, ενός out-of-range branch που κανείς δεν διάβασε ποτέ παραπέρα, ενός buffer μηδενικού μήκους του οποίου η μόνη προστασία ήταν ένα switch του range checking, ενός 1-based offset που μόνο ένα code path πέρναγε ποτέ ως 1, και μιας σύμβασης του TStream.Read που τα in-memory streams δεν εξασκούν ποτέ. Άλλαξε compiler, ή δώσε στον ίδιο κώδικα ένα malformed file, και η κατά λάθος συμφωνία σταματά να κρατάει

Ακολουθεί το συγκεκριμένο σχήμα του καθενός, το fix, και η πειθαρχία που βγήκε από εκεί: η ίδια πηγή πλέον πρέπει να παράγει την ίδια σημασιολογία εγγράφου και στους δύο compilers, και ένα test include ελέγχει ότι πράγματι το κάνει. Το αδελφό άρθρο για το σκληραγώγηση ενός Pascal PDF parser εναντίον malicious files κάλυψε το πλάτος των integers, το βάθος αναδρομής και τα μη αρχικοποιημένα buffers. Αυτό εδώ αφορά μια διαφορετική κατηγορία αποτυχίας: κώδικα που ήταν λάθος εξαρχής και είχε έναν compiler να τον σκεπάζει σιωπηλά

Γιατί δουλεύει χωρίς SetLength στο Delphi μια συνάρτηση που επιστρέφει dynamic array;

Επειδή το Delphi περνάει τη μεταβλητή του caller ως κρυφή παράμετρο result, οπότε μια συνάρτηση που δεν δεσμεύει ποτέ το result της μπορεί παρόλα αυτά να γράφει σε έναν πίνακα που είχε δεσμεύσει ο caller. Το TPLCCITTDecoder.GetNextChangingElement(a0: Integer; IsWhite: Boolean): TCCITTIntegerArray είναι η αναζήτηση reference-line στην καρδιά της δισδιάστατης αποκωδικοποίησης Group 3 και Group 4: δεδομένης της τρέχουσας θέσης a0 και του χρώματος του τρέχοντος run, ψάχνει τα changing elements της προηγούμενης scanline, τα b1 και b2 του δισδιάστατου σχήματος κωδικοποίησης ITU-T T.4 και T.6, και τα επιστρέφει ως πίνακα δύο θέσεων. Η αρχική συνάρτηση έγραφε Result[0] και Result[1] και καθόλου δεν καλούσε SetLength στο Result

Αυτό θα έπρεπε να βγάλει fault στο πρώτο γράψιμο, και στο Free Pascal το βγάζει. Στο Delphi δεν το βγάλει ποτέ, επειδή και τα δύο call sites στον decoder μοιάζουν έτσι: δηλώνεις b: TCCITTIntegerArray, τρέχεις SetLength(b, 2) μία φορά πριν τον βρόχο της scanline, και μέσα στον βρόχο αναθέτεις b := GetNextChangingElement(a0, IsWhite) και διαβάζεις b[0] και b[1]. Ο οδηγός γλώσσας του Delphi λέει ότι μια συνάρτηση της οποίας το result είναι long string, dynamic array ή άλλος managed type λαμβάνει αυτό το result ως επιπλέον παράμετρο var, και στην πράξη ο compiler περνάει τη διεύθυνση του στόχου της ανάθεσης. Έτσι το Result μέσα στη συνάρτηση είναι το ίδιο το b, ήδη δύο στοιχείων, και κάθε γράψιμο πέφτει σε μνήμη που ανήκει στον caller. Το Free Pascal δίνει στη συνάρτηση έναν φρέσκο nil πίνακα και τον αναθέτει στο b μετά, που είναι η ανάγνωση της σύμβασης στην οποία έπρεπε εξαρχής να είχε γραφτεί ο κώδικας

Απόκλιση αποκωδικοποίησης CCITT στο PDFlibPas: το Delphi περνάει τον πίνακα b του caller ως κρυφό var Result του GetNextChangingElement οπότε τα γραψίματα πέφτουν σε μνήμη του caller και ένα miss της αναζήτησης κρατά τις προηγούμενες τιμές, ενώ το Free Pascal δίνει στη συνάρτηση φρέσκο nil πίνακα που ο guard του Length πρέπει να μετρήσει με SetLength πριν το πρώτο γράψιμο
Το Delphi κάνει alias τον πίνακα του caller ως κρυφή παράμετρο Result οπότε και τα γραψίματα χωρίς guard πέφτουν σε ιδιόκτητη μνήμη, ενώ το Free Pascal φτάνει με nil και ο guard μίας γραμμής μετατρέπει το fault σε επιθυμητή συμπεριφορά χωρίς να αλλάξει τη διαδρομή αποκωδικοποίησης του Delphi

Το aliasing κουβαλούσε επίσης μια σημασιολογία στην οποία στηρίζεται ο decoder. Το Result[0] ανατίθεται μόνο όταν η σάρωση βρει στοιχείο μεγαλύτερο από το a0, και το Result[1] μόνο όταν υπάρχει στοιχείο μετά από αυτό, οπότε σε miss οι θέσεις κρατούν ό,τι άφησε εκεί η προηγούμενη επανάληψη στο b. Το προφανές fix, δύο θέσεις μηδενισμένες σε κάθε κλήση, θα είχε καταστρέψει αυτή τη μεταφορά και θα είχε αλλάξει την αποκωδικοποιημένη έξοδο στο Delphi. Το fix που κυκλοφόρησε είναι ένας guard αντί για reset: στο Delphi είναι dead code και η διαδρομή αποκωδικοποίησης μένει byte προς byte όπως ήταν, και στο Free Pascal μετατρέπει το fault στην επιθυμητή συμπεριφορά. Αυτή η ασυμμετρία είναι όλο το νόημα, αφού το fix έπρεπε να είναι no-op στον compiler όπου ο κώδικας παρήγε ήδη επαληθευμένη έξοδο

Function TPLCCITTDecoder.GetNextChangingElement(a0: Integer;
  IsWhite: Boolean): TCCITTIntegerArray;
Begin
  // Το Delphi φτάνει εδώ με τον πίνακα δύο στοιχείων του caller aliased
  // ως Result, οπότε εκεί είναι no-op. Το FPC φτάνει με nil.
  If (Length(Result) < 2) Then
    SetLength(Result, 2);
  ...
  // Το Result[0] / Result[1] γράφονται πάλι μόνο σε hit, οπότε ένα miss
  // κρατά τις τιμές της προηγούμενης επανάληψης ακριβώς όπως πριν
End;

Ένας μετρητής που ξεπέρασε τα δεδομένα του: η εγγραφή του TIFF directory

Όταν ακυρώνεις έναν πίνακα, πρέπει να ακυρώσεις τον μετρητή του στην ίδια εντολή, ή ο μετρητής θα πιστευτεί από κώδικα που δεν βλέπει ποτέ τον πίνακα. Μια εγγραφή TIFF image file directory (TIFF 6.0 §2, η διάταξη 12 bytes του tag, του τύπου, του μετρητή και του value ή offset) κουβαλάει έναν μετρητή 32-bit κατευθείαν από το αρχείο, και η PDF Library for Delphi διαβάζει κάθε μία μέσω του PopDE: TTIFFEntry, ενός record με Tag, TagType, Length, Offset και τους αποκωδικοποιημένους πίνακες IntegerValues και DoubleValues. Ο αρχικός κώδικας έλεγχε αν το Offset + TypeSize * Length ξεπέρναγε το τέλος του αρχείου, και αν ναι, έδινε μηδενικό μήκος και στους δύο πίνακες. Ασή το Result.Length στην τιμή από το αρχείο

Από εκεί πάει στραβά δύο πράγματα. Η συνάρτηση κλείνει με ένα fallback που λέει «αν το Length είναι μηδέν, δώσε στην εγγραφή ένα στοιχείο με τιμή μηδέν», ώστε οι callers να μπορούν πάντα να διαβάσουν το στοιχείο μηδέν. Επειδή το Length δεν καθαριζόταν ποτέ στη διαδρομή out-of-range, αυτό το fallback δεν ενεργοποιούνταν ποτέ για την ακριβώς περίπτωση που υπήρχε. Και οι callers πράγματι διαβάζουν το στοιχείο μηδέν, άνευ όρων: Width, Height, BitsPerSample, PhotometricInterpretation, FillOrder, SamplesPerPixel, RowsPerStrip και μια ντουζίνα ακόμα παίρνουν το E.IntegerValues[0], και οι strip tables κάνουν Move(E.IntegerValues[0], StripOffsets[0], E.Length * 4), αντιγράφοντας Length φορές τέσσερα bytes από έναν πίνακα που δεν έχει κανένα. Ένας καθαρισμένος πίνακας με ζωντανό μετρητή είναι αυστηρά πιο επικίνδυνος από έναν ανέλεγκτο, γιατί ο ανέλεγκτος τουλάχιστον κρατάει τα bytes που ισχυρίζεται ότι έχει

Το δεύτερο πρόβλημα ήταν η σειρά. Οι δύο κλήσεις SetLength τρέχανε πριν το range test, με μέγεθος από τον μετρητή του αρχείου, οπότε μια εχθρική εγγραφή μπορούσε να ζητήσει δέσμευση πολλών gigabyte πριν από έναν μοναδικό έλεγχο εγκυρότητας. Στο Delphi η εξαίρεση που προέκυπτε πιανόταν από ένα handler πιο πάνω στη διαδρομή φόρτωσης εικόνων και το αρχείο απλώς απέτυχε να φορτώσει, γι' αυτό κανείς δεν το παρατήρησε· αυτό που πραγματικά συνέβαινε ήταν ένα out-of-memory γεγονός που είχε διαλέξει το αρχείο. Το fix μετακινεί τη δέσμευση μετά το test και κάνει τον μετρητή να ταξιδεύει με τα δεδομένα

Σκληραγώγηση εγγραφής TIFF directory στο PDFlibPas: η εγγραφή 12 bytes κουβαλάει μετρητή που δίνει το αρχείο, η σπασμένη σειρά δεσμευε πίνακες από αυτόν τον μετρητή πριν το range test και άφηνε το Result.Length ζωντανό μετά τον καθαρισμό τους, και η διορθωμένη σειρά ελέγχει πρώτα την αριθμητική Int64 εναντίον του μήκους του αρχείου ώστε ο μετρητής καθαρίζει μαζί με τους πίνακες
Η δέσμευση πριν το range test άφηνε έναν εχθρικό μετρητή να ζητήσει gigabyte και κρατούσε ζωντανό μετρητή σε άδειασμένο πίνακα, οπότε το fix ελέγχει πρώτα το offset και καθαρίζει το Result.Length στην ίδια εντολή με τους πίνακες
OutOfRange := Int64(ValueOffset) + Int64(TypeSize) * Result.Length
              > Length(Source);
If OutOfRange Then
Begin
  Result.Length := 0;              // ο μετρητής φεύγει μαζί με τις τιμές
  SetLength(Result.IntegerValues, 0);
  SetLength(Result.DoubleValues, 0);
End
Else
Begin
  SetLength(Result.IntegerValues, Result.Length);  // μόνο τώρα
  SetLength(Result.DoubleValues, Result.Length);
End;
// ... αργότερα, το υπάρχον fallback φτάνει επιτέλους την περίπτωση που υπήρχε:
If (Result.Length = 0) Then
Begin
  SetLength(Result.IntegerValues, 1);
  Result.IntegerValues[0] := 0;
End;

Τίποτα σε αυτό το fix δεν είναι εξειδικευμένο για compiler, και αυτό ακριβώς το κάνει να ανήκει στη λίστα. Το ελάττωμα ήταν λανθάνον στο Delphi για τον ίδιο λόγο που ήταν λανθάνον στο Free Pascal: κανένα test file δεν είχε εγγραφή καταλόγου που να δείχνει πέρα από το τέλος του αρχείου. Το port δεν το αποκάλυψε. Το αποκάλυψε το διάβασμα του κώδικα με την ερώτηση «τι κάνει το Delphi για μένα εδώ που δεν κάνω ο ίδιος»

Τι γίνεται όταν ένα PNG IHDR ισχυρίζεται έναν color type που η μορφή δεν ορίζει;

Η PDF Library for Delphi πλέον απορρίπτει την εικόνα πριν τρέξουν τα row filters· πριν το v3.539.2 υπολόγιζε scanline μηδενικών bytes και έδινε στους βρόχους unfilter έναν άδειο buffer. Το ISO 15948 §11.2.2 ορίζει το chunk IHDR και ο Πίνακας 11.1 απαριθμεί τους έξι έννομους συνδυασμούς color type και bit depth: grayscale στα 1, 2, 4, 8 ή 16 bits, indexed color στα 1, 2, 4 ή 8, και truecolor, grayscale με alpha και truecolor με alpha στα 8 ή 16. Ο TPNGReader επικύρωνε τα πεδία compression method και filter method του IHDR και άφηνε το FColorType και το bit depth να περάσουν ανέγγιχτα

Ο κώδικας των row filters παίρνει όλα τα μεγέθη από ένα Case FColorType Of που αντιστοιχίζει κάθε color type σε πλήθος components. Ένας color type εκτός των έξι πέφτει στο branch Else, όπου SourceComponents είναι 0, άρα ScanlineByteCount είναι 0, άρα στο SetLength(PreviousScanline, 0) ακολουθεί αμέσως το FillChar(PreviousScanline[0], ScanlineByteCount, 0). Η δεικτοδότηση του στοιχείου μηδέν ενός άδειου dynamic array είναι μια διεύθυνση υπολογισμένη από nil. Με το range checking ανενεργό, ένα γέμισμα μηδενικών bytes μέσω εκείνης της διεύθυνσης είναι σιωπηλό no-op και ο decoder συνεχίζει μέσα από σειρές που δεν υπάρχουν· με το range checking ενεργό είναι ένα ERangeError στην πρώτη εικόνα· και οι κλήσεις Move που ακολουθούν απέχουν ένα βήμα από access violation. Ποιο από αυτά θα πάρετε εξαρτάται από τον compiler και από build switches και όχι από κάτι που αποφάσισε ο decoder, και αυτό είναι το σημάδι ότι ο decoder δεν αποφάσισε ποτέ καθόλου

Το fix είναι ο πίνακας από την προδιαγραφή, εφαρμοσμένος εκεί όπου τα υπόλοιπα πεδία του IHDR επικυρώνονταν ήδη: το COLOR_GRAYSCALE δέχεται FSourceBitDepth in [1, 2, 4, 8, 16], το COLOR_PALETTE δέχεται [1, 2, 4, 8], και το COLOR_RGB, το COLOR_GRAYSCALEALPHA και το COLOR_RGBALPHA δέχονται [8, 16]· οτιδήποτε άλλο καθαρίζει το ValidImage και η εικόνα απορρίπτεται με width και height άθικτα για diagnostics. Ένα chunk pHYs πιο σύντομο από τα εννιά του bytes κλείστηκε στο ίδιο πέρασμα, αφού ο αναγνώστης DPI δεικτοδοτούσε το S[1] έως το S[8] ενός string που το κοντό chunk είχε αφήσει άδειο

Ένα 1-based offset που μεταχειρίστηκε ως 0-based pointer

Το InflateStrFromPosition(Const Input: AnsiString; StartPos, MaxOutput: Integer; Out Consumed: Integer): AnsiString παίρνει 1-based StartPos, επειδή η είσοδός του είναι AnsiString και η υλοποίηση Delphi διευθύνει την είσοδο zlib ως @Input[StartPos]. Η υλοποίηση Free Pascal, γραμμένη πάνω στο paszlib ώστε και οι δύο στόχοι Windows να κάνουν στατική σύνδεση της συμπίεσης, έθετε το next_in σε PAnsiChar(Input) + StartPos και το avail_in σε Length(Input) - StartPos. Αυτό είναι pointer arithmetic, και είναι 0-based. Πέρνα 1, που είναι αυτό που σημαίνει «ξεκίνα από την αρχή» για αυτή τη συνάρτηση, και το build FPC αρχίζει να αποσυμπιέζει από το δεύτερο byte και σταματά ένα byte πριν το τέλος

Ο λόγος που επιβίωσε είναι ότι ο μόνος caller που φτάνουν οι περισσότεροι tests είναι το InflateStr, που περνάει 0. Το μηδέν τυχαίνει να είναι το σωστό 0-based offset, οπότε τα δύο builds συμφωνούσαν σε κάθε σκέτη κλήση InflateStr και σε κάθε test που περνούσε από εκεί. Το TPDFDocument.DecodeAllStreams, η ρουτίνα που χρησιμοποιούν το SaveQDFToFile και το ConvertFileToQDF για να ξεδιπλώσουν streams ενός FlateDecode σε αναγνώσιμη μορφή, περνάει 1. Στο build FPC το παραλειφμένο zlib header έκανε το inflate να αποτύχει, αλλά το zlib stream ανέφερε ούτως ή άλλως μη μηδενικό Consumed για τα bytes που είχε εξετάσει, οπότε το DecodeAllStreams πήρε το άδειο payload ως επιτυχή αποκωδικοποίηση και αντικατέστησε κάθε content stream με κενό string. Το προκύψαν QDF είχε το σωστό πλήθος σελίδων, έγκυρη δομή και κανένα περιεχόμενο σελίδας, που είναι ένα αρχείο που ανοίγει χωρίς error σε κάθε viewer και δεν δείχνει τίποτα

// Το FPC branch του InflateStrFromPosition, μετά το v3.539.16.
// Το StartPos είναι 1-based όπως το Delphi branch· κλάψε το, και μετά
// μετέτρεψέ το σε 0-based pointer offset ακριβώς μία φορά, στο όριο.
If (StartPos < 1) Then
  StartPos := 1;
If (Length(Input) = 0) Or (StartPos > Length(Input)) Then
  Exit;
...
strm.next_in  := Pointer(PAnsiChar(Input) + StartPos - 1);
strm.avail_in := Length(Input) - StartPos + 1;

Το regression που το καρφώνει είναι το πιο μικρό δυνατό: κάνε deflate σε ένα payload, κάνε το inflate από τη θέση 0 και από τη θέση 1, και ισχυρίσου ότι και τα δύο επιστρέφουν το ίδιο payload και ότι και τα δύο αναφέρουν Consumed ίσο με το πλήρες μήκος του stream. Ένα stream RFC 1950 έχει header δύο bytes και trailer Adler-32 τεσσάρων bytes, οπότε ένα off-by-one σε όποιο άκρο δεν είναι μια διακριτική διαφθορά, είναι ένα stream που είτε δεν ξεκινάει είτε δεν τελειώνει. Το μάθημα αφορά το όριο, όχι το zlib: όταν μια παράμετρος συνάρτησης ορίζεται σε μια βάση δεικτοδότησης και η υλοποίηση από κάτω χρησιμοποιεί την άλλη, η μετατροπή ανήκει σε ακριβώς μία γραμμή, και ένα test πρέπει να την καλεί με την τιμή που ξεχωρίζει τις δύο βάσεις

Γιατί ένα σύντομο TStream.Read δεν είναι το τέλος του stream;

Επειδή το TStream.Read επιτρέπεται να επιστρέψει λιγότερα bytes από όσα ζητήθηκαν για όποιον λόγο γουστάρει, και μόνο η επιστροφή 0 σημαίνει ότι δεν υπάρχει τίποτα άλλο. Το TMemoryStream και το TFileStream σε τοπικό δίσκο σχεδόν πάντα γεμίζουν το αίτημα, γι' αυτό κώδικας που μεταχειρίζεται το «επέστρεψε λιγότερα απ' όσα ζήτησα» ως end-of-file περνάει κάθε test που τους χρησιμοποιεί. Streams πάνω σε δίκτυο, streams αποσυμπίεσης, και οποιοσδήποτε απόγονος TStream που έγραψε ένας πελάτης μπορεί να επιστρέψουν δύο bytes όταν ζητηθούν εξήντα τέσσερις χιλιάδες και να έχουν ακόμα gigabytes πίσω τους

Το TPLBuffer είναι ο αναγνώστης από τον οποίο περνάει κάθε parser της PDF Library for Delphi, και μπορεί να τυλίξει AnsiString, pointer, πίνακα bytes ή TStream. Τα τέσσερα scanning queries του, DistanceToByte, DistanceToOtherByte, DistanceToAnyByte και DistanceToOtherBytes, όλα με επιστροφή Int64, διαβάζουν την πηγή σε blocks των 64 KB ψάχνοντας delimiter και αναφέρουν πόσο μακριά είναι χωρίς να μετακινήσουν τη λογική θέση. Κάθε βρόχος τελείωνε με Until ReadCount < BlockSize. Για τις τρεις in-memory πηγές αυτό είναι σωστό, αφού το ReadIntoBuffer παραδίδει πάντα το πλήρες block μέχρι το τελευταίο. Για την πηγή stream σημαίνει ότι η σάρωση τα παρατάει στο πρώτο σύντομο read, αναφέρει το delimiter ως απόν, και ο tokenizer από πάνω αποφασίζει ότι το αντικείμενο τελειώνει εκεί που δεν τελειώνει

Χειρισμός short read στο stream buffer του PDFlibPas: το DistanceToByte σαρώνει blocks των 64 KB, ο παλιός βρόχος μεταχειριζόταν το Until ReadCount < BlockSize ως τέλος δεδομένων και τα παρατούσε στο πρώτο σύντομο read, ενώ ο διορθωμένος βρόχος τρέχει μέχρι το ReadCount να γίνει μηδέν, βρίσκει το delimiter και επαναφέρει τη θέση σε block finally
Ένα stream μπορεί να επιστρέψει δύο bytes όταν ζητηθούν εξήντα τέσσερις χιλιάδες, οπότε το μηδέν είναι το μόνο σήμα τέλους δεδομένων που μπορεί να εμπιστευτεί η σάρωση, και ο όρος finally επαναφέρει τη λογική θέση όταν βρεθεί το delimiter και ο βρόχος εξέλθει νωρίς
// TPLBuffer.DistanceToByte, ο βρόχος μετά το v3.539.6.
// Το μηδέν είναι το μόνο σήμα τέλους δεδομένων που ορίζει το TStream.Read.
TempPosition := FPosition;
Try
  Repeat
    ReadCount := ReadIntoBuffer(@TempBuffer[0], BlockSize);
    For TestPos := 0 To ReadCount - 1 Do
      If TempBuffer[TestPos] = Value Then
      Begin
        Result := TotalSkipped + TestPos;
        Exit;
      End;
    Inc(TotalSkipped, ReadCount);
  Until ReadCount = 0;
Finally
  FPosition := TempPosition;   // ένα peek δεν πρέπει να μετακινήσει τον αναγνώστη
End;

Το test που το καρφώνει είναι ένας απόγονος TMemoryStream του οποίου το override του Read κόβει κάθε αίτημα στα δύο bytes. Τύλιξε μέσα του το string aaaaaX, βάλε τη θέση του buffer στο 1, και και τα τέσσερα queries πρέπει να αναφέρουν απόσταση 4 από το X, να αφήσουν τη θέση στο 1 μετά, και να αναφέρουν -1 για ένα byte που δεν υπάρχει. Πριν το fix το πρώτο query έβλεπε δύο bytes, συμπέραινε ότι το stream είχε εξαντληθεί, και επέστρεφε -1. Το finally μετράει όσο και η συνθήκη του βρόχου: ένα Exit μέσα στη σάρωση είναι η φυσιολογική διαδρομή επιτυχίας, και η λογική θέση πρέπει να επαναφέρεται και σε αυτή τη διαδρομή, όχι μόνο όταν ο βρόχος τρέχει ως το τέλος

Μια πηγή, δύο compilers, ένα σύνολο ισχυρισμών

Η πειθαρχία που βγήκε από αυτά τα πέντε είναι ότι το «το build Delphi περνάει» είναι απόδειξη για το Delphi, όχι για την πηγή. Από το v3.539.16 η DUnitX suite του Delphi και η console suite του Free Pascal περιλαμβάνουν και οι δύο το ίδιο Tests\CrossCompilerSemantics.inc, μια ενιαία ρουτίνα, το RunCrossCompilerFileSemantics, που χτίζει έγγραφο δύο σελίδων με συμπιεσμένο περιεχόμενο μέσω TPDFlib, το αποθηκεύει, το ξανααποθηκεύει ως QDF μέσω SaveQDFToFile, επιδιορθώνει το QDF με RepairQDFFile, κρυπτογραφεί το σκέτο αρχείο με AES-128 μέσω EncryptFile και μιας μάσκας δικαιωμάτων από EncodePermissions, και μετά ξαναφορτώνει κάθε τεχνουργημένο και ισχυρίζεται τα ίδια πράγματα και στους δύο compilers: το πλήθος σελίδων είναι 2, ο τίτλος επιζεί, το κείμενο της δεύτερης σελίδας εξάγεται άθικτο από τα plain, repaired και encrypted αρχεία, ο λάθος κωδικός απορρίπτεται με μη μηδενικό LastErrorCode, το EncryptionStrength είναι 128, το EncryptionAlgorithm είναι 2, και τα μεμονωμένα bits δικαιωμάτων από το GetUserPermissions έρχονται ακριβώς όπως κωδικοποιήθηκαν

Η σύγκριση είναι σκόπιμα κανονικοποιημένη και όχι byte προς byte. Η κρυπτογράφηση τραβάει τυχαία salts και ο writer αναθέτει document identifiers, οπότε δεν αναμένεται τα δύο builds να εκπέμψουν πανομοιότυπα αρχεία· αναμένεται να εκπέμψουν αρχεία που σημαίνουν το ίδιο πράγμα, και οι ισχυρισμοί διατυπώνονται σε αυτό το επίπεδο. Το σκέλος QDF υπάρχει ειδικά εξαιτίας του bug του offset: ένα QDF με δύο σελίδες και κανένα περιεχόμενο περνάει έλεγχο πλήθους σελίδων και αποτυγχάνει σε έλεγχο εξαγωγής κειμένου, και ο πίνακας ισχυρίζεται τον δεύτερο. Οποιοδήποτε μελλοντικό fix που είναι no-op στον έναν compiler και αλλαγή συμπεριφοράς στον άλλον, που περιγράφει τέσσερα από τα πέντε παραπάνω, πλέον πρέπει να περάσει τους ίδιους ισχυρισμούς δύο φορές πριν κυκλοφορήσει

Το μισό του ίδιου port που αφορά το link time, το να συμφωνήσουν τα OMF objects του Delphi με τις προσδοκίες COFF του Free Pascal, είναι δική του ιστορία στο linking OMF προς COFF objects σε FPC Win32, και η δομική σκλήρυνση του ίδιου TIFF reader απέναντι σε BigTIFF και tiled αρχεία βρίσκεται στις σημειώσεις του ενσωματωμένου TIFF decoder. Οι αποκωδικοποιητές του άρθρου, και το cross-compiler test που κάθεται πλέον από κάτω τους, κυκλοφορούν μέσα στο PDF Library for Delphi για Delphi, C++Builder και Free Pascal, όπου η ίδια πηγή αναμένεται να κερδίζει το ίδιο αποτέλεσμα σε κάθε compiler που στοχεύει, και όχι να της το χαρίζει ένας