Hi

xml_errorHandler() appends every libxml2 error to err_buf without a
limit. libxml2 caps reported parser errors at 100 (XML_MAX_ERRORS [1,2])
since 2.11, but older versions, still shipped by current distributions,
report them all. A document with one non-fatal namespace error per
element can make err_buf reach MaxAllocSize, and enlargeStringInfo()
then raises ERROR from inside the libxml2 callback. The longjmp abandons
the document libxml2 was building, so it is not freed until the session
ends.

Any user can trigger it with xpath(), xmlexists() or xmltable. Depending
on the host, a few calls get the backend killed by the OOM killer. I was
able to reproduce it with the postgres:18 docker image:

$ podman run -d --name pgtest --memory 3g --memory-swap 3g -e
POSTGRES_HOST_AUTH_METHOD=trust postgres:18 -c logging_collector=on
18b8de253790882cbbf3a0f63828d1ea136723f6d2e7a3f3379cf8e079b08f71

$ podman exec -itu postgres pgtest psql
psql (18.6 (Debian 18.6-1.pgdg13+2))
Type "help" for help.

postgres=# select xpath('/r', ('<r>' || repeat('<p:a/>', 5200000) ||
'</r>')::xml) is not null;
ERROR:  string buffer exceeds maximum allowed length (1073741823 bytes)
DETAIL:  Cannot enlarge string buffer containing 1073741630 bytes by 208
more bytes.
postgres=# select xpath('/r', ('<r>' || repeat('<p:a/>', 5200000) ||
'</r>')::xml) is not null;
ERROR:  string buffer exceeds maximum allowed length (1073741823 bytes)
DETAIL:  Cannot enlarge string buffer containing 1073741630 bytes by 208
more bytes.
postgres=# select xpath('/r', ('<r>' || repeat('<p:a/>', 5200000) ||
'</r>')::xml) is not null;
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.
!?> \! grep oom_kill /sys/fs/cgroup/memory.events
oom_kill 1
!?> \! grep -h "terminated by signal" $PGDATA/log/*.log
2026-11-11 10:18:22.585 UTC [1] LOG:  client backend (PID 76) was
terminated by signal 9: Killed
!?>

The attached patch stops collecting messages after 100, the same limit
libxml2 2.11+ applies itself with XML_MAX_ERRORS, but I am not sure this
is the right way to fix it. Any suggestions on this approach?

Best, Jim

1 - https://github.com/GNOME/libxml2/blob/master/parserInternals.c#L54
2 -
https://github.com/GNOME/libxml2/commit/59b33661784359c6d3a8309ddbd2129fb2688548
From 00a0490e138b0beb2724d6a23776c782373023fb Mon Sep 17 00:00:00 2001
From: Jim Jones <[email protected]>
Date: Wed, 11 Nov 2026 11:32:25 +0100
Subject: [PATCH v1] Limit the number of libxml2 messages collected in err_buf

xml_errorHandler() appended every libxml2 error to err_buf.  libxml2
versions before 2.11 do not limit the number of reported parser errors,
and a document can produce one non-fatal namespace error per element,
so err_buf could grow to MaxAllocSize.  The resulting ereport(ERROR)
was thrown from inside the libxml2 callback, longjmp'ing out of the
parser, and the partially built document was never freed.

Stop collecting messages after 100 of them.  This is the value of
libxml2's XML_MAX_ERRORS, added in 2.11.0.
---
 src/backend/utils/adt/xml.c | 28 ++++++++++++++++++++++++++++
 1 file changed, 28 insertions(+)

diff --git a/src/backend/utils/adt/xml.c b/src/backend/utils/adt/xml.c
index 44a3a21d135..a0c11f50f07 100644
--- a/src/backend/utils/adt/xml.c
+++ b/src/backend/utils/adt/xml.c
@@ -113,6 +113,14 @@ int			xmloption = XMLOPTION_CONTENT;
 /* random number to identify PgXmlErrorContext */
 #define ERRCXT_MAGIC	68275028
 
+/*
+ * Maximum number of libxml messages accumulated in err_buf.  This mimics
+ * libxml2's XML_MAX_ERRORS, which limits the parser errors reported per
+ * parser context since libxml2 2.11.  Older versions report them all, so
+ * we apply the same limit ourselves.
+ */
+#define PG_XML_MAX_ERRORS	100
+
 struct PgXmlErrorContext
 {
 	int			magic;
@@ -121,6 +129,8 @@ struct PgXmlErrorContext
 	/* current error status and accumulated message, if any */
 	bool		err_occurred;
 	StringInfoData err_buf;
+	/* number of messages in err_buf */
+	int			err_count;
 	/* previous libxml error handling state (saved by pg_xml_init) */
 	xmlStructuredErrorFunc saved_errfunc;
 	void	   *saved_errcxt;
@@ -1245,6 +1255,7 @@ pg_xml_init(PgXmlStrictness strictness)
 	errcxt->strictness = strictness;
 	errcxt->err_occurred = false;
 	initStringInfo(&errcxt->err_buf);
+	errcxt->err_count = 0;
 
 	/*
 	 * Save original error handler and install ours. libxml originally didn't
@@ -2227,6 +2238,21 @@ xml_errorHandler(void *data, PgXmlErrorPtr error)
 			break;
 	}
 
+	/*
+	 * Stop accumulating messages in err_buf once there are enough of them. A
+	 * document can produce one non-fatal error per element, and an unbounded
+	 * err_buf could exceed MaxAllocSize, which would ereport() from here and
+	 * longjmp out of libxml.
+	 */
+	if ((xmlerrcxt->strictness == PG_XML_STRICTNESS_LEGACY ||
+		 level >= XML_ERR_ERROR) &&
+		xmlerrcxt->err_count >= PG_XML_MAX_ERRORS)
+	{
+		if (xmlerrcxt->strictness != PG_XML_STRICTNESS_LEGACY)
+			xmlerrcxt->err_occurred = true;
+		return;
+	}
+
 	/* Prepare error message in errorBuf */
 	initStringInfo(&errorBuf);
 
@@ -2283,6 +2309,7 @@ xml_errorHandler(void *data, PgXmlErrorPtr error)
 		appendStringInfoLineSeparator(&xmlerrcxt->err_buf);
 		appendBinaryStringInfo(&xmlerrcxt->err_buf, errorBuf.data,
 							   errorBuf.len);
+		xmlerrcxt->err_count++;
 
 		pfree(errorBuf.data);
 		return;
@@ -2301,6 +2328,7 @@ xml_errorHandler(void *data, PgXmlErrorPtr error)
 		appendStringInfoLineSeparator(&xmlerrcxt->err_buf);
 		appendBinaryStringInfo(&xmlerrcxt->err_buf, errorBuf.data,
 							   errorBuf.len);
+		xmlerrcxt->err_count++;
 
 		xmlerrcxt->err_occurred = true;
 	}
-- 
2.56.0

Reply via email to