Hi
While investigating an interrupt issue with libxml2 queries I found a
query that cannot be cancelled and runs indefinitely. While inside
libxml2, a backend ignores cancel, statement_timeout and
pg_terminate_backend(), so the only way I found was a kill -9 and a
cluster restart.
psql (20devel)
Type "help" for help.
postgres=# SELECT xpath('/r/text()/following::comment()',
'<!DOCTYPE r [<!ENTITY e "y">]><r>u&e;</r>');
^CCancel request sent
^CCancel request sent
^CCancel request sent
Most likely a bug in libxml2 (xmlXPathNextFollowing) -- I will
investigate that and report.
It only happens because xpath_internal() and XmlTableSetDocument() parse
without XML_PARSE_NOENT, so entity references stay in the tree.
xml_parse() does use it:
...
options = XML_PARSE_NOENT | XML_PARSE_DTDATTR
| (preserve_whitespace ? 0 : XML_PARSE_NOBLANKS);
As a result,xpath() evaluates a different document than the rest of the
xml code sees:
psql (20devel)
Type "help" for help.
postgres=# select xmlserialize(document '<!DOCTYPE r [<!ENTITY e
"<b>z</b>">]><r>&e;&e;</r>'::xml as text indent);
xmlserialize
------------------------
<!DOCTYPE r [ +
<!ENTITY e "<b>z</b>">+
]> +
<r> +
<b>z</b> +
<b>z</b> +
</r>
(1 row)
postgres=# select xpath('count(//b)', '<!DOCTYPE r [<!ENTITY e
"<b>z</b>">]><r>&e;&e;</r>');
xpath
-------
{0}
(1 row)
postgres=# with t (x) as (
select xmlserialize(document '<!DOCTYPE r [<!ENTITY e
"<b>z</b>">]><r>&e;&e;</r>'::xml as text indent)
)
select xpath('count(//b)', x::xml) from t;
xpath
-------
{2}
(1 row)
BTW, the deprecated xml2 parses with XML_PARSE_NOENT .
The attached patch adds XML_PARSE_NOENT to both calls (+ some tests).
That fixes the endless loop and makes xpath(), xmlexists() and XMLTABLE
see entity content.
psql (20devel)
Type "help" for help.
postgres=# SELECT xpath('/r/text()/following::comment()',
'<!DOCTYPE r [<!ENTITY e "y">]><r>u&e;</r>');
xpath
-------
{}
(1 row)
postgres=# select xpath('count(//b)', '<!DOCTYPE r [<!ENTITY e
"<b>z</b>">]><r>&e;&e;</r>');
xpath
-------
{2}
(1 row)
This changes results for documents that use entity references, but it's
arguably an improvement, e.g.
-- master
postgres=# select xpath('/r/text()', '<!DOCTYPE r [<!ENTITY e
"y">]><r>u&e;</r>');
xpath
-------
{u}
(1 row)
-- patched
postgres=# select xpath('/r/text()', '<!DOCTYPE r [<!ENTITY e
"y">]><r>u&e;</r>');
xpath
-------
{uy}
(1 row)
Documents without entity references are not affected.
WDYT?
Best, JimFrom 7492b39c54f51c01323111daadbbf3ce8353609e Mon Sep 17 00:00:00 2001
From: Jim Jones <[email protected]>
Date: Thu, 29 Oct 2026 11:37:06 +0100
Subject: [PATCH v1] Expand entity references in xpath(), xmlexists() and
XMLTABLE
xpath_internal() and XmlTableSetDocument() parse the document without
XML_PARSE_NOENT, so references to internal entities stay in the tree
as entity reference nodes. xml_parse() uses XML_PARSE_NOENT, so XPath
evaluation sees a different document than the rest of the xml code.
Parse with XML_PARSE_NOENT in both places, as xml_parse() and
contrib/xml2 already do. This changes results for documents that use
entity references; e.g. /r/text() on <r>u&e;</r> now returns "uy"
instead of "u". Documents without entity references are not affected.
---
src/backend/utils/adt/xml.c | 5 +++--
src/test/regress/expected/xml.out | 26 ++++++++++++++++++++++++++
src/test/regress/sql/xml.sql | 6 ++++++
3 files changed, 35 insertions(+), 2 deletions(-)
diff --git a/src/backend/utils/adt/xml.c b/src/backend/utils/adt/xml.c
index dab00954c6f..99bdbe18ee7 100644
--- a/src/backend/utils/adt/xml.c
+++ b/src/backend/utils/adt/xml.c
@@ -4479,7 +4479,7 @@ xpath_internal(text *xpath_expr_text, xmltype *data, ArrayType *namespaces,
xml_ereport(xmlerrcxt, ERROR, ERRCODE_OUT_OF_MEMORY,
"could not allocate parser context");
doc = xmlCtxtReadMemory(ctxt, (char *) string + xmldecl_len,
- len - xmldecl_len, NULL, NULL, 0);
+ len - xmldecl_len, NULL, NULL, XML_PARSE_NOENT);
if (doc == NULL || xmlerrcxt->err_occurred)
xml_ereport(xmlerrcxt, ERROR, ERRCODE_INVALID_XML_DOCUMENT,
"could not parse XML document");
@@ -4816,7 +4816,8 @@ XmlTableSetDocument(TableFuncScanState *state, Datum value)
PG_TRY();
{
- doc = xmlCtxtReadMemory(xtCxt->ctxt, (char *) xstr, length, NULL, NULL, 0);
+ doc = xmlCtxtReadMemory(xtCxt->ctxt, (char *) xstr, length, NULL, NULL,
+ XML_PARSE_NOENT);
if (doc == NULL || xtCxt->xmlerrcxt->err_occurred)
xml_ereport(xtCxt->xmlerrcxt, ERROR, ERRCODE_INVALID_XML_DOCUMENT,
"could not parse XML document");
diff --git a/src/test/regress/expected/xml.out b/src/test/regress/expected/xml.out
index 6998745ee03..0e2c5e02680 100644
--- a/src/test/regress/expected/xml.out
+++ b/src/test/regress/expected/xml.out
@@ -1309,6 +1309,32 @@ SELECT XMLPARSE(DOCUMENT '<!DOCTYPE chapter PUBLIC "-//OASIS//DTD DocBook XML V4
<!DOCTYPE chapter PUBLIC "-//OASIS//DTD DocBook XML V4.1.2//EN" "http://www.oasis-open.org/docbook/xml/4.1.2/docbookx.dtd"><chapter> </chapter>
(1 row)
+-- Entity references are expanded before XPath evaluation.
+SELECT xpath('/r/text()', '<!DOCTYPE r [<!ENTITY e "y">]><r>u&e;</r>');
+ xpath
+-------
+ {uy}
+(1 row)
+
+SELECT xpath('count(//b)', '<!DOCTYPE r [<!ENTITY e "<b>z</b>">]><r>&e;&e;</r>');
+ xpath
+-------
+ {2}
+(1 row)
+
+SELECT xmlexists('//b' PASSING BY REF '<!DOCTYPE r [<!ENTITY e "<b>z</b>">]><r>&e;</r>');
+ xmlexists
+-----------
+ t
+(1 row)
+
+SELECT * FROM XMLTABLE('//b' PASSING '<!DOCTYPE r [<!ENTITY e "<b>z</b>">]><r>&e;&e;</r>' COLUMNS b text PATH '.');
+ b
+---
+ z
+ z
+(2 rows)
+
-- XMLPATH tests
CREATE TABLE xmldata(data xml);
INSERT INTO xmldata VALUES('<ROWS>
diff --git a/src/test/regress/sql/xml.sql b/src/test/regress/sql/xml.sql
index 0581d21b03f..251eafbae17 100644
--- a/src/test/regress/sql/xml.sql
+++ b/src/test/regress/sql/xml.sql
@@ -408,6 +408,12 @@ SELECT XMLPARSE(DOCUMENT '<!DOCTYPE foo [<!ENTITY c SYSTEM "/etc/no.such.file">]
-- This might or might not load the requested DTD, but it mustn't throw error.
SELECT XMLPARSE(DOCUMENT '<!DOCTYPE chapter PUBLIC "-//OASIS//DTD DocBook XML V4.1.2//EN" "http://www.oasis-open.org/docbook/xml/4.1.2/docbookx.dtd"><chapter> </chapter>');
+-- Entity references are expanded before XPath evaluation.
+SELECT xpath('/r/text()', '<!DOCTYPE r [<!ENTITY e "y">]><r>u&e;</r>');
+SELECT xpath('count(//b)', '<!DOCTYPE r [<!ENTITY e "<b>z</b>">]><r>&e;&e;</r>');
+SELECT xmlexists('//b' PASSING BY REF '<!DOCTYPE r [<!ENTITY e "<b>z</b>">]><r>&e;</r>');
+SELECT * FROM XMLTABLE('//b' PASSING '<!DOCTYPE r [<!ENTITY e "<b>z</b>">]><r>&e;&e;</r>' COLUMNS b text PATH '.');
+
-- XMLPATH tests
CREATE TABLE xmldata(data xml);
INSERT INTO xmldata VALUES('<ROWS>
--
2.56.0