From 7873f8334c8d31031f8cfa83bd97ac6029309e4f Mon Sep 17 00:00:00 2001
From: "Todd C. Miller" <Todd.Miller@sudo.ws>
Date: Sat, 9 Sep 2023 14:07:04 -0600
Subject: [PATCH] Try to make sudo less vulnerable to ROWHAMMER attacks.

We now use ROWHAMMER-resistent values for ALLOW, DENY, AUTH_SUCCESS,
AUTH_FAILURE, AUTH_FATAL and AUTH_NONINTERACTIVE.  In addition, we
explicitly test for expected values instead of using a negated test
against an error value.  In the parser match functions this means
explicitly checking for ALLOW or DENY instead of accepting anything
that is not set to UNSPEC.

Thanks to Andrew J. Adiletta, M. Caner Tol, Yarkin Doroz, and Berk
Sunar, all affiliated with the Vernam Applied Cryptography and
Cybersecurity Lab at Worcester Polytechnic Institute, for the report.
Paper preprint: https://arxiv.org/abs/2309.02545
---
 plugins/sudoers/auth/passwd.c    | 27 ++++++++++-------
 plugins/sudoers/auth/sudo_auth.c | 51 ++++++++++++++++++++++----------
 plugins/sudoers/auth/sudo_auth.h | 12 ++++----
 plugins/sudoers/parse.c          | 12 ++++----
 plugins/sudoers/match.c          | 25 ++++++++--------
 plugins/sudoers/parse.h          | 23 ++++++++++----
 6 files changed, 96 insertions(+), 54 deletions(-)

Index: sudo-1.8.20p2/plugins/sudoers/auth/passwd.c
===================================================================
--- sudo-1.8.20p2.orig/plugins/sudoers/auth/passwd.c
+++ sudo-1.8.20p2/plugins/sudoers/auth/passwd.c
@@ -59,7 +59,7 @@ sudo_passwd_verify(struct passwd *pw, ch
     char des_pass[9], *epass;
     char *pw_epasswd = auth->data;
     size_t pw_len;
-    int matched = 0;
+    int ret;
     debug_decl(sudo_passwd_verify, SUDOERS_DEBUG_AUTH)
 
     /* An empty plain-text password must match an empty encrypted password. */
@@ -71,7 +71,7 @@ sudo_passwd_verify(struct passwd *pw, ch
      */
     pw_len = strlen(pw_epasswd);
     if (pw_len == DESLEN || HAS_AGEINFO(pw_epasswd, pw_len)) {
-	strlcpy(des_pass, pass, sizeof(des_pass));
+	(void)strlcpy(des_pass, pass, sizeof(des_pass));
 	pass = des_pass;
     }
 
@@ -81,14 +81,18 @@ sudo_passwd_verify(struct passwd *pw, ch
      * only compare the first DESLEN characters in that case.
      */
     epass = (char *) crypt(pass, pw_epasswd);
+    ret = AUTH_FAILURE;
     if (epass != NULL) {
-	if (HAS_AGEINFO(pw_epasswd, pw_len) && strlen(epass) == DESLEN)
-	    matched = !strncmp(pw_epasswd, epass, DESLEN);
-	else
-	    matched = !strcmp(pw_epasswd, epass);
+	if (HAS_AGEINFO(pw_epasswd, pw_len) && strlen(epass) == DESLEN) {
+	    if (strncmp(pw_epasswd, epass, DESLEN) == 0)
+		ret = AUTH_SUCCESS;
+	} else {
+	    if (strcmp(pw_epasswd, epass) == 0)
+		ret = AUTH_SUCCESS;
+	}
     }
 
-    debug_return_int(matched ? AUTH_SUCCESS : AUTH_FAILURE);
+    debug_return_int(ret);
 }
 
 int
Index: sudo-1.8.20p2/plugins/sudoers/match.c
===================================================================
--- sudo-1.8.20p2.orig/plugins/sudoers/match.c
+++ sudo-1.8.20p2/plugins/sudoers/match.c
@@ -119,7 +119,7 @@ userlist_matches(const struct passwd *pw
 	    case ALIAS:
 		if ((a = alias_get(m->name, USERALIAS)) != NULL) {
 		    const int rc = userlist_matches(pw, &a->members);
-		    if (rc != UNSPEC) {
+		    if (SPECIFIED(rc)) {
 			if (m->negated) {
 			    matched = rc == ALLOW ? DENY : ALLOW;
 			} else {
@@ -135,7 +135,7 @@ userlist_matches(const struct passwd *pw
 		    matched = m->negated ? DENY : ALLOW;
 		break;
 	}
-	if (matched != UNSPEC)
+	if (SPECIFIED(matched))
 	    break;
     }
     debug_return_int(matched);
@@ -186,7 +186,7 @@ runaslist_matches(const struct member_li
 			if ((a = alias_get(m->name, RUNASALIAS)) != NULL) {
 			    rc = runaslist_matches(&a->members, &empty,
 				matching_user, NULL);
-			    if (rc != UNSPEC) {
+			    if (SPECIFIED(rc)) {
 				if (m->negated) {
 				    user_matched = rc == ALLOW ? DENY : ALLOW;
 				} else {
@@ -207,7 +207,7 @@ runaslist_matches(const struct member_li
 			    user_matched = m->negated ? DENY : ALLOW;
 			break;
 		}
-		if (user_matched != UNSPEC) {
+		if (SPECIFIED(user_matched)) {
 		    if (matching_user != NULL && m->type != ALIAS)
 			*matching_user = m;
 		    break;
@@ -220,7 +220,7 @@ runaslist_matches(const struct member_li
      * Skip checking runas group if none was specified.
      */
     if (ISSET(sudo_user.flags, RUNAS_GROUP_SPECIFIED)) {
-	if (user_matched == UNSPEC) {
+	if (!SPECIFIED(user_matched)) {
 	    if (strcmp(runas_pw->pw_name, user_name) == 0)
 		user_matched = ALLOW;	/* only changing group */
 	}
@@ -234,7 +234,7 @@ runaslist_matches(const struct member_li
 			if ((a = alias_get(m->name, RUNASALIAS)) != NULL) {
 			    rc = runaslist_matches(&empty, &a->members,
 				NULL, matching_group);
-			    if (rc != UNSPEC) {
+			    if (SPECIFIED(rc)) {
 				if (m->negated) {
 			    group_matched = rc == ALLOW ? DENY : ALLOW;
 				} else {
@@ -250,14 +250,14 @@ runaslist_matches(const struct member_li
 			    group_matched = m->negated ? DENY : ALLOW;
 			break;
 		}
-		if (group_matched != UNSPEC) {
+		if (SPECIFIED(group_matched)) {
 		    if (matching_group != NULL && m->type != ALIAS)
 			*matching_group = m;
 		    break;
 		}
 	    }
 	}
-	if (group_matched == UNSPEC) {
+	if (!SPECIFIED(group_matched)) {
 	    if (runas_pw->pw_gid == runas_gr->gr_gid)
 		group_matched = ALLOW;	/* runas group matches passwd db */
 	}
@@ -299,7 +299,7 @@ hostlist_matches(const struct passwd *pw
 	    case ALIAS:
 		if ((a = alias_get(m->name, HOSTALIAS)) != NULL) {
 		    const int rc = hostlist_matches(pw, &a->members);
-		    if (rc != UNSPEC) {
+		    if (SPECIFIED(rc)) {
 			if (m->negated) {
 			    matched = rc == ALLOW ? DENY : ALLOW;
 			} else {
@@ -315,7 +315,7 @@ hostlist_matches(const struct passwd *pw
 		    matched = m->negated ? DENY : ALLOW;
 		break;
 	}
-	if (matched != UNSPEC)
+	if (SPECIFIED(matched))
 	    break;
     }
     debug_return_int(matched);
@@ -334,7 +334,7 @@ cmndlist_matches(const struct member_lis
 
     TAILQ_FOREACH_REVERSE(m, list, member_list, entries) {
 	matched = cmnd_matches(m);
-	if (matched != UNSPEC)
+	if (SPECIFIED(matched))
 	    break;
     }
     debug_return_int(matched);
@@ -359,7 +359,7 @@ cmnd_matches(const struct member *m)
 	case ALIAS:
 	    if ((a = alias_get(m->name, CMNDALIAS)) != NULL) {
 		rc = cmndlist_matches(&a->members);
-		if (rc != UNSPEC) {
+		if (SPECIFIED(rc)) {
 		    if (m->negated) {
 			matched = rc == ALLOW ? DENY : ALLOW;
 		    } else {
Index: sudo-1.8.20p2/plugins/sudoers/parse.h
===================================================================
--- sudo-1.8.20p2.orig/plugins/sudoers/parse.h
+++ sudo-1.8.20p2/plugins/sudoers/parse.h
@@ -18,16 +18,29 @@
 #ifndef SUDOERS_PARSE_H
 #define SUDOERS_PARSE_H
 
+/* Allowed by policy (rowhammer resistent). */
+#undef ALLOW
+#define ALLOW	 0x52a2925	/* 0101001010100010100100100101 */
+
+/* Denied by policy (rowhammer resistent). */
+#undef DENY
+#define DENY	 0xad5d6da	/* 1010110101011101011011011010 */
+
+/* Neither allowed, nor denied. */
 #undef UNSPEC
 #define UNSPEC	-1
-#undef DENY
-#define DENY	 0
-#undef ALLOW
-#define ALLOW	 1
+
+/* Tag implied by root access (SETENV only). */
 #undef IMPLIED
 #define IMPLIED	 2
 
 /*
+ * We must explicitly check against ALLOW and DENY instead testing
+ * that the value is not UNSPEC to avoid potential ROWHAMMER issues.
+ */
+#define SPECIFIED(_v)	((_v) == ALLOW || (_v) == DENY)
+
+/*
  * Initialize all tags to UNSPEC.
  */
 #define TAGS_INIT(t)	do {						       \
@@ -53,7 +66,7 @@
  * Returns true if the specified tag is not UNSPEC or IMPLIED, else false.
  */
 #define TAG_SET(tt) \
-    ((tt) != UNSPEC && (tt) != IMPLIED)
+    ((tt) == true || (tt) == false)
 
 /*
  * Returns true if any tags set in nt differ between ot and nt, else false.
Index: sudo-1.8.20p2/plugins/sudoers/auth/sudo_auth.c
===================================================================
--- sudo-1.8.20p2.orig/plugins/sudoers/auth/sudo_auth.c
+++ sudo-1.8.20p2/plugins/sudoers/auth/sudo_auth.c
@@ -105,10 +105,16 @@ sudo_auth_init(struct passwd *pw)
 	if (auth->init && !IS_DISABLED(auth)) {
 	    /* Disable if it failed to init unless there was a fatal error. */
 	    status = (auth->init)(pw, auth);
-	    if (status == AUTH_FAILURE)
+	    switch (status) {
+	    case AUTH_SUCCESS:
+		break;
+	    case AUTH_FAILURE:
 		SET(auth->flags, FLAG_DISABLED);
-	    else if (status == AUTH_FATAL)
-		break;		/* assume error msg already printed */
+		break;
+	    default:
+		/* Assume error msg already printed. */
+		debug_return_int(-1);
+	    }
 	}
     }
 
@@ -155,7 +161,7 @@ sudo_auth_init(struct passwd *pw)
 	}
     }
 
-    debug_return_int(status == AUTH_FATAL ? -1 : 0);
+    debug_return_int(0);
 }
 
 /*
@@ -173,11 +179,13 @@ sudo_auth_cleanup(struct passwd *pw)
     for (auth = auth_switch; auth->name; auth++) {
 	if (auth->cleanup && !IS_DISABLED(auth)) {
 	    status = (auth->cleanup)(pw, auth);
-	    if (status == AUTH_FATAL)
-		break;		/* assume error msg already printed */
+	    if (status != AUTH_SUCCESS) {
+		/* assume error msg already printed */
+		debug_return_int(-1);
+	    }
 	}
     }
-    debug_return_int(status == AUTH_FATAL ? -1 : 0);
+    debug_return_int(0);
 }
 
 static void
@@ -266,7 +274,7 @@ verify_user(struct passwd *pw, char *pro
 		status = (auth->setup)(pw, &prompt, auth);
 		if (status == AUTH_FAILURE)
 		    SET(auth->flags, FLAG_DISABLED);
-		else if (status == AUTH_FATAL || user_interrupted())
+		else if (status != AUTH_SUCCESS || user_interrupted())
 		    goto done;		/* assume error msg already printed */
 	    }
 	}
@@ -320,7 +328,6 @@ done:
 	    log_auth_failure(validated, ntries);
 	    ret = false;
 	    break;
-	case AUTH_FATAL:
 	default:
 	    log_auth_failure(validated | FLAG_AUTH_ERROR, 0);
 	    ret = -1;
@@ -332,23 +339,33 @@ done:
 
 /*
  * Call authentication method begin session hooks.
- * Returns 1 on success and -1 on error.
+ * Returns true on success, false on failure and -1 on error.
  */
 int
 sudo_auth_begin_session(struct passwd *pw, char **user_env[])
 {
     sudo_auth *auth;
+    int ret = true;
     int status = AUTH_SUCCESS;
     debug_decl(sudo_auth_begin_session, SUDOERS_DEBUG_AUTH)
 
     for (auth = auth_switch; auth->name; auth++) {
 	if (auth->begin_session && !IS_DISABLED(auth)) {
 	    status = (auth->begin_session)(pw, user_env, auth);
-	    if (status != AUTH_SUCCESS)
-		break;		/* assume error msg already printed */
+	    switch (status) {
+	    case AUTH_SUCCESS:
+		break;
+	    case AUTH_FAILURE:
+		ret = false;
+		break;
+	    default:
+		/* Assume error msg already printed. */
+		ret = -1;
+		break;
+	    }
 	}
     }
-    debug_return_int(status == AUTH_SUCCESS ? 1 : -1);
+    debug_return_int(ret);
 }
 
 bool
@@ -369,23 +386,33 @@ sudo_auth_needs_end_session(void)
 
 /*
  * Call authentication method end session hooks.
- * Returns 1 on success and -1 on error.
+ * Returns true on success, false on failure and -1 on error.
  */
 int
 sudo_auth_end_session(struct passwd *pw)
 {
     sudo_auth *auth;
+    int ret = true;
     int status = AUTH_SUCCESS;
     debug_decl(sudo_auth_end_session, SUDOERS_DEBUG_AUTH)
 
     for (auth = auth_switch; auth->name; auth++) {
 	if (auth->end_session && !IS_DISABLED(auth)) {
 	    status = (auth->end_session)(pw, auth);
-	    if (status == AUTH_FATAL)
-		break;			/* assume error msg already printed */
+	    switch (status) {
+	    case AUTH_SUCCESS:
+		break;
+	    case AUTH_FAILURE:
+		ret = false;
+		break;
+	    default:
+		/* Assume error msg already printed. */
+		ret = -1;
+		break;
+	    }
 	}
     }
-    debug_return_int(status == AUTH_FATAL ? -1 : 1);
+    debug_return_int(ret);
 }
 
 /*
Index: sudo-1.8.20p2/plugins/sudoers/auth/sudo_auth.h
===================================================================
--- sudo-1.8.20p2.orig/plugins/sudoers/auth/sudo_auth.h
+++ sudo-1.8.20p2/plugins/sudoers/auth/sudo_auth.h
@@ -17,11 +17,11 @@
 #ifndef SUDO_AUTH_H
 #define SUDO_AUTH_H
 
-/* Auth function return values.  */
-#define AUTH_SUCCESS	0
-#define AUTH_FAILURE	1
-#define AUTH_INTR	2
-#define AUTH_FATAL	3
+/* Auth function return values (rowhammer resistent).  */
+#define AUTH_SUCCESS		0x52a2925	/* 0101001010100010100100100101 */
+#define AUTH_FAILURE		0xad5d6da	/* 1010110101011101011011011010 */
+#define AUTH_INTR		0x69d61fc8	/* 1101001110101100001111111001000 */
+#define AUTH_FATAL		0x1629e037	/* 0010110001010011110000000110111 */
 
 typedef struct sudo_auth {
     int flags;			/* various flags, see below */
