Hi

While investigating a libxml2 behaviour I found a bug in
xmltotext_with_options(): xmlserialize(.. indent) can crash the backend.

How to reproduce:

== master ==

psql (20devel)
Type "help" for help.

postgres=# \timing on
Timing is on.
postgres=# select xmlserialize(document ('<r>' || repeat('<b>bar</b>',
100000000) || '</r>')::xml AS text indent)::xml;
server closed the connection unexpectedly
        This probably means the server terminated abnormally
        before or while processing the request.
The connection to the server was lost. Attempting reset: Failed.
The connection to the server was lost. Attempting reset: Failed.
Time: 111002,809 ms (01:51,003)


The problem I see in this case is that ...

if (xmlSaveClose(ctxt) == -1 || xmlerrcxt->err_occurred)
{
  ctxt = NULL;            /* don't try to close it again */
}

... never sets ctxt to NULL, because xmlSaveClose succeeded and
xmlerrcxt->err_occurred is false -- no error just yet. Down the road ...

result = cstring_to_text_with_len(str, len)

... tries to palloc a result that is too large and jumps to PG_CATCH(),
which passes "if (ctxt)" but ctxt now points to a dangling address.

The patch attached makes the ctxt = NULL unconditional and stores the
value of xmlSaveClose(ctxt) into a variable, so that we can still call
xml_ereport in case of an error.


== patched ==

psql (20devel)
Type "help" for help.

postgres=# \timing on
Timing is on.
postgres=# select xmlserialize(document ('<r>' || repeat('<b>bar</b>',
100000000) || '</r>')::xml AS text indent)::xml;
ERROR:  invalid memory alloc request size 1300000012
Time: 69742,848 ms (01:09,743)


Thoughts?

Best, Jim


From 589142029734f27bb92b94f0a7430e5d2d0a1205 Mon Sep 17 00:00:00 2001
From: Jim Jones <[email protected]>
Date: Mon, 2 Nov 2026 00:50:57 +0100
Subject: [PATCH v1] Fix use-after-free when XMLSERIALIZE(... INDENT) output
 exceeds 1GB

In xmltotext_with_options(), xmlSaveClose() frees the save context
unconditionally, but ctxt was only reset to NULL on the error branch.
When the close succeeded and a later step threw an error --
cstring_to_text_with_len() rejecting a result larger than MaxAllocSize
-- control reached the PG_CATCH block with ctxt still pointing at the
freed context, so xmlSaveClose() was called a second time on freed
memory and crashed the backend.

Reset ctxt to NULL immediately after xmlSaveClose(), before any code
that can throw, keeping its return value in a separate variable for the
error check.
---
 src/backend/utils/adt/xml.c | 12 ++++++++----
 1 file changed, 8 insertions(+), 4 deletions(-)

diff --git a/src/backend/utils/adt/xml.c b/src/backend/utils/adt/xml.c
index 99bdbe18ee7..03301999fd4 100644
--- a/src/backend/utils/adt/xml.c
+++ b/src/backend/utils/adt/xml.c
@@ -733,6 +733,7 @@ xmltotext_with_options(xmltype *data, XmlOptionType xmloption_arg, bool indent)
 	PG_TRY();
 	{
 		size_t		decl_len = 0;
+		int			close_result;
 
 		xmlerrcxt = pg_xml_init(PG_XML_STRICTNESS_ALL);
 
@@ -817,12 +818,15 @@ xmltotext_with_options(xmltype *data, XmlOptionType xmloption_arg, bool indent)
 			xmlFreeNode(newline);
 		}
 
-		if (xmlSaveClose(ctxt) == -1 || xmlerrcxt->err_occurred)
-		{
-			ctxt = NULL;		/* don't try to close it again */
+		/*
+		 * xmlSaveClose frees ctxt whether or not it succeeds, so forget it
+		 * before anything below can throw an error.
+		 */
+		close_result = xmlSaveClose(ctxt);
+		ctxt = NULL;			/* don't try to close it again */
+		if (close_result == -1 || xmlerrcxt->err_occurred)
 			xml_ereport(xmlerrcxt, ERROR, ERRCODE_INTERNAL_ERROR,
 						"could not close xmlSaveCtxtPtr");
-		}
 
 		/*
 		 * xmlDocContentDumpOutput may add a trailing newline, so remove that.
-- 
2.56.0

Reply via email to