From 6ee66ff7bcc91803111d950512f02651e664f74f Mon Sep 17 00:00:00 2001 From: Simon McVittie Date: Tue, 30 Jun 2020 19:13:17 +0100 Subject: [PATCH 1/2] userdb: Make lookups return a const pointer This makes it more obvious that the returned pointer points to a struct owned by the userdb, which must not be freed or have its contents modified, and is only valid to dereference until the next modification to the userdb's underlying hash tables (which in practice means until the lock is released, because after that we have no guarantees about what might be going on in another thread). Signed-off-by: Simon McVittie --- dbus/dbus-userdb-util.c | 6 ++++-- dbus/dbus-userdb.c | 6 ++++-- dbus/dbus-userdb.h | 10 +++++----- 3 files changed, 13 insertions(+), 9 deletions(-) diff --git a/dbus/dbus-userdb-util.c b/dbus/dbus-userdb-util.c index a8608fa6..18621a17 100644 --- a/dbus/dbus-userdb-util.c +++ b/dbus/dbus-userdb-util.c @@ -249,9 +249,9 @@ _dbus_get_user_id_and_primary_group (const DBusString *username, * @param gid the group ID or #DBUS_GID_UNSET * @param groupname group name or #NULL * @param error error to fill in - * @returns the entry in the database + * @returns the entry in the database (borrowed, do not free) */ -DBusGroupInfo* +const DBusGroupInfo * _dbus_user_database_lookup_group (DBusUserDatabase *db, dbus_gid_t gid, const DBusString *groupname, @@ -337,6 +337,8 @@ _dbus_user_database_lookup_group (DBusUserDatabase *db, return NULL; } + /* Return a borrowed reference to the DBusGroupInfo owned by the + * two hash tables */ return info; } } diff --git a/dbus/dbus-userdb.c b/dbus/dbus-userdb.c index d3225371..78169e5b 100644 --- a/dbus/dbus-userdb.c +++ b/dbus/dbus-userdb.c @@ -129,9 +129,9 @@ _dbus_is_a_number (const DBusString *str, * @param uid the user ID or #DBUS_UID_UNSET * @param username username or #NULL * @param error error to fill in - * @returns the entry in the database + * @returns the entry in the database (borrowed, do not free) */ -DBusUserInfo* +const DBusUserInfo * _dbus_user_database_lookup (DBusUserDatabase *db, dbus_uid_t uid, const DBusString *username, @@ -218,6 +218,8 @@ _dbus_user_database_lookup (DBusUserDatabase *db, return NULL; } + /* Return a borrowed pointer to the DBusUserInfo owned by the + * hash tables */ return info; } } diff --git a/dbus/dbus-userdb.h b/dbus/dbus-userdb.h index 1eb4bd46..479b2311 100644 --- a/dbus/dbus-userdb.h +++ b/dbus/dbus-userdb.h @@ -67,15 +67,15 @@ dbus_bool_t _dbus_user_database_get_username (DBusUserDatabase *db, const DBusUserInfo **info, DBusError *error); DBUS_PRIVATE_EXPORT -DBusUserInfo* _dbus_user_database_lookup (DBusUserDatabase *db, +const DBusUserInfo *_dbus_user_database_lookup (DBusUserDatabase *db, dbus_uid_t uid, const DBusString *username, DBusError *error); DBUS_PRIVATE_EXPORT -DBusGroupInfo* _dbus_user_database_lookup_group (DBusUserDatabase *db, - dbus_gid_t gid, - const DBusString *groupname, - DBusError *error); +const DBusGroupInfo* _dbus_user_database_lookup_group (DBusUserDatabase *db, + dbus_gid_t gid, + const DBusString *groupname, + DBusError *error); DBUS_PRIVATE_EXPORT void _dbus_user_info_free_allocated (DBusUserInfo *info); DBUS_PRIVATE_EXPORT From 2b7948ef907669e844b52c4fa2268d6e3162a70c Mon Sep 17 00:00:00 2001 From: Simon McVittie Date: Tue, 30 Jun 2020 19:29:06 +0100 Subject: [PATCH 2/2] userdb: Reference-count DBusUserInfo, DBusGroupInfo Previously, the hash table indexed by uid (or gid) took ownership of the single reference to the heap-allocated struct, and the hash table indexed by username (or group name) had a borrowed pointer to the same struct that exists in the other hash table. However, this can break down if you have two or more distinct usernames that share a numeric identifier. This is generally a bad idea, because the user-space model in such situations does not match the kernel-space reality, and in particular there is no effective kernel-level security boundary between such users, but it is sometimes done anyway. In this case, when the second username is looked up in the userdb, it overwrites (replaces) the entry in the hash table that is indexed by uid, freeing the DBusUserInfo. This results in both the key and the value in the hash table that is indexed by username becoming dangling pointers (use-after-free), leading to undefined behaviour, which is certainly not what we want to see when doing access control. An equivalent situation can occur with groups, in the rare case where a numeric group ID has two names (although I have not heard of this being done in practice). Solve this by reference-counting the data structure. There are up to three references in practice: one held temporarily while the lookup function is populating and storing it, one held by the hash table that is indexed by uid, and one held by the hash table that is indexed by name. Closes: dbus#305 Signed-off-by: Simon McVittie --- dbus/dbus-sysdeps-unix.h | 2 ++ dbus/dbus-userdb-util.c | 38 ++++++++++++++++++----- dbus/dbus-userdb.c | 67 ++++++++++++++++++++++++++++++---------- dbus/dbus-userdb.h | 6 ++-- 4 files changed, 86 insertions(+), 27 deletions(-) diff --git a/dbus/dbus-sysdeps-unix.h b/dbus/dbus-sysdeps-unix.h index 3bbf34d1..e86de6d9 100644 --- a/dbus/dbus-sysdeps-unix.h +++ b/dbus/dbus-sysdeps-unix.h @@ -105,6 +105,7 @@ typedef struct DBusGroupInfo DBusGroupInfo; */ struct DBusUserInfo { + size_t refcount; /**< Reference count */ dbus_uid_t uid; /**< UID */ dbus_gid_t primary_gid; /**< GID */ dbus_gid_t *group_ids; /**< Groups IDs, *including* above primary group */ @@ -118,6 +119,7 @@ struct DBusUserInfo */ struct DBusGroupInfo { + size_t refcount; /**< Reference count */ dbus_gid_t gid; /**< GID */ char *groupname; /**< Group name */ }; diff --git a/dbus/dbus-userdb-util.c b/dbus/dbus-userdb-util.c index 18621a17..1ca21eb7 100644 --- a/dbus/dbus-userdb-util.c +++ b/dbus/dbus-userdb-util.c @@ -45,6 +45,15 @@ * @{ */ +static DBusGroupInfo * +_dbus_group_info_ref (DBusGroupInfo *info) +{ + _dbus_assert (info->refcount > 0); + _dbus_assert (info->refcount < SIZE_MAX); + info->refcount++; + return info; +} + /** * Checks to see if the UID sent in is the console user * @@ -296,13 +305,14 @@ _dbus_user_database_lookup_group (DBusUserDatabase *db, dbus_set_error (error, DBUS_ERROR_NO_MEMORY, NULL); return NULL; } + info->refcount = 1; if (gid != DBUS_GID_UNSET) { if (!_dbus_group_info_fill_gid (info, gid, error)) { _DBUS_ASSERT_ERROR_IS_SET (error); - _dbus_group_info_free_allocated (info); + _dbus_group_info_unref (info); return NULL; } } @@ -311,7 +321,7 @@ _dbus_user_database_lookup_group (DBusUserDatabase *db, if (!_dbus_group_info_fill (info, groupname, error)) { _DBUS_ASSERT_ERROR_IS_SET (error); - _dbus_group_info_free_allocated (info); + _dbus_group_info_unref (info); return NULL; } } @@ -320,23 +330,35 @@ _dbus_user_database_lookup_group (DBusUserDatabase *db, gid = DBUS_GID_UNSET; groupname = NULL; - if (!_dbus_hash_table_insert_uintptr (db->groups, info->gid, info)) + if (_dbus_hash_table_insert_uintptr (db->groups, info->gid, info)) + { + _dbus_group_info_ref (info); + } + else { dbus_set_error (error, DBUS_ERROR_NO_MEMORY, NULL); - _dbus_group_info_free_allocated (info); + _dbus_group_info_unref (info); return NULL; } - if (!_dbus_hash_table_insert_string (db->groups_by_name, - info->groupname, - info)) + if (_dbus_hash_table_insert_string (db->groups_by_name, + info->groupname, + info)) + { + _dbus_group_info_ref (info); + } + else { _dbus_hash_table_remove_uintptr (db->groups, info->gid); + _dbus_group_info_unref (info); dbus_set_error (error, DBUS_ERROR_NO_MEMORY, NULL); return NULL; } - + + /* Release the original reference */ + _dbus_group_info_unref (info); + /* Return a borrowed reference to the DBusGroupInfo owned by the * two hash tables */ return info; diff --git a/dbus/dbus-userdb.c b/dbus/dbus-userdb.c index 78169e5b..ddf41a3c 100644 --- a/dbus/dbus-userdb.c +++ b/dbus/dbus-userdb.c @@ -42,34 +42,57 @@ * @{ */ +static DBusUserInfo * +_dbus_user_info_ref (DBusUserInfo *info) +{ + _dbus_assert (info->refcount > 0); + _dbus_assert (info->refcount < SIZE_MAX); + info->refcount++; + return info; +} + /** - * Frees the given #DBusUserInfo's members with _dbus_user_info_free() + * Decrements the reference count. If it reaches 0, + * frees the given #DBusUserInfo's members with _dbus_user_info_free() * and also calls dbus_free() on the block itself * * @param info the info */ void -_dbus_user_info_free_allocated (DBusUserInfo *info) +_dbus_user_info_unref (DBusUserInfo *info) { if (info == NULL) /* hash table will pass NULL */ return; + _dbus_assert (info->refcount > 0); + _dbus_assert (info->refcount < SIZE_MAX); + + if (--info->refcount > 0) + return; + _dbus_user_info_free (info); dbus_free (info); } /** - * Frees the given #DBusGroupInfo's members with _dbus_group_info_free() + * Decrements the reference count. If it reaches 0, + * frees the given #DBusGroupInfo's members with _dbus_group_info_free() * and also calls dbus_free() on the block itself * * @param info the info */ void -_dbus_group_info_free_allocated (DBusGroupInfo *info) +_dbus_group_info_unref (DBusGroupInfo *info) { if (info == NULL) /* hash table will pass NULL */ return; + _dbus_assert (info->refcount > 0); + _dbus_assert (info->refcount < SIZE_MAX); + + if (--info->refcount > 0) + return; + _dbus_group_info_free (info); dbus_free (info); } @@ -177,13 +200,14 @@ _dbus_user_database_lookup (DBusUserDatabase *db, dbus_set_error (error, DBUS_ERROR_NO_MEMORY, NULL); return NULL; } + info->refcount = 1; if (uid != DBUS_UID_UNSET) { if (!_dbus_user_info_fill_uid (info, uid, error)) { _DBUS_ASSERT_ERROR_IS_SET (error); - _dbus_user_info_free_allocated (info); + _dbus_user_info_unref (info); return NULL; } } @@ -192,7 +216,7 @@ _dbus_user_database_lookup (DBusUserDatabase *db, if (!_dbus_user_info_fill (info, username, error)) { _DBUS_ASSERT_ERROR_IS_SET (error); - _dbus_user_info_free_allocated (info); + _dbus_user_info_unref (info); return NULL; } } @@ -202,22 +226,33 @@ _dbus_user_database_lookup (DBusUserDatabase *db, username = NULL; /* insert into hash */ - if (!_dbus_hash_table_insert_uintptr (db->users, info->uid, info)) + if (_dbus_hash_table_insert_uintptr (db->users, info->uid, info)) + { + _dbus_user_info_ref (info); + } + else { dbus_set_error (error, DBUS_ERROR_NO_MEMORY, NULL); - _dbus_user_info_free_allocated (info); + _dbus_user_info_unref (info); return NULL; } - if (!_dbus_hash_table_insert_string (db->users_by_name, - info->username, - info)) + if (_dbus_hash_table_insert_string (db->users_by_name, + info->username, + info)) + { + _dbus_user_info_ref (info); + } + else { _dbus_hash_table_remove_uintptr (db->users, info->uid); dbus_set_error (error, DBUS_ERROR_NO_MEMORY, NULL); + _dbus_user_info_unref (info); return NULL; } - + + _dbus_user_info_unref (info); + /* Return a borrowed pointer to the DBusUserInfo owned by the * hash tables */ return info; @@ -569,24 +604,24 @@ _dbus_user_database_new (void) db->refcount = 1; db->users = _dbus_hash_table_new (DBUS_HASH_UINTPTR, - NULL, (DBusFreeFunction) _dbus_user_info_free_allocated); + NULL, (DBusFreeFunction) _dbus_user_info_unref); if (db->users == NULL) goto failed; db->groups = _dbus_hash_table_new (DBUS_HASH_UINTPTR, - NULL, (DBusFreeFunction) _dbus_group_info_free_allocated); + NULL, (DBusFreeFunction) _dbus_group_info_unref); if (db->groups == NULL) goto failed; db->users_by_name = _dbus_hash_table_new (DBUS_HASH_STRING, - NULL, NULL); + NULL, (DBusFreeFunction) _dbus_user_info_unref); if (db->users_by_name == NULL) goto failed; db->groups_by_name = _dbus_hash_table_new (DBUS_HASH_STRING, - NULL, NULL); + NULL, (DBusFreeFunction) _dbus_group_info_unref); if (db->groups_by_name == NULL) goto failed; diff --git a/dbus/dbus-userdb.h b/dbus/dbus-userdb.h index 479b2311..fcb515c8 100644 --- a/dbus/dbus-userdb.h +++ b/dbus/dbus-userdb.h @@ -76,10 +76,10 @@ const DBusGroupInfo* _dbus_user_database_lookup_group (DBusUserDatabase *db, dbus_gid_t gid, const DBusString *groupname, DBusError *error); + +void _dbus_user_info_unref (DBusUserInfo *info); DBUS_PRIVATE_EXPORT -void _dbus_user_info_free_allocated (DBusUserInfo *info); -DBUS_PRIVATE_EXPORT -void _dbus_group_info_free_allocated (DBusGroupInfo *info); +void _dbus_group_info_unref (DBusGroupInfo *info); #endif /* DBUS_USERDB_INCLUDES_PRIVATE */ DBUS_PRIVATE_EXPORT