From 6a16454e0672e818423f6485fb14f4aa6f782aed Mon Sep 17 00:00:00 2001
From: Daniel Gustafsson <dgustafsson@postgresql.org>
Date: Fri, 15 Aug 2025 14:48:02 +0200
Subject: [PATCH v20260305] Online enabling and disabling of data checksums

This allows data checksums to be enabled, or disabled, in a running
cluster without restricting access to the cluster during processing.

Data checksums could prior to this only be enabled during initdb or
when the cluster is offline using the pg_checksums app. This commit
introduce functionality to enable, or disable, data checksums while
the cluster is running regardless of how it was initialized.

A background worker launcher process is responsible for launching a
dynamic per-database background worker which will mark all buffers
dirty for all relation with storage in order for them to have data
checksums calcuated on write.  Once all relations in all databases
have been processed, the data_checksums state will be set to on and
the cluster will at that point be identical to one which had data
checksums enabled during initialization or via offline processing.

When data checksums are being enabled, concurrent I/O operations
from backends other than the data checksums worker will write the
checksums but not verify them on reading.  Only when all backends
have absorbed the procsignalbarrier for setting data_checksums to
on will they also start verifying checksums on reading.  The same
process is repeated during disabling; all backends write checksums
but do not verify them until the barrier for setting the state to
off has been absorbed by all.  This in-progress state is used to
ensure there are no false negatives (or positives) due to reading
a checksum which is not in sync with the page.

A new testmodule, test_checksums, is introduced with an extensive
set of tests covering both online and offline data checksum mode
changes.  The tests for online processing are gated begind the
PG_TEST_EXTRA flag to some degree due to being very time consuming
to run.

This work is based on an earlier version of this patch which was
reviewed by among others Heikki Linnakangas, Robert Haas, Andres
Freund, Tomas Vondra, Michael Banck and Andrey Borodin.  During
the work on this new version, Tomas Vondra has given invaluable
assistance with not only coding and reviewing but very in-depth
testing.

Author: Daniel Gustafsson <daniel@yesql.se>
Author: Magnus Hagander <magnus@hagander.net>
Co-authored-by: Tomas Vondra <tomas@vondra.me>
Reviewed-by: Tomas Vondra <tomas@vondra.me>
Reviewed-by: Andres Freund <andres@anarazel.de>
Discussion: https://postgr.es/m/CABUevExz9hUUOLnJVr2kpw9Cx=o4MCr1SVKwbupzuxP7ckNutA@mail.gmail.com
Discussion: https://postgr.es/m/20181030051643.elbxjww5jjgnjaxg@alap3.anarazel.de
Discussion: https://postgr.es/m/CABUevEwE3urLtwxxqdgd5O2oQz9J717ZzMbh+ziCSa5YLLU_BA@mail.gmail.com
---
 doc/src/sgml/func/func-admin.sgml             |   77 +
 doc/src/sgml/glossary.sgml                    |   24 +
 doc/src/sgml/images/Makefile                  |    1 +
 doc/src/sgml/images/datachecksums.gv          |   14 +
 doc/src/sgml/images/datachecksums.svg         |   81 +
 doc/src/sgml/monitoring.sgml                  |  235 ++-
 doc/src/sgml/ref/pg_checksums.sgml            |    6 +
 doc/src/sgml/regress.sgml                     |   12 +
 doc/src/sgml/wal.sgml                         |  127 +-
 src/backend/access/rmgrdesc/xlogdesc.c        |   42 +-
 src/backend/access/transam/xlog.c             |  718 +++++++-
 src/backend/access/transam/xlogfuncs.c        |   48 +
 src/backend/access/transam/xlogrecovery.c     |    1 +
 src/backend/backup/basebackup.c               |   27 +-
 src/backend/catalog/system_functions.sql      |   12 +
 src/backend/catalog/system_views.sql          |   19 +
 src/backend/postmaster/Makefile               |    1 +
 src/backend/postmaster/auxprocess.c           |   19 +
 src/backend/postmaster/bgworker.c             |    9 +
 src/backend/postmaster/datachecksumsworker.c  | 1487 +++++++++++++++++
 src/backend/postmaster/meson.build            |    1 +
 src/backend/postmaster/postmaster.c           |    5 +
 src/backend/postmaster/walsummarizer.c        |    5 +-
 src/backend/replication/logical/decode.c      |    1 +
 src/backend/storage/ipc/ipci.c                |    3 +
 src/backend/storage/ipc/procsignal.c          |   14 +
 src/backend/storage/page/README               |    4 +-
 src/backend/storage/page/bufpage.c            |   22 +-
 src/backend/utils/activity/pgstat_backend.c   |    2 +
 src/backend/utils/activity/pgstat_io.c        |    2 +
 .../utils/activity/wait_event_names.txt       |    4 +
 src/backend/utils/adt/pgstatfuncs.c           |    8 +-
 src/backend/utils/init/miscinit.c             |    3 +-
 src/backend/utils/init/postinit.c             |   20 +-
 src/backend/utils/misc/guc_parameters.dat     |    5 +-
 src/backend/utils/misc/guc_tables.c           |    9 +-
 src/bin/pg_checksums/pg_checksums.c           |    4 +-
 src/bin/pg_controldata/pg_controldata.c       |    2 +
 src/bin/pg_upgrade/controldata.c              |    9 +
 src/include/access/xlog.h                     |   14 +-
 src/include/access/xlog_internal.h            |   13 +
 src/include/catalog/pg_control.h              |    6 +-
 src/include/catalog/pg_proc.dat               |   12 +
 src/include/commands/progress.h               |   16 +
 src/include/miscadmin.h                       |    6 +
 src/include/postmaster/datachecksumsworker.h  |   50 +
 src/include/postmaster/proctypelist.h         |    2 +
 src/include/storage/bufpage.h                 |    2 +-
 src/include/storage/checksum.h                |   14 +
 src/include/storage/lwlocklist.h              |    1 +
 src/include/storage/proc.h                    |    3 +-
 src/include/storage/procsignal.h              |    4 +
 src/include/utils/backend_progress.h          |    1 +
 src/test/modules/Makefile                     |    1 +
 src/test/modules/meson.build                  |    1 +
 src/test/modules/test_checksums/.gitignore    |    2 +
 src/test/modules/test_checksums/Makefile      |   40 +
 src/test/modules/test_checksums/README        |   30 +
 src/test/modules/test_checksums/meson.build   |   36 +
 .../modules/test_checksums/t/001_basic.pl     |   63 +
 .../modules/test_checksums/t/002_restarts.pl  |  110 ++
 .../test_checksums/t/003_standby_restarts.pl  |  114 ++
 .../modules/test_checksums/t/004_offline.pl   |   82 +
 .../modules/test_checksums/t/005_injection.pl |  109 ++
 .../test_checksums/t/006_pgbench_single.pl    |  252 +++
 .../test_checksums/t/007_pgbench_standby.pl   |  376 +++++
 .../test_checksums/t/DataChecksums/Utils.pm   |  261 +++
 .../test_checksums/test_checksums--1.0.sql    |   28 +
 .../modules/test_checksums/test_checksums.c   |  222 +++
 .../test_checksums/test_checksums.control     |    4 +
 src/test/perl/PostgreSQL/Test/Cluster.pm      |   36 +
 src/test/regress/expected/rules.out           |   35 +
 src/test/regress/expected/stats.out           |   18 +-
 src/tools/pgindent/typedefs.list              |    9 +
 74 files changed, 4997 insertions(+), 59 deletions(-)
 create mode 100644 doc/src/sgml/images/datachecksums.gv
 create mode 100644 doc/src/sgml/images/datachecksums.svg
 create mode 100644 src/backend/postmaster/datachecksumsworker.c
 create mode 100644 src/include/postmaster/datachecksumsworker.h
 create mode 100644 src/test/modules/test_checksums/.gitignore
 create mode 100644 src/test/modules/test_checksums/Makefile
 create mode 100644 src/test/modules/test_checksums/README
 create mode 100644 src/test/modules/test_checksums/meson.build
 create mode 100644 src/test/modules/test_checksums/t/001_basic.pl
 create mode 100644 src/test/modules/test_checksums/t/002_restarts.pl
 create mode 100644 src/test/modules/test_checksums/t/003_standby_restarts.pl
 create mode 100644 src/test/modules/test_checksums/t/004_offline.pl
 create mode 100644 src/test/modules/test_checksums/t/005_injection.pl
 create mode 100644 src/test/modules/test_checksums/t/006_pgbench_single.pl
 create mode 100644 src/test/modules/test_checksums/t/007_pgbench_standby.pl
 create mode 100644 src/test/modules/test_checksums/t/DataChecksums/Utils.pm
 create mode 100644 src/test/modules/test_checksums/test_checksums--1.0.sql
 create mode 100644 src/test/modules/test_checksums/test_checksums.c
 create mode 100644 src/test/modules/test_checksums/test_checksums.control

diff --git a/doc/src/sgml/func/func-admin.sgml b/doc/src/sgml/func/func-admin.sgml
index 3ac81905d1f..7762b774bf2 100644
--- a/doc/src/sgml/func/func-admin.sgml
+++ b/doc/src/sgml/func/func-admin.sgml
@@ -3087,4 +3087,81 @@ SELECT convert_from(pg_read_binary_file('file_in_utf8.txt'), 'UTF8');
 
   </sect2>
 
+  <sect2 id="functions-admin-checksum">
+   <title>Data Checksum Functions</title>
+
+   <para>
+    The functions shown in <xref linkend="functions-checksums-table" /> can
+    be used to enable or disable data checksums in a running cluster.
+   </para>
+   <para>
+    Changing data checksums can be done in a cluster with concurrent activity
+    without blocking queries, but overall system performance will be affected.
+    See <xref linkend="checksums" /> for further details on how changing the
+    data checksums mode can affect a system and possible mitigations for how
+    to reduce the impact.
+   </para>
+
+   <table id="functions-checksums-table">
+    <title>Data Checksum Functions</title>
+    <tgroup cols="1">
+     <thead>
+      <row>
+       <entry role="func_table_entry"><para role="func_signature">
+        Function
+       </para>
+       <para>
+        Description
+       </para></entry>
+      </row>
+     </thead>
+
+     <tbody>
+      <row>
+       <entry role="func_table_entry"><para role="func_signature">
+        <indexterm>
+         <primary>pg_enable_data_checksums</primary>
+        </indexterm>
+        <function>pg_enable_data_checksums</function> ( <optional><parameter>cost_delay</parameter> <type>int</type>, <parameter>cost_limit</parameter> <type>int</type></optional> )
+        <returnvalue>void</returnvalue>
+       </para>
+       <para>
+        Initiates the process of enabling data checksums for the cluster. This
+        will set the data checksums mode to <literal>inprogress-on</literal>
+        as well as start a background worker that will process all pages in all
+        databases and enable data checksums on them.  When all data checksums
+        have been calculated, and written, for all pages the cluster will
+        automatically set data checksums mode to <literal>on</literal>.
+       </para>
+       <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>
+       </entry>
+      </row>
+
+      <row>
+       <entry role="func_table_entry"><para role="func_signature">
+        <indexterm>
+         <primary>pg_disable_data_checksums</primary>
+        </indexterm>
+        <function>pg_disable_data_checksums</function> ()
+        <returnvalue>void</returnvalue>
+       </para>
+       <para>
+        Disables data checksum calculation and validation for the cluster. This
+        will set the data checksum mode to <literal>inprogress-off</literal>
+        while data checksums are being disabled.  When all active backends have
+        stopped validating data checksums, the data checksum mode will be
+        set to <literal>off</literal>.
+       </para>
+       </entry>
+      </row>
+     </tbody>
+    </tgroup>
+   </table>
+
+  </sect2>
+
   </sect1>
diff --git a/doc/src/sgml/glossary.sgml b/doc/src/sgml/glossary.sgml
index e2db5bcc78c..d9378af0606 100644
--- a/doc/src/sgml/glossary.sgml
+++ b/doc/src/sgml/glossary.sgml
@@ -199,6 +199,8 @@
      (but not the autovacuum workers),
      the <glossterm linkend="glossary-background-writer">background writer</glossterm>,
      the <glossterm linkend="glossary-checkpointer">checkpointer</glossterm>,
+     the <glossterm linkend="glossary-data-checksums-worker">data checksums worker</glossterm>,
+     the <glossterm linkend="glossary-data-checksums-worker-launcher">data checksums worker launcher</glossterm>,
      the <glossterm linkend="glossary-logger">logger</glossterm>,
      the <glossterm linkend="glossary-startup-process">startup process</glossterm>,
      the <glossterm linkend="glossary-wal-archiver">WAL archiver</glossterm>,
@@ -574,6 +576,28 @@
    <glosssee otherterm="glossary-data-directory" />
   </glossentry>
 
+  <glossentry id="glossary-data-checksums-worker">
+   <glossterm>Data Checksums Worker</glossterm>
+   <glossdef>
+    <para>
+     A <glossterm linkend="glossary-background-worker">background worker</glossterm>
+     which enables data checksums in a specific database.
+    </para>
+   </glossdef>
+  </glossentry>
+
+  <glossentry id="glossary-data-checksums-worker-launcher">
+   <glossterm>Data Checksums Worker Launcher</glossterm>
+   <glossdef>
+    <para>
+     A <glossterm linkend="glossary-background-worker">background worker</glossterm>
+     which starts <glossterm linkend="glossary-data-checksums-worker"> data
+     checksum worker</glossterm> processes for enabling data checksums in each
+     database, or disables data checksums cluster-wide.
+    </para>
+   </glossdef>
+  </glossentry>
+
   <glossentry id="glossary-database">
    <glossterm>Database</glossterm>
    <glossdef>
diff --git a/doc/src/sgml/images/Makefile b/doc/src/sgml/images/Makefile
index fd55b9ad23f..e805487344d 100644
--- a/doc/src/sgml/images/Makefile
+++ b/doc/src/sgml/images/Makefile
@@ -3,6 +3,7 @@
 # see README in this directory about image handling
 
 ALL_IMAGES = \
+	datachecksums.svg \
 	genetic-algorithm.svg \
 	gin.svg \
 	pagelayout.svg \
diff --git a/doc/src/sgml/images/datachecksums.gv b/doc/src/sgml/images/datachecksums.gv
new file mode 100644
index 00000000000..dff3ff7340a
--- /dev/null
+++ b/doc/src/sgml/images/datachecksums.gv
@@ -0,0 +1,14 @@
+digraph G {
+    A -> B [label="SELECT pg_enable_data_checksums()"];
+    B -> C;
+    D -> A;
+    C -> D [label="SELECT pg_disable_data_checksums()"];
+    E -> A [label=" --no-data-checksums"];
+    E -> C [label=" --data-checksums"];
+
+    A [label="off"];
+    B [label="inprogress-on"];
+    C [label="on"];
+    D [label="inprogress-off"];
+    E [label="initdb"];
+}
diff --git a/doc/src/sgml/images/datachecksums.svg b/doc/src/sgml/images/datachecksums.svg
new file mode 100644
index 00000000000..8c58f42922e
--- /dev/null
+++ b/doc/src/sgml/images/datachecksums.svg
@@ -0,0 +1,81 @@
+<?xml version="1.0" encoding="UTF-8" standalone="no"?>
+<!-- Generated by graphviz version 14.0.5 (20251129.0259)
+ -->
+<!-- Title: G Pages: 1 -->
+<svg width="409pt" height="383pt"
+ viewBox="0.00 0.00 409.00 383.00" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink">
+<g id="graph0" class="graph" transform="scale(1 1) rotate(0) translate(4 378.5)">
+<title>G</title>
+<polygon fill="white" stroke="none" points="-4,4 -4,-378.5 404.74,-378.5 404.74,4 -4,4"/>
+<!-- A -->
+<g id="node1" class="node">
+<title>A</title>
+<ellipse fill="none" stroke="black" cx="80.12" cy="-268" rx="27" ry="18"/>
+<text xml:space="preserve" text-anchor="middle" x="80.12" y="-262.95" font-family="Times,serif" font-size="14.00">off</text>
+</g>
+<!-- B -->
+<g id="node2" class="node">
+<title>B</title>
+<ellipse fill="none" stroke="black" cx="137.12" cy="-179.5" rx="61.59" ry="18"/>
+<text xml:space="preserve" text-anchor="middle" x="137.12" y="-174.45" font-family="Times,serif" font-size="14.00">inprogress&#45;on</text>
+</g>
+<!-- A&#45;&gt;B -->
+<g id="edge1" class="edge">
+<title>A&#45;&gt;B</title>
+<path fill="none" stroke="black" d="M76.5,-249.68C75.22,-239.14 75.3,-225.77 81.12,-215.5 84.2,-210.08 88.49,-205.38 93.35,-201.34"/>
+<polygon fill="black" stroke="black" points="95.22,-204.31 101.33,-195.66 91.16,-198.61 95.22,-204.31"/>
+<text xml:space="preserve" text-anchor="middle" x="187.62" y="-218.7" font-family="Times,serif" font-size="14.00">SELECT pg_enable_data_checksums()</text>
+</g>
+<!-- C -->
+<g id="node3" class="node">
+<title>C</title>
+<ellipse fill="none" stroke="black" cx="137.12" cy="-106.5" rx="27" ry="18"/>
+<text xml:space="preserve" text-anchor="middle" x="137.12" y="-101.45" font-family="Times,serif" font-size="14.00">on</text>
+</g>
+<!-- B&#45;&gt;C -->
+<g id="edge2" class="edge">
+<title>B&#45;&gt;C</title>
+<path fill="none" stroke="black" d="M137.12,-161.31C137.12,-153.73 137.12,-144.6 137.12,-136.04"/>
+<polygon fill="black" stroke="black" points="140.62,-136.04 137.12,-126.04 133.62,-136.04 140.62,-136.04"/>
+</g>
+<!-- D -->
+<g id="node4" class="node">
+<title>D</title>
+<ellipse fill="none" stroke="black" cx="63.12" cy="-18" rx="63.12" ry="18"/>
+<text xml:space="preserve" text-anchor="middle" x="63.12" y="-12.95" font-family="Times,serif" font-size="14.00">inprogress&#45;off</text>
+</g>
+<!-- C&#45;&gt;D -->
+<g id="edge4" class="edge">
+<title>C&#45;&gt;D</title>
+<path fill="none" stroke="black" d="M124.23,-90.43C113.36,-77.73 97.58,-59.28 84.77,-44.31"/>
+<polygon fill="black" stroke="black" points="87.78,-42.44 78.62,-37.12 82.46,-46.99 87.78,-42.44"/>
+<text xml:space="preserve" text-anchor="middle" x="214.75" y="-57.2" font-family="Times,serif" font-size="14.00">SELECT pg_disable_data_checksums()</text>
+</g>
+<!-- D&#45;&gt;A -->
+<g id="edge3" class="edge">
+<title>D&#45;&gt;A</title>
+<path fill="none" stroke="black" d="M62.52,-36.28C61.62,-68.21 60.54,-138.57 66.12,-197.5 67.43,-211.24 70.27,-226.28 73.06,-238.85"/>
+<polygon fill="black" stroke="black" points="69.64,-239.59 75.32,-248.54 76.46,-238 69.64,-239.59"/>
+</g>
+<!-- E -->
+<g id="node5" class="node">
+<title>E</title>
+<ellipse fill="none" stroke="black" cx="198.12" cy="-356.5" rx="32.41" ry="18"/>
+<text xml:space="preserve" text-anchor="middle" x="198.12" y="-351.45" font-family="Times,serif" font-size="14.00">initdb</text>
+</g>
+<!-- E&#45;&gt;A -->
+<g id="edge5" class="edge">
+<title>E&#45;&gt;A</title>
+<path fill="none" stroke="black" d="M179.16,-341.6C159.64,-327.29 129.05,-304.86 107.03,-288.72"/>
+<polygon fill="black" stroke="black" points="109.23,-286 99.1,-282.91 105.09,-291.64 109.23,-286"/>
+<text xml:space="preserve" text-anchor="middle" x="208.57" y="-307.2" font-family="Times,serif" font-size="14.00"> &#45;&#45;no&#45;data&#45;checksums</text>
+</g>
+<!-- E&#45;&gt;C -->
+<g id="edge6" class="edge">
+<title>E&#45;&gt;C</title>
+<path fill="none" stroke="black" d="M227.13,-348.04C242.29,-342.72 259.95,-334.06 271.12,-320.5 301.5,-283.62 316.36,-257.78 294.12,-215.5 268.41,-166.6 209.42,-135.53 171.52,-119.85"/>
+<polygon fill="black" stroke="black" points="172.96,-116.65 162.37,-116.21 170.37,-123.16 172.96,-116.65"/>
+<text xml:space="preserve" text-anchor="middle" x="350.87" y="-218.7" font-family="Times,serif" font-size="14.00"> &#45;&#45;data&#45;checksums</text>
+</g>
+</g>
+</svg>
diff --git a/doc/src/sgml/monitoring.sgml b/doc/src/sgml/monitoring.sgml
index b77d189a500..49a38c6a136 100644
--- a/doc/src/sgml/monitoring.sgml
+++ b/doc/src/sgml/monitoring.sgml
@@ -3622,9 +3622,14 @@ description | Waiting for a newly initialized WAL file to reach durable storage
       </para>
       <para>
        Number of data page checksum failures detected in this
-       database (or on a shared object), or NULL if data checksums are
-       disabled.
-      </para></entry>
+       database (or on a shared object).  Detected failures are not reset if
+       the <xref linkend="guc-data-checksums"/> setting changes.  Clusters
+       which are initialized without data checksums will show this as
+       <literal>0</literal>. In <productname>PostgreSQL</productname> version
+       18 and earlier, this was set to <literal>NULL</literal> for clusters
+       with data checksums disabled.
+      </para>
+     </entry>
      </row>
 
      <row>
@@ -3633,8 +3638,8 @@ description | Waiting for a newly initialized WAL file to reach durable storage
       </para>
       <para>
        Time at which the last data page checksum failure was detected in
-       this database (or on a shared object), or NULL if data checksums are
-       disabled.
+       this database (or on a shared object). Last failure is reported
+       regardless of the <xref linkend="guc-data-checksums"/> setting.
       </para></entry>
      </row>
 
@@ -7117,6 +7122,226 @@ FROM pg_stat_get_backend_idset() AS backendid;
 
  </sect2>
 
+ <sect2 id="data-checksum-progress-reporting">
+  <title>Data Checksum Progress Reporting</title>
+
+  <indexterm>
+   <primary>pg_stat_progress_data_checksums</primary>
+  </indexterm>
+
+  <para>
+   When data checksums are being enabled on a running cluster, the
+   <structname>pg_stat_progress_data_checksums</structname> view will contain
+   a row for the launcher process, and one row for each worker process which
+   is currently calculating and writing checksums for the data pages in a database.
+   The launcher provides overview of the overall progress (how many databases
+   have been processed, how many remain), while the workers track progress for
+   currently processed databases.
+  </para>
+
+  <table id="pg-stat-progress-data-checksums-view" xreflabel="pg_stat_progress_data_checksums">
+   <title><structname>pg_stat_progress_data_checksums</structname> View</title>
+   <tgroup cols="1">
+    <thead>
+     <row>
+      <entry role="catalog_table_entry">
+       <para role="column_definition">
+        Column Type
+       </para>
+       <para>
+        Description
+       </para>
+      </entry>
+     </row>
+    </thead>
+
+    <tbody>
+     <row>
+      <entry role="catalog_table_entry">
+       <para role="column_definition">
+        <structfield>pid</structfield> <type>integer</type>
+       </para>
+       <para>
+        Process ID of the data checksum process, launcher or worker.
+       </para>
+      </entry>
+     </row>
+
+     <row>
+      <entry role="catalog_table_entry">
+       <para role="column_definition">
+        <structfield>datid</structfield> <type>oid</type>
+       </para>
+       <para>
+        OID of this database, or <literal>0</literal> for the launcher process.
+       </para>
+      </entry>
+     </row>
+
+     <row>
+      <entry role="catalog_table_entry">
+       <para role="column_definition">
+        <structfield>datname</structfield> <type>name</type>
+       </para>
+       <para>
+        Name of this database, or <literal>NULL</literal> for the
+        launcher process.
+       </para>
+      </entry>
+     </row>
+
+     <row>
+      <entry role="catalog_table_entry">
+       <para role="column_definition">
+        <structfield>phase</structfield> <type>text</type>
+       </para>
+       <para>
+        Current processing phase, see <xref linkend="datachecksum-phases"/>
+        for description of the phases.
+       </para>
+      </entry>
+     </row>
+
+     <row>
+      <entry role="catalog_table_entry">
+       <para role="column_definition">
+        <structfield>databases_total</structfield> <type>integer</type>
+       </para>
+       <para>
+        The total number of databases which will be processed. Only the
+        launcher process has this value set, the worker processes have this
+        set to <literal>NULL</literal>.
+       </para>
+      </entry>
+     </row>
+
+     <row>
+      <entry role="catalog_table_entry">
+       <para role="column_definition">
+        <structfield>databases_done</structfield> <type>integer</type>
+       </para>
+       <para>
+        The number of databases which have been processed. Only the launcher
+        process has this value set, the worker processes have this set to
+        <literal>NULL</literal>.
+       </para>
+      </entry>
+     </row>
+
+     <row>
+      <entry role="catalog_table_entry">
+       <para role="column_definition">
+        <structfield>relations_total</structfield> <type>integer</type>
+       </para>
+       <para>
+        The total number of relations which will be processed, or
+        <literal>NULL</literal> if the worker process hasn't
+        calculated the number of relations yet. The launcher process has
+        this set to <literal>NULL</literal> since it isn't responsible for
+        processing relations, only launching worker processes.
+       </para>
+      </entry>
+     </row>
+
+     <row>
+      <entry role="catalog_table_entry">
+       <para role="column_definition">
+        <structfield>relations_done</structfield> <type>integer</type>
+       </para>
+       <para>
+        The number of relations which have been processed. The launcher
+        process has this set to <literal>NULL</literal>.
+       </para>
+      </entry>
+     </row>
+
+     <row>
+      <entry role="catalog_table_entry">
+       <para role="column_definition">
+        <structfield>blocks_total</structfield> <type>integer</type>
+       </para>
+       <para>
+        The number of blocks in the current relation which will be processed,
+        or <literal>NULL</literal> if the worker process hasn't
+        calculated the number of blocks yet. The launcher process has
+        this set to <literal>NULL</literal>.
+       </para>
+      </entry>
+     </row>
+
+     <row>
+      <entry role="catalog_table_entry">
+       <para role="column_definition">
+        <structfield>blocks_done</structfield> <type>integer</type>
+       </para>
+       <para>
+        The number of blocks in the current relation which have been processed.
+        The launcher process has this set to <literal>NULL</literal>.
+       </para>
+      </entry>
+     </row>
+
+    </tbody>
+   </tgroup>
+  </table>
+
+  <table id="datachecksum-phases">
+   <title>Data Checksum Phases</title>
+   <tgroup cols="2">
+    <colspec colname="col1" colwidth="1*"/>
+    <colspec colname="col2" colwidth="2*"/>
+    <thead>
+     <row>
+      <entry>Phase</entry>
+      <entry>Description</entry>
+     </row>
+    </thead>
+    <tbody>
+     <row>
+      <entry><literal>enabling</literal></entry>
+      <entry>
+       The command is currently enabling data checksums on the cluster.
+      </entry>
+     </row>
+     <row>
+      <entry><literal>disabling</literal></entry>
+      <entry>
+       The command is currently disabling data checksums on the cluster.
+      </entry>
+     </row>
+     <row>
+      <entry><literal>done</literal></entry>
+      <entry>
+       The command is done and the data checksum state in the cluster has
+       changed.
+      </entry>
+     </row>
+     <row>
+      <entry><literal>waiting on barrier</literal></entry>
+      <entry>
+       The command is currently waiting for the current active backends to
+       acknowledge the change in data checksum state.
+      </entry>
+     </row>
+     <row>
+      <entry><literal>waiting on checkpoint</literal></entry>
+      <entry>
+       The command is currently waiting for a checkpoint to update the checksum
+       state before finishing.
+      </entry>
+     </row>
+     <row>
+      <entry><literal>waiting on temporary tables</literal></entry>
+      <entry>
+       The command is currently waiting for all temporary tables which existed
+       at the time the command was started to be removed.
+      </entry>
+     </row>
+    </tbody>
+   </tgroup>
+  </table>
+ </sect2>
+
  </sect1>
 
  <sect1 id="dynamic-trace">
diff --git a/doc/src/sgml/ref/pg_checksums.sgml b/doc/src/sgml/ref/pg_checksums.sgml
index e9e393495df..e764b8be04d 100644
--- a/doc/src/sgml/ref/pg_checksums.sgml
+++ b/doc/src/sgml/ref/pg_checksums.sgml
@@ -45,6 +45,12 @@ PostgreSQL documentation
    exit status is nonzero if the operation failed.
   </para>
 
+  <para>
+   When enabling checksums, if checksums were in the process of being enabled
+   when the cluster was shut down, <application>pg_checksums</application>
+   will still process all relations regardless of the online processing.
+  </para>
+
   <para>
    When verifying checksums, every file in the cluster is scanned. When
    enabling checksums, each relation file block with a changed checksum is
diff --git a/doc/src/sgml/regress.sgml b/doc/src/sgml/regress.sgml
index d80dd46c5fd..26cf1c2868c 100644
--- a/doc/src/sgml/regress.sgml
+++ b/doc/src/sgml/regress.sgml
@@ -275,6 +275,18 @@ make check-world PG_TEST_EXTRA='kerberos ldap ssl load_balance libpq_encryption'
 </programlisting>
    The following values are currently supported:
    <variablelist>
+    <varlistentry>
+     <term><literal>checksum_extended</literal></term>
+     <listitem>
+      <para>
+       Runs additional tests for enabling data checksums which inject delays
+       and re-tries in the processing, as well as tests that run pgbench
+       concurrently and randomly restarts the cluster.  Some of these test
+       suites require injection points enabled in the installation.
+      </para>
+     </listitem>
+    </varlistentry>
+
     <varlistentry>
      <term><literal>kerberos</literal></term>
      <listitem>
diff --git a/doc/src/sgml/wal.sgml b/doc/src/sgml/wal.sgml
index f3b86b26be9..df41fb54cb6 100644
--- a/doc/src/sgml/wal.sgml
+++ b/doc/src/sgml/wal.sgml
@@ -246,9 +246,10 @@
   <para>
    Checksums can be disabled when the cluster is initialized using <link
    linkend="app-initdb-data-checksums"><application>initdb</application></link>.
-   They can also be enabled or disabled at a later time as an offline
-   operation. Data checksums are enabled or disabled at the full cluster
-   level, and cannot be specified individually for databases or tables.
+   They can also be enabled or disabled at a later time either as an offline
+   operation or online in a running cluster allowing concurrent access. Data
+   checksums are enabled or disabled at the full cluster level, and cannot be
+   specified individually for databases or tables.
   </para>
 
   <para>
@@ -265,7 +266,7 @@
   </para>
 
   <sect2 id="checksums-offline-enable-disable">
-   <title>Off-line Enabling of Checksums</title>
+   <title>Offline Enabling of Checksums</title>
 
    <para>
     The <link linkend="app-pgchecksums"><application>pg_checksums</application></link>
@@ -274,6 +275,124 @@
    </para>
 
   </sect2>
+
+  <sect2 id="checksums-online-enable-disable">
+   <title>Online Enabling of Checksums</title>
+
+   <para>
+    Checksums can be enabled or disabled online, by calling the appropriate
+    <link linkend="functions-admin-checksum">functions</link>.
+   </para>
+
+   <para>
+    Both enabling and disabling data checksums happens in two phases, separated
+    by a checkpoint to ensure durability.  The different states, and their
+    transitions, are illustrated in <xref linkend="data-checksums-states-figure"/>
+    and discussed in further detail in this section.
+   </para>
+
+   <para>
+    <figure id="data-checksums-states-figure">
+     <title>data checksums states</title>
+     <mediaobject>
+      <imageobject>
+       <imagedata fileref="images/datachecksums.svg" format="SVG" width="100%"/>
+      </imageobject>
+     </mediaobject>
+    </figure>
+   </para>
+
+   <para>
+    Enabling checksums will put the cluster checksum mode in
+    <literal>inprogress-on</literal> mode.  During this time, checksums will be
+    written but not verified. In addition to this, a background worker process
+    is started that enables checksums on all existing data in the cluster. Once
+    this worker has completed processing all databases in the cluster, the
+    checksum mode will automatically switch to <literal>on</literal>. The
+    processing will consume two background worker processes, make sure that
+    <varname>max_worker_processes</varname> allows for at least two more
+    additional processes.
+   </para>
+
+   <para>
+    The process will initially wait for all open transactions to finish before
+    it starts, so that it can be certain that there are no tables that have been
+    created inside a transaction that has not committed yet and thus would not
+    be visible to the process enabling checksums. It will also, for each database,
+    wait for all pre-existing temporary tables to get removed before it finishes.
+    If long-lived temporary tables are used in the application it may be necessary
+    to terminate these application connections to allow the process to complete.
+   </para>
+
+   <para>
+    If the cluster is stopped while in <literal>inprogress-on</literal> mode,
+    for any reason, then the checksum enable process must be restarted manually.
+    To do this, re-execute the function <function>pg_enable_data_checksums()</function>
+    once the cluster has been restarted. The process will start over, there is
+    no support for resuming work from where it was interrupted.  If the cluster
+    is stopped while in <literal>inprogress-off</literal>, then the checksum
+    state will be set to <literal>off</literal> when the cluster is
+    restarted.
+   </para>
+
+   <para>
+    Disabling data checksums will set the data checksum mode to
+    <literal>inprogress-off</literal>.  During this time, checksums will be
+    written but not verified.  After all processes acknowledge the change,
+    the mode will automatically be set to <literal>off</literal>.
+   </para>
+
+   <para>
+    Disabling data checksums while data checksums are actively being enabled
+    will abort the current processing.  There is no way to restart from where
+    processing was interrupted.
+   </para>
+
+   <sect3 id="checksums-online-system-impact">
+    <title>Impact on system of online operations</title>
+    <para>
+     Enabling data checksums can cause significant I/O to the system, as all of the
+     database pages will need to be rewritten, and will be written both to the
+     data files and the WAL. The impact may be limited by throttling using the
+     <parameter>cost_delay</parameter> and <parameter>cost_limit</parameter>
+     parameters of the <function>pg_enable_data_checksums</function> function.
+    </para>
+
+    <para>
+     <itemizedlist>
+      <listitem><para>
+       I/O: all pages need to have data checksums calculated and written which
+       will generate a lot of dirty pages that will need to be flushed to disk,
+       as well as WAL logged.
+      </para></listitem>
+      <listitem><para>
+       Replication: When the standby receives the data checksum state change
+       in the WAL stream it will issue a <glossterm linkend="glossary-restartpoint">
+       restartpoint</glossterm> in order to flush the current state into the
+       <filename>pg_control</filename> file.  The restartpoint will flush the
+       current state to disk and will block redo until finished.  This in turn
+       will induce replication lag, which on synchronous standbys also blocks
+       the primary.  Reducing <xref linkend="guc-max-wal-size"/> before the
+       process is started can help with reducing the time it takes for the
+       restartpoint to finish.
+      </para></listitem>
+      <listitem><para>
+       Shutdown/Restart: If the server is shut down or restarted when data
+       checksums are being enabled, the process will not resume and all pages
+       need to be recalculated and rewritten.  Enabling data checksums should
+       be done when there is no need for regular maintenance or during a
+       service window.
+      </para></listitem>
+     </itemizedlist>
+    </para>
+
+    <para>
+     Rewriting all pages is not needed when disabling data checksums, but
+     checkpoints are still required.
+    </para>
+   </sect3>
+
+  </sect2>
  </sect1>
 
   <sect1 id="wal-intro">
diff --git a/src/backend/access/rmgrdesc/xlogdesc.c b/src/backend/access/rmgrdesc/xlogdesc.c
index ff078f22264..8a854312ea1 100644
--- a/src/backend/access/rmgrdesc/xlogdesc.c
+++ b/src/backend/access/rmgrdesc/xlogdesc.c
@@ -18,6 +18,7 @@
 #include "access/xlog.h"
 #include "access/xlog_internal.h"
 #include "catalog/pg_control.h"
+#include "storage/bufpage.h"
 #include "utils/guc.h"
 #include "utils/timestamp.h"
 
@@ -54,6 +55,25 @@ get_wal_level_string(int wal_level)
 	return wal_level_str;
 }
 
+static const char *
+get_checksum_version_string(ChecksumType checksum)
+{
+	switch (checksum)
+	{
+		case PG_DATA_CHECKSUM_VERSION:
+			return "on";
+		case PG_DATA_CHECKSUM_INPROGRESS_OFF_VERSION:
+			return "inprogress-off";
+		case PG_DATA_CHECKSUM_INPROGRESS_ON_VERSION:
+			return "inprogress-on";
+		case PG_DATA_CHECKSUM_OFF:
+			return "off";
+	}
+
+	Assert(false);
+	return "?";
+}
+
 void
 xlog_desc(StringInfo buf, XLogReaderState *record)
 {
@@ -69,7 +89,8 @@ xlog_desc(StringInfo buf, XLogReaderState *record)
 						 "tli %u; prev tli %u; fpw %s; wal_level %s; logical decoding %s; xid %u:%u; oid %u; multi %u; offset %" PRIu64 "; "
 						 "oldest xid %u in DB %u; oldest multi %u in DB %u; "
 						 "oldest/newest commit timestamp xid: %u/%u; "
-						 "oldest running xid %u; %s",
+						 "oldest running xid %u; "
+						 "checksums %s; %s",
 						 LSN_FORMAT_ARGS(checkpoint->redo),
 						 checkpoint->ThisTimeLineID,
 						 checkpoint->PrevTimeLineID,
@@ -88,6 +109,7 @@ xlog_desc(StringInfo buf, XLogReaderState *record)
 						 checkpoint->oldestCommitTsXid,
 						 checkpoint->newestCommitTsXid,
 						 checkpoint->oldestActiveXid,
+						 get_checksum_version_string(checkpoint->dataChecksumVersion),
 						 (info == XLOG_CHECKPOINT_SHUTDOWN) ? "shutdown" : "online");
 	}
 	else if (info == XLOG_NEXTOID)
@@ -163,10 +185,12 @@ xlog_desc(StringInfo buf, XLogReaderState *record)
 	}
 	else if (info == XLOG_CHECKPOINT_REDO)
 	{
-		int			wal_level;
+		xl_checkpoint_redo xlrec;
 
-		memcpy(&wal_level, rec, sizeof(int));
-		appendStringInfo(buf, "wal_level %s", get_wal_level_string(wal_level));
+		memcpy(&xlrec, rec, sizeof(xl_checkpoint_redo));
+		appendStringInfo(buf, "wal_level %s; checksums %s",
+						 get_wal_level_string(xlrec.wal_level),
+						 get_checksum_version_string(xlrec.data_checksum_version));
 	}
 	else if (info == XLOG_LOGICAL_DECODING_STATUS_CHANGE)
 	{
@@ -175,6 +199,13 @@ xlog_desc(StringInfo buf, XLogReaderState *record)
 		memcpy(&enabled, rec, sizeof(bool));
 		appendStringInfoString(buf, enabled ? "true" : "false");
 	}
+	else if (info == XLOG_CHECKSUMS)
+	{
+		xl_checksum_state xlrec;
+
+		memcpy(&xlrec, rec, sizeof(xl_checksum_state));
+		appendStringInfoString(buf, get_checksum_version_string(xlrec.new_checksumtype));
+	}
 }
 
 const char *
@@ -229,6 +260,9 @@ xlog_identify(uint8 info)
 		case XLOG_LOGICAL_DECODING_STATUS_CHANGE:
 			id = "LOGICAL_DECODING_STATUS_CHANGE";
 			break;
+		case XLOG_CHECKSUMS:
+			id = "CHECKSUMS";
+			break;
 	}
 
 	return id;
diff --git a/src/backend/access/transam/xlog.c b/src/backend/access/transam/xlog.c
index 13ec6225b85..dd54bb8f3e0 100644
--- a/src/backend/access/transam/xlog.c
+++ b/src/backend/access/transam/xlog.c
@@ -552,6 +552,9 @@ typedef struct XLogCtlData
 	 */
 	XLogRecPtr	lastFpwDisableRecPtr;
 
+	/* last data_checksum_version we've seen */
+	uint32		data_checksum_version;
+
 	slock_t		info_lck;		/* locks shared variables shown above */
 } XLogCtlData;
 
@@ -575,6 +578,46 @@ static WALInsertLockPadded *WALInsertLocks = NULL;
  */
 static ControlFileData *ControlFile = NULL;
 
+/*
+ * This must match largest number of sets in barrier_eq and barrier_ne in the
+ * below checksum_barriers definition.
+ */
+#define MAX_BARRIER_CONDITIONS 2
+
+/*
+ * 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.
+ */
+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] =
+{
+	{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},
+};
+
 /*
  * Calculate the amount of space left on the page after 'endptr'. Beware
  * multiple evaluation!
@@ -649,6 +692,23 @@ static XLogRecPtr LocalMinRecoveryPoint;
 static TimeLineID LocalMinRecoveryPointTLI;
 static bool updateMinRecoveryPoint = true;
 
+/*
+ * Local state for Controlfile data_checksum_version.  After initialization
+ * this is only updated when absorbing a procsignal barrier during interrupt
+ * processing.  The reason for keeping a copy in backend-private memory is to
+ * avoid locking for interrogating the data checksum state.  Possible values
+ * are the data checksum versions defined in storage/checksum.h.
+ */
+static ChecksumType LocalDataChecksumVersion = 0;
+
+/*
+ * Variable backing the GUC, keep it in sync with LocalDataChecksumVersion.
+ * See SetLocalDataChecksumVersion().
+ */
+int			data_checksums = 0;
+
+static void SetLocalDataChecksumVersion(uint32 data_checksum_version);
+
 /* For WALInsertLockAcquire/Release functions */
 static int	MyLockNo = 0;
 static bool holdingAllLocks = false;
@@ -716,6 +776,8 @@ static void WALInsertLockAcquireExclusive(void);
 static void WALInsertLockRelease(void);
 static void WALInsertLockUpdateInsertingAt(XLogRecPtr insertingAt);
 
+static void XLogChecksums(uint32 new_type);
+
 /*
  * Insert an XLOG record represented by an already-constructed chain of data
  * chunks.  This is a low-level routine; to construct the WAL record header
@@ -830,9 +892,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
@@ -4270,6 +4333,12 @@ 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;
+
+	/*
+	 * 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;
 }
 
 static void
@@ -4604,10 +4673,6 @@ ReadControlFile(void)
 		(SizeOfXLogLongPHD - SizeOfXLogShortPHD);
 
 	CalculateCheckpointSegments();
-
-	/* Make the initdb settings visible as GUC variables, too */
-	SetConfigOption("data_checksums", DataChecksumsEnabled() ? "yes" : "no",
-					PGC_INTERNAL, PGC_S_DYNAMIC_DEFAULT);
 }
 
 /*
@@ -4641,13 +4706,411 @@ GetMockAuthenticationNonce(void)
 }
 
 /*
- * Are checksums enabled for data pages?
+ * DataChecksumsNeedWrite
+ *		Returns whether data checksums must be written or not
+ *
+ * Returns true iff data checksums are enabled or are in the process of being
+ * enabled. During "inprogress-on" and "inprogress-off" states checksums must
+ * be written even though they are not verified (see datachecksumsworker.c for
+ * a longer discussion).
+ *
+ * This function is intended for callsites which are about to write a data page
+ * to storage, and need to know whether to re-calculate the checksum for the
+ * page header. Calling this function must be performed as close to the write
+ * operation as possible to keep the critical section short.
+ */
+bool
+DataChecksumsNeedWrite(void)
+{
+	return (LocalDataChecksumVersion == PG_DATA_CHECKSUM_VERSION ||
+			LocalDataChecksumVersion == PG_DATA_CHECKSUM_INPROGRESS_ON_VERSION ||
+			LocalDataChecksumVersion == PG_DATA_CHECKSUM_INPROGRESS_OFF_VERSION);
+}
+
+/*
+ * DataChecksumsNeedVerify
+ *		Returns whether data checksums must be verified or not
+ *
+ * Data checksums are only verified if they are fully enabled in the cluster.
+ * During the "inprogress-on" and "inprogress-off" states they are only
+ * updated, not verified (see datachecksumsworker.c for a longer discussion).
+ *
+ * This function is intended for callsites which have read data and are about
+ * to perform checksum validation based on the result of this.  Calling this
+ * function must be performed as close to the validation call as possible to
+ * keep the critical section short. This is in order to protect against time of
+ * check/time of use situations around data checksum validation.
  */
 bool
-DataChecksumsEnabled(void)
+DataChecksumsNeedVerify(void)
+{
+	return (LocalDataChecksumVersion == PG_DATA_CHECKSUM_VERSION);
+}
+
+/*
+ * DataChecksumsOnInProgress
+ *		Returns whether data checksums are being enabled
+ *
+ * Most operations don't need to worry about the "inprogress" states, and
+ * should use DataChecksumsNeedVerify() or DataChecksumsNeedWrite(). The
+ * "inprogress-on" state for enabling checksums is used when the checksum
+ * worker is setting checksums on all pages, it can thus be used to check for
+ * aborted checksum processing which need to be restarted.
+ */
+inline bool
+DataChecksumsOnInProgress(void)
+{
+	return (LocalDataChecksumVersion == PG_DATA_CHECKSUM_INPROGRESS_ON_VERSION);
+}
+
+/*
+ * DataChecksumsOffInProgress
+ *		Returns whether data checksums are being disabled
+ *
+ * The "inprogress-off" state for disabling checksums is used for when the
+ * worker resets the catalog state.  DataChecksumsNeedVerify() or
+ * DataChecksumsNeedWrite() should be used for deciding whether to read/write
+ * checksums.
+ */
+bool
+DataChecksumsOffInProgress(void)
+{
+	return (LocalDataChecksumVersion == PG_DATA_CHECKSUM_INPROGRESS_OFF_VERSION);
+}
+
+/*
+ * SetDataChecksumsOnInProgress
+ *		Sets the data checksum state to "inprogress-on" to enable checksums
+ *
+ * To start the process of enabling data checksums in a running cluster the
+ * data_checksum_version state must be changed to "inprogress-on". See
+ * SetDataChecksumsOn below for a description on how this state change works.
+ * This function blocks until all backends in the cluster have acknowledged the
+ * state transition.
+ */
+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);
+	Assert(XLogCtl->data_checksum_version == PG_DATA_CHECKSUM_OFF);
+	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();
+
+	/*
+	 * 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);
+}
+
+/*
+ * SetDataChecksumsOn
+ *		Enables data checksums cluster-wide
+ *
+ * Enabling data checksums is performed using two barriers, the first one to
+ * set the state to "inprogress-on" (done by SetDataChecksumsOnInProgress())
+ * and the second one to set the state to "on" (done here). Below is a short
+ * description of the processing, a more detailed write-up can be found in
+ * datachecksumsworker.c.
+ *
+ * To start the process of enabling data checksums in a running cluster the
+ * data_checksum_version state must be changed to "inprogress-on".  This state
+ * requires data checksums to be written but not verified. This ensures that
+ * all data pages can be checksummed without the risk of false negatives in
+ * validation during the process.  When all existing pages are guaranteed to
+ * have checksums, and all new pages will be initiated with checksums, the
+ * state can be changed to "on". Once the state is "on" checksums will be both
+ * written and verified. See datachecksumsworker.c for a longer discussion on
+ * how data checksums can be enabled in a running cluster.
+ *
+ * This function blocks until all backends in the cluster have acknowledged the
+ * state transition.
+ */
+void
+SetDataChecksumsOn(void)
+{
+	uint64		barrier;
+
+	Assert(ControlFile != NULL);
+
+	SpinLockAcquire(&XLogCtl->info_lck);
+
+	/*
+	 * 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_VERSION)
+	{
+		SpinLockRelease(&XLogCtl->info_lck);
+		elog(PANIC, "checksums not in \"inprogress-on\" mode");
+	}
+
+	SpinLockRelease(&XLogCtl->info_lck);
+
+	INJECTION_POINT("datachecksums-enable-checksums-delay", NULL);
+	START_CRIT_SECTION();
+	MyProc->delayChkptFlags |= DELAY_CHKPT_START;
+
+	XLogChecksums(PG_DATA_CHECKSUM_VERSION);
+
+	SpinLockAcquire(&XLogCtl->info_lck);
+	XLogCtl->data_checksum_version = PG_DATA_CHECKSUM_VERSION;
+	SpinLockRelease(&XLogCtl->info_lck);
+
+	barrier = EmitProcSignalBarrier(PROCSIGNAL_BARRIER_CHECKSUM_ON);
+
+	MyProc->delayChkptFlags &= ~DELAY_CHKPT_START;
+	END_CRIT_SECTION();
+
+	/*
+	 * Await state transition of "on" in all backends. When done we know that
+	 * data checksums are enabled in all backends and data checksums are both
+	 * written and verified.
+	 */
+	WaitForProcSignalBarrier(barrier);
+}
+
+/*
+ * SetDataChecksumsOff
+ *		Disables data checksums cluster-wide
+ *
+ * Disabling data checksums must be performed with two sets of barriers, each
+ * carrying a different state. The state is first set to "inprogress-off"
+ * during which checksums are still written but not verified. This ensures that
+ * backends which have yet to observe the state change from "on" won't get
+ * validation errors on concurrently modified pages. Once all backends have
+ * changed to "inprogress-off", the barrier for moving to "off" can be emitted.
+ * This function blocks until all backends in the cluster have acknowledged the
+ * state transition.
+ */
+void
+SetDataChecksumsOff(void)
 {
+	uint64		barrier;
+
 	Assert(ControlFile != NULL);
-	return (ControlFile->data_checksum_version > 0);
+
+	SpinLockAcquire(&XLogCtl->info_lck);
+
+	/* If data checksums are already disabled there is nothing to do */
+	if (XLogCtl->data_checksum_version == 0)
+	{
+		SpinLockRelease(&XLogCtl->info_lck);
+		return;
+	}
+
+	/*
+	 * If data checksums are currently enabled we first transition to the
+	 * "inprogress-off" state during which backends continue to write
+	 * checksums without verifying them. When all backends are in
+	 * "inprogress-off" the next transition to "off" can be performed, after
+	 * which all data checksum processing is disabled.
+	 */
+	if (XLogCtl->data_checksum_version == PG_DATA_CHECKSUM_VERSION)
+	{
+		SpinLockRelease(&XLogCtl->info_lck);
+
+		START_CRIT_SECTION();
+		MyProc->delayChkptFlags |= DELAY_CHKPT_START;
+
+		XLogChecksums(PG_DATA_CHECKSUM_INPROGRESS_OFF_VERSION);
+
+		SpinLockAcquire(&XLogCtl->info_lck);
+		XLogCtl->data_checksum_version = PG_DATA_CHECKSUM_INPROGRESS_OFF_VERSION;
+		SpinLockRelease(&XLogCtl->info_lck);
+
+		barrier = EmitProcSignalBarrier(PROCSIGNAL_BARRIER_CHECKSUM_INPROGRESS_OFF);
+
+		MyProc->delayChkptFlags &= ~DELAY_CHKPT_START;
+		END_CRIT_SECTION();
+
+		/*
+		 * Update local state in all backends to ensure that any backend in
+		 * "on" state is changed to "inprogress-off".
+		 */
+		WaitForProcSignalBarrier(barrier);
+
+		/*
+		 * At this point we know that no backends are verifying data checksums
+		 * during reading. Next, we can safely move to state "off" to also
+		 * stop writing checksums.
+		 */
+	}
+	else
+	{
+		/*
+		 * 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);
+	}
+
+	START_CRIT_SECTION();
+	/* Ensure that we don't incur a checkpoint during disabling checksums */
+	MyProc->delayChkptFlags |= DELAY_CHKPT_START;
+
+	XLogChecksums(PG_DATA_CHECKSUM_OFF);
+
+	SpinLockAcquire(&XLogCtl->info_lck);
+	XLogCtl->data_checksum_version = 0;
+	SpinLockRelease(&XLogCtl->info_lck);
+
+	barrier = EmitProcSignalBarrier(PROCSIGNAL_BARRIER_CHECKSUM_OFF);
+
+	MyProc->delayChkptFlags &= ~DELAY_CHKPT_START;
+	END_CRIT_SECTION();
+
+	WaitForProcSignalBarrier(barrier);
+}
+
+/*
+ * AbsorbDataChecksumsBarrier
+ *		Generic function for absorbing data checksum state changes
+ *
+ * All procsignalbarriers regarding data checksum state changes are absorbed
+ * with this function.  The set of conditions required for the state change to
+ * be accepted are listed in the checksum_barriers struct, target_state is
+ * used to look up the relevant entry.
+ */
+bool
+AbsorbDataChecksumsBarrier(int target_state)
+{
+	const ChecksumBarrierCondition *condition = NULL;
+	int			current = LocalDataChecksumVersion;
+	bool		found = false;
+
+	/*
+	 * If the target state matches the current state then the barrier has been
+	 * repeated.
+	 */
+	if (current == target_state)
+		return true;
+
+	/*
+	 * If the cluster is in recovery we skip the validation of current state
+	 * since the replay is trusted.
+	 */
+	if (InRecovery)
+	{
+		SetLocalDataChecksumVersion(target_state);
+		return true;
+	}
+
+	/*
+	 * Find the barrier condition definition for the target state. Not finding
+	 * a condition would be a grave programmer error as the states are a
+	 * discrete set.
+	 */
+	for (int i = 0; i < lengthof(checksum_barriers); 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 imples 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 the relevant state criteria aren't satisfied, throw an error which
+	 * will be caught by the procsignal machinery for a later retry.
+	 */
+	if (!found)
+		ereport(ERROR,
+				errcode(ERRCODE_INVALID_PARAMETER_VALUE),
+				errmsg("incorrect data checksum state %i for target state %i",
+					   current, target_state));
+
+	SetLocalDataChecksumVersion(target_state);
+	return true;
+}
+
+/*
+ * 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);
+}
+
+void
+SetLocalDataChecksumVersion(uint32 data_checksum_version)
+{
+	LocalDataChecksumVersion = data_checksum_version;
+
+	data_checksums = data_checksum_version;
+}
+
+/* guc hook */
+const char *
+show_data_checksums(void)
+{
+	switch (LocalDataChecksumVersion)
+	{
+		case PG_DATA_CHECKSUM_VERSION:
+			return "on";
+		case PG_DATA_CHECKSUM_INPROGRESS_ON_VERSION:
+			return "inprogress-on";
+		case PG_DATA_CHECKSUM_INPROGRESS_OFF_VERSION:
+			return "inprogress-off";
+		case PG_DATA_CHECKSUM_OFF:
+			return "off";
+	}
+
+	Assert(false);
+	return "?";
 }
 
 /*
@@ -4941,6 +5404,7 @@ LocalProcessControlFile(bool reset)
 	Assert(reset || ControlFile == NULL);
 	ControlFile = palloc_object(ControlFileData);
 	ReadControlFile();
+	SetLocalDataChecksumVersion(ControlFile->data_checksum_version);
 }
 
 /*
@@ -5110,6 +5574,11 @@ XLOGShmemInit(void)
 	XLogCtl->InstallXLogFileSegmentActive = false;
 	XLogCtl->WalWriterSleeping = false;
 
+	/* Use the checksum info from control file */
+	XLogCtl->data_checksum_version = ControlFile->data_checksum_version;
+
+	SetLocalDataChecksumVersion(XLogCtl->data_checksum_version);
+
 	SpinLockInit(&XLogCtl->Insert.insertpos_lck);
 	SpinLockInit(&XLogCtl->info_lck);
 	pg_atomic_init_u64(&XLogCtl->logInsertResult, InvalidXLogRecPtr);
@@ -5184,6 +5653,7 @@ BootStrapXLOG(uint32 data_checksum_version)
 	checkPoint.newestCommitTsXid = InvalidTransactionId;
 	checkPoint.time = (pg_time_t) time(NULL);
 	checkPoint.oldestActiveXid = InvalidTransactionId;
+	checkPoint.dataChecksumVersion = data_checksum_version;
 
 	TransamVariables->nextXid = checkPoint.nextXid;
 	TransamVariables->nextOid = checkPoint.nextOid;
@@ -5798,6 +6268,11 @@ StartupXLOG(void)
 			XLogCtl->SharedRecoveryState = RECOVERY_STATE_ARCHIVE;
 		else
 			XLogCtl->SharedRecoveryState = RECOVERY_STATE_CRASH;
+
+		XLogCtl->data_checksum_version = checkPoint.dataChecksumVersion;
+		SetLocalDataChecksumVersion(checkPoint.dataChecksumVersion);
+		ControlFile->data_checksum_version = checkPoint.dataChecksumVersion;
+
 		SpinLockRelease(&XLogCtl->info_lck);
 
 		/*
@@ -6260,6 +6735,47 @@ StartupXLOG(void)
 	pfree(endOfRecoveryInfo->recoveryStopReason);
 	pfree(endOfRecoveryInfo);
 
+	/*
+	 * If we reach this point with checksums in the state inprogress-on, it
+	 * means that data checksums were in the process of being enabled when the
+	 * cluster shut down. Since processing didn't finish, the operation will
+	 * have to be restarted from scratch since there is no capability to
+	 * continue where it was when the cluster shut down. Thus, revert the
+	 * state back to off, and inform the user with a warning message. Being
+	 * able to restart processing is a TODO, but it wouldn't be possible to
+	 * restart here since we cannot launch a dynamic background worker
+	 * directly from here (it has to be from a regular backend).
+	 */
+	if (XLogCtl->data_checksum_version == PG_DATA_CHECKSUM_INPROGRESS_ON_VERSION)
+	{
+		XLogChecksums(PG_DATA_CHECKSUM_OFF);
+
+		SpinLockAcquire(&XLogCtl->info_lck);
+		XLogCtl->data_checksum_version = 0;
+		SetLocalDataChecksumVersion(XLogCtl->data_checksum_version);
+		SpinLockRelease(&XLogCtl->info_lck);
+
+		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."));
+	}
+
+	/*
+	 * If data checksums were being disabled when the cluster was shut down,
+	 * we know that we have a state where all backends have stopped validating
+	 * checksums and we can move to off instead of prompting the user to
+	 * perform any action.
+	 */
+	if (XLogCtl->data_checksum_version == PG_DATA_CHECKSUM_INPROGRESS_OFF_VERSION)
+	{
+		XLogChecksums(PG_DATA_CHECKSUM_OFF);
+
+		SpinLockAcquire(&XLogCtl->info_lck);
+		XLogCtl->data_checksum_version = 0;
+		SetLocalDataChecksumVersion(XLogCtl->data_checksum_version);
+		SpinLockRelease(&XLogCtl->info_lck);
+	}
+
 	/*
 	 * All done with end-of-recovery actions.
 	 *
@@ -6565,7 +7081,7 @@ GetRedoRecPtr(void)
 	XLogRecPtr	ptr;
 
 	/*
-	 * The possibly not up-to-date copy in XlogCtl is enough. Even if we
+	 * The possibly not up-to-date copy in XLogCtl is enough. Even if we
 	 * grabbed a WAL insertion lock to read the authoritative value in
 	 * Insert->RedoRecPtr, someone might update it just after we've released
 	 * the lock.
@@ -7133,6 +7649,12 @@ CreateCheckPoint(int flags)
 	checkPoint.fullPageWrites = Insert->fullPageWrites;
 	checkPoint.wal_level = wal_level;
 
+	/*
+	 * Get the current data_checksum_version value from xlogctl, valid at the
+	 * time of the checkpoint.
+	 */
+	checkPoint.dataChecksumVersion = XLogCtl->data_checksum_version;
+
 	if (shutdown)
 	{
 		XLogRecPtr	curInsert = XLogBytePosToRecPtr(Insert->CurrBytePos);
@@ -7185,9 +7707,18 @@ CreateCheckPoint(int flags)
 	 */
 	if (!shutdown)
 	{
+		xl_checkpoint_redo redo_rec;
+
+		WALInsertLockAcquire();
+		redo_rec.wal_level = wal_level;
+		SpinLockAcquire(&XLogCtl->info_lck);
+		redo_rec.data_checksum_version = XLogCtl->data_checksum_version;
+		SpinLockRelease(&XLogCtl->info_lck);
+		WALInsertLockRelease();
+
 		/* Include WAL level in record for WAL summarizer's benefit. */
 		XLogBeginInsert();
-		XLogRegisterData(&wal_level, sizeof(wal_level));
+		XLogRegisterData(&redo_rec, sizeof(xl_checkpoint_redo));
 		(void) XLogInsert(RM_XLOG_ID, XLOG_CHECKPOINT_REDO);
 
 		/*
@@ -7243,6 +7774,10 @@ CreateCheckPoint(int flags)
 		checkPoint.nextOid += TransamVariables->oidCount;
 	LWLockRelease(OidGenLock);
 
+	SpinLockAcquire(&XLogCtl->info_lck);
+	checkPoint.dataChecksumVersion = XLogCtl->data_checksum_version;
+	SpinLockRelease(&XLogCtl->info_lck);
+
 	checkPoint.logicalDecodingEnabled = IsLogicalDecodingEnabled();
 
 	MultiXactGetCheckptMulti(shutdown,
@@ -7392,6 +7927,9 @@ CreateCheckPoint(int flags)
 	ControlFile->minRecoveryPoint = InvalidXLogRecPtr;
 	ControlFile->minRecoveryPointTLI = 0;
 
+	/* make sure we start with the checksum version as of the checkpoint */
+	ControlFile->data_checksum_version = checkPoint.dataChecksumVersion;
+
 	/*
 	 * Persist unloggedLSN value. It's reset on crash recovery, so this goes
 	 * unused on non-shutdown checkpoints, but seems useful to store it always
@@ -7535,6 +8073,12 @@ CreateEndOfRecoveryRecord(void)
 	LWLockAcquire(ControlFileLock, LW_EXCLUSIVE);
 	ControlFile->minRecoveryPoint = recptr;
 	ControlFile->minRecoveryPointTLI = xlrec.ThisTimeLineID;
+
+	/* start with the latest checksum version (as of the end of recovery) */
+	SpinLockAcquire(&XLogCtl->info_lck);
+	ControlFile->data_checksum_version = XLogCtl->data_checksum_version;
+	SpinLockRelease(&XLogCtl->info_lck);
+
 	UpdateControlFile();
 	LWLockRelease(ControlFileLock);
 
@@ -7876,6 +8420,10 @@ CreateRestartPoint(int flags)
 			if (flags & CHECKPOINT_IS_SHUTDOWN)
 				ControlFile->state = DB_SHUTDOWNED_IN_RECOVERY;
 		}
+
+		/* we shall start with the latest checksum version */
+		ControlFile->data_checksum_version = lastCheckPoint.dataChecksumVersion;
+
 		UpdateControlFile();
 	}
 	LWLockRelease(ControlFileLock);
@@ -8290,6 +8838,24 @@ XLogReportParameters(void)
 	}
 }
 
+/*
+ * Log the new state of checksums
+ */
+static void
+XLogChecksums(uint32 new_type)
+{
+	xl_checksum_state xlrec;
+	XLogRecPtr	recptr;
+
+	xlrec.new_checksumtype = new_type;
+
+	XLogBeginInsert();
+	XLogRegisterData((char *) &xlrec, sizeof(xl_checksum_state));
+
+	recptr = XLogInsert(RM_XLOG_ID, XLOG_CHECKSUMS);
+	XLogFlush(recptr);
+}
+
 /*
  * Update full_page_writes in shared memory, and write an
  * XLOG_FPW_CHANGE record if necessary.
@@ -8416,6 +8982,11 @@ xlog_redo(XLogReaderState *record)
 		MultiXactAdvanceOldest(checkPoint.oldestMulti,
 							   checkPoint.oldestMultiDB);
 
+		SpinLockAcquire(&XLogCtl->info_lck);
+		XLogCtl->data_checksum_version = checkPoint.dataChecksumVersion;
+		SetLocalDataChecksumVersion(checkPoint.dataChecksumVersion);
+		SpinLockRelease(&XLogCtl->info_lck);
+
 		/*
 		 * No need to set oldestClogXid here as well; it'll be set when we
 		 * redo an xl_clog_truncate if it changed since initialization.
@@ -8475,6 +9046,7 @@ xlog_redo(XLogReaderState *record)
 		/* ControlFile->checkPointCopy always tracks the latest ckpt XID */
 		LWLockAcquire(ControlFileLock, LW_EXCLUSIVE);
 		ControlFile->checkPointCopy.nextXid = checkPoint.nextXid;
+		ControlFile->data_checksum_version = checkPoint.dataChecksumVersion;
 		LWLockRelease(ControlFileLock);
 
 		/*
@@ -8501,6 +9073,8 @@ xlog_redo(XLogReaderState *record)
 	{
 		CheckPoint	checkPoint;
 		TimeLineID	replayTLI;
+		bool		new_state = false;
+		uint64		barrier;
 
 		memcpy(&checkPoint, XLogRecGetData(record), sizeof(CheckPoint));
 		/* In an ONLINE checkpoint, treat the XID counter as a minimum */
@@ -8539,6 +9113,7 @@ xlog_redo(XLogReaderState *record)
 		/* ControlFile->checkPointCopy always tracks the latest ckpt XID */
 		LWLockAcquire(ControlFileLock, LW_EXCLUSIVE);
 		ControlFile->checkPointCopy.nextXid = checkPoint.nextXid;
+		ControlFile->data_checksum_version = checkPoint.dataChecksumVersion;
 		LWLockRelease(ControlFileLock);
 
 		/* TLI should not change in an on-line checkpoint */
@@ -8550,6 +9125,42 @@ xlog_redo(XLogReaderState *record)
 
 		RecoveryRestartPoint(&checkPoint, record);
 
+		/*
+		 * XXX: I'm not sure this is right, and if it is this needs
+		 * refactoring to be pretty (as well as in the _REDO case
+		 */
+		SpinLockAcquire(&XLogCtl->info_lck);
+		XLogCtl->data_checksum_version = checkPoint.dataChecksumVersion;
+		if (checkPoint.dataChecksumVersion != ControlFile->data_checksum_version)
+			new_state = true;
+		SpinLockRelease(&XLogCtl->info_lck);
+
+		if (new_state)
+		{
+			switch (checkPoint.dataChecksumVersion)
+			{
+				case PG_DATA_CHECKSUM_INPROGRESS_ON_VERSION:
+					barrier = EmitProcSignalBarrier(PROCSIGNAL_BARRIER_CHECKSUM_INPROGRESS_ON);
+					WaitForProcSignalBarrier(barrier);
+					break;
+
+				case PG_DATA_CHECKSUM_INPROGRESS_OFF_VERSION:
+					barrier = EmitProcSignalBarrier(PROCSIGNAL_BARRIER_CHECKSUM_INPROGRESS_OFF);
+					WaitForProcSignalBarrier(barrier);
+					break;
+
+				case PG_DATA_CHECKSUM_VERSION:
+					barrier = EmitProcSignalBarrier(PROCSIGNAL_BARRIER_CHECKSUM_ON);
+					WaitForProcSignalBarrier(barrier);
+					break;
+
+				case PG_DATA_CHECKSUM_OFF:
+					barrier = EmitProcSignalBarrier(PROCSIGNAL_BARRIER_CHECKSUM_OFF);
+					WaitForProcSignalBarrier(barrier);
+					break;
+			}
+		}
+
 		/*
 		 * After replaying a checkpoint record, free all smgr objects.
 		 * Otherwise we would never do so for dropped relations, as the
@@ -8707,7 +9318,43 @@ xlog_redo(XLogReaderState *record)
 	}
 	else if (info == XLOG_CHECKPOINT_REDO)
 	{
-		/* nothing to do here, just for informational purposes */
+		xl_checkpoint_redo redo_rec;
+		bool		new_state = false;
+		uint64		barrier;
+
+		memcpy(&redo_rec, XLogRecGetData(record), sizeof(xl_checkpoint_redo));
+
+		SpinLockAcquire(&XLogCtl->info_lck);
+		XLogCtl->data_checksum_version = redo_rec.data_checksum_version;
+		if (redo_rec.data_checksum_version != ControlFile->data_checksum_version)
+			new_state = true;
+		SpinLockRelease(&XLogCtl->info_lck);
+
+		if (new_state)
+		{
+			switch (redo_rec.data_checksum_version)
+			{
+				case PG_DATA_CHECKSUM_INPROGRESS_ON_VERSION:
+					barrier = EmitProcSignalBarrier(PROCSIGNAL_BARRIER_CHECKSUM_INPROGRESS_ON);
+					WaitForProcSignalBarrier(barrier);
+					break;
+
+				case PG_DATA_CHECKSUM_INPROGRESS_OFF_VERSION:
+					barrier = EmitProcSignalBarrier(PROCSIGNAL_BARRIER_CHECKSUM_INPROGRESS_OFF);
+					WaitForProcSignalBarrier(barrier);
+					break;
+
+				case PG_DATA_CHECKSUM_VERSION:
+					barrier = EmitProcSignalBarrier(PROCSIGNAL_BARRIER_CHECKSUM_ON);
+					WaitForProcSignalBarrier(barrier);
+					break;
+
+				case PG_DATA_CHECKSUM_OFF:
+					barrier = EmitProcSignalBarrier(PROCSIGNAL_BARRIER_CHECKSUM_OFF);
+					WaitForProcSignalBarrier(barrier);
+					break;
+			}
+		}
 	}
 	else if (info == XLOG_LOGICAL_DECODING_STATUS_CHANGE)
 	{
@@ -8758,6 +9405,51 @@ xlog_redo(XLogReaderState *record)
 			}
 		}
 	}
+	else if (info == XLOG_CHECKSUMS)
+	{
+		xl_checksum_state state;
+		uint64		barrier;
+
+		memcpy(&state, XLogRecGetData(record), sizeof(xl_checksum_state));
+
+		/*
+		 * XXX Could this end up written to the control file prematurely? IIRC
+		 * that happens during checkpoint, so what if that gets triggered e.g.
+		 * because someone runs CHECKPOINT? If we then crash (or something
+		 * like that), could that confuse the instance?
+		 */
+		SpinLockAcquire(&XLogCtl->info_lck);
+		XLogCtl->data_checksum_version = state.new_checksumtype;
+		SpinLockRelease(&XLogCtl->info_lck);
+
+		/*
+		 * Block on a procsignalbarrier to await all processes having seen the
+		 * change to checksum status. Once the barrier has been passed we can
+		 * initiate the corresponding processing.
+		 */
+		switch (state.new_checksumtype)
+		{
+			case PG_DATA_CHECKSUM_INPROGRESS_ON_VERSION:
+				barrier = EmitProcSignalBarrier(PROCSIGNAL_BARRIER_CHECKSUM_INPROGRESS_ON);
+				WaitForProcSignalBarrier(barrier);
+				break;
+
+			case PG_DATA_CHECKSUM_INPROGRESS_OFF_VERSION:
+				barrier = EmitProcSignalBarrier(PROCSIGNAL_BARRIER_CHECKSUM_INPROGRESS_OFF);
+				WaitForProcSignalBarrier(barrier);
+				break;
+
+			case PG_DATA_CHECKSUM_VERSION:
+				barrier = EmitProcSignalBarrier(PROCSIGNAL_BARRIER_CHECKSUM_ON);
+				WaitForProcSignalBarrier(barrier);
+				break;
+
+			case PG_DATA_CHECKSUM_OFF:
+				barrier = EmitProcSignalBarrier(PROCSIGNAL_BARRIER_CHECKSUM_OFF);
+				WaitForProcSignalBarrier(barrier);
+				break;
+		}
+	}
 }
 
 /*
diff --git a/src/backend/access/transam/xlogfuncs.c b/src/backend/access/transam/xlogfuncs.c
index 2efe4105efb..742ddcf124a 100644
--- a/src/backend/access/transam/xlogfuncs.c
+++ b/src/backend/access/transam/xlogfuncs.c
@@ -26,6 +26,7 @@
 #include "funcapi.h"
 #include "miscadmin.h"
 #include "pgstat.h"
+#include "postmaster/datachecksumsworker.h"
 #include "replication/walreceiver.h"
 #include "storage/fd.h"
 #include "storage/latch.h"
@@ -748,3 +749,50 @@ pg_promote(PG_FUNCTION_ARGS)
 						   wait_seconds)));
 	PG_RETURN_BOOL(false);
 }
+
+/*
+ * Disables data checksums for the cluster, if applicable. Starts a background
+ * worker which turns off the data checksums.
+ */
+Datum
+disable_data_checksums(PG_FUNCTION_ARGS)
+{
+	if (!superuser())
+		ereport(ERROR,
+				errcode(ERRCODE_INSUFFICIENT_PRIVILEGE),
+				errmsg("must be superuser to change data checksum state"));
+
+	StartDataChecksumsWorkerLauncher(DISABLE_DATACHECKSUMS, 0, 0);
+	PG_RETURN_VOID();
+}
+
+/*
+ * Enables data checksums for the cluster, if applicable.  Supports vacuum-
+ * like cost based throttling to limit system load. Starts a background worker
+ * which updates data checksums on existing data.
+ */
+Datum
+enable_data_checksums(PG_FUNCTION_ARGS)
+{
+	int			cost_delay = PG_GETARG_INT32(0);
+	int			cost_limit = PG_GETARG_INT32(1);
+
+	if (!superuser())
+		ereport(ERROR,
+				errcode(ERRCODE_INSUFFICIENT_PRIVILEGE),
+				errmsg("must be superuser to change data checksum state"));
+
+	if (cost_delay < 0)
+		ereport(ERROR,
+				errcode(ERRCODE_INVALID_PARAMETER_VALUE),
+				errmsg("cost delay cannot be a negative value"));
+
+	if (cost_limit <= 0)
+		ereport(ERROR,
+				errcode(ERRCODE_INVALID_PARAMETER_VALUE),
+				errmsg("cost limit must be greater than zero"));
+
+	StartDataChecksumsWorkerLauncher(ENABLE_DATACHECKSUMS, cost_delay, cost_limit);
+
+	PG_RETURN_VOID();
+}
diff --git a/src/backend/access/transam/xlogrecovery.c b/src/backend/access/transam/xlogrecovery.c
index 4fc37a031d9..99e6a5164aa 100644
--- a/src/backend/access/transam/xlogrecovery.c
+++ b/src/backend/access/transam/xlogrecovery.c
@@ -782,6 +782,7 @@ InitWalRecovery(ControlFileData *ControlFile, bool *wasShutdown_ptr,
 		CheckPointTLI = ControlFile->checkPointCopy.ThisTimeLineID;
 		RedoStartLSN = ControlFile->checkPointCopy.redo;
 		RedoStartTLI = ControlFile->checkPointCopy.ThisTimeLineID;
+
 		record = ReadCheckpointRecord(xlogprefetcher, CheckPointLoc,
 									  CheckPointTLI);
 		if (record != NULL)
diff --git a/src/backend/backup/basebackup.c b/src/backend/backup/basebackup.c
index 463c0756b5e..d394e044833 100644
--- a/src/backend/backup/basebackup.c
+++ b/src/backend/backup/basebackup.c
@@ -1613,8 +1613,7 @@ sendFile(bbsink *sink, const char *readfilename, const char *tarfilename,
 	 * enabled for this cluster, and if this is a relation file, then verify
 	 * the checksum.
 	 */
-	if (!noverify_checksums && DataChecksumsEnabled() &&
-		RelFileNumberIsValid(relfilenumber))
+	if (!noverify_checksums && RelFileNumberIsValid(relfilenumber))
 		verify_checksum = true;
 
 	/*
@@ -1747,7 +1746,7 @@ sendFile(bbsink *sink, const char *readfilename, const char *tarfilename,
 		 * If the amount of data we were able to read was not a multiple of
 		 * BLCKSZ, we cannot verify checksums, which are block-level.
 		 */
-		if (verify_checksum && (cnt % BLCKSZ != 0))
+		if (verify_checksum && DataChecksumsNeedVerify() && (cnt % BLCKSZ != 0))
 		{
 			ereport(WARNING,
 					(errmsg("could not verify checksum in file \"%s\", block "
@@ -1842,9 +1841,10 @@ sendFile(bbsink *sink, const char *readfilename, const char *tarfilename,
  * 'blkno' is the block number of the first page in the bbsink's buffer
  * relative to the start of the relation.
  *
- * 'verify_checksum' indicates whether we should try to verify checksums
- * for the blocks we read. If we do this, we'll update *checksum_failures
- * and issue warnings as appropriate.
+ * 'verify_checksum' determines if the user has asked to verify checksums, but
+ * since data checksums can be disabled, or become disabled, we need to check
+ * state before verifying individual pages.  If we do this, we'll update
+ * *checksum_failures and issue warnings as appropriate.
  */
 static off_t
 read_file_data_into_buffer(bbsink *sink, const char *readfilename, int fd,
@@ -1870,6 +1870,13 @@ read_file_data_into_buffer(bbsink *sink, const char *readfilename, int fd,
 		int			reread_cnt;
 		uint16		expected_checksum;
 
+		/*
+		 * The data checksum state can change at any point, so we need to
+		 * re-check before each page.
+		 */
+		if (!DataChecksumsNeedVerify())
+			return cnt;
+
 		page = sink->bbs_buffer + BLCKSZ * i;
 
 		/* If the page is OK, go on to the next one. */
@@ -1892,7 +1899,12 @@ read_file_data_into_buffer(bbsink *sink, const char *readfilename, int fd,
 		 * allows us to wait until we can be certain that no write to the
 		 * block is in progress. Since we don't have any such thing right now,
 		 * we just do this and hope for the best.
+		 *
+		 * The data checksum state may also have changed concurrently so check
+		 * again.
 		 */
+		if (!DataChecksumsNeedVerify())
+			return cnt;
 		reread_cnt =
 			basebackup_read_file(fd, sink->bbs_buffer + BLCKSZ * i,
 								 BLCKSZ, offset + BLCKSZ * i,
@@ -2007,6 +2019,9 @@ verify_page_checksum(Page page, XLogRecPtr start_lsn, BlockNumber blkno,
 	if (PageIsNew(page) || PageGetLSN(page) >= start_lsn)
 		return true;
 
+	if (!DataChecksumsNeedVerify())
+		return true;
+
 	/* Perform the actual checksum calculation. */
 	checksum = pg_checksum_page(page, blkno);
 
diff --git a/src/backend/catalog/system_functions.sql b/src/backend/catalog/system_functions.sql
index eb9e31ae1bf..b4c58652af1 100644
--- a/src/backend/catalog/system_functions.sql
+++ b/src/backend/catalog/system_functions.sql
@@ -657,6 +657,14 @@ LANGUAGE INTERNAL
 STRICT VOLATILE PARALLEL UNSAFE
 AS 'pg_replication_origin_session_setup';
 
+CREATE OR REPLACE FUNCTION
+  pg_enable_data_checksums(cost_delay integer DEFAULT 0,
+                           cost_limit integer DEFAULT 100)
+RETURNS void
+STRICT VOLATILE LANGUAGE internal
+PARALLEL RESTRICTED
+AS 'enable_data_checksums';
+
 --
 -- The default permissions for functions mean that anyone can execute them.
 -- A number of functions shouldn't be executable by just anyone, but rather
@@ -782,6 +790,10 @@ REVOKE EXECUTE ON FUNCTION pg_ls_logicalmapdir() FROM PUBLIC;
 
 REVOKE EXECUTE ON FUNCTION pg_ls_replslotdir(text) FROM PUBLIC;
 
+REVOKE EXECUTE ON FUNCTION pg_enable_data_checksums(integer, integer) FROM public;
+
+REVOKE EXECUTE ON FUNCTION pg_disable_data_checksums() FROM public;
+
 --
 -- We also set up some things as accessible to standard roles.
 --
diff --git a/src/backend/catalog/system_views.sql b/src/backend/catalog/system_views.sql
index 7553f31fef0..47c661fe678 100644
--- a/src/backend/catalog/system_views.sql
+++ b/src/backend/catalog/system_views.sql
@@ -1385,6 +1385,25 @@ CREATE VIEW pg_stat_progress_copy AS
     FROM pg_stat_get_progress_info('COPY') AS S
         LEFT JOIN pg_database D ON S.datid = D.oid;
 
+CREATE VIEW pg_stat_progress_data_checksums AS
+    SELECT
+        S.pid AS pid, S.datid, D.datname AS datname,
+        CASE S.param1 WHEN 0 THEN 'enabling'
+                      WHEN 1 THEN 'disabling'
+                      WHEN 2 THEN 'waiting on temporary tables'
+					  WHEN 3 THEN 'waiting on barrier'
+                      WHEN 4 THEN 'done'
+                      END AS phase,
+        CASE S.param2 WHEN -1 THEN NULL ELSE S.param2 END AS databases_total,
+        S.param3 AS databases_done,
+        CASE S.param4 WHEN -1 THEN NULL ELSE S.param4 END AS relations_total,
+        CASE S.param5 WHEN -1 THEN NULL ELSE S.param5 END AS relations_done,
+        CASE S.param6 WHEN -1 THEN NULL ELSE S.param6 END AS blocks_total,
+        CASE S.param7 WHEN -1 THEN NULL ELSE S.param7 END AS blocks_done
+    FROM pg_stat_get_progress_info('DATACHECKSUMS') AS S
+        LEFT JOIN pg_database D ON S.datid = D.oid
+    ORDER BY S.datid; -- return the launcher process first
+
 CREATE VIEW pg_user_mappings AS
     SELECT
         U.oid       AS umid,
diff --git a/src/backend/postmaster/Makefile b/src/backend/postmaster/Makefile
index 0f4435d2d97..0c36765acfe 100644
--- a/src/backend/postmaster/Makefile
+++ b/src/backend/postmaster/Makefile
@@ -18,6 +18,7 @@ OBJS = \
 	bgworker.o \
 	bgwriter.o \
 	checkpointer.o \
+	datachecksumsworker.o \
 	fork_process.o \
 	interrupt.o \
 	launch_backend.o \
diff --git a/src/backend/postmaster/auxprocess.c b/src/backend/postmaster/auxprocess.c
index 8ea800c0bbd..9dc71dadd8a 100644
--- a/src/backend/postmaster/auxprocess.c
+++ b/src/backend/postmaster/auxprocess.c
@@ -15,6 +15,7 @@
 #include <unistd.h>
 #include <signal.h>
 
+#include "access/xlog.h"
 #include "miscadmin.h"
 #include "pgstat.h"
 #include "postmaster/auxprocess.h"
@@ -68,6 +69,24 @@ AuxiliaryProcessMainCommon(void)
 
 	ProcSignalInit(NULL, 0);
 
+	/*
+	 * Initialize a local cache of the data_checksum_version, to be updated by
+	 * the procsignal-based barriers.
+	 *
+	 * This intentionally happens after initializing the procsignal, otherwise
+	 * we might miss a state change. This means we can get a barrier for the
+	 * state we've just initialized - but it can happen only once.
+	 *
+	 * 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
+	 * control file, which may be arbitrarily old).
+	 *
+	 * NB: Even if the postmaster handled barriers, the value might still be
+	 * stale, as it might have changed after this process forked.
+	 */
+	InitLocalDataChecksumVersion();
+
 	/*
 	 * Auxiliary processes don't run transactions, but they may need a
 	 * resource owner anyway to manage buffer pins acquired outside
diff --git a/src/backend/postmaster/bgworker.c b/src/backend/postmaster/bgworker.c
index 261ccd3f59c..0d9494bbb82 100644
--- a/src/backend/postmaster/bgworker.c
+++ b/src/backend/postmaster/bgworker.c
@@ -18,6 +18,7 @@
 #include "pgstat.h"
 #include "port/atomics.h"
 #include "postmaster/bgworker_internals.h"
+#include "postmaster/datachecksumsworker.h"
 #include "postmaster/postmaster.h"
 #include "replication/logicallauncher.h"
 #include "replication/logicalworker.h"
@@ -142,6 +143,14 @@ static const struct
 	{
 		.fn_name = "SequenceSyncWorkerMain",
 		.fn_addr = SequenceSyncWorkerMain
+	},
+	{
+		.fn_name = "DataChecksumsWorkerLauncherMain",
+		.fn_addr = DataChecksumsWorkerLauncherMain
+	},
+	{
+		.fn_name = "DataChecksumsWorkerMain",
+		.fn_addr = DataChecksumsWorkerMain
 	}
 };
 
diff --git a/src/backend/postmaster/datachecksumsworker.c b/src/backend/postmaster/datachecksumsworker.c
new file mode 100644
index 00000000000..d930d468866
--- /dev/null
+++ b/src/backend/postmaster/datachecksumsworker.c
@@ -0,0 +1,1487 @@
+/*-------------------------------------------------------------------------
+ *
+ * datachecksumsworker.c
+ *	  Background worker for enabling or disabling data checksums online
+ *
+ * When enabling data checksums on a database at initdb time or when shut down
+ * with pg_checksums, no extra process is required as each page is checksummed,
+ * and verified, when accessed.  When enabling checksums on an already running
+ * cluster, this worker will ensure that all pages are checksummed before
+ * verification of the checksums is turned on. In the case of disabling
+ * checksums, the state transition is performed only in the control file, no
+ * changes are performed on the data pages.
+ *
+ * Checksums can be either enabled or disabled cluster-wide, with on/off being
+ * the end state for data_checksums.
+ *
+ * 1. Enabling checksums
+ * ---------------------
+ * 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 wont 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.
+ *
+ * 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.
+
+ * Any new relation in a processed database, created during processing, will
+ * see the in-progress state and will automatically be checksummed.
+ *
+ * For each database, all relations which have storage are read and every data
+ * page is marked dirty to force a write with the checksum. This will generate
+ * a lot of WAL as the entire database is read and written.
+ *
+ * If the processing is interrupted by a cluster crash or restart, it needs to
+ * be restarted from the beginning again as state isn't persisted.
+ *
+ * 2. Disabling checksums
+ * ----------------------
+ * When disabling checksums, data_checksums will be set to "inprogress-off"
+ * which signals that checksums are written but no longer verified. This ensure
+ * that backends which have yet to move from the "on" state will still be able
+ * to process data checksum validation.
+ *
+ * 3. Synchronization and Correctness
+ * ----------------------------------
+ * The processes involved in enabling, or disabling, data checksums in an
+ * online cluster must be properly synchronized with the normal backends
+ * serving concurrent queries to ensure correctness. Correctness is defined
+ * as the following:
+ *
+ *    - Backends SHALL NOT violate the data_checksums state they have agreed to
+ *      by acknowledging the procsignalbarrier:  This means that all backends
+ *      MUST calculate and write data checksums during all states except off;
+ *      MUST validate checksums only in the 'on' state.
+ *    - Data checksums SHALL NOT be considered enabled cluster-wide until all
+ *      currently connected backends have state "on": This means that all
+ *      backends must wait on the procsignalbarrier to be acknowledged by all
+ *      before proceeding to validate data checksums.
+ *
+ * There are two steps of synchronization required for changing data_checksums
+ * in an online cluster: (i) changing state in the active backends ("on",
+ * "off", "inprogress-on" and "inprogress-off"), and (ii) ensuring no
+ * incompatible objects and processes are left in a database when workers end.
+ * The former deals with cluster-wide agreement on data checksum state and the
+ * latter with ensuring that any concurrent activity cannot break the data
+ * checksum contract during processing.
+ *
+ * Synchronizing the state change is done with procsignal barriers, before
+ * updating the controlfile with the state all other backends must absorb the
+ * barrier.  Barrier absorption will happen during interrupt processing, which
+ * 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.
+ *
+ * 3.1 When Enabling Data Checksums
+ * --------------------------------
+ * A process which fails to observe data checksums being enabled can induce two
+ * types of errors: failing to write the checksum when modifying the page and
+ * failing to validate the data checksum on the page when reading it.
+ *
+ * When processing starts all backends belong to one of the below sets, with
+ * one set being empty:
+ *
+ * Bd: Backends in "off" state
+ * Bi: Backends in "inprogress-on" state
+ *
+ * If processing is started in an online cluster then all backends are in Bd.
+ * 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".
+ *
+ * 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.
+ *
+ * 3.2 When Disabling Data Checksums
+ * ---------------------------------
+ * A process which fails to observe that data checksums have been disabled
+ * can induce two types of errors: writing the checksum when modifying the
+ * page and validating a data checksum which is no longer correct due to
+ * modifications to the page. The former is not an error per se as data
+ * integrity is maintained, but it is wasteful.  The latter will cause errors
+ * in user operations.  Assuming the following sets of backends:
+ *
+ * Bg: Backend updating the global state and emitting the procsignalbarrier
+ * Bd: Backends in "off" state
+ * Be: Backends in "on" state
+ * Bo: Backends in "inprogress-off" state
+ * Bi: Backends in "inprogress-on" state
+ *
+ * Backends transition from the Be state to Bd like so: Be -> Bo -> Bd.  From
+ * all other states, the transition can be straight to Bd.
+ *
+ * The goal is to transition all backends to Bd making the others empty sets.
+ * Backends in Bo write data checksums, but don't validate them, such that
+ * backends still in Be can continue to validate pages until the barrier has
+ * been absorbed such that they are in Bo. Once all backends are in Bo, the
+ * barrier to transition to "off" can be raised and all backends can safely
+ * stop writing data checksums as no backend is enforcing data checksum
+ * validation any longer.
+ *
+ * 4. Future opportunities for optimizations
+ * -----------------------------------------
+ * Below are some potential optimizations and improvements which were brought
+ * up during reviews of this feature, but which weren't implemented in the
+ * initial version. These are ideas listed without any validation on their
+ * feasibility or potential payoff. More discussion on (most of) these can be
+ * found on the -hackers threads linked to in the commit message of this
+ * feature.
+ *
+ *   * Launching datachecksumsworker for resuming operation from the startup
+ *     process: Currently users have to restart processing manually after a
+ *     restart since dynamic background worker cannot be started from the
+ *     postmaster. Changing the startup process could make restarting the
+ *     processing automatic on cluster restart.
+ *   * Avoid dirtying the page when checksums already match: Iff the checksum
+ *     on the page happens to already match we still dirty the page. It should
+ *     be enough to only do the log_newpage_buffer() call in that case.
+ *   * Teach pg_checksums to avoid checksummed pages when pg_checksums is used
+ *     to enable checksums on a cluster which is in inprogress-on state and
+ *     may have checksummed pages (make pg_checksums be able to resume an
+ *     online operation). This should only be attempted for wal_level minimal.
+ *   * Restartability (not necessarily with page granularity).
+ *   * Avoid processing databases which were created during inprogress-on.
+ *     Right now all databases are processed regardless to be safe.
+ *
+ *
+ * Portions Copyright (c) 1996-2026, PostgreSQL Global Development Group
+ * Portions Copyright (c) 1994, Regents of the University of California
+ *
+ *
+ * IDENTIFICATION
+ *	  src/backend/postmaster/datachecksumsworker.c
+ *
+ *-------------------------------------------------------------------------
+ */
+#include "postgres.h"
+
+#include "access/genam.h"
+#include "access/heapam.h"
+#include "access/htup_details.h"
+#include "access/xact.h"
+#include "access/xloginsert.h"
+#include "catalog/indexing.h"
+#include "catalog/pg_class.h"
+#include "catalog/pg_database.h"
+#include "commands/progress.h"
+#include "commands/vacuum.h"
+#include "common/relpath.h"
+#include "miscadmin.h"
+#include "pgstat.h"
+#include "postmaster/bgworker.h"
+#include "postmaster/bgwriter.h"
+#include "postmaster/datachecksumsworker.h"
+#include "storage/bufmgr.h"
+#include "storage/checksum.h"
+#include "storage/ipc.h"
+#include "storage/lmgr.h"
+#include "storage/lwlock.h"
+#include "storage/procarray.h"
+#include "storage/smgr.h"
+#include "tcop/tcopprot.h"
+#include "utils/fmgroids.h"
+#include "utils/injection_point.h"
+#include "utils/lsyscache.h"
+#include "utils/ps_status.h"
+#include "utils/syscache.h"
+
+/*
+ * Number of times we retry to open a database before giving up and consider
+ * it to have failed processing.
+ */
+#define DATACHECKSUMSWORKER_MAX_DB_RETRIES 5
+
+/*
+ * 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;
+
+	/*
+	 * Is a launcher process is currently running?  This is set by the main
+	 * launcher process, after it has read the above launch_* parameters.
+	 */
+	bool		launcher_running;
+
+	/*
+	 * If 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;
+
+	/*
+	 * These fields indicate the target state that the launcher is currently
+	 * working towards. They can be different from the corresponding launch_*
+	 * fields, if a new pg_enable/disable_data_checksums() call was made while
+	 * the launcher/worker was already running.
+	 *
+	 * The below members are set when the launcher starts, and are only
+	 * accessed read-only by the single worker. Thus, we can access these
+	 * without a lock. If multiple workers, or dynamic cost parameters, are
+	 * supported at some point then this would need to be revisited.
+	 */
+	DataChecksumsWorkerOperation operation;
+	int			cost_delay;
+	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.
+	 */
+
+	/* result, set by worker before exiting */
+	DataChecksumsWorkerResult success;
+
+	/*
+	 * tells the worker process whether it should also process the shared
+	 * catalogs
+	 */
+	bool		process_shared_catalogs;
+} DataChecksumsWorkerShmemStruct;
+
+/* Shared memory segment for datachecksumsworker */
+static DataChecksumsWorkerShmemStruct *DataChecksumsWorkerShmem;
+
+typedef struct DataChecksumsWorkerDatabase
+{
+	Oid			dboid;
+	char	   *dbname;
+} DataChecksumsWorkerDatabase;
+
+typedef struct DataChecksumsWorkerResultEntry
+{
+	Oid			dboid;
+	DataChecksumsWorkerResult result;
+	int			retries;
+} DataChecksumsWorkerResultEntry;
+
+
+/*
+ * Flag set by the interrupt handler
+ */
+static volatile sig_atomic_t abort_requested = false;
+
+/*
+ * Have we set the DataChecksumsWorkerShmemStruct->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?
+ */
+static DataChecksumsWorkerOperation operation;
+
+/* Prototypes */
+static List *BuildDatabaseList(void);
+static List *BuildRelationList(bool temp_relations, bool include_shared);
+static void FreeDatabaseList(List *dblist);
+static DataChecksumsWorkerResult ProcessDatabase(DataChecksumsWorkerDatabase *db);
+static bool ProcessAllDatabases(void);
+static bool ProcessSingleRelationFork(Relation reln, ForkNumber forkNum, BufferAccessStrategy strategy);
+static void launcher_cancel_handler(SIGNAL_ARGS);
+static void WaitForAllTransactionsToFinish(void);
+
+/*
+ * StartDataChecksumsWorkerLauncher
+ *		Main entry point for datachecksumsworker launcher process
+ *
+ * The main entrypoint for starting data checksums processing for enabling as
+ * well as disabling.
+ */
+void
+StartDataChecksumsWorkerLauncher(DataChecksumsWorkerOperation op,
+								 int cost_delay,
+								 int cost_limit)
+{
+	BackgroundWorker bgw;
+	BackgroundWorkerHandle *bgw_handle;
+	bool		launcher_running;
+
+#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
+
+	/* 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;
+
+	INJECTION_POINT("datachecksumsworker-startup-delay", NULL);
+
+	/* is the launcher already running? */
+	launcher_running = DataChecksumsWorkerShmem->launcher_running;
+
+	LWLockRelease(DataChecksumsWorkerLock);
+
+	/*
+	 * Launch a new launcher process, if it's not running already.
+	 *
+	 * 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.
+	 *
+	 * 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
+	 * twice.  That's OK, the second invocation will see that a launcher is
+	 * already running and exit quickly.
+	 *
+	 * TODO: We could optimize here and skip launching the launcher, if we are
+	 * already in the desired state, i.e. if the checksums are already enabled
+	 * and you call pg_enable_data_checksums().
+	 */
+	if (!launcher_running)
+	{
+		/*
+		 * Prepare the BackgroundWorker and launch it.
+		 */
+		memset(&bgw, 0, sizeof(bgw));
+		bgw.bgw_flags = BGWORKER_SHMEM_ACCESS | BGWORKER_BACKEND_DATABASE_CONNECTION;
+		bgw.bgw_start_time = BgWorkerStart_RecoveryFinished;
+		snprintf(bgw.bgw_library_name, BGW_MAXLEN, "postgres");
+		snprintf(bgw.bgw_function_name, BGW_MAXLEN, "DataChecksumsWorkerLauncherMain");
+		snprintf(bgw.bgw_name, BGW_MAXLEN, "datachecksum launcher");
+		snprintf(bgw.bgw_type, BGW_MAXLEN, "datachecksum launcher");
+		bgw.bgw_restart_time = BGW_NEVER_RESTART;
+		bgw.bgw_notify_pid = MyProcPid;
+		bgw.bgw_main_arg = (Datum) 0;
+
+		if (!RegisterDynamicBackgroundWorker(&bgw, &bgw_handle))
+			ereport(ERROR,
+					errcode(ERRCODE_INSUFFICIENT_RESOURCES),
+					errmsg("failed to start background worker to process data checksums"));
+	}
+	else
+		ereport(ERROR,
+				errmsg("data checksum processing already running"));
+}
+
+/*
+ * ProcessSingleRelationFork
+ *		Enable data checksums in a single relation/fork.
+ *
+ * Returns true if successful, and false if *aborted*. On error, an actual
+ * error is raised in the lower levels.
+ */
+static bool
+ProcessSingleRelationFork(Relation reln, ForkNumber forkNum, BufferAccessStrategy strategy)
+{
+	BlockNumber numblocks = RelationGetNumberOfBlocksInFork(reln, forkNum);
+	char		activity[NAMEDATALEN * 2 + 128];
+	char	   *relns;
+
+	relns = get_namespace_name(RelationGetNamespace(reln));
+
+	if (!relns)
+		return false;
+
+	/* Report the current relation to pgstat_activity */
+	snprintf(activity, sizeof(activity) - 1, "processing: %s.%s (%s, %dblocks)",
+			 relns, RelationGetRelationName(reln), forkNames[forkNum], numblocks);
+	pgstat_report_activity(STATE_RUNNING, activity);
+	pgstat_progress_update_param(PROGRESS_DATACHECKSUMS_BLOCKS_TOTAL, numblocks);
+
+	/*
+	 * We are looping over the blocks which existed at the time of process
+	 * start, which is safe since new blocks are created with checksums set
+	 * already due to the state being "inprogress-on".
+	 */
+	for (BlockNumber blknum = 0; blknum < numblocks; blknum++)
+	{
+		Buffer		buf = ReadBufferExtended(reln, forkNum, blknum, RBM_NORMAL, strategy);
+
+		/* Need to get an exclusive lock before we can flag as dirty */
+		LockBuffer(buf, BUFFER_LOCK_EXCLUSIVE);
+
+		/*
+		 * Mark the buffer as dirty and force a full page write.  We have to
+		 * re-write the page to WAL even if the checksum hasn't changed,
+		 * because if there is a replica it might have a slightly different
+		 * version of the page with an invalid checksum, caused by unlogged
+		 * changes (e.g. hintbits) on the master happening while checksums
+		 * were off. This can happen if there was a valid checksum on the page
+		 * at one point in the past, so only when checksums are first on, then
+		 * off, and then turned on again.  TODO: investigate if this could be
+		 * avoided if the checksum is calculated to be correct and wal_level
+		 * is set to "minimal",
+		 */
+		START_CRIT_SECTION();
+		MarkBufferDirty(buf);
+		log_newpage_buffer(buf, false);
+		END_CRIT_SECTION();
+
+		UnlockReleaseBuffer(buf);
+
+		/*
+		 * 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);
+
+		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));
+
+		/*
+		 * Processing is re-using the vacuum cost delay for process
+		 * throttling, hence why we call vacuum APIs here.
+		 */
+		vacuum_delay_point(false);
+	}
+
+	pfree(relns);
+	return true;
+}
+
+/*
+ * ProcessSingleRelationByOid
+ *		Process a single relation based on oid.
+ *
+ * Returns true if successful, and false if *aborted*. On error, an actual
+ * error is raised in the lower levels.
+ */
+static bool
+ProcessSingleRelationByOid(Oid relationId, BufferAccessStrategy strategy)
+{
+	Relation	rel;
+	bool		aborted = false;
+
+	StartTransactionCommand();
+
+	rel = try_relation_open(relationId, AccessShareLock);
+	if (rel == NULL)
+	{
+		/*
+		 * Relation no longer exists. We don't consider this an error since
+		 * there are no pages in it that need data checksums, and thus return
+		 * true. The worker operates off a list of relations generated at the
+		 * start of processing, so relations being dropped in the meantime is
+		 * to be expected.
+		 */
+		CommitTransactionCommand();
+		pgstat_report_activity(STATE_IDLE, NULL);
+		return true;
+	}
+	RelationGetSmgr(rel);
+
+	for (ForkNumber fnum = 0; fnum <= MAX_FORKNUM; fnum++)
+	{
+		if (smgrexists(rel->rd_smgr, fnum))
+		{
+			if (!ProcessSingleRelationFork(rel, fnum, strategy))
+			{
+				aborted = true;
+				break;
+			}
+		}
+	}
+	relation_close(rel, AccessShareLock);
+	elog(DEBUG2,
+		 "data checksum processing done for relation with OID %u: %s",
+		 relationId, (aborted ? "aborted" : "finished"));
+
+	CommitTransactionCommand();
+
+	pgstat_report_activity(STATE_IDLE, NULL);
+
+	return !aborted;
+}
+
+/*
+ * ProcessDatabase
+ *		Enable data checksums in a single database.
+ *
+ * We do this by launching a dynamic background worker into this database, and
+ * waiting for it to finish.  We have to do this in a separate worker, since
+ * each process can only be connected to one database during its lifetime.
+ */
+static DataChecksumsWorkerResult
+ProcessDatabase(DataChecksumsWorkerDatabase *db)
+{
+	BackgroundWorker bgw;
+	BackgroundWorkerHandle *bgw_handle;
+	BgwHandleStatus status;
+	pid_t		pid;
+	char		activity[NAMEDATALEN + 64];
+
+	DataChecksumsWorkerShmem->success = DATACHECKSUMSWORKER_FAILED;
+
+	memset(&bgw, 0, sizeof(bgw));
+	bgw.bgw_flags = BGWORKER_SHMEM_ACCESS | BGWORKER_BACKEND_DATABASE_CONNECTION;
+	bgw.bgw_start_time = BgWorkerStart_RecoveryFinished;
+	snprintf(bgw.bgw_library_name, BGW_MAXLEN, "postgres");
+	snprintf(bgw.bgw_function_name, BGW_MAXLEN, "%s", "DataChecksumsWorkerMain");
+	snprintf(bgw.bgw_name, BGW_MAXLEN, "datachecksum worker");
+	snprintf(bgw.bgw_type, BGW_MAXLEN, "datachecksum worker");
+	bgw.bgw_restart_time = BGW_NEVER_RESTART;
+	bgw.bgw_notify_pid = MyProcPid;
+	bgw.bgw_main_arg = ObjectIdGetDatum(db->dboid);
+
+	/*
+	 * If there are no worker slots available, make sure we retry processing
+	 * this database. This will make the datachecksumsworker move on to the
+	 * next database and quite likely fail with the same problem. TODO: Maybe
+	 * we need a backoff to avoid running through all the databases here in
+	 * short order.
+	 */
+	if (!RegisterDynamicBackgroundWorker(&bgw, &bgw_handle))
+	{
+		ereport(WARNING,
+				errmsg("failed to start worker for enabling data checksums in database \"%s\", retrying",
+					   db->dbname),
+				errhint("The max_worker_processes setting might be too low."));
+		return DATACHECKSUMSWORKER_RETRYDB;
+	}
+
+	status = WaitForBackgroundWorkerStartup(bgw_handle, &pid);
+	if (status == BGWH_STOPPED)
+	{
+		ereport(WARNING,
+				errmsg("could not start background worker for enabling data checksums in database \"%s\"",
+					   db->dbname),
+				errhint("More details on the error might be found in the server log."));
+		return DATACHECKSUMSWORKER_FAILED;
+	}
+
+	/*
+	 * 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()."));
+
+	Assert(status == BGWH_STARTED);
+	ereport(DEBUG1,
+			errmsg("initiating data checksum processing in database \"%s\"",
+				   db->dbname));
+
+	DataChecksumsWorkerShmem->worker_running = true;
+
+	snprintf(activity, sizeof(activity) - 1,
+			 "Waiting for worker in database %s (pid %ld)", db->dbname, (long) pid);
+	pgstat_report_activity(STATE_RUNNING, activity);
+
+	status = WaitForBackgroundWorkerShutdown(bgw_handle);
+	if (status == BGWH_POSTMASTER_DIED)
+		ereport(FATAL,
+				errcode(ERRCODE_ADMIN_SHUTDOWN),
+				errmsg("postmaster exited during data checksum processing in \"%s\"",
+					   db->dbname),
+				errhint("Restart the database and restart data checksum processing by calling pg_enable_data_checksums()."));
+
+	if (DataChecksumsWorkerShmem->success == DATACHECKSUMSWORKER_ABORTED)
+		ereport(LOG,
+				errmsg("data checksums processing was aborted in database \"%s\"",
+					   db->dbname));
+
+	pgstat_report_activity(STATE_IDLE, NULL);
+	DataChecksumsWorkerShmem->worker_running = false;
+
+	return DataChecksumsWorkerShmem->success;
+}
+
+/*
+ * 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);
+	}
+}
+
+/*
+ * launcher_cancel_handler
+ *
+ * Internal routine for reacting to SIGINT and flagging the worker to abort.
+ * The worker won't be interrupted immediately but will check for abort flag
+ * between each block in a relation.
+ */
+static void
+launcher_cancel_handler(SIGNAL_ARGS)
+{
+	int			save_errno = errno;
+
+	abort_requested = true;
+
+	/*
+	 * There is no sleeping in the main loop, the flag will be checked
+	 * periodically in ProcessSingleRelationFork. The worker does however
+	 * sleep when waiting for concurrent transactions to end so we still need
+	 * to set the latch.
+	 */
+	SetLatch(MyLatch);
+
+	errno = save_errno;
+}
+
+/*
+ * WaitForAllTransactionsToFinish
+ *		Blocks awaiting all current transactions to finish
+ *
+ * Returns when all transactions which are active at the call of the function
+ * have ended, or if the postmaster dies while waiting. If the postmaster dies
+ * the abort flag will be set to indicate that the caller of this shouldn't
+ * proceed.
+ *
+ * NB: this will return early, if aborted by SIGINT or if the target state
+ * is changed while we're running.
+ */
+static void
+WaitForAllTransactionsToFinish(void)
+{
+	TransactionId waitforxid;
+
+	LWLockAcquire(XidGenLock, LW_SHARED);
+	waitforxid = XidFromFullTransactionId(TransamVariables->nextXid);
+	LWLockRelease(XidGenLock);
+
+	while (TransactionIdPrecedes(GetOldestActiveTransactionId(false, true), waitforxid))
+	{
+		char		activity[64];
+		int			rc;
+
+		/* Oldest running xid is older than us, so wait */
+		snprintf(activity,
+				 sizeof(activity),
+				 "Waiting for current transactions to finish (waiting for %u)",
+				 waitforxid);
+		pgstat_report_activity(STATE_RUNNING, activity);
+
+		/* Retry every 3 seconds */
+		ResetLatch(MyLatch);
+		rc = WaitLatch(MyLatch,
+					   WL_LATCH_SET | WL_TIMEOUT | WL_POSTMASTER_DEATH,
+					   3000,
+					   WAIT_EVENT_CHECKSUM_ENABLE_STARTCONDITION);
+
+		/*
+		 * If the postmaster died we won't be able to enable checksums
+		 * cluster-wide so abort and hope to continue when restarted.
+		 */
+		if (rc & WL_POSTMASTER_DEATH)
+			ereport(FATAL,
+					errcode(ERRCODE_ADMIN_SHUTDOWN),
+					errmsg("postmaster exited during data checksums processing"),
+					errhint("Data checksums processing must be restarted manually after cluster restart."));
+
+		LWLockAcquire(DataChecksumsWorkerLock, LW_SHARED);
+		if (DataChecksumsWorkerShmem->launch_operation != operation)
+			abort_requested = true;
+		LWLockRelease(DataChecksumsWorkerLock);
+		if (abort_requested)
+			break;
+	}
+
+	pgstat_report_activity(STATE_IDLE, NULL);
+	return;
+}
+
+/*
+ * DataChecksumsWorkerLauncherMain
+ *
+ * Main function for launching dynamic background workers for processing data
+ * checksums in databases. This function has the bgworker management, with
+ * ProcessAllDatabases being responsible for looping over the databases and
+ * initiating processing.
+ */
+void
+DataChecksumsWorkerLauncherMain(Datum arg)
+{
+	on_shmem_exit(launcher_exit, 0);
+
+	ereport(DEBUG1,
+			errmsg("background worker \"datachecksums launcher\" started"));
+
+	pqsignal(SIGTERM, die);
+	pqsignal(SIGINT, launcher_cancel_handler);
+
+	BackgroundWorkerUnblockSignals();
+
+	MyBackendType = B_DATACHECKSUMSWORKER_LAUNCHER;
+	init_ps_display(NULL);
+
+	INJECTION_POINT("datachecksumsworker-launcher-delay", NULL);
+
+	LWLockAcquire(DataChecksumsWorkerLock, LW_EXCLUSIVE);
+
+	if (DataChecksumsWorkerShmem->launcher_running)
+	{
+		ereport(LOG,
+				errmsg("background worker \"datachecksums launcher\" already running, exiting"));
+		/* Launcher was already running, let it finish */
+		LWLockRelease(DataChecksumsWorkerLock);
+		return;
+	}
+
+	launcher_running = true;
+
+	/*
+	 * 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;
+	LWLockRelease(DataChecksumsWorkerLock);
+
+	/*
+	 * The target state can change while we are busy enabling/disabling
+	 * checksums, if the user calls pg_disable/enable_data_checksums() before
+	 * we are finished with the previous request. In that case, we will loop
+	 * back here, to process the new request.
+	 */
+again:
+
+	pgstat_progress_start_command(PROGRESS_COMMAND_DATACHECKSUMS,
+								  InvalidOid);
+
+	if (operation == ENABLE_DATACHECKSUMS)
+	{
+		/*
+		 * If we are asked to enable checksums in a cluster which already has
+		 * checksums enabled, exit immediately as there is nothing more to do.
+		 * Hold interrupts to make sure state doesn't change during checking.
+		 */
+		if (DataChecksumsNeedVerify())
+			goto done;
+
+		/*
+		 * Set the state to inprogress-on and wait on the procsignal barrier.
+		 */
+		pgstat_progress_update_param(PROGRESS_DATACHECKSUMS_PHASE,
+									 PROGRESS_DATACHECKSUMS_PHASE_ENABLING);
+		SetDataChecksumsOnInProgress();
+
+		/*
+		 * All backends are now in inprogress-on state and are writing data
+		 * checksums.  Start processing all data at rest.
+		 */
+		if (!ProcessAllDatabases())
+		{
+			/*
+			 * If the target state changed during processing then it's not a
+			 * failure, so restart processing instead.
+			 */
+			LWLockAcquire(DataChecksumsWorkerLock, LW_EXCLUSIVE);
+			if (DataChecksumsWorkerShmem->launch_operation != operation)
+			{
+				LWLockRelease(DataChecksumsWorkerLock);
+				goto done;
+			}
+			LWLockRelease(DataChecksumsWorkerLock);
+			ereport(ERROR,
+					errcode(ERRCODE_INSUFFICIENT_RESOURCES),
+					errmsg("unable to enable data checksums in cluster"));
+		}
+
+		/*
+		 * Data checksums have been set on all pages, set the state to on in
+		 * order to instruct backends to validate checksums on reading.
+		 */
+		SetDataChecksumsOn();
+	}
+	else if (operation == DISABLE_DATACHECKSUMS)
+	{
+		pgstat_progress_update_param(PROGRESS_DATACHECKSUMS_PHASE,
+									 PROGRESS_DATACHECKSUMS_PHASE_DISABLING);
+		SetDataChecksumsOff();
+	}
+	else
+	{
+		Assert(false);
+	}
+
+done:
+
+	/*
+	 * This state will only be displayed for a fleeting moment, but for the
+	 * sake of correctness it is still added before ending the command.
+	 */
+	pgstat_progress_update_param(PROGRESS_DATACHECKSUMS_PHASE,
+								 PROGRESS_DATACHECKSUMS_PHASE_DONE);
+
+	/*
+	 * All done. But before we exit, check if the target state was changed
+	 * while we were running. In that case we will have to start all over
+	 * again.
+	 */
+	LWLockAcquire(DataChecksumsWorkerLock, LW_EXCLUSIVE);
+	if (DataChecksumsWorkerShmem->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;
+		LWLockRelease(DataChecksumsWorkerLock);
+		goto again;
+	}
+
+	/* Shut down progress reporting as we are done */
+	pgstat_progress_end_command();
+
+	launcher_running = false;
+	DataChecksumsWorkerShmem->launcher_running = false;
+	LWLockRelease(DataChecksumsWorkerLock);
+}
+
+/*
+ * ProcessAllDatabases
+ *		Compute the list of all databases and process checksums in each
+ *
+ * This will repeatedly generate a list of databases to process for enabling
+ * checksums. Until no new databases are found, this will loop around computing
+ * a new list and comparing it to the already seen ones.
+ *
+ */
+static bool
+ProcessAllDatabases(void)
+{
+	List	   *DatabaseList;
+	HTAB	   *ProcessedDatabases = NULL;
+	HASHCTL		hash_ctl;
+	bool		found_failed = false;
+
+	/* Initialize a hash tracking all processed databases */
+	memset(&hash_ctl, 0, sizeof(hash_ctl));
+	hash_ctl.keysize = sizeof(Oid);
+	hash_ctl.entrysize = sizeof(DataChecksumsWorkerResultEntry);
+	ProcessedDatabases = hash_create("Processed databases",
+									 64,
+									 &hash_ctl,
+									 HASH_ELEM | HASH_BLOBS);
+
+	/*
+	 * Set up so first run processes shared catalogs, but not once in every
+	 * db.
+	 */
+	DataChecksumsWorkerShmem->process_shared_catalogs = true;
+
+	/*
+	 * 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();
+
+	/* Allow a test case to modify the initial list of databases */
+	INJECTION_POINT("datachecksumsworker-initial-dblist", DatabaseList);
+
+	/*
+	 * Update progress reporting with the total number of databases we need to
+	 * process.  This number should not be changed during processing, the
+	 * columns for processed databases is instead increased such that it can
+	 * be compared against the total.
+	 */
+	{
+		const int	index[] = {
+			PROGRESS_DATACHECKSUMS_DBS_TOTAL,
+			PROGRESS_DATACHECKSUMS_DBS_DONE,
+			PROGRESS_DATACHECKSUMS_RELS_TOTAL,
+			PROGRESS_DATACHECKSUMS_RELS_DONE,
+			PROGRESS_DATACHECKSUMS_BLOCKS_TOTAL,
+			PROGRESS_DATACHECKSUMS_BLOCKS_DONE,
+		};
+
+		int64		vals[6];
+
+		vals[0] = list_length(DatabaseList);
+		vals[1] = 0;
+
+		/* translated to NULL */
+		vals[2] = -1;
+		vals[3] = -1;
+		vals[4] = -1;
+		vals[5] = -1;
+
+		pgstat_progress_update_multi_param(6, index, vals);
+	}
+
+	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);
+
+			if (entry)
+			{
+				if (entry->result == DATACHECKSUMSWORKER_RETRYDB)
+				{
+					/*
+					 * Limit the number of retries to avoid infinite looping
+					 * in case there simply won't be enough workers in the
+					 * cluster to finish this operation.
+					 */
+					if (entry->retries > DATACHECKSUMSWORKER_MAX_DB_RETRIES)
+						entry->result = DATACHECKSUMSWORKER_FAILED;
+				}
+
+				/* Skip if this database has been processed already */
+				if (entry->result != DATACHECKSUMSWORKER_RETRYDB)
+					continue;
+			}
+
+			result = ProcessDatabase(db);
+			processed_databases++;
+
+			/*
+			 * Update the number of processed databases in the progress
+			 * report.
+			 */
+			pgstat_progress_update_param(PROGRESS_DATACHECKSUMS_DBS_DONE,
+										 processed_databases);
+
+			/* Allow a test process to alter the result of the operation */
+			INJECTION_POINT("datachecksumsworker-fail-db", &result);
+
+			if (result == DATACHECKSUMSWORKER_SUCCESSFUL)
+			{
+				/*
+				 * If one database has completed shared catalogs, we don't
+				 * have to process them again.
+				 */
+				if (DataChecksumsWorkerShmem->process_shared_catalogs)
+					DataChecksumsWorkerShmem->process_shared_catalogs = false;
+			}
+			else if (result == DATACHECKSUMSWORKER_ABORTED)
+			{
+				/* Abort flag set, so exit the whole process */
+				return false;
+			}
+
+			entry = hash_search(ProcessedDatabases, &db->dboid, HASH_ENTER, &found);
+			entry->dboid = db->dboid;
+			entry->result = result;
+			if (!found)
+				entry->retries = 0;
+			else
+				entry->retries++;
+		}
+
+		elog(DEBUG1,
+			 "%i databases processed for data checksum enabling, %s",
+			 processed_databases,
+			 (processed_databases ? "process with restart" : "process completed"));
+
+		FreeDatabaseList(DatabaseList);
+
+		/*
+		 * If no databases were processed in this run of the loop, we have now
+		 * finished all databases and no concurrently created ones can exist.
+		 */
+		if (processed_databases == 0)
+			break;
+
+		/*
+		 * Re-generate the list of databases for another pass in case any new
+		 * databases were created while we were running.  Since the initial
+		 * list was generated after waiting for all transaction to finish we
+		 * know that all new databases found here must have been created while
+		 * seeing the new checksum state.  By waiting for all transactions
+		 * here as well we know that any database created using an existing db
+		 * as a template (which may have been used before it had checksums
+		 * enabled) will be committed.
+		 */
+		WaitForAllTransactionsToFinish();
+		DatabaseList = BuildDatabaseList();
+	}
+
+	/*
+	 * ProcessedDatabases now has all databases and the results of their
+	 * processing. Failure to enable checksums for a database can be because
+	 * they actually failed for some reason, or because the database was
+	 * dropped between us getting the database list and trying to process it.
+	 * Get a fresh list of databases to detect the second case where the
+	 * database was dropped before we had started processing it. If a database
+	 * still exists, but enabling checksums failed then we fail the entire
+	 * checksumming process and exit with an error.
+	 */
+	DatabaseList = BuildDatabaseList();
+
+	foreach_ptr(DataChecksumsWorkerDatabase, db, DatabaseList)
+	{
+		DataChecksumsWorkerResultEntry *entry;
+		bool		found;
+
+		entry = hash_search(ProcessedDatabases, (void *) &db->dboid,
+							HASH_FIND, &found);
+
+		/*
+		 * We are only interested in the processed databases which failed, and
+		 * where the failed database still exists.  This indicates that
+		 * enabling checksums actually failed, and not that the failure was
+		 * due to the db being concurrently dropped.
+		 */
+		if (found && entry->result == DATACHECKSUMSWORKER_FAILED)
+		{
+			ereport(WARNING,
+					errmsg("failed to enable data checksums in \"%s\"", db->dbname));
+			found_failed = found;
+			continue;
+		}
+	}
+
+	FreeDatabaseList(DatabaseList);
+
+	if (found_failed)
+	{
+		/* Disable checksums on cluster, because we failed */
+		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."));
+	}
+
+	pgstat_progress_update_param(PROGRESS_DATACHECKSUMS_PHASE,
+								 PROGRESS_DATACHECKSUMS_PHASE_WAITING_BARRIER);
+	return true;
+}
+
+/*
+ * DataChecksumsWorkerShmemSize
+ *		Compute required space for datachecksumsworker-related shared memory
+ */
+Size
+DataChecksumsWorkerShmemSize(void)
+{
+	Size		size;
+
+	size = sizeof(DataChecksumsWorkerShmemStruct);
+	size = MAXALIGN(size);
+
+	return size;
+}
+
+/*
+ * DataChecksumsWorkerShmemInit
+ *		Allocate and initialize datachecksumsworker-related shared memory
+ */
+void
+DataChecksumsWorkerShmemInit(void)
+{
+	bool		found;
+
+	DataChecksumsWorkerShmem = (DataChecksumsWorkerShmemStruct *)
+		ShmemInitStruct("DataChecksumsWorker Data",
+						DataChecksumsWorkerShmemSize(),
+						&found);
+
+	if (!found)
+		MemSet(DataChecksumsWorkerShmem, 0, DataChecksumsWorkerShmemSize());
+}
+
+/*
+ * BuildDatabaseList
+ *		Compile a list of all currently available databases in the cluster
+ *
+ * This creates the list of databases for the datachecksumsworker workers to
+ * add checksums to. If the caller wants to ensure that no concurrently
+ * running CREATE DATABASE calls exist, this needs to be preceded by a call
+ * to WaitForAllTransactionsToFinish().
+ */
+static List *
+BuildDatabaseList(void)
+{
+	List	   *DatabaseList = NIL;
+	Relation	rel;
+	TableScanDesc scan;
+	HeapTuple	tup;
+	MemoryContext ctx = CurrentMemoryContext;
+	MemoryContext oldctx;
+
+	StartTransactionCommand();
+
+	rel = table_open(DatabaseRelationId, AccessShareLock);
+	scan = table_beginscan_catalog(rel, 0, NULL);
+
+	while (HeapTupleIsValid(tup = heap_getnext(scan, ForwardScanDirection)))
+	{
+		Form_pg_database pgdb = (Form_pg_database) GETSTRUCT(tup);
+		DataChecksumsWorkerDatabase *db;
+
+		oldctx = MemoryContextSwitchTo(ctx);
+
+		db = (DataChecksumsWorkerDatabase *) palloc0(sizeof(DataChecksumsWorkerDatabase));
+
+		db->dboid = pgdb->oid;
+		db->dbname = pstrdup(NameStr(pgdb->datname));
+
+		DatabaseList = lappend(DatabaseList, db);
+
+		MemoryContextSwitchTo(oldctx);
+	}
+
+	table_endscan(scan);
+	table_close(rel, AccessShareLock);
+
+	CommitTransactionCommand();
+
+	return DatabaseList;
+}
+
+static void
+FreeDatabaseList(List *dblist)
+{
+	if (!dblist)
+		return;
+
+	foreach_ptr(DataChecksumsWorkerDatabase, db, dblist)
+	{
+		if (db->dbname != NULL)
+			pfree(db->dbname);
+	}
+
+	list_free_deep(dblist);
+}
+
+/*
+ * BuildRelationList
+ *		Compile a list of relations in the database
+ *
+ * Returns a list of OIDs for the request 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
+ * non-temporary list. include_shared has no relevance when building a list of
+ * temporary relations.
+ */
+static List *
+BuildRelationList(bool temp_relations, bool include_shared)
+{
+	List	   *RelationList = NIL;
+	Relation	rel;
+	TableScanDesc scan;
+	HeapTuple	tup;
+	MemoryContext ctx = CurrentMemoryContext;
+	MemoryContext oldctx;
+
+	StartTransactionCommand();
+
+	rel = table_open(RelationRelationId, AccessShareLock);
+	scan = table_beginscan_catalog(rel, 0, NULL);
+
+	while (HeapTupleIsValid(tup = heap_getnext(scan, ForwardScanDirection)))
+	{
+		Form_pg_class pgc = (Form_pg_class) GETSTRUCT(tup);
+
+		/*
+		 * Only include temporary relations when asked for a temp relation
+		 * list.
+		 */
+		if (pgc->relpersistence == RELPERSISTENCE_TEMP)
+		{
+			if (!temp_relations)
+				continue;
+		}
+		else
+		{
+			/*
+			 * If we are only interested in temp relations then continue
+			 * immediately as the current relation isn't a temp relation.
+			 */
+			if (temp_relations)
+				continue;
+
+			if (!RELKIND_HAS_STORAGE(pgc->relkind))
+				continue;
+
+			if (pgc->relisshared && !include_shared)
+				continue;
+		}
+
+		oldctx = MemoryContextSwitchTo(ctx);
+		RelationList = lappend_oid(RelationList, pgc->oid);
+		MemoryContextSwitchTo(oldctx);
+	}
+
+	table_endscan(scan);
+	table_close(rel, AccessShareLock);
+
+	CommitTransactionCommand();
+
+	return RelationList;
+}
+
+/*
+ * DataChecksumsWorkerMain
+ *
+ * Main function for enabling checksums in a single database, This is the
+ * function set as the bgw_function_name in the dynamic background worker
+ * process initiated for each database by the worker launcher. After enabling
+ * data checksums in each applicable relation in the database, it will wait for
+ * all temporary relations that were present when the function started to
+ * disappear before returning. This is required since we cannot rewrite
+ * existing temporary relations with data checksums.
+ */
+void
+DataChecksumsWorkerMain(Datum arg)
+{
+	Oid			dboid = DatumGetObjectId(arg);
+	List	   *RelationList = NIL;
+	List	   *InitialTempTableList = NIL;
+	BufferAccessStrategy strategy;
+	bool		aborted = false;
+	int64		rels_done;
+
+	operation = ENABLE_DATACHECKSUMS;
+
+	pqsignal(SIGTERM, die);
+
+	BackgroundWorkerUnblockSignals();
+
+	MyBackendType = B_DATACHECKSUMSWORKER_WORKER;
+	init_ps_display(NULL);
+
+	BackgroundWorkerInitializeConnectionByOid(dboid, InvalidOid,
+											  BGWORKER_BYPASS_ALLOWCONN);
+
+	/* worker will have a separate entry in pg_stat_progress_data_checksums */
+	pgstat_progress_start_command(PROGRESS_COMMAND_DATACHECKSUMS,
+								  InvalidOid);
+
+	/*
+	 * Get a list of all temp tables present as we start in this database. We
+	 * need to wait until they are all gone until we are done, since we cannot
+	 * access these relations and modify them.
+	 */
+	InitialTempTableList = BuildRelationList(true, false);
+
+	/*
+	 * Enable vacuum cost delay, if any.  While this process isn't doing any
+	 * vacuuming, we are re-using the infrastructure that vacuum cost delay
+	 * provides rather than inventing something bespoke. This is an internal
+	 * 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;
+	VacuumCostActive = (VacuumCostDelay > 0);
+	VacuumCostBalance = 0;
+	VacuumCostPageHit = 0;
+	VacuumCostPageMiss = 0;
+	VacuumCostPageDirty = 0;
+
+	/*
+	 * Create and set the vacuum strategy as our buffer strategy.
+	 */
+	strategy = GetAccessStrategy(BAS_VACUUM);
+
+	RelationList = BuildRelationList(false,
+									 DataChecksumsWorkerShmem->process_shared_catalogs);
+
+	/* Update the total number of relations to be processed in this DB. */
+	{
+		const int	index[] = {
+			PROGRESS_DATACHECKSUMS_RELS_TOTAL,
+			PROGRESS_DATACHECKSUMS_RELS_DONE
+		};
+
+		int64		vals[2];
+
+		vals[0] = list_length(RelationList);
+		vals[1] = 0;
+
+		pgstat_progress_update_multi_param(2, index, vals);
+	}
+
+	/* Process the relations */
+	rels_done = 0;
+	foreach_oid(reloid, RelationList)
+	{
+		if (!ProcessSingleRelationByOid(reloid, strategy))
+		{
+			aborted = true;
+			break;
+		}
+
+		pgstat_progress_update_param(PROGRESS_DATACHECKSUMS_RELS_DONE,
+									 ++rels_done);
+	}
+	list_free(RelationList);
+
+	if (aborted)
+	{
+		DataChecksumsWorkerShmem->success = DATACHECKSUMSWORKER_ABORTED;
+		ereport(DEBUG1,
+				errmsg("data checksum processing aborted in database OID %u",
+					   dboid));
+		return;
+	}
+
+	/* The worker is about to wait for temporary tables to go away. */
+	pgstat_progress_update_param(PROGRESS_DATACHECKSUMS_PHASE,
+								 PROGRESS_DATACHECKSUMS_PHASE_WAITING_TEMPREL);
+
+	/*
+	 * Wait for all temp tables that existed when we started to go away. This
+	 * is necessary since we cannot "reach" them to enable checksums. Any temp
+	 * tables created after we started will already have checksums in them
+	 * (due to the "inprogress-on" state), so no need to wait for those.
+	 */
+	for (;;)
+	{
+		List	   *CurrentTempTables;
+		int			numleft;
+		char		activity[64];
+
+		CurrentTempTables = BuildRelationList(true, false);
+		numleft = 0;
+		foreach_oid(tmptbloid, InitialTempTableList)
+		{
+			if (list_member_oid(CurrentTempTables, tmptbloid))
+				numleft++;
+		}
+		list_free(CurrentTempTables);
+
+		INJECTION_POINT("datachecksumsworker-fake-temptable-wait", &numleft);
+
+		if (numleft == 0)
+			break;
+
+		/*
+		 * At least one temp table is left to wait for, indicate in pgstat
+		 * activity and progress reporting.
+		 */
+		snprintf(activity,
+				 sizeof(activity),
+				 "Waiting for %d temp tables to be removed", numleft);
+		pgstat_report_activity(STATE_RUNNING, activity);
+
+		/* Retry every 3 seconds */
+		ResetLatch(MyLatch);
+		(void) WaitLatch(MyLatch,
+						 WL_LATCH_SET | WL_TIMEOUT | WL_EXIT_ON_PM_DEATH,
+						 3000,
+						 WAIT_EVENT_CHECKSUM_ENABLE_TEMPTABLE_WAIT);
+
+		LWLockAcquire(DataChecksumsWorkerLock, LW_EXCLUSIVE);
+		aborted = DataChecksumsWorkerShmem->launch_operation != operation;
+		LWLockRelease(DataChecksumsWorkerLock);
+
+		if (aborted || abort_requested)
+		{
+			DataChecksumsWorkerShmem->success = DATACHECKSUMSWORKER_ABORTED;
+			ereport(DEBUG1,
+					errmsg("data checksum processing aborted in database OID %u",
+						   dboid));
+			return;
+		}
+	}
+
+	list_free(InitialTempTableList);
+
+	/* worker done */
+	pgstat_progress_end_command();
+
+	DataChecksumsWorkerShmem->success = DATACHECKSUMSWORKER_SUCCESSFUL;
+}
diff --git a/src/backend/postmaster/meson.build b/src/backend/postmaster/meson.build
index e1f70726604..78ae5cc6611 100644
--- a/src/backend/postmaster/meson.build
+++ b/src/backend/postmaster/meson.build
@@ -6,6 +6,7 @@ backend_sources += files(
   'bgworker.c',
   'bgwriter.c',
   'checkpointer.c',
+  'datachecksumsworker.c',
   'fork_process.c',
   'interrupt.c',
   'launch_backend.c',
diff --git a/src/backend/postmaster/postmaster.c b/src/backend/postmaster/postmaster.c
index d6133bfebc6..c6fa6460b41 100644
--- a/src/backend/postmaster/postmaster.c
+++ b/src/backend/postmaster/postmaster.c
@@ -2999,6 +2999,11 @@ PostmasterStateMachine(void)
 									B_INVALID,
 									B_STANDALONE_BACKEND);
 
+			/* also add checksumming processes */
+			remainMask = btmask_add(remainMask,
+									B_DATACHECKSUMSWORKER_LAUNCHER,
+									B_DATACHECKSUMSWORKER_WORKER);
+
 			/* All types should be included in targetMask or remainMask */
 			Assert((remainMask.mask | targetMask.mask) == BTYPE_MASK_ALL.mask);
 		}
diff --git a/src/backend/postmaster/walsummarizer.c b/src/backend/postmaster/walsummarizer.c
index 2d8f57099fd..02c1eb8a2c8 100644
--- a/src/backend/postmaster/walsummarizer.c
+++ b/src/backend/postmaster/walsummarizer.c
@@ -1429,8 +1429,11 @@ SummarizeXlogRecord(XLogReaderState *xlogreader, bool *new_fast_forward)
 
 	if (info == XLOG_CHECKPOINT_REDO)
 	{
+		xl_checkpoint_redo xlrec;
+
 		/* Payload is wal_level at the time record was written. */
-		memcpy(&record_wal_level, XLogRecGetData(xlogreader), sizeof(int));
+		memcpy(&xlrec, XLogRecGetData(xlogreader), sizeof(xl_checkpoint_redo));
+		record_wal_level = xlrec.wal_level;
 	}
 	else if (info == XLOG_CHECKPOINT_SHUTDOWN)
 	{
diff --git a/src/backend/replication/logical/decode.c b/src/backend/replication/logical/decode.c
index 32af1249610..7e606176d51 100644
--- a/src/backend/replication/logical/decode.c
+++ b/src/backend/replication/logical/decode.c
@@ -181,6 +181,7 @@ xlog_decode(LogicalDecodingContext *ctx, XLogRecordBuffer *buf)
 		case XLOG_FPW_CHANGE:
 		case XLOG_FPI_FOR_HINT:
 		case XLOG_FPI:
+		case XLOG_CHECKSUMS:
 		case XLOG_OVERWRITE_CONTRECORD:
 		case XLOG_CHECKPOINT_REDO:
 			break;
diff --git a/src/backend/storage/ipc/ipci.c b/src/backend/storage/ipc/ipci.c
index 1f7e933d500..359dc54d27e 100644
--- a/src/backend/storage/ipc/ipci.c
+++ b/src/backend/storage/ipc/ipci.c
@@ -31,6 +31,7 @@
 #include "postmaster/autovacuum.h"
 #include "postmaster/bgworker_internals.h"
 #include "postmaster/bgwriter.h"
+#include "postmaster/datachecksumsworker.h"
 #include "postmaster/walsummarizer.h"
 #include "replication/logicallauncher.h"
 #include "replication/origin.h"
@@ -141,6 +142,7 @@ CalculateShmemSize(void)
 	size = add_size(size, AioShmemSize());
 	size = add_size(size, WaitLSNShmemSize());
 	size = add_size(size, LogicalDecodingCtlShmemSize());
+	size = add_size(size, DataChecksumsWorkerShmemSize());
 
 	/* include additional requested shmem from preload libraries */
 	size = add_size(size, total_addin_request);
@@ -315,6 +317,7 @@ CreateOrAttachShmemStructs(void)
 	PgArchShmemInit();
 	ApplyLauncherShmemInit();
 	SlotSyncShmemInit();
+	DataChecksumsWorkerShmemInit();
 
 	/*
 	 * Set up other modules that need some shared memory space
diff --git a/src/backend/storage/ipc/procsignal.c b/src/backend/storage/ipc/procsignal.c
index 8e56922dcea..9be31ad7634 100644
--- a/src/backend/storage/ipc/procsignal.c
+++ b/src/backend/storage/ipc/procsignal.c
@@ -18,12 +18,14 @@
 #include <unistd.h>
 
 #include "access/parallel.h"
+#include "access/xlog.h"
 #include "commands/async.h"
 #include "miscadmin.h"
 #include "pgstat.h"
 #include "port/pg_bitutils.h"
 #include "replication/logicalworker.h"
 #include "replication/walsender.h"
+#include "storage/checksum.h"
 #include "storage/condition_variable.h"
 #include "storage/ipc.h"
 #include "storage/latch.h"
@@ -579,6 +581,18 @@ ProcessProcSignalBarrier(void)
 					case PROCSIGNAL_BARRIER_UPDATE_XLOG_LOGICAL_INFO:
 						processed = ProcessBarrierUpdateXLogLogicalInfo();
 						break;
+					case PROCSIGNAL_BARRIER_CHECKSUM_INPROGRESS_ON:
+						processed = AbsorbDataChecksumsBarrier(PG_DATA_CHECKSUM_INPROGRESS_ON_VERSION);
+						break;
+					case PROCSIGNAL_BARRIER_CHECKSUM_ON:
+						processed = AbsorbDataChecksumsBarrier(PG_DATA_CHECKSUM_VERSION);
+						break;
+					case PROCSIGNAL_BARRIER_CHECKSUM_INPROGRESS_OFF:
+						processed = AbsorbDataChecksumsBarrier(PG_DATA_CHECKSUM_INPROGRESS_OFF_VERSION);
+						break;
+					case PROCSIGNAL_BARRIER_CHECKSUM_OFF:
+						processed = AbsorbDataChecksumsBarrier(PG_DATA_CHECKSUM_OFF);
+						break;
 				}
 
 				/*
diff --git a/src/backend/storage/page/README b/src/backend/storage/page/README
index e30d7ac59ad..73c36a63908 100644
--- a/src/backend/storage/page/README
+++ b/src/backend/storage/page/README
@@ -10,7 +10,9 @@ http://www.cs.toronto.edu/~bianca/papers/sigmetrics09.pdf, discussed
 2010/12/22 on -hackers list.
 
 Current implementation requires this be enabled system-wide at initdb time, or
-by using the pg_checksums tool on an offline cluster.
+by using the pg_checksums tool on an offline cluster.  Checksums can also be
+enabled at runtime using pg_enable_data_checksums(), and disabled by using
+pg_disable_data_checksums().
 
 The checksum is not valid at all times on a data page!!
 The checksum is valid when the page leaves the shared pool and is checked
diff --git a/src/backend/storage/page/bufpage.c b/src/backend/storage/page/bufpage.c
index de85911e3ac..72e1c042cf1 100644
--- a/src/backend/storage/page/bufpage.c
+++ b/src/backend/storage/page/bufpage.c
@@ -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
@@ -151,8 +153,8 @@ PageIsVerified(PageData *page, BlockNumber blkno, int flags, bool *checksum_fail
 		if ((flags & (PIV_LOG_WARNING | PIV_LOG_LOG)) != 0)
 			ereport(flags & PIV_LOG_WARNING ? WARNING : LOG,
 					(errcode(ERRCODE_DATA_CORRUPTED),
-					 errmsg("page verification failed, calculated checksum %u but expected %u",
-							checksum, p->pd_checksum)));
+					 errmsg("page verification failed, calculated checksum %u but expected %u (page LSN %X/%08X)",
+							checksum, p->pd_checksum, LSN_FORMAT_ARGS(PageXLogRecPtrGet(p->pd_lsn)))));
 
 		if (header_sane && (flags & PIV_IGNORE_CHECKSUM_FAILURE))
 			return true;
@@ -1510,9 +1512,13 @@ PageSetChecksumCopy(Page page, BlockNumber blkno)
 {
 	static char *pageCopy = NULL;
 
+	HOLD_INTERRUPTS();
 	/* If we don't need a checksum, just return the passed-in data */
-	if (PageIsNew(page) || !DataChecksumsEnabled())
+	if (PageIsNew(page) || !DataChecksumsNeedWrite())
+	{
+		RESUME_INTERRUPTS();
 		return page;
+	}
 
 	/*
 	 * We allocate the copy space once and use it over on each subsequent
@@ -1528,6 +1534,7 @@ PageSetChecksumCopy(Page page, BlockNumber blkno)
 
 	memcpy(pageCopy, page, BLCKSZ);
 	((PageHeader) pageCopy)->pd_checksum = pg_checksum_page(pageCopy, blkno);
+	RESUME_INTERRUPTS();
 	return pageCopy;
 }
 
@@ -1540,9 +1547,14 @@ PageSetChecksumCopy(Page page, BlockNumber blkno)
 void
 PageSetChecksumInplace(Page page, BlockNumber blkno)
 {
+	HOLD_INTERRUPTS();
 	/* If we don't need a checksum, just return */
-	if (PageIsNew(page) || !DataChecksumsEnabled())
+	if (PageIsNew(page) || !DataChecksumsNeedWrite())
+	{
+		RESUME_INTERRUPTS();
 		return;
+	}
 
 	((PageHeader) page)->pd_checksum = pg_checksum_page(page, blkno);
+	RESUME_INTERRUPTS();
 }
diff --git a/src/backend/utils/activity/pgstat_backend.c b/src/backend/utils/activity/pgstat_backend.c
index 1350f5f62f1..47d34be1bab 100644
--- a/src/backend/utils/activity/pgstat_backend.c
+++ b/src/backend/utils/activity/pgstat_backend.c
@@ -380,6 +380,8 @@ pgstat_tracks_backend_bktype(BackendType bktype)
 		case B_CHECKPOINTER:
 		case B_IO_WORKER:
 		case B_STARTUP:
+		case B_DATACHECKSUMSWORKER_LAUNCHER:
+		case B_DATACHECKSUMSWORKER_WORKER:
 			return false;
 
 		case B_AUTOVAC_WORKER:
diff --git a/src/backend/utils/activity/pgstat_io.c b/src/backend/utils/activity/pgstat_io.c
index 28de24538dc..2be26e92283 100644
--- a/src/backend/utils/activity/pgstat_io.c
+++ b/src/backend/utils/activity/pgstat_io.c
@@ -362,6 +362,8 @@ pgstat_tracks_io_bktype(BackendType bktype)
 		case B_LOGGER:
 			return false;
 
+		case B_DATACHECKSUMSWORKER_LAUNCHER:
+		case B_DATACHECKSUMSWORKER_WORKER:
 		case B_AUTOVAC_LAUNCHER:
 		case B_AUTOVAC_WORKER:
 		case B_BACKEND:
diff --git a/src/backend/utils/activity/wait_event_names.txt b/src/backend/utils/activity/wait_event_names.txt
index 4aa864fe3c3..084f6a4f6d3 100644
--- a/src/backend/utils/activity/wait_event_names.txt
+++ b/src/backend/utils/activity/wait_event_names.txt
@@ -119,6 +119,9 @@ CHECKPOINT_DELAY_COMPLETE	"Waiting for a backend that blocks a checkpoint from c
 CHECKPOINT_DELAY_START	"Waiting for a backend that blocks a checkpoint from starting."
 CHECKPOINT_DONE	"Waiting for a checkpoint to complete."
 CHECKPOINT_START	"Waiting for a checkpoint to start."
+CHECKSUM_ENABLE_STARTCONDITION	"Waiting for data checksums enabling to start."
+CHECKSUM_ENABLE_FINISHCONDITION	"Waiting for data checksums to be enabled."
+CHECKSUM_ENABLE_TEMPTABLE_WAIT	"Waiting for temporary tables to be dropped for data checksums to be enabled."
 EXECUTE_GATHER	"Waiting for activity from a child process while executing a <literal>Gather</literal> plan node."
 HASH_BATCH_ALLOCATE	"Waiting for an elected Parallel Hash participant to allocate a hash table."
 HASH_BATCH_ELECT	"Waiting to elect a Parallel Hash participant to allocate a hash table."
@@ -366,6 +369,7 @@ SerialControl	"Waiting to read or update shared <filename>pg_serial</filename> s
 AioWorkerSubmissionQueue	"Waiting to access AIO worker submission queue."
 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 checksumsworker."
 
 #
 # END OF PREDEFINED LWLOCKS (DO NOT CHANGE THIS LINE)
diff --git a/src/backend/utils/adt/pgstatfuncs.c b/src/backend/utils/adt/pgstatfuncs.c
index 73ca0bb0b7f..b9a37af242c 100644
--- a/src/backend/utils/adt/pgstatfuncs.c
+++ b/src/backend/utils/adt/pgstatfuncs.c
@@ -295,6 +295,8 @@ pg_stat_get_progress_info(PG_FUNCTION_ARGS)
 		cmdtype = PROGRESS_COMMAND_BASEBACKUP;
 	else if (pg_strcasecmp(cmd, "COPY") == 0)
 		cmdtype = PROGRESS_COMMAND_COPY;
+	else if (pg_strcasecmp(cmd, "DATACHECKSUMS") == 0)
+		cmdtype = PROGRESS_COMMAND_DATACHECKSUMS;
 	else
 		ereport(ERROR,
 				(errcode(ERRCODE_INVALID_PARAMETER_VALUE),
@@ -1167,9 +1169,6 @@ pg_stat_get_db_checksum_failures(PG_FUNCTION_ARGS)
 	int64		result;
 	PgStat_StatDBEntry *dbentry;
 
-	if (!DataChecksumsEnabled())
-		PG_RETURN_NULL();
-
 	if ((dbentry = pgstat_fetch_stat_dbentry(dbid)) == NULL)
 		result = 0;
 	else
@@ -1185,9 +1184,6 @@ pg_stat_get_db_checksum_last_failure(PG_FUNCTION_ARGS)
 	TimestampTz result;
 	PgStat_StatDBEntry *dbentry;
 
-	if (!DataChecksumsEnabled())
-		PG_RETURN_NULL();
-
 	if ((dbentry = pgstat_fetch_stat_dbentry(dbid)) == NULL)
 		result = 0;
 	else
diff --git a/src/backend/utils/init/miscinit.c b/src/backend/utils/init/miscinit.c
index 563f20374ff..4b4804a7ce5 100644
--- a/src/backend/utils/init/miscinit.c
+++ b/src/backend/utils/init/miscinit.c
@@ -844,7 +844,8 @@ InitializeSessionUserIdStandalone(void)
 	 * workers, in slot sync worker and in background workers.
 	 */
 	Assert(!IsUnderPostmaster || AmAutoVacuumWorkerProcess() ||
-		   AmLogicalSlotSyncWorkerProcess() || AmBackgroundWorkerProcess());
+		   AmLogicalSlotSyncWorkerProcess() || AmBackgroundWorkerProcess() ||
+		   AmDataChecksumsWorkerProcess());
 
 	/* call only once */
 	Assert(!OidIsValid(AuthenticatedUserId));
diff --git a/src/backend/utils/init/postinit.c b/src/backend/utils/init/postinit.c
index 3f401faf3de..34113a7e8bb 100644
--- a/src/backend/utils/init/postinit.c
+++ b/src/backend/utils/init/postinit.c
@@ -749,6 +749,24 @@ InitPostgres(const char *in_dbname, Oid dboid,
 
 	ProcSignalInit(MyCancelKey, MyCancelKeyLength);
 
+	/*
+	 * Initialize a local cache of the data_checksum_version, to be updated by
+	 * the procsignal-based barriers.
+	 *
+	 * This intentionally happens after initializing the procsignal, otherwise
+	 * we might miss a state change. This means we can get a barrier for the
+	 * state we've just initialized.
+	 *
+	 * 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
+	 * control file, which may be arbitrarily old).
+	 *
+	 * NB: Even if the postmaster handled barriers, the value might still be
+	 * stale, as it might have changed after this process forked.
+	 */
+	InitLocalDataChecksumVersion();
+
 	/*
 	 * Also set up timeout handlers needed for backend operation.  We need
 	 * these in every case except bootstrap.
@@ -877,7 +895,7 @@ InitPostgres(const char *in_dbname, Oid dboid,
 					 errhint("You should immediately run CREATE USER \"%s\" SUPERUSER;.",
 							 username != NULL ? username : "postgres")));
 	}
-	else if (AmBackgroundWorkerProcess())
+	else if (AmBackgroundWorkerProcess() || AmDataChecksumsWorkerProcess())
 	{
 		if (username == NULL && !OidIsValid(useroid))
 		{
diff --git a/src/backend/utils/misc/guc_parameters.dat b/src/backend/utils/misc/guc_parameters.dat
index f0260e6e412..8fed24f0192 100644
--- a/src/backend/utils/misc/guc_parameters.dat
+++ b/src/backend/utils/misc/guc_parameters.dat
@@ -531,11 +531,12 @@
   max => '1.0',
 },
 
-{ name => 'data_checksums', type => 'bool', context => 'PGC_INTERNAL', group => 'PRESET_OPTIONS',
+{ name => 'data_checksums', type => 'enum', context => 'PGC_INTERNAL', group => 'PRESET_OPTIONS',
   short_desc => 'Shows whether data checksums are turned on for this cluster.',
   flags => 'GUC_NOT_IN_SAMPLE | GUC_DISALLOW_IN_FILE | GUC_RUNTIME_COMPUTED',
   variable => 'data_checksums',
-  boot_val => 'false',
+  boot_val => 'PG_DATA_CHECKSUM_OFF',
+  options => 'data_checksums_options',
 },
 
 # Can't be set by ALTER SYSTEM as it can lead to recursive definition
diff --git a/src/backend/utils/misc/guc_tables.c b/src/backend/utils/misc/guc_tables.c
index 13c569d8790..b657eab1119 100644
--- a/src/backend/utils/misc/guc_tables.c
+++ b/src/backend/utils/misc/guc_tables.c
@@ -491,6 +491,14 @@ static const struct config_enum_entry file_copy_method_options[] = {
 	{NULL, 0, false}
 };
 
+static const struct config_enum_entry data_checksums_options[] = {
+	{"on", PG_DATA_CHECKSUM_VERSION, true},
+	{"off", PG_DATA_CHECKSUM_OFF, true},
+	{"inprogress-on", PG_DATA_CHECKSUM_INPROGRESS_ON_VERSION, true},
+	{"inprogress-off", PG_DATA_CHECKSUM_INPROGRESS_OFF_VERSION, true},
+	{NULL, 0, false}
+};
+
 /*
  * Options for enum values stored in other modules
  */
@@ -618,7 +626,6 @@ static int	shared_memory_size_in_huge_pages;
 static int	wal_block_size;
 static int	num_os_semaphores;
 static int	effective_wal_level = WAL_LEVEL_REPLICA;
-static bool data_checksums;
 static bool integer_datetimes;
 
 #ifdef USE_ASSERT_CHECKING
diff --git a/src/bin/pg_checksums/pg_checksums.c b/src/bin/pg_checksums/pg_checksums.c
index 301e256fbb1..2a38f1d688b 100644
--- a/src/bin/pg_checksums/pg_checksums.c
+++ b/src/bin/pg_checksums/pg_checksums.c
@@ -585,7 +585,7 @@ main(int argc, char *argv[])
 		ControlFile->state != DB_SHUTDOWNED_IN_RECOVERY)
 		pg_fatal("cluster must be shut down");
 
-	if (ControlFile->data_checksum_version == 0 &&
+	if (ControlFile->data_checksum_version != PG_DATA_CHECKSUM_VERSION &&
 		mode == PG_MODE_CHECK)
 		pg_fatal("data checksums are not enabled in cluster");
 
@@ -593,7 +593,7 @@ main(int argc, char *argv[])
 		mode == PG_MODE_DISABLE)
 		pg_fatal("data checksums are already disabled in cluster");
 
-	if (ControlFile->data_checksum_version > 0 &&
+	if (ControlFile->data_checksum_version == PG_DATA_CHECKSUM_VERSION &&
 		mode == PG_MODE_ENABLE)
 		pg_fatal("data checksums are already enabled in cluster");
 
diff --git a/src/bin/pg_controldata/pg_controldata.c b/src/bin/pg_controldata/pg_controldata.c
index a4060309ae0..47afa82d9c5 100644
--- a/src/bin/pg_controldata/pg_controldata.c
+++ b/src/bin/pg_controldata/pg_controldata.c
@@ -287,6 +287,8 @@ main(int argc, char *argv[])
 		   ControlFile->checkPointCopy.oldestCommitTsXid);
 	printf(_("Latest checkpoint's newestCommitTsXid:%u\n"),
 		   ControlFile->checkPointCopy.newestCommitTsXid);
+	printf(_("Latest checkpoint's data_checksum_version:%u\n"),
+		   ControlFile->checkPointCopy.dataChecksumVersion);
 	printf(_("Time of latest checkpoint:            %s\n"),
 		   ckpttime_str);
 	printf(_("Fake LSN counter for unlogged rels:   %X/%08X\n"),
diff --git a/src/bin/pg_upgrade/controldata.c b/src/bin/pg_upgrade/controldata.c
index aa6e8b4de5d..b7fd4b83757 100644
--- a/src/bin/pg_upgrade/controldata.c
+++ b/src/bin/pg_upgrade/controldata.c
@@ -15,6 +15,7 @@
 #include "access/xlog_internal.h"
 #include "common/string.h"
 #include "pg_upgrade.h"
+#include "storage/bufpage.h"
 
 
 /*
@@ -736,6 +737,14 @@ check_control_data(ControlData *oldctrl,
 	 * check_for_isn_and_int8_passing_mismatch().
 	 */
 
+	/*
+	 * If data checksums are in any in-progress state then disallow the
+	 * upgrade. The user should either let the process finish, or turn off
+	 * data checksums, before retrying.
+	 */
+	if (oldctrl->data_checksum_version > PG_DATA_CHECKSUM_VERSION)
+		pg_fatal("checksums are being enabled in the old cluster");
+
 	/*
 	 * We might eventually allow upgrades from checksum to no-checksum
 	 * clusters.
diff --git a/src/include/access/xlog.h b/src/include/access/xlog.h
index fdfb572467b..352ce0477d4 100644
--- a/src/include/access/xlog.h
+++ b/src/include/access/xlog.h
@@ -57,6 +57,7 @@ extern PGDLLIMPORT int CommitDelay;
 extern PGDLLIMPORT int CommitSiblings;
 extern PGDLLIMPORT bool track_wal_io_timing;
 extern PGDLLIMPORT int wal_decode_buffer_size;
+extern PGDLLIMPORT int data_checksums;
 
 extern PGDLLIMPORT int CheckPointSegments;
 
@@ -119,7 +120,7 @@ extern PGDLLIMPORT bool XLogLogicalInfo;
  * of the bits make it to disk, but the checksum wouldn't match.  Also WAL-log
  * them if forced by wal_log_hints=on.
  */
-#define XLogHintBitIsNeeded() (DataChecksumsEnabled() || wal_log_hints)
+#define XLogHintBitIsNeeded() (wal_log_hints || DataChecksumsNeedWrite())
 
 /* Do we need to WAL-log information required only for Hot Standby and logical replication? */
 #define XLogStandbyInfoActive() (wal_level >= WAL_LEVEL_REPLICA)
@@ -242,7 +243,16 @@ extern XLogRecPtr GetXLogWriteRecPtr(void);
 
 extern uint64 GetSystemIdentifier(void);
 extern char *GetMockAuthenticationNonce(void);
-extern bool DataChecksumsEnabled(void);
+extern bool DataChecksumsNeedWrite(void);
+extern bool DataChecksumsNeedVerify(void);
+extern bool DataChecksumsOnInProgress(void);
+extern bool DataChecksumsOffInProgress(void);
+extern void SetDataChecksumsOnInProgress(void);
+extern void SetDataChecksumsOn(void);
+extern void SetDataChecksumsOff(void);
+extern bool AbsorbDataChecksumsBarrier(int target_state);
+extern const char *show_data_checksums(void);
+extern void InitLocalDataChecksumVersion(void);
 extern bool GetDefaultCharSignedness(void);
 extern XLogRecPtr GetFakeLSNForUnloggedRel(void);
 extern Size XLOGShmemSize(void);
diff --git a/src/include/access/xlog_internal.h b/src/include/access/xlog_internal.h
index fbc5d77ab39..53d1e2bebfd 100644
--- a/src/include/access/xlog_internal.h
+++ b/src/include/access/xlog_internal.h
@@ -25,6 +25,7 @@
 #include "lib/stringinfo.h"
 #include "pgtime.h"
 #include "storage/block.h"
+#include "storage/checksum.h"
 #include "storage/relfilelocator.h"
 
 
@@ -289,6 +290,12 @@ typedef struct xl_restore_point
 	char		rp_name[MAXFNAMELEN];
 } xl_restore_point;
 
+/* Information logged when data checksum level is changed */
+typedef struct xl_checksum_state
+{
+	ChecksumType new_checksumtype;
+} xl_checksum_state;
+
 /* Overwrite of prior contrecord */
 typedef struct xl_overwrite_contrecord
 {
@@ -305,6 +312,12 @@ typedef struct xl_end_of_recovery
 	int			wal_level;
 } xl_end_of_recovery;
 
+typedef struct xl_checkpoint_redo
+{
+	int			wal_level;
+	uint32		data_checksum_version;
+} xl_checkpoint_redo;
+
 /*
  * The functions in xloginsert.c construct a chain of XLogRecData structs
  * to represent the final WAL record.
diff --git a/src/include/catalog/pg_control.h b/src/include/catalog/pg_control.h
index 7503db1af51..7290ece5205 100644
--- a/src/include/catalog/pg_control.h
+++ b/src/include/catalog/pg_control.h
@@ -63,6 +63,9 @@ typedef struct CheckPoint
 	 * set to InvalidTransactionId.
 	 */
 	TransactionId oldestActiveXid;
+
+	/* data checksums state at the time of the checkpoint  */
+	uint32		dataChecksumVersion;
 } CheckPoint;
 
 /* XLOG info values for XLOG rmgr */
@@ -82,6 +85,7 @@ typedef struct CheckPoint
 #define XLOG_OVERWRITE_CONTRECORD		0xD0
 #define XLOG_CHECKPOINT_REDO			0xE0
 #define XLOG_LOGICAL_DECODING_STATUS_CHANGE	0xF0
+#define XLOG_CHECKSUMS					0xC0
 
 
 /*
@@ -223,7 +227,7 @@ typedef struct ControlFileData
 	bool		float8ByVal;	/* float8, int8, etc pass-by-value? */
 
 	/* Are data pages protected by checksums? Zero if no checksum version */
-	uint32		data_checksum_version;
+	uint32		data_checksum_version;	/* persistent */
 
 	/*
 	 * True if the default signedness of char is "signed" on a platform where
diff --git a/src/include/catalog/pg_proc.dat b/src/include/catalog/pg_proc.dat
index 83f6501df38..cb99709a701 100644
--- a/src/include/catalog/pg_proc.dat
+++ b/src/include/catalog/pg_proc.dat
@@ -12405,6 +12405,18 @@
   proname => 'jsonb_subscript_handler', prorettype => 'internal',
   proargtypes => 'internal', prosrc => 'jsonb_subscript_handler' },
 
+# data checksum management functions
+{ oid => '9258',
+  descr => 'disable data checksums',
+  proname => 'pg_disable_data_checksums', provolatile => 's', prorettype => 'void',
+  proparallel => 'r', prosrc => 'disable_data_checksums', proargtypes => '' },
+{ oid => '9257',
+  descr => 'enable data checksums',
+  proname => 'pg_enable_data_checksums', provolatile => 'v', prorettype => 'void',
+  proparallel => 'r', proargtypes => 'int4 int4', proallargtypes => '{int4,int4}',
+  proargmodes => '{i,i}', proargnames => '{cost_delay,cost_limit}',
+  prosrc => 'enable_data_checksums' },
+
 # collation management functions
 { oid => '3445', descr => 'import collations from operating system',
   proname => 'pg_import_system_collations', procost => '100',
diff --git a/src/include/commands/progress.h b/src/include/commands/progress.h
index 359221dc296..cdaf20a9243 100644
--- a/src/include/commands/progress.h
+++ b/src/include/commands/progress.h
@@ -179,4 +179,20 @@
 #define PROGRESS_COPY_TYPE_PIPE 3
 #define PROGRESS_COPY_TYPE_CALLBACK 4
 
+/* Progress parameters for PROGRESS_DATACHECKSUMS */
+#define PROGRESS_DATACHECKSUMS_PHASE		0
+#define PROGRESS_DATACHECKSUMS_DBS_TOTAL	1
+#define PROGRESS_DATACHECKSUMS_DBS_DONE		2
+#define PROGRESS_DATACHECKSUMS_RELS_TOTAL	3
+#define PROGRESS_DATACHECKSUMS_RELS_DONE	4
+#define PROGRESS_DATACHECKSUMS_BLOCKS_TOTAL	5
+#define PROGRESS_DATACHECKSUMS_BLOCKS_DONE	6
+
+/* Phases of datachecksumsworker operation */
+#define PROGRESS_DATACHECKSUMS_PHASE_ENABLING			0
+#define PROGRESS_DATACHECKSUMS_PHASE_DISABLING			1
+#define PROGRESS_DATACHECKSUMS_PHASE_WAITING_TEMPREL	2
+#define PROGRESS_DATACHECKSUMS_PHASE_WAITING_BARRIER	3
+#define PROGRESS_DATACHECKSUMS_PHASE_DONE				4
+
 #endif
diff --git a/src/include/miscadmin.h b/src/include/miscadmin.h
index db559b39c4d..13d111b0e57 100644
--- a/src/include/miscadmin.h
+++ b/src/include/miscadmin.h
@@ -367,6 +367,9 @@ typedef enum BackendType
 	B_WAL_SUMMARIZER,
 	B_WAL_WRITER,
 
+	B_DATACHECKSUMSWORKER_LAUNCHER,
+	B_DATACHECKSUMSWORKER_WORKER,
+
 	/*
 	 * Logger is not connected to shared memory and does not have a PGPROC
 	 * entry.
@@ -392,6 +395,9 @@ extern PGDLLIMPORT BackendType MyBackendType;
 #define AmWalSummarizerProcess()	(MyBackendType == B_WAL_SUMMARIZER)
 #define AmWalWriterProcess()		(MyBackendType == B_WAL_WRITER)
 #define AmIoWorkerProcess()			(MyBackendType == B_IO_WORKER)
+#define AmDataChecksumsWorkerProcess() \
+	(MyBackendType == B_DATACHECKSUMSWORKER_LAUNCHER || \
+	 MyBackendType == B_DATACHECKSUMSWORKER_WORKER)
 
 #define AmSpecialWorkerProcess() \
 	(AmAutoVacuumLauncherProcess() || \
diff --git a/src/include/postmaster/datachecksumsworker.h b/src/include/postmaster/datachecksumsworker.h
new file mode 100644
index 00000000000..dc4bf24be7c
--- /dev/null
+++ b/src/include/postmaster/datachecksumsworker.h
@@ -0,0 +1,50 @@
+/*-------------------------------------------------------------------------
+ *
+ * datachecksumsworker.h
+ *	  header file for data checksum helper background worker
+ *
+ *
+ * Portions Copyright (c) 1996-2026, PostgreSQL Global Development Group
+ * Portions Copyright (c) 1994, Regents of the University of California
+ *
+ * src/include/postmaster/datachecksumsworker.h
+ *
+ *-------------------------------------------------------------------------
+ */
+#ifndef DATACHECKSUMSWORKER_H
+#define DATACHECKSUMSWORKER_H
+
+/* Shared memory */
+extern Size DataChecksumsWorkerShmemSize(void);
+extern void DataChecksumsWorkerShmemInit(void);
+
+/* Possible operations the Datachecksumsworker can perform */
+typedef enum DataChecksumsWorkerOperation
+{
+	ENABLE_DATACHECKSUMS,
+	DISABLE_DATACHECKSUMS,
+	/* TODO: VERIFY_DATACHECKSUMS, */
+} DataChecksumsWorkerOperation;
+
+/*
+ * Possible states for a database entry which has been processed. Exported
+ * here since we want to be able to reference this from injection point tests.
+ */
+typedef enum
+{
+	DATACHECKSUMSWORKER_SUCCESSFUL = 0,
+	DATACHECKSUMSWORKER_ABORTED,
+	DATACHECKSUMSWORKER_FAILED,
+	DATACHECKSUMSWORKER_RETRYDB,
+} DataChecksumsWorkerResult;
+
+/* Start the background processes for enabling or disabling checksums */
+void		StartDataChecksumsWorkerLauncher(DataChecksumsWorkerOperation op,
+											 int cost_delay,
+											 int cost_limit);
+
+/* Background worker entrypoints */
+void		DataChecksumsWorkerLauncherMain(Datum arg);
+void		DataChecksumsWorkerMain(Datum arg);
+
+#endif							/* DATACHECKSUMSWORKER_H */
diff --git a/src/include/postmaster/proctypelist.h b/src/include/postmaster/proctypelist.h
index 0b99eaabfd0..3c9aa1478fd 100644
--- a/src/include/postmaster/proctypelist.h
+++ b/src/include/postmaster/proctypelist.h
@@ -38,6 +38,8 @@ PG_PROCTYPE(B_BACKEND, gettext_noop("client backend"), BackendMain, true)
 PG_PROCTYPE(B_BG_WORKER, gettext_noop("background worker"), BackgroundWorkerMain, true)
 PG_PROCTYPE(B_BG_WRITER, gettext_noop("background writer"), BackgroundWriterMain, true)
 PG_PROCTYPE(B_CHECKPOINTER, gettext_noop("checkpointer"), CheckpointerMain, true)
+PG_PROCTYPE(B_DATACHECKSUMSWORKER_LAUNCHER, gettext_noop("datachecksum launcher"), NULL, false)
+PG_PROCTYPE(B_DATACHECKSUMSWORKER_WORKER, gettext_noop("datachecksum worker"), NULL, false)
 PG_PROCTYPE(B_DEAD_END_BACKEND, gettext_noop("dead-end client backend"), BackendMain, true)
 PG_PROCTYPE(B_INVALID, gettext_noop("unrecognized"), NULL, false)
 PG_PROCTYPE(B_IO_WORKER, gettext_noop("io worker"), IoWorkerMain, true)
diff --git a/src/include/storage/bufpage.h b/src/include/storage/bufpage.h
index ae3725b3b81..8a668d14803 100644
--- a/src/include/storage/bufpage.h
+++ b/src/include/storage/bufpage.h
@@ -16,6 +16,7 @@
 
 #include "access/xlogdefs.h"
 #include "storage/block.h"
+#include "storage/checksum.h"
 #include "storage/off.h"
 
 /* GUC variable */
@@ -204,7 +205,6 @@ typedef PageHeaderData *PageHeader;
  * handling pages.
  */
 #define PG_PAGE_LAYOUT_VERSION		4
-#define PG_DATA_CHECKSUM_VERSION	1
 
 /* ----------------------------------------------------------------
  *						page support functions
diff --git a/src/include/storage/checksum.h b/src/include/storage/checksum.h
index ff417d5ae3e..b6bbba82949 100644
--- a/src/include/storage/checksum.h
+++ b/src/include/storage/checksum.h
@@ -15,6 +15,20 @@
 
 #include "storage/block.h"
 
+/*
+ * 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;
+
 /*
  * Compute the checksum for a Postgres page.  The page must be aligned on a
  * 4-byte boundary.
diff --git a/src/include/storage/lwlocklist.h b/src/include/storage/lwlocklist.h
index e94ebce95b9..0c02f2ae38f 100644
--- a/src/include/storage/lwlocklist.h
+++ b/src/include/storage/lwlocklist.h
@@ -87,6 +87,7 @@ PG_LWLOCK(52, SerialControl)
 PG_LWLOCK(53, AioWorkerSubmissionQueue)
 PG_LWLOCK(54, WaitLSN)
 PG_LWLOCK(55, LogicalDecodingControl)
+PG_LWLOCK(56, DataChecksumsWorker)
 
 /*
  * There also exist several built-in LWLock tranches.  As with the predefined
diff --git a/src/include/storage/proc.h b/src/include/storage/proc.h
index 679f0624f92..4c90e27d1af 100644
--- a/src/include/storage/proc.h
+++ b/src/include/storage/proc.h
@@ -463,12 +463,11 @@ extern PGDLLIMPORT PGPROC *PreparedXactProcs;
  * Background writer, checkpointer, WAL writer, WAL summarizer, and archiver
  * run during normal operation.  Startup process and WAL receiver also consume
  * 2 slots, but WAL writer is launched only after startup has exited, so we
- * only need 6 slots.
+ * only need 6 slots to cover these.
  */
 #define MAX_IO_WORKERS          32
 #define NUM_AUXILIARY_PROCS		(6 + MAX_IO_WORKERS)
 
-
 /* configurable options */
 extern PGDLLIMPORT int DeadlockTimeout;
 extern PGDLLIMPORT int StatementTimeout;
diff --git a/src/include/storage/procsignal.h b/src/include/storage/procsignal.h
index e52b8eb7697..5430386ee70 100644
--- a/src/include/storage/procsignal.h
+++ b/src/include/storage/procsignal.h
@@ -56,6 +56,10 @@ typedef enum
 	PROCSIGNAL_BARRIER_SMGRRELEASE, /* ask smgr to close files */
 	PROCSIGNAL_BARRIER_UPDATE_XLOG_LOGICAL_INFO,	/* ask to update
 													 * XLogLogicalInfo */
+	PROCSIGNAL_BARRIER_CHECKSUM_OFF,
+	PROCSIGNAL_BARRIER_CHECKSUM_INPROGRESS_ON,
+	PROCSIGNAL_BARRIER_CHECKSUM_INPROGRESS_OFF,
+	PROCSIGNAL_BARRIER_CHECKSUM_ON,
 } ProcSignalBarrierType;
 
 /*
diff --git a/src/include/utils/backend_progress.h b/src/include/utils/backend_progress.h
index 19f63b41431..34913667cab 100644
--- a/src/include/utils/backend_progress.h
+++ b/src/include/utils/backend_progress.h
@@ -28,6 +28,7 @@ typedef enum ProgressCommandType
 	PROGRESS_COMMAND_CREATE_INDEX,
 	PROGRESS_COMMAND_BASEBACKUP,
 	PROGRESS_COMMAND_COPY,
+	PROGRESS_COMMAND_DATACHECKSUMS,
 } ProgressCommandType;
 
 #define PGSTAT_NUM_PROGRESS_PARAM	20
diff --git a/src/test/modules/Makefile b/src/test/modules/Makefile
index 44c7163c1cd..319b42db4a2 100644
--- a/src/test/modules/Makefile
+++ b/src/test/modules/Makefile
@@ -20,6 +20,7 @@ SUBDIRS = \
 		  test_bitmapset \
 		  test_bloomfilter \
 		  test_cloexec \
+		  test_checksums \
 		  test_copy_callbacks \
 		  test_custom_rmgrs \
 		  test_custom_stats \
diff --git a/src/test/modules/meson.build b/src/test/modules/meson.build
index 2634a519935..194f635713e 100644
--- a/src/test/modules/meson.build
+++ b/src/test/modules/meson.build
@@ -20,6 +20,7 @@ subdir('test_binaryheap')
 subdir('test_bitmapset')
 subdir('test_bloomfilter')
 subdir('test_cloexec')
+subdir('test_checksums')
 subdir('test_copy_callbacks')
 subdir('test_cplusplusext')
 subdir('test_custom_rmgrs')
diff --git a/src/test/modules/test_checksums/.gitignore b/src/test/modules/test_checksums/.gitignore
new file mode 100644
index 00000000000..871e943d50e
--- /dev/null
+++ b/src/test/modules/test_checksums/.gitignore
@@ -0,0 +1,2 @@
+# Generated by test suite
+/tmp_check/
diff --git a/src/test/modules/test_checksums/Makefile b/src/test/modules/test_checksums/Makefile
new file mode 100644
index 00000000000..fa85b79ae57
--- /dev/null
+++ b/src/test/modules/test_checksums/Makefile
@@ -0,0 +1,40 @@
+#-------------------------------------------------------------------------
+#
+# Makefile for src/test/modules/test_checksums
+#
+# Portions Copyright (c) 1996-2026, PostgreSQL Global Development Group
+# Portions Copyright (c) 1994, Regents of the University of California
+#
+# src/test/modules/test_checksums/Makefile
+#
+#-------------------------------------------------------------------------
+
+EXTRA_INSTALL = src/test/modules/injection_points
+
+export enable_injection_points
+
+MODULE_big = test_checksums
+OBJS = \
+	$(WIN32RES) \
+	test_checksums.o
+PGFILEDESC = "test_checksums - test code for data checksums"
+
+EXTENSION = test_checksums
+DATA = test_checksums--1.0.sql
+
+ifdef USE_PGXS
+PG_CONFIG = pg_config
+PGXS := $(shell $(PG_CONFIG) --pgxs)
+include $(PGXS)
+else
+subdir = src/test/modules/test_checksums
+top_builddir = ../../../..
+include $(top_builddir)/src/Makefile.global
+include $(top_srcdir)/contrib/contrib-global.mk
+endif
+
+check:
+	$(prove_check)
+
+installcheck:
+	$(prove_installcheck)
diff --git a/src/test/modules/test_checksums/README b/src/test/modules/test_checksums/README
new file mode 100644
index 00000000000..6a23e4ff0ae
--- /dev/null
+++ b/src/test/modules/test_checksums/README
@@ -0,0 +1,30 @@
+src/test/modules/test_checksums/README
+
+Regression tests for data checksums
+===================================
+This directory contains a test suite for enabling, and disabling, data
+checksums both offline as well as in a running cluster.
+
+Running the tests with autoconf
+===============================
+
+    make check
+
+or
+
+    make installcheck
+
+Running the tests with meson
+============================
+From your build directory, issue the following command:
+
+    meson test -q --print-errorlogs --suite setup --suite test_checksums
+
+NOTE: This creates a temporary installation (in the case of "make check" or
+"--suite setup"), with multiple nodes, be they master or standby(s) for the
+purpose of the tests.
+
+NOTE: This test suite requires TAP tests to be enabled, a subset of the tests
+also require injection points to function.  In order to run the extended test
+then "checksum_extended" must be set in the PG_TEST_EXTRA environment
+variable.
diff --git a/src/test/modules/test_checksums/meson.build b/src/test/modules/test_checksums/meson.build
new file mode 100644
index 00000000000..3e6a155f35f
--- /dev/null
+++ b/src/test/modules/test_checksums/meson.build
@@ -0,0 +1,36 @@
+# Copyright (c) 2026, PostgreSQL Global Development Group
+
+test_checksums_sources = files(
+	'test_checksums.c',
+)
+
+test_checksums = shared_module('test_checksums',
+	test_checksums_sources,
+	kwargs: pg_test_mod_args,
+)
+test_install_libs += test_checksums
+
+test_install_data += files(
+	'test_checksums.control',
+	'test_checksums--1.0.sql',
+)
+
+tests += {
+  'name': 'test_checksums',
+  'sd': meson.current_source_dir(),
+  'bd': meson.current_build_dir(),
+  'tap': {
+    'env': {
+       'enable_injection_points': get_option('injection_points') ? 'yes' : 'no',
+	},
+    'tests': [
+      't/001_basic.pl',
+      't/002_restarts.pl',
+      't/003_standby_restarts.pl',
+      't/004_offline.pl',
+      't/005_injection.pl',
+      't/006_pgbench_single.pl',
+      't/007_pgbench_standby.pl',
+    ],
+  },
+}
diff --git a/src/test/modules/test_checksums/t/001_basic.pl b/src/test/modules/test_checksums/t/001_basic.pl
new file mode 100644
index 00000000000..397f6411cb5
--- /dev/null
+++ b/src/test/modules/test_checksums/t/001_basic.pl
@@ -0,0 +1,63 @@
+
+# Copyright (c) 2026, PostgreSQL Global Development Group
+
+# Test suite for testing enabling data checksums in an online cluster
+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;
+
+# Initialize node with checksums disabled.
+my $node = PostgreSQL::Test::Cluster->new('main');
+$node->init(no_data_checksums => 1);
+$node->start;
+
+# Create some content to have un-checksummed data in the cluster
+$node->safe_psql('postgres',
+	"CREATE TABLE t AS SELECT generate_series(1,10000) AS a;");
+
+# Ensure that checksums are turned off
+test_checksum_state($node, 'off');
+
+# Enable data checksums and wait for the state transition to 'on'
+enable_data_checksums($node, wait => 'on');
+
+# Run a dummy query just to make sure we can read back data
+my $result =
+  $node->safe_psql('postgres', "SELECT count(*) FROM t WHERE a > 1 ");
+is($result, '9999', 'ensure checksummed pages can be read back');
+
+# Enable data checksums again which should be a no-op so we explicitly don't
+# wait for any state transition as none should happen here
+enable_data_checksums($node);
+test_checksum_state($node, 'on');
+# ..and make sure we can still read/write data
+$node->safe_psql('postgres', "UPDATE t SET a = a + 1;");
+$result = $node->safe_psql('postgres', "SELECT count(*) FROM t WHERE a > 1");
+is($result, '10000', 'ensure checksummed pages can be read back');
+
+# Disable checksums again and wait for the state transition
+disable_data_checksums($node, wait => 'on');
+
+# Test reading data again
+$result = $node->safe_psql('postgres', "SELECT count(*) FROM t WHERE a > 1");
+is($result, '10000', 'ensure previously checksummed pages can be read back');
+
+# Re-enable checksums and make sure that the underlying data has changed to
+# ensure that checksums will be different.
+$node->safe_psql('postgres', "UPDATE t SET a = a + 1;");
+enable_data_checksums($node, wait => 'on');
+
+# Run a dummy query just to make sure we can read back the data
+$result = $node->safe_psql('postgres', "SELECT count(*) FROM t WHERE a > 1");
+is($result, '10000', 'ensure checksummed pages can be read back');
+
+$node->stop;
+done_testing();
diff --git a/src/test/modules/test_checksums/t/002_restarts.pl b/src/test/modules/test_checksums/t/002_restarts.pl
new file mode 100644
index 00000000000..a4d35e9f95c
--- /dev/null
+++ b/src/test/modules/test_checksums/t/002_restarts.pl
@@ -0,0 +1,110 @@
+
+# Copyright (c) 2026, PostgreSQL Global Development Group
+
+# Test suite for testing enabling data checksums in an online cluster with a
+# restart which breaks 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;
+
+# Initialize node with checksums disabled.
+my $node = PostgreSQL::Test::Cluster->new('main');
+$node->init(no_data_checksums => 1);
+$node->start;
+
+# Initialize result storage for queries
+my $result;
+
+# Create some content to have un-checksummed data in the cluster
+$node->safe_psql('postgres',
+	"CREATE TABLE t AS SELECT generate_series(1,10000) AS a;");
+
+# Ensure that checksums are disabled
+test_checksum_state($node, 'off');
+
+SKIP:
+{
+	skip 'Data checksum delay tests not enabled in PG_TEST_EXTRA', 6
+	  if (!$ENV{PG_TEST_EXTRA}
+		|| $ENV{PG_TEST_EXTRA} !~ /\bchecksum_extended\b/);
+
+	# Create a barrier for checksumming to block on, in this case a pre-
+	# existing temporary table which is kept open while processing is started.
+	# We can accomplish this by setting up an interactive psql process which
+	# keeps the temporary table created as we enable checksums in another psql
+	# process.
+	#
+	# This is a similar test to the synthetic variant in 005_injection.pl
+	# which fakes this scenario.
+	my $bsession = $node->background_psql('postgres');
+	$bsession->query_safe('CREATE TEMPORARY TABLE tt (a integer);');
+
+	# In another session, make sure we can see the blocking temp table but
+	# start processing anyways and check that we are blocked with a proper
+	# wait event.
+	$result = $node->safe_psql('postgres',
+		"SELECT relpersistence FROM pg_catalog.pg_class WHERE relname = 'tt';"
+	);
+	is($result, 't', 'ensure we can see the temporary table');
+
+	# Enabling data checksums shouldn't work as the process is blocked on the
+	# temporary table held open by $bsession. Ensure that we reach inprogress-
+	# on before we do more tests.
+	enable_data_checksums($node, wait => 'inprogress-on');
+
+	# Wait for processing to finish and the worker waiting for leftover temp
+	# relations to be able to actually finish
+	$result = $node->poll_query_until(
+		'postgres',
+		"SELECT wait_event FROM pg_catalog.pg_stat_activity "
+		  . "WHERE backend_type = 'datachecksum worker';",
+		'ChecksumEnableTemptableWait');
+
+	# The datachecksumsworker waits for temporary tables to disappear for 3
+	# seconds before retrying, so sleep for 4 seconds to be guaranteed to see
+	# a retry cycle
+	sleep(4);
+
+	# Re-check the wait event to ensure we are blocked on the right thing.
+	$result = $node->safe_psql('postgres',
+			"SELECT wait_event FROM pg_catalog.pg_stat_activity "
+		  . "WHERE backend_type = 'datachecksum worker';");
+	is($result, 'ChecksumEnableTemptableWait',
+		'ensure the correct wait condition is set');
+	test_checksum_state($node, 'inprogress-on');
+
+	# Stop the cluster while bsession is still attached.  We can't close the
+	# session first since the brief period between closing and stopping might
+	# be enough for checksums to get enabled.
+	$node->stop;
+	$bsession->quit;
+	$node->start;
+
+	# Ensure the checksums aren't enabled across the restart.  This leaves the
+	# cluster in the same state as before we entered the SKIP block.
+	test_checksum_state($node, 'off');
+}
+
+enable_data_checksums($node, wait => 'on');
+
+$result = $node->safe_psql('postgres', "SELECT count(*) FROM t WHERE a > 1");
+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%';",
+	'0');
+is($result, 1, 'await datachecksums worker/launcher termination');
+
+disable_data_checksums($node, wait => 1);
+
+$node->stop;
+done_testing();
diff --git a/src/test/modules/test_checksums/t/003_standby_restarts.pl b/src/test/modules/test_checksums/t/003_standby_restarts.pl
new file mode 100644
index 00000000000..c5732a2c7da
--- /dev/null
+++ b/src/test/modules/test_checksums/t/003_standby_restarts.pl
@@ -0,0 +1,114 @@
+
+# Copyright (c) 2026, PostgreSQL Global Development Group
+
+# Test suite for testing enabling data checksums in an online cluster with
+# streaming replication
+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;
+
+# Initialize primary node
+my $node_primary = PostgreSQL::Test::Cluster->new('primary');
+$node_primary->init(allows_streaming => 1, no_data_checksums => 1);
+$node_primary->start;
+
+my $slotname = 'physical_slot';
+$node_primary->safe_psql('postgres',
+	"SELECT pg_create_physical_replication_slot('$slotname')");
+
+# Take backup
+my $backup_name = 'my_backup';
+$node_primary->backup($backup_name);
+
+# Create streaming standby linking to primary
+my $node_standby_1 = PostgreSQL::Test::Cluster->new('standby_1');
+$node_standby_1->init_from_backup($node_primary, $backup_name,
+	has_streaming => 1);
+$node_standby_1->append_conf(
+	'postgresql.conf', qq[
+primary_slot_name = '$slotname'
+]);
+$node_standby_1->start;
+
+# Create some content on the primary to have un-checksummed data in the cluster
+$node_primary->safe_psql('postgres',
+	"CREATE TABLE t AS SELECT generate_series(1,10000) AS a;");
+
+# Wait for standbys to catch up
+$node_primary->wait_for_catchup($node_standby_1, 'replay',
+	$node_primary->lsn('insert'));
+
+# Check that checksums are turned off on all nodes
+test_checksum_state($node_primary, 'off');
+test_checksum_state($node_standby_1, 'off');
+
+# ---------------------------------------------------------------------------
+# Enable checksums for the cluster, and make sure that both the primary and
+# standby change state.
+#
+
+# Ensure that the primary switches to "inprogress-on"
+enable_data_checksums($node_primary, wait => 'inprogress-on');
+# Wait for checksum enable to be replayed
+$node_primary->wait_for_catchup($node_standby_1, 'replay');
+
+# Ensure that the standby has switched to "inprogress-on" or "on".  Normally it
+# would be "inprogress-on", but it is theoretically possible for the primary to
+# complete the checksum enabling *and* have the standby replay that record
+# before we reach the check below.
+my $result = $node_standby_1->poll_query_until(
+	'postgres',
+	"SELECT setting = 'off' FROM pg_catalog.pg_settings WHERE name = 'data_checksums';",
+	'f');
+is($result, 1, 'ensure standby has absorbed the inprogress-on barrier');
+$result = $node_standby_1->safe_psql('postgres',
+	"SELECT setting FROM pg_catalog.pg_settings WHERE name = 'data_checksums';"
+);
+
+is(($result eq 'inprogress-on' || $result eq 'on'),
+	1, 'ensure checksums are on, or in progress, on standby_1');
+
+# Insert some more data which should be checksummed on INSERT
+$node_primary->safe_psql('postgres',
+	"INSERT INTO t VALUES (generate_series(1, 10000));");
+
+# Wait for checksums enabled on the primary and standby
+wait_for_checksum_state($node_primary, 'on');
+wait_for_checksum_state($node_standby_1, 'on');
+
+$result =
+  $node_primary->safe_psql('postgres', "SELECT count(a) FROM t WHERE a > 1");
+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%';",
+	'0');
+is($result, 1, 'await datachecksums worker/launcher termination');
+
+#
+# Disable checksums and ensure it's propagated to standby and that we can
+# still read all data
+#
+
+# Disable checksums and wait for the operation to be replayed
+disable_data_checksums($node_primary);
+$node_primary->wait_for_catchup($node_standby_1, 'replay');
+# Ensure that the primary and standby has switched to off
+wait_for_checksum_state($node_primary, 'off');
+wait_for_checksum_state($node_standby_1, 'off');
+# Doublecheck reading data without errors
+$result =
+  $node_primary->safe_psql('postgres', "SELECT count(a) FROM t WHERE a > 1");
+is($result, "19998", 'ensure we can safely read all data without checksums');
+
+$node_standby_1->stop;
+$node_primary->stop;
+done_testing();
diff --git a/src/test/modules/test_checksums/t/004_offline.pl b/src/test/modules/test_checksums/t/004_offline.pl
new file mode 100644
index 00000000000..f33e278de28
--- /dev/null
+++ b/src/test/modules/test_checksums/t/004_offline.pl
@@ -0,0 +1,82 @@
+
+# Copyright (c) 2026, PostgreSQL Global Development Group
+
+# Test suite for testing enabling data checksums offline from various states
+# of checksum 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;
+
+# Initialize node with checksums disabled.
+my $node = PostgreSQL::Test::Cluster->new('main');
+$node->init(no_data_checksums => 1);
+$node->start;
+
+# Create some content to have un-checksummed data in the cluster
+$node->safe_psql('postgres',
+	"CREATE TABLE t AS SELECT generate_series(1,10000) AS a;");
+
+# Ensure that checksums are disabled
+test_checksum_state($node, 'off');
+
+# Enable checksums offline using pg_checksums
+$node->stop;
+$node->checksum_enable_offline;
+$node->start;
+
+# Ensure that checksums are enabled
+test_checksum_state($node, 'on');
+
+# Run a dummy query just to make sure we can read back some data
+my $result =
+  $node->safe_psql('postgres', "SELECT count(*) FROM t WHERE a > 1");
+is($result, '9999', 'ensure checksummed pages can be read back');
+
+# Disable checksums offline again using pg_checksums
+$node->stop;
+$node->checksum_disable_offline;
+$node->start;
+
+# Ensure that checksums are disabled
+test_checksum_state($node, 'off');
+
+# Create a barrier for checksumming to block on, in this case a pre-existing
+# temporary table which is kept open while processing is started. We can
+# accomplish this by setting up an interactive psql process which keeps the
+# temporary table created as we enable checksums in another psql process.
+
+my $bsession = $node->background_psql('postgres');
+$bsession->query_safe('CREATE TEMPORARY TABLE tt (a integer);');
+
+# In another session, make sure we can see the blocking temp table but start
+# processing anyways and check that we are blocked with a proper wait event.
+$result = $node->safe_psql('postgres',
+	"SELECT relpersistence FROM pg_catalog.pg_class WHERE relname = 'tt';");
+is($result, 't', 'ensure we can see the temporary table');
+
+enable_data_checksums($node, wait => 'inprogress-on');
+
+# Turn the cluster off and enable checksums offline, then start back up
+$bsession->quit;
+$node->stop;
+$node->checksum_enable_offline;
+$node->start;
+
+# Ensure that checksums are now enabled even though processing wasn't
+# restarted
+test_checksum_state($node, 'on');
+
+# Run a dummy query just to make sure we can read back some data
+$result = $node->safe_psql('postgres', "SELECT count(*) FROM t WHERE a > 1");
+is($result, '9999', 'ensure checksummed pages can be read back');
+
+$node->stop;
+done_testing();
diff --git a/src/test/modules/test_checksums/t/005_injection.pl b/src/test/modules/test_checksums/t/005_injection.pl
new file mode 100644
index 00000000000..6d374793eec
--- /dev/null
+++ b/src/test/modules/test_checksums/t/005_injection.pl
@@ -0,0 +1,109 @@
+
+# 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;
+
+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('main');
+$node->init(no_data_checksums => 1);
+$node->start;
+
+# Set up test environment
+$node->safe_psql('postgres', 'CREATE EXTENSION test_checksums;');
+
+# ---------------------------------------------------------------------------
+# Inducing failures and crashes in processing
+
+# Force enabling checksums to fail by marking one of the databases as having
+# failed in processing.
+disable_data_checksums($node, wait => 1);
+$node->safe_psql('postgres', 'SELECT dcw_inject_fail_database(true);');
+enable_data_checksums($node, wait => 'off');
+$node->safe_psql('postgres', 'SELECT dcw_inject_fail_database(false);');
+
+# Make sure that disabling after a failure works
+disable_data_checksums($node);
+test_checksum_state($node, 'off');
+
+# ---------------------------------------------------------------------------
+# Timing and retry related tests
+#
+
+# Force the enable checksums processing to make multiple passes by removing
+# one database from the list in the first pass.  This will simulate a CREATE
+# DATABASE during processing.  Doing this via fault injection makes the test
+# not be subject to exact timing.
+$node->safe_psql('postgres', 'SELECT dcw_prune_dblist(true);');
+enable_data_checksums($node, wait => 'on');
+
+SKIP:
+{
+	skip 'Data checksum delay tests not enabled in PG_TEST_EXTRA', 4
+	  if (!$ENV{PG_TEST_EXTRA}
+		|| $ENV{PG_TEST_EXTRA} !~ /\bchecksum_extended\b/);
+
+	# Inject a delay in the barrier for enabling checksums
+	disable_data_checksums($node, wait => 1);
+	$node->safe_psql('postgres', 'SELECT dcw_inject_delay_barrier();');
+	enable_data_checksums($node, wait => 'on');
+
+	# Fake the existence of a temporary table at the start of processing, which
+	# will force the processing to wait and retry in order to wait for it to
+	# disappear.
+	disable_data_checksums($node, wait => 1);
+	$node->safe_psql('postgres', 'SELECT dcw_fake_temptable(true);');
+	enable_data_checksums($node, wait => 'on');
+}
+
+# Test starting the data checksums launcher from two sessions concurrently. In
+# order reliably run things concurrently we use background psql's which block
+# on various fault injected delays.
+enable_data_checksums($node, wait => 'on');
+
+my $A = $node->background_psql('postgres');
+$A->query_safe('SELECT dcw_inject_launcher_delay(true);');
+$A->query_safe('SELECT pg_enable_data_checksums();');
+$node->safe_psql('postgres', 'SELECT dcw_inject_launcher_delay(false);');
+my ($ret, $stdout, $stderr) =
+  $node->psql('postgres', 'SELECT pg_disable_data_checksums();');
+wait_for_checksum_state($node, 'off');
+
+
+my $B = $node->background_psql('postgres');
+$B->query_safe('SELECT dcw_inject_startup_delay(true);');
+$B->query_safe('SELECT pg_enable_data_checksums();');
+$node->safe_psql('postgres', 'SELECT dcw_inject_startup_delay(false);');
+($ret, $stdout, $stderr) =
+  $node->psql('postgres', 'SELECT pg_enable_data_checksums();');
+like($stderr, qr/data checksum processing already running/,
+	'ERROR on stderr');
+wait_for_checksum_state($node, 'on');
+
+$A->quit;
+$B->quit;
+
+
+$node->stop;
+done_testing();
diff --git a/src/test/modules/test_checksums/t/006_pgbench_single.pl b/src/test/modules/test_checksums/t/006_pgbench_single.pl
new file mode 100644
index 00000000000..b9c3841404d
--- /dev/null
+++ b/src/test/modules/test_checksums/t/006_pgbench_single.pl
@@ -0,0 +1,252 @@
+
+# Copyright (c) 2026, PostgreSQL Global Development Group
+
+# Test suite for testing enabling data checksums in an online cluster with
+# concurrent activity via pgbench runs
+
+use strict;
+use warnings FATAL => 'all';
+
+use PostgreSQL::Test::Cluster;
+use PostgreSQL::Test::Utils;
+use Test::More;
+
+use FindBin;
+use lib $FindBin::RealBin;
+
+use DataChecksums::Utils;
+
+if (!$ENV{PG_TEST_EXTRA} || $ENV{PG_TEST_EXTRA} !~ /\bchecksum_extended\b/)
+{
+	plan skip_all => 'Extended tests not enabled';
+}
+
+if ($ENV{enable_injection_points} ne 'yes')
+{
+	plan skip_all => 'Injection points not supported by this build';
+}
+
+my $node;
+my $node_loglocation = 0;
+
+# The number of full test iterations which will be performed. The exact number
+# of tests performed and the wall time taken is non-deterministic as the test
+# performs a lot of randomized actions, but 10 iterations will be a long test
+# run regardless.
+my $TEST_ITERATIONS = 10;
+
+# Variables which record the current state of the cluster
+my $data_checksum_state = 'off';
+my $pgbench = undef;
+
+# 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;
+
+	# Randomize the number of pgbench clients a bit (range 1-16)
+	my $clients = 1 + int(rand(15));
+
+	my @cmd = ('pgbench', '-p', $port, '-T', '600', '-c', $clients);
+
+	# Randomize whether we spawn connections or not
+	push(@cmd, '-C') if (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));
+}
+
+# Invert the state of data checksums in the cluster, if data checksums are on
+# then disable them and vice versa. Also performs proper validation of the
+# before and after state.
+sub flip_data_checksums
+{
+	# First, make sure the cluster is in the state we expect it to be
+	test_checksum_state($node, $data_checksum_state);
+
+	if ($data_checksum_state eq 'off')
+	{
+		# Coin-toss to see if we are injecting a retry due to a temptable
+		$node->safe_psql('postgres', 'SELECT dcw_fake_temptable();')
+		  if cointoss();
+
+		# log LSN right before we start changing checksums
+		my $result =
+		  $node->safe_psql('postgres', "SELECT pg_current_wal_lsn()");
+		note("LSN before enabling: " . $result . "\n");
+
+		# Ensure that the primary switches to "inprogress-on"
+		enable_data_checksums($node, wait => 'inprogress-on');
+
+		random_sleep();
+
+		# Wait for checksums enabled on the primary
+		wait_for_checksum_state($node, 'on');
+
+		# log LSN right after the primary flips checksums to "on"
+		$result = $node->safe_psql('postgres', "SELECT pg_current_wal_lsn()");
+		note("LSN after enabling: " . $result . "\n");
+
+		random_sleep();
+
+		$node->safe_psql('postgres', 'SELECT dcw_fake_temptable(false);');
+		$data_checksum_state = 'on';
+	}
+	elsif ($data_checksum_state eq 'on')
+	{
+		random_sleep();
+
+		# log LSN right before we start changing checksums
+		my $result =
+		  $node->safe_psql('postgres', "SELECT pg_current_wal_lsn()");
+		note("LSN before disabling: " . $result . "\n");
+
+		disable_data_checksums($node);
+
+		# Wait for checksums disabled on the primary
+		wait_for_checksum_state($node, 'off');
+
+		# log LSN right after the primary flips checksums to "off"
+		$result = $node->safe_psql('postgres', "SELECT pg_current_wal_lsn()");
+		note("LSN after disabling: " . $result . "\n");
+
+		random_sleep();
+
+		$data_checksum_state = 'off';
+	}
+	else
+	{
+		# This should only happen due to programmer error when hacking on the
+		# test code, but since that might pass subtly we error out.
+		BAIL_OUT('data_checksum_state variable has invalid state:'
+				 . $data_checksum_state);
+	}
+}
+
+# Create and start a cluster with one node
+$node = PostgreSQL::Test::Cluster->new('main');
+$node->init(allows_streaming => 1, no_data_checksums => 1);
+# max_connections need to be bumped in order to accommodate for pgbench clients
+# and log_statement is dialled down since it otherwise will generate enormous
+# amounts of logging. Page verification failures are still logged.
+$node->append_conf(
+	'postgresql.conf',
+	qq[
+max_connections = 100
+log_statement = none
+]);
+$node->start;
+$node->safe_psql('postgres', 'CREATE EXTENSION test_checksums;');
+# Create some content to have un-checksummed data in the cluster
+$node->safe_psql('postgres',
+	"CREATE TABLE t AS SELECT generate_series(1, 100000) AS a;");
+# Initialize pgbench
+$node->command_ok([ 'pgbench', '-i', '-s', '100', '-q', 'postgres' ]);
+# Start the test suite with pgbench running.
+background_rw_pgbench($node->port);
+
+# Main test suite. This loop will start a pgbench run on the cluster and while
+# that's running flip the state of data checksums concurrently. It will then
+# randomly restart the cluster and then check for
+# the desired state.  The idea behind doing things randomly is to stress out
+# any timing related issues by subjecting the cluster for varied workloads.
+# A TODO is to generate a trace such that any test failure can be traced to
+# its order of operations for debugging.
+for (my $i = 0; $i < $TEST_ITERATIONS; $i++)
+{
+	note("iteration ", ($i + 1), " of ", $TEST_ITERATIONS);
+
+	if (!$node->is_alive)
+	{
+		# Start, to do recovery, and stop
+		$node->start;
+		$node->stop('fast');
+
+		# 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/,
+			"no checksum validation errors in primary log (during WAL recovery)"
+		);
+		$node_loglocation = -s $node->logfile;
+
+		# Randomize the WAL size, to trigger checkpoints less/more often
+		my $sb = 64 + int(rand(1024));
+		$node->append_conf('postgresql.conf', qq[max_wal_size = $sb]);
+		note("changing max_wal_size to " . $sb);
+
+		$node->start;
+
+		# Start a pgbench in the background against the primary
+		background_rw_pgbench($node->port);
+	}
+
+	$node->safe_psql('postgres', "UPDATE t SET a = a + 1;");
+
+	flip_data_checksums();
+	random_sleep();
+	my $result =
+	  $node->safe_psql('postgres', "SELECT count(*) FROM t WHERE a > 1");
+	is($result, '100000', 'ensure data pages can be read back on primary');
+
+	random_sleep();
+
+	# Potentially powercycle the node
+	if (cointoss())
+	{
+		$node->stop(stopmode());
+
+		PostgreSQL::Test::Utils::system_log("pg_controldata",
+			$node->data_dir);
+
+		my $log = PostgreSQL::Test::Utils::slurp_file($node->logfile,
+			$node_loglocation);
+		unlike(
+			$log,
+			qr/page verification failed/,
+			"no checksum validation errors in primary log (outside WAL recovery)"
+		);
+		$node_loglocation = -s $node->logfile;
+	}
+
+	random_sleep();
+}
+
+# Make sure the node is running
+if (!$node->is_alive)
+{
+	$node->start;
+}
+
+# Testrun is over, ensure that data reads back as expected and perform a final
+# verification of the data checksum state.
+my $result =
+  $node->safe_psql('postgres', "SELECT count(*) FROM t WHERE a > 1");
+is($result, '100000', 'ensure data pages can be read back on primary');
+test_checksum_state($node, $data_checksum_state);
+
+# Perform one final pass over the logs and hunt for unexpected errors
+my $log =
+  PostgreSQL::Test::Utils::slurp_file($node->logfile, $node_loglocation);
+unlike(
+	$log,
+	qr/page verification failed/,
+	"no checksum validation errors in primary log");
+$node_loglocation = -s $node->logfile;
+
+$node->teardown_node;
+
+done_testing();
diff --git a/src/test/modules/test_checksums/t/007_pgbench_standby.pl b/src/test/modules/test_checksums/t/007_pgbench_standby.pl
new file mode 100644
index 00000000000..e8ed152f472
--- /dev/null
+++ b/src/test/modules/test_checksums/t/007_pgbench_standby.pl
@@ -0,0 +1,376 @@
+
+# Copyright (c) 2026, PostgreSQL Global Development Group
+
+# Test suite for testing enabling data checksums in an online cluster,
+# comprising of a primary and a replicated standby, with concurrent activity
+# via pgbench runs
+
+use strict;
+use warnings FATAL => 'all';
+
+use PostgreSQL::Test::Cluster;
+use PostgreSQL::Test::Utils;
+use Test::More;
+
+use FindBin;
+use lib $FindBin::RealBin;
+
+use DataChecksums::Utils;
+
+if (!$ENV{PG_TEST_EXTRA} || $ENV{PG_TEST_EXTRA} !~ /\bchecksum_extended\b/)
+{
+	plan skip_all => 'Extended checksum tests not enabled';
+}
+
+if ($ENV{enable_injection_points} ne 'yes')
+{
+	plan skip_all => 'Injection points not supported by this build';
+}
+
+my $node_primary_slot = 'physical_slot';
+my $node_primary_backup = 'primary_backup';
+my $node_primary;
+my $node_primary_loglocation = 0;
+my $node_standby_1;
+my $node_standby_1_loglocation = 0;
+
+# The number of full test iterations which will be performed. The exact number
+# of tests performed and the wall time taken is non-deterministic as the test
+# performs a lot of randomized actions, but 5 iterations will be a long test
+# run regardless.
+my $TEST_ITERATIONS = 5;
+
+# Variables which record the current state of the cluster
+my $data_checksum_state = 'off';
+
+my $pgbench_primary = undef;
+my $pgbench_standby = undef;
+
+# Start a pgbench run in the background against the server specified via the
+# port passed as parameter
+sub background_pgbench
+{
+	my ($port, $standby) = @_;
+
+	# Terminate any currently running pgbench process before continuing
+	$pgbench_primary->finish if $pgbench_primary;
+
+	# Randomize the number of pgbench clients a bit (range 1-16)
+	my $clients = 1 + int(rand(15));
+
+	my @cmd = ('pgbench', '-p', $port, '-T', '600', '-c', $clients);
+	# Randomize whether we spawn connections or not
+	push(@cmd, '-C') if (cointoss());
+	# If we run on a standby it needs to be a read-only benchmark
+	push(@cmd, '-S') if ($standby);
+	# Finally add the database name to use
+	push(@cmd, 'postgres');
+
+	$pgbench_primary = IPC::Run::start(
+		\@cmd,
+		'<' => '/dev/null',
+		'>' => '/dev/null',
+		'2>' => '/dev/null',
+		IPC::Run::timer($PostgreSQL::Test::Utils::timeout_default));
+}
+
+# Invert the state of data checksums in the cluster, if data checksums are on
+# then disable them and vice versa. Also performs proper validation of the
+# before and after state.
+sub flip_data_checksums
+{
+	# First, make sure the cluster is in the state we expect it to be
+	test_checksum_state($node_primary, $data_checksum_state);
+	test_checksum_state($node_standby_1, $data_checksum_state);
+
+	if ($data_checksum_state eq 'off')
+	{
+		# Coin-toss to see if we are injecting a retry due to a temptable
+		$node_primary->safe_psql('postgres', 'SELECT dcw_fake_temptable();')
+		  if cointoss();
+
+		# log LSN right before we start changing checksums
+		my $result =
+		  $node_primary->safe_psql('postgres', "SELECT pg_current_wal_lsn()");
+		note("LSN before enabling: " . $result . "\n");
+
+		# Ensure that the primary switches to "inprogress-on"
+		enable_data_checksums($node_primary, wait => 'inprogress-on');
+
+		random_sleep();
+
+		# Wait for checksum enable to be replayed
+		$node_primary->wait_for_catchup($node_standby_1, 'replay');
+
+		# Ensure that the standby has switched to "inprogress-on" or "on".
+		# Normally it would be "inprogress-on", but it is theoretically
+		# possible for the primary to complete the checksum enabling *and* have
+		# the standby replay that record before we reach the check below.
+		$result = $node_standby_1->poll_query_until(
+			'postgres',
+			"SELECT setting = 'off' "
+			  . "FROM pg_catalog.pg_settings "
+			  . "WHERE name = 'data_checksums';",
+			'f');
+		is($result, 1,
+			'ensure standby has absorbed the inprogress-on barrier');
+		$result = $node_standby_1->safe_psql('postgres',
+				"SELECT setting "
+			  . "FROM pg_catalog.pg_settings "
+			  . "WHERE name = 'data_checksums';");
+
+		is(($result eq 'inprogress-on' || $result eq 'on'),
+			1, 'ensure checksums are on, or in progress, on standby_1');
+
+		# Wait for checksums enabled on the primary and standby
+		wait_for_checksum_state($node_primary, 'on');
+
+		# log LSN right after the primary flips checksums to "on"
+		$result =
+		  $node_primary->safe_psql('postgres', "SELECT pg_current_wal_lsn()");
+		note("LSN after enabling: " . $result . "\n");
+
+		random_sleep();
+		wait_for_checksum_state($node_standby_1, 'on');
+
+		$node_primary->safe_psql('postgres',
+			'SELECT dcw_fake_temptable(false);');
+		$data_checksum_state = 'on';
+	}
+	elsif ($data_checksum_state eq 'on')
+	{
+		random_sleep();
+
+		# log LSN right before we start changing checksums
+		my $result =
+		  $node_primary->safe_psql('postgres', "SELECT pg_current_wal_lsn()");
+		note("LSN before disabling: " . $result . "\n");
+
+		disable_data_checksums($node_primary);
+		$node_primary->wait_for_catchup($node_standby_1, 'replay');
+
+		# Wait for checksums disabled on the primary and standby
+		wait_for_checksum_state($node_primary, 'off');
+		wait_for_checksum_state($node_standby_1, 'off');
+
+		# log LSN right after the primary flips checksums to "off"
+		$result =
+		  $node_primary->safe_psql('postgres', "SELECT pg_current_wal_lsn()");
+		note("LSN after disabling: " . $result . "\n");
+
+		random_sleep();
+		wait_for_checksum_state($node_standby_1, 'off');
+
+		$data_checksum_state = 'off';
+	}
+	else
+	{
+		# This should only happen due to programmer error when hacking on the
+		# test code, but since that might pass subtly we error out.
+		BAIL_OUT('data_checksum_state variable has invalid state:'
+				 . $data_checksum_state);
+	}
+}
+
+# Create and start a cluster with one primary and one standby node, and ensure
+# they are caught up and in sync.
+$node_primary = PostgreSQL::Test::Cluster->new('main');
+$node_primary->init(allows_streaming => 1, no_data_checksums => 1);
+# max_connections need to be bumped in order to accommodate for pgbench clients
+# and log_statement is dialled down since it otherwise will generate enormous
+# amounts of logging. Page verification failures are still logged.
+$node_primary->append_conf(
+	'postgresql.conf',
+	qq[
+max_connections = 30
+log_statement = none
+]);
+$node_primary->start;
+$node_primary->safe_psql('postgres', 'CREATE EXTENSION test_checksums;');
+# Create some content to have un-checksummed data in the cluster
+$node_primary->safe_psql('postgres',
+	"CREATE TABLE t AS SELECT generate_series(1, 100000) AS a;");
+$node_primary->safe_psql('postgres',
+	"SELECT pg_create_physical_replication_slot('$node_primary_slot');");
+$node_primary->backup($node_primary_backup);
+
+$node_standby_1 = PostgreSQL::Test::Cluster->new('standby_1');
+$node_standby_1->init_from_backup($node_primary, $node_primary_backup,
+	has_streaming => 1);
+$node_standby_1->append_conf(
+	'postgresql.conf', qq[
+primary_slot_name = '$node_primary_slot'
+]);
+$node_standby_1->start;
+
+# Initialize pgbench and wait for the objects to be created on the standby
+$node_primary->command_ok([ 'pgbench', '-i', '-s', '100', '-q', 'postgres' ]);
+$node_primary->wait_for_catchup($node_standby_1, 'replay');
+
+# Start the test suite with pgbench running on all nodes
+background_pgbench($node_standby_1->port, 1);
+background_pgbench($node_primary->port, 0);
+
+# Main test suite. This loop will start a pgbench run on the cluster and while
+# that's running flip the state of data checksums concurrently. It will then
+# randomly restart the cluster and then check for
+# the desired state.  The idea behind doing things randomly is to stress out
+# any timing related issues by subjecting the cluster for varied workloads.
+# A TODO is to generate a trace such that any test failure can be traced to
+# its order of operations for debugging.
+for (my $i = 0; $i < $TEST_ITERATIONS; $i++)
+{
+	note("iteration ", ($i + 1), " of ", $TEST_ITERATIONS);
+
+	if (!$node_primary->is_alive)
+	{
+		# start, to do recovery, and stop
+		$node_primary->start;
+		$node_primary->stop('fast');
+
+		# 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_primary->logfile,
+			$node_primary_loglocation);
+		unlike(
+			$log,
+			qr/page verification failed/,
+			"no checksum validation errors in primary log (during WAL recovery)"
+		);
+		$node_primary_loglocation = -s $node_primary->logfile;
+
+		# randomize the WAL size, to trigger checkpoints less/more often
+		my $sb = 64 + int(rand(960));
+		$node_primary->append_conf('postgresql.conf', qq[max_wal_size = $sb]);
+
+		note("changing primary max_wal_size to " . $sb);
+
+		$node_primary->start;
+
+		# Start a pgbench in the background against the primary
+		background_pgbench($node_primary->port, 0);
+	}
+
+	if (!$node_standby_1->is_alive)
+	{
+		$node_standby_1->start;
+		$node_standby_1->stop('fast');
+
+		# 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_standby_1->logfile,
+			$node_standby_1_loglocation);
+		unlike(
+			$log,
+			qr/page verification failed/,
+			"no checksum validation errors in standby_1 log (during WAL recovery)"
+		);
+		$node_standby_1_loglocation = -s $node_standby_1->logfile;
+
+		# randomize the WAL size, to trigger checkpoints less/more often
+		my $sb = 64 + int(rand(960));
+		$node_standby_1->append_conf('postgresql.conf',
+			qq[max_wal_size = $sb]);
+
+		note("changing standby max_wal_size to " . $sb);
+
+		$node_standby_1->start;
+
+		# Start a read-only pgbench in the background on the standby
+		background_pgbench($node_standby_1->port, 1);
+	}
+
+	$node_primary->safe_psql('postgres', "UPDATE t SET a = a + 1;");
+	$node_primary->wait_for_catchup($node_standby_1, 'write');
+
+	flip_data_checksums();
+	random_sleep();
+	my $result = $node_primary->safe_psql('postgres',
+		"SELECT count(*) FROM t WHERE a > 1");
+	is($result, '100000', 'ensure data pages can be read back on primary');
+	random_sleep();
+
+	# Potentially powercycle the cluster (the nodes independently)
+	# XXX should maybe try stopping nodes in the opposite order too?
+	if (cointoss())
+	{
+		$node_primary->stop(stopmode());
+
+		# print the contents of the control file on the primary
+		PostgreSQL::Test::Utils::system_log("pg_controldata",
+			$node_primary->data_dir);
+
+		# slurp the file after shutdown, so that it doesn't interfere with the recovery
+		my $log = PostgreSQL::Test::Utils::slurp_file($node_primary->logfile,
+			$node_primary_loglocation);
+		unlike(
+			$log,
+			qr/page verification failed/,
+			"no checksum validation errors in primary log (outside WAL recovery)"
+		);
+		$node_primary_loglocation = -s $node_primary->logfile;
+	}
+
+	random_sleep();
+
+	if (cointoss())
+	{
+		$node_standby_1->stop(stopmode());
+
+		# print the contents of the control file on the standby
+		PostgreSQL::Test::Utils::system_log("pg_controldata",
+			$node_standby_1->data_dir);
+
+		# slurp the file after shutdown, so that it doesn't interfere with the recovery
+		my $log =
+		  PostgreSQL::Test::Utils::slurp_file($node_standby_1->logfile,
+			$node_standby_1_loglocation);
+		unlike(
+			$log,
+			qr/page verification failed/,
+			"no checksum validation errors in standby_1 log (outside WAL recovery)"
+		);
+		$node_standby_1_loglocation = -s $node_standby_1->logfile;
+	}
+}
+
+# make sure the nodes are running
+if (!$node_primary->is_alive)
+{
+	$node_primary->start;
+}
+
+if (!$node_standby_1->is_alive)
+{
+	$node_standby_1->start;
+}
+
+# Testrun is over, ensure that data reads back as expected and perform a final
+# verification of the data checksum state.
+my $result =
+  $node_primary->safe_psql('postgres', "SELECT count(*) FROM t WHERE a > 1");
+is($result, '100000', 'ensure data pages can be read back on primary');
+test_checksum_state($node_primary, $data_checksum_state);
+test_checksum_state($node_standby_1, $data_checksum_state);
+
+# Perform one final pass over the logs and hunt for unexpected errors
+my $log = PostgreSQL::Test::Utils::slurp_file($node_primary->logfile,
+	$node_primary_loglocation);
+unlike(
+	$log,
+	qr/page verification failed/,
+	"no checksum validation errors in primary log");
+$node_primary_loglocation = -s $node_primary->logfile;
+$log = PostgreSQL::Test::Utils::slurp_file($node_standby_1->logfile,
+	$node_standby_1_loglocation);
+unlike(
+	$log,
+	qr/page verification failed/,
+	"no checksum validation errors in standby_1 log");
+$node_standby_1_loglocation = -s $node_standby_1->logfile;
+
+$node_standby_1->teardown_node;
+$node_primary->teardown_node;
+
+done_testing();
diff --git a/src/test/modules/test_checksums/t/DataChecksums/Utils.pm b/src/test/modules/test_checksums/t/DataChecksums/Utils.pm
new file mode 100644
index 00000000000..d299a1ba01d
--- /dev/null
+++ b/src/test/modules/test_checksums/t/DataChecksums/Utils.pm
@@ -0,0 +1,261 @@
+
+# Copyright (c) 2026, PostgreSQL Global Development Group
+
+=pod
+
+=head1 NAME
+
+DataChecksums::Utils - Utility functions for testing data checksums in a running cluster
+
+=head1 SYNOPSIS
+
+  use PostgreSQL::Test::Cluster;
+  use DataChecksums::Utils qw( .. );
+
+  # Create, and start, a new cluster
+  my $node = PostgreSQL::Test::Cluster->new('primary');
+  $node->init;
+  $node->start;
+
+  test_checksum_state($node, 'off');
+
+  enable_data_checksums($node);
+
+  wait_for_checksum_state($node, 'on');
+
+
+=cut
+
+package DataChecksums::Utils;
+
+use strict;
+use warnings FATAL => 'all';
+use Exporter 'import';
+use PostgreSQL::Test::Cluster;
+use PostgreSQL::Test::Utils;
+use Test::More;
+
+our @EXPORT = qw(
+  cointoss
+  disable_data_checksums
+  enable_data_checksums
+  random_sleep
+  stopmode
+  test_checksum_state
+  wait_for_checksum_state
+  wait_for_cluster_crash
+);
+
+=pod
+
+=head1 METHODS
+
+=over
+
+=item test_checksum_state(node, state)
+
+Test that the current value of the data checksum GUC in the server running
+at B<node> matches B<state>.  If the values differ, a test failure is logged.
+Returns True if the values match, otherwise False.
+
+=cut
+
+sub test_checksum_state
+{
+	my ($postgresnode, $state) = @_;
+
+	my $result = $postgresnode->safe_psql('postgres',
+		"SELECT setting FROM pg_catalog.pg_settings WHERE name = 'data_checksums';"
+	);
+	is($result, $state, 'ensure checksums are set to ' . $state);
+	return $result eq $state;
+}
+
+=item wait_for_checksum_state(node, state)
+
+Test the value of the data checksum GUC in the server running at B<node>
+repeatedly until it matches B<state> or times out.  Processing will run for
+$PostgreSQL::Test::Utils::timeout_default seconds before timing out.  If the
+values differ when the process times out, False is returned and a test failure
+is logged, otherwise True.
+
+=cut
+
+sub wait_for_checksum_state
+{
+	my ($postgresnode, $state) = @_;
+
+	my $res = $postgresnode->poll_query_until(
+		'postgres',
+		"SELECT setting FROM pg_catalog.pg_settings WHERE name = 'data_checksums';",
+		$state);
+	is($res, 1, 'ensure data checksums are transitioned to ' . $state);
+	return $res == 1;
+}
+
+=item wait_for_cluster_crash(node, params)
+
+Repeatedly test if the cluster running at B<node> for responds to connections
+and return when it no longer does so, or when it times out.  Processing will
+run for $PostgreSQL::Test::Utils::timeout_default seconds unless a timeout
+value is specified as a parameter.  Returns True if the cluster crashed, else
+False if the process timed out.
+
+=over
+
+=item timeout
+
+Approximate number of seconds to wait for cluster to crash, default is
+$PostgreSQL::Test::Utils::timeout_default.  There are no real-time guarantee
+that the total process time won't exceed the timeout.
+
+=back
+
+=cut
+
+sub wait_for_cluster_crash
+{
+	my $postgresnode = shift;
+	my %params = @_;
+	my $crash = 0;
+
+	$params{timeout} = $PostgreSQL::Test::Utils::timeout_default
+	  unless (defined($params{timeout}));
+
+	for (my $naps = 0; $naps < $params{timeout}; $naps++)
+	{
+		if (!$postgresnode->is_alive)
+		{
+			$crash = 1;
+			last;
+		}
+		sleep(1);
+	}
+
+	return $crash == 1;
+}
+
+=item enable_data_checksums($node, %params)
+
+Function for enabling data checksums in the cluster running at B<node>.
+
+=over
+
+=item cost_delay
+
+The B<cost_delay> to use when enabling data checksums, default is 0.
+
+=item cost_limit
+
+The B<cost_limit> to use when enabling data checksums, default is 100.
+
+=item wait
+
+If defined, the function will wait for the state defined in this parameter,
+waiting timing out, before returning.  The function will wait for
+$PostgreSQL::Test::Utils::timeout_default seconds before timing out.
+
+=back
+
+=cut
+
+sub enable_data_checksums
+{
+	my $postgresnode = shift;
+	my %params = @_;
+
+	# Set sane defaults for the parameters
+	$params{cost_delay} = 0 unless (defined($params{cost_delay}));
+	$params{cost_limit} = 100 unless (defined($params{cost_limit}));
+
+	my $query = <<'EOQ';
+SELECT pg_enable_data_checksums(%s, %s);
+EOQ
+
+	$postgresnode->safe_psql('postgres',
+		sprintf($query, $params{cost_delay}, $params{cost_limit}));
+
+	wait_for_checksum_state($postgresnode, $params{wait})
+	  if (defined($params{wait}));
+}
+
+=item disable_data_checksums($node, %params)
+
+Function for disabling data checksums in the cluster running at B<node>.
+
+=over
+
+=item wait
+
+If defined, the function will wait for the state to turn to B<off>, or
+waiting timing out, before returning.  The function will wait for
+$PostgreSQL::Test::Utils::timeout_default seconds before timing out.
+Unlike in C<enable_data_checksums> the value of the parameter is discarded.
+
+=back
+
+=cut
+
+sub disable_data_checksums
+{
+	my $postgresnode = shift;
+	my %params = @_;
+
+	$postgresnode->safe_psql('postgres',
+		'SELECT pg_disable_data_checksums();');
+
+	wait_for_checksum_state($postgresnode, 'off') if (defined($params{wait}));
+}
+
+=item cointoss
+
+Helper for retrieving a binary value with random distribution for deciding
+whether to turn things off during testing.
+
+=back
+
+=cut
+
+sub cointoss
+{
+	return int(rand() < 0.5);
+}
+
+=item random_sleep(max)
+
+Helper for injecting random sleeps here and there in the testrun. The sleep
+duration will be in the range (0,B<max>), but won't be predictable in order to
+avoid sleep patterns that manage to avoid race conditions and timing bugs.
+The default B<max> is 3 seconds.
+
+=back
+
+=cut
+
+sub random_sleep
+{
+	my $max = shift;
+	sleep(int(rand(defined($max) ? $max : 3))) if cointoss;
+}
+
+=item stopmode
+
+Small helper function for randomly selecting a valid stopmode.
+
+=back
+
+=cut
+
+sub stopmode
+{
+	return 'immediate' if (cointoss);
+	return 'fast';
+}
+
+=pod
+
+=back
+
+=cut
+
+1;
diff --git a/src/test/modules/test_checksums/test_checksums--1.0.sql b/src/test/modules/test_checksums/test_checksums--1.0.sql
new file mode 100644
index 00000000000..fb1c802461b
--- /dev/null
+++ b/src/test/modules/test_checksums/test_checksums--1.0.sql
@@ -0,0 +1,28 @@
+/* src/test/modules/test_checksums/test_checksums--1.0.sql */
+
+-- complain if script is sourced in psql, rather than via CREATE EXTENSION
+\echo Use "CREATE EXTENSION test_checksums" to load this file. \quit
+
+CREATE FUNCTION dcw_inject_delay_barrier(attach boolean DEFAULT true)
+	RETURNS pg_catalog.void
+	AS 'MODULE_PATHNAME' LANGUAGE C;
+
+CREATE FUNCTION dcw_inject_launcher_delay(attach boolean DEFAULT true)
+	RETURNS pg_catalog.void
+	AS 'MODULE_PATHNAME' LANGUAGE C;
+
+CREATE FUNCTION dcw_inject_startup_delay(attach boolean DEFAULT true)
+	RETURNS pg_catalog.void
+	AS 'MODULE_PATHNAME' LANGUAGE C;
+
+CREATE FUNCTION dcw_inject_fail_database(attach boolean DEFAULT true)
+	RETURNS pg_catalog.void
+	AS 'MODULE_PATHNAME' LANGUAGE C;
+
+CREATE FUNCTION dcw_prune_dblist(attach boolean DEFAULT true)
+	RETURNS pg_catalog.void
+	AS 'MODULE_PATHNAME' LANGUAGE C;
+
+CREATE FUNCTION dcw_fake_temptable(attach boolean DEFAULT true)
+	RETURNS pg_catalog.void
+	AS 'MODULE_PATHNAME' LANGUAGE C;
diff --git a/src/test/modules/test_checksums/test_checksums.c b/src/test/modules/test_checksums/test_checksums.c
new file mode 100644
index 00000000000..493dc29a1d7
--- /dev/null
+++ b/src/test/modules/test_checksums/test_checksums.c
@@ -0,0 +1,222 @@
+/*--------------------------------------------------------------------------
+ *
+ * test_checksums.c
+ *		Test data checksums
+ *
+ * Copyright (c) 2026, PostgreSQL Global Development Group
+ *
+ * IDENTIFICATION
+ *		src/test/modules/test_checksums/test_checksums.c
+ *
+ * -------------------------------------------------------------------------
+ */
+#include "postgres.h"
+
+#include "funcapi.h"
+#include "miscadmin.h"
+#include "postmaster/datachecksumsworker.h"
+#include "storage/latch.h"
+#include "utils/injection_point.h"
+#include "utils/wait_event.h"
+
+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_dblist(const char *name, const void *private_data, void *arg);
+extern PGDLLEXPORT void dc_fake_temptable(const char *name, const void *private_data, void *arg);
+
+extern PGDLLEXPORT void crash(const char *name, const void *private_data, void *arg);
+
+/*
+ * Test for delaying emission of procsignalbarriers.
+ */
+void
+dc_delay_barrier(const char *name, const void *private_data, void *arg)
+{
+	(void) name;
+	(void) private_data;
+
+	(void) WaitLatch(MyLatch,
+					 WL_LATCH_SET | WL_TIMEOUT | WL_EXIT_ON_PM_DEATH,
+					 (3 * 1000),
+					 WAIT_EVENT_PG_SLEEP);
+}
+
+PG_FUNCTION_INFO_V1(dcw_inject_delay_barrier);
+Datum
+dcw_inject_delay_barrier(PG_FUNCTION_ARGS)
+{
+#ifdef USE_INJECTION_POINTS
+	bool		attach = PG_GETARG_BOOL(0);
+
+	if (attach)
+		InjectionPointAttach("datachecksums-enable-checksums-delay",
+							 "test_checksums",
+							 "dc_delay_barrier",
+							 NULL,
+							 0);
+	else
+		InjectionPointDetach("datachecksums-enable-checksums-delay");
+#else
+	elog(ERROR,
+		 "test is not working as intended when injection points are disabled");
+#endif
+	PG_RETURN_VOID();
+}
+
+PG_FUNCTION_INFO_V1(dcw_inject_launcher_delay);
+Datum
+dcw_inject_launcher_delay(PG_FUNCTION_ARGS)
+{
+#ifdef USE_INJECTION_POINTS
+	bool		attach = PG_GETARG_BOOL(0);
+
+	if (attach)
+		InjectionPointAttach("datachecksumsworker-launcher-delay",
+							 "test_checksums",
+							 "dc_delay_barrier",
+							 NULL,
+							 0);
+	else
+		InjectionPointDetach("datachecksumsworker-launcher-delay");
+#else
+	elog(ERROR,
+		 "test is not working as intended when injection points are disabled");
+#endif
+	PG_RETURN_VOID();
+}
+
+PG_FUNCTION_INFO_V1(dcw_inject_startup_delay);
+Datum
+dcw_inject_startup_delay(PG_FUNCTION_ARGS)
+{
+#ifdef USE_INJECTION_POINTS
+	bool		attach = PG_GETARG_BOOL(0);
+
+	if (attach)
+		InjectionPointAttach("datachecksumsworker-startup-delay",
+							 "test_checksums",
+							 "dc_delay_barrier",
+							 NULL,
+							 0);
+	else
+		InjectionPointDetach("datachecksumsworker-startup-delay");
+#else
+	elog(ERROR,
+		 "test is not working as intended when injection points are disabled");
+#endif
+	PG_RETURN_VOID();
+}
+
+void
+dc_fail_database(const char *name, const void *private_data, void *arg)
+{
+	static bool first_pass = true;
+	DataChecksumsWorkerResult *res = (DataChecksumsWorkerResult *) arg;
+
+	if (first_pass)
+		*res = DATACHECKSUMSWORKER_FAILED;
+	first_pass = false;
+}
+
+PG_FUNCTION_INFO_V1(dcw_inject_fail_database);
+Datum
+dcw_inject_fail_database(PG_FUNCTION_ARGS)
+{
+#ifdef USE_INJECTION_POINTS
+	bool		attach = PG_GETARG_BOOL(0);
+
+	if (attach)
+		InjectionPointAttach("datachecksumsworker-fail-db",
+							 "test_checksums",
+							 "dc_fail_database",
+							 NULL,
+							 0);
+	else
+		InjectionPointDetach("datachecksumsworker-fail-db");
+#else
+	elog(ERROR,
+		 "test is not working as intended when injection points are disabled");
+#endif
+	PG_RETURN_VOID();
+}
+
+/*
+ * 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)
+{
+	static bool first_pass = true;
+	List	   *DatabaseList = (List *) arg;
+
+	if (first_pass)
+		DatabaseList = list_delete_last(DatabaseList);
+	first_pass = false;
+}
+
+PG_FUNCTION_INFO_V1(dcw_prune_dblist);
+Datum
+dcw_prune_dblist(PG_FUNCTION_ARGS)
+{
+#ifdef USE_INJECTION_POINTS
+	bool		attach = PG_GETARG_BOOL(0);
+
+	if (attach)
+		InjectionPointAttach("datachecksumsworker-initial-dblist",
+							 "test_checksums",
+							 "dc_dblist",
+							 NULL,
+							 0);
+	else
+		InjectionPointDetach("datachecksumsworker-initial-dblist");
+#else
+	elog(ERROR,
+		 "test is not working as intended when injection points are disabled");
+#endif
+	PG_RETURN_VOID();
+}
+
+/*
+ * Test to force waiting for existing temptables.
+ */
+void
+dc_fake_temptable(const char *name, const void *private_data, void *arg)
+{
+	static bool first_pass = true;
+	int		   *numleft = (int *) arg;
+
+	if (first_pass)
+		*numleft = 1;
+	first_pass = false;
+}
+
+PG_FUNCTION_INFO_V1(dcw_fake_temptable);
+Datum
+dcw_fake_temptable(PG_FUNCTION_ARGS)
+{
+#ifdef USE_INJECTION_POINTS
+	bool		attach = PG_GETARG_BOOL(0);
+
+	if (attach)
+		InjectionPointAttach("datachecksumsworker-fake-temptable-wait",
+							 "test_checksums",
+							 "dc_fake_temptable",
+							 NULL,
+							 0);
+	else
+		InjectionPointDetach("datachecksumsworker-fake-temptable-wait");
+#else
+	elog(ERROR,
+		 "test is not working as intended when injection points are disabled");
+#endif
+	PG_RETURN_VOID();
+}
+
+void
+crash(const char *name, const void *private_data, void *arg)
+{
+	abort();
+}
diff --git a/src/test/modules/test_checksums/test_checksums.control b/src/test/modules/test_checksums/test_checksums.control
new file mode 100644
index 00000000000..84b4cc035a7
--- /dev/null
+++ b/src/test/modules/test_checksums/test_checksums.control
@@ -0,0 +1,4 @@
+comment = 'Test code for data checksums'
+default_version = '1.0'
+module_pathname = '$libdir/test_checksums'
+relocatable = true
diff --git a/src/test/perl/PostgreSQL/Test/Cluster.pm b/src/test/perl/PostgreSQL/Test/Cluster.pm
index e267ba868fe..8b3c4af3284 100644
--- a/src/test/perl/PostgreSQL/Test/Cluster.pm
+++ b/src/test/perl/PostgreSQL/Test/Cluster.pm
@@ -3863,6 +3863,42 @@ sub advance_wal
 	}
 }
 
+=item $node->checksum_enable_offline()
+
+Enable data page checksums in an offline cluster with B<pg_checksums>. The
+caller is responsible for ensuring that the cluster is in the right state for
+this operation.
+
+=cut
+
+sub checksum_enable_offline
+{
+	my ($self) = @_;
+
+	print "# Enabling checksums in \"$self->data_dir\"\n";
+	PostgreSQL::Test::Utils::system_or_bail('pg_checksums', '-D',
+		$self->data_dir, '-e');
+	return;
+}
+
+=item $node->checksum_disable_offline()
+
+Disable data page checksums in an offline cluster with B<pg_checksums>. The
+caller is responsible for ensuring that the cluster is in the right state for
+this operation.
+
+=cut
+
+sub checksum_disable_offline
+{
+	my ($self) = @_;
+
+	print "# Disabling checksums in \"$self->data_dir\"\n";
+	PostgreSQL::Test::Utils::system_or_bail('pg_checksums', '-D',
+		$self->data_dir, '-d');
+	return;
+}
+
 =pod
 
 =back
diff --git a/src/test/regress/expected/rules.out b/src/test/regress/expected/rules.out
index f4ee2bd7459..697e0329da4 100644
--- a/src/test/regress/expected/rules.out
+++ b/src/test/regress/expected/rules.out
@@ -2089,6 +2089,41 @@ pg_stat_progress_create_index| SELECT s.pid,
     s.param15 AS partitions_done
    FROM (pg_stat_get_progress_info('CREATE INDEX'::text) s(pid, datid, relid, param1, param2, param3, param4, param5, param6, param7, param8, param9, param10, param11, param12, param13, param14, param15, param16, param17, param18, param19, param20)
      LEFT JOIN pg_database d ON ((s.datid = d.oid)));
+pg_stat_progress_data_checksums| SELECT s.pid,
+    s.datid,
+    d.datname,
+        CASE s.param1
+            WHEN 0 THEN 'enabling'::text
+            WHEN 1 THEN 'disabling'::text
+            WHEN 2 THEN 'waiting on temporary tables'::text
+            WHEN 3 THEN 'waiting on barrier'::text
+            WHEN 4 THEN 'done'::text
+            ELSE NULL::text
+        END AS phase,
+        CASE s.param2
+            WHEN '-1'::integer THEN NULL::bigint
+            ELSE s.param2
+        END AS databases_total,
+    s.param3 AS databases_done,
+        CASE s.param4
+            WHEN '-1'::integer THEN NULL::bigint
+            ELSE s.param4
+        END AS relations_total,
+        CASE s.param5
+            WHEN '-1'::integer THEN NULL::bigint
+            ELSE s.param5
+        END AS relations_done,
+        CASE s.param6
+            WHEN '-1'::integer THEN NULL::bigint
+            ELSE s.param6
+        END AS blocks_total,
+        CASE s.param7
+            WHEN '-1'::integer THEN NULL::bigint
+            ELSE s.param7
+        END AS blocks_done
+   FROM (pg_stat_get_progress_info('DATACHECKSUMS'::text) s(pid, datid, relid, param1, param2, param3, param4, param5, param6, param7, param8, param9, param10, param11, param12, param13, param14, param15, param16, param17, param18, param19, param20)
+     LEFT JOIN pg_database d ON ((s.datid = d.oid)))
+  ORDER BY s.datid;
 pg_stat_progress_vacuum| SELECT s.pid,
     s.datid,
     d.datname,
diff --git a/src/test/regress/expected/stats.out b/src/test/regress/expected/stats.out
index cd00f35bf7a..aa86c11bd99 100644
--- a/src/test/regress/expected/stats.out
+++ b/src/test/regress/expected/stats.out
@@ -51,6 +51,22 @@ client backend|relation|vacuum
 client backend|temp relation|normal
 client backend|wal|init
 client backend|wal|normal
+datachecksum launcher|relation|bulkread
+datachecksum launcher|relation|bulkwrite
+datachecksum launcher|relation|init
+datachecksum launcher|relation|normal
+datachecksum launcher|relation|vacuum
+datachecksum launcher|temp relation|normal
+datachecksum launcher|wal|init
+datachecksum launcher|wal|normal
+datachecksum worker|relation|bulkread
+datachecksum worker|relation|bulkwrite
+datachecksum worker|relation|init
+datachecksum worker|relation|normal
+datachecksum worker|relation|vacuum
+datachecksum worker|temp relation|normal
+datachecksum worker|wal|init
+datachecksum worker|wal|normal
 io worker|relation|bulkread
 io worker|relation|bulkwrite
 io worker|relation|init
@@ -95,7 +111,7 @@ walsummarizer|wal|init
 walsummarizer|wal|normal
 walwriter|wal|init
 walwriter|wal|normal
-(79 rows)
+(95 rows)
 \a
 -- ensure that both seqscan and indexscan plans are allowed
 SET enable_seqscan TO on;
diff --git a/src/tools/pgindent/typedefs.list b/src/tools/pgindent/typedefs.list
index 9f5ee8fd482..f4cce543113 100644
--- a/src/tools/pgindent/typedefs.list
+++ b/src/tools/pgindent/typedefs.list
@@ -428,6 +428,8 @@ CheckPointStmt
 CheckpointStatsData
 CheckpointerRequest
 CheckpointerShmemStruct
+ChecksumBarrierCondition
+ChecksumType
 Chromosome
 CkptSortItem
 CkptTsStatus
@@ -598,6 +600,7 @@ CustomScan
 CustomScanMethods
 CustomScanState
 CycleCtr
+DataChecksumsWorkerOperation
 DBState
 DCHCacheEntry
 DEADLOCK_INFO
@@ -621,6 +624,10 @@ DataPageDeleteStack
 DataTypesUsageChecks
 DataTypesUsageVersionCheck
 DatabaseInfo
+DataChecksumsWorkerDatabase
+DataChecksumsWorkerResult
+DataChecksumsWorkerResultEntry
+DataChecksumsWorkerShmemStruct
 DateADT
 DateTimeErrorExtra
 Datum
@@ -4314,6 +4321,8 @@ xl_btree_split
 xl_btree_unlink_page
 xl_btree_update
 xl_btree_vacuum
+xl_checkpoint_redo
+xl_checksum_state
 xl_clog_truncate
 xl_commit_ts_truncate
 xl_dbase_create_file_copy_rec
-- 
2.39.3 (Apple Git-146)

