Hello, I wonder why crontab installers should care for misbehavior of users. I agree that one user can overload a computer with multiple environment assignments, triggered by a crontab.
By the way, do **you** create such crontabs? Why would a sane user
create such crontabs?
If some user **wants* to overload a system, they only need to launch
multiple resource-consuming tasks. There is no need to build a crontab
for that purpose.
Dear Brett, please provide some evidence that a cron package with your
enhancement would protect efficiently a computer against a user who
wants to overload the machine.
Best regards, Georges.
Brett Sutton a écrit :
> 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;
> +}
signature.asc
Description: PGP signature

