Hi, A while ago I was pondering on the options available for retrieving a fw_cfg blob from the guest-side (now that we can insert fw_cfg files on the host-side command line, see commit 81b2b8106).
So over the last couple of weekends I cooked up the sysfs kernel module below, which lists all fw_cfg files under /sys/firmware/fw_cfg/<filename>. I'm building it against the current Fedora (22) running kernel (after installing the kernel-devel rpm) using the following Makefile: obj-m := fw_cfg.o KDIR := /lib/modules/$(shell uname -r)/build PWD := $(shell pwd) default: $(MAKE) -C $(KDIR) SUBDIRS=$(PWD) modules I'm looking for early feedback before trying to push this into the kernel: 1. Right now, only fw_cfg *files* are listed (not sure it's worth trying for the pre-FW_CFG_FILE_FIRST blobs, since AFAICT there's no good way of figuring out their size, not to mention even knowing which ones are present in the first place. 2. File names are listed in /sys/fs/fw_cfg/... with slashes replaced exclamation marks, e.g.: # ls /sys/firmware/fw_cfg/ bootorder etc!e820 etc!table-loader etc!acpi!rsdp etc!smbios!smbios-anchor genroms!kvmvapic.bin etc!acpi!tables etc!smbios!smbios-tables etc!boot-fail-wait etc!system-states That's done automatically by kobject_init_and_add(), and I'm hoping it's acceptable, since the alternative involves parsing file names and building some sort of hierarchy of ksets representing subfolders like "etc", "etc/smbios/", "etc/acpi/", "opt/whatever/", etc. 3. I'm currently only handling x86 and I/O ports. I could drop the fw_cfg_dmi_whitelist and just check the signature, using mmio where appropriate, but I don't have a handy-dandy set of VMs for those architectures on which I could test. Wondering if that's something we should have before I officially try to submit this to the kernel, or whether it could wait for a second iteration. Speaking of the kernel: My default plan is to subscribe to kernelnewb...@kernelnewbies.org and submit it there (this is after all baby's first kernel module :) but if there's a better route for pushing it upstream, please feel free to suggest it to me. Thanks much, --Gabriel PS. This still leaves me with the open question of how to do something similar on Windows, but I'm less bothered by the concept of compiling an in-house, ad-hoc, binary-from-hell program to simply read/write from the fw_cfg I/O ports on Windows :) Although in principle it'd be nice to have a standard, cross-platform way of doing things, likely involving the guest agent... /* fw_cfg.c * * Expose entries from QEMU's firmware configuration (fw_cfg) device in * sysfs (read-only, under "/sys/firmware/fw_cfg/<fw_cfg_filename>"). * * NOTE: '/' chars in fw_cfg file names automatically converted to '!' by * the kobject_init_and_add() call. */ #include <linux/module.h> #include <linux/capability.h> #include <linux/slab.h> #include <linux/dmi.h> /* fw_cfg i/o ports */ #define FW_CFG_PORT_CTL 0x510 #define FW_CFG_PORT_DATA 0x511 /* 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]; }; /* provide atomic read access to hardware fw_cfg device * (critical section involves potentially lengthy i/o, using mutex) */ static DEFINE_MUTEX(fw_cfg_dev_lock); /* 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); outw(select, FW_CFG_PORT_CTL); while (pos-- > 0) inb(FW_CFG_PORT_DATA); insb(FW_CFG_PORT_DATA, buf, count); mutex_unlock(&fw_cfg_dev_lock); } /* 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 the kernel, so we need mutual exclusion; * (critical section is super-short, using spinlock) */ 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, }; /* whitelist only hardware likely to have a fw_cfg device */ static int fw_cfg_dmi_match(const struct dmi_system_id *id) { return 1; } static __initdata struct dmi_system_id fw_cfg_whitelist[] = { { fw_cfg_dmi_match, "QEMU Standard PC", { DMI_MATCH(DMI_SYS_VENDOR, "QEMU"), DMI_MATCH(DMI_PRODUCT_NAME, "Standard PC") }, }, { .ident = NULL } }; /* object set to represent fw_cfg sysfs subfolder */ static struct kset *fw_cfg_kset; static int __init fw_cfg_sysfs_init(void) { int ret = 0; uint32_t count, i; char sig[FW_CFG_SIG_SIZE]; struct fw_cfg_sysfs_entry *entry; /* only install on matching "hardware" */ if (!dmi_check_system(fw_cfg_whitelist)) { pr_warn("Supported QEMU Standard PC hardware not found!\n"); return -ENODEV; } /* 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) { pr_warn("QEMU fw_cfg signature not found!\n"); return -ENODEV; } /* create fw_cfg folder in sysfs */ fw_cfg_kset = kset_create_and_add("fw_cfg", NULL, firmware_kobj); if (!fw_cfg_kset) return -ENOMEM; /* process fw_cfg file directory entry */ mutex_lock(&fw_cfg_dev_lock); outw(FW_CFG_FILE_DIR, FW_CFG_PORT_CTL); /* select fw_cfg directory */ insb(FW_CFG_PORT_DATA, &count, sizeof(count)); /* get file count */ count = be32_to_cpu(count); /* create & register a sysfs node for each file in the directory */ for (i = 0; i < count; i++) { /* allocate new entry */ entry = kzalloc(sizeof(*entry), GFP_KERNEL); if (!entry) { ret = -ENOMEM; break; } /* acquire file information from directory */ insb(FW_CFG_PORT_DATA, &entry->f, sizeof(struct fw_cfg_file)); entry->f.size = be32_to_cpu(entry->f.size); entry->f.select = be16_to_cpu(entry->f.select); /* register sysfs entry for this file */ entry->kobj.kset = fw_cfg_kset; ret = kobject_init_and_add(&entry->kobj, &fw_cfg_sysfs_entry_ktype, NULL, /* NOTE: '/' represented as '!' */ "%s", entry->f.name); if (ret) break; /* add raw binary content access */ ret = sysfs_create_bin_file(&entry->kobj, &fw_cfg_sysfs_attr_raw); if (ret) break; /* add entry to global cache */ fw_cfg_sysfs_cache_enlist(entry); } mutex_unlock(&fw_cfg_dev_lock); if (ret) { fw_cfg_sysfs_cache_cleanup(); kset_unregister(fw_cfg_kset); } else pr_debug("fw_cfg: loaded.\n"); return ret; } static void __exit fw_cfg_sysfs_exit(void) { pr_debug("fw_cfg: unloading.\n"); fw_cfg_sysfs_cache_cleanup(); kset_unregister(fw_cfg_kset); } module_init(fw_cfg_sysfs_init); module_exit(fw_cfg_sysfs_exit); MODULE_AUTHOR("Gabriel L. Somlo <so...@cmu.edu>"); MODULE_DESCRIPTION("QEMU fw_cfg sysfs support"); MODULE_LICENSE("GPL"); MODULE_DEVICE_TABLE(dmi, fw_cfg_whitelist);