xref: /openbsd-src/usr.sbin/cron/database.c (revision df69c215c7c66baf660f3f65414fd34796c96152)
1 /*	$OpenBSD: database.c,v 1.38 2019/06/28 13:32:47 deraadt Exp $	*/
2 
3 /* Copyright 1988,1990,1993,1994 by Paul Vixie
4  * Copyright (c) 2004 by Internet Systems Consortium, Inc. ("ISC")
5  * Copyright (c) 1997,2000 by Internet Software Consortium, Inc.
6  *
7  * Permission to use, copy, modify, and distribute this software for any
8  * purpose with or without fee is hereby granted, provided that the above
9  * copyright notice and this permission notice appear in all copies.
10  *
11  * THE SOFTWARE IS PROVIDED "AS IS" AND ISC DISCLAIMS ALL WARRANTIES
12  * WITH REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF
13  * MERCHANTABILITY AND FITNESS.  IN NO EVENT SHALL ISC BE LIABLE FOR
14  * ANY SPECIAL, DIRECT, INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES
15  * WHATSOEVER RESULTING FROM LOSS OF USE, DATA OR PROFITS, WHETHER IN AN
16  * ACTION OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT
17  * OF OR IN CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE.
18  */
19 
20 #include <sys/types.h>
21 #include <sys/stat.h>
22 #include <sys/time.h>
23 
24 #include <bitstring.h>		/* for structs.h */
25 #include <dirent.h>
26 #include <fcntl.h>
27 #include <limits.h>
28 #include <pwd.h>
29 #include <stdio.h>
30 #include <stdlib.h>
31 #include <string.h>
32 #include <syslog.h>
33 #include <time.h>		/* for structs.h */
34 #include <unistd.h>
35 
36 #include "pathnames.h"
37 #include "globals.h"
38 #include "macros.h"
39 #include "structs.h"
40 #include "funcs.h"
41 
42 #define HASH(a,b) ((a)+(b))
43 
44 static	void		process_crontab(int, const char *, const char *,
45 					struct stat *, cron_db *, cron_db *);
46 
47 void
load_database(cron_db ** db)48 load_database(cron_db **db)
49 {
50 	struct stat statbuf, syscron_stat;
51 	cron_db *new_db, *old_db = *db;
52 	struct timespec mtime;
53 	struct dirent *dp;
54 	DIR *dir;
55 	user *u;
56 
57 	/* before we start loading any data, do a stat on _PATH_CRON_SPOOL
58 	 * so that if anything changes as of this moment (i.e., before we've
59 	 * cached any of the database), we'll see the changes next time.
60 	 */
61 	if (stat(_PATH_CRON_SPOOL, &statbuf) == -1) {
62 		syslog(LOG_ERR, "(CRON) STAT FAILED (%s)", _PATH_CRON_SPOOL);
63 		return;
64 	}
65 
66 	/* track system crontab file
67 	 */
68 	if (stat(_PATH_SYS_CRONTAB, &syscron_stat) == -1)
69 		timespecclear(&syscron_stat.st_mtim);
70 
71 	/* hash mtime of system crontab file and crontab dir
72 	 */
73 	mtime.tv_sec =
74 	    HASH(statbuf.st_mtim.tv_sec, syscron_stat.st_mtim.tv_sec);
75 	mtime.tv_nsec =
76 	    HASH(statbuf.st_mtim.tv_nsec, syscron_stat.st_mtim.tv_nsec);
77 
78 	/* if spooldir's mtime has not changed, we don't need to fiddle with
79 	 * the database.
80 	 */
81 	if (old_db != NULL && timespeccmp(&mtime, &old_db->mtime, ==))
82 		return;
83 
84 	/* something's different.  make a new database, moving unchanged
85 	 * elements from the old database, reloading elements that have
86 	 * actually changed.  Whatever is left in the old database when
87 	 * we're done is chaff -- crontabs that disappeared.
88 	 */
89 	if ((new_db = malloc(sizeof(*new_db))) == NULL)
90 		return;
91 	new_db->mtime = mtime;
92 	TAILQ_INIT(&new_db->users);
93 
94 	if (timespecisset(&syscron_stat.st_mtim)) {
95 		process_crontab(AT_FDCWD, "*system*", _PATH_SYS_CRONTAB,
96 				&syscron_stat, new_db, old_db);
97 	}
98 
99 	/* we used to keep this dir open all the time, for the sake of
100 	 * efficiency.  however, we need to close it in every fork, and
101 	 * we fork a lot more often than the mtime of the dir changes.
102 	 */
103 	if (!(dir = opendir(_PATH_CRON_SPOOL))) {
104 		syslog(LOG_ERR, "(CRON) OPENDIR FAILED (%s)", _PATH_CRON_SPOOL);
105 		/* Restore system crontab entry as needed. */
106 		if (!TAILQ_EMPTY(&new_db->users) &&
107 		    (u = TAILQ_FIRST(&old_db->users))) {
108 			if (strcmp(u->name, "*system*") == 0) {
109 				TAILQ_REMOVE(&old_db->users, u, entries);
110 				free_user(u);
111 				TAILQ_INSERT_HEAD(&old_db->users,
112 				    TAILQ_FIRST(&new_db->users), entries);
113 			}
114 		}
115 		free(new_db);
116 		return;
117 	}
118 
119 	while (NULL != (dp = readdir(dir))) {
120 		/* avoid file names beginning with ".".  this is good
121 		 * because we would otherwise waste two guaranteed calls
122 		 * to getpwnam() for . and .., and also because user names
123 		 * starting with a period are just too nasty to consider.
124 		 */
125 		if (dp->d_name[0] == '.')
126 			continue;
127 
128 		process_crontab(dirfd(dir), dp->d_name, dp->d_name,
129 				&statbuf, new_db, old_db);
130 	}
131 	closedir(dir);
132 
133 	/* if we don't do this, then when our children eventually call
134 	 * getpwnam() in do_command.c's child_process to verify MAILTO=,
135 	 * they will screw us up (and v-v).
136 	 */
137 	endpwent();
138 
139 	/* whatever's left in the old database is now junk.
140 	 */
141 	if (old_db != NULL) {
142 		while ((u = TAILQ_FIRST(&old_db->users))) {
143 			TAILQ_REMOVE(&old_db->users, u, entries);
144 			free_user(u);
145 		}
146 		free(old_db);
147 	}
148 
149 	/* overwrite the database control block with the new one.
150 	 */
151 	*db = new_db;
152 }
153 
154 user *
find_user(cron_db * db,const char * name)155 find_user(cron_db *db, const char *name)
156 {
157 	user *u = NULL;
158 
159 	if (db != NULL) {
160 		TAILQ_FOREACH(u, &db->users, entries) {
161 			if (strcmp(u->name, name) == 0)
162 				break;
163 		}
164 	}
165 	return (u);
166 }
167 
168 static void
process_crontab(int dfd,const char * uname,const char * fname,struct stat * statbuf,cron_db * new_db,cron_db * old_db)169 process_crontab(int dfd, const char *uname, const char *fname,
170 		struct stat *statbuf, cron_db *new_db, cron_db *old_db)
171 {
172 	struct passwd *pw = NULL;
173 	FILE *crontab_fp = NULL;
174 	user *u, *new_u;
175 	mode_t tabmask, tabperm;
176 	int fd;
177 
178 	/* Note: pw must remain NULL for system crontab (see below). */
179 	if (fname[0] != '/' && (pw = getpwnam(uname)) == NULL) {
180 		/* file doesn't have a user in passwd file.
181 		 */
182 		syslog(LOG_WARNING, "(%s) ORPHAN (no passwd entry)", uname);
183 		goto next_crontab;
184 	}
185 
186 	fd = openat(dfd, fname, O_RDONLY|O_NONBLOCK|O_NOFOLLOW|O_CLOEXEC);
187 	if (fd == -1) {
188 		/* crontab not accessible?
189 		 */
190 		syslog(LOG_ERR, "(%s) CAN'T OPEN (%s)", uname, fname);
191 		goto next_crontab;
192 	}
193 	if (!(crontab_fp = fdopen(fd, "r"))) {
194 		syslog(LOG_ERR, "(%s) FDOPEN (%m)", fname);
195 		close(fd);
196 		goto next_crontab;
197 	}
198 
199 	if (fstat(fileno(crontab_fp), statbuf) == -1) {
200 		syslog(LOG_ERR, "(%s) FSTAT FAILED (%s)", uname, fname);
201 		goto next_crontab;
202 	}
203 	if (!S_ISREG(statbuf->st_mode)) {
204 		syslog(LOG_WARNING, "(%s) NOT REGULAR (%s)", uname, fname);
205 		goto next_crontab;
206 	}
207 	/* Looser permissions on system crontab. */
208 	tabmask = pw ? ALLPERMS : (ALLPERMS & ~(S_IWUSR|S_IRGRP|S_IROTH));
209 	tabperm = pw ? (S_IRUSR|S_IWUSR) : S_IRUSR;
210 	if ((statbuf->st_mode & tabmask) != tabperm) {
211 		syslog(LOG_WARNING, "(%s) BAD FILE MODE (%s)", uname, fname);
212 		goto next_crontab;
213 	}
214 	if (statbuf->st_uid != 0 && (pw == NULL ||
215 	    statbuf->st_uid != pw->pw_uid || strcmp(uname, pw->pw_name) != 0)) {
216 		syslog(LOG_WARNING, "(%s) WRONG FILE OWNER (%s)", uname, fname);
217 		goto next_crontab;
218 	}
219 	if (pw != NULL && statbuf->st_gid != cron_gid) {
220 		syslog(LOG_WARNING, "(%s) WRONG FILE GROUP (%s)", uname, fname);
221 		goto next_crontab;
222 	}
223 	if (pw != NULL && statbuf->st_nlink != 1) {
224 		syslog(LOG_WARNING, "(%s) BAD LINK COUNT (%s)", uname, fname);
225 		goto next_crontab;
226 	}
227 
228 	u = find_user(old_db, fname);
229 	if (u != NULL) {
230 		/* if crontab has not changed since we last read it
231 		 * in, then we can just use our existing entry.
232 		 */
233 		if (timespeccmp(&u->mtime, &statbuf->st_mtim, ==)) {
234 			TAILQ_REMOVE(&old_db->users, u, entries);
235 			TAILQ_INSERT_TAIL(&new_db->users, u, entries);
236 			goto next_crontab;
237 		}
238 		syslog(LOG_INFO, "(%s) RELOAD (%s)", uname, fname);
239 	}
240 
241 	new_u = load_user(crontab_fp, pw, fname);
242 	if (new_u != NULL) {
243 		/* Insert user into the new database and remove from old. */
244 		new_u->mtime = statbuf->st_mtim;
245 		TAILQ_INSERT_TAIL(&new_db->users, new_u, entries);
246 		if (u != NULL) {
247 			TAILQ_REMOVE(&old_db->users, u, entries);
248 			free_user(u);
249 		}
250 	} else if (u != NULL) {
251 		/* New user crontab failed to load, preserve the old one. */
252 		TAILQ_REMOVE(&old_db->users, u, entries);
253 		TAILQ_INSERT_TAIL(&new_db->users, u, entries);
254 	}
255 
256  next_crontab:
257 	if (crontab_fp != NULL) {
258 		fclose(crontab_fp);
259 	}
260 }
261