qemu-devel
[Top][All Lists]
Advanced

[Date Prev][Date Next][Thread Prev][Thread Next][Date Index][Thread Index]

Re: [Qemu-devel] RFC: guest-side retrieval of fw_cfg file


From: Gabriel L. Somlo
Subject: Re: [Qemu-devel] RFC: guest-side retrieval of fw_cfg file
Date: Sat, 25 Jul 2015 19:21:53 -0400
User-agent: Mutt/1.5.23 (2014-03-12)

On Tue, Jul 21, 2015 at 12:07:08AM +0200, Laszlo Ersek wrote:
> On 07/20/15 23:19, Gabriel L. Somlo wrote:
> 
> ... I just love "find", sorry. :) Anyway, I don't envision myself as a
> user of this feature any time soon, so please feel free to ignore me.

How about this: I create /sys/firmware/fw_cfg/by_select with *all*
entries, named after their selector key, in a flat folder.

Then, I also create /sys/firmware/fw_cfg/by_name/... where I try to
build the folder hierarchy given by whatever the file names end up
being, *on a best effort basis*, i.e. I may fail to create *some* of
them, if they have brain-damaged names :)

The kernel will issue big scary warnings if a symlink already exists
where I'm trying to add a new subdirectory of the same name, or the
other way around.

The whole thing is below, tested and working on x86 and arm (so one
example of IOPort and one example of MMIO).

I'm out all of next week, but will submit this to the kernel (and cc
Greg-K-H as advised by Matt) once I get back.

In the mean time, any final words of wisdom, advice, testing, etc.
much much appreciated !

Cheers,
--Gabriel


/*
 * drivers/firmware/fw_cfg.c
 *
 * Expose entries from QEMU's firmware configuration (fw_cfg) device in
 * sysfs (read-only, under "/sys/firmware/fw_cfg/...").
 *
 * Copyright 2015 Gabriel L. Somlo <address@hidden>
 *
 * This program is free software; you can redistribute it and/or modify
 * it under the terms of the GNU General Public License v2 as published
 * by the Free Software Foundation.
 *
 * This program is distributed in the hope that it will be useful, but
 * WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY
 * or FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License
 * for more details.
 *
 * You should have received a copy of the GNU General Public License along
 * with this program; if not, write to the Free Software Foundation, Inc.,
 * 51 Franklin Street, Fifth Floor, Boston, MA  02110-1301, USA
 */

#include <linux/module.h>
#include <linux/capability.h>
#include <linux/slab.h>
#include <linux/io.h>
#include <linux/ioport.h>
#include <linux/ctype.h>

/* selector values for "well-known" fw_cfg entries */
#define FW_CFG_SIGNATURE  0x00
#define FW_CFG_FILE_DIR   0x19

/* size in bytes of fw_cfg signature */
#define FW_CFG_SIG_SIZE 4

/* fw_cfg "file name" is up to 56 characters (including terminating nul) */
#define FW_CFG_MAX_FILE_PATH 56

/* fw_cfg file directory entry type */
struct fw_cfg_file {
        uint32_t size;
        uint16_t select;
        uint16_t reserved;
        char name[FW_CFG_MAX_FILE_PATH];
};

/* fw_cfg device i/o access options type */
struct fw_cfg_access {
        phys_addr_t start;
        uint8_t size;
        uint8_t ctrl_offset;
        uint8_t data_offset;
        bool is_mmio;
        const char *name;
};

/* fw_cfg device i/o access available options for known architectures */
static struct fw_cfg_access fw_cfg_modes[] = {
        { 0x510,       2, 0, 1, false, "fw_cfg on i386, sun4u" },
        { 0x9020000,  10, 8, 0,  true, "fw_cfg on arm" },
        { 0xd00000510, 3, 0, 2,  true, "fw_cfg on sun4m" },
        { 0xf0000510,  3, 0, 2,  true, "fw_cfg on ppc/mac" },
        { }
};

/* fw_cfg device i/o currently selected option set */
static struct fw_cfg_access *fw_cfg_mode;

/* fw_cfg device i/o register addresses */
static void __iomem *fw_cfg_dev_base;
static void __iomem *fw_cfg_dev_ctrl;
static void __iomem *fw_cfg_dev_data;

/* atomic access to fw_cfg device (potentially slow i/o, so using mutex) */
static DEFINE_MUTEX(fw_cfg_dev_lock);

/* pick apropriate endianness for selector key */
static inline uint16_t fw_cfg_sel_endianness(uint16_t select)
{
        return fw_cfg_mode->is_mmio ? cpu_to_be16(select) : cpu_to_le16(select);
}

/* type for fw_cfg "directory scan" visitor/callback function */
typedef int (*fw_cfg_file_callback)(const struct fw_cfg_file *f);

/* run a given callback on each fw_cfg directory entry */
static int fw_cfg_scan_dir(fw_cfg_file_callback callback)
{
        int ret = 0;
        uint32_t count, i;
        struct fw_cfg_file f;

        mutex_lock(&fw_cfg_dev_lock);
        iowrite16(fw_cfg_sel_endianness(FW_CFG_FILE_DIR), fw_cfg_dev_ctrl);
        ioread8_rep(fw_cfg_dev_data, &count, sizeof(count));
        for (i = 0; i < be32_to_cpu(count); i++) {
                ioread8_rep(fw_cfg_dev_data, &f, sizeof(f));
                ret = callback(&f);
                if (ret)
                        break;
        }
        mutex_unlock(&fw_cfg_dev_lock);
        return ret;
}

/* read chunk of given fw_cfg blob (caller responsible for sanity-check) */
static inline void fw_cfg_read_blob(uint16_t select,
                                     void *buf, loff_t pos, size_t count)
{
        mutex_lock(&fw_cfg_dev_lock);
        iowrite16(fw_cfg_sel_endianness(select), fw_cfg_dev_ctrl);
        while (pos-- > 0)
                ioread8(fw_cfg_dev_data);
        ioread8_rep(fw_cfg_dev_data, buf, count);
        mutex_unlock(&fw_cfg_dev_lock);
}

/* clean up fw_cfg device i/o setup */
static void fw_cfg_io_cleanup(void)
{
        if (fw_cfg_mode->is_mmio) {
                iounmap(fw_cfg_dev_base);
                release_mem_region(fw_cfg_mode->start, fw_cfg_mode->size);
        } else {
                ioport_unmap(fw_cfg_dev_base);
                release_region(fw_cfg_mode->start, fw_cfg_mode->size);
        }
}

/* probe and map fw_cfg device */
static int __init fw_cfg_io_probe(void)
{
        char sig[FW_CFG_SIG_SIZE];

        for (fw_cfg_mode = &fw_cfg_modes[0];
             fw_cfg_mode->start; fw_cfg_mode++) {

                phys_addr_t start = fw_cfg_mode->start;
                uint8_t size = fw_cfg_mode->size;

                /* reserve and map mmio or ioport region */
                if (fw_cfg_mode->is_mmio) {
                        if (!request_mem_region(start, size, fw_cfg_mode->name))
                                continue;
                        fw_cfg_dev_base = ioremap(start, size);
                        if (!fw_cfg_dev_base) {
                                release_mem_region(start, size);
                                continue;
                        }
                } else {
                        if (!request_region(start, size, fw_cfg_mode->name))
                                continue;
                        fw_cfg_dev_base = ioport_map(start, size);
                        if (!fw_cfg_dev_base) {
                                release_region(start, size);
                                continue;
                        }
                }

                /* set control and data register addresses */
                fw_cfg_dev_ctrl = fw_cfg_dev_base + fw_cfg_mode->ctrl_offset;
                fw_cfg_dev_data = fw_cfg_dev_base + fw_cfg_mode->data_offset;

                /* verify fw_cfg device signature */
                fw_cfg_read_blob(FW_CFG_SIGNATURE, sig, 0, FW_CFG_SIG_SIZE);
                if (memcmp(sig, "QEMU", FW_CFG_SIG_SIZE) == 0)
                        /* success, we're done */
                        return 0;

                /* clean up before probing next access mode */
                fw_cfg_io_cleanup();
        }

        return -ENODEV;
}

/* fw_cfg_sysfs_entry type */
struct fw_cfg_sysfs_entry {
        struct kobject kobj;
        struct fw_cfg_file f;
        struct list_head list;
};

/* get fw_cfg_sysfs_entry from kobject member */
static inline struct fw_cfg_sysfs_entry *to_entry(struct kobject *kobj)
{
        return container_of(kobj, struct fw_cfg_sysfs_entry, kobj);
}

/* fw_cfg_sysfs_attribute type */
struct fw_cfg_sysfs_attribute {
        struct attribute attr;
        ssize_t (*show)(struct fw_cfg_sysfs_entry *entry, char *buf);
};

/* get fw_cfg_sysfs_attribute from attribute member */
static inline struct fw_cfg_sysfs_attribute *to_attr(struct attribute *attr)
{
        return container_of(attr, struct fw_cfg_sysfs_attribute, attr);
}

/* global cache of fw_cfg_sysfs_entry objects */
static LIST_HEAD(fw_cfg_entry_cache);

/* kobjects removed lazily by kernel, mutual exclusion needed */
static DEFINE_SPINLOCK(fw_cfg_cache_lock);

static inline void fw_cfg_sysfs_cache_enlist(struct fw_cfg_sysfs_entry *entry)
{
        spin_lock(&fw_cfg_cache_lock);
        list_add_tail(&entry->list, &fw_cfg_entry_cache);
        spin_unlock(&fw_cfg_cache_lock);
}

static inline void fw_cfg_sysfs_cache_delist(struct fw_cfg_sysfs_entry *entry)
{
        spin_lock(&fw_cfg_cache_lock);
        list_del(&entry->list);
        spin_unlock(&fw_cfg_cache_lock);
}

static void fw_cfg_sysfs_cache_cleanup(void)
{
        struct fw_cfg_sysfs_entry *entry, *next;

        list_for_each_entry_safe(entry, next, &fw_cfg_entry_cache, list) {
                /* will end up invoking fw_cfg_sysfs_cache_delist()
                 * via each object's release() method (i.e. destructor) */
                kobject_put(&entry->kobj);
        }
}

/* default_attrs: per-entry attributes and show methods */

#define FW_CFG_SYSFS_ATTR(_attr) \
struct fw_cfg_sysfs_attribute fw_cfg_sysfs_attr_##_attr = { \
        .attr = { .name = __stringify(_attr), .mode = 0400 }, \
        .show = fw_cfg_sysfs_show_##_attr, \
}

static ssize_t fw_cfg_sysfs_show_size(struct fw_cfg_sysfs_entry *e, char *buf)
{
        return sprintf(buf, "%d\n", e->f.size);
}

static ssize_t fw_cfg_sysfs_show_select(struct fw_cfg_sysfs_entry *e, char *buf)
{
        return sprintf(buf, "%d\n", e->f.select);
}

static ssize_t fw_cfg_sysfs_show_name(struct fw_cfg_sysfs_entry *e, char *buf)
{
        return sprintf(buf, "%s\n", e->f.name);
}

static FW_CFG_SYSFS_ATTR(size);
static FW_CFG_SYSFS_ATTR(select);
static FW_CFG_SYSFS_ATTR(name);

static struct attribute *fw_cfg_sysfs_entry_attrs[] = {
        &fw_cfg_sysfs_attr_size.attr,
        &fw_cfg_sysfs_attr_select.attr,
        &fw_cfg_sysfs_attr_name.attr,
        NULL,
};

/* sysfs_ops: find fw_cfg_[entry, attribute] and call appropriate show method */
static ssize_t fw_cfg_sysfs_attr_show(struct kobject *kobj, struct attribute *a,
                                      char *buf)
{
        struct fw_cfg_sysfs_entry *entry = to_entry(kobj);
        struct fw_cfg_sysfs_attribute *attr = to_attr(a);

        if (!capable(CAP_SYS_ADMIN))
                return -EACCES;

        return attr->show(entry, buf);
}

static const struct sysfs_ops fw_cfg_sysfs_attr_ops = {
        .show = fw_cfg_sysfs_attr_show,
};

/* release: destructor, to be called via kobject_put() */
static void fw_cfg_sysfs_release_entry(struct kobject *kobj)
{
        struct fw_cfg_sysfs_entry *entry = to_entry(kobj);

        fw_cfg_sysfs_cache_delist(entry);
        kfree(entry);
}

/* kobj_type: ties together all properties required to register an entry */
static struct kobj_type fw_cfg_sysfs_entry_ktype = {
        .default_attrs = fw_cfg_sysfs_entry_attrs,
        .sysfs_ops = &fw_cfg_sysfs_attr_ops,
        .release = fw_cfg_sysfs_release_entry,
};

/* raw-read method and attribute */
static ssize_t fw_cfg_sysfs_read_raw(struct file *filp, struct kobject *kobj,
                                     struct bin_attribute *bin_attr,
                                     char *buf, loff_t pos, size_t count)
{
        struct fw_cfg_sysfs_entry *entry = to_entry(kobj);

        if (!capable(CAP_SYS_ADMIN))
                return -EACCES;

        if (pos > entry->f.size)
                return -EINVAL;

        if (count > entry->f.size - pos)
                count = entry->f.size - pos;

        fw_cfg_read_blob(entry->f.select, buf, pos, count);
        return count;
}

static struct bin_attribute fw_cfg_sysfs_attr_raw = {
        .attr = { .name = "raw", .mode = 0400 },
        .read = fw_cfg_sysfs_read_raw,
};

/*
 * kset_find_obj() is not EXPORTed from lib/kobject.c, clone it here for now.
 * FIXME: patch adding EXPORT in lib/kobject.c tested working, but then
 *        building this module for older kernels wouldn't work anymore...
 */
struct kobject *my_kset_find_obj(struct kset *kset, const char *name)
{
        struct kobject *k;
        struct kobject *ret = NULL;

        spin_lock(&kset->list_lock);

        list_for_each_entry(k, &kset->list, entry) {
                if (kobject_name(k) && !strcmp(kobject_name(k), name)) {
                        ret = kobject_get(k);
                        break;
                }
        }

        spin_unlock(&kset->list_lock);
        return ret;
}

/*
 * Create a kset subdirectory matching each '/' delimited dirname token
 * in 'name', starting with sysfs kset/folder 'dir'; At the end, create
 * a symlink directed at the given 'target'.
 * NOTE: We do this on a best-effort basis, since 'name' is not guaranteed
 * to be a well-behaved path name. Whenever a symlink vs. kset directory
 * name collision occurs, the kernel will issue big scary warnings while
 * refusing to add the offending link or directory. We follow up with our
 * own, slightly less scary error messages explaining the situation :)
 */
static void __init fw_cfg_build_symlink(struct kset *dir,
                                        struct kobject *target,
                                        const char *name)
{
        struct kset *subdir;
        struct kobject *ko;
        char *name_copy, *p, *tok;

        if (!dir || !target || !name || !*name) {
                pr_err("fw_cfg: invalid argument to fw_cfg_build_symlink\n");
                return;
        }

        /* clone a copy of name for parsing */
        name_copy = p = kstrdup(name, GFP_KERNEL);
        if (!name_copy) {
                pr_err("fw_cfg: kstrdup failed while "
                       "creating symlink %s\n", name);
                return;
        }

        /* create folders for each dirname token, then symlink for basename */
        while ((tok = strsep(&p, "/")) && *tok) {

                /* last (basename) token? If so, add symlink here */
                if (!p || !*p) {
                        if (sysfs_create_link(&dir->kobj, target, tok)) {
                                pr_err("fw_cfg: can't create symlink %s "
                                       "of %s\n", tok, name);
                        }
                        break;
                }

                /* does the current dir contain an item named after tok ? */
                ko = my_kset_find_obj(dir, tok);
                if (ko) {
                        /* drop reference added by kset_find_obj */
                        kobject_put(ko);

                        /* ko MUST be a kset - we're about to use it as one ! */
                        if (ko->ktype != dir->kobj.ktype) {
                                pr_err("fw_cfg: non-folder token %s already "
                                       "exists while creating %s\n", tok, name);
                                break;
                        }

                        /* descend into already existing subdirectory */
                        dir = to_kset(ko);
                } else {
                        /* create new subdirectory kset */
                        subdir = kzalloc(sizeof(struct kset), GFP_KERNEL);
                        if (!subdir) {
                                pr_err("fw_cfg: alloc error creating subdir %s "
                                       "of %s\n", tok, name);
                                break;
                        }
                        subdir->kobj.kset = dir;
                        subdir->kobj.ktype = dir->kobj.ktype;
                        if (kobject_set_name(&subdir->kobj, "%s", tok)) {
                                kfree(subdir);
                                pr_err("fw_cfg: set_name failed on subdir %s "
                                       "of %s\n", tok, name);
                                break;
                        }
                        if (kset_register(subdir)) {
                                kfree(subdir);
                                pr_err("fw_cfg: register failed on subdir %s "
                                       "of %s\n", tok, name);
                                break;
                        }

                        /* descend into newly created subdirectory */
                        dir = subdir;
                }
        }

        /* we're done with cloned copy of name */
        kfree(name_copy);
}

/* recursively unregister fw_cfg/by_name/ kset directory tree */
static void fw_cfg_kset_unregister_recursive(struct kset *kset)
{
        struct kobject *k, *next;

        list_for_each_entry_safe(k, next, &kset->list, entry)
                /* all set members are ksets too, but check just in case... */
                if (k->ktype == kset->kobj.ktype)
                        fw_cfg_kset_unregister_recursive(to_kset(k));

        /* symlinks are cleanly and automatically removed with the directory */
        kset_unregister(kset);
}

/* kobjects & kset representing top-level, by_select, and by_name folders */
static struct kobject *fw_cfg_top_ko;
static struct kobject *fw_cfg_sel_ko;
static struct kset *fw_cfg_fname_kset;

/* callback function to register an individual fw_cfg file */
static int __init fw_cfg_register_file(const struct fw_cfg_file *f)
{
        int err;
        struct fw_cfg_sysfs_entry *entry;

        /* allocate new entry */
        entry = kzalloc(sizeof(*entry), GFP_KERNEL);
        if (!entry)
                return -ENOMEM;

        /* set file entry information */
        entry->f.size = be32_to_cpu(f->size);
        entry->f.select = be16_to_cpu(f->select);
        strcpy(entry->f.name, f->name);

        /* register entry under "/sys/firmware/fw_cfg/by_select/" */
        err = kobject_init_and_add(&entry->kobj, &fw_cfg_sysfs_entry_ktype,
                                   fw_cfg_sel_ko, "%d", entry->f.select);
        if (err)
                goto err_register;

        /* add raw binary content access */
        err = sysfs_create_bin_file(&entry->kobj, &fw_cfg_sysfs_attr_raw);
        if (err)
                goto err_add_raw;

        /* try adding "/sys/firmware/fw_cfg/by_name/" symlink (best-effort) */
        fw_cfg_build_symlink(fw_cfg_fname_kset, &entry->kobj, entry->f.name);

        /* success, add entry to global cache */
        fw_cfg_sysfs_cache_enlist(entry);
        return 0;

err_add_raw:
        kobject_del(&entry->kobj);
err_register:
        kfree(entry);
        return err;
}

/* unregister top-level or by_select folder */
static inline void fw_cfg_kobj_cleanup(struct kobject *kobj)
{
        kobject_del(kobj);
        kobject_put(kobj);
}

static int __init fw_cfg_sysfs_init(void)
{
        int err;

        /* probe for the fw_cfg "hardware" */
        err = fw_cfg_io_probe();
        if (err)
                return err;

        /* create sysfs toplevel, by_select, and by_name folders */
        err = -ENOMEM;
        fw_cfg_top_ko = kobject_create_and_add("fw_cfg", firmware_kobj);
        if (!fw_cfg_top_ko)
                goto err_sysfs_top;
        fw_cfg_sel_ko = kobject_create_and_add("by_select", fw_cfg_top_ko);
        if (!fw_cfg_sel_ko)
                goto err_sysfs_sel;
        fw_cfg_fname_kset = kset_create_and_add("by_name", NULL, fw_cfg_top_ko);
        if (!fw_cfg_fname_kset)
                goto err_sysfs_name;

        /* process fw_cfg file directory entry, registering each file */
        err = fw_cfg_scan_dir(fw_cfg_register_file);
        if (err)
                goto err_scan;

        /* success */
        pr_debug("fw_cfg: loaded.\n");
        return 0;

err_scan:
        fw_cfg_sysfs_cache_cleanup();
        fw_cfg_kset_unregister_recursive(fw_cfg_fname_kset);
err_sysfs_name:
        fw_cfg_kobj_cleanup(fw_cfg_sel_ko);
err_sysfs_sel:
        fw_cfg_kobj_cleanup(fw_cfg_top_ko);
err_sysfs_top:
        fw_cfg_io_cleanup();
        return err;
}

static void __exit fw_cfg_sysfs_exit(void)
{
        pr_debug("fw_cfg: unloading.\n");
        fw_cfg_sysfs_cache_cleanup();
        fw_cfg_kset_unregister_recursive(fw_cfg_fname_kset);
        fw_cfg_kobj_cleanup(fw_cfg_sel_ko);
        fw_cfg_kobj_cleanup(fw_cfg_top_ko);
        fw_cfg_io_cleanup();
}

module_init(fw_cfg_sysfs_init);
module_exit(fw_cfg_sysfs_exit);
MODULE_AUTHOR("Gabriel L. Somlo <address@hidden>");
MODULE_DESCRIPTION("QEMU fw_cfg sysfs support");
MODULE_LICENSE("GPL");



reply via email to

[Prev in Thread] Current Thread [Next in Thread]