From 239d268615a94b18febd82a42cfe019072ff69b8 Mon Sep 17 00:00:00 2001 From: Andrey Borodin Date: Tue, 6 Oct 2026 14:54:51 +0500 Subject: [PATCH v2 2/2] Detect incorrect committed transaction hints in verify_heapam() Incorrect committed hints can expose aborted inserts or hide tuples after aborted updates. Cross-check committed hints against aborted transactions, including lockers. Do not confuse a stale in-progress status with an abort. Add coverage for contradictory committed hints. Discussion: https://postgr.es/m/B993F6CC-03D0-440F-9D7C-FEF65D19C082@yandex-team.ru --- contrib/amcheck/verify_heapam.c | 27 +++++++++++++++++++ src/bin/pg_amcheck/t/004_verify_heapam.pl | 33 ++++++++++++++++++++--- 2 files changed, 57 insertions(+), 3 deletions(-) diff --git a/contrib/amcheck/verify_heapam.c b/contrib/amcheck/verify_heapam.c index 89a4d80935f..ef0dae151c1 100644 --- a/contrib/amcheck/verify_heapam.c +++ b/contrib/amcheck/verify_heapam.c @@ -1357,6 +1357,17 @@ check_tuple_visibility(HeapCheckContext *ctx, bool *xmin_commit_status_ok, return false; } } + else if (xmin_status == XID_ABORTED) + { + /* + * Only a final aborted status contradicts a committed hint. An + * in-progress status can be stale by the time we read the hint. + */ + report_corruption(ctx, + psprintf("xmin %u is aborted, but marked committed", + xmin)); + return false; /* don't check the tuple's contents */ + } /* * Okay, the inserter committed, so it was good at some point. Now what @@ -1409,6 +1420,22 @@ check_tuple_visibility(HeapCheckContext *ctx, bool *xmin_commit_status_ok, } } + /* + * A committed hint must agree with the transaction's outcome, even if + * xmax only locked the tuple. A multixact cannot have a committed hint; + * check_tuple_header() already reports that case. + */ + if ((tuphdr->t_infomask & (HEAP_XMAX_COMMITTED | HEAP_XMAX_IS_MULTI)) == + HEAP_XMAX_COMMITTED) + { + xmax = HeapTupleHeaderGetRawXmax(tuphdr); + if (get_xid_status(xmax, ctx, &xmax_status, NULL) == XID_BOUNDS_OK && + xmax_status == XID_ABORTED) + report_corruption(ctx, + psprintf("xmax %u is aborted, but marked committed", + xmax)); + } + if (tuphdr->t_infomask & HEAP_XMAX_INVALID) { /* diff --git a/src/bin/pg_amcheck/t/004_verify_heapam.pl b/src/bin/pg_amcheck/t/004_verify_heapam.pl index 9c282cd9603..3c196138c55 100644 --- a/src/bin/pg_amcheck/t/004_verify_heapam.pl +++ b/src/bin/pg_amcheck/t/004_verify_heapam.pl @@ -224,7 +224,7 @@ my $relpath = "$pgdata/$rel"; # $ROWCOUNT is the total number of rows that we expect to insert into the page. # $ROWCOUNT_BASIC is the number of those rows that are related to basic # tuple validation, rather than update chain validation. -my $ROWCOUNT = 57; +my $ROWCOUNT = 60; my $ROWCOUNT_BASIC = 16; # First insert data needed for tests unrelated to update chain validation. @@ -316,13 +316,13 @@ my $in_progress_xid = $node->safe_psql( SELECT transaction FROM pg_prepared_xacts; )); -# Tuples for checking hint bits, at offset numbers 45 through 57. +# Tuples for checking hint bits, at offset numbers 45 through 60. $node->safe_psql( 'postgres', qq( INSERT INTO public.test (a, b, c) SELECT x'DEADF9F9DEADF9F9'::bigint, 'abcdefg', repeat('w', 10000) - FROM generate_series(1, 13); + FROM generate_series(1, 16); )); # Make real multixacts with a committed updater, an aborted updater, and @@ -864,6 +864,26 @@ for (my $tupidx = 0; $tupidx < $ROWCOUNT; $tupidx++) } } } + elsif ($offnum == 58) + { + # An aborted inserter must not be hinted committed. + $tup->{t_xmin} = $aborted_xid; + $tup->{t_infomask} &= ~HEAP_XMIN_INVALID; + $tup->{t_infomask} |= HEAP_XMIN_COMMITTED; + push @expected, + qr/${header}xmin $aborted_xid is aborted, but marked committed/; + } + elsif ($offnum == 59 || $offnum == 60) + { + # An aborted xmax cannot be hinted committed, even for a locker. + $tup->{t_xmax} = $aborted_xid; + $tup->{t_infomask} &= ~HEAP_XMAX_INVALID; + $tup->{t_infomask} |= HEAP_XMAX_COMMITTED; + $tup->{t_infomask} |= HEAP_XMAX_LOCK_ONLY | HEAP_XMAX_KEYSHR_LOCK + if $offnum == 60; + push @expected, + qr/${header}xmax $aborted_xid is aborted, but marked committed/; + } else { # The tests for update chain validation end up creating a bunch of @@ -902,6 +922,13 @@ is( $node->safe_psql( "50\n54", 'only the incorrect xmax hint bits are reported'); +is( $node->safe_psql( + 'postgres', + q(SELECT offnum FROM verify_heapam('test', check_toast => false) + WHERE offnum >= 58 ORDER BY offnum)), + "58\n59\n60", + 'only contradictory committed hints are reported'); + $node->safe_psql( 'postgres', qq( COMMIT PREPARED 'in_progress_tx'; -- That's all, folks. May the source be with you.