Skip to content

Fix use-after-free in XPath php:function argument nodes#22468

Closed
iliaal wants to merge 1 commit into
php:PHP-8.4from
iliaal:fix/dom-xpath-callback-arg-uaf
Closed

Fix use-after-free in XPath php:function argument nodes#22468
iliaal wants to merge 1 commit into
php:PHP-8.4from
iliaal:fix/dom-xpath-callback-arg-uaf

Conversation

@iliaal

@iliaal iliaal commented Jun 26, 2026

Copy link
Copy Markdown
Contributor

When an XPath php:function() callback removes a node passed to it as an argument, the node is freed the moment the callback returns because the argument cleanup drops the only proxy holding it, while libxml still references that node in the running evaluation's node-set. The read inside xmlXPathEvalExpression() is then a use-after-free. Returned nodes are already kept alive in the registry node list for the evaluation's duration; argument node and node-set proxies now get the same treatment.

A php:function() callback receives DOM node arguments as proxies that own
the underlying libxml node. If the callback detaches such a node, the
argument cleanup frees it while libxml is still evaluating the expression
and still references it in the result node-set. Keep node and node-set
argument proxies alive until evaluation ends, as returned nodes already are.
@devnexen

Copy link
Copy Markdown
Member

question: args now land in node_list, but DOMXPath::query/evaluate never flushes it (only XSLT does). So a long-lived DOMXPath passing nodes to php:function callbacks holds every arg proxy until destruction, where before they were freed right away. is it a conscious trade off ?

@iliaal

iliaal commented Jun 26, 2026

Copy link
Copy Markdown
Contributor Author

Conscious, yes. The return-value proxies already lived in node_list for the object's lifetime (the callback_retval insert just above this one), so this gives the argument proxies the same treatment rather than a new lifetime model.

I deliberately avoided flushing per query/evaluate: a php:function callback can re-enter with another query()/evaluate() on the same object, and the nested call shares this node_list. Flushing at the end of an evaluation would free the outer call's still-live arg proxies and bring the use-after-free right back. XSLT cleans only at the top of the transform for the same reason. The proxies are thin wrappers, released on object destruction.

@iliaal iliaal closed this in f9821dd Jun 27, 2026
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants