diff options
Diffstat (limited to 'src/global/pipe_command.c')
-rw-r--r-- | src/global/pipe_command.c | 683 |
1 files changed, 683 insertions, 0 deletions
diff --git a/src/global/pipe_command.c b/src/global/pipe_command.c new file mode 100644 index 0000000..66aec8a --- /dev/null +++ b/src/global/pipe_command.c @@ -0,0 +1,683 @@ +/*++ +/* NAME +/* pipe_command 3 +/* SUMMARY +/* deliver message to external command +/* SYNOPSIS +/* #include <pipe_command.h> +/* +/* int pipe_command(src, why, key, value, ...) +/* VSTREAM *src; +/* DSN_BUF *why; +/* int key; +/* DESCRIPTION +/* pipe_command() runs a command with a message as standard +/* input. A limited amount of standard output and standard error +/* output is captured for diagnostics purposes. +/* +/* If the command invokes exit() with a non-zero status, +/* the delivery status is taken from an RFC 3463-style code +/* at the beginning of command output. If that information is +/* unavailable, the delivery status is taken from the command +/* exit status as per <sysexits.h>. +/* +/* Arguments: +/* .IP src +/* An open message queue file, positioned at the start of the actual +/* message content. +/* .IP why +/* Delivery status information. The reason attribute may contain +/* a limited portion of command output, among other free text. +/* .IP key +/* Specifies what value will follow. pipe_command() takes a list +/* of macros with arguments, terminated by CA_PIPE_CMD_END which +/* has no argument. The following is a listing of macros and +/* expected argument types. +/* .RS +/* .IP "CA_PIPE_CMD_COMMAND(const char *)" +/* Specifies the command to execute as a string. The string is +/* passed to the shell when it contains shell meta characters +/* or when it appears to be a shell built-in command, otherwise +/* the command is executed without invoking a shell. +/* One of CA_PIPE_CMD_COMMAND or CA_PIPE_CMD_ARGV must be specified. +/* See also the CA_PIPE_CMD_SHELL attribute below. +/* .IP "CA_PIPE_CMD_ARGV(char **)" +/* The command is specified as an argument vector. This vector is +/* passed without further inspection to the \fIexecvp\fR() routine. +/* One of CA_PIPE_CMD_COMMAND or CA_PIPE_CMD_ARGV must be specified. +/* .IP "CA_PIPE_CMD_CHROOT(const char *)" +/* Root and working directory for command execution. This takes +/* effect before CA_PIPE_CMD_CWD. A null pointer means don't +/* change root and working directory anyway. Failure to change +/* directory causes mail delivery to be deferred. +/* .IP "CA_PIPE_CMD_CWD(const char *)" +/* Working directory for command execution, after changing process +/* privileges to CA_PIPE_CMD_UID and CA_PIPE_CMD_GID. A null pointer means +/* don't change directory anyway. Failure to change directory +/* causes mail delivery to be deferred. +/* .IP "CA_PIPE_CMD_ENV(char **)" +/* Additional environment information, in the form of a null-terminated +/* list of name, value, name, value, ... elements. By default only the +/* command search path is initialized to _PATH_DEFPATH. +/* .IP "CA_PIPE_CMD_EXPORT(char **)" +/* Null-terminated array with names of environment parameters +/* that can be exported. By default, everything is exported. +/* .IP "CA_PIPE_CMD_COPY_FLAGS(int)" +/* Flags that are passed on to the \fImail_copy\fR() routine. +/* The default flags value is 0 (zero). +/* .IP "CA_PIPE_CMD_SENDER(const char *)" +/* The envelope sender address, which is passed on to the +/* \fImail_copy\fR() routine. +/* .IP "CA_PIPE_CMD_ORIG_RCPT(const char *)" +/* The original recipient envelope address, which is passed on +/* to the \fImail_copy\fR() routine. +/* .IP "CA_PIPE_CMD_DELIVERED(const char *)" +/* The recipient envelope address, which is passed on to the +/* \fImail_copy\fR() routine. +/* .IP "CA_PIPE_CMD_EOL(const char *)" +/* End-of-line delimiter. The default is to use the newline character. +/* .IP "CA_PIPE_CMD_UID(uid_t)" +/* The user ID to execute the command as. The default is +/* the user ID corresponding to the \fIdefault_privs\fR +/* configuration parameter. The user ID must be non-zero. +/* .IP "CA_PIPE_CMD_GID(gid_t)" +/* The group ID to execute the command as. The default is +/* the group ID corresponding to the \fIdefault_privs\fR +/* configuration parameter. The group ID must be non-zero. +/* .IP "CA_PIPE_CMD_TIME_LIMIT(int)" +/* The amount of time the command is allowed to run before it +/* is terminated with SIGKILL. A non-negative CA_PIPE_CMD_TIME_LIMIT +/* value must be specified. +/* .IP "CA_PIPE_CMD_SHELL(const char *)" +/* The shell to use when executing the command specified with +/* CA_PIPE_CMD_COMMAND. This shell is invoked regardless of the +/* command content. +/* .RE +/* DIAGNOSTICS +/* Panic: interface violations (for example, a zero-valued +/* user ID or group ID, or a missing command). +/* +/* pipe_command() returns one of the following status codes: +/* .IP PIPE_STAT_OK +/* The command has taken responsibility for further delivery of +/* the message. +/* .IP PIPE_STAT_DEFER +/* The command failed with a "try again" type error. +/* The reason is given via the \fIwhy\fR argument. +/* .IP PIPE_STAT_BOUNCE +/* The command indicated that the message was not acceptable, +/* or the command did not finish within the time limit. +/* The reason is given via the \fIwhy\fR argument. +/* .IP PIPE_STAT_CORRUPT +/* The queue file is corrupted. +/* SEE ALSO +/* mail_copy(3) deliver to any. +/* mark_corrupt(3) mark queue file as corrupt. +/* sys_exits(3) sendmail-compatible exit status codes. +/* LICENSE +/* .ad +/* .fi +/* The Secure Mailer license must be distributed with this software. +/* AUTHOR(S) +/* Wietse Venema +/* IBM T.J. Watson Research +/* P.O. Box 704 +/* Yorktown Heights, NY 10598, USA +/*--*/ + +/* System library. */ + +#include <sys_defs.h> +#include <sys/wait.h> +#include <signal.h> +#include <unistd.h> +#include <errno.h> +#include <stdarg.h> +#include <fcntl.h> +#include <stdlib.h> +#ifdef USE_PATHS_H +#include <paths.h> +#endif +#include <syslog.h> + +/* Utility library. */ + +#include <msg.h> +#include <vstream.h> +#include <msg_vstream.h> +#include <vstring.h> +#include <stringops.h> +#include <iostuff.h> +#include <timed_wait.h> +#include <set_ugid.h> +#include <set_eugid.h> +#include <argv.h> +#include <chroot_uid.h> + +/* Global library. */ + +#include <mail_params.h> +#include <mail_copy.h> +#include <clean_env.h> +#include <pipe_command.h> +#include <exec_command.h> +#include <sys_exits.h> +#include <dsn_util.h> +#include <dsn_buf.h> + +/* Application-specific. */ + +struct pipe_args { + int flags; /* see mail_copy.h */ + char *sender; /* envelope sender */ + char *orig_rcpt; /* original recipient */ + char *delivered; /* envelope recipient */ + char *eol; /* carriagecontrol */ + char **argv; /* either an array */ + char *command; /* or a plain string */ + uid_t uid; /* privileges */ + gid_t gid; /* privileges */ + char **env; /* extra environment */ + char **export; /* exportable environment */ + char *shell; /* command shell */ + char *cwd; /* preferred working directory */ + char *chroot; /* root directory */ +}; + +static int pipe_command_timeout; /* command has timed out */ +static int pipe_command_maxtime; /* available time to complete */ + +/* get_pipe_args - capture the variadic argument list */ + +static void get_pipe_args(struct pipe_args * args, va_list ap) +{ + const char *myname = "get_pipe_args"; + int key; + + /* + * First, set the default values. + */ + args->flags = 0; + args->sender = 0; + args->orig_rcpt = 0; + args->delivered = 0; + args->eol = "\n"; + args->argv = 0; + args->command = 0; + args->uid = var_default_uid; + args->gid = var_default_gid; + args->env = 0; + args->export = 0; + args->shell = 0; + args->cwd = 0; + args->chroot = 0; + + pipe_command_maxtime = -1; + + /* + * Then, override the defaults with user-supplied inputs. + */ + while ((key = va_arg(ap, int)) != PIPE_CMD_END) { + switch (key) { + case PIPE_CMD_COPY_FLAGS: + args->flags |= va_arg(ap, int); + break; + case PIPE_CMD_SENDER: + args->sender = va_arg(ap, char *); + break; + case PIPE_CMD_ORIG_RCPT: + args->orig_rcpt = va_arg(ap, char *); + break; + case PIPE_CMD_DELIVERED: + args->delivered = va_arg(ap, char *); + break; + case PIPE_CMD_EOL: + args->eol = va_arg(ap, char *); + break; + case PIPE_CMD_ARGV: + if (args->command) + msg_panic("%s: got PIPE_CMD_ARGV and PIPE_CMD_COMMAND", myname); + args->argv = va_arg(ap, char **); + break; + case PIPE_CMD_COMMAND: + if (args->argv) + msg_panic("%s: got PIPE_CMD_ARGV and PIPE_CMD_COMMAND", myname); + args->command = va_arg(ap, char *); + break; + case PIPE_CMD_UID: + args->uid = va_arg(ap, uid_t); /* in case uid_t is short */ + break; + case PIPE_CMD_GID: + args->gid = va_arg(ap, gid_t); /* in case gid_t is short */ + break; + case PIPE_CMD_TIME_LIMIT: + pipe_command_maxtime = va_arg(ap, int); + break; + case PIPE_CMD_ENV: + args->env = va_arg(ap, char **); + break; + case PIPE_CMD_EXPORT: + args->export = va_arg(ap, char **); + break; + case PIPE_CMD_SHELL: + args->shell = va_arg(ap, char *); + break; + case PIPE_CMD_CWD: + args->cwd = va_arg(ap, char *); + break; + case PIPE_CMD_CHROOT: + args->chroot = va_arg(ap, char *); + break; + default: + msg_panic("%s: unknown key: %d", myname, key); + } + } + if (args->command == 0 && args->argv == 0) + msg_panic("%s: missing PIPE_CMD_ARGV or PIPE_CMD_COMMAND", myname); + if (args->uid == 0) + msg_panic("%s: privileged uid", myname); + if (args->gid == 0) + msg_panic("%s: privileged gid", myname); + if (pipe_command_maxtime < 0) + msg_panic("%s: missing or invalid PIPE_CMD_TIME_LIMIT", myname); +} + +/* pipe_command_write - write to command with time limit */ + +static ssize_t pipe_command_write(int fd, void *buf, size_t len, + int unused_timeout, + void *unused_context) +{ + int maxtime = (pipe_command_timeout == 0) ? pipe_command_maxtime : 0; + const char *myname = "pipe_command_write"; + + /* + * Don't wait when all available time was already used up. + */ + if (write_wait(fd, maxtime) < 0) { + if (pipe_command_timeout == 0) { + msg_warn("%s: write time limit exceeded", myname); + pipe_command_timeout = 1; + } + return (0); + } else { + return (write(fd, buf, len)); + } +} + +/* pipe_command_read - read from command with time limit */ + +static ssize_t pipe_command_read(int fd, void *buf, size_t len, + int unused_timeout, + void *unused_context) +{ + int maxtime = (pipe_command_timeout == 0) ? pipe_command_maxtime : 0; + const char *myname = "pipe_command_read"; + + /* + * Don't wait when all available time was already used up. + */ + if (read_wait(fd, maxtime) < 0) { + if (pipe_command_timeout == 0) { + msg_warn("%s: read time limit exceeded", myname); + pipe_command_timeout = 1; + } + return (0); + } else { + return (read(fd, buf, len)); + } +} + +/* kill_command - terminate command forcibly */ + +static void kill_command(pid_t pid, int sig, uid_t kill_uid, gid_t kill_gid) +{ + uid_t saved_euid = geteuid(); + gid_t saved_egid = getegid(); + + /* + * Switch privileges to that of the child process. Terminate the child + * and its offspring. + */ + set_eugid(kill_uid, kill_gid); + if (kill(-pid, sig) < 0 && kill(pid, sig) < 0) + msg_warn("cannot kill process (group) %lu: %m", + (unsigned long) pid); + set_eugid(saved_euid, saved_egid); +} + +/* pipe_command_wait_or_kill - wait for command with time limit, or kill it */ + +static int pipe_command_wait_or_kill(pid_t pid, WAIT_STATUS_T *statusp, int sig, + uid_t kill_uid, gid_t kill_gid) +{ + int maxtime = (pipe_command_timeout == 0) ? pipe_command_maxtime : 1; + const char *myname = "pipe_command_wait_or_kill"; + int n; + + /* + * Don't wait when all available time was already used up. + */ + if ((n = timed_waitpid(pid, statusp, 0, maxtime)) < 0 && errno == ETIMEDOUT) { + if (pipe_command_timeout == 0) { + msg_warn("%s: child wait time limit exceeded", myname); + pipe_command_timeout = 1; + } + kill_command(pid, sig, kill_uid, kill_gid); + n = waitpid(pid, statusp, 0); + } + return (n); +} + +/* pipe_child_cleanup - child fatal error handler */ + +static void pipe_child_cleanup(void) +{ + + /* + * WARNING: don't place code here. This code may run as mail_owner, as + * root, or as the user/group specified with the "user" attribute. The + * only safe action is to terminate. + * + * Future proofing. If you need exit() here then you broke Postfix. + */ + _exit(EX_TEMPFAIL); +} + +/* pipe_command - execute command with extreme prejudice */ + +int pipe_command(VSTREAM *src, DSN_BUF *why,...) +{ + const char *myname = "pipe_command"; + va_list ap; + VSTREAM *cmd_in_stream; + VSTREAM *cmd_out_stream; + char log_buf[VSTREAM_BUFSIZE + 1]; + ssize_t log_len; + pid_t pid; + int write_status; + int write_errno; + WAIT_STATUS_T wait_status; + int cmd_in_pipe[2]; + int cmd_out_pipe[2]; + struct pipe_args args; + char **cpp; + ARGV *argv; + DSN_SPLIT dp; + const SYS_EXITS_DETAIL *sp; + + /* + * Process the variadic argument list. This also does sanity checks on + * what data the caller is passing to us. + */ + va_start(ap, why); + get_pipe_args(&args, ap); + va_end(ap); + + /* + * For convenience... + */ + if (args.command == 0) + args.command = args.argv[0]; + + /* + * Set up pipes that connect us to the command input and output streams. + * We're using a rather disgusting hack to capture command output: set + * the output to non-blocking mode, and don't attempt to read the output + * until AFTER the process has terminated. The rationale for this is: 1) + * the command output will be used only when delivery fails; 2) the + * amount of output is expected to be small; 3) the output can be + * truncated without too much loss. I could even argue that truncating + * the amount of diagnostic output is a good thing to do, but I won't go + * that far. + * + * Turn on non-blocking writes to the child process so that we can enforce + * timeouts after partial writes. + * + * XXX Too much trouble with different systems returning weird write() + * results when a pipe is writable. + */ + if (pipe(cmd_in_pipe) < 0 || pipe(cmd_out_pipe) < 0) + msg_fatal("%s: pipe: %m", myname); + non_blocking(cmd_out_pipe[1], NON_BLOCKING); +#if 0 + non_blocking(cmd_in_pipe[1], NON_BLOCKING); +#endif + + /* + * Spawn off a child process and irrevocably change privilege to the + * user. This includes revoking all rights on open files (via the close + * on exec flag). If we cannot run the command now, try again some time + * later. + */ + switch (pid = fork()) { + + /* + * Error. Instead of trying again right now, back off, give the + * system a chance to recover, and try again later. + */ + case -1: + msg_warn("fork: %m"); + dsb_unix(why, "4.3.0", sys_exits_detail(EX_OSERR)->text, + "Delivery failed: %m"); + return (PIPE_STAT_DEFER); + + /* + * Child. Run the child in a separate process group so that the + * parent can kill not just the child but also its offspring. + * + * Redirect fatal exits to our own fatal exit handler (never leave the + * parent's handler enabled :-) so we can replace random exit status + * codes by EX_TEMPFAIL. + */ + case 0: + (void) msg_cleanup(pipe_child_cleanup); + + /* + * In order to chroot it is necessary to switch euid back to root. + * Right after chroot we call set_ugid() so all privileges will be + * dropped again. + * + * XXX For consistency we use chroot_uid() to change root+current + * directory. However, we must not use chroot_uid() to change process + * privileges (assuming a version that accepts numeric privileges). + * That would create a maintenance problem, because we would have two + * different code paths to set the external command's privileges. + */ + if (args.chroot) { + seteuid(0); + chroot_uid(args.chroot, (char *) 0); + } + + /* + * XXX If we put code before the set_ugid() call, then the code that + * changes root directory must switch back to the mail_owner UID, + * otherwise we'd be running with root privileges. + */ + set_ugid(args.uid, args.gid); + if (setsid() < 0) + msg_warn("setsid failed: %m"); + + /* + * Pipe plumbing. + */ + close(cmd_in_pipe[1]); + close(cmd_out_pipe[0]); + if (DUP2(cmd_in_pipe[0], STDIN_FILENO) < 0 + || DUP2(cmd_out_pipe[1], STDOUT_FILENO) < 0 + || DUP2(cmd_out_pipe[1], STDERR_FILENO) < 0) + msg_fatal("%s: dup2: %m", myname); + close(cmd_in_pipe[0]); + close(cmd_out_pipe[1]); + + /* + * Working directory plumbing. + */ + if (args.cwd && chdir(args.cwd) < 0) + msg_fatal("cannot change directory to \"%s\" for uid=%lu gid=%lu: %m", + args.cwd, (unsigned long) args.uid, + (unsigned long) args.gid); + + /* + * Environment plumbing. Always reset the command search path. XXX + * That should probably be done by clean_env(). + */ + if (args.export) + clean_env(args.export); + if (setenv("PATH", _PATH_DEFPATH, 1)) + msg_fatal("%s: setenv: %m", myname); + if (args.env) + for (cpp = args.env; *cpp; cpp += 2) + if (setenv(cpp[0], cpp[1], 1)) + msg_fatal("setenv: %m"); + + /* + * Process plumbing. If possible, avoid running a shell. + * + * As a safety for buggy libraries, we close the syslog socket. + * Otherwise we could leak a file descriptor that was created by a + * privileged process. + * + * XXX To avoid losing fatal error messages we open a VSTREAM and + * capture the output in the parent process. + */ + closelog(); + msg_vstream_init(var_procname, VSTREAM_ERR); + if (args.argv) { + execvp(args.argv[0], args.argv); + msg_fatal("%s: execvp %s: %m", myname, args.argv[0]); + } else if (args.shell && *args.shell) { + argv = argv_split(args.shell, CHARS_SPACE); + argv_add(argv, args.command, (char *) 0); + argv_terminate(argv); + execvp(argv->argv[0], argv->argv); + msg_fatal("%s: execvp %s: %m", myname, argv->argv[0]); + } else { + exec_command(args.command); + } + /* NOTREACHED */ + + /* + * Parent. + */ + default: + close(cmd_in_pipe[0]); + close(cmd_out_pipe[1]); + + cmd_in_stream = vstream_fdopen(cmd_in_pipe[1], O_WRONLY); + cmd_out_stream = vstream_fdopen(cmd_out_pipe[0], O_RDONLY); + + /* + * Give the command a limited amount of time to run, by enforcing + * timeouts on all I/O from and to it. + */ + vstream_control(cmd_in_stream, + CA_VSTREAM_CTL_WRITE_FN(pipe_command_write), + CA_VSTREAM_CTL_END); + vstream_control(cmd_out_stream, + CA_VSTREAM_CTL_READ_FN(pipe_command_read), + CA_VSTREAM_CTL_END); + pipe_command_timeout = 0; + + /* + * Pipe the message into the command. Examine the error report only + * if we can't recognize a more specific error from the command exit + * status or from the command output. + */ + write_status = mail_copy(args.sender, args.orig_rcpt, + args.delivered, src, + cmd_in_stream, args.flags, + args.eol, why); + write_errno = errno; + + /* + * Capture a limited amount of command output, for inclusion in a + * bounce message. Turn tabs and newlines into whitespace, and + * replace other non-printable characters by underscore. + */ + log_len = vstream_fread(cmd_out_stream, log_buf, sizeof(log_buf) - 1); + (void) vstream_fclose(cmd_out_stream); + log_buf[log_len] = 0; + translit(log_buf, "\t\n", " "); + printable(log_buf, '_'); + + /* + * Just because the child closes its output streams, don't assume + * that it will terminate. Instead, be prepared for the situation + * that the child does not terminate, even when the parent + * experiences no read/write timeout. Make sure that the child + * terminates before the parent attempts to retrieve its exit status, + * otherwise the parent could become stuck, and the mail system would + * eventually run out of delivery agents. Do a thorough job, and kill + * not just the child process but also its offspring. + */ + if (pipe_command_timeout) + kill_command(pid, SIGKILL, args.uid, args.gid); + if (pipe_command_wait_or_kill(pid, &wait_status, SIGKILL, + args.uid, args.gid) < 0) + msg_fatal("wait: %m"); + if (pipe_command_timeout) { + dsb_unix(why, "5.3.0", log_len ? + log_buf : sys_exits_detail(EX_SOFTWARE)->text, + "Command time limit exceeded: \"%s\"%s%s", + args.command, + log_len ? ". Command output: " : "", log_buf); + return (PIPE_STAT_BOUNCE); + } + + /* + * Command exits. Give special treatment to sendmail style exit + * status codes. + */ + if (!NORMAL_EXIT_STATUS(wait_status)) { + if (WIFSIGNALED(wait_status)) { + dsb_unix(why, "4.3.0", log_len ? + log_buf : sys_exits_detail(EX_SOFTWARE)->text, + "Command died with signal %d: \"%s\"%s%s", + WTERMSIG(wait_status), args.command, + log_len ? ". Command output: " : "", log_buf); + return (PIPE_STAT_DEFER); + } + /* Use "D.S.N text" command output. XXX What diagnostic code? */ + else if (dsn_valid(log_buf) > 0) { + dsn_split(&dp, "5.3.0", log_buf); + dsb_unix(why, DSN_STATUS(dp.dsn), dp.text, "%s", dp.text); + return (DSN_CLASS(dp.dsn) == '4' ? + PIPE_STAT_DEFER : PIPE_STAT_BOUNCE); + } + /* Use <sysexits.h> compatible exit status. */ + else if (SYS_EXITS_CODE(WEXITSTATUS(wait_status))) { + sp = sys_exits_detail(WEXITSTATUS(wait_status)); + dsb_unix(why, sp->dsn, + log_len ? log_buf : sp->text, "%s%s%s", sp->text, + log_len ? ". Command output: " : "", log_buf); + return (sp->dsn[0] == '4' ? + PIPE_STAT_DEFER : PIPE_STAT_BOUNCE); + } + + /* + * No "D.S.N text" or <sysexits.h> compatible status. Fake it. + */ + else { + sp = sys_exits_detail(WEXITSTATUS(wait_status)); + dsb_unix(why, sp->dsn, + log_len ? log_buf : sp->text, + "Command died with status %d: \"%s\"%s%s", + WEXITSTATUS(wait_status), args.command, + log_len ? ". Command output: " : "", log_buf); + return (PIPE_STAT_BOUNCE); + } + } else if (write_status & + MAIL_COPY_STAT_CORRUPT) { + return (PIPE_STAT_CORRUPT); + } else if (write_status && write_errno != EPIPE) { + vstring_prepend(why->reason, "Command failed: ", + sizeof("Command failed: ") - 1); + vstring_sprintf_append(why->reason, ": \"%s\"", args.command); + return (PIPE_STAT_BOUNCE); + } else { + vstring_strcpy(why->reason, log_buf); + return (PIPE_STAT_OK); + } + } +} |