Annotation of src/libexec/atrun/atrun.c, Revision 1.20
1.20 ! mbalmer 1: /* $NetBSD: atrun.c,v 1.19 2008/04/05 20:17:37 christos Exp $ */
1.3 thorpej 2:
1.1 cgd 3: /*
1.5 christos 4: * atrun.c - run jobs queued by at; run with root privileges.
5: * Copyright (C) 1993, 1994 Thomas Koenig
1.1 cgd 6: *
7: * Redistribution and use in source and binary forms, with or without
8: * modification, are permitted provided that the following conditions
9: * are met:
10: * 1. Redistributions of source code must retain the above copyright
11: * notice, this list of conditions and the following disclaimer.
12: * 2. The name of the author(s) may not be used to endorse or promote
13: * products derived from this software without specific prior written
14: * permission.
15: *
16: * THIS SOFTWARE IS PROVIDED BY THE AUTHOR(S) ``AS IS'' AND ANY EXPRESS OR
17: * IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES
18: * OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE DISCLAIMED.
1.5 christos 19: * IN NO EVENT SHALL THE AUTHOR(S) BE LIABLE FOR ANY DIRECT, INDIRECT,
1.1 cgd 20: * INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT
21: * NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE,
22: * DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY
23: * THEORY OF LIABILITY, WETHER IN CONTRACT, STRICT LIABILITY, OR TORT
24: * (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF
25: * THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
26: */
27:
28: /* System Headers */
1.17 perry 29: #include <sys/cdefs.h>
1.4 mrg 30: #include <sys/types.h>
1.1 cgd 31: #include <sys/wait.h>
1.10 christos 32:
1.5 christos 33: #include <ctype.h>
1.10 christos 34: #include <errno.h>
1.1 cgd 35: #include <dirent.h>
1.6 kleink 36: #include <fcntl.h>
1.1 cgd 37: #include <stdio.h>
38: #include <stdlib.h>
39: #include <string.h>
1.10 christos 40: #include <limits.h>
1.1 cgd 41: #include <time.h>
42: #include <unistd.h>
43: #include <syslog.h>
1.10 christos 44: #include <pwd.h>
1.15 christos 45: #include <grp.h>
1.10 christos 46: #include <err.h>
1.1 cgd 47: #include <paths.h>
1.16 christos 48: #include <stdarg.h>
1.1 cgd 49:
50: /* Local headers */
51:
52: #define MAIN
53: #include "privs.h"
54: #include "pathnames.h"
55: #include "atrun.h"
56:
57: /* File scope variables */
58:
1.5 christos 59: #if 0
60: static char rcsid[] = "$OpenBSD: atrun.c,v 1.7 1997/09/08 22:12:10 millert Exp $";
61: #else
1.20 ! mbalmer 62: __RCSID("$NetBSD: atrun.c,v 1.19 2008/04/05 20:17:37 christos Exp $");
1.4 mrg 63: #endif
1.1 cgd 64:
1.5 christos 65: static int debug = 0;
66:
1.1 cgd 67: /* Local functions */
1.18 perry 68: static void perr(const char *, ...) __dead;
69: static void perrx(const char *, ...) __dead;
1.16 christos 70: static int write_string(int, const char *);
71: static void run_file(const char *, uid_t, gid_t);
72: static void become_user(struct passwd *, uid_t);
1.5 christos 73:
1.15 christos 74: static const char nobody[] = "nobody";
75:
1.1 cgd 76: static void
1.16 christos 77: perr(const char *fmt, ...)
1.1 cgd 78: {
1.16 christos 79: char buf[2048];
80: va_list ap;
81:
82: va_start(ap, fmt);
83: (void)vsnprintf(buf, sizeof(buf), fmt, ap);
84: va_end(ap);
85:
1.5 christos 86: if (debug)
1.16 christos 87: warn("%s", buf);
1.5 christos 88: else
1.16 christos 89: syslog(LOG_ERR, "%s: %m", buf);
1.5 christos 90:
91: exit(EXIT_FAILURE);
92: }
93:
94: static void
1.16 christos 95: perrx(const char *fmt, ...)
1.5 christos 96: {
1.16 christos 97: va_list ap;
98:
99: va_start(ap, fmt);
100:
101: if (debug)
102: vwarnx(fmt, ap);
103: else
104: vsyslog(LOG_ERR, fmt, ap);
105:
106: va_end(ap);
1.5 christos 107:
1.1 cgd 108: exit(EXIT_FAILURE);
109: }
110:
111: static int
1.16 christos 112: write_string(int fd, const char *a)
1.1 cgd 113: {
1.16 christos 114: return write(fd, a, strlen(a));
1.5 christos 115: }
116:
117: static void
1.16 christos 118: become_user(struct passwd *pentry, uid_t uid)
1.5 christos 119: {
1.16 christos 120: if (initgroups(pentry->pw_name, pentry->pw_gid) == -1)
121: perr("Cannot init group list for `%s'", pentry->pw_name);
1.5 christos 122:
1.16 christos 123: if (setegid(pentry->pw_gid) == -1 || setgid(pentry->pw_gid) == -1)
124: perr("Cannot change primary group to %lu",
125: (unsigned long)pentry->pw_gid);
1.12 dsl 126:
1.16 christos 127: if (setsid() == -1)
1.12 dsl 128: perr("Cannot create a session");
1.5 christos 129:
1.16 christos 130: if (setlogin(pentry->pw_name) == -1)
131: perr("Cannot set login name to `%s'", pentry->pw_name);
1.5 christos 132:
1.16 christos 133: if (setuid(uid) == -1 || seteuid(uid) == -1)
134: perr("Cannot set user id to %lu", (unsigned long)uid);
1.5 christos 135:
1.16 christos 136: if (chdir(pentry->pw_dir) == -1)
137: (void)chdir("/");
1.1 cgd 138: }
139:
140: static void
1.16 christos 141: run_file(const char *filename, uid_t uid, gid_t gid)
1.1 cgd 142: {
143: /*
144: * Run a file by by spawning off a process which redirects I/O,
145: * spawns a subshell, then waits for it to complete and spawns another
146: * process to send mail to the user.
147: */
148: pid_t pid;
149: int fd_out, fd_in;
150: int queue;
1.8 simonb 151: char mailbuf[LOGIN_NAME_MAX], fmt[49];
1.1 cgd 152: char *mailname = NULL;
153: FILE *stream;
154: int send_mail = 0;
1.5 christos 155: struct stat buf, lbuf;
1.1 cgd 156: off_t size;
157: struct passwd *pentry;
158: int fflags;
1.16 christos 159: uid_t nuid;
160: gid_t ngid;
161: int serrno;
1.5 christos 162:
1.19 christos 163: PRIV_START;
1.5 christos 164:
1.16 christos 165: if (chmod(filename, S_IRUSR) == -1)
166: perr("Cannot change file permissions to `%s'", filename);
1.5 christos 167:
1.19 christos 168: PRIV_END;
1.1 cgd 169:
170: pid = fork();
171: if (pid == -1)
172: perr("Cannot fork");
1.5 christos 173: else if (pid != 0)
1.1 cgd 174: return;
175:
176: /*
177: * Let's see who we mail to. Hopefully, we can read it from the
178: * command file; if not, send it to the owner, or, failing that, to
179: * root.
180: */
181:
1.5 christos 182: pentry = getpwuid(uid);
1.16 christos 183: if (pentry == NULL)
184: perrx("Userid %lu not found - aborting job `%s'",
185: (unsigned long)uid, filename);
186:
1.19 christos 187: PRIV_START;
1.1 cgd 188:
1.5 christos 189: stream = fopen(filename, "r");
1.16 christos 190: serrno = errno;
1.1 cgd 191:
1.19 christos 192: PRIV_END;
1.1 cgd 193:
1.16 christos 194: if (stream == NULL) {
195: errno = serrno;
196: perr("Cannot open input file");
1.5 christos 197: }
1.2 cgd 198:
1.16 christos 199: if (pentry->pw_expire && time(NULL) >= pentry->pw_expire)
200: perrx("Userid %lu has expired - aborting job `%s'",
201: (unsigned long)uid, filename);
1.1 cgd 202:
1.16 christos 203: if ((fd_in = dup(fileno(stream))) == -1)
1.1 cgd 204: perr("Error duplicating input file descriptor");
205:
1.5 christos 206: if (fstat(fd_in, &buf) == -1)
207: perr("Error in fstat of input file descriptor");
208:
1.19 christos 209: PRIV_START;
1.5 christos 210:
211: if (lstat(filename, &lbuf) == -1)
1.16 christos 212: perr("Error in lstat of `%s'", filename);
1.5 christos 213:
1.19 christos 214: PRIV_END;
1.5 christos 215:
1.16 christos 216: if (S_ISLNK(lbuf.st_mode))
217: perrx("Symbolic link encountered in job `%s' - aborting",
1.5 christos 218: filename);
1.16 christos 219:
1.5 christos 220: if ((lbuf.st_dev != buf.st_dev) || (lbuf.st_ino != buf.st_ino) ||
221: (lbuf.st_uid != buf.st_uid) || (lbuf.st_gid != buf.st_gid) ||
1.16 christos 222: (lbuf.st_size!=buf.st_size))
223: perrx("Somebody changed files from under us for job `%s' "
224: "- aborting", filename);
225:
226: if (buf.st_nlink > 1)
227: perrx("Somebody is trying to run a linked script for job `%s'",
1.5 christos 228: filename);
1.16 christos 229:
1.1 cgd 230: if ((fflags = fcntl(fd_in, F_GETFD)) < 0)
231: perr("Error in fcntl");
232:
1.5 christos 233: (void)fcntl(fd_in, F_SETFD, fflags & ~FD_CLOEXEC);
1.1 cgd 234:
1.5 christos 235: (void)snprintf(fmt, sizeof(fmt),
1.9 martin 236: "#!/bin/sh\n# atrun uid=%%u gid=%%u\n# mail %%%ds %%d",
1.8 simonb 237: LOGIN_NAME_MAX);
1.16 christos 238:
239: if (fscanf(stream, fmt, &nuid, &ngid, mailbuf, &send_mail) != 4)
240: perrx("File `%s' is in wrong format - aborting", filename);
241:
242: if (mailbuf[0] == '-')
243: perrx("Illegal mail name `%s' in `%s'", mailbuf, filename);
244:
1.5 christos 245: mailname = mailbuf;
1.16 christos 246: if (nuid != uid)
247: perrx("Job `%s' - userid %lu does not match file uid %lu",
248: filename, (unsigned long)nuid, (unsigned long)uid);
249:
250: if (ngid != gid)
251: perrx("Job `%s' - groupid %lu does not match file gid %lu",
252: filename, (unsigned long)ngid, (unsigned long)gid);
253:
1.5 christos 254: (void)fclose(stream);
255:
1.19 christos 256: PRIV_START;
1.5 christos 257:
1.16 christos 258: if (chdir(_PATH_ATSPOOL) == -1)
259: perr("Cannot chdir to `%s'", _PATH_ATSPOOL);
1.1 cgd 260:
261: /*
262: * Create a file to hold the output of the job we are about to
263: * run. Write the mail header.
264: */
1.5 christos 265:
1.1 cgd 266: if ((fd_out = open(filename,
1.16 christos 267: O_WRONLY | O_CREAT | O_EXCL, S_IWUSR | S_IRUSR)) == -1)
268: perr("Cannot create output file `%s'", filename);
1.1 cgd 269:
1.19 christos 270: PRIV_END;
1.5 christos 271:
272: write_string(fd_out, "To: ");
273: write_string(fd_out, mailname);
274: write_string(fd_out, "\nSubject: Output from your job ");
1.1 cgd 275: write_string(fd_out, filename);
276: write_string(fd_out, "\n\n");
1.5 christos 277: if (fstat(fd_out, &buf) == -1)
278: perr("Error in fstat of output file descriptor");
1.1 cgd 279: size = buf.st_size;
280:
1.5 christos 281: (void)close(STDIN_FILENO);
282: (void)close(STDOUT_FILENO);
283: (void)close(STDERR_FILENO);
1.1 cgd 284:
285: pid = fork();
286: if (pid < 0)
287: perr("Error in fork");
288: else if (pid == 0) {
289: char *nul = NULL;
290: char **nenvp = &nul;
291:
292: /*
293: * Set up things for the child; we want standard input from
294: * the input file, and standard output and error sent to
295: * our output file.
296: */
1.16 christos 297: if (lseek(fd_in, (off_t) 0, SEEK_SET) == (off_t)-1)
1.1 cgd 298: perr("Error in lseek");
299:
300: if (dup(fd_in) != STDIN_FILENO)
301: perr("Error in I/O redirection");
302:
303: if (dup(fd_out) != STDOUT_FILENO)
304: perr("Error in I/O redirection");
305:
306: if (dup(fd_out) != STDERR_FILENO)
307: perr("Error in I/O redirection");
308:
1.5 christos 309: (void)close(fd_in);
310: (void)close(fd_out);
311:
1.19 christos 312: PRIV_START;
1.5 christos 313:
1.16 christos 314: if (chdir(_PATH_ATJOBS) == -1)
315: perr("Cannot chdir to `%s'", _PATH_ATJOBS);
1.1 cgd 316:
317: queue = *filename;
318:
1.5 christos 319: if (queue > 'b')
320: nice(queue - 'b');
1.1 cgd 321:
1.5 christos 322: become_user(pentry, uid);
1.1 cgd 323:
1.16 christos 324: (void)execle("/bin/sh", "sh", (char *)NULL, nenvp);
325: perr("Exec failed for /bin/sh");
1.5 christos 326: }
327: /* We're the parent. Let's wait. */
328: (void)close(fd_in);
329: (void)close(fd_out);
1.16 christos 330: (void)waitpid(pid, (int *)NULL, 0);
1.1 cgd 331:
1.5 christos 332: /*
333: * Send mail. Unlink the output file first, so it is deleted
334: * after the run.
335: */
1.19 christos 336: PRIV_START;
1.1 cgd 337:
1.5 christos 338: if (stat(filename, &buf) == -1)
1.16 christos 339: perr("Error in stat of output file `%s'", filename);
1.5 christos 340: if (open(filename, O_RDONLY) != STDIN_FILENO)
1.16 christos 341: perr("Open of jobfile `%s' failed", filename);
1.1 cgd 342:
1.5 christos 343: (void)unlink(filename);
1.1 cgd 344:
1.19 christos 345: PRIV_END;
1.1 cgd 346:
347: if ((buf.st_size != size) || send_mail) {
348: /* Fork off a child for sending mail */
1.5 christos 349:
1.19 christos 350: PRIV_START;
1.5 christos 351:
352: become_user(pentry, uid);
353:
354: execl(_PATH_SENDMAIL, "sendmail", "-F", "Atrun Service",
355: "-odi", "-oem", "-t", (char *) NULL);
1.16 christos 356: perr("Exec failed for mail command `%s'", _PATH_SENDMAIL);
1.5 christos 357:
1.19 christos 358: PRIV_END;
1.1 cgd 359: }
360: exit(EXIT_SUCCESS);
361: }
362:
363: /* Global functions */
364:
365: int
1.16 christos 366: main(int argc, char *argv[])
1.1 cgd 367: {
368: /*
1.20 ! mbalmer 369: * Browse through _PATH_ATJOBS, checking all the jobfiles whether
1.1 cgd 370: * they should be executed and or deleted. The queue is coded into
371: * the first byte of the job filename, the date (in minutes since
372: * Eon) as a hex number in the following eight bytes, followed by
373: * a dot and a serial number. A file which has not been executed
374: * yet is denoted by its execute - bit set. For those files which
375: * are to be executed, run_file() is called, which forks off a
376: * child which takes care of I/O redirection, forks off another
377: * child for execution and yet another one, optionally, for sending
378: * mail. Files which already have run are removed during the
379: * next invocation.
380: */
381: DIR *spool;
382: struct dirent *dirent;
383: struct stat buf;
384: unsigned long ctm;
1.5 christos 385: int jobno;
1.1 cgd 386: char queue;
1.5 christos 387: time_t now, run_time;
388: char batch_name[] = "Z2345678901234";
389: uid_t batch_uid;
390: gid_t batch_gid;
391: int c;
392: int run_batch;
393: double la, load_avg = ATRUN_MAXLOAD;
1.15 christos 394: struct group *grp;
395: struct passwd *pwd;
396:
1.16 christos 397: openlog("atrun", LOG_PID, LOG_CRON);
398:
1.15 christos 399: if ((grp = getgrnam(nobody)) == NULL)
1.16 christos 400: perrx("Cannot get gid for `%s'", nobody);
1.15 christos 401:
402: if ((pwd = getpwnam(nobody)) == NULL)
1.16 christos 403: perrx("Cannot get uid for `%s'", nobody);
1.1 cgd 404:
405: /*
406: * We don't need root privileges all the time; running under uid
1.7 simonb 407: * and gid nobody is fine except for privileged operations.
1.1 cgd 408: */
1.19 christos 409: RELINQUISH_PRIVS_ROOT(pwd->pw_uid, grp->gr_gid);
1.1 cgd 410:
1.5 christos 411: opterr = 0;
412: errno = 0;
413: while ((c = getopt(argc, argv, "dl:")) != -1) {
414: switch (c) {
415: case 'l':
416: if (sscanf(optarg, "%lf", &load_avg) != 1)
1.16 christos 417: perrx("Bad argument to option -l: ", optarg);
418: if (load_avg <= 0)
1.5 christos 419: load_avg = ATRUN_MAXLOAD;
420: break;
421:
422: case 'd':
423: debug++;
424: break;
425:
426: case '?':
1.16 christos 427: perrx("Usage: %s [-l <loadav>] [-d]", getprogname());
428: /*NOTREACHED*/
1.5 christos 429:
430: default:
1.16 christos 431: perrx("Invalid option: %c", c);
432: /*NOTREACHED*/
1.5 christos 433: }
434: }
435:
1.19 christos 436: PRIV_START;
1.5 christos 437:
1.16 christos 438: if (chdir(_PATH_ATJOBS) == -1)
439: perr("Cannot change directory to `%s'", _PATH_ATJOBS);
1.1 cgd 440:
441: /*
442: * Main loop. Open spool directory for reading and look over all
443: * the files in there. If the filename indicates that the job
444: * should be run and the x bit is set, fork off a child which sets
445: * its user and group id to that of the files and exec a /bin/sh
446: * which executes the shell script. Unlink older files if they
447: * should no longer be run. For deletion, their r bit has to be
448: * turned on.
1.5 christos 449: *
450: * Also, pick the oldest batch job to run, at most one per
451: * invocation of atrun.
1.1 cgd 452: */
453: if ((spool = opendir(".")) == NULL)
1.16 christos 454: perr("Cannot open `%s'", _PATH_ATJOBS);
1.5 christos 455:
1.19 christos 456: PRIV_END;
1.5 christos 457:
458: now = time(NULL);
459: run_batch = 0;
460: batch_uid = (uid_t) -1;
461: batch_gid = (gid_t) -1;
1.1 cgd 462:
463: while ((dirent = readdir(spool)) != NULL) {
1.19 christos 464: PRIV_START;
1.1 cgd 465:
1.16 christos 466: if (stat(dirent->d_name, &buf) == -1)
467: perr("Cannot stat `%s' in `%s'", dirent->d_name,
468: _PATH_ATJOBS);
1.5 christos 469:
1.19 christos 470: PRIV_END;
1.1 cgd 471:
472: /* We don't want directories */
473: if (!S_ISREG(buf.st_mode))
474: continue;
475:
1.16 christos 476: if (sscanf(dirent->d_name, "%c%5x%8lx", &queue, &jobno,
477: &ctm) != 3)
1.1 cgd 478: continue;
479:
1.5 christos 480: run_time = (time_t) ctm * 60;
1.1 cgd 481:
1.5 christos 482: if ((S_IXUSR & buf.st_mode) && (run_time <= now)) {
1.14 dsl 483: if (isupper((unsigned char)queue) &&
1.5 christos 484: (strcmp(batch_name, dirent->d_name) > 0)) {
485: run_batch = 1;
1.13 itojun 486: (void)strlcpy(batch_name, dirent->d_name,
1.5 christos 487: sizeof(batch_name));
488: batch_uid = buf.st_uid;
489: batch_gid = buf.st_gid;
490: }
491:
492: /* The file is executable and old enough */
1.14 dsl 493: if (islower((unsigned char)queue))
1.16 christos 494: run_file(dirent->d_name, buf.st_uid,
495: buf.st_gid);
1.5 christos 496: }
1.1 cgd 497:
1.5 christos 498: /* Delete older files */
499: if ((run_time < now) && !(S_IXUSR & buf.st_mode) &&
500: (S_IRUSR & buf.st_mode)) {
1.19 christos 501: PRIV_START;
1.1 cgd 502:
1.5 christos 503: (void)unlink(dirent->d_name);
1.1 cgd 504:
1.19 christos 505: PRIV_END;
1.1 cgd 506: }
507: }
1.5 christos 508:
509: /* Run the single batch file, if any */
510: if (run_batch && ((getloadavg(&la, 1) == 1) && la < load_avg))
511: run_file(batch_name, batch_uid, batch_gid);
512:
1.1 cgd 513: closelog();
1.16 christos 514: return EXIT_SUCCESS;
1.1 cgd 515: }
CVSweb <webmaster@jp.NetBSD.org>