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