Skip to content

Commit

Permalink
efi: Add device path parser
Browse files Browse the repository at this point in the history
We're about to extended the efistub to retrieve device properties from
EFI on Apple Macs. The properties use EFI Device Paths to indicate the
device they belong to. This commit adds a parser which, given an EFI
Device Path, locates the corresponding struct device and returns a
reference to it.

Initially only ACPI and PCI Device Path nodes are supported, these are
the only types needed for Apple device properties (the corresponding
macOS function AppleACPIPlatformExpert::matchEFIDevicePath() does not
support any others). Further node types can be added with little to
moderate effort.

Apple device properties is currently the only use case of this parser,
but Peter Jones intends to use it to match up devices with the
ConInDev/ConOutDev/ErrOutDev variables and add sysfs attributes to these
devices to say the hardware supports using them as console. Thus,
make this parser a separate component which can be selected with config
option EFI_DEV_PATH_PARSER. It can in principle be compiled as a module
if acpi_get_first_physical_node() and acpi_bus_type are exported (and
efi_get_device_by_path() itself is exported).

The dependency on CONFIG_ACPI is needed for acpi_match_device_ids().
It can be removed if an empty inline stub is added for that function.

Signed-off-by: Lukas Wunner <lukas@wunner.de>
Signed-off-by: Matt Fleming <matt@codeblueprint.co.uk>
Cc: Andreas Noever <andreas.noever@gmail.com>
Cc: Ard Biesheuvel <ard.biesheuvel@linaro.org>
Cc: Linus Torvalds <torvalds@linux-foundation.org>
Cc: Peter Jones <pjones@redhat.com>
Cc: Peter Zijlstra <peterz@infradead.org>
Cc: Thomas Gleixner <tglx@linutronix.de>
Cc: linux-efi@vger.kernel.org
Link: http://lkml.kernel.org/r/20161112213237.8804-7-matt@codeblueprint.co.uk
Signed-off-by: Ingo Molnar <mingo@kernel.org>
  • Loading branch information
Lukas Wunner authored and Ingo Molnar committed Nov 13, 2016
1 parent 568bc4e commit 46cd4b7
Show file tree
Hide file tree
Showing 4 changed files with 229 additions and 0 deletions.
5 changes: 5 additions & 0 deletions drivers/firmware/efi/Kconfig
Original file line number Diff line number Diff line change
Expand Up @@ -133,3 +133,8 @@ endmenu

config UEFI_CPER
bool

config EFI_DEV_PATH_PARSER
bool
depends on ACPI
default n
1 change: 1 addition & 0 deletions drivers/firmware/efi/Makefile
Original file line number Diff line number Diff line change
Expand Up @@ -21,6 +21,7 @@ obj-$(CONFIG_EFI_STUB) += libstub/
obj-$(CONFIG_EFI_FAKE_MEMMAP) += fake_mem.o
obj-$(CONFIG_EFI_BOOTLOADER_CONTROL) += efibc.o
obj-$(CONFIG_EFI_TEST) += test/
obj-$(CONFIG_EFI_DEV_PATH_PARSER) += dev-path-parser.o

arm-obj-$(CONFIG_EFI) := arm-init.o arm-runtime.o
obj-$(CONFIG_ARM) += $(arm-obj-y)
Expand Down
203 changes: 203 additions & 0 deletions drivers/firmware/efi/dev-path-parser.c
Original file line number Diff line number Diff line change
@@ -0,0 +1,203 @@
/*
* dev-path-parser.c - EFI Device Path parser
* Copyright (C) 2016 Lukas Wunner <lukas@wunner.de>
*
* This program is free software; you can redistribute it and/or modify
* it under the terms of the GNU General Public License (version 2) 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, see <http://www.gnu.org/licenses/>.
*/

#include <linux/acpi.h>
#include <linux/efi.h>
#include <linux/pci.h>

struct acpi_hid_uid {
struct acpi_device_id hid[2];
char uid[11]; /* UINT_MAX + null byte */
};

static int __init match_acpi_dev(struct device *dev, void *data)
{
struct acpi_hid_uid hid_uid = *(struct acpi_hid_uid *)data;
struct acpi_device *adev = to_acpi_device(dev);

if (acpi_match_device_ids(adev, hid_uid.hid))
return 0;

if (adev->pnp.unique_id)
return !strcmp(adev->pnp.unique_id, hid_uid.uid);
else
return !strcmp("0", hid_uid.uid);
}

static long __init parse_acpi_path(struct efi_dev_path *node,
struct device *parent, struct device **child)
{
struct acpi_hid_uid hid_uid = {};
struct device *phys_dev;

if (node->length != 12)
return -EINVAL;

sprintf(hid_uid.hid[0].id, "%c%c%c%04X",
'A' + ((node->acpi.hid >> 10) & 0x1f) - 1,
'A' + ((node->acpi.hid >> 5) & 0x1f) - 1,
'A' + ((node->acpi.hid >> 0) & 0x1f) - 1,
node->acpi.hid >> 16);
sprintf(hid_uid.uid, "%u", node->acpi.uid);

*child = bus_find_device(&acpi_bus_type, NULL, &hid_uid,
match_acpi_dev);
if (!*child)
return -ENODEV;

phys_dev = acpi_get_first_physical_node(to_acpi_device(*child));
if (phys_dev) {
get_device(phys_dev);
put_device(*child);
*child = phys_dev;
}

return 0;
}

static int __init match_pci_dev(struct device *dev, void *data)
{
unsigned int devfn = *(unsigned int *)data;

return dev_is_pci(dev) && to_pci_dev(dev)->devfn == devfn;
}

static long __init parse_pci_path(struct efi_dev_path *node,
struct device *parent, struct device **child)
{
unsigned int devfn;

if (node->length != 6)
return -EINVAL;
if (!parent)
return -EINVAL;

devfn = PCI_DEVFN(node->pci.dev, node->pci.fn);

*child = device_find_child(parent, &devfn, match_pci_dev);
if (!*child)
return -ENODEV;

return 0;
}

/*
* Insert parsers for further node types here.
*
* Each parser takes a pointer to the @node and to the @parent (will be NULL
* for the first device path node). If a device corresponding to @node was
* found below @parent, its reference count should be incremented and the
* device returned in @child.
*
* The return value should be 0 on success or a negative int on failure.
* The special return values 0x01 (EFI_DEV_END_INSTANCE) and 0xFF
* (EFI_DEV_END_ENTIRE) signal the end of the device path, only
* parse_end_path() is supposed to return this.
*
* Be sure to validate the node length and contents before commencing the
* search for a device.
*/

static long __init parse_end_path(struct efi_dev_path *node,
struct device *parent, struct device **child)
{
if (node->length != 4)
return -EINVAL;
if (node->sub_type != EFI_DEV_END_INSTANCE &&
node->sub_type != EFI_DEV_END_ENTIRE)
return -EINVAL;
if (!parent)
return -ENODEV;

*child = get_device(parent);
return node->sub_type;
}

/**
* efi_get_device_by_path - find device by EFI Device Path
* @node: EFI Device Path
* @len: maximum length of EFI Device Path in bytes
*
* Parse a series of EFI Device Path nodes at @node and find the corresponding
* device. If the device was found, its reference count is incremented and a
* pointer to it is returned. The caller needs to drop the reference with
* put_device() after use. The @node pointer is updated to point to the
* location immediately after the "End of Hardware Device Path" node.
*
* If another Device Path instance follows, @len is decremented by the number
* of bytes consumed. Otherwise @len is set to %0.
*
* If a Device Path node is malformed or its corresponding device is not found,
* @node is updated to point to this offending node and an ERR_PTR is returned.
*
* If @len is initially %0, the function returns %NULL. Thus, to iterate over
* all instances in a path, the following idiom may be used:
*
* while (!IS_ERR_OR_NULL(dev = efi_get_device_by_path(&node, &len))) {
* // do something with dev
* put_device(dev);
* }
* if (IS_ERR(dev))
* // report error
*
* Devices can only be found if they're already instantiated. Most buses
* instantiate devices in the "subsys" initcall level, hence the earliest
* initcall level in which this function should be called is "fs".
*
* Returns the device on success or
* %ERR_PTR(-ENODEV) if no device was found,
* %ERR_PTR(-EINVAL) if a node is malformed or exceeds @len,
* %ERR_PTR(-ENOTSUPP) if support for a node type is not yet implemented.
*/
struct device * __init efi_get_device_by_path(struct efi_dev_path **node,
size_t *len)
{
struct device *parent = NULL, *child;
long ret = 0;

if (!*len)
return NULL;

while (!ret) {
if (*len < 4 || *len < (*node)->length)
ret = -EINVAL;
else if ((*node)->type == EFI_DEV_ACPI &&
(*node)->sub_type == EFI_DEV_BASIC_ACPI)
ret = parse_acpi_path(*node, parent, &child);
else if ((*node)->type == EFI_DEV_HW &&
(*node)->sub_type == EFI_DEV_PCI)
ret = parse_pci_path(*node, parent, &child);
else if (((*node)->type == EFI_DEV_END_PATH ||
(*node)->type == EFI_DEV_END_PATH2))
ret = parse_end_path(*node, parent, &child);
else
ret = -ENOTSUPP;

put_device(parent);
if (ret < 0)
return ERR_PTR(ret);

parent = child;
*node = (void *)*node + (*node)->length;
*len -= (*node)->length;
}

if (ret == EFI_DEV_END_ENTIRE)
*len = 0;

return child;
}
20 changes: 20 additions & 0 deletions include/linux/efi.h
Original file line number Diff line number Diff line change
Expand Up @@ -1148,6 +1148,26 @@ struct efi_generic_dev_path {
u16 length;
} __attribute ((packed));

struct efi_dev_path {
u8 type; /* can be replaced with unnamed */
u8 sub_type; /* struct efi_generic_dev_path; */
u16 length; /* once we've moved to -std=c11 */
union {
struct {
u32 hid;
u32 uid;
} acpi;
struct {
u8 fn;
u8 dev;
} pci;
};
} __attribute ((packed));

#if IS_ENABLED(CONFIG_EFI_DEV_PATH_PARSER)
struct device *efi_get_device_by_path(struct efi_dev_path **node, size_t *len);
#endif

static inline void memrange_efi_to_native(u64 *addr, u64 *npages)
{
*npages = PFN_UP(*addr + (*npages<<EFI_PAGE_SHIFT)) - PFN_DOWN(*addr);
Expand Down

0 comments on commit 46cd4b7

Please sign in to comment.