[PATCH] PCI: Add pcie_get_link_endpoints() helper

From: Priyank Rathod

Date: Mon Aug 31 2026 - 17:07:36 EST


In PCIe topologies, physical links are point-to-point connections
between an Upstream Component (Downstream Port, such as a Root Port
or Switch Downstream Port) and a Downstream Component (Upstream Port,
such as an Endpoint or Switch Upstream Port), per PCIe Base Specification
r7.0/r6.0 sec 1.3.1.

Currently, various drivers across drivers/pci/ independently infer the
two ends of a PCIe link using varied ad-hoc methods:
- ASPM (drivers/pci/pcie/aspm.c): pcie_aspm_get_link(),
alloc_pcie_link_state(), and pci_configure_ltr() resolve parent
bridges and subordinate function 0.
- Precision Time Measurement (drivers/pci/pcie/ptm.c):
pci_upstream_ptm() traverses pci_upstream_bridge() to validate
upstream link partner capabilities.
- PCIe Link Retrain (drivers/pci/pci.c): pcie_retrain_link() and
pcie_update_link_speed() coordinate link retraining from the
Downstream Port across the subordinate bus.
- AER/DPC Recovery (drivers/pci/pcie/err.c): pcie_do_recovery()
identifies the parent bridge via pci_upstream_bridge() to trigger
secondary bus reset and broadcast driver recovery callbacks.
- Lane Margining at Receiver (LMR): margin_enable_write() requires
both link endpoints to establish hierarchical locking and runtime PM
pinning.

Ad-hoc subordinate bus iteration risks races with concurrent device
removal or hotplug if pci_bus_sem is omitted.

Introduce pcie_get_link_endpoints() and pcie_put_link_endpoints() in the
PCI core to provide a standardized, symmetric, and race-safe helper:
- For Endpoints: resolves the parent Downstream Port via
pci_upstream_bridge(pdev).
- For Downstream Ports: safely inspects the subordinate bus under
down_read(&pci_bus_sem) and acquires a reference via pci_dev_get()
on the child device.

Callers release the acquired reference using pcie_put_link_endpoints().

Signed-off-by: Priyank Rathod <rathodpriyank@xxxxxxxxxx>
---
Hi Bjorn, Ilpo, and PCI maintainers,

During the review of the PCIe Lane Margining at Receiver (LMR) patch series
(v7: https://lore.kernel.org/linux-pci/20260828-pcie-lmt-v7-1-6012e9e0940a@xxxxxxxxxx/),
Ilpo Järvinen pointed out that feature drivers (such as LMR) resolving link
partners duplicate link traversal logic that is already present in drivers such
as ASPM:

"This feels like duplicating similar functionality with the aspm driver
that also wants to infer ends of the link when giving a pci_dev in.
The aspm driver currently does that within, but it kind of duplicating
pci_bus. It would be nice to avoid the duplication and have something
similar for this in PCI core."

In PCIe topologies, physical links are point-to-point interconnects
between an Upstream Component (Downstream Port, such as a Root Port or Switch
Downstream Port) and a Downstream Component (Upstream Port, such as an Endpoint
or Switch Upstream Port), per PCIe Base Specification Revision 7.0 / 6.0
Section 1.3.1.

Several subsystems across drivers/pci/ independently infer and coordinate both
ends of a PCIe link:
1. ASPM (drivers/pci/pcie/aspm.c): pcie_aspm_get_link(),
alloc_pcie_link_state(), and pci_configure_ltr() resolve parent bridges
and subordinate function 0 to configure ASPMC and L1SS.
2. Precision Time Measurement (drivers/pci/pcie/ptm.c): pci_upstream_ptm()
traverses pci_upstream_bridge() to validate upstream link partner
capabilities.
3. PCIe Link Retrain (drivers/pci/pci.c): pcie_retrain_link() and
pcie_update_link_speed() coordinate link retraining from the Downstream
Port across the subordinate bus.
4. AER & DPC Recovery (drivers/pci/pcie/err.c): pcie_do_recovery() identifies
the parent bridge via pci_upstream_bridge() to trigger secondary bus
resets and broadcast driver error callbacks.
5. Lane Margining at Receiver (LMR): margin_enable_write() requires both
link endpoints to establish hierarchical locking (pci_dev_lock) and
runtime PM pinning.

Currently, these drivers independently implement ad-hoc traversals via
pci_upstream_bridge() or subordinate bus device iteration. Ad-hoc subordinate
bus iteration is error-prone and risks races with concurrent hot-unplug or
device removal if pci_bus_sem is omitted.

This patch introduces pcie_get_link_endpoints() and pcie_put_link_endpoints()
in the PCI core (drivers/pci/pci.c and include/linux/pci.h) as a standalone
helper:
- For Endpoints: resolves the parent Downstream Port via
pci_upstream_bridge(pdev).
- For Downstream Ports: safely inspects the subordinate bus under
down_read(&pci_bus_sem) and acquires a reference via pci_dev_get() on the
child device.

Callers release the acquired reference with pcie_put_link_endpoints().

Validation:
- Compiled clean on x86_64 defconfig (0 warnings, 0 errors).
- Multi-architecture build validated for ARM64 and x86_64 targets.
- Passed checkpatch.pl (0 warnings, 0 errors).
- Passed Sashiko pre-commit test runner (ID: 46b5f8a3da1ca0056407b3db6c82b001264f86b7).
---
drivers/pci/pci.c | 50 ++++++++++++++++++++++++++++++++++++++++++++++++++
include/linux/pci.h | 6 ++++++
2 files changed, 56 insertions(+)

diff --git a/drivers/pci/pci.c b/drivers/pci/pci.c
index 77b17b13ee61..7bdfe7e5ab3a 100644
--- a/drivers/pci/pci.c
+++ b/drivers/pci/pci.c
@@ -4618,6 +4618,56 @@ int pcie_retrain_link(struct pci_dev *pdev, bool use_lt)
return rc;
}

+/**
+ * pcie_get_link_endpoints - Identify Upstream and Downstream ends of a PCIe link
+ * @pdev: Any PCIe device on the link (Downstream Port or Endpoint)
+ * @downstream_port: Output pointer to Downstream Port (Upstream Component)
+ * @upstream_port: Output pointer to Upstream Port (Downstream Component)
+ *
+ * Identifies both ends of a point-to-point PCIe link. Increments reference count
+ * on @upstream_port if dynamically discovered on a downstream port. Callers must
+ * release with pcie_put_link_endpoints().
+ *
+ * Return: 0 on success, or -EINVAL if @pdev is NULL or not PCIe.
+ */
+int pcie_get_link_endpoints(struct pci_dev *pdev,
+ struct pci_dev **downstream_port,
+ struct pci_dev **upstream_port)
+{
+ if (!pdev || !pci_is_pcie(pdev))
+ return -EINVAL;
+
+ if (pcie_downstream_port(pdev)) {
+ *downstream_port = pdev;
+ down_read(&pci_bus_sem);
+ *upstream_port = pdev->subordinate ?
+ pci_dev_get(list_first_entry_or_null(&pdev->subordinate->devices,
+ struct pci_dev, bus_list)) : NULL;
+ up_read(&pci_bus_sem);
+ } else {
+ *downstream_port = pci_upstream_bridge(pdev);
+ *upstream_port = pdev;
+ }
+
+ return 0;
+}
+EXPORT_SYMBOL_GPL(pcie_get_link_endpoints);
+
+/**
+ * pcie_put_link_endpoints - Release references acquired by pcie_get_link_endpoints
+ * @pdev: Device passed to pcie_get_link_endpoints()
+ * @downstream_port: Downstream Port pointer
+ * @upstream_port: Upstream Port pointer
+ */
+void pcie_put_link_endpoints(struct pci_dev *pdev,
+ struct pci_dev *downstream_port,
+ struct pci_dev *upstream_port)
+{
+ if (pdev && pcie_downstream_port(pdev) && upstream_port)
+ pci_dev_put(upstream_port);
+}
+EXPORT_SYMBOL_GPL(pcie_put_link_endpoints);
+
/**
* pcie_wait_for_link_delay - Wait until link is active or inactive
* @pdev: Bridge device
diff --git a/include/linux/pci.h b/include/linux/pci.h
index 64b308b6e61c..d30bf056a7ce 100644
--- a/include/linux/pci.h
+++ b/include/linux/pci.h
@@ -1264,6 +1264,12 @@ struct resource *pci_find_parent_resource(const struct pci_dev *dev,
u8 pci_swizzle_interrupt_pin(const struct pci_dev *dev, u8 pin);
int pci_get_interrupt_pin(struct pci_dev *dev, struct pci_dev **bridge);
u8 pci_common_swizzle(struct pci_dev *dev, u8 *pinp);
+int pcie_get_link_endpoints(struct pci_dev *pdev,
+ struct pci_dev **downstream_port,
+ struct pci_dev **upstream_port);
+void pcie_put_link_endpoints(struct pci_dev *pdev,
+ struct pci_dev *downstream_port,
+ struct pci_dev *upstream_port);
struct pci_dev *pci_dev_get(struct pci_dev *dev);
void pci_dev_put(struct pci_dev *dev);
DEFINE_FREE(pci_dev_put, struct pci_dev *, if (_T) pci_dev_put(_T))

---
base-commit: 0f23d56f17fdfc7db69d51f64c8b91bbab947aa9
change-id: 20260831-pcie-link-endpoints-978e100d5d06

Best regards,
--
Priyank Rathod <rathodpriyank@xxxxxxxxxx>