Received: from malur.postgresql.org ([217.196.149.56]) by arkaria.postgresql.org with esmtps (TLS1.3) tls TLS_ECDHE_RSA_WITH_AES_256_GCM_SHA384 (Exim 4.96) (envelope-from ) id 1wtVfL-001JhI-2r for pgsql-hackers@arkaria.postgresql.org; Mon, 10 Aug 2026 19:28:08 +0000 Received: from localhost ([127.0.0.1] helo=malur.postgresql.org) by malur.postgresql.org with esmtp (Exim 4.96) (envelope-from ) id 1wtVfL-000f4o-0h for pgsql-hackers@arkaria.postgresql.org; Mon, 10 Aug 2026 19:28:06 +0000 Received: from magus.postgresql.org ([2a02:c0:301:0:ffff::29]) by malur.postgresql.org with esmtps (TLS1.3) tls TLS_ECDHE_RSA_WITH_AES_256_GCM_SHA384 (Exim 4.96) (envelope-from ) id 1wtVfK-000f4g-0e for pgsql-hackers@lists.postgresql.org; Mon, 10 Aug 2026 19:28:06 +0000 Received: from flow-b8-smtp.messagingengine.com ([202.12.124.143]) by magus.postgresql.org with esmtps (TLS1.3) tls TLS_ECDHE_RSA_WITH_AES_256_GCM_SHA384 (Exim 4.98.2) (envelope-from ) id 1wtVfC-0000000021m-3bvQ for pgsql-hackers@lists.postgresql.org; Mon, 10 Aug 2026 19:28:05 +0000 Received: from phl-compute-02.internal (phl-compute-02.internal [10.202.2.42]) by mailflow.stl.internal (Postfix) with ESMTP id 3FA14130013D for ; Mon, 10 Aug 2026 15:27:56 -0400 (EDT) Received: from phl-frontend-04 ([10.202.2.163]) by phl-compute-02.internal (MEProxy); Mon, 10 Aug 2026 15:27:56 -0400 DKIM-Signature: v=1; a=rsa-sha256; c=relaxed/relaxed; d=abdiel.eu; h=cc :content-type:content-type:date:date:from:from:in-reply-to :message-id:mime-version:reply-to:subject:subject:to:to; s=fm1; t=1786390076; x=1786393676; bh=JJpLhbN9v9T8WlYcwZOfO1+sPsDrINEf r3Pn9sunZ3Y=; b=NKF6mhyHhESEutLi678yL0d7quX+CY1EIbPDnRdasP+UcaoB AJ0tMHeWda6qDMcfbQtkOoylWJkDtrwbt5n+STNApQi3uZLum78keAmIXdv9vP7W xj6gbsJwjwmkD/AawhEexeswwD//T6vf3R6FeA8F7u/5IhVfSz0dW6XkSNHjIgnP sUBG+12PLKqSQf9seynhDhRW1pJMzqC4mC4q3SALdIWSQLrrOs3lwNP1L2k+VdFi vtRnDQyACmLQJfwcyGL+vZmrgLqFYJSWXcV6MYIgUy/ErBCyS878lVWrGP2zz1LZ 31bZ+X7qZzjy2glvYZ6EgAXomBtzyUoh/CuyZg== DKIM-Signature: v=1; a=rsa-sha256; c=relaxed/relaxed; d= messagingengine.com; h=cc:content-type:content-type:date:date :feedback-id:feedback-id:from:from:in-reply-to:message-id :mime-version:reply-to:subject:subject:to:to:x-me-proxy :x-me-sender:x-me-sender:x-sasl-enc; s=fm3; t=1786390076; x= 1786393676; bh=JJpLhbN9v9T8WlYcwZOfO1+sPsDrINEfr3Pn9sunZ3Y=; b=V PnHQ0qCpbLSHa9aTjMJfNVSoJ23FptoFoJTa/6/jP/IZW0z1zpVXpxXVFQfljt1l 1td2E57Ygdb4UIgs5nihQeP9KrI6XZtys5ns5v2UPhuuFNd/qbvZEfPh1xDIovG8 pRhRb0Fcm/i3IMLsxVY4q/33lMERYcxr4N8ZUt4iqGr4tCe8VMtOgRyM4x9ZDOCC pbgp+OULxhVMUa3cmWUxZ9toWZysnS0r9m4m09NtwcGReYB5/+mZJl+7Av+nN7jm 0oQ4XQX7S930Dl3tswixlk1YAkaVnBMD6psk4QjXe5piJ5tgby7wTrLWjmX+gsuY 1aeqpb4hq7KP0wmtqNMzQ== X-ME-Sender: X-ME-Received: X-ME-Proxy-Cause: dmFkZTFF2DzV2aXdTh5ryFIRqMvw/FgDm0qnaTrautoQ5ZL+SioxIhrDuGN6KlxLxVJo2o mazjsMAqIf9ZI/50cWvQGpPe59z0zK0BBpoW4KHZH2kqLzVlFSijYeaE7SxUQYiLw3rmqd 72ZqZwScNl0dE/N6Egn1HvrWuvXP0NUNJg/PWe0qkysrzGw3KLdFKuXIydbSc5KO2soIzf cvAsr7Uw/bgiyHd3VL0fmztLrqfMTaz7JkNMuLDnT+B8uKlqqrgkAvKyj/UHHfS4AHCjju ApEomKzDYdPfAM5UbmkJjBjEHVrOfTy77TWT/zzsXYzgC4i1g94wi8BUcI1fb8hBLifaZn AWn/pRYKdyuxyn4Rk0o+8L0gDnmU9NxFiydADwR5Og2unt/p2c9eHDpdvb2XeQXS73REQ8 lwuXGqWD0mc3CQKX3YzroXkgIgFj+x9o7Zk+11OsO8D3ezvQgF3LOEaL2MFatk/3dFcGeJ cDZlNncUQFN9Gbxb6oZImVFL11z+S8rG0czddwev8em9RivTiCIzalLQp43QbxMWENKGwF l+jHNNko3xqba+uTosvMJESHp26OdLJS6dkQz9OpJ5iZhNV3Xc2/tltE9MK0OmirTRN1XP EA80O1L/ZiaRNCyUoumqPyT4PptypB8LWW8HSBrZgF3/q240R0k+ySWXvzAA X-ME-Proxy: Feedback-ID: i19364b5b:Fastmail Received: by mail.messagingengine.com (Postfix) with ESMTPA for ; Mon, 10 Aug 2026 15:27:54 -0400 (EDT) From: "Jonathan Gonzalez V." To: pgsql-hackers@lists.postgresql.org Subject: Introduce psystem() to replace system() Date: Mon, 10 Aug 2026 21:27:51 +0200 Message-ID: <87zeyt93ns.fsf@abdiel.eu> User-Agent: Gnus/5.13 (Gnus v5.13) MIME-Version: 1.0 Content-Type: multipart/mixed; boundary="=-=-=" List-Id: List-Help: List-Subscribe: List-Post: List-Owner: List-Archive: Archived-At: Precedence: bulk --=-=-= Content-Type: text/plain Hello!! For some time I've been wondering why PostgreSQL needs system() calls, which use a shell that can lead to many problems, and also why it requires a shell to run a command. I first started thinking about this when I was trying to run a full distroless PostgreSQL container. It turns out that isn't possible since the shell is a requirement, and distroless containers are secure exactly _because_ there's no shell to execute any command other than the ones that are meant to be executed. After some research I found out that using system() has other problems, like issues related to quoting that are really painful to solve [0][1], and also the exit codes control[2]. Both topics have already been discussed on the list. But the main argument now for me is security. Not having a shell avoids any possible PATH injection, missing quoting to escape a command, or new lines that the shell interprets differently from what you'd expect. After some thinking I came up with a small interface, which only purpose is to replace system() calls in a more smooth way using execv() under the hood. I suppose you could use execl() but I've decided to keep it simple, leaving the opportunity to expand in the future. I already implemented one call with `pg_ctl initdb` as an example. There's an important topic related to using shell versus not a shell. In some places like `archive_command` people may use `&&`, but this idea aims to avoid this kind of behavior since it's not secure. Probably we can implement a way to run commands in sequence, or simply tell the users that this isn't allowed anymore, but it's possible to trigger commands in sequence since the interface allows to manipulate the STDIN and STDOUT. I would like to open the discussion here if this is the right direction. There's a lot to do and this still a work in progress, the current patch is small and simple, but already provides building blocks in this direction. [0] https://www.postgresql.org/message-id/7606.1153326421%40sss.pgh.pa.us [1] https://www.postgresql.org/message-id/CA%2BTgmobBmWWCgPUd04NGoQ%3D_XvcidV%2BsE2F7KChEXfs8KBPg6w%40mail.gmail.com [2] https://www.postgresql.org/message-id/21292.1358698487%40sss.pgh.pa.us -- Jonathan Gonzalez V. EDB https://enterprisedb.com --=-=-= Content-Type: text/x-diff Content-Disposition: inline; filename=0001-replace-calls-to-system-with-PostgreSQL-own-implemen.patch From 22707f3f0c5a160d9483136a198b64dc1fbb9d82 Mon Sep 17 00:00:00 2001 From: "Jonathan Gonzalez V." Date: Mon, 3 Aug 2026 15:21:45 -0400 Subject: [PATCH 1/1] replace calls to system() with PostgreSQL own implementation Introduce PCommand and psystem() to execute commands from argument arrays, avoiding the shell on Unix. Windows implementation still on a work in progress --- src/backend/utils/init/postinit.c | 58 --------- src/bin/pg_ctl/pg_ctl.c | 30 ++--- src/common/Makefile | 1 + src/common/exec.c | 59 +++++++++ src/common/meson.build | 2 + src/common/pg_exec.c | 206 ++++++++++++++++++++++++++++++ src/include/common/pg_exec.h | 39 ++++++ src/include/miscadmin.h | 1 - src/include/port.h | 2 + 9 files changed, 324 insertions(+), 74 deletions(-) create mode 100644 src/common/pg_exec.c create mode 100644 src/include/common/pg_exec.h diff --git a/src/backend/utils/init/postinit.c b/src/backend/utils/init/postinit.c index 3d8c9bdebd5..78ea7d56022 100644 --- a/src/backend/utils/init/postinit.c +++ b/src/backend/utils/init/postinit.c @@ -492,64 +492,6 @@ CheckMyDatabase(const char *name, bool am_superuser, bool override_allow_connect ReleaseSysCache(tup); } - -/* - * pg_split_opts -- split a string of options and append it to an argv array - * - * The caller is responsible for ensuring the argv array is large enough. The - * maximum possible number of arguments added by this routine is - * (strlen(optstr) + 1) / 2. - * - * Because some option values can contain spaces we allow escaping using - * backslashes, with \\ representing a literal backslash. - */ -void -pg_split_opts(char **argv, int *argcp, const char *optstr) -{ - StringInfoData s; - - initStringInfo(&s); - - while (*optstr) - { - bool last_was_escape = false; - - resetStringInfo(&s); - - /* skip over leading space */ - while (isspace((unsigned char) *optstr)) - optstr++; - - if (*optstr == '\0') - break; - - /* - * Parse a single option, stopping at the first space, unless it's - * escaped. - */ - while (*optstr) - { - if (isspace((unsigned char) *optstr) && !last_was_escape) - break; - - if (!last_was_escape && *optstr == '\\') - last_was_escape = true; - else - { - last_was_escape = false; - appendStringInfoChar(&s, *optstr); - } - - optstr++; - } - - /* now store the option in the next argv[] position */ - argv[(*argcp)++] = pstrdup(s.data); - } - - pfree(s.data); -} - /* * Initialize MaxBackends value from config options. * diff --git a/src/bin/pg_ctl/pg_ctl.c b/src/bin/pg_ctl/pg_ctl.c index 6c604e2d962..19e884bac1f 100644 --- a/src/bin/pg_ctl/pg_ctl.c +++ b/src/bin/pg_ctl/pg_ctl.c @@ -25,6 +25,7 @@ #include "common/controldata_utils.h" #include "common/file_perm.h" #include "common/logging.h" +#include "common/pg_exec.h" #include "common/string.h" #include "datatype/timestamp.h" #include "getopt_long.h" @@ -42,6 +43,7 @@ typedef enum IMMEDIATE_MODE, } ShutdownMode; + typedef enum { POSTMASTER_READY, @@ -904,26 +906,18 @@ find_other_exec_or_die(const char *argv0, const char *target, const char *versio static void do_init(void) { - char *cmd; + PCommand *cmd; if (exec_path == NULL) exec_path = find_other_exec_or_die(argv0, "initdb", "initdb (PostgreSQL) " PG_VERSION "\n"); - if (pgdata_opt == NULL) - pgdata_opt = ""; - - if (post_opts == NULL) - post_opts = ""; - - if (!silent_mode) - cmd = psprintf("\"%s\" %s%s", - exec_path, pgdata_opt, post_opts); - else - cmd = psprintf("\"%s\" %s%s > \"%s\"", - exec_path, pgdata_opt, post_opts, DEVNULL); + cmd = pcommand_init(exec_path, "initdb"); + pcommand_append_arg(cmd, pgdata_opt); + pcommand_append_arg(cmd, post_opts); + cmd->silent = silent_mode; fflush(NULL); - if (system(cmd) != 0) + if (psystem(cmd) != 0) { write_stderr(_("%s: database system initialization failed\n"), progname); exit(1); @@ -2155,6 +2149,12 @@ adjust_data_dir(void) else my_exec_path = pg_strdup(exec_path); +/* + * command = pcommand_init(my_exec_path, "postgres"); + * pcommand_append_arg(command, "-C data_directory"); + * pcommand_append_arg(command, pgdata_opt); + * pcommand_append_arg(command, post_opts); + */ /* it's important for -C to be the first option, see main.c */ cmd = psprintf("\"%s\" -C data_directory %s%s", my_exec_path, @@ -2287,7 +2287,7 @@ main(int argc, char **argv) * We could pass PGDATA just in an environment variable * but we do -D too for clearer postmaster 'ps' display */ - pgdata_opt = psprintf("-D \"%s\" ", pgdata_D); + pgdata_opt = psprintf("-D %s ", pgdata_D); pg_free(pgdata_D); break; } diff --git a/src/common/Makefile b/src/common/Makefile index 1a2fbbe887f..753399f023a 100644 --- a/src/common/Makefile +++ b/src/common/Makefile @@ -68,6 +68,7 @@ OBJS_COMMON = \ md5_common.o \ parse_manifest.o \ percentrepl.o \ + pg_exec.o \ pg_get_line.o \ pg_lzcompress.o \ pg_prng.o \ diff --git a/src/common/exec.c b/src/common/exec.c index 2881aa92ca6..c5a31c17a95 100644 --- a/src/common/exec.c +++ b/src/common/exec.c @@ -33,6 +33,7 @@ #include #include #include +#include #ifdef EXEC_BACKEND #if defined(HAVE_SYS_PERSONALITY_H) @@ -43,6 +44,7 @@ #endif #include "common/string.h" +#include "lib/stringinfo.h" /* Inhibit mingw CRT's auto-globbing of command line arguments */ #if defined(WIN32) && !defined(_MSC_VER) @@ -711,3 +713,60 @@ GetTokenUser(HANDLE hToken, PTOKEN_USER *ppTokenUser) } #endif + +/* + * pg_split_opts -- split a string of options and append it to an argv array + * + * The caller is responsible for ensuring the argv array is large enough. The + * maximum possible number of arguments added by this routine is + * (strlen(optstr) + 1) / 2. + * + * Because some option values can contain spaces we allow escaping using + * backslashes, with \\ representing a literal backslash. + */ +void +pg_split_opts(char **argv, int *argcp, const char *optstr) +{ + StringInfoData s; + + initStringInfo(&s); + + while (*optstr) + { + bool last_was_escape = false; + + resetStringInfo(&s); + + /* skip over leading space */ + while (isspace((unsigned char) *optstr)) + optstr++; + + if (*optstr == '\0') + break; + + /* + * Parse a single option, stopping at the first space, unless it's + * escaped. + */ + while (*optstr) + { + if (isspace((unsigned char) *optstr) && !last_was_escape) + break; + + if (!last_was_escape && *optstr == '\\') + last_was_escape = true; + else + { + last_was_escape = false; + appendStringInfoChar(&s, *optstr); + } + + optstr++; + } + + /* now store the option in the next argv[] position */ + argv[(*argcp)++] = pstrdup(s.data); + } + + pfree(s.data); +} diff --git a/src/common/meson.build b/src/common/meson.build index 9bd55cda95b..06ebb4dd874 100644 --- a/src/common/meson.build +++ b/src/common/meson.build @@ -22,6 +22,7 @@ common_sources = files( 'md5_common.c', 'parse_manifest.c', 'percentrepl.c', + 'pg_exec.c', 'pg_get_line.c', 'pg_lzcompress.c', 'pg_prng.c', @@ -190,6 +191,7 @@ foreach name, opts : pgcommon_variants kwargs: opts + { 'include_directories': [ include_directories('.'), + include_directories('../interfaces/libpq'), opts.get('include_directories', []), ], 'dependencies': opts['dependencies'] + [ssl], diff --git a/src/common/pg_exec.c b/src/common/pg_exec.c new file mode 100644 index 00000000000..bb5613216f2 --- /dev/null +++ b/src/common/pg_exec.c @@ -0,0 +1,206 @@ +/*------------------------------------------------------------------------- + * + * pg_exec.c + * Functions fo execute and manage the input/output of commands + * + * + * Portions Copyright (c) 1996-2026, PostgreSQL Global Development Group + * Portions Copyright (c) 1994, Regents of the University of California + * + * + * IDENTIFICATION + * src/common/pg_exec.c + * + *------------------------------------------------------------------------- + */ + +#include +#include +#include + +#include "c.h" + +#include "postgres.h" +#include "common/pg_exec.h" +#include "utils/palloc.h" + +#ifdef WIN32 +#include "fe_utils/string_utils.h" +#endif + +static int +pcommand_count_args(const char *arg) +{ + int count = 0; + bool in_word = false; + + Assert(arg != NULL); + + while (*arg) + { + if (isspace((unsigned char) *arg)) + in_word = false; + else if (!in_word) + { + count++; + in_word = true; + } + + arg++; + } + + return count; +} + +void +pcommand_append_arg(PCommand * cmd, const char *arg) +{ + int args_count; + + if (arg == NULL) + return; + + args_count = pcommand_count_args(arg); + + if (args_count == 0) + return; + + if (cmd->argc + args_count >= cmd->nalloc) + { + cmd->nalloc += args_count + 1; + cmd->argv = repalloc_array(cmd->argv, char *, cmd->nalloc); + } + + if (args_count > 1) + { + pg_split_opts(cmd->argv, &cmd->argc, arg); + cmd->argv[cmd->argc] = NULL; + } + else + { + + cmd->argv[cmd->argc++] = pstrdup(arg); + cmd->argv[cmd->argc] = NULL; + } +} + + +#ifdef WIN32 +int +pcommand_win(PCommand * cmd) +{ + PQExpBufferData cmd_str; + int i; + + initPQExpBuffer(&cmd_str); + + appendShellString(&cmd_str, cmd->path); + + for (i = 1; i < cmd->argc; i++) + { + appendPQExpBufferChar(&cmd_str, ' '); + appendShellString(&cmd_str, cmd->argv[i]); + } + + if (cmd->silent) + { + appendPQExpBufferStr(&cmd_str, " > "); + appendShellString(&cmd_str, DEVNULL); + } + + return system(cmd_str.data); +} +#endif + +PCommand * +pcommand_init(char *path, char *command) +{ + PCommand *cmd = palloc(sizeof(PCommand)); + + cmd->path = path; + cmd->command = command; + + cmd->argc = 0; + cmd->nalloc = 2; + cmd->argv = palloc_array(char *, cmd->nalloc); + + pcommand_append_arg(cmd, command); + + cmd->stdin_fd = -1; + cmd->stdout_fd = -1; + cmd->stderr_fd = -1; + + return cmd; +} + +int +pcommand_exec(PCommand * cmd) +{ + if (cmd->stdin_fd >= 0 && dup2(cmd->stdin_fd, STDIN_FILENO) < 0) + return errno; + if (cmd->stdout_fd >= 0 && dup2(cmd->stdout_fd, STDOUT_FILENO) < 0) + return errno; + if (cmd->stderr_fd >= 0 && dup2(cmd->stderr_fd, STDERR_FILENO) < 0) + return errno; + + execv(cmd->path, cmd->argv); + return errno; +} + + +#ifndef WIN32 +int +pcommand_wait(PCommand * cmd) +{ + int status = -1; + + /* This should have some kind of timeout... do we have that somewhere else */ + while (true) + { + if (waitpid(cmd->pid, &status, 0) < 0) + { + if (errno == EINTR) + continue; + return status; + } + else + { + return status; + } + } +} +#endif + +/* + * psystem() is a replacement for system(3) that avoids the use of a shell + * It's not a full replacement yet since it doesn't support pipes. For now it + * should be used only in places where is known that no pipe is requried and the + * commands being call are known commands. + * This funciton shouldn't be used as a replacement for system(3) call in places + * like RestoreArchivedFile() or shell_archive_file() + * + * The return value is always the return of the executed command + */ +int +psystem(PCommand * cmd) +{ +#ifdef WIN32 + return pcommand_win(cmd); +#else + + cmd->pid = fork(); + if (cmd->pid < 0) + return -1; + + if (cmd->pid == 0) + { + if (cmd->silent) + cmd->stdout_fd = open(DEVNULL, O_WRONLY); + + errno = pcommand_exec(cmd); + _exit(127); + } + + return pcommand_wait(cmd); +#endif +} diff --git a/src/include/common/pg_exec.h b/src/include/common/pg_exec.h new file mode 100644 index 00000000000..d987449c615 --- /dev/null +++ b/src/include/common/pg_exec.h @@ -0,0 +1,39 @@ +/*------------------------------------------------------------------------- + * Exec commands for backend/frontend programs + * + * Copyright (c) 2018-2026, PostgreSQL Global Development Group + * + * src/include/common/pg_exec.h + * + *------------------------------------------------------------------------- + */ +#ifndef COMMON_PG_EXEC_H +#define COMMON_PG_EXEC_H + +typedef struct PCommand +{ + pid_t pid; + char *path; + char *command; + char *command_win; + char **argv; + int argc; + int nalloc; + bool silent; + int stdin_fd; + int stdout_fd; + int stderr_fd; +} PCommand; + +extern PCommand * pcommand_init(char *path, char *command); +extern void pcommand_append_arg(PCommand * cmd, const char *arg); +#ifdef WIN32 +extern int pcommand_win(PCommand *cmd); +#endif +#ifndef WIN32 +extern int pcommand_wait(PCommand *cmd); +#endif +extern int pcommand_exec(PCommand * cmd); +extern int psystem(PCommand * command); + +#endif diff --git a/src/include/miscadmin.h b/src/include/miscadmin.h index 0fc59af02b9..41d4c62c531 100644 --- a/src/include/miscadmin.h +++ b/src/include/miscadmin.h @@ -512,7 +512,6 @@ extern PGDLLIMPORT ProcessingMode Mode; #define INIT_PG_LOAD_SESSION_LIBS 0x0001 #define INIT_PG_OVERRIDE_ALLOW_CONNS 0x0002 #define INIT_PG_OVERRIDE_ROLE_LOGIN 0x0004 -extern void pg_split_opts(char **argv, int *argcp, const char *optstr); extern void InitializeMaxBackends(void); extern void InitializeFastPathLocks(void); extern void InitPostgres(const char *in_dbname, Oid dboid, diff --git a/src/include/port.h b/src/include/port.h index 172acf7d02f..82d900fcbc4 100644 --- a/src/include/port.h +++ b/src/include/port.h @@ -140,6 +140,8 @@ extern int find_my_exec(const char *argv0, char *retpath); extern int find_other_exec(const char *argv0, const char *target, const char *versionstr, char *retpath); extern char *pipe_read_line(char *cmd); +extern void pg_split_opts(char **argv, int *argcp, const char *optstr); + /* Doesn't belong here, but this is used with find_other_exec(), so... */ #define PG_BACKEND_VERSIONSTR "postgres (PostgreSQL) " PG_VERSION "\n" -- 2.53.0 --=-=-=--