1
0
mirror of https://github.com/postgres/postgres.git synced 2026-10-23 13:29:09 +03:00

pg_combinebackup: Refuse mixed data checksum states in a backup chain

check_control_files() detected a chain whose backups were taken under
different data checksum states, warned, and proceeded.  The output
directory keeps the last backup's control file, so a full backup taken
with checksums off combined with an incremental taken after an offline
enable produces a cluster whose control file says "on" while most of
its blocks carry no checksums; it starts, and then every connection
dies on the first unchecksummed catalog page.  An offline enable
between two backups of a chain is all it takes, since it rewrites
every page without logging anything, so the incremental backup does
not re-ship the pages.

Turn the warning into an error, matching what pg_rewind does for the
equivalent combinations.  The check stays asymmetric on purpose: when
the last backup was taken with checksums off, stale checksums from an
earlier backup are never verified, and that chain remains usable.

Author: Zsolt Parragi <zsolt.parragi@percona.com>
Reviewed-by: Bertrand Drouvot <bertranddrouvot.pg@gmail.com>
Reviewed-by: Daniel Gustafsson <daniel@yesql.se>
Discussion: https://postgr.es/m/anwm6UPxoVS41QA2@bdtpg
Backpatch-through: 19
This commit is contained in:
Daniel Gustafsson
2026-09-14 15:33:03 +02:00
parent 227f8ea19a
commit 724477d67e
4 changed files with 167 additions and 13 deletions

View File

@@ -306,18 +306,19 @@ PostgreSQL documentation
<para>
<literal>pg_combinebackup</literal> does not recompute page checksums when
writing the output directory. Therefore, if any of the backups used for
reconstruction were taken with checksums disabled, but the final backup was
taken with checksums enabled, the resulting directory may contain pages
with invalid checksums.
writing the output directory. It therefore refuses a chain in which some
of the backups used for reconstruction were taken with checksums disabled
but the final backup was taken with checksums enabled: the resulting
directory would contain pages with invalid checksums, and the cluster
would fail checksum verification as soon as it reads them. Take a new
full backup after enabling data checksums with
<xref linkend="app-pgchecksums"/>.
</para>
<para>
To avoid this problem, taking a new full backup after changing the checksum
state of the cluster using <xref linkend="app-pgchecksums"/> is
recommended. Otherwise, you can disable and then optionally reenable
checksums on the directory produced by <literal>pg_combinebackup</literal>
in order to correct the problem.
The reverse case is accepted: when the final backup was taken with
checksums disabled, stale checksums copied from an earlier backup are
never verified.
</para>
</refsect1>

View File

@@ -670,13 +670,19 @@ check_control_files(int n_backups, char **backup_dirs)
pg_log_debug("system identifier is %" PRIu64, system_identifier);
/*
* Warn the user if not all backups are in the same state with regards to
* checksums.
* Reject a chain whose backups were not all in the same state with
* regards to checksums. The loop above only flags that when the last
* backup has checksums enabled: the blocks taken from the older backups
* have no checksums, and pg_combinebackup does not recompute them, so the
* combined cluster would fail verification as soon as it reads them. The
* other direction is harmless, since stale checksums are never verified
* while checksums are disabled.
*/
if (data_checksum_mismatch)
{
pg_log_warning("only some backups have checksums enabled");
pg_log_warning_hint("Disable, and optionally reenable, checksums on the output directory to avoid failures.");
pg_log_error("only some backups have checksums enabled");
pg_log_error_hint("Take a new full backup after changing the data checksum state with pg_checksums.");
exit(1);
}
return system_identifier;

View File

@@ -47,6 +47,7 @@ tests += {
't/021_rewind_divergent_transitions.pl',
't/022_rewind_state.pl',
't/023_rewind_standby_target.pl',
't/024_combinebackup_mixed.pl',
],
},
}

View File

@@ -0,0 +1,146 @@
# Copyright (c) 2026, PostgreSQL Global Development Group
# Test that pg_combinebackup refuses a chain whose backups were taken under
# different data checksum states when the final state has them enabled.
#
# The output directory keeps the last backup's control file, so a full
# backup taken with checksums off plus an incremental taken after an
# offline enable would produce a directory whose control file says "on"
# while most of its blocks have no checksums. An offline enable between
# the two backups is enough to get there: it rewrites every page but logs
# nothing, so the incremental backup does not re-ship the pages.
#
# The reverse order stays allowed: with the final backup taken with
# checksums off, stale checksums from an earlier backup are never verified.
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;
# This test suite is expensive to execute, require PG_TEST_EXTRA to contain
# 'checksum' to run it.
if ($ENV{PG_TEST_EXTRA})
{
plan skip_all => 'Expensive data checksums test disabled'
unless ($ENV{PG_TEST_EXTRA} =~ /\bchecksum(_extended)?\b/);
}
else
{
plan skip_all => 'Expensive data checksums test disabled';
}
my $node = PostgreSQL::Test::Cluster->new('node');
$node->init(
no_data_checksums => 1,
has_archiving => 1,
allows_streaming => 1);
$node->append_conf('postgresql.conf', 'summarize_wal = on');
$node->append_conf('postgresql.conf', 'autovacuum = off');
$node->start;
$node->safe_psql('postgres',
"CREATE TABLE t1 AS SELECT generate_series(1,200000) AS a;");
$node->safe_psql('postgres', 'CHECKPOINT;');
test_checksum_state($node, 'off');
$node->backup('full');
# Offline enable: rewrites every page, logs nothing.
$node->stop;
system_or_bail('pg_checksums', '--enable', '--pgdata', $node->data_dir);
$node->start;
test_checksum_state($node, 'on');
$node->safe_psql('postgres',
"CREATE TABLE t2 AS SELECT generate_series(1,1000) AS a;");
$node->safe_psql('postgres', 'CHECKPOINT;');
$node->command_ok(
[
'pg_basebackup',
'--pgdata' => $node->backup_dir . '/incr',
'--dbname' => $node->connstr('postgres'),
'--no-sync',
'--checkpoint' => 'fast',
'--incremental' => $node->backup_dir . '/full/backup_manifest',
],
'incremental backup with checksums on');
# Combining the chain must be refused: the result would say "on" while the
# blocks inherited from the full backup have no checksums.
command_fails_like(
[
'pg_combinebackup',
$node->backup_dir . '/full',
$node->backup_dir . '/incr',
'--output' => $node->backup_dir . '/combined',
],
qr/only some backups have checksums enabled/,
'pg_combinebackup refuses a chain crossing an offline enable');
# The other direction: full backup with checksums on, offline disable, then
# an incremental. The last backup wins, so the combined cluster comes up off.
$node->backup('full2');
$node->stop;
system_or_bail('pg_checksums', '--disable', '--pgdata', $node->data_dir);
$node->start;
test_checksum_state($node, 'off');
$node->safe_psql('postgres',
"CREATE TABLE t3 AS SELECT generate_series(1,1000) AS a;");
$node->safe_psql('postgres', 'CHECKPOINT;');
$node->command_ok(
[
'pg_basebackup',
'--pgdata' => $node->backup_dir . '/incr2',
'--dbname' => $node->connstr('postgres'),
'--no-sync',
'--checkpoint' => 'fast',
'--incremental' => $node->backup_dir . '/full2/backup_manifest',
],
'incremental backup with checksums off');
my $restored = PostgreSQL::Test::Cluster->new('restored');
$restored->init_from_backup(
$node, 'incr2',
combine_with_prior => ['full2'],
has_restoring => 1,
standby => 0);
$restored->start;
my ($rc, $stdout, $stderr) = $restored->psql('postgres',
"SELECT setting FROM pg_settings WHERE name = 'data_checksums';");
is($rc, 0, 'combined backup accepts connections') or diag("stderr: $stderr");
is($stdout, 'off', 'combined backup follows the last backup state');
($rc, $stdout, $stderr) =
$restored->psql('postgres', 'SELECT count(*) FROM t3;');
is($rc, 0, 'blocks from the incremental backup are readable')
or diag("stderr: $stderr");
($rc, $stdout, $stderr) =
$restored->psql('postgres', 'SELECT count(*) FROM t1;');
is($rc, 0, 'blocks inherited from the full backup are readable')
or diag("stderr: $stderr");
my $log = PostgreSQL::Test::Utils::slurp_file($restored->logfile);
unlike(
$log,
qr/page verification failed/,
'no checksum verification failures in the combined backup');
$restored->stop('immediate');
$node->stop;
done_testing();