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;
> +}

Attachment: signature.asc
Description: PGP signature

Reply via email to