atrun.c revision 10154
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/* 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#ifdef __FreeBSD__ 46#include <paths.h> 47#else 48#include <getopt.h> 49#endif 50 51/* Local headers */ 52 53#include "gloadavg.h" 54#define MAIN 55#include "privs.h" 56 57/* Macros */ 58 59#ifndef ATJOB_DIR 60#define ATJOB_DIR "/usr/spool/atjobs/" 61#endif 62 63#ifndef ATSPOOL_DIR 64#define ATSPOOL_DIR "/usr/spool/atspool/" 65#endif 66 67#ifndef LOADAVG_MX 68#define LOADAVG_MX 1.5 69#endif 70 71/* File scope variables */ 72 73static char *namep; 74static char rcsid[] = "$Id: atrun.c,v 1.5 1995/08/10 04:06:53 ache Exp $"; 75static debug = 0; 76 77void perr(const char *a); 78 79/* Local functions */ 80static int 81write_string(int fd, const char* a) 82{ 83 return write(fd, a, strlen(a)); 84} 85 86#undef DEBUG_FORK 87#ifdef DEBUG_FORK 88static pid_t 89myfork() 90{ 91 pid_t res; 92 res = fork(); 93 if (res == 0) 94 kill(getpid(),SIGSTOP); 95 return res; 96} 97 98#define fork myfork 99#endif 100 101static void 102run_file(const char *filename, uid_t uid, gid_t gid) 103{ 104/* Run a file by by spawning off a process which redirects I/O, 105 * spawns a subshell, then waits for it to complete and sends 106 * mail to the user. 107 */ 108 pid_t pid; 109 int fd_out, fd_in; 110 int queue; 111 char mailbuf[9]; 112 char *mailname = NULL; 113 FILE *stream; 114 int send_mail = 0; 115 struct stat buf, lbuf; 116 off_t size; 117 struct passwd *pentry; 118 int fflags; 119 long nuid; 120 long ngid; 121 122 123 PRIV_START 124 125 if (chmod(filename, S_IRUSR) != 0) 126 { 127 perr("Cannot change file permissions"); 128 } 129 130 PRIV_END 131 132 pid = fork(); 133 if (pid == -1) 134 perr("Cannot fork"); 135 136 else if (pid != 0) 137 return; 138 139 /* Let's see who we mail to. Hopefully, we can read it from 140 * the command file; if not, send it to the owner, or, failing that, 141 * to root. 142 */ 143 144 pentry = getpwuid(uid); 145 if (pentry == NULL) 146 { 147 syslog(LOG_ERR,"Userid %lu not found - aborting job %s", 148 (unsigned long) uid, filename); 149 exit(EXIT_FAILURE); 150 } 151 PRIV_START 152 153 stream=fopen(filename, "r"); 154 155 PRIV_END 156 157 if (stream == NULL) 158 perr("Cannot open input file"); 159 160 if ((fd_in = dup(fileno(stream))) <0) 161 perr("Error duplicating input file descriptor"); 162 163 if (fstat(fd_in, &buf) == -1) 164 perr("Error in fstat of input file descriptor"); 165 166 if (lstat(filename, &lbuf) == -1) 167 perr("Error in fstat of input file"); 168 169 if (S_ISLNK(lbuf.st_mode)) { 170 syslog(LOG_ERR,"Symbolic link encountered in job %s - aborting", 171 filename); 172 exit(EXIT_FAILURE); 173 } 174 if ((lbuf.st_dev != buf.st_dev) || (lbuf.st_ino != buf.st_ino) || 175 (lbuf.st_uid != buf.st_uid) || (lbuf.st_gid != buf.st_gid) || 176 (lbuf.st_size!=buf.st_size)) { 177 syslog(LOG_ERR,"Somebody changed files from under us for job %s - " 178 "aborting",filename); 179 exit(EXIT_FAILURE); 180 } 181 if (buf.st_nlink > 1) { 182 syslog(LOG_ERR,"Someboy is trying to run a linked script for job %s", 183 filename); 184 exit(EXIT_FAILURE); 185 } 186 if ((fflags = fcntl(fd_in, F_GETFD)) <0) 187 perr("Error in fcntl"); 188 189 fcntl(fd_in, F_SETFD, fflags & ~FD_CLOEXEC); 190 191 if (fscanf(stream, "#!/bin/sh\n# atrun uid=%ld gid=%ld\n# mail %8s %d", 192 &nuid, &ngid, mailbuf, &send_mail) != 4) 193 { 194 syslog(LOG_ERR,"File %s is in wrong format - aborting", 195 filename); 196 exit(EXIT_FAILURE); 197 } 198 if (mailbuf[0] == '-') { 199 syslog(LOG_ERR,"illegal mail name %s in %s",mailbuf,filename); 200 exit(EXIT_FAILURE); 201 } 202 mailname = mailbuf; 203 if (nuid != uid) { 204 syslog(LOG_ERR,"Job %s - userid %d does not match file uid %d", 205 filename, nuid, uid); 206 exit(EXIT_FAILURE); 207 } 208 if (ngid != gid) { 209 syslog(LOG_ERR,"Job %s - groupid %d does not match file gid %d", 210 filename, ngid, gid); 211 exit(EXIT_FAILURE); 212 } 213 fclose(stream); 214 if (chdir(ATSPOOL_DIR) < 0) 215 perr("Cannot chdir to " ATSPOOL_DIR); 216 217 /* Create a file to hold the output of the job we are about to run. 218 * Write the mail header. 219 */ 220 if((fd_out=open(filename, 221 O_WRONLY | O_CREAT | O_EXCL, S_IWUSR | S_IRUSR)) < 0) 222 perr("Cannot create output file"); 223 224 write_string(fd_out, "Subject: Output from your job "); 225 write_string(fd_out, filename); 226 write_string(fd_out, "\n\n"); 227 fstat(fd_out, &buf); 228 size = buf.st_size; 229 230 close(STDIN_FILENO); 231 close(STDOUT_FILENO); 232 close(STDERR_FILENO); 233 234 pid = fork(); 235 if (pid < 0) 236 perr("Error in fork"); 237 238 else if (pid == 0) 239 { 240 char *nul = NULL; 241 char **nenvp = &nul; 242 243 /* Set up things for the child; we want standard input from the input file, 244 * and standard output and error sent to our output file. 245 */ 246 247 if (lseek(fd_in, (off_t) 0, SEEK_SET) < 0) 248 perr("Error in lseek"); 249 250 if (dup(fd_in) != STDIN_FILENO) 251 perr("Error in I/O redirection"); 252 253 if (dup(fd_out) != STDOUT_FILENO) 254 perr("Error in I/O redirection"); 255 256 if (dup(fd_out) != STDERR_FILENO) 257 perr("Error in I/O redirection"); 258 259 close(fd_in); 260 close(fd_out); 261 if (chdir(ATJOB_DIR) < 0) 262 perr("Cannot chdir to " ATJOB_DIR); 263 264 queue = *filename; 265 266 PRIV_START 267 268 nice(tolower(queue) - 'a'); 269 270 if (chdir(pentry->pw_dir)) 271 chdir("/"); 272 273 if (initgroups(pentry->pw_name,pentry->pw_gid)) 274 perr("Cannot delete saved userids"); 275 276 if (setgid(gid) < 0) 277 perr("Cannot change group"); 278 279 if (setuid(uid) < 0) 280 perr("Cannot set user id"); 281 282 if(execle("/bin/sh","sh",(char *) NULL, nenvp) != 0) 283 perr("Exec failed for /bin/sh"); 284 285 PRIV_END 286 } 287 /* We're the parent. Let's wait. 288 */ 289 close(fd_in); 290 close(fd_out); 291 waitpid(pid, (int *) NULL, 0); 292 293 /* Send mail. Unlink the output file first, so it is deleted after 294 * the run. 295 */ 296 stat(filename, &buf); 297 if (open(filename, O_RDONLY) != STDIN_FILENO) 298 perr("Open of jobfile failed"); 299 300 unlink(filename); 301 if ((buf.st_size != size) || send_mail) 302 { 303 PRIV_START 304 305 if (chdir(pentry->pw_dir)) 306 chdir("/"); 307 308 if (initgroups(pentry->pw_name,pentry->pw_gid)) 309 perr("Cannot delete saved userids"); 310 311 if (setgid(gid) < 0) 312 perr("Cannot change group"); 313 314 if (setuid(uid) < 0) 315 perr("Cannot set user id"); 316 317#ifdef __FreeBSD__ 318 execl(_PATH_SENDMAIL, "sendmail", "-F", "Atrun Service", 319 "-odi", "-oem", 320 mailname, (char *) NULL); 321#else 322 execl(MAIL_CMD, MAIL_CMD, mailname, (char *) NULL); 323#endif 324 perr("Exec failed for mail command"); 325 326 PRIV_END 327 } 328 exit(EXIT_SUCCESS); 329} 330 331/* Global functions */ 332 333/* Needed in gloadavg.c */ 334void 335perr(const char *a) 336{ 337 if (debug) 338 { 339 perror(a); 340 } 341 else 342 syslog(LOG_ERR, "%s: %m", a); 343 344 exit(EXIT_FAILURE); 345} 346 347int 348main(int argc, char *argv[]) 349{ 350/* Browse through ATJOB_DIR, checking all the jobfiles wether they should 351 * be executed and or deleted. The queue is coded into the first byte of 352 * the job filename, the date (in minutes since Eon) as a hex number in the 353 * following eight bytes, followed by a dot and a serial number. A file 354 * which has not been executed yet is denoted by its execute - bit set. 355 * For those files which are to be executed, run_file() is called, which forks 356 * off a child which takes care of I/O redirection, forks off another child 357 * for execution and yet another one, optionally, for sending mail. 358 * Files which already have run are removed during the next invocation. 359 */ 360 DIR *spool; 361 struct dirent *dirent; 362 struct stat buf; 363 unsigned long ctm; 364 unsigned long jobno; 365 char queue; 366 time_t now, run_time; 367 char batch_name[] = "Z2345678901234"; 368 uid_t batch_uid; 369 gid_t batch_gid; 370 int c; 371 int run_batch; 372 double load_avg = LOADAVG_MX; 373 374/* We don't need root privileges all the time; running under uid and gid daemon 375 * is fine. 376 */ 377 378 RELINQUISH_PRIVS_ROOT(DAEMON_UID, DAEMON_GID) 379 380 openlog("atrun", LOG_PID, LOG_CRON); 381 382 opterr = 0; 383 errno = 0; 384 while((c=getopt(argc, argv, "dl:"))!= EOF) 385 { 386 switch (c) 387 { 388 case 'l': 389 if (sscanf(optarg, "%lf", &load_avg) != 1) 390 perr("garbled option -l"); 391 if (load_avg <= 0.) 392 load_avg = LOADAVG_MX; 393 break; 394 395 case 'd': 396 debug ++; 397 break; 398 399 case '?': 400 perr("unknown option"); 401 break; 402 403 default: 404 perr("idiotic option - aborted"); 405 break; 406 } 407 } 408 409 namep = argv[0]; 410 if (chdir(ATJOB_DIR) != 0) 411 perr("Cannot change to " ATJOB_DIR); 412 413 /* Main loop. Open spool directory for reading and look over all the 414 * files in there. If the filename indicates that the job should be run 415 * and the x bit is set, fork off a child which sets its user and group 416 * id to that of the files and exec a /bin/sh which executes the shell 417 * script. Unlink older files if they should no longer be run. For 418 * deletion, their r bit has to be turned on. 419 * 420 * Also, pick the oldest batch job to run, at most one per invocation of 421 * atrun. 422 */ 423 if ((spool = opendir(".")) == NULL) 424 perr("Cannot read " ATJOB_DIR); 425 426 now = time(NULL); 427 run_batch = 0; 428 batch_uid = (uid_t) -1; 429 batch_gid = (gid_t) -1; 430 431 while ((dirent = readdir(spool)) != NULL) { 432 if (stat(dirent->d_name,&buf) != 0) 433 perr("Cannot stat in " ATJOB_DIR); 434 435 /* We don't want directories 436 */ 437 if (!S_ISREG(buf.st_mode)) 438 continue; 439 440 if (sscanf(dirent->d_name,"%c%5lx%8lx",&queue,&jobno,&ctm) != 3) 441 continue; 442 443 run_time = (time_t) ctm*60; 444 445 if ((S_IXUSR & buf.st_mode) && (run_time <=now)) { 446 if (isupper(queue) && (strcmp(batch_name,dirent->d_name) > 0)) { 447 run_batch = 1; 448 strncpy(batch_name, dirent->d_name, sizeof(batch_name)); 449 batch_uid = buf.st_uid; 450 batch_gid = buf.st_gid; 451 } 452 453 /* The file is executable and old enough 454 */ 455 if (islower(queue)) 456 run_file(dirent->d_name, buf.st_uid, buf.st_gid); 457 } 458 /* Delete older files 459 */ 460 if ((run_time < now) && !(S_IXUSR & buf.st_mode) && (S_IRUSR & buf.st_mode)) 461 unlink(dirent->d_name); 462 } 463 /* run the single batch file, if any 464 */ 465 if (run_batch && (gloadavg() < load_avg)) 466 run_file(batch_name, batch_uid, batch_gid); 467 468 closelog(); 469 exit(EXIT_SUCCESS); 470} 471