LCOV - code coverage report
Current view: top level - contrib/basebackup_to_shell - basebackup_to_shell.c (source / functions) Coverage Total Hit
Test: PostgreSQL 20devel Lines: 86.5 % 104 90
Test Date: 2026-07-03 19:57:34 Functions: 100.0 % 14 14
Legend: Lines:     hit not hit
Branches: + taken - not taken # not executed
Branches: 52.9 % 70 37

             Branch data     Line data    Source code
       1                 :             : /*-------------------------------------------------------------------------
       2                 :             :  *
       3                 :             :  * basebackup_to_shell.c
       4                 :             :  *    target base backup files to a shell command
       5                 :             :  *
       6                 :             :  * Copyright (c) 2016-2026, PostgreSQL Global Development Group
       7                 :             :  *
       8                 :             :  *    contrib/basebackup_to_shell/basebackup_to_shell.c
       9                 :             :  *-------------------------------------------------------------------------
      10                 :             :  */
      11                 :             : #include "postgres.h"
      12                 :             : 
      13                 :             : #include "access/xact.h"
      14                 :             : #include "backup/basebackup_target.h"
      15                 :             : #include "common/percentrepl.h"
      16                 :             : #include "miscadmin.h"
      17                 :             : #include "storage/fd.h"
      18                 :             : #include "utils/acl.h"
      19                 :             : #include "utils/guc.h"
      20                 :             : 
      21                 :           1 : PG_MODULE_MAGIC_EXT(
      22                 :             :                     .name = "basebackup_to_shell",
      23                 :             :                     .version = PG_VERSION
      24                 :             : );
      25                 :             : 
      26                 :             : typedef struct bbsink_shell
      27                 :             : {
      28                 :             :     /* Common information for all types of sink. */
      29                 :             :     bbsink      base;
      30                 :             : 
      31                 :             :     /* User-supplied target detail string. */
      32                 :             :     char       *target_detail;
      33                 :             : 
      34                 :             :     /* Shell command pattern being used for this backup. */
      35                 :             :     char       *shell_command;
      36                 :             : 
      37                 :             :     /* The command that is currently running. */
      38                 :             :     char       *current_command;
      39                 :             : 
      40                 :             :     /* Pipe to the running command. */
      41                 :             :     FILE       *pipe;
      42                 :             : } bbsink_shell;
      43                 :             : 
      44                 :             : static void *shell_check_detail(char *target, char *target_detail);
      45                 :             : static bbsink *shell_get_sink(bbsink *next_sink, void *detail_arg);
      46                 :             : 
      47                 :             : static void bbsink_shell_begin_archive(bbsink *sink,
      48                 :             :                                        const char *archive_name);
      49                 :             : static void bbsink_shell_archive_contents(bbsink *sink, size_t len);
      50                 :             : static void bbsink_shell_end_archive(bbsink *sink);
      51                 :             : static void bbsink_shell_begin_manifest(bbsink *sink);
      52                 :             : static void bbsink_shell_manifest_contents(bbsink *sink, size_t len);
      53                 :             : static void bbsink_shell_end_manifest(bbsink *sink);
      54                 :             : 
      55                 :             : static const bbsink_ops bbsink_shell_ops = {
      56                 :             :     .begin_backup = bbsink_forward_begin_backup,
      57                 :             :     .begin_archive = bbsink_shell_begin_archive,
      58                 :             :     .archive_contents = bbsink_shell_archive_contents,
      59                 :             :     .end_archive = bbsink_shell_end_archive,
      60                 :             :     .begin_manifest = bbsink_shell_begin_manifest,
      61                 :             :     .manifest_contents = bbsink_shell_manifest_contents,
      62                 :             :     .end_manifest = bbsink_shell_end_manifest,
      63                 :             :     .end_backup = bbsink_forward_end_backup,
      64                 :             :     .cleanup = bbsink_forward_cleanup
      65                 :             : };
      66                 :             : 
      67                 :             : static char *shell_command = "";
      68                 :             : static char *shell_required_role = "";
      69                 :             : 
      70                 :             : void
      71                 :           1 : _PG_init(void)
      72                 :             : {
      73                 :           1 :     DefineCustomStringVariable("basebackup_to_shell.command",
      74                 :             :                                "Shell command to be executed for each backup file.",
      75                 :             :                                NULL,
      76                 :             :                                &shell_command,
      77                 :             :                                "",
      78                 :             :                                PGC_SIGHUP,
      79                 :             :                                0,
      80                 :             :                                NULL, NULL, NULL);
      81                 :             : 
      82                 :           1 :     DefineCustomStringVariable("basebackup_to_shell.required_role",
      83                 :             :                                "Backup user must be a member of this role to use shell backup target.",
      84                 :             :                                NULL,
      85                 :             :                                &shell_required_role,
      86                 :             :                                "",
      87                 :             :                                PGC_SIGHUP,
      88                 :             :                                0,
      89                 :             :                                NULL, NULL, NULL);
      90                 :             : 
      91                 :           1 :     MarkGUCPrefixReserved("basebackup_to_shell");
      92                 :             : 
      93                 :           1 :     BaseBackupAddTarget("shell", shell_check_detail, shell_get_sink);
      94                 :           1 : }
      95                 :             : 
      96                 :             : /*
      97                 :             :  * We choose to defer sanity checking until shell_get_sink(), and so
      98                 :             :  * just pass the target detail through without doing anything. However, we do
      99                 :             :  * permissions checks here, before any real work has been done.
     100                 :             :  */
     101                 :             : static void *
     102                 :           6 : shell_check_detail(char *target, char *target_detail)
     103                 :             : {
     104         [ +  + ]:           6 :     if (shell_required_role[0] != '\0')
     105                 :             :     {
     106                 :             :         Oid         roleid;
     107                 :             : 
     108                 :           3 :         StartTransactionCommand();
     109                 :           3 :         roleid = get_role_oid(shell_required_role, true);
     110         [ +  + ]:           3 :         if (!has_privs_of_role(GetUserId(), roleid))
     111         [ +  - ]:           1 :             ereport(ERROR,
     112                 :             :                     (errcode(ERRCODE_INSUFFICIENT_PRIVILEGE),
     113                 :             :                      errmsg("permission denied to use basebackup_to_shell")));
     114                 :           2 :         CommitTransactionCommand();
     115                 :             :     }
     116                 :             : 
     117                 :           5 :     return target_detail;
     118                 :             : }
     119                 :             : 
     120                 :             : /*
     121                 :             :  * Set up a bbsink to implement this base backup target.
     122                 :             :  *
     123                 :             :  * This is also a convenient place to sanity check that a target detail was
     124                 :             :  * given if and only if %d is present.
     125                 :             :  */
     126                 :             : static bbsink *
     127                 :           5 : shell_get_sink(bbsink *next_sink, void *detail_arg)
     128                 :             : {
     129                 :             :     bbsink_shell *sink;
     130                 :           5 :     bool        has_detail_escape = false;
     131                 :             :     char       *c;
     132                 :             : 
     133                 :             :     /*
     134                 :             :      * Set up the bbsink.
     135                 :             :      *
     136                 :             :      * We remember the current value of basebackup_to_shell.shell_command to
     137                 :             :      * be certain that it can't change under us during the backup.
     138                 :             :      */
     139                 :           5 :     sink = palloc0_object(bbsink_shell);
     140                 :           5 :     *((const bbsink_ops **) &sink->base.bbs_ops) = &bbsink_shell_ops;
     141                 :           5 :     sink->base.bbs_next = next_sink;
     142                 :           5 :     sink->target_detail = detail_arg;
     143                 :           5 :     sink->shell_command = pstrdup(shell_command);
     144                 :             : 
     145                 :             :     /* Reject an empty shell command. */
     146         [ +  + ]:           5 :     if (sink->shell_command[0] == '\0')
     147         [ +  - ]:           1 :         ereport(ERROR,
     148                 :             :                 errcode(ERRCODE_INVALID_PARAMETER_VALUE),
     149                 :             :                 errmsg("shell command for backup is not configured"));
     150                 :             : 
     151                 :             :     /* Determine whether the shell command we're using contains %d. */
     152         [ +  + ]:         412 :     for (c = sink->shell_command; *c != '\0'; ++c)
     153                 :             :     {
     154   [ +  +  +  - ]:         408 :         if (c[0] == '%' && c[1] != '\0')
     155                 :             :         {
     156         [ +  + ]:           6 :             if (c[1] == 'd')
     157                 :           2 :                 has_detail_escape = true;
     158                 :           6 :             ++c;
     159                 :             :         }
     160                 :             :     }
     161                 :             : 
     162                 :             :     /* There should be a target detail if %d was used, and not otherwise. */
     163   [ +  +  +  + ]:           4 :     if (has_detail_escape && sink->target_detail == NULL)
     164         [ +  - ]:           1 :         ereport(ERROR,
     165                 :             :                 (errcode(ERRCODE_INVALID_PARAMETER_VALUE),
     166                 :             :                  errmsg("a target detail is required because the configured command includes %%d"),
     167                 :             :                  errhint("Try \"pg_basebackup --target shell:DETAIL ...\"")));
     168   [ +  +  +  + ]:           3 :     else if (!has_detail_escape && sink->target_detail != NULL)
     169         [ +  - ]:           1 :         ereport(ERROR,
     170                 :             :                 (errcode(ERRCODE_INVALID_PARAMETER_VALUE),
     171                 :             :                  errmsg("a target detail is not permitted because the configured command does not include %%d")));
     172                 :             : 
     173                 :             :     /*
     174                 :             :      * Since we're passing the string provided by the user to popen(), it will
     175                 :             :      * be interpreted by the shell, which is a potential security
     176                 :             :      * vulnerability, since the user invoking this module is not necessarily a
     177                 :             :      * superuser. To stay out of trouble, we must disallow any shell
     178                 :             :      * metacharacters here; to be conservative and keep things simple, we
     179                 :             :      * allow only alphanumerics.
     180                 :             :      */
     181         [ +  + ]:           2 :     if (sink->target_detail != NULL)
     182                 :             :     {
     183                 :             :         char       *d;
     184                 :           1 :         bool        scary = false;
     185                 :             : 
     186         [ +  + ]:           4 :         for (d = sink->target_detail; *d != '\0'; ++d)
     187                 :             :         {
     188   [ +  -  +  - ]:           3 :             if (*d >= 'a' && *d <= 'z')
     189                 :           3 :                 continue;
     190   [ #  #  #  # ]:           0 :             if (*d >= 'A' && *d <= 'Z')
     191                 :           0 :                 continue;
     192   [ #  #  #  # ]:           0 :             if (*d >= '0' && *d <= '9')
     193                 :           0 :                 continue;
     194                 :           0 :             scary = true;
     195                 :           0 :             break;
     196                 :             :         }
     197                 :             : 
     198         [ -  + ]:           1 :         if (scary)
     199         [ #  # ]:           0 :             ereport(ERROR,
     200                 :             :                     errcode(ERRCODE_INVALID_PARAMETER_VALUE),
     201                 :             :                     errmsg("target detail must contain only alphanumeric characters"));
     202                 :             :     }
     203                 :             : 
     204                 :           2 :     return &sink->base;
     205                 :             : }
     206                 :             : 
     207                 :             : /*
     208                 :             :  * Construct the exact shell command that we're actually going to run,
     209                 :             :  * making substitutions as appropriate for escape sequences.
     210                 :             :  */
     211                 :             : static char *
     212                 :           4 : shell_construct_command(const char *base_command, const char *filename,
     213                 :             :                         const char *target_detail)
     214                 :             : {
     215                 :           4 :     return replace_percent_placeholders(base_command, "basebackup_to_shell.command",
     216                 :             :                                         "df", target_detail, filename);
     217                 :             : }
     218                 :             : 
     219                 :             : /*
     220                 :             :  * Finish executing the shell command once all data has been written.
     221                 :             :  */
     222                 :             : static void
     223                 :           4 : shell_finish_command(bbsink_shell *sink)
     224                 :             : {
     225                 :             :     int         pclose_rc;
     226                 :             : 
     227                 :             :     /* There should be a command running. */
     228                 :             :     Assert(sink->current_command != NULL);
     229                 :             :     Assert(sink->pipe != NULL);
     230                 :             : 
     231                 :             :     /* Close down the pipe we opened. */
     232                 :           4 :     pclose_rc = ClosePipeStream(sink->pipe);
     233         [ -  + ]:           4 :     if (pclose_rc == -1)
     234         [ #  # ]:           0 :         ereport(ERROR,
     235                 :             :                 (errcode_for_file_access(),
     236                 :             :                  errmsg("could not close pipe to external command: %m")));
     237         [ -  + ]:           4 :     else if (pclose_rc != 0)
     238                 :             :     {
     239         [ #  # ]:           0 :         ereport(ERROR,
     240                 :             :                 (errcode(ERRCODE_EXTERNAL_ROUTINE_EXCEPTION),
     241                 :             :                  errmsg("shell command \"%s\" failed",
     242                 :             :                         sink->current_command),
     243                 :             :                  errdetail_internal("%s", wait_result_to_str(pclose_rc))));
     244                 :             :     }
     245                 :             : 
     246                 :             :     /* Clean up. */
     247                 :           4 :     sink->pipe = NULL;
     248                 :           4 :     pfree(sink->current_command);
     249                 :           4 :     sink->current_command = NULL;
     250                 :           4 : }
     251                 :             : 
     252                 :             : /*
     253                 :             :  * Start up the shell command, substituting %f in for the current filename.
     254                 :             :  */
     255                 :             : static void
     256                 :           4 : shell_run_command(bbsink_shell *sink, const char *filename)
     257                 :             : {
     258                 :             :     /* There should not be anything already running. */
     259                 :             :     Assert(sink->current_command == NULL);
     260                 :             :     Assert(sink->pipe == NULL);
     261                 :             : 
     262                 :             :     /* Construct a suitable command. */
     263                 :           8 :     sink->current_command = shell_construct_command(sink->shell_command,
     264                 :             :                                                     filename,
     265                 :           4 :                                                     sink->target_detail);
     266                 :             : 
     267                 :             :     /* Run it. */
     268                 :           4 :     sink->pipe = OpenPipeStream(sink->current_command, PG_BINARY_W);
     269         [ -  + ]:           4 :     if (sink->pipe == NULL)
     270         [ #  # ]:           0 :         ereport(ERROR,
     271                 :             :                 (errcode_for_file_access(),
     272                 :             :                  errmsg("could not execute command \"%s\": %m",
     273                 :             :                         sink->current_command)));
     274                 :           4 : }
     275                 :             : 
     276                 :             : /*
     277                 :             :  * Send accumulated data to the running shell command.
     278                 :             :  */
     279                 :             : static void
     280                 :        5648 : shell_send_data(bbsink_shell *sink, size_t len)
     281                 :             : {
     282                 :             :     /* There should be a command running. */
     283                 :             :     Assert(sink->current_command != NULL);
     284                 :             :     Assert(sink->pipe != NULL);
     285                 :             : 
     286                 :             :     /* Try to write the data. */
     287   [ +  -  -  + ]:       11296 :     if (fwrite(sink->base.bbs_buffer, len, 1, sink->pipe) != 1 ||
     288                 :        5648 :         ferror(sink->pipe))
     289                 :             :     {
     290         [ #  # ]:           0 :         if (errno == EPIPE)
     291                 :             :         {
     292                 :             :             /*
     293                 :             :              * The error we're about to throw would shut down the command
     294                 :             :              * anyway, but we may get a more meaningful error message by doing
     295                 :             :              * this. If not, we'll fall through to the generic error below.
     296                 :             :              */
     297                 :           0 :             shell_finish_command(sink);
     298                 :           0 :             errno = EPIPE;
     299                 :             :         }
     300         [ #  # ]:           0 :         ereport(ERROR,
     301                 :             :                 (errcode_for_file_access(),
     302                 :             :                  errmsg("could not write to shell backup program: %m")));
     303                 :             :     }
     304                 :        5648 : }
     305                 :             : 
     306                 :             : /*
     307                 :             :  * At start of archive, start up the shell command and forward to next sink.
     308                 :             :  */
     309                 :             : static void
     310                 :           2 : bbsink_shell_begin_archive(bbsink *sink, const char *archive_name)
     311                 :             : {
     312                 :           2 :     bbsink_shell *mysink = (bbsink_shell *) sink;
     313                 :             : 
     314                 :           2 :     shell_run_command(mysink, archive_name);
     315                 :           2 :     bbsink_forward_begin_archive(sink, archive_name);
     316                 :           2 : }
     317                 :             : 
     318                 :             : /*
     319                 :             :  * Send archive contents to command's stdin and forward to next sink.
     320                 :             :  */
     321                 :             : static void
     322                 :        5638 : bbsink_shell_archive_contents(bbsink *sink, size_t len)
     323                 :             : {
     324                 :        5638 :     bbsink_shell *mysink = (bbsink_shell *) sink;
     325                 :             : 
     326                 :        5638 :     shell_send_data(mysink, len);
     327                 :        5638 :     bbsink_forward_archive_contents(sink, len);
     328                 :        5638 : }
     329                 :             : 
     330                 :             : /*
     331                 :             :  * At end of archive, shut down the shell command and forward to next sink.
     332                 :             :  */
     333                 :             : static void
     334                 :           2 : bbsink_shell_end_archive(bbsink *sink)
     335                 :             : {
     336                 :           2 :     bbsink_shell *mysink = (bbsink_shell *) sink;
     337                 :             : 
     338                 :           2 :     shell_finish_command(mysink);
     339                 :           2 :     bbsink_forward_end_archive(sink);
     340                 :           2 : }
     341                 :             : 
     342                 :             : /*
     343                 :             :  * At start of manifest, start up the shell command and forward to next sink.
     344                 :             :  */
     345                 :             : static void
     346                 :           2 : bbsink_shell_begin_manifest(bbsink *sink)
     347                 :             : {
     348                 :           2 :     bbsink_shell *mysink = (bbsink_shell *) sink;
     349                 :             : 
     350                 :           2 :     shell_run_command(mysink, "backup_manifest");
     351                 :           2 :     bbsink_forward_begin_manifest(sink);
     352                 :           2 : }
     353                 :             : 
     354                 :             : /*
     355                 :             :  * Send manifest contents to command's stdin and forward to next sink.
     356                 :             :  */
     357                 :             : static void
     358                 :          10 : bbsink_shell_manifest_contents(bbsink *sink, size_t len)
     359                 :             : {
     360                 :          10 :     bbsink_shell *mysink = (bbsink_shell *) sink;
     361                 :             : 
     362                 :          10 :     shell_send_data(mysink, len);
     363                 :          10 :     bbsink_forward_manifest_contents(sink, len);
     364                 :          10 : }
     365                 :             : 
     366                 :             : /*
     367                 :             :  * At end of manifest, shut down the shell command and forward to next sink.
     368                 :             :  */
     369                 :             : static void
     370                 :           2 : bbsink_shell_end_manifest(bbsink *sink)
     371                 :             : {
     372                 :           2 :     bbsink_shell *mysink = (bbsink_shell *) sink;
     373                 :             : 
     374                 :           2 :     shell_finish_command(mysink);
     375                 :           2 :     bbsink_forward_end_manifest(sink);
     376                 :           2 : }
        

Generated by: LCOV version 2.0-1