Hi all,
(CCing Amit as the committer of this feature)

This was originally reported to pgsql-security by Anthropic OSS
program but the security team considered it as a non-vuln bug since
it's a v19-beta code, and I'm reporting here on behalf of them as it's
permitted now.

The reported problem is in sequencesync.c; the sequence
synchronization worker uses an integer that came back from the
publisher as a list subscript without checking it, and then writes
through the resulting pointer.

While it's not a problem in normal cases where the publisher is a
normal PostgreSQL, it could lead to out-of-bounds writes when the
publisher is a malicious server looking like a publisher.

Other fields that we get through get_and_validate_seq_info() could
also get the wrong value but they just show the wrong values rather
than OOB writes. So I think we need a safeguard only for seqidx.

I've attached the patch to fix it. Feedback is very welcome.

Regards,

--
Masahiko Sawada
Amazon Web Services: https://aws.amazon.com
From f79d9aa01bbb1f1fffff6a9b81bd9b9de48bac07 Mon Sep 17 00:00:00 2001
From: Masahiko Sawada <[email protected]>
Date: Mon, 21 Sep 2026 12:25:28 -0700
Subject: [PATCH v1] Add a range check on the sequence index from the
 publisher.

The sequencesync worker asks the publisher about a batch of sequences,
tagging each one with its position in the worker's own list, and the
publisher returns that position alongside the sequence's data. The
received position is used to subscript the list using list_nth(),
which bounds-checks only under assertinos, so it was possible that an
index we never sent made the worker read a pointer from past the end
of the list and then store the remote last_value through it.

Check the position against the list before using it. No sane publisher
can trigger this, but we should not let a remote server steer a memory
access. An in-range position from another batch still gets through and
would attach one sequence's data to another, but that's a wrong value
rather than a corrupt process.

Backpatch v19, where the sequence synchronization was introduced.

Reported-by: Anthropic OSS program
Discussion: https://postgr.es/m/
Backpatch-through: 19
---
 src/backend/replication/logical/sequencesync.c | 11 +++++++++++
 1 file changed, 11 insertions(+)

diff --git a/src/backend/replication/logical/sequencesync.c b/src/backend/replication/logical/sequencesync.c
index 6d551d45791..c226af246b9 100644
--- a/src/backend/replication/logical/sequencesync.c
+++ b/src/backend/replication/logical/sequencesync.c
@@ -285,6 +285,17 @@ get_and_validate_seq_info(TupleTableSlot *slot, Relation *sequence_rel,
 	*seqidx = DatumGetInt32(slot_getattr(slot, ++col, &isnull));
 	Assert(!isnull);
 
+	/*
+	 * The publisher only echoes back an index that we put in the VALUES list,
+	 * so this should always identify an entry of seqinfos. Check it anyway
+	 * before using it as a list subscript, since list_nth() does not
+	 * bounds-check outside assert-enabled builds and we would then write the
+	 * remote sequence state through a pointer fetched from beyond the list.
+	 */
+	if (*seqidx < 0 || *seqidx >= list_length(seqinfos))
+		elog(ERROR, "invalid sequence index %d received from the publisher",
+			 *seqidx);
+
 	/* Identify the corresponding local sequence for the given index. */
 	*seqinfo = seqinfo_local =
 		(LogicalRepSequenceInfo *) list_nth(seqinfos, *seqidx);
-- 
2.55.0

Reply via email to