CVE-2021-3995: Files or Directories Accessible to External Parties (CWE-552)
value: A logic error was found in the libmount library of util-linux in the function that allows an unprivileged user to unmount a FUSE filesystem. This flaw allows an unprivileged local attacker to unmount FUSE filesystems that belong to certain other users who have a UID that is a prefix of the UID of the attacker in its string form. An attacker may use this flaw to cause a denial of service to applications that use the affected filesystems.
CWE description: The product makes files or directories accessible to unauthorized actors, even though they should not be.
CVSS v3 base score: 5.5
Published: 2022-08-23T20:15Z
Repository: https://github.com/util-linux/util-linux
Language: C
Files changed: libmount/src/context_umount.c, libmount/src/mountP.h, libmount/src/optstr.c
Commit date: 2022-01-24 12:05:16 +0100
Commit message: libmount: fix UID check for FUSE umount [CVE-2021-3995]

Improper UID check allows an unprivileged user to unmount FUSE
filesystems of users with similar UID.

Signed-off-by: Karel Zak <kzak@redhat.com>

Diff: diff --git a/libmount/src/context_umount.c b/libmount/src/context_umount.c
index 25a409e22..9c6d190e1 100644
--- a/libmount/src/context_umount.c
+++ b/libmount/src/context_umount.c
@@ -450,14 +450,11 @@ static int prepare_helper_from_options(struct libmnt_context *cxt,
 static int is_fuse_usermount(struct libmnt_context *cxt, int *errsv)
 {
 	struct libmnt_ns *ns_old;
 	const char *type = mnt_fs_get_fstype(cxt->fs);
 	const char *optstr;
-	char *user_id = NULL;
-	size_t sz;
-	uid_t uid;
-	char uidstr[sizeof(stringify_value(ULONG_MAX))];
+	uid_t uid, entry_uid;
 
 	*errsv = 0;
 
 	if (!type)
 		return 0;
@@ -470,15 +467,11 @@ static int is_fuse_usermount(struct libmnt_context *cxt, int *errsv)
 
 	/* get user_id= from mount table */
 	optstr = mnt_fs_get_fs_options(cxt->fs);
 	if (!optstr)
 		return 0;
-
-	if (mnt_optstr_get_option(optstr, "user_id", &user_id, &sz) != 0)
-		return 0;
-
-	if (sz == 0 || user_id == NULL)
+	if (mnt_optstr_get_uid(optstr, "user_id", &entry_uid) != 0)
 		return 0;
 
 	/* get current user */
 	ns_old = mnt_context_switch_origin_ns(cxt);
 	if (!ns_old) {
@@ -491,12 +484,11 @@ static int is_fuse_usermount(struct libmnt_context *cxt, int *errsv)
 	if (!mnt_context_switch_ns(cxt, ns_old)) {
 		*errsv = -MNT_ERR_NAMESPACE;
 		return 0;
 	}
 
-	snprintf(uidstr, sizeof(uidstr), "%lu", (unsigned long) uid);
-	return strncmp(user_id, uidstr, sz) == 0;
+	return uid == entry_uid;
 }
 
 /*
  * Note that cxt->fs contains relevant mtab entry!
  */
diff --git a/libmount/src/mountP.h b/libmount/src/mountP.h
index b47640daa..0919dedb6 100644
--- a/libmount/src/mountP.h
+++ b/libmount/src/mountP.h
@@ -397,10 +397,11 @@ extern const struct libmnt_optmap *mnt_optmap_get_entry(
 			     const char *name,
                              size_t namelen,
 			     const struct libmnt_optmap **mapent);
 
 /* optstr.c */
+extern int mnt_optstr_get_uid(const char *optstr, const char *name, uid_t *uid);
 extern int mnt_optstr_remove_option_at(char **optstr, char *begin, char *end);
 extern int mnt_optstr_fix_gid(char **optstr, char *value, size_t valsz, char **next);
 extern int mnt_optstr_fix_uid(char **optstr, char *value, size_t valsz, char **next);
 extern int mnt_optstr_fix_secontext(char **optstr, char *value, size_t valsz, char **next);
 extern int mnt_optstr_fix_user(char **optstr);
diff --git a/libmount/src/optstr.c b/libmount/src/optstr.c
index 5adfd4dae..224293f65 100644
--- a/libmount/src/optstr.c
+++ b/libmount/src/optstr.c
@@ -1081,10 +1081,52 @@ int mnt_optstr_fix_user(char **optstr)
 
 	free(username);
 	return rc;
 }
 
+/*
+ * Converts value from @optstr addressed by @name to uid.
+ *
+ * Returns: 0 on success, 1 if not found, <0 on error
+ */
+int mnt_optstr_get_uid(const char *optstr, const char *name, uid_t *uid)
+{
+	char *value = NULL;
+	size_t valsz = 0;
+	char buf[sizeof(stringify_value(UINT64_MAX))];
+	int rc;
+	uint64_t num;
+
+	assert(optstr);
+	assert(name);
+	assert(uid);
+
+	rc = mnt_optstr_get_option(optstr, name, &value, &valsz);
+	if (rc != 0)
+		goto fail;
+
+	if (valsz > sizeof(buf) - 1) {
+		rc = -ERANGE;
+		goto fail;
+	}
+	mem2strcpy(buf, value, valsz, sizeof(buf));
+
+	rc = ul_strtou64(buf, &num, 10);
+	if (rc != 0)
+		goto fail;
+	if (num > ULONG_MAX || (uid_t) num != num) {
+		rc = -ERANGE;
+		goto fail;
+	}
+	*uid = (uid_t) num;
+
+	return 0;
+fail:
+	DBG(UTILS, ul_debug("failed to convert '%s'= to number [rc=%d]", name, rc));
+	return rc;
+}
+
 /**
  * mnt_match_options:
  * @optstr: options string
  * @pattern: comma delimited list of options
  *

Vulnerable code: 	char *user_id = NULL;
	size_t sz;
	uid_t uid;
	char uidstr[sizeof(stringify_value(ULONG_MAX))];

	if (mnt_optstr_get_option(optstr, "user_id", &user_id, &sz) != 0)
		return 0;

	if (sz == 0 || user_id == NULL)
	snprintf(uidstr, sizeof(uidstr), "%lu", (unsigned long) uid);
	return strncmp(user_id, uidstr, sz) == 0;
Fixed code: 	uid_t uid, entry_uid;
	if (mnt_optstr_get_uid(optstr, "user_id", &entry_uid) != 0)
	return uid == entry_uid;
extern int mnt_optstr_get_uid(const char *optstr, const char *name, uid_t *uid);
/*
 * Converts value from @optstr addressed by @name to uid.
 *
 * Returns: 0 on success, 1 if not found, <0 on error
 */
int mnt_optstr_get_uid(const char *optstr, const char *name, uid_t *uid)
{
	char *value = NULL;
	size_t valsz = 0;
	char buf[sizeof(stringify_value(UINT64_MAX))];
	int rc;
	uint64_t num;

	assert(optstr);
	assert(name);
	assert(uid);

	rc = mnt_optstr_get_option(optstr, name, &value, &valsz);
	if (rc != 0)
		goto fail;

	if (valsz > sizeof(buf) - 1) {
		rc = -ERANGE;
		goto fail;
	}
	mem2strcpy(buf, value, valsz, sizeof(buf));

	rc = ul_strtou64(buf, &num, 10);
	if (rc != 0)
		goto fail;
	if (num > ULONG_MAX || (uid_t) num != num) {
		rc = -ERANGE;
		goto fail;
	}
	*uid = (uid_t) num;

	return 0;
fail:
	DBG(UTILS, ul_debug("failed to convert '%s'= to number [rc=%d]", name, rc));
	return rc;
}