Skip to content

Keep Python opaque data alive for the lifetime of the request - #2445

Open
Gopalakrishnan Nallasamy (GopalakrishnanN) wants to merge 1 commit into
microsoft:mainfrom
GopalakrishnanN:fix/python-opaque-data-lifetime
Open

Keep Python opaque data alive for the lifetime of the request#2445
Gopalakrishnan Nallasamy (GopalakrishnanN) wants to merge 1 commit into
microsoft:mainfrom
GopalakrishnanN:fix/python-opaque-data-lifetime

Conversation

@GopalakrishnanN

Copy link
Copy Markdown
Contributor

Request.SetOpaqueData() stores the PyObject* unowned by design, and the Python binding passed opaque_data.ptr() straight through without holding a reference. Anything that is not independently kept alive by the caller is therefore freed as soon as set_opaque_data() returns, and get_opaque_data() hands back a dangling pointer.

The natural way to write it is a use-after-free:

req.set_opaque_data(Sink(prompt))     # temporary, freed immediately
...
sink = ready.get_opaque_data()        # dangling
sink.text += ...                      # general protection fault in libpython

That faults inside libpython rather than raising, so it looks like a runtime bug rather than an ownership mistake. The shipped continuous-batching example only avoids it because it passes a long-lived RequestPool.

Add pybind11::keep_alive<1, 2> so the object lives as long as the request that references it.

Request.SetOpaqueData() stores the PyObject* unowned by design, and the Python
binding passed opaque_data.ptr() straight through without holding a reference.
Anything that is not independently kept alive by the caller is therefore freed
as soon as set_opaque_data() returns, and get_opaque_data() hands back a dangling
pointer.

The natural way to write it is a use-after-free:

    req.set_opaque_data(Sink(prompt))     # temporary, freed immediately
    ...
    sink = ready.get_opaque_data()        # dangling
    sink.text += ...                      # general protection fault in libpython

That faults inside libpython rather than raising, so it looks like a runtime bug
rather than an ownership mistake. The shipped continuous-batching example only
avoids it because it passes a long-lived RequestPool.

Add pybind11::keep_alive<1, 2> so the object lives as long as the request that
references it.

Copilot AI left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Pull request overview

This PR fixes a Python binding lifetime hazard in ONNX Runtime GenAI’s request “opaque data” feature by ensuring Python objects passed to Request.set_opaque_data() are kept alive for as long as the Request instance that references them, preventing use-after-free crashes when retrieving the pointer later.

Changes:

  • Adds a pybind11::keep_alive<1, 2> call policy to Request.set_opaque_data() so the Python object’s lifetime is tied to the Request.
  • Adds an in-code comment explaining the unowned PyObject* storage in the core and why the binding must enforce lifetime.

💡 Add a code-review agent skill or configure MCP servers for context-aware, tailored reviews. Learn more in the docs.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants