Package: cron
Version: 3.0pl1-212
Severity: important
Tags: patch

An account permitted to install a crontab can substantially delay the
central cron daemon by supplying many environment assignments.
Debian's crontab command validates these assignments cheaply, while
the daemon repeatedly scans and reallocates its growing environment
during load_user(). With many distinct names sharing long prefixes,
loading grows approximately quadratically.

Isolated measurements on the packaged code: 1,000 assignments parsed
in 0.23 s; 8,000 in 14.35 s. A 9.28 MB crontab was accepted by
crontab(1) in about 0.2 s, then took about 22 s in the daemon's
database-loading path. Changing and reloading it incurred a similar
delay. We have not established the maximum practical delay or
demonstrated an out-of-memory failure. The daemon loads synchronously,
so other jobs can be delayed during the parse.

The attached Debian -212 quilt patch replaces repeated linear
environment-name lookup with a balanced index and geometrically grown
ordered vector, retaining last-value-wins and assignment order. It
also adds a 16 MiB whole-crontab size limit and a 16 MiB accounting
budget for copied job environments. Comments count toward file size
but are skipped by environment parsing. The budget is conservative for
repeated replacements. Both limits are checked at installation and
load time, with recovery when an oversized system file is corrected.

The patched Debian source built in a disposable trixie container. All
13 existing Debian integration tests passed, as did focused
environment, allocation-failure, installer-budget and loader-recovery
tests. The patch includes regression sources; build/test integration
for the complete patch series is available on request.

This affects Ubuntu cron 3.0pl1-200ubuntu1 and -209ubuntu1 as well.
Debian and Ubuntu Security Teams reviewed the findings and recommended
public bug reports. No privilege escalation was demonstrated.

Regards,
Brett Sutton
Description: Bound crontab resource use and index environment assignments
Origin: other, private Daybreak audit
Forwarded: no

--- a/Makefile
+++ b/Makefile
@@ -88,16 +88,16 @@
 
 INFOS		=	README CHANGES FEATURES INSTALL CONVERSION THANKS MAIL
 MANPAGES	=	bitstring.3 crontab.5 crontab.1 cron.8 putman.sh
-HEADERS		=	bitstring.h cron.h config.h pathnames.h \
+HEADERS		=	bitstring.h cron.h cron_limits.h env_builder.h config.h pathnames.h \
 			externs.h compat.h
 SOURCES		=	cron.c crontab.c database.c do_command.c entry.c \
-			env.c job.c user.c popen.c misc.c compat.c
+			env.c env_builder.c job.c user.c popen.c misc.c compat.c
 SHAR_SOURCE	=	$(INFOS) $(MANPAGES) Makefile $(HEADERS) $(SOURCES)
 LINT_CRON	=	cron.c database.c user.c entry.c compat.c \
-			misc.c job.c do_command.c env.c popen.c
+			misc.c job.c do_command.c env.c env_builder.c popen.c
 LINT_CRONTAB	=	crontab.c misc.c entry.c env.c compat.c
 CRON_OBJ	=	cron.o database.o user.o entry.o job.o do_command.o \
-			misc.o env.o popen.o compat.o
+			misc.o env.o env_builder.o popen.o compat.o
 CRONTAB_OBJ	=	crontab.o misc.o entry.o env.o compat.o
 
 all		:	cron crontab
@@ -127,5 +127,5 @@
 kit		:	$(SHAR_SOURCE)
 			makekit -m -s99k $(SHAR_SOURCE)
 
-$(CRON_OBJ)	:	cron.h compat.h config.h externs.h pathnames.h bitstring.h Makefile
-$(CRONTAB_OBJ)	:	cron.h compat.h config.h externs.h pathnames.h bitstring.h Makefile
+$(CRON_OBJ)	:	cron.h cron_limits.h env_builder.h compat.h config.h externs.h pathnames.h bitstring.h Makefile
+$(CRONTAB_OBJ)	:	cron.h cron_limits.h env_builder.h compat.h config.h externs.h pathnames.h bitstring.h Makefile
--- a/cron.h
+++ b/cron.h
@@ -83,6 +83,9 @@
 #define	MAX_TEMPSTR	1000	/* max length of envvar=value\0 strings */
 #define	MAX_ENVSTR	MAX_TEMPSTR	/* DO NOT change - buffer overruns otherwise */
 #define MAX_TAB_LINES	10000	/* max length of crontabs */
+/* Bounds apply to both installation and daemon loading. */
+#define MAX_CRONTAB_BYTES (16 * 1024 * 1024)
+#define MAX_ENV_SNAPSHOTS_BYTES (16 * 1024 * 1024)
 #define	MAX_UNAME	20	/* max length of username, should be overkill */
 #define	ROOT_UID	0	/* don't change this, it really must be root */
 #define	ROOT_USER	"root"	/* ditto */
--- /dev/null
+++ b/cron_limits.h
@@ -0,0 +1,70 @@
+#ifndef CRON_LIMITS_H
+#define CRON_LIMITS_H
+
+#include <stddef.h>
+#include <string.h>
+#include <stdint.h>
+#include <pwd.h>
+
+struct cron_limits {
+	size_t assignments;
+	size_t env_bytes;
+	size_t snapshots_bytes;
+};
+
+static inline int
+cron_account_env(struct cron_limits *limits, const char *assignment)
+{
+	size_t bytes = strlen(assignment) + 1;
+	if (limits->assignments == SIZE_MAX ||
+	    bytes > SIZE_MAX - limits->env_bytes)
+		return 0;
+	limits->assignments++;
+	limits->env_bytes += bytes;
+	return 1;
+}
+
+static inline int
+cron_take_budget(size_t *remaining, size_t bytes)
+{
+	if (bytes > *remaining)
+		return 0;
+	*remaining -= bytes;
+	return 1;
+}
+
+static inline size_t
+cron_default_assignment_bytes(const char *prefix, const char *value)
+{
+	size_t prefix_bytes = strlen(prefix);
+	size_t value_bytes = strlen(value);
+	if (value_bytes >= MAX_ENVSTR - prefix_bytes)
+		return MAX_ENVSTR;
+	return prefix_bytes + value_bytes + 1;
+}
+
+static inline int
+cron_account_entry(struct cron_limits *limits, const struct passwd *pw)
+{
+	/* Overcount replacements and defaults to bound per-job environment copies. */
+	size_t remaining = MAX_ENV_SNAPSHOTS_BYTES - limits->snapshots_bytes;
+	size_t defaults = cron_default_assignment_bytes("SHELL=", _PATH_BSHELL) +
+		(pw ? cron_default_assignment_bytes("HOME=", pw->pw_dir) : MAX_ENVSTR) +
+		cron_default_assignment_bytes("PATH=", _PATH_DEFPATH) +
+		(pw ? cron_default_assignment_bytes("LOGNAME=", pw->pw_name) : MAX_ENVSTR);
+#if defined(BSD)
+	defaults += pw ? cron_default_assignment_bytes("USER=", pw->pw_name) : MAX_ENVSTR;
+#endif
+	if (limits->assignments > (SIZE_MAX / 64) - 6)
+		return 0;
+	if (!cron_take_budget(&remaining, limits->env_bytes) ||
+	    !cron_take_budget(&remaining, defaults) ||
+	    !cron_take_budget(&remaining,
+		(limits->assignments + 6) * sizeof(char *)) ||
+	    !cron_take_budget(&remaining, (limits->assignments + 5) * 64))
+		return 0;
+	limits->snapshots_bytes = MAX_ENV_SNAPSHOTS_BYTES - remaining;
+	return 1;
+}
+
+#endif
--- a/crontab.5
+++ b/crontab.5
@@ -329,6 +329,16 @@
 All of the `@' commands that can appear in place of the first five fields
 are extensions.
 .SH LIMITATIONS
+A crontab is limited to 16 MiB including comments and its generated header.
+The accumulated environments copied into job entries have a 16 MiB budget.
+These limits are checked when a user installs a crontab and when the daemon
+loads it. A preexisting file that exceeds a limit is ignored by the daemon
+until it is reduced. The environment-copy budget is an accounting estimate,
+not a process memory limit. It conservatively counts all preceding environment
+assignments, including replaced values, and reserves space for default values.
+Consequently, repeated replacements can exhaust this budget even when the live
+environment is small.
+.PP
 The
 .I cron
 daemon runs with a defined timezone.  It currently does not support
--- a/crontab.c
+++ b/crontab.c
@@ -29,6 +29,7 @@
 
 
 #include "cron.h"
+#include "cron_limits.h"
 #include "misc.h"
 
 #include <errno.h>
@@ -829,6 +830,8 @@
 	char	n[MAX_FNAME], envstr[MAX_ENVSTR];
 	FILE	*tmp;
 	int	ch, eof, fd;
+	size_t input_bytes = 0;
+	struct cron_limits limits = {0};
 	int	nl = FALSE;
 	entry	*e;
 	time_t	now = time(NULL);
@@ -875,9 +878,16 @@
 	/* copy the crontab to the tmp
 	 */
 	rewind(NewCrontab);
+	input_bytes = (size_t)ftell(tmp);
 	Set_LineNum(1)
-	while (EOF != (ch = get_char(NewCrontab)))
+	while (EOF != (ch = get_char(NewCrontab))) {
+		if (++input_bytes > MAX_CRONTAB_BYTES) {
+			fprintf(stderr, "crontab exceeds %d bytes.\n", MAX_CRONTAB_BYTES);
+			fclose(tmp);  unlink(tn);
+			return (-1);
+		}
 		putc(ch, tmp);
+	}
 
 	if (ferror(tmp) || fflush(tmp) || fsync(fd)) {
 		fprintf(stderr, "%s: %s: %s\n",
@@ -903,11 +913,19 @@
 				nl = TRUE;
 			break;
 		case FALSE:
+			if (!cron_account_entry(&limits, pw)) {
+				check_error("job environments exceed memory budget");
+				break;
+			}
 			e = load_entry(tmp, check_error, pw, envp);
 			if (e)
 				free(e);
 			break;
 		case TRUE:
+			if (!cron_account_env(&limits, envstr)) {
+				check_error("environment exceeds memory budget");
+				break;
+			}
 		        /* here MAILTO and MAILFROM are checked */
 		        if (
 			    strncmp(envstr, "MAILTO=", 7) == 0 ||
--- a/database.c
+++ b/database.c
@@ -436,6 +436,12 @@
          */
 	Debug(DLOAD, ("\t%s:", fname))
 
+	if (statbuf->st_size > MAX_CRONTAB_BYTES) {
+		log_it(fname, getpid(), "CRONTAB TOO LARGE", tabname);
+		force_rescan_user(old_db, new_db, fname, statbuf->st_mtime);
+		goto next_crontab;
+	}
+
 	if (old_db != NULL)
 		u = find_user(old_db, fname);
 
--- a/env.c
+++ b/env.c
@@ -79,6 +79,7 @@
 {
 	register int	count, found;
 	register char	**p;
+	char *new_value;
 
 	/*
 	 * count the number of elements, including the null pointer;
@@ -96,12 +97,12 @@
 		 * it exists already, so just free the existing setting,
 		 * save our new one there, and return the existing array.
 		 */
-		free(envp[found]);
-		if ((envp[found] = strdup(envstr)) == NULL) {
-			envp[found] = "";
+		if ((new_value = strdup(envstr)) == NULL) {
 			errno = ENOMEM;
 			return NULL;
 		}
+		free(envp[found]);
+		envp[found] = new_value;
 		return (envp);
 	}
 
@@ -110,17 +111,19 @@
 	 * one, save our string over the old null pointer, and return resized
 	 * array.
 	 */
+	if ((new_value = strdup(envstr)) == NULL) {
+		errno = ENOMEM;
+		return NULL;
+	}
 	p = (char **) realloc((void *) envp,
 			      (unsigned) ((count+1) * sizeof(char **)));
 	if (p == NULL) 	{
+		free(new_value);
 		errno = ENOMEM;
 		return NULL;
 	}
 	p[count] = p[count-1];
-	if ((p[count-1] = strdup(envstr)) == NULL) {
-		errno = ENOMEM;
-		return NULL;
-	}
+	p[count-1] = new_value;
 	return (p);
 }
 
--- /dev/null
+++ b/env_builder.c
@@ -0,0 +1,100 @@
+#include "cron.h"
+#include "env_builder.h"
+
+#include <search.h>
+#include <string.h>
+
+struct env_key {
+	char *assignment;
+	size_t index;
+};
+
+static int
+compare_env_keys(const void *left, const void *right)
+{
+	const char *a = ((const struct env_key *)left)->assignment;
+	const char *b = ((const struct env_key *)right)->assignment;
+	unsigned char ac, bc;
+
+	for (;;) {
+		ac = *a == '=' ? 0 : (unsigned char)*a;
+		bc = *b == '=' ? 0 : (unsigned char)*b;
+		if (ac != bc || ac == 0)
+			return (int)ac - (int)bc;
+		a++;
+		b++;
+	}
+}
+
+int
+env_builder_init(struct env_builder *builder)
+{
+	memset(builder, 0, sizeof *builder);
+	builder->values = env_init();
+	return builder->values != NULL;
+}
+
+int
+env_builder_set(struct env_builder *builder, const char *assignment)
+{
+	struct env_key query = {(char *)assignment, 0};
+	struct env_key *key;
+	void *node;
+	char *copy;
+	char **values;
+	size_t capacity;
+
+	node = tfind(&query, &builder->index, compare_env_keys);
+	if ((copy = strdup(assignment)) == NULL)
+		return 0;
+	if (node != NULL) {
+		key = *(struct env_key **)node;
+		free(builder->values[key->index]);
+		builder->values[key->index] = copy;
+		key->assignment = copy;
+		return 1;
+	}
+
+	key = malloc(sizeof *key);
+	if (key == NULL) {
+		free(copy);
+		return 0;
+	}
+	if (builder->count == builder->capacity) {
+		capacity = builder->capacity ? builder->capacity * 2 : 8;
+		values = realloc(builder->values, (capacity + 1) * sizeof *values);
+		if (values == NULL) {
+			free(key);
+			free(copy);
+			return 0;
+		}
+		builder->values = values;
+		builder->capacity = capacity;
+	}
+	key->assignment = copy;
+	key->index = builder->count;
+	node = tsearch(key, &builder->index, compare_env_keys);
+	if (node == NULL) {
+		free(key);
+		free(copy);
+		return 0;
+	}
+	builder->values[builder->count++] = copy;
+	builder->values[builder->count] = NULL;
+	return 1;
+}
+
+void
+env_builder_destroy(struct env_builder *builder)
+{
+	size_t i;
+	for (i = 0; i < builder->count; i++) {
+		struct env_key query = {builder->values[i], 0};
+		void *node = tfind(&query, &builder->index, compare_env_keys);
+		if (node != NULL) {
+			struct env_key *key = *(struct env_key **)node;
+			tdelete(key, &builder->index, compare_env_keys);
+			free(key);
+		}
+	}
+}
--- /dev/null
+++ b/env_builder.h
@@ -0,0 +1,18 @@
+#ifndef CRON_ENV_BUILDER_H
+#define CRON_ENV_BUILDER_H
+
+#include <stddef.h>
+
+/* Build the ordered, NULL-terminated environment passed to load_entry(). */
+struct env_builder {
+	char **values;
+	size_t count;
+	size_t capacity;
+	void *index;
+};
+
+int env_builder_init(struct env_builder *builder);
+int env_builder_set(struct env_builder *builder, const char *assignment);
+void env_builder_destroy(struct env_builder *builder);
+
+#endif
--- a/user.c
+++ b/user.c
@@ -26,6 +26,8 @@
 #include <syslog.h>
 #include <string.h>
 #include "cron.h"
+#include "cron_limits.h"
+#include "env_builder.h"
 
 
 #ifdef WITH_SELINUX
@@ -205,7 +207,9 @@
 	user	*u;
 	entry	*e;
 	int	status;
-	char	**envp = NULL, **tenvp;
+	struct cron_limits limits = {0};
+	char	**envp;
+	struct env_builder builder = {0};
 
 	if (!(file = fdopen(crontab_fd, "r"))) {
 		perror("fdopen on crontab_fd in load_user");
@@ -247,11 +251,12 @@
 	/* 
 	 * init environment.  this will be copied/augmented for each entry.
 	 */
-	if ((envp = env_init()) == NULL) {
+	if (!env_builder_init(&builder)) {
 		free(u->name);
 		free(u);
 		return NULL;
 	}
+	envp = builder.values;
 
 	/*
 	 * load the crontab
@@ -276,6 +281,12 @@
 			}
 			goto done;
 		case FALSE:
+			if (!cron_account_entry(&limits, pw)) {
+				log_it(u->name, getpid(), "ERROR", "job environments exceed memory budget");
+				free_user(u);
+				u = NULL;
+				goto done;
+			}
 			err_user = fname;
 			e = load_entry(file, crontab_error, pw, envp);
 			err_user = NULL;
@@ -293,8 +304,14 @@
 			}
 			break;
 		case TRUE:
-			if ((tenvp = env_set(envp, envstr))) {
-				envp = tenvp;
+			if (!cron_account_env(&limits, envstr)) {
+				log_it(u->name, getpid(), "ERROR", "environment exceeds memory budget");
+				free_user(u);
+				u = NULL;
+				goto done;
+			}
+			if (env_builder_set(&builder, envstr)) {
+				envp = builder.values;
 			} else {
 				free_user(u);
 				u = NULL;
@@ -317,7 +334,8 @@
 	}
 
  done:
-	env_free(envp);
+	env_builder_destroy(&builder);
+	env_free(builder.values);
 	fclose(file);
 	Debug(DPARS, ("...load_user() done\n"))
 	return u;
--- /dev/null
+++ b/tests/env_builder_failure.c
@@ -0,0 +1,84 @@
+#include "../cron.h"
+#include "../env_builder.h"
+
+#include <assert.h>
+#include <search.h>
+#include <string.h>
+
+enum failure { NONE, FAIL_STRDUP, FAIL_MALLOC, FAIL_REALLOC, FAIL_TSEARCH };
+static enum failure fail_next;
+
+void *__real_malloc(size_t);
+void *__real_realloc(void *, size_t);
+char *__real_strdup(const char *);
+void *__real_tsearch(const void *, void **, int (*)(const void *, const void *));
+
+void *__wrap_malloc(size_t size)
+{
+	if (fail_next == FAIL_MALLOC) {
+		fail_next = NONE;
+		return NULL;
+	}
+	return __real_malloc(size);
+}
+
+void *__wrap_realloc(void *ptr, size_t size)
+{
+	if (fail_next == FAIL_REALLOC) {
+		fail_next = NONE;
+		return NULL;
+	}
+	return __real_realloc(ptr, size);
+}
+
+char *__wrap_strdup(const char *value)
+{
+	if (fail_next == FAIL_STRDUP) {
+		fail_next = NONE;
+		return NULL;
+	}
+	return __real_strdup(value);
+}
+
+void *__wrap_tsearch(const void *key, void **root,
+		int (*compare)(const void *, const void *))
+{
+	if (fail_next == FAIL_TSEARCH) {
+		fail_next = NONE;
+		return NULL;
+	}
+	return __real_tsearch(key, root, compare);
+}
+
+int main(void)
+{
+	struct env_builder builder;
+	char assignment[32];
+	int i;
+	assert(env_builder_init(&builder));
+	assert(env_builder_set(&builder, "A=old"));
+	fail_next = FAIL_STRDUP;
+	assert(!env_builder_set(&builder, "A=new"));
+	assert(strcmp(builder.values[0], "A=old") == 0);
+	fail_next = FAIL_MALLOC;
+	assert(!env_builder_set(&builder, "B=1"));
+	assert(builder.count == 1 && builder.values[1] == NULL);
+	fail_next = FAIL_TSEARCH;
+	assert(!env_builder_set(&builder, "B=1"));
+	assert(builder.count == 1 && builder.values[1] == NULL);
+	for (i = 1; i < 8; i++) {
+		snprintf(assignment, sizeof assignment, "K%d=x", i);
+		assert(env_builder_set(&builder, assignment));
+	}
+	assert(builder.count == 8 && builder.capacity == 8);
+	fail_next = FAIL_REALLOC;
+	assert(!env_builder_set(&builder, "K8=x"));
+	assert(builder.count == 8 && builder.values[8] == NULL);
+	assert(builder.capacity == 8);
+	assert(env_builder_set(&builder, "K8=x"));
+	assert(builder.count == 9 && builder.capacity == 16);
+	env_builder_destroy(&builder);
+	env_free(builder.values);
+	puts("environment builder failure tests passed");
+	return 0;
+}
--- /dev/null
+++ b/tests/installer_limits.c
@@ -0,0 +1,111 @@
+#define MAIN_PROGRAM
+#include "../cron.h"
+
+#include <assert.h>
+#include <fcntl.h>
+#include <sys/stat.h>
+#include <unistd.h>
+#include "budget_fixture.h"
+
+static int
+install_path(const char *path, int dry_run)
+{
+	int status;
+	pid_t child = fork();
+	assert(child >= 0);
+	if (child == 0) {
+		if (dry_run)
+			execl("/usr/bin/crontab", "crontab", "-n", path, (char *)NULL);
+		else
+			execl("/usr/bin/crontab", "crontab", path, (char *)NULL);
+		_exit(127);
+	}
+	assert(waitpid(child, &status, 0) == child);
+	assert(WIFEXITED(status));
+	return WEXITSTATUS(status);
+}
+
+static void
+check_installed_jobs(int expected)
+{
+	FILE *file = popen("/usr/bin/crontab -l", "r");
+	char line[2048];
+	int count = 0;
+	assert(file != NULL);
+	while (fgets(line, sizeof line, file))
+		if (strcmp(line, "* * * * * true\n") == 0)
+			count++;
+	assert(pclose(file) == 0);
+	assert(count == expected);
+}
+
+static void
+check_environment_budget(void)
+{
+	char path[] = "/tmp/daybreak-env.XXXXXX";
+	int fd = mkstemp(path);
+	FILE *file;
+	assert(fd >= 0);
+	file = fdopen(fd, "w");
+	assert(file != NULL);
+	write_budget_fixture(file, 0, 50);
+	assert(fclose(file) == 0);
+	assert(install_path(path, 1) == 0);
+	assert(install_path(path, 0) == 0);
+	check_installed_jobs(51);
+	file = fopen(path, "w");
+	assert(file != NULL);
+	write_budget_fixture(file, 0, 90);
+	assert(fclose(file) == 0);
+	assert(install_path(path, 1) != 0);
+	assert(install_path(path, 0) != 0);
+	/* Failed replacement must retain the previously installed full table. */
+	check_installed_jobs(51);
+	file = fopen(path, "w");
+	assert(file != NULL);
+	fputs("* * * * * true\n", file);
+	assert(fclose(file) == 0);
+	assert(install_path(path, 0) == 0);
+	check_installed_jobs(1);
+	assert(unlink(path) == 0);
+}
+
+static int
+check_crontab_size(off_t bytes)
+{
+	char path[] = "/tmp/daybreak-crontab.XXXXXX";
+	int fd = mkstemp(path), status;
+	pid_t child;
+	assert(fd >= 0 && bytes >= 2);
+	assert(write(fd, "#", 1) == 1);
+	assert(ftruncate(fd, bytes) == 0);
+	assert(lseek(fd, bytes - 1, SEEK_SET) == bytes - 1);
+	assert(write(fd, "\n", 1) == 1);
+	close(fd);
+	child = fork();
+	assert(child >= 0);
+	if (child == 0) {
+		int null_fd = open("/dev/null", O_WRONLY);
+		assert(null_fd >= 0);
+		dup2(null_fd, STDERR);
+		close(null_fd);
+		execl("/usr/bin/crontab", "crontab", "-n", path, (char *)NULL);
+		_exit(127);
+	}
+	assert(waitpid(child, &status, 0) == child);
+	assert(unlink(path) == 0);
+	assert(WIFEXITED(status));
+	return WEXITSTATUS(status);
+}
+
+int
+main(void)
+{
+	assert(getenv("DAYBREAK_DISPOSABLE_TESTBED") != NULL);
+	assert(geteuid() == 0);
+	assert(check_crontab_size(MAX_CRONTAB_BYTES - 512) == 0);
+	assert(check_crontab_size(MAX_CRONTAB_BYTES) != 0);
+	check_environment_budget();
+	puts("installer file-size and environment budget tests passed");
+	return 0;
+}
--- /dev/null
+++ b/tests/loader_recovery.c
@@ -0,0 +1,87 @@
+#define MAIN_PROGRAM
+#include "../cron.h"
+
+#include <assert.h>
+#include <fcntl.h>
+#include <sys/stat.h>
+#include <unistd.h>
+#include "budget_fixture.h"
+
+/* Run only inside a disposable, root-owned package testbed. */
+static const char path[] = SYSCRONDIR "/daybreak_recovery";
+static const char name[] = "*system*daybreak_recovery";
+
+static void
+write_test_file(off_t size, time_t mtime)
+{
+	int fd = open(path, O_WRONLY | O_CREAT | O_TRUNC | O_EXCL, 0644);
+	struct timespec times[2] = {{mtime, 0}, {mtime, 0}};
+	static const char valid[] = "* * * * * root true\n";
+	if (fd < 0) {
+		fd = open(path, O_WRONLY | O_TRUNC);
+		assert(fd >= 0);
+	}
+	assert(fchmod(fd, 0644) == 0);
+	if (size == 0) {
+		assert(write(fd, valid, sizeof valid - 1) == sizeof valid - 1);
+	} else {
+		assert(write(fd, "#", 1) == 1);
+		assert(ftruncate(fd, size) == 0);
+	}
+	assert(futimens(fd, times) == 0);
+	close(fd);
+}
+
+int
+main(void)
+{
+	cron_db db = {0};
+	user *loaded;
+	time_t now = time(NULL);
+	assert(getenv("DAYBREAK_DISPOSABLE_TESTBED") != NULL);
+	assert(geteuid() == 0);
+	assert(access(path, F_OK) != 0);
+	assert(chdir(CRONDIR) == 0);
+	write_test_file(0, now + 10);
+	load_database(&db);
+	loaded = find_user(&db, name);
+	assert(loaded != NULL && loaded->crontab != NULL);
+	write_test_file(MAX_CRONTAB_BYTES + 1, now + 20);
+	load_database(&db);
+	loaded = find_user(&db, name);
+	assert(loaded != NULL && loaded->crontab == NULL);
+	assert(loaded->mtime == now + 20);
+	write_test_file(0, now + 30);
+	load_database(&db);
+	loaded = find_user(&db, name);
+	assert(loaded != NULL && loaded->crontab != NULL);
+	{
+		int jobs[] = {50, 90, 50};
+		size_t i;
+		for (i = 0; i < sizeof jobs / sizeof jobs[0]; i++) {
+			FILE *file = fopen(path, "w");
+			time_t stamp = now + 40 + i * 10;
+			struct timespec times[2] = {{stamp, 0}, {stamp, 0}};
+			assert(file != NULL);
+			write_budget_fixture(file, 1, jobs[i]);
+			assert(fflush(file) == 0);
+			assert(futimens(fileno(file), times) == 0);
+			assert(fclose(file) == 0);
+			load_database(&db);
+			loaded = find_user(&db, name);
+			assert(loaded != NULL && loaded->mtime == stamp);
+			if (jobs[i] == 90) {
+				assert(loaded->crontab == NULL);
+			} else {
+				entry *e;
+				int count = 0;
+				for (e = loaded->crontab; e; e = e->next)
+					count++;
+				assert(count == jobs[i] + 1);
+			}
+		}
+	}
+	assert(unlink(path) == 0);
+	puts("system crontab oversized-file recovery passed");
+	return 0;
+}
--- /dev/null
+++ b/tests/selinux_cleanup.c
@@ -0,0 +1,58 @@
+#define MAIN_PROGRAM
+#include "../cron.h"
+
+#include <assert.h>
+#include <unistd.h>
+
+int __wrap_is_selinux_enabled(void) { return 1; }
+int __wrap_getcon(security_context_t *context)
+{
+	(void)context;
+	return -1;
+}
+
+void log_it(char *username, int pid, char *event, char *detail)
+{
+	(void)username;
+	(void)pid;
+	(void)event;
+	(void)detail;
+}
+
+int __wrap_load_env(char *envstr, FILE *file)
+{
+	(void)envstr;
+	(void)file;
+	return ERR;
+}
+
+entry *load_entry(FILE *file, void (*error)(char *),
+		struct passwd *pw, char **envp)
+{
+	(void)file;
+	(void)error;
+	(void)pw;
+	(void)envp;
+	return NULL;
+}
+
+void free_entry(entry *item)
+{
+	(void)item;
+}
+
+int main(void)
+{
+	FILE *file = tmpfile();
+	struct passwd pw = {0};
+	int fd;
+	assert(file != NULL);
+	fd = dup(fileno(file));
+	assert(fd >= 0);
+	fclose(file);
+	pw.pw_name = "selinux-test";
+	pw.pw_dir = "/tmp";
+	assert(load_user(fd, &pw, "selinux-test", "selinux-test", "test-tab") == NULL);
+	puts("SELinux early-cleanup test passed");
+	return 0;
+}
--- /dev/null
+++ b/tests/resource_regression.c
@@ -0,0 +1,89 @@
+#define MAIN_PROGRAM
+#include "../cron.h"
+#include "../cron_limits.h"
+#include "../env_builder.h"
+#include <assert.h>
+
+static void
+check_environment(void)
+{
+	char **envp = env_init();
+	struct env_builder builder;
+	struct cron_limits limits = {0};
+	struct passwd account = {0};
+	char long_home[MAX_ENVSTR + 100];
+	char long_name[MAX_ENVSTR + 100];
+	char assignment[950];
+	int i;
+	static const char *settings[] = {
+		"A=first", "B=", "AA=long prefix", "A=last", "B=second",
+		"QUOTED=hello world"
+	};
+	assert(envp != NULL);
+	envp = env_set(envp, "A=old");
+	assert(envp != NULL);
+	envp = env_set(envp, "A=new");
+	assert(envp != NULL && envp[1] == NULL);
+	assert(strcmp(envp[0], "A=new") == 0);
+	env_free(envp);
+	assert(env_builder_init(&builder));
+	envp = env_init();
+	assert(envp != NULL);
+	for (i = 0; i < (int)(sizeof settings / sizeof settings[0]); i++) {
+		assert(env_builder_set(&builder, settings[i]));
+		envp = env_set(envp, (char *)settings[i]);
+		assert(envp != NULL);
+	}
+	for (i = 0; envp[i] != NULL; i++)
+		assert(strcmp(envp[i], builder.values[i]) == 0);
+	assert(builder.values[i] == NULL);
+	env_free(envp);
+	env_builder_destroy(&builder);
+	env_free(builder.values);
+	assert(env_builder_init(&builder));
+	assert(env_builder_set(&builder, "A=old"));
+	memset(assignment, 'a', 900);
+	for (i = 0; i < 4000; i++) {
+		snprintf(assignment + 900, sizeof assignment - 900, "%05d=x", i);
+		assert(cron_account_env(&limits, assignment));
+		assert(env_builder_set(&builder, assignment));
+	}
+	assert(env_builder_set(&builder, "A=new"));
+	assert(builder.count == 4001);
+	assert(strcmp(builder.values[0], "A=new") == 0);
+	assert(strstr(builder.values[4000], "03999=x") != NULL);
+	assert(builder.values[4001] == NULL);
+	env_builder_destroy(&builder);
+	env_free(builder.values);
+	memset(long_home, 'h', sizeof long_home - 1);
+	memset(long_name, 'n', sizeof long_name - 1);
+	long_home[sizeof long_home - 1] = '\0';
+	long_name[sizeof long_name - 1] = '\0';
+	account.pw_dir = long_home;
+	account.pw_name = long_name;
+	assert(cron_account_entry(&limits, &account));
+	assert(limits.snapshots_bytes > limits.env_bytes + 2 * MAX_ENVSTR);
+	{
+		struct cron_limits exact = {0}, over = {0};
+		size_t entry_bytes = limits.snapshots_bytes;
+		exact.assignments = over.assignments = limits.assignments;
+		exact.env_bytes = over.env_bytes = limits.env_bytes;
+		exact.snapshots_bytes = MAX_ENV_SNAPSHOTS_BYTES - entry_bytes;
+		over.snapshots_bytes = exact.snapshots_bytes + 1;
+		assert(cron_account_entry(&exact, &account));
+		assert(exact.snapshots_bytes == MAX_ENV_SNAPSHOTS_BYTES);
+		assert(!cron_account_entry(&over, &account));
+		assert(over.snapshots_bytes == exact.snapshots_bytes - entry_bytes + 1);
+	}
+	while (cron_account_entry(&limits, &account))
+		;
+	assert(limits.snapshots_bytes <= MAX_ENV_SNAPSHOTS_BYTES);
+	limits = (struct cron_limits){0};
+	assert(cron_account_entry(&limits, NULL));
+	assert(limits.snapshots_bytes >= 2 * MAX_ENVSTR);
+	builder = (struct env_builder){0};
+	env_builder_destroy(&builder);
+	env_free(builder.values);
+}
+
+int main(void) { check_environment(); puts("resource regression tests passed"); return 0; }
--- /dev/null
+++ b/tests/budget_fixture.h
@@ -0,0 +1,18 @@
+/* Intentionally independent of cron_limits.h: removing parser accounting must
+ * make the above-budget cases fail. Historical replacements count, although
+ * only one 900-byte variable is live. Both fixtures are well below file/line
+ * limits; 50 copies cost <16 MiB and 90 copies cost >16 MiB. */
+static void
+write_budget_fixture(FILE *file, int system, int jobs)
+{
+	int i;
+	char value[901];
+	memset(value, 'x', sizeof value - 1);
+	value[sizeof value - 1] = '\0';
+	fprintf(file, "* * * * * %strue\n", system ? "root " : "");
+	for (i = 0; i < 256; i++)
+		fprintf(file, "REPLACED=%s\n", value);
+	for (i = 0; i < jobs; i++)
+		fprintf(file, "* * * * * %strue\n", system ? "root " : "");
+	assert(ftell(file) < MAX_CRONTAB_BYTES / 2);
+}
--- /dev/null
+++ b/tests/parser_environment.c
@@ -0,0 +1,90 @@
+#define MAIN_PROGRAM
+#include "../cron.h"
+#include <assert.h>
+#include "budget_fixture.h"
+
+static user *
+load_fixture(FILE *file, struct passwd *pw)
+{
+	int fd;
+	assert(fflush(file) == 0);
+	rewind(file);
+	fd = dup(fileno(file));
+	assert(fd >= 0);
+	return load_user(fd, pw, "root", "*system*environment", "environment");
+}
+
+static void
+check_budget(struct passwd *pw)
+{
+	int cases[] = {50, 90, 50};
+	size_t i;
+	for (i = 0; i < sizeof cases / sizeof cases[0]; i++) {
+		FILE *file = tmpfile();
+		user *u;
+		assert(file != NULL);
+		write_budget_fixture(file, pw == NULL, cases[i]);
+		u = load_fixture(file, pw);
+		fclose(file);
+		if (cases[i] == 90) {
+			/* No partially parsed jobs may escape rejection. */
+			assert(u == NULL);
+		} else {
+			entry *e;
+			int count = 0;
+			assert(u != NULL);
+			for (e = u->crontab; e; e = e->next)
+				count++;
+			assert(count == cases[i] + 1);
+			free_user(u);
+		}
+	}
+}
+
+static void
+check_snapshots(struct passwd *pw)
+{
+	FILE *file = tmpfile();
+	user *u;
+	entry *first, *second, *third;
+	const char *owner = pw ? "" : "root ";
+	struct passwd *expected = pw ? pw : getpwnam("root");
+	assert(file && expected);
+	fprintf(file, "* * * * * %sfirst\n", owner);
+	fprintf(file, "VALUE=old\nEMPTY=filled\nPATH=/custom\n* * * * * %ssecond\n", owner);
+	fprintf(file, "VALUE=new\nEMPTY=\n* * * * * %sthird\n", owner);
+	u = load_fixture(file, pw);
+	fclose(file);
+	assert(u && (third = u->crontab) && (second = third->next) && (first = second->next));
+	assert(first->next == NULL);
+	assert(strcmp(first->cmd, "first") == 0);
+	assert(strcmp(second->cmd, "second") == 0);
+	assert(strcmp(third->cmd, "third") == 0);
+	assert(env_get("VALUE", first->envp) == NULL);
+	assert(env_get("EMPTY", first->envp) == NULL);
+	assert(strcmp(env_get("VALUE", second->envp), "old") == 0);
+	assert(strcmp(env_get("EMPTY", second->envp), "filled") == 0);
+	assert(strcmp(env_get("VALUE", third->envp), "new") == 0);
+	assert(strcmp(env_get("EMPTY", third->envp), "") == 0);
+	assert(strcmp(env_get("PATH", first->envp), _PATH_DEFPATH) == 0);
+	assert(strcmp(env_get("PATH", second->envp), "/custom") == 0);
+	assert(strcmp(env_get("PATH", third->envp), "/custom") == 0);
+	for (; third; third = third->next) {
+		assert(strcmp(env_get("SHELL", third->envp), _PATH_BSHELL) == 0);
+		assert(strcmp(env_get("HOME", third->envp), expected->pw_dir) == 0);
+		assert(strcmp(env_get("LOGNAME", third->envp), expected->pw_name) == 0);
+	}
+	free_user(u);
+}
+
+int main(void)
+{
+	struct passwd *pw = getpwuid(getuid());
+	assert(pw != NULL);
+	check_budget(pw);
+	check_snapshots(pw);
+	check_budget(NULL);
+	check_snapshots(NULL);
+	puts("production loader environment budget and snapshot tests passed");
+	return 0;
+}

Reply via email to