summaryrefslogtreecommitdiffstats
path: root/src/util/spawn_command.c
blob: 739e0125bc5184531f31302bbf55f26e5a2525b1 (plain)
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
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);
    }
}