From: Christian Kastner <ckk@kvr.at>
Date: Sat, 09 Jan 2016 00:46:32 +0100
Subject: Drop-in cron.d directory support

Add support for /etc/cron.d, a drop-in directory for packages. This involves
numerous features, such as:
    * Efficiently detecting changes
    * LSB-conform naming convention
    * Security concerns

Contributed by Steve Greenland <stevegr@debian.org>.

Forwarded: no
Last-Update: 2016-01-09
Index: cron/database.c
===================================================================
--- cron.orig/database.c
+++ cron/database.c
@@ -47,7 +47,8 @@ going to be lazy */
 static	void		process_crontab __P((char *, char *, char *,
 					     struct stat *,
 					     cron_db *, cron_db *));
-
+static int valid_name (char *filename);
+static user *get_next_system_crontab __P((user *));
 
 void
 load_database(old_db)
@@ -59,6 +60,10 @@ load_database(old_db)
 	DIR_T   	*dp;
 	cron_db		new_db;
 	user		*u, *nu;
+	struct stat     syscrond_stat;
+	struct stat     syscrond_file_stat;
+	char		syscrond_fname[PATH_MAX+1];
+	int		syscrond_change = 0;
 
 	Debug(DLOAD, ("[%d] load_database()\n", getpid()))
 
@@ -73,8 +78,55 @@ load_database(old_db)
 
 	/* track system crontab file
 	 */
-	if (stat(SYSCRONTAB, &syscron_stat) < OK)
+	if (stat(SYSCRONTAB, &syscron_stat) < OK) {
+		log_it("CRON", getpid(), "STAT FAILED", SYSCRONTAB);
 		syscron_stat.st_mtime = 0;
+	}
+
+	/* Check mod time of SYSCRONDIR. This won't tell us if a file
+	 * in it changed, but will capture deletions, which the individual
+	 * file check won't
+	 */
+	if (stat(SYSCRONDIR, &syscrond_stat) < OK) {
+		log_it("CRON", getpid(), "STAT FAILED", SYSCRONDIR);
+		syscrond_stat.st_mtime = 0;
+	}
+
+	/* If SYSCRONDIR was modified, we know that something is changed and
+	 * there is no need for any further checks. If it wasn't, we should
+	 * pass through the old list of files in SYSCRONDIR and check their
+	 * mod time. Therefore a stopped hard drive won't be spun up, since
+	 * we avoid reading of SYSCRONDIR and don't change its access time.
+	 * This is especially important on laptops with APM.
+	 */
+	if (old_db->sysd_mtime != syscrond_stat.st_mtime) {
+		syscrond_change = 1;
+	} else {
+	        /* Look through the individual files */
+		user *systab;
+
+		Debug(DLOAD, ("[%d] system dir mtime unch, check files now.\n",
+				getpid()))
+
+		for (systab = old_db->head;
+			(systab = get_next_system_crontab (systab)) != NULL;
+			systab = systab->next) {
+
+			sprintf(syscrond_fname, "%s/%s", SYSCRONDIR,
+				systab->name + 8);
+
+			Debug(DLOAD, ("\t%s:", syscrond_fname))
+
+			if (stat(syscrond_fname, &syscrond_file_stat) < OK)
+				syscrond_file_stat.st_mtime = 0;
+
+			if (syscrond_file_stat.st_mtime != systab->mtime) {
+				syscrond_change = 1;
+                        }
+
+			Debug(DLOAD, (" [checked]\n"))
+		}
+	}
 
 	/* if spooldir's mtime has not changed, we don't need to fiddle with
 	 * the database.
@@ -83,7 +135,9 @@ load_database(old_db)
 	 * so is guaranteed to be different than the stat() mtime the first
 	 * time this function is called.
 	 */
-	if (old_db->mtime == TMAX(statbuf.st_mtime, syscron_stat.st_mtime)) {
+	if ((old_db->user_mtime == statbuf.st_mtime) &&
+		(old_db->sys_mtime == syscron_stat.st_mtime) &&
+		(!syscrond_change)) {
 		Debug(DLOAD, ("[%d] spool dir mtime unch, no load needed.\n",
 			      getpid()))
 		return;
@@ -94,7 +148,9 @@ load_database(old_db)
 	 * actually changed.  Whatever is left in the old database when
 	 * we're done is chaff -- crontabs that disappeared.
 	 */
-	new_db.mtime = TMAX(statbuf.st_mtime, syscron_stat.st_mtime);
+	new_db.user_mtime = statbuf.st_mtime;
+	new_db.sys_mtime = syscron_stat.st_mtime;
+	new_db.sysd_mtime = syscrond_stat.st_mtime;
 	new_db.head = new_db.tail = NULL;
 
 	if (syscron_stat.st_mtime) {
@@ -103,6 +159,44 @@ load_database(old_db)
 				&new_db, old_db);
 	}
 
+	/* Read all the package crontabs. */
+	if (!(dir = opendir(SYSCRONDIR))) {
+		log_it("CRON", getpid(), "OPENDIR FAILED", SYSCRONDIR);
+	}
+
+	while (dir != NULL && NULL != (dp = readdir(dir))) {
+		char	fname[MAXNAMLEN+1],
+			tabname[PATH_MAX+1];
+
+
+		/* avoid file names beginning with ".".  this is good
+		 * because we would otherwise waste two guaranteed calls
+		 * to stat() for . and .., and also because package names
+		 * starting with a period are just too nasty to consider.
+		 */
+		if (dp->d_name[0] == '.')
+			continue;
+
+		/* skipfile names with letters outside the set
+		 * [A-Za-z0-9_-], like run-parts.
+		 */
+		if (!valid_name(dp->d_name))
+		  continue;
+
+		/* Generate the "fname" */
+		(void) strcpy(fname,"*system*");
+		(void) strcat(fname, dp->d_name);
+		sprintf(tabname,"%s/%s", SYSCRONDIR, dp->d_name);
+
+		/* statbuf is used as working storage by process_crontab() --
+		   current contents are irrelevant */
+		process_crontab(SYSUSERNAME, fname, tabname,
+				&statbuf, &new_db, old_db);
+
+	}
+	if (dir)
+		closedir(dir);
+
 	/* we used to keep this dir open all the time, for the sake of
 	 * efficiency.  however, we need to close it in every fork, and
 	 * we fork a lot more often than the mtime of the dir changes.
@@ -215,51 +309,118 @@ process_crontab(uname, fname, tabname, s
 	int		crontab_fd = OK - 1;
 	user		*u;
 
-	if (strcmp(fname, "*system*") && !(pw = getpwnam(uname))) {
+	/* If the name begins with *system*, don't worry about password -
+	 it's part of the system crontab */
+	if (strncmp(fname, "*system*", 8) && !(pw = getpwnam(uname))) {
 		/* file doesn't have a user in passwd file.
 		 */
-		log_it(fname, getpid(), "ORPHAN", "no passwd entry");
+		if (strncmp(fname, "tmp.", 4)) {
+			/* don't log these temporary files */
+			log_it(fname, getpid(), "ORPHAN", "no passwd entry");
+		}
 		goto next_crontab;
 	}
 
-	if ((crontab_fd = open(tabname, O_RDONLY|O_NOFOLLOW, 0)) < OK) {
-		/* crontab not accessible?
-		 */
-		log_it(fname, getpid(), "CAN'T OPEN", tabname);
-		goto next_crontab;
-	}
+	if (pw) {
+		/* Path for user crontabs (including root's!) */
+		if ((crontab_fd = open(tabname, O_RDONLY|O_NOFOLLOW, 0)) < OK) {
+			/* crontab not accessible?
+			 */
+			log_it(fname, getpid(), "CAN'T OPEN", tabname);
+			goto next_crontab;
+		}
 
-	if (fstat(crontab_fd, statbuf) < OK) {
-		log_it(fname, getpid(), "FSTAT FAILED", tabname);
-		goto next_crontab;
-	}
-	/* Check to make sure that the crontab is owned by the correct user
-	   (or root) */
+		if (fstat(crontab_fd, statbuf) < OK) {
+			log_it(fname, getpid(), "FSTAT FAILED", tabname);
+			goto next_crontab;
+		}
+		/* Check to make sure that the crontab is owned by the correct user
+		   (or root) */
 
-	if (statbuf->st_uid != pw->pw_uid &&
-		statbuf->st_uid != ROOT_UID) {
-		log_it(fname, getpid(), "WRONG FILE OWNER", tabname);
-		goto next_crontab;
-	}
+		if (statbuf->st_uid != pw->pw_uid && statbuf->st_uid != ROOT_UID) {
+			log_it(fname, getpid(), "WRONG FILE OWNER", tabname);
+			goto next_crontab;
+		}
 
-	/* Check to make sure that the crontab is a regular file */
-	if (!S_ISREG(statbuf->st_mode)) {
-		log_it(fname, getpid(), "NOT A REGULAR FILE", tabname);
-		goto next_crontab;
-	}
+		/* Check to make sure that the crontab is a regular file */
+		if (!S_ISREG(statbuf->st_mode)) {
+			log_it(fname, getpid(), "NOT A REGULAR FILE", tabname);
+			goto next_crontab;
+		}
 
-	/* Check to make sure that the crontab's permissions are secure */
-	if ((statbuf->st_mode & 07777) != 0600) {
-		log_it(fname, getpid(), "INSECURE MODE (mode 0600 expected)", tabname);
-		goto next_crontab;
-	}
+		/* Check to make sure that the crontab's permissions are secure */
+		if ((statbuf->st_mode & 07777) != 0600) {
+			log_it(fname, getpid(), "INSECURE MODE (mode 0600 expected)", tabname);
+			goto next_crontab;
+		}
 
-	/* Check to make sure that there are no hardlinks to the crontab */
-	if (statbuf->st_nlink != 1) {
-		log_it(fname, getpid(), "NUMBER OF HARD LINKS > 1", tabname);
-		goto next_crontab;
+		/* Check to make sure that there are no hardlinks to the crontab */
+		if (statbuf->st_nlink != 1) {
+			log_it(fname, getpid(), "NUMBER OF HARD LINKS > 1", tabname);
+			goto next_crontab;
+		}
+	} else {
+		/* System crontab path. These can be symlinks, but the
+		   symlink and the target must be owned by root. */
+		if (lstat(tabname, statbuf) < OK) {
+			log_it(fname, getpid(), "LSTAT FAILED", tabname);
+			goto next_crontab;
+		}
+		if (S_ISLNK(statbuf->st_mode) && statbuf->st_uid != ROOT_UID) {
+			log_it(fname, getpid(), "WRONG SYMLINK OWNER", tabname);
+			goto next_crontab;
+		}
+		if ((crontab_fd = open(tabname, O_RDONLY, 0)) < OK) {
+			/* crontab not accessible?
+			 */
+			log_it(fname, getpid(), "CAN'T OPEN", tabname);
+			goto next_crontab;
+		}
+
+		if (fstat(crontab_fd, statbuf) < OK) {
+			log_it(fname, getpid(), "FSTAT FAILED", tabname);
+			goto next_crontab;
+		}
+
+		/* Check to make sure that the crontab is owned by root */
+		if (statbuf->st_uid != ROOT_UID) {
+			log_it(fname, getpid(), "WRONG FILE OWNER", tabname);
+			goto next_crontab;
+		}
+
+		/* Check to make sure that the crontab is a regular file */
+		if (!S_ISREG(statbuf->st_mode)) {
+			log_it(fname, getpid(), "NOT A REGULAR FILE", tabname);
+			goto next_crontab;
+		}
+
+		/* Check to make sure that the crontab is writable only by root
+		 * This should really be in sync with the check for users above
+		 * (mode 0600). An upgrade path could be implemented for 4.1
+		 */
+		if ((statbuf->st_mode & S_IWGRP) || (statbuf->st_mode & S_IWOTH)) {
+			log_it(fname, getpid(), "INSECURE MODE (group/other writable)", tabname);
+			goto next_crontab;
+		}
+		/* Technically, we should also check whether the parent dir is
+		 * writable, and so on. This would only make proper sense for
+		 * regular files; we can't realistically check all possible
+		 * security issues resulting from symlinks. We'll just assume that
+		 * root will handle responsible when creating them.
+		 */
+
+		/* Check to make sure that there are no hardlinks to the crontab */
+		if (statbuf->st_nlink != 1) {
+			log_it(fname, getpid(), "NUMBER OF HARD LINKS > 1", tabname);
+			goto next_crontab;
+		}
 	}
 
+        /*
+         * The link count check is not sufficient (the owner may
+         * delete their original link, reducing the link count back to
+         * 1), but this is all we've got.
+         */
 	Debug(DLOAD, ("\t%s:", fname))
 	u = find_user(old_db, fname);
 	if (u != NULL) {
@@ -298,3 +459,53 @@ next_crontab:
 		close(crontab_fd);
 	}
 }
+
+
+#include <regex.h>
+
+/* True or false? Is this a valid filename? */
+
+/* Taken from Clint Adams 'run-parts' version to support lsb style
+   names, originally GPL, but relicensed to cron license per e-mail of
+   27 September 2003. I've changed it to do regcomp() only once. */
+
+static int
+valid_name(char *filename)
+{
+  static regex_t hierre, tradre, excsre, classicalre;
+  static int donere = 0;
+
+  if (!donere) {
+      donere = 1;
+      if (regcomp(&hierre, "^_?([a-z0-9_.]+-)+[a-z0-9]+$",
+                  REG_EXTENDED | REG_NOSUB)
+          || regcomp(&excsre, "^[a-z0-9-].*dpkg-(old|dist)$",
+                     REG_EXTENDED | REG_NOSUB)
+          || regcomp(&tradre, "^[a-z0-9][a-z0-9-]*$", REG_NOSUB)
+          || regcomp(&classicalre, "^[a-zA-Z0-9_-]+$",
+                     REG_EXTENDED | REG_NOSUB)) {
+          log_it("CRON", getpid(), "REGEX FAILED", "valid_name");
+          (void) exit(ERROR_EXIT);
+      }
+  }
+  if (lsbsysinit_mode) {
+      if (!regexec(&hierre, filename, 0, NULL, 0)) {
+          return regexec(&excsre, filename, 0, NULL, 0);
+      } else {
+          return !regexec(&tradre, filename, 0, NULL, 0);
+      }
+  }
+  /* Old standard style */
+  return !regexec(&classicalre, filename, 0, NULL, 0);
+}
+
+
+static user *
+get_next_system_crontab (curtab)
+	user	*curtab;
+{
+	for ( ; curtab != NULL; curtab = curtab->next)
+		if (!strncmp(curtab->name, "*system*", 8) && curtab->name [8])
+			break;
+	return curtab;
+}
Index: cron/pathnames.h
===================================================================
--- cron.orig/pathnames.h
+++ cron/pathnames.h
@@ -62,6 +62,8 @@
 
 			/* 4.3BSD-style crontab */
 #define SYSCRONTAB	"/etc/crontab"
+			/* where package specific crontabs live */
+#define SYSCRONDIR      "/etc/cron.d"
 
 			/* what editor to use if no EDITOR or VISUAL
 			 * environment variable specified.
Index: cron/cron.h
===================================================================
--- cron.orig/cron.h
+++ cron/cron.h
@@ -192,7 +192,9 @@ typedef	struct _user {
 
 typedef	struct _cron_db {
 	user		*head, *tail;	/* links */
-	time_t		mtime;		/* last modtime on spooldir */
+	time_t		user_mtime;     /* last modtime on spooldir */
+	time_t		sys_mtime;      /* last modtime on system crontab */
+	time_t		sysd_mtime;     /* last modtime on system crondir */
 } cron_db;
 
 
@@ -271,6 +273,8 @@ char	*ProgramName;
 int	LineNumber;
 time_t	TargetTime;
 
+int	lsbsysinit_mode;
+
 # if DEBUGGING
 int	DebugFlags;
 char	*DebugFlagNames[] = {	/* sync with #defines */
@@ -283,6 +287,7 @@ extern	char	*copyright[],
 		*MonthNames[],
 		*DowNames[],
 		*ProgramName;
+extern	int	lsbsysinit_mode;
 extern	int	LineNumber;
 extern	time_t	TargetTime;
 # if DEBUGGING
Index: cron/cron.c
===================================================================
--- cron.orig/cron.c
+++ cron/cron.c
@@ -131,7 +131,9 @@ main(argc, argv)
 	acquire_daemonlock(0);
 	database.head = NULL;
 	database.tail = NULL;
-	database.mtime = (time_t) 0;
+	database.sys_mtime = (time_t) 0;
+	database.user_mtime = (time_t) 0;
+	database.sysd_mtime = (time_t) 0;
 	load_database(&database);
 	run_reboot_jobs(&database);
 	cron_sync();
@@ -329,9 +331,9 @@ sighup_handler(int x) {
 
 
 #if DEBUGGING
-const char *getoptarg = "x:";
+const char *getoptarg = "lx:";
 #else
-const char *getoptarg = "";
+const char *getoptarg = "l";
 #endif
 
 static void
@@ -341,10 +343,15 @@ parse_args(argc, argv)
 {
 	int	argch;
 
+	lsbsysinit_mode = 0;
+
 	while (EOF != (argch = getopt(argc, argv, getoptarg))) {
 		switch (argch) {
 		default:
 			usage();
+		case 'l':
+			lsbsysinit_mode = 1;
+			break;
 #if DEBUGGING
 		case 'x':
 			if (!set_debug_flags(optarg))
Index: cron/cron.8
===================================================================
--- cron.orig/cron.8
+++ cron/cron.8
@@ -23,10 +23,17 @@
 cron \- daemon to execute scheduled commands (Vixie Cron)
 .SH SYNOPSIS
 cron
+.RB [ \-l ]
 .SH DESCRIPTION
 .I cron
 is started automatically from /etc/init.d on entering multi-user
 runlevels.
+.SH OPTIONS
+.TP 8
+.B \-l
+Enable LSB compliant names for /etc/cron.d files.  This setting, however, does
+not affect the parsing of files under /etc/cron.hourly, /etc/cron.daily,
+/etc/cron.weekly or /etc/cron.monthly.
 .SH NOTES
 .PP
 .I cron
@@ -47,8 +54,39 @@ to run programs under /etc/cron.hourly,
 Debian, see the note under
 .B DEBIAN SPECIFIC
 below.
-/etc/crontab must be owned by root, and must not
-be group-or other-writable.
+
+Additionally, in Debian,
+.I cron
+reads the files in the /etc/cron.d directory.
+.I cron
+treats the files in /etc/cron.d as in the same way as the /etc/crontab
+file (they follow the special format of that file,
+i.e.\& they include the
+.I user
+field).  However, they are independent of /etc/crontab: they do not, for
+example, inherit environment variable settings from it.  This change is
+specific to Debian see the note under
+.B DEBIAN SPECIFIC
+below.
+
+Like /etc/crontab, the files in the /etc/cron.d directory are
+monitored for changes.
+In general, the system administrator should not use /etc/cron.d/,
+but use the standard system crontab /etc/crontab.
+
+/etc/crontab and the files in /etc/cron.d must be owned by root, and must not
+be group- or other-writable.  In contrast to the spool area, the files
+under /etc/cron.d or the files under /etc/cron.hourly, /etc/cron.daily,
+/etc/cron.weekly and /etc/cron.monthly may also be symlinks,
+provided that both the symlink and the file it points to are owned by root.
+The files under /etc/cron.d do not need to be executable, while the files
+under /etc/cron.hourly, /etc/cron.daily,
+/etc/cron.weekly and /etc/cron.monthly do, as they are run by
+.I run-parts
+(see
+.IR run-parts (8)
+for more information).
+
 .I cron
 then wakes up every minute, examining all stored crontabs, checking
 each command to see if it should be run in the current minute.  When
@@ -116,6 +154,8 @@ changes introduced are:
 .IP \(em
 Support for /etc/cron.{hourly,daily,weekly,monthly} via /etc/crontab,
 .IP \(em
+Support for /etc/cron.d (drop-in dir for package crontabs),
+.IP \(em
 PAM support,
 .IP \(em
 SELinux support,
@@ -141,6 +181,63 @@ These tasks are disabled if
 .B anacron
 is installed (except for the hourly task) to prevent conflicts between
 both daemons.
+
+As described above, the files under these directories have to pass
+some sanity checks including the following: be executable, be owned by root,
+not be writable by group or other and, if symlinks, point to files owned by
+root.  Additionally, the file names must conform to the filename requirements
+of
+.BR run-parts :
+they must be entirely made up of letters, digits and can only contain the
+special signs underscores ('_') and hyphens ('-').  Any file that does
+not conform to these requirements will not be executed by
+.BR run-parts .
+For example, any file containing dots will be ignored.
+This is done to prevent cron from running any of the files
+that are left by the Debian package management system when handling files in
+/etc/cron.d/ as configuration files (i.e.\& files ending in
+\&.dpkg-dist, \&.dpkg-orig, \&.dpkg-old, and \&.dpkg-new).
+
+This feature can be used by system administrators and packages to include
+tasks that will be run at defined intervals.  Files created by packages in these
+directories should be named after the package that supplies them.
+
+.PP
+Support for /etc/cron.d is included in the
+.I cron
+daemon itself, which handles this location as the system-wide crontab spool.
+This directory can contain any file defining tasks following the format
+used in /etc/crontab, i.e.\& unlike the user cron spool, these files must
+provide the username to run the task as in the task definition.
+
+Files in this directory have to be owned by root, do not need to be executable
+(they are configuration files, just like /etc/crontab) and
+must conform to the same naming convention as used by
+.IR run-parts "(8) :"
+they
+must consist solely of upper- and lower-case letters, digits, underscores,
+and hyphens.  This means that they
+.B cannot
+contain any dots.
+If the
+.B \-l
+option is specified to
+.I cron
+(this option can be setup through /etc/default/cron, see below), then they must
+conform to the LSB namespace specification, exactly as in the
+.B \-\-lsbsysinit
+option in
+.IR run-parts .
+
+The intended purpose of this feature is to allow packages that require
+finer control of their scheduling than the
+/etc/cron.{hourly,daily,weekly,monthly}
+directories to add a crontab file to /etc/cron.d.  Such files
+should be named after the package that supplies them.
+
+
+Also, the default configuration of
+.I cron
 is controlled by
 .I /etc/default/cron
 which is read by the init.d script that launches the
@@ -150,9 +247,11 @@ daemon.  This file determines whether
 will read the system's environment variables and makes it possible to add
 additional options to the
 .I cron
-program before it is executed.
+program before it is executed, for example to define how
+it will treat the files under /etc/cron.d.
+
 .SH "SEE ALSO"
-crontab(1), crontab(5)
+crontab(1), crontab(5), run-parts(8)
 .SH AUTHOR
 Paul Vixie <paul@vix.com> is the author of
 .I cron
