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

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