1 /* $NetBSD: atrun.c,v 1.18 2007/12/15 19:44:45 perry Exp $ */
4 * atrun.c - run jobs queued by at; run with root privileges.
5 * Copyright (C) 1993, 1994 Thomas Koenig
7 * Redistribution and use in source and binary forms, with or without
8 * modification, are permitted provided that the following conditions
10 * 1. Redistributions of source code must retain the above copyright
11 * notice, this list of conditions and the following disclaimer.
12 * 2. The name of the author(s) may not be used to endorse or promote
13 * products derived from this software without specific prior written
16 * THIS SOFTWARE IS PROVIDED BY THE AUTHOR(S) ``AS IS'' AND ANY EXPRESS OR
17 * IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES
18 * OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE DISCLAIMED.
19 * IN NO EVENT SHALL THE AUTHOR(S) BE LIABLE FOR ANY DIRECT, INDIRECT,
20 * INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT
21 * NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE,
22 * DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY
23 * THEORY OF LIABILITY, WETHER IN CONTRACT, STRICT LIABILITY, OR TORT
24 * (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF
25 * THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
29 #include <sys/cdefs.h>
30 #include <sys/types.h>
54 #include "pathnames.h"
57 /* File scope variables */
60 static char rcsid
[] = "$OpenBSD: atrun.c,v 1.7 1997/09/08 22:12:10 millert Exp $";
62 __RCSID("$NetBSD: atrun.c,v 1.18 2007/12/15 19:44:45 perry Exp $");
68 static void perr(const char *, ...) __dead
;
69 static void perrx(const char *, ...) __dead
;
70 static int write_string(int, const char *);
71 static void run_file(const char *, uid_t
, gid_t
);
72 static void become_user(struct passwd
*, uid_t
);
74 static const char nobody
[] = "nobody";
77 perr(const char *fmt
, ...)
83 (void)vsnprintf(buf
, sizeof(buf
), fmt
, ap
);
89 syslog(LOG_ERR
, "%s: %m", buf
);
95 perrx(const char *fmt
, ...)
104 vsyslog(LOG_ERR
, fmt
, ap
);
112 write_string(int fd
, const char *a
)
114 return write(fd
, a
, strlen(a
));
118 become_user(struct passwd
*pentry
, uid_t uid
)
120 if (initgroups(pentry
->pw_name
, pentry
->pw_gid
) == -1)
121 perr("Cannot init group list for `%s'", pentry
->pw_name
);
123 if (setegid(pentry
->pw_gid
) == -1 || setgid(pentry
->pw_gid
) == -1)
124 perr("Cannot change primary group to %lu",
125 (unsigned long)pentry
->pw_gid
);
128 perr("Cannot create a session");
130 if (setlogin(pentry
->pw_name
) == -1)
131 perr("Cannot set login name to `%s'", pentry
->pw_name
);
133 if (setuid(uid
) == -1 || seteuid(uid
) == -1)
134 perr("Cannot set user id to %lu", (unsigned long)uid
);
136 if (chdir(pentry
->pw_dir
) == -1)
141 run_file(const char *filename
, uid_t uid
, gid_t gid
)
144 * Run a file by by spawning off a process which redirects I/O,
145 * spawns a subshell, then waits for it to complete and spawns another
146 * process to send mail to the user.
151 char mailbuf
[LOGIN_NAME_MAX
], fmt
[49];
152 char *mailname
= NULL
;
155 struct stat buf
, lbuf
;
157 struct passwd
*pentry
;
165 if (chmod(filename
, S_IRUSR
) == -1)
166 perr("Cannot change file permissions to `%s'", filename
);
177 * Let's see who we mail to. Hopefully, we can read it from the
178 * command file; if not, send it to the owner, or, failing that, to
182 pentry
= getpwuid(uid
);
184 perrx("Userid %lu not found - aborting job `%s'",
185 (unsigned long)uid
, filename
);
189 stream
= fopen(filename
, "r");
194 if (stream
== NULL
) {
196 perr("Cannot open input file");
199 if (pentry
->pw_expire
&& time(NULL
) >= pentry
->pw_expire
)
200 perrx("Userid %lu has expired - aborting job `%s'",
201 (unsigned long)uid
, filename
);
203 if ((fd_in
= dup(fileno(stream
))) == -1)
204 perr("Error duplicating input file descriptor");
206 if (fstat(fd_in
, &buf
) == -1)
207 perr("Error in fstat of input file descriptor");
211 if (lstat(filename
, &lbuf
) == -1)
212 perr("Error in lstat of `%s'", filename
);
216 if (S_ISLNK(lbuf
.st_mode
))
217 perrx("Symbolic link encountered in job `%s' - aborting",
220 if ((lbuf
.st_dev
!= buf
.st_dev
) || (lbuf
.st_ino
!= buf
.st_ino
) ||
221 (lbuf
.st_uid
!= buf
.st_uid
) || (lbuf
.st_gid
!= buf
.st_gid
) ||
222 (lbuf
.st_size
!=buf
.st_size
))
223 perrx("Somebody changed files from under us for job `%s' "
224 "- aborting", filename
);
226 if (buf
.st_nlink
> 1)
227 perrx("Somebody is trying to run a linked script for job `%s'",
230 if ((fflags
= fcntl(fd_in
, F_GETFD
)) < 0)
231 perr("Error in fcntl");
233 (void)fcntl(fd_in
, F_SETFD
, fflags
& ~FD_CLOEXEC
);
235 (void)snprintf(fmt
, sizeof(fmt
),
236 "#!/bin/sh\n# atrun uid=%%u gid=%%u\n# mail %%%ds %%d",
239 if (fscanf(stream
, fmt
, &nuid
, &ngid
, mailbuf
, &send_mail
) != 4)
240 perrx("File `%s' is in wrong format - aborting", filename
);
242 if (mailbuf
[0] == '-')
243 perrx("Illegal mail name `%s' in `%s'", mailbuf
, filename
);
247 perrx("Job `%s' - userid %lu does not match file uid %lu",
248 filename
, (unsigned long)nuid
, (unsigned long)uid
);
251 perrx("Job `%s' - groupid %lu does not match file gid %lu",
252 filename
, (unsigned long)ngid
, (unsigned long)gid
);
254 (void)fclose(stream
);
258 if (chdir(_PATH_ATSPOOL
) == -1)
259 perr("Cannot chdir to `%s'", _PATH_ATSPOOL
);
262 * Create a file to hold the output of the job we are about to
263 * run. Write the mail header.
266 if ((fd_out
= open(filename
,
267 O_WRONLY
| O_CREAT
| O_EXCL
, S_IWUSR
| S_IRUSR
)) == -1)
268 perr("Cannot create output file `%s'", filename
);
272 write_string(fd_out
, "To: ");
273 write_string(fd_out
, mailname
);
274 write_string(fd_out
, "\nSubject: Output from your job ");
275 write_string(fd_out
, filename
);
276 write_string(fd_out
, "\n\n");
277 if (fstat(fd_out
, &buf
) == -1)
278 perr("Error in fstat of output file descriptor");
281 (void)close(STDIN_FILENO
);
282 (void)close(STDOUT_FILENO
);
283 (void)close(STDERR_FILENO
);
287 perr("Error in fork");
293 * Set up things for the child; we want standard input from
294 * the input file, and standard output and error sent to
297 if (lseek(fd_in
, (off_t
) 0, SEEK_SET
) == (off_t
)-1)
298 perr("Error in lseek");
300 if (dup(fd_in
) != STDIN_FILENO
)
301 perr("Error in I/O redirection");
303 if (dup(fd_out
) != STDOUT_FILENO
)
304 perr("Error in I/O redirection");
306 if (dup(fd_out
) != STDERR_FILENO
)
307 perr("Error in I/O redirection");
314 if (chdir(_PATH_ATJOBS
) == -1)
315 perr("Cannot chdir to `%s'", _PATH_ATJOBS
);
322 become_user(pentry
, uid
);
324 (void)execle("/bin/sh", "sh", (char *)NULL
, nenvp
);
325 perr("Exec failed for /bin/sh");
327 /* We're the parent. Let's wait. */
330 (void)waitpid(pid
, (int *)NULL
, 0);
333 * Send mail. Unlink the output file first, so it is deleted
338 if (stat(filename
, &buf
) == -1)
339 perr("Error in stat of output file `%s'", filename
);
340 if (open(filename
, O_RDONLY
) != STDIN_FILENO
)
341 perr("Open of jobfile `%s' failed", filename
);
343 (void)unlink(filename
);
347 if ((buf
.st_size
!= size
) || send_mail
) {
348 /* Fork off a child for sending mail */
352 become_user(pentry
, uid
);
354 execl(_PATH_SENDMAIL
, "sendmail", "-F", "Atrun Service",
355 "-odi", "-oem", "-t", (char *) NULL
);
356 perr("Exec failed for mail command `%s'", _PATH_SENDMAIL
);
363 /* Global functions */
366 main(int argc
, char *argv
[])
369 * Browse through _PATH_ATJOBS, checking all the jobfiles wether
370 * they should be executed and or deleted. The queue is coded into
371 * the first byte of the job filename, the date (in minutes since
372 * Eon) as a hex number in the following eight bytes, followed by
373 * a dot and a serial number. A file which has not been executed
374 * yet is denoted by its execute - bit set. For those files which
375 * are to be executed, run_file() is called, which forks off a
376 * child which takes care of I/O redirection, forks off another
377 * child for execution and yet another one, optionally, for sending
378 * mail. Files which already have run are removed during the
382 struct dirent
*dirent
;
387 time_t now
, run_time
;
388 char batch_name
[] = "Z2345678901234";
393 double la
, load_avg
= ATRUN_MAXLOAD
;
397 openlog("atrun", LOG_PID
, LOG_CRON
);
399 if ((grp
= getgrnam(nobody
)) == NULL
)
400 perrx("Cannot get gid for `%s'", nobody
);
402 if ((pwd
= getpwnam(nobody
)) == NULL
)
403 perrx("Cannot get uid for `%s'", nobody
);
406 * We don't need root privileges all the time; running under uid
407 * and gid nobody is fine except for privileged operations.
409 RELINQUISH_PRIVS_ROOT(pwd
->pw_uid
, grp
->gr_gid
);
413 while ((c
= getopt(argc
, argv
, "dl:")) != -1) {
416 if (sscanf(optarg
, "%lf", &load_avg
) != 1)
417 perrx("Bad argument to option -l: ", optarg
);
419 load_avg
= ATRUN_MAXLOAD
;
427 perrx("Usage: %s [-l <loadav>] [-d]", getprogname());
431 perrx("Invalid option: %c", c
);
438 if (chdir(_PATH_ATJOBS
) == -1)
439 perr("Cannot change directory to `%s'", _PATH_ATJOBS
);
442 * Main loop. Open spool directory for reading and look over all
443 * the files in there. If the filename indicates that the job
444 * should be run and the x bit is set, fork off a child which sets
445 * its user and group id to that of the files and exec a /bin/sh
446 * which executes the shell script. Unlink older files if they
447 * should no longer be run. For deletion, their r bit has to be
450 * Also, pick the oldest batch job to run, at most one per
451 * invocation of atrun.
453 if ((spool
= opendir(".")) == NULL
)
454 perr("Cannot open `%s'", _PATH_ATJOBS
);
460 batch_uid
= (uid_t
) -1;
461 batch_gid
= (gid_t
) -1;
463 while ((dirent
= readdir(spool
)) != NULL
) {
466 if (stat(dirent
->d_name
, &buf
) == -1)
467 perr("Cannot stat `%s' in `%s'", dirent
->d_name
,
472 /* We don't want directories */
473 if (!S_ISREG(buf
.st_mode
))
476 if (sscanf(dirent
->d_name
, "%c%5x%8lx", &queue
, &jobno
,
480 run_time
= (time_t) ctm
* 60;
482 if ((S_IXUSR
& buf
.st_mode
) && (run_time
<= now
)) {
483 if (isupper((unsigned char)queue
) &&
484 (strcmp(batch_name
, dirent
->d_name
) > 0)) {
486 (void)strlcpy(batch_name
, dirent
->d_name
,
488 batch_uid
= buf
.st_uid
;
489 batch_gid
= buf
.st_gid
;
492 /* The file is executable and old enough */
493 if (islower((unsigned char)queue
))
494 run_file(dirent
->d_name
, buf
.st_uid
,
498 /* Delete older files */
499 if ((run_time
< now
) && !(S_IXUSR
& buf
.st_mode
) &&
500 (S_IRUSR
& buf
.st_mode
)) {
503 (void)unlink(dirent
->d_name
);
509 /* Run the single batch file, if any */
510 if (run_batch
&& ((getloadavg(&la
, 1) == 1) && la
< load_avg
))
511 run_file(batch_name
, batch_uid
, batch_gid
);