Linux kernel mirror (for testing) git.kernel.org/pub/scm/linux/kernel/git/torvalds/linux.git
kernel os linux

PCI: pciehp: Detect device replacement during system sleep

Ricky reports that replacing a device in a hotplug slot during ACPI sleep
state S3 does not cause re-enumeration on resume, as one would expect.
Instead, the new device is treated as if it was the old one.

There is no bulletproof way to detect device replacement, but as a
heuristic, check whether the device identity in config space matches cached
data in struct pci_dev (Vendor ID, Device ID, Class Code, Revision ID,
Subsystem Vendor ID, Subsystem ID). Additionally, cache and compare the
Device Serial Number (PCIe r6.2 sec 7.9.3). If a mismatch is detected,
mark the old device disconnected (to prevent its driver from accessing the
new device) and synthesize a Presence Detect Changed event.

The device identity in config space which is compared here is the same as
the one included in the signed Subject Alternative Name per PCIe r6.1 sec
6.31.3. Thus, the present commit prevents attacks where a valid device is
replaced with a malicious device during system sleep and the valid device's
driver obliviously accesses the malicious device.

This is about as much as can be done at the PCI layer. Drivers may have
additional ways to identify devices (such as reading a WWID from some
register) and may trigger re-enumeration when detecting an identity change
on resume.

Link: https://lore.kernel.org/r/a1afaa12f341d146ecbea27c1743661c71683833.1716992815.git.lukas@wunner.de
Reported-by: Ricky Wu <ricky_wu@realtek.com>
Closes: https://lore.kernel.org/r/a608b5930d0a48f092f717c0e137454b@realtek.com
Tested-by: Ricky Wu <ricky_wu@realtek.com>
Signed-off-by: Lukas Wunner <lukas@wunner.de>
Signed-off-by: Bjorn Helgaas <bhelgaas@google.com>

authored by

Lukas Wunner and committed by
Bjorn Helgaas
9d573d19 1613e604

+54 -1
+4
drivers/pci/hotplug/pciehp.h
··· 46 46 /** 47 47 * struct controller - PCIe hotplug controller 48 48 * @pcie: pointer to the controller's PCIe port service device 49 + * @dsn: cached copy of Device Serial Number of Function 0 in the hotplug slot 50 + * (PCIe r6.2 sec 7.9.3); used to determine whether a hotplugged device 51 + * was replaced with a different one during system sleep 49 52 * @slot_cap: cached copy of the Slot Capabilities register 50 53 * @inband_presence_disabled: In-Band Presence Detect Disable supported by 51 54 * controller and disabled per spec recommendation (PCIe r5.0, appendix I ··· 90 87 */ 91 88 struct controller { 92 89 struct pcie_device *pcie; 90 + u64 dsn; 93 91 94 92 u32 slot_cap; /* capabilities and quirks */ 95 93 unsigned int inband_presence_disabled:1;
+41 -1
drivers/pci/hotplug/pciehp_core.c
··· 284 284 return 0; 285 285 } 286 286 287 + static bool pciehp_device_replaced(struct controller *ctrl) 288 + { 289 + struct pci_dev *pdev __free(pci_dev_put); 290 + u32 reg; 291 + 292 + pdev = pci_get_slot(ctrl->pcie->port->subordinate, PCI_DEVFN(0, 0)); 293 + if (!pdev) 294 + return true; 295 + 296 + if (pci_read_config_dword(pdev, PCI_VENDOR_ID, &reg) || 297 + reg != (pdev->vendor | (pdev->device << 16)) || 298 + pci_read_config_dword(pdev, PCI_CLASS_REVISION, &reg) || 299 + reg != (pdev->revision | (pdev->class << 8))) 300 + return true; 301 + 302 + if (pdev->hdr_type == PCI_HEADER_TYPE_NORMAL && 303 + (pci_read_config_dword(pdev, PCI_SUBSYSTEM_VENDOR_ID, &reg) || 304 + reg != (pdev->subsystem_vendor | (pdev->subsystem_device << 16)))) 305 + return true; 306 + 307 + if (pci_get_dsn(pdev) != ctrl->dsn) 308 + return true; 309 + 310 + return false; 311 + } 312 + 287 313 static int pciehp_resume_noirq(struct pcie_device *dev) 288 314 { 289 315 struct controller *ctrl = get_service_data(dev); ··· 319 293 ctrl->cmd_busy = true; 320 294 321 295 /* clear spurious events from rediscovery of inserted card */ 322 - if (ctrl->state == ON_STATE || ctrl->state == BLINKINGOFF_STATE) 296 + if (ctrl->state == ON_STATE || ctrl->state == BLINKINGOFF_STATE) { 323 297 pcie_clear_hotplug_events(ctrl); 298 + 299 + /* 300 + * If hotplugged device was replaced with a different one 301 + * during system sleep, mark the old device disconnected 302 + * (to prevent its driver from accessing the new device) 303 + * and synthesize a Presence Detect Changed event. 304 + */ 305 + if (pciehp_device_replaced(ctrl)) { 306 + ctrl_dbg(ctrl, "device replaced during system sleep\n"); 307 + pci_walk_bus(ctrl->pcie->port->subordinate, 308 + pci_dev_set_disconnected, NULL); 309 + pciehp_request(ctrl, PCI_EXP_SLTSTA_PDC); 310 + } 311 + } 324 312 325 313 return 0; 326 314 }
+5
drivers/pci/hotplug/pciehp_hpc.c
··· 1055 1055 } 1056 1056 } 1057 1057 1058 + pdev = pci_get_slot(subordinate, PCI_DEVFN(0, 0)); 1059 + if (pdev) 1060 + ctrl->dsn = pci_get_dsn(pdev); 1061 + pci_dev_put(pdev); 1062 + 1058 1063 return ctrl; 1059 1064 } 1060 1065
+4
drivers/pci/hotplug/pciehp_pci.c
··· 72 72 pci_bus_add_devices(parent); 73 73 down_read_nested(&ctrl->reset_lock, ctrl->depth); 74 74 75 + dev = pci_get_slot(parent, PCI_DEVFN(0, 0)); 76 + ctrl->dsn = pci_get_dsn(dev); 77 + pci_dev_put(dev); 78 + 75 79 out: 76 80 pci_unlock_rescan_remove(); 77 81 return ret;