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

This is the already-public orphan-rescan crash tracked in Ubuntu as LP #1846025:
https://bugs.launchpad.net/ubuntu/+source/cron/+bug/1846025

When an orphaned crontab account becomes resolvable again,
check_orphans() invokes process_crontab() with old_db == NULL. If the
spool file then fails validation (for example, insecure mode or wrong
ownership), process_crontab() calls force_rescan_user(), which
dereferences old_db via find_user(). An isolated ASan/UBSan
reproduction crashed the daemon. The likely trigger is account/NSS
lifecycle plus an invalid protected spool file; ordinary users cannot
directly alter that spool, so attacker control is limited.

The attached one-line guard and focused regression test apply
independently to Debian cron 3.0pl1-212. The regression passes with
the fix and reproduces the crash when reverted. The patched Debian
source built in a disposable trixie container; all 13 existing
integration tests passed. The identical fix has been attached to the
existing Ubuntu report and acknowledged there.

Please consider the patch for Debian and any supported-release
backport. This is a follow-up to a known public issue, not a new
vulnerability claim.

Regards,
Brett Sutton
Description: Handle orphan rescans without an old database
Origin: other, private Daybreak audit
Forwarded: no

--- a/database.c
+++ b/database.c
@@ -554,7 +554,7 @@
         user *u;
 
 	/* Remove from old DB and free resources */
-	u = find_user(old_db, fname);
+	u = old_db != NULL ? find_user(old_db, fname) : NULL;
 	if (u != NULL) {
 		Debug(DLOAD, (" [delete old data]"))
 		unlink_user(old_db, u);
--- /dev/null
+++ b/tests/orphan_regression.c
@@ -0,0 +1,82 @@
+#define _GNU_SOURCE
+#define MAIN_PROGRAM
+#include "cron.h"
+
+#include <fcntl.h>
+#include <sys/stat.h>
+#include <assert.h>
+
+static int account_exists;
+static const char orphan_name[] = "orphan-audit";
+
+struct passwd *
+getpwnam(const char *name)
+{
+	static struct passwd pw;
+
+	if (!account_exists || strcmp(name, orphan_name) != 0)
+		return NULL;
+	pw.pw_name = (char *)orphan_name;
+	pw.pw_uid = getuid();
+	pw.pw_gid = getgid();
+	pw.pw_dir = "/tmp";
+	pw.pw_shell = "/bin/sh";
+	return &pw;
+}
+
+void
+log_it(char *username, int pid, char *event, char *detail)
+{
+	(void)username;
+	(void)pid;
+	(void)event;
+	(void)detail;
+}
+
+user *
+load_user(int fd, struct passwd *pw, char *uname, char *fname, char *tabname)
+{
+	(void)fd;
+	(void)pw;
+	(void)uname;
+	(void)fname;
+	(void)tabname;
+	return NULL;
+}
+
+void
+free_user(user *u)
+{
+	free(u->name);
+	free(u);
+}
+
+int
+main(void)
+{
+	char directory[] = "/tmp/cron-orphan-audit.XXXXXX";
+	cron_db db = {0};
+	int fd;
+
+	if (mkdtemp(directory) == NULL || chdir(directory) != 0 ||
+	    mkdir(SPOOL_DIR, 0700) != 0)
+		return 2;
+	fd = open("crontabs/orphan-audit", O_CREAT | O_WRONLY, 0644);
+	if (fd < 0)
+		return 2;
+	{
+		const char contents[] = "* * * * * true\n";
+		write(fd, contents, strlen(contents));
+	}
+	close(fd);
+
+	/* First scan records a file whose account cannot be resolved. */
+	account_exists = 0;
+	load_database(&db);
+	/* A later NSS/account resolution triggers the old_db == NULL path. */
+	account_exists = 1;
+	check_orphans(&db);
+	assert(find_user(&db, orphan_name) != NULL);
+
+	return 0;
+}

Reply via email to