Received: from malur.postgresql.org ([217.196.149.56]) by arkaria.postgresql.org with esmtps (TLS1.3) tls TLS_ECDHE_RSA_WITH_AES_256_GCM_SHA384 (Exim 4.96) (envelope-from ) id 1woayd-0019B2-0c for pgsql-hackers@arkaria.postgresql.org; Tue, 28 Jul 2026 06:07:43 +0000 Received: from localhost ([127.0.0.1] helo=malur.postgresql.org) by malur.postgresql.org with esmtp (Exim 4.96) (envelope-from ) id 1woayb-00GZzp-2s for pgsql-hackers@arkaria.postgresql.org; Tue, 28 Jul 2026 06:07:41 +0000 Received: from magus.postgresql.org ([2a02:c0:301:0:ffff::29]) by malur.postgresql.org with esmtps (TLS1.3) tls TLS_ECDHE_RSA_WITH_AES_256_GCM_SHA384 (Exim 4.96) (envelope-from ) id 1woayb-00GZzf-1r for pgsql-hackers@lists.postgresql.org; Tue, 28 Jul 2026 06:07:41 +0000 Received: from mail-wm1-x32a.google.com ([2a00:1450:4864:20::32a]) by magus.postgresql.org with esmtps (TLS1.3) tls TLS_ECDHE_RSA_WITH_AES_128_GCM_SHA256 (Exim 4.98.2) (envelope-from ) id 1woayY-00000000fau-3UFi for pgsql-hackers@lists.postgresql.org; Tue, 28 Jul 2026 06:07:40 +0000 Received: by mail-wm1-x32a.google.com with SMTP id 5b1f17b1804b1-493f6de72faso4279255e9.0 for ; Mon, 27 Jul 2026 23:07:37 -0700 (PDT) DKIM-Signature: v=1; a=rsa-sha256; c=relaxed/relaxed; d=gmail.com; s=20251104; t=1785218856; x=1785823656; darn=lists.postgresql.org; h=in-reply-to:content-disposition:content-type:mime-version :references:message-id:subject:cc:to:from:date:from:to:cc:subject :date:message-id:reply-to:content-type; bh=pbm9Cnzk9hVi9BArCU6iQD3MvjAlORQarXvVZfWvm0Q=; b=mMP2XgXw2eJRINToo3cBsN/ePes47DC79fjiQzBRGM07r3SCA26L9QmFyXnYlZW2Wo 6iNm/8wuhdJZJEIIUorzmmpAJGx8ag9V8y+juT8y+LrHifE+bXEjCX5OqMHE3GAitR3d 6D32VOAv9fwG3xEm9Fv7NYWNiWhkcoMggrLaGOBl3e3dVHOqmVlkK0lOqTqyzqZuGOmV E1pYDbp3CN0IZvK6C2cGCix99ON+l6WwuMYG4Ml5+VcT7er3fywwKoSHD8cPnS9hV3ST hIECfGpC/8M0OI/1qL6HYwGJbM4RGRMHgreDdoz43+U/WF3vy1yfC1ijrR85Aq9a2eRR b54w== X-Google-DKIM-Signature: v=1; a=rsa-sha256; c=relaxed/relaxed; d=1e100.net; s=20251104; t=1785218856; x=1785823656; h=in-reply-to:content-disposition:content-type:mime-version :references:message-id:subject:cc:to:from:date:x-gm-gg :x-gm-message-state:from:to:cc:subject:date:message-id:reply-to :content-type; bh=pbm9Cnzk9hVi9BArCU6iQD3MvjAlORQarXvVZfWvm0Q=; b=Tnr3R0qGKFoxN0lh2cQZZrSppA67s2JVJxRVNgWO3aw7Tuunes/sltIKu1l/9Fajad CHm/iC1vVg9Kn0SV7i+wUkTF3QI+dJZtX/XkuooPk+KLaEGanrgKHbrjJCexCvrVYYay AVYsijtcZ4CwTjiinBitTxoI9N8I+jbG17heVQt3Y9h5aDu/PYW+Bn8yRv3wKo0YNG2x uzrubB2BUR3+cGC2EHJ/LZu/jmzNxICtntfLM8xghyBPCuxCmNnzJfnunyc5e2LYJxhW 0oiQe1R1xPfoOjtE+8baTefnfsiRipplKPDou+D8OkUOWeXzMVyK8ui4kmdjwudz3yoo Dy2g== X-Gm-Message-State: AOJu0Yyv1BAaBWFyRVlIqk+ySZbtwcJyHOaKHT311/dFKEePzVoDua+z WBwM2/Bb7XUIcIS+0csoUOCMIqBpCkq8qG+dIU7hvXzzhYcQjdgQ/QgR X-Gm-Gg: AR+sD115V1a77iyLzRW2eYU/RuQOJR84wfqnFjBeNtXvftas1agD+j0GAmILlKF6kOl KMIpTzliKVFA9BafH5llv57T+BebOAxmSFQvgtJVDn4xIbe06O62XerQZ2ZW/P/mD3KLhJGoREP HKxyZQ+ARNqTHwg4+DF/Y13oWBIe95ARjKz99T+/NoJ81NU/B99/SIUTZ5HoLuHxQL3c9JuPbsd gQjO41okCgORgWtVFMA/m30TQPOkKqQ10SJfwWaZBvTOfatfap9wDhEJeZ2w2wjSw1PvHjlDd3n XKTpS2wZ4JL/gApLnQ/b3uPXmYvYGOrzK0R7Z/39iLIOjrYb679t8bDOmNccUbjvAos8JiW9xqx I6BtPJs7TmXneM5utcA5UUVd3FoYkwrD61yCsoH2z8oc5DbUJxf6zISSVFMRBzagofUtTwepYSK 0kGbA4CybhqBddqqs2GmpvrSOyQinVXN0/kXJwskEizw9puGeYZ4m/ZUSeala/n+TJlmIupyhi X-Received: by 2002:a7b:c3d7:0:b0:495:71e9:d92b with SMTP id 5b1f17b1804b1-496c6561f0emr5968505e9.10.1785218856260; Mon, 27 Jul 2026 23:07:36 -0700 (PDT) Received: from bdtpg (ec2-15-237-197-144.eu-west-3.compute.amazonaws.com. [15.237.197.144]) by smtp.gmail.com with ESMTPSA id ffacd0b85a97d-47f85c6f076sm55882147f8f.34.2026.07.27.23.07.35 (version=TLS1_3 cipher=TLS_AES_256_GCM_SHA384 bits=256/256); Mon, 27 Jul 2026 23:07:35 -0700 (PDT) Date: Tue, 28 Jul 2026 06:07:34 +0000 From: Bertrand Drouvot To: Peter Geoghegan Cc: PostgreSQL Hackers , Andres Freund , scott@scottray.io Subject: Re: Snapshot export on a standby corrupts hint bits on subxact overflow Message-ID: References: MIME-Version: 1.0 Content-Type: multipart/mixed; boundary="G5mbVXPGyfR4obnE" Content-Disposition: inline In-Reply-To: List-Id: List-Help: List-Subscribe: List-Post: List-Owner: List-Archive: Archived-At: Precedence: bulk --G5mbVXPGyfR4obnE Content-Type: text/plain; charset=us-ascii Content-Disposition: inline Hi, On Sun, Jul 26, 2026 at 10:17:21PM -0400, Peter Geoghegan wrote: > I decided to reinvestigate the problem today, with help from Claude > code. I found a bug that exactly matches the known symptoms. Attached > patch 0001 has a reproducer + draft bug fix. This is likely a bug in > 2017 commit 6c2003f8. There's also a second patch 0002 that fixes > another bug found along the way (though that's much less serious than > the one that 0001 deals with). Thanks for having looked at it! The first problem is also something I worked on in the past without success. > The test case in 0001 shows a scenario where pg_export_snapshot on a > standby hands out a snapshot that claims no transaction is running, > which is wrong. A backend that imports it writes wrong hint bits, > which are then seen by every other session on that standby -- > including sessions that never touched the exported snapshot. > User-visible symptoms include rows reappearing after deletion, rows > vanishing after insertion, and duplicate entries in unique indexes > (all symptoms that I've personally seen in the wild). The fix in 0001 makes sense to me. Some comments: === 1 + if (snapshot->subxcnt + nchildren > GetMaxSnapshotSubxidCount()) + ereport(ERROR, + (errcode(ERRCODE_PROGRAM_LIMIT_EXCEEDED), Yes, this check is needed here. + for (int32 i = 0; i < snapshot->subxcnt; i++) + appendStringInfo(&buf, "sxp:%u\n", snapshot->subxip[i]); + for (int32 i = 0; i < nchildren; i++) + appendStringInfo(&buf, "sxp:%u\n", children[i]); We count every existing subxip entry and every committed child without filtering against snapshot->xmax. A committed child created after the imported snapshot was taken can have an XID >= snapshot->xmax. I think that such an entry is unnecessary for visibility, since XidInMVCCSnapshot() classifies every XID >= xmax as still in progress before looking at subxip. It can be observed by applying the attached post-xmax.txt on top of 0001, which produces: # BDT snapshot saved for post-promotion re-export 00000007-00000002-1: sof=1 # BDT re-exported snapshot 00000000-00000004-1: sof=1; xmax=781; sxp=[698, 763, 764, 765, 766, 767, 768, 769, 770, 771, 772, 773, 774, 775, 776, 777, 778, 782]; sxp >= xmax=[782] We can see that xmax=781, and that 782 has been exported. That's not a visibility issue, however, those entries count toward GetMaxSnapshotSubxidCount() and could therefore cause an avoidable PROGRAM_LIMIT_EXCEEDED. === 2 > 0002 is a separate bug of the same general nature. + if (cur->takenDuringRecovery) + { + nxip = cur->subxcnt; + xip = cur->subxip; + } + else SnapshotData.subxip permits entries at or above xmax, whereas pg_snapshot.xip requires every entry to satisfy xmin <= xip[i] < xmax. So, I think filtering is appropriate in both places: 1/ In ExportSnapshot(), do not include recovery subxip entries and committed child XIDs at or above xmax when counting and serializing them, so unnecessary entries do not consume the limited recovery subxip capacity. 2/ In pg_current_snapshot(), do not include source XIDs outside [xmin, xmax), so that it enforces the rule regardless of how the source snapshot was produced. === 3 0002 can now expose subtransaction IDs, so I think some comments: "Note that only top-level transaction IDs are exposed to user sessions" "Note that only top-transaction XIDs are included in the snapshot" and the docs for pg_current_snapshot() and xip_list need updates to mention the recovery specific exception. Regards, -- Bertrand Drouvot PostgreSQL Contributors Team RDS Open Source Databases Amazon Web Services: https://aws.amazon.com --G5mbVXPGyfR4obnE Content-Type: text/plain; charset=us-ascii Content-Disposition: attachment; filename="post-xmax.txt" diff --git a/src/test/recovery/t/055_standby_snapshot_export.pl b/src/test/recovery/t/055_standby_snapshot_export.pl index 139190cf231..4cf3e78c766 100644 --- a/src/test/recovery/t/055_standby_snapshot_export.pl +++ b/src/test/recovery/t/055_standby_snapshot_export.pl @@ -122,7 +122,6 @@ $s1->query_safe('COMMIT'); $u->query_safe('COMMIT'); $primary->safe_psql('postgres', q[INSERT INTO victim VALUES (7, repeat('y', 1200))]); -$o->query_safe('COMMIT'); $primary->wait_for_replay_catchup($standby); # Sessions that never touched the exported snapshot must agree with the @@ -151,6 +150,13 @@ $s3->query_safe('BEGIN ISOLATION LEVEL REPEATABLE READ'); my $snap2 = $s3->query_safe('SELECT pg_export_snapshot()'); my $before = $s3->query_safe('SELECT count(*) FROM victim'); +my $file2 = slurp_file($standby->data_dir . "/pg_snapshots/$snap2"); +my ($sof2) = $file2 =~ /^sof:(\d+)$/m; +note("BDT snapshot saved for post-promotion re-export $snap2: sof=$sof2"); + +$o->query_safe('COMMIT'); +$primary->wait_for_replay_catchup($standby); + $standby->promote; $standby->poll_query_until('postgres', 'SELECT NOT pg_is_in_recovery()') or die "standby never finished promotion"; @@ -172,6 +178,16 @@ $s4->query_safe('RELEASE sp'); my $snap3 = $s4->query_safe('SELECT pg_export_snapshot()'); my $file3 = slurp_file($standby->data_dir . "/pg_snapshots/$snap3"); +my ($sof3) = $file3 =~ /^sof:(\d+)$/m; +my ($xmax3) = $file3 =~ /^xmax:(\d+)$/m; +my @subxids3 = $file3 =~ /^sxp:(\d+)$/mg; +my @post_xmax_subxids3 = grep { $_ >= $xmax3 } @subxids3; +note( + "BDT re-exported snapshot $snap3: sof=$sof3; xmax=$xmax3; sxp=[" + . join(', ', @subxids3) + . "]; sxp >= xmax=[" + . join(', ', @post_xmax_subxids3) + . "]"); like($file3, qr/^sxcnt:[1-9]/m, 'a recovery-taken snapshot re-exports its subxip array after a write'); --G5mbVXPGyfR4obnE--