1
0
mirror of https://github.com/pgbackrest/pgbackrest.git synced 2025-07-05 00:28:52 +02:00

Prevent invalid recovery when backup_label removed.

If backup_label is removed from a restored backup then PostgreSQL will instead use checkpoint information from pg_control to attempt (what is thinks is) crash recovery. This will nearly always result in a corrupt cluster because the checkpoint will not be from the beginning of the backup, and even if it is, the end point will not be specified, which could lead to recovery stopping too early.

To prevent this, invalidate the checkpoint LSN in pg_control on restore. If backup_label is removed then recovery will still fail because PostgreSQL will not be able to find the invalid checkpoint. The LSN of the checkpoint is not logged but it will be visible in pg_controldata output as 0/DEAD. This value is invalid because PostgreSQL always skips the first WAL segment when initializing a cluster.
This commit is contained in:
David Steele
2024-03-10 17:08:42 +13:00
committed by GitHub
parent 960b43589d
commit e634fd85ce
9 changed files with 228 additions and 53 deletions

View File

@ -71,6 +71,9 @@ typedef struct PgInterface
// Get control crc offset
size_t (*controlCrcOffset)(void);
// Invalidate control checkpoint
void (*controlCheckpointInvalidate)(unsigned char *);
// Get the control version for this version of PostgreSQL
uint32_t (*controlVersion)(void);
@ -191,6 +194,88 @@ pgWalSegmentSizeCheck(unsigned int pgVersion, unsigned int walSegmentSize)
FUNCTION_TEST_RETURN_VOID();
}
/***********************************************************************************************************************************
Validate the CRC in pg_control. If the version has been forced this may required scanning pg_control for the offset. Return the
offset found so it can be used to update the CRC.
***********************************************************************************************************************************/
static size_t
pgControlCrcValidate(const Buffer *const controlFile, const PgInterface *const interface, const bool pgVersionForce)
{
FUNCTION_LOG_BEGIN(logLevelTrace);
FUNCTION_LOG_PARAM(BUFFER, controlFile);
FUNCTION_LOG_PARAM_P(VOID, interface);
FUNCTION_LOG_PARAM(BOOL, pgVersionForce);
FUNCTION_LOG_END();
ASSERT(controlFile != NULL);
ASSERT(interface != NULL);
size_t result = interface->controlCrcOffset();
do
{
// Calculate CRC and retrieve expected CRC
const uint32_t crcCalculated =
interface->version > PG_VERSION_94 ?
crc32cOne(bufPtrConst(controlFile), result) : crc32One(bufPtrConst(controlFile), result);
const uint32_t crcExpected = *((uint32_t *)(bufPtrConst(controlFile) + result));
// If CRC does not match
if (crcCalculated != crcExpected)
{
// If version is forced then the CRC might be later in the file (assuming the fork added extra fields to pg_control).
// Increment the offset by CRC data size and continue to try again.
if (pgVersionForce)
{
result += sizeof(uint32_t);
if (result <= bufUsed(controlFile) - sizeof(uint32_t))
continue;
}
// If no retry then error
THROW_FMT(
ChecksumError,
"calculated " PG_FILE_PGCONTROL " checksum does not match expected value\n"
"HINT: calculated 0x%x but expected value is 0x%x\n"
"%s"
"HINT: is " PG_FILE_PGCONTROL " corrupt?\n"
"HINT: does " PG_FILE_PGCONTROL " have a different layout than expected?",
crcCalculated, crcExpected,
pgVersionForce ? "HINT: checksum values may be misleading due to forced version scan\n" : "");
}
// Do not retry if the CRC is valid
break;
}
while (true);
FUNCTION_LOG_RETURN(SIZE, result);
}
/***********************************************************************************************************************************
Update the CRC in pg_control
***********************************************************************************************************************************/
static void
pgControlCrcUpdate(Buffer *const controlFile, const unsigned int pgVersion, const size_t crcOffset)
{
FUNCTION_LOG_BEGIN(logLevelTrace);
FUNCTION_LOG_PARAM(BUFFER, controlFile);
FUNCTION_LOG_PARAM(UINT, pgVersion);
FUNCTION_LOG_PARAM(SIZE, crcOffset);
FUNCTION_LOG_END();
ASSERT(controlFile != NULL);
ASSERT(pgVersion != 0);
ASSERT(crcOffset != 0);
*((uint32_t *)(bufPtr(controlFile) + crcOffset)) =
pgVersion > PG_VERSION_94 ?
crc32cOne(bufPtrConst(controlFile), crcOffset) : crc32One(bufPtrConst(controlFile), crcOffset);
FUNCTION_LOG_RETURN_VOID();
}
/**********************************************************************************************************************************/
static PgControl
pgControlFromBuffer(const Buffer *controlFile, const String *const pgVersionForce)
@ -237,46 +322,8 @@ pgControlFromBuffer(const Buffer *controlFile, const String *const pgVersionForc
PgControl result = interface->control(bufPtrConst(controlFile));
result.version = interface->version;
// Check CRC
size_t crcOffset = interface->controlCrcOffset();
do
{
// Calculate CRC and retrieve expected CRC
const uint32_t crcCalculated =
result.version > PG_VERSION_94 ?
crc32cOne(bufPtrConst(controlFile), crcOffset) : crc32One(bufPtrConst(controlFile), crcOffset);
const uint32_t crcExpected = *((uint32_t *)(bufPtrConst(controlFile) + crcOffset));
// If CRC does not match
if (crcCalculated != crcExpected)
{
// If version is forced then the CRC might be later in the file (assuming the fork added extra fields to pg_control).
// Increment the offset by CRC data size and continue to try again.
if (pgVersionForce != NULL)
{
crcOffset += sizeof(uint32_t);
if (crcOffset <= bufUsed(controlFile) - sizeof(uint32_t))
continue;
}
// If no retry then error
THROW_FMT(
ChecksumError,
"calculated " PG_FILE_PGCONTROL " checksum does not match expected value\n"
"HINT: calculated 0x%x but expected value is 0x%x\n"
"%s"
"HINT: is " PG_FILE_PGCONTROL " corrupt?\n"
"HINT: does " PG_FILE_PGCONTROL " have a different layout than expected?",
crcCalculated, crcExpected,
pgVersionForce == NULL ? "" : "HINT: checksum values may be misleading due to forced version scan\n");
}
// Do not retry if the CRC is valid
break;
}
while (true);
// Validate the CRC
pgControlCrcValidate(controlFile, interface, pgVersionForce != NULL);
// Check the segment size
pgWalSegmentSizeCheck(result.version, result.walSegmentSize);
@ -389,6 +436,27 @@ pgControlFromFile(const Storage *const storage, const String *const pgVersionFor
FUNCTION_LOG_RETURN(PG_CONTROL, result);
}
/**********************************************************************************************************************************/
FN_EXTERN void
pgControlCheckpointInvalidate(Buffer *const buffer, const String *const pgVersionForce)
{
FUNCTION_LOG_BEGIN(logLevelDebug);
FUNCTION_LOG_PARAM(BUFFER, buffer);
FUNCTION_LOG_PARAM(STRING, pgVersionForce);
FUNCTION_LOG_END();
ASSERT(buffer != NULL);
const PgControl pgControl = pgControlFromBuffer(buffer, pgVersionForce);
const PgInterface *const interface = pgInterfaceVersion(pgControl.version);
const size_t crcOffset = pgControlCrcValidate(buffer, interface, pgVersionForce);
pgInterfaceVersion(pgControl.version)->controlCheckpointInvalidate(bufPtr(buffer));
pgControlCrcUpdate(buffer, interface->version, crcOffset);
FUNCTION_LOG_RETURN_VOID();
}
/**********************************************************************************************************************************/
FN_EXTERN uint32_t
pgControlVersion(unsigned int pgVersion)

View File

@ -94,6 +94,11 @@ WAL segment size is supported for versions below 11.
***********************************************************************************************************************************/
#define PG_WAL_SEGMENT_SIZE_DEFAULT ((unsigned int)(16 * 1024 * 1024))
/***********************************************************************************************************************************
Checkpoint written into pg_control on restore. This will prevent PostgreSQL from starting if backup_label is not present.
***********************************************************************************************************************************/
#define PG_CONTROL_CHECKPOINT_INVALID 0xDEAD
/***********************************************************************************************************************************
PostgreSQL Control File Info
***********************************************************************************************************************************/
@ -138,6 +143,9 @@ FN_EXTERN bool pgDbIsSystemId(unsigned int id);
FN_EXTERN Buffer *pgControlBufferFromFile(const Storage *storage, const String *pgVersionForce);
FN_EXTERN PgControl pgControlFromFile(const Storage *storage, const String *pgVersionForce);
// Invalidate checkpoint record in pg_control
FN_EXTERN void pgControlCheckpointInvalidate(Buffer *buffer, const String *pgVersionForce);
// Get the control version for a PostgreSQL version
FN_EXTERN uint32_t pgControlVersion(unsigned int pgVersion);

View File

@ -92,6 +92,22 @@ Get control crc offset
#endif
/***********************************************************************************************************************************
Invalidate control checkpoint. PostgreSQL skips the first segment so any LSN in that segment is invalid.
***********************************************************************************************************************************/
#if PG_VERSION > PG_VERSION_MAX
#elif PG_VERSION >= PG_VERSION_93
#define PG_INTERFACE_CONTROL_CHECKPOINT_INVALIDATE(version) \
static void \
pgInterfaceControlCheckpointInvalidate##version(unsigned char *const controlFile) \
{ \
((ControlFileData *)controlFile)->checkPoint = PG_CONTROL_CHECKPOINT_INVALID; \
}
#endif
/***********************************************************************************************************************************
Get the control version
***********************************************************************************************************************************/