atrun.c revision 10401
110154Sache/*
27768Sache *  atrun.c - run jobs queued by at; run with root privileges.
37768Sache *  Copyright (C) 1993, 1994 Thomas Koenig
4939Snate *
5939Snate * Redistribution and use in source and binary forms, with or without
6939Snate * modification, are permitted provided that the following conditions
7939Snate * are met:
8939Snate * 1. Redistributions of source code must retain the above copyright
9939Snate *    notice, this list of conditions and the following disclaimer.
10939Snate * 2. The name of the author(s) may not be used to endorse or promote
11939Snate *    products derived from this software without specific prior written
12939Snate *    permission.
13939Snate *
14939Snate * THIS SOFTWARE IS PROVIDED BY THE AUTHOR(S) ``AS IS'' AND ANY EXPRESS OR
15939Snate * IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES
16939Snate * OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE DISCLAIMED.
1710154Sache * IN NO EVENT SHALL THE AUTHOR(S) BE LIABLE FOR ANY DIRECT, INDIRECT,
18939Snate * INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT
19939Snate * NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE,
20939Snate * DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY
21939Snate * THEORY OF LIABILITY, WETHER IN CONTRACT, STRICT LIABILITY, OR TORT
22939Snate * (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF
23939Snate * THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
24939Snate */
25939Snate
26939Snate/* System Headers */
27939Snate
28939Snate#include <sys/fcntl.h>
29939Snate#include <sys/types.h>
30939Snate#include <sys/stat.h>
31939Snate#include <sys/wait.h>
327768Sache#include <ctype.h>
33939Snate#include <dirent.h>
34939Snate#include <errno.h>
35939Snate#include <pwd.h>
367768Sache#include <grp.h>
37939Snate#include <signal.h>
38939Snate#include <stddef.h>
39939Snate#include <stdio.h>
40939Snate#include <stdlib.h>
41939Snate#include <string.h>
42939Snate#include <time.h>
43939Snate#include <unistd.h>
44939Snate#include <syslog.h>
4510154Sache#ifdef __FreeBSD__
4610154Sache#include <paths.h>
4710154Sache#else
487768Sache#include <getopt.h>
497768Sache#endif
50939Snate
51939Snate/* Local headers */
52939Snate
537768Sache#include "gloadavg.h"
54939Snate#define MAIN
55939Snate#include "privs.h"
56939Snate
577768Sache/* Macros */
587768Sache
597768Sache#ifndef ATJOB_DIR
607768Sache#define ATJOB_DIR "/usr/spool/atjobs/"
617768Sache#endif
627768Sache
637768Sache#ifndef ATSPOOL_DIR
647768Sache#define ATSPOOL_DIR "/usr/spool/atspool/"
657768Sache#endif
667768Sache
677768Sache#ifndef LOADAVG_MX
687768Sache#define LOADAVG_MX 1.5
697768Sache#endif
707768Sache
71939Snate/* File scope variables */
72939Snate
73939Snatestatic char *namep;
7410401Smppstatic char rcsid[] = "$Id: atrun.c,v 1.5 1995/08/21 12:34:17 ache Exp $";
757768Sachestatic debug = 0;
76939Snate
7710154Sachevoid perr(const char *a);
7810154Sache
79939Snate/* Local functions */
80939Snatestatic int
817768Sachewrite_string(int fd, const char* a)
82939Snate{
837768Sache    return write(fd, a, strlen(a));
84939Snate}
85939Snate
8610154Sache#undef DEBUG_FORK
877768Sache#ifdef DEBUG_FORK
887768Sachestatic pid_t
897768Sachemyfork()
907768Sache{
917768Sache	pid_t res;
927768Sache	res = fork();
937768Sache	if (res == 0)
947768Sache	    kill(getpid(),SIGSTOP);
957768Sache	return res;
967768Sache}
977768Sache
987768Sache#define fork myfork
997768Sache#endif
1007768Sache
101939Snatestatic void
1027768Sacherun_file(const char *filename, uid_t uid, gid_t gid)
103939Snate{
1047768Sache/* Run a file by by spawning off a process which redirects I/O,
1057768Sache * spawns a subshell, then waits for it to complete and sends
1067768Sache * mail to the user.
1077768Sache */
1087768Sache    pid_t pid;
1097768Sache    int fd_out, fd_in;
1107768Sache    int queue;
1117768Sache    char mailbuf[9];
1127768Sache    char *mailname = NULL;
1137768Sache    FILE *stream;
1147768Sache    int send_mail = 0;
11510154Sache    struct stat buf, lbuf;
1167768Sache    off_t size;
1177768Sache    struct passwd *pentry;
1187768Sache    int fflags;
11910154Sache    long nuid;
12010154Sache    long ngid;
121939Snate
122939Snate
1237768Sache    PRIV_START
124939Snate
1257768Sache    if (chmod(filename, S_IRUSR) != 0)
1267768Sache    {
1277768Sache	perr("Cannot change file permissions");
1287768Sache    }
129939Snate
1307768Sache    PRIV_END
131939Snate
1327768Sache    pid = fork();
1337768Sache    if (pid == -1)
1347768Sache	perr("Cannot fork");
13510154Sache
13610154Sache    else if (pid != 0)
1377768Sache	return;
138939Snate
1397768Sache    /* Let's see who we mail to.  Hopefully, we can read it from
1407768Sache     * the command file; if not, send it to the owner, or, failing that,
1417768Sache     * to root.
1427768Sache     */
143939Snate
1447768Sache    pentry = getpwuid(uid);
1457768Sache    if (pentry == NULL)
1467768Sache    {
1477768Sache	syslog(LOG_ERR,"Userid %lu not found - aborting job %s",
1487768Sache	       (unsigned long) uid, filename);
14910154Sache        exit(EXIT_FAILURE);
1507768Sache    }
1517768Sache    PRIV_START
152939Snate
1537768Sache    stream=fopen(filename, "r");
154939Snate
1557768Sache    PRIV_END
156939Snate
15710401Smpp#ifdef __FreeBSD__
15810401Smpp    if (pentry->pw_expire && time(NULL) >= pentry->pw_expire)
15910401Smpp    {
16010401Smpp	syslog(LOG_ERR, "Userid %lu is expired - aborting job %s",
16110401Smpp		(unsigned long) uid, filename);
16210401Smpp	exit(EXIT_FAILURE);
16310401Smpp    }
16410401Smpp#endif
16510401Smpp
1667768Sache    if (stream == NULL)
1677768Sache	perr("Cannot open input file");
1687768Sache
1697768Sache    if ((fd_in = dup(fileno(stream))) <0)
1707768Sache	perr("Error duplicating input file descriptor");
1717768Sache
17210154Sache    if (fstat(fd_in, &buf) == -1)
17310154Sache	perr("Error in fstat of input file descriptor");
17410154Sache
17510154Sache    if (lstat(filename, &lbuf) == -1)
17610154Sache	perr("Error in fstat of input file");
17710154Sache
17810154Sache    if (S_ISLNK(lbuf.st_mode)) {
17910154Sache	syslog(LOG_ERR,"Symbolic link encountered in job %s - aborting",
18010154Sache		filename);
18110154Sache	exit(EXIT_FAILURE);
18210154Sache    }
18310154Sache    if ((lbuf.st_dev != buf.st_dev) || (lbuf.st_ino != buf.st_ino) ||
18410154Sache        (lbuf.st_uid != buf.st_uid) || (lbuf.st_gid != buf.st_gid) ||
18510154Sache        (lbuf.st_size!=buf.st_size)) {
18610154Sache	syslog(LOG_ERR,"Somebody changed files from under us for job %s - "
18710154Sache	"aborting",filename);
18810154Sache	exit(EXIT_FAILURE);
18910154Sache    }
19010154Sache    if (buf.st_nlink > 1) {
19110154Sache	syslog(LOG_ERR,"Someboy is trying to run a linked script for job %s",
19210154Sache		filename);
19310154Sache	exit(EXIT_FAILURE);
19410154Sache    }
1957768Sache    if ((fflags = fcntl(fd_in, F_GETFD)) <0)
1967768Sache	perr("Error in fcntl");
1977768Sache
1987768Sache    fcntl(fd_in, F_SETFD, fflags & ~FD_CLOEXEC);
1997768Sache
20010154Sache    if (fscanf(stream, "#!/bin/sh\n# atrun uid=%ld gid=%ld\n# mail %8s %d",
20110154Sache         &nuid, &ngid, mailbuf, &send_mail) != 4)
2027768Sache    {
20310154Sache	syslog(LOG_ERR,"File %s is in wrong format - aborting",
20410154Sache		filename);
20510154Sache	exit(EXIT_FAILURE);
2067768Sache    }
20710154Sache    if (mailbuf[0] == '-') {
20810154Sache	syslog(LOG_ERR,"illegal mail name %s in %s",mailbuf,filename);
20910154Sache	exit(EXIT_FAILURE);
2107768Sache    }
21110154Sache    mailname = mailbuf;
21210154Sache    if (nuid != uid) {
21310154Sache	syslog(LOG_ERR,"Job %s - userid %d does not match file uid %d",
21410154Sache		filename, nuid, uid);
21510154Sache	exit(EXIT_FAILURE);
21610154Sache    }
21710154Sache    if (ngid != gid) {
21810154Sache	syslog(LOG_ERR,"Job %s - groupid %d does not match file gid %d",
21910154Sache		filename, ngid, gid);
22010154Sache	exit(EXIT_FAILURE);
22110154Sache    }
2227768Sache    fclose(stream);
2237768Sache    if (chdir(ATSPOOL_DIR) < 0)
2247768Sache	perr("Cannot chdir to " ATSPOOL_DIR);
22510154Sache
2267768Sache    /* Create a file to hold the output of the job we are about to run.
2277768Sache     * Write the mail header.
22810154Sache     */
2297768Sache    if((fd_out=open(filename,
2307768Sache		O_WRONLY | O_CREAT | O_EXCL, S_IWUSR | S_IRUSR)) < 0)
2317768Sache	perr("Cannot create output file");
232939Snate
2337768Sache    write_string(fd_out, "Subject: Output from your job ");
2347768Sache    write_string(fd_out, filename);
2357768Sache    write_string(fd_out, "\n\n");
2367768Sache    fstat(fd_out, &buf);
2377768Sache    size = buf.st_size;
238939Snate
2397768Sache    close(STDIN_FILENO);
2407768Sache    close(STDOUT_FILENO);
2417768Sache    close(STDERR_FILENO);
24210154Sache
2437768Sache    pid = fork();
2447768Sache    if (pid < 0)
2457768Sache	perr("Error in fork");
246939Snate
2477768Sache    else if (pid == 0)
2487768Sache    {
2497768Sache	char *nul = NULL;
2507768Sache	char **nenvp = &nul;
251939Snate
2527768Sache	/* Set up things for the child; we want standard input from the input file,
2537768Sache	 * and standard output and error sent to our output file.
2547768Sache	 */
255939Snate
2567768Sache	if (lseek(fd_in, (off_t) 0, SEEK_SET) < 0)
2577768Sache	    perr("Error in lseek");
258939Snate
2597768Sache	if (dup(fd_in) != STDIN_FILENO)
2607768Sache	    perr("Error in I/O redirection");
261939Snate
2627768Sache	if (dup(fd_out) != STDOUT_FILENO)
2637768Sache	    perr("Error in I/O redirection");
264939Snate
2657768Sache	if (dup(fd_out) != STDERR_FILENO)
2667768Sache	    perr("Error in I/O redirection");
267939Snate
2687768Sache	close(fd_in);
2697768Sache	close(fd_out);
2707768Sache	if (chdir(ATJOB_DIR) < 0)
2717768Sache	    perr("Cannot chdir to " ATJOB_DIR);
272939Snate
2737768Sache	queue = *filename;
274939Snate
2757768Sache	PRIV_START
276939Snate
2777768Sache        nice(tolower(queue) - 'a');
27810154Sache
27910154Sache	if (chdir(pentry->pw_dir))
28010154Sache		chdir("/");
2818870Srgrimes
2827768Sache	if (initgroups(pentry->pw_name,pentry->pw_gid))
2837768Sache	    perr("Cannot delete saved userids");
284939Snate
2857768Sache	if (setgid(gid) < 0)
2867768Sache	    perr("Cannot change group");
287939Snate
2887768Sache	if (setuid(uid) < 0)
2897768Sache	    perr("Cannot set user id");
290939Snate
2917768Sache	if(execle("/bin/sh","sh",(char *) NULL, nenvp) != 0)
29210154Sache	    perr("Exec failed for /bin/sh");
293939Snate
2947768Sache	PRIV_END
2957768Sache    }
2967768Sache    /* We're the parent.  Let's wait.
2977768Sache     */
2987768Sache    close(fd_in);
2997768Sache    close(fd_out);
3007768Sache    waitpid(pid, (int *) NULL, 0);
301939Snate
3027768Sache    /* Send mail.  Unlink the output file first, so it is deleted after
3037768Sache     * the run.
3047768Sache     */
3057768Sache    stat(filename, &buf);
3067768Sache    if (open(filename, O_RDONLY) != STDIN_FILENO)
3077768Sache        perr("Open of jobfile failed");
308939Snate
3097768Sache    unlink(filename);
3107768Sache    if ((buf.st_size != size) || send_mail)
31110154Sache    {
31210154Sache	PRIV_START
31310154Sache
31410154Sache	if (chdir(pentry->pw_dir))
31510154Sache		chdir("/");
31610154Sache
31710154Sache	if (initgroups(pentry->pw_name,pentry->pw_gid))
31810154Sache	    perr("Cannot delete saved userids");
31910154Sache
32010154Sache	if (setgid(gid) < 0)
32110154Sache	    perr("Cannot change group");
32210154Sache
32310154Sache	if (setuid(uid) < 0)
32410154Sache	    perr("Cannot set user id");
32510154Sache
3267768Sache#ifdef __FreeBSD__
3277768Sache	execl(_PATH_SENDMAIL, "sendmail", "-F", "Atrun Service",
3287777Sache			"-odi", "-oem",
3297768Sache			mailname, (char *) NULL);
3307768Sache#else
3317768Sache        execl(MAIL_CMD, MAIL_CMD, mailname, (char *) NULL);
3327768Sache#endif
33310154Sache	    perr("Exec failed for mail command");
33410154Sache
33510154Sache	PRIV_END
3367768Sache    }
3377768Sache    exit(EXIT_SUCCESS);
338939Snate}
339939Snate
340939Snate/* Global functions */
341939Snate
34210154Sache/* Needed in gloadavg.c */
34310154Sachevoid
34410154Sacheperr(const char *a)
34510154Sache{
34610154Sache    if (debug)
34710154Sache    {
34810154Sache	perror(a);
34910154Sache    }
35010154Sache    else
35110154Sache	syslog(LOG_ERR, "%s: %m", a);
35210154Sache
35310154Sache    exit(EXIT_FAILURE);
35410154Sache}
35510154Sache
356939Snateint
3577768Sachemain(int argc, char *argv[])
358939Snate{
3597768Sache/* Browse through  ATJOB_DIR, checking all the jobfiles wether they should
3607768Sache * be executed and or deleted. The queue is coded into the first byte of
3617768Sache * the job filename, the date (in minutes since Eon) as a hex number in the
3627768Sache * following eight bytes, followed by a dot and a serial number.  A file
3637768Sache * which has not been executed yet is denoted by its execute - bit set.
3647768Sache * For those files which are to be executed, run_file() is called, which forks
3657768Sache * off a child which takes care of I/O redirection, forks off another child
3667768Sache * for execution and yet another one, optionally, for sending mail.
3677768Sache * Files which already have run are removed during the next invocation.
3687768Sache */
3697768Sache    DIR *spool;
3707768Sache    struct dirent *dirent;
3717768Sache    struct stat buf;
3727768Sache    unsigned long ctm;
37310154Sache    unsigned long jobno;
3747768Sache    char queue;
3757768Sache    time_t now, run_time;
3767768Sache    char batch_name[] = "Z2345678901234";
3777768Sache    uid_t batch_uid;
3787768Sache    gid_t batch_gid;
3797768Sache    int c;
3807768Sache    int run_batch;
3817768Sache    double load_avg = LOADAVG_MX;
382939Snate
3837768Sache/* We don't need root privileges all the time; running under uid and gid daemon
3847768Sache * is fine.
3857768Sache */
386939Snate
3877768Sache    RELINQUISH_PRIVS_ROOT(DAEMON_UID, DAEMON_GID)
388939Snate
3897768Sache    openlog("atrun", LOG_PID, LOG_CRON);
390939Snate
3917768Sache    opterr = 0;
3927768Sache    errno = 0;
3937768Sache    while((c=getopt(argc, argv, "dl:"))!= EOF)
3947768Sache    {
3957768Sache	switch (c)
3967768Sache	{
39710154Sache	case 'l':
3987768Sache	    if (sscanf(optarg, "%lf", &load_avg) != 1)
3997768Sache		perr("garbled option -l");
4007768Sache	    if (load_avg <= 0.)
4017768Sache		load_avg = LOADAVG_MX;
4027768Sache	    break;
403939Snate
4047768Sache	case 'd':
4057768Sache	    debug ++;
4067768Sache	    break;
407939Snate
4087768Sache	case '?':
4097768Sache	    perr("unknown option");
4107768Sache	    break;
411939Snate
4127768Sache	default:
4137768Sache	    perr("idiotic option - aborted");
4147768Sache	    break;
4157768Sache	}
4167768Sache    }
417939Snate
4187768Sache    namep = argv[0];
4197768Sache    if (chdir(ATJOB_DIR) != 0)
4207768Sache	perr("Cannot change to " ATJOB_DIR);
421939Snate
4227768Sache    /* Main loop. Open spool directory for reading and look over all the
4237768Sache     * files in there. If the filename indicates that the job should be run
4247768Sache     * and the x bit is set, fork off a child which sets its user and group
4257768Sache     * id to that of the files and exec a /bin/sh which executes the shell
4267768Sache     * script. Unlink older files if they should no longer be run.  For
4277768Sache     * deletion, their r bit has to be turned on.
4287768Sache     *
4297768Sache     * Also, pick the oldest batch job to run, at most one per invocation of
4307768Sache     * atrun.
4317768Sache     */
4327768Sache    if ((spool = opendir(".")) == NULL)
4337768Sache	perr("Cannot read " ATJOB_DIR);
434939Snate
4357768Sache    now = time(NULL);
4367768Sache    run_batch = 0;
4377768Sache    batch_uid = (uid_t) -1;
4387768Sache    batch_gid = (gid_t) -1;
439939Snate
44010154Sache    while ((dirent = readdir(spool)) != NULL) {
4417768Sache	if (stat(dirent->d_name,&buf) != 0)
4427768Sache	    perr("Cannot stat in " ATJOB_DIR);
443939Snate
4447768Sache	/* We don't want directories
4457768Sache	 */
44610154Sache	if (!S_ISREG(buf.st_mode))
4477768Sache	    continue;
448939Snate
44910154Sache	if (sscanf(dirent->d_name,"%c%5lx%8lx",&queue,&jobno,&ctm) != 3)
4507768Sache	    continue;
451939Snate
4527768Sache	run_time = (time_t) ctm*60;
453939Snate
45410154Sache	if ((S_IXUSR & buf.st_mode) && (run_time <=now)) {
45510154Sache	    if (isupper(queue) && (strcmp(batch_name,dirent->d_name) > 0)) {
4567768Sache		run_batch = 1;
4577768Sache		strncpy(batch_name, dirent->d_name, sizeof(batch_name));
4587768Sache		batch_uid = buf.st_uid;
4597768Sache		batch_gid = buf.st_gid;
4607768Sache	    }
46110154Sache
4627768Sache	/* The file is executable and old enough
4637768Sache	 */
4647768Sache	    if (islower(queue))
4657768Sache		run_file(dirent->d_name, buf.st_uid, buf.st_gid);
466939Snate	}
4677768Sache	/*  Delete older files
4687768Sache	 */
4697768Sache	if ((run_time < now) && !(S_IXUSR & buf.st_mode) && (S_IRUSR & buf.st_mode))
4707768Sache	    unlink(dirent->d_name);
4717768Sache    }
4727768Sache    /* run the single batch file, if any
4737768Sache    */
47410154Sache    if (run_batch && (gloadavg() < load_avg))
47510154Sache	run_file(batch_name, batch_uid, batch_gid);
4767768Sache
4777768Sache    closelog();
4787768Sache    exit(EXIT_SUCCESS);
479939Snate}
480