From 097f139c8e999973cd69212e935baccaa031a537 Mon Sep 17 00:00:00 2001 From: Zsolt Parragi Date: Tue, 21 Jul 2026 10:39:19 +0000 Subject: [PATCH] createdb: recheck checksum state before file_copy The file_copy strategy check in createdb() runs during option validation, before the transaction has an XID and before the pg_database row exists, so the datachecksumsworker launcher can start in that window and see neither the new database nor the transaction creating it. It then raw-copies a template that was not processed yet, and those files stay unchecksummed, failing verification from then on. Recheck the state in CreateDatabaseUsingFileCopy(): the XID is assigned by then, so a launcher starting after this point waits for the transaction and finds the new database, and the copy errors out instead. Add an injection point before the catalog insert to test the window. --- src/backend/commands/dbcommands.c | 17 ++++ src/test/modules/test_checksums/meson.build | 1 + .../test_checksums/t/010_filecopy_createdb.pl | 82 +++++++++++++++++++ 3 files changed, 100 insertions(+) create mode 100644 src/test/modules/test_checksums/t/010_filecopy_createdb.pl diff --git a/src/backend/commands/dbcommands.c b/src/backend/commands/dbcommands.c index 51dcbd9cace..72c7d8ccff2 100644 --- a/src/backend/commands/dbcommands.c +++ b/src/backend/commands/dbcommands.c @@ -65,6 +65,7 @@ #include "utils/acl.h" #include "utils/builtins.h" #include "utils/fmgroids.h" +#include "utils/injection_point.h" #include "utils/lsyscache.h" #include "utils/pg_locale.h" #include "utils/relmapper.h" @@ -558,6 +559,20 @@ CreateDatabaseUsingFileCopy(Oid src_dboid, Oid dst_dboid, Oid src_tsid, Relation rel; HeapTuple tuple; + /* + * The strategy check in createdb() runs before our transaction has an + * XID and before the pg_database row exists, so the datachecksumsworker + * launcher can start in that window and miss both the new database and + * our transaction, leaving the raw-copied files without checksums. + * Recheck here: the XID is assigned by now, so a launcher starting after + * this point will wait for us and find the new database. + */ + if (DataChecksumsInProgressOn()) + ereport(ERROR, + errcode(ERRCODE_OBJECT_NOT_IN_PREREQUISITE_STATE), + errmsg("create database strategy \"%s\" not allowed when data checksums are being enabled", + "file_copy")); + /* * Force a checkpoint before starting the copy. This will force all dirty * buffers, including those of unlogged tables, out to disk, to ensure @@ -1510,6 +1525,8 @@ createdb(ParseState *pstate, const CreatedbStmt *stmt) tuple = heap_form_tuple(RelationGetDescr(pg_database_rel), new_record, new_record_nulls); + INJECTION_POINT("createdb-before-catalog-insert", NULL); + CatalogTupleInsert(pg_database_rel, tuple); /* diff --git a/src/test/modules/test_checksums/meson.build b/src/test/modules/test_checksums/meson.build index 9b1421a9b91..687b5f55740 100644 --- a/src/test/modules/test_checksums/meson.build +++ b/src/test/modules/test_checksums/meson.build @@ -33,6 +33,7 @@ tests += { 't/007_pgbench_standby.pl', 't/008_pitr.pl', 't/009_fpi.pl', + 't/010_filecopy_createdb.pl', ], }, } diff --git a/src/test/modules/test_checksums/t/010_filecopy_createdb.pl b/src/test/modules/test_checksums/t/010_filecopy_createdb.pl new file mode 100644 index 00000000000..4b91731850d --- /dev/null +++ b/src/test/modules/test_checksums/t/010_filecopy_createdb.pl @@ -0,0 +1,82 @@ + +# Copyright (c) 2026, PostgreSQL Global Development Group + +# CREATE DATABASE file_copy racing checksum enable: the strategy check runs +# before the transaction has an XID, so the launcher can miss both. +use strict; +use warnings FATAL => 'all'; + +use PostgreSQL::Test::Cluster; +use PostgreSQL::Test::Utils; +use Test::More; + +use FindBin; +use lib $FindBin::RealBin; + +use DataChecksums::Utils; + +if ($ENV{enable_injection_points} ne 'yes') +{ + plan skip_all => 'Injection points not supported by this build'; +} + +my $node = PostgreSQL::Test::Cluster->new('filecopy_node'); +$node->init(no_data_checksums => 1); +$node->start; + +$node->safe_psql('postgres', 'CREATE EXTENSION injection_points;'); +$node->safe_psql('postgres', + "CREATE TABLE t AS SELECT generate_series(1,10000) AS a;"); + +$node->safe_psql('postgres', + "SELECT injection_points_attach('createdb-before-catalog-insert','wait');" +); +$node->safe_psql('postgres', + "SELECT injection_points_attach('datachecksumsworker-fake-temptable-wait','wait');" +); + +# Hold CREATE DATABASE after the strategy check, before its xact is visible. +my $bg = $node->background_psql('postgres'); +$bg->query_until( + qr/starting_create/, q( +\echo starting_create +CREATE DATABASE fcdb TEMPLATE template0 STRATEGY file_copy; +)); +$node->wait_for_event('client backend', 'createdb-before-catalog-insert'); + +# Enable checksums, worker holds before processing template0. +enable_data_checksums($node); +$node->wait_for_event('datachecksums worker', + 'datachecksumsworker-fake-temptable-wait'); + +# Release CREATE DATABASE, must fail on the recheck instead of raw-copying. +$node->safe_psql('postgres', + "SELECT injection_points_wakeup('createdb-before-catalog-insert');"); +$node->safe_psql('postgres', + "SELECT injection_points_detach('createdb-before-catalog-insert');"); + +# Wait for the CREATE DATABASE xact to finish before releasing the worker. +$node->poll_query_until('postgres', + "SELECT count(*) = 0 FROM pg_stat_activity " + . "WHERE query LIKE 'CREATE DATABASE%' AND state != 'idle';"); + +$node->safe_psql('postgres', + "SELECT injection_points_wakeup('datachecksumsworker-fake-temptable-wait');" +); +$node->safe_psql('postgres', + "SELECT injection_points_detach('datachecksumsworker-fake-temptable-wait');" +); + +wait_for_checksum_state($node, 'on'); + +my $result = $node->safe_psql('postgres', + "SELECT count(*) FROM pg_database WHERE datname = 'fcdb';"); +is($result, '0', 'file_copy database creation was refused'); + +$bg->quit; + +$node->stop; +command_ok([ 'pg_checksums', '--check', '-D', $node->data_dir ], + 'offline checksum verification passes'); + +done_testing(); -- 2.54.0