diff options
Diffstat (limited to 'ipc/kdbus/names.c')
-rw-r--r-- | ipc/kdbus/names.c | 770 |
1 files changed, 770 insertions, 0 deletions
diff --git a/ipc/kdbus/names.c b/ipc/kdbus/names.c new file mode 100644 index 000000000..057f8061c --- /dev/null +++ b/ipc/kdbus/names.c @@ -0,0 +1,770 @@ +/* + * Copyright (C) 2013-2015 Kay Sievers + * Copyright (C) 2013-2015 Greg Kroah-Hartman <gregkh@linuxfoundation.org> + * Copyright (C) 2013-2015 Daniel Mack <daniel@zonque.org> + * Copyright (C) 2013-2015 David Herrmann <dh.herrmann@gmail.com> + * Copyright (C) 2013-2015 Linux Foundation + * Copyright (C) 2014-2015 Djalal Harouni <tixxdz@opendz.org> + * + * kdbus is free software; you can redistribute it and/or modify it under + * the terms of the GNU Lesser General Public License as published by the + * Free Software Foundation; either version 2.1 of the License, or (at + * your option) any later version. + */ + +#include <linux/ctype.h> +#include <linux/fs.h> +#include <linux/hash.h> +#include <linux/idr.h> +#include <linux/init.h> +#include <linux/module.h> +#include <linux/mutex.h> +#include <linux/rwsem.h> +#include <linux/sched.h> +#include <linux/slab.h> +#include <linux/uaccess.h> +#include <linux/uio.h> + +#include "bus.h" +#include "connection.h" +#include "endpoint.h" +#include "handle.h" +#include "item.h" +#include "names.h" +#include "notify.h" +#include "policy.h" + +struct kdbus_name_pending { + u64 flags; + struct kdbus_conn *conn; + struct kdbus_name_entry *name; + struct list_head conn_entry; + struct list_head name_entry; +}; + +static int kdbus_name_pending_new(struct kdbus_name_entry *e, + struct kdbus_conn *conn, u64 flags) +{ + struct kdbus_name_pending *p; + + kdbus_conn_assert_active(conn); + + p = kmalloc(sizeof(*p), GFP_KERNEL); + if (!p) + return -ENOMEM; + + p->flags = flags; + p->conn = conn; + p->name = e; + list_add_tail(&p->conn_entry, &conn->names_queue_list); + list_add_tail(&p->name_entry, &e->queue); + + return 0; +} + +static void kdbus_name_pending_free(struct kdbus_name_pending *p) +{ + if (!p) + return; + + list_del(&p->name_entry); + list_del(&p->conn_entry); + kfree(p); +} + +static struct kdbus_name_entry * +kdbus_name_entry_new(struct kdbus_name_registry *r, u32 hash, const char *name) +{ + struct kdbus_name_entry *e; + size_t namelen; + + namelen = strlen(name); + + e = kmalloc(sizeof(*e) + namelen + 1, GFP_KERNEL); + if (!e) + return ERR_PTR(-ENOMEM); + + e->name_id = ++r->name_seq_last; + e->flags = 0; + e->conn = NULL; + e->activator = NULL; + INIT_LIST_HEAD(&e->queue); + INIT_LIST_HEAD(&e->conn_entry); + hash_add(r->entries_hash, &e->hentry, hash); + memcpy(e->name, name, namelen + 1); + + return e; +} + +static void kdbus_name_entry_free(struct kdbus_name_entry *e) +{ + if (!e) + return; + + WARN_ON(!list_empty(&e->conn_entry)); + WARN_ON(!list_empty(&e->queue)); + WARN_ON(e->activator); + WARN_ON(e->conn); + + hash_del(&e->hentry); + kfree(e); +} + +static void kdbus_name_entry_set_owner(struct kdbus_name_entry *e, + struct kdbus_conn *conn, u64 flags) +{ + WARN_ON(e->conn); + + e->conn = kdbus_conn_ref(conn); + e->flags = flags; + atomic_inc(&conn->name_count); + list_add_tail(&e->conn_entry, &e->conn->names_list); +} + +static void kdbus_name_entry_remove_owner(struct kdbus_name_entry *e) +{ + WARN_ON(!e->conn); + + list_del_init(&e->conn_entry); + atomic_dec(&e->conn->name_count); + e->flags = 0; + e->conn = kdbus_conn_unref(e->conn); +} + +static void kdbus_name_entry_replace_owner(struct kdbus_name_entry *e, + struct kdbus_conn *conn, u64 flags) +{ + if (WARN_ON(!e->conn) || WARN_ON(conn == e->conn)) + return; + + kdbus_notify_name_change(conn->ep->bus, KDBUS_ITEM_NAME_CHANGE, + e->conn->id, conn->id, + e->flags, flags, e->name); + kdbus_name_entry_remove_owner(e); + kdbus_name_entry_set_owner(e, conn, flags); +} + +/** + * kdbus_name_is_valid() - check if a name is valid + * @p: The name to check + * @allow_wildcard: Whether or not to allow a wildcard name + * + * A name is valid if all of the following criterias are met: + * + * - The name has two or more elements separated by a period ('.') character. + * - All elements must contain at least one character. + * - Each element must only contain the ASCII characters "[A-Z][a-z][0-9]_-" + * and must not begin with a digit. + * - The name must not exceed KDBUS_NAME_MAX_LEN. + * - If @allow_wildcard is true, the name may end on '.*' + */ +bool kdbus_name_is_valid(const char *p, bool allow_wildcard) +{ + bool dot, found_dot = false; + const char *q; + + for (dot = true, q = p; *q; q++) { + if (*q == '.') { + if (dot) + return false; + + found_dot = true; + dot = true; + } else { + bool good; + + good = isalpha(*q) || (!dot && isdigit(*q)) || + *q == '_' || *q == '-' || + (allow_wildcard && dot && + *q == '*' && *(q + 1) == '\0'); + + if (!good) + return false; + + dot = false; + } + } + + if (q - p > KDBUS_NAME_MAX_LEN) + return false; + + if (dot) + return false; + + if (!found_dot) + return false; + + return true; +} + +/** + * kdbus_name_registry_new() - create a new name registry + * + * Return: a new kdbus_name_registry on success, ERR_PTR on failure. + */ +struct kdbus_name_registry *kdbus_name_registry_new(void) +{ + struct kdbus_name_registry *r; + + r = kmalloc(sizeof(*r), GFP_KERNEL); + if (!r) + return ERR_PTR(-ENOMEM); + + hash_init(r->entries_hash); + init_rwsem(&r->rwlock); + r->name_seq_last = 0; + + return r; +} + +/** + * kdbus_name_registry_free() - drop a name reg's reference + * @reg: The name registry, may be %NULL + * + * Cleanup the name registry's internal structures. + */ +void kdbus_name_registry_free(struct kdbus_name_registry *reg) +{ + if (!reg) + return; + + WARN_ON(!hash_empty(reg->entries_hash)); + kfree(reg); +} + +static struct kdbus_name_entry * +kdbus_name_find(struct kdbus_name_registry *reg, u32 hash, const char *name) +{ + struct kdbus_name_entry *e; + + lockdep_assert_held(®->rwlock); + + hash_for_each_possible(reg->entries_hash, e, hentry, hash) + if (strcmp(e->name, name) == 0) + return e; + + return NULL; +} + +/** + * kdbus_name_lookup_unlocked() - lookup name in registry + * @reg: name registry + * @name: name to lookup + * + * This looks up @name in the given name-registry and returns the + * kdbus_name_entry object. The caller must hold the registry-lock and must not + * access the returned object after releasing the lock. + * + * Return: Pointer to name-entry, or NULL if not found. + */ +struct kdbus_name_entry * +kdbus_name_lookup_unlocked(struct kdbus_name_registry *reg, const char *name) +{ + return kdbus_name_find(reg, kdbus_strhash(name), name); +} + +/** + * kdbus_name_acquire() - acquire a name + * @reg: The name registry + * @conn: The connection to pin this entry to + * @name: The name to acquire + * @flags: Acquisition flags (KDBUS_NAME_*) + * @return_flags: Pointer to return flags for the acquired name + * (KDBUS_NAME_*), may be %NULL + * + * Callers must ensure that @conn is either a privileged bus user or has + * sufficient privileges in the policy-db to own the well-known name @name. + * + * Return: 0 success, negative error number on failure. + */ +int kdbus_name_acquire(struct kdbus_name_registry *reg, + struct kdbus_conn *conn, const char *name, + u64 flags, u64 *return_flags) +{ + struct kdbus_name_entry *e; + u64 rflags = 0; + int ret = 0; + u32 hash; + + kdbus_conn_assert_active(conn); + + down_write(®->rwlock); + + if (!kdbus_conn_policy_own_name(conn, current_cred(), name)) { + ret = -EPERM; + goto exit_unlock; + } + + hash = kdbus_strhash(name); + e = kdbus_name_find(reg, hash, name); + if (!e) { + /* claim new name */ + + if (conn->activator_of) { + ret = -EINVAL; + goto exit_unlock; + } + + e = kdbus_name_entry_new(reg, hash, name); + if (IS_ERR(e)) { + ret = PTR_ERR(e); + goto exit_unlock; + } + + if (kdbus_conn_is_activator(conn)) { + e->activator = kdbus_conn_ref(conn); + conn->activator_of = e; + } + + kdbus_name_entry_set_owner(e, conn, flags); + kdbus_notify_name_change(e->conn->ep->bus, KDBUS_ITEM_NAME_ADD, + 0, e->conn->id, 0, e->flags, e->name); + } else if (e->conn == conn || e == conn->activator_of) { + /* connection already owns that name */ + ret = -EALREADY; + } else if (kdbus_conn_is_activator(conn)) { + /* activator claims existing name */ + + if (conn->activator_of) { + ret = -EINVAL; /* multiple names not allowed */ + } else if (e->activator) { + ret = -EEXIST; /* only one activator per name */ + } else { + e->activator = kdbus_conn_ref(conn); + conn->activator_of = e; + } + } else if (e->flags & KDBUS_NAME_ACTIVATOR) { + /* claim name of an activator */ + + kdbus_conn_move_messages(conn, e->activator, 0); + kdbus_name_entry_replace_owner(e, conn, flags); + } else if ((flags & KDBUS_NAME_REPLACE_EXISTING) && + (e->flags & KDBUS_NAME_ALLOW_REPLACEMENT)) { + /* claim name of a previous owner */ + + if (e->flags & KDBUS_NAME_QUEUE) { + /* move owner back to queue if they asked for it */ + ret = kdbus_name_pending_new(e, e->conn, e->flags); + if (ret < 0) + goto exit_unlock; + } + + kdbus_name_entry_replace_owner(e, conn, flags); + } else if (flags & KDBUS_NAME_QUEUE) { + /* add to waiting-queue of the name */ + + ret = kdbus_name_pending_new(e, conn, flags); + if (ret >= 0) + /* tell the caller that we queued it */ + rflags |= KDBUS_NAME_IN_QUEUE; + } else { + /* the name is busy, return a failure */ + ret = -EEXIST; + } + + if (ret == 0 && return_flags) + *return_flags = rflags; + +exit_unlock: + up_write(®->rwlock); + kdbus_notify_flush(conn->ep->bus); + return ret; +} + +static void kdbus_name_release_unlocked(struct kdbus_name_registry *reg, + struct kdbus_name_entry *e) +{ + struct kdbus_name_pending *p; + + lockdep_assert_held(®->rwlock); + + p = list_first_entry_or_null(&e->queue, struct kdbus_name_pending, + name_entry); + + if (p) { + /* give it to first active waiter in the queue */ + kdbus_name_entry_replace_owner(e, p->conn, p->flags); + kdbus_name_pending_free(p); + } else if (e->activator && e->activator != e->conn) { + /* hand it back to an active activator connection */ + kdbus_conn_move_messages(e->activator, e->conn, e->name_id); + kdbus_name_entry_replace_owner(e, e->activator, + KDBUS_NAME_ACTIVATOR); + } else { + /* release the name */ + kdbus_notify_name_change(e->conn->ep->bus, + KDBUS_ITEM_NAME_REMOVE, + e->conn->id, 0, e->flags, 0, e->name); + kdbus_name_entry_remove_owner(e); + kdbus_name_entry_free(e); + } +} + +static int kdbus_name_release(struct kdbus_name_registry *reg, + struct kdbus_conn *conn, + const char *name) +{ + struct kdbus_name_pending *p; + struct kdbus_name_entry *e; + int ret = 0; + + down_write(®->rwlock); + e = kdbus_name_find(reg, kdbus_strhash(name), name); + if (!e) { + ret = -ESRCH; + } else if (e->conn == conn) { + kdbus_name_release_unlocked(reg, e); + } else { + ret = -EADDRINUSE; + list_for_each_entry(p, &e->queue, name_entry) { + if (p->conn == conn) { + kdbus_name_pending_free(p); + ret = 0; + break; + } + } + } + up_write(®->rwlock); + + kdbus_notify_flush(conn->ep->bus); + return ret; +} + +/** + * kdbus_name_release_all() - remove all name entries of a given connection + * @reg: name registry + * @conn: connection + */ +void kdbus_name_release_all(struct kdbus_name_registry *reg, + struct kdbus_conn *conn) +{ + struct kdbus_name_pending *p; + struct kdbus_conn *activator = NULL; + struct kdbus_name_entry *e; + + down_write(®->rwlock); + + if (conn->activator_of) { + activator = conn->activator_of->activator; + conn->activator_of->activator = NULL; + } + + while ((p = list_first_entry_or_null(&conn->names_queue_list, + struct kdbus_name_pending, + conn_entry))) + kdbus_name_pending_free(p); + while ((e = list_first_entry_or_null(&conn->names_list, + struct kdbus_name_entry, + conn_entry))) + kdbus_name_release_unlocked(reg, e); + + up_write(®->rwlock); + + kdbus_conn_unref(activator); + kdbus_notify_flush(conn->ep->bus); +} + +/** + * kdbus_cmd_name_acquire() - handle KDBUS_CMD_NAME_ACQUIRE + * @conn: connection to operate on + * @argp: command payload + * + * Return: >=0 on success, negative error code on failure. + */ +int kdbus_cmd_name_acquire(struct kdbus_conn *conn, void __user *argp) +{ + const char *item_name; + struct kdbus_cmd *cmd; + int ret; + + struct kdbus_arg argv[] = { + { .type = KDBUS_ITEM_NEGOTIATE }, + { .type = KDBUS_ITEM_NAME, .mandatory = true }, + }; + struct kdbus_args args = { + .allowed_flags = KDBUS_FLAG_NEGOTIATE | + KDBUS_NAME_REPLACE_EXISTING | + KDBUS_NAME_ALLOW_REPLACEMENT | + KDBUS_NAME_QUEUE, + .argv = argv, + .argc = ARRAY_SIZE(argv), + }; + + if (!kdbus_conn_is_ordinary(conn)) + return -EOPNOTSUPP; + + ret = kdbus_args_parse(&args, argp, &cmd); + if (ret != 0) + return ret; + + item_name = argv[1].item->str; + if (!kdbus_name_is_valid(item_name, false)) { + ret = -EINVAL; + goto exit; + } + + /* + * Do atomic_inc_return here to reserve our slot, then decrement + * it before returning. + */ + if (atomic_inc_return(&conn->name_count) > KDBUS_CONN_MAX_NAMES) { + ret = -E2BIG; + goto exit_dec; + } + + ret = kdbus_name_acquire(conn->ep->bus->name_registry, conn, item_name, + cmd->flags, &cmd->return_flags); + +exit_dec: + atomic_dec(&conn->name_count); +exit: + return kdbus_args_clear(&args, ret); +} + +/** + * kdbus_cmd_name_release() - handle KDBUS_CMD_NAME_RELEASE + * @conn: connection to operate on + * @argp: command payload + * + * Return: >=0 on success, negative error code on failure. + */ +int kdbus_cmd_name_release(struct kdbus_conn *conn, void __user *argp) +{ + struct kdbus_cmd *cmd; + int ret; + + struct kdbus_arg argv[] = { + { .type = KDBUS_ITEM_NEGOTIATE }, + { .type = KDBUS_ITEM_NAME, .mandatory = true }, + }; + struct kdbus_args args = { + .allowed_flags = KDBUS_FLAG_NEGOTIATE, + .argv = argv, + .argc = ARRAY_SIZE(argv), + }; + + if (!kdbus_conn_is_ordinary(conn)) + return -EOPNOTSUPP; + + ret = kdbus_args_parse(&args, argp, &cmd); + if (ret != 0) + return ret; + + ret = kdbus_name_release(conn->ep->bus->name_registry, conn, + argv[1].item->str); + return kdbus_args_clear(&args, ret); +} + +static int kdbus_list_write(struct kdbus_conn *conn, + struct kdbus_conn *c, + struct kdbus_pool_slice *slice, + size_t *pos, + struct kdbus_name_entry *e, + bool write) +{ + struct kvec kvec[4]; + size_t cnt = 0; + int ret; + + /* info header */ + struct kdbus_info info = { + .size = 0, + .id = c->id, + .flags = c->flags, + }; + + /* fake the header of a kdbus_name item */ + struct { + u64 size; + u64 type; + u64 flags; + } h = {}; + + if (e && !kdbus_conn_policy_see_name_unlocked(conn, current_cred(), + e->name)) + return 0; + + kdbus_kvec_set(&kvec[cnt++], &info, sizeof(info), &info.size); + + /* append name */ + if (e) { + size_t slen = strlen(e->name) + 1; + + h.size = offsetof(struct kdbus_item, name.name) + slen; + h.type = KDBUS_ITEM_OWNED_NAME; + h.flags = e->flags; + + kdbus_kvec_set(&kvec[cnt++], &h, sizeof(h), &info.size); + kdbus_kvec_set(&kvec[cnt++], e->name, slen, &info.size); + cnt += !!kdbus_kvec_pad(&kvec[cnt], &info.size); + } + + if (write) { + ret = kdbus_pool_slice_copy_kvec(slice, *pos, kvec, + cnt, info.size); + if (ret < 0) + return ret; + } + + *pos += info.size; + return 0; +} + +static int kdbus_list_all(struct kdbus_conn *conn, u64 flags, + struct kdbus_pool_slice *slice, + size_t *pos, bool write) +{ + struct kdbus_conn *c; + size_t p = *pos; + int ret, i; + + hash_for_each(conn->ep->bus->conn_hash, i, c, hentry) { + bool added = false; + + /* skip monitors */ + if (kdbus_conn_is_monitor(c)) + continue; + + /* skip activators */ + if (!(flags & KDBUS_LIST_ACTIVATORS) && + kdbus_conn_is_activator(c)) + continue; + + /* all names the connection owns */ + if (flags & (KDBUS_LIST_NAMES | KDBUS_LIST_ACTIVATORS)) { + struct kdbus_name_entry *e; + + list_for_each_entry(e, &c->names_list, conn_entry) { + struct kdbus_conn *a = e->activator; + + if ((flags & KDBUS_LIST_ACTIVATORS) && + a && a != c) { + ret = kdbus_list_write(conn, a, slice, + &p, e, write); + if (ret < 0) { + mutex_unlock(&c->lock); + return ret; + } + + added = true; + } + + if (flags & KDBUS_LIST_NAMES || + kdbus_conn_is_activator(c)) { + ret = kdbus_list_write(conn, c, slice, + &p, e, write); + if (ret < 0) { + mutex_unlock(&c->lock); + return ret; + } + + added = true; + } + } + } + + /* queue of names the connection is currently waiting for */ + if (flags & KDBUS_LIST_QUEUED) { + struct kdbus_name_pending *q; + + list_for_each_entry(q, &c->names_queue_list, + conn_entry) { + ret = kdbus_list_write(conn, c, slice, &p, + q->name, write); + if (ret < 0) { + mutex_unlock(&c->lock); + return ret; + } + + added = true; + } + } + + /* nothing added so far, just add the unique ID */ + if (!added && flags & KDBUS_LIST_UNIQUE) { + ret = kdbus_list_write(conn, c, slice, &p, NULL, write); + if (ret < 0) + return ret; + } + } + + *pos = p; + return 0; +} + +/** + * kdbus_cmd_list() - handle KDBUS_CMD_LIST + * @conn: connection to operate on + * @argp: command payload + * + * Return: >=0 on success, negative error code on failure. + */ +int kdbus_cmd_list(struct kdbus_conn *conn, void __user *argp) +{ + struct kdbus_name_registry *reg = conn->ep->bus->name_registry; + struct kdbus_pool_slice *slice = NULL; + struct kdbus_cmd_list *cmd; + size_t pos, size; + int ret; + + struct kdbus_arg argv[] = { + { .type = KDBUS_ITEM_NEGOTIATE }, + }; + struct kdbus_args args = { + .allowed_flags = KDBUS_FLAG_NEGOTIATE | + KDBUS_LIST_UNIQUE | + KDBUS_LIST_NAMES | + KDBUS_LIST_ACTIVATORS | + KDBUS_LIST_QUEUED, + .argv = argv, + .argc = ARRAY_SIZE(argv), + }; + + ret = kdbus_args_parse(&args, argp, &cmd); + if (ret != 0) + return ret; + + /* lock order: domain -> bus -> ep -> names -> conn */ + down_read(®->rwlock); + down_read(&conn->ep->bus->conn_rwlock); + down_read(&conn->ep->policy_db.entries_rwlock); + + /* size of records */ + size = 0; + ret = kdbus_list_all(conn, cmd->flags, NULL, &size, false); + if (ret < 0) + goto exit_unlock; + + if (size == 0) { + kdbus_pool_publish_empty(conn->pool, &cmd->offset, + &cmd->list_size); + } else { + slice = kdbus_pool_slice_alloc(conn->pool, size, false); + if (IS_ERR(slice)) { + ret = PTR_ERR(slice); + slice = NULL; + goto exit_unlock; + } + + /* copy the records */ + pos = 0; + ret = kdbus_list_all(conn, cmd->flags, slice, &pos, true); + if (ret < 0) + goto exit_unlock; + + WARN_ON(pos != size); + kdbus_pool_slice_publish(slice, &cmd->offset, &cmd->list_size); + } + + if (kdbus_member_set_user(&cmd->offset, argp, typeof(*cmd), offset) || + kdbus_member_set_user(&cmd->list_size, argp, + typeof(*cmd), list_size)) + ret = -EFAULT; + +exit_unlock: + up_read(&conn->ep->policy_db.entries_rwlock); + up_read(&conn->ep->bus->conn_rwlock); + up_read(®->rwlock); + kdbus_pool_slice_release(slice); + return kdbus_args_clear(&args, ret); +} |