test: add sysdata loader tester -- late This add a load tester for the new extensible sysdata file loader, part firmware_class. The usermode helper can be safely ignored here. The sysdata API one main dynamic test tmain interfaces. Signed-off-by: Luis R. Rodriguez <mcgrof@kernel.org>
diff --git a/lib/Makefile b/lib/Makefile index 7bd6fd4..5d1e97e 100644 --- a/lib/Makefile +++ b/lib/Makefile
@@ -47,7 +47,7 @@ obj-$(CONFIG_TEST_HEXDUMP) += test_hexdump.o obj-y += kstrtox.o obj-$(CONFIG_TEST_BPF) += test_bpf.o -obj-$(CONFIG_TEST_FIRMWARE) += test_firmware.o +obj-$(CONFIG_TEST_FIRMWARE) += test_firmware.o test_sysdata.o obj-$(CONFIG_TEST_KASAN) += test_kasan.o obj-$(CONFIG_TEST_KSTRTOX) += test-kstrtox.o obj-$(CONFIG_TEST_LKM) += test_module.o
diff --git a/lib/test_sysdata.c b/lib/test_sysdata.c new file mode 100644 index 0000000..f64e87fe --- /dev/null +++ b/lib/test_sysdata.c
@@ -0,0 +1,1055 @@ +/* + * System Data test interface + * + * Copyright (C) 2016 Luis R. Rodriguez <mcgrof@kernel.org> + * + * This program is free software; you can redistribute it and/or + * modify it under the terms of the GNU General Public Licence + * as published by the Free Software Foundation; either version + * 2 of the Licence, or (at your option) any later version. + * + * This module provides an interface to trigger and test system data API + * through a series of configurations and a few triggers. This driver + * lacks any extra dependencies, and will not normally be loaded by the + * system unless explicitly requested by name. You can also build this + * driver into your kernel. + * + * Although all configurations are already written for and will be supported + * for this test driver, ideally we should strive to see what mechanisms we + * can put in place to instead automatically generate this sort of test + * interface, test cases, and infer results. Its a simple enough interface that + * should hopefully enable more exploring in this area. + */ +#define pr_fmt(fmt) KBUILD_MODNAME ": " fmt + +#include <linux/init.h> +#include <linux/list.h> +#include <linux/module.h> +#include <linux/printk.h> +#include <linux/completion.h> +#include <linux/sysdata.h> +#include <linux/device.h> +#include <linux/fs.h> +#include <linux/miscdevice.h> +#include <linux/slab.h> +#include <linux/uaccess.h> +#include <linux/async.h> +#include <linux/delay.h> +#include <linux/vmalloc.h> + +/* Used for the fallback default to test against */ +#define TEST_SYSDATA "test-sysdata.bin" + +/* + * For device allocation / registration + */ +static DEFINE_MUTEX(reg_dev_mutex); +static LIST_HEAD(reg_test_devs); + +/* + * num_test_devs actually represents the *next* ID of the next + * device we will allow to create. + */ +int num_test_devs; + +/** + * test_config - represents configuration for the sysdata API + * + * @name: the name of the primary sysdata file to look for + * @default_name: a fallback example, used to test the optional callback + * mechanism. + * @async: true if you want to trigger an async request. This will use + * sysdata_file_request_async(). If false the synchronous call will + * be used, sysdata_file_request(). + * @optional: whether or not the sysdata is optional refer to the + * struct sysdata_file_desc @optional field for more information. + * @keep: whether or not we wish to free the sysdata on our own, refer to + * the struct sysdata_file_desc @keep field for more information. + * @enable_opt_cb: whether or not the optional callback should be set + * on a trigger. There is no equivalent setting on the struct + * sysdata_file_desc as this is implementation specific, and in + * in sysdata API its explicit if you had defined an optional call + * back for your descriptor with either SYSDATA_SYNC_OPT_CB() or + * SYSDATA_ASYNC_OPT_CB(). Since the descriptor is a const we have + * no option but to use a flag and two const structs to decide which + * one we should use. + * @test_result: a test may use this to collect the result from the call + * of the sysdata_file_request_async() or sysdata_file_request() calls + * used in their tests. Note that for async calls this typically will + * be a successful result (0) unless of course you've have sent in + * a bogus descriptor, or the system is out of memory. Tests against + * the callbacks can only be implementation specific, so we don't + * test for that for now but it may make sense to build tests cases + * against a series of semantically similar family of callbacks that + * generally represents usage in the kernel. Synchronous calls return + * bogus error checks against the descriptor as well, but also return + * the result of the work from the callbacks. You can therefore rely + * on sync calls if you really want to test for the callback results + * as well. Errors you can expect: + * + * API specific: + * + * 0: success for sync, for async it means request was sent + * -EINVAL: invalid descriptor or request + * -ENOENT: files not found + * + * System environment: + * + * -ENOMEM: memory pressure on system + * -ENODEV: out of number of devices to test + * + * The ordering of elements in this struct must match the exact order of the + * elements in the ATTRIBUTE_GROUPS(test_dev_config), this is done to know + * what corresponding field each device attribute configuration entry maps + * to what struct member on test_alloc_dev_attrs(). + */ +struct test_config { + char *name; + char *default_name; + bool async; + bool optional; + bool keep; + bool enable_opt_cb; + + int test_result; +}; + +/** + * test_sysdata_private - private device driver sysdata representation + * + * @size: size of the data copied, in bytes + * @data: the actual data we copied over from sysdata + * @written: true if a callback managed to copy data over to the device + * successfully. Since different callbacks are used for this purpose + * having the data written does not necessarily mean a test case + * completed successfully. Each tests case has its own specific + * goals. + * + * Private representation of buffer where we put the device system data */ +struct test_sysdata_private { + size_t size; + u8 *data; + bool written; +}; + +/** + * sysdata_test_device - test device to help test sysdata + * + * @dev_idx: unique ID for test device + * @config: this keeps the device's own configuration. Instead of creating + * different triggers for all possible test cases we can think of in + * kernel, we expose a set possible device attributes for tuning the + * sysdata API and we to let you tune them in userspace. We then just + * provide one trigger. + * @test_sysdata: internal private representation of a storage area + * a driver might typically use to stuff firmware / sysdata. + * @misc_dev: we use a misc device under the hood + * @dev: pointer to misc_dev's own struct device + * @sysdata_mutex: for access into the @sysdata, the fake storage location for + * the system data we copy. + * @config_mutex: + * @trigger_mutex: all triggers are mutually exclusive when testing. To help + * enable testing you can create a different device, each device has its + * own set of protections, mimicking real devices. + * list: needed to be part of the reg_test_devs + */ +struct sysdata_test_device { + int dev_idx; + struct test_config config; + struct test_sysdata_private test_sysdata; + struct miscdevice misc_dev; + struct device *dev; + + struct mutex sysdata_mutex; + struct mutex config_mutex; + struct mutex trigger_mutex; + struct list_head list; +}; + +static struct miscdevice *dev_to_misc_dev(struct device *dev) +{ + return dev_get_drvdata(dev); +} + +static +struct sysdata_test_device *misc_dev_to_test_dev(struct miscdevice *misc_dev) +{ + return container_of(misc_dev, struct sysdata_test_device, misc_dev); +} + +static struct sysdata_test_device *dev_to_test_dev(struct device *dev) +{ + struct miscdevice *misc_dev; + + misc_dev = dev_to_misc_dev(dev); + + return misc_dev_to_test_dev(misc_dev); +} + +static ssize_t test_fw_misc_read(struct file *f, char __user *buf, + size_t size, loff_t *offset) +{ + struct miscdevice *misc_dev = f->private_data; + struct sysdata_test_device *test_dev = misc_dev_to_test_dev(misc_dev); + struct test_sysdata_private *test_sysdata = &test_dev->test_sysdata; + ssize_t rc = 0; + + mutex_lock(&test_dev->sysdata_mutex); + if (test_sysdata->written) + rc = simple_read_from_buffer(buf, size, offset, + test_sysdata->data, + test_sysdata->size); + mutex_unlock(&test_dev->sysdata_mutex); + + return rc; +} + +static const struct file_operations test_fw_fops = { + .owner = THIS_MODULE, + .read = test_fw_misc_read, +}; + +static void free_test_sysdata(struct test_sysdata_private *test_sysdata) +{ + kfree(test_sysdata->data); + test_sysdata->data = NULL; + test_sysdata->size = 0; + test_sysdata->written = false; +} + +static int test_load_sysdata(struct sysdata_test_device *test_dev, + const struct sysdata_file *sysdata) +{ + struct test_sysdata_private *test_sysdata = &test_dev->test_sysdata; + int ret = 0; + + if (!sysdata) + return -ENOENT; + + mutex_lock(&test_dev->sysdata_mutex); + + free_test_sysdata(test_sysdata); + + test_sysdata->data = kzalloc(sysdata->size, GFP_KERNEL); + if (!test_sysdata->data) { + ret = -ENOMEM; + goto out; + } + + memcpy(test_sysdata->data, sysdata->data, sysdata->size); + test_sysdata->size = sysdata->size; + test_sysdata->written = true; + + dev_info(test_dev->dev, "loaded: %zu\n", test_sysdata->size); + +out: + mutex_unlock(&test_dev->sysdata_mutex); + + return ret; +} + +static int sync_found_cb(void *context, const struct sysdata_file *sysdata) +{ + struct sysdata_test_device *test_dev = context; + int ret; + + ret = test_load_sysdata(test_dev, sysdata); + if (ret) + dev_info(test_dev->dev, "unable to write sysdata: %d\n", ret); + return ret; +} + +static ssize_t config_show(struct device *dev, + struct device_attribute *attr, + char *buf) +{ + struct sysdata_test_device *test_dev = dev_to_test_dev(dev); + struct test_config *config = &test_dev->config; + int len = 0; + + mutex_lock(&test_dev->config_mutex); + + len += sprintf(buf, "Custom trigger configuration for: %s\n", + dev_name(dev)); + + if (config->default_name) + len += sprintf(buf+len, "default name:\t%s\n", + config->default_name); + else + len += sprintf(buf+len, "default name:\tEMTPY\n"); + + if (config->name) + len += sprintf(buf+len, "name:\t\t%s\n", config->name); + else + len += sprintf(buf+len, "name:\t\tEMPTY\n"); + + len += sprintf(buf+len, "type:\t\t%s\n", + config->async ? "async" : "sync"); + len += sprintf(buf+len, "optional:\t%s\n", + config->optional ? "true" : "false"); + len += sprintf(buf+len, "enable_opt_cb:\t%s\n", + config->enable_opt_cb ? "true" : "false"); + len += sprintf(buf+len, "keep:\t\t%s\n", + config->keep ? "true" : "false"); + + mutex_unlock(&test_dev->config_mutex); + + return len; +} +static DEVICE_ATTR_RO(config); + +static int config_load_data(struct sysdata_test_device *test_dev, + const struct sysdata_file *sysdata) +{ + struct test_config *config = &test_dev->config; + int ret; + + ret = test_load_sysdata(test_dev, sysdata); + if (ret) { + if (!config->optional) + dev_info(test_dev->dev, "unable to write sysdata\n"); + } + if (config->keep) { + release_sysdata_file(sysdata); + sysdata = NULL; + } + return ret; +} + +static int config_req_default(struct sysdata_test_device *test_dev) +{ + struct test_config *config = &test_dev->config; + int rc; + /* + * Note: we don't chain config->optional here, we make this + * fallback file a requirement. It doesn't make much sense to test + * chaining further as the optional callback is implementation + * specific, by testing it once we test it for any possible + * chains. We provide this as an example of what people can do + * and use a default non-optional fallback. + */ + const struct sysdata_file_desc sysdata_desc = { + SYSDATA_DEFAULT_SYNC(sync_found_cb, test_dev), + }; + + if (config->async) + dev_info(test_dev->dev, + "loading default fallback '%s' using sync request now\n", + config->default_name); + else + dev_info(test_dev->dev, + "loading default fallback '%s'\n", + config->default_name); + + rc = sysdata_file_request(config->default_name, + &sysdata_desc, test_dev->dev); + if (rc) + dev_info(test_dev->dev, + "load of default '%s' failed: %d\n", + config->default_name, rc); + + return rc; +} + +/* + * This is the default sync fallback callback, as a fallback this + * then uses a sync request. + */ +static int config_sync_req_default_cb(void *context) +{ + struct sysdata_test_device *test_dev = context; + int rc; + + rc = config_req_default(test_dev); + + return rc; + + /* Leave all the error checking for the main caller */ +} + +/* + * This is the default config->async fallback callback, as a fallback this + * then uses a sync request. + */ +static void config_async_req_default_cb(void *context) +{ + struct sysdata_test_device *test_dev = context; + + config_req_default(test_dev); + + /* Leave all the error checking for the main caller */ +} + +static int config_sync_req_cb(void *context, + const struct sysdata_file *sysdata) +{ + struct sysdata_test_device *test_dev = context; + + return config_load_data(test_dev, sysdata); +} + +static int trigger_config_sync(struct sysdata_test_device *test_dev) +{ + struct test_config *config = &test_dev->config; + int rc; + const struct sysdata_file_desc sysdata_desc_default = { + SYSDATA_DEFAULT_SYNC(config_sync_req_cb, test_dev), + .optional = config->optional, + .keep = config->keep, + }; + const struct sysdata_file_desc sysdata_desc_opt_cb = { + SYSDATA_DEFAULT_SYNC(config_sync_req_cb, test_dev), + SYSDATA_SYNC_OPT_CB(config_sync_req_default_cb, test_dev), + .optional = config->optional, + .keep = config->keep, + }; + const struct sysdata_file_desc *sysdata_desc; + + if (config->enable_opt_cb) + sysdata_desc = &sysdata_desc_opt_cb; + else + sysdata_desc = &sysdata_desc_default; + + rc = sysdata_file_request(config->name, sysdata_desc, test_dev->dev); + if (rc) + dev_err(test_dev->dev, "sync load of '%s' failed: %d\n", + config->name, rc); + + return rc; +} + +static void config_async_req_cb(const struct sysdata_file *sysdata, + void *context) +{ + struct sysdata_test_device *test_dev = context; + config_load_data(test_dev, sysdata); +} + +static int trigger_config_async(struct sysdata_test_device *test_dev) +{ + struct test_config *config = &test_dev->config; + int rc; + const struct sysdata_file_desc sysdata_desc_default = { + SYSDATA_DEFAULT_ASYNC(config_async_req_cb, test_dev), + .sync_reqs.mode = config->async ? + SYNCDATA_ASYNC : SYNCDATA_SYNC, + .optional = config->optional, + .keep = config->keep, + }; + const struct sysdata_file_desc sysdata_desc_opt_cb = { + SYSDATA_DEFAULT_ASYNC(config_async_req_cb, test_dev), + SYSDATA_ASYNC_OPT_CB(config_async_req_default_cb, test_dev), + .sync_reqs.mode = config->async ? + SYNCDATA_ASYNC : SYNCDATA_SYNC, + .optional = config->optional, + .keep = config->keep, + }; + const struct sysdata_file_desc *sysdata_desc; + async_cookie_t async_cookie; + + if (config->enable_opt_cb) + sysdata_desc = &sysdata_desc_opt_cb; + else + sysdata_desc = &sysdata_desc_default; + + rc = sysdata_file_request_async(config->name, sysdata_desc, + test_dev->dev, + &async_cookie); + if (rc) { + dev_err(test_dev->dev, "async load of '%s' failed: %d\n", + config->name, rc); + goto out; + } + + sysdata_synchronize_request(async_cookie); +out: + return rc; +} + +static ssize_t +trigger_config_store(struct device *dev, + struct device_attribute *attr, + const char *buf, size_t count) +{ + struct sysdata_test_device *test_dev = dev_to_test_dev(dev); + struct test_sysdata_private *test_sysdata = &test_dev->test_sysdata; + struct test_config *config = &test_dev->config; + int rc; + + mutex_lock(&test_dev->trigger_mutex); + mutex_lock(&test_dev->config_mutex); + + dev_info(dev, "loading '%s'\n", config->name); + + if (config->async) + rc = trigger_config_async(test_dev); + else + rc = trigger_config_sync(test_dev); + + config->test_result = rc; + + if (rc) + goto out; + + if (test_sysdata->written) { + dev_info(dev, "loaded: %zu\n", test_sysdata->size); + rc = count; + } else { + dev_err(dev, "failed to load firmware\n"); + rc = -ENODEV; + } + +out: + mutex_unlock(&test_dev->config_mutex); + mutex_unlock(&test_dev->trigger_mutex); + + return rc; +} +static DEVICE_ATTR_WO(trigger_config); + +/* + * XXX: move to kstrncpy() once merged. + * + * Users should use kfree_const() when freeing these. + */ +static int __kstrncpy(char **dst, const char *name, size_t count, gfp_t gfp) +{ + *dst = kstrndup(name, count, gfp); + if (!*dst) + return -ENOSPC; + return count; +} + +static int config_copy_name(struct test_config *config, + const char *name, + size_t count) +{ + return __kstrncpy(&config->name, name, count, GFP_KERNEL); +} + +static int config_copy_default_name(struct test_config *config, + const char *name, + size_t count) +{ + return __kstrncpy(&config->default_name, name, count, GFP_KERNEL); +} + +static void __sysdata_config_free(struct test_config *config) +{ + kfree_const(config->name); + config->name = NULL; + kfree_const(config->default_name); + config->default_name = NULL; +} + +static void sysdata_config_free(struct sysdata_test_device *test_dev) +{ + struct test_config *config = &test_dev->config; + + mutex_lock(&test_dev->config_mutex); + __sysdata_config_free(config); + mutex_unlock(&test_dev->config_mutex); +} + +static int __sysdata_config_init(struct test_config *config) +{ + int ret; + + ret = config_copy_name(config, TEST_SYSDATA, strlen(TEST_SYSDATA)); + if (ret < 0) + goto out; + + ret = config_copy_default_name(config, TEST_SYSDATA, + strlen(TEST_SYSDATA)); + if (ret < 0) + goto out; + + config->async = false; + config->optional = false; + config->keep = false; + config->enable_opt_cb = false; + config->test_result = 0; + +out: + return ret; +} + +int sysdata_config_init(struct sysdata_test_device *test_dev) +{ + struct test_config *config = &test_dev->config; + int ret; + + mutex_lock(&test_dev->config_mutex); + ret = __sysdata_config_init(config); + mutex_unlock(&test_dev->config_mutex); + + return ret; +} + +static ssize_t config_name_store(struct device *dev, + struct device_attribute *attr, + const char *buf, size_t count) +{ + struct sysdata_test_device *test_dev = dev_to_test_dev(dev); + struct test_config *config = &test_dev->config; + int rc; + + mutex_lock(&test_dev->config_mutex); + rc = config_copy_name(config, buf, count); + mutex_unlock(&test_dev->config_mutex); + + return rc; +} + +static ssize_t config_name_show(struct device *dev, + struct device_attribute *attr, + char *buf) +{ + struct sysdata_test_device *test_dev = dev_to_test_dev(dev); + struct test_config *config = &test_dev->config; + + mutex_lock(&test_dev->config_mutex); + strcpy(buf, config->name); + strcat(buf, "\n"); + mutex_unlock(&test_dev->config_mutex); + + return strlen(buf) + 1; +} +static DEVICE_ATTR(config_name, 0644, config_name_show, config_name_store); + +static ssize_t config_default_name_store(struct device *dev, + struct device_attribute *attr, + const char *buf, size_t count) +{ + struct sysdata_test_device *test_dev = dev_to_test_dev(dev); + struct test_config *config = &test_dev->config; + int rc; + + mutex_lock(&test_dev->config_mutex); + rc = config_copy_default_name(config, buf, count); + mutex_unlock(&test_dev->config_mutex); + + return rc; +} + +static ssize_t config_default_name_show(struct device *dev, + struct device_attribute *attr, + char *buf) +{ + struct sysdata_test_device *test_dev = dev_to_test_dev(dev); + struct test_config *config = &test_dev->config; + + mutex_lock(&test_dev->config_mutex); + strcpy(buf, config->default_name); + strcat(buf, "\n"); + mutex_unlock(&test_dev->config_mutex); + + return strlen(buf) + 1; +} +static DEVICE_ATTR(config_default_name, 0644, config_default_name_show, + config_default_name_store); + +static ssize_t reset_store(struct device *dev, + struct device_attribute *attr, + const char *buf, size_t count) +{ + struct sysdata_test_device *test_dev = dev_to_test_dev(dev); + struct test_config *config = &test_dev->config; + int ret; + + mutex_lock(&test_dev->trigger_mutex); + + mutex_lock(&test_dev->sysdata_mutex); + free_test_sysdata(&test_dev->test_sysdata); + mutex_unlock(&test_dev->sysdata_mutex); + + mutex_lock(&test_dev->config_mutex); + + __sysdata_config_free(config); + + ret = __sysdata_config_init(config); + if (ret < 0) { + ret = -ENOMEM; + dev_err(dev, "could not alloc settings for config trigger: %d\n", + ret); + goto out; + } + + dev_info(dev, "reset\n"); + ret = count; + +out: + mutex_unlock(&test_dev->config_mutex); + mutex_unlock(&test_dev->trigger_mutex); + + return ret; +} +static DEVICE_ATTR_WO(reset); + +/* + * XXX: consider a soluton to generalize drivers to specify their own + * mutex, adding it to dev core after this gets merged. This may not + * be important for once-in-a-while system tuning parameters, but if + * we want to enable fuzz testing, this is really important. + * + * It may make sense to just have a "struct device configuration mutex" + * for these sorts of things, although there is difficulty in that we'd + * need dynamically allocated attributes for that. Its the same reason + * why we ended up not using the provided standard device attribute + * bool, int interfaces. + */ + +static int test_dev_config_update_bool(struct sysdata_test_device *test_dev, + const char *buf, size_t size, + bool *config) +{ + int ret; + + mutex_lock(&test_dev->config_mutex); + if (strtobool(buf, config) < 0) + ret = -EINVAL; + else + ret = size; + mutex_unlock(&test_dev->config_mutex); + + return ret; +} + +static ssize_t test_dev_config_show_bool(struct sysdata_test_device *test_dev, + char *buf, + bool config) +{ + bool val; + + mutex_lock(&test_dev->config_mutex); + val = config; + mutex_unlock(&test_dev->config_mutex); + + return snprintf(buf, PAGE_SIZE, "%d\n", val); +} + +static int test_dev_config_update_int(struct sysdata_test_device *test_dev, + const char *buf, size_t size, + int *config) +{ + char *end; + long new = simple_strtol(buf, &end, 0); + if (end == buf || new > INT_MAX || new < INT_MIN) + return -EINVAL; + mutex_lock(&test_dev->config_mutex); + *(int *)config = new; + mutex_unlock(&test_dev->config_mutex); + /* Always return full write size even if we didn't consume all */ + return size; +} + +static ssize_t test_dev_config_show_int(struct sysdata_test_device *test_dev, + char *buf, + int config) +{ + int val; + + mutex_lock(&test_dev->config_mutex); + val = config; + mutex_unlock(&test_dev->config_mutex); + + return snprintf(buf, PAGE_SIZE, "%d\n", val); +} + +static ssize_t config_async_store(struct device *dev, + struct device_attribute *attr, + const char *buf, size_t count) +{ + struct sysdata_test_device *test_dev = dev_to_test_dev(dev); + struct test_config *config = &test_dev->config; + + return test_dev_config_update_bool(test_dev, buf, count, + &config->async); +} + +static ssize_t config_async_show(struct device *dev, + struct device_attribute *attr, + char *buf) +{ + struct sysdata_test_device *test_dev = dev_to_test_dev(dev); + struct test_config *config = &test_dev->config; + + return test_dev_config_show_bool(test_dev, buf, config->async); +} +static DEVICE_ATTR(config_async, 0644, config_async_show, config_async_store); + +static ssize_t config_optional_store(struct device *dev, + struct device_attribute *attr, + const char *buf, size_t count) +{ + struct sysdata_test_device *test_dev = dev_to_test_dev(dev); + struct test_config *config = &test_dev->config; + + return test_dev_config_update_bool(test_dev, buf, count, + &config->optional); +} + +static ssize_t config_optional_show(struct device *dev, + struct device_attribute *attr, + char *buf) +{ + struct sysdata_test_device *test_dev = dev_to_test_dev(dev); + struct test_config *config = &test_dev->config; + + return test_dev_config_show_bool(test_dev, buf, config->optional); +} +static DEVICE_ATTR(config_optional, 0644, config_optional_show, + config_optional_store); + +static ssize_t config_keep_store(struct device *dev, + struct device_attribute *attr, + const char *buf, size_t count) +{ + struct sysdata_test_device *test_dev = dev_to_test_dev(dev); + struct test_config *config = &test_dev->config; + + return test_dev_config_update_bool(test_dev, buf, count, + &config->keep); +} + +static ssize_t config_keep_show(struct device *dev, + struct device_attribute *attr, + char *buf) +{ + struct sysdata_test_device *test_dev = dev_to_test_dev(dev); + struct test_config *config = &test_dev->config; + + return test_dev_config_show_bool(test_dev, buf, config->keep); +} +static DEVICE_ATTR(config_keep, 0644, config_keep_show, config_keep_store); + +static ssize_t config_enable_opt_cb_store(struct device *dev, + struct device_attribute *attr, + const char *buf, size_t count) +{ + struct sysdata_test_device *test_dev = dev_to_test_dev(dev); + struct test_config *config = &test_dev->config; + + return test_dev_config_update_bool(test_dev, buf, count, + &config->enable_opt_cb); +} + +static ssize_t config_enable_opt_cb_show(struct device *dev, + struct device_attribute *attr, + char *buf) +{ + struct sysdata_test_device *test_dev = dev_to_test_dev(dev); + struct test_config *config = &test_dev->config; + + return test_dev_config_show_bool(test_dev, buf, + config->enable_opt_cb); +} +static DEVICE_ATTR(config_enable_opt_cb, 0644, + config_enable_opt_cb_show, + config_enable_opt_cb_store); + +static ssize_t test_result_store(struct device *dev, + struct device_attribute *attr, + const char *buf, size_t count) +{ + struct sysdata_test_device *test_dev = dev_to_test_dev(dev); + struct test_config *config = &test_dev->config; + + return test_dev_config_update_int(test_dev, buf, count, + &config->test_result); +} + +static ssize_t test_result_show(struct device *dev, + struct device_attribute *attr, + char *buf) +{ + struct sysdata_test_device *test_dev = dev_to_test_dev(dev); + struct test_config *config = &test_dev->config; + + return test_dev_config_show_int(test_dev, buf, config->test_result); +} +static DEVICE_ATTR(test_result, 0644, test_result_show, test_result_store); + +#define SYSDATA_DEV_ATTR(name) &dev_attr_##name.attr + +static struct attribute *test_dev_attrs[] = { + SYSDATA_DEV_ATTR(trigger_config), + SYSDATA_DEV_ATTR(config), + SYSDATA_DEV_ATTR(reset), + + SYSDATA_DEV_ATTR(config_name), + SYSDATA_DEV_ATTR(config_default_name), + SYSDATA_DEV_ATTR(config_async), + SYSDATA_DEV_ATTR(config_optional), + SYSDATA_DEV_ATTR(config_keep), + SYSDATA_DEV_ATTR(config_enable_opt_cb), + SYSDATA_DEV_ATTR(test_result), + + NULL, +}; + +ATTRIBUTE_GROUPS(test_dev); + +/* + * XXX: this could perhaps be made generic already too, but a hunt + * for actual users would be needed first. It could be generic + * if other test drivers end up using a similar mechanism. + */ +const char *test_dev_get_name(const char *base, int idx, gfp_t gfp) +{ + const char *name_const; + char *name; + + if (!base) + return NULL; + if (strlen(base) > 30) + return NULL; + name = kzalloc(1024, gfp); + if (!name) + return NULL; + + strncat(name, base, strlen(base)); + sprintf(name+(strlen(base)), "%d", idx); + name_const = kstrdup_const(name, gfp); + + kfree(name); + + return name_const; +} + +void free_test_dev_sysdata(struct sysdata_test_device *test_dev) +{ + kfree_const(test_dev->misc_dev.name); + test_dev->misc_dev.name = NULL; + vfree(test_dev); + test_dev = NULL; + sysdata_config_free(test_dev); +} + +void unregister_test_dev_sysdata(struct sysdata_test_device *test_dev) +{ + dev_info(test_dev->dev, "removing interface\n"); + misc_deregister(&test_dev->misc_dev); + free_test_dev_sysdata(test_dev); +} + +struct sysdata_test_device *alloc_test_dev_sysdata(int idx) +{ + int rc; + struct sysdata_test_device *test_dev; + struct miscdevice *misc_dev; + + test_dev = vmalloc(sizeof(struct sysdata_test_device)); + if (!test_dev) { + pr_err("Cannot alloc test_dev\n"); + goto err_out; + } + + mutex_init(&test_dev->sysdata_mutex); + mutex_init(&test_dev->config_mutex); + mutex_init(&test_dev->trigger_mutex); + + rc = sysdata_config_init(test_dev); + if (rc < 0) { + pr_err("Cannot alloc sysdata_config_init()\n"); + goto err_out_free; + } + + test_dev->dev_idx = idx; + misc_dev = &test_dev->misc_dev; + + misc_dev->minor = MISC_DYNAMIC_MINOR; + misc_dev->name = test_dev_get_name("test_sysdata", test_dev->dev_idx, + GFP_KERNEL); + if (!misc_dev->name) { + pr_err("Cannot alloc misc_dev->name\n"); + goto err_out_free_config; + } + misc_dev->fops = &test_fw_fops; + misc_dev->groups = test_dev_groups; + + return test_dev; + +err_out_free_config: + __sysdata_config_free(&test_dev->config); +err_out_free: + kfree(test_dev); +err_out: + return NULL; +} + +static int register_test_dev_sysdata(void) +{ + struct sysdata_test_device *test_dev = NULL; + int rc = -ENODEV; + + mutex_lock(®_dev_mutex); + + /* int should suffice for number of devices, test for wrap */ + if (unlikely(num_test_devs + 1) < 0) { + pr_err("reached limit of number of test devices\n"); + goto out; + } + + test_dev = alloc_test_dev_sysdata(num_test_devs); + if (!test_dev) { + rc = -ENOMEM; + goto out; + } + + rc = misc_register(&test_dev->misc_dev); + if (rc) { + pr_err("could not register misc device: %d\n", rc); + free_test_dev_sysdata(test_dev); + return rc; + } + + test_dev->dev = test_dev->misc_dev.this_device; + list_add_tail(&test_dev->list, ®_test_devs); + dev_info(test_dev->dev, "interface ready\n"); + + num_test_devs++; + + mutex_unlock(®_dev_mutex); + +out: + return rc; +} + +static void do_init_work(struct work_struct *work); +static DECLARE_WORK(init_work, do_init_work); + +static void do_init_work(struct work_struct *work) +{ + int rc; + ssleep(3); + rc = register_test_dev_sysdata(); + if (rc) + pr_err("Cannot add first test sysdata device\n"); +} + +/* Let's not clobber init large allocations */ +static int __init test_sysdata_init(void) +{ + schedule_work(&init_work); + return 0; +} + +subsys_initcall(test_sysdata_init); + +static void __exit test_sysdata_exit(void) +{ + struct sysdata_test_device *test_dev, *tmp; + + mutex_lock(®_dev_mutex); + list_for_each_entry_safe(test_dev, tmp, ®_test_devs, list) { + list_del(&test_dev->list); + unregister_test_dev_sysdata(test_dev); + } + mutex_unlock(®_dev_mutex); +} + +module_exit(test_sysdata_exit); + +MODULE_AUTHOR("Luis R. Rodriguez <mcgrof@kernel.org>"); +MODULE_LICENSE("GPL");
diff --git a/tools/testing/selftests/firmware/sysdata.sh b/tools/testing/selftests/firmware/sysdata.sh new file mode 100755 index 0000000..7bc8f9f --- /dev/null +++ b/tools/testing/selftests/firmware/sysdata.sh
@@ -0,0 +1,633 @@ +#!/bin/bash + +# This performs a series tests against firmware_class to excercise the +# firmware_class driver with focus only on the extensible system data API. +# +# To make this test self contained, and note pollute your distribution +# firmware install paths, we reset the custom load directory to a +# temporary location. + +set -e + +DIR=/sys/devices/virtual/misc/test_sysdata0/ + +if [ ! -d $DIR ]; then + modprobe test_sysdata + if [ ! -d $DIR ]; then + echo "$0: $DIR not present" + echo "You must have CONFIG_TEST_FIRMWARE=m or CONFIG_TEST_FIRMWARE=y" + exit 1 + fi +fi + +OLD_FWPATH=$(cat /sys/module/firmware_class/parameters/path) + +FWPATH=$(mktemp -d) +DEFAULT_SYSDATA="test-sysdata.bin" +FW="$FWPATH/$DEFAULT_SYSDATA" + +test_reqs() +{ + if ! which diff 2> /dev/null > /dev/null; then + echo "$0: You need diff installed" + exit 1 + fi +} + +test_finish() +{ + echo -n "$OLD_PATH" >/sys/module/firmware_class/parameters/path + rm -f "$FW" + rmdir "$FWPATH" +} + +trap "test_finish" EXIT + +# Set the kernel search path. +echo -n "$FWPATH" >/sys/module/firmware_class/parameters/path + +# This is an unlikely real-world firmware content. :) +echo "ABCD0123" >"$FW" + +NAME=$(basename "$FW") + +errno_name_to_val() +{ + case "$1" in + SUCCESS) + echo 0;; + -EPERM) + echo -1;; + -ENOENT) + echo -2;; + -EINVAL) + echo -22;; + -ERR_ANY) + echo -123456;; + *) + echo invalid;; + esac +} + +errno_val_to_name() + case "$1" in + 0) + echo SUCCESS;; + -1) + echo -EPERM;; + -2) + echo -ENOENT;; + -22) + echo -EINVAL;; + -123456) + echo -ERR_ANY;; + *) + echo invalid;; + esac + +config_set_async() +{ + if ! echo -n 1 >$DIR/config_async ; then + echo "$0: Unable to set to async" >&2 + exit 1 + fi +} + +config_disable_async() +{ + if ! echo -n 0 >$DIR/config_async ; then + echo "$0: Unable to set to sync" >&2 + exit 1 + fi +} + +config_set_optional() +{ + if ! echo -n 1 >$DIR/config_optional ; then + echo "$0: Unable to set to optional" >&2 + exit 1 + fi +} + +config_disable_optional() +{ + if ! echo -n 0 >$DIR/config_optional ; then + echo "$0: Unable to disable optional" >&2 + exit 1 + fi +} + +config_set_keep() +{ + if ! echo -n 1 >$DIR/config_keep; then + echo "$0: Unable to set to keep" >&2 + exit 1 + fi +} + +config_disable_keep() +{ + if ! echo -n 0 >$DIR/config_keep; then + echo "$0: Unable to disable keep option" >&2 + exit 1 + fi +} + +config_enable_opt_cb() +{ + if ! echo -n 1 >$DIR/config_enable_opt_cb; then + echo "$0: Unable to set to optional" >&2 + exit 1 + fi +} + +config_disable_opt_cb() +{ + if ! echo -n 0 >$DIR/config_enable_opt_cb; then + echo "$0: Unable to disable keep option" >&2 + exit 1 + fi +} + + +# For special characters use printf directly, +# refer to sysdata_test_0001 +config_set_name() +{ + if ! echo -n $1 >$DIR/config_name; then + echo "$0: Unable to set name" >&2 + exit 1 + fi +} + +config_get_name() +{ + cat $DIR/config_name +} + +# For special characters use printf directly, +# refer to sysdata_test_0001 +config_set_default_name() +{ + if ! echo -n $1 >$DIR/config_default_name; then + echo "$0: Unable to set default_name" >&2 + exit 1 + fi +} + +config_get_default_name() +{ + cat $DIR/config_default_name +} + +config_get_test_result() +{ + cat $DIR/test_result +} + +config_reset() +{ + if ! echo -n "1" >"$DIR"/reset; then + echo "$0: reset shuld have worked" >&2 + exit 1 + fi +} + +config_show_config() +{ + echo "----------------------------------------------------" + cat "$DIR"/config + echo "----------------------------------------------------" +} + +config_trigger() +{ + if ! echo -n "1" >"$DIR"/trigger_config 2>/dev/null; then + echo "$1: FAIL - loading should have worked" + config_show_config + exit 1 + fi + echo "$1: OK! - loading sysdata" +} + +config_trigger_want_fail() +{ + if echo "1" > $DIR/trigger_config 2>/dev/null; then + echo "$1: FAIL - loading was expected to fail" + config_show_config + exit 1 + fi + echo "$1: OK! - loading failed as expected" +} + +config_file_should_match() +{ + FILE=$(config_get_name) + # On this one we expect the file to exist so leave stderr in + if ! $(diff -q "$FWPATH"/"$FILE" /dev/test_sysdata0 > /dev/null) > /dev/null; then + echo "$1: FAIL - file $FILE did not match contents in /dev/test_sysdata0" >&2 + config_show_config + exit 1 + fi + echo "$1: OK! - $FILE == /dev/test_sysdata0" +} + +config_file_should_match_default() +{ + FILE=$(config_get_default_name) + # On this one we expect the file to exist so leave stderr in + if ! $(diff -q "$FWPATH"/"$FILE" /dev/test_sysdata0 > /dev/null) > /dev/null; then + echo "$1: FAIL - file $FILE did not match contents in /dev/test_sysdata0" >&2 + config_show_config + exit 1 + fi + echo "$1: OK! - $FILE == /dev/test_sysdata0" +} + +config_file_should_not_match() +{ + FILE=$(config_get_name) + # File may not exist, so skip those error messages as well + if $(diff -q $FWPATH/$FILE /dev/test_sysdata0 2> /dev/null) 2> /dev/null ; then + echo "$1: FAIL - file $FILE was not expected to match /dev/null" >&2 + config_show_config + exit 1 + fi + echo "$1: OK! - $FILE != /dev/test_sysdata0" +} + +config_default_file_should_match() +{ + FILE=$(config_get_default_name) + diff -q $FWPATH/$FILE /dev/test_sysdata0 2> /dev/null + if ! $? ; then + echo "$1: FAIL - file $FILE expected to match /dev/test_sysdata0" >&2 + config_show_config + exit 1 + fi + echo "$1: OK! [file integrity matches]" +} + +config_default_file_should_not_match() +{ + FILE=$(config_get_default_name) + diff -q FWPATH/$FILE /dev/test_sysdata0 2> /dev/null + if $? 2> /dev/null ; then + echo "$1: FAIL - file $FILE was not expected to match test_sysdata0" >&2 + config_show_config + exit 1 + fi + echo "$1: OK!" +} + +config_expect_result() +{ + RC=$(config_get_test_result) + RC_NAME=$(errno_val_to_name $RC) + + ERRNO_NAME=$2 + ERRNO=$(errno_name_to_val $ERRNO_NAME) + + if [[ $ERRNO_NAME = "-ERR_ANY" ]]; then + if [[ $RC -ge 0 ]]; then + echo "$1: FAIL, test expects $ERRNO_NAME - got $RC_NAME ($RC)" >&2 + config_show_config + exit 1 + fi + elif [[ $RC != $ERRNO ]]; then + echo "$1: FAIL, test expects $ERRNO_NAME ($ERRNO) - got $RC_NAME ($RC)" >&2 + config_show_config + exit 1 + fi + echo "$1: OK! - Return value: $RC ($RC_NAME), expected $ERRNO_NAME" +} + +sysdata_set_sync_defaults() +{ + config_reset +} + +sysdata_set_async_defaults() +{ + config_reset + config_set_async +} + +sysdata_test_0001s() +{ + NAME='\000' + + sysdata_set_sync_defaults + config_set_name $NAME + printf '\000' >"$DIR"/config_name + config_trigger_want_fail ${FUNCNAME[0]} + config_expect_result ${FUNCNAME[0]} -EINVAL +} + +sysdata_test_0001a() +{ + NAME='\000' + + sysdata_set_async_defaults + printf '\000' >"$DIR"/config_name + config_trigger_want_fail ${FUNCNAME[0]} + config_expect_result ${FUNCNAME[0]} -EINVAL +} + +sysdata_test_0001() +{ + sysdata_test_0001s + sysdata_test_0001a +} + +sysdata_test_0002s() +{ + NAME="nope-$DEFAULT_SYSDATA" + + sysdata_set_sync_defaults + config_set_name ${FUNCNAME[0]} + config_trigger_want_fail ${FUNCNAME[0]} + config_expect_result ${FUNCNAME[0]} -ENOENT +} + +sysdata_test_0002a() +{ + NAME="nope-$DEFAULT_SYSDATA" + + sysdata_set_async_defaults + config_set_name $NAME + config_trigger_want_fail ${FUNCNAME[0]} + # This may seem odd to expect success on a bogus + # file but remember this is an async call, the actual + # error handling is managed by the async callbacks. + config_expect_result ${FUNCNAME[0]} SUCCESS +} + +sysdata_test_0002() +{ + #sysdata_test_0002s + sysdata_test_0002a +} + +sysdata_test_0003() +{ + config_reset + config_file_should_not_match ${FUNCNAME[0]} +} + +sysdata_test_0004s() +{ + TEST="sysdata_test_0004s" + + sysdata_set_sync_defaults + config_trigger ${FUNCNAME[0]} + config_file_should_match ${FUNCNAME[0]} + config_expect_result ${FUNCNAME[0]} SUCCESS +} + +sysdata_test_0004a() +{ + sysdata_set_async_defaults + config_trigger ${FUNCNAME[0]} + config_file_should_match ${FUNCNAME[0]} + config_expect_result ${FUNCNAME[0]} SUCCESS +} + +sysdata_test_0004() +{ + sysdata_test_0004s + sysdata_test_0004a +} + +sysdata_test_0005s() +{ + NAME="nope-$DEFAULT_SYSDATA" + + sysdata_set_sync_defaults + config_set_optional + config_set_name $NAME + config_trigger_want_fail ${FUNCNAME[0]} + # We do this to ensure the default backup callback hasn't + # been called yet + config_file_should_not_match ${FUNCNAME[0]} + config_expect_result ${FUNCNAME[0]} SUCCESS +} + +sysdata_test_0005a() +{ + NAME="nope-$DEFAULT_SYSDATA" + + sysdata_set_async_defaults + config_set_optional + config_set_name $NAME + config_trigger_want_fail ${FUNCNAME[0]} + # We do this to ensure the default backup callback hasn't + # been called yet + config_file_should_not_match ${FUNCNAME[0]} + config_expect_result ${FUNCNAME[0]} SUCCESS +} + +sysdata_test_0005() +{ + sysdata_test_0005s + sysdata_test_0005a +} + +sysdata_test_0006s() +{ + sysdata_set_sync_defaults + config_set_optional + config_trigger ${FUNCNAME[0]} + config_file_should_match ${FUNCNAME[0]} + config_expect_result ${FUNCNAME[0]} SUCCESS +} + +sysdata_test_0006a() +{ + sysdata_set_async_defaults + config_set_optional + config_trigger ${FUNCNAME[0]} + config_file_should_match ${FUNCNAME[0]} + config_expect_result ${FUNCNAME[0]} SUCCESS +} + +sysdata_test_0006() +{ + sysdata_test_0006s + sysdata_test_0006a +} + +sysdata_test_0007s() +{ + sysdata_set_sync_defaults + config_set_keep + config_trigger ${FUNCNAME[0]} + config_file_should_match ${FUNCNAME[0]} + config_expect_result ${FUNCNAME[0]} SUCCESS +} + +sysdata_test_0007a() +{ + sysdata_set_async_defaults + config_set_keep + config_trigger ${FUNCNAME[0]} + config_file_should_match ${FUNCNAME[0]} + config_expect_result ${FUNCNAME[0]} SUCCESS +} + +sysdata_test_0007() +{ + sysdata_test_0007s + sysdata_test_0007a +} + +sysdata_test_0008s() +{ + NAME="nope-$DEFAULT_SYSDATA" + + sysdata_set_sync_defaults + config_set_name $NAME + config_set_optional + config_enable_opt_cb + config_trigger ${FUNCNAME[0]} + config_file_should_match_default ${FUNCNAME[0]} + config_expect_result ${FUNCNAME[0]} SUCCESS +} + +sysdata_test_0008a() +{ + NAME="nope-$DEFAULT_SYSDATA" + + sysdata_set_async_defaults + config_set_name $NAME + config_set_optional + config_enable_opt_cb + config_trigger ${FUNCNAME[0]} + config_file_should_match_default ${FUNCNAME[0]} + config_expect_result ${FUNCNAME[0]} SUCCESS +} + +sysdata_test_0008() +{ + sysdata_test_0008s + sysdata_test_0008a +} + +sysdata_test_0009s() +{ + NAME="nope-$DEFAULT_SYSDATA" + + sysdata_set_sync_defaults + config_set_name $NAME + config_set_keep + config_set_optional + config_enable_opt_cb + config_trigger ${FUNCNAME[0]} + config_file_should_match_default ${FUNCNAME[0]} + config_expect_result ${FUNCNAME[0]} SUCCESS +} + +sysdata_test_0009a() +{ + NAME="nope-$DEFAULT_SYSDATA" + + sysdata_set_async_defaults + config_set_name $NAME + config_set_keep + config_set_optional + config_enable_opt_cb + config_trigger ${FUNCNAME[0]} + config_file_should_match_default ${FUNCNAME[0]} + config_expect_result ${FUNCNAME[0]} SUCCESS +} + +sysdata_test_0009() +{ + sysdata_test_0009s + sysdata_test_0009a +} + +sysdata_test_0010s() +{ + NAME="nope-$DEFAULT_SYSDATA" + + sysdata_set_sync_defaults + config_set_name $NAME + config_set_default_name $NAME + config_set_keep + config_set_optional + config_enable_opt_cb + config_trigger_want_fail ${FUNCNAME[0]} + config_file_should_not_match ${FUNCNAME[0]} + config_expect_result ${FUNCNAME[0]} -ENOENT +} + +sysdata_test_0010a() +{ + NAME="nope-$DEFAULT_SYSDATA" + + sysdata_set_async_defaults + config_set_name $NAME + config_set_default_name $NAME + config_set_keep + config_set_optional + config_enable_opt_cb + config_trigger_want_fail ${FUNCNAME[0]} + config_file_should_not_match ${FUNCNAME[0]} + config_expect_result ${FUNCNAME[0]} SUCCESS +} + +sysdata_test_0010() +{ + sysdata_test_0010s + sysdata_test_0010a +} + +test_reqs + +usage() +{ + echo "Usage: $0 [ -t <4-number-digit> ]" + echo Valid tests: 0001-0005 + echo + echo 0001 - Empty string should be ignored + echo 0002 - Files that do not exist should be ignored + echo 0003 - Verify test_sysdata0 has nothing loaded upon reset + echo 0004 - Simple sync and async loader + echo 0005 - Verify optional loading is not fatal + echo 0006 - Verify optional loading enables loading + echo 0007 - Verify keep works + echo 0008 - Verify optional callback works + echo 0009 - Verify optional callback works, keep + echo 0010 - Verify when fallback file is not present + exit 1 +} + +# You can ask for a specific test: +if [[ $# > 0 ]] ; then + if [[ $1 != "-t" ]]; then + usage + fi + + re='^[0-9]+$' + if ! [[ $2 =~ $re ]]; then + usage + fi + + RUN_TEST=sysdata_test_$2 + $RUN_TEST + exit 0 +fi + +sysdata_test_0001 +sysdata_test_0002 +sysdata_test_0003 +sysdata_test_0004 +sysdata_test_0005 +sysdata_test_0006 +sysdata_test_0007 +sysdata_test_0008 +sysdata_test_0009 +sysdata_test_0010 + +exit 0