atrun.c revision 7768
1/*
2 *  atrun.c - run jobs queued by at; run with root privileges.
3 *  Copyright (C) 1993, 1994 Thomas Koenig
4 *
5 * Redistribution and use in source and binary forms, with or without
6 * modification, are permitted provided that the following conditions
7 * are met:
8 * 1. Redistributions of source code must retain the above copyright
9 *    notice, this list of conditions and the following disclaimer.
10 * 2. The name of the author(s) may not be used to endorse or promote
11 *    products derived from this software without specific prior written
12 *    permission.
13 *
14 * THIS SOFTWARE IS PROVIDED BY THE AUTHOR(S) ``AS IS'' AND ANY EXPRESS OR
15 * IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES
16 * OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE DISCLAIMED.
17 * IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR ANY DIRECT, INDIRECT,
18 * INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT
19 * NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE,
20 * DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY
21 * THEORY OF LIABILITY, WETHER IN CONTRACT, STRICT LIABILITY, OR TORT
22 * (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF
23 * THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
24 */
25
26/* System Headers */
27
28#include <sys/fcntl.h>
29#include <sys/types.h>
30#include <sys/stat.h>
31#include <sys/wait.h>
32#include <ctype.h>
33#include <dirent.h>
34#include <errno.h>
35#include <pwd.h>
36#include <grp.h>
37#include <signal.h>
38#include <stddef.h>
39#include <stdio.h>
40#include <stdlib.h>
41#include <string.h>
42#include <time.h>
43#include <unistd.h>
44#include <syslog.h>
45#ifndef __FreeBSD__
46#include <getopt.h>
47#else
48#include <paths.h>
49#endif
50
51/* Local headers */
52
53#ifndef __FreeBSD__
54#include "gloadavg.h"
55#endif
56#define MAIN
57#include "privs.h"
58
59/* Macros */
60
61#ifndef ATJOB_DIR
62#define ATJOB_DIR "/usr/spool/atjobs/"
63#endif
64
65#ifndef ATSPOOL_DIR
66#define ATSPOOL_DIR "/usr/spool/atspool/"
67#endif
68
69#ifndef LOADAVG_MX
70#define LOADAVG_MX 1.5
71#endif
72
73/* File scope variables */
74
75static char *namep;
76static char rcsid[] = "$Id: atrun.c,v 1.1 1994/05/10 18:23:08 kernel Exp $";
77static debug = 0;
78
79/* Local functions */
80static void
81perr(const char *a)
82{
83    if (debug)
84    {
85	perror(a);
86    }
87    else
88	syslog(LOG_ERR, "%s: %m", a);
89
90    exit(EXIT_FAILURE);
91}
92
93static int
94write_string(int fd, const char* a)
95{
96    return write(fd, a, strlen(a));
97}
98
99#ifdef DEBUG_FORK
100static pid_t
101myfork()
102{
103	pid_t res;
104	res = fork();
105	if (res == 0)
106	    kill(getpid(),SIGSTOP);
107	return res;
108}
109
110#define fork myfork
111#endif
112
113static void
114run_file(const char *filename, uid_t uid, gid_t gid)
115{
116/* Run a file by by spawning off a process which redirects I/O,
117 * spawns a subshell, then waits for it to complete and sends
118 * mail to the user.
119 */
120    pid_t pid;
121    int fd_out, fd_in;
122    int queue;
123    char mailbuf[9];
124    char *mailname = NULL;
125    FILE *stream;
126    int send_mail = 0;
127    struct stat buf;
128    off_t size;
129    struct passwd *pentry;
130    int fflags;
131
132
133    PRIV_START
134
135    if (chmod(filename, S_IRUSR) != 0)
136    {
137	perr("Cannot change file permissions");
138    }
139
140    PRIV_END
141
142    pid = fork();
143    if (pid == -1)
144	perr("Cannot fork");
145
146    else if (pid > 0)
147	return;
148
149    /* Let's see who we mail to.  Hopefully, we can read it from
150     * the command file; if not, send it to the owner, or, failing that,
151     * to root.
152     */
153
154    pentry = getpwuid(uid);
155    if (pentry == NULL)
156    {
157	syslog(LOG_ERR,"Userid %lu not found - aborting job %s",
158	       (unsigned long) uid, filename);
159	exit(EXIT_FAILURE);
160    }
161    PRIV_START
162
163    stream=fopen(filename, "r");
164
165    PRIV_END
166
167    if (stream == NULL)
168	perr("Cannot open input file");
169
170    if ((fd_in = dup(fileno(stream))) <0)
171	perr("Error duplicating input file descriptor");
172
173    if ((fflags = fcntl(fd_in, F_GETFD)) <0)
174	perr("Error in fcntl");
175
176    fcntl(fd_in, F_SETFD, fflags & ~FD_CLOEXEC);
177
178    if (fscanf(stream, "#! /bin/sh\n# mail %8s %d", mailbuf, &send_mail) == 2)
179    {
180	mailname = mailbuf;
181	pentry = getpwnam(mailname);
182	if (pentry == NULL || pentry->pw_uid != uid) {
183		syslog(LOG_ERR,"Userid %lu mismatch name %s - aborting job %s",
184		       (unsigned long) uid, mailname, filename);
185		exit(EXIT_FAILURE);
186	}
187    }
188    else
189    {
190	mailname = pentry->pw_name;
191    }
192    fclose(stream);
193    if (chdir(ATSPOOL_DIR) < 0)
194	perr("Cannot chdir to " ATSPOOL_DIR);
195
196    /* Create a file to hold the output of the job we are about to run.
197     * Write the mail header.
198     */
199    if((fd_out=open(filename,
200		O_WRONLY | O_CREAT | O_EXCL, S_IWUSR | S_IRUSR)) < 0)
201	perr("Cannot create output file");
202
203    write_string(fd_out, "Subject: Output from your job ");
204    write_string(fd_out, filename);
205    write_string(fd_out, "\n\n");
206    fstat(fd_out, &buf);
207    size = buf.st_size;
208
209    close(STDIN_FILENO);
210    close(STDOUT_FILENO);
211    close(STDERR_FILENO);
212
213    pid = fork();
214    if (pid < 0)
215	perr("Error in fork");
216
217    else if (pid == 0)
218    {
219	char *nul = NULL;
220	char **nenvp = &nul;
221
222	/* Set up things for the child; we want standard input from the input file,
223	 * and standard output and error sent to our output file.
224	 */
225
226	if (lseek(fd_in, (off_t) 0, SEEK_SET) < 0)
227	    perr("Error in lseek");
228
229	if (dup(fd_in) != STDIN_FILENO)
230	    perr("Error in I/O redirection");
231
232	if (dup(fd_out) != STDOUT_FILENO)
233	    perr("Error in I/O redirection");
234
235	if (dup(fd_out) != STDERR_FILENO)
236	    perr("Error in I/O redirection");
237
238	close(fd_in);
239	close(fd_out);
240	if (chdir(ATJOB_DIR) < 0)
241	    perr("Cannot chdir to " ATJOB_DIR);
242
243	queue = *filename;
244
245	PRIV_START
246
247        nice(tolower(queue) - 'a');
248
249	chdir(pentry->pw_dir);
250
251	if (initgroups(pentry->pw_name,pentry->pw_gid))
252	    perr("Cannot delete saved userids");
253
254	if (setgid(gid) < 0)
255	    perr("Cannot change group");
256
257	if (setuid(uid) < 0)
258	    perr("Cannot set user id");
259
260	if(execle("/bin/sh","sh",(char *) NULL, nenvp) != 0)
261	    perr("Exec failed");
262
263	PRIV_END
264    }
265    /* We're the parent.  Let's wait.
266     */
267    close(fd_in);
268    close(fd_out);
269    waitpid(pid, (int *) NULL, 0);
270
271    /* Send mail.  Unlink the output file first, so it is deleted after
272     * the run.
273     */
274    stat(filename, &buf);
275    if (open(filename, O_RDONLY) != STDIN_FILENO)
276        perr("Open of jobfile failed");
277
278    unlink(filename);
279    if ((buf.st_size != size) || send_mail)
280    {
281#ifdef __FreeBSD__
282	execl(_PATH_SENDMAIL, "sendmail", "-F", "Atrun Service",
283			mailname, (char *) NULL);
284#else
285        execl(MAIL_CMD, MAIL_CMD, mailname, (char *) NULL);
286#endif
287	perr("Exec failed");
288    }
289    exit(EXIT_SUCCESS);
290}
291
292/* Global functions */
293
294int
295main(int argc, char *argv[])
296{
297/* Browse through  ATJOB_DIR, checking all the jobfiles wether they should
298 * be executed and or deleted. The queue is coded into the first byte of
299 * the job filename, the date (in minutes since Eon) as a hex number in the
300 * following eight bytes, followed by a dot and a serial number.  A file
301 * which has not been executed yet is denoted by its execute - bit set.
302 * For those files which are to be executed, run_file() is called, which forks
303 * off a child which takes care of I/O redirection, forks off another child
304 * for execution and yet another one, optionally, for sending mail.
305 * Files which already have run are removed during the next invocation.
306 */
307    DIR *spool;
308    struct dirent *dirent;
309    struct stat buf;
310    unsigned long ctm;
311    char queue;
312    time_t now, run_time;
313    char batch_name[] = "Z2345678901234";
314    uid_t batch_uid;
315    gid_t batch_gid;
316    int c;
317    int run_batch;
318    double load_avg = LOADAVG_MX;
319
320/* We don't need root privileges all the time; running under uid and gid daemon
321 * is fine.
322 */
323
324    RELINQUISH_PRIVS_ROOT(DAEMON_UID, DAEMON_GID)
325
326    openlog("atrun", LOG_PID, LOG_CRON);
327
328    opterr = 0;
329    errno = 0;
330    while((c=getopt(argc, argv, "dl:"))!= EOF)
331    {
332	switch (c)
333	{
334	case 'l':
335	    if (sscanf(optarg, "%lf", &load_avg) != 1)
336		perr("garbled option -l");
337	    if (load_avg <= 0.)
338		load_avg = LOADAVG_MX;
339	    break;
340
341	case 'd':
342	    debug ++;
343	    break;
344
345	case '?':
346	    perr("unknown option");
347	    break;
348
349	default:
350	    perr("idiotic option - aborted");
351	    break;
352	}
353    }
354
355    namep = argv[0];
356    if (chdir(ATJOB_DIR) != 0)
357	perr("Cannot change to " ATJOB_DIR);
358
359    /* Main loop. Open spool directory for reading and look over all the
360     * files in there. If the filename indicates that the job should be run
361     * and the x bit is set, fork off a child which sets its user and group
362     * id to that of the files and exec a /bin/sh which executes the shell
363     * script. Unlink older files if they should no longer be run.  For
364     * deletion, their r bit has to be turned on.
365     *
366     * Also, pick the oldest batch job to run, at most one per invocation of
367     * atrun.
368     */
369    if ((spool = opendir(".")) == NULL)
370	perr("Cannot read " ATJOB_DIR);
371
372    now = time(NULL);
373    run_batch = 0;
374    batch_uid = (uid_t) -1;
375    batch_gid = (gid_t) -1;
376
377    while ((dirent = readdir(spool)) != NULL)
378    {
379	if (stat(dirent->d_name,&buf) != 0)
380	    perr("Cannot stat in " ATJOB_DIR);
381
382	/* We don't want directories
383	 */
384	if (!S_ISREG(buf.st_mode))
385	    continue;
386
387	if (sscanf(dirent->d_name,"%c%8lx",&queue,&ctm) != 2)
388	    continue;
389
390	run_time = (time_t) ctm*60;
391
392	if ((S_IXUSR & buf.st_mode) && (run_time <=now))
393	{
394	    if (isupper(queue) && (strcmp(batch_name,dirent->d_name) > 0))
395	    {
396		run_batch = 1;
397		strncpy(batch_name, dirent->d_name, sizeof(batch_name));
398		batch_uid = buf.st_uid;
399		batch_gid = buf.st_gid;
400	    }
401
402	/* The file is executable and old enough
403	 */
404	    if (islower(queue))
405		run_file(dirent->d_name, buf.st_uid, buf.st_gid);
406	}
407	/*  Delete older files
408	 */
409	if ((run_time < now) && !(S_IXUSR & buf.st_mode) && (S_IRUSR & buf.st_mode))
410	    unlink(dirent->d_name);
411    }
412    /* run the single batch file, if any
413    */
414#ifndef __FreeBSD__
415    if (run_batch && (gloadavg() < load_avg)) {
416#else
417    if (run_batch) {
418	double la;
419
420	if (getloadavg(&la, 1) != 1)
421	    perr("Error in getloadavg");
422	if (la < load_avg)
423#endif
424	    run_file(batch_name, batch_uid, batch_gid);
425    }
426
427    closelog();
428    exit(EXIT_SUCCESS);
429}
430