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

When a hostname fills cron's fixed-size buffer, the mail-output path
can treat it as a NUL-terminated string even though gethostname() did
not supply a terminator. An AddressSanitizer harness reproduced a read
past the buffer. The relevant conditions are a maximum-length
administrator-controlled hostname, job output, and mail delivery. We
have not demonstrated a useful information leak or a production crash.

The attached Debian -212 patch reserves room for termination, checks
gethostname()'s return value, and uses a safe fallback on error. Its
regression test covers ordinary success, error, and a successful
full-buffer result without a NUL terminator.

The patched Debian source built in a disposable trixie container; the
focused regression and all 13 existing Debian integration tests
passed. Ubuntu versions 3.0pl1-200ubuntu1 and -209ubuntu1 have the
same issue. Debian and Ubuntu Security Teams recommended public bug
reports.

Regards,
Brett Sutton
Description: Terminate mail hostnames and handle lookup failures
Origin: other, private Daybreak audit
Forwarded: no

--- a/cron.h
+++ b/cron.h
@@ -345,3 +345,5 @@
 extern	char	*DebugFlagNames[];
 # endif /* DEBUGGING */
 #endif /*MAIN_PROGRAM*/
+
+int cron_gethostname(char *, size_t);
--- a/do_command.c
+++ b/do_command.c
@@ -534,12 +534,12 @@
 	register char	**env;
 	char		**jobenv = build_env(e->envp);
 	auto char	mailcmd[MAX_COMMAND];
-	auto char	hostname[MAXHOSTNAMELEN];
+	auto char	hostname[MAXHOSTNAMELEN + 1];
 	char		*content_type = env_get("CONTENT_TYPE",jobenv),
 			*content_transfer_encoding = env_get("CONTENT_TRANSFER_ENCODING",jobenv);
 	int encoding_auto = 0;
 
-	(void) gethostname(hostname, MAXHOSTNAMELEN);
+	(void) cron_gethostname(hostname, sizeof hostname);
 	(void) snprintf(mailcmd, sizeof(mailcmd),
 			MAILARGS, MAILCMD, mailto);
 	if (!(mail = cron_popen(mailcmd, "w", e))) {
--- a/misc.c
+++ b/misc.c
@@ -792,3 +792,14 @@
 	return (offset);
 }
 #endif /* HAVE_TM_GMTOFF */
+
+int
+cron_gethostname(char *hostname, size_t size)
+{
+	if (size < sizeof "localhost")
+		return FALSE;
+	if (gethostname(hostname, size) != 0)
+		(void) strcpy(hostname, "localhost");
+	hostname[size - 1] = '\0';
+	return TRUE;
+}
\ No newline at end of file
--- /dev/null
+++ b/tests/hostname_regression.c
@@ -0,0 +1,49 @@
+#define MAIN_PROGRAM
+#define gethostname fake_gethostname
+#include "../misc.c"
+#undef gethostname
+#include <assert.h>
+
+static int hostname_mode;
+
+int
+fake_gethostname(char *name, size_t size)
+{
+	if (hostname_mode == 3) {
+		memset(name, 'h', size);
+		return 0;
+	}
+	if (hostname_mode == 1) {
+		memset(name, 'h', size);
+		errno = ENAMETOOLONG;
+		return -1;
+	}
+	if (hostname_mode == 2) {
+		if (size <= MAXHOSTNAMELEN)
+			return -1;
+		memset(name, 'h', MAXHOSTNAMELEN);
+		name[MAXHOSTNAMELEN] = '\0';
+		return 0;
+	}
+	memcpy(name, "testhost", 9);
+	return 0;
+}
+
+
+int main(void) {
+	char hostname[MAXHOSTNAMELEN + 1];
+	hostname_mode = 1;
+	assert(cron_gethostname(hostname, sizeof hostname));
+	assert(strcmp(hostname, "localhost") == 0);
+	hostname_mode = 2;
+	assert(cron_gethostname(hostname, sizeof hostname));
+	assert(strlen(hostname) == MAXHOSTNAMELEN);
+	hostname_mode = 3;
+	assert(cron_gethostname(hostname, sizeof hostname));
+	assert(hostname[sizeof hostname - 1] == '\0');
+	assert(strlen(hostname) == MAXHOSTNAMELEN);
+	hostname_mode = 0;
+	assert(cron_gethostname(hostname, sizeof hostname));
+	assert(strcmp(hostname, "testhost") == 0);
+	return 0;
+}

Reply via email to