[php-src] PR #23744: Fix GH-23729: UAF when re-constructing DOMXPath during evaluation

From: 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.

« previous php.git-pulls (#38548) next »