Hi, While following up on the recent xml2 XPath leak fixes at https://postgr.es/m/20260601010124.5edf9a20@andrnote, I noticed the same class of libxml/libxslt ownership issue in xslt_process().
xslt_process() parses the stylesheet argument with xmlReadMemory(), then passes the resulting xmlDoc to xsltParseStylesheetDoc(). On failure, libxslt leaves that document owned by the caller, as can be seen from its own xsltParseStylesheetFile() wrapper. Postgres currently cannot release it in the error cleanup path because ssdoc is scoped inside the PG_TRY block. The attached patch keeps ssdoc visible to the cleanup path, clears it once ownership has been transferred to the stylesheet, and frees it in PG_CATCH if parsing failed before that transfer completed. I also attached a manual repro script. It repeatedly calls xslt_process() with a large XML document that is not a stylesheet, catches the ERROR in one backend, and samples VmRSS via /proc. On my machine, without the patch, the same backend kept growing after each failed parse: NOTICE: xslt_process i=1, failures=1, total_kb=34668, diff_kb=12992 NOTICE: xslt_process i=2, failures=1, total_kb=44428, diff_kb=9760 NOTICE: xslt_process i=3, failures=1, total_kb=54120, diff_kb=9692 NOTICE: xslt_process i=4, failures=1, total_kb=63808, diff_kb=9688 NOTICE: xslt_process i=5, failures=1, total_kb=73496, diff_kb=9688 NOTICE: xslt_process i=6, failures=1, total_kb=83188, diff_kb=9692 NOTICE: xslt_process i=7, failures=1, total_kb=92876, diff_kb=9688 NOTICE: xslt_process i=8, failures=1, total_kb=102564, diff_kb=9688 NOTICE: xslt_process i=9, failures=1, total_kb=112256, diff_kb=9692 NOTICE: xslt_process i=10, failures=1, total_kb=121944, diff_kb=9688 With the patch, it plateaued after the initial warmup: NOTICE: xslt_process i=1, failures=1, total_kb=23228, diff_kb=1596 NOTICE: xslt_process i=2, failures=1, total_kb=23888, diff_kb=660 NOTICE: xslt_process i=3, failures=1, total_kb=23888, diff_kb=0 NOTICE: xslt_process i=4, failures=1, total_kb=23888, diff_kb=0 NOTICE: xslt_process i=5, failures=1, total_kb=23888, diff_kb=0 NOTICE: xslt_process i=6, failures=1, total_kb=23888, diff_kb=0 NOTICE: xslt_process i=7, failures=1, total_kb=23888, diff_kb=0 NOTICE: xslt_process i=8, failures=1, total_kb=23888, diff_kb=0 NOTICE: xslt_process i=9, failures=1, total_kb=23888, diff_kb=0 NOTICE: xslt_process i=10, failures=1, total_kb=23888, diff_kb=0 -- Andrey Chernyy
xml2-xslt-process-leak-repro.sql
Description: application/sql
>From c183f9b1d0d1bceb2e57db64ee928d7697b175d4 Mon Sep 17 00:00:00 2001 From: Andrey Chernyy <[email protected]> Date: Fri, 5 Jun 2026 02:43:39 +0300 Subject: [PATCH] xml2: Fix stylesheet document leak in xslt_process() xslt_process() parses the stylesheet text into an xmlDoc before passing it to xsltParseStylesheetDoc(). On success, the returned stylesheet owns that document and frees it through xsltFreeStylesheet(). On failure, libxslt leaves the caller responsible for the xmlDoc, as shown by its own xsltParseStylesheetFile() wrapper. Keep tracking the stylesheet document until ownership has been transferred so the error cleanup path can free it. --- contrib/xml2/xslt_proc.c | 11 +++++++++-- 1 file changed, 9 insertions(+), 2 deletions(-) diff --git a/contrib/xml2/xslt_proc.c b/contrib/xml2/xslt_proc.c index 8ceb8c46494..b83d28fe579 100644 --- a/contrib/xml2/xslt_proc.c +++ b/contrib/xml2/xslt_proc.c @@ -55,6 +55,7 @@ xslt_process(PG_FUNCTION_ARGS) PgXmlErrorContext *xmlerrcxt; volatile xsltStylesheetPtr stylesheet = NULL; volatile xmlDocPtr doctree = NULL; + volatile xmlDocPtr ssdoc = NULL; volatile xmlDocPtr restree = NULL; volatile xsltSecurityPrefsPtr xslt_sec_prefs = NULL; volatile xsltTransformContextPtr xslt_ctxt = NULL; @@ -78,7 +79,6 @@ xslt_process(PG_FUNCTION_ARGS) PG_TRY(); { - xmlDocPtr ssdoc; bool xslt_sec_prefs_error; int reslen = 0; @@ -100,8 +100,13 @@ xslt_process(PG_FUNCTION_ARGS) xml_ereport(xmlerrcxt, ERROR, ERRCODE_INVALID_XML_DOCUMENT, "error parsing stylesheet as XML document"); - /* After this call we need not free ssdoc separately */ + /* + * On success, the stylesheet owns ssdoc. On failure, libxslt leaves + * the caller responsible for freeing ssdoc. + */ stylesheet = xsltParseStylesheetDoc(ssdoc); + if (stylesheet != NULL) + ssdoc = NULL; if (stylesheet == NULL || pg_xml_error_occurred(xmlerrcxt)) xml_ereport(xmlerrcxt, ERROR, ERRCODE_INVALID_ARGUMENT_FOR_XQUERY, @@ -167,6 +172,8 @@ xslt_process(PG_FUNCTION_ARGS) xsltFreeSecurityPrefs(xslt_sec_prefs); if (stylesheet != NULL) xsltFreeStylesheet(stylesheet); + if (ssdoc != NULL) + xmlFreeDoc(ssdoc); if (doctree != NULL) xmlFreeDoc(doctree); if (resstr != NULL) -- 2.54.0
