From 2323f03a1789bc52bee980c56b8057470458701a Mon Sep 17 00:00:00 2001 From: Heikki Linnakangas Date: Fri, 16 Oct 2026 17:29:57 +0300 Subject: [PATCH] pg_upgrade: Fix last multixid ending at offset wraparound On v18 and below, if the newest multixid's members end exactly at the 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. Author: Zsolt Parragi Discussion: https://www.postgresql.org/message-id/CAN4CZFM8BCazGz6s1Vn5tMEXYiXE2fi8T2rwmwXSty5gD+h9rA@mail.gmail.com 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();