1/*	$NetBSD: database.c,v 1.7 2011/10/14 14:38:20 christos Exp $	*/
2
3/* Copyright 1988,1990,1993,1994 by Paul Vixie
4 * All rights reserved
5 */
6
7/*
8 * Copyright (c) 2004 by Internet Systems Consortium, Inc. ("ISC")
9 * Copyright (c) 1997,2000 by Internet Software Consortium, Inc.
10 *
11 * Permission to use, copy, modify, and distribute this software for any
12 * purpose with or without fee is hereby granted, provided that the above
13 * copyright notice and this permission notice appear in all copies.
14 *
15 * THE SOFTWARE IS PROVIDED "AS IS" AND ISC DISCLAIMS ALL WARRANTIES
16 * WITH REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF
17 * MERCHANTABILITY AND FITNESS.  IN NO EVENT SHALL ISC BE LIABLE FOR
18 * ANY SPECIAL, DIRECT, INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES
19 * WHATSOEVER RESULTING FROM LOSS OF USE, DATA OR PROFITS, WHETHER IN AN
20 * ACTION OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT
21 * OF OR IN CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE.
22 */
23#include <sys/cdefs.h>
24#if !defined(lint) && !defined(LINT)
25#if 0
26static char rcsid[] = "Id: database.c,v 1.7 2004/01/23 18:56:42 vixie Exp";
27#else
28__RCSID("$NetBSD: database.c,v 1.7 2011/10/14 14:38:20 christos Exp $");
29#endif
30#endif
31
32/* vix 26jan87 [RCS has the log]
33 */
34
35#include "cron.h"
36
37#define TMAX(a,b) ((a)>(b)?(a):(b))
38
39static	void		process_crontab(const char *, const char *,
40					const char *, struct stat *,
41					cron_db *, cron_db *);
42
43static void
44process_dir(const char *dname, struct stat *st, int sys, cron_db *new_db,
45    cron_db *old_db)
46{
47	DIR *dir;
48	DIR_T *dp;
49
50	/* we used to keep this dir open all the time, for the sake of
51	 * efficiency.  however, we need to close it in every fork, and
52	 * we fork a lot more often than the mtime of the dir changes.
53	 */
54	if (!(dir = opendir(dname))) {
55		log_it("CRON", getpid(), "OPENDIR FAILED", dname);
56		(void) exit(ERROR_EXIT);
57	}
58
59	while (NULL != (dp = readdir(dir))) {
60		char	fname[MAXNAMLEN+1], tabname[MAXNAMLEN+1];
61		size_t i, len;
62		/*
63		 * Homage to...
64		 */
65		static const char *junk[] = {
66			"rpmsave", "rpmorig", "rpmnew",
67		};
68
69		/* avoid file names beginning with ".".  this is good
70		 * because we would otherwise waste two guaranteed calls
71		 * to getpwnam() for . and .., and there shouldn't be
72		 * hidden files in here anyway (in the non system case).
73		 */
74		if (dp->d_name[0] == '.')
75			continue;
76
77		/* ignore files starting with # ... */
78		if (dp->d_name[0] == '#')
79			continue;
80
81		len = strlen(dp->d_name);
82
83		/* ... or too big or to small ... */
84		if (len == 0 || len >= sizeof(fname)) {
85			log_it(dp->d_name, getpid(), "ORPHAN",
86			    "name too short or long");
87			continue;
88		}
89
90		/* ... or ending with ~ ... */
91		if (dp->d_name[len - 1] == '~')
92			continue;
93
94		(void)strlcpy(fname, dp->d_name, sizeof(fname));
95
96		/* ... or look for blacklisted extensions */
97		for (i = 0; i < __arraycount(junk); i++) {
98			char *p;
99			if ((p = strrchr(fname, '.')) != NULL &&
100			    strcmp(p + 1, junk[i]) == 0)
101				break;
102		}
103		if (i != __arraycount(junk))
104			continue;
105
106		if (!glue_strings(tabname, sizeof tabname, dname, fname, '/')) {
107			log_it(fname, getpid(), "ORPHAN",
108			    "could not glue strings");
109			continue;
110		}
111
112		process_crontab(sys ? "root" : fname, sys ? "*system*" :
113				fname, tabname, st, new_db, old_db);
114	}
115	(void)closedir(dir);
116}
117
118void
119load_database(cron_db *old_db) {
120	struct stat spool_stat, syscron_stat, crond_stat;
121	cron_db new_db;
122	user *u, *nu;
123	time_t new_mtime;
124
125	Debug(DLOAD, ("[%ld] load_database()\n", (long)getpid()));
126
127	/* before we start loading any data, do a stat on SPOOL_DIR
128	 * so that if anything changes as of this moment (i.e., before we've
129	 * cached any of the database), we'll see the changes next time.
130	 */
131	if (stat(SPOOL_DIR, &spool_stat) < OK) {
132		log_it("CRON", getpid(), "STAT FAILED", SPOOL_DIR);
133		(void) exit(ERROR_EXIT);
134	}
135
136	/* track system crontab directory
137	 */
138	if (stat(CROND_DIR, &crond_stat) < OK)
139		crond_stat.st_mtime = 0;
140
141	/* track system crontab file
142	 */
143	if (stat(SYSCRONTAB, &syscron_stat) < OK)
144		syscron_stat.st_mtime = 0;
145
146	/* if spooldir's mtime has not changed, we don't need to fiddle with
147	 * the database.
148	 *
149	 * Note that old_db->mtime is initialized to 0 in main(), and
150	 * so is guaranteed to be different than the stat() mtime the first
151	 * time this function is called.
152	 */
153	new_mtime = TMAX(crond_stat.st_mtime, TMAX(spool_stat.st_mtime,
154	    syscron_stat.st_mtime));
155	if (old_db->mtime == new_mtime) {
156		Debug(DLOAD, ("[%ld] spool dir mtime unch, no load needed.\n",
157			      (long)getpid()));
158		return;
159	}
160
161	/* something's different.  make a new database, moving unchanged
162	 * elements from the old database, reloading elements that have
163	 * actually changed.  Whatever is left in the old database when
164	 * we're done is chaff -- crontabs that disappeared.
165	 */
166	new_db.mtime = new_mtime;
167	new_db.head = new_db.tail = NULL;
168
169	if (syscron_stat.st_mtime)
170		process_crontab("root", NULL, SYSCRONTAB, &syscron_stat,
171				&new_db, old_db);
172
173	if (crond_stat.st_mtime)
174		process_dir(CROND_DIR, &crond_stat, 1, &new_db, old_db);
175
176	process_dir(SPOOL_DIR, &spool_stat, 0, &new_db, old_db);
177
178	/* if we don't do this, then when our children eventually call
179	 * getpwnam() in do_command.c's child_process to verify MAILTO=,
180	 * they will screw us up (and v-v).
181	 */
182	endpwent();
183
184	/* whatever's left in the old database is now junk.
185	 */
186	Debug(DLOAD, ("unlinking old database:\n"));
187	for (u = old_db->head;  u != NULL;  u = nu) {
188		Debug(DLOAD, ("\t%s\n", u->name));
189		nu = u->next;
190		unlink_user(old_db, u);
191		free_user(u);
192	}
193
194	/* overwrite the database control block with the new one.
195	 */
196	*old_db = new_db;
197	Debug(DLOAD, ("load_database is done\n"));
198}
199
200void
201link_user(cron_db *db, user *u) {
202	if (db->head == NULL)
203		db->head = u;
204	if (db->tail)
205		db->tail->next = u;
206	u->prev = db->tail;
207	u->next = NULL;
208	db->tail = u;
209}
210
211void
212unlink_user(cron_db *db, user *u) {
213	if (u->prev == NULL)
214		db->head = u->next;
215	else
216		u->prev->next = u->next;
217
218	if (u->next == NULL)
219		db->tail = u->prev;
220	else
221		u->next->prev = u->prev;
222}
223
224user *
225find_user(cron_db *db, const char *name) {
226	user *u;
227
228	for (u = db->head;  u != NULL;  u = u->next)
229		if (strcmp(u->name, name) == 0)
230			break;
231	return (u);
232}
233
234static void
235process_crontab(const char *uname, const char *fname, const char *tabname,
236		struct stat *statbuf, cron_db *new_db, cron_db *old_db)
237{
238	struct passwd *pw = NULL;
239	int crontab_fd = OK - 1;
240	mode_t eqmode = 0600, badmode = 0;
241	user *u;
242
243	if (fname == NULL) {
244		/*
245		 * SYSCRONTAB:
246		 * set fname to something for logging purposes.
247		 * Allow it to become readable by group and others, but
248		 * not writable.
249		 */
250		fname = "*system*";
251		eqmode = 0;
252		badmode = 022;
253	} else if ((pw = getpwnam(uname)) == NULL) {
254		/* file doesn't have a user in passwd file.
255		 */
256		log_it(fname, getpid(), "ORPHAN", "no passwd entry");
257		goto next_crontab;
258	}
259
260	if ((crontab_fd = open(tabname, O_RDONLY|O_NONBLOCK|O_NOFOLLOW, 0)) < OK) {
261		/* crontab not accessible?
262		 */
263		log_it(fname, getpid(), "CAN'T OPEN", tabname);
264		goto next_crontab;
265	}
266
267	if (fstat(crontab_fd, statbuf) < OK) {
268		log_it(fname, getpid(), "FSTAT FAILED", tabname);
269		goto next_crontab;
270	}
271	if (!S_ISREG(statbuf->st_mode)) {
272		log_it(fname, getpid(), "NOT REGULAR", tabname);
273		goto next_crontab;
274	}
275	if ((eqmode && (statbuf->st_mode & 07777) != eqmode) ||
276	    (badmode && (statbuf->st_mode & badmode) != 0)) {
277		log_it(fname, getpid(), "BAD FILE MODE", tabname);
278		goto next_crontab;
279	}
280	if (statbuf->st_uid != ROOT_UID && (pw == NULL ||
281	    statbuf->st_uid != pw->pw_uid || strcmp(uname, pw->pw_name) != 0)) {
282		log_it(fname, getpid(), "WRONG FILE OWNER", tabname);
283		goto next_crontab;
284	}
285	if (statbuf->st_nlink != 1) {
286		log_it(fname, getpid(), "BAD LINK COUNT", tabname);
287		goto next_crontab;
288	}
289
290	Debug(DLOAD, ("\t%s:", fname));
291	u = find_user(old_db, fname);
292	if (u != NULL) {
293		/* if crontab has not changed since we last read it
294		 * in, then we can just use our existing entry.
295		 */
296		if (u->mtime == statbuf->st_mtime) {
297			Debug(DLOAD, (" [no change, using old data]"));
298			unlink_user(old_db, u);
299			link_user(new_db, u);
300			goto next_crontab;
301		}
302
303		/* before we fall through to the code that will reload
304		 * the user, let's deallocate and unlink the user in
305		 * the old database.  This is more a point of memory
306		 * efficiency than anything else, since all leftover
307		 * users will be deleted from the old database when
308		 * we finish with the crontab...
309		 */
310		Debug(DLOAD, (" [delete old data]"));
311		unlink_user(old_db, u);
312		free_user(u);
313		log_it(fname, getpid(), "RELOAD", tabname);
314	}
315	u = load_user(crontab_fd, pw, fname);
316	if (u != NULL) {
317		u->mtime = statbuf->st_mtime;
318		link_user(new_db, u);
319	}
320
321 next_crontab:
322	if (crontab_fd >= OK) {
323		Debug(DLOAD, (" [done]\n"));
324		(void)close(crontab_fd);
325	}
326}
327