บทความเทคนิค

โค้ด Delphi ที่บังเอิญรันผ่าน: ห้า bug ตอนพอร์ตขึ้น FPC

PDF Library for Delphi เจอข้อบกพร่องใน decoder ห้าจุดตอนยกโค้ด CCITT, TIFF, PNG, Flate และ stream buffer ขึ้นไปบน Free Pascal และทั้งห้าจุดผ่านชุดทดสอบเต็มของ Delphi มาหลายปี ไม่มีสักจุดที่เป็น bug ของ compiler แต่ละจุดเป็น Pascal ที่ Delphi บังเอิญรันถูกเพราะรายละเอียดของการ implement: result parameter ที่ซ่อนอยู่ซึ่ง alias array ของ caller, branch ที่ index เกินขอบเขตซึ่งไม่มีใครอ่านต่อ, buffer ความยาวศูนย์ที่มี range-check switch เป็น guard ตัวเดียว, offset แบบ 1-based ที่มีแค่เส้นทางเดียวส่งค่า 1 เข้ามา และ contract ของ TStream.Read ที่ stream ในหน่วยความจำไม่เคยแตะ เปลี่ยน compiler หรือป้อนไฟล์ผิดรูปให้โค้ดชุดเดิม แล้วความบังเอิญนั้นก็หยุดทำงาน

ต่อจากนี้คือรูปร่างของแต่ละข้อ fix ของมัน และวินัยที่ตามมาทีหลัง: source ชุดเดียวกันต้องให้ semantics ของเอกสารเหมือนกันบนทั้งสอง compiler และ test include ตัวหนึ่งคอยยืนยันว่าได้ผลจริง บทความพี่น้องเรื่องการเสริมความแข็งแรงให้ parser PDF ที่เขียนด้วย Pascal รับมือไฟล์อันตรายครอบคลุมความกว้างของ integer, ความลึกของ recursion และ buffer ที่ไม่ถูก initialize ส่วนบทความนี้พูดถึงความล้มเหลวอีกคลาสหนึ่ง: โค้ดที่ผิดมาตลอดและมี compiler คอยปิดให้เงียบ ๆ

ทำไมฟังก์ชันที่คืน dynamic array ถึงทำงานได้โดยไม่เรียก SetLength บน Delphi

เพราะ Delphi ส่งตัวแปรของ caller เองเข้ามาเป็น hidden result parameter ฟังก์ชันที่ไม่ได้ allocate result ของตัวเองจึงยังเขียนลง array ที่ caller allocate ไว้ได้ TPLCCITTDecoder.GetNextChangingElement(a0: Integer; IsWhite: Boolean): TCCITTIntegerArray คือการค้นหา reference line ที่เป็นหัวใจของการ decode Group 3 และ Group 4 แบบสองมิติ: รับตำแหน่งปัจจุบัน a0 กับสีของ run ปัจจุบัน แล้วค้นหา changing element ของ scanline ก่อนหน้า ซึ่งก็คือ b1 กับ b2 ตามการเข้ารหัสสองมิติของ ITU-T T.4 และ T.6 แล้วคืนออกมาเป็น array สองช่อง โค้ดเวอร์ชันเดิมเขียน Result[0] กับ Result[1] และไม่เคยเรียก SetLength กับ Result เลยสักครั้ง

แบบนั้นควรพังตั้งแต่การเขียนครั้งแรก และบน Free Pascal มันพังจริง แต่บน Delphi ไม่เคยพัง เพราะ call site ทั้งสองจุดใน decoder เขียนแบบนี้: ประกาศ b: TCCITTIntegerArray เรียก SetLength(b, 2) หนึ่งครั้งก่อน loop ของ scanline จากนั้นใน loop ค่อย assign b := GetNextChangingElement(a0, IsWhite) แล้วอ่าน b[0] กับ b[1] คู่มือภาษา Delphi ระบุว่าฟังก์ชันที่ result เป็น long string, dynamic array หรือ managed type อื่น ๆ จะได้รับ result นั้นเป็น var parameter เพิ่มอีกตัว และในทางปฏิบัติ compiler ส่ง address ของตัวแปรปลายทางมาให้ ดังนั้น Result ในฟังก์ชันก็คือ b เอง ซึ่งยาวสองช่องอยู่แล้ว ทุกการเขียนจึงลงในหน่วยความจำที่ caller เป็นเจ้าของ ส่วน Free Pascal ส่ง array nil ตัวใหม่ให้ฟังก์ชันแล้วค่อย assign ให้ b ทีหลัง ซึ่งเป็นการตีความ contract ที่โค้ดควรถูกเขียนขึ้นมาบนนั้นตั้งแต่แรก

แผนภาพความต่างของการ decode CCITT ใน PDFlibPas: Delphi ส่ง array b ของ caller เข้ามาเป็น hidden var Result ของ GetNextChangingElement การเขียนจึงลงในหน่วยความจำที่ caller เป็นเจ้าของและการค้นหาที่พลาดทำให้ค่าก่อนหน้ายังอยู่ ส่วน Free Pascal ส่ง array nil ตัวใหม่มาให้ฟังก์ชัน ซึ่ง Length guard ต้องตั้งขนาดด้วย SetLength ก่อนการเขียนครั้งแรก
Delphi alias array ของ caller เป็น hidden Result parameter การเขียนที่ไม่มี guard จึงยังลงในหน่วยความจำที่มีเจ้าของ ส่วน Free Pascal มาถึงพร้อม nil และ guard บรรทัดเดียวก็เปลี่ยน fault ให้กลายเป็นพฤติกรรมที่ตั้งใจไว้โดยไม่แตะเส้นทาง decode ของ Delphi

การ alias นี้ยังพา semantic ที่ decoder พึ่งพาอยู่ติดมาด้วย Result[0] ถูก assign ก็ต่อเมื่อการสแกนเจอ element ที่มากกว่า a0 และ Result[1] ก็ต่อเมื่อมี element ถัดจากนั้น ดังนั้นเมื่อค้นไม่เจอ ช่องทั้งสองจะเก็บค่าที่ iteration ก่อนหน้าทิ้งไว้ใน b fix ที่ดูตรงไปตรงมา คือ allocate สองช่องแล้ว zero ทุกครั้ง จะทำลายการส่งต่อค่านั้นและเปลี่ยน output ที่ decode ได้บน Delphi fix ที่ ship จริงจึงเป็น guard ไม่ใช่การ reset: บน Delphi มันเป็น dead code และเส้นทาง decode ยังเหมือนเดิมทุกไบต์ ส่วนบน Free Pascal มันเปลี่ยน fault ให้กลายเป็นพฤติกรรมที่ตั้งใจ ความไม่สมมาตรนี้คือประเด็นทั้งหมด เพราะ fix ต้องเป็น no-op บน compiler ที่โค้ดผลิต output ที่ผ่านการตรวจสอบอยู่แล้ว

Function TPLCCITTDecoder.GetNextChangingElement(a0: Integer;
  IsWhite: Boolean): TCCITTIntegerArray;
Begin
  // Delphi มาถึงตรงนี้โดยมี array สองช่องของ caller ถูก alias
  // เป็น Result จึงเป็น no-op บน Delphi ส่วน FPC มาถึงพร้อม nil
  If (Length(Result) < 2) Then
    SetLength(Result, 2);
  ...
  // Result[0] / Result[1] ยังถูกเขียนเฉพาะตอนที่เจอ ค่าที่ค้นไม่เจอ
  // จึงเก็บค่าของ iteration ก่อนหน้าไว้เหมือนเดิมทุกประการ
End;

count ที่อายุยืนกว่าข้อมูล: directory entry ของ TIFF

เมื่อคุณทำให้ array ใช้ไม่ได้ คุณต้องทำให้ count ของมันใช้ไม่ได้ในประโยคเดียวกันด้วย ไม่งั้นโค้ดที่ไม่เคยเห็น array จะยังเชื่อ count นั้นอยู่ directory entry ของไฟล์ภาพ TIFF (TIFF 6.0 §2 โครง 12 ไบต์ของ tag, type, count และ value-or-offset) พา count ขนาด 32 บิตจากไฟล์เข้ามาตรง ๆ และ PDF Library for Delphi อ่านแต่ละรายการผ่าน PopDE: TTIFFEntry ซึ่งเป็น record ที่มี Tag, TagType, Length, Offset และ array ที่ decode แล้วอย่าง IntegerValues กับ DoubleValues โค้ดเดิมตรวจว่า Offset + TypeSize * Length ล่วงเลยปลายไฟล์หรือไม่ และถ้าล่วง ก็ตั้ง array ทั้งสองให้ยาวศูนย์ แต่ยังปล่อย Result.Length ไว้ที่ค่าจากไฟล์

จากตรงนั้นมีสองอย่างที่พัง ฟังก์ชันจบด้วย fallback ที่อ่านว่า ถ้า Length เป็นศูนย์ ก็ให้ entry หนึ่งสมาชิกที่มีค่าเป็นศูนย์ เพื่อให้ caller อ่านสมาชิกช่องศูนย์ได้เสมอ เพราะ Length ไม่เคยถูกล้างในเส้นทาง out-of-range fallback นั้นจึงไม่เคยทำงานในเคสเดียวที่มันถูกสร้างมาเพื่อรองรับ และ caller ก็อ่านสมาชิกช่องศูนย์แบบไม่มีเงื่อนไขจริง ๆ: Width, Height, BitsPerSample, PhotometricInterpretation, FillOrder, SamplesPerPixel, RowsPerStrip และอีกเป็นสิบใช้ E.IntegerValues[0] ส่วนตาราง strip ก็ทำ Move(E.IntegerValues[0], StripOffsets[0], E.Length * 4) คือคัดลอก Length คูณสี่ไบต์ออกจาก array ที่ไม่เหลืออะไรเลย array ที่ถูกล้างแต่ count ยังมีชีวิตอันตรายกว่า array ที่ไม่ได้ตรวจอย่างชัดเจน เพราะตัวหลังอย่างน้อยก็มีไบต์ตามที่มันอ้างอยู่จริง

ปัญหาที่สองคือลำดับการทำงาน SetLength สองครั้งรันก่อนการตรวจช่วง โดยตั้งขนาดจาก count ในไฟล์ entry ที่มุ่งร้ายจึงขอ allocation ระดับหลายกิกะไบต์ได้ก่อนจะมีการตรวจความถูกต้องแม้แต่ครั้งเดียว บน Delphi exception ที่ตามมาถูก handler ซึ่งอยู่สูงขึ้นไปในเส้นทางโหลดภาพดักไว้ ไฟล์ก็แค่โหลดไม่ผ่าน ซึ่งเป็นเหตุผลที่ไม่มีใครสังเกต สิ่งที่เกิดขึ้นจริงคือ out-of-memory ที่ไฟล์เป็นคนเลือกเอง fix ย้าย allocation ไปไว้หลังการตรวจและให้ count เดินทางไปพร้อมข้อมูล

แผนภาพการเสริมความแข็งแรงของ directory entry ใน TIFF ของ PDFlibPas: entry 12 ไบต์พา count ที่มาจากไฟล์ ลำดับที่ผิด allocate array จาก count นั้นก่อนตรวจช่วงและปล่อย Result.Length ให้ยังมีชีวิตหลังล้าง array ส่วนลำดับที่แก้แล้วตรวจเลขคณิตแบบ Int64 กับความยาวไฟล์ก่อน count จึงถูกล้างพร้อมกับ array
การ allocate ก่อนตรวจช่วงเปิดโอกาสให้ count ที่มุ่งร้ายขอ memory ระดับกิกะไบต์และทิ้ง count ที่ยังมีชีวิตไว้บน array ที่ว่างเปล่า fix จึงตรวจ offset ก่อนแล้วล้าง Result.Length ในประโยคเดียวกับ array
OutOfRange := Int64(ValueOffset) + Int64(TypeSize) * Result.Length
              > Length(Source);
If OutOfRange Then
Begin
  Result.Length := 0;              // count ไปพร้อมกับค่า
  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: ไม่มีไฟล์ทดสอบไหนมี directory entry ที่ชี้เลยปลายไฟล์ การพอร์ตไม่ได้ทำให้มันโผล่ แต่การอ่านโค้ดด้วยคำถามว่า Delphi ทำให้เราตรงไหนที่เราทำเองไม่ได้ต่างหากที่ทำให้มันโผล่

เกิดอะไรขึ้นเมื่อ IHDR ของ PNG อ้าง color type ที่ฟอร์แมตไม่ได้นิยามไว้

ตอนนี้ PDF Library for Delphi ปฏิเสธภาพก่อน row filter จะรัน ก่อน v3.539.2 มันคำนวณ scanline ขนาดศูนย์ไบต์แล้วส่ง buffer ว่างเปล่าให้ loop ของ unfilter ISO 15948 §11.2.2 นิยาม IHDR chunk และตาราง 11.1 ระบุชุดค่าที่ถูกต้องของ color type กับ bit depth ทั้งหกแบบ: grayscale ที่ 1, 2, 4, 8 หรือ 16 บิต, indexed color ที่ 1, 2, 4 หรือ 8 และ truecolor, grayscale พร้อม alpha กับ truecolor พร้อม alpha ที่ 8 หรือ 16 TPNGReader ตรวจ compression method กับ filter method ของ IHDR แล้วปล่อย FColorType กับ bit depth ผ่านไปโดยไม่แตะ

โค้ด row filter ตั้งขนาดทุกอย่างจาก Case FColorType Of ที่ map color type แต่ละตัวไปเป็นจำนวน component color type ที่อยู่นอกเหนือหกแบบนั้นตกไปที่ branch Else ซึ่ง SourceComponents เป็น 0 ทำให้ ScanlineByteCount เป็น 0 ดังนั้น SetLength(PreviousScanline, 0) จึงตามด้วย FillChar(PreviousScanline[0], ScanlineByteCount, 0) ทันที การ index สมาชิกช่องศูนย์ของ dynamic array ที่ว่างเปล่าคือ address ที่คำนวณจาก nil เมื่อปิด range checking การ fill ศูนย์ไบต์ผ่าน address นั้นก็เป็น no-op เงียบ ๆ แล้ว decoder ก็เดินหน้าต่อไปบนแถวที่ไม่มีอยู่จริง เมื่อเปิด range checking มันคือ ERangeError ตั้งแต่ภาพแรก และ Move ที่ตามมาอีกก้าวเดียวก็ถึง access violation จะได้อันไหนขึ้นกับ compiler และ build switch ไม่ใช่สิ่งที่ 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 แล้วภาพถูกปฏิเสธโดยยังคงความกว้างกับความสูงไว้เพื่อการวินิจฉัย ส่วน pHYs chunk ที่สั้นกว่าเก้าไบต์ของมันถูกปิดในรอบเดียวกันด้วย เพราะตัวอ่าน DPI index S[1] ถึง S[8] ของสตริงที่ chunk สั้น ๆ ทิ้งให้ว่างเปล่า

offset แบบ 1-based ที่ถูกใช้เป็น pointer แบบ 0-based

InflateStrFromPosition(Const Input: AnsiString; StartPos, MaxOutput: Integer; Out Consumed: Integer): AnsiString รับ StartPos แบบ 1-based เพราะ input ของมันเป็น AnsiString และ implementation ฝั่ง Delphi เข้าถึง input ของ zlib ด้วย @Input[StartPos] ส่วน implementation ฝั่ง Free Pascal ที่เขียนบน paszlib เพื่อให้ทั้งสอง target บน Windows link compression แบบ static ตั้ง next_in เป็น PAnsiChar(Input) + StartPos และ avail_in เป็น Length(Input) - StartPos นั่นคือ pointer arithmetic ซึ่งเป็นแบบ 0-based ส่ง 1 ซึ่งก็คือความหมายของคำว่าเริ่มที่ต้นของฟังก์ชันนี้ แล้วบิลด์ FPC จะเริ่ม inflate ที่ไบต์ที่สองและหยุดก่อนปลายหนึ่งไบต์

เหตุผลที่มันรอดมาได้คือ caller ตัวเดียวที่ test ส่วนใหญ่ไปถึงคือ InflateStr ซึ่งส่ง 0 เลขศูนย์บังเอิญเป็น offset แบบ 0-based ที่ถูกต้อง บิลด์ทั้งสองจึงเห็นตรงกันในทุกการเรียก InflateStr ธรรมดาและทุก test ที่ผ่านเส้นทางนั้น แต่ TPDFDocument.DecodeAllStreams ซึ่งเป็น routine ที่ SaveQDFToFile กับ ConvertFileToQDF ใช้ขยาย stream ที่เป็น FlateDecode เดี่ยวให้อยู่ในรูปที่อ่านได้ กลับส่ง 1 บนบิลด์ FPC zlib header ที่ถูกข้ามทำให้ inflate ล้มเหลว แต่ zlib stream ยังรายงาน Consumed ที่ไม่ใช่ศูนย์สำหรับไบต์ที่มันตรวจไปแล้ว DecodeAllStreams จึงตีความ payload ว่างเปล่าเป็นการ decode ที่สำเร็จและแทน content stream ทุกตัวด้วยสตริงว่าง QDF ที่ได้จึงมีจำนวนหน้าถูกต้อง โครงสร้างใช้ได้ และไม่มีเนื้อหาหน้าเลย ซึ่งเป็นไฟล์ที่เปิดได้โดยไม่มี error ในทุก viewer และไม่แสดงอะไรเลย

// branch FPC ของ InflateStrFromPosition หลัง v3.539.16
// StartPos เป็นแบบ 1-based เหมือนฝั่ง Delphi; clamp มัน แล้วแปลง
// เป็น pointer offset แบบ 0-based ครั้งเดียวตรงขอบพอดี
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 จากนั้น assert ว่าทั้งคู่ได้ payload เดิมและทั้งคู่รายงาน Consumed เท่ากับความยาว stream เต็ม RFC 1950 stream มี header สองไบต์กับ trailer Adler-32 สี่ไบต์ off-by-one ที่ปลายใดปลายหนึ่งจึงไม่ใช่ความเสียหายที่ละเอียดอ่อน แต่เป็น stream ที่เริ่มไม่ขึ้นหรือจบไม่ลง บทเรียนนี้เกี่ยวกับขอบเขต ไม่ใช่เกี่ยวกับ zlib: เมื่อ parameter ของฟังก์ชันนิยามบน index base หนึ่งและ implementation ข้างใต้ใช้ฐานอีกแบบ การแปลงต้องอยู่บรรทัดเดียวเท่านั้น และ test ต้องเรียกมันด้วยค่าที่แยกสองฐานออกจากกัน

ทำไมการที่ TStream.Read อ่านได้สั้นกว่าที่ขอจึงไม่ใช่ปลาย stream

เพราะ TStream.Read ได้รับอนุญาตให้คืนไบต์น้อยกว่าที่ขอด้วยเหตุผลอะไรก็ได้ และมีแค่การคืนค่า 0 เท่านั้นที่หมายถึงไม่มีอะไรเหลือแล้ว TMemoryStream กับ TFileStream บนดิสก์ท้องถิ่นมักเติมเต็มคำขอเสมอ โค้ดที่ถือว่าคืนน้อยกว่าที่ขอเท่ากับสิ้นสุดไฟล์จึงผ่านทุก test ที่ใช้สองตัวนี้ แต่ stream ที่พักบนเครือข่าย stream ที่ decompress และ TStream descendant ตัวใดก็ตามที่ลูกค้าเขียนขึ้นมา คืนสองไบต์ตอนขอหกหมื่นสี่พันได้สบายและยังมีอีกหลายกิกะไบต์ต่อท้าย

TPLBuffer คือ reader ที่ parser ทุกตัวใน PDF Library for Delphi ต้องผ่าน และมันห่อ AnsiString, pointer, byte array หรือ TStream ก็ได้ คำสั่งสแกนทั้งสี่ตัวของมันอย่าง DistanceToByte, DistanceToOtherByte, DistanceToAnyByte และ DistanceToOtherBytes ซึ่งคืน Int64 ทั้งหมด อ่าน source ทีละบล็อก 64 KB เพื่อหา delimiter แล้วรายงานว่าระยะห่างเท่าไรโดยไม่ขยับตำแหน่ง logical แต่ละ loop จบด้วย Until ReadCount < BlockSize สำหรับ source สามแบบที่อยู่ในหน่วยความจำนั่นถูกต้อง เพราะ ReadIntoBuffer ส่งบล็อกเต็มเสมอจนถึงบล็อกสุดท้าย แต่สำหรับ source ที่เป็น stream มันหมายความว่าการสแกนยอมแพ้ตั้งแต่ short read ครั้งแรก รายงานว่าไม่มี delimiter แล้ว tokenizer ข้างบนก็ตัดสินว่า object จบตรงที่มันไม่ได้จบ

แผนภาพการจัดการ short read ใน stream buffer ของ PDFlibPas: DistanceToByte สแกนบล็อกละ 64 KB loop เดิมถือ Until ReadCount < BlockSize เป็นการสิ้นสุดข้อมูลและยอมแพ้ตั้งแต่ short read ครั้งแรก ส่วน loop ที่แก้แล้วรันจน ReadCount เป็นศูนย์ เจอ delimiter และคืนตำแหน่งใน finally block
stream อาจคืนสองไบต์ตอนขอหกหมื่นสี่พัน ศูนย์จึงเป็นสัญญาณสิ้นสุดข้อมูลเดียวที่การสแกนเชื่อได้ และ clause finally คืนตำแหน่ง logical ให้เมื่อเจอ delimiter แล้ว loop ออกก่อนกำหนด
// TPLBuffer.DistanceToByte, loop หลัง 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 ต้องไม่ขยับ reader
End;

test ที่ล็อกพฤติกรรมนี้ไว้คือ descendant ของ TMemoryStream ที่ override Read แล้วจำกัดทุกคำขอไว้ที่สองไบต์ ห่อสตริง aaaaaX ไว้ในนั้น ตั้งตำแหน่ง buffer เป็น 1 แล้วคำสั่งสแกนทั้งสี่ต้องรายงานระยะห่าง 4 ถึง X ปล่อยตำแหน่งไว้ที่ 1 หลังจากนั้น และรายงาน -1 สำหรับไบต์ที่ไม่มีอยู่ ก่อน fix คำสั่งแรกเห็นสองไบต์ สรุปว่า stream หมดแล้ว และคืน -1 ส่วน finally สำคัญพอ ๆ กับเงื่อนไขของ loop: Exit จากข้างในระหว่างการสแกนคือเส้นทางสำเร็จปกติ และตำแหน่ง logical ต้องถูกคืนในเส้นทางนั้นด้วย ไม่ใช่เฉพาะตอน loop วนจนจบ

source เดียว สอง compiler หนึ่งชุด assertion

วินัยที่ได้จากทั้งห้าข้อนี้คือ ข้อความว่าบิลด์ Delphi ผ่านเป็นหลักฐานเกี่ยวกับ Delphi ไม่ใช่เกี่ยวกับ source ตั้งแต่ v3.539.16 ชุด DUnitX ของ Delphi กับชุด console ของ Free Pascal ต่างก็ include Tests\CrossCompilerSemantics.inc ไฟล์เดียวกัน ซึ่งมี routine เดียวชื่อ RunCrossCompilerFileSemantics ที่สร้างเอกสารสองหน้าพร้อม content ที่บีบอัดผ่าน TPDFlib เซฟมัน เซฟซ้ำเป็น QDF ผ่าน SaveQDFToFile ซ่อม QDF ด้วย RepairQDFFile เข้ารหัสไฟล์ธรรมดาด้วย AES-128 ผ่าน EncryptFile และ permission mask จาก EncodePermissions จากนั้นโหลดทุกชิ้นงานกลับมาและ assert สิ่งเดียวกันบนทั้งสอง compiler: จำนวนหน้าเป็น 2 ชื่อเรื่องยังอยู่ ข้อความของหน้าที่สอง extract ออกมาครบจากไฟล์ธรรมดา ไฟล์ที่ซ่อมแล้ว และไฟล์ที่เข้ารหัส รหัสผ่านผิดถูกปฏิเสธพร้อม LastErrorCode ที่ไม่ใช่ศูนย์ EncryptionStrength เป็น 128 EncryptionAlgorithm เป็น 2 และบิต permission แต่ละตัวจาก GetUserPermissions กลับมาตรงกับที่เข้ารหัสไว้เป๊ะ

การเปรียบเทียบจงใจทำให้เป็น normalized ไม่ใช่เทียบทีละไบต์ การเข้ารหัสสุ่ม salt และตัว writer กำหนด document identifier บิลด์ทั้งสองจึงไม่ถูกคาดหวังให้ปล่อยไฟล์ที่เหมือนกันทุกไบต์ แต่ถูกคาดหวังให้ปล่อยไฟล์ที่มีความหมายเดียวกัน และ assertion ก็เขียนไว้ที่ระดับนั้น กิ่ง QDF อยู่ตรงนั้นก็เพราะ bug เรื่อง offset โดยเฉพาะ: QDF ที่มีสองหน้าและไม่มีเนื้อหาผ่านการตรวจจำนวนหน้าแต่ตกการตรวจ extract ข้อความ และ matrix ก็ assert ตัวหลังไว้ fix ในอนาคตตัวใดที่เป็น no-op บน compiler หนึ่งและเปลี่ยนพฤติกรรมบนอีกตัว ซึ่งตรงกับสี่ในห้าข้อข้างบน ตอนนี้ต้องผ่าน assertion ชุดเดิมสองครั้งก่อนจะ ship ได้

อีกครึ่งของการพอร์ตครั้งเดียวกันซึ่งอยู่ที่ขั้น link คือการทำให้ OMF object ของ Delphi กับความคาดหวัง COFF ของ Free Pascal ตรงกัน มีเรื่องของตัวเองอยู่ใน การ link object แบบ static จาก OMF ของ FPC Win32 ไปเป็น COFF ส่วนการเสริมความแข็งแรงเชิงโครงสร้างของ TIFF reader ตัวเดียวกันให้รับมือ BigTIFF กับไฟล์แบบ tiled อยู่ใน บันทึกเรื่อง decoder TIFF ที่มีมาให้ในตัว decoder ในบทความนี้และ test ข้าม compiler ที่ตอนนี้รองรับอยู่ข้างใต้ ship อยู่ใน PDF Library for Delphi สำหรับ Delphi, C++Builder และ Free Pascal ที่ซึ่ง source ชุดเดียวกันต้องได้ผลลัพธ์เดียวกันบนทุก compiler ที่รองรับ ไม่ใช่ได้มาเพราะ compiler ตัวใดตัวหนึ่งช่วยให้