1 /*        $NetBSD: spawn_command.c,v 1.3 2025/02/25 19:15:52 christos Exp $     */
2 
3 /*++
4 /* NAME
5 /*        spawn_command 3
6 /* SUMMARY
7 /*        run external command
8 /* SYNOPSIS
9 /*        #include <spawn_command.h>
10 /*
11 /*        WAIT_STATUS_T spawn_command(key, value, ...)
12 /*        int       key;
13 /* DESCRIPTION
14 /*        spawn_command() runs a command in a child process and returns
15 /*        the command exit status.
16 /*
17 /*        Arguments:
18 /* .IP key
19 /*        spawn_command() takes a list of macros with arguments,
20 /*        terminated by CA_SPAWN_CMD_END which has no arguments. The
21 /*        following is a listing of macros and expected argument
22 /*        types.
23 /* .RS
24 /* .IP "CA_SPAWN_CMD_COMMAND(const char *)"
25 /*        Specifies the command to execute as a string. The string is
26 /*        passed to the shell when it contains shell meta characters
27 /*        or when it appears to be a shell built-in command, otherwise
28 /*        the command is executed without invoking a shell.
29 /*        One of CA_SPAWN_CMD_COMMAND or CA_SPAWN_CMD_ARGV must be specified.
30 /*        See also the SPAWN_CMD_SHELL attribute below.
31 /* .IP "CA_SPAWN_CMD_ARGV(char **)"
32 /*        The command is specified as an argument vector. This vector is
33 /*        passed without further inspection to the \fIexecvp\fR() routine.
34 /*        One of CA_SPAWN_CMD_COMMAND or CA_SPAWN_CMD_ARGV must be specified.
35 /* .IP "CA_SPAWN_CMD_ENV(char **)"
36 /*        Additional environment information, in the form of a null-terminated
37 /*        list of name, value, name, value, ... elements. By default only the
38 /*        command search path is initialized to _PATH_DEFPATH.
39 /* .IP "CA_SPAWN_CMD_EXPORT(char **)"
40 /*        Null-terminated array of names of environment parameters that can
41 /*        be exported. By default, everything is exported.
42 /* .IP "CA_SPAWN_CMD_STDIN(int)"
43 /* .IP "CA_SPAWN_CMD_STDOUT(int)"
44 /* .IP "CA_SPAWN_CMD_STDERR(int)"
45 /*        Each of these specifies I/O redirection of one of the standard file
46 /*        descriptors for the command.
47 /* .IP "CA_SPAWN_CMD_UID(uid_t)"
48 /*        The user ID to execute the command as. The value -1 is reserved
49 /*        and cannot be specified.
50 /* .IP "CA_SPAWN_CMD_GID(gid_t)"
51 /*        The group ID to execute the command as. The value -1 is reserved
52 /*        and cannot be specified.
53 /* .IP "CA_SPAWN_CMD_TIME_LIMIT(int)"
54 /*        The amount of time in seconds the command is allowed to run before
55 /*        it is terminated with SIGKILL. The default is no time limit.
56 /* .IP "CA_SPAWN_CMD_SHELL(const char *)"
57 /*        The shell to use when executing the command specified with
58 /*        CA_SPAWN_CMD_COMMAND. This shell is invoked regardless of the
59 /*        command content.
60 /* .RE
61 /* DIAGNOSTICS
62 /*        Panic: interface violations (for example, a missing command).
63 /*
64 /*        Fatal error: fork() failure, other system call failures.
65 /*
66 /*        spawn_command() returns the exit status as defined by wait(2).
67 /* LICENSE
68 /* .ad
69 /* .fi
70 /*        The Secure Mailer license must be distributed with this software.
71 /* SEE ALSO
72 /*        exec_command(3) execute command
73 /* AUTHOR(S)
74 /*        Wietse Venema
75 /*        IBM T.J. Watson Research
76 /*        P.O. Box 704
77 /*        Yorktown Heights, NY 10598, USA
78 /*--*/
79 
80 /* System library. */
81 
82 #include <sys_defs.h>
83 #include <sys/wait.h>
84 #include <signal.h>
85 #include <unistd.h>
86 #include <errno.h>
87 #include <stdarg.h>
88 #include <stdlib.h>
89 #ifdef USE_PATHS_H
90 #include <paths.h>
91 #endif
92 #include <syslog.h>
93 
94 /* Utility library. */
95 
96 #include <msg.h>
97 #include <timed_wait.h>
98 #include <set_ugid.h>
99 #include <set_eugid.h>
100 #include <argv.h>
101 #include <spawn_command.h>
102 #include <exec_command.h>
103 #include <clean_env.h>
104 
105 /* Application-specific. */
106 
107 struct spawn_args {
108     char  **argv;                       /* argument vector */
109     char   *command;                              /* or a plain string */
110     int     stdin_fd;                             /* read stdin here */
111     int     stdout_fd;                            /* write stdout here */
112     int     stderr_fd;                            /* write stderr here */
113     uid_t   uid;                        /* privileges */
114     gid_t   gid;                        /* privileges */
115     char  **env;                        /* extra environment */
116     char  **export;                     /* exportable environment */
117     char   *shell;                      /* command shell */
118     int     time_limit;                           /* command time limit */
119 };
120 
121 /* get_spawn_args - capture the variadic argument list */
122 
get_spawn_args(struct spawn_args * args,int init_key,va_list ap)123 static void get_spawn_args(struct spawn_args * args, int init_key, va_list ap)
124 {
125     const char *myname = "get_spawn_args";
126     int     key;
127 
128     /*
129      * First, set the default values.
130      */
131     args->argv = 0;
132     args->command = 0;
133     args->stdin_fd = -1;
134     args->stdout_fd = -1;
135     args->stderr_fd = -1;
136     args->uid = (uid_t) - 1;
137     args->gid = (gid_t) - 1;
138     args->env = 0;
139     args->export = 0;
140     args->shell = 0;
141     args->time_limit = 0;
142 
143     /*
144      * Then, override the defaults with user-supplied inputs.
145      */
146     for (key = init_key; key != SPAWN_CMD_END; key = va_arg(ap, int)) {
147           switch (key) {
148           case SPAWN_CMD_ARGV:
149               if (args->command)
150                     msg_panic("%s: specify SPAWN_CMD_ARGV or SPAWN_CMD_COMMAND",
151                                 myname);
152               args->argv = va_arg(ap, char **);
153               break;
154           case SPAWN_CMD_COMMAND:
155               if (args->argv)
156                     msg_panic("%s: specify SPAWN_CMD_ARGV or SPAWN_CMD_COMMAND",
157                                 myname);
158               args->command = va_arg(ap, char *);
159               break;
160           case SPAWN_CMD_STDIN:
161               args->stdin_fd = va_arg(ap, int);
162               break;
163           case SPAWN_CMD_STDOUT:
164               args->stdout_fd = va_arg(ap, int);
165               break;
166           case SPAWN_CMD_STDERR:
167               args->stderr_fd = va_arg(ap, int);
168               break;
169           case SPAWN_CMD_UID:
170               args->uid = va_arg(ap, uid_t);
171               if (args->uid == (uid_t) (-1))
172                     msg_panic("spawn_command: request with reserved user ID: -1");
173               break;
174           case SPAWN_CMD_GID:
175               args->gid = va_arg(ap, gid_t);
176               if (args->gid == (gid_t) (-1))
177                     msg_panic("spawn_command: request with reserved group ID: -1");
178               break;
179           case SPAWN_CMD_TIME_LIMIT:
180               args->time_limit = va_arg(ap, int);
181               break;
182           case SPAWN_CMD_ENV:
183               args->env = va_arg(ap, char **);
184               break;
185           case SPAWN_CMD_EXPORT:
186               args->export = va_arg(ap, char **);
187               break;
188           case SPAWN_CMD_SHELL:
189               args->shell = va_arg(ap, char *);
190               break;
191           default:
192               msg_panic("%s: unknown key: %d", myname, key);
193           }
194     }
195     if (args->command == 0 && args->argv == 0)
196           msg_panic("%s: missing SPAWN_CMD_ARGV or SPAWN_CMD_COMMAND", myname);
197     if (args->command == 0 && args->shell != 0)
198           msg_panic("%s: SPAWN_CMD_ARGV cannot be used with SPAWN_CMD_SHELL",
199                       myname);
200 }
201 
202 /* spawn_command - execute command with extreme prejudice */
203 
spawn_command(int key,...)204 WAIT_STATUS_T spawn_command(int key,...)
205 {
206     const char *myname = "spawn_comand";
207     va_list ap;
208     pid_t   pid;
209     WAIT_STATUS_T wait_status;
210     struct spawn_args args;
211     char  **cpp;
212     ARGV   *argv;
213     int     err;
214 
215     /*
216      * Process the variadic argument list. This also does sanity checks on
217      * what data the caller is passing to us.
218      */
219     va_start(ap, key);
220     get_spawn_args(&args, key, ap);
221     va_end(ap);
222 
223     /*
224      * For convenience...
225      */
226     if (args.command == 0)
227           args.command = args.argv[0];
228 
229     /*
230      * Spawn off a child process and irrevocably change privilege to the
231      * user. This includes revoking all rights on open files (via the close
232      * on exec flag). If we cannot run the command now, try again some time
233      * later.
234      */
235     switch (pid = fork()) {
236 
237           /*
238            * Error. Instead of trying again right now, back off, give the
239            * system a chance to recover, and try again later.
240            */
241     case -1:
242           msg_fatal("fork: %m");
243 
244           /*
245            * Child. Run the child in a separate process group so that the
246            * parent can kill not just the child but also its offspring.
247            */
248     case 0:
249           if (args.uid != (uid_t) - 1 || args.gid != (gid_t) - 1)
250               set_ugid(args.uid, args.gid);
251           if (setsid() < 0)
252               msg_warn("child: setsid: %m");
253 
254           /*
255            * Pipe plumbing.
256            */
257           if ((args.stdin_fd >= 0 && DUP2(args.stdin_fd, STDIN_FILENO) < 0)
258            || (args.stdout_fd >= 0 && DUP2(args.stdout_fd, STDOUT_FILENO) < 0)
259           || (args.stderr_fd >= 0 && DUP2(args.stderr_fd, STDERR_FILENO) < 0))
260               msg_fatal("%s: dup2: %m", myname);
261 
262           /*
263            * Environment plumbing. Always reset the command search path. XXX
264            * That should probably be done by clean_env().
265            */
266           if (args.export)
267               clean_env(args.export);
268           if (setenv("PATH", _PATH_DEFPATH, 1))
269               msg_fatal("%s: setenv: %m", myname);
270           if (args.env)
271               for (cpp = args.env; *cpp; cpp += 2)
272                     if (setenv(cpp[0], cpp[1], 1))
273                         msg_fatal("setenv: %m");
274 
275           /*
276            * Process plumbing. If possible, avoid running a shell.
277            */
278           closelog();
279           if (args.argv) {
280               execvp(args.argv[0], args.argv);
281               msg_fatal("%s: execvp %s: %m", myname, args.argv[0]);
282           } else if (args.shell && *args.shell) {
283               argv = argv_split(args.shell, CHARS_SPACE);
284               argv_add(argv, args.command, (char *) 0);
285               argv_terminate(argv);
286               execvp(argv->argv[0], argv->argv);
287               msg_fatal("%s: execvp %s: %m", myname, argv->argv[0]);
288           } else {
289               exec_command(args.command);
290           }
291           /* NOTREACHED */
292 
293           /*
294            * Parent.
295            */
296     default:
297 
298           /*
299            * Be prepared for the situation that the child does not terminate.
300            * Make sure that the child terminates before the parent attempts to
301            * retrieve its exit status, otherwise the parent could become stuck,
302            * and the mail system would eventually run out of exec daemons. Do a
303            * thorough job, and kill not just the child process but also its
304            * offspring.
305            */
306           if ((err = timed_waitpid(pid, &wait_status, 0, args.time_limit)) < 0
307               && errno == ETIMEDOUT) {
308               uid_t   saved_euid = geteuid();
309               gid_t   saved_egid = getegid();
310 
311               msg_warn("%s: process id %lu: command time limit exceeded",
312                          args.command, (unsigned long) pid);
313               set_eugid(args.uid, args.gid);
314               if (kill(-pid, SIGKILL) < 0)
315                     msg_warn("parent: kill: %m");
316               set_eugid(saved_euid, saved_egid);
317               err = waitpid(pid, &wait_status, 0);
318           }
319           if (err < 0)
320               msg_fatal("wait: %m");
321           return (wait_status);
322     }
323 }
324