diff options
Diffstat (limited to 'src/util/spawn_command.c')
-rw-r--r-- | src/util/spawn_command.c | 313 |
1 files changed, 313 insertions, 0 deletions
diff --git a/src/util/spawn_command.c b/src/util/spawn_command.c new file mode 100644 index 0000000..739e012 --- /dev/null +++ b/src/util/spawn_command.c @@ -0,0 +1,313 @@ +/*++ +/* NAME +/* spawn_command 3 +/* SUMMARY +/* run external command +/* SYNOPSIS +/* #include <spawn_command.h> +/* +/* WAIT_STATUS_T spawn_command(key, value, ...) +/* int key; +/* DESCRIPTION +/* spawn_command() runs a command in a child process and returns +/* the command exit status. +/* +/* Arguments: +/* .IP key +/* spawn_command() takes a list of macros with arguments, +/* terminated by CA_SPAWN_CMD_END which has no arguments. The +/* following is a listing of macros and expected argument +/* types. +/* .RS +/* .IP "CA_SPAWN_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_SPAWN_CMD_COMMAND or CA_SPAWN_CMD_ARGV must be specified. +/* See also the SPAWN_CMD_SHELL attribute below. +/* .IP "CA_SPAWN_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_SPAWN_CMD_COMMAND or CA_SPAWN_CMD_ARGV must be specified. +/* .IP "CA_SPAWN_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_SPAWN_CMD_EXPORT(char **)" +/* Null-terminated array of names of environment parameters that can +/* be exported. By default, everything is exported. +/* .IP "CA_SPAWN_CMD_STDIN(int)" +/* .IP "CA_SPAWN_CMD_STDOUT(int)" +/* .IP "CA_SPAWN_CMD_STDERR(int)" +/* Each of these specifies I/O redirection of one of the standard file +/* descriptors for the command. +/* .IP "CA_SPAWN_CMD_UID(uid_t)" +/* The user ID to execute the command as. The value -1 is reserved +/* and cannot be specified. +/* .IP "CA_SPAWN_CMD_GID(gid_t)" +/* The group ID to execute the command as. The value -1 is reserved +/* and cannot be specified. +/* .IP "CA_SPAWN_CMD_TIME_LIMIT(int)" +/* The amount of time in seconds the command is allowed to run before +/* it is terminated with SIGKILL. The default is no time limit. +/* .IP "CA_SPAWN_CMD_SHELL(const char *)" +/* The shell to use when executing the command specified with +/* CA_SPAWN_CMD_COMMAND. This shell is invoked regardless of the +/* command content. +/* .RE +/* DIAGNOSTICS +/* Panic: interface violations (for example, a missing command). +/* +/* Fatal error: fork() failure, other system call failures. +/* +/* spawn_command() returns the exit status as defined by wait(2). +/* LICENSE +/* .ad +/* .fi +/* The Secure Mailer license must be distributed with this software. +/* SEE ALSO +/* exec_command(3) execute command +/* 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 <stdlib.h> +#ifdef USE_PATHS_H +#include <paths.h> +#endif +#include <syslog.h> + +/* Utility library. */ + +#include <msg.h> +#include <timed_wait.h> +#include <set_ugid.h> +#include <argv.h> +#include <spawn_command.h> +#include <exec_command.h> +#include <clean_env.h> + +/* Application-specific. */ + +struct spawn_args { + char **argv; /* argument vector */ + char *command; /* or a plain string */ + int stdin_fd; /* read stdin here */ + int stdout_fd; /* write stdout here */ + int stderr_fd; /* write stderr here */ + uid_t uid; /* privileges */ + gid_t gid; /* privileges */ + char **env; /* extra environment */ + char **export; /* exportable environment */ + char *shell; /* command shell */ + int time_limit; /* command time limit */ +}; + +/* get_spawn_args - capture the variadic argument list */ + +static void get_spawn_args(struct spawn_args * args, int init_key, va_list ap) +{ + const char *myname = "get_spawn_args"; + int key; + + /* + * First, set the default values. + */ + args->argv = 0; + args->command = 0; + args->stdin_fd = -1; + args->stdout_fd = -1; + args->stderr_fd = -1; + args->uid = (uid_t) - 1; + args->gid = (gid_t) - 1; + args->env = 0; + args->export = 0; + args->shell = 0; + args->time_limit = 0; + + /* + * Then, override the defaults with user-supplied inputs. + */ + for (key = init_key; key != SPAWN_CMD_END; key = va_arg(ap, int)) { + switch (key) { + case SPAWN_CMD_ARGV: + if (args->command) + msg_panic("%s: specify SPAWN_CMD_ARGV or SPAWN_CMD_COMMAND", + myname); + args->argv = va_arg(ap, char **); + break; + case SPAWN_CMD_COMMAND: + if (args->argv) + msg_panic("%s: specify SPAWN_CMD_ARGV or SPAWN_CMD_COMMAND", + myname); + args->command = va_arg(ap, char *); + break; + case SPAWN_CMD_STDIN: + args->stdin_fd = va_arg(ap, int); + break; + case SPAWN_CMD_STDOUT: + args->stdout_fd = va_arg(ap, int); + break; + case SPAWN_CMD_STDERR: + args->stderr_fd = va_arg(ap, int); + break; + case SPAWN_CMD_UID: + args->uid = va_arg(ap, uid_t); + if (args->uid == (uid_t) (-1)) + msg_panic("spawn_command: request with reserved user ID: -1"); + break; + case SPAWN_CMD_GID: + args->gid = va_arg(ap, gid_t); + if (args->gid == (gid_t) (-1)) + msg_panic("spawn_command: request with reserved group ID: -1"); + break; + case SPAWN_CMD_TIME_LIMIT: + args->time_limit = va_arg(ap, int); + break; + case SPAWN_CMD_ENV: + args->env = va_arg(ap, char **); + break; + case SPAWN_CMD_EXPORT: + args->export = va_arg(ap, char **); + break; + case SPAWN_CMD_SHELL: + args->shell = 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 SPAWN_CMD_ARGV or SPAWN_CMD_COMMAND", myname); + if (args->command == 0 && args->shell != 0) + msg_panic("%s: SPAWN_CMD_ARGV cannot be used with SPAWN_CMD_SHELL", + myname); +} + +/* spawn_command - execute command with extreme prejudice */ + +WAIT_STATUS_T spawn_command(int key,...) +{ + const char *myname = "spawn_comand"; + va_list ap; + pid_t pid; + WAIT_STATUS_T wait_status; + struct spawn_args args; + char **cpp; + ARGV *argv; + int err; + + /* + * Process the variadic argument list. This also does sanity checks on + * what data the caller is passing to us. + */ + va_start(ap, key); + get_spawn_args(&args, key, ap); + va_end(ap); + + /* + * For convenience... + */ + if (args.command == 0) + args.command = args.argv[0]; + + /* + * 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_fatal("fork: %m"); + + /* + * Child. Run the child in a separate process group so that the + * parent can kill not just the child but also its offspring. + */ + case 0: + if (args.uid != (uid_t) - 1 || args.gid != (gid_t) - 1) + set_ugid(args.uid, args.gid); + setsid(); + + /* + * Pipe plumbing. + */ + if ((args.stdin_fd >= 0 && DUP2(args.stdin_fd, STDIN_FILENO) < 0) + || (args.stdout_fd >= 0 && DUP2(args.stdout_fd, STDOUT_FILENO) < 0) + || (args.stderr_fd >= 0 && DUP2(args.stderr_fd, STDERR_FILENO) < 0)) + msg_fatal("%s: dup2: %m", myname); + + /* + * 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. + */ + closelog(); + 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: + + /* + * Be prepared for the situation that the child does not terminate. + * 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 exec daemons. Do a + * thorough job, and kill not just the child process but also its + * offspring. + */ + if ((err = timed_waitpid(pid, &wait_status, 0, args.time_limit)) < 0 + && errno == ETIMEDOUT) { + msg_warn("%s: process id %lu: command time limit exceeded", + args.command, (unsigned long) pid); + kill(-pid, SIGKILL); + err = waitpid(pid, &wait_status, 0); + } + if (err < 0) + msg_fatal("wait: %m"); + return (wait_status); + } +} |