From ebc527e0c6e98f25420529cf220bfa2551c4de60 Mon Sep 17 00:00:00 2001
From: Paul Kim <mok03127@gmail.com>
Date: Sat, 12 Sep 2026 21:02:26 +0900
Subject: [PATCH v3 2/2] Add a TAP test for the WAL insertion adjustment in
 XLogBackgroundFlush

The failure reproduces without any fault injection: pad WAL so that an
xlog-switch record starts exactly SizeOfXLogRecord bytes before a
segment boundary.  The switch's overridden EndPos then points past the
next segment's long page header, and since pg_switch_wal() allocates
no XID, the surrounding commit hands that position to
XLogSetAsyncXactLSN() even with synchronous_commit = on.  The
walwriter's next cycle requests a flush past the end of reserved WAL.

The test verifies that the walwriter survives the request and that the
advertised flush position does not advance past the end of reserved
WAL until real WAL is generated.
---
 src/test/modules/test_misc/meson.build        |   1 +
 .../test_misc/t/016_walwriter_flush_adjust.pl | 135 ++++++++++++++++++
 2 files changed, 136 insertions(+)
 create mode 100644 src/test/modules/test_misc/t/016_walwriter_flush_adjust.pl

diff --git a/src/test/modules/test_misc/meson.build b/src/test/modules/test_misc/meson.build
index 5d81f5b13be..3b083025a4c 100644
--- a/src/test/modules/test_misc/meson.build
+++ b/src/test/modules/test_misc/meson.build
@@ -24,6 +24,7 @@ tests += {
       't/013_temp_obj_multisession.pl',
       't/014_log_statement_max_length.pl',
       't/015_temp_schema_exit_deferrable.pl',
+      't/016_walwriter_flush_adjust.pl',
     ],
     # The injection points are cluster-wide, so disable installcheck
     'runningcheck': false,
diff --git a/src/test/modules/test_misc/t/016_walwriter_flush_adjust.pl b/src/test/modules/test_misc/t/016_walwriter_flush_adjust.pl
new file mode 100644
index 00000000000..f670ce04b3d
--- /dev/null
+++ b/src/test/modules/test_misc/t/016_walwriter_flush_adjust.pl
@@ -0,0 +1,135 @@
+# Copyright (c) 2026, PostgreSQL Global Development Group
+
+# Test that XLogBackgroundFlush() honors the adjustment applied by
+# WaitXLogInsertionsToFinish() when the flush request is past the end
+# of generated WAL.
+#
+# Such a request arises naturally when an xlog-switch record starts
+# exactly SizeOfXLogRecord bytes before a segment boundary: the
+# overridden EndPos (segment boundary + SizeOfXLogLongPHD) reaches
+# XLogSetAsyncXactLSN() via XactLastRecEnd, because the transaction
+# around pg_switch_wal() has no XID and therefore commits
+# asynchronously.  Without the fix, the walwriter fails on one of
+# XLogWrite()'s sanity checks (which one fires first depends on the
+# WAL buffer state) and takes the server down, in both assert and
+# production builds.
+
+use strict;
+use warnings FATAL => 'all';
+use PostgreSQL::Test::Cluster;
+use PostgreSQL::Test::Utils;
+use Test::More;
+
+my $node = PostgreSQL::Test::Cluster->new('primary');
+
+# Small segments keep the padding cheap.
+$node->init(extra => ['--wal-segsize', '1']);
+$node->append_conf(
+	'postgresql.conf', qq(
+autovacuum = off
+wal_writer_delay = 10ms
+wal_writer_flush_after = 0
+));
+$node->start;
+
+# Create a table for the later INSERT.
+$node->safe_psql('postgres', 'CREATE TABLE t AS SELECT 1 AS i');
+
+# Pad WAL with logical messages until the insert position is exactly
+# SizeOfXLogRecord (24) bytes before a segment boundary, then switch.
+# The record sizes involved are all MAXALIGNed, so filling the exact
+# gap is possible; a record that crosses a page boundary picks up an
+# extra page header and overshoots, in which case the loop moves on to
+# the next segment boundary and tries again.
+my $pad_and_switch = q{
+DO $$
+DECLARE
+    segsz  numeric := (SELECT setting::numeric FROM pg_settings
+                       WHERE name = 'wal_segment_size');
+    zero   pg_lsn := '0/0';
+    base   numeric;
+    cur    numeric;
+    target numeric;
+    gap    numeric;
+    seg0   numeric;
+BEGIN
+    -- size of a logical message record with an empty payload
+    cur := pg_current_wal_insert_lsn() - zero;
+    PERFORM pg_logical_emit_message(false, 'x', '');
+    base := (pg_current_wal_insert_lsn() - zero) - cur;
+
+    seg0 := floor((pg_current_wal_insert_lsn() - zero) / segsz);
+    LOOP
+        cur := pg_current_wal_insert_lsn() - zero;
+        target := (floor(cur / segsz) + 1) * segsz - 24;
+        gap := target - cur;
+        EXIT WHEN gap = 0;
+        IF floor(cur / segsz) - seg0 > 3 THEN
+            RAISE EXCEPTION 'could not align the insert position';
+        END IF;
+        IF gap >= base + 8192 THEN
+            PERFORM pg_logical_emit_message(false, 'x', repeat('a', 4096));
+        ELSIF gap >= base THEN
+            PERFORM pg_logical_emit_message(false, 'x',
+                                            repeat('a', (gap - base)::int));
+        ELSE
+            -- too close to the boundary, step over it and retry
+            PERFORM pg_logical_emit_message(false, 'x', '');
+        END IF;
+    END LOOP;
+END
+$$;
+SELECT pg_switch_wal() - '0/0'::pg_lsn;
+};
+
+my $segsz = 1024 * 1024;
+my $bogus;
+my $log_offset;
+
+# A concurrent WAL record (e.g. a bgwriter snapshot) between the
+# padding and the switch can spoil the alignment; the switch then does
+# not report the overridden position and we simply try again.
+foreach my $attempt (1 .. 10)
+{
+	$log_offset = -s $node->logfile;
+	my $off = $node->safe_psql('postgres', $pad_and_switch);
+
+	# SizeOfXLogLongPHD is 40 bytes: the switch record occupied the
+	# last 24 bytes of the segment, and pg_switch_wal() reported the
+	# overridden EndPos past the next segment's long page header.
+	if ($off % $segsz == 40)
+	{
+		$bogus = $off;
+		last;
+	}
+}
+die "could not hit the segment-boundary switch window"
+  unless defined $bogus;
+
+# The walwriter's next cycle picks up the bogus request and logs the
+# adjustment.
+$node->wait_for_log(qr/request to flush past end of generated WAL/,
+	$log_offset);
+
+# The advertised flush position must not include the bogus request.
+my $result = $node->safe_psql('postgres',
+	qq{SELECT pg_current_wal_flush_lsn() - '0/0'::pg_lsn < $bogus});
+is($result, 't', 'flush position stays below the bogus request');
+
+# The walwriter must not have failed one of XLogWrite()'s sanity
+# checks: no child process may have been terminated.
+my $log = slurp_file($node->logfile, $log_offset);
+unlike(
+	$log,
+	qr/terminating any other active server processes/,
+	'no crash after the bogus flush request');
+
+# Normal WAL activity gets past the bogus position.
+$node->safe_psql('postgres', 'INSERT INTO t VALUES (2)');
+$node->safe_psql('postgres', 'SELECT pg_switch_wal()');
+$result = $node->safe_psql('postgres',
+	qq{SELECT pg_current_wal_flush_lsn() - '0/0'::pg_lsn > $bogus});
+is($result, 't', 'flush position advances past the bogus request');
+
+$node->stop;
+done_testing();
-- 
2.50.1 (Apple Git-155)

