| From 0db4447e8fadbaf61ac9e0c2265b869475e7ff8e Mon Sep 17 00:00:00 2001 |
| From: Rajat Jain <rajatja@google.com> |
| Date: Fri, 10 Jul 2020 13:53:50 -0700 |
| Subject: [PATCH] CHROMIUM: PCI: Allowlist for drivers allowed to bind to |
| untrusted devices |
| |
| Today, firmware can mark certain rootports as "external-facing" and the |
| devices downstream those are treated as "untrusted". In chromeos, this |
| mechanism is used to mark thunderbolt port and SD express ports as |
| external-facing, an thus any external PCI devices further downstream as |
| untrusted. |
| |
| This patch restricts the PCI drivers that are allowed to bind to those |
| external PCI (untrusted) devices. The allowlist of drivers is to be fed |
| by userspace via a sysfs attribute: |
| |
| /sys/bus/pci/drivers/drivers_allowlist |
| When read, shows the current allowlist of drivers (one per line). |
| When written, allows to add a driver to the allowlist. |
| |
| Note that adding drivers to allowlist only affects future binding of |
| that driver (to external devices that are discovered in future). |
| |
| PS: The approach of having an allowlist for PCI drivers for external PCI |
| devices was discussed at length upstream, but unfortunately nack'ed by |
| upstream. The discussion was scattered across multiple (long) threads. |
| https://lore.kernel.org/linux-pci/20200607113632.GA49147@kroah.com/t/#u |
| https://lkml.org/lkml/2020/6/30/23 |
| https://lore.kernel.org/linux-pci/20200626002710.110200-2-rajatja@google.com/ |
| https://lore.kernel.org/linux-pci/20200616011742.138975-4-rajatja@google.com/T/#u |
| |
| BUG=b:153180503,b:151065827,b:140645321 |
| TEST=Test using the Caldigit docking station that the external PCI |
| devices are attached to driver only when the driver entry is added |
| to the allowlist. |
| |
| Signed-off-by: Rajat Jain <rajatja@google.com> |
| Change-Id: I0647e671c1c098dbe59b19883a1d78c37069161d |
| Reviewed-on: https://chromium-review.googlesource.com/c/chromiumos/third_party/kernel/+/2298049 |
| |
| [rebase66(tzungbi): |
| Revised commit message. |
| Squashed: |
| CHROMIUM: pci/allowlist: Log & send udev events for allowing/blocking drivers |
| (https://crrev.com/c/2465192) |
| CHROMIUM: pci/allowlist: sysfs attribute to lockdown external devices completely |
| (https://crrev.com/c/2494960) |
| CHROMIUM: pci/allowlist: Release lockdown_sem before attaching devices |
| (https://crrev.com/c/2551845) |
| CHROMIUM: pci-drvr-allowlist: Use the upstreamed attribute |
| (https://crrev.com/c/3163527) |
| CHROMIUM: pci/allowlist: Fixup the log message |
| (https://crrev.com/c/3191481) |
| FIXUP: CHROMIUM: PCI: Allowlist for drivers allowed to bind to untrusted devices |
| ] |
| Signed-off-by: Tzung-Bi Shih <tzungbi@chromium.org> |
| --- |
| drivers/pci/Makefile | 4 +- |
| drivers/pci/drvr-allowlist.c | 238 +++++++++++++++++++++++++++++++++++ |
| drivers/pci/pci-driver.c | 2 +- |
| drivers/pci/pci.h | 2 + |
| 4 files changed, 243 insertions(+), 3 deletions(-) |
| create mode 100644 drivers/pci/drvr-allowlist.c |
| |
| diff --git a/drivers/pci/Makefile b/drivers/pci/Makefile |
| index 1753020368909e1f8e770ef171ff7d7c1bf40437..96d45e89e70563dd0810a7ddb78349ad52e0d729 100644 |
| --- a/drivers/pci/Makefile |
| +++ b/drivers/pci/Makefile |
| @@ -5,8 +5,8 @@ |
| obj-$(CONFIG_PCI) += access.o bus.o probe.o host-bridge.o \ |
| remove.o pci.o pci-driver.o search.o \ |
| rom.o setup-res.o irq.o vpd.o \ |
| - setup-bus.o vc.o mmap.o devres.o |
| - |
| + setup-bus.o vc.o mmap.o devres.o \ |
| + drvr-allowlist.o |
| obj-$(CONFIG_PCI) += msi/ |
| obj-$(CONFIG_PCI) += pcie/ |
| |
| diff --git a/drivers/pci/drvr-allowlist.c b/drivers/pci/drvr-allowlist.c |
| new file mode 100644 |
| index 0000000000000000000000000000000000000000..c3754b87c838a658923e6f19bb78f90997244e60 |
| --- /dev/null |
| +++ b/drivers/pci/drvr-allowlist.c |
| @@ -0,0 +1,238 @@ |
| +// SPDX-License-Identifier: GPL-2.0 |
| +/* |
| + * Allowlist of PCI drivers that are allowed to bind to external devices |
| + */ |
| + |
| +#include <linux/ctype.h> |
| +#include <linux/module.h> |
| +#include <linux/pci.h> |
| +#include "pci.h" |
| + |
| +/* |
| + * Parameter to essentially disable allowlist code (thus allow all drivers to |
| + * connect to any external PCI devices). |
| + */ |
| +static bool trust_external_pci_devices; |
| +core_param(trust_external_pci_devices, trust_external_pci_devices, bool, 0444); |
| + |
| +/* Driver allowlist */ |
| +struct allowlist_entry { |
| + const char *drvr_name; |
| + struct list_head node; |
| +}; |
| + |
| +static LIST_HEAD(allowlist); |
| +static DECLARE_RWSEM(allowlist_sem); |
| + |
| +#define TRUNCATED "...<truncated>\n" |
| + |
| +/* |
| + * Locks down the binding of drivers to untrusted devices |
| + * (No PCI drivers to bind to any new untrusted PCI device) |
| + */ |
| +static bool drivers_allowlist_lockdown = true; |
| +static DECLARE_RWSEM(lockdown_sem); |
| + |
| +static ssize_t drivers_allowlist_show(const struct bus_type *bus, char *buf) |
| +{ |
| + size_t count = 0; |
| + struct allowlist_entry *entry; |
| + |
| + down_read(&allowlist_sem); |
| + list_for_each_entry(entry, &allowlist, node) { |
| + if (count + strlen(entry->drvr_name) + sizeof(TRUNCATED) < |
| + PAGE_SIZE) { |
| + count += snprintf(buf + count, PAGE_SIZE - count, |
| + "%s\n", entry->drvr_name); |
| + } else { |
| + count += snprintf(buf + count, PAGE_SIZE - count, |
| + TRUNCATED); |
| + break; |
| + } |
| + } |
| + up_read(&allowlist_sem); |
| + return count; |
| +} |
| + |
| +static ssize_t drivers_allowlist_store(const struct bus_type *bus, const char *buf, |
| + size_t count) |
| +{ |
| + struct allowlist_entry *entry; |
| + ssize_t ret = count; |
| + unsigned int i; |
| + char *drv; |
| + |
| + if (!count) |
| + return -EINVAL; |
| + |
| + drv = kstrndup(buf, count, GFP_KERNEL); |
| + if (!drv) |
| + return -ENOMEM; |
| + |
| + /* Remove any trailing white spaces */ |
| + strim(drv); |
| + if (!*drv) { |
| + ret = -EINVAL; |
| + goto out_kfree; |
| + } |
| + |
| + /* Driver names cannot have special characters */ |
| + for (i = 0; i < strlen(drv); i++) |
| + if (!isalnum(drv[i]) && drv[i] != '_') { |
| + ret = -EINVAL; |
| + goto out_kfree; |
| + } |
| + |
| + down_write(&allowlist_sem); |
| + |
| + /* Lookup in the allowlist */ |
| + list_for_each_entry(entry, &allowlist, node) |
| + if (!strcmp(drv, entry->drvr_name)) { |
| + ret = -EEXIST; |
| + goto out_release_sem; |
| + } |
| + |
| + /* Add a driver to the allowlist */ |
| + entry = kmalloc(sizeof(*entry), GFP_KERNEL); |
| + if (!entry) { |
| + ret = -ENOMEM; |
| + goto out_release_sem; |
| + } |
| + entry->drvr_name = drv; |
| + list_add_tail(&entry->node, &allowlist); |
| + up_write(&allowlist_sem); |
| + return ret; |
| + |
| +out_release_sem: |
| + up_write(&allowlist_sem); |
| +out_kfree: |
| + kfree(drv); |
| + return ret; |
| +} |
| +static BUS_ATTR_RW(drivers_allowlist); |
| + |
| +static ssize_t drivers_allowlist_lockdown_show(const struct bus_type *bus, char *buf) |
| +{ |
| + int ret; |
| + |
| + down_read(&lockdown_sem); |
| + ret = sprintf(buf, "%u\n", drivers_allowlist_lockdown); |
| + up_read(&lockdown_sem); |
| + |
| + return ret; |
| +} |
| + |
| +static ssize_t |
| +drivers_allowlist_lockdown_store(const struct bus_type *bus, const char *buf, |
| + size_t count) |
| +{ |
| + bool lockdown, state_changed = false; |
| + struct pci_dev *dev = NULL; |
| + |
| + if (strtobool(buf, &lockdown)) |
| + return -EINVAL; |
| + |
| + down_write(&lockdown_sem); |
| + if (drivers_allowlist_lockdown != lockdown) { |
| + drivers_allowlist_lockdown = lockdown; |
| + state_changed = true; |
| + } |
| + up_write(&lockdown_sem); |
| + |
| + if (state_changed && !lockdown) { |
| + /* Attach any devices blocked earlier, subject to allowlist */ |
| + for_each_pci_dev(dev) { |
| + if (dev_is_removable(&dev->dev) && |
| + !device_attach(&dev->dev)) |
| + pci_dbg(dev, "No driver\n"); |
| + } |
| + } |
| + return count; |
| +} |
| +static BUS_ATTR_RW(drivers_allowlist_lockdown); |
| + |
| +static int __init pci_drivers_allowlist_init(void) |
| +{ |
| + int ret; |
| + |
| + if (trust_external_pci_devices) |
| + return 0; |
| + |
| + ret = bus_create_file(&pci_bus_type, &bus_attr_drivers_allowlist); |
| + if (ret) { |
| + pr_err("%s: failed to create allowlist in sysfs\n", __func__); |
| + return ret; |
| + } |
| + |
| + ret = bus_create_file(&pci_bus_type, |
| + &bus_attr_drivers_allowlist_lockdown); |
| + if (ret) { |
| + pr_err("%s: failed to create allowlist_lockdown\n", __func__); |
| + bus_remove_file(&pci_bus_type, &bus_attr_drivers_allowlist); |
| + } |
| + return ret; |
| +} |
| +late_initcall(pci_drivers_allowlist_init); |
| + |
| +static bool pci_driver_is_allowed(const char *name) |
| +{ |
| + struct allowlist_entry *entry; |
| + |
| + down_read(&allowlist_sem); |
| + list_for_each_entry(entry, &allowlist, node) { |
| + if (!strcmp(name, entry->drvr_name)) { |
| + up_read(&allowlist_sem); |
| + return true; |
| + } |
| + } |
| + up_read(&allowlist_sem); |
| + return false; |
| +} |
| + |
| +bool pci_allowed_to_attach(struct pci_driver *drv, struct pci_dev *dev) |
| +{ |
| + char event[16], drvr[32], *reason; |
| + char *udev_env[] = { event, drvr, NULL }; |
| + |
| + snprintf(drvr, sizeof(drvr), "DRVR=%s", drv->name); |
| + |
| + /* Bypass Allowlist code, if platform wants so */ |
| + if (trust_external_pci_devices) { |
| + reason = "trust_external_pci_devices"; |
| + goto allowed; |
| + } |
| + |
| + /* Allow internal devices */ |
| + if (!dev_is_removable(&dev->dev)) { |
| + reason = "internal device"; |
| + goto allowed; |
| + } |
| + |
| + /* Don't allow any driver attaches, if locked down */ |
| + down_read(&lockdown_sem); |
| + if (drivers_allowlist_lockdown) { |
| + up_read(&lockdown_sem); |
| + reason = "drivers_allowlist_lockdown enforced"; |
| + goto not_allowed; |
| + } |
| + up_read(&lockdown_sem); |
| + |
| + /* Allow if driver is in allowlist */ |
| + if (pci_driver_is_allowed(drv->name)) { |
| + reason = "drvr in allowlist"; |
| + goto allowed; |
| + } |
| + reason = "drvr not in allowlist"; |
| + |
| +not_allowed: |
| + pci_err(dev, "attach not allowed to drvr %s [%s]\n", drv->name, reason); |
| + snprintf(event, sizeof(event), "EVENT=BLOCKED"); |
| + kobject_uevent_env(&dev->dev.kobj, KOBJ_CHANGE, udev_env); |
| + return false; |
| + |
| +allowed: |
| + pci_info(dev, "attach allowed to drvr %s [%s]\n", drv->name, reason); |
| + snprintf(event, sizeof(event), "EVENT=ALLOWED"); |
| + kobject_uevent_env(&dev->dev.kobj, KOBJ_CHANGE, udev_env); |
| + return true; |
| +} |
| diff --git a/drivers/pci/pci-driver.c b/drivers/pci/pci-driver.c |
| index af2996d0d17ffc0fa2ae730eb09d58243cf79a5b..7a047a871e74119ef377b909e9bd9332ae948828 100644 |
| --- a/drivers/pci/pci-driver.c |
| +++ b/drivers/pci/pci-driver.c |
| @@ -1514,7 +1514,7 @@ static int pci_bus_match(struct device *dev, struct device_driver *drv) |
| |
| pci_drv = to_pci_driver(drv); |
| found_id = pci_match_device(pci_drv, pci_dev); |
| - if (found_id) |
| + if (found_id && pci_allowed_to_attach(pci_drv, pci_dev)) |
| return 1; |
| |
| return 0; |
| diff --git a/drivers/pci/pci.h b/drivers/pci/pci.h |
| index 17fed18468474b8ed75694216c742ef55c9079e1..a2a2e71ea458950cc42be80f2ab5c6fd62462a96 100644 |
| --- a/drivers/pci/pci.h |
| +++ b/drivers/pci/pci.h |
| @@ -792,6 +792,8 @@ extern const struct attribute_group aspm_ctrl_attr_group; |
| #endif |
| |
| extern const struct attribute_group pci_dev_reset_method_attr_group; |
| +bool pci_drv_allowed_for_untrusted_devs(struct device_driver *drvr); |
| +bool pci_allowed_to_attach(struct pci_driver *drv, struct pci_dev *dev); |
| |
| #ifdef CONFIG_X86_INTEL_MID |
| bool pci_use_mid_pm(void); |
| -- |
| 2.44.0.478.gd926399ef9-goog |
| |