LCOV - code coverage report
Current view: top level - contrib/basebackup_to_shell - basebackup_to_shell.c (source / functions) Hit Total Coverage
Test: PostgreSQL 18devel Lines: 90 104 86.5 %
Date: 2025-04-24 12:15:10 Functions: 14 14 100.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-2025, 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           2 : 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           2 : _PG_init(void)
      72             : {
      73           2 :     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           2 :     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           2 :     MarkGUCPrefixReserved("basebackup_to_shell");
      92             : 
      93           2 :     BaseBackupAddTarget("shell", shell_check_detail, shell_get_sink);
      94           2 : }
      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          12 : shell_check_detail(char *target, char *target_detail)
     103             : {
     104          12 :     if (shell_required_role[0] != '\0')
     105             :     {
     106             :         Oid         roleid;
     107             : 
     108           6 :         StartTransactionCommand();
     109           6 :         roleid = get_role_oid(shell_required_role, true);
     110           6 :         if (!has_privs_of_role(GetUserId(), roleid))
     111           2 :             ereport(ERROR,
     112             :                     (errcode(ERRCODE_INSUFFICIENT_PRIVILEGE),
     113             :                      errmsg("permission denied to use basebackup_to_shell")));
     114           4 :         CommitTransactionCommand();
     115             :     }
     116             : 
     117          10 :     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          10 : shell_get_sink(bbsink *next_sink, void *detail_arg)
     128             : {
     129             :     bbsink_shell *sink;
     130          10 :     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          10 :     sink = palloc0(sizeof(bbsink_shell));
     140          10 :     *((const bbsink_ops **) &sink->base.bbs_ops) = &bbsink_shell_ops;
     141          10 :     sink->base.bbs_next = next_sink;
     142          10 :     sink->target_detail = detail_arg;
     143          10 :     sink->shell_command = pstrdup(shell_command);
     144             : 
     145             :     /* Reject an empty shell command. */
     146          10 :     if (sink->shell_command[0] == '\0')
     147           2 :         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         824 :     for (c = sink->shell_command; *c != '\0'; ++c)
     153             :     {
     154         816 :         if (c[0] == '%' && c[1] != '\0')
     155             :         {
     156          12 :             if (c[1] == 'd')
     157           4 :                 has_detail_escape = true;
     158          12 :             ++c;
     159             :         }
     160             :     }
     161             : 
     162             :     /* There should be a target detail if %d was used, and not otherwise. */
     163           8 :     if (has_detail_escape && sink->target_detail == NULL)
     164           2 :         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           6 :     else if (!has_detail_escape && sink->target_detail != NULL)
     169           2 :         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           4 :     if (sink->target_detail != NULL)
     182             :     {
     183             :         char       *d;
     184           2 :         bool        scary = false;
     185             : 
     186           8 :         for (d = sink->target_detail; *d != '\0'; ++d)
     187             :         {
     188           6 :             if (*d >= 'a' && *d <= 'z')
     189           6 :                 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           2 :         if (scary)
     199           0 :             ereport(ERROR,
     200             :                     errcode(ERRCODE_INVALID_PARAMETER_VALUE),
     201             :                     errmsg("target detail must contain only alphanumeric characters"));
     202             :     }
     203             : 
     204           4 :     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           8 : shell_construct_command(const char *base_command, const char *filename,
     213             :                         const char *target_detail)
     214             : {
     215           8 :     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           8 : 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           8 :     pclose_rc = ClosePipeStream(sink->pipe);
     233           8 :     if (pclose_rc == -1)
     234           0 :         ereport(ERROR,
     235             :                 (errcode_for_file_access(),
     236             :                  errmsg("could not close pipe to external command: %m")));
     237           8 :     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           8 :     sink->pipe = NULL;
     248           8 :     pfree(sink->current_command);
     249           8 :     sink->current_command = NULL;
     250           8 : }
     251             : 
     252             : /*
     253             :  * Start up the shell command, substituting %f in for the current filename.
     254             :  */
     255             : static void
     256           8 : 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          16 :     sink->current_command = shell_construct_command(sink->shell_command,
     264             :                                                     filename,
     265           8 :                                                     sink->target_detail);
     266             : 
     267             :     /* Run it. */
     268           8 :     sink->pipe = OpenPipeStream(sink->current_command, PG_BINARY_W);
     269           8 :     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           8 : }
     275             : 
     276             : /*
     277             :  * Send accumulated data to the running shell command.
     278             :  */
     279             : static void
     280       10854 : 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       21708 :     if (fwrite(sink->base.bbs_buffer, len, 1, sink->pipe) != 1 ||
     288       10854 :         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       10854 : }
     305             : 
     306             : /*
     307             :  * At start of archive, start up the shell command and forward to next sink.
     308             :  */
     309             : static void
     310           4 : bbsink_shell_begin_archive(bbsink *sink, const char *archive_name)
     311             : {
     312           4 :     bbsink_shell *mysink = (bbsink_shell *) sink;
     313             : 
     314           4 :     shell_run_command(mysink, archive_name);
     315           4 :     bbsink_forward_begin_archive(sink, archive_name);
     316           4 : }
     317             : 
     318             : /*
     319             :  * Send archive contents to command's stdin and forward to next sink.
     320             :  */
     321             : static void
     322       10834 : bbsink_shell_archive_contents(bbsink *sink, size_t len)
     323             : {
     324       10834 :     bbsink_shell *mysink = (bbsink_shell *) sink;
     325             : 
     326       10834 :     shell_send_data(mysink, len);
     327       10834 :     bbsink_forward_archive_contents(sink, len);
     328       10834 : }
     329             : 
     330             : /*
     331             :  * At end of archive, shut down the shell command and forward to next sink.
     332             :  */
     333             : static void
     334           4 : bbsink_shell_end_archive(bbsink *sink)
     335             : {
     336           4 :     bbsink_shell *mysink = (bbsink_shell *) sink;
     337             : 
     338           4 :     shell_finish_command(mysink);
     339           4 :     bbsink_forward_end_archive(sink);
     340           4 : }
     341             : 
     342             : /*
     343             :  * At start of manifest, start up the shell command and forward to next sink.
     344             :  */
     345             : static void
     346           4 : bbsink_shell_begin_manifest(bbsink *sink)
     347             : {
     348           4 :     bbsink_shell *mysink = (bbsink_shell *) sink;
     349             : 
     350           4 :     shell_run_command(mysink, "backup_manifest");
     351           4 :     bbsink_forward_begin_manifest(sink);
     352           4 : }
     353             : 
     354             : /*
     355             :  * Send manifest contents to command's stdin and forward to next sink.
     356             :  */
     357             : static void
     358          20 : bbsink_shell_manifest_contents(bbsink *sink, size_t len)
     359             : {
     360          20 :     bbsink_shell *mysink = (bbsink_shell *) sink;
     361             : 
     362          20 :     shell_send_data(mysink, len);
     363          20 :     bbsink_forward_manifest_contents(sink, len);
     364          20 : }
     365             : 
     366             : /*
     367             :  * At end of manifest, shut down the shell command and forward to next sink.
     368             :  */
     369             : static void
     370           4 : bbsink_shell_end_manifest(bbsink *sink)
     371             : {
     372           4 :     bbsink_shell *mysink = (bbsink_shell *) sink;
     373             : 
     374           4 :     shell_finish_command(mysink);
     375           4 :     bbsink_forward_end_manifest(sink);
     376           4 : }

Generated by: LCOV version 1.14