atrun.c revision 90148
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
2631306Scharnier#ifndef lint
2731306Scharnierstatic const char rcsid[] =
2850476Speter  "$FreeBSD: head/libexec/atrun/atrun.c 90148 2002-02-03 15:53:02Z imp $";
2931306Scharnier#endif /* not lint */
3031306Scharnier
31939Snate/* System Headers */
32939Snate
33939Snate#include <sys/fcntl.h>
34939Snate#include <sys/types.h>
35939Snate#include <sys/stat.h>
36939Snate#include <sys/wait.h>
3724829Sdavidn#include <sys/param.h>
387768Sache#include <ctype.h>
39939Snate#include <dirent.h>
4031306Scharnier#include <err.h>
4131306Scharnier#include <grp.h>
42939Snate#include <pwd.h>
43939Snate#include <signal.h>
44939Snate#include <stddef.h>
45939Snate#include <stdio.h>
46939Snate#include <stdlib.h>
47939Snate#include <string.h>
4831306Scharnier#include <syslog.h>
49939Snate#include <time.h>
50939Snate#include <unistd.h>
5124829Sdavidn#include <utmp.h>
5210154Sache#ifdef __FreeBSD__
5310154Sache#include <paths.h>
5410154Sache#else
557768Sache#include <getopt.h>
567768Sache#endif
57939Snate
5824829Sdavidn#if (MAXLOGNAME-1) > UT_NAMESIZE
5924829Sdavidn#define LOGNAMESIZE UT_NAMESIZE
6024829Sdavidn#else
6124829Sdavidn#define LOGNAMESIZE (MAXLOGNAME-1)
6224829Sdavidn#endif
6324829Sdavidn
64939Snate/* Local headers */
65939Snate
667768Sache#include "gloadavg.h"
67939Snate#define MAIN
68939Snate#include "privs.h"
69939Snate
707768Sache/* Macros */
717768Sache
727768Sache#ifndef ATJOB_DIR
737768Sache#define ATJOB_DIR "/usr/spool/atjobs/"
747768Sache#endif
757768Sache
767768Sache#ifndef ATSPOOL_DIR
777768Sache#define ATSPOOL_DIR "/usr/spool/atspool/"
787768Sache#endif
797768Sache
807768Sache#ifndef LOADAVG_MX
817768Sache#define LOADAVG_MX 1.5
827768Sache#endif
837768Sache
84939Snate/* File scope variables */
85939Snate
867768Sachestatic debug = 0;
87939Snate
8810154Sachevoid perr(const char *a);
8990148Simpstatic void usage(void);
9010154Sache
91939Snate/* Local functions */
92939Snatestatic int
937768Sachewrite_string(int fd, const char* a)
94939Snate{
957768Sache    return write(fd, a, strlen(a));
96939Snate}
97939Snate
9810154Sache#undef DEBUG_FORK
997768Sache#ifdef DEBUG_FORK
1007768Sachestatic pid_t
10190148Simpmyfork(void)
1027768Sache{
1037768Sache	pid_t res;
1047768Sache	res = fork();
1057768Sache	if (res == 0)
1067768Sache	    kill(getpid(),SIGSTOP);
1077768Sache	return res;
1087768Sache}
1097768Sache
1107768Sache#define fork myfork
1117768Sache#endif
1127768Sache
113939Snatestatic void
1147768Sacherun_file(const char *filename, uid_t uid, gid_t gid)
115939Snate{
11680201Skris/* Run a file by spawning off a process which redirects I/O,
1177768Sache * spawns a subshell, then waits for it to complete and sends
1187768Sache * mail to the user.
1197768Sache */
1207768Sache    pid_t pid;
1217768Sache    int fd_out, fd_in;
1227768Sache    int queue;
12324829Sdavidn    char mailbuf[LOGNAMESIZE + 1], fmt[49];
1247768Sache    char *mailname = NULL;
1257768Sache    FILE *stream;
1267768Sache    int send_mail = 0;
12710154Sache    struct stat buf, lbuf;
1287768Sache    off_t size;
1297768Sache    struct passwd *pentry;
1307768Sache    int fflags;
13110154Sache    long nuid;
13210154Sache    long ngid;
133939Snate
134939Snate
1357768Sache    PRIV_START
136939Snate
1377768Sache    if (chmod(filename, S_IRUSR) != 0)
1387768Sache    {
13931306Scharnier	perr("cannot change file permissions");
1407768Sache    }
141939Snate
1427768Sache    PRIV_END
143939Snate
1447768Sache    pid = fork();
1457768Sache    if (pid == -1)
14631306Scharnier	perr("cannot fork");
14710154Sache
14810154Sache    else if (pid != 0)
1497768Sache	return;
150939Snate
1517768Sache    /* Let's see who we mail to.  Hopefully, we can read it from
1527768Sache     * the command file; if not, send it to the owner, or, failing that,
1537768Sache     * to root.
1547768Sache     */
155939Snate
1567768Sache    pentry = getpwuid(uid);
1577768Sache    if (pentry == NULL)
1587768Sache    {
1597768Sache	syslog(LOG_ERR,"Userid %lu not found - aborting job %s",
1607768Sache	       (unsigned long) uid, filename);
16110154Sache        exit(EXIT_FAILURE);
1627768Sache    }
1637768Sache    PRIV_START
164939Snate
1657768Sache    stream=fopen(filename, "r");
166939Snate
1677768Sache    PRIV_END
168939Snate
16910401Smpp#ifdef __FreeBSD__
17010401Smpp    if (pentry->pw_expire && time(NULL) >= pentry->pw_expire)
17110401Smpp    {
17210401Smpp	syslog(LOG_ERR, "Userid %lu is expired - aborting job %s",
17310401Smpp		(unsigned long) uid, filename);
17410401Smpp	exit(EXIT_FAILURE);
17510401Smpp    }
17610401Smpp#endif
17710401Smpp
1787768Sache    if (stream == NULL)
17931306Scharnier	perr("cannot open input file");
1807768Sache
1817768Sache    if ((fd_in = dup(fileno(stream))) <0)
18231306Scharnier	perr("error duplicating input file descriptor");
1837768Sache
18410154Sache    if (fstat(fd_in, &buf) == -1)
18531306Scharnier	perr("error in fstat of input file descriptor");
18610154Sache
18710154Sache    if (lstat(filename, &lbuf) == -1)
18831306Scharnier	perr("error in fstat of input file");
18910154Sache
19010154Sache    if (S_ISLNK(lbuf.st_mode)) {
19110154Sache	syslog(LOG_ERR,"Symbolic link encountered in job %s - aborting",
19210154Sache		filename);
19310154Sache	exit(EXIT_FAILURE);
19410154Sache    }
19510154Sache    if ((lbuf.st_dev != buf.st_dev) || (lbuf.st_ino != buf.st_ino) ||
19610154Sache        (lbuf.st_uid != buf.st_uid) || (lbuf.st_gid != buf.st_gid) ||
19710154Sache        (lbuf.st_size!=buf.st_size)) {
19810154Sache	syslog(LOG_ERR,"Somebody changed files from under us for job %s - "
19910154Sache	"aborting",filename);
20010154Sache	exit(EXIT_FAILURE);
20110154Sache    }
20210154Sache    if (buf.st_nlink > 1) {
20310154Sache	syslog(LOG_ERR,"Someboy is trying to run a linked script for job %s",
20410154Sache		filename);
20510154Sache	exit(EXIT_FAILURE);
20610154Sache    }
2077768Sache    if ((fflags = fcntl(fd_in, F_GETFD)) <0)
20831306Scharnier	perr("error in fcntl");
2097768Sache
2107768Sache    fcntl(fd_in, F_SETFD, fflags & ~FD_CLOEXEC);
2117768Sache
21269199Skris    snprintf(fmt, sizeof(fmt),
21369199Skris	"#!/bin/sh\n# atrun uid=%%ld gid=%%ld\n# mail %%%ds %%d",
21424829Sdavidn                          LOGNAMESIZE);
21524829Sdavidn    if (fscanf(stream, fmt, &nuid, &ngid, mailbuf, &send_mail) != 4) {
21624829Sdavidn	syslog(LOG_ERR,"File %s is in wrong format - aborting", filename);
21710154Sache	exit(EXIT_FAILURE);
2187768Sache    }
21910154Sache    if (mailbuf[0] == '-') {
22010154Sache	syslog(LOG_ERR,"illegal mail name %s in %s",mailbuf,filename);
22110154Sache	exit(EXIT_FAILURE);
2227768Sache    }
22310154Sache    mailname = mailbuf;
22410154Sache    if (nuid != uid) {
22538024Sbde	syslog(LOG_ERR,"Job %s - userid %ld does not match file uid %lu",
22638024Sbde		filename, nuid, (unsigned long)uid);
22710154Sache	exit(EXIT_FAILURE);
22810154Sache    }
22910154Sache    if (ngid != gid) {
23038024Sbde	syslog(LOG_ERR,"Job %s - groupid %ld does not match file gid %lu",
23138024Sbde		filename, ngid, (unsigned long)gid);
23210154Sache	exit(EXIT_FAILURE);
23310154Sache    }
2347768Sache    fclose(stream);
2357768Sache    if (chdir(ATSPOOL_DIR) < 0)
23631306Scharnier	perr("cannot chdir to " ATSPOOL_DIR);
23710154Sache
2387768Sache    /* Create a file to hold the output of the job we are about to run.
2397768Sache     * Write the mail header.
24010154Sache     */
2417768Sache    if((fd_out=open(filename,
2427768Sache		O_WRONLY | O_CREAT | O_EXCL, S_IWUSR | S_IRUSR)) < 0)
24331306Scharnier	perr("cannot create output file");
244939Snate
2457768Sache    write_string(fd_out, "Subject: Output from your job ");
2467768Sache    write_string(fd_out, filename);
2477768Sache    write_string(fd_out, "\n\n");
2487768Sache    fstat(fd_out, &buf);
2497768Sache    size = buf.st_size;
250939Snate
2517768Sache    close(STDIN_FILENO);
2527768Sache    close(STDOUT_FILENO);
2537768Sache    close(STDERR_FILENO);
25410154Sache
2557768Sache    pid = fork();
2567768Sache    if (pid < 0)
25731306Scharnier	perr("error in fork");
258939Snate
2597768Sache    else if (pid == 0)
2607768Sache    {
2617768Sache	char *nul = NULL;
2627768Sache	char **nenvp = &nul;
263939Snate
2647768Sache	/* Set up things for the child; we want standard input from the input file,
2657768Sache	 * and standard output and error sent to our output file.
2667768Sache	 */
267939Snate
2687768Sache	if (lseek(fd_in, (off_t) 0, SEEK_SET) < 0)
26931306Scharnier	    perr("error in lseek");
270939Snate
2717768Sache	if (dup(fd_in) != STDIN_FILENO)
27231306Scharnier	    perr("error in I/O redirection");
273939Snate
2747768Sache	if (dup(fd_out) != STDOUT_FILENO)
27531306Scharnier	    perr("error in I/O redirection");
276939Snate
2777768Sache	if (dup(fd_out) != STDERR_FILENO)
27831306Scharnier	    perr("error in I/O redirection");
279939Snate
2807768Sache	close(fd_in);
2817768Sache	close(fd_out);
2827768Sache	if (chdir(ATJOB_DIR) < 0)
28331306Scharnier	    perr("cannot chdir to " ATJOB_DIR);
284939Snate
2857768Sache	queue = *filename;
286939Snate
2877768Sache	PRIV_START
288939Snate
2897768Sache        nice(tolower(queue) - 'a');
29010154Sache
2917768Sache	if (initgroups(pentry->pw_name,pentry->pw_gid))
29231306Scharnier	    perr("cannot delete saved userids");
293939Snate
29429231Sdima	if (setgid(gid) < 0 || setegid(pentry->pw_gid) < 0)
29531306Scharnier	    perr("cannot change group");
296939Snate
29729231Sdima	if (setlogin(pentry->pw_name))
29831306Scharnier	    perr("cannot set login name");
29929231Sdima
30029231Sdima	if (setuid(uid) < 0 || seteuid(uid) < 0)
30131306Scharnier	    perr("cannot set user id");
302939Snate
30329231Sdima	if (chdir(pentry->pw_dir))
30429231Sdima		chdir("/");
30529231Sdima
3067768Sache	if(execle("/bin/sh","sh",(char *) NULL, nenvp) != 0)
30731306Scharnier	    perr("exec failed for /bin/sh");
308939Snate
3097768Sache	PRIV_END
3107768Sache    }
3117768Sache    /* We're the parent.  Let's wait.
3127768Sache     */
3137768Sache    close(fd_in);
3147768Sache    close(fd_out);
3157768Sache    waitpid(pid, (int *) NULL, 0);
316939Snate
3177768Sache    /* Send mail.  Unlink the output file first, so it is deleted after
3187768Sache     * the run.
3197768Sache     */
3207768Sache    stat(filename, &buf);
3217768Sache    if (open(filename, O_RDONLY) != STDIN_FILENO)
32231306Scharnier        perr("open of jobfile failed");
323939Snate
3247768Sache    unlink(filename);
3257768Sache    if ((buf.st_size != size) || send_mail)
32610154Sache    {
32710154Sache	PRIV_START
32810154Sache
32910154Sache	if (initgroups(pentry->pw_name,pentry->pw_gid))
33031306Scharnier	    perr("cannot delete saved userids");
33110154Sache
33229231Sdima	if (setgid(gid) < 0 || setegid(pentry->pw_gid) < 0)
33331306Scharnier	    perr("cannot change group");
33410154Sache
33529231Sdima	if (setlogin(pentry->pw_name))
33631306Scharnier	    perr("cannot set login name");
33729231Sdima
33829231Sdima	if (setuid(uid) < 0 || seteuid(uid) < 0)
33931306Scharnier	    perr("cannot set user id");
34010154Sache
34129231Sdima	if (chdir(pentry->pw_dir))
34229231Sdima		chdir("/");
34329231Sdima
3447768Sache#ifdef __FreeBSD__
3457768Sache	execl(_PATH_SENDMAIL, "sendmail", "-F", "Atrun Service",
3467777Sache			"-odi", "-oem",
3477768Sache			mailname, (char *) NULL);
3487768Sache#else
3497768Sache        execl(MAIL_CMD, MAIL_CMD, mailname, (char *) NULL);
3507768Sache#endif
35131306Scharnier	    perr("exec failed for mail command");
35210154Sache
35310154Sache	PRIV_END
3547768Sache    }
3557768Sache    exit(EXIT_SUCCESS);
356939Snate}
357939Snate
358939Snate/* Global functions */
359939Snate
36010154Sache/* Needed in gloadavg.c */
36110154Sachevoid
36210154Sacheperr(const char *a)
36310154Sache{
36410154Sache    if (debug)
36510154Sache    {
36631306Scharnier	warn("%s", a);
36710154Sache    }
36810154Sache    else
36910154Sache	syslog(LOG_ERR, "%s: %m", a);
37010154Sache
37110154Sache    exit(EXIT_FAILURE);
37210154Sache}
37310154Sache
374939Snateint
3757768Sachemain(int argc, char *argv[])
376939Snate{
3777768Sache/* Browse through  ATJOB_DIR, checking all the jobfiles wether they should
3787768Sache * be executed and or deleted. The queue is coded into the first byte of
3797768Sache * the job filename, the date (in minutes since Eon) as a hex number in the
3807768Sache * following eight bytes, followed by a dot and a serial number.  A file
3817768Sache * which has not been executed yet is denoted by its execute - bit set.
3827768Sache * For those files which are to be executed, run_file() is called, which forks
3837768Sache * off a child which takes care of I/O redirection, forks off another child
3847768Sache * for execution and yet another one, optionally, for sending mail.
3857768Sache * Files which already have run are removed during the next invocation.
3867768Sache */
3877768Sache    DIR *spool;
3887768Sache    struct dirent *dirent;
3897768Sache    struct stat buf;
3907768Sache    unsigned long ctm;
39110154Sache    unsigned long jobno;
3927768Sache    char queue;
3937768Sache    time_t now, run_time;
3947768Sache    char batch_name[] = "Z2345678901234";
3957768Sache    uid_t batch_uid;
3967768Sache    gid_t batch_gid;
3977768Sache    int c;
3987768Sache    int run_batch;
3997768Sache    double load_avg = LOADAVG_MX;
400939Snate
4017768Sache/* We don't need root privileges all the time; running under uid and gid daemon
4027768Sache * is fine.
4037768Sache */
404939Snate
4057768Sache    RELINQUISH_PRIVS_ROOT(DAEMON_UID, DAEMON_GID)
406939Snate
4077768Sache    openlog("atrun", LOG_PID, LOG_CRON);
408939Snate
4097768Sache    opterr = 0;
41024349Simp    while((c=getopt(argc, argv, "dl:"))!= -1)
4117768Sache    {
4127768Sache	switch (c)
4137768Sache	{
41410154Sache	case 'l':
4157768Sache	    if (sscanf(optarg, "%lf", &load_avg) != 1)
4167768Sache		perr("garbled option -l");
4177768Sache	    if (load_avg <= 0.)
4187768Sache		load_avg = LOADAVG_MX;
4197768Sache	    break;
420939Snate
4217768Sache	case 'd':
4227768Sache	    debug ++;
4237768Sache	    break;
424939Snate
4257768Sache	case '?':
4267768Sache	default:
42731306Scharnier	    usage();
4287768Sache	}
4297768Sache    }
430939Snate
4317768Sache    if (chdir(ATJOB_DIR) != 0)
43231306Scharnier	perr("cannot change to " ATJOB_DIR);
433939Snate
4347768Sache    /* Main loop. Open spool directory for reading and look over all the
4357768Sache     * files in there. If the filename indicates that the job should be run
4367768Sache     * and the x bit is set, fork off a child which sets its user and group
4377768Sache     * id to that of the files and exec a /bin/sh which executes the shell
4387768Sache     * script. Unlink older files if they should no longer be run.  For
4397768Sache     * deletion, their r bit has to be turned on.
4407768Sache     *
4417768Sache     * Also, pick the oldest batch job to run, at most one per invocation of
4427768Sache     * atrun.
4437768Sache     */
4447768Sache    if ((spool = opendir(".")) == NULL)
44531306Scharnier	perr("cannot read " ATJOB_DIR);
446939Snate
4477768Sache    now = time(NULL);
4487768Sache    run_batch = 0;
4497768Sache    batch_uid = (uid_t) -1;
4507768Sache    batch_gid = (gid_t) -1;
451939Snate
45210154Sache    while ((dirent = readdir(spool)) != NULL) {
4537768Sache	if (stat(dirent->d_name,&buf) != 0)
45431306Scharnier	    perr("cannot stat in " ATJOB_DIR);
455939Snate
4567768Sache	/* We don't want directories
4577768Sache	 */
45810154Sache	if (!S_ISREG(buf.st_mode))
4597768Sache	    continue;
460939Snate
46110154Sache	if (sscanf(dirent->d_name,"%c%5lx%8lx",&queue,&jobno,&ctm) != 3)
4627768Sache	    continue;
463939Snate
4647768Sache	run_time = (time_t) ctm*60;
465939Snate
46610154Sache	if ((S_IXUSR & buf.st_mode) && (run_time <=now)) {
46710154Sache	    if (isupper(queue) && (strcmp(batch_name,dirent->d_name) > 0)) {
4687768Sache		run_batch = 1;
4697768Sache		strncpy(batch_name, dirent->d_name, sizeof(batch_name));
4707768Sache		batch_uid = buf.st_uid;
4717768Sache		batch_gid = buf.st_gid;
4727768Sache	    }
47310154Sache
4747768Sache	/* The file is executable and old enough
4757768Sache	 */
4767768Sache	    if (islower(queue))
4777768Sache		run_file(dirent->d_name, buf.st_uid, buf.st_gid);
478939Snate	}
4797768Sache	/*  Delete older files
4807768Sache	 */
4817768Sache	if ((run_time < now) && !(S_IXUSR & buf.st_mode) && (S_IRUSR & buf.st_mode))
4827768Sache	    unlink(dirent->d_name);
4837768Sache    }
4847768Sache    /* run the single batch file, if any
4857768Sache    */
48610154Sache    if (run_batch && (gloadavg() < load_avg))
48710154Sache	run_file(batch_name, batch_uid, batch_gid);
4887768Sache
4897768Sache    closelog();
4907768Sache    exit(EXIT_SUCCESS);
491939Snate}
49231306Scharnier
49331306Scharnierstatic void
49490148Simpusage(void)
49531306Scharnier{
49631306Scharnier    if (debug)
49731306Scharnier	fprintf(stderr, "usage: atrun [-l load_avg] [-d]\n");
49831306Scharnier    else
49931306Scharnier	syslog(LOG_ERR, "usage: atrun [-l load_avg] [-d]");
50031306Scharnier
50131306Scharnier    exit(EXIT_FAILURE);
50231306Scharnier}
503