LCOV - code coverage report
Current view: top level - contrib/basebackup_to_shell - basebackup_to_shell.c (source / functions) Coverage Total Hit
Test: Code coverage Lines: 0.0 % 125 0
Test Date: 2026-01-26 10:56:24 Functions: 0.0 % 14 0
Legend: Lines:     hit not hit

            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            0 : 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            0 : _PG_init(void)
      72              : {
      73            0 :         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            0 :         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            0 :         MarkGUCPrefixReserved("basebackup_to_shell");
      92              : 
      93            0 :         BaseBackupAddTarget("shell", shell_check_detail, shell_get_sink);
      94            0 : }
      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            0 : shell_check_detail(char *target, char *target_detail)
     103              : {
     104            0 :         if (shell_required_role[0] != '\0')
     105              :         {
     106            0 :                 Oid                     roleid;
     107              : 
     108            0 :                 StartTransactionCommand();
     109            0 :                 roleid = get_role_oid(shell_required_role, true);
     110            0 :                 if (!has_privs_of_role(GetUserId(), roleid))
     111            0 :                         ereport(ERROR,
     112              :                                         (errcode(ERRCODE_INSUFFICIENT_PRIVILEGE),
     113              :                                          errmsg("permission denied to use basebackup_to_shell")));
     114            0 :                 CommitTransactionCommand();
     115            0 :         }
     116              : 
     117            0 :         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            0 : shell_get_sink(bbsink *next_sink, void *detail_arg)
     128              : {
     129            0 :         bbsink_shell *sink;
     130            0 :         bool            has_detail_escape = false;
     131            0 :         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            0 :         sink = palloc0_object(bbsink_shell);
     140            0 :         *((const bbsink_ops **) &sink->base.bbs_ops) = &bbsink_shell_ops;
     141            0 :         sink->base.bbs_next = next_sink;
     142            0 :         sink->target_detail = detail_arg;
     143            0 :         sink->shell_command = pstrdup(shell_command);
     144              : 
     145              :         /* Reject an empty shell command. */
     146            0 :         if (sink->shell_command[0] == '\0')
     147            0 :                 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            0 :         for (c = sink->shell_command; *c != '\0'; ++c)
     153              :         {
     154            0 :                 if (c[0] == '%' && c[1] != '\0')
     155              :                 {
     156            0 :                         if (c[1] == 'd')
     157            0 :                                 has_detail_escape = true;
     158            0 :                         ++c;
     159            0 :                 }
     160            0 :         }
     161              : 
     162              :         /* There should be a target detail if %d was used, and not otherwise. */
     163            0 :         if (has_detail_escape && sink->target_detail == NULL)
     164            0 :                 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            0 :         else if (!has_detail_escape && sink->target_detail != NULL)
     169            0 :                 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            0 :         if (sink->target_detail != NULL)
     182              :         {
     183            0 :                 char       *d;
     184            0 :                 bool            scary = false;
     185              : 
     186            0 :                 for (d = sink->target_detail; *d != '\0'; ++d)
     187              :                 {
     188            0 :                         if (*d >= 'a' && *d <= 'z')
     189            0 :                                 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            0 :                 if (scary)
     199            0 :                         ereport(ERROR,
     200              :                                         errcode(ERRCODE_INVALID_PARAMETER_VALUE),
     201              :                                         errmsg("target detail must contain only alphanumeric characters"));
     202            0 :         }
     203              : 
     204            0 :         return &sink->base;
     205            0 : }
     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            0 : shell_construct_command(const char *base_command, const char *filename,
     213              :                                                 const char *target_detail)
     214              : {
     215            0 :         return replace_percent_placeholders(base_command, "basebackup_to_shell.command",
     216            0 :                                                                                 "df", target_detail, filename);
     217              : }
     218              : 
     219              : /*
     220              :  * Finish executing the shell command once all data has been written.
     221              :  */
     222              : static void
     223            0 : shell_finish_command(bbsink_shell *sink)
     224              : {
     225            0 :         int                     pclose_rc;
     226              : 
     227              :         /* There should be a command running. */
     228            0 :         Assert(sink->current_command != NULL);
     229            0 :         Assert(sink->pipe != NULL);
     230              : 
     231              :         /* Close down the pipe we opened. */
     232            0 :         pclose_rc = ClosePipeStream(sink->pipe);
     233            0 :         if (pclose_rc == -1)
     234            0 :                 ereport(ERROR,
     235              :                                 (errcode_for_file_access(),
     236              :                                  errmsg("could not close pipe to external command: %m")));
     237            0 :         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            0 :         }
     245              : 
     246              :         /* Clean up. */
     247            0 :         sink->pipe = NULL;
     248            0 :         pfree(sink->current_command);
     249            0 :         sink->current_command = NULL;
     250            0 : }
     251              : 
     252              : /*
     253              :  * Start up the shell command, substituting %f in for the current filename.
     254              :  */
     255              : static void
     256            0 : shell_run_command(bbsink_shell *sink, const char *filename)
     257              : {
     258              :         /* There should not be anything already running. */
     259            0 :         Assert(sink->current_command == NULL);
     260            0 :         Assert(sink->pipe == NULL);
     261              : 
     262              :         /* Construct a suitable command. */
     263            0 :         sink->current_command = shell_construct_command(sink->shell_command,
     264            0 :                                                                                                         filename,
     265            0 :                                                                                                         sink->target_detail);
     266              : 
     267              :         /* Run it. */
     268            0 :         sink->pipe = OpenPipeStream(sink->current_command, PG_BINARY_W);
     269            0 :         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            0 : }
     275              : 
     276              : /*
     277              :  * Send accumulated data to the running shell command.
     278              :  */
     279              : static void
     280            0 : shell_send_data(bbsink_shell *sink, size_t len)
     281              : {
     282              :         /* There should be a command running. */
     283            0 :         Assert(sink->current_command != NULL);
     284            0 :         Assert(sink->pipe != NULL);
     285              : 
     286              :         /* Try to write the data. */
     287            0 :         if (fwrite(sink->base.bbs_buffer, len, 1, sink->pipe) != 1 ||
     288            0 :                 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            0 :                 }
     300            0 :                 ereport(ERROR,
     301              :                                 (errcode_for_file_access(),
     302              :                                  errmsg("could not write to shell backup program: %m")));
     303            0 :         }
     304            0 : }
     305              : 
     306              : /*
     307              :  * At start of archive, start up the shell command and forward to next sink.
     308              :  */
     309              : static void
     310            0 : bbsink_shell_begin_archive(bbsink *sink, const char *archive_name)
     311              : {
     312            0 :         bbsink_shell *mysink = (bbsink_shell *) sink;
     313              : 
     314            0 :         shell_run_command(mysink, archive_name);
     315            0 :         bbsink_forward_begin_archive(sink, archive_name);
     316            0 : }
     317              : 
     318              : /*
     319              :  * Send archive contents to command's stdin and forward to next sink.
     320              :  */
     321              : static void
     322            0 : bbsink_shell_archive_contents(bbsink *sink, size_t len)
     323              : {
     324            0 :         bbsink_shell *mysink = (bbsink_shell *) sink;
     325              : 
     326            0 :         shell_send_data(mysink, len);
     327            0 :         bbsink_forward_archive_contents(sink, len);
     328            0 : }
     329              : 
     330              : /*
     331              :  * At end of archive, shut down the shell command and forward to next sink.
     332              :  */
     333              : static void
     334            0 : bbsink_shell_end_archive(bbsink *sink)
     335              : {
     336            0 :         bbsink_shell *mysink = (bbsink_shell *) sink;
     337              : 
     338            0 :         shell_finish_command(mysink);
     339            0 :         bbsink_forward_end_archive(sink);
     340            0 : }
     341              : 
     342              : /*
     343              :  * At start of manifest, start up the shell command and forward to next sink.
     344              :  */
     345              : static void
     346            0 : bbsink_shell_begin_manifest(bbsink *sink)
     347              : {
     348            0 :         bbsink_shell *mysink = (bbsink_shell *) sink;
     349              : 
     350            0 :         shell_run_command(mysink, "backup_manifest");
     351            0 :         bbsink_forward_begin_manifest(sink);
     352            0 : }
     353              : 
     354              : /*
     355              :  * Send manifest contents to command's stdin and forward to next sink.
     356              :  */
     357              : static void
     358            0 : bbsink_shell_manifest_contents(bbsink *sink, size_t len)
     359              : {
     360            0 :         bbsink_shell *mysink = (bbsink_shell *) sink;
     361              : 
     362            0 :         shell_send_data(mysink, len);
     363            0 :         bbsink_forward_manifest_contents(sink, len);
     364            0 : }
     365              : 
     366              : /*
     367              :  * At end of manifest, shut down the shell command and forward to next sink.
     368              :  */
     369              : static void
     370            0 : bbsink_shell_end_manifest(bbsink *sink)
     371              : {
     372            0 :         bbsink_shell *mysink = (bbsink_shell *) sink;
     373              : 
     374            0 :         shell_finish_command(mysink);
     375            0 :         bbsink_forward_end_manifest(sink);
     376            0 : }
        

Generated by: LCOV version 2.3.2-1