Subject: Bump soft open file limit (RLIMIT_NOFILE) to hard limit on startup

From: Jelte Fennema-Nio <[email-masked]>
Date: Tue, 11 Feb 2025 19:52:34 +0100

The default open file limit of 1024 is extremely low, given modern
resources and kernel architectures. The reason that this hasn't changed
change is because doing so would break legacy programs that use the
select(2) system call in hard to debug ways. So instead programs that
want to opt-in to a higher open file limit are expected to bump their
soft limit to their hard limit on startup. Details on this are very well
explained in a blogpost by the systemd author[1]. There's also a similar
change done by the Go language[2].

So this starts bumping postmaster and pgbench its soft open file limit
to the hard open file limit. Doing so is especially useful for the AIO
work that Andres is doing, because io_uring consumes a lot of file
descriptors. But even without looking at AIO there is a large number of
reports from people that require changing their soft file limit before
starting postgres, sometimes falling back to lowering
max_files_per_process when they fail to do so[3-8]. It's also not all
that strange to fail at setting the soft open file limit because there
are multiple places where one can configure such limits and usually only
one of them is effective (which one depends on how Postgres is started).

[1]: https://0pointer.net/blog/file-descriptor-limits.html
[2]: https://github.com/golang/go/issues/46279
[3]: https://serverfault.com/questions/785330/getting-too-many-open-files-error-for-postgres
[4]: https://serverfault.com/questions/716982/how-to-raise-max-no-of-file-descriptors-for-daemons-running-on-debian-jessie
[5]: https://www.postgresql.org/message-id/flat/CAKtc8vXh7NvP_qWj8EqqorPY97bvxSaX3h5u7a9PptRFHW5x7g%40mail.gmail.com
[6]: https://www.postgresql.org/message-id/flat/113ce31b0908120955w77029099i7ececc053084095a%40mail.gmail.com
[7]: https://github.com/abiosoft/colima/discussions/836
[8]: https://www.postgresql.org/message-id/flat/29663.1007738957%40sss.pgh.pa.us#2079ec9e2d8b251593812a3711bfe9e9
From ecf7fc5f34b4e025eaa4dda809874f18dfdb24b4 Mon Sep 17 00:00:00 2001
From: Jelte Fennema-Nio <[email-masked]>
Date: Tue, 11 Feb 2025 19:15:36 +0100
Subject: [PATCH v1] Bump soft open file limit (RLIMIT_NOFILE) to hard limit on
 startup

The default open file limit of 1024 is extremely low, given modern
resources and kernel architectures. The reason that this hasn't changed
change is because doing so would break legacy programs that use the
select(2) system call in hard to debug ways. So instead programs that
want to opt-in to a higher open file limit are expected to bump their
soft limit to their hard limit on startup. Details on this are very well
explained in a blogpost by the systemd author[1]. There's also a similar
change done by the Go language[2].

So this starts bumping postmaster and pgbench its soft open file limit
to the hard open file limit. Doing so is especially useful for the AIO
work that Andres is doing, because io_uring consumes a lot of file
descriptors. But even without looking at AIO there is a large number of
reports from people that require changing their soft file limit before
starting postgres, sometimes falling back to lowering
max_files_per_process when they fail to do so[3-8]. It's also not all
that strange to fail at setting the soft open file limit because there
are multiple places where one can configure such limits and usually only
one of them is effective (which one depends on how Postgres is started).

[1]: https://0pointer.net/blog/file-descriptor-limits.html
[2]: https://github.com/golang/go/issues/46279
[3]: https://serverfault.com/questions/785330/getting-too-many-open-files-error-for-postgres
[4]: https://serverfault.com/questions/716982/how-to-raise-max-no-of-file-descriptors-for-daemons-running-on-debian-jessie
[5]: https://www.postgresql.org/message-id/flat/CAKtc8vXh7NvP_qWj8EqqorPY97bvxSaX3h5u7a9PptRFHW5x7g%40mail.gmail.com
[6]: https://www.postgresql.org/message-id/flat/113ce31b0908120955w77029099i7ececc053084095a%40mail.gmail.com
[7]: https://github.com/abiosoft/colima/discussions/836
[8]: https://www.postgresql.org/message-id/flat/29663.1007738957%40sss.pgh.pa.us#2079ec9e2d8b251593812a3711bfe9e9
---
 src/backend/postmaster/postmaster.c | 22 ++++++++++++++++++++++
 src/bin/pgbench/pgbench.c           |  5 +++++
 2 files changed, 27 insertions(+)

diff --git a/src/backend/postmaster/postmaster.c b/src/backend/postmaster/postmaster.c
index bb22b13adef..3c2fbebfa62 100644
--- a/src/backend/postmaster/postmaster.c
+++ b/src/backend/postmaster/postmaster.c
@@ -72,6 +72,7 @@
 #include <ctype.h>
 #include <sys/stat.h>
 #include <sys/socket.h>
+#include <sys/resource.h>
 #include <fcntl.h>
 #include <sys/param.h>
 #include <netdb.h>
@@ -406,6 +407,8 @@ static DNSServiceRef bonjour_sdref = NULL;
 /*
  * postmaster.c - function prototypes
  */
+
+static void ConfigureFileLimit(void);
 static void CloseServerPorts(int status, Datum arg);
 static void unlink_external_pid_file(int status, Datum arg);
 static void getInstallationPaths(const char *argv0);
@@ -573,6 +576,8 @@ PostmasterMain(int argc, char *argv[])
 	/* Begin accepting signals. */
 	sigprocmask(SIG_SETMASK, &UnBlockSig, NULL);
 
+	ConfigureFileLimit();
+
 	/*
 	 * Options setup
 	 */
@@ -1393,6 +1398,23 @@ PostmasterMain(int argc, char *argv[])
 	abort();					/* not reached */
 }
 
+static void
+ConfigureFileLimit(void)
+{
+#ifdef HAVE_GETRLIMIT
+	struct rlimit rlim;
+
+	if (getrlimit(RLIMIT_NOFILE, &rlim) != 0)
+	{
+		elog(WARNING, "could not get open file limit: %m");
+		return;
+	}
+
+	rlim.rlim_cur = rlim.rlim_max;
+	if (setrlimit(RLIMIT_NOFILE, &rlim) != 0)
+		elog(WARNING, "could not set open file limit: %m");
+#endif
+}
 
 /*
  * on_proc_exit callback to close server's listen sockets
diff --git a/src/bin/pgbench/pgbench.c b/src/bin/pgbench/pgbench.c
index f303bdeec8d..161b231d591 100644
--- a/src/bin/pgbench/pgbench.c
+++ b/src/bin/pgbench/pgbench.c
@@ -6794,6 +6794,11 @@ main(int argc, char **argv)
 #ifdef HAVE_GETRLIMIT
 				if (getrlimit(RLIMIT_NOFILE, &rlim) == -1)
 					pg_fatal("getrlimit failed: %m");
+
+				rlim.rlim_cur = rlim.rlim_max;
+				if (setrlimit(RLIMIT_NOFILE, &rlim) == -1)
+					pg_fatal("getrlimit failed: %m");
+
 				if (rlim.rlim_cur < nclients + 3)
 				{
 					pg_log_error("need at least %d open files, but system limit is %ld",

base-commit: c366d2bdba7c3b9b2cca1429d4535866e231ca55
-- 
2.43.0

---

From: Andres Freund <[email-masked]>
Date: Tue, 11 Feb 2025 14:42:21 -0500

Hi,

On 2025-02-11 19:52:34 +0100, Jelte Fennema-Nio wrote:
> to the hard open file limit.

Not sure that's quite the right thing to do for postmaster. What I'd start
with is to increase the soft limit to
  "already used files" + max_files_per_process.

That way we still limit "resource" usage, but react better to already used
FDs. If io_uring, listen_addresses, whatnot use FDs
max_files_per_process would be added ontop.

Then we can separately discuss increasing max_files_per_process more
aggressively.

I don't see a downside to just increasing the soft limit for pgbench. It
avoids the stupid cycle of getting "need at least %d open files, but system
limit is %ld", increase ulimit, retry, without any non-theoretical downsides.

> Doing so is especially useful for the AIO work that Andres is doing, because
> io_uring consumes a lot of file descriptors.

Yep.

One more reason this is a good idea is that we'll also need this for
threading, since there all client connections obviously will eat into the
"normal file descriptor" budget.

Greetings,

Andres Freund

---

From: Tom Lane <[email-masked]>
Date: Tue, 11 Feb 2025 15:04:02 -0500

Andres Freund <[email-masked]> writes:
> I don't see a downside to just increasing the soft limit for
> pgbench.

Agreed, that end of the patch seems relatively harmless.

			regards, tom lane

---

From: "Jelte Fennema-Nio" <[email-masked]>
Date: Sun, 07 Dec 2025 16:55:17 +0100

>   "already used files" + max_files_per_process.

I needed to rebase this patch, and that made me finally take the time to
do the restoration of the file limit for subprocesses properly: In
previous versions of this patch it restored the limit before the call to
system() and it didn't restore it at all for popen. This latest version
the patch adds custom pg_system() and pg_popen() functions that restore
the limits in the child process right after the fork, but before the
exec.

There are two reasons to do this:
1. Any executables that still use select(2) will get clear "out of file
   descriptors" errors instead of failing in mysterious ways.
2. Future looking when we'll have multi-threading (which this change is
   needed for) it would be problematic to restore the original limit
   temporarily in the postgres process tree. Some other thread might
   want to open a file while the limit is too low. By only calling
   setrlimit with the lower value in the child process there's not a
   single moment where the original Postgres process has a too low limit.
From 699c49d1f75c143277007b6171c9499bfb6757fb Mon Sep 17 00:00:00 2001
From: Jelte Fennema-Nio <[email-masked]>
Date: Sun, 7 Dec 2025 15:07:08 +0100
Subject: [PATCH v9 1/2] Bump postmaster soft open file limit (RLIMIT_NOFILE)
 when necessary

The default open file limit of 1024 on Linux is extremely low. The
reason that this hasn't changed change is because doing so would break
legacy programs that use the select(2) system call in hard to debug
ways. So instead programs that want to opt-in to a higher open file
limit are expected to bump their soft limit to their hard limit on
startup. Details on this are very well explained in a blogpost by the
systemd author[1]. There's also a similar change done by the Go
language[2].

This starts bumping postmaster its soft open file limit when we realize
that we'll run into the soft limit with the requested
max_files_per_process GUC. We do so by slightly changing the meaning of
the max_files_per_process GUC. The actual (not publicly exposed) limit
is max_safe_fds, previously this would be set to:
max_files_per_process - already_open_files - NUM_RESERVED_FDS
After this change we now try to set max_safe_fds to
max_files_per_process if the system allows that. This is deemed more
natural to understand for users, because now the limit of files that
they can open is actually what they configured in max_files_per_process.

Adding this infrastructure to change RLIMIT_NOFILE when needed is
especially useful for the AIO work, because io_uring consumes a lot of
file descriptors. Even without looking at AIO there is a large number of
reports from people that require changing their soft file limit before
starting Postgres, sometimes falling back to lowering
max_files_per_process when they fail to do so[3-8].
It's also not all that strange to fail at setting the soft open file
limit because there are multiple places where one can configure such
limits and usually only one of them is effective (which one depends on
how Postgres is started). In cloud environments its also often not
possible for user to change the soft limit, because they don't control
the way that Postgres is started. Finally, for the multi-threading work
this is also very important because then the open file limit is not
per-backend anymore, but for the whole system.

The most complex change in this patch is how we shell out to other
systems executables. Instead of using system and popen directly we now
implement our own versions of these commands that restore the original
file limits before starting the other executables. This is done as a
precaution in case those executables are using select(2).

P.S. The initdb test needed to be updated because on our OpenBSD CI the
stderr would now contain:

LOG:  increased open file limit to 1004

[1]: https://0pointer.net/blog/file-descriptor-limits.html
[2]: https://github.com/golang/go/issues/46279
[3]: https://serverfault.com/questions/785330/getting-too-many-open-files-error-for-postgres
[4]: https://serverfault.com/questions/716982/how-to-raise-max-no-of-file-descriptors-for-daemons-running-on-debian-jessie
[5]: https://www.postgresql.org/message-id/flat/CAKtc8vXh7NvP_qWj8EqqorPY97bvxSaX3h5u7a9PptRFHW5x7g%40mail.gmail.com
[6]: https://www.postgresql.org/message-id/flat/113ce31b0908120955w77029099i7ececc053084095a%40mail.gmail.com
[7]: https://github.com/abiosoft/colima/discussions/836
[8]: https://www.postgresql.org/message-id/flat/29663.1007738957%40sss.pgh.pa.us#2079ec9e2d8b251593812a3711bfe9e9
---
 meson.build                              |   1 +
 src/backend/access/transam/xlogarchive.c |  18 +-
 src/backend/archive/shell_archive.c      |   3 +-
 src/backend/storage/file/fd.c            | 442 +++++++++++++++++++++--
 src/bin/initdb/t/001_initdb.pl           |   6 +-
 src/include/pg_config.h.in               |   3 +
 src/include/storage/fd.h                 |   1 +
 7 files changed, 437 insertions(+), 37 deletions(-)

diff --git a/meson.build b/meson.build
index 6e7ddd74683..ac778b87818 100644
--- a/meson.build
+++ b/meson.build
@@ -2891,6 +2891,7 @@ func_checks = [
   ['localeconv_l'],
   ['mbstowcs_l'],
   ['mkdtemp'],
+  ['pipe2'],
   ['posix_fadvise'],
   ['posix_fallocate'],
   ['ppoll'],
diff --git a/src/backend/access/transam/xlogarchive.c b/src/backend/access/transam/xlogarchive.c
index 1ef1713c91a..5a99ddc291e 100644
--- a/src/backend/access/transam/xlogarchive.c
+++ b/src/backend/access/transam/xlogarchive.c
@@ -158,10 +158,9 @@ RestoreArchivedFile(char *path, const char *xlogfname,
 			(errmsg_internal("executing restore command \"%s\"",
 							 xlogRestoreCmd)));
 
-	fflush(NULL);
-	pgstat_report_wait_start(WAIT_EVENT_RESTORE_COMMAND);
-
 	/*
+	 * Copy xlog from archival storage to XLOGDIR
+	 *
 	 * PreRestoreCommand() informs the SIGTERM handler for the startup process
 	 * that it should proc_exit() right away.  This is done for the duration
 	 * of the system() call because there isn't a good way to break out while
@@ -169,16 +168,13 @@ RestoreArchivedFile(char *path, const char *xlogfname,
 	 * it is best to put any additional logic before or after the
 	 * PreRestoreCommand()/PostRestoreCommand() section.
 	 */
+	fflush(NULL);
+	pgstat_report_wait_start(WAIT_EVENT_RESTORE_COMMAND);
 	PreRestoreCommand();
-
-	/*
-	 * Copy xlog from archival storage to XLOGDIR
-	 */
-	rc = system(xlogRestoreCmd);
-
+	rc = pg_system(xlogRestoreCmd);
 	PostRestoreCommand();
-
 	pgstat_report_wait_end();
+
 	pfree(xlogRestoreCmd);
 
 	if (rc == 0)
@@ -327,7 +323,7 @@ ExecuteRecoveryCommand(const char *command, const char *commandName,
 	 */
 	fflush(NULL);
 	pgstat_report_wait_start(wait_event_info);
-	rc = system(xlogRecoveryCmd);
+	rc = pg_system(xlogRecoveryCmd);
 	pgstat_report_wait_end();
 
 	pfree(xlogRecoveryCmd);
diff --git a/src/backend/archive/shell_archive.c b/src/backend/archive/shell_archive.c
index 828723afe47..beb81ee7d9d 100644
--- a/src/backend/archive/shell_archive.c
+++ b/src/backend/archive/shell_archive.c
@@ -22,6 +22,7 @@
 #include "archive/shell_archive.h"
 #include "common/percentrepl.h"
 #include "pgstat.h"
+#include "storage/fd.h"
 
 static bool shell_archive_configured(ArchiveModuleState *state);
 static bool shell_archive_file(ArchiveModuleState *state,
@@ -77,7 +78,7 @@ shell_archive_file(ArchiveModuleState *state, const char *file,
 
 	fflush(NULL);
 	pgstat_report_wait_start(WAIT_EVENT_ARCHIVE_COMMAND);
-	rc = system(xlogarchcmd);
+	rc = pg_system(xlogarchcmd);
 	pgstat_report_wait_end();
 
 	if (rc != 0)
diff --git a/src/backend/storage/file/fd.c b/src/backend/storage/file/fd.c
index e9eaaf9c829..40212d733e9 100644
--- a/src/backend/storage/file/fd.c
+++ b/src/backend/storage/file/fd.c
@@ -80,6 +80,7 @@
 #include <sys/types.h>
 #ifndef WIN32
 #include <sys/mman.h>
+#include <sys/wait.h>
 #endif
 #include <limits.h>
 #include <unistd.h>
@@ -91,6 +92,7 @@
 #include "common/file_perm.h"
 #include "common/file_utils.h"
 #include "common/pg_prng.h"
+#include "libpq/pqsignal.h"
 #include "miscadmin.h"
 #include "pgstat.h"
 #include "postmaster/startup.h"
@@ -158,6 +160,13 @@ int			max_files_per_process = 1000;
  */
 int			max_safe_fds = FD_MINFREE;	/* default if not changed */
 
+#ifdef HAVE_GETRLIMIT
+static bool saved_original_max_open_files;
+static struct rlimit original_max_open_files;
+static struct rlimit custom_max_open_files;
+#endif
+
+
 /* Whether it is safe to continue running after fsync() fails. */
 bool		data_sync_retry = false;
 
@@ -261,6 +270,11 @@ typedef struct
 		FILE	   *file;
 		DIR		   *dir;
 		int			fd;
+		struct
+		{
+			FILE	   *file;
+			pid_t		pid;
+		}			pipe;
 	}			desc;
 } AllocateDesc;
 
@@ -943,6 +957,128 @@ InitTemporaryFileAccess(void)
 #endif
 }
 
+/*
+ * Returns true if the passed in highestfd is the last one that we're allowed
+ * to open based on our. This should only be called if
+ */
+static bool
+IsOpenFileLimit(int highestfd)
+{
+#ifdef HAVE_GETRLIMIT
+	if (!saved_original_max_open_files)
+	{
+		return false;
+	}
+
+	return highestfd >= custom_max_open_files.rlim_cur - 1;
+#else
+	return false;
+#endif
+}
+
+/*
+ * Increases the open file limit (RLIMIT_NOFILE) by the requested amount.
+ * Returns true if successful, false otherwise.
+ */
+static bool
+IncreaseOpenFileLimit(int extra_files)
+{
+#ifdef HAVE_GETRLIMIT
+	struct rlimit rlim;
+
+	if (!saved_original_max_open_files)
+	{
+		return false;
+	}
+
+	rlim = custom_max_open_files;
+
+	/* If we're already at the max we reached our limit */
+	if (rlim.rlim_cur == original_max_open_files.rlim_max)
+		return false;
+
+	/* Otherwise try to increase the soft limit to what we need */
+	rlim.rlim_cur = Min(rlim.rlim_cur + extra_files, rlim.rlim_max);
+
+	if (setrlimit(RLIMIT_NOFILE, &rlim) != 0)
+	{
+		/* We made sure not to exceed the hard limit, so this shouldn't fail */
+		ereport(WARNING, (errmsg("setrlimit failed: %m")));
+		return false;
+	}
+
+	custom_max_open_files = rlim;
+
+	elog(LOG, "increased open file limit to %ld", (long) rlim.rlim_cur);
+
+	return true;
+#else
+	return false;
+#endif
+}
+
+/*
+ * Saves the original open file limit (RLIMIT_NOFILE) the first time when this
+ * is called. If called again it's a no-op.
+ *
+ * Returns true if successful, false otherwise.
+ */
+static void
+SaveOriginalOpenFileLimit(void)
+{
+#ifdef HAVE_GETRLIMIT
+	int			status;
+
+	if (saved_original_max_open_files)
+	{
+		/* Already saved, no need to do it again */
+		return;
+	}
+
+	status = getrlimit(RLIMIT_NOFILE, &original_max_open_files);
+	if (status != 0)
+	{
+		ereport(WARNING, (errmsg("getrlimit failed: %m")));
+		return;
+	}
+
+	custom_max_open_files = original_max_open_files;
+	saved_original_max_open_files = true;
+	return;
+#endif
+}
+
+#ifndef WIN32
+/*
+ * UseOriginalOpenFileLimit --- Makes the process use the original open file
+ * 		limit that was present at postmaster start.
+ *
+ * This should be called before spawning subprocesses that might use select(2)
+ * which can only handle file descriptors up to 1024.
+ */
+static void
+UseOriginalOpenFileLimit(void)
+{
+#ifdef HAVE_GETRLIMIT
+	if (!saved_original_max_open_files)
+	{
+		return;
+	}
+
+	if (custom_max_open_files.rlim_cur == original_max_open_files.rlim_cur)
+	{
+		/* Not changed, so no need to call setrlimit at all */
+		return;
+	}
+
+	if (setrlimit(RLIMIT_NOFILE, &original_max_open_files) != 0)
+	{
+		ereport(WARNING, (errmsg("setrlimit failed: %m")));
+	}
+#endif
+}
+#endif							/* WIN32 */
+
 /*
  * count_usable_fds --- count how many FDs the system will let us open,
  *		and estimate how many are already open.
@@ -966,38 +1102,39 @@ count_usable_fds(int max_to_probe, int *usable_fds, int *already_open)
 	int			highestfd = 0;
 	int			j;
 
-#ifdef HAVE_GETRLIMIT
-	struct rlimit rlim;
-	int			getrlimit_status;
-#endif
-
 	size = 1024;
 	fd = (int *) palloc(size * sizeof(int));
 
-#ifdef HAVE_GETRLIMIT
-	getrlimit_status = getrlimit(RLIMIT_NOFILE, &rlim);
-	if (getrlimit_status != 0)
-		ereport(WARNING, (errmsg("getrlimit failed: %m")));
-#endif							/* HAVE_GETRLIMIT */
+	SaveOriginalOpenFileLimit();
 
 	/* dup until failure or probe limit reached */
 	for (;;)
 	{
 		int			thisfd;
 
-#ifdef HAVE_GETRLIMIT
-
 		/*
 		 * don't go beyond RLIMIT_NOFILE; causes irritating kernel logs on
 		 * some platforms
 		 */
-		if (getrlimit_status == 0 && highestfd >= rlim.rlim_cur - 1)
-			break;
-#endif
+		if (IsOpenFileLimit(highestfd))
+		{
+			if (!IncreaseOpenFileLimit(max_to_probe - used))
+				break;
+		}
 
 		thisfd = dup(2);
 		if (thisfd < 0)
 		{
+			/*
+			 * Eventhough we do the pre-check above, it's still possible that
+			 * the call to dup fails with EMFILE. This can happen if the last
+			 * file descriptor was already assigned to an "already open" file.
+			 * One example of this happening, is if we're already at the soft
+			 * limit when we call count_usable_fds.
+			 */
+			if (errno == EMFILE && IncreaseOpenFileLimit(max_to_probe - used))
+				continue;
+
 			/* Expect EMFILE or ENFILE, else it's fishy */
 			if (errno != EMFILE && errno != ENFILE)
 				elog(WARNING, "duplicating stderr file descriptor failed after %d successes: %m", used);
@@ -2731,6 +2868,265 @@ OpenTransientFilePerm(const char *fileName, int fileFlags, mode_t fileMode)
 	return -1;					/* failure */
 }
 
+#ifndef WIN32
+/*
+ * Helper function to fork a child process for executing a shell command.
+ *
+ * This handles all the standard child process setup:
+ * - Blocks signals before fork to avoid race conditions
+ * - Lowers the file limit in the child (UseOriginalOpenFileLimit)
+ * - Resets signal handlers that the backend has set to SIG_IGN
+ * - Unblocks signals in both parent and child after setup
+ *
+ * Returns -1 on fork failure, 0 in the child, or the child PID in the parent.
+ * After this returns in the child, the caller should do any additional setup
+ * (like pipe redirection) and then call exec_shell_command().
+ */
+static pid_t
+fork_for_shell_command(void)
+{
+	pid_t		pid;
+	sigset_t	save_mask;
+
+	/* Block signals during fork to avoid race conditions in child setup */
+	sigprocmask(SIG_SETMASK, &BlockSig, &save_mask);
+
+	pid = fork();
+	if (pid == 0)
+	{
+		/* Child process */
+		UseOriginalOpenFileLimit();
+
+		/*
+		 * Reset signal handlers to default. The backend ignores these, and
+		 * SIG_IGN is preserved across exec, so we must reset them to allow
+		 * the child process to respond to signals normally.
+		 */
+		pqsignal(SIGINT, SIG_DFL);
+		pqsignal(SIGQUIT, SIG_DFL);
+		pqsignal(SIGPIPE, SIG_DFL);
+	}
+
+	/* Restore/unblock signals in both parent and child (also if fork failed) */
+	sigprocmask(SIG_SETMASK, &save_mask, NULL);
+
+	return pid;
+}
+
+/*
+ * Execute a shell command via /bin/sh. This function does not return.
+ * Should be called in the child process after fork_for_shell_command().
+ */
+static pg_noreturn void
+exec_shell_command(const char *command)
+{
+	execl("/bin/sh", "sh", "-c", command, (char *) NULL);
+	_exit(127);
+}
+#endif
+
+/*
+ * pg_system - Custom system() that lowers file limit in child process.
+ *
+ * This is needed because we may have bumped the soft RLIMIT_NOFILE limit
+ * above its original value. The child process should use the original limit
+ * to avoid issues with programs that use select(2), which can only handle
+ * FDs up to FD_SETSIZE (typically 1024).
+ *
+ * We can't just call UseOriginalOpenFileLimit() before the standard system()
+ * because that would temporarily lower the limit in the parent process,
+ * which could cause concurrent file operations to fail in a multithreaded
+ * environment.
+ */
+int
+pg_system(const char *command)
+{
+#ifndef WIN32
+	pid_t		pid;
+	int			status;
+	pid_t		waitresult;
+
+	pid = fork_for_shell_command();
+	if (pid == (pid_t) -1)
+		return -1;
+	if (pid == 0)
+		exec_shell_command(command);	/* does not return */
+
+	/* Wait for child */
+	do
+	{
+		waitresult = waitpid(pid, &status, 0);
+	} while (waitresult == (pid_t) -1 && errno == EINTR);
+
+	if (waitresult != pid)
+		return -1;
+
+	return status;
+#else
+	/* On Windows, just use standard system() - no RLIMIT_NOFILE bumping there */
+	return system(command);
+#endif
+}
+
+
+/*
+ * pg_popen - Custom popen() that lowers file limit in child process.
+ *
+ * This is needed because we may have bumped the soft RLIMIT_NOFILE limit
+ * above its original value. The child process should use the original limit
+ * to avoid issues with programs that use select(2), which can only handle
+ * FDs up to FD_SETSIZE (typically 1024).
+ *
+ * We can't just call UseOriginalOpenFileLimit() before popen() of the stdlib
+ * because popen() opens a file descriptor in the parent process (the parent's
+ * side of the pipe), which would fail if we're already above the original
+ * limit.
+ *
+ * Returns the FILE* for the pipe, or NULL on error.
+ * On success, *child_pid is set to the child's PID (or 0 on Windows where
+ * we use standard popen).
+ */
+static FILE *
+pg_popen(const char *command, const char *mode, pid_t *child_pid)
+{
+#ifndef WIN32
+	int			pipe_fds[2];
+	int			parent_fd;
+	int			child_fd;
+	int			child_target_fd;
+	pid_t		pid;
+	FILE	   *pipe_file;
+	int			save_errno;
+
+	/* Only "r" and "w" modes are supported */
+	if (mode[0] == 'r')
+	{
+		/* Parent reads from child's stdout */
+		parent_fd = 0;
+		child_fd = 1;
+		child_target_fd = STDOUT_FILENO;
+	}
+	else if (mode[0] == 'w')
+	{
+		/* Parent writes to child's stdin */
+		parent_fd = 1;
+		child_fd = 0;
+		child_target_fd = STDIN_FILENO;
+	}
+	else
+	{
+		errno = EINVAL;
+		return NULL;
+	}
+
+	/*
+	 * Set close-on-exec to prevent other concurrently spawned child processes
+	 * from inheriting the pipe file descriptors. This is impossible while
+	 * we're not using threads, but since this is part of adding such
+	 * multithreading support let's pretend that we already have it. For
+	 * systems that don't have pipe2, there's always a risk of race
+	 * conditions, but let's minimize that by quickly setting the
+	 * close-on-exec flag with fnctl.
+	 */
+#ifdef HAVE_PIPE2
+	if (pipe2(pipe_fds, O_CLOEXEC) < 0)
+		return NULL;
+#else
+	if (pipe(pipe_fds) < 0)
+		return NULL;
+	if (fcntl(pipe_fds[0], F_SETFD, FD_CLOEXEC) == -1 ||
+		fcntl(pipe_fds[1], F_SETFD, FD_CLOEXEC) == -1)
+	{
+		save_errno = errno;
+		close(pipe_fds[0]);
+		close(pipe_fds[1]);
+		errno = save_errno;
+		return NULL;
+	}
+#endif
+
+	fflush(NULL);
+
+	pid = fork_for_shell_command();
+	if (pid < 0)
+	{
+		/* fork failed */
+		save_errno = errno;
+		close(pipe_fds[0]);
+		close(pipe_fds[1]);
+		errno = save_errno;
+		return NULL;
+	}
+
+	if (pid == 0)
+	{
+		/* Child process - set up pipe redirection and exec */
+		close(pipe_fds[parent_fd]);
+		if (pipe_fds[child_fd] != child_target_fd)
+		{
+			dup2(pipe_fds[child_fd], child_target_fd);
+			close(pipe_fds[child_fd]);
+		}
+
+		exec_shell_command(command);	/* does not return */
+	}
+
+	/* Parent process */
+	close(pipe_fds[child_fd]);
+	pipe_file = fdopen(pipe_fds[parent_fd], mode);
+	if (pipe_file == NULL)
+	{
+		save_errno = errno;
+		close(pipe_fds[parent_fd]);
+		errno = save_errno;
+		return NULL;
+	}
+
+	*child_pid = pid;
+	return pipe_file;
+#else
+	/* On Windows, just use standard popen - no RLIMIT_NOFILE bumping there */
+	fflush(NULL);
+	*child_pid = 0;
+	return popen(command, mode);
+#endif
+}
+
+/*
+ * pg_pclose - Close a pipe opened by pg_popen().
+ *
+ * On Unix, this closes the FILE* and waits for the child process.
+ * On Windows, this just calls pclose().
+ *
+ * Returns the child's exit status (like pclose), or -1 on error.
+ */
+static int
+pg_pclose(FILE *fp, pid_t child_pid)
+{
+#ifndef WIN32
+	int			status;
+	pid_t		waitresult;
+
+	/* Close the FILE* first */
+	if (fclose(fp) != 0)
+		return -1;
+
+	/* Then wait for the child process, like pclose() does */
+	do
+	{
+		waitresult = waitpid(child_pid, &status, 0);
+	} while (waitresult == (pid_t) -1 && errno == EINTR);
+
+	if (waitresult == child_pid)
+		return status;
+	else
+		return -1;
+#else
+	(void) child_pid;			/* unused on Windows */
+	return pclose(fp);
+#endif
+}
+
 /*
  * Routines that want to initiate a pipe stream should use OpenPipeStream
  * rather than plain popen().  This lets fd.c deal with freeing FDs if
@@ -2745,6 +3141,7 @@ OpenPipeStream(const char *command, const char *mode)
 {
 	FILE	   *file;
 	int			save_errno;
+	pid_t		child_pid;
 
 	DO_DB(elog(LOG, "OpenPipeStream: Allocated %d (%s)",
 			   numAllocatedDescs, command));
@@ -2760,22 +3157,21 @@ OpenPipeStream(const char *command, const char *mode)
 	ReleaseLruFiles();
 
 TryAgain:
-	fflush(NULL);
-	pqsignal(SIGPIPE, SIG_DFL);
+
 	errno = 0;
-	file = popen(command, mode);
+	file = pg_popen(command, mode, &child_pid);
 	save_errno = errno;
 	pqsignal(SIGPIPE, SIG_IGN);
-	errno = save_errno;
 	if (file != NULL)
 	{
 		AllocateDesc *desc = &allocatedDescs[numAllocatedDescs];
 
 		desc->kind = AllocateDescPipe;
-		desc->desc.file = file;
+		desc->desc.pipe.file = file;
+		desc->desc.pipe.pid = child_pid;
 		desc->create_subid = GetCurrentSubTransactionId();
 		numAllocatedDescs++;
-		return desc->desc.file;
+		return desc->desc.pipe.file;
 	}
 
 	if (errno == EMFILE || errno == ENFILE)
@@ -2808,7 +3204,7 @@ FreeDesc(AllocateDesc *desc)
 			result = fclose(desc->desc.file);
 			break;
 		case AllocateDescPipe:
-			result = pclose(desc->desc.file);
+			result = pg_pclose(desc->desc.pipe.file, desc->desc.pipe.pid);
 			break;
 		case AllocateDescDir:
 			result = closedir(desc->desc.dir);
@@ -3060,7 +3456,7 @@ ClosePipeStream(FILE *file)
 	{
 		AllocateDesc *desc = &allocatedDescs[i];
 
-		if (desc->kind == AllocateDescPipe && desc->desc.file == file)
+		if (desc->kind == AllocateDescPipe && desc->desc.pipe.file == file)
 			return FreeDesc(desc);
 	}
 
diff --git a/src/bin/initdb/t/001_initdb.pl b/src/bin/initdb/t/001_initdb.pl
index 1e9543c2585..9a758e26fb7 100644
--- a/src/bin/initdb/t/001_initdb.pl
+++ b/src/bin/initdb/t/001_initdb.pl
@@ -131,7 +131,7 @@ if ($ENV{with_icu} eq 'yes')
 		],
 		'option --icu-locale');
 
-	command_like(
+	command_checks_all(
 		[
 			'initdb', '--no-sync',
 			'--auth' => 'trust',
@@ -145,7 +145,9 @@ if ($ENV{with_icu} eq 'yes')
 			'--lc-time' => 'C',
 			"$tempdir/data4"
 		],
-		qr/^\s+default collation:\s+und\n/ms,
+		0,
+		[qr/^\s+default collation:\s+und\n/ms],
+		[],    # stderr may have LOG messages
 		'options --locale-provider=icu --locale=und --lc-*=C');
 
 	command_fails_like(
diff --git a/src/include/pg_config.h.in b/src/include/pg_config.h.in
index b0b0cfdaf79..beeee61017d 100644
--- a/src/include/pg_config.h.in
+++ b/src/include/pg_config.h.in
@@ -307,6 +307,9 @@
 /* Define to 1 if you have the <pam/pam_appl.h> header file. */
 #undef HAVE_PAM_PAM_APPL_H
 
+/* Define to 1 if you have the `pipe2' function. */
+#undef HAVE_PIPE2
+
 /* Define to 1 if you have the `posix_fadvise' function. */
 #undef HAVE_POSIX_FADVISE
 
diff --git a/src/include/storage/fd.h b/src/include/storage/fd.h
index a1bdefec4a5..4484d557c37 100644
--- a/src/include/storage/fd.h
+++ b/src/include/storage/fd.h
@@ -186,6 +186,7 @@ extern int	pg_fsync_writethrough(int fd);
 extern int	pg_fdatasync(int fd);
 extern bool pg_file_exists(const char *name);
 extern void pg_flush_data(int fd, pgoff_t offset, pgoff_t nbytes);
+extern int	pg_system(const char *command);
 extern int	pg_truncate(const char *path, pgoff_t length);
 extern void fsync_fname(const char *fname, bool isdir);
 extern int	fsync_fname_ext(const char *fname, bool isdir, bool ignore_perm, int elevel);

base-commit: 6498287696dafc1ebd380ea4eb249124989294d3
-- 
2.52.0

From 94c19d3857102dbf6470d64295fc263b6e68556e Mon Sep 17 00:00:00 2001
From: Jelte Fennema-Nio <[email-masked]>
Date: Sun, 7 Dec 2025 15:06:54 +0100
Subject: [PATCH v9 2/2] Reflect the value of max_safe_fds in
 max_files_per_process

It is currently hard to figure out if max_safe_fds is significantly
lower than max_files_per_process. This starts reflecting the value of
max_safe_fds in max_files_per_process after our limit detection. We
still want to have two separate variables because for the bootstrap or
standalone-backend cases their values differ on purpose.
---
 src/backend/storage/file/fd.c | 11 +++++++++++
 1 file changed, 11 insertions(+)

diff --git a/src/backend/storage/file/fd.c b/src/backend/storage/file/fd.c
index 40212d733e9..2d27207ab89 100644
--- a/src/backend/storage/file/fd.c
+++ b/src/backend/storage/file/fd.c
@@ -1179,6 +1179,7 @@ set_max_safe_fds(void)
 {
 	int			usable_fds;
 	int			already_open;
+	char	   *max_safe_fds_string;
 
 	/*----------
 	 * We want to set max_safe_fds to
@@ -1194,6 +1195,16 @@ set_max_safe_fds(void)
 
 	max_safe_fds = Min(usable_fds, max_files_per_process);
 
+	/*
+	 * Update GUC variable to allow users to see if the result is different
+	 * than what the used value turns out to be different than what they had
+	 * configured.
+	 */
+	max_safe_fds_string = psprintf("%d", max_safe_fds);
+	SetConfigOption("max_files_per_process", max_safe_fds_string,
+					PGC_POSTMASTER, PGC_S_OVERRIDE);
+	pfree(max_safe_fds_string);
+
 	/*
 	 * Take off the FDs reserved for system() etc.
 	 */
-- 
2.52.0

---

From: "Jelte Fennema-Nio" <[email-masked]>
Date: Sat, 14 Mar 2026 15:16:56 +0100

On Sun Dec 7, 2025 at 4:55 PM CET, Jelte Fennema-Nio wrote:
> do the restoration of the file limit for subprocesses properly: 

Rebased version attached.
From e8803cdddda226554b18ae00f6ff16f35b784927 Mon Sep 17 00:00:00 2001
From: Jelte Fennema-Nio <[email-masked]>
Date: Sun, 7 Dec 2025 15:07:08 +0100
Subject: [PATCH v10 1/2] Bump postmaster soft open file limit (RLIMIT_NOFILE)
 when necessary

diff --git a/meson.build b/meson.build
index 70dc64c349a..c602a11e05d 100644
--- a/meson.build
+++ b/meson.build
@@ -3103,6 +3103,7 @@ func_checks = [
   ['mbstowcs_l'],
   ['memset_explicit'],
   ['mkdtemp'],
+  ['pipe2'],
   ['posix_fadvise'],
   ['posix_fallocate'],
   ['ppoll'],
diff --git a/src/backend/access/transam/xlogarchive.c b/src/backend/access/transam/xlogarchive.c
index 9a0c8097cb1..ef3136b2d10 100644
--- a/src/backend/access/transam/xlogarchive.c
+++ b/src/backend/access/transam/xlogarchive.c
@@ -159,10 +159,9 @@ RestoreArchivedFile(char *path, const char *xlogfname,
 			(errmsg_internal("executing restore command \"%s\"",
 							 xlogRestoreCmd)));
 
-	fflush(NULL);
@@ -170,16 +169,13 @@ RestoreArchivedFile(char *path, const char *xlogfname,
 
 	if (rc == 0)
@@ -328,7 +324,7 @@ ExecuteRecoveryCommand(const char *command, const char *commandName,
 	 */
 	fflush(NULL);
 
 	pfree(xlogRecoveryCmd);
diff --git a/src/backend/archive/shell_archive.c b/src/backend/archive/shell_archive.c
index 0b427a68809..051aeda9fc5 100644
--- a/src/backend/archive/shell_archive.c
+++ b/src/backend/archive/shell_archive.c
@@ -22,6 +22,7 @@
 #include "archive/shell_archive.h"
 #include "common/percentrepl.h"
 #include "pgstat.h"
+#include "storage/fd.h"
 #include "utils/wait_event.h"
 
 static bool shell_archive_configured(ArchiveModuleState *state);
@@ -78,7 +79,7 @@ shell_archive_file(ArchiveModuleState *state, const char *file,
 
 	fflush(NULL);
 
 	if (rc != 0)
index 01f1bd6e687..21696fad0f6 100644
@@ -159,6 +161,13 @@ int			max_files_per_process = 1000;
 
+#ifdef HAVE_GETRLIMIT
 
@@ -265,6 +274,11 @@ typedef struct
 		FILE	   *file;
 		DIR		   *dir;
 		int			fd;
+		struct
+		{
+			FILE	   *file;
+			pid_t		pid;
+		}			pipe;
 	}			desc;
 } AllocateDesc;
 
@@ -947,6 +961,128 @@ InitTemporaryFileAccess(void)
 #endif
 }
 
+/*
@@ -970,38 +1106,39 @@ count_usable_fds(int max_to_probe, int *usable_fds, int *already_open)
 	int			highestfd = 0;
 	int			j;
 
-#ifdef HAVE_GETRLIMIT
-	struct rlimit rlim;
-	int			getrlimit_status;
-#endif
-
 	size = 1024;
 
-#ifdef HAVE_GETRLIMIT

-#ifdef HAVE_GETRLIMIT
-
 		/*
 
 		thisfd = dup(2);
 		if (thisfd < 0)
 		{
+			/*
@@ -2718,6 +2855,265 @@ OpenTransientFilePerm(const char *fileName, int fileFlags, mode_t fileMode)
 	return -1;					/* failure */
 }
 
+#ifndef WIN32
+/*
@@ -2732,6 +3128,7 @@ OpenPipeStream(const char *command, const char *mode)
 {
 	FILE	   *file;
 	int			save_errno;
+	pid_t		child_pid;
 
 	DO_DB(elog(LOG, "OpenPipeStream: Allocated %d (%s)",
 			   numAllocatedDescs, command));
@@ -2747,22 +3144,21 @@ OpenPipeStream(const char *command, const char *mode)
 	ReleaseLruFiles();
 
 TryAgain:
-	fflush(NULL);

 	if (errno == EMFILE || errno == ENFILE)
@@ -2795,7 +3191,7 @@ FreeDesc(AllocateDesc *desc)
 			result = fclose(desc->desc.file);
 			break;
 		case AllocateDescPipe:
-			result = pclose(desc->desc.file);
+			result = pg_pclose(desc->desc.pipe.file, desc->desc.pipe.pid);
 			break;
 		case AllocateDescDir:
 			result = closedir(desc->desc.dir);
@@ -3047,7 +3443,7 @@ ClosePipeStream(FILE *file)
 	{
 		AllocateDesc *desc = &allocatedDescs[i];

index 081535a22e4..d4784705fc5 100644
 
-	command_like(
+	command_checks_all(
 		[
 			'initdb', '--no-sync',
 			'--auth' => 'trust',
 
 	command_fails_like(
index 79379a4d125..d8a18545c91 100644
@@ -300,6 +300,9 @@

index 8ac466fd346..2111ec0ce83 100644
@@ -213,6 +213,7 @@ extern int	pg_fsync_writethrough(int fd);

base-commit: ae58189a4d523f0156ebe30f4534180555669e88
-- 
2.53.0

From 927c1ae8e702c4c780842a7615b290f5d06fcd68 Mon Sep 17 00:00:00 2001
From: Jelte Fennema-Nio <[email-masked]>
Date: Sun, 7 Dec 2025 15:06:54 +0100
Subject: [PATCH v10 2/2] Reflect the value of max_safe_fds in
 max_files_per_process

diff --git a/src/backend/storage/file/fd.c b/src/backend/storage/file/fd.c
index 21696fad0f6..7579ffec57b 100644
--- a/src/backend/storage/file/fd.c
+++ b/src/backend/storage/file/fd.c
@@ -1183,6 +1183,7 @@ set_max_safe_fds(void)
 {
 	int			usable_fds;
 	int			already_open;
+	char	   *max_safe_fds_string;
 
 	/*----------
 	 * We want to set max_safe_fds to
@@ -1198,6 +1199,16 @@ set_max_safe_fds(void)

+	/*
2.53.0