GCC Code Coverage Report
Directory: ./ Exec Total Coverage
File: usr.sbin/cron/atrun.c Lines: 0 276 0.0 %
Date: 2017-11-07 Branches: 0 212 0.0 %

Line Branch Exec Source
1
/*	$OpenBSD: atrun.c,v 1.46 2017/06/08 16:23:39 millert Exp $	*/
2
3
/*
4
 * Copyright (c) 2002-2003 Todd C. Miller <Todd.Miller@courtesan.com>
5
 *
6
 * Permission to use, copy, modify, and distribute this software for any
7
 * purpose with or without fee is hereby granted, provided that the above
8
 * copyright notice and this permission notice appear in all copies.
9
 *
10
 * THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES
11
 * WITH REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF
12
 * MERCHANTABILITY AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR
13
 * ANY SPECIAL, DIRECT, INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES
14
 * WHATSOEVER RESULTING FROM LOSS OF USE, DATA OR PROFITS, WHETHER IN AN
15
 * ACTION OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT OF
16
 * OR IN CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE.
17
 *
18
 * Sponsored in part by the Defense Advanced Research Projects
19
 * Agency (DARPA) and Air Force Research Laboratory, Air Force
20
 * Materiel Command, USAF, under agreement number F39502-99-1-0512.
21
 */
22
23
#include <sys/types.h>
24
#include <sys/resource.h>
25
#include <sys/stat.h>
26
#include <sys/time.h>
27
#include <sys/wait.h>
28
29
#include <bitstring.h>		/* for structs.h */
30
#include <bsd_auth.h>
31
#include <ctype.h>
32
#include <dirent.h>
33
#include <err.h>
34
#include <errno.h>
35
#include <fcntl.h>
36
#include <limits.h>
37
#include <login_cap.h>
38
#include <pwd.h>
39
#include <signal.h>
40
#include <stdio.h>
41
#include <stdlib.h>
42
#include <string.h>
43
#include <syslog.h>
44
#include <time.h>
45
#include <unistd.h>
46
47
#include "config.h"
48
#include "pathnames.h"
49
#include "macros.h"
50
#include "structs.h"
51
#include "funcs.h"
52
#include "globals.h"
53
54
static void run_job(const atjob *, int, const char *);
55
56
static int
57
strtot(const char *nptr, char **endptr, time_t *tp)
58
{
59
	long long ll;
60
61
	errno = 0;
62
	ll = strtoll(nptr, endptr, 10);
63
	if (*endptr == nptr)
64
		return (-1);
65
	if (ll < 0 || (errno == ERANGE && ll == LLONG_MAX) || (time_t)ll != ll)
66
		return (-1);
67
	*tp = (time_t)ll;
68
	return (0);
69
}
70
71
/*
72
 * Scan the at jobs dir and build up a list of jobs found.
73
 */
74
int
75
scan_atjobs(at_db **db, struct timespec *ts)
76
{
77
	DIR *atdir = NULL;
78
	int dfd, queue, pending;
79
	time_t run_time;
80
	char *ep;
81
	at_db *new_db, *old_db = *db;
82
	atjob *job;
83
	struct dirent *file;
84
	struct stat sb;
85
86
	if ((dfd = open(_PATH_AT_SPOOL, O_RDONLY|O_DIRECTORY)) == -1) {
87
		syslog(LOG_ERR, "(CRON) OPEN FAILED (%s)", _PATH_AT_SPOOL);
88
		return (0);
89
	}
90
	if (fstat(dfd, &sb) != 0) {
91
		syslog(LOG_ERR, "(CRON) FSTAT FAILED (%s)", _PATH_AT_SPOOL);
92
		close(dfd);
93
		return (0);
94
	}
95
	if (old_db != NULL && timespeccmp(&old_db->mtime, &sb.st_mtim, ==)) {
96
		close(dfd);
97
		return (0);
98
	}
99
100
	if ((atdir = fdopendir(dfd)) == NULL) {
101
		syslog(LOG_ERR, "(CRON) OPENDIR FAILED (%s)", _PATH_AT_SPOOL);
102
		close(dfd);
103
		return (0);
104
	}
105
106
	if ((new_db = malloc(sizeof(*new_db))) == NULL) {
107
		closedir(atdir);
108
		return (0);
109
	}
110
	new_db->mtime = sb.st_mtim;	/* stash at dir mtime */
111
	TAILQ_INIT(&new_db->jobs);
112
113
	pending = 0;
114
	while ((file = readdir(atdir)) != NULL) {
115
		if (fstatat(dfd, file->d_name, &sb, AT_SYMLINK_NOFOLLOW) != 0 ||
116
		    !S_ISREG(sb.st_mode))
117
			continue;
118
119
		/*
120
		 * at jobs are named as RUNTIME.QUEUE
121
		 * RUNTIME is the time to run in seconds since the epoch
122
		 * QUEUE is a letter that designates the job's queue
123
		 */
124
		if (strtot(file->d_name, &ep, &run_time) == -1)
125
			continue;
126
		if (ep[0] != '.' || !isalpha((unsigned char)ep[1]))
127
			continue;
128
		queue = (unsigned char)ep[1];
129
130
		job = malloc(sizeof(*job));
131
		if (job == NULL) {
132
			while ((job = TAILQ_FIRST(&new_db->jobs))) {
133
				TAILQ_REMOVE(&new_db->jobs, job, entries);
134
				free(job);
135
			}
136
			free(new_db);
137
			closedir(atdir);
138
			return (0);
139
		}
140
		job->uid = sb.st_uid;
141
		job->gid = sb.st_gid;
142
		job->queue = queue;
143
		job->run_time = run_time;
144
		TAILQ_INSERT_TAIL(&new_db->jobs, job, entries);
145
		if (ts != NULL && run_time <= ts->tv_sec)
146
			pending = 1;
147
	}
148
	closedir(atdir);
149
150
	/* Free up old at db and install new one */
151
	if (old_db != NULL) {
152
		while ((job = TAILQ_FIRST(&old_db->jobs))) {
153
			TAILQ_REMOVE(&old_db->jobs, job, entries);
154
			free(job);
155
		}
156
		free(old_db);
157
	}
158
	*db = new_db;
159
160
	return (pending);
161
}
162
163
/*
164
 * Loop through the at job database and run jobs whose time have come.
165
 */
166
void
167
atrun(at_db *db, double batch_maxload, time_t now)
168
{
169
	char atfile[PATH_MAX];
170
	struct stat sb;
171
	double la;
172
	int dfd, len;
173
	atjob *job, *tjob, *batch = NULL;
174
175
	if (db == NULL)
176
		return;
177
178
	if ((dfd = open(_PATH_AT_SPOOL, O_RDONLY|O_DIRECTORY)) == -1) {
179
		syslog(LOG_ERR, "(CRON) OPEN FAILED (%s)", _PATH_AT_SPOOL);
180
		return;
181
	}
182
183
	TAILQ_FOREACH_SAFE(job, &db->jobs, entries, tjob) {
184
		/* Skip jobs in the future */
185
		if (job->run_time > now)
186
			continue;
187
188
		len = snprintf(atfile, sizeof(atfile), "%lld.%c",
189
		    (long long)job->run_time, job->queue);
190
		if (len >= sizeof(atfile)) {
191
			TAILQ_REMOVE(&db->jobs, job, entries);
192
			free(job);
193
			continue;
194
		}
195
196
		if (fstatat(dfd, atfile, &sb, AT_SYMLINK_NOFOLLOW) != 0) {
197
			TAILQ_REMOVE(&db->jobs, job, entries);
198
			free(job);
199
			continue;		/* disapeared from queue */
200
		}
201
		if (!S_ISREG(sb.st_mode)) {
202
			syslog(LOG_WARNING, "(CRON) NOT REGULAR (%s)",
203
			    atfile);
204
			TAILQ_REMOVE(&db->jobs, job, entries);
205
			free(job);
206
			continue;		/* was a file, no longer is */
207
		}
208
209
		/*
210
		 * Pending jobs have the user execute bit set.
211
		 */
212
		if (sb.st_mode & S_IXUSR) {
213
			/* new job to run */
214
			if (isupper(job->queue)) {
215
				/* we run one batch job per atrun() call */
216
				if (batch == NULL ||
217
				    job->run_time < batch->run_time)
218
					batch = job;
219
			} else {
220
				/* normal at job */
221
				run_job(job, dfd, atfile);
222
				TAILQ_REMOVE(&db->jobs, job, entries);
223
				free(job);
224
			}
225
		}
226
	}
227
228
	/* Run a single batch job if there is one pending. */
229
	if (batch != NULL
230
	    && (batch_maxload == 0.0 ||
231
	    ((getloadavg(&la, 1) == 1) && la <= batch_maxload))
232
	    ) {
233
		len = snprintf(atfile, sizeof(atfile), "%lld.%c",
234
		    (long long)batch->run_time, batch->queue);
235
		if (len < sizeof(atfile))
236
			run_job(batch, dfd, atfile);
237
		TAILQ_REMOVE(&db->jobs, batch, entries);
238
		free(job);
239
	}
240
241
	close(dfd);
242
}
243
244
/*
245
 * Run the specified job contained in atfile.
246
 */
247
static void
248
run_job(const atjob *job, int dfd, const char *atfile)
249
{
250
	struct stat sb;
251
	struct passwd *pw;
252
	login_cap_t *lc;
253
	auth_session_t *as;
254
	pid_t pid;
255
	long nuid, ngid;
256
	FILE *fp;
257
	int waiter;
258
	size_t nread;
259
	char *cp, *ep, mailto[MAX_UNAME], buf[BUFSIZ];
260
	int fd, always_mail;
261
	int output_pipe[2];
262
	char *nargv[2], *nenvp[1];
263
264
	/* Open the file and unlink it so we don't try running it again. */
265
	if ((fd = openat(dfd, atfile, O_RDONLY|O_NONBLOCK|O_NOFOLLOW, 0)) < 0) {
266
		syslog(LOG_ERR, "(CRON) CAN'T OPEN (%s)", atfile);
267
		return;
268
	}
269
	unlinkat(dfd, atfile, 0);
270
271
	/* Fork so other pending jobs don't have to wait for us to finish. */
272
	switch (fork()) {
273
	case 0:
274
		/* child */
275
		break;
276
	case -1:
277
		/* error */
278
		syslog(LOG_ERR, "(CRON) CAN'T FORK (%m)");
279
		/* FALLTHROUGH */
280
	default:
281
		/* parent */
282
		close(fd);
283
		return;
284
	}
285
286
	/*
287
	 * We don't want the main cron daemon to wait for our children--
288
	 * we will do it ourselves via waitpid().
289
	 */
290
	(void) signal(SIGCHLD, SIG_DFL);
291
292
	/*
293
	 * Verify the user still exists and their account has not expired.
294
	 */
295
	pw = getpwuid(job->uid);
296
	if (pw == NULL) {
297
		syslog(LOG_WARNING, "(CRON) ORPHANED JOB (%s)", atfile);
298
		_exit(EXIT_FAILURE);
299
	}
300
	if (pw->pw_expire && time(NULL) >= pw->pw_expire) {
301
		syslog(LOG_NOTICE, "(%s) ACCOUNT EXPIRED, JOB ABORTED (%s)",
302
		    pw->pw_name, atfile);
303
		_exit(EXIT_FAILURE);
304
	}
305
306
	/* Sanity checks */
307
	if (fstat(fd, &sb) < 0) {
308
		syslog(LOG_ERR, "(%s) FSTAT FAILED (%s)", pw->pw_name, atfile);
309
		_exit(EXIT_FAILURE);
310
	}
311
	if (!S_ISREG(sb.st_mode)) {
312
		syslog(LOG_WARNING, "(%s) NOT REGULAR (%s)", pw->pw_name,
313
		    atfile);
314
		_exit(EXIT_FAILURE);
315
	}
316
	if ((sb.st_mode & ALLPERMS) != (S_IRUSR | S_IWUSR | S_IXUSR)) {
317
		syslog(LOG_WARNING, "(%s) BAD FILE MODE (%s)", pw->pw_name,
318
		    atfile);
319
		_exit(EXIT_FAILURE);
320
	}
321
	if (sb.st_uid != 0 && sb.st_uid != job->uid) {
322
		syslog(LOG_WARNING, "(%s) WRONG FILE OWNER (%s)", pw->pw_name,
323
		    atfile);
324
		_exit(EXIT_FAILURE);
325
	}
326
	if (sb.st_gid != cron_gid) {
327
		syslog(LOG_WARNING, "(%s) WRONG FILE GROUP (%s)", pw->pw_name,
328
		    atfile);
329
		_exit(EXIT_FAILURE);
330
	}
331
	if (sb.st_nlink > 1) {
332
		syslog(LOG_WARNING, "(%s) BAD LINK COUNT (%s)", pw->pw_name,
333
		    atfile);
334
		_exit(EXIT_FAILURE);
335
	}
336
337
	if ((fp = fdopen(dup(fd), "r")) == NULL) {
338
		syslog(LOG_ERR, "(CRON) DUP FAILED (%m)");
339
		_exit(EXIT_FAILURE);
340
	}
341
342
	/*
343
	 * Check the at job header for sanity and extract the
344
	 * uid, gid, mailto user and always_mail flag.
345
	 *
346
	 * The header should look like this:
347
	 * #!/bin/sh
348
	 * # atrun uid=123 gid=123
349
	 * # mail                         joeuser 0
350
	 */
351
	if (fgets(buf, sizeof(buf), fp) == NULL ||
352
	    strcmp(buf, "#!/bin/sh\n") != 0 ||
353
	    fgets(buf, sizeof(buf), fp) == NULL ||
354
	    strncmp(buf, "# atrun uid=", 12) != 0)
355
		goto bad_file;
356
357
	/* Pull out uid */
358
	cp = buf + 12;
359
	errno = 0;
360
	nuid = strtol(cp, &ep, 10);
361
	if (errno == ERANGE || (uid_t)nuid > UID_MAX || cp == ep ||
362
	    strncmp(ep, " gid=", 5) != 0)
363
		goto bad_file;
364
365
	/* Pull out gid */
366
	cp = ep + 5;
367
	errno = 0;
368
	ngid = strtol(cp, &ep, 10);
369
	if (errno == ERANGE || (gid_t)ngid > GID_MAX || cp == ep || *ep != '\n')
370
		goto bad_file;
371
372
	/* Pull out mailto user (and always_mail flag) */
373
	if (fgets(buf, sizeof(buf), fp) == NULL ||
374
	    strncmp(buf, "# mail ", 7) != 0)
375
		goto bad_file;
376
	cp = buf + 7;
377
	while (isspace((unsigned char)*cp))
378
		cp++;
379
	ep = cp;
380
	while (!isspace((unsigned char)*ep) && *ep != '\0')
381
		ep++;
382
	if (*ep == '\0' || *ep != ' ' || ep - cp >= sizeof(mailto))
383
		goto bad_file;
384
	memcpy(mailto, cp, ep - cp);
385
	mailto[ep - cp] = '\0';
386
	always_mail = ep[1] == '1';
387
388
	(void)fclose(fp);
389
	if (!safe_p(pw->pw_name, mailto))
390
		_exit(EXIT_FAILURE);
391
	if ((uid_t)nuid != job->uid) {
392
		syslog(LOG_WARNING, "(%s) UID MISMATCH (%s)", pw->pw_name,
393
		    atfile);
394
		_exit(EXIT_FAILURE);
395
	}
396
	if ((gid_t)ngid != job->gid) {
397
		syslog(LOG_WARNING, "(%s) GID MISMATCH (%s)", pw->pw_name,
398
		    atfile);
399
		_exit(EXIT_FAILURE);
400
	}
401
402
	/* mark ourselves as different to PS command watchers */
403
	setproctitle("atrun %s", atfile);
404
405
	if (pipe(output_pipe) != 0) {	/* child's stdout/stderr */
406
		syslog(LOG_ERR, "(CRON) PIPE (%m)");
407
		_exit(EXIT_FAILURE);
408
	}
409
410
	/* Fork again, child will run the job, parent will catch output. */
411
	switch ((pid = fork())) {
412
	case -1:
413
		syslog(LOG_ERR, "(CRON) CAN'T FORK (%m)");
414
		_exit(EXIT_FAILURE);
415
		/*NOTREACHED*/
416
	case 0:
417
		/* Write log message now that we have our real pid. */
418
		syslog(LOG_INFO, "(%s) ATJOB (%s)", pw->pw_name, atfile);
419
420
		/* Connect grandchild's stdin to the at job file. */
421
		if (lseek(fd, 0, SEEK_SET) < 0) {
422
			syslog(LOG_ERR, "(CRON) LSEEK (%m)");
423
			_exit(EXIT_FAILURE);
424
		}
425
		if (fd != STDIN_FILENO) {
426
			dup2(fd, STDIN_FILENO);
427
			close(fd);
428
		}
429
430
		/* Connect stdout/stderr to the pipe from our parent. */
431
		if (output_pipe[WRITE_PIPE] != STDOUT_FILENO) {
432
			dup2(output_pipe[WRITE_PIPE], STDOUT_FILENO);
433
			close(output_pipe[WRITE_PIPE]);
434
		}
435
		dup2(STDOUT_FILENO, STDERR_FILENO);
436
		close(output_pipe[READ_PIPE]);
437
438
		(void) setsid();
439
440
		/*
441
		 * From this point on, anything written to stderr will be
442
		 * mailed to the user as output.
443
		 */
444
445
		/* Setup execution environment as per login.conf */
446
		if ((lc = login_getclass(pw->pw_class)) == NULL) {
447
			warnx("unable to get login class for %s",
448
			    pw->pw_name);
449
			syslog(LOG_ERR, "(CRON) CAN'T GET LOGIN CLASS (%s)",
450
			    pw->pw_name);
451
			_exit(EXIT_FAILURE);
452
453
		}
454
		if (setusercontext(lc, pw, pw->pw_uid, LOGIN_SETALL)) {
455
			warn("setusercontext failed for %s", pw->pw_name);
456
			syslog(LOG_ERR, "(%s) SETUSERCONTEXT FAILED (%m)",
457
			    pw->pw_name);
458
			_exit(EXIT_FAILURE);
459
		}
460
461
		/* Run any approval scripts. */
462
		as = auth_open();
463
		if (as == NULL || auth_setpwd(as, pw) != 0) {
464
			warn("auth_setpwd");
465
			syslog(LOG_ERR, "(%s) AUTH_SETPWD FAILED (%m)",
466
			    pw->pw_name);
467
			_exit(EXIT_FAILURE);
468
		}
469
		if (auth_approval(as, lc, pw->pw_name, "cron") <= 0) {
470
			warnx("approval failed for %s", pw->pw_name);
471
			syslog(LOG_ERR, "(%s) APPROVAL FAILED (cron)",
472
			    pw->pw_name);
473
			_exit(EXIT_FAILURE);
474
		}
475
		auth_close(as);
476
		login_close(lc);
477
478
		/* If this is a low priority job, nice ourself. */
479
		if (job->queue > 'b') {
480
			if (setpriority(PRIO_PROCESS, 0, job->queue - 'b') != 0)
481
				syslog(LOG_ERR, "(%s) CAN'T NICE (%m)",
482
				    pw->pw_name);
483
		}
484
485
		(void) signal(SIGPIPE, SIG_DFL);
486
487
		/*
488
		 * Exec /bin/sh with stdin connected to the at job file
489
		 * and stdout/stderr hooked up to our parent.
490
		 * The at file will set the environment up for us.
491
		 */
492
		nargv[0] = "sh";
493
		nargv[1] = NULL;
494
		nenvp[0] = NULL;
495
		if (execve(_PATH_BSHELL, nargv, nenvp) != 0) {
496
			warn("unable to execute %s", _PATH_BSHELL);
497
			syslog(LOG_ERR, "(%s) CAN'T EXEC (%s: %m)", pw->pw_name,
498
			    _PATH_BSHELL);
499
			_exit(EXIT_FAILURE);
500
		}
501
		break;
502
	default:
503
		/* parent */
504
		break;
505
	}
506
507
	/* Close the atfile's fd and the end of the pipe we don't use. */
508
	close(fd);
509
	close(output_pipe[WRITE_PIPE]);
510
511
	/* Read piped output (if any) from the at job. */
512
	if ((fp = fdopen(output_pipe[READ_PIPE], "r")) == NULL) {
513
		syslog(LOG_ERR, "(%s) FDOPEN (%m)", pw->pw_name);
514
		(void) _exit(EXIT_FAILURE);
515
	}
516
	nread = fread(buf, 1, sizeof(buf), fp);
517
	if (nread != 0 || always_mail) {
518
		FILE	*mail;
519
		pid_t	mailpid;
520
		size_t	bytes = 0;
521
		int	status = 0;
522
		char	mailcmd[MAX_COMMAND];
523
		char	hostname[HOST_NAME_MAX + 1];
524
525
		if (gethostname(hostname, sizeof(hostname)) != 0)
526
			strlcpy(hostname, "unknown", sizeof(hostname));
527
		if (snprintf(mailcmd, sizeof mailcmd, MAILFMT,
528
		    MAILARG) >= sizeof mailcmd) {
529
			syslog(LOG_ERR, "(%s) ERROR (mailcmd too long)",
530
			    pw->pw_name);
531
			(void) _exit(EXIT_FAILURE);
532
		}
533
		if (!(mail = cron_popen(mailcmd, "w", pw, &mailpid))) {
534
			syslog(LOG_ERR, "(%s) POPEN (%s)", pw->pw_name, mailcmd);
535
			(void) _exit(EXIT_FAILURE);
536
		}
537
		fprintf(mail, "From: %s (Atrun Service)\n", pw->pw_name);
538
		fprintf(mail, "To: %s\n", mailto);
539
		fprintf(mail, "Subject: Output from \"at\" job\n");
540
		fprintf(mail, "Auto-Submitted: auto-generated\n");
541
		fprintf(mail, "\nYour \"at\" job on %s\n\"%s/%s\"\n",
542
		    hostname, _PATH_AT_SPOOL, atfile);
543
		fprintf(mail, "\nproduced the following output:\n\n");
544
545
		/* Pipe the job's output to sendmail. */
546
		do {
547
			bytes += nread;
548
			fwrite(buf, nread, 1, mail);
549
		} while ((nread = fread(buf, 1, sizeof(buf), fp)) != 0);
550
551
		/*
552
		 * If the mailer exits with non-zero exit status, log
553
		 * this fact so the problem can (hopefully) be debugged.
554
		 */
555
		if ((status = cron_pclose(mail, mailpid)) != 0) {
556
			syslog(LOG_NOTICE, "(%s) MAIL (mailed %zu byte%s of "
557
			    "output but got status 0x%04x)", pw->pw_name,
558
			    bytes, (bytes == 1) ? "" : "s", status);
559
		}
560
	}
561
562
	fclose(fp);	/* also closes output_pipe[READ_PIPE] */
563
564
	/* Wait for grandchild to die.  */
565
	for (;;) {
566
		if (waitpid(pid, &waiter, 0) == -1) {
567
			if (errno == EINTR)
568
				continue;
569
			break;
570
		} else {
571
			/*
572
			if (WIFSIGNALED(waiter) && WCOREDUMP(waiter))
573
				Debug(DPROC, (", dumped core"))
574
			*/
575
			break;
576
		}
577
	}
578
	_exit(EXIT_SUCCESS);
579
580
bad_file:
581
	syslog(LOG_ERR, "(%s) BAD FILE FORMAT (%s)", pw->pw_name, atfile);
582
	_exit(EXIT_FAILURE);
583
}