atrun.c revision 267654
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(S) 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#ifndef lint
27static const char rcsid[] =
28  "$FreeBSD: releng/9.3/libexec/atrun/atrun.c 242990 2012-11-13 19:17:21Z mjg $";
29#endif /* not lint */
30
31/* System Headers */
32
33#include <sys/fcntl.h>
34#include <sys/types.h>
35#include <sys/stat.h>
36#ifdef __FreeBSD__
37#include <sys/sysctl.h>
38#endif
39#include <sys/wait.h>
40#include <sys/param.h>
41#include <ctype.h>
42#include <dirent.h>
43#include <err.h>
44#include <grp.h>
45#include <pwd.h>
46#include <signal.h>
47#include <stdarg.h>
48#include <stddef.h>
49#include <stdio.h>
50#include <stdlib.h>
51#include <string.h>
52#include <syslog.h>
53#include <time.h>
54#include <unistd.h>
55#ifdef __FreeBSD__
56#include <paths.h>
57#else
58#include <getopt.h>
59#endif
60#ifdef LOGIN_CAP
61#include <login_cap.h>
62#endif
63#ifdef PAM
64#include <security/pam_appl.h>
65#include <security/openpam.h>
66#endif
67
68/* Local headers */
69
70#include "gloadavg.h"
71#define MAIN
72#include "privs.h"
73
74/* Macros */
75
76#ifndef ATJOB_DIR
77#define ATJOB_DIR "/usr/spool/atjobs/"
78#endif
79
80#ifndef ATSPOOL_DIR
81#define ATSPOOL_DIR "/usr/spool/atspool/"
82#endif
83
84#ifndef LOADAVG_MX
85#define LOADAVG_MX 1.5
86#endif
87
88/* File scope variables */
89
90static const char * const atrun = "atrun"; /* service name for syslog etc. */
91static int debug = 0;
92
93void perr(const char *fmt, ...);
94void perrx(const char *fmt, ...);
95static void usage(void);
96
97/* Local functions */
98static int
99write_string(int fd, const char* a)
100{
101    return write(fd, a, strlen(a));
102}
103
104#undef DEBUG_FORK
105#ifdef DEBUG_FORK
106static pid_t
107myfork(void)
108{
109	pid_t res;
110	res = fork();
111	if (res == 0)
112	    kill(getpid(),SIGSTOP);
113	return res;
114}
115
116#define fork myfork
117#endif
118
119static void
120run_file(const char *filename, uid_t uid, gid_t gid)
121{
122/* Run a file by spawning off a process which redirects I/O,
123 * spawns a subshell, then waits for it to complete and sends
124 * mail to the user.
125 */
126    pid_t pid;
127    int fd_out, fd_in;
128    int queue;
129    char mailbuf[MAXLOGNAME], fmt[64];
130    char *mailname = NULL;
131    FILE *stream;
132    int send_mail = 0;
133    struct stat buf, lbuf;
134    off_t size;
135    struct passwd *pentry;
136    int fflags;
137    long nuid;
138    long ngid;
139#ifdef PAM
140    pam_handle_t *pamh = NULL;
141    int pam_err;
142    struct pam_conv pamc = {
143	.conv = openpam_nullconv,
144	.appdata_ptr = NULL
145    };
146#endif
147
148    PRIV_START
149
150    if (chmod(filename, S_IRUSR) != 0)
151    {
152	perr("cannot change file permissions");
153    }
154
155    PRIV_END
156
157    pid = fork();
158    if (pid == -1)
159	perr("cannot fork");
160
161    else if (pid != 0)
162	return;
163
164    /* Let's see who we mail to.  Hopefully, we can read it from
165     * the command file; if not, send it to the owner, or, failing that,
166     * to root.
167     */
168
169    pentry = getpwuid(uid);
170    if (pentry == NULL)
171	perrx("Userid %lu not found - aborting job %s",
172		(unsigned long) uid, filename);
173
174#ifdef PAM
175    PRIV_START
176
177    pam_err = pam_start(atrun, pentry->pw_name, &pamc, &pamh);
178    if (pam_err != PAM_SUCCESS)
179	perrx("cannot start PAM: %s", pam_strerror(pamh, pam_err));
180
181    pam_err = pam_acct_mgmt(pamh, PAM_SILENT);
182    /* Expired password shouldn't prevent the job from running. */
183    if (pam_err != PAM_SUCCESS && pam_err != PAM_NEW_AUTHTOK_REQD)
184	perrx("Account %s (userid %lu) unavailable for job %s: %s",
185	    pentry->pw_name, (unsigned long)uid,
186	    filename, pam_strerror(pamh, pam_err));
187
188    pam_end(pamh, pam_err);
189
190    PRIV_END
191#endif /* PAM */
192
193    PRIV_START
194
195    stream=fopen(filename, "r");
196
197    PRIV_END
198
199    if (stream == NULL)
200	perr("cannot open input file");
201
202    if ((fd_in = dup(fileno(stream))) <0)
203	perr("error duplicating input file descriptor");
204
205    if (fstat(fd_in, &buf) == -1)
206	perr("error in fstat of input file descriptor");
207
208    if (lstat(filename, &lbuf) == -1)
209	perr("error in fstat of input file");
210
211    if (S_ISLNK(lbuf.st_mode))
212	perrx("Symbolic link encountered in job %s - aborting", filename);
213
214    if ((lbuf.st_dev != buf.st_dev) || (lbuf.st_ino != buf.st_ino) ||
215        (lbuf.st_uid != buf.st_uid) || (lbuf.st_gid != buf.st_gid) ||
216        (lbuf.st_size!=buf.st_size))
217	perrx("Somebody changed files from under us for job %s - aborting",
218		filename);
219
220    if (buf.st_nlink > 1)
221	perrx("Somebody is trying to run a linked script for job %s", filename);
222
223    if ((fflags = fcntl(fd_in, F_GETFD)) <0)
224	perr("error in fcntl");
225
226    fcntl(fd_in, F_SETFD, fflags & ~FD_CLOEXEC);
227
228    snprintf(fmt, sizeof(fmt),
229	"#!/bin/sh\n# atrun uid=%%ld gid=%%ld\n# mail %%%ds %%d",
230                          MAXLOGNAME - 1);
231
232    if (fscanf(stream, fmt, &nuid, &ngid, mailbuf, &send_mail) != 4)
233	perrx("File %s is in wrong format - aborting", filename);
234
235    if (mailbuf[0] == '-')
236	perrx("Illegal mail name %s in %s", mailbuf, filename);
237
238    mailname = mailbuf;
239
240    if (nuid != uid)
241	perrx("Job %s - userid %ld does not match file uid %lu",
242		filename, nuid, (unsigned long)uid);
243
244    if (ngid != gid)
245	perrx("Job %s - groupid %ld does not match file gid %lu",
246		filename, ngid, (unsigned long)gid);
247
248    fclose(stream);
249
250    if (chdir(ATSPOOL_DIR) < 0)
251	perr("cannot chdir to %s", ATSPOOL_DIR);
252
253    /* Create a file to hold the output of the job we are about to run.
254     * Write the mail header.
255     */
256    if((fd_out=open(filename,
257		O_WRONLY | O_CREAT | O_EXCL, S_IWUSR | S_IRUSR)) < 0)
258	perr("cannot create output file");
259
260    write_string(fd_out, "Subject: Output from your job ");
261    write_string(fd_out, filename);
262    write_string(fd_out, "\n\n");
263    fstat(fd_out, &buf);
264    size = buf.st_size;
265
266    close(STDIN_FILENO);
267    close(STDOUT_FILENO);
268    close(STDERR_FILENO);
269
270    pid = fork();
271    if (pid < 0)
272	perr("error in fork");
273
274    else if (pid == 0)
275    {
276	char *nul = NULL;
277	char **nenvp = &nul;
278
279	/* Set up things for the child; we want standard input from the input file,
280	 * and standard output and error sent to our output file.
281	 */
282
283	if (lseek(fd_in, (off_t) 0, SEEK_SET) < 0)
284	    perr("error in lseek");
285
286	if (dup(fd_in) != STDIN_FILENO)
287	    perr("error in I/O redirection");
288
289	if (dup(fd_out) != STDOUT_FILENO)
290	    perr("error in I/O redirection");
291
292	if (dup(fd_out) != STDERR_FILENO)
293	    perr("error in I/O redirection");
294
295	close(fd_in);
296	close(fd_out);
297	if (chdir(ATJOB_DIR) < 0)
298	    perr("cannot chdir to %s", ATJOB_DIR);
299
300	queue = *filename;
301
302	PRIV_START
303
304        nice(tolower(queue) - 'a');
305
306#ifdef LOGIN_CAP
307	/*
308	 * For simplicity and safety, set all aspects of the user context
309	 * except for a selected subset:  Don't set priority, which was
310	 * set based on the queue file name according to the tradition.
311	 * Don't bother to set environment, including path vars, either
312	 * because it will be discarded anyway.  Although the job file
313	 * should set umask, preset it here just in case.
314	 */
315	if (setusercontext(NULL, pentry, uid, LOGIN_SETALL &
316		~(LOGIN_SETPRIORITY | LOGIN_SETPATH | LOGIN_SETENV)) != 0)
317	    exit(EXIT_FAILURE);	/* setusercontext() logged the error */
318#else /* LOGIN_CAP */
319	if (initgroups(pentry->pw_name,pentry->pw_gid))
320	    perr("cannot init group access list");
321
322	if (setgid(gid) < 0 || setegid(pentry->pw_gid) < 0)
323	    perr("cannot change group");
324
325	if (setlogin(pentry->pw_name))
326	    perr("cannot set login name");
327
328	if (setuid(uid) < 0 || seteuid(uid) < 0)
329	    perr("cannot set user id");
330#endif /* LOGIN_CAP */
331
332	if (chdir(pentry->pw_dir))
333		chdir("/");
334
335	if(execle("/bin/sh","sh",(char *) NULL, nenvp) != 0)
336	    perr("exec failed for /bin/sh");
337
338	PRIV_END
339    }
340    /* We're the parent.  Let's wait.
341     */
342    close(fd_in);
343    close(fd_out);
344    waitpid(pid, (int *) NULL, 0);
345
346    /* Send mail.  Unlink the output file first, so it is deleted after
347     * the run.
348     */
349    stat(filename, &buf);
350    if (open(filename, O_RDONLY) != STDIN_FILENO)
351        perr("open of jobfile failed");
352
353    unlink(filename);
354    if ((buf.st_size != size) || send_mail)
355    {
356	PRIV_START
357
358#ifdef LOGIN_CAP
359	/*
360	 * This time set full context to run the mailer.
361	 */
362	if (setusercontext(NULL, pentry, uid, LOGIN_SETALL) != 0)
363	    exit(EXIT_FAILURE);	/* setusercontext() logged the error */
364#else /* LOGIN_CAP */
365	if (initgroups(pentry->pw_name,pentry->pw_gid))
366	    perr("cannot init group access list");
367
368	if (setgid(gid) < 0 || setegid(pentry->pw_gid) < 0)
369	    perr("cannot change group");
370
371	if (setlogin(pentry->pw_name))
372	    perr("cannot set login name");
373
374	if (setuid(uid) < 0 || seteuid(uid) < 0)
375	    perr("cannot set user id");
376#endif /* LOGIN_CAP */
377
378	if (chdir(pentry->pw_dir))
379		chdir("/");
380
381#ifdef __FreeBSD__
382	execl(_PATH_SENDMAIL, "sendmail", "-F", "Atrun Service",
383			"-odi", "-oem",
384			mailname, (char *) NULL);
385#else
386        execl(MAIL_CMD, MAIL_CMD, mailname, (char *) NULL);
387#endif
388	    perr("exec failed for mail command");
389
390	PRIV_END
391    }
392    exit(EXIT_SUCCESS);
393}
394
395/* Global functions */
396
397/* Needed in gloadavg.c */
398void
399perr(const char *fmt, ...)
400{
401    const char * const fmtadd = ": %m";
402    char nfmt[strlen(fmt) + strlen(fmtadd) + 1];
403    va_list ap;
404
405    va_start(ap, fmt);
406    if (debug)
407    {
408	vwarn(fmt, ap);
409    }
410    else
411    {
412	snprintf(nfmt, sizeof(nfmt), "%s%s", fmt, fmtadd);
413	vsyslog(LOG_ERR, nfmt, ap);
414    }
415    va_end(ap);
416
417    exit(EXIT_FAILURE);
418}
419
420void
421perrx(const char *fmt, ...)
422{
423    va_list ap;
424
425    va_start(ap, fmt);
426    if (debug)
427	vwarnx(fmt, ap);
428    else
429	vsyslog(LOG_ERR, fmt, ap);
430    va_end(ap);
431
432    exit(EXIT_FAILURE);
433}
434
435int
436main(int argc, char *argv[])
437{
438/* Browse through  ATJOB_DIR, checking all the jobfiles wether they should
439 * be executed and or deleted. The queue is coded into the first byte of
440 * the job filename, the date (in minutes since Eon) as a hex number in the
441 * following eight bytes, followed by a dot and a serial number.  A file
442 * which has not been executed yet is denoted by its execute - bit set.
443 * For those files which are to be executed, run_file() is called, which forks
444 * off a child which takes care of I/O redirection, forks off another child
445 * for execution and yet another one, optionally, for sending mail.
446 * Files which already have run are removed during the next invocation.
447 */
448    DIR *spool;
449    struct dirent *dirent;
450    struct stat buf;
451    unsigned long ctm;
452    unsigned long jobno;
453    char queue;
454    time_t now, run_time;
455    char batch_name[] = "Z2345678901234";
456    uid_t batch_uid;
457    gid_t batch_gid;
458    int c;
459    int run_batch;
460#ifdef __FreeBSD__
461    size_t ncpu, ncpusz;
462    double load_avg = -1;
463#else
464    double load_avg = LOADAVG_MX;
465#endif
466
467/* We don't need root privileges all the time; running under uid and gid daemon
468 * is fine.
469 */
470
471    RELINQUISH_PRIVS_ROOT(DAEMON_UID, DAEMON_GID)
472
473    openlog(atrun, LOG_PID, LOG_CRON);
474
475    opterr = 0;
476    while((c=getopt(argc, argv, "dl:"))!= -1)
477    {
478	switch (c)
479	{
480	case 'l':
481	    if (sscanf(optarg, "%lf", &load_avg) != 1)
482		perr("garbled option -l");
483#ifndef __FreeBSD__
484	    if (load_avg <= 0.)
485		load_avg = LOADAVG_MX;
486#endif
487	    break;
488
489	case 'd':
490	    debug ++;
491	    break;
492
493	case '?':
494	default:
495	    usage();
496	}
497    }
498
499    if (chdir(ATJOB_DIR) != 0)
500	perr("cannot change to %s", ATJOB_DIR);
501
502#ifdef __FreeBSD__
503    if (load_avg <= 0.) {
504	ncpusz = sizeof(size_t);
505	if (sysctlbyname("hw.ncpu", &ncpu, &ncpusz, NULL, 0) < 0)
506		ncpu = 1;
507	load_avg = LOADAVG_MX * ncpu;
508    }
509#endif
510
511    /* Main loop. Open spool directory for reading and look over all the
512     * files in there. If the filename indicates that the job should be run
513     * and the x bit is set, fork off a child which sets its user and group
514     * id to that of the files and exec a /bin/sh which executes the shell
515     * script. Unlink older files if they should no longer be run.  For
516     * deletion, their r bit has to be turned on.
517     *
518     * Also, pick the oldest batch job to run, at most one per invocation of
519     * atrun.
520     */
521    if ((spool = opendir(".")) == NULL)
522	perr("cannot read %s", ATJOB_DIR);
523
524    now = time(NULL);
525    run_batch = 0;
526    batch_uid = (uid_t) -1;
527    batch_gid = (gid_t) -1;
528
529    while ((dirent = readdir(spool)) != NULL) {
530	if (stat(dirent->d_name,&buf) != 0)
531	    perr("cannot stat in %s", ATJOB_DIR);
532
533	/* We don't want directories
534	 */
535	if (!S_ISREG(buf.st_mode))
536	    continue;
537
538	if (sscanf(dirent->d_name,"%c%5lx%8lx",&queue,&jobno,&ctm) != 3)
539	    continue;
540
541	run_time = (time_t) ctm*60;
542
543	if ((S_IXUSR & buf.st_mode) && (run_time <=now)) {
544	    if (isupper(queue) && (strcmp(batch_name,dirent->d_name) > 0)) {
545		run_batch = 1;
546		strlcpy(batch_name, dirent->d_name, sizeof(batch_name));
547		batch_uid = buf.st_uid;
548		batch_gid = buf.st_gid;
549	    }
550
551	/* The file is executable and old enough
552	 */
553	    if (islower(queue))
554		run_file(dirent->d_name, buf.st_uid, buf.st_gid);
555	}
556	/*  Delete older files
557	 */
558	if ((run_time < now) && !(S_IXUSR & buf.st_mode) && (S_IRUSR & buf.st_mode))
559	    unlink(dirent->d_name);
560    }
561    /* run the single batch file, if any
562    */
563    if (run_batch && (gloadavg() < load_avg))
564	run_file(batch_name, batch_uid, batch_gid);
565
566    closelog();
567    exit(EXIT_SUCCESS);
568}
569
570static void
571usage(void)
572{
573    if (debug)
574	fprintf(stderr, "usage: atrun [-l load_avg] [-d]\n");
575    else
576	syslog(LOG_ERR, "usage: atrun [-l load_avg] [-d]");
577
578    exit(EXIT_FAILURE);
579}
580