xref: /netbsd-src/external/bsd/cron/dist/do_command.c (revision 7330f729ccf0bd976a06f95fad452fe774fc7fd1)
1 /*	$NetBSD: do_command.c,v 1.14 2019/08/03 07:06:47 christos Exp $	*/
2 
3 /* Copyright 1988,1990,1993,1994 by Paul Vixie
4  * All rights reserved
5  */
6 
7 /*
8  * Copyright (c) 2004 by Internet Systems Consortium, Inc. ("ISC")
9  * Copyright (c) 1997,2000 by Internet Software Consortium, Inc.
10  *
11  * Permission to use, copy, modify, and distribute this software for any
12  * purpose with or without fee is hereby granted, provided that the above
13  * copyright notice and this permission notice appear in all copies.
14  *
15  * THE SOFTWARE IS PROVIDED "AS IS" AND ISC DISCLAIMS ALL WARRANTIES
16  * WITH REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF
17  * MERCHANTABILITY AND FITNESS.  IN NO EVENT SHALL ISC BE LIABLE FOR
18  * ANY SPECIAL, DIRECT, INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES
19  * WHATSOEVER RESULTING FROM LOSS OF USE, DATA OR PROFITS, WHETHER IN AN
20  * ACTION OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT
21  * OF OR IN CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE.
22  */
23 #include <sys/cdefs.h>
24 #if !defined(lint) && !defined(LINT)
25 #if 0
26 static char rcsid[] = "Id: do_command.c,v 1.9 2004/01/23 18:56:42 vixie Exp";
27 #else
28 __RCSID("$NetBSD: do_command.c,v 1.14 2019/08/03 07:06:47 christos Exp $");
29 #endif
30 #endif
31 
32 #include "cron.h"
33 #include <unistd.h>
34 
35 static int		child_process(entry *);
36 static int		safe_p(const char *, const char *);
37 
38 void
39 do_command(entry *e, user *u) {
40 	int retval;
41 
42 	Debug(DPROC, ("[%ld] do_command(%s, (%s,%ld,%ld))\n",
43 		      (long)getpid(), e->cmd, u->name,
44 		      (long)e->pwd->pw_uid, (long)e->pwd->pw_gid));
45 
46 	/* fork to become asynchronous -- parent process is done immediately,
47 	 * and continues to run the normal cron code, which means return to
48 	 * tick().  the child and grandchild don't leave this function, alive.
49 	 *
50 	 * vfork() is unsuitable, since we have much to do, and the parent
51 	 * needs to be able to run off and fork other processes.
52 	 */
53 
54 	pid_t	jobpid;
55 	switch (jobpid = fork()) {
56 	case -1:
57 		log_it("CRON", getpid(), "error", "can't fork");
58 		break;
59 	case 0:
60 		/* child process */
61 		acquire_daemonlock(1);
62 		retval = child_process(e);
63 		Debug(DPROC, ("[%ld] child process done (rc=%d), exiting\n",
64 			      (long)getpid(), retval));
65 		_exit(retval);
66 		break;
67 	default:
68 		/* parent process */
69 		break;
70 	}
71 	Debug(DPROC, ("[%ld] main process returning to work\n",(long)getpid()));
72 }
73 
74 static void
75 sigchld_handler(int signo) {
76 	for (;;) {
77 		WAIT_T waiter;
78 		PID_T pid = waitpid(-1, &waiter, WNOHANG);
79 
80 		switch (pid) {
81 		case -1:
82 			if (errno == EINTR)
83 				continue;
84 		case 0:
85 			return;
86 		default:
87 			break;
88 		}
89 	}
90 }
91 
92 static void
93 write_data(char *volatile input_data, int *stdin_pipe, int *stdout_pipe)
94 {
95 	FILE *out = fdopen(stdin_pipe[WRITE_PIPE], "w");
96 	int need_newline = FALSE;
97 	int escaped = FALSE;
98 	int ch;
99 
100 	Debug(DPROC, ("[%ld] child2 sending data to grandchild\n",
101 		      (long)getpid()));
102 
103 #ifdef USE_PAM
104 	cron_pam_child_close();
105 #else
106 	log_close();
107 #endif
108 
109 	/* close the pipe we don't use, since we inherited it and
110 	 * are part of its reference count now.
111 	 */
112 	(void)close(stdout_pipe[READ_PIPE]);
113 
114 	/* translation:
115 	 *	\% -> %
116 	 *	%  -> \n
117 	 *	\x -> \x	for all x != %
118 	 */
119 	while ((ch = *input_data++) != '\0') {
120 		if (escaped) {
121 			if (ch != '%')
122 				(void)putc('\\', out);
123 		} else {
124 			if (ch == '%')
125 				ch = '\n';
126 		}
127 
128 		if (!(escaped = (ch == '\\'))) {
129 			(void)putc(ch, out);
130 			need_newline = (ch != '\n');
131 		}
132 	}
133 	if (escaped)
134 		(void)putc('\\', out);
135 	if (need_newline)
136 		(void)putc('\n', out);
137 
138 	/* close the pipe, causing an EOF condition.  fclose causes
139 	 * stdin_pipe[WRITE_PIPE] to be closed, too.
140 	 */
141 	(void)fclose(out);
142 
143 	Debug(DPROC, ("[%ld] child2 done sending to grandchild\n",
144 		      (long)getpid()));
145 }
146 
147 static int
148 read_data(entry *e, const char *mailto, const char *usernm, char **envp,
149     int *stdout_pipe, pid_t jobpid)
150 {
151 	FILE	*in = fdopen(stdout_pipe[READ_PIPE], "r");
152 	FILE	*mail = NULL;
153 	int	bytes = 1;
154 	int	status = 0;
155 	int	ch = getc(in);
156 	int	retval = 0;
157 	sig_t	oldchld = NULL;
158 
159 	if (ch == EOF)
160 		goto out;
161 
162 	Debug(DPROC|DEXT, ("[%ld] got data (%x:%c) from grandchild\n",
163 	    (long)getpid(), ch, ch));
164 
165 	/* get name of recipient.  this is MAILTO if set to a
166 	 * valid local username; USER otherwise.
167 	 */
168 	if (mailto) {
169 		/* MAILTO was present in the environment
170 		 */
171 		if (!*mailto) {
172 			/* ... but it's empty. set to NULL
173 			 */
174 			mailto = NULL;
175 		}
176 	} else {
177 		/* MAILTO not present, set to USER.
178 		 */
179 		mailto = usernm;
180 	}
181 
182 	/*
183 	 * Unsafe, disable mailing.
184 	 */
185 	if (mailto && !safe_p(usernm, mailto))
186 		mailto = NULL;
187 
188 	/* if we are supposed to be mailing, MAILTO will
189 	 * be non-NULL.  only in this case should we set
190 	 * up the mail command and subjects and stuff...
191 	 */
192 
193 	if (mailto) {
194 		char	**env;
195 		char	mailcmd[MAX_COMMAND];
196 		char	hostname[MAXHOSTNAMELEN + 1];
197 
198 		(void)gethostname(hostname, MAXHOSTNAMELEN);
199 		if (strlens(MAILFMT, MAILARG, NULL) + 1 >= sizeof mailcmd) {
200 			log_it(usernm, getpid(), "MAIL", "mailcmd too long");
201 			retval = ERROR_EXIT;
202 			goto out;
203 		}
204 		(void)snprintf(mailcmd, sizeof(mailcmd), MAILFMT, MAILARG);
205 		oldchld = signal(SIGCHLD, SIG_DFL);
206 		if (!(mail = cron_popen(mailcmd, "w", e->pwd))) {
207 			log_itx(usernm, getpid(), "MAIL",
208 			    "cannot run `%s'", mailcmd);
209 			(void) signal(SIGCHLD, oldchld);
210 			retval = ERROR_EXIT;
211 			goto out;
212 		}
213 		(void)fprintf(mail, "From: root (Cron Daemon)\n");
214 		(void)fprintf(mail, "To: %s\n", mailto);
215 		(void)fprintf(mail, "Subject: Cron <%s@%s> %s\n",
216 		    usernm, hostname, e->cmd);
217 		(void)fprintf(mail, "Auto-Submitted: auto-generated\n");
218 #ifdef MAIL_DATE
219 		(void)fprintf(mail, "Date: %s\n", arpadate(&StartTime));
220 #endif /*MAIL_DATE*/
221 		for (env = envp;  *env;  env++)
222 			(void)fprintf(mail, "X-Cron-Env: <%s>\n", *env);
223 		(void)fprintf(mail, "\n");
224 
225 		/* this was the first char from the pipe
226 		 */
227 		(void)putc(ch, mail);
228 	}
229 
230 	/* we have to read the input pipe no matter whether
231 	 * we mail or not, but obviously we only write to
232 	 * mail pipe if we ARE mailing.
233 	 */
234 
235 	while (EOF != (ch = getc(in))) {
236 		bytes++;
237 		if (mailto)
238 			(void)putc(ch, mail);
239 	}
240 
241 	/* only close pipe if we opened it -- i.e., we're
242 	 * mailing...
243 	 */
244 
245 	if (mailto) {
246 		if (e->flags & MAIL_WHEN_ERR) {
247 			int jstatus = -1;
248 			if (jobpid <= 0)
249 				log_it("CRON", getpid(), "error",
250 				    "no job pid");
251 			else {
252 				while (waitpid(jobpid, &jstatus, WNOHANG) == -1)
253 					if (errno != EINTR) {
254 						log_it("CRON", getpid(),
255 						    "error", "no job pid");
256 						break;
257 					}
258 			}
259 			/* If everything went well, and -n was set, _and_ we
260 			 * have mail, we won't be mailing... so shoot the
261 			 * messenger!
262 			 */
263 			if (WIFEXITED(jstatus) && WEXITSTATUS(jstatus) == 0) {
264 				Debug(DPROC, ("[%ld] aborting pipe to mail\n",
265 				    (long)getpid()));
266 				status = cron_pabort(mail);
267 				mailto = NULL;
268 			}
269 		}
270 
271 		if (mailto) {
272 			Debug(DPROC, ("[%ld] closing pipe to mail\n",
273 			    (long)getpid()));
274 			/* Note: the pclose will probably see
275 			 * the termination of the grandchild
276 			 * in addition to the mail process, since
277 			 * it (the grandchild) is likely to exit
278 			 * after closing its stdout.
279 			 */
280 			status = cron_pclose(mail);
281 			mail = NULL;
282 		}
283 		(void) signal(SIGCHLD, oldchld);
284 	}
285 
286 	/* if there was output and we could not mail it,
287 	 * log the facts so the poor user can figure out
288 	 * what's going on.
289 	 */
290 	if (mailto && status) {
291 		log_itx(usernm, getpid(), "MAIL",
292 		    "mailed %d byte%s of output to `%s' but"
293 		    " got status %#04x", bytes,
294 		    bytes == 1 ? "" : "s", mailto, status);
295 	}
296 
297 out:
298 	Debug(DPROC, ("[%ld] got EOF from grandchild\n", (long)getpid()));
299 
300 	(void)fclose(in);	/* also closes stdout_pipe[READ_PIPE] */
301 	return retval;
302 }
303 
304 extern char **environ;
305 static int
306 exec_user_command(entry *e, char **envp, char *usernm, int *stdin_pipe,
307     int *stdout_pipe, pid_t *jobpid)
308 {
309 	char *homedir;
310 	char * volatile *ep;
311 
312 	switch (*jobpid = vfork()) {
313 	case -1:
314 		return -1;
315 	case 0:
316 		ep = envp;
317 		Debug(DPROC, ("[%ld] grandchild process vfork()'ed\n",
318 			      (long)getpid()));
319 
320 		/* write a log message.  we've waited this long to do it
321 		 * because it was not until now that we knew the PID that
322 		 * the actual user command shell was going to get and the
323 		 * PID is part of the log message.
324 		 */
325 		if ((e->flags & DONT_LOG) == 0) {
326 			char *x = mkprints(e->cmd, strlen(e->cmd));
327 
328 			log_it(usernm, getpid(), "CMD START", x);
329 			free(x);
330 		}
331 
332 		/* that's the last thing we'll log.  close the log files.
333 		 */
334 		log_close();
335 
336 		/* get new pgrp, void tty, etc.
337 		 */
338 		if (setsid() == -1)
339 			syslog(LOG_ERR, "setsid() failure: %m");
340 
341 		/* close the pipe ends that we won't use.  this doesn't affect
342 		 * the parent, who has to read and write them; it keeps the
343 		 * kernel from recording us as a potential client TWICE --
344 		 * which would keep it from sending SIGPIPE in otherwise
345 		 * appropriate circumstances.
346 		 */
347 		(void)close(stdin_pipe[WRITE_PIPE]);
348 		(void)close(stdout_pipe[READ_PIPE]);
349 
350 		/* grandchild process.  make std{in,out} be the ends of
351 		 * pipes opened by our daddy; make stderr go to stdout.
352 		 */
353 		if (stdin_pipe[READ_PIPE] != STDIN) {
354 			(void)dup2(stdin_pipe[READ_PIPE], STDIN);
355 			(void)close(stdin_pipe[READ_PIPE]);
356 		}
357 		if (stdout_pipe[WRITE_PIPE] != STDOUT) {
358 			(void)dup2(stdout_pipe[WRITE_PIPE], STDOUT);
359 			(void)close(stdout_pipe[WRITE_PIPE]);
360 		}
361 		(void)dup2(STDOUT, STDERR);
362 
363 		/* set our directory, uid and gid.  Set gid first, since once
364 		 * we set uid, we've lost root privledges.
365 		 */
366 #ifdef LOGIN_CAP
367 		{
368 #ifdef BSD_AUTH
369 			auth_session_t *as;
370 #endif
371 			login_cap_t *lc;
372 			char *p;
373 
374 			if ((lc = login_getclass(e->pwd->pw_class)) == NULL) {
375 				warnx("unable to get login class for `%s'",
376 				    e->pwd->pw_name);
377 				_exit(ERROR_EXIT);
378 			}
379 			if (setusercontext(lc, e->pwd, e->pwd->pw_uid, LOGIN_SETALL) < 0) {
380 				warnx("setusercontext failed for `%s'",
381 				    e->pwd->pw_name);
382 				_exit(ERROR_EXIT);
383 			}
384 #ifdef BSD_AUTH
385 			as = auth_open();
386 			if (as == NULL || auth_setpwd(as, e->pwd) != 0) {
387 				warn("can't malloc");
388 				_exit(ERROR_EXIT);
389 			}
390 			if (auth_approval(as, lc, usernm, "cron") <= 0) {
391 				warnx("approval failed for `%s'",
392 				    e->pwd->pw_name);
393 				_exit(ERROR_EXIT);
394 			}
395 			auth_close(as);
396 #endif /* BSD_AUTH */
397 			login_close(lc);
398 
399 			/* If no PATH specified in crontab file but
400 			 * we just added one via login.conf, add it to
401 			 * the crontab environment.
402 			 */
403 			if (env_get("PATH", envp) == NULL && environ != NULL) {
404 				if ((p = getenv("PATH")) != NULL)
405 					ep = env_set(envp, p);
406 			}
407 		}
408 #else
409 		if (setgid(e->pwd->pw_gid) != 0) {
410 			syslog(LOG_ERR, "setgid(%d) failed for %s: %m",
411 			    e->pwd->pw_gid, e->pwd->pw_name);
412 			_exit(ERROR_EXIT);
413 		}
414 		if (initgroups(usernm, e->pwd->pw_gid) != 0) {
415 			syslog(LOG_ERR, "initgroups(%s, %d) failed for %s: %m",
416 			    usernm, e->pwd->pw_gid, e->pwd->pw_name);
417 			_exit(ERROR_EXIT);
418 		}
419 #if (defined(BSD)) && (BSD >= 199103)
420 		if (setlogin(usernm) < 0) {
421 			syslog(LOG_ERR, "setlogin(%s) failure for %s: %m",
422 			    usernm, e->pwd->pw_name);
423 			_exit(ERROR_EXIT);
424 		}
425 #endif /* BSD */
426 #ifdef USE_PAM
427 		if (!cron_pam_setcred())
428 			_exit(ERROR_EXIT);
429 		cron_pam_child_close();
430 #endif
431 		if (setuid(e->pwd->pw_uid) != 0) {
432 			syslog(LOG_ERR, "setuid(%d) failed for %s: %m",
433 			    e->pwd->pw_uid, e->pwd->pw_name);
434 			_exit(ERROR_EXIT);
435 		}
436 		/* we aren't root after this... */
437 #endif /* LOGIN_CAP */
438 		homedir = env_get("HOME", __UNVOLATILE(ep));
439 		if (chdir(homedir) != 0) {
440 			syslog(LOG_ERR, "chdir(%s) $HOME failed for %s: %m",
441 			    homedir, e->pwd->pw_name);
442 			_exit(ERROR_EXIT);
443 		}
444 
445 #ifdef USE_SIGCHLD
446 		/* our grandparent is watching for our death by catching
447 		 * SIGCHLD.  the parent is ignoring SIGCHLD's; we want
448 		 * to restore default behaviour.
449 		 */
450 		(void) signal(SIGCHLD, SIG_DFL);
451 #endif
452 		(void) signal(SIGPIPE, SIG_DFL);
453 		(void) signal(SIGUSR1, SIG_DFL);
454 		(void) signal(SIGHUP, SIG_DFL);
455 
456 		/*
457 		 * Exec the command.
458 		 */
459 		{
460 			char	*shell = env_get("SHELL", __UNVOLATILE(ep));
461 
462 # if DEBUGGING
463 			if (DebugFlags & DTEST) {
464 				(void)fprintf(stderr,
465 				"debug DTEST is on, not exec'ing command.\n");
466 				(void)fprintf(stderr,
467 				"\tcmd='%s' shell='%s'\n", e->cmd, shell);
468 				_exit(OK_EXIT);
469 			}
470 # endif /*DEBUGGING*/
471 			(void)execle(shell, shell, "-c", e->cmd, NULL, envp);
472 			warn("execl: couldn't exec `%s'", shell);
473 			_exit(ERROR_EXIT);
474 		}
475 		return 0;
476 	default:
477 		/* parent process */
478 		return 0;
479 	}
480 }
481 
482 static int
483 child_process(entry *e) {
484 	int stdin_pipe[2], stdout_pipe[2];
485 	char * volatile input_data;
486 	char *usernm, * volatile mailto;
487 	struct sigaction sact;
488 	char **envp = e->envp;
489 	int retval = OK_EXIT;
490 	pid_t jobpid = 0;
491 
492 	Debug(DPROC, ("[%ld] child_process('%s')\n", (long)getpid(), e->cmd));
493 
494 	setproctitle("running job");
495 
496 	/* discover some useful and important environment settings
497 	 */
498 	usernm = e->pwd->pw_name;
499 	mailto = env_get("MAILTO", envp);
500 
501 	memset(&sact, 0, sizeof(sact));
502 	sigemptyset(&sact.sa_mask);
503 	sact.sa_flags = 0;
504 #ifdef SA_RESTART
505 	sact.sa_flags |= SA_RESTART;
506 #endif
507 	sact.sa_handler = sigchld_handler;
508 	(void) sigaction(SIGCHLD, &sact, NULL);
509 
510 	/* create some pipes to talk to our future child
511 	 */
512 	if (pipe(stdin_pipe) == -1) 	/* child's stdin */
513 		log_it("CRON", getpid(), "error", "create child stdin pipe");
514 	if (pipe(stdout_pipe) == -1)	/* child's stdout */
515 		log_it("CRON", getpid(), "error", "create child stdout pipe");
516 
517 	/* since we are a forked process, we can diddle the command string
518 	 * we were passed -- nobody else is going to use it again, right?
519 	 *
520 	 * if a % is present in the command, previous characters are the
521 	 * command, and subsequent characters are the additional input to
522 	 * the command.  An escaped % will have the escape character stripped
523 	 * from it.  Subsequent %'s will be transformed into newlines,
524 	 * but that happens later.
525 	 */
526 	/*local*/{
527 		int escaped = FALSE;
528 		int ch;
529 		char *p;
530 
531 		/* translation:
532 		 *	\% -> %
533 		 *	%  -> end of command, following is command input.
534 		 *	\x -> \x	for all x != %
535 		 */
536 		input_data = p = e->cmd;
537 		while ((ch = *input_data++) != '\0') {
538  			if (escaped) {
539 				if (ch != '%')
540 					*p++ = '\\';
541 			} else {
542 				if (ch == '%') {
543 					break;
544 				}
545 			}
546 
547 			if (!(escaped = (ch == '\\'))) {
548 				*p++ = (char)ch;
549 			}
550 		}
551 		if (ch == '\0') {
552 			/* move pointer back, so that code below
553 			 * won't think we encountered % sequence */
554 			input_data--;
555 		}
556 		if (escaped)
557 			*p++ = '\\';
558 
559 		*p = '\0';
560 	}
561 
562 #ifdef USE_PAM
563 	if (!cron_pam_start(usernm))
564 		return ERROR_EXIT;
565 
566 	if (!(envp = cron_pam_getenvlist(envp))) {
567 		retval = ERROR_EXIT;
568 		goto child_process_end;
569 	}
570 #endif
571 
572 	/* fork again, this time so we can exec the user's command.
573 	 */
574 	if (exec_user_command(e, envp, usernm, stdin_pipe, stdout_pipe,
575 	    &jobpid) == -1) {
576 		retval = ERROR_EXIT;
577 		goto child_process_end;
578 	}
579 
580 
581 	/* middle process, child of original cron, parent of process running
582 	 * the user's command.
583 	 */
584 
585 	Debug(DPROC, ("[%ld] child continues, closing pipes\n",(long)getpid()));
586 
587 	/* close the ends of the pipe that will only be referenced in the
588 	 * grandchild process...
589 	 */
590 	(void)close(stdin_pipe[READ_PIPE]);
591 	(void)close(stdout_pipe[WRITE_PIPE]);
592 
593 	/*
594 	 * write, to the pipe connected to child's stdin, any input specified
595 	 * after a % in the crontab entry.  while we copy, convert any
596 	 * additional %'s to newlines.  when done, if some characters were
597 	 * written and the last one wasn't a newline, write a newline.
598 	 *
599 	 * Note that if the input data won't fit into one pipe buffer (2K
600 	 * or 4K on most BSD systems), and the child doesn't read its stdin,
601 	 * we would block here.  thus we must fork again.
602 	 */
603 
604 	if (*input_data) {
605 		switch (fork()) {
606 		case 0:
607 			write_data(input_data, stdin_pipe, stdout_pipe);
608 			exit(EXIT_SUCCESS);
609 		case -1:
610 			retval = ERROR_EXIT;
611 			goto child_process_end;
612 		default:
613 			break;
614 		}
615 	}
616 
617 	/* close the pipe to the grandkiddie's stdin, since its wicked uncle
618 	 * ernie back there has it open and will close it when he's done.
619 	 */
620 	(void)close(stdin_pipe[WRITE_PIPE]);
621 
622 	/*
623 	 * read output from the grandchild.  it's stderr has been redirected to
624 	 * it's stdout, which has been redirected to our pipe.  if there is any
625 	 * output, we'll be mailing it to the user whose crontab this is...
626 	 * when the grandchild exits, we'll get EOF.
627 	 */
628 
629 	Debug(DPROC, ("[%ld] child reading output from grandchild\n",
630 		      (long)getpid()));
631 
632 	retval = read_data(e, mailto, usernm, envp, stdout_pipe, jobpid);
633 	if (retval)
634 		goto child_process_end;
635 
636 
637 	/* wait for children to die.
638 	 */
639 	sigchld_handler(0);
640 
641 	/* Log the time when we finished deadling with the job */
642 	/*local*/{
643 		char *x = mkprints(e->cmd, strlen(e->cmd));
644 
645 		log_it(usernm, getpid(), "CMD FINISH", x);
646 		free(x);
647 	}
648 
649 child_process_end:
650 #ifdef USE_PAM
651 	cron_pam_finish();
652 #endif
653 	return retval;
654 }
655 
656 static int
657 safe_p(const char *usernm, const char *s) {
658 	static const char safe_delim[] = "@!:%-.,";     /* conservative! */
659 	const char *t;
660 	int ch, first;
661 
662 	for (t = s, first = 1; (ch = *t++) != '\0'; first = 0) {
663 		if (isascii(ch) && isprint(ch) &&
664 		    (isalnum(ch) || (!first && strchr(safe_delim, ch))))
665 			continue;
666 		log_it(usernm, getpid(), "UNSAFE", s);
667 		return (FALSE);
668 	}
669 	return (TRUE);
670 }
671