Subject: Re: Changing the state of data checksums in a running cluster

From: Tomas Vondra <[email-masked]>
Date: Fri, 6 Feb 2026 00:15:27 +0100

On 2/5/26 21:01, Daniel Gustafsson wrote:
>> On 3 Feb 2026, at 17:58, Daniel Gustafsson <[email-masked]> wrote:
> 
>> I've implemented this in the attached patch and it seems very promising.
> 
> Turns out I was a bit quick on the send button, and having been focused on the
> failing test_checksums suite I missed running the other suites.  (I also
> managed to off-by-one-year name it 202503..) Thanks to some quick off-list
> feedback, here is an updated version which fixes the failing tests and has
> quite a number of smaller touchups.
> 
> Like the previous version, I am not sure I have the CHECKPOINT_xxx logic right
> and would love some eyes on it.
> 

Thanks for the updated patch. Indeed, I see occasional failures in the
006 test (I suppose 007 would fail too, but I haven't tried).

It's been ages since I looked at this patch, and I managed to forget
most of it. And I'm still learning how this new approach works, so I
don't have a great idea what's the root cause yet. But let me describe
what I see and ask some questions. Maybe that'll help us to figure out
what might be wrong.

Here's what the failing run did:

[22:11:00.610](0.364s) ok 2 - ensure checksums are set to off
[22:11:00.616](0.006s) # LSN before enabling: 0/4F0BCD78
[22:11:00.629](0.013s) ok 3 - ensure data checksums are transitioned to
inprogress-on
[22:11:09.594](8.965s) ok 4 - ensure data checksums are transitioned to on
[22:11:09.600](0.005s) # LSN after enabling: 0/B1CBD290
[22:11:09.675](0.075s) ok 5 - ensure data pages can be read back on primary
### Stopping node "main" using mode fast
# Running: pg_ctl --pgdata
/home/user/work/postgres/src/test/modules/test_checksums/tmp_check/t_006_pgbench_single_main_data/pgdata
--mode fast stop
waiting for server to shut down.... done
Latest checkpoint location:           0/B28CA0D8
Latest checkpoint's REDO location:    0/B28CA0D8
[22:11:11.517](0.486s) ok 8 - ensure checksums are set to on
[22:11:11.525](0.008s) # LSN before disabling: 0/B3B23C88
[22:11:11.539](0.013s) ok 9 - ensure data checksums are transitioned to off
[22:11:11.545](0.006s) # LSN after disabling: 0/B3B3F430
[22:11:13.596](2.052s) ok 10 - ensure data pages can be read back on primary
### Stopping node "main" using mode immediate
# Running: pg_ctl --pgdata
/home/user/work/postgres/src/test/modules/test_checksums/tmp_check/t_006_pgbench_single_main_data/pgdata
--mode immediate stop
waiting for server to shut down.... done
Latest checkpoint location:           0/B28CA158
Latest checkpoint's REDO location:    0/B28CA158
### Starting node "main"
### Stopping node "main" using mode fast
[22:11:14.130](0.428s) not ok 12 - no checksum validation errors in
primary log (during WAL recovery)
[22:11:14.130](0.000s)
[22:11:14.130](0.000s) #   Failed test 'no checksum validation errors in
primary log (during WAL recovery)'
#   at t/006_pgbench_single.pl line 180.

So it:

1) starts with checksums=off
2) flips them on by LSN 0/B1CBD290
3) restarts the instance with 'fast' mode
4) flips checksums to 'off'
5) restarts the instance with 'immediate' mode (=> crash)
6) finds checksum failures in the server log (after redo)

The failures like this (all seem to be from VM, and the VM page LSNs are
somewhat interesting too - I'll get to that shortly):

  2026-02-05 22:11:13.724 CET startup[286330] LOG:  redo starts at
0/B28CA1D8
  2026-02-05 22:11:13.729 CET startup[286330] LOG:  page verification
failed, calculated checksum 17285 but expected 50010 (page LSN 0/A1CF17F0)
  2026-02-05 22:11:13.729 CET startup[286330] CONTEXT:  WAL redo at
0/B295E930 for Heap/LOCK: ...
  2026-02-05 22:11:13.729 CET startup[286330] LOG:  invalid page in
block 4 of relation "base/5/16409_vm"; zeroing out page
  2026-02-05 22:11:13.732 CET startup[286330] LOG:  page verification
failed, calculated checksum 49687 but expected 51187 (page LSN 0/B29F5A20)
  2026-02-05 22:11:13.732 CET startup[286330] CONTEXT:  WAL redo at
0/B299E5F8 for Heap/UPDATE: ...
  2026-02-05 22:11:13.732 CET startup[286330] LOG:  invalid page in
block 42 of relation "base/5/16409_fsm"; zeroing out page
  ...

During the test, the cluster went through these checkpoints / checksums:

  lsn: 0/4E00E600, desc: CHECKPOINT_REDO wal_level replica; checksums off
  lsn: 0/4F0C8B10, prev 0/4F0C6DD8, desc: CHECKSUMS inprogress-on
  lsn: 0/52010FA8, desc: CHECKPOINT_REDO wal_level replica; checksums
inprogress-on
  lsn: 0/56015400, desc: CHECKPOINT_REDO wal_level replica; checksums
inprogress-on
  ...
  lsn: 0/AA005E00, desc: CHECKPOINT_REDO wal_level replica; checksums
inprogress-on
  lsn: 0/AE004988, desc: CHECKPOINT_REDO wal_level replica; checksums
inprogress-on
  lsn: 0/B1C7DFC8, prev 0/B1C7DFA0, desc: CHECKSUMS on
  lsn: 0/B200BE38, desc: CHECKPOINT_REDO wal_level replica; checksums on
  lsn: 0/B28CA0D8, desc: CHECKPOINT_SHUTDOWN redo 0/B28CA0D8; ...
checksums on; shutdown
  lsn: 0/B28CA158, desc: CHECKPOINT_SHUTDOWN redo 0/B28CA158; ...
checksums on; shutdown
  lsn: 0/B3B2F780, prev 0/B3B2F730, desc: CHECKSUMS inprogress-off
  lsn: 0/B3B2F858, prev 0/B3B2F810, desc: CHECKSUMS off
  ... crash / immediate restart ... (after 0/B3B3F430)
  lsn: 0/B47DED08, desc: CHECKPOINT_SHUTDOWN redo 0/B47DED08; ...
checksums off; shutdown
  lsn: 0/B5A7A600, prev 0/B5A788C8, desc: CHECKSUMS inprogress-on

The redo starts right after the checkpoint in 0/B28CA158. The failure
happens when trying to apply a FPW record to block in 1663/5/16409, but
the failure is actually in the associated VM, where block 4 has the
wrong checksum. How come?

Grepping the WAL, it seems the last time that VM block was changed is at
LSN 0/A1CF17F0 (which does match the failure log message):

lsn: 0/A1CEF7A0, ... desc: FPI , blkref #0: rel 1663/5/16409 fork vm blk
4 FPW

And AFAICS there are no other WAL records touching this particular vm
page (not even the XLOG_HEAP2_VISIBLE records).

Judging by the CHECKSUMS messages, the LSN 0/A1CEF7A0 is after the
cluster switched to the inprogress-on state, so at that point the
checksum should have been correct, right?

So how come the checksum is not correct when applying the heap page FPW
at LSN 0/B295E930? Either it got broken after enabling checksums in some
way, or it's the redo that got confused about the current "current"
checksum state.

It's a bit unfortunate we only detect invalid checksums if the failures
appear in the server log. I wonder if we could run pg_checksums after
the cluster gets shut down in 'fast' mode (we can't do that with
immediate shutdowns, of course). Didn't we do that in the past,
actually? I suppose there was a reason why we removed it, but maybe we
could still run that at least in some cases?

Anyway, let's assume the checksum was correct after enabling checksums.
That means it had to be broken by the redo, somehow? But why would that
be? The XLOG_CHECKSUMS record says we switched to "inprogress-off" at
0/B3B2F780, and "off" at 0/B3B2F858. Only then we stop updating the
checksums, right?

We "crash" shortly after LSN 0/B3B3F430 (because that's where the
immediate restart is, roughly), and there's a read-write pgbench running
in the background during all this. It might have modified the VM, but
why wouldn't that set the page LSN?

The second checksum failure is a bit more interesting - the VM page has
LSN 0/B29F5A20, which is *after* the redo location (and that checkpoint
has set "checksums on"). And the inprogress-off does not appear until
LSN 0/B3B2F780. So how come the VM page has invalid checksum?

I've only ever seen checksum failured on VM pages, never on any other
object. I have a feeling we're missing some important detail about how
visibility maps work ...

regards

-- 
Tomas Vondra

---

From: Tomas Vondra <[email-masked]>
Date: Fri, 6 Feb 2026 18:15:53 +0100

Hi,

I spent a bit more time looking at this today, and I figured out a
simpler way to "cause trouble" by using PITR. I don't know if this has
the same root cause as the failures in the 006 TAP test, but I find it
interesting and I think it highlights some issues with the new patch.

The 006 test enables/disables checksums while running pgbench in the
background, and randomly kills the instance in different ways (restart
with fast/immediate mode). It then checks if the recovery hits any
checksum errors during redo. And every now and then it sees failures,
but it's tedious to investigate that because a lot can happen between
the checksum state change and the crash, and it's not clear at which
point it actually got broken.

I realized I can do a simpler thing. I can enable WAL archiving, run the
pgbench, enable/disable checksums, etc. And then I can do PITR into
different places in the WAL, possibly going record by record, and verify
checksums in every of those places. That should make it much more
deterministic than the "random" 006 test. And it really does.

So I did that, mostly like this:

1) setup instance with WAL archiving enabled
2) create basebackup
3) initialize pgbench
4) run read-write pgbench in the background
5) disable checksums
5) wait for data_checksums to change to "off"
6) stop the instance

Then I look for CHECKSUMS records in the WAL using pg_waldump, which can
tell look something like this:

  ...
  lsn: 0/66707368, prev 0/66707318, desc: COMMIT 2026-02-06 ...
  lsn: 0/66707390, prev 0/66707368, desc: CHECKSUMS inprogress-off
  lsn: 0/667073B0, prev 0/66707390, desc: LOCK xmax: 48107, off: ...
  ...
  lsn: 0/66715238, prev 0/66715200, desc: HOT_UPDATE old_xmax: ...
  lsn: 0/66715288, prev 0/66715238, desc: CHECKSUMS off
  lsn: 0/667152A8, prev 0/66715288, desc: HOT_UPDATE old_xmax: ...
  ...

And then do PITR to each of those LSNs (or any other interesting LSN)
using this:

  recovery_target_lsn = '$LSN'
  recovery_target_action = 'shutdown'

And once the instance shuts down, I can verify checksums on the data
directory using pg_checksums. And for these LSNs listed above I get:

  0/66707368 - OK
  0/66707390 - OK
  0/667073B0 - OK
  0/66715238 - OK
  0/66715288 - 16155 failures
  0/667152A8 - 15948 failures

There's a couple interesting details/questions here:

1) It seems a bit surprising we can run pg_checksums even after the
checksums flip to "off" at LSN 0/66715288. AFAICS this is a direct
consequence of separating this from checkpoints, but checkpoints are
still responsible for writing the state into the control file.

But during redo we don't generate new checkpoints, so we get into a
state when the control file still says "checksums on", but the data
files may already contain pages without correct checksums.

FWIW the other direction (when enabling checksums) can end up in a
similar "disagreement". The control file will still say "off" (or maybe
"inprogress-on") while the in-memory state will say "on". But I guess
that's harmless, as it won't cause checksum failures. Or maybe it can
cause some other issues, not sure.

I'm not sure what to do about this. The control file is updated only
lazily, but e.g. pg_checksums relies on it not being stale. Or at least
not stale "too much". The last patch ensured we have a checkpoint for
each state change, i.e. we can't go through both (on -> inprogress-off)
and (inprogress-off -> off) within a single checkpoint interval.

And that would prevent this issue, AFAIK. If we updated the control file
to say "inprogress-off" at some point, pg_checksums would know not to
try to verify checksums.

Maybe there are other issues, though. Having two places determining the
checksum state of an instance, and allowing them to get out of sync in
some way seems a bit tricky.

2) I don't understand how applying a single WAL record can trigger so
many checksum failures. Going from 0/66715238 to 0/66715288, which
applies the XLOG_CHECKSUMS record, triggered ~16k failures. How come?
That doesn't even touch any pages, AFAICS.

Similarly, applying the single HOT_UPDATE at 0/667152A8 (which per
pg_waldump touches only a single block) makes ~200 failures to go away.

I'm sure there is a simple explanation for this, but it's puzzling.

regards

-- 
Tomas Vondra

---

From: Michael Banck <[email-masked]>
Date: Mon, 9 Feb 2026 08:52:10 +0100

Hi,

On Fri, Feb 06, 2026 at 12:15:27AM +0100, Tomas Vondra wrote:
> immediate shutdowns, of course).

Well, maybe we could do that with immediate shutdowns if we add a
--force parameter to pg_checksums that is clearly labelled/docmented as
being dangerous for running instances (but the instance wouldn't run in
this case).  Or something undocumented like
--debug-force-unclean-controlfile.

Michael

---

From: Daniel Gustafsson <[email-masked]>
Date: Thu, 12 Mar 2026 00:56:07 +0100

> On 6 Feb 2026, at 18:15, Tomas Vondra <[email-masked]> wrote:

> interesting and I think it highlights some issues with the new patch.

I've encoded some of this into a new test suite in the attached patchset,
having it in a repeatable test should make it easier to reproduce and reason
about.

Attached is a rebase of the patchset which further refines the switch to using
XLOG_CHECKPOINT_* records which was proposed by Andres upstream.  The errors I
encountered could be traced to replay across multiple state transitions (with
at least one case of ON->OFF), and re-introducing checkpoints at on and off
state transitions helped removing those.

* A lot of code has been moved out from xlog.c and into the file which contains
  the code for the datachecksumsworker.  This file has in turn been renamed to
  signal what it's used for.  It should probably be moved from backend/postmaster
  but it's not clear to me where it should go.
* Improved signalhandling in the worker code.
* A new testsuite for PITR is added, which is based on Tomas' findings.  This
  can be further increased and refined but it's at least a start.
* Another new testsuite which changes full_page_writes settings is also added.
  This one should perhaps be rolled into 001_basic, not sure.
* Tests 006 and 007, the pgbench "torture test" suites, are now being run in a
  pared-down fashion when PG_TEST_EXTRA isn't defined, rather than being skipped
  entirely
* The minimum max_wal_size in tests is now 32 instead of 64.
* Lots of polish and small tweaking and new comments

There are a few TODOs, the more interesting ones include:

* The change to XLOG_CHECKPOINT_REDO to move the wal_level into a proper record
  structure should be pulled out as a 0001 patch as it's an cleanup that has
  value on its own.
* Try to move even more code awy from xlog.c
* Address, and remove, all XXX comments (most of which stem from a much earlier
  version of the patchset).
* The code for emitting, and waiting on, the procsignalbarrier exists in
  multiple copies which needs to be refactored into a single one.
* Tests failed in Autoconf on CI, which I need to fix.

My next step will be addressing these TODOs, but I wanted to get a new version
on the list sooner rather than later.

--
Daniel Gustafsson

---

From: Tomas Vondra <[email-masked]>
Date: Sun, 15 Mar 2026 23:47:36 +0100

>   structure should be pulled out as a 0001 patch as it's an cleanup that has

Makes sense, but it's going to be harder because since d774072f0040 all
4 bits in XLR_INFO are used.

> * Tests failed in Autoconf on CI, which I need to fix.

A couple minor issues I noticed:

1) Is this actually doing the expected thing?

   INJECTION_POINT("datachecksumsworker-initial-dblist", DatabaseList);

We're passing a regular pointer to the database list, so can the
injection point actually modify it? I suppose it happens to work because
dc_dblist() removes the last item, so the pointer to the list does not
change. But that's luck.

2) ProcessAllDatabases may be misusing processed_databases

The loop is using processed_databases to track number of databases
processed in each interation, but it's also being used for progress
reporting through pgstat_progress_update_param(). So if we have to go
through multiple iterations of the loop, it'll reset to 0 and then only
count newly processed ones. But the progress still shows the original
total number of databases. IMO it needs to track the cumulative number
of processed databases.

3) DATACHECKSUMSWORKER_MAX_DB_RETRIES / DATACHECKSUMSWORKER_FAILED

What happens if a database reaches the maximum number of retries? We
mark that entry as failed, but AFAIK we'll still try to process any
remaining databases. Isn't that already doomed and we won't be able to
enable checksums? So why not to simply abort the loop right away?

regards

-- 
Tomas Vondra

---

From: Tomas Vondra <[email-masked]>
Date: Sun, 15 Mar 2026 23:56:27 +0100

On 3/12/26 23:56, Daniel Gustafsson wrote:
>> On 12 Mar 2026, at 00:56, Daniel Gustafsson <[email-masked]> wrote:
> 
> One thing I forgot to mention was that this patch uncovered the same vmap (and
> fsm) issue as is being discussed in [0].  At the time I didn't connect the dots
> that it was an issue even without this patch so I tried to solve it as part of
> this work.  After speaking with Andres I ripped out my attempts and will let
> the other thread take care of the issue independently of this.
> 

Yeah, this is quite annoying. It makes 100% sense to wait for the
visibility map to get fixed (by properly WAL-logging it). But I'm not
aware of any such fixes for the FSM though, so I what's the plan for
that? IMO the right solution would be to do proper WAL for FSM, but that
seems like a significant amount of work.

regards

-- 
Tomas Vondra

---

From: Tomas Vondra <[email-masked]>
Date: Sun, 15 Mar 2026 23:59:04 +0100

> 4 bits in XLR_INFO are used.

Correction. This is not a problem for the XLOG_CHECKPOINT_REDO patch,
but for adding XLOG_CHECKSUMS. I guess we'll need a second xlog rmgr.

regards

-- 
Tomas Vondra

---

From: Daniel Gustafsson <[email-masked]>
Date: Tue, 17 Mar 2026 23:01:13 +0100

> On 17 Mar 2026, at 12:45, Heikki Linnakangas <[email-masked]> wrote:
> 
> Here's a quick, non-exhaustive review of v20260316 of the patch. I haven't followed the thread, so forgive me if some of these things have already been discussed.
> 
> I don't see any major issues, just a bunch of small things:

Thanks you so much for looking!

>> +/*
>> + * Checksum version 0 is used for when data checksums are disabled (OFF).
>> + * PG_DATA_CHECKSUM_VERSION defines that data checksums are enabled in the
>> + * cluster and PG_DATA_CHECKSUM_INPROGRESS_{ON|OFF}_VERSION defines that data
>> + * checksums are either currently being enabled or disabled.
>> + */
>> +typedef enum ChecksumType
>> +{
>> + PG_DATA_CHECKSUM_OFF = 0,
>> + PG_DATA_CHECKSUM_VERSION,
>> + PG_DATA_CHECKSUM_INPROGRESS_ON_VERSION,
>> + PG_DATA_CHECKSUM_INPROGRESS_OFF_VERSION
>> +} ChecksumType;
> 
> Naming: This is called "VERSION", but also "Type", and I'm a little confused. I think the field was called "version" because we envisioned that we might want to use a different checksum algorithm in the future, which could then be indicated by a different checksum version number. If we do that in the future, how will these values look like?

This is an old leftover the patch has carried for too long, the inprogress
states were named _VERSION simply due to the ON state being so.  I've renamed
them to just INPROGRESS_{ON|OFF} and made sure to place the ON state as the
last element in the enum.  If we add other checksum algorithms in the future
there are different naming schemes we could use depending on which level of
implementation abstraction we want.  We can either append a version number (and
consider the current ON state to be version 1 or 0), or an algorithm name (with
the current ON state a defuault algorithm).

    * PG_DATA_CHECKSUM_<alg>_VERSION
    * PG_DATA_CHECKSUM_VERSION_<n>

To further reduce this naming confusing I have done the following:

s/LocalDataChecksumVersion/LocalDataChecksumState/
s/ChecksumType/ChecksumStateType/
s/xl_checksum_state->new_checksumtype/xl_checksum_state->new_checksum_state/
s/Checkpoint->dataChecksumVersion/Checkpoint->dataChecksumState/

>> @@ -831,9 +852,10 @@ XLogInsertRecord(XLogRecData *rdata,
>> * only happen just after a checkpoint, so it's better to be slow in
>> * this case and fast otherwise.
>> *
>> - * Also check to see if fullPageWrites was just turned on or there's a
>> - * running backup (which forces full-page writes); if we weren't
>> - * already doing full-page writes then go back and recompute.
>> + * Also check to see if fullPageWrites was just turned on, there's a
>> + * running backup or if checksums are enabled (all of which forces
>> + * full-page writes); if we weren't already doing full-page writes
>> + * then go back and recompute.
>> *
>> * If we aren't doing full-page writes then RedoRecPtr doesn't
>> * actually affect the contents of the XLOG record, so we'll update
> 
> It's true that if checksums are enabled, we must do full-page writes. But that's already included in the "fullpageWrites" flag. We're not specifically checking whether checksums are enabled here, so I find this comment change more confusing than helpful.

Fair enough, removed.

>> void
>> SetDataChecksumsOnInProgress(void)
>> {
>> uint64 barrier;
>> Assert(ControlFile != NULL);
>> /*
>> * The state transition is performed in a critical section with
>> * checkpoints held off to provide crash safety.
>> */
>> START_CRIT_SECTION();
>> MyProc->delayChkptFlags |= DELAY_CHKPT_START;
>> XLogChecksums(PG_DATA_CHECKSUM_INPROGRESS_ON_VERSION);
>> SpinLockAcquire(&XLogCtl->info_lck);
>> XLogCtl->data_checksum_version = PG_DATA_CHECKSUM_INPROGRESS_ON_VERSION;
>> SpinLockRelease(&XLogCtl->info_lck);
>> barrier = EmitProcSignalBarrier(PROCSIGNAL_BARRIER_CHECKSUM_INPROGRESS_ON);
>> MyProc->delayChkptFlags &= ~DELAY_CHKPT_START;
>> END_CRIT_SECTION();
>> /*
>> * Update the controlfile before waiting since if we have an immediate
>> * shutdown while waiting we want to come back up with checksums enabled.
>> */
>> LWLockAcquire(ControlFileLock, LW_EXCLUSIVE);
>> ControlFile->data_checksum_version = PG_DATA_CHECKSUM_INPROGRESS_ON_VERSION;
>> UpdateControlFile();
>> LWLockRelease(ControlFileLock);
>> /*
>> * Await state change in all backends to ensure that all backends are in
>> * "inprogress-on". Once done we know that all backends are writing data
>> * checksums.
>> */
>> WaitForProcSignalBarrier(barrier);
>> }
> 
> If a checkpoint starts, finishes, and you then crash, all between the END_CRIT_SECTION() and LWLockAcquire here, are you left with with wrong 'data_checksum_version' in the control file? (Highly theoretical, the window is minuscule, but still)

Thats true.  In that case the REDO record should have the right value though so
during replay it will be handled.

>> +/*
>> + * InitLocalControlData
>> + *
>> + * Set up backend local caches of controldata variables which may change at
>> + * any point during runtime and thus require special cased locking. So far
>> + * this only applies to data_checksum_version, but it's intended to be general
>> + * purpose enough to handle future cases.
>> + */
>> +void
>> +InitLocalDataChecksumVersion(void)
>> +{
>> + SpinLockAcquire(&XLogCtl->info_lck);
>> + SetLocalDataChecksumVersion(XLogCtl->data_checksum_version);
>> + SpinLockRelease(&XLogCtl->info_lck);
>> +}
> 
> Function name in the comment doesn't match.

Fixed.

>> + ereport(WARNING,
>> + errmsg("data checksums state has been set to off"),
>> + errhint("If checksums were being enabled during shutdown then processing must be manually restarted."));
> 
> This is a little unclear. If we reach this, we know that the checkum calculation was interrupted, no need to caveat it with "if checksums were being enabled", right? Maybe something like:
> 
> WARNING: enabling data checksums was interrupted
> HINT: The checksum processing must be manually restarted
> 
> I feel we're missing a term for the process of enabling checkums. "checksum enablement"? "initial checksum calculation"? In some comments it's called "checksumming".

I'm not a fan of "checksumming" so I've gone over all occurrences and reworded
them.

>> * The DataChecksumsWorker will compile a list of databases which exist at the
>> * start of checksumming, and once all are processed will regenerate the list
>> * and start over processing any new entries. Once there are no new entries on
>> * the list, processing will end.  All databases MUST BE successfully processed
>> * in order for data checksums to be enabled, the only exception are databases
>> * which are dropped before having been processed.
> 
> Why does the list need to be regenerated? Any newly created databases would already have checksums enabled, right? Is this because you might use database that hadn't been fully processed yet as the template? Would be good to mention if that's the reason.

Correct, we cannot rely on the initial list being the full list.  Comment
expanded.

> Could CREATE DATABASE calculate the checksums when it copies the files?

I guess it could, though I think that should be a separate patch as this is
already (too) big.  It's a good idea though, added to the "ideas for future
optimizations" section in the documentation comment.

>> * means that connected backends will change state at different times.  If
>> * waiting for a barrier is done during startup, for example during replay, it
>> * is important to realize that any locks held by the startup process might
>> * cause deadlocks if backends end up waiting for those locks while startup
>> * is waiting for a procsignalbarrier.
> 
> Don't we absorb procsignalbarriers while waiting on a lock? Or does this mean LWlocks?

It is referring to LWlocks.

>> * Backends transition Bd -> Bi via a procsignalbarrier which is emitted by the
>> * DataChecksumsLauncher.  When all backends have acknowledged the barrier then
>> * Bd will be empty and the next phase can begin: calculating and writing data
>> * checksums with DataChecksumsWorkers.  When the DataChecksumsWorker processes
>> * have finished writing checksums on all pages, data checksums are enabled
>> * cluster-wide via another procsignalbarrier. There are four sets of backends
>> * where Bd shall be an empty set:
>> *
>> * Bg: Backend updating the global state and emitting the procsignalbarrier
>> * Bd: Backends in "off" state
>> * Be: Backends in "on" state
>> * Bi: Backends in "inprogress-on" state
>> *
>> * Backends in Bi and Be will write checksums when modifying a page, but only
>> * backends in Be will verify the checksum during reading. The Bg backend is
>> * blocked waiting for all backends in Bi to process interrupts and move to
>> * Be. Any backend starting while Bg is waiting on the procsignalbarrier will
>> * observe the global state being "on" and will thus automatically belong to
>> * Be.  Checksums are enabled cluster-wide when Bi is an empty set. Bi and Be
>> * are compatible sets while still operating based on their local state as
>> * both write data checksums.
> 
> Isn't "Bg" always the data checksums launcher process? In the previous paragraph before this, you didn't list the data checksums launcher as a separate "set".

Good catch, added.

>> /*
>> * Configuration of conditions which must match when absorbing a procsignal
>> * barrier during data checksum enable/disable operations.  A single function
>> * is used for absorbing all barriers, and the set of conditions to use is
>> * looked up in the checksum_barriers struct.  The struct member for the target
>> * state defines which state the backend must currently be in, and which it
>> * must not be in.
>> *
>> * The reason for this explicit checking is to ensure that processing cannot
>> * be started such that it breaks the assumptions of the state machine.  See
>> * datachecksumsworker.c for a lengthy discussion on these states.
> 
> This is "datachecksumworker.c", aka "datachecksum_state.c", so this "See datachecksumworker.c" refers to itself.

Ugh, I missed fixing this when I recently moved code out of xlog.c. Fixed.

>> *
>> * MAX_BARRIER_CONDITIONS must match largest number of sets in barrier_eq and
>> * barrier_ne in the below checksum_barriers definition.
>> */
>> #define MAX_BARRIER_CONDITIONS 2
>> typedef struct ChecksumBarrierCondition
>> {
>> /* The target state of the barrier */
>> int target;
>> /* A set of states in which at least one MUST match the current state */
>> int barrier_eq[MAX_BARRIER_CONDITIONS];
>> /* The number of elements in the barrier_eq set */
>> int barrier_eq_sz;
>> /* A set of states which all MUST NOT match the current state */
>> int barrier_ne[MAX_BARRIER_CONDITIONS];
>> /* The number of elements in the barrier_ne set */
>> int barrier_ne_sz;
>> } ChecksumBarrierCondition;
> 
> I don't understand the logic here. Why do you need both 'barrier_eq' and 'barrier_ne'? Wouldn't it be enough to just list the allowed states, and everything else is not allowed?

It was in an attempt to make it more readable, to avoid long lists of states
which look quite similar.  It could be handled with just a set of required
allowed states if that's what is preferred, I don't have strong opinions myself
really.

>> static const ChecksumBarrierCondition checksum_barriers[4] =
>> {
>> {PG_DATA_CHECKSUM_OFF, {PG_DATA_CHECKSUM_INPROGRESS_ON_VERSION, PG_DATA_CHECKSUM_INPROGRESS_OFF_VERSION}, 2, {PG_DATA_CHECKSUM_VERSION}, 1},
>> {PG_DATA_CHECKSUM_VERSION, {PG_DATA_CHECKSUM_INPROGRESS_ON_VERSION}, 1, {0}, 0},
>> {PG_DATA_CHECKSUM_INPROGRESS_ON_VERSION, {0}, 0, {PG_DATA_CHECKSUM_VERSION}, 1},
>> {PG_DATA_CHECKSUM_INPROGRESS_OFF_VERSION, {PG_DATA_CHECKSUM_VERSION}, 1, {0}, 0},
>> };
> 
> This is pretty jarring to read. Maybe use C99 designated initializers, or at least split over multiple lines. Or a plain array of of allowed transitions, (current, target) -> bool. Or a function with switch-case statement.

I moved to designated initializers which I think made quite the improvement.
This code was at first in one function per barrier, with the states encoded in
if/then/else, later it was merged into a single function.  The logic was
however getting so convoluted that several subtle bugs were found there, after
moving to this configuration-driven approach it has been more reliable and (IMHO)
easier to review.

>> /*
>> * If the postmaster crashed we cannot end up with a processed database so
>> * we have no alternative other than exiting. When enabling checksums we
>> * won't at this time have changed the pg_control version to enabled so
>> * when the cluster comes back up processing will have to be restarted.
>> * When disabling, the pg_control version will be set to off before this
>> * so when the cluster comes up checksums will be off as expected.
>> */
>> if (status == BGWH_POSTMASTER_DIED)
>> ereport(FATAL,
>> errcode(ERRCODE_ADMIN_SHUTDOWN),
>> errmsg("cannot enable data checksums without the postmaster process"),
>> errhint("Restart the database and restart data checksum processing by calling pg_enable_data_checksums()."));
> 
> Do we reach here when disabling checksums? The comment suggests so, but in that case the hint would be wrong.

The comment is wrong, we only process databases when enabling checksums so
during disabling we cannot reach here.  Fixed.

>> /*
>> * launcher_exit
>> *
>> * Internal routine for cleaning up state when the launcher process exits. We
>> * need to clean up the abort flag to ensure that processing started again if
>> * it was previously aborted (note: started again, *not* restarted from where
>> * it left off).
>> */
>> static void
>> launcher_exit(int code, Datum arg)
>> {
>> if (launcher_running)
>> {
>> LWLockAcquire(DataChecksumsWorkerLock, LW_EXCLUSIVE);
>> launcher_running = false;
>> DataChecksumsWorkerShmem->launcher_running = false;
>> /*
>> * TODO: how to really handle the worker still running when the
>> * launcher exits?
>> */
>> if (DataChecksumsWorkerShmem->worker_running)
>> ereport(LOG,
>> errmsg("data checksums launcher exiting while worker is still running"));
>> LWLockRelease(DataChecksumsWorkerLock);
>> }
>> }
> 
> What "abort flag"?

There is an abort_requested flag which is set by the signal handler, but I'm
not sure why the clearing of the flag wasn't done here as the comment states it
should be. Fixed.

>> /*
>> * Get a list of all databases to process. This may include databases that
>> * were created during our runtime.  Since a database can be created as a
>> * copy of any other database (which may not have existed in our last
>> * run), we have to repeat this loop until no new databases show up in the
>> * list.  Here the initial list for the loop processing is generated after
>> * waiting for all existing transactions to finish to ensure that we can
>> * see any database which was created even if the transaction in which it
>> * was created started before checksums were being processed.
>> */
>> WaitForAllTransactionsToFinish();
>> DatabaseList = BuildDatabaseList();
> 
> What if a database is dropped and another one is created with the same database OID?

Good point.  We have the name of the database so we could compare Oid and name,
and require both to match.  That would still be foiled by a new database using
the same Oid and name though.  Any ideas on what could be done?

>> /*
>> * This is the only place where we check if we are asked to abort, the
>> * abortion will bubble up from here. It's safe to check this without
>> * a lock, because if we miss it being set, we will try again soon.
>> */
>> Assert(operation == ENABLE_DATACHECKSUMS);
>> LWLockAcquire(DataChecksumsWorkerLock, LW_SHARED);
>> if (DataChecksumsWorkerShmem->launch_operation == DISABLE_DATACHECKSUMS)
>> abort_requested = true;
>> LWLockRelease(DataChecksumsWorkerLock);
> 
> The comment justifies that it's safe to do this without a lock, but it grabs the lock anyway.

I have a feeling that's an old comment that was missed when updating, we should
grab a lock here.  Fixed.

>> @@ -107,7 +107,8 @@ PageIsVerified(PageData *page, BlockNumber blkno, int flags, bool *checksum_fail
>> */
>> if (!PageIsNew(page))
>> {
>> - if (DataChecksumsEnabled())
>> + HOLD_INTERRUPTS();
>> + if (DataChecksumsNeedVerify())
>> {
>> checksum = pg_checksum_page(page, blkno);
>> @@ -118,6 +119,7 @@ PageIsVerified(PageData *page, BlockNumber blkno, int flags, bool *checksum_fail
>> *checksum_failure_p = true;
>> }
>> }
>> + RESUME_INTERRUPTS();
>>   /*
>> * The following checks don't prove the header is correct, only that
> 
> This is to ensure that we don't absorb a barrier while calculating the checksum, which could transition us from "on" to "off" ? This is probably not really needed because there are no CHECK_FOR_INTERRUPTS() calls in pg_checksum_page(), but better safe than sorry I guess. Another approach would be to check DataChecksumsNeedVerify() again if the checksum didn't match. In any case, a comment would be in order.

Right, it's a belts-and-suspenders type of safety measure. Comment added.

> s/datachecksumsworker.c/datachecksum_state.c/

Fixed

> DataChecksumsOnInProgress() and DataChecksumsOffInProgress() are unused.

Fixed

> Attached are a few more trivial fixes, in patch format.

Thanks, applied.

The above changes are kept in 0003 in order to make them easier to see in this
somewhat large patchset.

--
Daniel Gustafsson

---

From: Daniel Gustafsson <[email-masked]>
Date: Thu, 19 Mar 2026 14:49:21 +0100

> On 18 Mar 2026, at 09:06, Heikki Linnakangas <[email-masked]> wrote:
> On 18/03/2026 00:01, Daniel Gustafsson wrote:

> Well, if CREATE DATABASE computed the checksums, that would fix this too. Actually, at least in the default wal_log=true mode, doesn't it already do that? It goes through the buffer cache as usual, I presume the checksums will be computed too.

That's a very good point, if the CREATE DATABASE is issued with wal_log and not
file_copy it will mark the buffers dirty during copying which will calculate
and set the checkpoint.  If file_copy isn't allowed during inprogress-on then
the logic around rescanning databases for new entries could be quite
simplified.

--
Daniel Gustafsson

---

From: Daniel Gustafsson <[email-masked]>
Date: Fri, 20 Mar 2026 00:34:02 +0100

> On 19 Mar 2026, at 14:49, Daniel Gustafsson <[email-masked]> wrote:

> the logic around rescanning databases for new entries could be quite

I tried to implement this on top of a fresh rebase, the attached 0003 contains
a very lightly tested version of this simplified logic (there is a removal of a
useless debug logging as well which while unrelated happened to sneak into this
work).

It now works off a single databaselist and relies on wal_copy to handle any
created database regardless of which template is used (file_copy is not allowed
while inprogress-on).  This gives a chance to do better erroring out in case of
failed databases, the code is still vulnerable to a the DROP/CREATE with the
same Oid, but that seems doable to solve.

It needs more testing, and likely has bugs, but I wanted to share it as soon as
possible to see if was along what you were thinking.  Personally I think the
code a lot easier to follow with this.

--
Daniel Gustafsson

---

From: Heikki Linnakangas <[email-masked]>
Date: Sat, 28 Mar 2026 01:30:56 +0200

On 28/03/2026 00:03, Daniel Gustafsson wrote:
> The attached rebase contains lots more polish, mostly renaming variable names
> for clarity, tidying up comments and documentation and some smaller bits of
> cleanup like moving more code out of xlog.c.
> 
> This version runs all the tests in a normal test-run, with a few of them pared
> down with larger runs gated by PG_TEST_EXTRA.  I thinkt the tests are still too
> expensive in the event of getting committed, but it's helpful to have them
> during dev and test.  Executing pgbench sometimes fails in CI but I've been
> unable to reproduce that so not entirely sure what is going on there.
> 
> Heikki, Andres and Tomas; as you have been reviewing this patchset, what do you
> feel is left for considering this for commit?  (Apart from figuring out the CI
> test thing mentioned above which I think is a buildsystem issue.) I think 0001
> could be considered independently of 0002 and is cleanup in it's own right.

+1 for committing 0001 right away.

Some leftover stuff remains, related to the change that it no longer 
rebuilds the list of databases:

> + * The DataChecksumsWorker will compile a list of all databases at the start,
> + * and once all are processed will regenerate the list and start over
> + * processing any new entries. Once there are no new entries on the list,
> + * processing will end.  The regenerated list is required since databases can
> + * be created concurrently with data checksum processing, using a template
> + * database which has yet to be processed. All databases MUST BE successfully
> + * processed in order for data checksums to be enabled, the only exception are
> + * databases which are dropped before having been processed.

and here:

> +/*
> + * Test to remove an entry from the Databaselist to force re-processing since
> + * not all databases could be processed in the first iteration of the loop.
> + */
> +void
> +dc_dblist(const char *name, const void *private_data, void *arg)

The above talks about re-processing, but that doesn't happen anymore. I 
suspect the test that uses this is now obsolete or broken.

> +	while (true)
> +	{
> +		int			processed_databases = 0;
> +
> +		foreach_ptr(DataChecksumsWorkerDatabase, db, DatabaseList)
> +		{
> +			DataChecksumsWorkerResult result;
> +			DataChecksumsWorkerResultEntry *entry;
> +			bool		found;
> +
> +			/*
> +			 * Check if this database has been processed already, and if so
> +			 * whether it should be retried or skipped.
> +			 */
> +			entry = (DataChecksumsWorkerResultEntry *) hash_search(ProcessedDatabases, &db->dboid,
> +																   HASH_FIND, NULL);
> +

I guess this works, but wouldn't it be simpler to remove entries from 
DatabaseList when they're successfully processed?

> --- a/src/backend/commands/dbcommands.c
> +++ b/src/backend/commands/dbcommands.c
> @@ -1044,7 +1044,14 @@ createdb(ParseState *pstate, const CreatedbStmt *stmt)
>  		if (pg_strcasecmp(strategy, "wal_log") == 0)
>  			dbstrategy = CREATEDB_WAL_LOG;
>  		else if (pg_strcasecmp(strategy, "file_copy") == 0)
> +		{
> +			if (DataChecksumsInProgress())
> +				ereport(ERROR,
> +						errcode(ERRCODE_INVALID_PARAMETER_VALUE),
> +						errmsg("create database strategy \"%s\" not allowed when data checksums are being enabled",
> +							   strategy));
>  			dbstrategy = CREATEDB_FILE_COPY;
> +		}
>  		else
>  			ereport(ERROR,
>  					(errcode(ERRCODE_INVALID_PARAMETER_VALUE),

Is this enough? Is it possible that you start CREATE DATABASE in 
file_copy mode, and while it's already running but hasn't copied 
everything yet, you turn checksums on?

- Heikki

---

From: Daniel Gustafsson <[email-masked]>
Date: Mon, 30 Mar 2026 16:43:15 +0200

Rebase to keep the CFBot happy since it conflicted with recent commits.

--
Daniel Gustafsson

---

From: Daniel Gustafsson <[email-masked]>
Date: Thu, 2 Apr 2026 01:01:49 +0200

Attached is another rebase which adds a note on checksum failure logging for
buffers with ZERO_ON_ERROR, which - apart from being useful information to have
for the user investigating a failure - allows the tests to differentiate
between false positives due to the VM buffer issues discussed in [1].

This patch has now undergone a nontrivial amount of review and testing, have
addressed all review comments and have had no architectural changes in quite
some time.  It builds green on CI, and assuming this attached patch also builds
green in CFBot - and no concerns are raised here - I intend to commit this
wihin the next few days ahead of the freeze.  The more expensive tests which
are currently enabled will again be moved under PG_TEST_EXTRA to accommodate
for slower machines present in the buildfarm.

--
Daniel Gustafsson

[1] https://postgr.es/m/[email-masked]

---

From: Heikki Linnakangas <[email-masked]>
Date: Thu, 2 Apr 2026 12:27:51 +0300

It'd be good to print a LOG message when the checksums state changes, to 
have a trail in the log of when checksums were enabled/disabled. 
Something like:

LOG:  enabling checksums was requested, starting checksum calculation
...
LOG:  checksums calculation finished, checksums are now enabled

On 02/04/2026 02:01, Daniel Gustafsson wrote:
> +		if (result == DATACHECKSUMSWORKER_FAILED)
> +		{
> +			/*
> +			 * Disable checksums on cluster, because we failed one of the
> +			 * databases and this is an all or nothing process.
> +			 */
> +			SetDataChecksumsOff();
> +			ereport(ERROR,
> +					errcode(ERRCODE_INSUFFICIENT_RESOURCES),
> +					errmsg("data checksums failed to get enabled in all databases, aborting"),
> +					errhint("The server log might have more information on the cause of the error."));
> +		}

This got me thinking, what happens if the the data checksums launcher 
encounters some other error, for example if you SIGTERM it? The system 
is left in 'inprogress-on' state, but because the launcher is gone it 
will never finish and 'pg_stat_progress_data_checksums' will be empty.

Perhaps launcher_exit() should call SetDataChecksumsOff()?

> 					errmsg("data checksums launcher exiting while worker is still running"));

That TODO should be addressed somehow.

> +		/*
> +		 * As of now we only update the block counter for main forks in order
> +		 * to not cause too frequent calls. TODO: investigate whether we
> +		 * should do it more frequent?
> +		 */
> +		if (forkNum == MAIN_FORKNUM)
> +			pgstat_progress_update_param(PROGRESS_DATACHECKSUMS_BLOCKS_DONE,
> +										 (blknum + 1));

We're updating it for every block in the main fork, but not at all for 
other forks? What a bizarre way of avoiding too frequent calls :-). I 
think you could just call this on every page, 
pgstat_progress_update_param() is supposed to be very fast. For 
comparison, e.g. index build calls it for every tuple.

> +/*
> + * Configuration of conditions which must match when absorbing a procsignal
> + * barrier during data checksum enable/disable operations.  A single function
> + * is used for absorbing all barriers, and the set of conditions to use is
> + * looked up in the checksum_barriers struct.  The struct member for the target
> + * state defines which state the backend must currently be in, and which it
> + * must not be in.
> + *
> + * The reason for this explicit checking is to ensure that processing cannot
> + * be started such that it breaks the assumptions of the state machine.
> + *
> + * MAX_BARRIER_CONDITIONS must match largest number of sets in barrier_eq and
> + * barrier_ne in the below checksum_barriers definition.
> + */
> +#define MAX_BARRIER_CONDITIONS 2
> +typedef struct ChecksumBarrierCondition
> +{
> +	/* The target state of the barrier */
> +	int			target;
> +	/* A set of states in which at least one MUST match the current state */
> +	int			barrier_eq[MAX_BARRIER_CONDITIONS];
> +	/* The number of elements in the barrier_eq set */
> +	int			barrier_eq_sz;
> +	/* A set of states which all MUST NOT match the current state */
> +	int			barrier_ne[MAX_BARRIER_CONDITIONS];
> +	/* The number of elements in the barrier_ne set */
> +	int			barrier_ne_sz;
> +} ChecksumBarrierCondition;
> +
> +static const ChecksumBarrierCondition checksum_barriers[4] =
> +{
> +	/*
> +	 * When disabling checksums, either inprogress state is Ok but checksums
> +	 * must not be in the enabled state.
> +	 */
> +	{
> +		.target = PG_DATA_CHECKSUM_OFF,
> +		.barrier_eq = {PG_DATA_CHECKSUM_INPROGRESS_ON, PG_DATA_CHECKSUM_INPROGRESS_OFF},
> +		.barrier_eq_sz = 2,
> +		.barrier_ne = {PG_DATA_CHECKSUM_VERSION},
> +		.barrier_ne_sz = 1
> +	},
> +	/* When enabling the current state must be inprogress-on */
> +	{
> +		.target = PG_DATA_CHECKSUM_VERSION,
> +		.barrier_eq = {PG_DATA_CHECKSUM_INPROGRESS_ON},
> +		.barrier_eq_sz = 1,
> +		{0}, 0
> +	},
> +
> +	/*
> +	 * When moving to inprogress-on the current state cannot enabled, but when
> +	 * moving to inprogress-off the current state must be enabled.
> +	 */
> +	{
> +		.target = PG_DATA_CHECKSUM_INPROGRESS_ON,
> +		{0}, 0,
> +		.barrier_ne = {PG_DATA_CHECKSUM_VERSION},
> +		.barrier_ne_sz = 1
> +	},
> +	{
> +		.target = PG_DATA_CHECKSUM_INPROGRESS_OFF,
> +		.barrier_eq = {PG_DATA_CHECKSUM_VERSION},
> +		.barrier_eq_sz = 1,
> +		{0}, 0
> +	},
> +};

I find this to still be a pretty complicated and unclear way of 
representing the allowed transitions. There are only 16 possible 
transitions, and only 6 of them are allowed. How about listing the 
allowed ones directly:

/* Allowed transitions: from, to */
{
     /*
      * Disabling checksums: If checksums are currently enabled,
      * disabling must go through the 'inprogress-off' state.
      */
     {PG_DATA_CHECKSUM_VERSION, PG_DATA_CHECKSUM_INPROGRESS_OFF},
     {PG_DATA_CHECKSUM_INPROGRESS_OFF, PG_DATA_CHECKSUM_OFF},

     /*
      * If checksums are in the process of being enabled, but are
      * not yet being verified, we can abort by going back to 'off'
      * state.
      */
     {PG_DATA_CHECKSUM_INPROGRESS_ON, PG_DATA_CHECKSUM_OFF},

     /*
      * Enabling checksums must normally go through the 'inprogress-on'
      * state.
      */
     {PG_DATA_CHECKSUM_OFF, PG_DATA_CHECKSUM_INPROGRESS_ON},
     {PG_DATA_CHECKSUM_INPROGRESS_ON, PG_DATA_CHECKSUM_VERSION},

     /*
      * If checksums are being disabled but all backends are still
      * computing checksums, we can go straight back to 'on'
      */
     {PG_DATA_CHECKSUM_INPROGRESS_OFF, PG_DATA_CHECKSUM_VERSION},
}

> +/*
> + * Signaling between backends calling pg_enable/disable_data_checksums, the
> + * checksums launcher process, and the checksums worker process.
> + *
> + * This struct is protected by DataChecksumsWorkerLock
> + */
> +typedef struct DataChecksumsWorkerShmemStruct
> +{
> +	/*
> +	 * These are set by pg_{enable|disable|verify}_data_checksums, to tell the
> +	 * launcher what the target state is.
> +	 */
> +	DataChecksumsWorkerOperation launch_operation;
> +	int			launch_cost_delay;
> +	int			launch_cost_limit;

The naming feels a little weird with this struct. It's called 
"DataChecksumsWorkerShmemStruct", but it's also accessed by the backends 
calling pg_enable/disable_data_checksums(). And 
"DataChecksumsWorkerOperation" is not accessed by workers at all. Or I 
guess the "operation" global variable is used in 
DataChecksumsWorkerMain(), but it's always set to ENABLE_DATACHECKSUMS 
in the worker. Do you need the "operation" global variable at all?

> +void
> +SetDataChecksumsOn(void)
> +{
> +	uint64		barrier;
> +
> +	Assert(ControlFile != NULL);
> +
> +	 * The only allowed state transition to "on" is from "inprogress-on" since
> +	 * that state ensures that all pages will have data checksums written.
> +	 */
> +	if (XLogCtl->data_checksum_version != PG_DATA_CHECKSUM_INPROGRESS_ON)
> +	{
> +		elog(PANIC, "checksums not in \"inprogress-on\" mode");
> +	}
> +

The PANIC seems a little harsh, you haven't done anything destructive 
here. It's unexpected for this to be called in any other state, so this 
is a "can't happen" scenario, but I don't think we usually PANIC on those.

> +       <para>
> +        If <parameter>cost_delay</parameter> and <parameter>cost_limit</parameter> are
> +        specified, the process is throttled using the same principles as
> +        <link linkend="runtime-config-resource-vacuum-cost">Cost-based Vacuum Delay</link>.
> +       </para>

Ugh, yet another place where we expose the "cost delay/limit" throttling 
mechanism. I agree it's good to be consistent here and use the same 
method we use for vacuum, I just wish we had something more user-friendly..

Grammar / spelling:

> + * state will also be set of "off".

> +	 * When moving to inprogress-on the current state cannot enabled, but when

> +	 * If a worker process currently running?  This is set by the worker

> +	 * These are set by pg_{enable|disable|verify}_data_checksums, to tell the

there is no "pg_verify_data_checksums" function.

"calcuated" in commit message

- Heikki

---

From: Daniel Gustafsson <[email-masked]>
Date: Thu, 2 Apr 2026 20:48:17 +0200

> On 2 Apr 2026, at 11:27, Heikki Linnakangas <[email-masked]> wrote:

Thanks for looking!

> It'd be good to print a LOG message when the checksums state changes, to have a trail in the log of when checksums were enabled/disabled. Something like:
> 

Done.

> This got me thinking, what happens if the the data checksums launcher encounters some other error, for example if you SIGTERM it? The system is left in 'inprogress-on' state, but because the launcher is gone it will never finish and 'pg_stat_progress_data_checksums' will be empty.
> 

I made launcher_exit call SetDataChecksumsOff in case the current state is
in-progress.

> That TODO should be addressed somehow.

Since processing cannot reach a new state if the launcher goes away, I resorted
to signalling the worker to die in this case.

> We're updating it for every block in the main fork, but not at all for other forks? What a bizarre way of avoiding too frequent calls :-). I think you could just call this on every page, pgstat_progress_update_param() is supposed to be very fast. For comparison, e.g. index build calls it for every tuple.

Ok, done.

>> + .barrier_eq = {PG_DATA_CHECKSUM_INPROGRESS_ON, PG_DATA_CHECKSUM_INPROGRESS_OFF},
<..snip..>
> I find this to still be a pretty complicated and unclear way of representing the allowed transitions. There are only 16 possible transitions, and only 6 of them are allowed. How about listing the allowed ones directly:

Fair enough, I've replaced it with a simpler to/from struct and the for loop
then checks for a struct member which contains the current as .from and target
as .to.

> The naming feels a little weird with this struct. It's called "DataChecksumsWorkerShmemStruct", but it's also accessed by the backends calling pg_enable/disable_data_checksums(). And "DataChecksumsWorkerOperation" is not accessed by workers at all. Or I guess the "operation" global variable is used in DataChecksumsWorkerMain(), but it's always set to ENABLE_DATACHECKSUMS in the worker. Do you need the "operation" global variable at all?

I admittedly hadn't realized but you are quite right.  I renamed the structure
to DataChecksumsState instead which seems to fit better.  The operation
variable is used to check against the launch_operation such that a call to
disable checksums while an enabling is running will abort processing
gracefully.

> The PANIC seems a little harsh, you haven't done anything destructive here. It's unexpected for this to be called in any other state, so this is a "can't happen" scenario, but I don't think we usually PANIC on those.

That's a good point, I've replaced with a WARNING and a SetDataChecksumsOff()
call to move to a safe state.

> Ugh, yet another place where we expose the "cost delay/limit" throttling mechanism. I agree it's good to be consistent here and use the same method we use for vacuum, I just wish we had something more user-friendly..

Agreed on both counts.

Fixed.

Fixed.

Thank you so much the continued reviewing support.  The attached rebase
contains the above fixes as well a few small cleanups detected by Github
CoPilot review.  This version removes more than it adds, a trend which has been
going on for quite some versions, and the patch is now just north of 5000 loc
compared to the 5300 it was not long ago.  The changes from the previous are
attached as a text file as well for easier teasing apart what was new.

--
Daniel Gustafsson

commit a2b37f504a88b8d479af9eb07e6fcb42c591abf4
Author: Daniel Gustafsson <[email-masked]>
Date:   Thu Apr 2 20:09:57 2026 +0200

    fixupå

diff --git a/src/backend/access/transam/xlog.c b/src/backend/access/transam/xlog.c
index 9a62d002002..1ce81d00d85 100644
--- a/src/backend/access/transam/xlog.c
+++ b/src/backend/access/transam/xlog.c
@@ -4781,12 +4781,17 @@ SetDataChecksumsOn(void)
 
 	/*
 	 * The only allowed state transition to "on" is from "inprogress-on" since
-	 * that state ensures that all pages will have data checksums written.
+	 * that state ensures that all pages will have data checksums written.  No
+	 * such state transition exists, if it does happen it's likely due to a
+	 * programmer error.
 	 */
 	if (XLogCtl->data_checksum_version != PG_DATA_CHECKSUM_INPROGRESS_ON)
 	{
 		SpinLockRelease(&XLogCtl->info_lck);
-		elog(PANIC, "checksums not in \"inprogress-on\" mode");
+		elog(WARNING,
+			 "cannot set data checksums to \"on\", current state is not \"inprogress-on\", disabling");
+		SetDataChecksumsOff();
+		return;
 	}
 
 	SpinLockRelease(&XLogCtl->info_lck);
diff --git a/src/backend/postmaster/datachecksum_state.c b/src/backend/postmaster/datachecksum_state.c
index 9923a18c92c..754edc5c746 100644
--- a/src/backend/postmaster/datachecksum_state.c
+++ b/src/backend/postmaster/datachecksum_state.c
@@ -96,7 +96,7 @@
  * If processing was halted by the cluster shutting down (due to a crash or
  * intentional restart), the controlfile state "inprogress-on" will be observed
  * on system startup and all backends will be placed in Bd. The controlfile
- * state will also be set of "off".
+ * state will also be set to "off".
  *
  * Backends transition Bd -> Bi via a procsignalbarrier which is emitted by the
  * DataChecksumsLauncher.  When all backends have acknowledged the barrier then
@@ -223,69 +223,43 @@
 /*
  * Configuration of conditions which must match when absorbing a procsignal
  * barrier during data checksum enable/disable operations.  A single function
- * is used for absorbing all barriers, and the set of conditions to use is
- * looked up in the checksum_barriers struct.  The struct member for the target
- * state defines which state the backend must currently be in, and which it
- * must not be in.
- *
- * The reason for this explicit checking is to ensure that processing cannot
- * be started such that it breaks the assumptions of the state machine.
- *
- * MAX_BARRIER_CONDITIONS must match largest number of sets in barrier_eq and
- * barrier_ne in the below checksum_barriers definition.
+ * is used for absorbing all barriers, and the current and target states must
+ * be defined as a from/to tuple in the checksum_barriers struct.
  */
-#define MAX_BARRIER_CONDITIONS 2
 typedef struct ChecksumBarrierCondition
 {
-	/* The target state of the barrier */
-	int			target;
-	/* A set of states in which at least one MUST match the current state */
-	int			barrier_eq[MAX_BARRIER_CONDITIONS];
-	/* The number of elements in the barrier_eq set */
-	int			barrier_eq_sz;
-	/* A set of states which all MUST NOT match the current state */
-	int			barrier_ne[MAX_BARRIER_CONDITIONS];
-	/* The number of elements in the barrier_ne set */
-	int			barrier_ne_sz;
+	/* Current state of data checksums */
+	int			from;
+	/* Target state for data checksums */
+	int			to;
 } ChecksumBarrierCondition;
 
-static const ChecksumBarrierCondition checksum_barriers[4] =
+static const ChecksumBarrierCondition checksum_barriers[6] =
 {
 	/*
-	 * When disabling checksums, either inprogress state is Ok but checksums
-	 * must not be in the enabled state.
+	 * Disabling checksums: If checksums are currently enabled, disabling must
+	 * go through the 'inprogress-off' state.
 	 */
-	{
-		.target = PG_DATA_CHECKSUM_OFF,
-		.barrier_eq = {PG_DATA_CHECKSUM_INPROGRESS_ON, PG_DATA_CHECKSUM_INPROGRESS_OFF},
-		.barrier_eq_sz = 2,
-		.barrier_ne = {PG_DATA_CHECKSUM_VERSION},
-		.barrier_ne_sz = 1
-	},
-	/* When enabling the current state must be inprogress-on */
-	{
-		.target = PG_DATA_CHECKSUM_VERSION,
-		.barrier_eq = {PG_DATA_CHECKSUM_INPROGRESS_ON},
-		.barrier_eq_sz = 1,
-		{0}, 0
-	},
+	{PG_DATA_CHECKSUM_VERSION, PG_DATA_CHECKSUM_INPROGRESS_OFF},
+	{PG_DATA_CHECKSUM_INPROGRESS_OFF, PG_DATA_CHECKSUM_OFF},
 
 	/*
-	 * When moving to inprogress-on the current state cannot enabled, but when
-	 * moving to inprogress-off the current state must be enabled.
+	 * If checksums are in the process of being enabled, but are not yet being
+	 * verified, we can abort by going back to 'off' state.
 	 */
-	{
-		.target = PG_DATA_CHECKSUM_INPROGRESS_ON,
-		{0}, 0,
-		.barrier_ne = {PG_DATA_CHECKSUM_VERSION},
-		.barrier_ne_sz = 1
-	},
-	{
-		.target = PG_DATA_CHECKSUM_INPROGRESS_OFF,
-		.barrier_eq = {PG_DATA_CHECKSUM_VERSION},
-		.barrier_eq_sz = 1,
-		{0}, 0
-	},
+	{PG_DATA_CHECKSUM_INPROGRESS_ON, PG_DATA_CHECKSUM_OFF},
+
+	/*
+	 * Enabling checksums must normally go through the 'inprogress-on' state.
+	 */
+	{PG_DATA_CHECKSUM_OFF, PG_DATA_CHECKSUM_INPROGRESS_ON},
+	{PG_DATA_CHECKSUM_INPROGRESS_ON, PG_DATA_CHECKSUM_VERSION},
+
+	/*
+	 * If checksums are being disabled but all backends are still computing
+	 * checksums, we can go straight back to 'on'
+	 */
+	{PG_DATA_CHECKSUM_INPROGRESS_OFF, PG_DATA_CHECKSUM_VERSION},
 };
 
 /*
@@ -294,10 +268,10 @@ static const ChecksumBarrierCondition checksum_barriers[4] =
  *
  * This struct is protected by DataChecksumsWorkerLock
  */
-typedef struct DataChecksumsWorkerShmemStruct
+typedef struct DataChecksumsStateStruct
 {
 	/*
-	 * These are set by pg_{enable|disable|verify}_data_checksums, to tell the
+	 * These are set by pg_{enable|disable}_data_checksums, to tell the
 	 * launcher what the target state is.
 	 */
 	DataChecksumsWorkerOperation launch_operation;
@@ -311,10 +285,10 @@ typedef struct DataChecksumsWorkerShmemStruct
 	bool		launcher_running;
 
 	/*
-	 * If a worker process currently running?  This is set by the worker
+	 * Is a worker process currently running?  This is set by the worker
 	 * launcher when it starts waiting for a worker process to finish.
 	 */
-	bool		worker_running;
+	int			worker_running;
 
 	/*
 	 * These fields indicate the target state that the launcher is currently
@@ -348,10 +322,10 @@ typedef struct DataChecksumsWorkerShmemStruct
 	 * catalogs
 	 */
 	bool		process_shared_catalogs;
-} DataChecksumsWorkerShmemStruct;
+} DataChecksumsStateStruct;
 
 /* Shared memory segment for datachecksumsworker */
-static DataChecksumsWorkerShmemStruct *DataChecksumsWorkerShmem;
+static DataChecksumsStateStruct *DataChecksumState;
 
 typedef struct DataChecksumsWorkerDatabase
 {
@@ -359,21 +333,16 @@ typedef struct DataChecksumsWorkerDatabase
 	char	   *dbname;
 } DataChecksumsWorkerDatabase;
 
-
-/*
- * Flag set by the interrupt handler
- */
+/* Flag set by the interrupt handler */
 static volatile sig_atomic_t abort_requested = false;
 
 /*
- * Have we set the DataChecksumsWorkerShmemStruct->launcher_running flag?
+ * Have we set the DataChecksumsStateStruct->launcher_running flag?
  * If we have, we need to clear it before exiting!
  */
 static volatile sig_atomic_t launcher_running = false;
 
-/*
- * Are we enabling data checksums, or disabling them?
- */
+/* Are we enabling data checksums, or disabling them? */
 static DataChecksumsWorkerOperation operation;
 
 /* Prototypes */
@@ -435,7 +404,6 @@ EmitAndWaitDataChecksumsBarrier(uint32 state)
 bool
 AbsorbDataChecksumsBarrier(ProcSignalBarrierType barrier)
 {
-	const ChecksumBarrierCondition *condition = NULL;
 	uint32		target_state;
 	int			current = data_checksums;
 	bool		found = false;
@@ -485,38 +453,10 @@ AbsorbDataChecksumsBarrier(ProcSignalBarrierType barrier)
 	 * a condition would be a grave programmer error as the states are a
 	 * discrete set.
 	 */
-	for (int i = 0; i < lengthof(checksum_barriers); i++)
+	for (int i = 0; i < lengthof(checksum_barriers) && !found; i++)
 	{
-		if (checksum_barriers[i].target == target_state)
-			condition = &checksum_barriers[i];
-	}
-	Assert(condition);
-
-	/*
-	 * The current state MUST be equal to one of the EQ states defined in this
-	 * barrier condition. If the EQ states array is zero then that implies
-	 * that the current state can match any state, so fastpath check for that
-	 * first.
-	 */
-	if (condition->barrier_eq_sz == 0)
-		found = true;
-	else
-	{
-		for (int i = 0; i < condition->barrier_eq_sz; i++)
-		{
-			if (current == condition->barrier_eq[i])
-				found = true;
-		}
-	}
-
-	/*
-	 * The current state MUST NOT be equal to any of the NE states defined in
-	 * this barrier condition.
-	 */
-	for (int i = 0; i < condition->barrier_ne_sz; i++)
-	{
-		if (current == condition->barrier_ne[i])
-			found = false;
+		if (checksum_barriers[i].from == current && checksum_barriers[i].to == target_state)
+			found = true;
 	}
 
 	/*
@@ -614,14 +554,14 @@ StartDataChecksumsWorkerLauncher(DataChecksumsWorkerOperation op,
 	/* Store the desired state in shared memory */
 	LWLockAcquire(DataChecksumsWorkerLock, LW_EXCLUSIVE);
 
-	DataChecksumsWorkerShmem->launch_operation = op;
-	DataChecksumsWorkerShmem->launch_cost_delay = cost_delay;
-	DataChecksumsWorkerShmem->launch_cost_limit = cost_limit;
+	DataChecksumState->launch_operation = op;
+	DataChecksumState->launch_cost_delay = cost_delay;
+	DataChecksumState->launch_cost_limit = cost_limit;
 
 	/* Is the launcher already running? If so, what is it doing? */
-	launcher_running = DataChecksumsWorkerShmem->launcher_running;
+	launcher_running = DataChecksumState->launcher_running;
 	if (launcher_running)
-		launcher_running_op = DataChecksumsWorkerShmem->operation;
+		launcher_running_op = DataChecksumState->operation;

@@ -733,21 +673,16 @@ ProcessSingleRelationFork(Relation reln, ForkNumber forkNum, BufferAccessStrateg
 		 */
 		Assert(operation == ENABLE_DATACHECKSUMS);
 		LWLockAcquire(DataChecksumsWorkerLock, LW_SHARED);
-		if (DataChecksumsWorkerShmem->launch_operation == DISABLE_DATACHECKSUMS)
+		if (DataChecksumState->launch_operation == DISABLE_DATACHECKSUMS)
 			abort_requested = true;
 		LWLockRelease(DataChecksumsWorkerLock);
 
 		if (abort_requested)
 			return false;
 
-		/*
-		 * As of now we only update the block counter for main forks in order
-		 * to not cause too frequent calls. TODO: investigate whether we
-		 * should do it more frequent?
-		 */
-		if (forkNum == MAIN_FORKNUM)
-			pgstat_progress_update_param(PROGRESS_DATACHECKSUMS_BLOCKS_DONE,
-										 (blknum + 1));
+		/* update the block counter */
+		pgstat_progress_update_param(PROGRESS_DATACHECKSUMS_BLOCKS_DONE,
+									 (blknum + 1));
 
 		/*
 		 * Processing is re-using the vacuum cost delay for process
@@ -826,7 +761,7 @@ ProcessDatabase(DataChecksumsWorkerDatabase *db)
 	pid_t		pid;
 	char		activity[NAMEDATALEN + 64];
 
-	DataChecksumsWorkerShmem->success = DATACHECKSUMSWORKER_FAILED;
+	DataChecksumState->success = DATACHECKSUMSWORKER_FAILED;
 
 	memset(&bgw, 0, sizeof(bgw));
 	bgw.bgw_flags = BGWORKER_SHMEM_ACCESS | BGWORKER_BACKEND_DATABASE_CONNECTION;
@@ -890,8 +825,9 @@ ProcessDatabase(DataChecksumsWorkerDatabase *db)
 			errmsg("initiating data checksum processing in database \"%s\"",
 				   db->dbname));
 
+	/* Save the pid of the worker so we can signal it later */
 	LWLockAcquire(DataChecksumsWorkerLock, LW_EXCLUSIVE);
-	DataChecksumsWorkerShmem->worker_running = true;
+	DataChecksumState->worker_running = pid;
 	LWLockRelease(DataChecksumsWorkerLock);
 
 	snprintf(activity, sizeof(activity) - 1,
@@ -906,17 +842,17 @@ ProcessDatabase(DataChecksumsWorkerDatabase *db)
 					   db->dbname),
 				errhint("Restart the database and restart data checksum processing by calling pg_enable_data_checksums()."));
 
-	if (DataChecksumsWorkerShmem->success == DATACHECKSUMSWORKER_ABORTED)
+	if (DataChecksumState->success == DATACHECKSUMSWORKER_ABORTED)
 		ereport(LOG,
 				errmsg("data checksums processing was aborted in database \"%s\"",
 					   db->dbname));
 
 	pgstat_report_activity(STATE_IDLE, NULL);
 	LWLockAcquire(DataChecksumsWorkerLock, LW_EXCLUSIVE);
-	DataChecksumsWorkerShmem->worker_running = false;
+	DataChecksumState->worker_running = InvalidPid;
 	LWLockRelease(DataChecksumsWorkerLock);
 
-	return DataChecksumsWorkerShmem->success;
+	return DataChecksumState->success;
 }
 
 /*
@@ -936,17 +872,23 @@ launcher_exit(int code, Datum arg)
 	{
 		LWLockAcquire(DataChecksumsWorkerLock, LW_EXCLUSIVE);
 		launcher_running = false;
-		DataChecksumsWorkerShmem->launcher_running = false;
+		DataChecksumState->launcher_running = false;
 
-		/*
-		 * TODO: how to really handle the worker still running when the
-		 * launcher exits?
-		 */
-		if (DataChecksumsWorkerShmem->worker_running)
+		if (DataChecksumState->worker_running != InvalidPid)
+		{
 			ereport(LOG,
-					errmsg("data checksums launcher exiting while worker is still running"));
+					errmsg("data checksums launcher exiting while worker is still running, signalling worker"));
+			kill(DataChecksumState->worker_running, SIGTERM);
+		}
 		LWLockRelease(DataChecksumsWorkerLock);
 	}
+
+	/*
+	 * If the launcher is exiting before data checksums are enabled then set
+	 * the state to off since processing cannot be resumed.
+	 */
+	if (DataChecksumsInProgress())
+		SetDataChecksumsOff();
 }
 
 /*
@@ -1027,7 +969,7 @@ WaitForAllTransactionsToFinish(void)
 		CHECK_FOR_INTERRUPTS();
 
 		LWLockAcquire(DataChecksumsWorkerLock, LW_SHARED);
-		if (DataChecksumsWorkerShmem->launch_operation != operation)
+		if (DataChecksumState->launch_operation != operation)
 			abort_requested = true;
 		LWLockRelease(DataChecksumsWorkerLock);
 		if (abort_requested)
@@ -1068,7 +1010,7 @@ DataChecksumsWorkerLauncherMain(Datum arg)

-	if (DataChecksumsWorkerShmem->launcher_running)
+	if (DataChecksumState->launcher_running)
 	{
 		ereport(LOG,
 				errmsg("background worker \"datachecksums launcher\" already running, exiting"));
@@ -1082,11 +1024,11 @@ DataChecksumsWorkerLauncherMain(Datum arg)
 	/* Initialize a connection to shared catalogs only */
 	BackgroundWorkerInitializeConnectionByOid(InvalidOid, InvalidOid, 0);
 
-	operation = DataChecksumsWorkerShmem->launch_operation;
-	DataChecksumsWorkerShmem->launcher_running = true;
-	DataChecksumsWorkerShmem->operation = operation;
-	DataChecksumsWorkerShmem->cost_delay = DataChecksumsWorkerShmem->launch_cost_delay;
-	DataChecksumsWorkerShmem->cost_limit = DataChecksumsWorkerShmem->launch_cost_limit;
+	operation = DataChecksumState->launch_operation;
+	DataChecksumState->launcher_running = true;
+	DataChecksumState->operation = operation;
+	DataChecksumState->cost_delay = DataChecksumState->launch_cost_delay;
+	DataChecksumState->cost_limit = DataChecksumState->launch_cost_limit;
 	LWLockRelease(DataChecksumsWorkerLock);
 
 	/*
@@ -1109,6 +1051,9 @@ again:
 		if (DataChecksumsNeedVerify())
 			goto done;
 
+		ereport(LOG,
+				errmsg("enabling data checksums requested, starting data checksum calculation"));
+
 		/*
 		 * Set the state to inprogress-on and wait on the procsignal barrier.
 		 */
@@ -1127,7 +1072,7 @@ again:
 			 * failure, so restart processing instead.
 			 */
 			LWLockAcquire(DataChecksumsWorkerLock, LW_EXCLUSIVE);
-			if (DataChecksumsWorkerShmem->launch_operation != operation)
+			if (DataChecksumState->launch_operation != operation)
 			{
 				LWLockRelease(DataChecksumsWorkerLock);
 				goto done;
@@ -1143,12 +1088,20 @@ again:
 		 * order to instruct backends to validate checksums on reading.
 		 */
 		SetDataChecksumsOn();
+
+		ereport(LOG,
+				errmsg("data checksums are now enabled"));
 	}
 	else if (operation == DISABLE_DATACHECKSUMS)
 	{
+		ereport(LOG,
+				errmsg("disabling data checksums requested"));
+
 		pgstat_progress_update_param(PROGRESS_DATACHECKSUMS_PHASE,
 									 PROGRESS_DATACHECKSUMS_PHASE_DISABLING);
 		SetDataChecksumsOff();
+		ereport(LOG,
+				errmsg("data checksums are now disabled"));
 	}
 	else
 		Assert(false);
@@ -1168,12 +1121,12 @@ done:
 	 * again.
 	 */
 	LWLockAcquire(DataChecksumsWorkerLock, LW_EXCLUSIVE);
-	if (DataChecksumsWorkerShmem->launch_operation != operation)
+	if (DataChecksumState->launch_operation != operation)
 	{
-		DataChecksumsWorkerShmem->operation = DataChecksumsWorkerShmem->launch_operation;
-		operation = DataChecksumsWorkerShmem->launch_operation;
-		DataChecksumsWorkerShmem->cost_delay = DataChecksumsWorkerShmem->launch_cost_delay;
-		DataChecksumsWorkerShmem->cost_limit = DataChecksumsWorkerShmem->launch_cost_limit;
+		DataChecksumState->operation = DataChecksumState->launch_operation;
+		operation = DataChecksumState->launch_operation;
+		DataChecksumState->cost_delay = DataChecksumState->launch_cost_delay;
+		DataChecksumState->cost_limit = DataChecksumState->launch_cost_limit;
 		LWLockRelease(DataChecksumsWorkerLock);
 		goto again;
 	}
@@ -1182,7 +1135,7 @@ done:
 	pgstat_progress_end_command();
 
 	launcher_running = false;
-	DataChecksumsWorkerShmem->launcher_running = false;
+	DataChecksumState->launcher_running = false;
 	LWLockRelease(DataChecksumsWorkerLock);
 }
 
@@ -1201,7 +1154,7 @@ ProcessAllDatabases(void)
 	int			cumulative_total = 0;
 
 	/* Set up so first run processes shared catalogs, not once in every db */
-	DataChecksumsWorkerShmem->process_shared_catalogs = true;
+	DataChecksumState->process_shared_catalogs = true;
 
 	/* Get a list of all databases to process */
 	WaitForAllTransactionsToFinish();
@@ -1270,7 +1223,7 @@ ProcessAllDatabases(void)
 		 * When one database has completed, it will have done shared catalogs
 		 * so we don't have to process them again.
 		 */
-		DataChecksumsWorkerShmem->process_shared_catalogs = false;
+		DataChecksumState->process_shared_catalogs = false;
 	}
 
 	FreeDatabaseList(DatabaseList);
@@ -1281,35 +1234,35 @@ ProcessAllDatabases(void)
 }
 
 /*
- * DataChecksumsWorkerShmemSize
+ * DataChecksumStateSize
  *		Compute required space for datachecksumsworker-related shared memory
  */
 Size
-DataChecksumsWorkerShmemSize(void)
+DataChecksumsShmemSize(void)
 {
 	Size		size;
 
-	size = sizeof(DataChecksumsWorkerShmemStruct);
+	size = sizeof(DataChecksumsStateStruct);
 	size = MAXALIGN(size);
 
 	return size;
 }
 
 /*
- * DataChecksumsWorkerShmemInit
+ * DataChecksumStateInit
  *		Allocate and initialize datachecksumsworker-related shared memory
  */
 void
-DataChecksumsWorkerShmemInit(void)
+DataChecksumsShmemInit(void)
 {
 	bool		found;
 
-	DataChecksumsWorkerShmem = (DataChecksumsWorkerShmemStruct *)
+	DataChecksumState = (DataChecksumsStateStruct *)
 		ShmemInitStruct("DataChecksumsWorker Data",
-						DataChecksumsWorkerShmemSize(),
+						DataChecksumsShmemSize(),
 						&found);
 	if (!found)
-		MemSet(DataChecksumsWorkerShmem, 0, DataChecksumsWorkerShmemSize());
+		MemSet(DataChecksumState, 0, DataChecksumsShmemSize());
 }
 
 /*
@@ -1528,9 +1481,9 @@ DataChecksumsWorkerMain(Datum arg)
 	 * implementation detail and care should be taken to avoid it bleeding
 	 * through to the user to avoid confusion.
 	 */
-	Assert(DataChecksumsWorkerShmem->operation == ENABLE_DATACHECKSUMS);
-	VacuumCostDelay = DataChecksumsWorkerShmem->cost_delay;
-	VacuumCostLimit = DataChecksumsWorkerShmem->cost_limit;
+	Assert(DataChecksumState->operation == ENABLE_DATACHECKSUMS);
+	VacuumCostDelay = DataChecksumState->cost_delay;
+	VacuumCostLimit = DataChecksumState->cost_limit;
 	VacuumCostActive = (VacuumCostDelay > 0);
 	VacuumCostBalance = 0;
 	VacuumCostPageHit = 0;
@@ -1543,7 +1496,7 @@ DataChecksumsWorkerMain(Datum arg)
 	strategy = GetAccessStrategy(BAS_VACUUM);
 
 	RelationList = BuildRelationList(false,
-									 DataChecksumsWorkerShmem->process_shared_catalogs);
+									 DataChecksumState->process_shared_catalogs);
 
 	/* Update the total number of relations to be processed in this DB. */
 	{
@@ -1579,7 +1532,7 @@ DataChecksumsWorkerMain(Datum arg)
 
 	if (aborted)
 	{
-		DataChecksumsWorkerShmem->success = DATACHECKSUMSWORKER_ABORTED;
+		DataChecksumState->success = DATACHECKSUMSWORKER_ABORTED;
 		ereport(DEBUG1,
 				errmsg("data checksum processing aborted in database OID %u",
 					   dboid));
@@ -1633,14 +1586,14 @@ DataChecksumsWorkerMain(Datum arg)
 						 WAIT_EVENT_CHECKSUM_ENABLE_TEMPTABLE_WAIT);
 
 		LWLockAcquire(DataChecksumsWorkerLock, LW_EXCLUSIVE);
-		aborted = DataChecksumsWorkerShmem->launch_operation != operation;
+		aborted = DataChecksumState->launch_operation != operation;
 		LWLockRelease(DataChecksumsWorkerLock);
 
 		CHECK_FOR_INTERRUPTS();
 
 		if (aborted || abort_requested)
 		{
-			DataChecksumsWorkerShmem->success = DATACHECKSUMSWORKER_ABORTED;
+			DataChecksumState->success = DATACHECKSUMSWORKER_ABORTED;
 			ereport(LOG,
 					errmsg("data checksum processing aborted in database OID %u",
 						   dboid));
@@ -1653,5 +1606,5 @@ DataChecksumsWorkerMain(Datum arg)
 	/* worker done */
 	pgstat_progress_end_command();
 
-	DataChecksumsWorkerShmem->success = DATACHECKSUMSWORKER_SUCCESSFUL;
+	DataChecksumState->success = DATACHECKSUMSWORKER_SUCCESSFUL;
 }
diff --git a/src/backend/storage/buffer/bufmgr.c b/src/backend/storage/buffer/bufmgr.c
index d2018860eca..3cc0b0bdd92 100644
--- a/src/backend/storage/buffer/bufmgr.c
+++ b/src/backend/storage/buffer/bufmgr.c
@@ -8566,6 +8566,7 @@ buffer_readv_complete_one(PgAioTargetData *td, uint8 buf_off, Buffer buffer,
 	/* the local zero_damaged_pages may differ from the definer's */
 	if (flags & READ_BUFFERS_IGNORE_CHECKSUM_FAILURES)
 		piv_flags |= PIV_IGNORE_CHECKSUM_FAILURE;
+
 	/*
 	 * If the buffers are marked for zero on error, we want to log that in
 	 * case of a checksum failure.
diff --git a/src/backend/storage/ipc/ipci.c b/src/backend/storage/ipc/ipci.c
index d6826709728..7aab5da3386 100644
--- a/src/backend/storage/ipc/ipci.c
+++ b/src/backend/storage/ipc/ipci.c
@@ -143,7 +143,7 @@ CalculateShmemSize(void)
 	size = add_size(size, AioShmemSize());
 	size = add_size(size, WaitLSNShmemSize());
 	size = add_size(size, LogicalDecodingCtlShmemSize());
-	size = add_size(size, DataChecksumsWorkerShmemSize());
+	size = add_size(size, DataChecksumsShmemSize());
 
 	/* include additional requested shmem from preload libraries */
 	size = add_size(size, total_addin_request);
@@ -312,7 +312,7 @@ CreateOrAttachShmemStructs(void)
 	PgArchShmemInit();
 	ApplyLauncherShmemInit();
 	SlotSyncShmemInit();
-	DataChecksumsWorkerShmemInit();
+	DataChecksumsShmemInit();
 
 	/*
 	 * Set up other modules that need some shared memory space
diff --git a/src/include/postmaster/datachecksum_state.h b/src/include/postmaster/datachecksum_state.h
index 8e21b7d673a..343494edcc8 100644
--- a/src/include/postmaster/datachecksum_state.h
+++ b/src/include/postmaster/datachecksum_state.h
@@ -18,8 +18,8 @@
 #include "storage/procsignal.h"
 
 /* Shared memory */
-extern Size DataChecksumsWorkerShmemSize(void);
-extern void DataChecksumsWorkerShmemInit(void);
+extern Size DataChecksumsShmemSize(void);
+extern void DataChecksumsShmemInit(void);
 
 /* Possible operations the Datachecksumsworker can perform */
 typedef enum DataChecksumsWorkerOperation
diff --git a/src/test/modules/test_checksums/t/002_restarts.pl b/src/test/modules/test_checksums/t/002_restarts.pl
index c63d01f3ba7..bab59be82bd 100644
--- a/src/test/modules/test_checksums/t/002_restarts.pl
+++ b/src/test/modules/test_checksums/t/002_restarts.pl
@@ -100,7 +100,7 @@ is($result, '9999', 'ensure checksummed pages can be read back');
 
 $result = $node->poll_query_until(
 	'postgres',
-	"SELECT count(*) FROM pg_stat_activity WHERE backend_type LIKE 'datachecksumsworker%';",
+	"SELECT count(*) FROM pg_stat_activity WHERE backend_type LIKE 'datachecksum%';",
 	'0');
 is($result, 1, 'await datachecksums worker/launcher termination');
 
diff --git a/src/test/modules/test_checksums/t/003_standby_restarts.pl b/src/test/modules/test_checksums/t/003_standby_restarts.pl
index cc4d964989a..6b016925651 100644
--- a/src/test/modules/test_checksums/t/003_standby_restarts.pl
+++ b/src/test/modules/test_checksums/t/003_standby_restarts.pl
@@ -89,7 +89,7 @@ is($result, '19998', 'ensure we can safely read all data with checksums');
 
 $result = $node_primary->poll_query_until(
 	'postgres',
-	"SELECT count(*) FROM pg_stat_activity WHERE backend_type LIKE 'datachecksumsworker%';",
+	"SELECT count(*) FROM pg_stat_activity WHERE backend_type LIKE 'datachecksum%';",
 	'0');
 is($result, 1, 'await datachecksums worker/launcher termination');
 
diff --git a/src/test/modules/test_checksums/test_checksums.c b/src/test/modules/test_checksums/test_checksums.c
index 6ac715a76f7..edded415931 100644
--- a/src/test/modules/test_checksums/test_checksums.c
+++ b/src/test/modules/test_checksums/test_checksums.c
@@ -22,7 +22,6 @@
 PG_MODULE_MAGIC;
 
 extern PGDLLEXPORT void dc_delay_barrier(const char *name, const void *private_data, void *arg);
-extern PGDLLEXPORT void dc_fail_database(const char *name, const void *private_data, void *arg);
 extern PGDLLEXPORT void dc_modify_db_result(const char *name, const void *private_data, void *arg);
 extern PGDLLEXPORT void dc_fake_temptable(const char *name, const void *private_data, void *arg);
 
@@ -115,7 +114,7 @@ void
 dc_modify_db_result(const char *name, const void *private_data, void *arg)
 {
 	DataChecksumsWorkerResult *res = (DataChecksumsWorkerResult *) arg;
-	int			new_res = *(int *) private_data;
+	uint32			new_res = *(uint32 *) private_data;
 
 	*res = new_res;
 }
diff --git a/src/tools/pgindent/typedefs.list b/src/tools/pgindent/typedefs.list
index 892117d113a..61132ee3377 100644
--- a/src/tools/pgindent/typedefs.list
+++ b/src/tools/pgindent/typedefs.list
@@ -631,7 +631,7 @@ DSMRegistryEntry
 DWORD
 DataChecksumsWorkerDatabase
 DataChecksumsWorkerResult
-DataChecksumsWorkerShmemStruct
+DataChecksumsStateStruct
 DataDirSyncMethod
 DataDumperPtr
 DataPageDeleteStack

---

From: Daniel Gustafsson <[email-masked]>
Date: Fri, 3 Apr 2026 17:33:03 +0200

The attached rebase with a PG_CONTROL_VERSION bump is what I have staged for
later tonight, submitting here to have the (hopefully) final patch archived as
well as another CFBot run.

--
Daniel Gustafsson

---

From: Heikki Linnakangas <[email-masked]>
Date: Fri, 3 Apr 2026 19:00:52 +0300

A few more small comments, I'm sorry about drip-feeding these:

> +/*
> + * launcher_exit
> + *
> + * Internal routine for cleaning up state when the launcher process exits. We
> + * need to clean up the abort flag to ensure that processing started again if
> + * it was previously aborted (note: started again, *not* restarted from where
> + * it left off).
> + */
> +static void
> +launcher_exit(int code, Datum arg)
> +{
> +	abort_requested = false;
> +
> +	if (launcher_running)
> +	{
> +		LWLockAcquire(DataChecksumsWorkerLock, LW_EXCLUSIVE);
> +		launcher_running = false;
> +		LWLockRelease(DataChecksumsWorkerLock);
> +	}
> +
> +	/*

Is there still a race condition if the launcher is killed, it gets here, 
sends SIGTERM to the worker process, but before the worker process has 
exited, the user calls pg_enable_data_checksums() again and a new 
launcher is started? What happens?

> +	 * launcher when it starts waiting for a worker process to finish.
> +	 */
> +	int			worker_running;

'worker_running' sounds like a boolean, but it's actually a PID. 
Especially when 'launcher_running' really is a boolean. Maybe rename to 
'worker_pid' or 'worker_running_pid' or 'running_worker_pid' or something.

> +bool
> +DataChecksumsInProgress(void)
> +{
> +	return LocalDataChecksumState == PG_DATA_CHECKSUM_INPROGRESS_ON;
> +}

Perhaps this should be DataChecksumsInProgressOn()? I saw the caller in 
launcher_exit() first, and had to look up the implementation to check if 
it returns true for 'inprogress-off' state.

> diff --git a/src/include/storage/checksum.h b/src/include/storage/checksum.h
> index ff417d5ae3e..fe5d30b4349 100644
> --- a/src/include/storage/checksum.h
> +++ b/src/include/storage/checksum.h
> @@ -15,6 +15,20 @@
>  
>  #include "storage/block.h"
>  
> +/*
> + * Checksum state 0 is used for when data checksums are disabled (OFF).
> + * PG_DATA_CHECKSUM_INPROGRESS_{ON|OFF} defines that data checksums are either
> + * currently being enabled or disabled, and PG_DATA_CHECKSUM_VERSION defines
> + * that data checksums are enabled.
> + */
> +typedef enum ChecksumStateType
> +{
> +	PG_DATA_CHECKSUM_INPROGRESS_OFF,
> +	PG_DATA_CHECKSUM_INPROGRESS_ON,
> +} ChecksumStateType;
> +
>  /*
>   * Compute the checksum for a Postgres page.  The page must be aligned on a
>   * 4-byte boundary.

It'd be good to mention that this value is stored in the control file, 
so changing it needs a catversion bump. Also it's important that 
PG_DATA_CHECKSUM_VERSION = 1, for backwards-compatibility in pg_upgrade. 
I'd suggest assigning explicit values 1, 2, 3, 4 for each of the enum 
constants, to emphasize that they values are fixed.

There's an #include "storage/bufpage.h" in 
src/bin/pg_upgrade/controldata.c and 
src/backend/access/rmgrdesc/xlogdesc.c. I think they should both include 
"storage/checksum.h" directly instead. And "bufpage.h" probably doesn't 
need to include "storage/checksum.h".

- Heikki

---

From: Daniel Gustafsson <[email-masked]>
Date: Fri, 3 Apr 2026 19:14:09 +0200

> On 3 Apr 2026, at 18:00, Heikki Linnakangas <[email-masked]> wrote:
> 

Not at all, thanks for looking!  I'm sure there will be a few more things once
there is BF coverage of the patch as well.

> Is there still a race condition if the launcher is killed, it gets here, sends SIGTERM to the worker process, but before the worker process has exited, the user calls pg_enable_data_checksums() again and a new launcher is started? What happens?

If the new launcher sets the state to inprogress-on before the old one checks
for that in launcher_exit, it would disable checksums, and the new launcher
would fail in the next state transitiom as the state is incorrect.  The
solution would be to hold the DataChecksumsWorkerLock exclusively during
launcher_exit to ensure the old one can exit before the new one starts.  Done
in the attached.

> 'worker_running' sounds like a boolean, but it's actually a PID. Especially when 'launcher_running' really is a boolean. Maybe rename to 'worker_pid' or 'worker_running_pid' or 'running_worker_pid' or something.

Renamed to worker_pid.

> Perhaps this should be DataChecksumsInProgressOn()? I saw the caller in launcher_exit() first, and had to look up the implementation to check if it returns true for 'inprogress-off' state.

Fixed.

> It'd be good to mention that this value is stored in the control file, so changing it needs a catversion bump. Also it's important that PG_DATA_CHECKSUM_VERSION = 1, for backwards-compatibility in pg_upgrade. I'd suggest assigning explicit values 1, 2, 3, 4 for each of the enum constants, to emphasize that they values are fixed.

Fixed.

> There's an #include "storage/bufpage.h" in src/bin/pg_upgrade/controldata.c and src/backend/access/rmgrdesc/xlogdesc.c. I think they should both include "storage/checksum.h" directly instead. And "bufpage.h" probably doesn't need to include "storage/checksum.h".

Fixed, and validated with headerscheck.  Doing this required adding checksum.h
to bootstrap/bootstrap.c which seems very correct as it needs to know about the
state enum.

--
Daniel Gustafsson

---

From: Daniel Gustafsson <[email-masked]>
Date: Fri, 3 Apr 2026 19:56:46 +0200

CFBot claims it needs a rebase which I'm not sure why (I can't reproduce a
rebase error), but here is a rebase anyways.

--
Daniel Gustafsson

---

From: Daniel Gustafsson <[email-masked]>
Date: Fri, 3 Apr 2026 21:29:02 +0200

> On 3 Apr 2026, at 19:56, Daniel Gustafsson <[email-masked]> wrote:

Come to think of it, a cleaner solution is to just postpone updating the
launcher_running flag until it actually not running anymore.  Done in the
attached which has done several CI runs now.

> rebase error), but here is a rebase anyways.

Turns out it's applying on 57706799186 rather than HEAD, and the catversion
bump in fd7a25af11e cause this patch to not apply.

--
Daniel Gustafsson

---

From: Daniel Gustafsson <[email-masked]>
Date: Fri, 3 Apr 2026 23:46:09 +0200

After many more runs on CI I ended up pushing this version, and I see BF
members being angry due the test not waiting for the launcher to exit.  I am
working on a fix right now.

--
Daniel Gustafsson

---

From: Daniel Gustafsson <[email-masked]>
Date: Sat, 4 Apr 2026 00:59:55 +0200

> On 3 Apr 2026, at 23:46, Daniel Gustafsson <[email-masked]> wrote:
> 

0036232ba8f seems to have made the failing animals slightly happier, I will
continue to monitor the buildfarm for other fallout.

--
Daniel Gustafsson

---

From: Daniel Gustafsson <[email-masked]>
Date: Sat, 4 Apr 2026 02:35:31 +0200

> On 4 Apr 2026, at 00:59, Daniel Gustafsson <[email-masked]> wrote:
> 

The intermittent failure on kestrel implies timing similar to the one fixed in
0036232ba8fb28, a tentative fix is to make it part of waiting for an endstate
(on or off) to make sure the cluster is always in the right state for new
operations.  Right now kestrel is the one which has been flapping, I'm waiting
a bit to see if more will follow and give further clues.

--
Daniel Gustafsson

---

From: Daniel Gustafsson <[email-masked]>
Date: Sun, 5 Apr 2026 00:27:00 +0200

> On 4 Apr 2026, at 02:35, Daniel Gustafsson <[email-masked]> wrote:
> 

mylodon had the same failure, and I believe the bug is in my injection point
test code.  I have a tentative fix in the attached refactoring which moves over
to using the injection_point extension module.  It's still fairly rare so I'm
holding off for a little bit before pushing it to see if I can collect a little
bit more evidence.

--
Daniel Gustafsson

---

From: Andres Freund <[email-masked]>
Date: Sun, 5 Apr 2026 00:56:13 -0400

Hi,

> holding off for a little bit before pushing it to see if I can collect a little

There are a lot checksum related errors on CI:

https://cirrus-ci.com/task/4848298592305152
https://cirrus-ci.com/task/5338691381493760
https://cirrus-ci.com/task/6271077241847808
https://cirrus-ci.com/task/6150048418889728

They probably are mostly the issues you know about.  It'd be nice to get them
fixed soon-ish...

Greetings,

Andres Freund

---

From: Daniel Gustafsson <[email-masked]>
Date: Sun, 5 Apr 2026 16:36:08 +0200

> On 5 Apr 2026, at 06:56, Andres Freund <[email-masked]> wrote:
> 
> Hi,
> 

I am investigating them and have tentative fixes that I will apply tonight.

--
Daniel Gustafsson

---

From: Daniel Gustafsson <[email-masked]>
Date: Mon, 6 Apr 2026 02:20:34 +0200

> https://cirrus-ci.com/task/4848298592305152

[22:35:56.818] # poll_query_until timed out executing this query:
[22:35:56.818] # SELECT setting FROM pg_catalog.pg_settings WHERE name = 'data_checksums';
[22:35:56.818] # expecting this output:
[22:35:56.818] # inprogress-on
[22:35:56.818] # last actual query output:
[22:35:56.818] # on

Another timing error, solved by allowing for on as well as inprogress-on and
expanding the wait fix already committed into a more generic one.

> https://cirrus-ci.com/task/5338691381493760

This one was interesting, it managed to hit a bug when the worker process
starts, and finishes, before the launcher manages to wait for it to start up.
The BGWH_STOPPED return was erroneously interpreted as a failure.

> https://cirrus-ci.com/task/6271077241847808

Cheeky, the processing managed to finish between closing the connection
blocking progress and before shutting down the cluster. Reordering to keep the 

> https://cirrus-ci.com/task/6150048418889728

Seems like the same error as the first one.

I've pushed fixes for all of these as well as the intermittent failures that
were seen on some BF animals, and will stare at the buildfarm for a while now.
So far 10 or so machines have built these green so it looks decent so far.

--
Daniel Gustafsson

---

From: Alexander Lakhin <[email-masked]>
Date: Mon, 6 Apr 2026 20:00:00 +0300

Hello Daniel,

> working on a fix right now.

Maybe this is already known or even expected, but I'd still like to let
you know that starting from f19c0ecca, I'm observing checksum errors in a
running instance. I've modified PageIsVerified() to catch errors sooner:
@@ -158,7 +158,7 @@ PageIsVerified(PageData *page, BlockNumber blkno, int flags, bool *checksum_fail
      if (checksum_failure)
      {
          if ((flags & (PIV_LOG_WARNING | PIV_LOG_LOG)) != 0)
-            ereport(flags & PIV_LOG_WARNING ? WARNING : LOG,
+            ereport(PANIC,
                      (errcode(ERRCODE_DATA_CORRUPTED),
                       errmsg("page verification failed, calculated checksum %u but expected %u%s",
                              checksum, p->pd_checksum,

And I'm getting, e.g.:
2026-04-06 18:09:12.077 EEST|postgres|regress_215|69d3cc86.3bfbdc|PANIC:  page verification failed, calculated checksum 
40178 but expected 50558, buffer will be zeroed
2026-04-06 18:09:12.077 EEST|postgres|regress_215|69d3cc86.3bfbdc|STATEMENT:  update information_schema.sql_features set
...

Core was generated by `postgres: postgres regress_215 127.0.0.1(42448) UPDATE        '.
Program terminated with signal SIGABRT, Aborted.
(gdb) bt
#0  __pthread_kill_implementation (no_tid=0, signo=6, threadid=<optimized out>) at ./nptl/pthread_kill.c:44
#1  __pthread_kill_internal (signo=6, threadid=<optimized out>) at ./nptl/pthread_kill.c:78
#2  __GI___pthread_kill (threadid=<optimized out>, signo=signo@entry=6) at ./nptl/pthread_kill.c:89
#3  0x0000796d0004527e in __GI_raise (sig=sig@entry=6) at ../sysdeps/posix/raise.c:26
#4  0x0000796d000288ff in __GI_abort () at ./stdlib/abort.c:79
#5  0x000055fe3f92c855 in errfinish (filename=filename@entry=0x55fe3fa54bad "bufpage.c", lineno=lineno@entry=161, 
funcname=funcname@entry=0x55fe3fb70ff8 <__func__.6> "PageIsVerified") at elog.c:620
#6  0x000055fe3f7c2415 in PageIsVerified (page=page@entry=0x796cf6884000 "", blkno=blkno@entry=0, flags=10, 
checksum_failure_p=checksum_failure_p@entry=0x7ffd2c524bef) at bufpage.c:161
#7  0x000055fe3f78a93d in buffer_readv_complete_one (zeroed_buffer=<synthetic pointer>, ignored_checksum=<synthetic 
pointer>, failed_checksum=0x7ffd2c524bef, buffer_invalid=<synthetic pointer>, is_temp=false, failed=false, flags=9 '\t', 
buffer=15424, buf_off=0 '\000', td=0x796cfc69c2d8) at bufmgr.c:8593
#8  buffer_readv_complete (is_temp=false, cb_data=<optimized out>, prior_result=..., ioh=<optimized out>) at bufmgr.c:8724
#9  shared_buffer_readv_complete (ioh=<optimized out>, prior_result=..., cb_data=<optimized out>) at bufmgr.c:8883
#10 0x000055fe3f77ec61 in pgaio_io_call_complete_shared (ioh=ioh@entry=0x796cfc69c260) at aio_callback.c:258
#11 0x000055fe3f77d4f6 in pgaio_io_process_completion (ioh=ioh@entry=0x796cfc69c260, result=<optimized out>) at aio.c:540
#12 0x000055fe3f77fe42 in pgaio_io_perform_synchronously (ioh=ioh@entry=0x796cfc69c260) at aio_io.c:146
#13 0x000055fe3f77e121 in pgaio_io_stage (ioh=ioh@entry=0x796cfc69c260, op=op@entry=PGAIO_OP_READV) at aio.c:476
#14 0x000055fe3f77fd6d in pgaio_io_start_readv (ioh=ioh@entry=0x796cfc69c260, fd=166, iovcnt=iovcnt@entry=1, 
offset=offset@entry=0) at aio_io.c:87
#15 0x000055fe3f795bae in FileStartReadV (ioh=ioh@entry=0x796cfc69c260, file=<optimized out>, iovcnt=iovcnt@entry=1, 
offset=offset@entry=0, wait_event_info=wait_event_info@entry=167772183) at fd.c:2225
#16 0x000055fe3f7c648b in mdstartreadv (ioh=0x796cfc69c260, reln=0x55fe73aeba98, forknum=VISIBILITYMAP_FORKNUM, 
blocknum=0, buffers=<optimized out>, nblocks=1) at md.c:1041
#17 0x000055fe3f7c809c in smgrstartreadv (ioh=ioh@entry=0x796cfc69c260, reln=<optimized out>, 
forknum=forknum@entry=VISIBILITYMAP_FORKNUM, blocknum=blocknum@entry=0, buffers=buffers@entry=0x7ffd2c524e70, 
nblocks=nblocks@entry=1) at smgr.c:758
#18 0x000055fe3f78a1c7 in AsyncReadBuffers (operation=operation@entry=0x7ffd2c5253a0, 
nblocks_progress=nblocks_progress@entry=0x7ffd2c52530c) at bufmgr.c:2144
#19 0x000055fe3f78ce19 in StartReadBuffersImpl (allow_forwarding=false, flags=9, nblocks=0x7ffd2c52530c, blockNum=0, 
buffers=0x7ffd2c52539c, operation=0x7ffd2c5253a0) at bufmgr.c:1548
#20 StartReadBuffer (operation=operation@entry=0x7ffd2c5253a0, buffer=buffer@entry=0x7ffd2c52539c, 
blocknum=blocknum@entry=0, flags=9) at bufmgr.c:1636
#21 0x000055fe3f78d870 in ReadBuffer_common (strategy=0x0, mode=RBM_ZERO_ON_ERROR, blockNum=0, 
forkNum=VISIBILITYMAP_FORKNUM, smgr_persistence=0 '\000', smgr=0x55fe73aeba98, rel=0x796d006a31a8) at bufmgr.c:1358
#22 ReadBufferExtended (reln=reln@entry=0x796d006a31a8, forkNum=forkNum@entry=VISIBILITYMAP_FORKNUM, 
blockNum=blockNum@entry=0, mode=mode@entry=RBM_ZERO_ON_ERROR, strategy=strategy@entry=0x0) at bufmgr.c:945
#23 0x000055fe3f3d7e00 in vm_readbuf (rel=rel@entry=0x796d006a31a8, blkno=blkno@entry=0, extend=extend@entry=true) at 
visibilitymap.c:577
#24 0x000055fe3f3d7fda in visibilitymap_pin (rel=rel@entry=0x796d006a31a8, heapBlk=<optimized out>, 
vmbuf=vmbuf@entry=0x55fe73ed2b18) at visibilitymap.c:216
#25 0x000055fe3f3d1f7a in heap_page_prune_opt (relation=0x796d006a31a8, buffer=buffer@entry=15403, 
vmbuffer=vmbuffer@entry=0x55fe73ed2b18, rel_read_only=false) at pruneheap.c:339
#26 0x000055fe3f3c1dcf in heap_prepare_pagescan (sscan=sscan@entry=0x55fe73ed2a88) at heapam.c:636
#27 0x000055fe3f3c242f in heapgettup_pagemode (scan=scan@entry=0x55fe73ed2a88, dir=ForwardScanDirection, nkeys=0, 
key=0x0) at heapam.c:1111
#28 0x000055fe3f3c27ab in heap_getnextslot (sscan=0x55fe73ed2a88, direction=<optimized out>, slot=0x55fe73ed13a8) at 
heapam.c:1467
#29 0x000055fe3f5e8d62 in table_scan_getnextslot (sscan=<optimized out>, direction=direction@entry=ForwardScanDirection, 
slot=slot@entry=0x55fe73ed13a8) at ../../../src/include/access/tableam.h:1099
#30 0x000055fe3f5e939e in SeqNext (node=0x55fe73ed1188) at nodeSeqscan.c:83
#31 ExecScanFetch (recheckMtd=0x55fe3f5e8d2e <SeqRecheck>, accessMtd=0x55fe3f5e8c9c <SeqNext>, epqstate=0x0, 
node=0x55fe73ed1188) at ../../../src/include/executor/execScan.h:135
#32 ExecScanExtended (projInfo=0x55fe73ed19d8, qual=0x0, epqstate=0x0, recheckMtd=0x55fe3f5e8d2e <SeqRecheck>, 
accessMtd=0x55fe3f5e8c9c <SeqNext>, node=0x55fe73ed1188) at ../../../src/include/executor/execScan.h:196
#33 ExecSeqScanWithProject (pstate=<optimized out>) at nodeSeqscan.c:164
#34 0x000055fe3f5b65f9 in ExecProcNodeFirst (node=0x55fe73ed1188) at execProcnode.c:470
#35 0x000055fe3f5dfd3b in ExecProcNode (node=node@entry=0x55fe73ed1188) at ../../../src/include/executor/executor.h:320
...

2026-04-06 18:09:12.289 EEST|postgres|regress_147|69d3cc44.3bfaaa|PANIC:  page verification failed, calculated checksum 
8769 but expected 0
2026-04-06 18:09:12.289 EEST|postgres|regress_147|69d3cc44.3bfaaa|STATEMENT:  insert into 
information_schema.sql_features values (
...

Core was generated by `postgres: postgres regress_147 127.0.0.1(35968) INSERT        '.
Program terminated with signal SIGABRT, Aborted.
(gdb) bt
#0  __pthread_kill_implementation (no_tid=0, signo=6, threadid=<optimized out>) at ./nptl/pthread_kill.c:44
#1  __pthread_kill_internal (signo=6, threadid=<optimized out>) at ./nptl/pthread_kill.c:78
#2  __GI___pthread_kill (threadid=<optimized out>, signo=signo@entry=6) at ./nptl/pthread_kill.c:89
#3  0x0000796d0004527e in __GI_raise (sig=sig@entry=6) at ../sysdeps/posix/raise.c:26
#4  0x0000796d000288ff in __GI_abort () at ./stdlib/abort.c:79
#5  0x000055fe3f92c855 in errfinish (filename=filename@entry=0x55fe3fa54bad "bufpage.c", lineno=lineno@entry=161, 
funcname=funcname@entry=0x55fe3fb70ff8 <__func__.6> "PageIsVerified") at elog.c:620
#6  0x000055fe3f7c2415 in PageIsVerified (page=page@entry=0x796cf0c80000 "", blkno=blkno@entry=21, flags=2, 
checksum_failure_p=checksum_failure_p@entry=0x7ffd2c526b1f) at bufpage.c:161
#7  0x000055fe3f78a93d in buffer_readv_complete_one (zeroed_buffer=<synthetic pointer>, ignored_checksum=<synthetic 
pointer>, failed_checksum=0x7ffd2c526b1f, buffer_invalid=<synthetic pointer>, is_temp=false, failed=false, flags=8 '\b', 
buffer=3646, buf_off=0 '\000', td=0x796cfc63e7c8) at bufmgr.c:8593
#8  buffer_readv_complete (is_temp=false, cb_data=<optimized out>, prior_result=..., ioh=<optimized out>) at bufmgr.c:8724
#9  shared_buffer_readv_complete (ioh=<optimized out>, prior_result=..., cb_data=<optimized out>) at bufmgr.c:8883
#10 0x000055fe3f77ec61 in pgaio_io_call_complete_shared (ioh=ioh@entry=0x796cfc63e750) at aio_callback.c:258
#11 0x000055fe3f77d4f6 in pgaio_io_process_completion (ioh=ioh@entry=0x796cfc63e750, result=<optimized out>) at aio.c:540
#12 0x000055fe3f77fe42 in pgaio_io_perform_synchronously (ioh=ioh@entry=0x796cfc63e750) at aio_io.c:146
#13 0x000055fe3f77e121 in pgaio_io_stage (ioh=ioh@entry=0x796cfc63e750, op=op@entry=PGAIO_OP_READV) at aio.c:476
#14 0x000055fe3f77fd6d in pgaio_io_start_readv (ioh=ioh@entry=0x796cfc63e750, fd=199, iovcnt=iovcnt@entry=1, 
offset=offset@entry=172032) at aio_io.c:87
#15 0x000055fe3f795bae in FileStartReadV (ioh=ioh@entry=0x796cfc63e750, file=<optimized out>, iovcnt=iovcnt@entry=1, 
offset=offset@entry=172032, wait_event_info=wait_event_info@entry=167772183) at fd.c:2225
#16 0x000055fe3f7c648b in mdstartreadv (ioh=0x796cfc63e750, reln=0x55fe73b833b8, forknum=MAIN_FORKNUM, blocknum=21, 
buffers=<optimized out>, nblocks=1) at md.c:1041
#17 0x000055fe3f7c809c in smgrstartreadv (ioh=ioh@entry=0x796cfc63e750, reln=<optimized out>, 
forknum=forknum@entry=MAIN_FORKNUM, blocknum=blocknum@entry=21, buffers=buffers@entry=0x7ffd2c526da0, 
nblocks=nblocks@entry=1) at smgr.c:758
#18 0x000055fe3f78a1c7 in AsyncReadBuffers (operation=operation@entry=0x7ffd2c5272d0, 
nblocks_progress=nblocks_progress@entry=0x7ffd2c52723c) at bufmgr.c:2144
#19 0x000055fe3f78ce19 in StartReadBuffersImpl (allow_forwarding=false, flags=8, nblocks=0x7ffd2c52723c, blockNum=21, 
buffers=0x7ffd2c5272cc, operation=0x7ffd2c5272d0) at bufmgr.c:1548
#20 StartReadBuffer (operation=operation@entry=0x7ffd2c5272d0, buffer=buffer@entry=0x7ffd2c5272cc, 
blocknum=blocknum@entry=21, flags=8) at bufmgr.c:1636
#21 0x000055fe3f78d870 in ReadBuffer_common (strategy=0x0, mode=RBM_NORMAL, blockNum=21, forkNum=MAIN_FORKNUM, 
smgr_persistence=0 '\000', smgr=0x55fe73b833b8, rel=0x796d0066aa18) at bufmgr.c:1358
#22 ReadBufferExtended (reln=0x796d0066aa18, forkNum=forkNum@entry=MAIN_FORKNUM, blockNum=blockNum@entry=21, 
mode=mode@entry=RBM_NORMAL, strategy=strategy@entry=0x0) at bufmgr.c:945
#23 0x000055fe3f3ce074 in ReadBufferBI (relation=relation@entry=0x796d0066aa18, targetBlock=targetBlock@entry=21, 
mode=mode@entry=RBM_NORMAL, bistate=bistate@entry=0x0) at hio.c:93
#24 0x000055fe3f3cea30 in RelationGetBufferForTuple (relation=relation@entry=0x796d0066aa18, len=24, 
otherBuffer=otherBuffer@entry=0, options=options@entry=0, bistate=bistate@entry=0x0, 
vmbuffer=vmbuffer@entry=0x7ffd2c527468, vmbuffer_other=0x0, num_pages=1) at hio.c:617
#25 0x000055fe3f3bcb50 in heap_insert (relation=relation@entry=0x796d0066aa18, tup=tup@entry=0x55fe73be9638, 
cid=cid@entry=0, options=options@entry=0, bistate=bistate@entry=0x0) at heapam.c:2179
#26 0x000055fe3f3c7c82 in heapam_tuple_insert (relation=0x796d0066aa18, slot=0x55fe73be9528, cid=0, options=0, 
bistate=0x0) at heapam_handler.c:267
#27 0x000055fe3f5e2da2 in table_tuple_insert (bistate=0x0, options=0, cid=<optimized out>, slot=0x55fe73be9528, 
rel=0x796d0066aa18) at ../../../src/include/access/tableam.h:1456
#28 ExecInsert (context=context@entry=0x7ffd2c527620, resultRelInfo=resultRelInfo@entry=0x55fe737c8b00, 
slot=0x55fe73be9528, canSetTag=true, inserted_tuple=inserted_tuple@entry=0x0, insert_destrel=insert_destrel@entry=0x0) 
at nodeModifyTable.c:1272
#29 0x000055fe3f5e5542 in ExecModifyTable (pstate=0x55fe737c88f0) at nodeModifyTable.c:4933
#30 0x000055fe3f5b65f9 in ExecProcNodeFirst (node=0x55fe737c88f0) at execProcnode.c:470
...

I reproduce it rather easily (within 30 minutes) with 600 instances of
"sqlsmith --max-queries=1000" running against separate empty databases, on
my workstation with Ryzen 7900. I think I can compose a self-contained
repro, if needed... If you need more information/diagnostics, I'd be glad
to help.

Best regards,
Alexander

---

From: Daniel Gustafsson <[email-masked]>
Date: Mon, 6 Apr 2026 19:34:44 +0200

> On 6 Apr 2026, at 19:00, Alexander Lakhin <[email-masked]> wrote:
> 
> Hello Daniel,
> 
> running instance.

Thanks a lot for looking, and reporting!

> I'm getting, e.g.:
> 2026-04-06 18:09:12.077 EEST|postgres|regress_215|69d3cc86.3bfbdc|PANIC:  page verification failed, calculated checksum 40178 but expected 50558, buffer will be zeroed

I believe this is related to the same issue with visibilitymap clearing which
is discussed in:

https://postgr.es/m/[email-masked]

It was raised in this thread as well, but fixing it was deemed to be an
independent effort (and the above thread found it independently of the work in
this thread).

> repro, if needed... If you need more information/diagnostics, I'd be glad

If you have, or can easily make, a reproducer which can be boile down into a
test case then that would for sure help.

--
Daniel Gustafsson

---

From: SATYANARAYANA NARLAPURAM <[email-masked]>
Date: Wed, 8 Apr 2026 00:31:13 -0700

Hi Daniel,

> well as another CFBot run.

Sorry for the late review. pg_enable/disable_data_checksums() aren't needed
to be run
on the standby. It appears the call would silently register a dynamic
background worker that
could never start , and could unexpectedly fire after promotion.

Please find the attached patch to address this.

Thanks,
Satya

---

From: Daniel Gustafsson <[email-masked]>
Date: Wed, 8 Apr 2026 23:10:44 +0300

> On 8 Apr 2026, at 10:31, SATYANARAYANA NARLAPURAM <[email-masked]> wrote:
> On Fri, Apr 3, 2026 at 8:33 AM Daniel Gustafsson <[email-masked] <mailto:[email-masked]>> wrote:
> Sorry for the late review.

No need to apologise, reviewing already committed features is one of the most
valuable contributions there is. Thank you for looking!

> pg_enable/disable_data_checksums() aren't needed to be run 
> on the standby. It appears the call would silently register a dynamic background worker that 

I think this is correct, and the right fix.  Since I am travelling right now I
am a bit cautious with applying code changes, so I have this one staged for
when I get back.  I'll re-review, test and might add a test for it as well when
I am back in the office in Monday.

--
Daniel Gustafsson

---

From: Daniel Gustafsson <[email-masked]>
Date: Thu, 23 Apr 2026 12:16:44 +0200

While performing extensive post-commit testing using the built-in TAP tests in
checksum_extended mode, Tomas observed a couple issues.  The testing was done
on multiple machines, ranging from a fast x86 machine to a much slower rpi4,
but they all performed a very large number of iterations.  The combination of
hardware and the large number of tests executed is likely why some of the
issues went unnoticed.

Below are the issues described in more detail:

1) race condition between checkpoint and checksum state changes

The main issue is a race condition causing invalid state transitions.
When analyzing the failure (which we couldn't synthetically reproduce, only
observe by running tests over and over for a long time) we realized that the
original coding was performing checksum state transitions while replaying
online checkpoints as well as redo checkpoints.

Updating checksum state while replaying online checkpoints is incorrect.  After
a crash we'll start with the REDO record, which initializes checksums to the
right state.  And then later the checksum state is updated by the regular
XLOG_CHECKSUMS entries.

Moreover, the checksum state in the XLOG_CHECKPOINT_ONLINE record can be stale,
because the value is determined at the checkpoint start, but the WAL entry
is added at the end.  If there is a concurrent checksum change, the value
written to the WAL record will be stale.  Replaying it will cause an "invalid
transition" error later, during the next checksum state update. The fix is to
remove the checksum update from the online checkpoint altogether.

In fact, there's no need for CreateCheckPoint to update checksum state
in the ControlFile. If the value is stale, it could make it permanent.
But it's unnecessary - the ControlFile is updated by the process
performing the checksum state change. So remove that.

This, along with an re-ordering updating the controlfile and procsignal barrier
enission fixes the issue.  The re-ordering makes sure that the controlfile is
always updated *before* a procsignalbarrier is emitted to avoid a race like the
one described below:

1. A barrier for off to inprogress-on is emitted
2. All active backends absorbs the barrier
  - All processes in the cluster are in state inprogress-on
3. A new backend b' forks, reads controlfile and sets state of off
4. The controlfile is updated
5. A new backend b'' forks, reads controlfile and sets state to inprogress-on

b' and b'' have different states, and b' has an incompatible state with the
rest of the cluster.  Re-ordering as done in the attached makes this go away.

2) race condition in launcher exit

Another timing related issue was that reverting to the "off" state then
launcher errors out had synchronization logic which was racy as it was relying
on the cached checksum state and not the value from XLogCtl.  The logic for
determining if a launcher/worker was already active was also fragile as it
started another launcher which would overwrite certain data in shared memory.
The attached patch inspects shared memory instead and use that to signal the
running launcher to either abort (disable), or change cost parameters on a
running enable process.  These fixes makes erroring out and going to off state
stable.

3) Concurrency issue with ProcSignalInit / InitLocalDataChecksumState

The checksum barriers must not be consumed before the initial value gets
properly set.  On very slow systems, there could potentially be multiple
checksum state transitions between a fork and InitLocalDataChecksumState.  In
such cases we might get failures due to incorrect transitions.

With the current code this is not a live issue, as there is no place checking
interrupts in between the two functions.  But it's fragile, as it's trivial to
break this by adding an elog() somewhere.  Which is what happened to us while
debugging the other issues.  So better to explicitly hold interrupts for a
brief moment.

To find the issues, and to validate their fix, Tomas developed a new testsuite
which is attached as a .txt.  This is not proposed for adding to v19, it is
included to showcase what was done, and what will be further hacked on for a
new suite during the v20 cycle.  It is gated behind PG_TEST_EXTRA and is
intended to be executed by select members of the buildfarm.

As part of this postcommit review we also found a few more cleanups and smaller
fixes which are included.  The patchset also includes the patch submitted
upthread by Satyanarayana Narlapuram.

More details can be found in the individual commit messages.

Unless there are objections I would like to go ahead with this fixup fairly soon.

--
Daniel Gustafsson

commit f502fa990e877c6e4d1d03518e524519d8e88806
Author: Tomas Vondra <[email-masked]>
Date:   Wed Apr 15 12:50:06 2026 +0200

    POC: Test checksum state transitions using step through injection points

diff --git a/src/backend/access/transam/xlog.c b/src/backend/access/transam/xlog.c
index be92b6af20f..5b481784543 100644
--- a/src/backend/access/transam/xlog.c
+++ b/src/backend/access/transam/xlog.c
@@ -4762,6 +4762,9 @@ SetDataChecksumsOnInProgress(void)
 	uint32	data_checksum_version;
 
 	elog(LOG, "SetDataChecksumsOnInProgress / start");
+
+	INJECTION_POINT("datachecksums-enable-inprogress-checksums-delay", NULL);
+
 	/*
 	 * The state transition is performed in a critical section with
 	 * checkpoints held off to provide crash safety.
@@ -4782,6 +4785,8 @@ SetDataChecksumsOnInProgress(void)
 	MyProc->delayChkptFlags &= ~DELAY_CHKPT_START;
 	END_CRIT_SECTION();
 
+	INJECTION_POINT("datachecksums-enable-inprogress-checksums-after-xlogctl", NULL);
+
 	LWLockAcquire(ControlFileLock, LW_EXCLUSIVE);
 
 	elog(LOG, "SetDataChecksumsOnInProgress ControlFile->data_checksum_version  %u => %u",
@@ -4791,6 +4796,8 @@ SetDataChecksumsOnInProgress(void)
 	UpdateControlFile();
 	LWLockRelease(ControlFileLock);
 
+	INJECTION_POINT("datachecksums-enable-inprogress-checksums-after-controlfile", NULL);
+
 	elog(LOG, "SetDataChecksumsOnInProgress / EmitAndWaitDataChecksumsBarrier(PG_DATA_CHECKSUM_INPROGRESS_ON)");
 	EmitAndWaitDataChecksumsBarrier(PG_DATA_CHECKSUM_INPROGRESS_ON);
 
@@ -4862,6 +4869,8 @@ SetDataChecksumsOn(void)
 	MyProc->delayChkptFlags &= ~DELAY_CHKPT_START;
 	END_CRIT_SECTION();
 
+	INJECTION_POINT("datachecksums-enable-checksums-after-xlogctl", NULL);
+
 	/*
 	 * Update the controlfile before waiting since if we have an immediate
 	 * shutdown while waiting we want to come back up with checksums enabled.
@@ -4875,9 +4884,13 @@ SetDataChecksumsOn(void)
 	UpdateControlFile();
 	LWLockRelease(ControlFileLock);
 
+	INJECTION_POINT("datachecksums-enable-checksums-after-controlfile", NULL);
+
 	elog(LOG, "SetDataChecksumsOn / RequestCheckpoint(CHECKPOINT_FORCE | CHECKPOINT_WAIT | CHECKPOINT_FAST)");
 	RequestCheckpoint(CHECKPOINT_FORCE | CHECKPOINT_WAIT | CHECKPOINT_FAST);
 
+	INJECTION_POINT("datachecksums-enable-checksums-after-checkpoint", NULL);
+
 	elog(LOG, "SetDataChecksumsOn / EmitAndWaitDataChecksumsBarrier(PG_DATA_CHECKSUM_VERSION)");
 	EmitAndWaitDataChecksumsBarrier(PG_DATA_CHECKSUM_VERSION);
 
@@ -4924,6 +4937,8 @@ SetDataChecksumsOff(void)
 	{
 		SpinLockRelease(&XLogCtl->info_lck);
 
+		INJECTION_POINT("datachecksums-disable-inprogress-checksums-delay", NULL);
+
 		START_CRIT_SECTION();
 		MyProc->delayChkptFlags |= DELAY_CHKPT_START;
 
@@ -4934,24 +4949,30 @@ SetDataChecksumsOff(void)
 		XLogCtl->data_checksum_version = PG_DATA_CHECKSUM_INPROGRESS_OFF;
 		SpinLockRelease(&XLogCtl->info_lck);
 
-		elog(LOG, "SetDataChecksumsOff / XLogCtl->data_checksum_version %u = %u",
+		elog(LOG, "SetDataChecksumsOff / XLogCtl->data_checksum_version %u => %u",
 			 data_checksum_version, PG_DATA_CHECKSUM_INPROGRESS_OFF);

+		INJECTION_POINT("datachecksums-disable-inprogress-checksums-after-xlogctl", NULL);
+
 		LWLockAcquire(ControlFileLock, LW_EXCLUSIVE);
 
-		elog(LOG, "SetDataChecksumsOff / ControlFile->data_checksum_version %u = %u",
+		elog(LOG, "SetDataChecksumsOff / ControlFile->data_checksum_version %u => %u",
 			 ControlFile->data_checksum_version, PG_DATA_CHECKSUM_INPROGRESS_OFF);
 
 		ControlFile->data_checksum_version = PG_DATA_CHECKSUM_INPROGRESS_OFF;
 		UpdateControlFile();
 		LWLockRelease(ControlFileLock);
 
+		INJECTION_POINT("datachecksums-disable-inprogress-checksums-after-controlfile", NULL);
+
 		elog(LOG, "SetDataChecksumsOff / RequestCheckpoint(CHECKPOINT_FORCE | CHECKPOINT_WAIT | CHECKPOINT_FAST)");
 		RequestCheckpoint(CHECKPOINT_FORCE | CHECKPOINT_WAIT | CHECKPOINT_FAST);
 
+		INJECTION_POINT("datachecksums-disable-inprogress-checksums-after-checkpoint", NULL);
+
 		elog(LOG, "SetDataChecksumsOff / EmitAndWaitDataChecksumsBarrier(PG_DATA_CHECKSUM_INPROGRESS_OFF)");
 		EmitAndWaitDataChecksumsBarrier(PG_DATA_CHECKSUM_INPROGRESS_OFF);
 
@@ -4971,6 +4992,8 @@ SetDataChecksumsOff(void)
 		SpinLockRelease(&XLogCtl->info_lck);
 	}
 
+	INJECTION_POINT("datachecksums-disable-checksums-delay", NULL);
+
 	START_CRIT_SECTION();
 	/* Ensure that we don't incur a checkpoint during disabling checksums */
 	MyProc->delayChkptFlags |= DELAY_CHKPT_START;
@@ -4988,6 +5011,8 @@ SetDataChecksumsOff(void)
 	MyProc->delayChkptFlags &= ~DELAY_CHKPT_START;
 	END_CRIT_SECTION();
 
+	INJECTION_POINT("datachecksums-disable-checksums-after-xlogctl", NULL);
+
 	LWLockAcquire(ControlFileLock, LW_EXCLUSIVE);
 
 	elog(LOG, "SetDataChecksumsOff / ControlFile->data_checksum_version %u => %u",
@@ -4997,9 +5022,13 @@ SetDataChecksumsOff(void)
 	UpdateControlFile();
 	LWLockRelease(ControlFileLock);
 
+	INJECTION_POINT("datachecksums-disable-checksums-after-controlfile", NULL);
+
 	elog(LOG, "SetDataChecksumsOff / RequestCheckpoint(CHECKPOINT_FORCE | CHECKPOINT_WAIT | CHECKPOINT_FAST)");
 	RequestCheckpoint(CHECKPOINT_FORCE | CHECKPOINT_WAIT | CHECKPOINT_FAST);
 
+	INJECTION_POINT("datachecksums-disable-checksums-after-checkpoint", NULL);
+
 	elog(LOG, "SetDataChecksumsOff / EmitAndWaitDataChecksumsBarrier(PG_DATA_CHECKSUM_OFF)");
 	EmitAndWaitDataChecksumsBarrier(PG_DATA_CHECKSUM_OFF);
 
diff --git a/src/test/modules/test_checksums/t/010_injection_2.pl b/src/test/modules/test_checksums/t/010_injection_2.pl
new file mode 100644
index 00000000000..a8ae9ffd151
--- /dev/null
+++ b/src/test/modules/test_checksums/t/010_injection_2.pl
@@ -0,0 +1,209 @@
+
+# Copyright (c) 2026, PostgreSQL Global Development Group
+
+# Test suite for testing enabling data checksums in an online cluster with
+# injection point tests injecting failures into the processing
+
+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, or very expensive, to execute.  There are two
+# PG_TEST_EXTRA options for running it, "checksum" for a pared-down test suite
+# an "checksum_extended" for the full suite.  The full suite can run for hours
+# on slow or constrained systems.
+my $extended = undef;
+if ($ENV{PG_TEST_EXTRA})
+{
+	$extended = 1 if ($ENV{PG_TEST_EXTRA} =~ /\bchecksum_extended\b/);
+	plan skip_all => 'Expensive data checksums test disabled'
+	  unless ($ENV{PG_TEST_EXTRA} =~ /\bchecksum(_extended)?\b/);
+}
+
+if ($ENV{enable_injection_points} ne 'yes')
+{
+	plan skip_all => 'Injection points not supported by this build';
+}
+
+# ---------------------------------------------------------------------------
+# Test cluster setup
+#
+
+# Initiate testcluster
+my $node = PostgreSQL::Test::Cluster->new('injection_node');
+$node->init(no_data_checksums => 1);
+$node->start;
+
+# Set up test environment
+$node->safe_psql('postgres', 'CREATE EXTENSION test_checksums;');
+$node->safe_psql('postgres', 'CREATE EXTENSION injection_points;');
+
+my $pgbench = undef;
+my $scalefactor = ($extended ? 10 : 1);
+my $node_loglocation = 0;
+
+$node->command_ok(
+	[
+		'pgbench', '-p', $node->port, '-i',
+		'-s', $scalefactor, '-q', 'postgres'
+	]);
+
+# Start a pgbench run in the background against the server specified via the
+# port passed as parameter.
+sub background_rw_pgbench
+{
+	my $port = shift;
+
+	# If a previous pgbench is still running, start by shutting it down.
+	$pgbench->finish if $pgbench;
+
+	my $clients = 1;
+	my $runtime = 2;
+
+	if ($extended)
+	{
+		# Randomize the number of pgbench clients a bit (range 1-16)
+		$clients = 1 + int(rand(15));
+		$runtime = 600;
+	}
+	my @cmd = ('pgbench', '-p', $port, '-T', $runtime, '-c', $clients);
+
+	# Randomize whether we spawn connections or not
+	push(@cmd, '-C') if ($extended && cointoss);
+	# Finally add the database name to use
+	push(@cmd, 'postgres');
+
+	$pgbench = IPC::Run::start(
+		\@cmd,
+		'<' => '/dev/null',
+		'>' => '/dev/null',
+		'2>' => '/dev/null',
+		IPC::Run::timer($PostgreSQL::Test::Utils::timeout_default));
+}
+
+# Test checksum transition. The function has these arguments:
+#
+# - start checksum state (enabled/disabled)
+# - first - first checksum change
+# - second - second checksum change
+# - point - injection point the first change should wait on
+# - final - expected checksum state at the end
+#
+# The test puts the instance into the initial checksum state, triggers two
+# checksum changes, and verifies the final state is as expected. The first
+# state change is paused on a selected injection point, and unpaused after
+# the second change gets initiated.
+#
+# The injection point is triggered only by the datachecksum launcher, and
+# there can be only one such process. So there's no risk of hitting the
+# injection point by both changes.
+sub test_checksum_transition
+{
+	my ($start, $first, $second, $point, $final) = @_;
+
+	$node->safe_psql('postgres',
+		"SELECT '========== " . $start . " / " . $first . " / " . $second . " / " . $point . " / " . $final . " =========='");
+
+	note($start . " / " . $first . " / " . $second . " / " . $point . " / " . $final);
+
+	note('changing checksums into initial state: ' . $start);
+
+	enable_data_checksums($node, wait => 'on') if ($start eq 'enabled');
+	disable_data_checksums($node, wait => 'off') if ($start eq 'disabled');
+
+	note('attaching injection point: ' . $point);
+	$node->safe_psql('postgres',
+		"SELECT injection_points_attach('" . $point . "','wait');"
+	);
+
+	note("triggering first checksum change: " . $first);
+
+	enable_data_checksums($node) if ($first eq 'enable');
+	disable_data_checksums($node) if ($first eq 'disable');
+
+	note("waiting for the injection point to be hit");
+	$node->poll_query_until(
+		'postgres',
+		"SELECT COUNT(*) FROM pg_catalog.pg_stat_activity WHERE wait_event = '" . $point . "'",
+		'1');
+
+	note("triggering second checksum change: " . $second);
+
+	enable_data_checksums($node) if ($second eq 'enable');
+	disable_data_checksums($node) if ($second eq 'disable');
+
+	note("waking and detaching injection point");
+	$node->safe_psql('postgres',
+		"SELECT injection_points_wakeup('" . $point . "');");
+
+	note("detaching injection point");
+	$node->safe_psql('postgres',
+		"SELECT injection_points_detach('" . $point . "');");
+
+	note('wait for the checksum launcher to exit');
+	$node->poll_query_until('postgres',
+			"SELECT count(*) = 0 "
+		  . "FROM pg_catalog.pg_stat_activity "
+		  . "WHERE backend_type = 'datachecksum launcher';");
+
+	test_checksum_state($node, $final);
+
+	# Since the log isn't being written to now, parse the log and check
+	# for instances of checksum verification failures.
+	my $log = PostgreSQL::Test::Utils::slurp_file($node->logfile,
+		$node_loglocation);
+	unlike(
+		$log,
+		qr/page verification failed,.+\d$/,
+		"no checksum validation errors in primary log (during WAL recovery)"
+	);
+	$node_loglocation = -s $node->logfile;
+}
+
+# Start the test suite with pgbench running.
+background_rw_pgbench($node->port);
+
+test_checksum_transition('disabled', 'enable', 'disable', 'datachecksums-enable-inprogress-checksums-delay', 'off');
+test_checksum_transition('disabled', 'enable', 'disable', 'datachecksums-enable-inprogress-checksums-after-xlogctl', 'off');
+test_checksum_transition('disabled', 'enable', 'disable', 'datachecksums-enable-inprogress-checksums-after-controlfile', 'off');
+test_checksum_transition('disabled', 'enable', 'disable', 'datachecksums-enable-checksums-delay', 'off');
+test_checksum_transition('disabled', 'enable', 'disable', 'datachecksums-enable-checksums-after-xlogctl', 'off');
+test_checksum_transition('disabled', 'enable', 'disable', 'datachecksums-enable-checksums-after-controlfile', 'off');
+test_checksum_transition('disabled', 'enable', 'disable', 'datachecksums-enable-checksums-after-checkpoint', 'off');
+
+test_checksum_transition('disabled', 'enable', 'enable', 'datachecksums-enable-inprogress-checksums-delay', 'on');
+test_checksum_transition('disabled', 'enable', 'enable', 'datachecksums-enable-inprogress-checksums-after-xlogctl', 'on');
+test_checksum_transition('disabled', 'enable', 'enable', 'datachecksums-enable-inprogress-checksums-after-controlfile', 'on');
+test_checksum_transition('disabled', 'enable', 'enable', 'datachecksums-enable-checksums-delay', 'on');
+test_checksum_transition('disabled', 'enable', 'enable', 'datachecksums-enable-checksums-after-xlogctl', 'on');
+test_checksum_transition('disabled', 'enable', 'enable', 'datachecksums-enable-checksums-after-controlfile', 'on');
+test_checksum_transition('disabled', 'enable', 'enable', 'datachecksums-enable-checksums-after-checkpoint', 'on');
+
+test_checksum_transition('enabled', 'disable', 'disable', 'datachecksums-disable-inprogress-checksums-delay', 'off');
+test_checksum_transition('enabled', 'disable', 'disable', 'datachecksums-disable-inprogress-checksums-after-xlogctl', 'off');
+test_checksum_transition('enabled', 'disable', 'disable', 'datachecksums-disable-inprogress-checksums-after-controlfile', 'off');
+test_checksum_transition('enabled', 'disable', 'disable', 'datachecksums-disable-inprogress-checksums-after-checkpoint', 'off');
+test_checksum_transition('enabled', 'disable', 'disable', 'datachecksums-disable-checksums-delay', 'off');
+test_checksum_transition('enabled', 'disable', 'disable', 'datachecksums-disable-checksums-after-xlogctl', 'off');
+test_checksum_transition('enabled', 'disable', 'disable', 'datachecksums-disable-checksums-after-controlfile', 'off');
+test_checksum_transition('enabled', 'disable', 'disable', 'datachecksums-disable-checksums-after-checkpoint', 'off');
+
+test_checksum_transition('enabled', 'disable', 'enable', 'datachecksums-disable-inprogress-checksums-delay', 'on');
+test_checksum_transition('enabled', 'disable', 'enable', 'datachecksums-disable-inprogress-checksums-after-xlogctl', 'on');
+test_checksum_transition('enabled', 'disable', 'enable', 'datachecksums-disable-inprogress-checksums-after-controlfile', 'on');
+test_checksum_transition('enabled', 'disable', 'enable', 'datachecksums-disable-inprogress-checksums-after-checkpoint', 'on');
+test_checksum_transition('enabled', 'disable', 'enable', 'datachecksums-disable-checksums-delay', 'on');
+test_checksum_transition('enabled', 'disable', 'enable', 'datachecksums-disable-checksums-after-xlogctl', 'on');
+test_checksum_transition('enabled', 'disable', 'enable', 'datachecksums-disable-checksums-after-controlfile', 'on');
+test_checksum_transition('enabled', 'disable', 'enable', 'datachecksums-disable-checksums-after-checkpoint', 'on');
+
+$node->stop;
+done_testing();
diff --git a/src/test/modules/test_checksums/t/011_injection_checkpoint.pl b/src/test/modules/test_checksums/t/011_injection_checkpoint.pl
new file mode 100644
index 00000000000..ce1553e8ecd
--- /dev/null
+++ b/src/test/modules/test_checksums/t/011_injection_checkpoint.pl
@@ -0,0 +1,196 @@
+
+# Copyright (c) 2026, PostgreSQL Global Development Group
+
+# Test suite for testing enabling data checksums in an online cluster with
+# injection point tests injecting failures into the processing
+
+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, or very expensive, to execute.  There are two
+# PG_TEST_EXTRA options for running it, "checksum" for a pared-down test suite
+# an "checksum_extended" for the full suite.  The full suite can run for hours
+# on slow or constrained systems.
+my $extended = undef;
+if ($ENV{PG_TEST_EXTRA})
+{
+	$extended = 1 if ($ENV{PG_TEST_EXTRA} =~ /\bchecksum_extended\b/);
+	plan skip_all => 'Expensive data checksums test disabled'
+	  unless ($ENV{PG_TEST_EXTRA} =~ /\bchecksum(_extended)?\b/);
+}
+
+if ($ENV{enable_injection_points} ne 'yes')
+{
+	plan skip_all => 'Injection points not supported by this build';
+}
+
+# ---------------------------------------------------------------------------
+# Test cluster setup
+#
+
+# Initiate testcluster
+my $node = PostgreSQL::Test::Cluster->new('injection_node');
+$node->init(no_data_checksums => 1);
+$node->start;
+
+# Set up test environment
+$node->safe_psql('postgres', 'CREATE EXTENSION test_checksums;');
+$node->safe_psql('postgres', 'CREATE EXTENSION injection_points;');
+
+my $pgbench = undef;
+my $scalefactor = ($extended ? 10 : 1);
+my $node_loglocation = 0;
+
+$node->command_ok(
+	[
+		'pgbench', '-p', $node->port, '-i',
+		'-s', $scalefactor, '-q', 'postgres'
+	]);
+
+# Start a pgbench run in the background against the server specified via the
+# port passed as parameter.
+sub background_rw_pgbench
+{
+	my $port = shift;
+
+	# If a previous pgbench is still running, start by shutting it down.
+	$pgbench->finish if $pgbench;
+
+	my $clients = 1;
+	my $runtime = 2;
+
+	if ($extended)
+	{
+		# Randomize the number of pgbench clients a bit (range 1-16)
+		$clients = 1 + int(rand(15));
+		$runtime = 600;
+	}
+	my @cmd = ('pgbench', '-p', $port, '-T', $runtime, '-c', $clients);
+
+	# Randomize whether we spawn connections or not
+	push(@cmd, '-C') if ($extended && cointoss);
+	# Finally add the database name to use
+	push(@cmd, 'postgres');
+
+	$pgbench = IPC::Run::start(
+		\@cmd,
+		'<' => '/dev/null',
+		'>' => '/dev/null',
+		'2>' => '/dev/null',
+		IPC::Run::timer($PostgreSQL::Test::Utils::timeout_default));
+}
+
+# Test checksum transition concurrent with a checkpoint.
+#
+# The function has these arguments:
+#
+# - start checksum state (enabled/disabled)
+# - change - checksum change to initiate
+# - point - injection point the first change should wait on
+# - final - expected checksum state at the end
+#
+# The test puts the instance into the initial checksum state, triggers a
+# checksum change concurrent with a checkpoint, and verifies the final state
+# is as expected. The state change is paused on a selected injection
+# point, and unpaused after performing a checkpoint.
+#
+# Finally, the instance is restarted (in either fast ot immediate mode),
+# the final checksum state is validated against the expected value, and
+# the server log is checked for checksum failures.
+sub test_checksum_transition
+{
+	my ($start, $change, $point, $final) = @_;
+
+	# Start the test suite with pgbench running.
+	background_rw_pgbench($node->port);
+
+	$node->safe_psql('postgres',
+		"SELECT '========== " . $start . " / " . $change . " / " . $point . " / " . $final . " =========='");
+
+	note($start . " / " . $change . " / " . $point . " / " . $final);
+
+	note('changing checksums into initial state: ' . $start);
+
+	enable_data_checksums($node, wait => 'on') if ($start eq 'enabled');
+	disable_data_checksums($node, wait => 'off') if ($start eq 'disabled');
+
+	note('attaching injection point: ' . $point);
+	$node->safe_psql('postgres',
+		"SELECT injection_points_attach('" . $point . "','wait');"
+	);
+
+	note("triggering checksum change: " . $change);
+
+	enable_data_checksums($node) if ($change eq 'enable');
+	disable_data_checksums($node) if ($change eq 'disable');
+
+	note("waiting for the injection point to be hit");
+	$node->poll_query_until(
+		'postgres',
+		"SELECT COUNT(*) FROM pg_catalog.pg_stat_activity WHERE wait_event = '" . $point . "'",
+		'1');
+
+	note('checkpoint');
+	$node->safe_psql('postgres', "CHECKPOINT");
+
+	note("waking and detaching injection point");
+	$node->safe_psql('postgres',
+		"SELECT injection_points_wakeup('" . $point . "');");
+
+	note("detaching injection point");
+	$node->safe_psql('postgres',
+		"SELECT injection_points_detach('" . $point . "');");
+
+	note('wait for the checksum launcher to exit');
+	$node->poll_query_until('postgres',
+			"SELECT count(*) = 0 "
+		  . "FROM pg_catalog.pg_stat_activity "
+		  . "WHERE backend_type = 'datachecksum launcher';");
+
+	test_checksum_state($node, $final);
+
+	$node->stop(stopmode());
+	$node->start;
+
+	test_checksum_state($node, $final);
+
+	# Since the log isn't being written to now, parse the log and check
+	# for instances of checksum verification failures.
+	my $log = PostgreSQL::Test::Utils::slurp_file($node->logfile,
+		$node_loglocation);
+	unlike(
+		$log,
+		qr/page verification failed,.+\d$/,
+		"no checksum validation errors in primary log (during WAL recovery)"
+	);
+	$node_loglocation = -s $node->logfile;
+}
+
+test_checksum_transition('disabled', 'enable', 'datachecksums-enable-inprogress-checksums-delay', 'on');
+test_checksum_transition('disabled', 'enable', 'datachecksums-enable-inprogress-checksums-after-xlogctl', 'on');
+test_checksum_transition('disabled', 'enable', 'datachecksums-enable-inprogress-checksums-after-controlfile', 'on');
+test_checksum_transition('disabled', 'enable', 'datachecksums-enable-checksums-delay', 'on');
+test_checksum_transition('disabled', 'enable', 'datachecksums-enable-checksums-after-xlogctl', 'on');
+test_checksum_transition('disabled', 'enable', 'datachecksums-enable-checksums-after-controlfile', 'on');
+test_checksum_transition('disabled', 'enable', 'datachecksums-enable-checksums-after-checkpoint', 'on');
+
+test_checksum_transition('enabled', 'disable', 'datachecksums-disable-inprogress-checksums-delay', 'off');
+test_checksum_transition('enabled', 'disable', 'datachecksums-disable-inprogress-checksums-after-xlogctl', 'off');
+test_checksum_transition('enabled', 'disable', 'datachecksums-disable-inprogress-checksums-after-controlfile', 'off');
+test_checksum_transition('enabled', 'disable', 'datachecksums-disable-inprogress-checksums-after-checkpoint', 'off');
+test_checksum_transition('enabled', 'disable', 'datachecksums-disable-checksums-delay', 'off');
+test_checksum_transition('enabled', 'disable', 'datachecksums-disable-checksums-after-xlogctl', 'off');
+test_checksum_transition('enabled', 'disable', 'datachecksums-disable-checksums-after-controlfile', 'off');
+test_checksum_transition('enabled', 'disable', 'datachecksums-disable-checksums-after-checkpoint', 'off');
+
+$node->stop;
+done_testing();
diff --git a/src/test/modules/test_checksums/t/012_injection_checkpoint_crash.pl b/src/test/modules/test_checksums/t/012_injection_checkpoint_crash.pl
new file mode 100644
index 00000000000..98e9c805c41
--- /dev/null
+++ b/src/test/modules/test_checksums/t/012_injection_checkpoint_crash.pl
@@ -0,0 +1,214 @@
+
+# Copyright (c) 2026, PostgreSQL Global Development Group
+
+# Test suite for testing enabling data checksums in an online cluster with
+# injection point tests injecting failures into the processing
+
+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, or very expensive, to execute.  There are two
+# PG_TEST_EXTRA options for running it, "checksum" for a pared-down test suite
+# an "checksum_extended" for the full suite.  The full suite can run for hours
+# on slow or constrained systems.
+my $extended = undef;
+if ($ENV{PG_TEST_EXTRA})
+{
+	$extended = 1 if ($ENV{PG_TEST_EXTRA} =~ /\bchecksum_extended\b/);
+	plan skip_all => 'Expensive data checksums test disabled'
+	  unless ($ENV{PG_TEST_EXTRA} =~ /\bchecksum(_extended)?\b/);
+}
+
+if ($ENV{enable_injection_points} ne 'yes')
+{
+	plan skip_all => 'Injection points not supported by this build';
+}
+
+# ---------------------------------------------------------------------------
+# Test cluster setup
+#
+
+# Initiate testcluster
+my $node = PostgreSQL::Test::Cluster->new('injection_node');
+$node->init(no_data_checksums => 1);
+$node->start;
+
+# Set up test environment
+$node->safe_psql('postgres', 'CREATE EXTENSION test_checksums;');
+$node->safe_psql('postgres', 'CREATE EXTENSION injection_points;');
+
+my $pgbench = undef;
+my $scalefactor = ($extended ? 10 : 1);
+my $node_loglocation = 0;
+
+$node->command_ok(
+	[
+		'pgbench', '-p', $node->port, '-i',
+		'-s', $scalefactor, '-q', 'postgres'
+	]);
+
+# Start a pgbench run in the background against the server specified via the
+# port passed as parameter.
+sub background_rw_pgbench
+{
+	my $port = shift;
+
+	# If a previous pgbench is still running, start by shutting it down.
+	$pgbench->finish if $pgbench;
+
+	my $clients = 1;
+	my $runtime = 2;
+
+	if ($extended)
+	{
+		# Randomize the number of pgbench clients a bit (range 1-16)
+		$clients = 1 + int(rand(15));
+		$runtime = 600;
+	}
+	my @cmd = ('pgbench', '-p', $port, '-T', $runtime, '-c', $clients);
+
+	# Randomize whether we spawn connections or not
+	push(@cmd, '-C') if ($extended && cointoss);
+	# Finally add the database name to use
+	push(@cmd, 'postgres');
+
+	$pgbench = IPC::Run::start(
+		\@cmd,
+		'<' => '/dev/null',
+		'>' => '/dev/null',
+		'2>' => '/dev/null',
+		IPC::Run::timer($PostgreSQL::Test::Utils::timeout_default));
+}
+
+# Test checksum transition concurrent with a checkpoint.
+#
+# The function has these arguments:
+#
+# - start checksum state (enabled/disabled)
+# - change - checksum change to initiate
+# - point1 - injection point before checkpoint
+# - point2 - injection point after checkpoint
+# - final - expected checksum state at the end
+#
+# The test puts the instance into the initial checksum state, triggers a
+# checksum change that pauses on a selected injection point. Then performs
+# a checkpoint, unpauses the change so that it proceeds to a second
+# injection point.
+#
+# Then the instance is restarted in immediate mode to simulate failure,
+# and the final checksum state is validated against the expected value.
+# The server log is checked for checksum failures.
+sub test_checksum_transition
+{
+	my ($start, $change, $point1, $point2, $final) = @_;
+
+	# Start the test suite with pgbench running.
+	background_rw_pgbench($node->port);
+
+	$node->safe_psql('postgres',
+		"SELECT '========== " . $start . " / " . $change . " / " . $point1 . " / " . $final . " =========='");
+
+	note($start . " / " . $change . " / " . $point1 . " / " . $final);
+
+	note('changing checksums into initial state: ' . $start);
+
+	enable_data_checksums($node, wait => 'on') if ($start eq 'enabled');
+	disable_data_checksums($node, wait => 'off') if ($start eq 'disabled');
+
+	note('attaching injection point: ' . $point1);
+	$node->safe_psql('postgres',
+		"SELECT injection_points_attach('" . $point1 . "','wait');"
+	);
+
+	if (defined($point2))
+	{
+		note('attaching injection point: ' . $point2);
+		$node->safe_psql('postgres',
+			"SELECT injection_points_attach('" . $point2 . "','wait');"
+		);
+	}
+
+	note("triggering checksum change: " . $change);
+
+	enable_data_checksums($node) if ($change eq 'enable');
+	disable_data_checksums($node) if ($change eq 'disable');
+
+	note("waiting for the injection point to be hit");
+	$node->poll_query_until(
+		'postgres',
+		"SELECT COUNT(*) FROM pg_catalog.pg_stat_activity WHERE wait_event = '" . $point1 . "'",
+		'1');
+
+	note('checkpoint');
+	$node->safe_psql('postgres', "CHECKPOINT");
+
+	note("waking and detaching injection point");
+	$node->safe_psql('postgres',
+		"SELECT injection_points_wakeup('" . $point1 . "');");
+
+	note("detaching injection point");
+	$node->safe_psql('postgres',
+		"SELECT injection_points_detach('" . $point1 . "');");
+
+	if (defined($point2))
+	{
+		note("waiting for the injection point to be hit");
+		$node->poll_query_until(
+			'postgres',
+			"SELECT COUNT(*) FROM pg_catalog.pg_stat_activity WHERE wait_event = '" . $point2 . "'",
+			'1');
+	}
+	else
+	{
+		note('wait for the checksum launcher to exit');
+		$node->poll_query_until('postgres',
+				"SELECT count(*) = 0 "
+			  . "FROM pg_catalog.pg_stat_activity "
+			  . "WHERE backend_type = 'datachecksum launcher';");
+	}
+
+	$node->stop('immediate');
+	$node->start;
+
+	test_checksum_state($node, $final);
+
+	# Since the log isn't being written to now, parse the log and check
+	# for instances of checksum verification failures.
+	my $log = PostgreSQL::Test::Utils::slurp_file($node->logfile,
+		$node_loglocation);
+	unlike(
+		$log,
+		qr/page verification failed,.+\d$/,
+		"no checksum validation errors in primary log (during WAL recovery)"
+	);
+	$node_loglocation = -s $node->logfile;
+}
+
+test_checksum_transition('disabled', 'enable', 'datachecksums-enable-inprogress-checksums-delay', 'datachecksums-enable-inprogress-checksums-after-xlogctl', 'off');
+test_checksum_transition('disabled', 'enable', 'datachecksums-enable-inprogress-checksums-after-xlogctl', 'datachecksums-enable-inprogress-checksums-after-controlfile', 'off');
+test_checksum_transition('disabled', 'enable', 'datachecksums-enable-inprogress-checksums-after-controlfile', 'datachecksums-enable-checksums-delay', 'off');
+test_checksum_transition('disabled', 'enable', 'datachecksums-enable-checksums-delay', 'datachecksums-enable-checksums-after-xlogctl', 'on');
+test_checksum_transition('disabled', 'enable', 'datachecksums-enable-checksums-after-xlogctl', 'datachecksums-enable-checksums-after-controlfile', 'on');
+test_checksum_transition('disabled', 'enable', 'datachecksums-enable-checksums-after-controlfile', 'datachecksums-enable-checksums-after-checkpoint', 'on');
+test_checksum_transition('disabled', 'enable', 'datachecksums-enable-checksums-after-checkpoint', undef, 'on');
+
+test_checksum_transition('enabled', 'disable', 'datachecksums-disable-inprogress-checksums-delay', 'datachecksums-disable-inprogress-checksums-after-xlogctl', 'off');
+test_checksum_transition('enabled', 'disable', 'datachecksums-disable-inprogress-checksums-after-xlogctl', 'datachecksums-disable-inprogress-checksums-after-controlfile', 'off');
+test_checksum_transition('enabled', 'disable', 'datachecksums-disable-inprogress-checksums-after-controlfile', 'datachecksums-disable-inprogress-checksums-after-checkpoint', 'off');
+test_checksum_transition('enabled', 'disable', 'datachecksums-disable-inprogress-checksums-after-checkpoint', 'datachecksums-disable-checksums-delay', 'off');
+test_checksum_transition('enabled', 'disable', 'datachecksums-disable-checksums-delay', 'datachecksums-disable-checksums-after-xlogctl', 'off');
+test_checksum_transition('enabled', 'disable', 'datachecksums-disable-checksums-after-xlogctl', 'datachecksums-disable-checksums-after-controlfile', 'off');
+test_checksum_transition('enabled', 'disable', 'datachecksums-disable-checksums-after-controlfile', 'datachecksums-disable-checksums-after-checkpoint', 'off');
+test_checksum_transition('enabled', 'disable', 'datachecksums-disable-checksums-after-checkpoint', undef, 'off');
+
+$node->stop;
+done_testing();

---

From: Ayush Tiwari <[email-masked]>
Date: Sat, 25 Apr 2026 17:29:05 +0530

Hi,

> hardware and the large number of tests executed is likely why some of the
Thanks for the patchset. I applied the eight proposed patches on top of
current master and did a review/test pass. I do have one concern
about the duplicate-launcher race discussed earlier, though.

I don't think 0006 fully addresses the duplicate-launcher exit race [1]. I
was
able to reproduce the issue with a deterministic test using the existing
test_checksums delay hooks: delay
the initial StartDataChecksumsWorkerLauncher()
path so two quick pg_enable_data_checksums() calls can queue two launchers,
delay the final transition to on, then fire follow-up
pg_enable_data_checksums()
calls while the real launcher is still active.

The relevant logs from that run:

[37940] DEBUG:  background worker "datachecksums launcher" started
[37941] DEBUG:  background worker "datachecksums launcher" started
[37940] LOG:  enabling data checksums requested, starting data checksum
calculation
[37941] LOG:  background worker "datachecksums launcher" already running,
exiting
[37940] LOG:  initiating data checksum processing in database "postgres"
[37952] DEBUG:  background worker "datachecksums launcher" started
[37952] LOG:  enabling data checksums requested, starting data checksum
calculation
[37952] LOG:  initiating data checksum processing in database "postgres"
[37952] WARNING:  cannot set data checksums to "on", current state is not
"inprogress-on", disabling
[37989] DEBUG:  background worker "datachecksums launcher" started
[37989] LOG:  enabling data checksums requested, starting data checksum
calculation
[38006] DEBUG:  background worker "datachecksums launcher" started
[38006] LOG:  enabling data checksums requested, starting data checksum
calculation

So, at least the duplicate-launcher part is still reproducible after the
fixup
series. The warning also suggests the state can be perturbed by the
duplicate
launcher, although in my run the final state still ended up as on.

A few other smaller comments/nits:

1. In src/backend/access/transam/xlog.c, the comment in
SetDataChecksumsOff()
says the branch implies the state is inprogress-on or inprogress-off, but
after the patch inprogress-on is handled by the preceding if condition. It
looks like this should probably only mention inprogress-off.

2. There is a repeated typo in comments:

src/backend/utils/init/postinit.c: "interrups" -> "interrupts"
src/backend/postmaster/auxprocess.c: "interrups" -> "interrupts"

3. A few commit messages could use a quick polish pass:

0002: "onine" -> "online"
0004: "Additionelly" -> "Additionally"
0006: "being enabled of disabled" -> "being enabled or disabled"
0006: "change it's operation" -> "change its operation"
0006: "ss now avoided" -> "is now avoided"

4. One minor question on 0006: the runtime cost-parameter update path is
only
meaningful while checksums are being enabled. That looks fine from the
current
control flow, but would it be worth adding a short comment or assertion near
that update path to make the assumption explicit?

Other than the above concern and cleanup items, the approach and behavior
looked good to me.

Regards,
Ayush

[1]  PostgreSQL: [BUG] Race in online checksums launcher_exit()
<https://www.postgresql.org/message-id/CAJTYsWWg6tFrdMhWs5PkwESTNeeUUsMuY17O4UmPPh771c3stA%40mail.gmail.com>

---

From: SATYANARAYANA NARLAPURAM <[email-masked]>
Date: Tue, 28 Apr 2026 13:25:30 -0700

Hi,

> Unless there are objections I would like to go ahead with this fixup

All the patches applied cleanly and the tests passing. A few minor comments:

In SetDataChecksumsOff, stale comment, only INPROGRESS_OFF can reach the
else section
/*
* Ending up here implies that the checksums state is "inprogress-on"
* or "inprogress-off" and we can transition directly to "off" from
* there.
*/
SpinLockRelease(&XLogCtl->info_lck);

Should we update ControlFile->data_checksum_version at the end-of-recovery?
If not, the disk
is stale compared to in memory until the next checkpoint. The code two
lines below updates
the control file anyways to set the DB_IN_PRODUCTION. Maybe combine the
update with that?
It's no big deal if we don't do it because it will be self correct but
tools like pg_controldata
give stale value for some time.

Thanks,
Satya

---

From: Daniel Gustafsson <[email-masked]>
Date: Wed, 29 Apr 2026 00:06:14 +0200

> On 25 Apr 2026, at 13:59, Ayush Tiwari <[email-masked]> wrote:

> I don't think 0006 fully addresses the duplicate-launcher exit race [1]. I was
> delay the final transition to on, then fire follow-up pg_enable_data_checksums()

Thanks to the really great reproducer script I got offlist (which is attached
here) I was able to reproduce and figure out what was wrong.  This sequence
exposed a thinko in the cleanup handler where a process exiting due to an
already running launcher would enter cleanup, and thus wipe state from the
running launcher.  It needed this quite narrow window to hit, but it was good
to catch that.  Fixed in the attached by giving processes a way to register
themselves as needing no cleanup when exiting.

> 1. In src/backend/access/transam/xlog.c, the comment in SetDataChecksumsOff()

Fixed.

Fixed.

Fixed.

> 4. One minor question on 0006: the runtime cost-parameter update path is only
> meaningful while checksums are being enabled. That looks fine from the current

We have this assertion which makes sure that there are no costs passed for
disabling, not sure if we need much more since there is now way for the user to
inject any cost parameters.

  #ifdef USE_ASSERT_CHECKING
      /* The cost delay settings have no effect when disabling */
      if (op == DISABLE_DATACHECKSUMS)
          Assert(cost_delay == 0 && cost_limit == 0);
  #endif

--
Daniel Gustafsson

---

From: Daniel Gustafsson <[email-masked]>
Date: Wed, 29 Apr 2026 14:41:32 +0200

> On 28 Apr 2026, at 22:25, SATYANARAYANA NARLAPURAM <[email-masked]> wrote:

> Should we update ControlFile->data_checksum_version at the end-of-recovery? If not, the disk
> is stale compared to in memory until the next checkpoint. The code two lines below updates
> the control file anyways to set the DB_IN_PRODUCTION. Maybe combine the update with that?
> It's no big deal if we don't do it because it will be self correct but tools like pg_controldata

I've been thinking about this one and in the end went with adding it.  It is
written already with PerformRecoveryXLogAction but we might have an
inprogress-off->off transition after that which seems better to reflect in the
controlfile.

Also, an off-list question on yesterday's bugfix made me realize that it can be
simplified even further.  Instead of tracking which processes need cleanup in
the exit handler, we can simply defer installing it at all till we know that
there will be cleanup to process.

The attached also updates the commit messages to reflect the reviewers of this
patchset as well as email id's.

--
Daniel Gustafsson

---

From: Ayush Tiwari <[email-masked]>
Date: Wed, 29 Apr 2026 19:12:28 +0530

Hi,

> patchset as well as email id's.
Thanks for the updated patchset. I applied v3 on top of current master and
did
a focused review/test pass.

Reran the duplicate-launcher reproducer three times against v3 with a more
aggressive variant, could not see any issue.

The v3 launcher_exit change looks good to me. Deferring installation of the
on_shmem_exit() handler until after the process has confirmed that it owns
the
launcher role avoids the duplicate-launcher cleanup problem.

I also looked at the end-of-recovery control-file update. Copying
XLogCtl->data_checksum_version into ControlFile->data_checksum_version under
ControlFileLock before the existing UpdateControlFile() call is a good idea.

The patchset looks good.

Regards,
Ayush

---

From: Daniel Gustafsson <[email-masked]>
Date: Thu, 30 Apr 2026 17:20:39 +0200

> On 29 Apr 2026, at 15:42, Ayush Tiwari <[email-masked]> wrote:

> The patchset looks good.

Thanks for review, I've applied the patchset after some editorializing.

--
Daniel Gustafsson

---

From: SATYANARAYANA NARLAPURAM <[email-masked]>
Date: Fri, 1 May 2026 09:57:20 -0700

Hi Daniel,

> Thanks for review, I've applied the patchset after some editorializing.

While further testing this feature, I realized that
ProcessSingleRelationFork()
unconditionally called log_newpage_buffer() for every page of every
relation
during pg_enable_data_checksums(). This included unlogged relations,
which by definition never generate WAL for data changes and are reset to
their
init fork on any recovery.

Guard the log_newpage_buffer() call with RelationNeedsWAL() so that
unlogged relations still get their pages dirtied (ensuring the checksum
is flushed to disk at the next checkpoint) but do not emit WAL.

Attached a patch to address this and added a test for the same. My current
test checks if standby has main fork, I could just checked WAL to verify
this
using pg_waldump. Any other test ideas are welcome.

Thanks,
Satya

---

From: Daniel Gustafsson <[email-masked]>
Date: Fri, 1 May 2026 23:13:16 +0200

> On 1 May 2026, at 18:57, SATYANARAYANA NARLAPURAM <[email-masked]> wrote:

> While further testing this feature, I realized that ProcessSingleRelationFork() 
> unconditionally called log_newpage_buffer() for every page of every relation 
> which by definition never generate WAL for data changes and are reset to their
> test checks if standby has main fork, I could just checked WAL to verify this

Thanks for the report, I agree that this is an oversight that should be fixed.
Your patch looks good on first glance, I am travelling till Sunday evening so
will take another look when back in the office and will apply it then. Thanks!

--
Daniel Gustafsson

---

From: Tomas Vondra <[email-masked]>
Date: Mon, 4 May 2026 15:16:34 +0200

Hi,

Thanks for getting this feature pushed, and for resolving the failures
reported since the feature freeze. I consider this to be an important
improvement, not just for the feature itself, but also because of all
the useful infrastructure it added.

Attached is a refined version of the TAP tests already posted by Daniel
some time ago [1]. Unfortunately, that .txt did not apply cleanly for
some reason, so here's a better version.

I found these tests quite useful when reasoning about how the patch
behaves in concurrent environment (e.g. with multiple sessions
triggering checksum enable/disable, or with a checkpoint, crashes, etc).

At this point all the tests pass, but there are a couple cases with
correct but slightly surprising behavior, worth discussing. Which is
what this e-mail is going to be about.

I'll explain what the TAP tests aim to do first, and then discuss the
slightly surprising behavior.

It's not meant for inclusion into PG19, at least not in this shape - I
wrote those TAP tests while investigating some of the earlier failures
and/or when wondering about behavior in various situations (sequence of
concurrent steps, race conditions, ...). So it's more of an exhaustive,
and the tests are somewhat redundant (N+1 is often just (N + some small
tweak)).

I can imagine distilling it into a tiny subset, and adding that. But
that's up to discussion. But that's for later.

Let me briefly explain what the various TAP tests aim to do. From the
very beginning, my main concern regarding this patch was race conditions
when updating the shared state about effective data_checksum_version.
Because the state is effectively split into about three or four places:

* LocalDataChecksumVersion (local cache)
* XLogCtl->data_checksum_version (XLogCtl->info_lck)
* ControlFile->data_checksum_version (ControlFileLock)
* state in control file on disk

These pieces are protected by different locks, the protocol for updating
and/or reading the various flags is not trivial (and some of the fixed
issues were due to ControlFile->data_checksum_version being updated from
a place that shouldn't have).

So the primary goal of the TAP tests was to check for race conditions by
leveraging injection points to step through concurrent processes in a
deterministic way. The first couple patches (0001-0004) add debug
logging and injection points into a lot of places. And by "a lot" I mean
~80 new injection points, which is about the number of injection points
we have in master now. Anyway, this allows stepping through concurrent
checksum changes, and also checksum change vs. checkpointer.

Then come the actual TAP tests:

1) 0005-TAP-10-concurrent-checksum-changes.patch

Two concurrent checksum changes. The first one gets paused at an
injection point, then the second one gets initiated.

2) 0006-TAP-11-concurrency-with-checkpoints.patch

A checksum change + checkpoint. The change gets paused at an injection
point, a synchronous checkpoint is performed.

3) 0007-TAP-12-crashes-at-injection-points.patch

Similar to 0006, but with a crash + recovery. A checksum change gets
paused at an injection point, a synchronous checkpoint is performed. The
changes gets wpken up and either completes, or pauses on a different
injection point. A restart/crash happens.

4) 0008-TAP-13-concurrency-with-checkpoint-REDO.patch

Similar to 0007, but the checkpoint is not synchronous - happens in the
background, so that the TAP can step through both sides and interleave
them in an arbitrary way. This matters because the checksum change
updates the different state pieces (XlogCtl/ControlFile), while the
checkpointer reads them to record initial state for REDO etc.

5) 0009-TAP-14-checkpoints-with-crashes.patch

Similar to 0008, except that the steps are more fine grained, and
focused on two particular cases with surprisingly different final state.

AFAIK everything works as expected, except for two cases in the "TAP
012" test. One for the "enabling" direction, one for the "disabling"
direction. I'm going to discuss the "enabling" direction, I believe the
other case is just a mirror with the same root cause.

The TAP 012 tests checksum change with a concurrent checkpoint, followed
by a crash, and tests the final state. It pauses the change at an
injection point, does a checkpoint, proceeds to the next injection
point, crashes and does recovery. The expectation is that the final
state "flips" at some injection point, once it gets further enough, and
stays there. But what actually happens is this:

a) test_checksum_transition(
    'disabled', 'enable', undef,
    'datachecksums-enable-inprogress-checksums-end',
    'datachecksums-enable-checksums-start',
    'off');

b) test_checksum_transition(
    'disabled', 'enable', undef,
    'datachecksums-enable-checksums-start',
    'datachecksums-enable-checksums-after-xlog',
    'on');

c) test_checksum_transition(
    'disabled', 'enable', 'datachecksums-enable-checksums-start',
    'datachecksums-enable-checksums-after-xlogs',
    'datachecksums-enable-checksums-after-xlogctl',
    'off');

This says that if the checkpoint happens after
'datachecksums-enable-inprogress-checksums-end' or after
'datachecksums-enable-checksums-after-xlog', we end up with 'off' (i.e.
enabling checksums fails).

But if the checkpoint happens after
'datachecksums-enable-checksums-start', we end up with "on" (after
recovery).

This is a bit surprising, because that injection point is before
'datachecksums-enable-checksums-after-xlog'. So the enabling process
gets further and further, but the final state flips off -> on -> off,
contradicting the expectation that it changes once.

I haven't quite wrapped my head around it yet, but my understanding is
this is due to a race condition between the checksum launcher (writing
XLOG2_CHECKSUMS and updating the shmem state), and the checkpointer
(reading the shmem state and generating REDO).

The launcher does this sequence of steps:

1) write XLOG2_CHECKSUMS with new state
2) update XLogCtl->data_checksum_version
3) update ControlFile->data_checksum_version
4) UpdateControlFile()
5) emits barrier

while the checkpointer (CreateCheckPoint) does this:

A) read XLogCtl->data_checksum_version (while holding insert locks)
B) insert XLOG_CHECKPOINT_REDO (reads XLogCtl->data_checksum_version)
C) UpdateControlFile()

The outcome depends on how exactly these two sequences interleave. For
example, this can happen:

1) write XLOG2_CHECKSUMS with new state
  A) read XLogCtl->data_checksum_version (while holding insert locks)
  B) insert XLOG_CHECKPOINT_REDO (reads XLogCtl->data_checksum_version)
  C) UpdateControlFile()
2) update XLogCtl->data_checksum_version
3) update ControlFile->data_checksum_version
4) UpdateControlFile()
5) emits barrier

Which means the XLOG_CHECKPOINT_REDO will be after XLOG2_CHECKSUMS (and
so redo won't see it), but the checkpoint will still get the old
checksum state from XLogCtl. And so the outcome is "off", per case (c).

But it can also happen what case (b) does:

  A) read XLogCtl->data_checksum_version (while holding insert locks)
  B) insert XLOG_CHECKPOINT_REDO (reads XLogCtl->data_checksum_version)
  C) UpdateControlFile()
1) write XLOG2_CHECKSUMS with new state
2) update XLogCtl->data_checksum_version
3) update ControlFile->data_checksum_version
4) UpdateControlFile()
5) emits barrier

In which case the REDO will have the old state, but the recovery will
read the XLOG2_CHECKSUMS, and so end up with "on".

This is the root cause of the surprising behavior in TAP 012, I think.

I attempted to trigger these race conditions in TAP 013, but without
much success. In the end I realized it probably needs more control,
waiting for the other process to hit the next injection point before
unpausing the current one. TAP 014 does that, and it shows that with the
right interleaving of steps the (c) case can end up with both "on" and
"off" final state.

As I said, I don't claim I fully understand this yet. But I wouldn't
call this "bug" - AFAICS it won't produce an incorrect final state (I
haven't seen any such cases).

Still, I wonder if there's a potential issue I failed to notice.

The other question I had when looking at this (concurrency with
checkpoints) is what we get by doing

whenever updating the state in SetDataChecksums... functions. Because
the only thing that guarantees is the updates happen on one side of the
checkpoint record. What does that give us, actually?

It does not seem to prevent this surprising behavior, and it does not
say the XLOG2_CHECKSUMS happens before/after the XLOG_CHECKPOINT_REDO.

regards

[1]
https://www.postgresql.org/message-id/9197F930-DDEB-4CAC-82A2-16FEC715CCE8%40yesql.se

-- 
Tomas Vondra
From 696077acf842ce1de0bba320b10c18e5c22870e1 Mon Sep 17 00:00:00 2001
From: Tomas Vondra <[email-masked]>
Date: Mon, 13 Apr 2026 14:36:43 +0200
Subject: [PATCH 01/13] debug: checkpoint logging

---
 src/backend/access/transam/xlog.c | 7 +++++++
 1 file changed, 7 insertions(+)

diff --git a/src/backend/access/transam/xlog.c b/src/backend/access/transam/xlog.c
index 18d5dee06e0..94b7afeb6c7 100644
--- a/src/backend/access/transam/xlog.c
+++ b/src/backend/access/transam/xlog.c
@@ -7568,6 +7568,9 @@ CreateCheckPoint(int flags)
 	 */
 	WALInsertLockRelease();
 
+	elog(LOG, "CreateCheckPoint start redo %X/%X checksums %d",
+		 LSN_FORMAT_ARGS(RedoRecPtr), checkPoint.dataChecksumState);
+
 	/*
 	 * If this is an online checkpoint, we have not yet determined the redo
 	 * point. We do so now by inserting the special XLOG_CHECKPOINT_REDO
@@ -7811,6 +7814,10 @@ CreateCheckPoint(int flags)
 	 */
 	END_CRIT_SECTION();
 
+	elog(LOG, "CreateCheckPoint end lsn %X/%X redo %X/%X checksums %d",
+		 LSN_FORMAT_ARGS(recptr), LSN_FORMAT_ARGS(RedoRecPtr),
+		 checkPoint.dataChecksumState);
+
 	/*
 	 * WAL summaries end when the next XLOG_CHECKPOINT_REDO or
 	 * XLOG_CHECKPOINT_SHUTDOWN record is reached. This is the first point
-- 
2.54.0

From 15c2237b02cbf93558f6f8110f046acce976e0e4 Mon Sep 17 00:00:00 2001
From: Tomas Vondra <[email-masked]>
Date: Wed, 8 Apr 2026 22:07:21 +0200
Subject: [PATCH 02/13] debug: state transitions

---
 src/backend/access/transam/xlog.c | 125 ++++++++++++++++++++++++++++++
 1 file changed, 125 insertions(+)

diff --git a/src/backend/access/transam/xlog.c b/src/backend/access/transam/xlog.c
index 94b7afeb6c7..72110d92c8b 100644
--- a/src/backend/access/transam/xlog.c
+++ b/src/backend/access/transam/xlog.c
@@ -4288,12 +4288,14 @@ InitControlFile(uint64 sysidentifier, uint32 data_checksum_version)
 	ControlFile->wal_log_hints = wal_log_hints;
 	ControlFile->track_commit_timestamp = track_commit_timestamp;
 	ControlFile->data_checksum_version = data_checksum_version;
+	elog(LOG, "InitControlFile ControlFile->data_checksum_version = %u", data_checksum_version);
 
 	/*
 	 * Set the data_checksum_version value into XLogCtl, which is where all
 	 * processes get the current value from.
 	 */
 	XLogCtl->data_checksum_version = data_checksum_version;
+	elog(LOG, "InitControlFile XLogCtl->data_checksum_version = %u", data_checksum_version);
 }
 
 static void
@@ -4753,6 +4755,9 @@ void
 SetDataChecksumsOnInProgress(void)
 {
 	uint64		barrier;
+	uint32	data_checksum_version;
+
+	elog(LOG, "SetDataChecksumsOnInProgress / start");
 
 	/*
 	 * The state transition is performed in a critical section with
@@ -4764,20 +4769,32 @@ SetDataChecksumsOnInProgress(void)
 	XLogChecksums(PG_DATA_CHECKSUM_INPROGRESS_ON);
 
 	SpinLockAcquire(&XLogCtl->info_lck);
+	data_checksum_version = XLogCtl->data_checksum_version;
 	XLogCtl->data_checksum_version = PG_DATA_CHECKSUM_INPROGRESS_ON;
 	SpinLockRelease(&XLogCtl->info_lck);
 
+	elog(LOG, "SetDataChecksumsOnInProgress XLogCtl->data_checksum_version %u => %u",
+		 data_checksum_version, PG_DATA_CHECKSUM_INPROGRESS_ON);
+
 	LWLockAcquire(ControlFileLock, LW_EXCLUSIVE);
+
+	elog(LOG, "SetDataChecksumsOnInProgress ControlFile->data_checksum_version  %u => %u",
+		 ControlFile->data_checksum_version, PG_DATA_CHECKSUM_INPROGRESS_ON);
+
 	ControlFile->data_checksum_version = PG_DATA_CHECKSUM_INPROGRESS_ON;
 	UpdateControlFile();
 	LWLockRelease(ControlFileLock);
 
+	elog(LOG, "SetDataChecksumsOnInProgress / EmitProcSignalBarrier(PROCSIGNAL_BARRIER_CHECKSUM_INPROGRESS_ON)");
 	barrier = EmitProcSignalBarrier(PROCSIGNAL_BARRIER_CHECKSUM_INPROGRESS_ON);

+	elog(LOG, "SetDataChecksumsOnInProgress / WaitForProcSignalBarrier(PROCSIGNAL_BARRIER_CHECKSUM_INPROGRESS_ON)");
 	WaitForProcSignalBarrier(barrier);
+
+	elog(LOG, "SetDataChecksumsOnInProgress / end");
 }
 
 /*
@@ -4806,6 +4823,9 @@ void
 SetDataChecksumsOn(void)
 {
 	uint64		barrier;
+	uint32	data_checksum_version;
+
+	elog(LOG, "SetDataChecksumsOn / start");

@@ -4833,25 +4853,39 @@ SetDataChecksumsOn(void)
 	XLogChecksums(PG_DATA_CHECKSUM_VERSION);
 
 	SpinLockAcquire(&XLogCtl->info_lck);
+	data_checksum_version = XLogCtl->data_checksum_version;
 	XLogCtl->data_checksum_version = PG_DATA_CHECKSUM_VERSION;
 	SpinLockRelease(&XLogCtl->info_lck);
 
+	elog(LOG, "SetDataChecksumsOn / XLogCtl->data_checksum_version %u => %u",
+		 data_checksum_version, PG_DATA_CHECKSUM_VERSION);
+
 	/*
 	 * Update the controlfile before waiting since if we have an immediate
 	 * shutdown while waiting we want to come back up with checksums enabled.
 	 */
 	LWLockAcquire(ControlFileLock, LW_EXCLUSIVE);
+
+	elog(LOG, "SetDataChecksumsOn / ControlFile->data_checksum_version %u => %u",
+		 ControlFile->data_checksum_version, PG_DATA_CHECKSUM_VERSION);
+
 	ControlFile->data_checksum_version = PG_DATA_CHECKSUM_VERSION;
 	UpdateControlFile();
 	LWLockRelease(ControlFileLock);
 
+	elog(LOG, "SetDataChecksumsOn / EmitProcSignalBarrier(PG_DATA_CHECKSUM_VERSION)");
 	barrier = EmitProcSignalBarrier(PROCSIGNAL_BARRIER_CHECKSUM_ON);

+	elog(LOG, "SetDataChecksumsOn / RequestCheckpoint(CHECKPOINT_FORCE | CHECKPOINT_WAIT | CHECKPOINT_FAST)");
 	RequestCheckpoint(CHECKPOINT_FORCE | CHECKPOINT_WAIT | CHECKPOINT_FAST);
+
+	elog(LOG, "SetDataChecksumsOn / WaitForProcSignalBarrier(PG_DATA_CHECKSUM_VERSION)");
 	WaitForProcSignalBarrier(barrier);
+
+	elog(LOG, "SetDataChecksumsOn / end");
 }
 
 /*
@@ -4871,6 +4905,9 @@ void
 SetDataChecksumsOff(void)
 {
 	uint64		barrier;
+	uint32	data_checksum_version;
+
+	elog(LOG, "SetDataChecksumsOff / start");

@@ -4899,20 +4936,32 @@ SetDataChecksumsOff(void)
 		XLogChecksums(PG_DATA_CHECKSUM_INPROGRESS_OFF);
 
 		SpinLockAcquire(&XLogCtl->info_lck);
+		data_checksum_version = XLogCtl->data_checksum_version;
 		XLogCtl->data_checksum_version = PG_DATA_CHECKSUM_INPROGRESS_OFF;
 		SpinLockRelease(&XLogCtl->info_lck);
 
+		elog(LOG, "SetDataChecksumsOff / XLogCtl->data_checksum_version %u => %u",
+			 data_checksum_version, PG_DATA_CHECKSUM_INPROGRESS_OFF);
+
 		LWLockAcquire(ControlFileLock, LW_EXCLUSIVE);
+
+		elog(LOG, "SetDataChecksumsOff / ControlFile->data_checksum_version %u => %u",
+			 ControlFile->data_checksum_version, PG_DATA_CHECKSUM_INPROGRESS_OFF);
+
 		ControlFile->data_checksum_version = PG_DATA_CHECKSUM_INPROGRESS_OFF;
 		UpdateControlFile();
 		LWLockRelease(ControlFileLock);
 
+		elog(LOG, "SetDataChecksumsOff / EmitProcSignalBarrier(PG_DATA_CHECKSUM_INPROGRESS_OFF)");
 		barrier = EmitProcSignalBarrier(PROCSIGNAL_BARRIER_CHECKSUM_INPROGRESS_OFF);

+		elog(LOG, "SetDataChecksumsOff / RequestCheckpoint(CHECKPOINT_FORCE | CHECKPOINT_WAIT | CHECKPOINT_FAST)");
 		RequestCheckpoint(CHECKPOINT_FORCE | CHECKPOINT_WAIT | CHECKPOINT_FAST);
+
+		elog(LOG, "SetDataChecksumsOff / WaitForProcSignalBarrier(PG_DATA_CHECKSUM_INPROGRESS_OFF)");
 		WaitForProcSignalBarrier(barrier);
 
 		/*
@@ -4937,21 +4986,35 @@ SetDataChecksumsOff(void)
 	XLogChecksums(PG_DATA_CHECKSUM_OFF);
 
 	SpinLockAcquire(&XLogCtl->info_lck);
+	data_checksum_version = XLogCtl->data_checksum_version;
 	XLogCtl->data_checksum_version = PG_DATA_CHECKSUM_OFF;
 	SpinLockRelease(&XLogCtl->info_lck);
 
+	elog(LOG, "SetDataChecksumsOff / XLogCtl->data_checksum_version %u => %u",
+		 data_checksum_version, PG_DATA_CHECKSUM_OFF);
+
 	LWLockAcquire(ControlFileLock, LW_EXCLUSIVE);
+
+	elog(LOG, "SetDataChecksumsOff / ControlFile->data_checksum_version %u => %u",
+		 ControlFile->data_checksum_version, PG_DATA_CHECKSUM_OFF);
+
 	ControlFile->data_checksum_version = PG_DATA_CHECKSUM_OFF;
 	UpdateControlFile();
 	LWLockRelease(ControlFileLock);
 
+	elog(LOG, "SetDataChecksumsOff / EmitProcSignalBarrier(PROCSIGNAL_BARRIER_CHECKSUM_OFF)");
 	barrier = EmitProcSignalBarrier(PROCSIGNAL_BARRIER_CHECKSUM_OFF);

+	elog(LOG, "SetDataChecksumsOff / RequestCheckpoint(CHECKPOINT_FORCE | CHECKPOINT_WAIT | CHECKPOINT_FAST)");
 	RequestCheckpoint(CHECKPOINT_FORCE | CHECKPOINT_WAIT | CHECKPOINT_FAST);
+
+	elog(LOG, "SetDataChecksumsOff / WaitForProcSignalBarrier(PROCSIGNAL_BARRIER_CHECKSUM_OFF)");
 	WaitForProcSignalBarrier(barrier);
+
+	elog(LOG, "SetDataChecksumsOff / end");
 }
 
 /*
@@ -4966,9 +5029,14 @@ void
 InitLocalDataChecksumState(void)
 {
 	Assert(InterruptHoldoffCount > 0);
+
+	elog(LOG, "InitLocalDataChecksumState start %d", LocalDataChecksumState);
+
 	SpinLockAcquire(&XLogCtl->info_lck);
 	SetLocalDataChecksumState(XLogCtl->data_checksum_version);
 	SpinLockRelease(&XLogCtl->info_lck);
+
+	elog(LOG, "InitLocalDataChecksumState end %d", LocalDataChecksumState);
 }
 
 void
@@ -5278,7 +5346,9 @@ LocalProcessControlFile(bool reset)
 	LocalControlFile = palloc_object(ControlFileData);
 	ControlFile = LocalControlFile;
 	ReadControlFile();
+	elog(LOG, "LocalProcessControlFile start %d", LocalDataChecksumState);
 	SetLocalDataChecksumState(ControlFile->data_checksum_version);
+	elog(LOG, "LocalProcessControlFile end %d", LocalDataChecksumState);
 }
 
 /*
@@ -5430,9 +5500,15 @@ XLOGShmemInit(void *arg)
 	XLogCtl->InstallXLogFileSegmentActive = false;
 	XLogCtl->WalWriterSleeping = false;
 
+	elog(LOG, "XLOGShmemInit / XLogCtl->data_checksum_version %u => %u",
+		 XLogCtl->data_checksum_version, ControlFile->data_checksum_version);
+
 	/* Use the checksum info from control file */
 	XLogCtl->data_checksum_version = ControlFile->data_checksum_version;
+
+	elog(LOG, "XLOGShmemInit start %d", LocalDataChecksumState);
 	SetLocalDataChecksumState(XLogCtl->data_checksum_version);
+	elog(LOG, "XLOGShmemInit end %d", LocalDataChecksumState);
 
 	SpinLockInit(&XLogCtl->Insert.insertpos_lck);
 	SpinLockInit(&XLogCtl->info_lck);
@@ -6607,13 +6683,22 @@ StartupXLOG(void)
 	 */
 	if (XLogCtl->data_checksum_version == PG_DATA_CHECKSUM_INPROGRESS_ON)
 	{
+		uint32	data_checksum_version;
 		XLogChecksums(PG_DATA_CHECKSUM_OFF);
 
+		elog(LOG, "StartupXLOG start %d", LocalDataChecksumState);
+
 		SpinLockAcquire(&XLogCtl->info_lck);
+		data_checksum_version = XLogCtl->data_checksum_version;
 		XLogCtl->data_checksum_version = PG_DATA_CHECKSUM_OFF;
 		SetLocalDataChecksumState(XLogCtl->data_checksum_version);
 		SpinLockRelease(&XLogCtl->info_lck);
 
+		elog(LOG, "StartupXLOG XLogCtl->data_checksum_version %u => %u",
+			 data_checksum_version, PG_DATA_CHECKSUM_OFF);
+
+		elog(LOG, "StartupXLOG end %d", LocalDataChecksumState);
+
 		ereport(WARNING,
 				errmsg("enabling data checksums was interrupted"),
 				errhint("Data checksum processing must be manually restarted for checksums to be enabled"));
@@ -6627,12 +6712,21 @@ StartupXLOG(void)
 	 */
 	if (XLogCtl->data_checksum_version == PG_DATA_CHECKSUM_INPROGRESS_OFF)
 	{
+		uint32		data_checksum_version;
 		XLogChecksums(PG_DATA_CHECKSUM_OFF);
 
+		elog(LOG, "StartupXLOG start %d", LocalDataChecksumState);
+
 		SpinLockAcquire(&XLogCtl->info_lck);
+		data_checksum_version = XLogCtl->data_checksum_version;
 		XLogCtl->data_checksum_version = PG_DATA_CHECKSUM_OFF;
 		SetLocalDataChecksumState(XLogCtl->data_checksum_version);
 		SpinLockRelease(&XLogCtl->info_lck);
+
+		elog(LOG, "StartupXLOG XLogCtl->data_checksum_version %u => %u",
+			 data_checksum_version, PG_DATA_CHECKSUM_OFF);
+
+		elog(LOG, "StartupXLOG end %d", LocalDataChecksumState);
 	}
 
 	/*
@@ -7917,6 +8011,8 @@ CreateEndOfRecoveryRecord(void)
 {
 	xl_end_of_recovery xlrec;
 	XLogRecPtr	recptr;
+	uint32		old_data_checksum_version;
+	uint32		new_data_checksum_version;
 
 	/* sanity check */
 	if (!RecoveryInProgress())
@@ -7948,9 +8044,14 @@ CreateEndOfRecoveryRecord(void)
 
 	/* start with the latest checksum version (as of the end of recovery) */
 	SpinLockAcquire(&XLogCtl->info_lck);
+	old_data_checksum_version = ControlFile->data_checksum_version;
+	new_data_checksum_version = XLogCtl->data_checksum_version;
 	ControlFile->data_checksum_version = XLogCtl->data_checksum_version;
 	SpinLockRelease(&XLogCtl->info_lck);
 
+	elog(LOG, "CreateEndOfRecoveryRecord ControlFile->data_checksum_version %u => %u",
+		 old_data_checksum_version, new_data_checksum_version);
+
 	UpdateControlFile();
 	LWLockRelease(ControlFileLock);
 
@@ -8293,6 +8394,9 @@ CreateRestartPoint(int flags)
 				ControlFile->state = DB_SHUTDOWNED_IN_RECOVERY;
 		}
 
+		elog(LOG, "CreateRestartPoint ControlFile->data_checksum_version %u => %u",
+			 ControlFile->data_checksum_version, lastCheckPoint.dataChecksumState);
+
 		/* we shall start with the latest checksum version */
 		ControlFile->data_checksum_version = lastCheckPoint.dataChecksumState;
 
@@ -8750,6 +8854,8 @@ XLogChecksums(uint32 new_type)
 
 	recptr = XLogInsert(RM_XLOG2_ID, XLOG2_CHECKSUMS);
 	XLogFlush(recptr);
+
+	elog(LOG, "XLogChecksums %d %X/%X", new_type, LSN_FORMAT_ARGS(recptr));
 }
 
 /*
@@ -8862,6 +8968,7 @@ xlog_redo(XLogReaderState *record)
 	{
 		CheckPoint	checkPoint;
 		TimeLineID	replayTLI;
+		uint32		data_checksum_version;
 
 		memcpy(&checkPoint, XLogRecGetData(record), sizeof(CheckPoint));
 		/* In a SHUTDOWN checkpoint, believe the counters exactly */
@@ -8936,6 +9043,10 @@ xlog_redo(XLogReaderState *record)
 
 		/* ControlFile->checkPointCopy always tracks the latest ckpt XID */
 		LWLockAcquire(ControlFileLock, LW_EXCLUSIVE);
+
+		elog(LOG, "xlog_redo / ControlFile->data_checksum_version %u => %u",
+			 ControlFile->data_checksum_version, checkPoint.dataChecksumState);
+
 		ControlFile->checkPointCopy.nextXid = checkPoint.nextXid;
 		ControlFile->data_checksum_version = checkPoint.dataChecksumState;
 
@@ -9178,16 +9289,21 @@ xlog_redo(XLogReaderState *record)
 	{
 		xl_checkpoint_redo redo_rec;
 		bool		new_state = false;
+		uint32		data_checksum_version;
 
 		memcpy(&redo_rec, XLogRecGetData(record), sizeof(xl_checkpoint_redo));
 
 		SpinLockAcquire(&XLogCtl->info_lck);
+		data_checksum_version = XLogCtl->data_checksum_version;
 		XLogCtl->data_checksum_version = redo_rec.data_checksum_version;
 		SetLocalDataChecksumState(redo_rec.data_checksum_version);
 		if (redo_rec.data_checksum_version != ControlFile->data_checksum_version)
 			new_state = true;
 		SpinLockRelease(&XLogCtl->info_lck);
 
+		elog(LOG, "xlog_redo / XLogCtl->data_checksum_version %u => %u",
+			 data_checksum_version, redo_rec.data_checksum_version);
+
 		if (new_state)
 			EmitAndWaitDataChecksumsBarrier(redo_rec.data_checksum_version);
 	}
@@ -9250,18 +9366,27 @@ xlog2_redo(XLogReaderState *record)
 	if (info == XLOG2_CHECKSUMS)
 	{
 		xl_checksum_state state;
+		uint32	data_checksum_version;
 
 		memcpy(&state, XLogRecGetData(record), sizeof(xl_checksum_state));
 
 		SpinLockAcquire(&XLogCtl->info_lck);
+		data_checksum_version = XLogCtl->data_checksum_version;
 		XLogCtl->data_checksum_version = state.new_checksum_state;
 		SpinLockRelease(&XLogCtl->info_lck);
 
+		elog(LOG, "xlog2_redo / XLogCtl->data_checksum_version %u => %u",
+			 data_checksum_version, state.new_checksum_state);
+
 		LWLockAcquire(ControlFileLock, LW_EXCLUSIVE);
+		data_checksum_version = ControlFile->data_checksum_version;
 		ControlFile->data_checksum_version = state.new_checksum_state;
 		UpdateControlFile();
 		LWLockRelease(ControlFileLock);
 
+		elog(LOG, "xlog2_redo / ControlFile->data_checksum_version %u => %u",
+			 data_checksum_version, state.new_checksum_state);
+
 		/*
 		 * Block on a procsignalbarrier to await all processes having seen the
 		 * change to checksum status. Once the barrier has been passed we can
-- 
2.54.0

From 1e58b18a70502339c5608ba352f6e351f6114359 Mon Sep 17 00:00:00 2001
From: test <test>
Date: Fri, 1 May 2026 22:14:18 +0200
Subject: [PATCH 03/13] debug: checksums injection points

---
 src/backend/access/transam/xlog.c | 74 ++++++++++++++++++++++++++++++-
 1 file changed, 72 insertions(+), 2 deletions(-)

diff --git a/src/backend/access/transam/xlog.c b/src/backend/access/transam/xlog.c
index 72110d92c8b..8dd003d25e2 100644
--- a/src/backend/access/transam/xlog.c
+++ b/src/backend/access/transam/xlog.c
@@ -4759,6 +4759,13 @@ SetDataChecksumsOnInProgress(void)

+	INJECTION_POINT("datachecksums-enable-inprogress-checksums-start", NULL);
+
+	/* load before critical section */
+	INJECTION_POINT_LOAD("datachecksums-enable-inprogress-checksums-after-xlog");
+	INJECTION_POINT_LOAD("datachecksums-enable-inprogress-checksums-after-xlogctl");
+	INJECTION_POINT_LOAD("datachecksums-enable-inprogress-checksums-after-controlfile");
+
 	/*
 	 * The state transition is performed in a critical section with
 	 * checkpoints held off to provide crash safety.
@@ -4768,6 +4775,8 @@ SetDataChecksumsOnInProgress(void)
 
 	XLogChecksums(PG_DATA_CHECKSUM_INPROGRESS_ON);
 
+	INJECTION_POINT_CACHED("datachecksums-enable-inprogress-checksums-after-xlog", NULL);
+
 	SpinLockAcquire(&XLogCtl->info_lck);
 	data_checksum_version = XLogCtl->data_checksum_version;
 	XLogCtl->data_checksum_version = PG_DATA_CHECKSUM_INPROGRESS_ON;
@@ -4776,6 +4785,8 @@ SetDataChecksumsOnInProgress(void)
 	elog(LOG, "SetDataChecksumsOnInProgress XLogCtl->data_checksum_version %u => %u",
 		 data_checksum_version, PG_DATA_CHECKSUM_INPROGRESS_ON);
 
+	INJECTION_POINT_CACHED("datachecksums-enable-inprogress-checksums-after-xlogctl", NULL);
+
 	LWLockAcquire(ControlFileLock, LW_EXCLUSIVE);
 
 	elog(LOG, "SetDataChecksumsOnInProgress ControlFile->data_checksum_version  %u => %u",
@@ -4785,15 +4796,21 @@ SetDataChecksumsOnInProgress(void)
 	UpdateControlFile();
 	LWLockRelease(ControlFileLock);
 
+	INJECTION_POINT_CACHED("datachecksums-enable-inprogress-checksums-after-controlfile", NULL);
+
 	elog(LOG, "SetDataChecksumsOnInProgress / EmitProcSignalBarrier(PROCSIGNAL_BARRIER_CHECKSUM_INPROGRESS_ON)");
 	barrier = EmitProcSignalBarrier(PROCSIGNAL_BARRIER_CHECKSUM_INPROGRESS_ON);

+	INJECTION_POINT("datachecksums-enable-inprogress-checksums-before-barrier-wait", NULL);
+
 	elog(LOG, "SetDataChecksumsOnInProgress / WaitForProcSignalBarrier(PROCSIGNAL_BARRIER_CHECKSUM_INPROGRESS_ON)");
 	WaitForProcSignalBarrier(barrier);
 
+	INJECTION_POINT("datachecksums-enable-inprogress-checksums-end", NULL);
+
 	elog(LOG, "SetDataChecksumsOnInProgress / end");
 }
 
@@ -4846,12 +4863,20 @@ SetDataChecksumsOn(void)

-	INJECTION_POINT("datachecksums-enable-checksums-delay", NULL);
+	INJECTION_POINT("datachecksums-enable-checksums-start", NULL);
+
+	/* load before critical section */
+	INJECTION_POINT_LOAD("datachecksums-enable-checksums-after-xlog");
+	INJECTION_POINT_LOAD("datachecksums-enable-checksums-after-xlogctl");
+	INJECTION_POINT_LOAD("datachecksums-enable-checksums-after-controlfile");
+
 	START_CRIT_SECTION();
 	MyProc->delayChkptFlags |= DELAY_CHKPT_START;
 
 	XLogChecksums(PG_DATA_CHECKSUM_VERSION);
 
+	INJECTION_POINT_CACHED("datachecksums-enable-checksums-after-xlog", NULL);
+
 	SpinLockAcquire(&XLogCtl->info_lck);
 	data_checksum_version = XLogCtl->data_checksum_version;
 	XLogCtl->data_checksum_version = PG_DATA_CHECKSUM_VERSION;
@@ -4860,6 +4885,8 @@ SetDataChecksumsOn(void)
 	elog(LOG, "SetDataChecksumsOn / XLogCtl->data_checksum_version %u => %u",
 		 data_checksum_version, PG_DATA_CHECKSUM_VERSION);
 
+	INJECTION_POINT_CACHED("datachecksums-enable-checksums-after-xlogctl", NULL);
+
 	/*
 	 * Update the controlfile before waiting since if we have an immediate
 	 * shutdown while waiting we want to come back up with checksums enabled.
@@ -4873,18 +4900,26 @@ SetDataChecksumsOn(void)
 	UpdateControlFile();
 	LWLockRelease(ControlFileLock);
 
+	INJECTION_POINT_CACHED("datachecksums-enable-checksums-after-controlfile", NULL);
+
 	elog(LOG, "SetDataChecksumsOn / EmitProcSignalBarrier(PG_DATA_CHECKSUM_VERSION)");
 	barrier = EmitProcSignalBarrier(PROCSIGNAL_BARRIER_CHECKSUM_ON);

+	INJECTION_POINT("datachecksums-enable-checksums-before-checkpoint", NULL);
+
 	elog(LOG, "SetDataChecksumsOn / RequestCheckpoint(CHECKPOINT_FORCE | CHECKPOINT_WAIT | CHECKPOINT_FAST)");
 	RequestCheckpoint(CHECKPOINT_FORCE | CHECKPOINT_WAIT | CHECKPOINT_FAST);
 
+	INJECTION_POINT("datachecksums-enable-checksums-before-barrier-wait", NULL);
+
 	elog(LOG, "SetDataChecksumsOn / WaitForProcSignalBarrier(PG_DATA_CHECKSUM_VERSION)");
 	WaitForProcSignalBarrier(barrier);
 
+	INJECTION_POINT("datachecksums-enable-checksums-end", NULL);
+
 	elog(LOG, "SetDataChecksumsOn / end");
 }
 
@@ -4930,11 +4965,20 @@ SetDataChecksumsOff(void)
 	{
 		SpinLockRelease(&XLogCtl->info_lck);
 
+		INJECTION_POINT("datachecksums-disable-inprogress-checksums-start", NULL);
+
+		/* load before critical section */
+		INJECTION_POINT_LOAD("datachecksums-disable-inprogress-checksums-after-xlog");
+		INJECTION_POINT_LOAD("datachecksums-disable-inprogress-checksums-after-xlogctl");
+		INJECTION_POINT_LOAD("datachecksums-disable-inprogress-checksums-after-controlfile");
+
 		START_CRIT_SECTION();
 		MyProc->delayChkptFlags |= DELAY_CHKPT_START;
 
 		XLogChecksums(PG_DATA_CHECKSUM_INPROGRESS_OFF);
 
+		INJECTION_POINT_CACHED("datachecksums-disable-inprogress-checksums-after-xlog", NULL);
+
 		SpinLockAcquire(&XLogCtl->info_lck);
 		data_checksum_version = XLogCtl->data_checksum_version;
 		XLogCtl->data_checksum_version = PG_DATA_CHECKSUM_INPROGRESS_OFF;
@@ -4943,6 +4987,8 @@ SetDataChecksumsOff(void)
 		elog(LOG, "SetDataChecksumsOff / XLogCtl->data_checksum_version %u => %u",
 			 data_checksum_version, PG_DATA_CHECKSUM_INPROGRESS_OFF);
 
+		INJECTION_POINT_CACHED("datachecksums-disable-inprogress-checksums-after-xlogctl", NULL);
+
 		LWLockAcquire(ControlFileLock, LW_EXCLUSIVE);
 
 		elog(LOG, "SetDataChecksumsOff / ControlFile->data_checksum_version %u => %u",
@@ -4952,15 +4998,21 @@ SetDataChecksumsOff(void)
 		UpdateControlFile();
 		LWLockRelease(ControlFileLock);
 
+		INJECTION_POINT_CACHED("datachecksums-disable-inprogress-checksums-after-controlfile", NULL);
+
 		elog(LOG, "SetDataChecksumsOff / EmitProcSignalBarrier(PG_DATA_CHECKSUM_INPROGRESS_OFF)");
 		barrier = EmitProcSignalBarrier(PROCSIGNAL_BARRIER_CHECKSUM_INPROGRESS_OFF);

+		INJECTION_POINT("datachecksums-disable-inprogress-checksums-before-checkpoint", NULL);
+
 		elog(LOG, "SetDataChecksumsOff / RequestCheckpoint(CHECKPOINT_FORCE | CHECKPOINT_WAIT | CHECKPOINT_FAST)");
 		RequestCheckpoint(CHECKPOINT_FORCE | CHECKPOINT_WAIT | CHECKPOINT_FAST);
 
+		INJECTION_POINT("datachecksums-disable-inprogress-checksums-before-barrier-wait", NULL);
+
 		elog(LOG, "SetDataChecksumsOff / WaitForProcSignalBarrier(PG_DATA_CHECKSUM_INPROGRESS_OFF)");
 		WaitForProcSignalBarrier(barrier);
 
@@ -4979,12 +5031,21 @@ SetDataChecksumsOff(void)
 		SpinLockRelease(&XLogCtl->info_lck);
 	}
 
+	INJECTION_POINT("datachecksums-disable-checksums-start", NULL);
+
+	/* load before critical section */
+	INJECTION_POINT_LOAD("datachecksums-disable-checksums-after-xlog");
+	INJECTION_POINT_LOAD("datachecksums-disable-checksums-after-xlogctl");
+	INJECTION_POINT_LOAD("datachecksums-disable-checksums-after-controlfile");
+
 	START_CRIT_SECTION();
 	/* Ensure that we don't incur a checkpoint during disabling checksums */
 	MyProc->delayChkptFlags |= DELAY_CHKPT_START;
 
 	XLogChecksums(PG_DATA_CHECKSUM_OFF);
 
+	INJECTION_POINT_CACHED("datachecksums-disable-checksums-after-xlog", NULL);
+
 	SpinLockAcquire(&XLogCtl->info_lck);
 	data_checksum_version = XLogCtl->data_checksum_version;
 	XLogCtl->data_checksum_version = PG_DATA_CHECKSUM_OFF;
@@ -4993,6 +5054,8 @@ SetDataChecksumsOff(void)
 	elog(LOG, "SetDataChecksumsOff / XLogCtl->data_checksum_version %u => %u",
 		 data_checksum_version, PG_DATA_CHECKSUM_OFF);
 
+	INJECTION_POINT_CACHED("datachecksums-disable-checksums-after-xlogctl", NULL);
+
 	LWLockAcquire(ControlFileLock, LW_EXCLUSIVE);
 
 	elog(LOG, "SetDataChecksumsOff / ControlFile->data_checksum_version %u => %u",
@@ -5002,18 +5065,26 @@ SetDataChecksumsOff(void)
 	UpdateControlFile();
 	LWLockRelease(ControlFileLock);
 
+	INJECTION_POINT_CACHED("datachecksums-disable-checksums-after-controlfile", NULL);
+
 	elog(LOG, "SetDataChecksumsOff / EmitProcSignalBarrier(PROCSIGNAL_BARRIER_CHECKSUM_OFF)");
 	barrier = EmitProcSignalBarrier(PROCSIGNAL_BARRIER_CHECKSUM_OFF);

+	INJECTION_POINT("datachecksums-disable-checksums-before-checkpoint", NULL);
+
 	elog(LOG, "SetDataChecksumsOff / RequestCheckpoint(CHECKPOINT_FORCE | CHECKPOINT_WAIT | CHECKPOINT_FAST)");
 	RequestCheckpoint(CHECKPOINT_FORCE | CHECKPOINT_WAIT | CHECKPOINT_FAST);
 
+	INJECTION_POINT("datachecksums-disable-checksums-before-barrier-wait", NULL);
+
 	elog(LOG, "SetDataChecksumsOff / WaitForProcSignalBarrier(PROCSIGNAL_BARRIER_CHECKSUM_OFF)");
 	WaitForProcSignalBarrier(barrier);
 
+	INJECTION_POINT("datachecksums-disable-checksums-end", NULL);
+
 	elog(LOG, "SetDataChecksumsOff / end");
 }
 
@@ -8968,7 +9039,6 @@ xlog_redo(XLogReaderState *record)
 	{
 		CheckPoint	checkPoint;
 		TimeLineID	replayTLI;
-		uint32		data_checksum_version;
 
 		memcpy(&checkPoint, XLogRecGetData(record), sizeof(CheckPoint));
 		/* In a SHUTDOWN checkpoint, believe the counters exactly */
-- 
2.54.0

From 0c3dd8ac1df7fd1686374555eb766e4517c4c1a8 Mon Sep 17 00:00:00 2001
From: test <test>
Date: Fri, 1 May 2026 22:15:52 +0200
Subject: [PATCH 04/13] debug: checkpointer injection points

---
 src/backend/access/transam/xlog.c | 30 ++++++++++++++++++++++++++++++
 1 file changed, 30 insertions(+)

diff --git a/src/backend/access/transam/xlog.c b/src/backend/access/transam/xlog.c
index 8dd003d25e2..7c4e412093c 100644
--- a/src/backend/access/transam/xlog.c
+++ b/src/backend/access/transam/xlog.c
@@ -4762,6 +4762,7 @@ SetDataChecksumsOnInProgress(void)
 	INJECTION_POINT("datachecksums-enable-inprogress-checksums-start", NULL);
 
 	/* load before critical section */
+	INJECTION_POINT_LOAD("datachecksums-enable-inprogress-checksums-before-xlog");
 	INJECTION_POINT_LOAD("datachecksums-enable-inprogress-checksums-after-xlog");
 	INJECTION_POINT_LOAD("datachecksums-enable-inprogress-checksums-after-xlogctl");
 	INJECTION_POINT_LOAD("datachecksums-enable-inprogress-checksums-after-controlfile");
@@ -4773,6 +4774,8 @@ SetDataChecksumsOnInProgress(void)
 	START_CRIT_SECTION();
 	MyProc->delayChkptFlags |= DELAY_CHKPT_START;
 
+	INJECTION_POINT_CACHED("datachecksums-enable-inprogress-checksums-before-xlog", NULL);
+
 	XLogChecksums(PG_DATA_CHECKSUM_INPROGRESS_ON);
 
 	INJECTION_POINT_CACHED("datachecksums-enable-inprogress-checksums-after-xlog", NULL);
@@ -4866,6 +4869,7 @@ SetDataChecksumsOn(void)
 	INJECTION_POINT("datachecksums-enable-checksums-start", NULL);
 
 	/* load before critical section */
+	INJECTION_POINT_LOAD("datachecksums-enable-checksums-before-xlog");
 	INJECTION_POINT_LOAD("datachecksums-enable-checksums-after-xlog");
 	INJECTION_POINT_LOAD("datachecksums-enable-checksums-after-xlogctl");
 	INJECTION_POINT_LOAD("datachecksums-enable-checksums-after-controlfile");
@@ -4873,6 +4877,8 @@ SetDataChecksumsOn(void)
 	START_CRIT_SECTION();
 	MyProc->delayChkptFlags |= DELAY_CHKPT_START;
 
+	INJECTION_POINT_CACHED("datachecksums-enable-checksums-before-xlog", NULL);
+
 	XLogChecksums(PG_DATA_CHECKSUM_VERSION);
 
 	INJECTION_POINT_CACHED("datachecksums-enable-checksums-after-xlog", NULL);
@@ -4968,6 +4974,7 @@ SetDataChecksumsOff(void)
 		INJECTION_POINT("datachecksums-disable-inprogress-checksums-start", NULL);
 
 		/* load before critical section */
+		INJECTION_POINT_LOAD("datachecksums-disable-inprogress-checksums-before-xlog");
 		INJECTION_POINT_LOAD("datachecksums-disable-inprogress-checksums-after-xlog");
 		INJECTION_POINT_LOAD("datachecksums-disable-inprogress-checksums-after-xlogctl");
 		INJECTION_POINT_LOAD("datachecksums-disable-inprogress-checksums-after-controlfile");
@@ -4975,6 +4982,8 @@ SetDataChecksumsOff(void)
 		START_CRIT_SECTION();
 		MyProc->delayChkptFlags |= DELAY_CHKPT_START;
 
+		INJECTION_POINT_CACHED("datachecksums-disable-inprogress-checksums-before-xlog", NULL);
+
 		XLogChecksums(PG_DATA_CHECKSUM_INPROGRESS_OFF);
 
 		INJECTION_POINT_CACHED("datachecksums-disable-inprogress-checksums-after-xlog", NULL);
@@ -5034,6 +5043,7 @@ SetDataChecksumsOff(void)
 	INJECTION_POINT("datachecksums-disable-checksums-start", NULL);
 
 	/* load before critical section */
+	INJECTION_POINT_LOAD("datachecksums-disable-checksums-before-xlog");
 	INJECTION_POINT_LOAD("datachecksums-disable-checksums-after-xlog");
 	INJECTION_POINT_LOAD("datachecksums-disable-checksums-after-xlogctl");
 	INJECTION_POINT_LOAD("datachecksums-disable-checksums-after-controlfile");
@@ -5042,6 +5052,8 @@ SetDataChecksumsOff(void)
 	/* Ensure that we don't incur a checkpoint during disabling checksums */
 	MyProc->delayChkptFlags |= DELAY_CHKPT_START;
 
+	INJECTION_POINT_CACHED("datachecksums-disable-checksums-before-xlog", NULL);
+
 	XLogChecksums(PG_DATA_CHECKSUM_OFF);
 
 	INJECTION_POINT_CACHED("datachecksums-disable-checksums-after-xlog", NULL);
@@ -7611,6 +7623,12 @@ CreateCheckPoint(int flags)
 	/* Run these points outside the critical section. */
 	INJECTION_POINT("create-checkpoint-initial", NULL);
 	INJECTION_POINT_LOAD("create-checkpoint-run");
+	INJECTION_POINT_LOAD("checkpoint-before-redo");
+	INJECTION_POINT_LOAD("checkpoint-before-xlogctl-checksums");
+	INJECTION_POINT_LOAD("checkpoint-after-xlogctl-checksums");
+	INJECTION_POINT_LOAD("checkpoint-before-redo-checksums");
+	INJECTION_POINT_LOAD("checkpoint-before-redo-wal");
+	INJECTION_POINT_LOAD("checkpoint-after-redo-wal");
 
 	/*
 	 * Use a critical section to force system panic if we have trouble.
@@ -7676,6 +7694,8 @@ CreateCheckPoint(int flags)
 	else
 		checkPoint.PrevTimeLineID = checkPoint.ThisTimeLineID;
 
+	INJECTION_POINT_CACHED("checkpoint-before-redo", NULL);
+
 	/*
 	 * We must block concurrent insertions while examining insert state.
 	 */
@@ -7684,6 +7704,8 @@ CreateCheckPoint(int flags)
 	checkPoint.fullPageWrites = Insert->fullPageWrites;
 	checkPoint.wal_level = wal_level;
 
+	INJECTION_POINT_CACHED("checkpoint-before-xlogctl-checksums", NULL);
+
 	/*
 	 * Get the current data_checksum_version value from xlogctl, valid at the
 	 * time of the checkpoint.
@@ -7692,6 +7714,8 @@ CreateCheckPoint(int flags)
 	checkPoint.dataChecksumState = XLogCtl->data_checksum_version;
 	SpinLockRelease(&XLogCtl->info_lck);
 
+	INJECTION_POINT_CACHED("checkpoint-after-xlogctl-checksums", NULL);
+
 	if (shutdown)
 	{
 		XLogRecPtr	curInsert = XLogBytePosToRecPtr(Insert->CurrBytePos);
@@ -7749,6 +7773,8 @@ CreateCheckPoint(int flags)
 	{
 		xl_checkpoint_redo redo_rec;
 
+		INJECTION_POINT_CACHED("checkpoint-before-redo-checksums", NULL);
+
 		WALInsertLockAcquire();
 		redo_rec.wal_level = wal_level;
 		SpinLockAcquire(&XLogCtl->info_lck);
@@ -7756,6 +7782,8 @@ CreateCheckPoint(int flags)
 		SpinLockRelease(&XLogCtl->info_lck);
 		WALInsertLockRelease();
 
+		INJECTION_POINT_CACHED("checkpoint-before-redo-wal", NULL);
+
 		/* Include WAL level in record for WAL summarizer's benefit. */
 		XLogBeginInsert();
 		XLogRegisterData(&redo_rec, sizeof(xl_checkpoint_redo));
@@ -7768,6 +7796,8 @@ CreateCheckPoint(int flags)
 		 * checkpoint is complete.
 		 */
 		checkPoint.redo = RedoRecPtr;
+
+		INJECTION_POINT_CACHED("checkpoint-after-redo-wal", NULL);
 	}
 
 	/* Update the info_lck-protected copy of RedoRecPtr as well */
-- 
2.54.0

From 24ab81ae666ed9de683091d6feecbf80b2dd5bb1 Mon Sep 17 00:00:00 2001
From: test <test>
Date: Fri, 1 May 2026 22:14:49 +0200
Subject: [PATCH 05/13] TAP 10: concurrent checksum changes

---
 .../t/010_concurrent_changes.pl               | 296 ++++++++++++++++++
 1 file changed, 296 insertions(+)
 create mode 100644 src/test/modules/test_checksums/t/010_concurrent_changes.pl

diff --git a/src/test/modules/test_checksums/t/010_concurrent_changes.pl b/src/test/modules/test_checksums/t/010_concurrent_changes.pl
new file mode 100644
index 00000000000..1b17b496033
--- /dev/null
+++ b/src/test/modules/test_checksums/t/010_concurrent_changes.pl
@@ -0,0 +1,296 @@
+
+# Copyright (c) 2026, PostgreSQL Global Development Group
+
+# Test suite for testing enabling data checksums in an online cluster with
+# injection point tests injecting failures into the processing
+
+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, or very expensive, to execute.  There are two
+# PG_TEST_EXTRA options for running it, "checksum" for a pared-down test suite
+# an "checksum_extended" for the full suite.  The full suite can run for hours
+# on slow or constrained systems.
+my $extended = undef;
+if ($ENV{PG_TEST_EXTRA})
+{
+	$extended = 1 if ($ENV{PG_TEST_EXTRA} =~ /\bchecksum_extended\b/);
+	plan skip_all => 'Expensive data checksums test disabled'
+	  unless ($ENV{PG_TEST_EXTRA} =~ /\bchecksum(_extended)?\b/);
+}
+
+if ($ENV{enable_injection_points} ne 'yes')
+{
+	plan skip_all => 'Injection points not supported by this build';
+}
+
+# ---------------------------------------------------------------------------
+# Test cluster setup
+#
+
+# Initiate testcluster
+my $node = PostgreSQL::Test::Cluster->new('injection_node');
+$node->init(no_data_checksums => 1);
+$node->start;
+
+# Set up test environment
+$node->safe_psql('postgres', 'CREATE EXTENSION test_checksums;');
+$node->safe_psql('postgres', 'CREATE EXTENSION injection_points;');
+
+my $pgbench = undef;
+my $scalefactor = ($extended ? 10 : 1);
+my $node_loglocation = 0;
+
+$node->command_ok(
+	[
+		'pgbench', '-p', $node->port, '-i',
+		'-s', $scalefactor, '-q', 'postgres'
+	]);
+
+# Start a pgbench run in the background against the server specified via the
+# port passed as parameter.
+sub background_rw_pgbench
+{
+	my $port = shift;
+
+	# If a previous pgbench is still running, start by shutting it down.
+	$pgbench->finish if $pgbench;
+
+	my $clients = 1;
+	my $runtime = 2;
+
+	if ($extended)
+	{
+		# Randomize the number of pgbench clients a bit (range 1-16)
+		$clients = 1 + int(rand(15));
+		$runtime = 600;
+	}
+	my @cmd = ('pgbench', '-p', $port, '-T', $runtime, '-c', $clients);
+
+	# Randomize whether we spawn connections or not
+	push(@cmd, '-C') if ($extended && cointoss);
+	# Finally add the database name to use
+	push(@cmd, 'postgres');
+
+	$pgbench = IPC::Run::start(
+		\@cmd,
+		'<' => '/dev/null',
+		'>' => '/dev/null',
+		'2>' => '/dev/null',
+		IPC::Run::timer($PostgreSQL::Test::Utils::timeout_default));
+}
+
+sub attach_injection_point
+{
+	my ($node, $point) = @_;
+	note('attaching injection point: ' . $point);
+	$node->safe_psql('postgres',
+		"SELECT injection_points_attach('" . $point . "','wait');"
+	);
+}
+
+sub wait_injection_point
+{
+	my ($node, $point) = @_;
+
+	note("waiting for the injection point to be hit");
+	$node->poll_query_until(
+		'postgres',
+		"SELECT COUNT(*) FROM pg_catalog.pg_stat_activity WHERE wait_event = '" . $point . "'",
+		'1');
+}
+
+sub wakeup_injection_point
+{
+	my ($node, $point) = @_;
+
+	note('waking the init injection point: ' . $point);
+	$node->safe_psql('postgres',
+		"SELECT injection_points_wakeup('" . $point . "');"
+	);
+
+	note("detaching the init injection point");
+	$node->safe_psql('postgres',
+		"SELECT injection_points_detach('" . $point . "');");
+}
+
+# Test behavior with two concurrent checksum transitions.
+#
+# The cluster is initialized into checksum state $start (enabled/disabled),
+# and two changes are initiated in a controled way. The $first change gets
+# paused at a selected injection point $point, at which point $second change
+# gets initiated. Once the checksum resolves the cluster state, the state
+# is compared to the expected $final state.
+#
+# arguments:
+#
+# - start  - starting checksum state (enabled/disabled)
+# - first  - first checksum change
+# - second - second checksum change
+# - init   - initial injection point (see comment later)
+# - point  - injection point the first change should wait on
+# - final  - expected checksum state at the end
+#
+# The injection point is triggered only by the datachecksum launcher, and
+# there can be only one such process. So there's no risk of hitting the
+# injection point by both changes.
+#
+# XXX Some of the injection points are in a critical section, which does
+# not allow memory allocations etc. INJECTION_POINT_LOAD/_CACHED handles
+# just private memory allocation, but 'wait' requires a shmem allocation.
+# To deal with that, we setup an injection point $init outside a critical
+# section to initialize the shmem stuff needed by 'wait'.
+#
+# XXX The first change gets "unpaused" after the second checksum change
+# gets initiated, but maybe that's not a sufficiently deterministic?
+#
+# XXX Should we detach the injection point $point before initiating the
+# second change? I'm not sure if there's a small race condition because
+# of waking up and detaching the injection point.
+sub test_checksum_transition
+{
+	my ($start, $first, $second, $init, $point, $final) = @_;
+
+	# print the current test instructions, both into TAP output and into
+	# the server log, to make correlation easier
+
+	my $initstr = 'undef';
+	$initstr = $init if defined($init);
+
+	$node->safe_psql('postgres',
+		"SELECT '========== " . $start . " / " . $first . " / " . $second . " / " . $initstr . " / " . $point . " / " . $final . " =========='");
+
+	note($start . " / " . $first . " / " . $second . " / " . $initstr . " / " . $point . " / " . $final);
+
+	# put the cluster into the initial checksum state, synchronously
+	note('changing checksums into initial state: ' . $start);
+	enable_data_checksums($node, wait => 'on') if ($start eq 'enabled');
+	disable_data_checksums($node, wait => 'off') if ($start eq 'disabled');
+
+	 # Wait on an injection point outside a critical section, to initialize
+	 # the shmem (which can't be done in critical section).
+	attach_injection_point($node, $init) if defined($init);
+
+	# Wait on the injection point, to pause the first checksum change.
+	note('attaching injection point: ' . $point);
+	$node->safe_psql('postgres',
+		"SELECT injection_points_attach('" . $point . "','wait');"
+	);
+
+	# Trigger the checksum change, asynchronously
+	note("triggering first checksum change: " . $first);
+	enable_data_checksums($node) if ($first eq 'enable');
+	disable_data_checksums($node) if ($first eq 'disable');
+
+	# Handle the initial injection point - wait, wakeup and detatch. This
+	# initializes the shmem for the 'wait' action.
+	wait_injection_point($node, $init) if defined($init);
+	wakeup_injection_point($node, $init) if defined($init);
+
+	# Wait for the actual injection point to be hit by the state change.
+	wait_injection_point($node, $point);
+
+	# The first checksum state change is waitinig on the injection point.
+	# Trigger the concurrent change (also asynchronously).
+	note("triggering second checksum change: " . $second);
+	enable_data_checksums($node) if ($second eq 'enable');
+	disable_data_checksums($node) if ($second eq 'disable');
+
+	# Wake the injection point, so that the first change can proceed.
+	wakeup_injection_point($node, $point);
+
+	# Wait until there are no ongoing checksum changes, which we determine
+	# by looking for a checksum launcher process.
+	note('wait for the checksum launcher to exit');
+	$node->poll_query_until('postgres',
+			"SELECT count(*) = 0 "
+		  . "FROM pg_catalog.pg_stat_activity "
+		  . "WHERE backend_type = 'datachecksum launcher';");
+
+	# Does the final checksum state match the expected state?
+	test_checksum_state($node, $final);
+
+	# Since the log isn't being written to now, parse the log and check
+	# for instances of checksum verification failures.
+	my $log = PostgreSQL::Test::Utils::slurp_file($node->logfile,
+		$node_loglocation);
+	unlike(
+		$log,
+		qr/page verification failed,.+\d$/,
+		"no checksum validation errors in primary log (during WAL recovery)"
+	);
+	$node_loglocation = -s $node->logfile;
+}
+
+# Start the test suite with pgbench running.
+background_rw_pgbench($node->port);
+
+# concurrent enable + disable, different injection points in the "enable" process
+test_checksum_transition('disabled', 'enable', 'disable', undef, 'datachecksums-enable-inprogress-checksums-start', 'off');
+test_checksum_transition('disabled', 'enable', 'disable', 'datachecksums-enable-inprogress-checksums-start', 'datachecksums-enable-inprogress-checksums-after-xlog', 'off');
+test_checksum_transition('disabled', 'enable', 'disable', 'datachecksums-enable-inprogress-checksums-start', 'datachecksums-enable-inprogress-checksums-after-xlogctl', 'off');
+test_checksum_transition('disabled', 'enable', 'disable', 'datachecksums-enable-inprogress-checksums-start', 'datachecksums-enable-inprogress-checksums-after-controlfile', 'off');
+test_checksum_transition('disabled', 'enable', 'disable', undef, 'datachecksums-enable-inprogress-checksums-before-barrier-wait', 'off');
+test_checksum_transition('disabled', 'enable', 'disable', undef, 'datachecksums-enable-inprogress-checksums-end', 'off');
+test_checksum_transition('disabled', 'enable', 'disable', undef, 'datachecksums-enable-checksums-start', 'off');
+test_checksum_transition('disabled', 'enable', 'disable', 'datachecksums-enable-checksums-start', 'datachecksums-enable-checksums-after-xlog', 'off');
+test_checksum_transition('disabled', 'enable', 'disable', 'datachecksums-enable-checksums-start', 'datachecksums-enable-checksums-after-xlogctl', 'off');
+test_checksum_transition('disabled', 'enable', 'disable', 'datachecksums-enable-checksums-start', 'datachecksums-enable-checksums-after-controlfile', 'off');
+test_checksum_transition('disabled', 'enable', 'disable', undef, 'datachecksums-enable-checksums-before-checkpoint', 'off');
+test_checksum_transition('disabled', 'enable', 'disable', undef, 'datachecksums-enable-checksums-before-barrier-wait', 'off');
+test_checksum_transition('disabled', 'enable', 'disable', undef, 'datachecksums-enable-checksums-end', 'off');
+
+# concurrent enable + enable, different injection points in the "enable" process
+test_checksum_transition('disabled', 'enable', 'enable', undef, 'datachecksums-enable-inprogress-checksums-start', 'on');
+test_checksum_transition('disabled', 'enable', 'enable', 'datachecksums-enable-inprogress-checksums-start', 'datachecksums-enable-inprogress-checksums-after-xlog', 'on');
+test_checksum_transition('disabled', 'enable', 'enable', 'datachecksums-enable-inprogress-checksums-start', 'datachecksums-enable-inprogress-checksums-after-xlogctl', 'on');
+test_checksum_transition('disabled', 'enable', 'enable', 'datachecksums-enable-inprogress-checksums-start', 'datachecksums-enable-inprogress-checksums-after-controlfile', 'on');
+test_checksum_transition('disabled', 'enable', 'enable', undef, 'datachecksums-enable-inprogress-checksums-before-barrier-wait', 'on');
+test_checksum_transition('disabled', 'enable', 'enable', undef, 'datachecksums-enable-inprogress-checksums-end', 'on');
+test_checksum_transition('disabled', 'enable', 'enable', undef, 'datachecksums-enable-checksums-start', 'on');
+test_checksum_transition('disabled', 'enable', 'enable', 'datachecksums-enable-checksums-start', 'datachecksums-enable-checksums-after-xlog', 'on');
+test_checksum_transition('disabled', 'enable', 'enable', 'datachecksums-enable-checksums-start', 'datachecksums-enable-checksums-after-xlogctl', 'on');
+test_checksum_transition('disabled', 'enable', 'enable', 'datachecksums-enable-checksums-start', 'datachecksums-enable-checksums-after-controlfile', 'on');
+test_checksum_transition('disabled', 'enable', 'enable', undef, 'datachecksums-enable-checksums-before-checkpoint', 'on');
+test_checksum_transition('disabled', 'enable', 'enable', undef, 'datachecksums-enable-checksums-before-barrier-wait', 'on');
+test_checksum_transition('disabled', 'enable', 'enable', undef, 'datachecksums-enable-checksums-end', 'on');
+
+# concurrent disable + disable, different injection points in the "disable" process
+test_checksum_transition('enabled', 'disable', 'disable', undef, 'datachecksums-disable-inprogress-checksums-start', 'off');
+test_checksum_transition('enabled', 'disable', 'disable', 'datachecksums-disable-inprogress-checksums-start', 'datachecksums-disable-inprogress-checksums-after-xlog', 'off');
+test_checksum_transition('enabled', 'disable', 'disable', 'datachecksums-disable-inprogress-checksums-start', 'datachecksums-disable-inprogress-checksums-after-xlogctl', 'off');
+test_checksum_transition('enabled', 'disable', 'disable', 'datachecksums-disable-inprogress-checksums-start', 'datachecksums-disable-inprogress-checksums-after-controlfile', 'off');
+test_checksum_transition('enabled', 'disable', 'disable', undef, 'datachecksums-disable-inprogress-checksums-before-barrier-wait', 'off');
+test_checksum_transition('enabled', 'disable', 'disable', undef, 'datachecksums-disable-checksums-start', 'off');
+test_checksum_transition('enabled', 'disable', 'disable', 'datachecksums-disable-checksums-start', 'datachecksums-disable-checksums-after-xlog', 'off');
+test_checksum_transition('enabled', 'disable', 'disable', 'datachecksums-disable-checksums-start', 'datachecksums-disable-checksums-after-xlogctl', 'off');
+test_checksum_transition('enabled', 'disable', 'disable', 'datachecksums-disable-checksums-start', 'datachecksums-disable-checksums-after-controlfile', 'off');
+test_checksum_transition('enabled', 'disable', 'disable', undef, 'datachecksums-disable-checksums-before-checkpoint', 'off');
+test_checksum_transition('enabled', 'disable', 'disable', undef, 'datachecksums-disable-checksums-before-barrier-wait', 'off');
+test_checksum_transition('enabled', 'disable', 'disable', undef, 'datachecksums-disable-checksums-end', 'off');
+
+# concurrent disable + enable, different injection points in the "disable" process
+test_checksum_transition('enabled', 'disable', 'enable', undef, 'datachecksums-disable-inprogress-checksums-start', 'on');
+test_checksum_transition('enabled', 'disable', 'enable', 'datachecksums-disable-inprogress-checksums-start', 'datachecksums-disable-inprogress-checksums-after-xlog', 'on');
+test_checksum_transition('enabled', 'disable', 'enable', 'datachecksums-disable-inprogress-checksums-start', 'datachecksums-disable-inprogress-checksums-after-xlogctl', 'on');
+test_checksum_transition('enabled', 'disable', 'enable', 'datachecksums-disable-inprogress-checksums-start', 'datachecksums-disable-inprogress-checksums-after-controlfile', 'on');
+test_checksum_transition('enabled', 'disable', 'enable', undef, 'datachecksums-disable-inprogress-checksums-before-barrier-wait', 'on');
+test_checksum_transition('enabled', 'disable', 'enable', undef, 'datachecksums-disable-checksums-start', 'on');
+test_checksum_transition('enabled', 'disable', 'enable', 'datachecksums-disable-checksums-start', 'datachecksums-disable-checksums-after-xlog', 'on');
+test_checksum_transition('enabled', 'disable', 'enable', 'datachecksums-disable-checksums-start', 'datachecksums-disable-checksums-after-xlogctl', 'on');
+test_checksum_transition('enabled', 'disable', 'enable', 'datachecksums-disable-checksums-start', 'datachecksums-disable-checksums-after-controlfile', 'on');
+test_checksum_transition('enabled', 'disable', 'enable', undef, 'datachecksums-disable-checksums-before-checkpoint', 'on');
+test_checksum_transition('enabled', 'disable', 'enable', undef, 'datachecksums-disable-checksums-before-barrier-wait', 'on');
+test_checksum_transition('enabled', 'disable', 'enable', undef, 'datachecksums-disable-checksums-end', 'on');
+
+$node->stop;
+done_testing();
-- 
2.54.0

From ae1ef6cd0a2c4d2fcddb34db282c55a7f2fe509c Mon Sep 17 00:00:00 2001
From: Tomas Vondra <[email-masked]>
Date: Wed, 15 Apr 2026 22:22:48 +0200
Subject: [PATCH 06/13] TAP 11: concurrency with checkpoints

---
 .../t/011_concurrent_checkpoint.pl            | 276 ++++++++++++++++++
 1 file changed, 276 insertions(+)
 create mode 100644 src/test/modules/test_checksums/t/011_concurrent_checkpoint.pl

diff --git a/src/test/modules/test_checksums/t/011_concurrent_checkpoint.pl b/src/test/modules/test_checksums/t/011_concurrent_checkpoint.pl
new file mode 100644
index 00000000000..4cea74914d4
--- /dev/null
+++ b/src/test/modules/test_checksums/t/011_concurrent_checkpoint.pl
@@ -0,0 +1,276 @@
+
+# Copyright (c) 2026, PostgreSQL Global Development Group
+
+# Test suite for testing enabling data checksums in an online cluster with
+# injection point tests injecting failures into the processing
+
+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, or very expensive, to execute.  There are two
+# PG_TEST_EXTRA options for running it, "checksum" for a pared-down test suite
+# an "checksum_extended" for the full suite.  The full suite can run for hours
+# on slow or constrained systems.
+my $extended = undef;
+if ($ENV{PG_TEST_EXTRA})
+{
+	$extended = 1 if ($ENV{PG_TEST_EXTRA} =~ /\bchecksum_extended\b/);
+	plan skip_all => 'Expensive data checksums test disabled'
+	  unless ($ENV{PG_TEST_EXTRA} =~ /\bchecksum(_extended)?\b/);
+}
+
+if ($ENV{enable_injection_points} ne 'yes')
+{
+	plan skip_all => 'Injection points not supported by this build';
+}
+
+# ---------------------------------------------------------------------------
+# Test cluster setup
+#
+
+# Initiate testcluster
+my $node = PostgreSQL::Test::Cluster->new('injection_node');
+$node->init(no_data_checksums => 1);
+$node->start;
+
+# Set up test environment
+$node->safe_psql('postgres', 'CREATE EXTENSION test_checksums;');
+$node->safe_psql('postgres', 'CREATE EXTENSION injection_points;');
+
+my $pgbench = undef;
+my $scalefactor = ($extended ? 10 : 1);
+my $node_loglocation = 0;
+
+$node->command_ok(
+	[
+		'pgbench', '-p', $node->port, '-i',
+		'-s', $scalefactor, '-q', 'postgres'
+	]);
+
+# Start a pgbench run in the background against the server specified via the
+# port passed as parameter.
+sub background_rw_pgbench
+{
+	my $port = shift;
+
+	# If a previous pgbench is still running, start by shutting it down.
+	$pgbench->finish if $pgbench;
+
+	my $clients = 1;
+	my $runtime = 2;
+
+	if ($extended)
+	{
+		# Randomize the number of pgbench clients a bit (range 1-16)
+		$clients = 1 + int(rand(15));
+		$runtime = 600;
+	}
+	my @cmd = ('pgbench', '-p', $port, '-T', $runtime, '-c', $clients);
+
+	# Randomize whether we spawn connections or not
+	push(@cmd, '-C') if ($extended && cointoss);
+	# Finally add the database name to use
+	push(@cmd, 'postgres');
+
+	$pgbench = IPC::Run::start(
+		\@cmd,
+		'<' => '/dev/null',
+		'>' => '/dev/null',
+		'2>' => '/dev/null',
+		IPC::Run::timer($PostgreSQL::Test::Utils::timeout_default));
+}
+
+sub attach_injection_point
+{
+	my ($node, $point) = @_;
+	note('attaching injection point: ' . $point);
+	$node->safe_psql('postgres',
+		"SELECT injection_points_attach('" . $point . "','wait');"
+	);
+}
+
+sub wait_injection_point
+{
+	my ($node, $point) = @_;
+
+	note("waiting for the injection point to be hit");
+	$node->poll_query_until(
+		'postgres',
+		"SELECT COUNT(*) FROM pg_catalog.pg_stat_activity WHERE wait_event = '" . $point . "'",
+		'1');
+}
+
+sub wakeup_injection_point
+{
+	my ($node, $point) = @_;
+
+	note('waking the init injection point: ' . $point);
+	$node->safe_psql('postgres',
+		"SELECT injection_points_wakeup('" . $point . "');"
+	);
+
+	note("detaching the init injection point");
+	$node->safe_psql('postgres',
+		"SELECT injection_points_detach('" . $point . "');");
+}
+
+# Test behavior with a checksum transitions and a concurrent checkpoint.
+#
+# The cluster is initialized into checksum state $start (enabled/disabled),
+# and a checksum change is initiated in a controled way. It gets paused at
+# a selected injection point $point, and a checkpoint is performed. Then
+# the injection point is waken up. Once the checksum change completes, the
+# checksum state is compared to the expected $final state.
+#
+# The checkpoint is performed synchronously, while the checksum worker
+# is paused on the injection point.
+#
+# Finally, after the checksum change completes, the instance is restarted
+# (in either fast ot immediate mode, picked randomly), and the final
+# checksum state is validated against the expected value. The server log
+# is checked for checksum failures.
+#
+# While the checksum change is happening, there's a r/w pgbench running in
+# the background, to generate writes.
+#
+# arguments:
+#
+# - start - starting checksum state (enabled/disabled)
+# - change - checksum change
+# - init - initial injection point (see comment later)
+# - point - injection point the first change should wait on
+# - final - expected checksum state at the end
+#
+# XXX Some of the injection points are in a critical section, which does
+# not allow memory allocations etc. INJECTION_POINT_LOAD/_CACHED handles
+# just private memory allocation, but 'wait' requires a shmem allocation.
+# To deal with that, we setup an injection point $init outside a critical
+# section to initialize the shmem stuff needed by 'wait'.
+#
+# XXX We could also validate the checksums using pg_checksums, if the
+# state is 'on'.
+sub test_checksum_transition
+{
+	my ($start, $change, $init, $point, $final) = @_;
+
+	# print the current test instructions, both into TAP output and into
+	# the server log, to make correlation easier
+
+	my $initstr = 'undef';
+	$initstr = $init if defined($init);
+
+	# Start the test suite with pgbench running.
+	background_rw_pgbench($node->port);
+
+	$node->safe_psql('postgres',
+		"SELECT '========== " . $start . " / " . $change . " / " . $initstr . " / " . $point . " / " . $final . " =========='");
+
+	note($start . " / " . $change . " / " . $initstr . " / " . $point . " / " . $final);
+
+	# put the cluster into the initial checksum state, synchronously
+	note('changing checksums into initial state: ' . $start);
+	enable_data_checksums($node, wait => 'on') if ($start eq 'enabled');
+	disable_data_checksums($node, wait => 'off') if ($start eq 'disabled');
+
+	# Wait on an injection point outside a critical section, to initialize
+	# the shmem (which can't be done in critical section).
+	attach_injection_point($node, $init) if defined($init);
+
+	# Wait on the injection point, to pause the checksum change.
+	attach_injection_point($node, $point);
+
+	# Trigger the checksum change, asynchronously
+	note("triggering checksum change: " . $change);
+	enable_data_checksums($node) if ($change eq 'enable');
+	disable_data_checksums($node) if ($change eq 'disable');
+
+	# Handle the initial injection point - wait, wakeup and detatch. This
+	# initializes the shmem for the 'wait' action.
+	wait_injection_point($node, $init) if defined($init);
+	wakeup_injection_point($node, $init) if defined($init);
+
+	# Wait for the injection point to be hit by the state change.
+	wait_injection_point($node, $point);
+
+	# The checksum state change is paused on the injection point. Perform
+	# the checkpoint (synchronously).
+	note('checkpoint');
+	$node->safe_psql('postgres', "CHECKPOINT");
+
+	# Wake the injection point, so that the first change can proceed.
+	wakeup_injection_point($node, $point);
+
+	# Wait until there are no ongoing checksum changes, which we determine
+	# by looking for a checksum launcher process.
+	note('wait for the checksum launcher to exit');
+	$node->poll_query_until('postgres',
+			"SELECT count(*) = 0 "
+		  . "FROM pg_catalog.pg_stat_activity "
+		  . "WHERE backend_type = 'datachecksum launcher';");
+
+	# Does the final checksum state match the expected state?
+	test_checksum_state($node, $final);
+
+	# restart the cluster, possibly in immediate mode, to simulate a crash
+	#
+	# XXX maybe we should do just immediate shutdowns, because the shutdown
+	# checkpoint will overwrite the state state with "current" one, not the
+	# state produced by the concurrent change + checkpoint.
+	$node->stop(stopmode());
+	$node->start;
+
+	# Does the final checksum state match the expected state?
+	test_checksum_state($node, $final);
+
+	# Since the log isn't being written to now, parse the log and check
+	# for instances of checksum verification failures.
+	my $log = PostgreSQL::Test::Utils::slurp_file($node->logfile,
+		$node_loglocation);
+	unlike(
+		$log,
+		qr/page verification failed,.+\d$/,
+		"no checksum validation errors in primary log (during WAL recovery)"
+	);
+	$node_loglocation = -s $node->logfile;
+}
+
+# concurrent enable + checkpoint, different injection points in the "enable" process
+test_checksum_transition('disabled', 'enable', undef, 'datachecksums-enable-inprogress-checksums-start', 'on');
+test_checksum_transition('disabled', 'enable', 'datachecksums-enable-inprogress-checksums-start', 'datachecksums-enable-inprogress-checksums-after-xlog', 'on');
+test_checksum_transition('disabled', 'enable', 'datachecksums-enable-inprogress-checksums-start', 'datachecksums-enable-inprogress-checksums-after-xlogctl', 'on');
+test_checksum_transition('disabled', 'enable', 'datachecksums-enable-inprogress-checksums-start', 'datachecksums-enable-inprogress-checksums-after-controlfile', 'on');
+test_checksum_transition('disabled', 'enable', undef, 'datachecksums-enable-inprogress-checksums-before-barrier-wait', 'on');
+test_checksum_transition('disabled', 'enable', undef, 'datachecksums-enable-inprogress-checksums-end', 'on');
+test_checksum_transition('disabled', 'enable', undef, 'datachecksums-enable-checksums-start', 'on');
+test_checksum_transition('disabled', 'enable', 'datachecksums-enable-checksums-start', 'datachecksums-enable-checksums-after-xlog', 'on');
+test_checksum_transition('disabled', 'enable', 'datachecksums-enable-checksums-start', 'datachecksums-enable-checksums-after-xlogctl', 'on');
+test_checksum_transition('disabled', 'enable', 'datachecksums-enable-checksums-start', 'datachecksums-enable-checksums-after-controlfile', 'on');
+test_checksum_transition('disabled', 'enable', undef, 'datachecksums-enable-checksums-before-checkpoint', 'on');
+test_checksum_transition('disabled', 'enable', undef, 'datachecksums-enable-checksums-before-barrier-wait', 'on');
+test_checksum_transition('disabled', 'enable', undef, 'datachecksums-enable-checksums-end', 'on');
+
+# concurrent disable + checkpoint, different injection points in the "disable" process
+test_checksum_transition('enabled', 'disable', undef, 'datachecksums-disable-inprogress-checksums-start', 'off');
+test_checksum_transition('enabled', 'disable', 'datachecksums-disable-inprogress-checksums-start', 'datachecksums-disable-inprogress-checksums-after-xlog', 'off');
+test_checksum_transition('enabled', 'disable', 'datachecksums-disable-inprogress-checksums-start', 'datachecksums-disable-inprogress-checksums-after-xlogctl', 'off');
+test_checksum_transition('enabled', 'disable', 'datachecksums-disable-inprogress-checksums-start', 'datachecksums-disable-inprogress-checksums-after-controlfile', 'off');
+test_checksum_transition('enabled', 'disable', undef, 'datachecksums-disable-inprogress-checksums-before-barrier-wait', 'off');
+test_checksum_transition('enabled', 'disable', undef, 'datachecksums-disable-checksums-start', 'off');
+test_checksum_transition('enabled', 'disable', 'datachecksums-disable-checksums-start', 'datachecksums-disable-checksums-after-xlog', 'off');
+test_checksum_transition('enabled', 'disable', 'datachecksums-disable-checksums-start', 'datachecksums-disable-checksums-after-xlogctl', 'off');
+test_checksum_transition('enabled', 'disable', 'datachecksums-disable-checksums-start', 'datachecksums-disable-checksums-after-controlfile', 'off');
+test_checksum_transition('enabled', 'disable', undef, 'datachecksums-disable-checksums-before-checkpoint', 'off');
+test_checksum_transition('enabled', 'disable', undef, 'datachecksums-disable-checksums-before-barrier-wait', 'off');
+test_checksum_transition('enabled', 'disable', undef, 'datachecksums-disable-checksums-end', 'off');
+
+$node->stop;
+done_testing();
-- 
2.54.0

From 57a5e353dbfcaaac1fa7cd25dc38f3e1807ccafa Mon Sep 17 00:00:00 2001
From: Tomas Vondra <[email-masked]>
Date: Wed, 15 Apr 2026 23:06:14 +0200
Subject: [PATCH 07/13] TAP 12: crashes at injection points

---
 .../t/012_concurrent_checkpoint_crash.pl      | 282 ++++++++++++++++++
 1 file changed, 282 insertions(+)
 create mode 100644 src/test/modules/test_checksums/t/012_concurrent_checkpoint_crash.pl

diff --git a/src/test/modules/test_checksums/t/012_concurrent_checkpoint_crash.pl b/src/test/modules/test_checksums/t/012_concurrent_checkpoint_crash.pl
new file mode 100644
index 00000000000..de169f47034
--- /dev/null
+++ b/src/test/modules/test_checksums/t/012_concurrent_checkpoint_crash.pl
@@ -0,0 +1,282 @@
+
+# Copyright (c) 2026, PostgreSQL Global Development Group
+
+# Test suite for testing enabling data checksums in an online cluster with
+# injection point tests injecting failures into the processing
+
+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, or very expensive, to execute.  There are two
+# PG_TEST_EXTRA options for running it, "checksum" for a pared-down test suite
+# an "checksum_extended" for the full suite.  The full suite can run for hours
+# on slow or constrained systems.
+my $extended = undef;
+if ($ENV{PG_TEST_EXTRA})
+{
+	$extended = 1 if ($ENV{PG_TEST_EXTRA} =~ /\bchecksum_extended\b/);
+	plan skip_all => 'Expensive data checksums test disabled'
+	  unless ($ENV{PG_TEST_EXTRA} =~ /\bchecksum(_extended)?\b/);
+}
+
+if ($ENV{enable_injection_points} ne 'yes')
+{
+	plan skip_all => 'Injection points not supported by this build';
+}
+
+# ---------------------------------------------------------------------------
+# Test cluster setup
+#
+
+# Initiate testcluster
+my $node = PostgreSQL::Test::Cluster->new('injection_node');
+$node->init(no_data_checksums => 1);
+$node->start;
+
+# Set up test environment
+$node->safe_psql('postgres', 'CREATE EXTENSION test_checksums;');
+$node->safe_psql('postgres', 'CREATE EXTENSION injection_points;');
+
+my $pgbench = undef;
+my $scalefactor = ($extended ? 10 : 1);
+my $node_loglocation = 0;
+
+$node->command_ok(
+	[
+		'pgbench', '-p', $node->port, '-i',
+		'-s', $scalefactor, '-q', 'postgres'
+	]);
+
+# Start a pgbench run in the background against the server specified via the
+# port passed as parameter.
+sub background_rw_pgbench
+{
+	my $port = shift;
+
+	# If a previous pgbench is still running, start by shutting it down.
+	$pgbench->finish if $pgbench;
+
+	my $clients = 1;
+	my $runtime = 2;
+
+	if ($extended)
+	{
+		# Randomize the number of pgbench clients a bit (range 1-16)
+		$clients = 1 + int(rand(15));
+		$runtime = 600;
+	}
+	my @cmd = ('pgbench', '-p', $port, '-T', $runtime, '-c', $clients);
+
+	# Randomize whether we spawn connections or not
+	push(@cmd, '-C') if ($extended && cointoss);
+	# Finally add the database name to use
+	push(@cmd, 'postgres');
+
+	$pgbench = IPC::Run::start(
+		\@cmd,
+		'<' => '/dev/null',
+		'>' => '/dev/null',
+		'2>' => '/dev/null',
+		IPC::Run::timer($PostgreSQL::Test::Utils::timeout_default));
+}
+
+sub attach_injection_point
+{
+	my ($node, $point) = @_;
+	note('attaching injection point: ' . $point);
+	$node->safe_psql('postgres',
+		"SELECT injection_points_attach('" . $point . "','wait');"
+	);
+}
+
+sub wait_injection_point
+{
+	my ($node, $point) = @_;
+
+	note("waiting for the injection point to be hit");
+	$node->poll_query_until(
+		'postgres',
+		"SELECT COUNT(*) FROM pg_catalog.pg_stat_activity WHERE wait_event = '" . $point . "'",
+		'1');
+}
+
+sub wakeup_injection_point
+{
+	my ($node, $point) = @_;
+
+	note('waking the init injection point: ' . $point);
+	$node->safe_psql('postgres',
+		"SELECT injection_points_wakeup('" . $point . "');"
+	);
+
+	note("detaching the init injection point");
+	$node->safe_psql('postgres',
+		"SELECT injection_points_detach('" . $point . "');");
+}
+
+# Test behavior with a checksum transitions and a concurrent checkpoint,
+# followed by a crash.
+#
+# The test puts the instance into the initial checksum state $start,
+# triggers a checksum change that pauses on a first injection point. Then
+# a checkpoint is performed, and the checksum change proceeds either to
+# a second injection point or finishes.
+#
+# Then the instance get restarted in immediate mode to simulate failure,
+# and the final checksum state (after recovery) is validated against the
+# expected value. The server log is checked for checksum failures.
+#
+# While the checksum change is happening, there's a r/w pgbench running in
+# the background, to generate writes.
+#
+# arguments:
+#
+# - start  - start checksum state (enabled/disabled)
+# - change - checksum change to initiate
+# - point1 - injection point before checkpoint
+# - point2 - injection point after checkpoint
+# - final  - expected checksum state at the end
+#
+# XXX Some of the injection points are in a critical section, which does
+# not allow memory allocations etc. INJECTION_POINT_LOAD/_CACHED handles
+# just private memory allocation, but 'wait' requires a shmem allocation.
+# To deal with that, we setup an injection point $init outside a critical
+# section to initialize the shmem stuff needed by 'wait'.
+#
+# XXX We could also validate the checksums using pg_checksums, if the
+# state is 'on'.
+sub test_checksum_transition
+{
+	my ($start, $change, $init, $point1, $point2, $final) = @_;
+
+	my $initstr = 'undef';
+	my $point2str = 'undef';
+
+	# print the current test instructions, both into TAP output and into
+	# the server log, to make correlation easier
+
+	$initstr = $init if defined($init);
+	$point2str = $point2 if defined($point2);
+
+	$node->safe_psql('postgres',
+		"SELECT '========== " . $start . " / " . $change . " / " . $initstr . " / " . $point1 . " / " . $point2str . " / " . $final . " =========='");
+
+	note($start . " / " . $change . " / " . $initstr . " / " . $point1 . " / " . $point2str . " / " . $final);
+
+	# Start the test suite with pgbench running.
+	background_rw_pgbench($node->port);
+
+	# put the cluster into the initial checksum state, synchronously
+	note('changing checksums into initial state: ' . $start);
+	enable_data_checksums($node, wait => 'on') if ($start eq 'enabled');
+	disable_data_checksums($node, wait => 'off') if ($start eq 'disabled');
+
+	 # Wait on an injection point outside a critical section, to initialize
+	 # the shmem (which can't be done in critical section).
+	attach_injection_point($node, $init) if defined($init);
+
+	# Wait on the two injection points, to pause the checksum change.
+	attach_injection_point($node, $point1);
+
+	# The second injection point is optional, so only attach it then.
+	attach_injection_point($node, $point2) if defined($point2);
+
+	# Trigger the checksum change, asynchronously
+	note("triggering checksum change: " . $change);
+	enable_data_checksums($node) if ($change eq 'enable');
+	disable_data_checksums($node) if ($change eq 'disable');
+
+	# Handle the initial injection point - wait, wakeup and detatch. This
+	# initializes the shmem for the 'wait' action.
+	wait_injection_point($node, $init) if defined($init);
+	wakeup_injection_point($node, $init) if defined($init);
+
+	# Wait for the first injection point to be hit by the state change.
+	wait_injection_point($node, $point1);
+
+	# The checksum state change is paused on the first injection point.
+	# Perform the checkpoint (synchronously).
+	note('checkpoint');
+	$node->safe_psql('postgres', "CHECKPOINT");
+
+	# Wake the injection point, so that the first change can proceed.
+	wakeup_injection_point($node, $point1);
+
+	# Either wait for the second injection point - if defined, or for the
+	# checksum change to complete.
+
+	if (defined($point2))
+	{
+		wait_injection_point($node, $point2);
+	}
+	else
+	{
+		# Wait until there are no ongoing checksum changes, which we determine
+		# by looking for a checksum launcher process.
+		note('wait for the checksum launcher to exit');
+		$node->poll_query_until('postgres',
+				"SELECT count(*) = 0 "
+			  . "FROM pg_catalog.pg_stat_activity "
+			  . "WHERE backend_type = 'datachecksum launcher';");
+		note('checksum launcher exited');
+	}
+
+	# restart the cluster, in immediate mode, to simulate a crash
+	$node->stop('immediate');
+	$node->start;
+
+	# Does the final checksum state match the expected state?
+	test_checksum_state($node, $final);
+
+	# Since the log isn't being written to now, parse the log and check
+	# for instances of checksum verification failures.
+	my $log = PostgreSQL::Test::Utils::slurp_file($node->logfile,
+		$node_loglocation);
+	unlike(
+		$log,
+		qr/page verification failed,.+\d$/,
+		"no checksum validation errors in primary log (during WAL recovery)"
+	);
+	$node_loglocation = -s $node->logfile;
+}
+
+# concurrent enable + checkpoint, different injection points in the "enable" process
+test_checksum_transition('disabled', 'enable', undef, 'datachecksums-enable-inprogress-checksums-start', 'datachecksums-enable-inprogress-checksums-after-xlog', 'off');
+test_checksum_transition('disabled', 'enable', 'datachecksums-enable-inprogress-checksums-start', 'datachecksums-enable-inprogress-checksums-after-xlog', 'datachecksums-enable-inprogress-checksums-after-xlogctl', 'off');
+test_checksum_transition('disabled', 'enable', 'datachecksums-enable-inprogress-checksums-start', 'datachecksums-enable-inprogress-checksums-after-xlogctl', 'datachecksums-enable-inprogress-checksums-after-controlfile', 'off');
+test_checksum_transition('disabled', 'enable', 'datachecksums-enable-inprogress-checksums-start', 'datachecksums-enable-inprogress-checksums-after-controlfile', 'datachecksums-enable-inprogress-checksums-before-barrier-wait', 'off');
+test_checksum_transition('disabled', 'enable', undef, 'datachecksums-enable-inprogress-checksums-before-barrier-wait', 'datachecksums-enable-inprogress-checksums-end', 'off');
+test_checksum_transition('disabled', 'enable', undef, 'datachecksums-enable-inprogress-checksums-end', 'datachecksums-enable-checksums-start', 'off');
+test_checksum_transition('disabled', 'enable', undef, 'datachecksums-enable-checksums-start', 'datachecksums-enable-checksums-after-xlog', 'on');
+test_checksum_transition('disabled', 'enable', 'datachecksums-enable-checksums-start', 'datachecksums-enable-checksums-after-xlog', 'datachecksums-enable-checksums-after-xlogctl', 'off');
+test_checksum_transition('disabled', 'enable', 'datachecksums-enable-checksums-start', 'datachecksums-enable-checksums-after-xlogctl', 'datachecksums-enable-checksums-after-controlfile', 'on');
+test_checksum_transition('disabled', 'enable', 'datachecksums-enable-checksums-start', 'datachecksums-enable-checksums-after-controlfile', 'datachecksums-enable-checksums-before-checkpoint', 'on');
+test_checksum_transition('disabled', 'enable', undef, 'datachecksums-enable-checksums-before-checkpoint', 'datachecksums-enable-checksums-before-barrier-wait', 'on');
+test_checksum_transition('disabled', 'enable', undef, 'datachecksums-enable-checksums-before-barrier-wait', 'datachecksums-enable-checksums-end', 'on');
+test_checksum_transition('disabled', 'enable', undef, 'datachecksums-enable-checksums-end', undef, 'on');
+
+# concurrent disable + checkpoint, different injection points in the "disable" process
+test_checksum_transition('enabled', 'disable', undef, 'datachecksums-disable-inprogress-checksums-start', 'datachecksums-disable-inprogress-checksums-after-xlog', 'off');
+test_checksum_transition('enabled', 'disable', 'datachecksums-disable-inprogress-checksums-start', 'datachecksums-disable-inprogress-checksums-after-xlog', 'datachecksums-disable-inprogress-checksums-after-xlogctl', 'on');
+test_checksum_transition('enabled', 'disable', 'datachecksums-disable-inprogress-checksums-start', 'datachecksums-disable-inprogress-checksums-after-xlogctl', 'datachecksums-disable-inprogress-checksums-after-controlfile', 'off');
+test_checksum_transition('enabled', 'disable', 'datachecksums-disable-inprogress-checksums-start', 'datachecksums-disable-inprogress-checksums-after-controlfile', 'datachecksums-disable-inprogress-checksums-before-barrier-wait', 'off');
+test_checksum_transition('enabled', 'disable', undef, 'datachecksums-disable-inprogress-checksums-before-barrier-wait', 'datachecksums-disable-checksums-start', 'off');
+test_checksum_transition('enabled', 'disable', undef, 'datachecksums-disable-checksums-start', 'datachecksums-disable-checksums-after-xlog', 'off');
+test_checksum_transition('enabled', 'disable', 'datachecksums-disable-checksums-start', 'datachecksums-disable-checksums-after-xlog', 'datachecksums-disable-checksums-after-xlogctl', 'off');
+test_checksum_transition('enabled', 'disable', 'datachecksums-disable-checksums-start', 'datachecksums-disable-checksums-after-xlogctl', 'datachecksums-disable-checksums-after-controlfile', 'off');
+test_checksum_transition('enabled', 'disable', 'datachecksums-disable-checksums-start', 'datachecksums-disable-checksums-after-controlfile', 'datachecksums-disable-checksums-before-checkpoint', 'off');
+test_checksum_transition('enabled', 'disable', undef, 'datachecksums-disable-checksums-before-checkpoint', 'datachecksums-disable-checksums-before-barrier-wait', 'off');
+test_checksum_transition('enabled', 'disable', undef, 'datachecksums-disable-checksums-before-barrier-wait', 'datachecksums-disable-checksums-end', 'off');
+test_checksum_transition('enabled', 'disable', undef, 'datachecksums-disable-checksums-end', undef, 'off');
+
+$node->stop;
+done_testing();
-- 
2.54.0

From ddaca81f530a371a2dfab67ad9da49ef1f8aa69e Mon Sep 17 00:00:00 2001
From: test <test>
Date: Fri, 1 May 2026 22:16:00 +0200
Subject: [PATCH 08/13] TAP 13: concurrency with checkpoint REDO

---
 .../t/013_async_checkpoint_crash.pl           | 604 ++++++++++++++++++
 1 file changed, 604 insertions(+)
 create mode 100644 src/test/modules/test_checksums/t/013_async_checkpoint_crash.pl

diff --git a/src/test/modules/test_checksums/t/013_async_checkpoint_crash.pl b/src/test/modules/test_checksums/t/013_async_checkpoint_crash.pl
new file mode 100644
index 00000000000..4a310506482
--- /dev/null
+++ b/src/test/modules/test_checksums/t/013_async_checkpoint_crash.pl
@@ -0,0 +1,604 @@
+
+# Copyright (c) 2026, PostgreSQL Global Development Group
+
+# Test suite for testing enabling data checksums in an online cluster with
+# injection point tests injecting failures into the processing
+
+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, or very expensive, to execute.  There are two
+# PG_TEST_EXTRA options for running it, "checksum" for a pared-down test suite
+# an "checksum_extended" for the full suite.  The full suite can run for hours
+# on slow or constrained systems.
+my $extended = undef;
+if ($ENV{PG_TEST_EXTRA})
+{
+	$extended = 1 if ($ENV{PG_TEST_EXTRA} =~ /\bchecksum_extended\b/);
+	plan skip_all => 'Expensive data checksums test disabled'
+	  unless ($ENV{PG_TEST_EXTRA} =~ /\bchecksum(_extended)?\b/);
+}
+
+if ($ENV{enable_injection_points} ne 'yes')
+{
+	plan skip_all => 'Injection points not supported by this build';
+}
+
+# ---------------------------------------------------------------------------
+# Test cluster setup
+#
+
+# Initiate testcluster
+my $node = PostgreSQL::Test::Cluster->new('injection_node');
+$node->init(no_data_checksums => 1);
+
+# make sure we don't hit checkpoints unless desired
+$node->append_conf(
+	'postgresql.conf',
+	qq[
+checkpoint_timeout = 1h
+max_wal_size = 32GB
+]);
+$node->start;
+
+# Set up test environment
+$node->safe_psql('postgres', 'CREATE EXTENSION test_checksums;');
+$node->safe_psql('postgres', 'CREATE EXTENSION injection_points;');
+
+my $pgbench = undef;
+my $scalefactor = ($extended ? 10 : 1);
+my $node_loglocation = 0;
+
+$node->command_ok(
+	[
+		'pgbench', '-p', $node->port, '-i',
+		'-s', $scalefactor, '-q', 'postgres'
+	]);
+
+# Start a pgbench run in the background against the server specified via the
+# port passed as parameter.
+sub background_rw_pgbench
+{
+	my $port = shift;
+
+	# If a previous pgbench is still running, start by shutting it down.
+	$pgbench->finish if $pgbench;
+
+	my $clients = 1;
+	my $runtime = 2;
+
+	if ($extended)
+	{
+		# Randomize the number of pgbench clients a bit (range 1-16)
+		$clients = 1 + int(rand(15));
+		$runtime = 600;
+	}
+	my @cmd = ('pgbench', '-p', $port, '-T', $runtime, '-c', $clients);
+
+	# Randomize whether we spawn connections or not
+	push(@cmd, '-C') if ($extended && cointoss);
+	# Finally add the database name to use
+	push(@cmd, 'postgres');
+
+	$pgbench = IPC::Run::start(
+		\@cmd,
+		'<' => '/dev/null',
+		'>' => '/dev/null',
+		'2>' => '/dev/null',
+		IPC::Run::timer($PostgreSQL::Test::Utils::timeout_default));
+}
+
+# Start a checkpoint in the background against the server specified via the
+# port passed as parameter.
+sub background_checkpoint
+{
+	my $port = shift;
+
+	my @cmd = ('psql', '-p', $port, '-c', 'checkpoint', 'postgres');
+
+	$pgbench = IPC::Run::start(
+		\@cmd,
+		'<' => '/dev/null',
+		'>' => '/dev/null',
+		'2>' => '/dev/null',
+		IPC::Run::timer($PostgreSQL::Test::Utils::timeout_default));
+}
+
+sub attach_injection_point
+{
+	my ($node, $point) = @_;
+	note('attaching injection point: ' . $point);
+	$node->safe_psql('postgres',
+		"SELECT injection_points_attach('" . $point . "','wait');"
+	);
+}
+
+sub wait_injection_point
+{
+	my ($node, $point) = @_;
+
+	note("waiting for the injection point to be hit");
+	$node->poll_query_until(
+		'postgres',
+		"SELECT COUNT(*) FROM pg_catalog.pg_stat_activity WHERE wait_event = '" . $point . "'",
+		'1');
+}
+
+sub wakeup_injection_point
+{
+	my ($node, $point) = @_;
+
+	# detach before wakeup, so that we can't hit it right away
+	note("detaching the init injection point");
+	$node->safe_psql('postgres',
+		"SELECT injection_points_detach('" . $point . "');");
+
+	note('waking the init injection point: ' . $point);
+	$node->safe_psql('postgres',
+		"SELECT injection_points_wakeup('" . $point . "');"
+	);
+}
+
+# Test behavior with a checksum transitions and a concurrent checkpoint,
+# followed by a crash.
+#
+# The test puts the instance into the initial checksum state $start,
+# initiates a checksum change and waits for it to pause on the $init
+# injection point. Then a checkpoint is started (in the background), and
+# the two processes go through a list of injection points to interleave
+# them in a particular way.
+#
+# After reaching a $stop injection point, the cluster get restarted in an
+# immediate mode to simulate failure. The checksum state (after recovery)
+# is validated against the expected value. The server log is checked for
+# checksum failures.
+#
+# While the checksum change is happening, there's a r/w pgbench running in
+# the background, to generate writes.
+#
+# arguments:
+#
+# - start  - start checksum state (enabled/disabled)
+# - change - checksum change to initiate
+# - init   - initial injection point to wait on
+# - stop   - injection point to wait on before restarting
+# - final  - expected checksum state at the end
+# - steps  - a list of injection points to wait on / unpause
+#
+# XXX Some of the injection points are in a critical section, which does
+# not allow memory allocations etc. INJECTION_POINT_LOAD/_CACHED handles
+# just private memory allocation, but 'wait' requires a shmem allocation.
+# To deal with that, we first need to pause on an injection point outside
+# a critical section to initialize the shmem stuff needed by 'wait'.
+#
+# XXX We could also validate the checksums using pg_checksums, if the
+# state is 'on'.
+#
+# XXX The steps are not entirely deterministic, because when unpausing
+# an injection point, we don't know if the other process is already
+# paused on the following point.
+#
+sub test_checksum_sequence
+{
+	my ($start, $change, $init, $stop, $final, @steps) = @_;
+
+	# print the current test instructions, both into TAP output and into
+	# the server log, to make correlation easier
+
+	$node->safe_psql('postgres',
+		"SELECT '========== " . $start . " / " . $change . " / (" . @steps . ") / " . $init . " / " . $stop . " / " . $final . " =========='");
+
+	note($start . " / " . $change . " / (" . @steps . ") / " . $init . " / " . $stop . " / " . $final);
+
+	# Start the test suite with pgbench running.
+	background_rw_pgbench($node->port);
+
+	# put the cluster into the initial checksum state, synchronously
+	note('changing checksums into initial state: ' . $start);
+	enable_data_checksums($node, wait => 'on') if ($start eq 'enabled');
+	disable_data_checksums($node, wait => 'off') if ($start eq 'disabled');
+
+	# attach the initial injection point
+	attach_injection_point($node, $init);
+
+	# when we're at it, attach the stop injection point
+	attach_injection_point($node, $stop);
+
+	# Do a checkpoint first, so that we don't accidentaly start the next
+	# checkpoint at an unecpected time. We want the next checkpoint to be
+	# the background one.
+	$node->safe_psql('postgres', "CHECKPOINT");
+
+	# Trigger the checksum change, asynchronously
+	note("triggering checksum change: " . $change);
+	enable_data_checksums($node) if ($change eq 'enable');
+	disable_data_checksums($node) if ($change eq 'disable');
+
+	# wait for the initial injection point
+	wait_injection_point($node, $init);
+
+	# now attach all the regular injection points
+	my $n = @steps;
+	for ($a = 0; $a < $n; $a++)
+	{
+		my $point = $steps[$a];
+		attach_injection_point($node, $point);
+	}
+
+	# initiate a background checkpoint
+	background_checkpoint($node->port);
+
+	# wakeup the initial injection point, to start the main part
+	wakeup_injection_point($node, $init);
+
+	# wait for regular injection points in sequence, wake and detach them
+	for ($a = 0; $a < $n; $a++)
+	{
+		my $point = $steps[$a];
+
+		wait_injection_point($node, $point);
+		wakeup_injection_point($node, $point);
+	}
+
+	# wait for the stop injection point
+	wait_injection_point($node, $stop);
+
+	# restart the cluster, in immediate mode, to simulate a crash
+	$node->stop('immediate');
+	$node->start;
+
+	# Does the final checksum state match the expected state?
+	test_checksum_state($node, $final);
+
+	# Since the log isn't being written to now, parse the log and check
+	# for instances of checksum verification failures.
+	my $log = PostgreSQL::Test::Utils::slurp_file($node->logfile,
+		$node_loglocation);
+	unlike(
+		$log,
+		qr/page verification failed,.+\d$/,
+		"no checksum validation errors in primary log (during WAL recovery)"
+	);
+	$node_loglocation = -s $node->logfile;
+}
+
+my @steps = undef;
+
+## checksums INPROGRESS ON
+
+note('TEST INPROGRESS-ON/1');
+@steps = qw(datachecksums-enable-inprogress-checksums-before-xlog
+			 datachecksums-enable-inprogress-checksums-after-xlog
+			 datachecksums-enable-inprogress-checksums-after-xlogctl
+			 datachecksums-enable-inprogress-checksums-after-controlfile
+			 create-checkpoint-initial
+			 checkpoint-before-redo
+			 checkpoint-before-xlogctl-checksums
+			 checkpoint-after-xlogctl-checksums
+			 checkpoint-before-redo-checksums
+			 checkpoint-before-redo-wal
+			 checkpoint-after-redo-wal
+			 checkpoint-before-old-wal-removal);
+test_checksum_sequence('disabled', 'enable', 'datachecksums-enable-inprogress-checksums-start', 'datachecksums-enable-inprogress-checksums-end', 'off', @steps);
+
+note('TEST INPROGRESS-ON/2');
+@steps = qw(create-checkpoint-initial
+			 datachecksums-enable-inprogress-checksums-before-xlog
+			 datachecksums-enable-inprogress-checksums-after-xlog
+			 checkpoint-before-redo
+			 checkpoint-before-xlogctl-checksums
+			 datachecksums-enable-inprogress-checksums-after-xlogctl
+			 datachecksums-enable-inprogress-checksums-after-controlfile
+			 checkpoint-after-xlogctl-checksums
+			 checkpoint-before-redo-checksums
+			 checkpoint-before-redo-wal
+			 checkpoint-after-redo-wal
+			 checkpoint-before-old-wal-removal);
+test_checksum_sequence('disabled', 'enable', 'datachecksums-enable-inprogress-checksums-start', 'datachecksums-enable-inprogress-checksums-end', 'off', @steps);
+
+note('TEST INPROGRESS-ON/3');
+@steps = qw(create-checkpoint-initial
+			 datachecksums-enable-inprogress-checksums-before-xlog
+			 datachecksums-enable-inprogress-checksums-after-xlog
+			 checkpoint-before-redo
+			 checkpoint-before-xlogctl-checksums
+			 checkpoint-after-xlogctl-checksums
+			 checkpoint-before-redo-checksums
+			 checkpoint-before-redo-wal
+			 datachecksums-enable-inprogress-checksums-after-xlogctl
+			 datachecksums-enable-inprogress-checksums-after-controlfile
+			 checkpoint-after-redo-wal
+			 checkpoint-before-old-wal-removal);
+test_checksum_sequence('disabled', 'enable', 'datachecksums-enable-inprogress-checksums-start', 'datachecksums-enable-inprogress-checksums-end', 'off', @steps);
+
+note('TEST INPROGRESS-ON/4');
+@steps = qw(create-checkpoint-initial
+			 datachecksums-enable-inprogress-checksums-before-xlog
+			 datachecksums-enable-inprogress-checksums-after-xlog
+			 checkpoint-before-redo
+			 checkpoint-before-xlogctl-checksums
+			 checkpoint-before-redo-checksums
+			 checkpoint-before-redo-wal
+			 checkpoint-after-redo-wal
+			 datachecksums-enable-inprogress-checksums-after-xlogctl
+			 datachecksums-enable-inprogress-checksums-after-controlfile
+			 checkpoint-before-old-wal-removal);
+test_checksum_sequence('disabled', 'enable', 'datachecksums-enable-inprogress-checksums-start', 'datachecksums-enable-inprogress-checksums-end', 'off', @steps);
+
+note('TEST INPROGRESS-ON/5');
+@steps = qw(create-checkpoint-initial
+			 datachecksums-enable-inprogress-checksums-before-xlog
+			 datachecksums-enable-inprogress-checksums-after-xlog
+			 checkpoint-before-redo
+			 checkpoint-before-xlogctl-checksums
+			 checkpoint-after-xlogctl-checksums
+			 checkpoint-before-redo-checksums
+			 checkpoint-before-redo-wal
+			 checkpoint-after-redo-wal
+			 checkpoint-before-old-wal-removal
+			 datachecksums-enable-inprogress-checksums-after-xlogctl
+			 datachecksums-enable-inprogress-checksums-after-controlfile);
+test_checksum_sequence('disabled', 'enable', 'datachecksums-enable-inprogress-checksums-start', 'datachecksums-enable-inprogress-checksums-end', 'off', @steps);
+
+note('TEST INPROGRESS-ON/6');
+@steps = qw(create-checkpoint-initial
+			 checkpoint-before-redo
+			 checkpoint-before-xlogctl-checksums
+			 checkpoint-after-xlogctl-checksums
+			 checkpoint-before-redo-checksums
+			 datachecksums-enable-inprogress-checksums-before-xlog
+			 datachecksums-enable-inprogress-checksums-after-xlog
+			 datachecksums-enable-inprogress-checksums-after-xlogctl
+			 datachecksums-enable-inprogress-checksums-after-controlfile
+			 checkpoint-before-redo-wal
+			 checkpoint-after-redo-wal
+			 checkpoint-before-old-wal-removal);
+test_checksum_sequence('disabled', 'enable', 'datachecksums-enable-inprogress-checksums-start', 'datachecksums-enable-inprogress-checksums-end', 'off', @steps);
+
+## checksums ON
+
+note('TEST ON/1');
+@steps = qw(datachecksums-enable-checksums-before-xlog
+			 datachecksums-enable-checksums-after-xlog
+			 datachecksums-enable-checksums-after-xlogctl
+			 datachecksums-enable-checksums-after-controlfile
+			 create-checkpoint-initial
+			 checkpoint-before-xlogctl-checksums
+			 checkpoint-after-xlogctl-checksums
+			 checkpoint-before-redo-checksums
+			 checkpoint-before-redo-wal
+			 checkpoint-after-redo-wal
+			 checkpoint-before-old-wal-removal);
+test_checksum_sequence('disabled', 'enable', 'datachecksums-enable-checksums-start', 'datachecksums-enable-checksums-end', 'on', @steps);
+
+note('TEST ON/2');
+@steps = qw(create-checkpoint-initial
+			 datachecksums-enable-checksums-before-xlog
+			 datachecksums-enable-checksums-after-xlog
+			 checkpoint-before-redo
+			 checkpoint-before-xlogctl-checksums
+			 checkpoint-after-xlogctl-checksums
+			 checkpoint-before-redo-checksums
+			 checkpoint-before-redo-wal
+			 checkpoint-after-redo-wal
+			 checkpoint-before-old-wal-removal
+			 datachecksums-enable-checksums-after-xlogctl
+			 datachecksums-enable-checksums-after-controlfile);
+test_checksum_sequence('disabled', 'enable', 'datachecksums-enable-checksums-start', 'datachecksums-enable-checksums-end', 'on', @steps);
+
+note('TEST ON/3');
+@steps = qw(create-checkpoint-initial
+			 checkpoint-before-redo
+			 checkpoint-before-xlogctl-checksums
+			 checkpoint-after-xlogctl-checksums
+			 checkpoint-before-redo-checksums
+			 datachecksums-enable-checksums-before-xlog
+			 datachecksums-enable-checksums-after-xlog
+			 checkpoint-before-redo-wal
+			 checkpoint-after-redo-wal
+			 checkpoint-before-old-wal-removal
+			 datachecksums-enable-checksums-after-xlogctl
+			 datachecksums-enable-checksums-after-controlfile);
+test_checksum_sequence('disabled', 'enable', 'datachecksums-enable-checksums-start', 'datachecksums-enable-checksums-end', 'on', @steps);
+
+note('TEST ON/4');
+@steps = qw(create-checkpoint-initial
+			 checkpoint-before-redo
+			 checkpoint-before-xlogctl-checksums
+			 checkpoint-after-xlogctl-checksums
+			 checkpoint-before-redo-checksums
+			 checkpoint-before-redo-wal
+			 datachecksums-enable-checksums-before-xlog
+			 datachecksums-enable-checksums-after-xlog
+			 checkpoint-after-redo-wal
+			 checkpoint-before-old-wal-removal
+			 datachecksums-enable-checksums-after-xlogctl
+			 datachecksums-enable-checksums-after-controlfile);
+test_checksum_sequence('disabled', 'enable', 'datachecksums-enable-checksums-start', 'datachecksums-enable-checksums-end', 'on', @steps);
+
+
+## checksums INPROGRESS OFF
+
+note('TEST INPROGRESS-OFF/1');
+@steps = qw(datachecksums-disable-inprogress-checksums-before-xlog
+			 datachecksums-disable-inprogress-checksums-after-xlog
+			 datachecksums-disable-inprogress-checksums-after-xlogctl
+			 datachecksums-disable-inprogress-checksums-after-controlfile
+			 datachecksums-disable-inprogress-checksums-before-checkpoint
+			 create-checkpoint-initial
+			 checkpoint-before-redo
+			 checkpoint-before-xlogctl-checksums
+			 checkpoint-after-xlogctl-checksums
+			 checkpoint-before-redo-checksums
+			 checkpoint-before-redo-wal
+			 checkpoint-after-redo-wal
+			 checkpoint-before-old-wal-removal
+			 datachecksums-disable-inprogress-checksums-before-barrier-wait);
+test_checksum_sequence('enabled', 'disable', 'datachecksums-disable-inprogress-checksums-start', 'datachecksums-disable-checksums-start', 'off', @steps);
+
+note('TEST INPROGRESS-OFF/2');
+@steps = qw(create-checkpoint-initial
+			 datachecksums-disable-inprogress-checksums-before-xlog
+			 datachecksums-disable-inprogress-checksums-after-xlog
+			 checkpoint-before-redo
+			 checkpoint-before-xlogctl-checksums
+			 checkpoint-after-xlogctl-checksums
+			 checkpoint-before-redo-checksums
+			 checkpoint-before-redo-wal
+			 checkpoint-after-redo-wal
+			 checkpoint-before-old-wal-removal
+			 datachecksums-disable-inprogress-checksums-after-xlogctl
+			 datachecksums-disable-inprogress-checksums-after-controlfile
+			 datachecksums-disable-inprogress-checksums-before-checkpoint
+			 datachecksums-disable-inprogress-checksums-before-barrier-wait);
+test_checksum_sequence('enabled', 'disable', 'datachecksums-disable-inprogress-checksums-start', 'datachecksums-disable-checksums-start', 'off', @steps);
+
+note('TEST INPROGRESS-OFF/3');
+@steps = qw(create-checkpoint-initial
+			 checkpoint-before-redo
+			 checkpoint-before-xlogctl-checksums
+			 checkpoint-after-xlogctl-checksums
+			 checkpoint-before-redo-checksums
+			 datachecksums-disable-inprogress-checksums-before-xlog
+			 datachecksums-disable-inprogress-checksums-after-xlog
+			 checkpoint-before-redo-wal
+			 checkpoint-after-redo-wal
+			 checkpoint-before-old-wal-removal
+			 datachecksums-disable-inprogress-checksums-after-xlogctl
+			 datachecksums-disable-inprogress-checksums-after-controlfile
+			 datachecksums-disable-inprogress-checksums-before-checkpoint
+			 datachecksums-disable-inprogress-checksums-before-barrier-wait);
+test_checksum_sequence('enabled', 'disable', 'datachecksums-disable-inprogress-checksums-start', 'datachecksums-disable-checksums-start', 'off', @steps);
+
+note('TEST INPROGRESS-OFF/4');
+@steps = qw(create-checkpoint-initial
+			 checkpoint-before-redo
+			 checkpoint-before-xlogctl-checksums
+			 checkpoint-after-xlogctl-checksums
+			 checkpoint-before-redo-checksums
+			 datachecksums-disable-inprogress-checksums-before-xlog
+			 datachecksums-disable-inprogress-checksums-after-xlog
+			 datachecksums-disable-inprogress-checksums-after-xlogctl
+			 datachecksums-disable-inprogress-checksums-after-controlfile
+			 datachecksums-disable-inprogress-checksums-before-checkpoint
+			 checkpoint-before-redo-wal
+			 checkpoint-after-redo-wal
+			 checkpoint-before-old-wal-removal
+			 datachecksums-disable-inprogress-checksums-before-barrier-wait);
+test_checksum_sequence('enabled', 'disable', 'datachecksums-disable-inprogress-checksums-start', 'datachecksums-disable-checksums-start', 'off', @steps);
+
+## checksums OFF
+
+note('TEST OFF/1');
+@steps = qw(datachecksums-disable-checksums-before-xlog
+			 datachecksums-disable-checksums-after-xlog
+			 datachecksums-disable-checksums-after-xlogctl
+			 datachecksums-disable-checksums-after-controlfile
+			 datachecksums-disable-checksums-before-checkpoint
+			 create-checkpoint-initial
+			 checkpoint-before-redo
+			 checkpoint-before-xlogctl-checksums
+			 checkpoint-after-xlogctl-checksums
+			 checkpoint-before-redo-checksums
+			 checkpoint-before-redo-wal
+			 checkpoint-after-redo-wal
+			 checkpoint-before-old-wal-removal
+			 datachecksums-disable-checksums-before-barrier-wait);
+test_checksum_sequence('enabled', 'disable', 'datachecksums-disable-checksums-start', 'datachecksums-disable-checksums-end', 'off', @steps);
+
+note('TEST OFF/2');
+@steps = qw(create-checkpoint-initial
+			 datachecksums-disable-checksums-before-xlog
+			 datachecksums-disable-checksums-after-xlog
+			 checkpoint-before-redo
+			 checkpoint-before-xlogctl-checksums
+			 checkpoint-after-xlogctl-checksums
+			 datachecksums-disable-checksums-after-xlogctl
+			 datachecksums-disable-checksums-after-controlfile
+			 datachecksums-disable-checksums-before-checkpoint
+			 checkpoint-before-redo-checksums
+			 checkpoint-before-redo-wal
+			 checkpoint-after-redo-wal
+			 checkpoint-before-old-wal-removal
+			 datachecksums-disable-checksums-before-barrier-wait);
+test_checksum_sequence('enabled', 'disable', 'datachecksums-disable-checksums-start', 'datachecksums-disable-checksums-end', 'off', @steps);
+
+note('TEST OFF/3');
+@steps = qw(create-checkpoint-initial
+			 checkpoint-before-redo
+			 checkpoint-before-xlogctl-checksums
+			 checkpoint-after-xlogctl-checksums
+			 checkpoint-before-redo-checksums
+			 datachecksums-disable-checksums-before-xlog
+			 datachecksums-disable-checksums-after-xlog
+			 checkpoint-before-redo-wal
+			 checkpoint-after-redo-wal
+			 datachecksums-disable-checksums-after-xlogctl
+			 datachecksums-disable-checksums-after-controlfile
+			 datachecksums-disable-checksums-before-checkpoint
+			 checkpoint-before-old-wal-removal
+			 datachecksums-disable-checksums-before-barrier-wait);
+test_checksum_sequence('enabled', 'disable', 'datachecksums-disable-checksums-start', 'datachecksums-disable-checksums-end', 'off', @steps);
+
+note('TEST OFF/4');
+@steps = qw(create-checkpoint-initial
+			 checkpoint-before-redo
+			 checkpoint-before-xlogctl-checksums
+			 checkpoint-after-xlogctl-checksums
+			 checkpoint-before-redo-checksums
+			 checkpoint-before-redo-wal
+			 checkpoint-after-redo-wal
+			 datachecksums-disable-checksums-before-xlog
+			 datachecksums-disable-checksums-after-xlog
+			 datachecksums-disable-checksums-after-xlogctl
+			 datachecksums-disable-checksums-after-controlfile
+			 datachecksums-disable-checksums-before-checkpoint
+			 checkpoint-before-old-wal-removal
+			 datachecksums-disable-checksums-before-barrier-wait);
+test_checksum_sequence('enabled', 'disable', 'datachecksums-disable-checksums-start', 'datachecksums-disable-checksums-end', 'off', @steps);
+
+note('TEST OFF/5');
+@steps = qw(create-checkpoint-initial
+			 checkpoint-before-redo
+			 checkpoint-before-xlogctl-checksums
+			 checkpoint-after-xlogctl-checksums
+			 checkpoint-before-redo-checksums
+			 datachecksums-disable-checksums-before-xlog
+			 datachecksums-disable-checksums-after-xlog
+			 checkpoint-before-redo-wal
+			 checkpoint-after-redo-wal
+			 datachecksums-disable-checksums-after-xlogctl
+			 datachecksums-disable-checksums-after-controlfile
+			 datachecksums-disable-checksums-before-checkpoint
+			 checkpoint-before-old-wal-removal
+			 datachecksums-disable-checksums-before-barrier-wait);
+test_checksum_sequence('enabled', 'disable', 'datachecksums-disable-checksums-start', 'datachecksums-disable-checksums-end', 'off', @steps);
+
+note('TEST OFF/6');
+@steps = qw(create-checkpoint-initial
+			 datachecksums-disable-checksums-before-xlog
+			 datachecksums-disable-checksums-after-xlog
+			 checkpoint-before-redo
+			 checkpoint-before-xlogctl-checksums
+			 checkpoint-after-xlogctl-checksums
+			 datachecksums-disable-checksums-after-xlogctl
+			 checkpoint-before-redo-checksums
+			 checkpoint-before-redo-wal
+			 checkpoint-after-redo-wal
+			 datachecksums-disable-checksums-after-controlfile
+			 datachecksums-disable-checksums-before-checkpoint
+			 checkpoint-before-old-wal-removal
+			 datachecksums-disable-checksums-before-barrier-wait);
+test_checksum_sequence('enabled', 'disable', 'datachecksums-disable-checksums-start', 'datachecksums-disable-checksums-end', 'off', @steps);
+
+$node->stop;
+done_testing();
-- 
2.54.0

From ff3cd3e661d508db102e18752fee3335fb4ceaca Mon Sep 17 00:00:00 2001
From: test <test>
Date: Fri, 1 May 2026 13:35:56 +0200
Subject: [PATCH 09/13] TAP 14: checkpoints with crashes

This shows the non-determinism / race condition with the step in test
012_crashes, thanks to better sequencing of the two sides (checksum
worker + checkpointer). The related 012 test is this:

test_checksum_transition('disabled', 'enable',
                         'datachecksums-enable-checksums-start',
                         'datachecksums-enable-checksums-after-xlog',
                         'datachecksums-enable-checksums-after-xlogctl',
                         'off');
---
 ...ncurrent_checkpoint_crash_deterministic.pl | 359 ++++++++++++++++++
 1 file changed, 359 insertions(+)
 create mode 100644 src/test/modules/test_checksums/t/014_concurrent_checkpoint_crash_deterministic.pl

diff --git a/src/test/modules/test_checksums/t/014_concurrent_checkpoint_crash_deterministic.pl b/src/test/modules/test_checksums/t/014_concurrent_checkpoint_crash_deterministic.pl
new file mode 100644
index 00000000000..bfca3983965
--- /dev/null
+++ b/src/test/modules/test_checksums/t/014_concurrent_checkpoint_crash_deterministic.pl
@@ -0,0 +1,359 @@
+
+# Copyright (c) 2026, PostgreSQL Global Development Group
+
+# Test suite for testing enabling data checksums in an online cluster with
+# injection point tests injecting failures into the processing
+
+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, or very expensive, to execute.  There are two
+# PG_TEST_EXTRA options for running it, "checksum" for a pared-down test suite
+# an "checksum_extended" for the full suite.  The full suite can run for hours
+# on slow or constrained systems.
+my $extended = undef;
+if ($ENV{PG_TEST_EXTRA})
+{
+	$extended = 1 if ($ENV{PG_TEST_EXTRA} =~ /\bchecksum_extended\b/);
+	plan skip_all => 'Expensive data checksums test disabled'
+	  unless ($ENV{PG_TEST_EXTRA} =~ /\bchecksum(_extended)?\b/);
+}
+
+if ($ENV{enable_injection_points} ne 'yes')
+{
+	plan skip_all => 'Injection points not supported by this build';
+}
+
+# ---------------------------------------------------------------------------
+# Test cluster setup
+#
+
+# Initiate testcluster
+my $node = PostgreSQL::Test::Cluster->new('injection_node');
+$node->init(no_data_checksums => 1);
+$node->start;
+
+# Set up test environment
+$node->safe_psql('postgres', 'CREATE EXTENSION test_checksums;');
+$node->safe_psql('postgres', 'CREATE EXTENSION injection_points;');
+
+my $pgbench = undef;
+my $scalefactor = ($extended ? 10 : 1);
+my $node_loglocation = 0;
+
+$node->command_ok(
+	[
+		'pgbench', '-p', $node->port, '-i',
+		'-s', $scalefactor, '-q', 'postgres'
+	]);
+
+# Start a pgbench run in the background against the server specified via the
+# port passed as parameter.
+sub background_rw_pgbench
+{
+	my $port = shift;
+
+	# If a previous pgbench is still running, start by shutting it down.
+	$pgbench->finish if $pgbench;
+
+	my $clients = 1;
+	my $runtime = 2;
+
+	if ($extended)
+	{
+		# Randomize the number of pgbench clients a bit (range 1-16)
+		$clients = 1 + int(rand(15));
+		$runtime = 600;
+	}
+	my @cmd = ('pgbench', '-p', $port, '-T', $runtime, '-c', $clients);
+
+	# Randomize whether we spawn connections or not
+	push(@cmd, '-C') if ($extended && cointoss);
+	# Finally add the database name to use
+	push(@cmd, 'postgres');
+
+	$pgbench = IPC::Run::start(
+		\@cmd,
+		'<' => '/dev/null',
+		'>' => '/dev/null',
+		'2>' => '/dev/null',
+		IPC::Run::timer($PostgreSQL::Test::Utils::timeout_default));
+}
+
+# Run a SQL in the background against the server specified via the
+# port passed as parameter.
+sub background_sql
+{
+	my ($port, $sql) = @_;
+
+	my @cmd = ('psql', '-p', $port, '-c', $sql, 'postgres');
+
+	$pgbench = IPC::Run::start(
+		\@cmd,
+		'<' => '/dev/null',
+		'>' => '/dev/null',
+		'2>' => '/dev/null',
+		IPC::Run::timer($PostgreSQL::Test::Utils::timeout_default));
+}
+
+sub attach_injection_point
+{
+	my ($node, $point) = @_;
+	note('attaching injection point: ' . $point);
+	$node->safe_psql('postgres',
+		"SELECT injection_points_attach('" . $point . "','wait');"
+	);
+}
+
+sub wait_injection_point
+{
+	my ($node, $point) = @_;
+
+	note("waiting for the injection point to be hit");
+	$node->poll_query_until(
+		'postgres',
+		"SELECT COUNT(*) FROM pg_catalog.pg_stat_activity WHERE wait_event = '" . $point . "'",
+		'1');
+}
+
+sub wakeup_injection_point
+{
+	my ($node, $point) = @_;
+
+	# detach before wakeup, so that we can't hit it right away
+	note("detaching the init injection point");
+	$node->safe_psql('postgres',
+		"SELECT injection_points_detach('" . $point . "');");
+
+	note('waking the init injection point: ' . $point);
+	$node->safe_psql('postgres',
+		"SELECT injection_points_wakeup('" . $point . "');"
+	);
+}
+
+# Test behavior with a checksum transitions and a concurrent checkpoint,
+# followed by a crash.
+#
+# The test puts the instance into the initial checksum state $start,
+# triggers a checksum change. A concurrent checkpoint is performed, and
+# and the test steps through the checksum change and the checkpoint in
+# a deterministic way.
+#
+# Then the instance get restarted in immediate mode to simulate failure,
+# and the final checksum state (after recovery) is validated against the
+# expected value. The server log is checked for checksum failures.
+#
+# While the checksum change is happening, there's a r/w pgbench running in
+# the background, to generate writes.
+#
+# arguments:
+#
+# - name   - name of the test sequence (for easy identification)
+# - start  - start checksum state (enabled/disabled)
+# - change - checksum change to initiate
+# - final  - expected checksum state at the end
+# - steps  - steps to go through (wait, wakeup, sql)
+#
+# XXX This is similar to 012_concurrent_checkpoint_crash, except that the
+# checkpoint happens asynchronously (in the background), and the steps
+# step through the injection points in a deterministic way.
+#
+# XXX Some of the injection points are in a critical section, which does
+# not allow memory allocations etc. INJECTION_POINT_LOAD/_CACHED handles
+# just private memory allocation, but 'wait' requires a shmem allocation.
+# To deal with that, we setup an injection point $init outside a critical
+# section to initialize the shmem stuff needed by 'wait'. This needs to
+# be done for individual processes (e.g. the checkpointer needs to init a
+# 'wait' point too, it's not enough to init one in the checksum worker).
+#
+# XXX We could also validate the checksums using pg_checksums, if the
+# state is 'on'.
+sub test_checksum_sequence
+{
+	my ($name, $start, $change, $final, @steps) = @_;
+
+	# Start the test suite with pgbench running.
+	background_rw_pgbench($node->port);
+
+	# print the current test instructions, both into TAP output and into
+	# the server log, to make correlation easier
+
+	note($name);
+	$node->safe_psql('postgres',
+		"SELECT '========== " . $name . " =========='");
+
+	# put the cluster into the initial checksum state, synchronously
+	note('changing checksums into initial state: ' . $start);
+	enable_data_checksums($node, wait => 'on') if ($start eq 'enabled');
+	disable_data_checksums($node, wait => 'off') if ($start eq 'disabled');
+
+	# attach all the injection points mentioned in 'wait' steps
+	my $n = @steps;
+	my $s;
+	note('processing ' . $n . ' steps');
+	for ($s = 0; $s < $n; $s++)
+	{
+		my @step = $steps[$s];
+		my $action = $steps[$s][0];
+		my $value = $steps[$s][1];
+
+		note('step ' . $s . ' action ' . $action . ' / ' . $value);
+		if ($action eq 'wait')
+		{
+			attach_injection_point($node, $value);
+		}
+	}
+
+	# Trigger the checksum change, asynchronously
+	note("triggering checksum change: " . $change);
+	enable_data_checksums($node) if ($change eq 'enable');
+	disable_data_checksums($node) if ($change eq 'disable');
+
+	# now process all the steps - wait, wakeup, sql, etc.
+	$n = @steps;
+	note('processing ' . $n . ' steps');
+	for ($s = 0; $s < $n; $s++)
+	{
+		my @step = $steps[$s];
+		my $action = $steps[$s][0];
+		my $value = $steps[$s][1];
+
+		note('step ' . $s . ' action ' . $action . ' / ' . $value);
+		if ($action eq 'wait')
+		{
+			wait_injection_point($node, $value);
+		}
+		elsif ($action eq 'wakeup')
+		{
+			wakeup_injection_point($node, $value);
+		}
+		elsif ($action eq 'sql')
+		{
+			note('sql: ' . $value);
+
+			# initiate a background sql
+			background_sql($node->port, $value);
+		}
+	}
+
+	# restart the cluster, in immediate mode, to simulate a crash
+	$node->stop('immediate');
+	$node->start;
+
+	# Does the final checksum state match the expected state?
+	test_checksum_state($node, $final);
+
+	# Since the log isn't being written to now, parse the log and check
+	# for instances of checksum verification failures.
+	my $log = PostgreSQL::Test::Utils::slurp_file($node->logfile,
+		$node_loglocation);
+	unlike(
+		$log,
+		qr/page verification failed,.+\d$/,
+		"no checksum validation errors in primary log (during WAL recovery)"
+	);
+	$node_loglocation = -s $node->logfile;
+}
+
+# sequence of steps to perform, each step is defined as an array of actions
+#
+# - type of an action (wait, wakeup, sql)
+# - injection point or SQL command
+#
+# after processing all commands, the instance gets killed / shut down with
+# immediate mode
+my @steps = undef;
+
+# CHECKSUMS-ON-1
+#
+# checkpoint after: datachecksums-enable-checksums-after-xlog
+# crash after: datachecksums-enable-checksums-after-xlogctl
+# checkpoint completes
+#
+# steps:
+# 1) checksums:    write XLOG2_CHECKSUMS to WAL
+# 2) start checkpoint
+# 3) checkpointer: read XLogCtl->data_checksum_version
+# 4) checksums:    update XLogCtl->data_checksum_version
+# 5) checkpointer: write CHECKPOINT_REDO
+# 6) checkpointer: complete the checkpoint
+# 7) crash
+@steps = (
+    ['wait', 'datachecksums-enable-checksums-start'],	# initialize the wait
+    ['wakeup', 'datachecksums-enable-checksums-start'],
+    ['wait', 'datachecksums-enable-checksums-after-xlog'],
+    ['sql', 'checkpoint'],
+    ['wait', 'create-checkpoint-initial'],				# initialize the wait
+    ['wakeup', 'create-checkpoint-initial'],
+    ['wait', 'checkpoint-before-redo-wal'],
+    ['wakeup', 'datachecksums-enable-checksums-after-xlog'],
+    ['wait', 'datachecksums-enable-checksums-after-xlogctl'],
+    ['wakeup', 'checkpoint-before-redo-wal'],
+    ['wait', 'checkpoint-before-old-wal-removal'],
+    ['wakeup', 'checkpoint-before-old-wal-removal']
+);
+
+test_checksum_sequence('CHECKSUMS-ON-1', 'disabled', 'enable', 'off', @steps);
+
+# CHECKSUMS-ON-2
+#
+# checkpoint after: datachecksums-enable-checksums-after-xlog
+# crash after: datachecksums-enable-checksums-after-xlogctl
+# checkpoint completes
+#
+# steps:
+# 1) checksums:    write XLOG2_CHECKSUMS to WAL
+# 2) start checkpoint
+# 3) checksums:    update XLogCtl->data_checksum_version
+# 4) checkpointer: read XLogCtl->data_checksum_version
+# 5) checkpointer: write CHECKPOINT_REDO
+# 6) checkpointer: complete the checkpoint
+# 7) crash
+@steps = (
+    ['wait', 'datachecksums-enable-checksums-start'],	# initialize the wait
+    ['wakeup', 'datachecksums-enable-checksums-start'],
+    ['wait', 'datachecksums-enable-checksums-after-xlog'],
+    ['sql', 'checkpoint'],
+    ['wait', 'create-checkpoint-initial'],				# initialize the wait
+    ['wakeup', 'datachecksums-enable-checksums-after-xlog'],
+    ['wakeup', 'create-checkpoint-initial'],
+    ['wait', 'checkpoint-before-xlogctl-checksums'],
+    ['wait', 'datachecksums-enable-checksums-after-xlogctl'],
+    ['wakeup', 'checkpoint-before-xlogctl-checksums'],
+    ['wait', 'checkpoint-before-old-wal-removal'],
+    ['wakeup', 'checkpoint-before-old-wal-removal']
+);
+
+test_checksum_sequence('CHECKSUMS-ON-2', 'disabled', 'enable', 'off', @steps);
+
+# CHECKSUMS-ON-3
+#
+# checkpoint happens after checksum worker updates XLogCtl
+@steps = (
+    ['wait', 'datachecksums-enable-checksums-start'],	# initialize the wait
+    ['wakeup', 'datachecksums-enable-checksums-start'],
+    ['wait', 'datachecksums-enable-checksums-after-xlogctl'],
+    ['sql', 'checkpoint'],
+    ['wait', 'create-checkpoint-initial'],				# initialize the wait
+    ['wakeup', 'create-checkpoint-initial'],
+    ['wait', 'checkpoint-before-redo-checksums'],
+    ['wakeup', 'checkpoint-before-redo-checksums'],
+    ['wait', 'checkpoint-before-old-wal-removal'],
+    ['wakeup', 'checkpoint-before-old-wal-removal']
+);
+
+test_checksum_sequence('CHECKSUMS-ON-3', 'disabled', 'enable', 'off', @steps);
+
+## FIXME do similar sequences for the opposite direction (enabled -> disabled)
+
+$node->stop;
+done_testing();
-- 
2.54.0

---

From: SATYANARAYANA NARLAPURAM <[email-masked]>
Date: Tue, 5 May 2026 00:43:03 -0700

Hi Hackers, Daniel,

Further testing this feature, I noticed that the cost_delay and cost_limit
arguments
 to pg_enable_data_checksums() in practice have no effect.

It appears we have two independent issues in DataChecksumsWorkerMain():

(1) The worker writes the user-supplied values to VacuumCostDelay and
VacuumCostLimit (the GUC-bound globals in globals.c). However,
vacuum_delay_point() reads vacuum_cost_delay / vacuum_cost_limit
declared in vacuum.c. The two pairs are kept in sync only by
VacuumUpdateCosts(), which the worker never calls. Therefore, the napping
formula always sees the defaults (vacuum_cost_delay = 0) and never
sleeps.

(2) The worker also resets VacuumCostPageHit/Miss/Dirty to 0 at startup.
With all per-page weights at zero, VacuumCostBalance never reaches
vacuum_cost_limit, which would defeat the throttling on its own even
if (1) were fixed.

Repro:

Create a database and load data (say 3 GB)

SELECT pg_disable_data_checksums();
SELECT pg_enable_data_checksums(100, 1);   -- 100 ms/page, balance limit 1

Without the fix, this completes in ~10 seconds and pg_stat_activity
never shows wait_event = VacuumDelay for the worker. With even moderate
parameters (e.g. (50, 200)) the worker is continuously in VacuumDelay
after the patch, and total runtime stretches as one would expect.
Also manually tested with cost_delay 0 and higher cost limits.

Attached a patch to fix this.

Thanks,
Satya

---

From: Daniel Gustafsson <[email-masked]>
Date: Tue, 5 May 2026 15:46:03 +0200

> init fork on any recovery. 

I've tested your patch, and also expanded the test you wrote a little with a
persistence change.

> Further testing this feature, I noticed that the cost_delay and cost_limit arguments

Ugh, the API for updating the costs changed between when this code was written
and tested, and when it was committed (and since I was the one committing the
new API I really should've caught that). Thanks for the report and fix!

While hacking on your patches I realized that the regexes for finding page
verification failures in the logs were anchoring at the wrong point, the
attached 0003 fixes that.

Attached are editorialized versions of the patches, as well as my testfix, that
I'm planning to go ahead with.

--
Daniel Gustafsson

---

From: SATYANARAYANA NARLAPURAM <[email-masked]>
Date: Tue, 5 May 2026 07:24:07 -0700

Hi,

> new API I really should've caught that). Thanks for the report and fix!

Any thoughts on adding an injection point test to verify the values are
configured correctly?
This can be a different patch, perhaps included in Tomas' tests?

> I'm planning to go ahead with.

LGTM. Thanks!

Thanks,
Satya

---

From: Ayush Tiwari <[email-masked]>
Date: Tue, 5 May 2026 20:51:22 +0530

Hi,

> I've tested your patch, and also expanded the test you wrote a little with
I tested the patches, it mostly looks good.

I've a small concern in 0001.  The new guard uses only
RelationNeedsWAL(reln),
but ProcessSingleRelationByOid() iterates all forks.  For unlogged
relations,
the init fork is special, there are several existing call sites that
preserve
WAL for INIT_FORKNUM, for example using

  RelationNeedsWAL(rel) || forknum == INIT_FORKNUM

and catalog/storage.c notes that unlogged init forks need WAL and sync.

So I think the condition in ProcessSingleRelationFork() should preserve the
init-fork case, e.g.

  if (RelationNeedsWAL(reln) || forkNum == INIT_FORKNUM)
      log_newpage_buffer(buf, false);

It would also be good to extend the new test so it exercises a non-empty
init
fork, perhaps with an unlogged table that has an index, and then verifies
the
standby/recovery behavior.

0002 and 0003 look good to me.

Regards,
Ayush

---

From: Daniel Gustafsson <[email-masked]>
Date: Tue, 5 May 2026 21:08:30 +0200

> On 5 May 2026, at 17:21, Ayush Tiwari <[email-masked]> wrote:

> I've a small concern in 0001.  The new guard uses only RelationNeedsWAL(reln),
> but ProcessSingleRelationByOid() iterates all forks.  For unlogged relations,
> the init fork is special, there are several existing call sites that preserve

Which failure scenario are you thinking about here?  When dealing with the
catalog relation I can see the need but here we are reading, and writing, data
pages.  In which case would we need to issue an FPI for an unlogged relation
init fork? I might be missing something obvious here.

Thanks for looking!

--
Daniel Gustafsson

---

From: Ayush Tiwari <[email-masked]>
Date: Wed, 6 May 2026 00:48:53 +0530

Hi,

> init fork? I might be missing something obvious here.

The case I was thinking about is not the unlogged relation contents
themselves, but the init fork used as the reset template.  Some unlogged
indexes can have initialized pages in the init fork, and recovery later
copies
that fork to the main fork when resetting unlogged relations.

So my concern was that, during online checksum enable, we might update the
checksum state of an init-fork page on the primary but not WAL-log an FPI
for
it because RelationNeedsWAL(reln) is false.  Then a standby, or recovery
after
a crash, could still have the old version of that init fork.  If that fork
is
later copied to the main fork after checksums are enabled, it might lead to
checksum verification failures?

Maybe there is another guarantee that makes this impossible, but I did not
see
it from the patch/test.  That is why I wondered whether the condition should
preserve the existing special treatment for INIT_FORKNUM.

Regards,
Ayush

---

From: SATYANARAYANA NARLAPURAM <[email-masked]>
Date: Tue, 5 May 2026 12:19:00 -0700

Hi

> init fork? I might be missing something obvious here.

Good catch,IIUC init page has valid checksum and when we set checksum on
primary I think we need to pass it to standby. Otherwise, after failover we
may see invalid checksum for unlogged relations. I haven’t validated it,
will test it and update the patch.

---

From: SATYANARAYANA NARLAPURAM <[email-masked]>
Date: Tue, 5 May 2026 12:45:09 -0700

Hi,

> preserve the existing special treatment for INIT_FORKNUM.

It is a bug in the code, I added a test in the v2 patch to test this
scenario and the test
failed earlier.

Thanks,
Satya

---

From: SATYANARAYANA NARLAPURAM <[email-masked]>
Date: Tue, 5 May 2026 13:05:51 -0700

Hi,

On Tue, May 5, 2026 at 12:45 PM SATYANARAYANA NARLAPURAM <
[email-masked]> wrote:

>> a crash, could still have the old version of that init fork.  If that
>> fork is
>> Maybe there is another guarantee that makes this impossible, but I did
>> not see

Please find the latest v3. Earlier I took dependency on amcheck and removed
it in v3
and instead added queries that forces necessary checks.

 Thanks,
Satya

---

From: Ayush Tiwari <[email-masked]>
Date: Wed, 6 May 2026 02:31:59 +0530

Hi,

On Wed, 6 May 2026 at 01:36, SATYANARAYANA NARLAPURAM <
[email-masked]> wrote:

>>> checksum state of an init-fork page on the primary but not WAL-log an
>>> FPI for
> Please find the latest v3. Earlier I took dependency on amcheck and
> removed it in v3

Thanks, the v3 patch addresses my concern.

I also checked the new test by
temporarily removing the INIT_FORKNUM exception, and it failed on the
promoted
standby while reading the unlogged relation, so the test does cover the init
fork failure scenario.

The code change looks right to me now.

Regards,
Ayush

---

From: Daniel Gustafsson <[email-masked]>
Date: Wed, 6 May 2026 17:22:47 +0200

> On 5 May 2026, at 23:01, Ayush Tiwari <[email-masked]> wrote:

> Thanks, the v3 patch addresses my concern.

Thanks both of you for the report, patch and testing.  Now I see what you meant
yesterday and I agree with the conclusion.  I've pushed 0001-0003 today with minor
tweaks.

--
Daniel Gustafsson

---

From: Tomas Vondra <[email-masked]>
Date: Tue, 26 May 2026 20:12:35 +0200

Hi,

On 5/4/26 15:16, Tomas Vondra wrote:
> ... snip ...

I still have a funny feeling about the "global checksum version" stored
in multiple places with separate locks, and the protocol when updating
them (e.g. which process does that, when, etc.). I haven't found any
fundamental correctness issue, though.

I kept looking for issues, and I realized StartupXLOG may not do the
right thing on the standby after promotion.

      ereport(...);
  }

Consider this sequence of steps:

1) primary/standby cluster has checksums OFF
2) primary starts enabling checksums
3) primary moves to inprogress-on
4) standby receives that and moves to inprogress-on too
5) primary crashes / shuts down / ...
6) standby gets promoted, and does the StartupXLOG thing
7) standby moves from inprogress-on back to off

However, as there's no EmitProcSignalBarrier(), existing backends on the
hot standby won't be notified about the "off" change. And so there will
be a somewhat inconsistent checksum state, with new backends knowing
it's "off", and old backends thinking it's still inprogress-on.

It's not difficult to reproduce this. I guess the existing TAP tests may
not catch this because they always open a new connection when checking
the state, and so see the new (and correct) version.

I believe a similar issue happens for the inprogress-off case a couple
lines later, but I haven't tried reproducing that one.

Ultimately, neither of these cases should cause checksum failures,
because the "correct" new state is "off", and the old backends are not
verifying checksums either.

I suppose this means we should not be updating the checksum state
without emitting the barrier? I think all other places do that.

> ... snip ...

I kept thinking about this non-determinism, and I still think I'm right
about the root cause. The checkpointer and datachecksum worker may
interleave in different ways, affecting the final checksum state. The
existing interlock (or lack of of it) is not sufficient.

To verify this hypothesis, I've done a simple thing - I've introduced a
new LWLock, protecting the "critical part" so that the checkpoint_redo
can't happen in between XLogChecksums() and XLogCtl update. Patch
attached, but I don't propose this for commit, I'm not sure if it's
right (or safe), it's merely a PoC to confirm the issue. But it resolves
the weird cases, simply by prohibiting some of the step sequences (so
some of the tests needed to be commented out, as they'd block).

I'm still not sure if it really is an issue or just an annoyance,
because I've not been able to find a case where it'd lead to checksum
failures (or obviously incorrect final state after recovery).

> say the XLOG2_CHECKSUMS happens before/after the XLOG_CHECKPOINT_REDO.

I still don't understand why this needs DELAY_CHKPT_START ...

I also noticed a couple minor comment issues, per attached patch (this
may need pgindent).

regards

-- 
Tomas Vondra
diff --git a/src/backend/access/transam/xlog.c b/src/backend/access/transam/xlog.c
index 483e3ea77f5..cb1a77aabb8 100644
--- a/src/backend/access/transam/xlog.c
+++ b/src/backend/access/transam/xlog.c
@@ -4772,6 +4772,8 @@ SetDataChecksumsOnInProgress(void)
 
 	INJECTION_POINT_CACHED("datachecksums-enable-inprogress-checksums-before-xlog", NULL);
 
+	LWLockAcquire(ChecksumStateLock, LW_EXCLUSIVE);
+
 	XLogChecksums(PG_DATA_CHECKSUM_INPROGRESS_ON);
 
 	INJECTION_POINT_CACHED("datachecksums-enable-inprogress-checksums-after-xlog", NULL);
@@ -4781,6 +4783,8 @@ SetDataChecksumsOnInProgress(void)
 	XLogCtl->data_checksum_version = PG_DATA_CHECKSUM_INPROGRESS_ON;
 	SpinLockRelease(&XLogCtl->info_lck);
 
+	LWLockRelease(ChecksumStateLock);
+
 	elog(LOG, "SetDataChecksumsOnInProgress XLogCtl->data_checksum_version %u => %u",
 		 data_checksum_version, PG_DATA_CHECKSUM_INPROGRESS_ON);
 
@@ -4875,6 +4879,8 @@ SetDataChecksumsOn(void)
 
 	INJECTION_POINT_CACHED("datachecksums-enable-checksums-before-xlog", NULL);
 
+	LWLockAcquire(ChecksumStateLock, LW_EXCLUSIVE);
+
 	XLogChecksums(PG_DATA_CHECKSUM_VERSION);
 
 	INJECTION_POINT_CACHED("datachecksums-enable-checksums-after-xlog", NULL);
@@ -4884,6 +4890,8 @@ SetDataChecksumsOn(void)
 	XLogCtl->data_checksum_version = PG_DATA_CHECKSUM_VERSION;
 	SpinLockRelease(&XLogCtl->info_lck);
 
+	LWLockRelease(ChecksumStateLock);
+
 	elog(LOG, "SetDataChecksumsOn / XLogCtl->data_checksum_version %u => %u",
 		 data_checksum_version, PG_DATA_CHECKSUM_VERSION);
 
@@ -4980,6 +4988,8 @@ SetDataChecksumsOff(void)
 
 		INJECTION_POINT_CACHED("datachecksums-disable-inprogress-checksums-before-xlog", NULL);
 
+		LWLockAcquire(ChecksumStateLock, LW_EXCLUSIVE);
+
 		XLogChecksums(PG_DATA_CHECKSUM_INPROGRESS_OFF);
 
 		INJECTION_POINT_CACHED("datachecksums-disable-inprogress-checksums-after-xlog", NULL);
@@ -4989,6 +4999,8 @@ SetDataChecksumsOff(void)
 		XLogCtl->data_checksum_version = PG_DATA_CHECKSUM_INPROGRESS_OFF;
 		SpinLockRelease(&XLogCtl->info_lck);
 
+		LWLockRelease(ChecksumStateLock);
+
 		elog(LOG, "SetDataChecksumsOff / XLogCtl->data_checksum_version %u => %u",
 			 data_checksum_version, PG_DATA_CHECKSUM_INPROGRESS_OFF);
 
@@ -5050,6 +5062,8 @@ SetDataChecksumsOff(void)
 
 	INJECTION_POINT_CACHED("datachecksums-disable-checksums-before-xlog", NULL);
 
+	LWLockAcquire(ChecksumStateLock, LW_EXCLUSIVE);
+
 	XLogChecksums(PG_DATA_CHECKSUM_OFF);
 
 	INJECTION_POINT_CACHED("datachecksums-disable-checksums-after-xlog", NULL);
@@ -5059,6 +5073,8 @@ SetDataChecksumsOff(void)
 	XLogCtl->data_checksum_version = PG_DATA_CHECKSUM_OFF;
 	SpinLockRelease(&XLogCtl->info_lck);
 
+	LWLockRelease(ChecksumStateLock);
+
 	elog(LOG, "SetDataChecksumsOff / XLogCtl->data_checksum_version %u => %u",
 		 data_checksum_version, PG_DATA_CHECKSUM_OFF);
 
@@ -7771,6 +7787,9 @@ CreateCheckPoint(int flags)
 
 		INJECTION_POINT_CACHED("checkpoint-before-redo-checksums", NULL);
 
+		/* make XLogChecksums + XLogCtl update atomic */
+		LWLockAcquire(ChecksumStateLock, LW_EXCLUSIVE);
+
 		WALInsertLockAcquire();
 		redo_rec.wal_level = wal_level;
 		SpinLockAcquire(&XLogCtl->info_lck);
@@ -7785,6 +7804,8 @@ CreateCheckPoint(int flags)
 		XLogRegisterData(&redo_rec, sizeof(xl_checkpoint_redo));
 		(void) XLogInsert(RM_XLOG_ID, XLOG_CHECKPOINT_REDO);
 
+		LWLockRelease(ChecksumStateLock);
+
 		/*
 		 * XLogInsertRecord will have updated XLogCtl->Insert.RedoRecPtr in
 		 * shared memory and RedoRecPtr in backend-local memory, but we need
diff --git a/src/backend/utils/activity/wait_event_names.txt b/src/backend/utils/activity/wait_event_names.txt
index 560659f9568..a9b1e213c7e 100644
--- a/src/backend/utils/activity/wait_event_names.txt
+++ b/src/backend/utils/activity/wait_event_names.txt
@@ -370,6 +370,7 @@ WaitLSN	"Waiting to read or update shared Wait-for-LSN state."
 LogicalDecodingControl	"Waiting to read or update logical decoding status information."
 DataChecksumsWorker	"Waiting for data checksums worker."
 AioWorkerControl	"Waiting to update AIO worker information."
+ChecksumState	"Waiting to update data checksum state."
 
 #
 # END OF PREDEFINED LWLOCKS (DO NOT CHANGE THIS LINE)
diff --git a/src/include/storage/lwlocklist.h b/src/include/storage/lwlocklist.h
index d7eb648bd27..2e566c4cbe3 100644
--- a/src/include/storage/lwlocklist.h
+++ b/src/include/storage/lwlocklist.h
@@ -89,6 +89,7 @@ PG_LWLOCK(54, WaitLSN)
 PG_LWLOCK(55, LogicalDecodingControl)
 PG_LWLOCK(56, DataChecksumsWorker)
 PG_LWLOCK(57, AioWorkerControl)
+PG_LWLOCK(58, ChecksumState)
 
 /*
  * There also exist several built-in LWLock tranches.  As with the predefined
diff --git a/src/test/modules/test_checksums/t/011_concurrent_checkpoint.pl b/src/test/modules/test_checksums/t/011_concurrent_checkpoint.pl
index 4cea74914d4..c6f7842caa9 100644
--- a/src/test/modules/test_checksums/t/011_concurrent_checkpoint.pl
+++ b/src/test/modules/test_checksums/t/011_concurrent_checkpoint.pl
@@ -245,13 +245,13 @@ sub test_checksum_transition
 
 # concurrent enable + checkpoint, different injection points in the "enable" process
 test_checksum_transition('disabled', 'enable', undef, 'datachecksums-enable-inprogress-checksums-start', 'on');
-test_checksum_transition('disabled', 'enable', 'datachecksums-enable-inprogress-checksums-start', 'datachecksums-enable-inprogress-checksums-after-xlog', 'on');
+#test_checksum_transition('disabled', 'enable', 'datachecksums-enable-inprogress-checksums-start', 'datachecksums-enable-inprogress-checksums-after-xlog', 'on');
 test_checksum_transition('disabled', 'enable', 'datachecksums-enable-inprogress-checksums-start', 'datachecksums-enable-inprogress-checksums-after-xlogctl', 'on');
 test_checksum_transition('disabled', 'enable', 'datachecksums-enable-inprogress-checksums-start', 'datachecksums-enable-inprogress-checksums-after-controlfile', 'on');
 test_checksum_transition('disabled', 'enable', undef, 'datachecksums-enable-inprogress-checksums-before-barrier-wait', 'on');
 test_checksum_transition('disabled', 'enable', undef, 'datachecksums-enable-inprogress-checksums-end', 'on');
 test_checksum_transition('disabled', 'enable', undef, 'datachecksums-enable-checksums-start', 'on');
-test_checksum_transition('disabled', 'enable', 'datachecksums-enable-checksums-start', 'datachecksums-enable-checksums-after-xlog', 'on');
+#test_checksum_transition('disabled', 'enable', 'datachecksums-enable-checksums-start', 'datachecksums-enable-checksums-after-xlog', 'on');
 test_checksum_transition('disabled', 'enable', 'datachecksums-enable-checksums-start', 'datachecksums-enable-checksums-after-xlogctl', 'on');
 test_checksum_transition('disabled', 'enable', 'datachecksums-enable-checksums-start', 'datachecksums-enable-checksums-after-controlfile', 'on');
 test_checksum_transition('disabled', 'enable', undef, 'datachecksums-enable-checksums-before-checkpoint', 'on');
@@ -260,12 +260,12 @@ test_checksum_transition('disabled', 'enable', undef, 'datachecksums-enable-chec
 
 # concurrent disable + checkpoint, different injection points in the "disable" process
 test_checksum_transition('enabled', 'disable', undef, 'datachecksums-disable-inprogress-checksums-start', 'off');
-test_checksum_transition('enabled', 'disable', 'datachecksums-disable-inprogress-checksums-start', 'datachecksums-disable-inprogress-checksums-after-xlog', 'off');
+#test_checksum_transition('enabled', 'disable', 'datachecksums-disable-inprogress-checksums-start', 'datachecksums-disable-inprogress-checksums-after-xlog', 'off');
 test_checksum_transition('enabled', 'disable', 'datachecksums-disable-inprogress-checksums-start', 'datachecksums-disable-inprogress-checksums-after-xlogctl', 'off');
 test_checksum_transition('enabled', 'disable', 'datachecksums-disable-inprogress-checksums-start', 'datachecksums-disable-inprogress-checksums-after-controlfile', 'off');
 test_checksum_transition('enabled', 'disable', undef, 'datachecksums-disable-inprogress-checksums-before-barrier-wait', 'off');
 test_checksum_transition('enabled', 'disable', undef, 'datachecksums-disable-checksums-start', 'off');
-test_checksum_transition('enabled', 'disable', 'datachecksums-disable-checksums-start', 'datachecksums-disable-checksums-after-xlog', 'off');
+#test_checksum_transition('enabled', 'disable', 'datachecksums-disable-checksums-start', 'datachecksums-disable-checksums-after-xlog', 'off');
 test_checksum_transition('enabled', 'disable', 'datachecksums-disable-checksums-start', 'datachecksums-disable-checksums-after-xlogctl', 'off');
 test_checksum_transition('enabled', 'disable', 'datachecksums-disable-checksums-start', 'datachecksums-disable-checksums-after-controlfile', 'off');
 test_checksum_transition('enabled', 'disable', undef, 'datachecksums-disable-checksums-before-checkpoint', 'off');
diff --git a/src/test/modules/test_checksums/t/012_concurrent_checkpoint_crash.pl b/src/test/modules/test_checksums/t/012_concurrent_checkpoint_crash.pl
index de169f47034..77c34b8b16d 100644
--- a/src/test/modules/test_checksums/t/012_concurrent_checkpoint_crash.pl
+++ b/src/test/modules/test_checksums/t/012_concurrent_checkpoint_crash.pl
@@ -251,13 +251,13 @@ sub test_checksum_transition
 
 # concurrent enable + checkpoint, different injection points in the "enable" process
 test_checksum_transition('disabled', 'enable', undef, 'datachecksums-enable-inprogress-checksums-start', 'datachecksums-enable-inprogress-checksums-after-xlog', 'off');
-test_checksum_transition('disabled', 'enable', 'datachecksums-enable-inprogress-checksums-start', 'datachecksums-enable-inprogress-checksums-after-xlog', 'datachecksums-enable-inprogress-checksums-after-xlogctl', 'off');
+#test_checksum_transition('disabled', 'enable', 'datachecksums-enable-inprogress-checksums-start', 'datachecksums-enable-inprogress-checksums-after-xlog', 'datachecksums-enable-inprogress-checksums-after-xlogctl', 'off');
 test_checksum_transition('disabled', 'enable', 'datachecksums-enable-inprogress-checksums-start', 'datachecksums-enable-inprogress-checksums-after-xlogctl', 'datachecksums-enable-inprogress-checksums-after-controlfile', 'off');
 test_checksum_transition('disabled', 'enable', 'datachecksums-enable-inprogress-checksums-start', 'datachecksums-enable-inprogress-checksums-after-controlfile', 'datachecksums-enable-inprogress-checksums-before-barrier-wait', 'off');
 test_checksum_transition('disabled', 'enable', undef, 'datachecksums-enable-inprogress-checksums-before-barrier-wait', 'datachecksums-enable-inprogress-checksums-end', 'off');
 test_checksum_transition('disabled', 'enable', undef, 'datachecksums-enable-inprogress-checksums-end', 'datachecksums-enable-checksums-start', 'off');
 test_checksum_transition('disabled', 'enable', undef, 'datachecksums-enable-checksums-start', 'datachecksums-enable-checksums-after-xlog', 'on');
-test_checksum_transition('disabled', 'enable', 'datachecksums-enable-checksums-start', 'datachecksums-enable-checksums-after-xlog', 'datachecksums-enable-checksums-after-xlogctl', 'off');
+#test_checksum_transition('disabled', 'enable', 'datachecksums-enable-checksums-start', 'datachecksums-enable-checksums-after-xlog', 'datachecksums-enable-checksums-after-xlogctl', 'off');
 test_checksum_transition('disabled', 'enable', 'datachecksums-enable-checksums-start', 'datachecksums-enable-checksums-after-xlogctl', 'datachecksums-enable-checksums-after-controlfile', 'on');
 test_checksum_transition('disabled', 'enable', 'datachecksums-enable-checksums-start', 'datachecksums-enable-checksums-after-controlfile', 'datachecksums-enable-checksums-before-checkpoint', 'on');
 test_checksum_transition('disabled', 'enable', undef, 'datachecksums-enable-checksums-before-checkpoint', 'datachecksums-enable-checksums-before-barrier-wait', 'on');
@@ -266,12 +266,12 @@ test_checksum_transition('disabled', 'enable', undef, 'datachecksums-enable-chec
 
 # concurrent disable + checkpoint, different injection points in the "disable" process
 test_checksum_transition('enabled', 'disable', undef, 'datachecksums-disable-inprogress-checksums-start', 'datachecksums-disable-inprogress-checksums-after-xlog', 'off');
-test_checksum_transition('enabled', 'disable', 'datachecksums-disable-inprogress-checksums-start', 'datachecksums-disable-inprogress-checksums-after-xlog', 'datachecksums-disable-inprogress-checksums-after-xlogctl', 'on');
+#test_checksum_transition('enabled', 'disable', 'datachecksums-disable-inprogress-checksums-start', 'datachecksums-disable-inprogress-checksums-after-xlog', 'datachecksums-disable-inprogress-checksums-after-xlogctl', 'on');
 test_checksum_transition('enabled', 'disable', 'datachecksums-disable-inprogress-checksums-start', 'datachecksums-disable-inprogress-checksums-after-xlogctl', 'datachecksums-disable-inprogress-checksums-after-controlfile', 'off');
 test_checksum_transition('enabled', 'disable', 'datachecksums-disable-inprogress-checksums-start', 'datachecksums-disable-inprogress-checksums-after-controlfile', 'datachecksums-disable-inprogress-checksums-before-barrier-wait', 'off');
 test_checksum_transition('enabled', 'disable', undef, 'datachecksums-disable-inprogress-checksums-before-barrier-wait', 'datachecksums-disable-checksums-start', 'off');
 test_checksum_transition('enabled', 'disable', undef, 'datachecksums-disable-checksums-start', 'datachecksums-disable-checksums-after-xlog', 'off');
-test_checksum_transition('enabled', 'disable', 'datachecksums-disable-checksums-start', 'datachecksums-disable-checksums-after-xlog', 'datachecksums-disable-checksums-after-xlogctl', 'off');
+#test_checksum_transition('enabled', 'disable', 'datachecksums-disable-checksums-start', 'datachecksums-disable-checksums-after-xlog', 'datachecksums-disable-checksums-after-xlogctl', 'off');
 test_checksum_transition('enabled', 'disable', 'datachecksums-disable-checksums-start', 'datachecksums-disable-checksums-after-xlogctl', 'datachecksums-disable-checksums-after-controlfile', 'off');
 test_checksum_transition('enabled', 'disable', 'datachecksums-disable-checksums-start', 'datachecksums-disable-checksums-after-controlfile', 'datachecksums-disable-checksums-before-checkpoint', 'off');
 test_checksum_transition('enabled', 'disable', undef, 'datachecksums-disable-checksums-before-checkpoint', 'datachecksums-disable-checksums-before-barrier-wait', 'off');
diff --git a/src/test/modules/test_checksums/t/013_async_checkpoint_crash.pl b/src/test/modules/test_checksums/t/013_async_checkpoint_crash.pl
index 4a310506482..0c59f6d4095 100644
--- a/src/test/modules/test_checksums/t/013_async_checkpoint_crash.pl
+++ b/src/test/modules/test_checksums/t/013_async_checkpoint_crash.pl
@@ -349,20 +349,20 @@ note('TEST INPROGRESS-ON/5');
 			 datachecksums-enable-inprogress-checksums-after-controlfile);
 test_checksum_sequence('disabled', 'enable', 'datachecksums-enable-inprogress-checksums-start', 'datachecksums-enable-inprogress-checksums-end', 'off', @steps);
 
-note('TEST INPROGRESS-ON/6');
-@steps = qw(create-checkpoint-initial
-			 checkpoint-before-redo
-			 checkpoint-before-xlogctl-checksums
-			 checkpoint-after-xlogctl-checksums
-			 checkpoint-before-redo-checksums
-			 datachecksums-enable-inprogress-checksums-before-xlog
-			 datachecksums-enable-inprogress-checksums-after-xlog
-			 datachecksums-enable-inprogress-checksums-after-xlogctl
-			 datachecksums-enable-inprogress-checksums-after-controlfile
-			 checkpoint-before-redo-wal
-			 checkpoint-after-redo-wal
-			 checkpoint-before-old-wal-removal);
-test_checksum_sequence('disabled', 'enable', 'datachecksums-enable-inprogress-checksums-start', 'datachecksums-enable-inprogress-checksums-end', 'off', @steps);
+#note('TEST INPROGRESS-ON/6');
+#@steps = qw(create-checkpoint-initial
+#			 checkpoint-before-redo
+#			 checkpoint-before-xlogctl-checksums
+#			 checkpoint-after-xlogctl-checksums
+#			 checkpoint-before-redo-checksums
+#			 datachecksums-enable-inprogress-checksums-before-xlog
+#			 datachecksums-enable-inprogress-checksums-after-xlog
+#			 datachecksums-enable-inprogress-checksums-after-xlogctl
+#			 datachecksums-enable-inprogress-checksums-after-controlfile
+#			 checkpoint-before-redo-wal
+#			 checkpoint-after-redo-wal
+#			 checkpoint-before-old-wal-removal);
+#test_checksum_sequence('disabled', 'enable', 'datachecksums-enable-inprogress-checksums-start', 'datachecksums-enable-inprogress-checksums-end', 'off', @steps);
 
 ## checksums ON
 
@@ -395,20 +395,20 @@ note('TEST ON/2');
 			 datachecksums-enable-checksums-after-controlfile);
 test_checksum_sequence('disabled', 'enable', 'datachecksums-enable-checksums-start', 'datachecksums-enable-checksums-end', 'on', @steps);
 
-note('TEST ON/3');
-@steps = qw(create-checkpoint-initial
-			 checkpoint-before-redo
-			 checkpoint-before-xlogctl-checksums
-			 checkpoint-after-xlogctl-checksums
-			 checkpoint-before-redo-checksums
-			 datachecksums-enable-checksums-before-xlog
-			 datachecksums-enable-checksums-after-xlog
-			 checkpoint-before-redo-wal
-			 checkpoint-after-redo-wal
-			 checkpoint-before-old-wal-removal
-			 datachecksums-enable-checksums-after-xlogctl
-			 datachecksums-enable-checksums-after-controlfile);
-test_checksum_sequence('disabled', 'enable', 'datachecksums-enable-checksums-start', 'datachecksums-enable-checksums-end', 'on', @steps);
+#note('TEST ON/3');
+#@steps = qw(create-checkpoint-initial
+#			 checkpoint-before-redo
+#			 checkpoint-before-xlogctl-checksums
+#			 checkpoint-after-xlogctl-checksums
+#			 checkpoint-before-redo-checksums
+#			 datachecksums-enable-checksums-before-xlog
+#			 datachecksums-enable-checksums-after-xlog
+#			 checkpoint-before-redo-wal
+#			 checkpoint-after-redo-wal
+#			 checkpoint-before-old-wal-removal
+#			 datachecksums-enable-checksums-after-xlogctl
+#			 datachecksums-enable-checksums-after-controlfile);
+#test_checksum_sequence('disabled', 'enable', 'datachecksums-enable-checksums-start', 'datachecksums-enable-checksums-end', 'on', @steps);
 
 note('TEST ON/4');
 @steps = qw(create-checkpoint-initial
@@ -462,39 +462,39 @@ note('TEST INPROGRESS-OFF/2');
 			 datachecksums-disable-inprogress-checksums-before-barrier-wait);
 test_checksum_sequence('enabled', 'disable', 'datachecksums-disable-inprogress-checksums-start', 'datachecksums-disable-checksums-start', 'off', @steps);
 
-note('TEST INPROGRESS-OFF/3');
-@steps = qw(create-checkpoint-initial
-			 checkpoint-before-redo
-			 checkpoint-before-xlogctl-checksums
-			 checkpoint-after-xlogctl-checksums
-			 checkpoint-before-redo-checksums
-			 datachecksums-disable-inprogress-checksums-before-xlog
-			 datachecksums-disable-inprogress-checksums-after-xlog
-			 checkpoint-before-redo-wal
-			 checkpoint-after-redo-wal
-			 checkpoint-before-old-wal-removal
-			 datachecksums-disable-inprogress-checksums-after-xlogctl
-			 datachecksums-disable-inprogress-checksums-after-controlfile
-			 datachecksums-disable-inprogress-checksums-before-checkpoint
-			 datachecksums-disable-inprogress-checksums-before-barrier-wait);
-test_checksum_sequence('enabled', 'disable', 'datachecksums-disable-inprogress-checksums-start', 'datachecksums-disable-checksums-start', 'off', @steps);
-
-note('TEST INPROGRESS-OFF/4');
-@steps = qw(create-checkpoint-initial
-			 checkpoint-before-redo
-			 checkpoint-before-xlogctl-checksums
-			 checkpoint-after-xlogctl-checksums
-			 checkpoint-before-redo-checksums
-			 datachecksums-disable-inprogress-checksums-before-xlog
-			 datachecksums-disable-inprogress-checksums-after-xlog
-			 datachecksums-disable-inprogress-checksums-after-xlogctl
-			 datachecksums-disable-inprogress-checksums-after-controlfile
-			 datachecksums-disable-inprogress-checksums-before-checkpoint
-			 checkpoint-before-redo-wal
-			 checkpoint-after-redo-wal
-			 checkpoint-before-old-wal-removal
-			 datachecksums-disable-inprogress-checksums-before-barrier-wait);
-test_checksum_sequence('enabled', 'disable', 'datachecksums-disable-inprogress-checksums-start', 'datachecksums-disable-checksums-start', 'off', @steps);
+#note('TEST INPROGRESS-OFF/3');
+#@steps = qw(create-checkpoint-initial
+#			 checkpoint-before-redo
+#			 checkpoint-before-xlogctl-checksums
+#			 checkpoint-after-xlogctl-checksums
+#			 checkpoint-before-redo-checksums
+#			 datachecksums-disable-inprogress-checksums-before-xlog
+#			 datachecksums-disable-inprogress-checksums-after-xlog
+#			 checkpoint-before-redo-wal
+#			 checkpoint-after-redo-wal
+#			 checkpoint-before-old-wal-removal
+#			 datachecksums-disable-inprogress-checksums-after-xlogctl
+#			 datachecksums-disable-inprogress-checksums-after-controlfile
+#			 datachecksums-disable-inprogress-checksums-before-checkpoint
+#			 datachecksums-disable-inprogress-checksums-before-barrier-wait);
+#test_checksum_sequence('enabled', 'disable', 'datachecksums-disable-inprogress-checksums-start', 'datachecksums-disable-checksums-start', 'off', @steps);
+
+#note('TEST INPROGRESS-OFF/4');
+#@steps = qw(create-checkpoint-initial
+#			 checkpoint-before-redo
+#			 checkpoint-before-xlogctl-checksums
+#			 checkpoint-after-xlogctl-checksums
+#			 checkpoint-before-redo-checksums
+#			 datachecksums-disable-inprogress-checksums-before-xlog
+#			 datachecksums-disable-inprogress-checksums-after-xlog
+#			 datachecksums-disable-inprogress-checksums-after-xlogctl
+#			 datachecksums-disable-inprogress-checksums-after-controlfile
+#			 datachecksums-disable-inprogress-checksums-before-checkpoint
+#			 checkpoint-before-redo-wal
+#			 checkpoint-after-redo-wal
+#			 checkpoint-before-old-wal-removal
+#			 datachecksums-disable-inprogress-checksums-before-barrier-wait);
+#test_checksum_sequence('enabled', 'disable', 'datachecksums-disable-inprogress-checksums-start', 'datachecksums-disable-checksums-start', 'off', @steps);
 
 ## checksums OFF
 
@@ -532,22 +532,22 @@ note('TEST OFF/2');
 			 datachecksums-disable-checksums-before-barrier-wait);
 test_checksum_sequence('enabled', 'disable', 'datachecksums-disable-checksums-start', 'datachecksums-disable-checksums-end', 'off', @steps);
 
-note('TEST OFF/3');
-@steps = qw(create-checkpoint-initial
-			 checkpoint-before-redo
-			 checkpoint-before-xlogctl-checksums
-			 checkpoint-after-xlogctl-checksums
-			 checkpoint-before-redo-checksums
-			 datachecksums-disable-checksums-before-xlog
-			 datachecksums-disable-checksums-after-xlog
-			 checkpoint-before-redo-wal
-			 checkpoint-after-redo-wal
-			 datachecksums-disable-checksums-after-xlogctl
-			 datachecksums-disable-checksums-after-controlfile
-			 datachecksums-disable-checksums-before-checkpoint
-			 checkpoint-before-old-wal-removal
-			 datachecksums-disable-checksums-before-barrier-wait);
-test_checksum_sequence('enabled', 'disable', 'datachecksums-disable-checksums-start', 'datachecksums-disable-checksums-end', 'off', @steps);
+#note('TEST OFF/3');
+#@steps = qw(create-checkpoint-initial
+#			 checkpoint-before-redo
+#			 checkpoint-before-xlogctl-checksums
+#			 checkpoint-after-xlogctl-checksums
+#			 checkpoint-before-redo-checksums
+#			 datachecksums-disable-checksums-before-xlog
+#			 datachecksums-disable-checksums-after-xlog
+#			 checkpoint-before-redo-wal
+#			 checkpoint-after-redo-wal
+#			 datachecksums-disable-checksums-after-xlogctl
+#			 datachecksums-disable-checksums-after-controlfile
+#			 datachecksums-disable-checksums-before-checkpoint
+#			 checkpoint-before-old-wal-removal
+#			 datachecksums-disable-checksums-before-barrier-wait);
+#test_checksum_sequence('enabled', 'disable', 'datachecksums-disable-checksums-start', 'datachecksums-disable-checksums-end', 'off', @steps);
 
 note('TEST OFF/4');
 @steps = qw(create-checkpoint-initial
@@ -566,22 +566,22 @@ note('TEST OFF/4');
 			 datachecksums-disable-checksums-before-barrier-wait);
 test_checksum_sequence('enabled', 'disable', 'datachecksums-disable-checksums-start', 'datachecksums-disable-checksums-end', 'off', @steps);
 
-note('TEST OFF/5');
-@steps = qw(create-checkpoint-initial
-			 checkpoint-before-redo
-			 checkpoint-before-xlogctl-checksums
-			 checkpoint-after-xlogctl-checksums
-			 checkpoint-before-redo-checksums
-			 datachecksums-disable-checksums-before-xlog
-			 datachecksums-disable-checksums-after-xlog
-			 checkpoint-before-redo-wal
-			 checkpoint-after-redo-wal
-			 datachecksums-disable-checksums-after-xlogctl
-			 datachecksums-disable-checksums-after-controlfile
-			 datachecksums-disable-checksums-before-checkpoint
-			 checkpoint-before-old-wal-removal
-			 datachecksums-disable-checksums-before-barrier-wait);
-test_checksum_sequence('enabled', 'disable', 'datachecksums-disable-checksums-start', 'datachecksums-disable-checksums-end', 'off', @steps);
+#note('TEST OFF/5');
+#@steps = qw(create-checkpoint-initial
+#			 checkpoint-before-redo
+#			 checkpoint-before-xlogctl-checksums
+#			 checkpoint-after-xlogctl-checksums
+#			 checkpoint-before-redo-checksums
+#			 datachecksums-disable-checksums-before-xlog
+#			 datachecksums-disable-checksums-after-xlog
+#			 checkpoint-before-redo-wal
+#			 checkpoint-after-redo-wal
+#			 datachecksums-disable-checksums-after-xlogctl
+#			 datachecksums-disable-checksums-after-controlfile
+#			 datachecksums-disable-checksums-before-checkpoint
+#			 checkpoint-before-old-wal-removal
+#			 datachecksums-disable-checksums-before-barrier-wait);
+#test_checksum_sequence('enabled', 'disable', 'datachecksums-disable-checksums-start', 'datachecksums-disable-checksums-end', 'off', @steps);
 
 note('TEST OFF/6');
 @steps = qw(create-checkpoint-initial
diff --git a/src/backend/access/transam/xlog.c b/src/backend/access/transam/xlog.c
index fecdf0d4b05..462414323a1 100644
--- a/src/backend/access/transam/xlog.c
+++ b/src/backend/access/transam/xlog.c
@@ -4807,9 +4807,8 @@ SetDataChecksumsOn(void)
 
 	/*
 	 * The only allowed state transition to "on" is from "inprogress-on" since
-	 * that state ensures that all pages will have data checksums written.  No
-	 * such state transition exists, if it does happen it's likely due to a
-	 * programmer error.
+	 * that state ensures that all pages will have data checksums written. Any
+	 * other attempted state transition is likely due to a programmer error.
 	 */
 	if (XLogCtl->data_checksum_version != PG_DATA_CHECKSUM_INPROGRESS_ON)
 	{
diff --git a/src/backend/postmaster/auxprocess.c b/src/backend/postmaster/auxprocess.c
index 9803a0ee2a1..ad4bf4bd2a8 100644
--- a/src/backend/postmaster/auxprocess.c
+++ b/src/backend/postmaster/auxprocess.c
@@ -88,7 +88,7 @@ AuxiliaryProcessMainCommon(void)
 	 *
 	 * The postmaster (which is what gets forked into the new child process)
 	 * does not handle barriers, therefore it may not have the current value
-	 * of LocalDataChecksumVersion value (it'll have the value read from the
+	 * of LocalDataChecksumState value (it'll have the value read from the
 	 * control file, which may be arbitrarily old).
 	 *
 	 * NB: Even if the postmaster handled barriers, the value might still be
diff --git a/src/backend/postmaster/datachecksum_state.c b/src/backend/postmaster/datachecksum_state.c
index 33430147ff2..520eb8db16b 100644
--- a/src/backend/postmaster/datachecksum_state.c
+++ b/src/backend/postmaster/datachecksum_state.c
@@ -20,8 +20,8 @@
  * When enabling checksums in an online cluster, data_checksums will be set to
  * "inprogress-on" which signals that write operations MUST compute and write
  * the checksum on the data page, but during reading the checksum SHALL NOT be
- * verified. This ensures that all objects created during when checksums are
- * being enabled will have checksums set, but reads won't fail due to missing or
+ * verified. This ensures that all objects created while checksums are being
+ * enabled will have checksums set, but reads won't fail due to missing or
  * invalid checksums. Invalid checksums can be present in case the cluster had
  * checksums enabled, then disabled them and updated the page while they were
  * disabled.
@@ -299,10 +299,11 @@ typedef struct DataChecksumsStateStruct
 	bool		launcher_running;
 
 	/*
-	 * Is a worker process currently running?  This is set by the worker
-	 * launcher when it starts waiting for a worker process to finish.
+	 * PID of the worker process, if it's currently running, of InvalidPid
+	 * if none. This is set by the worker launcher when it starts waiting
+	 * for a worker process to finish.
 	 */
-	int			worker_pid;
+	pid_t		worker_pid;
 
 	/*
 	 * These fields indicate the target state that the launcher is currently
@@ -320,12 +321,8 @@ typedef struct DataChecksumsStateStruct
 	int			cost_limit;
 
 	/*
-	 * Signaling between the launcher and the worker process.
-	 *
-	 * As there is only a single worker, and the launcher won't read these
-	 * until the worker exits, they can be accessed without the need for a
-	 * lock. If multiple workers are supported then this will have to be
-	 * revisited.
+	 * Signaling between the launcher and the worker process. Protected by
+	 * DataChecksumsWorkerLock.
 	 */
 
 	/* result, set by worker before exiting */
@@ -599,9 +596,9 @@ StartDataChecksumsWorkerLauncher(DataChecksumsWorkerOperation op,
 	 *
 	 * If the launcher is currently busy enabling the checksums, and we want
 	 * them disabled (or vice versa), the launcher will notice that at latest
-	 * when it's about to exit, and will loop back process the new request. So
-	 * if the launcher is already running, we don't need to do anything more
-	 * here to abort it.
+	 * when it's about to exit, and will loop back to process the new request.
+	 * So if the launcher is already running, we don't need to do anything
+	 * more here to abort it.
 	 *
 	 * If you call pg_enable/disable_data_checksums() twice in a row, before
 	 * the launcher has had a chance to start up, we still end up launching it
@@ -710,10 +707,7 @@ ProcessSingleRelationFork(Relation reln, ForkNumber forkNum, BufferAccessStrateg
 
 		UnlockReleaseBuffer(buf);
 
-		/*
-		 * This is the only place where we check if we are asked to abort, the
-		 * abortion will bubble up from here.
-		 */
+		/* Check if we are asked to abort, the abortion will bubble up. */
 		Assert(operation == ENABLE_DATACHECKSUMS);
 		LWLockAcquire(DataChecksumsWorkerLock, LW_SHARED);
 		if (DataChecksumState->launch_operation == DISABLE_DATACHECKSUMS)
@@ -924,8 +918,8 @@ ProcessDatabase(DataChecksumsWorkerDatabase *db)
  * performed checksum operations exits. A launcher process which is exiting due
  * to a duplicate started launcher does not need to perform any cleanup and
  * this function should not be called. Otherwise, we need to clean up the abort
- * flag to ensure that processing started again if it was previously aborted
- * (note: started again, *not* restarted from where it left off).
+ * flag to ensure that processing can be started again if it was previously
+ * aborted (note: started again, *not* restarted from where it left off).
  */
 static void
 launcher_exit(int code, Datum arg)
@@ -1434,7 +1428,7 @@ FreeDatabaseList(List *dblist)
  * BuildRelationList
  *		Compile a list of relations in the database
  *
- * Returns a list of OIDs for the request relation types. If temp_relations
+ * Returns a list of OIDs for the requested relation types. If temp_relations
  * is True then only temporary relations are returned. If temp_relations is
  * False then non-temporary relations which have data checksums are returned.
  * If include_shared is True then shared relations are included as well in a
diff --git a/src/backend/utils/init/postinit.c b/src/backend/utils/init/postinit.c
index 2460e550f96..c1457eb34f0 100644
--- a/src/backend/utils/init/postinit.c
+++ b/src/backend/utils/init/postinit.c
@@ -773,7 +773,7 @@ InitPostgres(const char *in_dbname, Oid dboid,
 	 *
 	 * The postmaster (which is what gets forked into the new child process)
 	 * does not handle barriers, therefore it may not have the current value
-	 * of LocalDataChecksumVersion value (it'll have the value read from the
+	 * of LocalDataChecksumState value (it'll have the value read from the
 	 * control file, which may be arbitrarily old).
 	 *
 	 * NB: Even if the postmaster handled barriers, the value might still be

---

From: Daniel Gustafsson <[email-masked]>
Date: Thu, 28 May 2026 13:28:49 +0200

> On 26 May 2026, at 20:12, Tomas Vondra <[email-masked]> wrote:

> without emitting the barrier? I think all other places do that.

Good catch, it's indeed a bug, any state change must emit a procsignalbarrier
to maintain cluster consistency.  I ended up writing a test for this very case
as well.

> failures (or obviously incorrect final state after recovery).

I've tried to get it to reach an incorrect end state but failed, but I do agree
that maybe we need an improved locking protocol around state updates.  Need to
spend some more time thinking about this.

> I still don't understand why this needs DELAY_CHKPT_START ...

Having stared at this for some time, and going over old threads, I think this
is a mistake.  AFAICT though it cannot cause any error, so I'd lean towards
erring on the safe side by leaving as is and looking at removing in 20.  What
do you think?

> I also noticed a couple minor comment issues, per attached patch (this

I ended up splitting this into two, one for the comment fixes and one for the
data type change.

I propose applying the three patches below to v19 to fix the promotion issue
before we wrap beta1.

--
Daniel Gustafsson

---

From: Tomas Vondra <[email-masked]>
Date: Thu, 28 May 2026 13:51:14 +0200

> to maintain cluster consistency.  I ended up writing a test for this very case

Good.

> spend some more time thinking about this.

OK

> erring on the safe side by leaving as is and looking at removing in 20.  What

I'd probably try to fix this for 19, otherwise it may be confusing
people looking at the code in the future. We're still months from 19
getting released. Ofc, maybe I'm underestimating the risk.

> I propose applying the three patches below to v19 to fix the promotion issue

WFM

-- 
Tomas Vondra

---

From: Daniel Gustafsson <[email-masked]>
Date: Fri, 29 May 2026 22:08:11 +0200

> On 28 May 2026, at 13:51, Tomas Vondra <[email-masked]> wrote:
> 

I've pushed this now, along with your other findings, ahead of the beta1
deadline, buildfarm seems happy so far.

> getting released. Ofc, maybe I'm underestimating the risk.

You're probably right.  Once beta1 is out I'll work on getting this fixed.

--
Daniel Gustafsson

---

From: Tomas Vondra <[email-masked]>
Date: Fri, 29 May 2026 22:27:20 +0200

> deadline, buildfarm seems happy so far.

Thanks!

> You're probably right.  Once beta1 is out I'll work on getting this fixed.

+1

regards

-- 
Tomas Vondra

---

From: Palak Chaturvedi <[email-masked]>
Date: Thu, 2 Jul 2026 10:36:21 +0530

Hi Tomas,

Thanks for these test patches. They are quite useful. I've been going
through them because we think they'll also help with the TAP tests for the
online shared_buffers resize work , where I'm ending up reusing several of
the same helpers (attach_injection_points, background_rw_pgbench, and a
couple of others).
Would you be open to lifting these helpers out into a shared Perl module
(for example under src/test/perl/PostgreSQL/Test/…) so both test suites can
consume them instead of each copy-pasting? I'm happy to prepare a small
prep patch on top of your latest version if that sounds reasonable to you.

Thanks,
Palak

---

From: Tomas Vondra <[email-masked]>
Date: Thu, 2 Jul 2026 10:29:53 +0200

On 7/2/26 07:06, Palak Chaturvedi wrote:
> through them because we think they'll also help with the TAP tests for
> the online shared_buffers resize work , where I'm ending up reusing
> several of the same helpers (attach_injection_points,
> background_rw_pgbench, and a couple of others).
> (for example under src/test/perl/PostgreSQL/Test/…) so both test suites
> can consume them instead of each copy-pasting? I'm happy to prepare a
> small prep patch on top of your latest version if that sounds reasonable
> to you.
> 

I suppose you mean the functions for managing injection points (waiting,
waking, attachin/detaching, ...)? Yes, it might be useful to have that
in a Perl module, so feel free to create one.

regards

-- 
Tomas Vondra