From 126665d60b81583019ab0c1f7799d1c4936316d6 Mon Sep 17 00:00:00 2001
From: snppg <slpmcf@gmail.com>
Date: Wed, 22 Jul 2026 19:26:03 +0700
Subject: [PATCH v1] Fix archive restore race that could unlink WAL before
 rename.

---
 src/backend/access/transam/xlogarchive.c      | 11 +++--
 src/test/recovery/meson.build                 |  1 +
 .../t/055_archive_restore_walsender_race.pl   | 42 +++++++++++++++++++
 3 files changed, 48 insertions(+), 6 deletions(-)
 create mode 100644 src/test/recovery/t/055_archive_restore_walsender_race.pl

diff --git a/src/backend/access/transam/xlogarchive.c b/src/backend/access/transam/xlogarchive.c
index 9a0c8097cb1..4e81eab85e5 100644
--- a/src/backend/access/transam/xlogarchive.c
+++ b/src/backend/access/transam/xlogarchive.c
@@ -31,6 +31,7 @@
 #include "replication/walsender.h"
 #include "storage/fd.h"
 #include "storage/ipc.h"
+#include "utils/injection_point.h"
 #include "utils/wait_event.h"
 
 /*
@@ -366,9 +367,8 @@ KeepFileRestoredFromArchive(const char *path, const char *xlogfname)
 
 	if (stat(xlogfpath, &statbuf) == 0)
 	{
-		char		oldpath[MAXPGPATH];
-
 #ifdef WIN32
+		char		oldpath[MAXPGPATH];
 		static unsigned int deletedcounter = 1;
 
 		/*
@@ -390,18 +390,17 @@ KeepFileRestoredFromArchive(const char *path, const char *xlogfname)
 					 errmsg("could not rename file \"%s\" to \"%s\": %m",
 							xlogfpath, oldpath)));
 		}
-#else
-		/* same-size buffers, so this never truncates */
-		strlcpy(oldpath, xlogfpath, MAXPGPATH);
-#endif
 		if (unlink(oldpath) != 0)
 			ereport(FATAL,
 					(errcode_for_file_access(),
 					 errmsg("could not remove file \"%s\": %m",
 							xlogfpath)));
+#endif
 		reload = true;
 	}
 
+	INJECTION_POINT("keepfile-restored-before-rename", NULL);
+
 	durable_rename(path, xlogfpath, ERROR);
 
 	/*
diff --git a/src/test/recovery/meson.build b/src/test/recovery/meson.build
index ad0d85f4189..41a239f5a59 100644
--- a/src/test/recovery/meson.build
+++ b/src/test/recovery/meson.build
@@ -63,6 +63,7 @@ tests += {
       't/052_checkpoint_segment_missing.pl',
       't/053_standby_login_event_trigger.pl',
       't/054_unlogged_sequence_promotion.pl',
+      't/055_archive_restore_walsender_race.pl',
     ],
   },
 }
diff --git a/src/test/recovery/t/055_archive_restore_walsender_race.pl b/src/test/recovery/t/055_archive_restore_walsender_race.pl
new file mode 100644
index 00000000000..c77fca449b5
--- /dev/null
+++ b/src/test/recovery/t/055_archive_restore_walsender_race.pl
@@ -0,0 +1,42 @@
+use strict;
+use warnings FATAL => 'all';
+use PostgreSQL::Test::Cluster;
+use PostgreSQL::Test::Utils;
+use Test::More;
+
+plan skip_all => 'injection points required'
+  unless $ENV{enable_injection_points} eq 'yes';
+
+my $inj = 'keepfile-restored-before-rename';
+my $gate = "$PostgreSQL::Test::Utils::tmp_check/g$$";
+
+my $p = PostgreSQL::Test::Cluster->new('p');
+$p->init(
+	has_archiving => 1, allows_streaming => 1,
+	extra => ['--wal-segsize', '1']);
+$p->append_conf('postgresql.conf',
+	"shared_preload_libraries = 'injection_points'");
+$p->start;
+$p->safe_psql('postgres', 'CREATE EXTENSION injection_points');
+$p->advance_wal(2);
+$p->backup('b');
+$p->advance_wal(3);
+
+my $a = $p->archive_dir;
+my $w = (sort grep { /^[0-9A-F]{24}$/ } slurp_dir($a))[-1];
+my $prev = substr($w, 0, 16) . sprintf('%08X', hex(substr($w, 16, 8)) - 1);
+my $s = PostgreSQL::Test::Cluster->new('s');
+$s->init_from_backup($p, 'b', has_restoring => 1);
+system('cp', "$a/$w", $s->data_dir . "/pg_wal/$w") == 0 or die $!;
+$s->append_conf('postgresql.conf', qq{
+recovery_prefetch = off
+shared_preload_libraries = 'injection_points'
+restore_command = 'case "%f" in $w) while [ ! -f $gate ]; do sleep 0.01; done ;; esac; cp "$a/%f" "%p"'
+});
+$s->start;
+$s->wait_for_log(qr/restored log file "\Q$prev\E" from archive/);
+$s->safe_psql('postgres', "SELECT injection_points_attach('$inj','wait')");
+system('touch', $gate);
+$s->wait_for_event('startup', $inj);
+ok(-e $s->data_dir . "/pg_wal/$w");
+done_testing();
-- 
2.55.0

