summaryrefslogtreecommitdiffstats
path: root/kernel/gsp_phys_gpu.c
diff options
context:
space:
mode:
Diffstat (limited to 'kernel/gsp_phys_gpu.c')
-rw-r--r--kernel/gsp_phys_gpu.c262
1 files changed, 250 insertions, 12 deletions
diff --git a/kernel/gsp_phys_gpu.c b/kernel/gsp_phys_gpu.c
index 24ed887..f9fffc4 100644
--- a/kernel/gsp_phys_gpu.c
+++ b/kernel/gsp_phys_gpu.c
@@ -19,6 +19,17 @@
* of the cycles that survived). Nothing in the module's own code
* crashed; the trigger is the loss of the dGPU power domain.
*
+ * Mitigation (implemented here): before the unbind, the whole board - the
+ * GPU, its same-vendor sibling functions on the bus (audio, xHCI, UCSI)
+ * and the upstream root port - is locked into D0 with
+ * pm_runtime_forbid() and pci_d3cold_disable(). The locks are held across
+ * the whole unbind -> claim -> release window. The GPU config ID and the
+ * root-port link status are re-read after the claim and again before
+ * rebinding the distribution driver; an unresponsive board aborts the
+ * handoff and the locks are deliberately left in place. Residual risk: a
+ * platform power cut that is not driven by PCI D3 (EC/SMI or thermal) has
+ * the same signature; if it happens, do not retry in that boot.
+ *
* Therefore claim now requires BOTH gsp_phys_gpu=1 AND
* gsp_phys_hazard_ack=1, and must not be enabled without a reviewed
* recovery plan. The normal emulation path (module load, mdev, VM) does
@@ -32,6 +43,7 @@
#include <linux/module.h>
#include <linux/kernel.h>
#include <linux/pci.h>
+#include <linux/pm_runtime.h>
#include <linux/delay.h>
#include <linux/io.h>
#include <linux/slab.h>
@@ -89,6 +101,21 @@ MODULE_PARM_DESC(gsp_phys_hazard_ack,
#define GSP_PHYS_CPUCTL_HALTED BIT(4)
#define GSP_PHYS_RISCV_ACTIVE BIT(0)
+/* Settle time after the distribution driver probes before releasing D0. */
+#define GSP_PHYS_POWER_SETTLE_MS 1500
+/* GPU + up to 7 sibling functions + upstream root port. */
+#define GSP_PHYS_MAX_POWER_LOCKS 16
+
+/*
+ * One device held in D0 across the claim window. The pre-lock runtime-PM
+ * and D3cold policy is recorded so the unlock restores exactly that policy.
+ */
+struct gsp_phys_power_lock {
+ struct pci_dev *pdev;
+ bool was_runtime_auto;
+ bool was_d3cold_enabled;
+};
+
struct gsp_phys_gpu {
struct pci_dev *pdev;
void __iomem *bar0;
@@ -106,6 +133,8 @@ struct gsp_phys_gpu {
bool drv_registered;
bool claimed;
struct gsp_phys_boot boot;
+ struct gsp_phys_power_lock locks[GSP_PHYS_MAX_POWER_LOCKS];
+ unsigned int nr_locks;
};
static struct gsp_phys_gpu phys;
@@ -373,6 +402,170 @@ static struct pci_driver gsp_phys_driver = {
.remove = gsp_phys_remove,
};
+/*
+ * Power-domain lock: the board's ACPI power resource hangs off the root
+ * port, so a D3 transition of the root port (or of the bridge below it)
+ * cuts the whole board rail. A driverless GPU would let the root port
+ * autosuspend. Forbid runtime PM and D3cold on the GPU, its sibling
+ * functions and the root port before unbinding anything; both knobs live
+ * in struct device and survive driver unbind. Only the exact devices that
+ * were locked are unlocked, restoring their pre-lock policy.
+ */
+static bool gsp_phys_power_lock_one(struct pci_dev *pdev)
+{
+ struct gsp_phys_power_lock *lock;
+
+ if (phys.nr_locks >= ARRAY_SIZE(phys.locks)) {
+ pr_warn("gsp-phys: power-lock table full, %s left unlocked\n",
+ pci_name(pdev));
+ return false;
+ }
+
+ lock = &phys.locks[phys.nr_locks++];
+ lock->pdev = pci_dev_get(pdev);
+ lock->was_runtime_auto = pdev->dev.power.runtime_auto;
+ lock->was_d3cold_enabled = !pdev->no_d3cold;
+
+ pm_runtime_forbid(&pdev->dev);
+ pci_d3cold_disable(pdev);
+
+ pr_info("gsp-phys: power domain locked: %s (runtime_auto was %d, d3cold was %d)\n",
+ pci_name(pdev), lock->was_runtime_auto,
+ lock->was_d3cold_enabled);
+ return true;
+}
+
+static int gsp_phys_power_lock(struct pci_dev *pdev)
+{
+ struct pci_dev *dev;
+ int fn;
+
+ /* The GPU must go first: if it cannot be locked, do not unbind. */
+ if (!gsp_phys_power_lock_one(pdev))
+ return -ENOSPC;
+
+ /* Sibling functions of the same board (audio, xHCI, UCSI, ...). */
+ for (fn = 0; fn < 8; fn++) {
+ dev = pci_get_slot(pdev->bus,
+ PCI_DEVFN(PCI_SLOT(pdev->devfn), fn));
+ if (!dev)
+ continue;
+ if (dev != pdev && dev->vendor == GSP_PHYS_VENDOR)
+ gsp_phys_power_lock_one(dev);
+ pci_dev_put(dev);
+ }
+
+ /* The root port whose D3 triggers the platform board power switch. */
+ dev = pci_upstream_bridge(pdev);
+ if (dev)
+ gsp_phys_power_lock_one(dev);
+
+ return 0;
+}
+
+static void gsp_phys_power_unlock(void)
+{
+ while (phys.nr_locks) {
+ struct gsp_phys_power_lock *lock =
+ &phys.locks[--phys.nr_locks];
+
+ if (lock->was_runtime_auto)
+ pm_runtime_allow(&lock->pdev->dev);
+ if (lock->was_d3cold_enabled)
+ pci_d3cold_enable(lock->pdev);
+
+ pr_info("gsp-phys: power domain released: %s\n",
+ pci_name(lock->pdev));
+ pci_dev_put(lock->pdev);
+ lock->pdev = NULL;
+ }
+}
+
+static const char *gsp_phys_link_speed(u16 lnksta)
+{
+ switch (lnksta & PCI_EXP_LNKSTA_CLS) {
+ case PCI_EXP_LNKSTA_CLS_2_5GB:
+ return "2.5GT/s";
+ case PCI_EXP_LNKSTA_CLS_5_0GB:
+ return "5.0GT/s";
+ case PCI_EXP_LNKSTA_CLS_8_0GB:
+ return "8.0GT/s";
+ case PCI_EXP_LNKSTA_CLS_16_0GB:
+ return "16.0GT/s";
+ default:
+ return "unknown";
+ }
+}
+
+/*
+ * Abort signal for the handoff. The board must answer config reads while
+ * it is claimed and again just before it is handed back; an invalid ID
+ * means the power domain was cut (or never came back) and neither the
+ * deeper boot work nor the distribution driver's power-up path may run.
+ */
+static int gsp_phys_board_check(struct pci_dev *pdev, const char *phase)
+{
+ struct pci_dev *root = pci_upstream_bridge(pdev);
+ u32 id = 0;
+ u16 lnksta = 0;
+ int ret;
+
+ ret = pci_read_config_dword(pdev, PCI_VENDOR_ID, &id);
+ if (ret != PCIBIOS_SUCCESSFUL || id == 0xffffffff ||
+ (id & 0xffff) == 0x0000) {
+ WARN(1, "gsp-phys: board unresponsive %s: %s config id=0x%08x rc=%d; aborting - do not retry in this boot\n",
+ phase, pci_name(pdev), id, ret);
+ return -EIO;
+ }
+
+ if (root && pci_is_pcie(root)) {
+ ret = pcie_capability_read_word(root, PCI_EXP_LNKSTA, &lnksta);
+ if (ret != PCIBIOS_SUCCESSFUL)
+ pr_warn("gsp-phys: %s: root port %s link status read failed (rc=%d)\n",
+ phase, pci_name(root), ret);
+ else
+ pr_info("gsp-phys: board check %s: %s id=%04x:%04x, root %s link speed %s width x%u\n",
+ phase, pci_name(pdev), id & 0xffff, id >> 16,
+ pci_name(root), gsp_phys_link_speed(lnksta),
+ (lnksta & PCI_EXP_LNKSTA_NLW) >>
+ PCI_EXP_LNKSTA_NLW_SHIFT);
+ } else {
+ pr_info("gsp-phys: board check %s: %s id=%04x:%04x (no PCIe root port)\n",
+ phase, pci_name(pdev), id & 0xffff, id >> 16);
+ }
+
+ return 0;
+}
+
+/*
+ * Abort of a claim that did not get as far as owning the GPU: give the
+ * device back to the distribution driver, let its probe settle, then
+ * release the D0 locks.
+ */
+static void gsp_phys_claim_failed(struct pci_dev *pdev)
+{
+ struct device_driver *drv;
+ int ret;
+
+ if (phys.drv_registered) {
+ pci_unregister_driver(&gsp_phys_driver);
+ phys.drv_registered = false;
+ }
+ phys.claimed = false;
+
+ ret = device_attach(&pdev->dev);
+ drv = pdev->dev.driver;
+ if (drv)
+ pr_info("gsp-phys: claim aborted; %s rebound to %s (rc=%d)\n",
+ pci_name(pdev), drv->name, ret);
+ else
+ pr_warn("gsp-phys: claim aborted; %s has no driver (rc=%d)\n",
+ pci_name(pdev), ret);
+
+ msleep(GSP_PHYS_POWER_SETTLE_MS);
+ gsp_phys_power_unlock();
+}
+
int gsp_phys_gpu_claim(void)
{
struct pci_dev *pdev;
@@ -393,6 +586,16 @@ int gsp_phys_gpu_claim(void)
return -ENODEV;
}
+ /*
+ * Lock the whole board and its root port into D0 before the unbind;
+ * see the hazard note at the top of this file.
+ */
+ ret = gsp_phys_power_lock(pdev);
+ if (ret) {
+ pci_dev_put(pdev);
+ return ret;
+ }
+
if (pdev->dev.driver) {
pr_info("gsp-phys: unbinding %s from %s\n",
pdev->dev.driver->name, pci_name(pdev));
@@ -401,26 +604,27 @@ int gsp_phys_gpu_claim(void)
if (pdev->dev.driver) {
pr_err("gsp-phys: %s still bound after unbind (usage count?)\n",
pdev->dev.driver->name);
- pci_dev_put(pdev);
- return -EBUSY;
+ ret = -EBUSY;
+ goto err_recover;
}
ret = pci_register_driver(&gsp_phys_driver);
if (ret) {
pr_err("gsp-phys: register driver failed: %d\n", ret);
- pci_dev_put(pdev);
- return ret;
+ goto err_recover;
}
phys.drv_registered = true;
if (!phys.claimed) {
pr_err("gsp-phys: our driver did not bind %s\n", pci_name(pdev));
- pci_unregister_driver(&gsp_phys_driver);
- phys.drv_registered = false;
- pci_dev_put(pdev);
- return -ENODEV;
+ ret = -ENODEV;
+ goto err_recover;
}
+ ret = gsp_phys_board_check(pdev, "after claim");
+ if (ret)
+ goto err_board;
+
pci_dev_put(pdev);
pr_info("gsp-phys: physical GPU claimed (backend rung 1)\n");
@@ -436,14 +640,32 @@ int gsp_phys_gpu_claim(void)
"backend stays at rung 2\n");
}
return 0;
+
+err_board:
+ if (phys.drv_registered) {
+ pci_unregister_driver(&gsp_phys_driver);
+ phys.drv_registered = false;
+ }
+ phys.claimed = false;
+ pr_err("gsp-phys: claim aborted with the power domain LOCKED and no driver on %s; not rebinding the distribution driver (do not retry in this boot)\n",
+ pci_name(pdev));
+ pci_dev_put(pdev);
+ return ret;
+
+err_recover:
+ gsp_phys_claim_failed(pdev);
+ pci_dev_put(pdev);
+ return ret;
}
EXPORT_SYMBOL_GPL(gsp_phys_gpu_claim);
void gsp_phys_gpu_release(void)
{
struct pci_dev *pdev;
+ struct device_driver *drv;
+ int ret;
- if (!phys.drv_registered && !phys.claimed)
+ if (!phys.drv_registered && !phys.claimed && !phys.nr_locks)
return;
if (phys.pdev)
@@ -457,11 +679,24 @@ void gsp_phys_gpu_release(void)
pci_unregister_driver(&gsp_phys_driver);
phys.drv_registered = false;
}
+ phys.claimed = false;
if (pdev) {
- int ret = device_attach(&pdev->dev);
- struct device_driver *drv = pdev->dev.driver;
+ /*
+ * Abort signal before handing the GPU back: if the board no
+ * longer answers config reads, do not run the distribution
+ * driver's power-up path. Keep the D0 locks so that a
+ * driverless board cannot be power-gated again.
+ */
+ if (gsp_phys_board_check(pdev, "before rebind")) {
+ pr_err("gsp-phys: NOT rebinding %s: board unresponsive; power domain stays LOCKED (do not retry in this boot)\n",
+ pci_name(pdev));
+ pci_dev_put(pdev);
+ return;
+ }
+ ret = device_attach(&pdev->dev);
+ drv = pdev->dev.driver;
if (drv)
pr_info("gsp-phys: released %s; rebound to %s (rc=%d)\n",
pci_name(pdev), drv->name, ret);
@@ -470,6 +705,9 @@ void gsp_phys_gpu_release(void)
"(rc=%d), run `modprobe nvidia`\n",
pci_name(pdev), ret);
pci_dev_put(pdev);
+
+ msleep(GSP_PHYS_POWER_SETTLE_MS);
}
- phys.claimed = false;
+
+ gsp_phys_power_unlock();
}