1 /* $OpenBSD: atrun.c,v 1.54 2022/12/28 21:30:16 jmc Exp $ */
2
3 /*
4 * Copyright (c) 2002-2003 Todd C. Miller <millert@openbsd.org>
5 *
6 * Permission to use, copy, modify, and distribute this software for any
7 * purpose with or without fee is hereby granted, provided that the above
8 * copyright notice and this permission notice appear in all copies.
9 *
10 * THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES
11 * WITH REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF
12 * MERCHANTABILITY AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR
13 * ANY SPECIAL, DIRECT, INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES
14 * WHATSOEVER RESULTING FROM LOSS OF USE, DATA OR PROFITS, WHETHER IN AN
15 * ACTION OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT OF
16 * OR IN CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE.
17 *
18 * Sponsored in part by the Defense Advanced Research Projects
19 * Agency (DARPA) and Air Force Research Laboratory, Air Force
20 * Materiel Command, USAF, under agreement number F39502-99-1-0512.
21 */
22
23 #include <sys/types.h>
24 #include <sys/resource.h>
25 #include <sys/stat.h>
26 #include <sys/time.h>
27 #include <sys/wait.h>
28
29 #include <bitstring.h> /* for structs.h */
30 #include <bsd_auth.h>
31 #include <ctype.h>
32 #include <dirent.h>
33 #include <err.h>
34 #include <errno.h>
35 #include <fcntl.h>
36 #include <limits.h>
37 #include <login_cap.h>
38 #include <pwd.h>
39 #include <signal.h>
40 #include <stdio.h>
41 #include <stdlib.h>
42 #include <string.h>
43 #include <syslog.h>
44 #include <time.h>
45 #include <unistd.h>
46
47 #include "config.h"
48 #include "pathnames.h"
49 #include "macros.h"
50 #include "structs.h"
51 #include "funcs.h"
52 #include "globals.h"
53
54 static void run_job(const atjob *, int, const char *);
55
56 /*
57 * Scan the at jobs dir and build up a list of jobs found.
58 */
59 int
scan_atjobs(at_db ** db,struct timespec * ts)60 scan_atjobs(at_db **db, struct timespec *ts)
61 {
62 DIR *atdir = NULL;
63 int dfd, pending;
64 const char *errstr;
65 time_t run_time;
66 char *queue;
67 at_db *new_db, *old_db = *db;
68 atjob *job;
69 struct dirent *file;
70 struct stat sb;
71
72 dfd = open(_PATH_AT_SPOOL, O_RDONLY|O_DIRECTORY|O_CLOEXEC);
73 if (dfd == -1) {
74 syslog(LOG_ERR, "(CRON) OPEN FAILED (%s)", _PATH_AT_SPOOL);
75 return (0);
76 }
77 if (fstat(dfd, &sb) != 0) {
78 syslog(LOG_ERR, "(CRON) FSTAT FAILED (%s)", _PATH_AT_SPOOL);
79 close(dfd);
80 return (0);
81 }
82 if (old_db != NULL && timespeccmp(&old_db->mtime, &sb.st_mtim, ==)) {
83 close(dfd);
84 return (0);
85 }
86
87 if ((atdir = fdopendir(dfd)) == NULL) {
88 syslog(LOG_ERR, "(CRON) OPENDIR FAILED (%s)", _PATH_AT_SPOOL);
89 close(dfd);
90 return (0);
91 }
92
93 if ((new_db = malloc(sizeof(*new_db))) == NULL) {
94 closedir(atdir);
95 return (0);
96 }
97 new_db->mtime = sb.st_mtim; /* stash at dir mtime */
98 TAILQ_INIT(&new_db->jobs);
99
100 pending = 0;
101 while ((file = readdir(atdir)) != NULL) {
102 if (fstatat(dfd, file->d_name, &sb, AT_SYMLINK_NOFOLLOW) != 0 ||
103 !S_ISREG(sb.st_mode))
104 continue;
105
106 /*
107 * at jobs are named as RUNTIME.QUEUE
108 * RUNTIME is the time to run in seconds since the epoch
109 * QUEUE is a letter that designates the job's queue
110 */
111 if ((queue = strchr(file->d_name, '.')) == NULL)
112 continue;
113 *queue++ = '\0';
114 run_time = strtonum(file->d_name, 0, LLONG_MAX, &errstr);
115 if (errstr != NULL)
116 continue;
117 if (!isalpha((unsigned char)*queue))
118 continue;
119
120 job = malloc(sizeof(*job));
121 if (job == NULL) {
122 while ((job = TAILQ_FIRST(&new_db->jobs))) {
123 TAILQ_REMOVE(&new_db->jobs, job, entries);
124 free(job);
125 }
126 free(new_db);
127 closedir(atdir);
128 return (0);
129 }
130 job->uid = sb.st_uid;
131 job->gid = sb.st_gid;
132 job->queue = *queue;
133 job->run_time = run_time;
134 TAILQ_INSERT_TAIL(&new_db->jobs, job, entries);
135 if (ts != NULL && run_time <= ts->tv_sec)
136 pending = 1;
137 }
138 closedir(atdir);
139
140 /* Free up old at db and install new one */
141 if (old_db != NULL) {
142 while ((job = TAILQ_FIRST(&old_db->jobs))) {
143 TAILQ_REMOVE(&old_db->jobs, job, entries);
144 free(job);
145 }
146 free(old_db);
147 }
148 *db = new_db;
149
150 return (pending);
151 }
152
153 /*
154 * Loop through the at job database and run jobs whose time have come.
155 */
156 void
atrun(at_db * db,double batch_maxload,time_t now)157 atrun(at_db *db, double batch_maxload, time_t now)
158 {
159 char atfile[PATH_MAX];
160 struct stat sb;
161 double la;
162 int dfd, len;
163 atjob *job, *tjob, *batch = NULL;
164
165 if (db == NULL)
166 return;
167
168 dfd = open(_PATH_AT_SPOOL, O_RDONLY|O_DIRECTORY|O_CLOEXEC);
169 if (dfd == -1) {
170 syslog(LOG_ERR, "(CRON) OPEN FAILED (%s)", _PATH_AT_SPOOL);
171 return;
172 }
173
174 TAILQ_FOREACH_SAFE(job, &db->jobs, entries, tjob) {
175 /* Skip jobs in the future */
176 if (job->run_time > now)
177 continue;
178
179 len = snprintf(atfile, sizeof(atfile), "%lld.%c",
180 (long long)job->run_time, job->queue);
181 if (len < 0 || len >= sizeof(atfile)) {
182 TAILQ_REMOVE(&db->jobs, job, entries);
183 free(job);
184 continue;
185 }
186
187 if (fstatat(dfd, atfile, &sb, AT_SYMLINK_NOFOLLOW) != 0) {
188 TAILQ_REMOVE(&db->jobs, job, entries);
189 free(job);
190 continue; /* disappeared from queue */
191 }
192 if (!S_ISREG(sb.st_mode)) {
193 syslog(LOG_WARNING, "(CRON) NOT REGULAR (%s)",
194 atfile);
195 TAILQ_REMOVE(&db->jobs, job, entries);
196 free(job);
197 continue; /* was a file, no longer is */
198 }
199
200 /*
201 * Pending jobs have the user execute bit set.
202 */
203 if (sb.st_mode & S_IXUSR) {
204 /* new job to run */
205 if (isupper(job->queue)) {
206 /* we run one batch job per atrun() call */
207 if (batch == NULL ||
208 job->run_time < batch->run_time)
209 batch = job;
210 } else {
211 /* normal at job */
212 run_job(job, dfd, atfile);
213 TAILQ_REMOVE(&db->jobs, job, entries);
214 free(job);
215 }
216 }
217 }
218
219 /* Run a single batch job if there is one pending. */
220 if (batch != NULL
221 && (batch_maxload == 0.0 ||
222 ((getloadavg(&la, 1) == 1) && la <= batch_maxload))
223 ) {
224 len = snprintf(atfile, sizeof(atfile), "%lld.%c",
225 (long long)batch->run_time, batch->queue);
226 if (len < 0 || len >= sizeof(atfile))
227 ;
228 else
229 run_job(batch, dfd, atfile);
230 TAILQ_REMOVE(&db->jobs, batch, entries);
231 free(job);
232 }
233
234 close(dfd);
235 }
236
237 /*
238 * Check the at job header for sanity and extract the
239 * uid, gid, mailto user and always_mail flag.
240 *
241 * The header should look like this:
242 * #!/bin/sh
243 * # atrun uid=123 gid=123
244 * # mail joeuser 0
245 */
246 static int
parse_header(FILE * fp,uid_t * nuid,gid_t * ngid,char * mailto,int * always_mail)247 parse_header(FILE *fp, uid_t *nuid, gid_t *ngid, char *mailto, int *always_mail)
248 {
249 char *cp, *ep, *line = NULL;
250 const char *errstr;
251 size_t size = 0;
252 int lineno = 0;
253 ssize_t len;
254 int ret = -1;
255
256 for (lineno = 1; (len = getline(&line, &size, fp)) != -1; lineno++) {
257 if (line[--len] != '\n')
258 break;
259 line[len] = '\0';
260
261 switch (lineno) {
262 case 1:
263 if (strcmp(line, "#!/bin/sh") != 0)
264 goto done;
265 break;
266 case 2:
267 if (strncmp(line, "# atrun uid=", 12) != 0)
268 goto done;
269
270 /* Pull out uid */
271 cp = line + 12;
272 if ((ep = strchr(cp, ' ')) == NULL)
273 goto done;
274 *ep++ = '\0';
275 *nuid = strtonum(cp, 0, UID_MAX - 1, &errstr);
276 if (errstr != NULL)
277 goto done;
278
279 /* Pull out gid */
280 if (strncmp(ep, "gid=", 4) != 0)
281 goto done;
282 cp = ep + 4;
283 *ngid = strtonum(cp, 0, GID_MAX - 1, &errstr);
284 if (errstr != NULL)
285 goto done;
286 break;
287 case 3:
288 /* Pull out mailto user (and always_mail flag) */
289 if (strncmp(line, "# mail ", 7) != 0)
290 goto done;
291 for (cp = line + 7; *cp == ' '; cp++)
292 continue;
293 if (*cp == '\0')
294 goto done;
295 for (ep = cp; *ep != ' ' && *ep != '\0'; ep++)
296 continue;
297 if (*ep != ' ')
298 goto done;
299 *ep++ = '\0';
300 if (strlcpy(mailto, cp, MAX_UNAME) >= MAX_UNAME)
301 goto done;
302 *always_mail = *ep == '1';
303
304 /* success */
305 ret = 0;
306 goto done;
307 default:
308 /* can't happen */
309 goto done;
310 }
311 }
312 done:
313 free(line);
314 return ret;
315 }
316
317 /*
318 * Run the specified job contained in atfile.
319 */
320 static void
run_job(const atjob * job,int dfd,const char * atfile)321 run_job(const atjob *job, int dfd, const char *atfile)
322 {
323 struct stat sb;
324 struct passwd *pw;
325 login_cap_t *lc;
326 auth_session_t *as;
327 pid_t pid;
328 uid_t nuid;
329 gid_t ngid;
330 FILE *fp;
331 int waiter;
332 size_t nread;
333 char mailto[MAX_UNAME], buf[BUFSIZ];
334 int fd, always_mail;
335 int output_pipe[2];
336 char *nargv[2], *nenvp[1];
337
338 /* Open the file and unlink it so we don't try running it again. */
339 if ((fd = openat(dfd, atfile, O_RDONLY|O_NONBLOCK|O_NOFOLLOW)) == -1) {
340 syslog(LOG_ERR, "(CRON) CAN'T OPEN (%s)", atfile);
341 return;
342 }
343 unlinkat(dfd, atfile, 0);
344
345 /* Fork so other pending jobs don't have to wait for us to finish. */
346 switch (fork()) {
347 case 0:
348 /* child */
349 break;
350 case -1:
351 /* error */
352 syslog(LOG_ERR, "(CRON) CAN'T FORK (%m)");
353 /* FALLTHROUGH */
354 default:
355 /* parent */
356 close(fd);
357 return;
358 }
359
360 /* Close fds opened by the parent. */
361 close(cronSock);
362 close(dfd);
363
364 /*
365 * We don't want the main cron daemon to wait for our children--
366 * we will do it ourselves via waitpid().
367 */
368 (void) signal(SIGCHLD, SIG_DFL);
369
370 /*
371 * Verify the user still exists and their account has not expired.
372 */
373 pw = getpwuid(job->uid);
374 if (pw == NULL) {
375 syslog(LOG_WARNING, "(CRON) ORPHANED JOB (%s)", atfile);
376 _exit(EXIT_FAILURE);
377 }
378 if (pw->pw_expire && time(NULL) >= pw->pw_expire) {
379 syslog(LOG_NOTICE, "(%s) ACCOUNT EXPIRED, JOB ABORTED (%s)",
380 pw->pw_name, atfile);
381 _exit(EXIT_FAILURE);
382 }
383
384 /* Sanity checks */
385 if (fstat(fd, &sb) == -1) {
386 syslog(LOG_ERR, "(%s) FSTAT FAILED (%s)", pw->pw_name, atfile);
387 _exit(EXIT_FAILURE);
388 }
389 if (!S_ISREG(sb.st_mode)) {
390 syslog(LOG_WARNING, "(%s) NOT REGULAR (%s)", pw->pw_name,
391 atfile);
392 _exit(EXIT_FAILURE);
393 }
394 if ((sb.st_mode & ALLPERMS) != (S_IRUSR | S_IWUSR | S_IXUSR)) {
395 syslog(LOG_WARNING, "(%s) BAD FILE MODE (%s)", pw->pw_name,
396 atfile);
397 _exit(EXIT_FAILURE);
398 }
399 if (sb.st_uid != 0 && sb.st_uid != job->uid) {
400 syslog(LOG_WARNING, "(%s) WRONG FILE OWNER (%s)", pw->pw_name,
401 atfile);
402 _exit(EXIT_FAILURE);
403 }
404 if (sb.st_gid != cron_gid) {
405 syslog(LOG_WARNING, "(%s) WRONG FILE GROUP (%s)", pw->pw_name,
406 atfile);
407 _exit(EXIT_FAILURE);
408 }
409 if (sb.st_nlink > 1) {
410 syslog(LOG_WARNING, "(%s) BAD LINK COUNT (%s)", pw->pw_name,
411 atfile);
412 _exit(EXIT_FAILURE);
413 }
414 if ((fp = fdopen(dup(fd), "r")) == NULL) {
415 syslog(LOG_ERR, "(CRON) DUP FAILED (%m)");
416 _exit(EXIT_FAILURE);
417 }
418 if (parse_header(fp, &nuid, &ngid, mailto, &always_mail) == -1) {
419 syslog(LOG_ERR, "(%s) BAD FILE FORMAT (%s)", pw->pw_name,
420 atfile);
421 _exit(EXIT_FAILURE);
422 }
423 (void)fclose(fp);
424 if (!safe_p(pw->pw_name, mailto))
425 _exit(EXIT_FAILURE);
426 if ((uid_t)nuid != job->uid) {
427 syslog(LOG_WARNING, "(%s) UID MISMATCH (%s)", pw->pw_name,
428 atfile);
429 _exit(EXIT_FAILURE);
430 }
431 if ((gid_t)ngid != job->gid) {
432 syslog(LOG_WARNING, "(%s) GID MISMATCH (%s)", pw->pw_name,
433 atfile);
434 _exit(EXIT_FAILURE);
435 }
436
437 /* mark ourselves as different to PS command watchers */
438 setproctitle("atrun %s", atfile);
439
440 if (pipe(output_pipe) != 0) { /* child's stdout/stderr */
441 syslog(LOG_ERR, "(CRON) PIPE (%m)");
442 _exit(EXIT_FAILURE);
443 }
444
445 /* Fork again, child will run the job, parent will catch output. */
446 switch ((pid = fork())) {
447 case -1:
448 syslog(LOG_ERR, "(CRON) CAN'T FORK (%m)");
449 _exit(EXIT_FAILURE);
450 /*NOTREACHED*/
451 case 0:
452 /* Write log message now that we have our real pid. */
453 syslog(LOG_INFO, "(%s) ATJOB (%s)", pw->pw_name, atfile);
454
455 /* Connect grandchild's stdin to the at job file. */
456 if (lseek(fd, 0, SEEK_SET) == -1) {
457 syslog(LOG_ERR, "(CRON) LSEEK (%m)");
458 _exit(EXIT_FAILURE);
459 }
460 if (fd != STDIN_FILENO) {
461 dup2(fd, STDIN_FILENO);
462 close(fd);
463 }
464
465 /* Connect stdout/stderr to the pipe from our parent. */
466 if (output_pipe[WRITE_PIPE] != STDOUT_FILENO) {
467 dup2(output_pipe[WRITE_PIPE], STDOUT_FILENO);
468 close(output_pipe[WRITE_PIPE]);
469 }
470 dup2(STDOUT_FILENO, STDERR_FILENO);
471 close(output_pipe[READ_PIPE]);
472
473 (void) setsid();
474
475 /*
476 * From this point on, anything written to stderr will be
477 * mailed to the user as output.
478 */
479
480 /* Setup execution environment as per login.conf */
481 if ((lc = login_getclass(pw->pw_class)) == NULL) {
482 warnx("unable to get login class for %s",
483 pw->pw_name);
484 syslog(LOG_ERR, "(CRON) CAN'T GET LOGIN CLASS (%s)",
485 pw->pw_name);
486 _exit(EXIT_FAILURE);
487
488 }
489 if (setusercontext(lc, pw, pw->pw_uid, LOGIN_SETALL)) {
490 warn("setusercontext failed for %s", pw->pw_name);
491 syslog(LOG_ERR, "(%s) SETUSERCONTEXT FAILED (%m)",
492 pw->pw_name);
493 _exit(EXIT_FAILURE);
494 }
495
496 /* Run any approval scripts. */
497 as = auth_open();
498 if (as == NULL || auth_setpwd(as, pw) != 0) {
499 warn("auth_setpwd");
500 syslog(LOG_ERR, "(%s) AUTH_SETPWD FAILED (%m)",
501 pw->pw_name);
502 _exit(EXIT_FAILURE);
503 }
504 if (auth_approval(as, lc, pw->pw_name, "cron") <= 0) {
505 warnx("approval failed for %s", pw->pw_name);
506 syslog(LOG_ERR, "(%s) APPROVAL FAILED (cron)",
507 pw->pw_name);
508 _exit(EXIT_FAILURE);
509 }
510 auth_close(as);
511 login_close(lc);
512
513 /* If this is a low priority job, nice ourself. */
514 if (job->queue > 'b') {
515 if (setpriority(PRIO_PROCESS, 0, job->queue - 'b') != 0)
516 syslog(LOG_ERR, "(%s) CAN'T NICE (%m)",
517 pw->pw_name);
518 }
519
520 (void) signal(SIGPIPE, SIG_DFL);
521
522 /*
523 * Exec /bin/sh with stdin connected to the at job file
524 * and stdout/stderr hooked up to our parent.
525 * The at file will set the environment up for us.
526 */
527 nargv[0] = "sh";
528 nargv[1] = NULL;
529 nenvp[0] = NULL;
530 if (execve(_PATH_BSHELL, nargv, nenvp) != 0) {
531 warn("unable to execute %s", _PATH_BSHELL);
532 syslog(LOG_ERR, "(%s) CAN'T EXEC (%s: %m)", pw->pw_name,
533 _PATH_BSHELL);
534 _exit(EXIT_FAILURE);
535 }
536 break;
537 default:
538 /* parent */
539 break;
540 }
541
542 /* Close the atfile's fd and the end of the pipe we don't use. */
543 close(fd);
544 close(output_pipe[WRITE_PIPE]);
545
546 /* Read piped output (if any) from the at job. */
547 if ((fp = fdopen(output_pipe[READ_PIPE], "r")) == NULL) {
548 syslog(LOG_ERR, "(%s) FDOPEN (%m)", pw->pw_name);
549 (void) _exit(EXIT_FAILURE);
550 }
551 nread = fread(buf, 1, sizeof(buf), fp);
552 if (nread != 0 || always_mail) {
553 FILE *mail;
554 pid_t mailpid;
555 size_t bytes = 0;
556 int status = 0;
557 char mailcmd[MAX_COMMAND];
558 char hostname[HOST_NAME_MAX + 1];
559
560 if (gethostname(hostname, sizeof(hostname)) != 0)
561 strlcpy(hostname, "unknown", sizeof(hostname));
562 if (snprintf(mailcmd, sizeof mailcmd, MAILFMT,
563 MAILARG) >= sizeof mailcmd) {
564 syslog(LOG_ERR, "(%s) ERROR (mailcmd too long)",
565 pw->pw_name);
566 (void) _exit(EXIT_FAILURE);
567 }
568 if (!(mail = cron_popen(mailcmd, "w", pw, &mailpid))) {
569 syslog(LOG_ERR, "(%s) POPEN (%s)", pw->pw_name, mailcmd);
570 (void) _exit(EXIT_FAILURE);
571 }
572 fprintf(mail, "From: %s (Atrun Service)\n", pw->pw_name);
573 fprintf(mail, "To: %s\n", mailto);
574 fprintf(mail, "Subject: Output from \"at\" job\n");
575 fprintf(mail, "Auto-Submitted: auto-generated\n");
576 fprintf(mail, "\nYour \"at\" job on %s\n\"%s/%s\"\n",
577 hostname, _PATH_AT_SPOOL, atfile);
578 fprintf(mail, "\nproduced the following output:\n\n");
579
580 /* Pipe the job's output to sendmail. */
581 do {
582 bytes += nread;
583 fwrite(buf, nread, 1, mail);
584 } while ((nread = fread(buf, 1, sizeof(buf), fp)) != 0);
585
586 /*
587 * If the mailer exits with non-zero exit status, log
588 * this fact so the problem can (hopefully) be debugged.
589 */
590 if ((status = cron_pclose(mail, mailpid)) != 0) {
591 syslog(LOG_NOTICE, "(%s) MAIL (mailed %zu byte%s of "
592 "output but got status 0x%04x)", pw->pw_name,
593 bytes, (bytes == 1) ? "" : "s", status);
594 }
595 }
596
597 fclose(fp); /* also closes output_pipe[READ_PIPE] */
598
599 /* Wait for grandchild to die. */
600 for (;;) {
601 if (waitpid(pid, &waiter, 0) == -1) {
602 if (errno == EINTR)
603 continue;
604 break;
605 } else {
606 /*
607 if (WIFSIGNALED(waiter) && WCOREDUMP(waiter))
608 Debug(DPROC, (", dumped core"))
609 */
610 break;
611 }
612 }
613 _exit(EXIT_SUCCESS);
614 }
615