/*++ /* NAME /* pipe_command 3 /* SUMMARY /* deliver message to external command /* SYNOPSIS /* #include /* /* 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 . /* /* 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 #include #include #include #include #include #include #include #ifdef USE_PATHS_H #include #endif #include /* Utility library. */ #include #include #include #include #include #include #include #include #include #include #include /* Global library. */ #include #include #include #include #include #include #include #include /* 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 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 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); } } }