[php-src] PR #23744: Fix GH-23729: UAF when re-constructing DOMXPath during evaluation
| From: | iliasabk | Date: | Fri, 18 Sep 2026 00:00:22 +0000 |
| Subject: | [php-src] PR #23744: Fix GH-23729: UAF when re-constructing DOMXPath during evaluation | ||
| Groups: | php.git-pulls | ||
| Request: | Send a blank email to git-pulls+get-38548@lists.php.net to get a copy of this message | ||
Pull Request: https://github.com/php/php-src/pull/23744
Author: iliasabk
## Issue
Fixes #23729 — heap-use-after-free (WRITE of size 8) in
php_xpath_eval when
DOMXPath::__construct() is invoked reentrantly from a php:function()
callback during expression evaluation.
## Root cause
php_xpath_eval() fetches the object's xmlXPathContextPtr and calls
xmlXPathEvalExpression(). If the expression invokes a php:function()
callback, arbitrary userland code runs while the evaluation is in flight. Calling
__construct() on the *same* DOMXPath object from inside that callback
frees the live xmlXPathContext (xmlXPathFreeContext in
dom_xpath_construct) — the still-running evaluation then writes into freed memory
(ASan WRITE(8) in php_xpath_eval, freed in dom_xpath_construct, exactly
matching the reported trace).
## Fix
Add an evaluation_count to dom_xpath_object, incremented around the
xmlXPathEvalExpression() call in php_xpath_eval().
dom_xpath_construct() refuses to re-initialize the object while an evaluation is in
flight:
Error: Cannot re-initialize DOMXPath while an XPath expression is being evaluated
Re-constructing outside of evaluation stays legal (the existing
DOMXPath_reconstruct_callbacks_* tests still pass), and constructing a *different*
DOMXPath inside a callback is unaffected — the guard only covers the destructive
re-init of the object whose context is currently being used.
## Verification
- Unpatched ASan build: the reporter's PoC reproduces the heap-use-after-free WRITE(8) in
php_xpath_eval, freed by dom_xpath_construct — identical to the report.
- Patched build: PoC throws Error: Cannot re-initialize DOMXPath while an XPath expression is
being evaluated; no sanitizer findings, and the object remains usable for subsequent
evaluate() calls.
- New regression test ext/dom/tests/DOMXPath_reconstruct_during_eval.phpt.
- run-tests.php ext/dom/tests/: 867/870 pass; the 3 failures (gh23116,
gh23120, gh23117 deep-nesting stack tests) are environment artifacts of
this minimal local build (stack-size detection), unrelated to this change.
## Falsification
- The guard counts *evaluations in flight* rather than blocking __construct outright,
so the documented "re-construct to reset registrations" pattern keeps working — verified
by DOMXPath_reconstruct_callbacks_gc.phpt and
DOMXPath_reconstruct_callbacks_ns_gc.phpt still passing.
- Alternative considered and rejected: deferring the context free (refcount/swap) until evaluation
ends — it still tears down callback registrations mid-eval and changes more observable behavior
than refusing the re-init.
- Residual risk: a callback mutating the *document* mid-eval is a different, already-covered class
(xpath_php_function_removes_argument_node.phpt); nested evaluate() calls
remain legal since the counter permits reentrant non-destructive evaluation.
- Confidence: high — direct root-cause guard, red→green verified against the exact ASan trace,
full ext/dom suite green.