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