atrun.c revision 7777
129088Smarkm/*
229088Smarkm *  atrun.c - run jobs queued by at; run with root privileges.
329088Smarkm *  Copyright (C) 1993, 1994 Thomas Koenig
429088Smarkm *
529088Smarkm * Redistribution and use in source and binary forms, with or without
629088Smarkm * modification, are permitted provided that the following conditions
729088Smarkm * are met:
829088Smarkm * 1. Redistributions of source code must retain the above copyright
929088Smarkm *    notice, this list of conditions and the following disclaimer.
1029088Smarkm * 2. The name of the author(s) may not be used to endorse or promote
1129088Smarkm *    products derived from this software without specific prior written
1229088Smarkm *    permission.
1329088Smarkm *
1429088Smarkm * THIS SOFTWARE IS PROVIDED BY THE AUTHOR(S) ``AS IS'' AND ANY EXPRESS OR
1529088Smarkm * IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES
1629088Smarkm * OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE DISCLAIMED.
1729088Smarkm * IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR ANY DIRECT, INDIRECT,
1829088Smarkm * INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT
1929088Smarkm * NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE,
2029088Smarkm * DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY
2129088Smarkm * THEORY OF LIABILITY, WETHER IN CONTRACT, STRICT LIABILITY, OR TORT
2229088Smarkm * (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF
2329088Smarkm * THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
2429088Smarkm */
2529088Smarkm
2629088Smarkm/* System Headers */
2729088Smarkm
2829088Smarkm#include <sys/fcntl.h>
2929088Smarkm#include <sys/types.h>
3029088Smarkm#include <sys/stat.h>
3129088Smarkm#include <sys/wait.h>
3229088Smarkm#include <ctype.h>
3329088Smarkm#include <dirent.h>
34114630Sobrien#include <errno.h>
3529088Smarkm#include <pwd.h>
3629181Smarkm#include <grp.h>
3731622Scharnier#include <signal.h>
38114630Sobrien#include <stddef.h>
39114630Sobrien#include <stdio.h>
40114630Sobrien#include <stdlib.h>
4129088Smarkm#include <string.h>
4279981Sru#include <time.h>
4329088Smarkm#include <unistd.h>
4487139Smarkm#include <syslog.h>
4529088Smarkm#ifndef __FreeBSD__
4629088Smarkm#include <getopt.h>
4787139Smarkm#else
4829181Smarkm#include <paths.h>
4929181Smarkm#endif
5029088Smarkm
5129088Smarkm/* Local headers */
5229088Smarkm
5329088Smarkm#ifndef __FreeBSD__
5429088Smarkm#include "gloadavg.h"
5529088Smarkm#endif
5629088Smarkm#define MAIN
5729088Smarkm#include "privs.h"
5829088Smarkm
5929088Smarkm/* Macros */
6029088Smarkm
6129088Smarkm#ifndef ATJOB_DIR
6229088Smarkm#define ATJOB_DIR "/usr/spool/atjobs/"
6329088Smarkm#endif
6429088Smarkm
6529088Smarkm#ifndef ATSPOOL_DIR
6629088Smarkm#define ATSPOOL_DIR "/usr/spool/atspool/"
6729088Smarkm#endif
6829088Smarkm
6929088Smarkm#ifndef LOADAVG_MX
7029088Smarkm#define LOADAVG_MX 1.5
7129088Smarkm#endif
7229088Smarkm
7329088Smarkm/* File scope variables */
7429088Smarkm
7529088Smarkmstatic char *namep;
7629088Smarkmstatic char rcsid[] = "$Id: atrun.c,v 1.2 1995/04/12 02:52:15 ache Exp $";
7729088Smarkmstatic debug = 0;
7829088Smarkm
7929088Smarkm/* Local functions */
8029088Smarkmstatic void
8129088Smarkmperr(const char *a)
8229088Smarkm{
8329088Smarkm    if (debug)
8429088Smarkm    {
8529088Smarkm	perror(a);
8629088Smarkm    }
8729088Smarkm    else
8829088Smarkm	syslog(LOG_ERR, "%s: %m", a);
8929088Smarkm
9029088Smarkm    exit(EXIT_FAILURE);
9129088Smarkm}
9287139Smarkm
9387139Smarkmstatic int
9487139Smarkmwrite_string(int fd, const char* a)
9587139Smarkm{
9629088Smarkm    return write(fd, a, strlen(a));
9787139Smarkm}
9829088Smarkm
9929088Smarkm#ifdef DEBUG_FORK
10029088Smarkmstatic pid_t
10129088Smarkmmyfork()
10229088Smarkm{
10329088Smarkm	pid_t res;
10429088Smarkm	res = fork();
10529088Smarkm	if (res == 0)
10629088Smarkm	    kill(getpid(),SIGSTOP);
10729088Smarkm	return res;
10829088Smarkm}
10929088Smarkm
11029088Smarkm#define fork myfork
11129088Smarkm#endif
11229088Smarkm
11329088Smarkmstatic void
11429088Smarkmrun_file(const char *filename, uid_t uid, gid_t gid)
11529088Smarkm{
116103956Smarkm/* Run a file by by spawning off a process which redirects I/O,
11729088Smarkm * spawns a subshell, then waits for it to complete and sends
11829088Smarkm * mail to the user.
11929088Smarkm */
12029088Smarkm    pid_t pid;
12129088Smarkm    int fd_out, fd_in;
12229088Smarkm    int queue;
12329088Smarkm    char mailbuf[9];
12429088Smarkm    char *mailname = NULL;
12529088Smarkm    FILE *stream;
12629088Smarkm    int send_mail = 0;
12729088Smarkm    struct stat buf;
12829088Smarkm    off_t size;
12929088Smarkm    struct passwd *pentry;
13029088Smarkm    int fflags;
13129088Smarkm
13229088Smarkm
13329088Smarkm    PRIV_START
13429088Smarkm
13529088Smarkm    if (chmod(filename, S_IRUSR) != 0)
13629088Smarkm    {
13729088Smarkm	perr("Cannot change file permissions");
13829088Smarkm    }
13929088Smarkm
14029088Smarkm    PRIV_END
14129088Smarkm
14229088Smarkm    pid = fork();
14329088Smarkm    if (pid == -1)
14429088Smarkm	perr("Cannot fork");
14529088Smarkm
14629088Smarkm    else if (pid > 0)
14729088Smarkm	return;
14829088Smarkm
14929088Smarkm    /* Let's see who we mail to.  Hopefully, we can read it from
15029088Smarkm     * the command file; if not, send it to the owner, or, failing that,
15129088Smarkm     * to root.
15229088Smarkm     */
15329088Smarkm
15429088Smarkm    pentry = getpwuid(uid);
15529088Smarkm    if (pentry == NULL)
15629088Smarkm    {
15729088Smarkm	syslog(LOG_ERR,"Userid %lu not found - aborting job %s",
15829088Smarkm	       (unsigned long) uid, filename);
15929088Smarkm	exit(EXIT_FAILURE);
16029088Smarkm    }
16129088Smarkm    PRIV_START
16229088Smarkm
16329088Smarkm    stream=fopen(filename, "r");
16429088Smarkm
16529088Smarkm    PRIV_END
16629088Smarkm
16729088Smarkm    if (stream == NULL)
16829088Smarkm	perr("Cannot open input file");
16929088Smarkm
17029088Smarkm    if ((fd_in = dup(fileno(stream))) <0)
17129088Smarkm	perr("Error duplicating input file descriptor");
17229088Smarkm
17329088Smarkm    if ((fflags = fcntl(fd_in, F_GETFD)) <0)
17429088Smarkm	perr("Error in fcntl");
17529088Smarkm
17629088Smarkm    fcntl(fd_in, F_SETFD, fflags & ~FD_CLOEXEC);
17729088Smarkm
17829088Smarkm    if (fscanf(stream, "#! /bin/sh\n# mail %8s %d", mailbuf, &send_mail) == 2)
17929088Smarkm    {
18029088Smarkm	mailname = mailbuf;
18129088Smarkm	pentry = getpwnam(mailname);
18229088Smarkm	if (pentry == NULL || pentry->pw_uid != uid) {
18329088Smarkm		syslog(LOG_ERR,"Userid %lu mismatch name %s - aborting job %s",
18429088Smarkm		       (unsigned long) uid, mailname, filename);
18529088Smarkm		exit(EXIT_FAILURE);
18629088Smarkm	}
18729088Smarkm    }
18829088Smarkm    else
18929088Smarkm    {
19029088Smarkm	mailname = pentry->pw_name;
19129088Smarkm    }
19229088Smarkm    fclose(stream);
19329088Smarkm    if (chdir(ATSPOOL_DIR) < 0)
19429088Smarkm	perr("Cannot chdir to " ATSPOOL_DIR);
19529088Smarkm
19629088Smarkm    /* Create a file to hold the output of the job we are about to run.
19729088Smarkm     * Write the mail header.
19829088Smarkm     */
19929088Smarkm    if((fd_out=open(filename,
20029088Smarkm		O_WRONLY | O_CREAT | O_EXCL, S_IWUSR | S_IRUSR)) < 0)
20129088Smarkm	perr("Cannot create output file");
20229088Smarkm
20329088Smarkm    write_string(fd_out, "Subject: Output from your job ");
20429088Smarkm    write_string(fd_out, filename);
20529088Smarkm    write_string(fd_out, "\n\n");
20629088Smarkm    fstat(fd_out, &buf);
20729088Smarkm    size = buf.st_size;
20879981Sru
20929088Smarkm    close(STDIN_FILENO);
21029088Smarkm    close(STDOUT_FILENO);
21129088Smarkm    close(STDERR_FILENO);
21229088Smarkm
21329088Smarkm    pid = fork();
21429088Smarkm    if (pid < 0)
21529088Smarkm	perr("Error in fork");
21629088Smarkm
21729088Smarkm    else if (pid == 0)
21829088Smarkm    {
21929088Smarkm	char *nul = NULL;
22029088Smarkm	char **nenvp = &nul;
22129088Smarkm
22229088Smarkm	/* Set up things for the child; we want standard input from the input file,
22329088Smarkm	 * and standard output and error sent to our output file.
22429088Smarkm	 */
22529088Smarkm
22629088Smarkm	if (lseek(fd_in, (off_t) 0, SEEK_SET) < 0)
22729088Smarkm	    perr("Error in lseek");
22829088Smarkm
22929088Smarkm	if (dup(fd_in) != STDIN_FILENO)
23029088Smarkm	    perr("Error in I/O redirection");
23129088Smarkm
23229088Smarkm	if (dup(fd_out) != STDOUT_FILENO)
23329088Smarkm	    perr("Error in I/O redirection");
23429088Smarkm
23529088Smarkm	if (dup(fd_out) != STDERR_FILENO)
23629088Smarkm	    perr("Error in I/O redirection");
23729088Smarkm
23829088Smarkm	close(fd_in);
23929088Smarkm	close(fd_out);
24029088Smarkm	if (chdir(ATJOB_DIR) < 0)
24129088Smarkm	    perr("Cannot chdir to " ATJOB_DIR);
24229088Smarkm
24329088Smarkm	queue = *filename;
24429088Smarkm
24529088Smarkm	PRIV_START
24629088Smarkm
24729088Smarkm        nice(tolower(queue) - 'a');
24829088Smarkm
24929088Smarkm	chdir(pentry->pw_dir);
25029088Smarkm
25129088Smarkm	if (initgroups(pentry->pw_name,pentry->pw_gid))
25229088Smarkm	    perr("Cannot delete saved userids");
25329088Smarkm
25429088Smarkm	if (setgid(gid) < 0)
25529088Smarkm	    perr("Cannot change group");
25629088Smarkm
25729088Smarkm	if (setuid(uid) < 0)
25829088Smarkm	    perr("Cannot set user id");
25929088Smarkm
26029088Smarkm	if(execle("/bin/sh","sh",(char *) NULL, nenvp) != 0)
26129088Smarkm	    perr("Exec failed");
26229088Smarkm
26329088Smarkm	PRIV_END
26429088Smarkm    }
26529088Smarkm    /* We're the parent.  Let's wait.
26629088Smarkm     */
26729088Smarkm    close(fd_in);
26829088Smarkm    close(fd_out);
26929088Smarkm    waitpid(pid, (int *) NULL, 0);
27029088Smarkm
27129088Smarkm    /* Send mail.  Unlink the output file first, so it is deleted after
27229088Smarkm     * the run.
27329088Smarkm     */
27429088Smarkm    stat(filename, &buf);
27529088Smarkm    if (open(filename, O_RDONLY) != STDIN_FILENO)
27629088Smarkm        perr("Open of jobfile failed");
27729088Smarkm
27829088Smarkm    unlink(filename);
27929088Smarkm    if ((buf.st_size != size) || send_mail)
28029088Smarkm    {
28129088Smarkm#ifdef __FreeBSD__
28229088Smarkm	execl(_PATH_SENDMAIL, "sendmail", "-F", "Atrun Service",
28329088Smarkm			"-odi", "-oem",
28429088Smarkm			mailname, (char *) NULL);
28529088Smarkm#else
28629088Smarkm        execl(MAIL_CMD, MAIL_CMD, mailname, (char *) NULL);
28729088Smarkm#endif
28829088Smarkm	perr("Exec failed");
28929088Smarkm    }
29029088Smarkm    exit(EXIT_SUCCESS);
29129088Smarkm}
29229088Smarkm
29329088Smarkm/* Global functions */
29429088Smarkm
29529088Smarkmint
29629088Smarkmmain(int argc, char *argv[])
29729088Smarkm{
29829088Smarkm/* Browse through  ATJOB_DIR, checking all the jobfiles wether they should
29929088Smarkm * be executed and or deleted. The queue is coded into the first byte of
30029088Smarkm * the job filename, the date (in minutes since Eon) as a hex number in the
30129088Smarkm * following eight bytes, followed by a dot and a serial number.  A file
30229088Smarkm * which has not been executed yet is denoted by its execute - bit set.
30329088Smarkm * For those files which are to be executed, run_file() is called, which forks
30429088Smarkm * off a child which takes care of I/O redirection, forks off another child
30529088Smarkm * for execution and yet another one, optionally, for sending mail.
30629088Smarkm * Files which already have run are removed during the next invocation.
30729088Smarkm */
30829088Smarkm    DIR *spool;
30929088Smarkm    struct dirent *dirent;
31029088Smarkm    struct stat buf;
31129088Smarkm    unsigned long ctm;
31229088Smarkm    char queue;
31329088Smarkm    time_t now, run_time;
31429088Smarkm    char batch_name[] = "Z2345678901234";
31529088Smarkm    uid_t batch_uid;
31629088Smarkm    gid_t batch_gid;
31729088Smarkm    int c;
31829088Smarkm    int run_batch;
31929088Smarkm    double load_avg = LOADAVG_MX;
32029088Smarkm
32129088Smarkm/* We don't need root privileges all the time; running under uid and gid daemon
32229088Smarkm * is fine.
32329088Smarkm */
32429088Smarkm
32529088Smarkm    RELINQUISH_PRIVS_ROOT(DAEMON_UID, DAEMON_GID)
32629088Smarkm
32729088Smarkm    openlog("atrun", LOG_PID, LOG_CRON);
32829088Smarkm
32929088Smarkm    opterr = 0;
33029088Smarkm    errno = 0;
33129088Smarkm    while((c=getopt(argc, argv, "dl:"))!= EOF)
33229088Smarkm    {
33329088Smarkm	switch (c)
33429088Smarkm	{
33529088Smarkm	case 'l':
33629088Smarkm	    if (sscanf(optarg, "%lf", &load_avg) != 1)
33729088Smarkm		perr("garbled option -l");
33829088Smarkm	    if (load_avg <= 0.)
33929088Smarkm		load_avg = LOADAVG_MX;
34029088Smarkm	    break;
34129088Smarkm
34229088Smarkm	case 'd':
34329088Smarkm	    debug ++;
34429088Smarkm	    break;
34529088Smarkm
34629088Smarkm	case '?':
34729088Smarkm	    perr("unknown option");
34829088Smarkm	    break;
34929088Smarkm
35029088Smarkm	default:
35129088Smarkm	    perr("idiotic option - aborted");
35229088Smarkm	    break;
35329088Smarkm	}
35429088Smarkm    }
35529088Smarkm
35629088Smarkm    namep = argv[0];
35729088Smarkm    if (chdir(ATJOB_DIR) != 0)
35829088Smarkm	perr("Cannot change to " ATJOB_DIR);
35929088Smarkm
36029088Smarkm    /* Main loop. Open spool directory for reading and look over all the
36129088Smarkm     * files in there. If the filename indicates that the job should be run
36229088Smarkm     * and the x bit is set, fork off a child which sets its user and group
36329088Smarkm     * id to that of the files and exec a /bin/sh which executes the shell
36431622Scharnier     * script. Unlink older files if they should no longer be run.  For
36529088Smarkm     * deletion, their r bit has to be turned on.
36629088Smarkm     *
36729088Smarkm     * Also, pick the oldest batch job to run, at most one per invocation of
36829088Smarkm     * atrun.
36929088Smarkm     */
37029088Smarkm    if ((spool = opendir(".")) == NULL)
37129088Smarkm	perr("Cannot read " ATJOB_DIR);
37229088Smarkm
37329088Smarkm    now = time(NULL);
37429088Smarkm    run_batch = 0;
37529088Smarkm    batch_uid = (uid_t) -1;
37629088Smarkm    batch_gid = (gid_t) -1;
37729088Smarkm
37829088Smarkm    while ((dirent = readdir(spool)) != NULL)
37929088Smarkm    {
38029088Smarkm	if (stat(dirent->d_name,&buf) != 0)
38129088Smarkm	    perr("Cannot stat in " ATJOB_DIR);
38229088Smarkm
38329088Smarkm	/* We don't want directories
38429088Smarkm	 */
38529088Smarkm	if (!S_ISREG(buf.st_mode))
38629088Smarkm	    continue;
38729088Smarkm
38829088Smarkm	if (sscanf(dirent->d_name,"%c%8lx",&queue,&ctm) != 2)
38929088Smarkm	    continue;
39029088Smarkm
39129088Smarkm	run_time = (time_t) ctm*60;
39229088Smarkm
39329088Smarkm	if ((S_IXUSR & buf.st_mode) && (run_time <=now))
39429088Smarkm	{
39529088Smarkm	    if (isupper(queue) && (strcmp(batch_name,dirent->d_name) > 0))
39629088Smarkm	    {
39729088Smarkm		run_batch = 1;
39829088Smarkm		strncpy(batch_name, dirent->d_name, sizeof(batch_name));
39929088Smarkm		batch_uid = buf.st_uid;
40029088Smarkm		batch_gid = buf.st_gid;
40129088Smarkm	    }
40229088Smarkm
40329088Smarkm	/* The file is executable and old enough
40429088Smarkm	 */
40529088Smarkm	    if (islower(queue))
40629088Smarkm		run_file(dirent->d_name, buf.st_uid, buf.st_gid);
40729088Smarkm	}
40829088Smarkm	/*  Delete older files
40929088Smarkm	 */
41029088Smarkm	if ((run_time < now) && !(S_IXUSR & buf.st_mode) && (S_IRUSR & buf.st_mode))
41129088Smarkm	    unlink(dirent->d_name);
41229088Smarkm    }
41329088Smarkm    /* run the single batch file, if any
41429088Smarkm    */
41529088Smarkm#ifndef __FreeBSD__
41629088Smarkm    if (run_batch && (gloadavg() < load_avg)) {
41729088Smarkm#else
41829088Smarkm    if (run_batch) {
41929088Smarkm	double la;
42029088Smarkm
42129088Smarkm	if (getloadavg(&la, 1) != 1)
42229088Smarkm	    perr("Error in getloadavg");
42329088Smarkm	if (la < load_avg)
42429088Smarkm#endif
42529088Smarkm	    run_file(batch_name, batch_uid, batch_gid);
42629088Smarkm    }
42787139Smarkm
42887139Smarkm    closelog();
42929088Smarkm    exit(EXIT_SUCCESS);
43029088Smarkm}
43129088Smarkm