From 726fbc8ec0792d7337cacd08a849f37b69271a39 Mon Sep 17 00:00:00 2001 From: Zsolt Parragi Date: Thu, 15 Oct 2026 08:54:55 +0000 Subject: [PATCH v1] pg_upgrade: Fix last multixid ending at offset wraparound On v18 and below, if the newest multixid's members end exactly at the 32-bit offset wraparound point, the next offset in the control file is 0. GetOldMultiXactIdSingleMember() treated that as an invalid entry and dropped the multixid's members, so after upgrade accessing the tuple failed with "MultiXact N has invalid offset". To fix, only check for zero offset when it was read from the next multixid's slot, like GetMultiXactIdMembers() did in v18. This is unlikely to happen in practice, but better to get it right. The bug came with commit bd8d9c9bdfa in v19. Backpatch-through: 19 --- src/bin/pg_upgrade/multixact_read_v18.c | 17 ++++--- .../pg_upgrade/t/007_multixact_conversion.pl | 44 +++++++++++++++++++ 2 files changed, 55 insertions(+), 6 deletions(-) diff --git a/src/bin/pg_upgrade/multixact_read_v18.c b/src/bin/pg_upgrade/multixact_read_v18.c index c92b977530a..1a8dee29eb2 100644 --- a/src/bin/pg_upgrade/multixact_read_v18.c +++ b/src/bin/pg_upgrade/multixact_read_v18.c @@ -226,7 +226,11 @@ GetOldMultiXactIdSingleMember(OldMultiXactReader *state, MultiXactId multi, if (nextMXact == tmpMXact) { - /* Corner case 1: there is no next multixact */ + /* + * Corner case 1: there is no next multixact. nextOffset can be zero + * here, if the members of this multixact ended exactly at offset + * wraparound. The length calculation below copes with that. + */ nextMXOffset = nextOffset; } else @@ -246,13 +250,14 @@ GetOldMultiXactIdSingleMember(OldMultiXactReader *state, MultiXactId multi, offptr = (MultiXactOffset32 *) buf; offptr += entryno; nextMXOffset = *offptr; - } - if (nextMXOffset == 0) - { - /* Invalid entry. These can be left behind on a server crash. */ - return false; + if (nextMXOffset == 0) + { + /* Invalid entry. These can be left behind on a server crash. */ + return false; + } } + length = nextMXOffset - offset; if (length < 0) diff --git a/src/bin/pg_upgrade/t/007_multixact_conversion.pl b/src/bin/pg_upgrade/t/007_multixact_conversion.pl index 867a0623153..5c87c2527ee 100644 --- a/src/bin/pg_upgrade/t/007_multixact_conversion.pl +++ b/src/bin/pg_upgrade/t/007_multixact_conversion.pl @@ -440,4 +440,48 @@ SKIP: upgrade_and_compare($tag, $old, $new); } +# Offset wraparound at the last multixid: The members of the newest +# multixid end exactly at the 32-bit offset wraparound point, so that +# the old cluster's next offset is 0. +SKIP: +{ + skip + "skipping mxoffset conversion tests because upgrading from the old version does not require conversion" + if ($old_version >= '19devel'); + + my $tag = 'last_wraparound'; + my $old = + PostgreSQL::Test::Cluster->new("${tag}_oldnode", + install_path => $ENV{oldinstall}); + my $new = PostgreSQL::Test::Cluster->new("${tag}_newnode"); + + $old->init(extra => ['-k']); + + reset_mxid_mxoffset_pre_v19($old, 0xFFFFFA00, 0xFFFFFFFE); + + # Create one multixid with two members, a locker and an updater + $old->start; + $old->safe_psql( + 'postgres', qq[ + CREATE TABLE mxofftest (id INT PRIMARY KEY, n_updated INT) + WITH (AUTOVACUUM_ENABLED=FALSE); + INSERT INTO mxofftest SELECT G, 0 FROM GENERATE_SERIES(1, 2) G; + ]); + my $locker = + $new->background_psql('', connstr => $old->connstr('postgres')); + $locker->query_safe("BEGIN"); + $locker->query_safe("SELECT * FROM mxofftest WHERE id = 1 FOR KEY SHARE"); + $old->safe_psql('postgres', + "UPDATE mxofftest SET n_updated = n_updated + 1 WHERE id = 1"); + $locker->query_safe("COMMIT"); + $locker->quit; + $old->stop; + + my (undef, undef, $finish_mxoff) = read_multixid_fields($old); + is($finish_mxoff, 0, "mxoff wrapped around to 0 in old cluster"); + + $new->init; + upgrade_and_compare($tag, $old, $new); +} + done_testing(); -- 2.56.0