This code is based on the original filesystem context based configuration parser by David Howells, but lifted up so it can apply to anything rather than only filesystem contexts. Signed-off-by: James Bottomley <James.Bottomley@xxxxxxxxxxxxxxxxxxxxx> --- fs/Makefile | 3 +- fs/configfd.c | 450 ++++++++++++++++++++++++++++++++++++++++++ include/linux/configfd.h | 61 ++++++ include/uapi/linux/configfd.h | 20 ++ 4 files changed, 533 insertions(+), 1 deletion(-) create mode 100644 fs/configfd.c create mode 100644 include/linux/configfd.h create mode 100644 include/uapi/linux/configfd.h diff --git a/fs/Makefile b/fs/Makefile index 14231b4cf383..569563f6c0d5 100644 --- a/fs/Makefile +++ b/fs/Makefile @@ -13,7 +13,8 @@ obj-y := open.o read_write.o file_table.o super.o \ seq_file.o xattr.o libfs.o fs-writeback.o \ pnode.o splice.o sync.o utimes.o d_path.o \ stack.o fs_struct.o statfs.o fs_pin.o nsfs.o \ - fs_types.o fs_context.o fs_parser.o fsopen.o + fs_types.o fs_context.o fs_parser.o fsopen.o \ + configfd.o ifeq ($(CONFIG_BLOCK),y) obj-y += buffer.o block_dev.o direct-io.o mpage.o diff --git a/fs/configfd.c b/fs/configfd.c new file mode 100644 index 000000000000..7f2c750ac7e3 --- /dev/null +++ b/fs/configfd.c @@ -0,0 +1,450 @@ +// SPDX-License-Identifier: GPL-2.0-or-later +/* + * Generic configuration file descriptor handling + * + * Copyright (C) 2019 James.Bottomley@xxxxxxxxxxxxxxxxxxxxx + */ + +#include <linux/anon_inodes.h> +#include <linux/configfd.h> +#include <linux/namei.h> +#include <linux/slab.h> +#include <linux/syscalls.h> +#include <linux/rwlock.h> +#include <linux/uaccess.h> + + +static struct configfd_type *configfds; +static DEFINE_RWLOCK(configfds_lock); + +static ssize_t configfd_read(struct file *file, + char __user *buf, size_t len, loff_t *pos) +{ + struct configfd_context *cfc = file->private_data; + + return logger_read(cfc->log, buf, len); +} + +static void configfd_type_put(const struct configfd_type *cft) +{ + module_put(cft->owner); +} + +static void configfd_context_free(struct configfd_context *cfc) +{ + if (cfc->cft->ops->free) + cfc->cft->ops->free(cfc); + logger_put(&cfc->log); + configfd_type_put(cfc->cft); + kfree(cfc); +} + +static int configfd_release(struct inode *inode, struct file *file) +{ + struct configfd_context *cfc = file->private_data; + + if (cfc) { + file->private_data = NULL; + configfd_context_free(cfc); + } + return 0; +} + +const struct file_operations configfd_context_fops = { + .read = configfd_read, + .release = configfd_release, + .llseek = no_llseek, +}; + +static int configfd_create_fd(struct configfd_context *cfc, + unsigned int flags) +{ + int fd; + + fd = anon_inode_getfd("[configfd]", &configfd_context_fops, cfc, + O_RDWR | flags); + if (fd < 0) + configfd_context_free(cfc); + return fd; +} + +static struct configfd_type **configfd_type_find(const char *name) +{ + struct configfd_type **c; + + for (c = &configfds; *c; c = &(*c)->next) { + if (strcmp((*c)->name, name) == 0) + break; + } + + return c; +} + +static struct configfd_type *configfd_type_get(const char *name) +{ + struct configfd_type *cft; + + read_lock(&configfds_lock); + cft = *(configfd_type_find(name)); + if (cft && !try_module_get(cft->owner)) + cft = NULL; + read_unlock(&configfds_lock); + + return cft; +} + +struct configfd_context *configfd_context_alloc(const struct configfd_type *cft, + unsigned int op) +{ + struct configfd_context *cfc; + struct logger *log; + int ret; + + cfc = kzalloc(sizeof(*cfc) + cft->data_size, GFP_KERNEL); + if (!cfc) + return ERR_PTR(-ENOMEM); + + if (cft->data_size) + cfc->data = &cfc[1]; + + cfc->cft = cft; + cfc->op = op; + log = logger_alloc(cft->owner); + if (IS_ERR(log)) { + ret = PTR_ERR(log); + goto out_free; + } + + cfc->log = log; + if (cft->ops->alloc) { + ret = cft->ops->alloc(cfc); + if (ret) + goto out_put; + } + + return cfc; + out_put: + logger_put(&cfc->log); + out_free: + kfree(cfc); + return ERR_PTR(ret); +} + +int kern_configfd_open(const char *config_name, unsigned int flags, + unsigned int op) +{ + const struct configfd_type *cft; + struct configfd_context *cfc; + + if (flags & ~O_CLOEXEC) + return -EINVAL; + if (op != CONFIGFD_CMD_CREATE && op != CONFIGFD_CMD_RECONFIGURE) + return -EINVAL; + + cft = configfd_type_get(config_name); + if (!cft) + return -ENODEV; + cfc = configfd_context_alloc(cft, op); + if (IS_ERR(cfc)) { + configfd_type_put(cft); + return PTR_ERR(cfc); + } + + return configfd_create_fd(cfc, flags); +} + +long ksys_configfd_open(const char __user *_config_name, unsigned int flags, + unsigned int op) +{ + const char *config_name = strndup_user(_config_name, PAGE_SIZE); + int ret; + + if (IS_ERR(config_name)) + return PTR_ERR(config_name); + ret = kern_configfd_open(config_name, flags, op); + kfree(config_name); + + return ret; +} + +SYSCALL_DEFINE3(configfd_open, + const char __user *, _config_name, + unsigned int, flags, + unsigned int, op) +{ + return ksys_configfd_open(_config_name, flags, op); +} + +int kern_configfd_action(int fd, struct configfd_param *p) +{ + struct fd f = fdget(fd); + struct configfd_context *cfc; + const struct configfd_ops *ops; + int ret = -EINVAL; + /* upper 24 bits are available to consumers */ + u8 our_cmd = p->cmd & 0xff; + + if (!f.file) + return -EBADF; + if (f.file->f_op != &configfd_context_fops) + goto out_f; + cfc = f.file->private_data; + + ops = cfc->cft->ops; + + /* check allowability */ + ret = -EOPNOTSUPP; + switch (our_cmd) { + case CONFIGFD_SET_FLAG: + case CONFIGFD_SET_STRING: + case CONFIGFD_SET_BINARY: + case CONFIGFD_SET_PATH: + case CONFIGFD_SET_PATH_EMPTY: + case CONFIGFD_SET_INT: + if (!ops->set) + goto out_f; + break; + case CONFIGFD_GET_FD: + if (!ops->get) + goto out_f; + break; + case CONFIGFD_CMD_CREATE: + case CONFIGFD_CMD_RECONFIGURE: + if (our_cmd != cfc->op) { + logger_err(cfc->log, "Wrong operation, we were opened for %d", cfc->op); + goto out_f; + } + if (!ops->act) + goto out_f; + break; + default: + break; + } + + /* + * Execute + */ + switch (our_cmd) { + case CONFIGFD_SET_STRING: + case CONFIGFD_SET_BINARY: + case CONFIGFD_SET_PATH: + case CONFIGFD_SET_PATH_EMPTY: + case CONFIGFD_SET_FD: + case CONFIGFD_SET_FLAG: + case CONFIGFD_SET_INT: + ret = ops->set(cfc, p); + break; + case CONFIGFD_GET_FD: + ret = ops->get(cfc, p); + if (ret == 0) { + int fd; + + fd = get_unused_fd_flags(p->aux & O_CLOEXEC); + if (fd < 0) { + ret = fd; + break; + } + fd_install(fd, p->file); + p->file = NULL; /* consume the file */ + p->aux = fd; + } + break; + case CONFIGFD_CMD_RECONFIGURE: + case CONFIGFD_CMD_CREATE: + ret = ops->act(cfc, p->cmd); + break; + default: + break; + } +out_f: + fdput(f); + return ret; +} + +long ksys_configfd_action(int fd, unsigned int cmd, const char __user *_key, + void __user *_value, int aux) +{ + struct configfd_param param = { + .cmd = cmd, + }; + u8 our_cmd = cmd & 0xff; + int ret; + + /* check parameters required for action */ + switch (our_cmd) { + case CONFIGFD_SET_FLAG: + if (!_key || _value || aux) + return -EINVAL; + break; + case CONFIGFD_SET_STRING: + case CONFIGFD_GET_FD: + if (!_key || !_value) + return -EINVAL; + break; + case CONFIGFD_SET_BINARY: + if (!_key || !_value || aux <= 0 || aux > 1024 * 1024) + return -EINVAL; + break; + case CONFIGFD_SET_PATH: + case CONFIGFD_SET_PATH_EMPTY: + if (!_key || !_value || (aux != AT_FDCWD && aux < 0)) + return -EINVAL; + break; + case CONFIGFD_SET_FD: + if (!_key || _value || aux < 0) + return -EINVAL; + break; + case CONFIGFD_SET_INT: + if (!_key) + return -EINVAL; + break; + case CONFIGFD_CMD_CREATE: + case CONFIGFD_CMD_RECONFIGURE: + if (_key || _value || aux) + return -EINVAL; + break; + default: + return -EOPNOTSUPP; + } + + if (_key) { + param.key = strndup_user(_key, 256); + if (IS_ERR(param.key)) + return PTR_ERR(param.key); + } + + /* now pull the parameters into the kernel */ + switch (our_cmd) { + case CONFIGFD_SET_STRING: + param.string = strndup_user(_value, 256); + if (IS_ERR(param.string)) { + ret = PTR_ERR(param.string); + goto out_key; + } + param.size = strlen(param.string); + break; + case CONFIGFD_SET_BINARY: + param.size = aux; + param.blob = memdup_user_nul(_value, aux); + if (IS_ERR(param.blob)) { + ret = PTR_ERR(param.blob); + goto out_key; + } + break; + case CONFIGFD_SET_PATH: + param.name = getname_flags(_value, 0, NULL); + if (IS_ERR(param.name)) { + ret = PTR_ERR(param.name); + goto out_key; + } + param.aux = aux; + param.size = strlen(param.name->name); + break; + case CONFIGFD_SET_PATH_EMPTY: + param.name = getname_flags(_value, LOOKUP_EMPTY, NULL); + if (IS_ERR(param.name)) { + ret = PTR_ERR(param.name); + goto out_key; + } + param.aux = aux; + param.size = strlen(param.name->name); + break; + case CONFIGFD_SET_FD: + ret = -EBADF; + param.file = fget_raw(aux); + if (!param.file) + goto out_key; + break; + case CONFIGFD_SET_INT: + param.aux = aux; + break; + default: + break; + } + ret = kern_configfd_action(fd, ¶m); + /* clean up unconsumed parameters */ + switch (our_cmd) { + case CONFIGFD_SET_STRING: + case CONFIGFD_SET_BINARY: + kfree(param.string); + break; + case CONFIGFD_SET_PATH: + case CONFIGFD_SET_PATH_EMPTY: + if (param.name) + putname(param.name); + break; + case CONFIGFD_GET_FD: + if (!ret) + ret = put_user(param.aux, (int __user *)_value); + /* FALL THROUGH */ + case CONFIGFD_SET_FD: + if (param.file) + fput(param.file); + break; + default: + break; + } + out_key: + kfree(param.key); + + return ret; +} + + +SYSCALL_DEFINE5(configfd_action, + int, fd, unsigned int, cmd, + const char __user *, _key, + void __user *, _value, + int, aux) +{ + return ksys_configfd_action(fd, cmd, _key, _value, aux); +} + +int configfd_type_register(struct configfd_type *cft) +{ + int ret = 0; + struct configfd_type **c; + + if (WARN(cft->next, + "BUG: registering already registered configfd_type: %s", + cft->name)) + return -EBUSY; + + if (WARN(cft->ops == NULL, + "BUG: configfd_type has no ops set: %s", cft->name)) + return -EINVAL; + + if (WARN(cft->ops->alloc && (!cft->ops->free), + "BUG: if configfd ops alloc is set, free must also be")) + return -EINVAL; + + if (WARN(cft->name == NULL || cft->name[0] == '\0', + "BUG: configfd_type has no name")) + return -EINVAL; + + write_lock(&configfds_lock); + c = configfd_type_find(cft->name); + if (WARN(*c, "BUG: configfd_type name already exists: %s", + cft->name)) + ret = -EBUSY; + else + *c = cft; + write_unlock(&configfds_lock); + + return ret; +} + +void configfd_type_unregister(struct configfd_type *cft) +{ + struct configfd_type **c; + + write_lock(&configfds_lock); + c = configfd_type_find(cft->name); + if (WARN(*c != cft, "BUG: trying to register %s from wrong structure", + cft->name)) + goto out; + *c = cft->next; + cft->next = NULL; + out: + write_unlock(&configfds_lock); +} diff --git a/include/linux/configfd.h b/include/linux/configfd.h new file mode 100644 index 000000000000..2a2efd7eabe2 --- /dev/null +++ b/include/linux/configfd.h @@ -0,0 +1,61 @@ +/* SPDX-License-Identifier: GPL-2.0-or-later */ + +#ifndef _LINUX_CONFIGFD_H +#define _LINUX_CONFIGFD_H + +#include <linux/logger.h> + +#include <uapi/linux/configfd.h> + +struct configfd_context; + +struct configfd_param { + const char *key; + union { + char *string; + void *blob; + struct filename *name; + struct file *file; + }; + int aux; + unsigned int cmd; + size_t size; +}; + +struct configfd_ops { + int (*alloc)(struct configfd_context *cfc); + void (*free)(const struct configfd_context *cfc); + int (*set)(const struct configfd_context *cfc, + struct configfd_param *p); + int (*get)(const struct configfd_context *cfc, + struct configfd_param *p); + int (*act)(const struct configfd_context *cfc, unsigned int cmd); +}; + +struct configfd_type { + const char *name; + size_t data_size; + struct module *owner; + struct configfd_ops *ops; + struct configfd_type *next; +}; + +struct configfd_context { + const struct configfd_type *cft; + struct logger *log; + void *data; + unsigned int op; +}; + +int configfd_type_register(struct configfd_type *cft); +void configfd_type_unregister(struct configfd_type *cft); + +long ksys_configfd_open(const char __user *config_name, unsigned int flags, + unsigned int op); +long ksys_configfd_action(int fd, unsigned int cmd, const char __user *key, + void __user *value, int aux); +int kern_configfd_action(int fd, struct configfd_param *p); +int kern_configfd_open(const char *name, unsigned int flags, + unsigned int op); + +#endif diff --git a/include/uapi/linux/configfd.h b/include/uapi/linux/configfd.h new file mode 100644 index 000000000000..3e54cfef0182 --- /dev/null +++ b/include/uapi/linux/configfd.h @@ -0,0 +1,20 @@ +/* SPDX-License-Identifier: GPL-2.0-or-later WITH Linux-syscall-note */ + +#ifndef _UAPI_LINUX_CONFIGFD_H +#define _UAPI_LINUX_CONFIGFD_H + +enum configfd_cmd { + CONFIGFD_SET_FLAG = 0, + CONFIGFD_SET_STRING = 1, + CONFIGFD_SET_BINARY = 2, + CONFIGFD_SET_PATH = 3, + CONFIGFD_SET_PATH_EMPTY = 4, + CONFIGFD_SET_FD = 5, + CONFIGFD_CMD_CREATE = 6, + CONFIGFD_CMD_RECONFIGURE = 7, + /* gap for 30 other commands */ + CONFIGFD_SET_INT = 38, + CONFIGFD_GET_FD = 39, +}; + +#endif -- 2.16.4