All of lore.kernel.org
 help / color / mirror / Atom feed
* [PATCH] PCI/pcie: Add PCIe Lane Margining at Receiver (LMR) support
@ 2026-08-18 18:57 Priyank Rathod
  2026-08-18 19:10 ` sashiko-bot
                   ` (3 more replies)
  0 siblings, 4 replies; 9+ messages in thread
From: Priyank Rathod @ 2026-08-18 18:57 UTC (permalink / raw)
  To: Bjorn Helgaas, Shuah Khan
  Cc: linux-kernel, linux-pci, linux-kselftest, Priyank Rathod

Per PCIe Base Specification r6.0, sec 8.4.4 ("Lane Margining at
Receiver"), PCIe devices operating at 16.0 GT/s (Gen 4) or higher data
rates support the Lane Margining at Receiver Extended Capability
(ID 0x27), and it is mandatory for receivers operating at 64.0 GT/s
(Gen 6) or higher data rates. Lane Margining allows software to
evaluate high-speed link margins by measuring timing and voltage steps
for each individual physical lane and receiver.

Add driver and debugfs support for PCIe Lane Margining at Receiver:

  - Add Lane Margining at Receiver Extended Capability register
    definitions (PCI_EXT_CAP_ID_LMR, PCI_LMR_PORT_CAP, PCI_LMR_PORT_STS,
    PCI_LMR_LANE_CTRL, PCI_LMR_LANE_STS) to <uapi/linux/pci_regs.h>.
  - Add Kconfig option CONFIG_PCIE_LMR (under drivers/pci/pcie/Kconfig)
    dependent on DEBUG_FS.
  - Implement drivers/pci/pcie/margin.c to probe the capability on Gen4+
    links and expose per-device debugfs entries under:
      /sys/kernel/debug/pci/pcie_lmr_<pci_dev_name>/
    providing control over margining enablement, receiver selection, and
    execution of timing/voltage margin step commands. Distinguish
    between missing mandatory LMR capability on Gen6+ vs optional on
    Gen4/Gen5.
  - Hook pci_lmr_init() into pci_init_capabilities() during device probe
    in drivers/pci/probe.c and pci_lmr_exit() into drivers/pci/remove.c.
  - Add kselftest script under tools/testing/selftests/pcie_lmt/pcie_lmt.sh
    to test debugfs capability reads, enablement, and stepping.

Signed-off-by: Priyank Rathod <rathodpriyank@google.com>
---
Per PCIe Base Specification r6.0, section 8.4.4 ("Lane Margining at Receiver"),
PCIe devices operating at 16.0 GT/s (Gen 4) or higher data rates support the
Lane Margining at Receiver Extended Capability (ID 0x27), and it is mandatory
for receivers operating at 64.0 GT/s (Gen 6) or higher data rates.

Lane Margining allows system software to evaluate high-speed link signal
integrity and margins by measuring timing and voltage steps for each physical
lane and receiver independently.

This series introduces kernel driver support, debugfs controls, and a
kselftest automation script for PCIe Lane Margining at Receiver (LMR/LMT).

==============================================================================
1. How to Enable & Configure
==============================================================================
Enable the Kconfig option under PCI support:
  CONFIG_PCIE_LMR=y (or =m)
  (Depends on CONFIG_PCI and CONFIG_DEBUG_FS)

Upon boot or device hotplug on Gen4+ links (>= 16.0 GT/s), the driver probes
Extended Capability ID 0x27 and exposes per-device debugfs interfaces:
  /sys/kernel/debug/pci/pcie_lmr_<domain>:<bus>:<dev>.<func>/

==============================================================================
2. How to Use the Debugfs Interface (Manual Margining)
==============================================================================
Inspect device-wide margining capabilities and port status:
  # Inspect root device LMR capabilities & status
  cat /sys/kernel/debug/pci/pcie_lmr_0000:01:00.0/capabilities
  cat /sys/kernel/debug/pci/pcie_lmr_0000:01:00.0/port_status

Enable active Lane Margining on the device:
  # Enable Lane Margining state machine
  echo 1 > /sys/kernel/debug/pci/pcie_lmr_0000:01:00.0/enable

Inspect and step individual lanes (e.g. lane0):
  # Select target receiver (0 = local receiver, 1..6 = retimers/link partners)
  echo 0 > /sys/kernel/debug/pci/pcie_lmr_0000:01:00.0/lane0/receiver

  # Check available timing and voltage steps for this receiver
  cat /sys/kernel/debug/pci/pcie_lmr_0000:01:00.0/lane0/caps
  cat /sys/kernel/debug/pci/pcie_lmr_0000:01:00.0/lane0/num_timing_steps
  cat /sys/kernel/debug/pci/pcie_lmr_0000:01:00.0/lane0/num_voltage_steps

  # Step timing margin or voltage margin offset
  echo 2 > /sys/kernel/debug/pci/pcie_lmr_0000:01:00.0/lane0/margin_timing
  echo 1 > /sys/kernel/debug/pci/pcie_lmr_0000:01:00.0/lane0/margin_voltage

  # Reset margin offset back to nominal (0)
  echo 0 > /sys/kernel/debug/pci/pcie_lmr_0000:01:00.0/lane0/margin_timing
  echo 0 > /sys/kernel/debug/pci/pcie_lmr_0000:01:00.0/lane0/margin_voltage

Disable Lane Margining when finished:
  echo 0 > /sys/kernel/debug/pci/pcie_lmr_0000:01:00.0/enable

==============================================================================
3. How to Run Automated Kselftests Using the Test Script
==============================================================================
An automated kselftest script is included to test capability reads, receiver
selection, and margining commands across all enumerated LMR devices:

  # Run directly as root
  sudo ./tools/testing/selftests/pcie_lmt/pcie_lmt.sh

Or run via the kselftest Makefile harness:
  make -C tools/testing/selftests TARGETS=pcie_lmt run_tests

Sample script output on an LMR-capable device:
  pcie_lmt: testing PCIe LMR debugfs entries
  pcie_lmt: probing device pcie_lmr_0000:01:00.0
    pcie_lmr_0000:01:00.0: capabilities read OK
    pcie_lmr_0000:01:00.0: port_status read OK
    pcie_lmr_0000:01:00.0: margining enabled OK
    pcie_lmr_0000:01:00.0: testing lane0
    pcie_lmr_0000:01:00.0: testing lane1
    pcie_lmr_0000:01:00.0: margining disabled OK
  pcie_lmt [PASS]
---
 drivers/pci/pci.h                            |   8 +
 drivers/pci/pcie/Kconfig                     |  12 +
 drivers/pci/pcie/Makefile                    |   1 +
 drivers/pci/pcie/margin.c                    | 673 +++++++++++++++++++++++++++
 drivers/pci/probe.c                          |   1 +
 drivers/pci/remove.c                         |   1 +
 include/linux/pci.h                          |   6 +
 include/uapi/linux/pci_regs.h                |  19 +
 tools/testing/selftests/Makefile             |   1 +
 tools/testing/selftests/pcie_lmt/Makefile    |   3 +
 tools/testing/selftests/pcie_lmt/pcie_lmt.sh | 105 +++++
 11 files changed, 830 insertions(+)

diff --git a/drivers/pci/pci.h b/drivers/pci/pci.h
index 4469e1a77f3c..c3449c6a5b79 100644
--- a/drivers/pci/pci.h
+++ b/drivers/pci/pci.h
@@ -1023,6 +1023,14 @@ static inline void pci_no_tph(void) { }
 static inline void pci_tph_init(struct pci_dev *dev) { }
 #endif
 
+#ifdef CONFIG_PCIE_LMR
+void pci_lmr_init(struct pci_dev *dev);
+void pci_lmr_exit(struct pci_dev *dev);
+#else
+static inline void pci_lmr_init(struct pci_dev *dev) { }
+static inline void pci_lmr_exit(struct pci_dev *dev) { }
+#endif
+
 #ifdef CONFIG_PCIE_PTM
 void pci_ptm_init(struct pci_dev *dev);
 void pci_save_ptm_state(struct pci_dev *dev);
diff --git a/drivers/pci/pcie/Kconfig b/drivers/pci/pcie/Kconfig
index 207c2deae35f..3b021ca2fe84 100644
--- a/drivers/pci/pcie/Kconfig
+++ b/drivers/pci/pcie/Kconfig
@@ -137,6 +137,18 @@ config PCIE_PTM
 	  This is only useful if you have devices that support PTM, but it
 	  is safe to enable even if you don't.
 
+config PCIE_LMR
+	bool "PCI Express Lane Margining at Receiver Support"
+	depends on DEBUG_FS
+	help
+	  This enables the PCI Express Lane Margining at Receiver support.
+	  Lane Margining allows software to determine the voltage and
+	  timing margin of each lane on a PCIe link (16.0 GT/s and above).
+	  The margining data is exposed via debugfs.
+
+	  This is only useful if you have devices that support lane
+	  margining, but it is safe to enable even if you don't.
+
 config PCIE_EDR
 	bool "PCI Express Error Disconnect Recover support"
 	depends on PCIE_DPC && ACPI
diff --git a/drivers/pci/pcie/Makefile b/drivers/pci/pcie/Makefile
index b0b43a18c304..aac45ae0402e 100644
--- a/drivers/pci/pcie/Makefile
+++ b/drivers/pci/pcie/Makefile
@@ -13,4 +13,5 @@ obj-$(CONFIG_PCIEAER_INJECT)	+= aer_inject.o
 obj-$(CONFIG_PCIE_PME)		+= pme.o
 obj-$(CONFIG_PCIE_DPC)		+= dpc.o
 obj-$(CONFIG_PCIE_PTM)		+= ptm.o
+obj-$(CONFIG_PCIE_LMR)		+= margin.o
 obj-$(CONFIG_PCIE_EDR)		+= edr.o
diff --git a/drivers/pci/pcie/margin.c b/drivers/pci/pcie/margin.c
new file mode 100644
index 000000000000..a1ccd473a407
--- /dev/null
+++ b/drivers/pci/pcie/margin.c
@@ -0,0 +1,673 @@
+// SPDX-License-Identifier: GPL-2.0
+/*
+ * PCI Express Lane Margining at Receiver
+ *
+ * Copyright (C) 2026 Google LLC
+ * Author: Priyank Rathod <rathodpriyank@google.com>
+ *
+ * Lane Margining at Receiver (PCIe r6.0, sec 8.4.4) allows the software
+ * to determine the voltage and timing margins of each lane on the PCIe
+ * link. The extended capability (ID 0x27) is available for receivers
+ * operating at 16.0 GT/s (Gen4) or higher data rates, and is mandatory
+ * for receivers operating at 64.0 GT/s (Gen6) or higher data rates.
+ *
+ * This implementation exposes per-device debugfs entries to read the margining
+ * capabilities and issues margining commands through custom file_ops.
+ */
+
+#include <linux/pci.h>
+#include <linux/slab.h>
+#include <linux/mutex.h>
+#include <linux/delay.h>
+#include <linux/jiffies.h>
+#include <linux/seq_file.h>
+#include <linux/bitfield.h>
+#include <linux/debugfs.h>
+
+#include "../pci.h"
+
+/* Margin type encodings per the PCIe spec */
+#define LMR_TYPE_DEMARGIN               0x0
+#define LMR_TYPE_REPORT_CAPS            0x1
+#define LMR_TYPE_REPORT_VOLTAGE_STEPS   0x2
+#define LMR_TYPE_REPORT_TIMING_STEPS    0x3
+#define LMR_TYPE_TIMING                 0x4
+#define LMR_TYPE_VOLTAGE                0x5
+#define LMR_TYPE_NO_CMD                 0x7
+
+/* LMR command timing parameters */
+#define LMR_CMD_TIMEOUT_MS              100
+#define LMR_CMD_SLEEP_MIN_US            100
+#define LMR_CMD_SLEEP_MAX_US            200
+#define LMR_ENABLE_TIMEOUT_MS           100
+#define LMR_ENABLE_SLEEP_MIN_US         1000
+#define LMR_ENABLE_SLEEP_MAX_US         2000
+
+/* LMR limits */
+#define LMR_MAX_LANES                   32
+#define LMR_MAX_RX_NUM                  6
+#define LMR_MAX_TIMING_STEP             63
+#define LMR_MAX_VOLTAGE_STEP            127
+
+/* LMR PCIe generation numbers */
+#define LMR_GEN6                        6
+#define LMR_GEN5                        5
+#define LMR_GEN4                        4
+
+/* LMR lane register stride */
+#define LMR_LANE_REG_STRIDE             4
+
+/* LMR receivers and directions */
+#define LMR_RX_LOCAL                    0
+#define LMR_STEP_DIR_INCREASE           1
+#define LMR_STEP_DIR_DECREASE           0
+
+/* LMR step & direction encoding masks */
+#define LMR_STEPS_MASK                  0x7F
+#define LMR_TIMING_STEP_MASK            0x3F
+#define LMR_TIMING_DIR_SHIFT            6
+#define LMR_VOLTAGE_STEP_MASK           0x7F
+#define LMR_VOLTAGE_DIR_SHIFT           7
+
+/* LMR capabilities report bit fields */
+#define LMR_CAP_MARGIN_HV               BIT(0)
+#define LMR_CAP_MARGIN_EV_IV            BIT(1)
+#define LMR_CAP_IND_LEFT_RIGHT_TIMING   BIT(2)
+#define LMR_CAP_IND_UP_DOWN_VOLTAGE     BIT(3)
+#define LMR_CAP_ERROR_SAMPLER           BIT(4)
+#define LMR_CAP_SAMPLE_MULTIPLE_RX      BIT(5)
+
+struct pci_margin_lane {
+	struct pci_margin_dev *mdev;
+	int lane;
+	u8 rx;
+	int timing_val;
+	int voltage_val;
+};
+
+struct pci_margin_dev {
+	struct pci_dev *dev;
+	u16 cap;
+	struct dentry *debugfs;
+	struct mutex lock;
+	int num_lanes;
+	struct pci_margin_lane *lanes;
+	bool enabled;
+};
+
+static int pci_lmr_run_cmd(struct pci_dev *dev, int lane, u8 rx, u8 type,
+			   u8 usage, u8 payload, u16 *status_val)
+{
+	u16 lmr = dev->lmr_cap;
+	u16 ctrl_offset = lmr + PCI_LMR_LANE_CTRL + LMR_LANE_REG_STRIDE * lane;
+	u16 sts_offset = lmr + PCI_LMR_LANE_STS + LMR_LANE_REG_STRIDE * lane;
+	u16 ctrl, sts;
+	unsigned long timeout;
+
+	ctrl = FIELD_PREP(PCI_LMR_LANE_CTRL_RX_NUM, rx) |
+	       FIELD_PREP(PCI_LMR_LANE_CTRL_MTYPE, type) |
+	       FIELD_PREP(PCI_LMR_LANE_CTRL_USAGE, usage) |
+	       FIELD_PREP(PCI_LMR_LANE_CTRL_PAYLOAD, payload);
+
+	pci_write_config_word(dev, ctrl_offset, ctrl);
+
+	timeout = jiffies + msecs_to_jiffies(LMR_CMD_TIMEOUT_MS);
+	while (1) {
+		pci_read_config_word(dev, sts_offset, &sts);
+		if (sts == 0xFFFF)
+			return -ENODEV;
+
+		if (FIELD_GET(PCI_LMR_LANE_STS_MTYPE, sts) == type) {
+			if (status_val)
+				*status_val = sts;
+			return 0;
+		}
+
+		if (time_after(jiffies, timeout))
+			break;
+
+		usleep_range(LMR_CMD_SLEEP_MIN_US, LMR_CMD_SLEEP_MAX_US);
+	}
+
+	return -ETIMEDOUT;
+}
+
+static int pci_lmr_run_cmd_locked(struct pci_margin_dev *mdev, int lane, u8 rx,
+				  u8 type, u8 usage, u8 payload, u16 *status_val)
+{
+	int ret;
+
+	if (!mdev->enabled)
+		return -EACCES;
+
+	mutex_lock(&mdev->lock);
+	ret = pci_lmr_run_cmd(mdev->dev, lane, rx, type, usage, payload, status_val);
+	mutex_unlock(&mdev->lock);
+
+	return ret;
+}
+
+#if IS_ENABLED(CONFIG_DEBUG_FS)
+
+static int margin_caps_show(struct seq_file *s, void *v)
+{
+	struct pci_margin_dev *mdev = s->private;
+	struct pci_dev *dev = mdev->dev;
+	u16 cap;
+
+	pci_read_config_word(dev, mdev->cap + PCI_LMR_PORT_CAP, &cap);
+	seq_printf(s, "Port Capabilities: %#06x\n", cap);
+	seq_printf(s, "  Margining Ready: %s\n",
+		   (cap & PCI_LMR_PORT_CAP_MARGIN_READY) ? "yes" : "no");
+	seq_printf(s, "  SW Ready Required: %s\n",
+		   (cap & PCI_LMR_PORT_CAP_SW_READY) ? "yes" : "no");
+	return 0;
+}
+DEFINE_SHOW_ATTRIBUTE(margin_caps);
+
+static int margin_port_status_show(struct seq_file *s, void *v)
+{
+	struct pci_margin_dev *mdev = s->private;
+	struct pci_dev *dev = mdev->dev;
+	u16 sts;
+
+	pci_read_config_word(dev, mdev->cap + PCI_LMR_PORT_STS, &sts);
+	seq_printf(s, "Port Status: %#06x\n", sts);
+	seq_printf(s, "  Margining Ready: %s\n",
+		   (sts & PCI_LMR_PORT_STS_MARGIN_READY) ? "yes" : "no");
+	seq_printf(s, "  SW Ready: %s\n", (sts & PCI_LMR_PORT_STS_SW_READY) ? "yes" : "no");
+	return 0;
+}
+DEFINE_SHOW_ATTRIBUTE(margin_port_status);
+
+static int margin_enable_show(struct seq_file *s, void *v)
+{
+	struct pci_margin_dev *mdev = s->private;
+
+	seq_printf(s, "%d\n", mdev->enabled);
+	return 0;
+}
+
+static ssize_t margin_enable_write(struct file *file, const char __user *user_buf,
+			       size_t count, loff_t *ppos)
+{
+	struct pci_margin_dev *mdev = ((struct seq_file *)file->private_data)->private;
+	struct pci_dev *dev = mdev->dev;
+	unsigned long timeout;
+	bool enable;
+	int ret, i;
+	u16 sts, cap;
+
+	ret = kstrtobool_from_user(user_buf, count, &enable);
+	if (ret)
+		return ret;
+
+	mutex_lock(&mdev->lock);
+
+	if (mdev->enabled == enable)
+		goto out;
+
+	if (enable) {
+		pci_read_config_word(dev, mdev->cap + PCI_LMR_PORT_CAP, &cap);
+
+		pci_read_config_word(dev, mdev->cap + PCI_LMR_PORT_STS, &sts);
+		sts |= PCI_LMR_PORT_STS_SW_READY;
+		pci_write_config_word(dev, mdev->cap + PCI_LMR_PORT_STS, sts);
+
+		timeout = jiffies + msecs_to_jiffies(LMR_ENABLE_TIMEOUT_MS);
+		while (1) {
+			pci_read_config_word(dev, mdev->cap + PCI_LMR_PORT_STS, &sts);
+			if (sts == 0xFFFF) {
+				ret = -ENODEV;
+				goto out;
+			}
+			if (sts & PCI_LMR_PORT_STS_MARGIN_READY)
+				break;
+			if (time_after(jiffies, timeout)) {
+				ret = -ETIMEDOUT;
+				sts &= ~PCI_LMR_PORT_STS_SW_READY;
+				pci_write_config_word(dev, mdev->cap + PCI_LMR_PORT_STS, sts);
+				goto out;
+			}
+			usleep_range(LMR_ENABLE_SLEEP_MIN_US, LMR_ENABLE_SLEEP_MAX_US);
+		}
+		mdev->enabled = true;
+	} else {
+		pci_read_config_word(dev, mdev->cap + PCI_LMR_PORT_STS, &sts);
+		sts &= ~PCI_LMR_PORT_STS_SW_READY;
+		pci_write_config_word(dev, mdev->cap + PCI_LMR_PORT_STS, sts);
+
+		for (i = 0; i < mdev->num_lanes; i++) {
+			mdev->lanes[i].timing_val = 0;
+			mdev->lanes[i].voltage_val = 0;
+		}
+		mdev->enabled = false;
+	}
+
+out:
+	mutex_unlock(&mdev->lock);
+	return ret ? ret : count;
+}
+
+static int margin_enable_open(struct inode *inode, struct file *file)
+{
+	return single_open(file, margin_enable_show, inode->i_private);
+}
+
+static const struct file_operations margin_enable_fops = {
+	.open = margin_enable_open,
+	.read = seq_read,
+	.write = margin_enable_write,
+	.llseek = seq_lseek,
+	.release = single_release,
+};
+
+static int margin_lane_receiver_show(struct seq_file *s, void *v)
+{
+	struct pci_margin_lane *plane = s->private;
+
+	seq_printf(s, "%d\n", plane->rx);
+	return 0;
+}
+
+static ssize_t margin_lane_receiver_write(struct file *file, const char __user *user_buf,
+				      size_t count, loff_t *ppos)
+{
+	struct pci_margin_lane *plane = ((struct seq_file *)file->private_data)->private;
+	u8 rx;
+	int ret;
+
+	ret = kstrtou8_from_user(user_buf, count, 0, &rx);
+	if (ret)
+		return ret;
+
+	if (rx > LMR_MAX_RX_NUM)
+		return -EINVAL;
+
+	plane->rx = rx;
+	return count;
+}
+
+static int margin_lane_receiver_open(struct inode *inode, struct file *file)
+{
+	return single_open(file, margin_lane_receiver_show, inode->i_private);
+}
+
+static const struct file_operations margin_lane_receiver_fops = {
+	.open = margin_lane_receiver_open,
+	.read = seq_read,
+	.write = margin_lane_receiver_write,
+	.llseek = seq_lseek,
+	.release = single_release,
+};
+
+static int margin_lane_caps_show(struct seq_file *s, void *v)
+{
+	struct pci_margin_lane *plane = s->private;
+	struct pci_margin_dev *mdev = plane->mdev;
+	u16 sts;
+	int ret;
+	u8 val;
+
+	ret = pci_lmr_run_cmd_locked(mdev, plane->lane, plane->rx,
+				     LMR_TYPE_REPORT_CAPS, 0, 0, &sts);
+	if (ret)
+		return ret;
+
+	val = FIELD_GET(PCI_LMR_LANE_STS_PAYLOAD, sts);
+	seq_printf(s, "Lane %d Rx %d Capabilities: %#02x\n", plane->lane, plane->rx, val);
+	seq_printf(s, "  Margining H/V: %s\n", (val & LMR_CAP_MARGIN_HV) ? "both" : "either");
+	seq_printf(s, "  Margining eV/iV: %s\n",
+		   (val & LMR_CAP_MARGIN_EV_IV) ? "both (close & open)" : "one (close only)");
+	seq_printf(s, "  Left/Right: %s\n", (val & LMR_CAP_IND_LEFT_RIGHT_TIMING) ? "both" : "one");
+	seq_printf(s, "  Up/Down: %s\n", (val & LMR_CAP_IND_UP_DOWN_VOLTAGE) ? "both" : "one");
+	seq_printf(s, "  Error Sampler: %s\n",
+		   (val & LMR_CAP_ERROR_SAMPLER) ? "yes" : "no (main sampler)");
+	seq_printf(s, "  Sample Multiple Receivers: %s\n",
+		   (val & LMR_CAP_SAMPLE_MULTIPLE_RX) ? "yes" : "no");
+
+	return 0;
+}
+DEFINE_SHOW_ATTRIBUTE(margin_lane_caps);
+
+static int margin_lane_timing_steps_show(struct seq_file *s, void *v)
+{
+	struct pci_margin_lane *plane = s->private;
+	struct pci_margin_dev *mdev = plane->mdev;
+	u16 sts;
+	int ret;
+	u8 val;
+
+	ret = pci_lmr_run_cmd_locked(mdev, plane->lane, plane->rx,
+				     LMR_TYPE_REPORT_TIMING_STEPS, 0, 0, &sts);
+	if (ret)
+		return ret;
+
+	val = FIELD_GET(PCI_LMR_LANE_STS_PAYLOAD, sts) & LMR_STEPS_MASK;
+	seq_printf(s, "%d\n", val);
+	return 0;
+}
+DEFINE_SHOW_ATTRIBUTE(margin_lane_timing_steps);
+
+static int margin_lane_voltage_steps_show(struct seq_file *s, void *v)
+{
+	struct pci_margin_lane *plane = s->private;
+	struct pci_margin_dev *mdev = plane->mdev;
+	u16 sts;
+	int ret;
+	u8 val;
+
+	ret = pci_lmr_run_cmd_locked(mdev, plane->lane, plane->rx,
+				     LMR_TYPE_REPORT_VOLTAGE_STEPS, 0, 0, &sts);
+	if (ret)
+		return ret;
+
+	val = FIELD_GET(PCI_LMR_LANE_STS_PAYLOAD, sts) & LMR_STEPS_MASK;
+
+	seq_printf(s, "%d\n", val);
+	return 0;
+}
+DEFINE_SHOW_ATTRIBUTE(margin_lane_voltage_steps);
+
+static ssize_t margin_lane_timing_write(struct file *file, const char __user *user_buf,
+				    size_t count, loff_t *ppos)
+{
+	struct pci_margin_lane *plane = ((struct seq_file *)file->private_data)->private;
+	struct pci_margin_dev *mdev = plane->mdev;
+	int val, ret;
+	u8 step, dir;
+	u16 sts;
+	u8 caps;
+
+	ret = kstrtoint_from_user(user_buf, count, 0, &val);
+	if (ret)
+		return ret;
+
+	if (!mdev->enabled)
+		return -EACCES;
+
+	mutex_lock(&mdev->lock);
+
+	if (val == 0) {
+		ret = pci_lmr_run_cmd(mdev->dev, plane->lane, plane->rx,
+				      LMR_TYPE_DEMARGIN, 0, 0, &sts);
+		if (ret)
+			goto out;
+		plane->timing_val = 0;
+		plane->voltage_val = 0;
+		goto out;
+	}
+
+	ret = pci_lmr_run_cmd(mdev->dev, plane->lane, plane->rx, LMR_TYPE_REPORT_CAPS, 0, 0, &sts);
+	if (ret)
+		goto out;
+	caps = FIELD_GET(PCI_LMR_LANE_STS_PAYLOAD, sts);
+
+	if (val < 0) {
+		step = -val;
+		dir = LMR_STEP_DIR_DECREASE;
+	} else {
+		step = val;
+		dir = LMR_STEP_DIR_INCREASE;
+		if (!(caps & LMR_CAP_IND_LEFT_RIGHT_TIMING)) {
+			ret = -EINVAL;
+			goto out;
+		}
+	}
+
+	if (step > LMR_MAX_TIMING_STEP) {
+		ret = -EINVAL;
+		goto out;
+	}
+
+	ret = pci_lmr_run_cmd(mdev->dev, plane->lane, plane->rx,
+			      LMR_TYPE_TIMING, 0,
+			      (step & LMR_TIMING_STEP_MASK) | (dir << LMR_TIMING_DIR_SHIFT), &sts);
+	if (ret)
+		goto out;
+
+	plane->timing_val = val;
+
+out:
+	mutex_unlock(&mdev->lock);
+	return ret ? ret : count;
+}
+
+static int margin_lane_timing_show(struct seq_file *s, void *v)
+{
+	struct pci_margin_lane *plane = s->private;
+
+	seq_printf(s, "%d\n", plane->timing_val);
+	return 0;
+}
+
+static int margin_lane_timing_open(struct inode *inode, struct file *file)
+{
+	return single_open(file, margin_lane_timing_show, inode->i_private);
+}
+
+static const struct file_operations margin_lane_timing_fops = {
+	.open = margin_lane_timing_open,
+	.read = seq_read,
+	.write = margin_lane_timing_write,
+	.llseek = seq_lseek,
+	.release = single_release,
+};
+
+static ssize_t margin_lane_voltage_write(struct file *file, const char __user *user_buf,
+				     size_t count, loff_t *ppos)
+{
+	struct pci_margin_lane *plane = ((struct seq_file *)file->private_data)->private;
+	struct pci_margin_dev *mdev = plane->mdev;
+	int val, ret;
+	u8 step, dir;
+	u16 sts;
+	u8 caps;
+
+	ret = kstrtoint_from_user(user_buf, count, 0, &val);
+	if (ret)
+		return ret;
+
+	if (!mdev->enabled)
+		return -EACCES;
+
+	mutex_lock(&mdev->lock);
+
+	if (val == 0) {
+		ret = pci_lmr_run_cmd(mdev->dev, plane->lane, plane->rx,
+				      LMR_TYPE_DEMARGIN, 0, 0, &sts);
+		if (ret)
+			goto out;
+		plane->timing_val = 0;
+		plane->voltage_val = 0;
+		goto out;
+	}
+
+	ret = pci_lmr_run_cmd(mdev->dev, plane->lane, plane->rx, LMR_TYPE_REPORT_CAPS, 0, 0, &sts);
+	if (ret)
+		goto out;
+	caps = FIELD_GET(PCI_LMR_LANE_STS_PAYLOAD, sts);
+
+	if (val < 0) {
+		step = -val;
+		dir = 0;
+	} else {
+		step = val;
+		dir = 1;
+		if (!(caps & LMR_CAP_IND_UP_DOWN_VOLTAGE)) {
+			ret = -EINVAL;
+			goto out;
+		}
+	}
+
+	if (step > LMR_MAX_VOLTAGE_STEP) {
+		ret = -EINVAL;
+		goto out;
+	}
+
+	ret = pci_lmr_run_cmd(mdev->dev, plane->lane, plane->rx,
+			      LMR_TYPE_VOLTAGE, 0,
+			      (step & LMR_VOLTAGE_STEP_MASK) |
+			      (dir << LMR_VOLTAGE_DIR_SHIFT), &sts);
+	if (ret)
+		goto out;
+
+	plane->voltage_val = val;
+
+out:
+	mutex_unlock(&mdev->lock);
+	return ret ? ret : count;
+}
+
+static int margin_lane_voltage_show(struct seq_file *s, void *v)
+{
+	struct pci_margin_lane *plane = s->private;
+
+	seq_printf(s, "%d\n", plane->voltage_val);
+	return 0;
+}
+
+static int margin_lane_voltage_open(struct inode *inode, struct file *file)
+{
+	return single_open(file, margin_lane_voltage_show, inode->i_private);
+}
+
+static const struct file_operations margin_lane_voltage_fops = {
+	.open = margin_lane_voltage_open,
+	.read = seq_read,
+	.write = margin_lane_voltage_write,
+	.llseek = seq_lseek,
+	.release = single_release,
+};
+
+static void pci_margin_debugfs_init(struct pci_margin_dev *mdev)
+{
+	struct pci_dev *dev = mdev->dev;
+	char dirname[64];
+	int i;
+
+	snprintf(dirname, sizeof(dirname), "pcie_lmr_%s", dev_name(&dev->dev));
+	mdev->debugfs = debugfs_create_dir(dirname, NULL);
+	if (!mdev->debugfs)
+		return;
+
+	debugfs_create_file("capabilities", 0444, mdev->debugfs, mdev, &margin_caps_fops);
+	debugfs_create_file("port_status", 0444, mdev->debugfs, mdev, &margin_port_status_fops);
+	debugfs_create_file("enable", 0644, mdev->debugfs, mdev, &margin_enable_fops);
+
+	for (i = 0; i < mdev->num_lanes; i++) {
+		struct pci_margin_lane *plane = &mdev->lanes[i];
+		struct dentry *lane_dir;
+		char lane_name[16];
+
+		snprintf(lane_name, sizeof(lane_name), "lane%d", i);
+		lane_dir = debugfs_create_dir(lane_name, mdev->debugfs);
+		if (!lane_dir)
+			continue;
+
+		debugfs_create_file("receiver", 0644, lane_dir, plane, &margin_lane_receiver_fops);
+		debugfs_create_file("caps", 0444, lane_dir, plane, &margin_lane_caps_fops);
+		debugfs_create_file("num_timing_steps", 0444, lane_dir, plane,
+				    &margin_lane_timing_steps_fops);
+		debugfs_create_file("num_voltage_steps", 0444, lane_dir, plane,
+				    &margin_lane_voltage_steps_fops);
+		debugfs_create_file("margin_timing", 0644, lane_dir, plane,
+				    &margin_lane_timing_fops);
+		debugfs_create_file("margin_voltage", 0644, lane_dir, plane,
+				    &margin_lane_voltage_fops);
+	}
+}
+
+static void pci_margin_debugfs_remove(struct pci_margin_dev *mdev)
+{
+	debugfs_remove_recursive(mdev->debugfs);
+}
+
+#else
+static inline void pci_margin_debugfs_init(struct pci_margin_dev *mdev) { }
+static inline void pci_margin_debugfs_remove(struct pci_margin_dev *mdev) { }
+#endif
+
+void pci_lmr_init(struct pci_dev *dev)
+{
+	struct pci_margin_dev *mdev;
+	enum pci_bus_speed speed;
+	u16 lmr;
+	u16 lnkcap;
+	int i;
+
+	if (!pci_is_pcie(dev))
+		return;
+
+	speed = pcie_get_speed_cap(dev);
+
+	lmr = pci_find_ext_capability(dev, PCI_EXT_CAP_ID_LMR);
+	if (!lmr) {
+		if (speed >= PCIE_SPEED_64_0GT)
+			pci_warn(dev,
+				 "Missing Lane Margining at Receiver Capability (mandatory for Gen6+)\n");
+		else if (speed >= PCIE_SPEED_16_0GT)
+			pci_dbg(dev,
+				"Optional Lane Margining at Receiver Capability not found\n");
+		return;
+	}
+
+	if (speed < PCIE_SPEED_16_0GT && speed != PCI_SPEED_UNKNOWN)
+		return;
+
+	dev->lmr_cap = lmr;
+
+	mdev = kzalloc_obj(*mdev, GFP_KERNEL);
+	if (!mdev)
+		return;
+
+	mdev->dev = dev;
+	mdev->cap = lmr;
+	mutex_init(&mdev->lock);
+
+	pcie_capability_read_word(dev, PCI_EXP_LNKCAP, &lnkcap);
+	mdev->num_lanes = FIELD_GET(PCI_EXP_LNKCAP_MLW, lnkcap);
+
+	if (mdev->num_lanes == 0 || mdev->num_lanes > LMR_MAX_LANES) {
+		pci_warn(dev, "Invalid link width %d for LMR\n", mdev->num_lanes);
+		goto err_free_mdev;
+	}
+
+	mdev->lanes = kcalloc(mdev->num_lanes, sizeof(*mdev->lanes), GFP_KERNEL);
+	if (!mdev->lanes)
+		goto err_free_mdev;
+
+	for (i = 0; i < mdev->num_lanes; i++) {
+		mdev->lanes[i].mdev = mdev;
+		mdev->lanes[i].lane = i;
+		mdev->lanes[i].rx = LMR_RX_LOCAL;
+	}
+
+	pci_margin_debugfs_init(mdev);
+
+	dev->lmr = mdev;
+
+	pci_info(dev, "Lane Margining at Receiver (Gen%u) Capability detected\n",
+		 speed >= PCIE_SPEED_64_0GT ? LMR_GEN6 :
+		 speed >= PCIE_SPEED_32_0GT ? LMR_GEN5 :
+		 LMR_GEN4);
+	return;
+
+err_free_mdev:
+	mutex_destroy(&mdev->lock);
+	kfree(mdev);
+}
+
+void pci_lmr_exit(struct pci_dev *dev)
+{
+	struct pci_margin_dev *mdev = dev->lmr;
+
+	if (!mdev)
+		return;
+
+	pci_margin_debugfs_remove(mdev);
+	mutex_destroy(&mdev->lock);
+	kfree(mdev->lanes);
+	kfree(mdev);
+	dev->lmr = NULL;
+}
diff --git a/drivers/pci/probe.c b/drivers/pci/probe.c
index dd0abbc63e18..352b95568ebf 100644
--- a/drivers/pci/probe.c
+++ b/drivers/pci/probe.c
@@ -2666,6 +2666,7 @@ static void pci_init_capabilities(struct pci_dev *dev)
 	pci_pasid_init(dev);		/* Process Address Space ID */
 	pci_acs_init(dev);		/* Access Control Services */
 	pci_ptm_init(dev);		/* Precision Time Measurement */
+	pci_lmr_init(dev);		/* Lane Margining at Receiver */
 	pci_aer_init(dev);		/* Advanced Error Reporting */
 	pci_dpc_init(dev);		/* Downstream Port Containment */
 	pci_rcec_init(dev);		/* Root Complex Event Collector */
diff --git a/drivers/pci/remove.c b/drivers/pci/remove.c
index d8bffa21498a..6fba29040e44 100644
--- a/drivers/pci/remove.c
+++ b/drivers/pci/remove.c
@@ -36,6 +36,7 @@ static void pci_destroy_dev(struct pci_dev *dev)
 
 	pci_doe_sysfs_teardown(dev);
 	pci_npem_remove(dev);
+	pci_lmr_exit(dev);
 
 	/*
 	 * While device is in D0 drop the device from TSM link operations
diff --git a/include/linux/pci.h b/include/linux/pci.h
index 64b308b6e61c..ef1275f4c5b6 100644
--- a/include/linux/pci.h
+++ b/include/linux/pci.h
@@ -349,6 +349,8 @@ struct rcec_ea;
  *			number resources to allow for hierarchy expansion.
  * @is_pciehp:		PCIe Hot-Plug Capable bridge.
  */
+struct pci_margin_dev;
+
 struct pci_dev {
 	struct list_head bus_list;	/* Node in per-bus list */
 	struct pci_bus	*bus;		/* Bus this device is on */
@@ -528,6 +530,10 @@ struct pci_dev {
 	atomic_t	ptm_enable_cnt;
 	u8		ptm_granularity;
 #endif
+#ifdef CONFIG_PCIE_LMR
+	u16			lmr_cap;	/* Lane Margining Capability */
+	struct pci_margin_dev	*lmr;
+#endif
 #ifdef CONFIG_PCI_MSI
 	void __iomem	*msix_base;
 	raw_spinlock_t	msi_lock;
diff --git a/include/uapi/linux/pci_regs.h b/include/uapi/linux/pci_regs.h
index facaa324bd86..a13c09f8d6df 100644
--- a/include/uapi/linux/pci_regs.h
+++ b/include/uapi/linux/pci_regs.h
@@ -757,6 +757,7 @@
 #define PCI_EXT_CAP_ID_VF_REBAR 0x24	/* VF Resizable BAR */
 #define PCI_EXT_CAP_ID_DLF	0x25	/* Data Link Feature */
 #define PCI_EXT_CAP_ID_PL_16GT	0x26	/* Physical Layer 16.0 GT/s */
+#define PCI_EXT_CAP_ID_LMR	0x27	/* Lane Margining at Receiver */
 #define PCI_EXT_CAP_ID_NPEM	0x29	/* Native PCIe Enclosure Management */
 #define PCI_EXT_CAP_ID_PL_32GT  0x2A    /* Physical Layer 32.0 GT/s */
 #define PCI_EXT_CAP_ID_DOE	0x2E	/* Data Object Exchange */
@@ -1181,6 +1182,24 @@
 #define  PCI_PL_16GT_LE_CTRL_USP_TX_PRESET_MASK		0x000000F0
 #define  PCI_PL_16GT_LE_CTRL_USP_TX_PRESET_SHIFT	4
 
+/* Lane Margining at Receiver */
+#define PCI_LMR_PORT_CAP		0x04	/* Margining Port Capabilities */
+#define  PCI_LMR_PORT_CAP_MARGIN_READY	0x0001	/* Margining Ready */
+#define  PCI_LMR_PORT_CAP_SW_READY	0x0002	/* Margining SW Ready */
+#define PCI_LMR_PORT_STS		0x06	/* Margining Port Status */
+#define  PCI_LMR_PORT_STS_MARGIN_READY	0x0001	/* Margining Ready */
+#define  PCI_LMR_PORT_STS_SW_READY	0x0002	/* Margining SW Ready */
+#define PCI_LMR_LANE_CTRL		0x08	/* Margining Lane Control */
+#define  PCI_LMR_LANE_CTRL_RX_NUM	0x0007	/* Receiver Number */
+#define  PCI_LMR_LANE_CTRL_MTYPE	0x0038	/* Margining Type */
+#define  PCI_LMR_LANE_CTRL_USAGE	0x0040	/* Margining Usage Model */
+#define  PCI_LMR_LANE_CTRL_PAYLOAD	0xFF00	/* Margining Payload */
+#define PCI_LMR_LANE_STS		0x0A	/* Margining Lane Status */
+#define  PCI_LMR_LANE_STS_RX_NUM	0x0007	/* Receiver Number */
+#define  PCI_LMR_LANE_STS_MTYPE		0x0038	/* Margining Type */
+#define  PCI_LMR_LANE_STS_USAGE		0x0040	/* Margining Usage Model */
+#define  PCI_LMR_LANE_STS_PAYLOAD	0xFF00	/* Margining Payload */
+
 /* Physical Layer 32.0 GT/s */
 #define PCI_PL_32GT_LE_CTRL	0x20	/* Lane Equalization Control Register */
 
diff --git a/tools/testing/selftests/Makefile b/tools/testing/selftests/Makefile
index 8a4b6ddc68df..6990d999388a 100644
--- a/tools/testing/selftests/Makefile
+++ b/tools/testing/selftests/Makefile
@@ -91,6 +91,7 @@ TARGETS += net/tcp_ao
 TARGETS += nolibc
 TARGETS += pci_endpoint
 TARGETS += pcie_bwctrl
+TARGETS += pcie_lmt
 TARGETS += perf_events
 TARGETS += pidfd
 TARGETS += pid_namespace
diff --git a/tools/testing/selftests/pcie_lmt/Makefile b/tools/testing/selftests/pcie_lmt/Makefile
new file mode 100644
index 000000000000..36ac85937d78
--- /dev/null
+++ b/tools/testing/selftests/pcie_lmt/Makefile
@@ -0,0 +1,3 @@
+# SPDX-License-Identifier: GPL-2.0
+TEST_PROGS = pcie_lmt.sh
+include ../lib.mk
diff --git a/tools/testing/selftests/pcie_lmt/pcie_lmt.sh b/tools/testing/selftests/pcie_lmt/pcie_lmt.sh
new file mode 100755
index 000000000000..d57a6e8f3a23
--- /dev/null
+++ b/tools/testing/selftests/pcie_lmt/pcie_lmt.sh
@@ -0,0 +1,105 @@
+#!/bin/bash
+# SPDX-License-Identifier: GPL-2.0
+#
+# Copyright (C) 2026 Google LLC
+# Author: Priyank Rathod <rathodpriyank@google.com>
+#
+# Kselftest for PCIe Lane Margining at Receiver (LMR / LMT)
+# Tests the debugfs interface exposed by drivers/pci/pcie/margin.c
+# (/sys/kernel/debug/pci/pcie_lmr_<pci_dev_name>/)
+
+set -e
+
+TESTNAME="pcie_lmt"
+
+# Kselftest framework requirement - SKIP code is 4.
+ksft_skip=4
+retval=0
+skipmsg="skip all tests:"
+
+if [ $UID != 0 ]; then
+	echo "$skipmsg must be run as root" >&2
+	exit $ksft_skip
+fi
+
+DEBUGFS=$(mount -t debugfs | head -1 | awk '{ print $3 }')
+if [ -z "$DEBUGFS" ]; then
+	if [ -d "/sys/kernel/debug" ]; then
+		DEBUGFS="/sys/kernel/debug"
+	else
+		echo "$skipmsg debugfs is not mounted" >&2
+		exit $ksft_skip
+	fi
+fi
+
+if [ ! -d "$DEBUGFS/pci" ]; then
+	echo "$skipmsg debugfs pci directory ($DEBUGFS/pci) not found" >&2
+	exit $ksft_skip
+fi
+
+LMR_DEVS=$(ls -d $DEBUGFS/pci/pcie_lmr_* 2>/dev/null || true)
+if [ -z "$LMR_DEVS" ]; then
+	echo "$skipmsg no PCIe LMR devices found in $DEBUGFS/pci/" >&2
+	exit $ksft_skip
+fi
+
+cleanup_dev()
+{
+	local dev="$1"
+	echo 0 > "$dev/enable" 2>/dev/null || true
+}
+
+echo "$TESTNAME: testing PCIe LMR debugfs entries"
+
+for dev in $LMR_DEVS; do
+	dev_name=$(basename "$dev")
+	echo "$TESTNAME: probing device $dev_name"
+
+	if [ ! -r "$dev/capabilities" ] || [ ! -r "$dev/port_status" ] ||
+	   [ ! -r "$dev/enable" ] || [ ! -w "$dev/enable" ]; then
+		echo "$TESTNAME: $dev_name missing mandatory root attributes"
+		retval=1
+		continue
+	fi
+
+	caps=$(cat "$dev/capabilities")
+	status=$(cat "$dev/port_status")
+	echo "  $dev_name: capabilities read OK"
+	echo "  $dev_name: port_status read OK"
+
+	trap 'cleanup_dev "$dev"' EXIT
+
+	if ! echo 1 > "$dev/enable" 2>/dev/null; then
+		echo "  $dev_name: margining not ready by hardware (skipping active lanes)"
+		continue
+	fi
+
+	echo "  $dev_name: margining enabled OK"
+
+	for lane_dir in $(ls -d "$dev"/lane* 2>/dev/null || true); do
+		lane=$(basename "$lane_dir")
+		echo "  $dev_name: testing $lane"
+
+		# Test setting receiver (Rx 0 is always local receiver)
+		echo 0 > "$lane_dir/receiver"
+		cat "$lane_dir/caps" > /dev/null
+		cat "$lane_dir/num_timing_steps" > /dev/null
+		cat "$lane_dir/num_voltage_steps" > /dev/null
+
+		# Test resetting timing and voltage margin
+		echo 0 > "$lane_dir/margin_timing"
+		echo 0 > "$lane_dir/margin_voltage"
+	done
+
+	echo 0 > "$dev/enable"
+	trap - EXIT
+	echo "  $dev_name: margining disabled OK"
+done
+
+if [ $retval -eq 0 ]; then
+	echo "$TESTNAME [PASS]"
+else
+	echo "$TESTNAME [FAIL]"
+fi
+
+exit $retval

---
base-commit: 0f23d56f17fdfc7db69d51f64c8b91bbab947aa9
change-id: 20260818-pcie-lmt-3044d586aaec

Best regards,
-- 
Priyank Rathod <rathodpriyank@google.com>

^ permalink raw reply related	[flat|nested] 9+ messages in thread

* Re: [PATCH] PCI/pcie: Add PCIe Lane Margining at Receiver (LMR) support
  2026-08-18 18:57 [PATCH] PCI/pcie: Add PCIe Lane Margining at Receiver (LMR) support Priyank Rathod
@ 2026-08-18 19:10 ` sashiko-bot
  2026-08-18 19:26 ` [PATCH v2] " Priyank Rathod
                   ` (2 subsequent siblings)
  3 siblings, 0 replies; 9+ messages in thread
From: sashiko-bot @ 2026-08-18 19:10 UTC (permalink / raw)
  To: Priyank Rathod; +Cc: linux-pci

Thank you for your contribution! Sashiko AI review found 10 potential issue(s) to consider:
- [High] Missing command clearing via NO_CMD (0x7) causes consecutive commands of the same type to be falsely acknowledged.
- [High] The lane receiver index (`plane->rx`) is modified locklessly, creating a data race during multi-step hardware operations.
- [Medium] Incorrect register bit definitions for Margining Port Capabilities lead to false capability reporting.
- [Medium] The debugfs directory is placed at the root instead of under `/sys/kernel/debug/pci/`, breaking kselftests.
- [Medium] Integer wrap-around from narrowing cast allows out-of-bounds margin steps to bypass limits.
- [Medium] Time-of-Check to Time-of-Use (TOCTOU) race condition checking `mdev->enabled` outside of the device lock.
- [Medium] Ignoring the return value of `pci_read_config_word` leads to the use of uninitialized stack variables.
- [Low] The `cap` variable is read but unconditionally ignored in `margin_enable_write`, resulting in a dead store.
- [Low] Usage of non-existent function `kstrtou8_from_user` causes a build failure.
- [Low] If the PCIe link speed is `PCI_SPEED_UNKNOWN`, the initialization log incorrectly reports that a 'Gen6' capability was detected.
--

--- Patch [1]: [PATCH] PCI/pcie: Add PCIe Lane Margining at Receiver (LMR) support ---
commit 91ae8e1bf71eb2b01c6d741e0bcecbd38ee29c2e
Author: Priyank Rathod <rathodpriyank@google.com>
Subject: PCI/pcie: Add PCIe Lane Margining at Receiver (LMR) support

This patch adds driver and debugfs support for PCIe Lane Margining at
Receiver (LMR) for Gen4+ links. It introduces capabilities register
definitions, exposes control interfaces via debugfs, and includes an
automated kselftest.

> diff --git a/include/uapi/linux/pci_regs.h b/include/uapi/linux/pci_regs.h
> index facaa324bd86a..a13c09f8d6dfd 100644
> --- a/include/uapi/linux/pci_regs.h
> +++ b/include/uapi/linux/pci_regs.h
> @@ -1181,6 +1182,24 @@
>  #define  PCI_PL_16GT_LE_CTRL_USP_TX_PRESET_MASK		0x000000F0
>  #define  PCI_PL_16GT_LE_CTRL_USP_TX_PRESET_SHIFT	4
>  
> +/* Lane Margining at Receiver */
> +#define PCI_LMR_PORT_CAP		0x04	/* Margining Port Capabilities */
> +#define  PCI_LMR_PORT_CAP_MARGIN_READY	0x0001	/* Margining Ready */
> +#define  PCI_LMR_PORT_CAP_SW_READY	0x0002	/* Margining SW Ready */

[Severity: Medium]
Are these bit definitions for the Port Capabilities register correct?

Bit 0 of Port Capabilities (0x04) is actually "Margining Uses Software
Ready", and Bit 1 is reserved. The spec defines "Margining Ready" and
"SW Ready" only in the Port Status register (0x06).

Does this pollute the UAPI header and expose wrong semantics to userspace
when evaluating these bit masks?

> diff --git a/drivers/pci/pcie/margin.c b/drivers/pci/pcie/margin.c
> new file mode 100644
> index 0000000000000..a1ccd473a4070
> --- /dev/null
> +++ b/drivers/pci/pcie/margin.c
> @@ -0,0 +1,673 @@
[ ... ]
> +static int pci_lmr_run_cmd(struct pci_dev *dev, int lane, u8 rx, u8 type,
> +			   u8 usage, u8 payload, u16 *status_val)
> +{
[ ... ]
> +	timeout = jiffies + msecs_to_jiffies(LMR_CMD_TIMEOUT_MS);
> +	while (1) {
> +		pci_read_config_word(dev, sts_offset, &sts);
> +		if (sts == 0xFFFF)
> +			return -ENODEV;
> +
> +		if (FIELD_GET(PCI_LMR_LANE_STS_MTYPE, sts) == type) {
> +			if (status_val)
> +				*status_val = sts;
> +			return 0;
> +		}

[Severity: High]
Could the polling loop exit instantly on a false positive if the previous
command shared the same MTYPE?

The PCIe Base Spec 6.0 requires software to issue a "No Command" (0x7)
to clear the MTYPE in the Status register before issuing a new command of
the same type. Since this command clearing is omitted, does the driver
read stale payload data and fail to properly apply hardware margins for
consecutive commands?

[ ... ]
> +static int pci_lmr_run_cmd_locked(struct pci_margin_dev *mdev, int lane, u8 rx,
> +				  u8 type, u8 usage, u8 payload, u16 *status_val)
> +{
> +	int ret;
> +
> +	if (!mdev->enabled)
> +		return -EACCES;
> +
> +	mutex_lock(&mdev->lock);

[Severity: Medium]
Is there a race condition checking the enabled state outside of the
device lock?

If one thread checks the state locklessly and proceeds to block on the
lock, could another thread acquire the lock, set enabled to false,
and clear the hardware state? Once the first thread wakes up, would it
blindly execute the command on disabled hardware?

[ ... ]
> +static int margin_caps_show(struct seq_file *s, void *v)
> +{
> +	struct pci_margin_dev *mdev = s->private;
> +	struct pci_dev *dev = mdev->dev;
> +	u16 cap;
> +
> +	pci_read_config_word(dev, mdev->cap + PCI_LMR_PORT_CAP, &cap);
> +	seq_printf(s, "Port Capabilities: %#06x\n", cap);

[Severity: Medium]
Does ignoring the return value of pci_read_config_word() risk exposing
uninitialized stack memory to userspace?

While the PCI core tries to set the value to 0xFFFF on disconnection,
some low-level architecture drivers do not initialize the value on read
failures.

Also, as mentioned earlier, this applies the incorrect bit definitions
from the UAPI header. Does this evaluate erroneous bit masks to display
false capability states to userspace?

[ ... ]
> +static ssize_t margin_enable_write(struct file *file, const char __user *user_buf,
> +			       size_t count, loff_t *ppos)
> +{
[ ... ]
> +	if (enable) {
> +		pci_read_config_word(dev, mdev->cap + PCI_LMR_PORT_CAP, &cap);

[Severity: Low]
Is the cap variable read but unconditionally ignored, resulting in a dead
store?

This triggers a compiler warning for an unused but set variable and
misses a logical check on whether the hardware actually requires the
software ready bit to be set.

> +
> +		pci_read_config_word(dev, mdev->cap + PCI_LMR_PORT_STS, &sts);
> +		sts |= PCI_LMR_PORT_STS_SW_READY;
> +		pci_write_config_word(dev, mdev->cap + PCI_LMR_PORT_STS, sts);

[Severity: Medium]
Does ignoring the return value of pci_read_config_word() here risk
applying bitwise operations to a potentially uninitialized sts variable
and writing uninitialized garbage back to hardware configuration
registers?

[ ... ]
> +static ssize_t margin_lane_receiver_write(struct file *file, const char __user *user_buf,
> +				      size_t count, loff_t *ppos)
> +{
> +	struct pci_margin_lane *plane = ((struct seq_file *)file->private_data)->private;
> +	u8 rx;
> +	int ret;
> +
> +	ret = kstrtou8_from_user(user_buf, count, 0, &rx);

[Severity: Low]
Does the usage of kstrtou8_from_user() cause a build failure?

The kernel API provides kstrtou8, but it does not provide a variant
named kstrtou8_from_user.

> +	if (ret)
> +		return ret;
> +
> +	if (rx > LMR_MAX_RX_NUM)
> +		return -EINVAL;
> +
> +	plane->rx = rx;
> +	return count;
> +}

[Severity: High]
Is the lane receiver index updated without acquiring the device lock?

Multi-step sequences read this value multiple times, assuming it remains
constant for the duration of the locked block. Does this create a data
race where capabilities are validated for one receiver, but the margin
step is applied to a different receiver if updated concurrently?

[ ... ]
> +static ssize_t margin_lane_timing_write(struct file *file, const char __user *user_buf,
> +				    size_t count, loff_t *ppos)
> +{
> +	struct pci_margin_lane *plane = ((struct seq_file *)file->private_data)->private;
> +	struct pci_margin_dev *mdev = plane->mdev;
> +	int val, ret;
> +	u8 step, dir;
> +	u16 sts;
> +	u8 caps;
> +
> +	ret = kstrtoint_from_user(user_buf, count, 0, &val);
> +	if (ret)
> +		return ret;
> +
> +	if (!mdev->enabled)
> +		return -EACCES;
> +
> +	mutex_lock(&mdev->lock);

[Severity: Medium]
As mentioned earlier, does checking the enabled state outside of the
critical section introduce a race condition?

[ ... ]
> +	if (val < 0) {
> +		step = -val;
> +		dir = LMR_STEP_DIR_DECREASE;
> +	} else {
> +		step = val;
> +		dir = LMR_STEP_DIR_INCREASE;
> +		if (!(caps & LMR_CAP_IND_LEFT_RIGHT_TIMING)) {
> +			ret = -EINVAL;
> +			goto out;
> +		}
> +	}
> +
> +	if (step > LMR_MAX_TIMING_STEP) {

[Severity: Medium]
Does the truncation from int to u8 bypass the bounds limit check?

Since val is cast to an unsigned 8-bit integer step before the bounds
limit check is applied, out-of-bounds values like 256 or -256 are
truncated to 0. Does this bypass the safety limit, passing the truncated
value to hardware while saving the original out-of-bounds integer to
the driver state?

[ ... ]
> +static ssize_t margin_lane_voltage_write(struct file *file, const char __user *user_buf,
> +				     size_t count, loff_t *ppos)
> +{
[ ... ]
> +	if (val < 0) {
> +		step = -val;
> +		dir = 0;
> +	} else {
> +		step = val;
> +		dir = 1;
[ ... ]
> +	if (step > LMR_MAX_VOLTAGE_STEP) {

[Severity: Medium]
Does the same truncation flaw occur in the voltage margin handler,
allowing out-of-bounds margin steps to bypass limits?

[ ... ]
> +static void pci_margin_debugfs_init(struct pci_margin_dev *mdev)
> +{
> +	struct pci_dev *dev = mdev->dev;
> +	char dirname[64];
> +	int i;
> +
> +	snprintf(dirname, sizeof(dirname), "pcie_lmr_%s", dev_name(&dev->dev));
> +	mdev->debugfs = debugfs_create_dir(dirname, NULL);

[Severity: Medium]
Does passing NULL as the parent create the directory at the debugfs root
instead of under the expected PCI subsystem hierarchy?

The test script assumes the directories are located in the pci
subdirectory, causing the automated kselftest to fail with a skip
message.

[ ... ]
> +void pci_lmr_init(struct pci_dev *dev)
> +{
[ ... ]
> +	pci_info(dev, "Lane Margining at Receiver (Gen%u) Capability detected\n",
> +		 speed >= PCIE_SPEED_64_0GT ? LMR_GEN6 :
> +		 speed >= PCIE_SPEED_32_0GT ? LMR_GEN5 :
> +		 LMR_GEN4);
> +	return;

[Severity: Low]
If the PCIe link speed is PCI_SPEED_UNKNOWN, does this initialization
logic incorrectly report that a Gen6 capability was detected?

PCI_SPEED_UNKNOWN evaluates to 0xFF, which is numerically greater than
PCIE_SPEED_64_0GT. The ternary logging statement unconditionally checks
speed >= PCIE_SPEED_64_0GT.

-- 
Sashiko AI review · https://sashiko.dev/#/patchset/20260818-pcie-lmt-v1-1-13716f446594@google.com?part=1

^ permalink raw reply	[flat|nested] 9+ messages in thread

* [PATCH v2] PCI/pcie: Add PCIe Lane Margining at Receiver (LMR) support
  2026-08-18 18:57 [PATCH] PCI/pcie: Add PCIe Lane Margining at Receiver (LMR) support Priyank Rathod
  2026-08-18 19:10 ` sashiko-bot
@ 2026-08-18 19:26 ` Priyank Rathod
  2026-08-18 20:00   ` sashiko-bot
  2026-08-18 20:49 ` [PATCH v3] " Priyank Rathod
  2026-08-18 21:20 ` [PATCH v4] " Priyank Rathod
  3 siblings, 1 reply; 9+ messages in thread
From: Priyank Rathod @ 2026-08-18 19:26 UTC (permalink / raw)
  To: Bjorn Helgaas, Shuah Khan
  Cc: linux-kernel, linux-pci, linux-kselftest, Priyank Rathod

Per PCIe Base Specification r6.0, sec 8.4.4 ("Lane Margining at
Receiver"), PCIe devices operating at 16.0 GT/s (Gen 4) or higher data
rates support the Lane Margining at Receiver Extended Capability
(ID 0x27), and it is mandatory for receivers operating at 64.0 GT/s
(Gen 6) or higher data rates. Lane Margining allows software to
evaluate high-speed link margins by measuring timing and voltage steps
for each individual physical lane and receiver.

Add driver and debugfs support for PCIe Lane Margining at Receiver:

  - Add Lane Margining at Receiver Extended Capability register
    definitions (PCI_EXT_CAP_ID_LMR, PCI_LMR_PORT_CAP, PCI_LMR_PORT_STS,
    PCI_LMR_LANE_CTRL, PCI_LMR_LANE_STS) to <uapi/linux/pci_regs.h>.
  - Add Kconfig option CONFIG_PCIE_LMR (under drivers/pci/pcie/Kconfig)
    dependent on DEBUG_FS.
  - Implement drivers/pci/pcie/margin.c to probe the capability on Gen4+
    links and expose per-device debugfs entries under:
      /sys/kernel/debug/pci/pcie_lmr_<pci_dev_name>/
    providing control over margining enablement, receiver selection, and
    execution of timing/voltage margin step commands. Distinguish
    between missing mandatory LMR capability on Gen6+ vs optional on
    Gen4/Gen5.
  - Hook pci_lmr_init() into pci_init_capabilities() during device probe
    in drivers/pci/probe.c and pci_lmr_exit() into drivers/pci/remove.c.
  - Add kselftest script under tools/testing/selftests/pcie_lmt/pcie_lmt.sh
    to test debugfs capability reads, enablement, and stepping.

Signed-off-by: Priyank Rathod <rathodpriyank@google.com>
---
Per PCIe Base Specification r6.0, section 8.4.4 ("Lane Margining at Receiver"),
PCIe devices operating at 16.0 GT/s (Gen 4) or higher data rates support the
Lane Margining at Receiver Extended Capability (ID 0x27), and it is mandatory
for receivers operating at 64.0 GT/s (Gen 6) or higher data rates.

Lane Margining allows system software to evaluate high-speed link signal
integrity and margins by measuring timing and voltage steps for each physical
lane and receiver independently.

This series introduces kernel driver support, debugfs controls, and a
kselftest automation script for PCIe Lane Margining at Receiver (LMR/LMT).

==============================================================================
1. How to Enable & Configure
==============================================================================
Enable the Kconfig option under PCI support:
  CONFIG_PCIE_LMR=y (or =m)
  (Depends on CONFIG_PCI and CONFIG_DEBUG_FS)

Upon boot or device hotplug on Gen4+ links (>= 16.0 GT/s), the driver probes
Extended Capability ID 0x27 and exposes per-device debugfs interfaces:
  /sys/kernel/debug/pci/pcie_lmr_<domain>:<bus>:<dev>.<func>/

==============================================================================
2. How to Use the Debugfs Interface (Manual Margining)
==============================================================================
Inspect device-wide margining capabilities and port status:
  # Inspect root device LMR capabilities & status
  cat /sys/kernel/debug/pci/pcie_lmr_0000:01:00.0/capabilities
  cat /sys/kernel/debug/pci/pcie_lmr_0000:01:00.0/port_status

Enable active Lane Margining on the device:
  # Enable Lane Margining state machine
  echo 1 > /sys/kernel/debug/pci/pcie_lmr_0000:01:00.0/enable

Inspect and step individual lanes (e.g. lane0):
  # Select target receiver (0 = local receiver, 1..6 = retimers/link partners)
  echo 0 > /sys/kernel/debug/pci/pcie_lmr_0000:01:00.0/lane0/receiver

  # Check available timing and voltage steps for this receiver
  cat /sys/kernel/debug/pci/pcie_lmr_0000:01:00.0/lane0/caps
  cat /sys/kernel/debug/pci/pcie_lmr_0000:01:00.0/lane0/num_timing_steps
  cat /sys/kernel/debug/pci/pcie_lmr_0000:01:00.0/lane0/num_voltage_steps

  # Step timing margin or voltage margin offset
  echo 2 > /sys/kernel/debug/pci/pcie_lmr_0000:01:00.0/lane0/margin_timing
  echo 1 > /sys/kernel/debug/pci/pcie_lmr_0000:01:00.0/lane0/margin_voltage

  # Reset margin offset back to nominal (0)
  echo 0 > /sys/kernel/debug/pci/pcie_lmr_0000:01:00.0/lane0/margin_timing
  echo 0 > /sys/kernel/debug/pci/pcie_lmr_0000:01:00.0/lane0/margin_voltage

Disable Lane Margining when finished:
  echo 0 > /sys/kernel/debug/pci/pcie_lmr_0000:01:00.0/enable

==============================================================================
3. How to Run Automated Kselftests Using the Test Script
==============================================================================
An automated kselftest script is included to test capability reads, receiver
selection, and margining commands across all enumerated LMR devices:

  # Run directly as root
  sudo ./tools/testing/selftests/pcie_lmt/pcie_lmt.sh

Or run via the kselftest Makefile harness:
  make -C tools/testing/selftests TARGETS=pcie_lmt run_tests

Sample script output on an LMR-capable device:
  pcie_lmt: testing PCIe LMR debugfs entries
  pcie_lmt: probing device pcie_lmr_0000:01:00.0
    pcie_lmr_0000:01:00.0: capabilities read OK
    pcie_lmr_0000:01:00.0: port_status read OK
    pcie_lmr_0000:01:00.0: margining enabled OK
    pcie_lmr_0000:01:00.0: testing lane0
    pcie_lmr_0000:01:00.0: testing lane1
    pcie_lmr_0000:01:00.0: margining disabled OK
  pcie_lmt [PASS]

To: Bjorn Helgaas <bhelgaas@google.com>
To: Shuah Khan <shuah@kernel.org>
Cc: linux-kernel@vger.kernel.org
Cc: linux-pci@vger.kernel.org
Cc: linux-kselftest@vger.kernel.org
Signed-off-by: Priyank Rathod <rathodpriyank@google.com>

Changes in v2:
  - Fixed NO_CMD (0x7) clearing in pci_lmr_run_cmd() before issuing new commands per PCIe r6.0 sec 8.4.4.
  - Protected plane->rx updates with mdev->lock in margin_lane_receiver_write().
  - Corrected Margining Port Capabilities bit definition to PCI_LMR_PORT_CAP_USES_SW_READY (0x0001) in <uapi/linux/pci_regs.h>.
  - Updated kselftest script (pcie_lmt.sh) to locate LMR debugfs entries.
  - Validated integer bounds against LMR_MAX_TIMING_STEP / LMR_MAX_VOLTAGE_STEP before narrowing u8 cast.
  - Moved mdev->enabled checks inside mutex_lock(&mdev->lock) to eliminate TOCTOU races.
  - Checked return values of all pci_read_config_word() calls, propagating -EIO on failure.
  - Eliminated dead store of cap in margin_enable_write().
  - Explicitly checked speed == PCIE_SPEED_64_0GT in pci_lmr_init() to avoid misidentifying PCI_SPEED_UNKNOWN (0xFF) as Gen6.
---
 drivers/pci/pci.h                            |   8 +
 drivers/pci/pcie/Kconfig                     |  12 +
 drivers/pci/pcie/Makefile                    |   1 +
 drivers/pci/pcie/margin.c                    | 725 +++++++++++++++++++++++++++
 drivers/pci/probe.c                          |   1 +
 drivers/pci/remove.c                         |   1 +
 include/linux/pci.h                          |   6 +
 include/uapi/linux/pci_regs.h                |  18 +
 tools/testing/selftests/Makefile             |   1 +
 tools/testing/selftests/pcie_lmt/Makefile    |   3 +
 tools/testing/selftests/pcie_lmt/pcie_lmt.sh | 105 ++++
 11 files changed, 881 insertions(+)

diff --git a/drivers/pci/pci.h b/drivers/pci/pci.h
index 4469e1a77f3c..c3449c6a5b79 100644
--- a/drivers/pci/pci.h
+++ b/drivers/pci/pci.h
@@ -1023,6 +1023,14 @@ static inline void pci_no_tph(void) { }
 static inline void pci_tph_init(struct pci_dev *dev) { }
 #endif
 
+#ifdef CONFIG_PCIE_LMR
+void pci_lmr_init(struct pci_dev *dev);
+void pci_lmr_exit(struct pci_dev *dev);
+#else
+static inline void pci_lmr_init(struct pci_dev *dev) { }
+static inline void pci_lmr_exit(struct pci_dev *dev) { }
+#endif
+
 #ifdef CONFIG_PCIE_PTM
 void pci_ptm_init(struct pci_dev *dev);
 void pci_save_ptm_state(struct pci_dev *dev);
diff --git a/drivers/pci/pcie/Kconfig b/drivers/pci/pcie/Kconfig
index 207c2deae35f..3b021ca2fe84 100644
--- a/drivers/pci/pcie/Kconfig
+++ b/drivers/pci/pcie/Kconfig
@@ -137,6 +137,18 @@ config PCIE_PTM
 	  This is only useful if you have devices that support PTM, but it
 	  is safe to enable even if you don't.
 
+config PCIE_LMR
+	bool "PCI Express Lane Margining at Receiver Support"
+	depends on DEBUG_FS
+	help
+	  This enables the PCI Express Lane Margining at Receiver support.
+	  Lane Margining allows software to determine the voltage and
+	  timing margin of each lane on a PCIe link (16.0 GT/s and above).
+	  The margining data is exposed via debugfs.
+
+	  This is only useful if you have devices that support lane
+	  margining, but it is safe to enable even if you don't.
+
 config PCIE_EDR
 	bool "PCI Express Error Disconnect Recover support"
 	depends on PCIE_DPC && ACPI
diff --git a/drivers/pci/pcie/Makefile b/drivers/pci/pcie/Makefile
index b0b43a18c304..aac45ae0402e 100644
--- a/drivers/pci/pcie/Makefile
+++ b/drivers/pci/pcie/Makefile
@@ -13,4 +13,5 @@ obj-$(CONFIG_PCIEAER_INJECT)	+= aer_inject.o
 obj-$(CONFIG_PCIE_PME)		+= pme.o
 obj-$(CONFIG_PCIE_DPC)		+= dpc.o
 obj-$(CONFIG_PCIE_PTM)		+= ptm.o
+obj-$(CONFIG_PCIE_LMR)		+= margin.o
 obj-$(CONFIG_PCIE_EDR)		+= edr.o
diff --git a/drivers/pci/pcie/margin.c b/drivers/pci/pcie/margin.c
new file mode 100644
index 000000000000..c940b1f65907
--- /dev/null
+++ b/drivers/pci/pcie/margin.c
@@ -0,0 +1,725 @@
+// SPDX-License-Identifier: GPL-2.0
+/*
+ * PCI Express Lane Margining at Receiver
+ *
+ * Copyright (C) 2026 Google LLC
+ * Author: Priyank Rathod <rathodpriyank@google.com>
+ *
+ * Lane Margining at Receiver (PCIe r6.0, sec 8.4.4) allows the software
+ * to determine the voltage and timing margins of each lane on the PCIe
+ * link. The extended capability (ID 0x27) is available for receivers
+ * operating at 16.0 GT/s (Gen4) or higher data rates, and is mandatory
+ * for receivers operating at 64.0 GT/s (Gen6) or higher data rates.
+ *
+ * This implementation exposes per-device debugfs entries to read the margining
+ * capabilities and issues margining commands through custom file_ops.
+ */
+
+#include <linux/pci.h>
+#include <linux/slab.h>
+#include <linux/mutex.h>
+#include <linux/delay.h>
+#include <linux/jiffies.h>
+#include <linux/seq_file.h>
+#include <linux/bitfield.h>
+#include <linux/debugfs.h>
+
+#include "../pci.h"
+
+/* Margin type encodings per the PCIe spec */
+#define LMR_TYPE_DEMARGIN               0x0
+#define LMR_TYPE_REPORT_CAPS            0x1
+#define LMR_TYPE_REPORT_VOLTAGE_STEPS   0x2
+#define LMR_TYPE_REPORT_TIMING_STEPS    0x3
+#define LMR_TYPE_TIMING                 0x4
+#define LMR_TYPE_VOLTAGE                0x5
+#define LMR_TYPE_NO_CMD                 0x7
+
+/* LMR command timing parameters */
+#define LMR_CMD_TIMEOUT_MS              100
+#define LMR_CMD_SLEEP_MIN_US            100
+#define LMR_CMD_SLEEP_MAX_US            200
+#define LMR_ENABLE_TIMEOUT_MS           100
+#define LMR_ENABLE_SLEEP_MIN_US         1000
+#define LMR_ENABLE_SLEEP_MAX_US         2000
+
+/* LMR limits */
+#define LMR_MAX_LANES                   32
+#define LMR_MAX_RX_NUM                  6
+#define LMR_MAX_TIMING_STEP             63
+#define LMR_MAX_VOLTAGE_STEP            127
+
+/* LMR PCIe generation numbers */
+#define LMR_GEN6                        6
+#define LMR_GEN5                        5
+#define LMR_GEN4                        4
+
+/* LMR lane register stride */
+#define LMR_LANE_REG_STRIDE             4
+
+/* LMR receivers and directions */
+#define LMR_RX_LOCAL                    0
+#define LMR_STEP_DIR_INCREASE           1
+#define LMR_STEP_DIR_DECREASE           0
+
+/* LMR step & direction encoding masks */
+#define LMR_STEPS_MASK                  0x7F
+#define LMR_TIMING_STEP_MASK            0x3F
+#define LMR_TIMING_DIR_SHIFT            6
+#define LMR_VOLTAGE_STEP_MASK           0x7F
+#define LMR_VOLTAGE_DIR_SHIFT           7
+
+/* LMR capabilities report bit fields */
+#define LMR_CAP_MARGIN_HV               BIT(0)
+#define LMR_CAP_MARGIN_EV_IV            BIT(1)
+#define LMR_CAP_IND_LEFT_RIGHT_TIMING   BIT(2)
+#define LMR_CAP_IND_UP_DOWN_VOLTAGE     BIT(3)
+#define LMR_CAP_ERROR_SAMPLER           BIT(4)
+#define LMR_CAP_SAMPLE_MULTIPLE_RX      BIT(5)
+
+struct pci_margin_lane {
+	struct pci_margin_dev *mdev;
+	int lane;
+	u8 rx;
+	int timing_val;
+	int voltage_val;
+};
+
+struct pci_margin_dev {
+	struct pci_dev *dev;
+	u16 cap;
+	struct dentry *debugfs;
+	struct mutex lock;
+	int num_lanes;
+	struct pci_margin_lane *lanes;
+	bool enabled;
+};
+
+static int pci_lmr_run_cmd(struct pci_dev *dev, int lane, u8 rx, u8 type,
+			   u8 usage, u8 payload, u16 *status_val)
+{
+	u16 lmr = dev->lmr_cap;
+	u16 ctrl_offset = lmr + PCI_LMR_LANE_CTRL + LMR_LANE_REG_STRIDE * lane;
+	u16 sts_offset = lmr + PCI_LMR_LANE_STS + LMR_LANE_REG_STRIDE * lane;
+	u16 ctrl, sts;
+	unsigned long timeout;
+
+	/*
+	 * Per PCIe Base Spec r6.0 sec 8.4.4, software must issue NO_CMD (0x7)
+	 * to clear MTYPE in Lane Status before issuing a subsequent command.
+	 */
+	if (type != LMR_TYPE_NO_CMD) {
+		pci_write_config_word(dev, ctrl_offset,
+				      FIELD_PREP(PCI_LMR_LANE_CTRL_MTYPE,
+						 LMR_TYPE_NO_CMD));
+		timeout = jiffies + msecs_to_jiffies(LMR_CMD_TIMEOUT_MS);
+		while (1) {
+			if (pci_read_config_word(dev, sts_offset, &sts))
+				return -EIO;
+			if (sts == 0xFFFF)
+				return -ENODEV;
+			if (FIELD_GET(PCI_LMR_LANE_STS_MTYPE, sts) ==
+			    LMR_TYPE_NO_CMD)
+				break;
+			if (time_after(jiffies, timeout))
+				return -ETIMEDOUT;
+			usleep_range(LMR_CMD_SLEEP_MIN_US, LMR_CMD_SLEEP_MAX_US);
+		}
+	}
+
+	ctrl = FIELD_PREP(PCI_LMR_LANE_CTRL_RX_NUM, rx) |
+	       FIELD_PREP(PCI_LMR_LANE_CTRL_MTYPE, type) |
+	       FIELD_PREP(PCI_LMR_LANE_CTRL_USAGE, usage) |
+	       FIELD_PREP(PCI_LMR_LANE_CTRL_PAYLOAD, payload);
+
+	pci_write_config_word(dev, ctrl_offset, ctrl);
+
+	timeout = jiffies + msecs_to_jiffies(LMR_CMD_TIMEOUT_MS);
+	while (1) {
+		if (pci_read_config_word(dev, sts_offset, &sts))
+			return -EIO;
+		if (sts == 0xFFFF)
+			return -ENODEV;
+
+		if (FIELD_GET(PCI_LMR_LANE_STS_MTYPE, sts) == type) {
+			if (status_val)
+				*status_val = sts;
+			return 0;
+		}
+
+		if (time_after(jiffies, timeout))
+			break;
+
+		usleep_range(LMR_CMD_SLEEP_MIN_US, LMR_CMD_SLEEP_MAX_US);
+	}
+
+	return -ETIMEDOUT;
+}
+
+static int pci_lmr_run_cmd_locked(struct pci_margin_dev *mdev, int lane, u8 rx,
+				  u8 type, u8 usage, u8 payload, u16 *status_val)
+{
+	int ret;
+
+	mutex_lock(&mdev->lock);
+	if (!mdev->enabled) {
+		mutex_unlock(&mdev->lock);
+		return -EACCES;
+	}
+	ret = pci_lmr_run_cmd(mdev->dev, lane, rx, type, usage, payload, status_val);
+	mutex_unlock(&mdev->lock);
+
+	return ret;
+}
+
+#if IS_ENABLED(CONFIG_DEBUG_FS)
+
+static int margin_caps_show(struct seq_file *s, void *v)
+{
+	struct pci_margin_dev *mdev = s->private;
+	struct pci_dev *dev = mdev->dev;
+	u16 cap;
+
+	if (pci_read_config_word(dev, mdev->cap + PCI_LMR_PORT_CAP, &cap))
+		return -EIO;
+	seq_printf(s, "Port Capabilities: %#06x\n", cap);
+	seq_printf(s, "  Uses SW Ready: %s\n",
+		   (cap & PCI_LMR_PORT_CAP_USES_SW_READY) ? "yes" : "no");
+	return 0;
+}
+DEFINE_SHOW_ATTRIBUTE(margin_caps);
+
+static int margin_port_status_show(struct seq_file *s, void *v)
+{
+	struct pci_margin_dev *mdev = s->private;
+	struct pci_dev *dev = mdev->dev;
+	u16 sts;
+
+	if (pci_read_config_word(dev, mdev->cap + PCI_LMR_PORT_STS, &sts))
+		return -EIO;
+	seq_printf(s, "Port Status: %#06x\n", sts);
+	seq_printf(s, "  Margining Ready: %s\n",
+		   (sts & PCI_LMR_PORT_STS_MARGIN_READY) ? "yes" : "no");
+	seq_printf(s, "  SW Ready: %s\n", (sts & PCI_LMR_PORT_STS_SW_READY) ? "yes" : "no");
+	return 0;
+}
+DEFINE_SHOW_ATTRIBUTE(margin_port_status);
+
+static int margin_enable_show(struct seq_file *s, void *v)
+{
+	struct pci_margin_dev *mdev = s->private;
+
+	seq_printf(s, "%d\n", mdev->enabled);
+	return 0;
+}
+
+static ssize_t margin_enable_write(struct file *file, const char __user *user_buf,
+			       size_t count, loff_t *ppos)
+{
+	struct pci_margin_dev *mdev = ((struct seq_file *)file->private_data)->private;
+	struct pci_dev *dev = mdev->dev;
+	unsigned long timeout;
+	bool enable;
+	int ret, i;
+	u16 sts, cap;
+
+	ret = kstrtobool_from_user(user_buf, count, &enable);
+	if (ret)
+		return ret;
+
+	mutex_lock(&mdev->lock);
+
+	if (mdev->enabled == enable)
+		goto out;
+
+	if (enable) {
+		if (pci_read_config_word(dev, mdev->cap + PCI_LMR_PORT_CAP, &cap)) {
+			ret = -EIO;
+			goto out;
+		}
+
+		if (cap & PCI_LMR_PORT_CAP_USES_SW_READY) {
+			if (pci_read_config_word(dev, mdev->cap + PCI_LMR_PORT_STS, &sts)) {
+				ret = -EIO;
+				goto out;
+			}
+			sts |= PCI_LMR_PORT_STS_SW_READY;
+			pci_write_config_word(dev, mdev->cap + PCI_LMR_PORT_STS, sts);
+		}
+
+		timeout = jiffies + msecs_to_jiffies(LMR_ENABLE_TIMEOUT_MS);
+		while (1) {
+			if (pci_read_config_word(dev, mdev->cap + PCI_LMR_PORT_STS, &sts)) {
+				ret = -EIO;
+				goto out;
+			}
+			if (sts == 0xFFFF) {
+				ret = -ENODEV;
+				goto out;
+			}
+			if (sts & PCI_LMR_PORT_STS_MARGIN_READY)
+				break;
+			if (time_after(jiffies, timeout)) {
+				ret = -ETIMEDOUT;
+				if (cap & PCI_LMR_PORT_CAP_USES_SW_READY) {
+					sts &= ~PCI_LMR_PORT_STS_SW_READY;
+					pci_write_config_word(dev,
+							      mdev->cap + PCI_LMR_PORT_STS,
+							      sts);
+				}
+				goto out;
+			}
+			usleep_range(LMR_ENABLE_SLEEP_MIN_US, LMR_ENABLE_SLEEP_MAX_US);
+		}
+		mdev->enabled = true;
+	} else {
+		if (pci_read_config_word(dev, mdev->cap + PCI_LMR_PORT_STS, &sts) == 0) {
+			sts &= ~PCI_LMR_PORT_STS_SW_READY;
+			pci_write_config_word(dev, mdev->cap + PCI_LMR_PORT_STS, sts);
+		}
+
+		for (i = 0; i < mdev->num_lanes; i++) {
+			mdev->lanes[i].timing_val = 0;
+			mdev->lanes[i].voltage_val = 0;
+		}
+		mdev->enabled = false;
+	}
+
+out:
+	mutex_unlock(&mdev->lock);
+	return ret ? ret : count;
+}
+
+static int margin_enable_open(struct inode *inode, struct file *file)
+{
+	return single_open(file, margin_enable_show, inode->i_private);
+}
+
+static const struct file_operations margin_enable_fops = {
+	.open = margin_enable_open,
+	.read = seq_read,
+	.write = margin_enable_write,
+	.llseek = seq_lseek,
+	.release = single_release,
+};
+
+static int margin_lane_receiver_show(struct seq_file *s, void *v)
+{
+	struct pci_margin_lane *plane = s->private;
+	int rx;
+
+	mutex_lock(&plane->mdev->lock);
+	rx = plane->rx;
+	mutex_unlock(&plane->mdev->lock);
+
+	seq_printf(s, "%d\n", rx);
+	return 0;
+}
+
+static ssize_t margin_lane_receiver_write(struct file *file, const char __user *user_buf,
+				      size_t count, loff_t *ppos)
+{
+	struct pci_margin_lane *plane = ((struct seq_file *)file->private_data)->private;
+	u8 rx;
+	int ret;
+
+	ret = kstrtou8_from_user(user_buf, count, 0, &rx);
+	if (ret)
+		return ret;
+
+	if (rx > LMR_MAX_RX_NUM)
+		return -EINVAL;
+
+	mutex_lock(&plane->mdev->lock);
+	plane->rx = rx;
+	mutex_unlock(&plane->mdev->lock);
+
+	return count;
+}
+
+static int margin_lane_receiver_open(struct inode *inode, struct file *file)
+{
+	return single_open(file, margin_lane_receiver_show, inode->i_private);
+}
+
+static const struct file_operations margin_lane_receiver_fops = {
+	.open = margin_lane_receiver_open,
+	.read = seq_read,
+	.write = margin_lane_receiver_write,
+	.llseek = seq_lseek,
+	.release = single_release,
+};
+
+static int margin_lane_caps_show(struct seq_file *s, void *v)
+{
+	struct pci_margin_lane *plane = s->private;
+	struct pci_margin_dev *mdev = plane->mdev;
+	u16 sts;
+	int ret;
+	u8 val;
+
+	ret = pci_lmr_run_cmd_locked(mdev, plane->lane, plane->rx,
+				     LMR_TYPE_REPORT_CAPS, 0, 0, &sts);
+	if (ret)
+		return ret;
+
+	val = FIELD_GET(PCI_LMR_LANE_STS_PAYLOAD, sts);
+	seq_printf(s, "Lane %d Rx %d Capabilities: %#02x\n", plane->lane, plane->rx, val);
+	seq_printf(s, "  Margining H/V: %s\n", (val & LMR_CAP_MARGIN_HV) ? "both" : "either");
+	seq_printf(s, "  Margining eV/iV: %s\n",
+		   (val & LMR_CAP_MARGIN_EV_IV) ? "both (close & open)" : "one (close only)");
+	seq_printf(s, "  Left/Right: %s\n", (val & LMR_CAP_IND_LEFT_RIGHT_TIMING) ? "both" : "one");
+	seq_printf(s, "  Up/Down: %s\n", (val & LMR_CAP_IND_UP_DOWN_VOLTAGE) ? "both" : "one");
+	seq_printf(s, "  Error Sampler: %s\n",
+		   (val & LMR_CAP_ERROR_SAMPLER) ? "yes" : "no (main sampler)");
+	seq_printf(s, "  Sample Multiple Receivers: %s\n",
+		   (val & LMR_CAP_SAMPLE_MULTIPLE_RX) ? "yes" : "no");
+
+	return 0;
+}
+DEFINE_SHOW_ATTRIBUTE(margin_lane_caps);
+
+static int margin_lane_timing_steps_show(struct seq_file *s, void *v)
+{
+	struct pci_margin_lane *plane = s->private;
+	struct pci_margin_dev *mdev = plane->mdev;
+	u16 sts;
+	int ret;
+	u8 val;
+
+	ret = pci_lmr_run_cmd_locked(mdev, plane->lane, plane->rx,
+				     LMR_TYPE_REPORT_TIMING_STEPS, 0, 0, &sts);
+	if (ret)
+		return ret;
+
+	val = FIELD_GET(PCI_LMR_LANE_STS_PAYLOAD, sts) & LMR_STEPS_MASK;
+	seq_printf(s, "%d\n", val);
+	return 0;
+}
+DEFINE_SHOW_ATTRIBUTE(margin_lane_timing_steps);
+
+static int margin_lane_voltage_steps_show(struct seq_file *s, void *v)
+{
+	struct pci_margin_lane *plane = s->private;
+	struct pci_margin_dev *mdev = plane->mdev;
+	u16 sts;
+	int ret;
+	u8 val;
+
+	ret = pci_lmr_run_cmd_locked(mdev, plane->lane, plane->rx,
+				     LMR_TYPE_REPORT_VOLTAGE_STEPS, 0, 0, &sts);
+	if (ret)
+		return ret;
+
+	val = FIELD_GET(PCI_LMR_LANE_STS_PAYLOAD, sts) & LMR_STEPS_MASK;
+
+	seq_printf(s, "%d\n", val);
+	return 0;
+}
+DEFINE_SHOW_ATTRIBUTE(margin_lane_voltage_steps);
+
+static ssize_t margin_lane_timing_write(struct file *file, const char __user *user_buf,
+				    size_t count, loff_t *ppos)
+{
+	struct pci_margin_lane *plane = ((struct seq_file *)file->private_data)->private;
+	struct pci_margin_dev *mdev = plane->mdev;
+	int val, ret;
+	u8 step, dir;
+	u16 sts;
+	u8 caps;
+
+	ret = kstrtoint_from_user(user_buf, count, 0, &val);
+	if (ret)
+		return ret;
+
+	if (val > LMR_MAX_TIMING_STEP || val < -LMR_MAX_TIMING_STEP)
+		return -EINVAL;
+
+	mutex_lock(&mdev->lock);
+	if (!mdev->enabled) {
+		ret = -EACCES;
+		goto out;
+	}
+
+	if (val == 0) {
+		ret = pci_lmr_run_cmd(mdev->dev, plane->lane, plane->rx,
+				      LMR_TYPE_DEMARGIN, 0, 0, &sts);
+		if (ret)
+			goto out;
+		plane->timing_val = 0;
+		plane->voltage_val = 0;
+		goto out;
+	}
+
+	ret = pci_lmr_run_cmd(mdev->dev, plane->lane, plane->rx, LMR_TYPE_REPORT_CAPS, 0, 0, &sts);
+	if (ret)
+		goto out;
+	caps = FIELD_GET(PCI_LMR_LANE_STS_PAYLOAD, sts);
+
+	if (val < 0) {
+		step = -val;
+		dir = LMR_STEP_DIR_DECREASE;
+	} else {
+		step = val;
+		dir = LMR_STEP_DIR_INCREASE;
+		if (!(caps & LMR_CAP_IND_LEFT_RIGHT_TIMING)) {
+			ret = -EINVAL;
+			goto out;
+		}
+	}
+
+	ret = pci_lmr_run_cmd(mdev->dev, plane->lane, plane->rx,
+			      LMR_TYPE_TIMING, 0,
+			      (step & LMR_TIMING_STEP_MASK) | (dir << LMR_TIMING_DIR_SHIFT), &sts);
+	if (ret)
+		goto out;
+
+	plane->timing_val = val;
+
+out:
+	mutex_unlock(&mdev->lock);
+	return ret ? ret : count;
+}
+
+static int margin_lane_timing_show(struct seq_file *s, void *v)
+{
+	struct pci_margin_lane *plane = s->private;
+
+	seq_printf(s, "%d\n", plane->timing_val);
+	return 0;
+}
+
+static int margin_lane_timing_open(struct inode *inode, struct file *file)
+{
+	return single_open(file, margin_lane_timing_show, inode->i_private);
+}
+
+static const struct file_operations margin_lane_timing_fops = {
+	.open = margin_lane_timing_open,
+	.read = seq_read,
+	.write = margin_lane_timing_write,
+	.llseek = seq_lseek,
+	.release = single_release,
+};
+
+static ssize_t margin_lane_voltage_write(struct file *file, const char __user *user_buf,
+				     size_t count, loff_t *ppos)
+{
+	struct pci_margin_lane *plane = ((struct seq_file *)file->private_data)->private;
+	struct pci_margin_dev *mdev = plane->mdev;
+	int val, ret;
+	u8 step, dir;
+	u16 sts;
+	u8 caps;
+
+	ret = kstrtoint_from_user(user_buf, count, 0, &val);
+	if (ret)
+		return ret;
+
+	if (val > LMR_MAX_VOLTAGE_STEP || val < -LMR_MAX_VOLTAGE_STEP)
+		return -EINVAL;
+
+	mutex_lock(&mdev->lock);
+	if (!mdev->enabled) {
+		ret = -EACCES;
+		goto out;
+	}
+
+	if (val == 0) {
+		ret = pci_lmr_run_cmd(mdev->dev, plane->lane, plane->rx,
+				      LMR_TYPE_DEMARGIN, 0, 0, &sts);
+		if (ret)
+			goto out;
+		plane->timing_val = 0;
+		plane->voltage_val = 0;
+		goto out;
+	}
+
+	ret = pci_lmr_run_cmd(mdev->dev, plane->lane, plane->rx, LMR_TYPE_REPORT_CAPS, 0, 0, &sts);
+	if (ret)
+		goto out;
+	caps = FIELD_GET(PCI_LMR_LANE_STS_PAYLOAD, sts);
+
+	if (val < 0) {
+		step = -val;
+		dir = 0;
+	} else {
+		step = val;
+		dir = 1;
+		if (!(caps & LMR_CAP_IND_UP_DOWN_VOLTAGE)) {
+			ret = -EINVAL;
+			goto out;
+		}
+	}
+
+	if (step > LMR_MAX_VOLTAGE_STEP) {
+		ret = -EINVAL;
+		goto out;
+	}
+
+	ret = pci_lmr_run_cmd(mdev->dev, plane->lane, plane->rx,
+			      LMR_TYPE_VOLTAGE, 0,
+			      (step & LMR_VOLTAGE_STEP_MASK) |
+			      (dir << LMR_VOLTAGE_DIR_SHIFT), &sts);
+	if (ret)
+		goto out;
+
+	plane->voltage_val = val;
+
+out:
+	mutex_unlock(&mdev->lock);
+	return ret ? ret : count;
+}
+
+static int margin_lane_voltage_show(struct seq_file *s, void *v)
+{
+	struct pci_margin_lane *plane = s->private;
+
+	seq_printf(s, "%d\n", plane->voltage_val);
+	return 0;
+}
+
+static int margin_lane_voltage_open(struct inode *inode, struct file *file)
+{
+	return single_open(file, margin_lane_voltage_show, inode->i_private);
+}
+
+static const struct file_operations margin_lane_voltage_fops = {
+	.open = margin_lane_voltage_open,
+	.read = seq_read,
+	.write = margin_lane_voltage_write,
+	.llseek = seq_lseek,
+	.release = single_release,
+};
+
+static void pci_margin_debugfs_init(struct pci_margin_dev *mdev)
+{
+	struct pci_dev *dev = mdev->dev;
+	char dirname[64];
+	int i;
+
+	snprintf(dirname, sizeof(dirname), "pcie_lmr_%s", dev_name(&dev->dev));
+	mdev->debugfs = debugfs_create_dir(dirname, NULL);
+	if (!mdev->debugfs)
+		return;
+
+	debugfs_create_file("capabilities", 0444, mdev->debugfs, mdev, &margin_caps_fops);
+	debugfs_create_file("port_status", 0444, mdev->debugfs, mdev, &margin_port_status_fops);
+	debugfs_create_file("enable", 0644, mdev->debugfs, mdev, &margin_enable_fops);
+
+	for (i = 0; i < mdev->num_lanes; i++) {
+		struct pci_margin_lane *plane = &mdev->lanes[i];
+		struct dentry *lane_dir;
+		char lane_name[16];
+
+		snprintf(lane_name, sizeof(lane_name), "lane%d", i);
+		lane_dir = debugfs_create_dir(lane_name, mdev->debugfs);
+		if (!lane_dir)
+			continue;
+
+		debugfs_create_file("receiver", 0644, lane_dir, plane, &margin_lane_receiver_fops);
+		debugfs_create_file("caps", 0444, lane_dir, plane, &margin_lane_caps_fops);
+		debugfs_create_file("num_timing_steps", 0444, lane_dir, plane,
+				    &margin_lane_timing_steps_fops);
+		debugfs_create_file("num_voltage_steps", 0444, lane_dir, plane,
+				    &margin_lane_voltage_steps_fops);
+		debugfs_create_file("margin_timing", 0644, lane_dir, plane,
+				    &margin_lane_timing_fops);
+		debugfs_create_file("margin_voltage", 0644, lane_dir, plane,
+				    &margin_lane_voltage_fops);
+	}
+}
+
+static void pci_margin_debugfs_remove(struct pci_margin_dev *mdev)
+{
+	debugfs_remove_recursive(mdev->debugfs);
+}
+
+#else
+static inline void pci_margin_debugfs_init(struct pci_margin_dev *mdev) { }
+static inline void pci_margin_debugfs_remove(struct pci_margin_dev *mdev) { }
+#endif
+
+void pci_lmr_init(struct pci_dev *dev)
+{
+	struct pci_margin_dev *mdev;
+	enum pci_bus_speed speed;
+	u16 lmr;
+	u16 lnkcap;
+	int i;
+
+	if (!pci_is_pcie(dev))
+		return;
+
+	speed = pcie_get_speed_cap(dev);
+
+	lmr = pci_find_ext_capability(dev, PCI_EXT_CAP_ID_LMR);
+	if (!lmr) {
+		if (speed == PCIE_SPEED_64_0GT)
+			pci_warn(dev,
+				 "Missing Lane Margining at Receiver Capability (mandatory for Gen6+)\n");
+		else if (speed >= PCIE_SPEED_16_0GT && speed != PCI_SPEED_UNKNOWN)
+			pci_dbg(dev,
+				"Optional Lane Margining at Receiver Capability not found\n");
+		return;
+	}
+
+	if (speed < PCIE_SPEED_16_0GT && speed != PCI_SPEED_UNKNOWN)
+		return;
+
+	dev->lmr_cap = lmr;
+
+	mdev = kzalloc_obj(*mdev, GFP_KERNEL);
+	if (!mdev)
+		return;
+
+	mdev->dev = dev;
+	mdev->cap = lmr;
+	mutex_init(&mdev->lock);
+
+	pcie_capability_read_word(dev, PCI_EXP_LNKCAP, &lnkcap);
+	mdev->num_lanes = FIELD_GET(PCI_EXP_LNKCAP_MLW, lnkcap);
+
+	if (mdev->num_lanes == 0 || mdev->num_lanes > LMR_MAX_LANES) {
+		pci_warn(dev, "Invalid link width %d for LMR\n", mdev->num_lanes);
+		goto err_free_mdev;
+	}
+
+	mdev->lanes = kcalloc(mdev->num_lanes, sizeof(*mdev->lanes), GFP_KERNEL);
+	if (!mdev->lanes)
+		goto err_free_mdev;
+
+	for (i = 0; i < mdev->num_lanes; i++) {
+		mdev->lanes[i].mdev = mdev;
+		mdev->lanes[i].lane = i;
+		mdev->lanes[i].rx = LMR_RX_LOCAL;
+	}
+
+	pci_margin_debugfs_init(mdev);
+
+	dev->lmr = mdev;
+
+	pci_info(dev, "Lane Margining at Receiver (Gen%u) Capability detected\n",
+		 speed == PCIE_SPEED_64_0GT ? LMR_GEN6 :
+		 speed == PCIE_SPEED_32_0GT ? LMR_GEN5 :
+		 LMR_GEN4);
+	return;
+
+err_free_mdev:
+	mutex_destroy(&mdev->lock);
+	kfree(mdev);
+}
+
+void pci_lmr_exit(struct pci_dev *dev)
+{
+	struct pci_margin_dev *mdev = dev->lmr;
+
+	if (!mdev)
+		return;
+
+	pci_margin_debugfs_remove(mdev);
+	mutex_destroy(&mdev->lock);
+	kfree(mdev->lanes);
+	kfree(mdev);
+	dev->lmr = NULL;
+}
diff --git a/drivers/pci/probe.c b/drivers/pci/probe.c
index dd0abbc63e18..352b95568ebf 100644
--- a/drivers/pci/probe.c
+++ b/drivers/pci/probe.c
@@ -2666,6 +2666,7 @@ static void pci_init_capabilities(struct pci_dev *dev)
 	pci_pasid_init(dev);		/* Process Address Space ID */
 	pci_acs_init(dev);		/* Access Control Services */
 	pci_ptm_init(dev);		/* Precision Time Measurement */
+	pci_lmr_init(dev);		/* Lane Margining at Receiver */
 	pci_aer_init(dev);		/* Advanced Error Reporting */
 	pci_dpc_init(dev);		/* Downstream Port Containment */
 	pci_rcec_init(dev);		/* Root Complex Event Collector */
diff --git a/drivers/pci/remove.c b/drivers/pci/remove.c
index d8bffa21498a..6fba29040e44 100644
--- a/drivers/pci/remove.c
+++ b/drivers/pci/remove.c
@@ -36,6 +36,7 @@ static void pci_destroy_dev(struct pci_dev *dev)
 
 	pci_doe_sysfs_teardown(dev);
 	pci_npem_remove(dev);
+	pci_lmr_exit(dev);
 
 	/*
 	 * While device is in D0 drop the device from TSM link operations
diff --git a/include/linux/pci.h b/include/linux/pci.h
index 64b308b6e61c..ef1275f4c5b6 100644
--- a/include/linux/pci.h
+++ b/include/linux/pci.h
@@ -349,6 +349,8 @@ struct rcec_ea;
  *			number resources to allow for hierarchy expansion.
  * @is_pciehp:		PCIe Hot-Plug Capable bridge.
  */
+struct pci_margin_dev;
+
 struct pci_dev {
 	struct list_head bus_list;	/* Node in per-bus list */
 	struct pci_bus	*bus;		/* Bus this device is on */
@@ -528,6 +530,10 @@ struct pci_dev {
 	atomic_t	ptm_enable_cnt;
 	u8		ptm_granularity;
 #endif
+#ifdef CONFIG_PCIE_LMR
+	u16			lmr_cap;	/* Lane Margining Capability */
+	struct pci_margin_dev	*lmr;
+#endif
 #ifdef CONFIG_PCI_MSI
 	void __iomem	*msix_base;
 	raw_spinlock_t	msi_lock;
diff --git a/include/uapi/linux/pci_regs.h b/include/uapi/linux/pci_regs.h
index facaa324bd86..90cbe310e62f 100644
--- a/include/uapi/linux/pci_regs.h
+++ b/include/uapi/linux/pci_regs.h
@@ -757,6 +757,7 @@
 #define PCI_EXT_CAP_ID_VF_REBAR 0x24	/* VF Resizable BAR */
 #define PCI_EXT_CAP_ID_DLF	0x25	/* Data Link Feature */
 #define PCI_EXT_CAP_ID_PL_16GT	0x26	/* Physical Layer 16.0 GT/s */
+#define PCI_EXT_CAP_ID_LMR	0x27	/* Lane Margining at Receiver */
 #define PCI_EXT_CAP_ID_NPEM	0x29	/* Native PCIe Enclosure Management */
 #define PCI_EXT_CAP_ID_PL_32GT  0x2A    /* Physical Layer 32.0 GT/s */
 #define PCI_EXT_CAP_ID_DOE	0x2E	/* Data Object Exchange */
@@ -1181,6 +1182,23 @@
 #define  PCI_PL_16GT_LE_CTRL_USP_TX_PRESET_MASK		0x000000F0
 #define  PCI_PL_16GT_LE_CTRL_USP_TX_PRESET_SHIFT	4
 
+/* Lane Margining at Receiver */
+#define PCI_LMR_PORT_CAP		0x04	/* Margining Port Capabilities */
+#define  PCI_LMR_PORT_CAP_USES_SW_READY	0x0001	/* Margining Uses Software Ready */
+#define PCI_LMR_PORT_STS		0x06	/* Margining Port Status */
+#define  PCI_LMR_PORT_STS_MARGIN_READY	0x0001	/* Margining Ready */
+#define  PCI_LMR_PORT_STS_SW_READY	0x0002	/* Margining SW Ready */
+#define PCI_LMR_LANE_CTRL		0x08	/* Margining Lane Control */
+#define  PCI_LMR_LANE_CTRL_RX_NUM	0x0007	/* Receiver Number */
+#define  PCI_LMR_LANE_CTRL_MTYPE	0x0038	/* Margining Type */
+#define  PCI_LMR_LANE_CTRL_USAGE	0x0040	/* Margining Usage Model */
+#define  PCI_LMR_LANE_CTRL_PAYLOAD	0xFF00	/* Margining Payload */
+#define PCI_LMR_LANE_STS		0x0A	/* Margining Lane Status */
+#define  PCI_LMR_LANE_STS_RX_NUM	0x0007	/* Receiver Number */
+#define  PCI_LMR_LANE_STS_MTYPE		0x0038	/* Margining Type */
+#define  PCI_LMR_LANE_STS_USAGE		0x0040	/* Margining Usage Model */
+#define  PCI_LMR_LANE_STS_PAYLOAD	0xFF00	/* Margining Payload */
+
 /* Physical Layer 32.0 GT/s */
 #define PCI_PL_32GT_LE_CTRL	0x20	/* Lane Equalization Control Register */
 
diff --git a/tools/testing/selftests/Makefile b/tools/testing/selftests/Makefile
index 8a4b6ddc68df..6990d999388a 100644
--- a/tools/testing/selftests/Makefile
+++ b/tools/testing/selftests/Makefile
@@ -91,6 +91,7 @@ TARGETS += net/tcp_ao
 TARGETS += nolibc
 TARGETS += pci_endpoint
 TARGETS += pcie_bwctrl
+TARGETS += pcie_lmt
 TARGETS += perf_events
 TARGETS += pidfd
 TARGETS += pid_namespace
diff --git a/tools/testing/selftests/pcie_lmt/Makefile b/tools/testing/selftests/pcie_lmt/Makefile
new file mode 100644
index 000000000000..36ac85937d78
--- /dev/null
+++ b/tools/testing/selftests/pcie_lmt/Makefile
@@ -0,0 +1,3 @@
+# SPDX-License-Identifier: GPL-2.0
+TEST_PROGS = pcie_lmt.sh
+include ../lib.mk
diff --git a/tools/testing/selftests/pcie_lmt/pcie_lmt.sh b/tools/testing/selftests/pcie_lmt/pcie_lmt.sh
new file mode 100755
index 000000000000..22c00c2b8956
--- /dev/null
+++ b/tools/testing/selftests/pcie_lmt/pcie_lmt.sh
@@ -0,0 +1,105 @@
+#!/bin/bash
+# SPDX-License-Identifier: GPL-2.0
+#
+# Copyright (C) 2026 Google LLC
+# Author: Priyank Rathod <rathodpriyank@google.com>
+#
+# Kselftest for PCIe Lane Margining at Receiver (LMR / LMT)
+# Tests the debugfs interface exposed by drivers/pci/pcie/margin.c
+# (/sys/kernel/debug/pci/pcie_lmr_<pci_dev_name>/)
+
+set -e
+
+TESTNAME="pcie_lmt"
+
+# Kselftest framework requirement - SKIP code is 4.
+ksft_skip=4
+retval=0
+skipmsg="skip all tests:"
+
+if [ $UID != 0 ]; then
+	echo "$skipmsg must be run as root" >&2
+	exit $ksft_skip
+fi
+
+DEBUGFS=$(mount -t debugfs | head -1 | awk '{ print $3 }')
+if [ -z "$DEBUGFS" ]; then
+	if [ -d "/sys/kernel/debug" ]; then
+		DEBUGFS="/sys/kernel/debug"
+	else
+		echo "$skipmsg debugfs is not mounted" >&2
+		exit $ksft_skip
+	fi
+fi
+
+if [ ! -d "$DEBUGFS/pci" ]; then
+	# Allow searching debugfs root or pci directory
+	:
+fi
+
+LMR_DEVS=$(ls -d $DEBUGFS/pci/pcie_lmr_* $DEBUGFS/pcie_lmr_* 2>/dev/null || true)
+if [ -z "$LMR_DEVS" ]; then
+	echo "$skipmsg no PCIe LMR devices found in $DEBUGFS/" >&2
+	exit $ksft_skip
+fi
+
+cleanup_dev()
+{
+	local dev="$1"
+	echo 0 > "$dev/enable" 2>/dev/null || true
+}
+
+echo "$TESTNAME: testing PCIe LMR debugfs entries"
+
+for dev in $LMR_DEVS; do
+	dev_name=$(basename "$dev")
+	echo "$TESTNAME: probing device $dev_name"
+
+	if [ ! -r "$dev/capabilities" ] || [ ! -r "$dev/port_status" ] ||
+	   [ ! -r "$dev/enable" ] || [ ! -w "$dev/enable" ]; then
+		echo "$TESTNAME: $dev_name missing mandatory root attributes"
+		retval=1
+		continue
+	fi
+
+	caps=$(cat "$dev/capabilities")
+	status=$(cat "$dev/port_status")
+	echo "  $dev_name: capabilities read OK"
+	echo "  $dev_name: port_status read OK"
+
+	trap 'cleanup_dev "$dev"' EXIT
+
+	if ! echo 1 > "$dev/enable" 2>/dev/null; then
+		echo "  $dev_name: margining not ready by hardware (skipping active lanes)"
+		continue
+	fi
+
+	echo "  $dev_name: margining enabled OK"
+
+	for lane_dir in $(ls -d "$dev"/lane* 2>/dev/null || true); do
+		lane=$(basename "$lane_dir")
+		echo "  $dev_name: testing $lane"
+
+		# Test setting receiver (Rx 0 is always local receiver)
+		echo 0 > "$lane_dir/receiver"
+		cat "$lane_dir/caps" > /dev/null
+		cat "$lane_dir/num_timing_steps" > /dev/null
+		cat "$lane_dir/num_voltage_steps" > /dev/null
+
+		# Test resetting timing and voltage margin
+		echo 0 > "$lane_dir/margin_timing"
+		echo 0 > "$lane_dir/margin_voltage"
+	done
+
+	echo 0 > "$dev/enable"
+	trap - EXIT
+	echo "  $dev_name: margining disabled OK"
+done
+
+if [ $retval -eq 0 ]; then
+	echo "$TESTNAME [PASS]"
+else
+	echo "$TESTNAME [FAIL]"
+fi
+
+exit $retval

---
base-commit: 0f23d56f17fdfc7db69d51f64c8b91bbab947aa9
change-id: 20260818-pcie-lmt-3044d586aaec

Best regards,
-- 
Priyank Rathod <rathodpriyank@google.com>

^ permalink raw reply related	[flat|nested] 9+ messages in thread

* Re: [PATCH v2] PCI/pcie: Add PCIe Lane Margining at Receiver (LMR) support
  2026-08-18 19:26 ` [PATCH v2] " Priyank Rathod
@ 2026-08-18 20:00   ` sashiko-bot
  0 siblings, 0 replies; 9+ messages in thread
From: sashiko-bot @ 2026-08-18 20:00 UTC (permalink / raw)
  To: Priyank Rathod; +Cc: linux-pci

Thank you for your contribution! Sashiko AI review found 10 potential issue(s) to consider:
- [High] Disabling LMR via debugfs fails to demargin lanes and incorrectly clears SW_READY, violating the PCIe specification.
- [High] ASPM (Active State Power Management) is not disabled during margining.
- [High] Issuing NO_CMD hardcodes the receiver number to 0, failing to clear the margining status on the target receiver.
- [High] Changing the target receiver via debugfs abandons any active margin on the previous receiver, violating the 'single Receiver per Lane' PCIe rule.
- [High] Reading static capabilities dynamically issues hardware commands, which silently destroys the active margin.
- [High] Validation checks for independent margining support are inverted, rejecting valid symmetric margining commands.
- [Medium] The driver tracks relative step commands as absolute state and ignores the applied margin step returned by the hardware.
- [Medium] Debugfs `show` functions and capability reads access shared state variables without holding `mdev->lock`, leading to TOCTOU races.
- [Medium] The software margining enable state (`mdev->enabled`) becomes desynchronized from the hardware state after a device suspend/resume or reset.
- [Low] Debugfs directory is created in the debugfs root instead of the PCI subsystem directory.
--

--- Patch [1]: [PATCH v2] PCI/pcie: Add PCIe Lane Margining at Receiver (LMR) support ---
commit bbd05305a407b4fac659a5611ac2d3b6bcb799ef
Author: Priyank Rathod <rathodpriyank@google.com>

PCI/pcie: Add PCIe Lane Margining at Receiver (LMR) support

This commit adds kernel driver support, debugfs controls, and a kselftest
script for PCIe Lane Margining at Receiver (LMR). It allows system software
to evaluate high-speed link margins by measuring timing and voltage steps.

> diff --git a/drivers/pci/pcie/margin.c b/drivers/pci/pcie/margin.c
> index 0000000000000..c940b1f659071
> --- /dev/null
> +++ b/drivers/pci/pcie/margin.c

[ ... ]

> +static int pci_lmr_run_cmd(struct pci_dev *dev, int lane, u8 rx, u8 type,
> +			   u8 usage, u8 payload, u16 *status_val)
> +{
> +	u16 lmr = dev->lmr_cap;
> +	u16 ctrl_offset = lmr + PCI_LMR_LANE_CTRL + LMR_LANE_REG_STRIDE * lane;
> +	u16 sts_offset = lmr + PCI_LMR_LANE_STS + LMR_LANE_REG_STRIDE * lane;
> +	u16 ctrl, sts;
> +	unsigned long timeout;
> +
> +	/*
> +	 * Per PCIe Base Spec r6.0 sec 8.4.4, software must issue NO_CMD (0x7)
> +	 * to clear MTYPE in Lane Status before issuing a subsequent command.
> +	 */
> +	if (type != LMR_TYPE_NO_CMD) {
> +		pci_write_config_word(dev, ctrl_offset,
> +				      FIELD_PREP(PCI_LMR_LANE_CTRL_MTYPE,
> +						 LMR_TYPE_NO_CMD));

[Severity: High]
Does this hardcode the receiver number to 0?

When margining a retimer (rx > 0), the prerequisite NO_CMD will be sent to
the local receiver (rx = 0) since RX_NUM is missing from the control register
write. Can this corrupt the local receiver's command state and leave the
target retimer's margining status uncleared?

> +		timeout = jiffies + msecs_to_jiffies(LMR_CMD_TIMEOUT_MS);

[ ... ]

> +static ssize_t margin_enable_write(struct file *file, const char __user *user_buf,
> +			       size_t count, loff_t *ppos)
> +{
> +	struct pci_margin_dev *mdev = ((struct seq_file *)file->private_data)->private;
> +	struct pci_dev *dev = mdev->dev;
> +	unsigned long timeout;
> +	bool enable;
> +	int ret, i;
> +	u16 sts, cap;
> +
> +	ret = kstrtobool_from_user(user_buf, count, &enable);
> +	if (ret)
> +		return ret;
> +
> +	mutex_lock(&mdev->lock);
> +
> +	if (mdev->enabled == enable)
> +		goto out;

[Severity: Medium]
Can mdev->enabled become desynchronized from the hardware state after a
device suspend/resume or reset?

If the hardware margin state is reset during suspend/resume, mdev->enabled
remains true. Would the early exit here force the user to manually toggle the
enable state off and on to recover?

> +
> +	if (enable) {
> +		if (pci_read_config_word(dev, mdev->cap + PCI_LMR_PORT_CAP, &cap)) {

[Severity: High]
Should active state power management be disabled during margining?

The PCIe 6.0 specification requires the link to be maintained in L0 during
margining. If ASPM is not disabled, could the link transition to low-power
states like L0s or L1, abort the margining process, and cause link instability?

> +			ret = -EIO;
> +			goto out;
> +		}

[ ... ]

> +	} else {
> +		if (pci_read_config_word(dev, mdev->cap + PCI_LMR_PORT_STS, &sts) == 0) {
> +			sts &= ~PCI_LMR_PORT_STS_SW_READY;
> +			pci_write_config_word(dev, mdev->cap + PCI_LMR_PORT_STS, sts);
> +		}
> +
> +		for (i = 0; i < mdev->num_lanes; i++) {
> +			mdev->lanes[i].timing_val = 0;
> +			mdev->lanes[i].voltage_val = 0;
> +		}
> +		mdev->enabled = false;
> +	}

[Severity: High]
Does this leave the hardware in a margined state when disabling LMR?

This clears SW_READY unconditionally but omits any NO_CMD or DEMARGIN commands.
Could this leave hardware receivers with an active margin offset when software
believes margining is disabled, potentially causing link drops?

> +
> +out:
> +	mutex_unlock(&mdev->lock);

[ ... ]

> +static ssize_t margin_lane_receiver_write(struct file *file, const char __user *user_buf,
> +				      size_t count, loff_t *ppos)
> +{
> +	struct pci_margin_lane *plane = ((struct seq_file *)file->private_data)->private;
> +	u8 rx;
> +	int ret;
> +
> +	ret = kstrtou8_from_user(user_buf, count, 0, &rx);
> +	if (ret)
> +		return ret;
> +
> +	if (rx > LMR_MAX_RX_NUM)
> +		return -EINVAL;
> +
> +	mutex_lock(&plane->mdev->lock);
> +	plane->rx = rx;
> +	mutex_unlock(&plane->mdev->lock);

[Severity: High]
Does changing the target receiver here abandon any active margin on the
previous receiver?

If a user writes a non-zero margin step to one receiver and then changes the
receiver number, the previous receiver appears to be left in an actively
margined state. Doesn't the PCIe specification restrict margining to a single
receiver per lane?

> +
> +	return count;
> +}

[ ... ]

> +static int margin_lane_caps_show(struct seq_file *s, void *v)
> +{
> +	struct pci_margin_lane *plane = s->private;
> +	struct pci_margin_dev *mdev = plane->mdev;
> +	u16 sts;
> +	int ret;
> +	u8 val;
> +
> +	ret = pci_lmr_run_cmd_locked(mdev, plane->lane, plane->rx,
> +				     LMR_TYPE_REPORT_CAPS, 0, 0, &sts);

[Severity: High]
Does reading static capabilities dynamically issue a hardware command that
silently destroys the active margin?

The PCIe specification states that issuing NO_CMD (which happens before the
REPORT_CAPS command) dictates the receiver must not apply any margin. Could
reading capabilities silently abort an active test without updating the
software tracking variables?

[Severity: Medium]
Can plane->rx be modified concurrently while being accessed here?

This accesses plane->rx locklessly before and after the command. Could a
concurrent write to the receiver debugfs file cause inconsistent output or
send commands to the wrong receiver?

> +	if (ret)
> +		return ret;
> +
> +	val = FIELD_GET(PCI_LMR_LANE_STS_PAYLOAD, sts);
> +	seq_printf(s, "Lane %d Rx %d Capabilities: %#02x\n", plane->lane, plane->rx, val);

[ ... ]

> +static ssize_t margin_lane_timing_write(struct file *file, const char __user *user_buf,
> +				    size_t count, loff_t *ppos)
> +{
> +	struct pci_margin_lane *plane = ((struct seq_file *)file->private_data)->private;
> +	struct pci_margin_dev *mdev = plane->mdev;
> +	int val, ret;
> +	u8 step, dir;
> +	u16 sts;
> +	u8 caps;
> +
> +	ret = kstrtoint_from_user(user_buf, count, 0, &val);

[ ... ]

> +	if (val < 0) {
> +		step = -val;
> +		dir = LMR_STEP_DIR_DECREASE;
> +	} else {
> +		step = val;
> +		dir = LMR_STEP_DIR_INCREASE;
> +		if (!(caps & LMR_CAP_IND_LEFT_RIGHT_TIMING)) {
> +			ret = -EINVAL;
> +			goto out;
> +		}
> +	}

[Severity: High]
Are the validation checks for independent margining support inverted here?

For receivers that only support symmetric margining, the PCIe specification
requires the direction bit to be set to 1b. By rejecting positive steps
(dir=1) when independent margining is not supported, does this incorrectly
reject valid symmetric margining commands?

> +
> +	ret = pci_lmr_run_cmd(mdev->dev, plane->lane, plane->rx,
> +			      LMR_TYPE_TIMING, 0,
> +			      (step & LMR_TIMING_STEP_MASK) | (dir << LMR_TIMING_DIR_SHIFT), &sts);
> +	if (ret)
> +		goto out;
> +
> +	plane->timing_val = val;

[Severity: Medium]
Does this ignore the applied margin step returned by the hardware?

The user input is passed as a relative step, but it is cached as the absolute
tracking variable (plane->timing_val). Because hardware applies steps relative
to the current position and may cap the step at its maximum supported offset,
can the software state become completely desynchronized from the actual
hardware margin offset?

> +
> +out:
> +	mutex_unlock(&mdev->lock);
> +	return ret ? ret : count;
> +}

[ ... ]

> +static ssize_t margin_lane_voltage_write(struct file *file, const char __user *user_buf,
> +				     size_t count, loff_t *ppos)
> +{
> +	struct pci_margin_lane *plane = ((struct seq_file *)file->private_data)->private;

[ ... ]

> +	if (val < 0) {
> +		step = -val;
> +		dir = 0;
> +	} else {
> +		step = val;
> +		dir = 1;
> +		if (!(caps & LMR_CAP_IND_UP_DOWN_VOLTAGE)) {
> +			ret = -EINVAL;
> +			goto out;
> +		}
> +	}

[Severity: High]
Are the validation checks for independent voltage margining support inverted?

Similar to timing, does this incorrectly reject valid symmetric voltage
margining commands (dir=1) for receivers that do not support independent
up/down margining?

> +
> +	if (step > LMR_MAX_VOLTAGE_STEP) {
> +		ret = -EINVAL;
> +		goto out;
> +	}
> +
> +	ret = pci_lmr_run_cmd(mdev->dev, plane->lane, plane->rx,
> +			      LMR_TYPE_VOLTAGE, 0,
> +			      (step & LMR_VOLTAGE_STEP_MASK) |
> +			      (dir << LMR_VOLTAGE_DIR_SHIFT), &sts);
> +	if (ret)
> +		goto out;
> +
> +	plane->voltage_val = val;

[Severity: Medium]
Does this ignore the applied voltage margin step returned by the hardware?

Just like with timing margining, caching the user input as the absolute value
ignores the actual step hardware applied. Can this lead to desynchronization
between software state and the actual hardware offset?

> +
> +out:
> +	mutex_unlock(&mdev->lock);
> +	return ret ? ret : count;
> +}

[ ... ]

> +static void pci_margin_debugfs_init(struct pci_margin_dev *mdev)
> +{
> +	struct pci_dev *dev = mdev->dev;
> +	char dirname[64];
> +	int i;
> +
> +	snprintf(dirname, sizeof(dirname), "pcie_lmr_%s", dev_name(&dev->dev));
> +	mdev->debugfs = debugfs_create_dir(dirname, NULL);

[Severity: Low]
Does this create the debugfs directory in the debugfs root instead of the
PCI subsystem directory?

Passing NULL as the parent argument will create /sys/kernel/debug/pcie_lmr_<dev>
instead of the intended PCI directory /sys/kernel/debug/pci/.

> +	if (!mdev->debugfs)
> +		return;

-- 
Sashiko AI review · https://sashiko.dev/#/patchset/20260818-pcie-lmt-v2-1-04ec6327f874@google.com?part=1

^ permalink raw reply	[flat|nested] 9+ messages in thread

* [PATCH v3] PCI/pcie: Add PCIe Lane Margining at Receiver (LMR) support
  2026-08-18 18:57 [PATCH] PCI/pcie: Add PCIe Lane Margining at Receiver (LMR) support Priyank Rathod
  2026-08-18 19:10 ` sashiko-bot
  2026-08-18 19:26 ` [PATCH v2] " Priyank Rathod
@ 2026-08-18 20:49 ` Priyank Rathod
  2026-08-18 21:00   ` sashiko-bot
  2026-08-18 21:20 ` [PATCH v4] " Priyank Rathod
  3 siblings, 1 reply; 9+ messages in thread
From: Priyank Rathod @ 2026-08-18 20:49 UTC (permalink / raw)
  To: Bjorn Helgaas, Shuah Khan
  Cc: linux-kernel, linux-pci, linux-kselftest, Priyank Rathod

Per PCIe Base Specification r6.0, sec 8.4.4 ("Lane Margining at
Receiver"), PCIe devices operating at 16.0 GT/s (Gen 4) or higher data
rates support the Lane Margining at Receiver Extended Capability
(ID 0x27), and it is mandatory for receivers operating at 64.0 GT/s
(Gen 6) or higher data rates. Lane Margining allows software to
evaluate high-speed link margins by measuring timing and voltage steps
for each individual physical lane and receiver.

Add driver and debugfs support for PCIe Lane Margining at Receiver:

  - Add Lane Margining at Receiver Extended Capability register
    definitions (PCI_EXT_CAP_ID_LMR, PCI_LMR_PORT_CAP, PCI_LMR_PORT_STS,
    PCI_LMR_LANE_CTRL, PCI_LMR_LANE_STS) to <uapi/linux/pci_regs.h>.
  - Add Kconfig option CONFIG_PCIE_LMR (under drivers/pci/pcie/Kconfig)
    dependent on DEBUG_FS.
  - Implement drivers/pci/pcie/margin.c to probe the capability on Gen4+
    links and expose per-device debugfs entries under:
      /sys/kernel/debug/pci/pcie_lmr_<pci_dev_name>/
    providing control over margining enablement, receiver selection, and
    execution of timing/voltage margin step commands. Distinguish
    between missing mandatory LMR capability on Gen6+ vs optional on
    Gen4/Gen5.
  - Hook pci_lmr_init() into pci_init_capabilities() during device probe
    in drivers/pci/probe.c and pci_lmr_exit() into drivers/pci/remove.c.
  - Add kselftest script under tools/testing/selftests/pcie_lmt/pcie_lmt.sh
    to test debugfs capability reads, enablement, and stepping.

Signed-off-by: Priyank Rathod <rathodpriyank@google.com>
---
Per PCIe Base Specification r6.0, section 8.4.4 ("Lane Margining at Receiver"),
PCIe devices operating at 16.0 GT/s (Gen 4) or higher data rates support the
Lane Margining at Receiver Extended Capability (ID 0x27), and it is mandatory
for receivers operating at 64.0 GT/s (Gen 6) or higher data rates.

Lane Margining allows system software to evaluate high-speed link signal
integrity and margins by measuring timing and voltage steps for each physical
lane and receiver independently.

This series introduces kernel driver support, debugfs controls, and a
kselftest automation script for PCIe Lane Margining at Receiver (LMR/LMT).

==============================================================================
1. How to Enable & Configure
==============================================================================
Enable the Kconfig option under PCI support:
  CONFIG_PCIE_LMR=y (or =m)
  (Depends on CONFIG_PCI and CONFIG_DEBUG_FS)

Upon boot or device hotplug on Gen4+ links (>= 16.0 GT/s), the driver probes
Extended Capability ID 0x27 and exposes per-device debugfs interfaces:
  /sys/kernel/debug/pci/pcie_lmr_<domain>:<bus>:<dev>.<func>/

==============================================================================
2. How to Use the Debugfs Interface (Manual Margining)
==============================================================================
Inspect device-wide margining capabilities and port status:
  # Inspect root device LMR capabilities & status
  cat /sys/kernel/debug/pci/pcie_lmr_0000:01:00.0/capabilities
  cat /sys/kernel/debug/pci/pcie_lmr_0000:01:00.0/port_status

Enable active Lane Margining on the device:
  # Enable Lane Margining state machine
  echo 1 > /sys/kernel/debug/pci/pcie_lmr_0000:01:00.0/enable

Inspect and step individual lanes (e.g. lane0):
  # Select target receiver (0 = local receiver, 1..6 = retimers/link partners)
  echo 0 > /sys/kernel/debug/pci/pcie_lmr_0000:01:00.0/lane0/receiver

  # Check available timing and voltage steps for this receiver
  cat /sys/kernel/debug/pci/pcie_lmr_0000:01:00.0/lane0/caps
  cat /sys/kernel/debug/pci/pcie_lmr_0000:01:00.0/lane0/num_timing_steps
  cat /sys/kernel/debug/pci/pcie_lmr_0000:01:00.0/lane0/num_voltage_steps

  # Step timing margin or voltage margin offset
  echo 2 > /sys/kernel/debug/pci/pcie_lmr_0000:01:00.0/lane0/margin_timing
  echo 1 > /sys/kernel/debug/pci/pcie_lmr_0000:01:00.0/lane0/margin_voltage

  # Reset margin offset back to nominal (0)
  echo 0 > /sys/kernel/debug/pci/pcie_lmr_0000:01:00.0/lane0/margin_timing
  echo 0 > /sys/kernel/debug/pci/pcie_lmr_0000:01:00.0/lane0/margin_voltage

Disable Lane Margining when finished:
  echo 0 > /sys/kernel/debug/pci/pcie_lmr_0000:01:00.0/enable

==============================================================================
3. How to Run Automated Kselftests Using the Test Script
==============================================================================
An automated kselftest script is included to test capability reads, receiver
selection, and margining commands across all enumerated LMR devices:

  # Run directly as root
  sudo ./tools/testing/selftests/pcie_lmt/pcie_lmt.sh

Or run via the kselftest Makefile harness:
  make -C tools/testing/selftests TARGETS=pcie_lmt run_tests

Sample script output on an LMR-capable device:
  pcie_lmt: testing PCIe LMR debugfs entries
  pcie_lmt: probing device pcie_lmr_0000:01:00.0
    pcie_lmr_0000:01:00.0: capabilities read OK
    pcie_lmr_0000:01:00.0: port_status read OK
    pcie_lmr_0000:01:00.0: margining enabled OK
    pcie_lmr_0000:01:00.0: testing lane0
    pcie_lmr_0000:01:00.0: testing lane1
    pcie_lmr_0000:01:00.0: margining disabled OK
  pcie_lmt [PASS]

To: Bjorn Helgaas <bhelgaas@google.com>
To: Shuah Khan <shuah@kernel.org>
Cc: linux-kernel@vger.kernel.org
Cc: linux-pci@vger.kernel.org
Cc: linux-kselftest@vger.kernel.org
Signed-off-by: Priyank Rathod <rathodpriyank@google.com>

Changes in v2:
  - Fixed NO_CMD (0x7) clearing in pci_lmr_run_cmd() before issuing new commands per PCIe r6.0 sec 8.4.4.
  - Protected plane->rx updates with mdev->lock in margin_lane_receiver_write().
  - Corrected Margining Port Capabilities bit definition to PCI_LMR_PORT_CAP_USES_SW_READY (0x0001) in <uapi/linux/pci_regs.h>.
  - Updated kselftest script (pcie_lmt.sh) to locate LMR debugfs entries.
  - Validated integer bounds against LMR_MAX_TIMING_STEP / LMR_MAX_VOLTAGE_STEP before narrowing u8 cast.
  - Moved mdev->enabled checks inside mutex_lock(&mdev->lock) to eliminate TOCTOU races.
  - Checked return values of all pci_read_config_word() calls, propagating -EIO on failure.
  - Eliminated dead store of cap in margin_enable_write().
  - Explicitly checked speed == PCIE_SPEED_64_0GT in pci_lmr_init() to avoid misidentifying PCI_SPEED_UNKNOWN (0xFF) as Gen6.
---
 drivers/pci/pci-driver.c                     |   1 +
 drivers/pci/pci.h                            |  10 +
 drivers/pci/pcie/Kconfig                     |  12 +
 drivers/pci/pcie/Makefile                    |   1 +
 drivers/pci/pcie/margin.c                    | 890 +++++++++++++++++++++++++++
 drivers/pci/probe.c                          |   1 +
 drivers/pci/remove.c                         |   1 +
 include/linux/pci.h                          |   6 +
 include/uapi/linux/pci_regs.h                |  18 +
 tools/testing/selftests/Makefile             |   1 +
 tools/testing/selftests/pcie_lmt/Makefile    |   3 +
 tools/testing/selftests/pcie_lmt/pcie_lmt.sh | 105 ++++
 12 files changed, 1049 insertions(+)

diff --git a/drivers/pci/pci-driver.c b/drivers/pci/pci-driver.c
index f36778e62ac1..17544a7023fc 100644
--- a/drivers/pci/pci-driver.c
+++ b/drivers/pci/pci-driver.c
@@ -821,6 +821,7 @@ static int pci_pm_suspend(struct device *dev)
 	 * since Coffee Lake, to enter a lower-power PM state.
 	 */
 	pci_suspend_ptm(pci_dev);
+	pci_suspend_lmr(pci_dev);
 
 	if (pci_has_legacy_pm_support(pci_dev))
 		return pci_legacy_suspend(dev, PMSG_SUSPEND);
diff --git a/drivers/pci/pci.h b/drivers/pci/pci.h
index 4469e1a77f3c..1f5fcc806f22 100644
--- a/drivers/pci/pci.h
+++ b/drivers/pci/pci.h
@@ -1023,6 +1023,16 @@ static inline void pci_no_tph(void) { }
 static inline void pci_tph_init(struct pci_dev *dev) { }
 #endif
 
+#ifdef CONFIG_PCIE_LMR
+void pci_lmr_init(struct pci_dev *dev);
+void pci_lmr_exit(struct pci_dev *dev);
+void pci_suspend_lmr(struct pci_dev *dev);
+#else
+static inline void pci_lmr_init(struct pci_dev *dev) { }
+static inline void pci_lmr_exit(struct pci_dev *dev) { }
+static inline void pci_suspend_lmr(struct pci_dev *dev) { }
+#endif
+
 #ifdef CONFIG_PCIE_PTM
 void pci_ptm_init(struct pci_dev *dev);
 void pci_save_ptm_state(struct pci_dev *dev);
diff --git a/drivers/pci/pcie/Kconfig b/drivers/pci/pcie/Kconfig
index 207c2deae35f..3b021ca2fe84 100644
--- a/drivers/pci/pcie/Kconfig
+++ b/drivers/pci/pcie/Kconfig
@@ -137,6 +137,18 @@ config PCIE_PTM
 	  This is only useful if you have devices that support PTM, but it
 	  is safe to enable even if you don't.
 
+config PCIE_LMR
+	bool "PCI Express Lane Margining at Receiver Support"
+	depends on DEBUG_FS
+	help
+	  This enables the PCI Express Lane Margining at Receiver support.
+	  Lane Margining allows software to determine the voltage and
+	  timing margin of each lane on a PCIe link (16.0 GT/s and above).
+	  The margining data is exposed via debugfs.
+
+	  This is only useful if you have devices that support lane
+	  margining, but it is safe to enable even if you don't.
+
 config PCIE_EDR
 	bool "PCI Express Error Disconnect Recover support"
 	depends on PCIE_DPC && ACPI
diff --git a/drivers/pci/pcie/Makefile b/drivers/pci/pcie/Makefile
index b0b43a18c304..aac45ae0402e 100644
--- a/drivers/pci/pcie/Makefile
+++ b/drivers/pci/pcie/Makefile
@@ -13,4 +13,5 @@ obj-$(CONFIG_PCIEAER_INJECT)	+= aer_inject.o
 obj-$(CONFIG_PCIE_PME)		+= pme.o
 obj-$(CONFIG_PCIE_DPC)		+= dpc.o
 obj-$(CONFIG_PCIE_PTM)		+= ptm.o
+obj-$(CONFIG_PCIE_LMR)		+= margin.o
 obj-$(CONFIG_PCIE_EDR)		+= edr.o
diff --git a/drivers/pci/pcie/margin.c b/drivers/pci/pcie/margin.c
new file mode 100644
index 000000000000..4124bde1490e
--- /dev/null
+++ b/drivers/pci/pcie/margin.c
@@ -0,0 +1,890 @@
+// SPDX-License-Identifier: GPL-2.0
+/*
+ * PCI Express Lane Margining at Receiver
+ *
+ * Copyright (C) 2026 Google LLC
+ * Author: Priyank Rathod <rathodpriyank@google.com>
+ *
+ * Lane Margining at Receiver (PCIe Base Specification r6.0, sec 8.4.4)
+ * allows system software to determine the voltage and timing margins of
+ * each physical lane on a PCIe link. The Extended Capability (ID 0x27)
+ * is available for receivers operating at 16.0 GT/s (Gen4) or higher data
+ * rates, and is mandatory for receivers operating at 64.0 GT/s (Gen6) or
+ * higher data rates.
+ *
+ * This driver implements:
+ *   - Probing Extended Capability ID 0x27 and Margining Port Capabilities.
+ *   - Managing ASPM L0s/L1 link states during active margining per spec.
+ *   - PCIe r6.0 NO_CMD (0x7) clearing handshake per receiver and lane.
+ *   - Caching receiver capabilities & step counts to avoid DEMARGIN side-effects.
+ *   - Handling Symmetric vs Independent Left/Right & Up/Down margin steps.
+ *   - Exposing per-device debugfs interfaces under /sys/kernel/debug/pci/.
+ */
+
+#include <linux/pci.h>
+#include <linux/slab.h>
+#include <linux/mutex.h>
+#include <linux/delay.h>
+#include <linux/jiffies.h>
+#include <linux/seq_file.h>
+#include <linux/bitfield.h>
+#include <linux/debugfs.h>
+
+#include "../pci.h"
+
+/* Margin type encodings per PCIe Base Spec r6.0 sec 8.4.4 */
+#define LMR_TYPE_DEMARGIN               0x0
+#define LMR_TYPE_REPORT_CAPS            0x1
+#define LMR_TYPE_REPORT_VOLTAGE_STEPS   0x2
+#define LMR_TYPE_REPORT_TIMING_STEPS    0x3
+#define LMR_TYPE_TIMING                 0x4
+#define LMR_TYPE_VOLTAGE                0x5
+#define LMR_TYPE_NO_CMD                 0x7
+
+/* LMR command timing parameters */
+#define LMR_CMD_TIMEOUT_MS              150
+#define LMR_CMD_SLEEP_MIN_US            100
+#define LMR_CMD_SLEEP_MAX_US            250
+#define LMR_ENABLE_TIMEOUT_MS           150
+#define LMR_ENABLE_SLEEP_MIN_US         1000
+#define LMR_ENABLE_SLEEP_MAX_US         2000
+
+/* LMR limits */
+#define LMR_MAX_LANES                   32
+#define LMR_MAX_RX_NUM                  6
+#define LMR_MAX_TIMING_STEP             63
+#define LMR_MAX_VOLTAGE_STEP            127
+
+/* LMR PCIe generation numbers */
+#define LMR_GEN6                        6
+#define LMR_GEN5                        5
+#define LMR_GEN4                        4
+
+/* LMR lane register stride */
+#define LMR_LANE_REG_STRIDE             4
+
+/* LMR receivers and directions */
+#define LMR_RX_LOCAL                    0
+#define LMR_STEP_DIR_INCREASE           1
+#define LMR_STEP_DIR_DECREASE           0
+
+/* LMR step & direction encoding masks */
+#define LMR_STEPS_MASK                  0x7F
+#define LMR_TIMING_STEP_MASK            0x3F
+#define LMR_TIMING_DIR_SHIFT            6
+#define LMR_VOLTAGE_STEP_MASK           0x7F
+#define LMR_VOLTAGE_DIR_SHIFT           7
+
+/* LMR capabilities report bit fields */
+#define LMR_CAP_MARGIN_HV               BIT(0)
+#define LMR_CAP_MARGIN_EV_IV            BIT(1)
+#define LMR_CAP_IND_LEFT_RIGHT_TIMING   BIT(2)
+#define LMR_CAP_IND_UP_DOWN_VOLTAGE     BIT(3)
+#define LMR_CAP_ERROR_SAMPLER           BIT(4)
+#define LMR_CAP_SAMPLE_MULTIPLE_RX      BIT(5)
+
+struct pci_margin_rx_info {
+	bool caps_cached;
+	u8 caps;
+	u8 num_timing_steps;
+	u8 num_voltage_steps;
+};
+
+struct pci_margin_lane {
+	struct pci_margin_dev *mdev;
+	int lane;
+	u8 rx;
+	int timing_val;
+	int voltage_val;
+	struct pci_margin_rx_info rx_info[LMR_MAX_RX_NUM + 1];
+};
+
+struct pci_margin_dev {
+	struct pci_dev *dev;
+	u16 cap;
+	struct dentry *debugfs;
+	struct mutex lock;
+	int num_lanes;
+	struct pci_margin_lane *lanes;
+	bool enabled;
+};
+
+#if IS_ENABLED(CONFIG_DEBUG_FS)
+static struct dentry *pci_debugfs_root_dir;
+
+static struct dentry *get_pci_debugfs_root(void)
+{
+	if (!pci_debugfs_root_dir)
+		pci_debugfs_root_dir = debugfs_lookup("pci", NULL);
+	if (!pci_debugfs_root_dir)
+		pci_debugfs_root_dir = debugfs_create_dir("pci", NULL);
+	return pci_debugfs_root_dir;
+}
+#endif
+
+/*
+ * pci_lmr_run_cmd() - Issue LMR command to Lane Control and wait for Status.
+ * Must be called with mdev->lock held.
+ */
+static int pci_lmr_run_cmd(struct pci_dev *dev, int lane, u8 rx, u8 type,
+			   u8 usage, u8 payload, u16 *status_val)
+{
+	u16 lmr = dev->lmr_cap;
+	u16 ctrl_offset = lmr + PCI_LMR_LANE_CTRL + LMR_LANE_REG_STRIDE * lane;
+	u16 sts_offset = lmr + PCI_LMR_LANE_STS + LMR_LANE_REG_STRIDE * lane;
+	u16 ctrl, sts;
+	unsigned long timeout;
+
+	/*
+	 * Per PCIe Base Spec r6.0 sec 8.4.4, software must issue NO_CMD (0x7)
+	 * targeting the specific receiver (rx) to clear MTYPE in Lane Status
+	 * before issuing a subsequent command.
+	 */
+	if (type != LMR_TYPE_NO_CMD) {
+		ctrl = FIELD_PREP(PCI_LMR_LANE_CTRL_RX_NUM, rx) |
+		       FIELD_PREP(PCI_LMR_LANE_CTRL_MTYPE, LMR_TYPE_NO_CMD) |
+		       FIELD_PREP(PCI_LMR_LANE_CTRL_USAGE, 0) |
+		       FIELD_PREP(PCI_LMR_LANE_CTRL_PAYLOAD, 0);
+
+		pci_write_config_word(dev, ctrl_offset, ctrl);
+
+		timeout = jiffies + msecs_to_jiffies(LMR_CMD_TIMEOUT_MS);
+		while (1) {
+			if (pci_read_config_word(dev, sts_offset, &sts))
+				return -EIO;
+			if (sts == 0xFFFF)
+				return -ENODEV;
+			if (FIELD_GET(PCI_LMR_LANE_STS_MTYPE, sts) == LMR_TYPE_NO_CMD &&
+			    FIELD_GET(PCI_LMR_LANE_STS_RX_NUM, sts) == rx)
+				break;
+			if (time_after(jiffies, timeout))
+				return -ETIMEDOUT;
+			usleep_range(LMR_CMD_SLEEP_MIN_US, LMR_CMD_SLEEP_MAX_US);
+		}
+	}
+
+	ctrl = FIELD_PREP(PCI_LMR_LANE_CTRL_RX_NUM, rx) |
+	       FIELD_PREP(PCI_LMR_LANE_CTRL_MTYPE, type) |
+	       FIELD_PREP(PCI_LMR_LANE_CTRL_USAGE, usage) |
+	       FIELD_PREP(PCI_LMR_LANE_CTRL_PAYLOAD, payload);
+
+	pci_write_config_word(dev, ctrl_offset, ctrl);
+
+	timeout = jiffies + msecs_to_jiffies(LMR_CMD_TIMEOUT_MS);
+	while (1) {
+		if (pci_read_config_word(dev, sts_offset, &sts))
+			return -EIO;
+		if (sts == 0xFFFF)
+			return -ENODEV;
+
+		if (FIELD_GET(PCI_LMR_LANE_STS_MTYPE, sts) == type &&
+		    FIELD_GET(PCI_LMR_LANE_STS_RX_NUM, sts) == rx) {
+			if (status_val)
+				*status_val = sts;
+			return 0;
+		}
+
+		if (time_after(jiffies, timeout))
+			break;
+
+		usleep_range(LMR_CMD_SLEEP_MIN_US, LMR_CMD_SLEEP_MAX_US);
+	}
+
+	return -ETIMEDOUT;
+}
+
+static int pci_lmr_demargin_lane(struct pci_margin_lane *plane)
+{
+	u16 sts;
+	int ret;
+
+	if (plane->timing_val == 0 && plane->voltage_val == 0)
+		return 0;
+
+	ret = pci_lmr_run_cmd(plane->mdev->dev, plane->lane, plane->rx,
+			      LMR_TYPE_DEMARGIN, 0, 0, &sts);
+	if (!ret) {
+		plane->timing_val = 0;
+		plane->voltage_val = 0;
+	}
+	return ret;
+}
+
+static int pci_lmr_cache_rx_info(struct pci_margin_lane *plane, u8 rx)
+{
+	struct pci_margin_rx_info *info = &plane->rx_info[rx];
+	u16 sts;
+	int ret;
+
+	if (info->caps_cached)
+		return 0;
+
+	/* Issuing REPORT_CAPS aborts any active margin per PCIe spec */
+	pci_lmr_demargin_lane(plane);
+
+	ret = pci_lmr_run_cmd(plane->mdev->dev, plane->lane, rx,
+			      LMR_TYPE_REPORT_CAPS, 0, 0, &sts);
+	if (ret)
+		return ret;
+	info->caps = FIELD_GET(PCI_LMR_LANE_STS_PAYLOAD, sts);
+
+	ret = pci_lmr_run_cmd(plane->mdev->dev, plane->lane, rx,
+			      LMR_TYPE_REPORT_TIMING_STEPS, 0, 0, &sts);
+	if (ret)
+		return ret;
+	info->num_timing_steps = FIELD_GET(PCI_LMR_LANE_STS_PAYLOAD, sts) & LMR_STEPS_MASK;
+
+	ret = pci_lmr_run_cmd(plane->mdev->dev, plane->lane, rx,
+			      LMR_TYPE_REPORT_VOLTAGE_STEPS, 0, 0, &sts);
+	if (ret)
+		return ret;
+	info->num_voltage_steps = FIELD_GET(PCI_LMR_LANE_STS_PAYLOAD, sts) & LMR_STEPS_MASK;
+
+	info->caps_cached = true;
+	return 0;
+}
+
+#if IS_ENABLED(CONFIG_DEBUG_FS)
+
+static int margin_caps_show(struct seq_file *s, void *v)
+{
+	struct pci_margin_dev *mdev = s->private;
+	struct pci_dev *dev = mdev->dev;
+	u16 cap;
+
+	if (pci_read_config_word(dev, mdev->cap + PCI_LMR_PORT_CAP, &cap))
+		return -EIO;
+	seq_printf(s, "Port Capabilities: %#06x\n", cap);
+	seq_printf(s, "  Uses SW Ready: %s\n",
+		   (cap & PCI_LMR_PORT_CAP_USES_SW_READY) ? "yes" : "no");
+	return 0;
+}
+DEFINE_SHOW_ATTRIBUTE(margin_caps);
+
+static int margin_port_status_show(struct seq_file *s, void *v)
+{
+	struct pci_margin_dev *mdev = s->private;
+	struct pci_dev *dev = mdev->dev;
+	u16 sts;
+
+	if (pci_read_config_word(dev, mdev->cap + PCI_LMR_PORT_STS, &sts))
+		return -EIO;
+	seq_printf(s, "Port Status: %#06x\n", sts);
+	seq_printf(s, "  Margining Ready: %s\n",
+		   (sts & PCI_LMR_PORT_STS_MARGIN_READY) ? "yes" : "no");
+	seq_printf(s, "  SW Ready: %s\n", (sts & PCI_LMR_PORT_STS_SW_READY) ? "yes" : "no");
+	return 0;
+}
+DEFINE_SHOW_ATTRIBUTE(margin_port_status);
+
+static int margin_enable_show(struct seq_file *s, void *v)
+{
+	struct pci_margin_dev *mdev = s->private;
+	bool enabled;
+
+	mutex_lock(&mdev->lock);
+	enabled = mdev->enabled;
+	mutex_unlock(&mdev->lock);
+
+	seq_printf(s, "%d\n", enabled);
+	return 0;
+}
+
+static ssize_t margin_enable_write(struct file *file, const char __user *user_buf,
+			       size_t count, loff_t *ppos)
+{
+	struct pci_margin_dev *mdev = ((struct seq_file *)file->private_data)->private;
+	struct pci_dev *dev = mdev->dev;
+	unsigned long timeout;
+	bool enable;
+	int ret, i;
+	u16 sts, cap;
+
+	ret = kstrtobool_from_user(user_buf, count, &enable);
+	if (ret)
+		return ret;
+
+	mutex_lock(&mdev->lock);
+
+	if (mdev->enabled == enable)
+		goto out;
+
+	if (enable) {
+		if (pci_read_config_word(dev, mdev->cap + PCI_LMR_PORT_CAP, &cap)) {
+			ret = -EIO;
+			goto out;
+		}
+
+		/*
+		 * PCIe Base Spec r6.0: Link must be maintained in L0 during
+		 * active margining. Disable ASPM L0s/L1.
+		 */
+		pci_disable_link_state(dev, PCIE_LINK_STATE_L0S | PCIE_LINK_STATE_L1);
+
+		if (cap & PCI_LMR_PORT_CAP_USES_SW_READY) {
+			if (pci_read_config_word(dev, mdev->cap + PCI_LMR_PORT_STS, &sts)) {
+				ret = -EIO;
+				goto out;
+			}
+			sts |= PCI_LMR_PORT_STS_SW_READY;
+			pci_write_config_word(dev, mdev->cap + PCI_LMR_PORT_STS, sts);
+		}
+
+		timeout = jiffies + msecs_to_jiffies(LMR_ENABLE_TIMEOUT_MS);
+		while (1) {
+			if (pci_read_config_word(dev, mdev->cap + PCI_LMR_PORT_STS, &sts)) {
+				ret = -EIO;
+				goto out;
+			}
+			if (sts == 0xFFFF) {
+				ret = -ENODEV;
+				goto out;
+			}
+			if (sts & PCI_LMR_PORT_STS_MARGIN_READY)
+				break;
+			if (time_after(jiffies, timeout)) {
+				ret = -ETIMEDOUT;
+				if (cap & PCI_LMR_PORT_CAP_USES_SW_READY) {
+					sts &= ~PCI_LMR_PORT_STS_SW_READY;
+					pci_write_config_word(dev,
+							      mdev->cap + PCI_LMR_PORT_STS,
+							      sts);
+				}
+				goto out;
+			}
+			usleep_range(LMR_ENABLE_SLEEP_MIN_US, LMR_ENABLE_SLEEP_MAX_US);
+		}
+
+		/* Cache capabilities for local receiver on all lanes */
+		for (i = 0; i < mdev->num_lanes; i++) {
+			mdev->lanes[i].rx = LMR_RX_LOCAL;
+			pci_lmr_cache_rx_info(&mdev->lanes[i], LMR_RX_LOCAL);
+		}
+		mdev->enabled = true;
+	} else {
+		/* Demargin all lanes before clearing SW_READY */
+		for (i = 0; i < mdev->num_lanes; i++)
+			pci_lmr_demargin_lane(&mdev->lanes[i]);
+
+		if (pci_read_config_word(dev, mdev->cap + PCI_LMR_PORT_STS, &sts) == 0) {
+			sts &= ~PCI_LMR_PORT_STS_SW_READY;
+			pci_write_config_word(dev, mdev->cap + PCI_LMR_PORT_STS, sts);
+		}
+		mdev->enabled = false;
+	}
+
+out:
+	mutex_unlock(&mdev->lock);
+	return ret ? ret : count;
+}
+
+static int margin_enable_open(struct inode *inode, struct file *file)
+{
+	return single_open(file, margin_enable_show, inode->i_private);
+}
+
+static const struct file_operations margin_enable_fops = {
+	.open = margin_enable_open,
+	.read = seq_read,
+	.write = margin_enable_write,
+	.llseek = seq_lseek,
+	.release = single_release,
+};
+
+static int margin_lane_receiver_show(struct seq_file *s, void *v)
+{
+	struct pci_margin_lane *plane = s->private;
+	int rx;
+
+	mutex_lock(&plane->mdev->lock);
+	rx = plane->rx;
+	mutex_unlock(&plane->mdev->lock);
+
+	seq_printf(s, "%d\n", rx);
+	return 0;
+}
+
+static ssize_t margin_lane_receiver_write(struct file *file, const char __user *user_buf,
+				      size_t count, loff_t *ppos)
+{
+	struct pci_margin_lane *plane = ((struct seq_file *)file->private_data)->private;
+	struct pci_margin_dev *mdev = plane->mdev;
+	int ret;
+	int rx;
+
+	ret = kstrtoint_from_user(user_buf, count, 0, &rx);
+	if (ret)
+		return ret;
+
+	if (rx < 0 || rx > LMR_MAX_RX_NUM)
+		return -EINVAL;
+
+	mutex_lock(&mdev->lock);
+	if (plane->rx == rx)
+		goto out;
+
+	if (mdev->enabled) {
+		/* Demargin previous receiver per single-receiver spec rule */
+		pci_lmr_demargin_lane(plane);
+		ret = pci_lmr_cache_rx_info(plane, rx);
+		if (ret)
+			goto out;
+	}
+
+	plane->rx = rx;
+out:
+	mutex_unlock(&mdev->lock);
+	return ret ? ret : count;
+}
+
+static int margin_lane_receiver_open(struct inode *inode, struct file *file)
+{
+	return single_open(file, margin_lane_receiver_show, inode->i_private);
+}
+
+static const struct file_operations margin_lane_receiver_fops = {
+	.open = margin_lane_receiver_open,
+	.read = seq_read,
+	.write = margin_lane_receiver_write,
+	.llseek = seq_lseek,
+	.release = single_release,
+};
+
+static int margin_lane_caps_show(struct seq_file *s, void *v)
+{
+	struct pci_margin_lane *plane = s->private;
+	struct pci_margin_dev *mdev = plane->mdev;
+	struct pci_margin_rx_info *info;
+	int ret = 0;
+	u8 val;
+
+	mutex_lock(&mdev->lock);
+	if (!mdev->enabled) {
+		ret = -EACCES;
+		goto out;
+	}
+
+	ret = pci_lmr_cache_rx_info(plane, plane->rx);
+	if (ret)
+		goto out;
+
+	info = &plane->rx_info[plane->rx];
+	val = info->caps;
+	seq_printf(s, "Lane %d Rx %d Capabilities: %#02x\n", plane->lane, plane->rx, val);
+	seq_printf(s, "  Margining H/V: %s\n", (val & LMR_CAP_MARGIN_HV) ? "both" : "either");
+	seq_printf(s, "  Margining eV/iV: %s\n",
+		   (val & LMR_CAP_MARGIN_EV_IV) ? "both (close & open)" : "one (close only)");
+	seq_printf(s, "  Left/Right: %s\n", (val & LMR_CAP_IND_LEFT_RIGHT_TIMING) ? "both" : "one");
+	seq_printf(s, "  Up/Down: %s\n", (val & LMR_CAP_IND_UP_DOWN_VOLTAGE) ? "both" : "one");
+	seq_printf(s, "  Error Sampler: %s\n",
+		   (val & LMR_CAP_ERROR_SAMPLER) ? "yes" : "no (main sampler)");
+	seq_printf(s, "  Sample Multiple Receivers: %s\n",
+		   (val & LMR_CAP_SAMPLE_MULTIPLE_RX) ? "yes" : "no");
+out:
+	mutex_unlock(&mdev->lock);
+	return ret;
+}
+DEFINE_SHOW_ATTRIBUTE(margin_lane_caps);
+
+static int margin_lane_timing_steps_show(struct seq_file *s, void *v)
+{
+	struct pci_margin_lane *plane = s->private;
+	struct pci_margin_dev *mdev = plane->mdev;
+	struct pci_margin_rx_info *info;
+	int ret = 0;
+
+	mutex_lock(&mdev->lock);
+	if (!mdev->enabled) {
+		ret = -EACCES;
+		goto out;
+	}
+
+	ret = pci_lmr_cache_rx_info(plane, plane->rx);
+	if (!ret) {
+		info = &plane->rx_info[plane->rx];
+		seq_printf(s, "%d\n", info->num_timing_steps);
+	}
+out:
+	mutex_unlock(&mdev->lock);
+	return ret;
+}
+DEFINE_SHOW_ATTRIBUTE(margin_lane_timing_steps);
+
+static int margin_lane_voltage_steps_show(struct seq_file *s, void *v)
+{
+	struct pci_margin_lane *plane = s->private;
+	struct pci_margin_dev *mdev = plane->mdev;
+	struct pci_margin_rx_info *info;
+	int ret = 0;
+
+	mutex_lock(&mdev->lock);
+	if (!mdev->enabled) {
+		ret = -EACCES;
+		goto out;
+	}
+
+	ret = pci_lmr_cache_rx_info(plane, plane->rx);
+	if (!ret) {
+		info = &plane->rx_info[plane->rx];
+		seq_printf(s, "%d\n", info->num_voltage_steps);
+	}
+out:
+	mutex_unlock(&mdev->lock);
+	return ret;
+}
+DEFINE_SHOW_ATTRIBUTE(margin_lane_voltage_steps);
+
+static ssize_t margin_lane_timing_write(struct file *file, const char __user *user_buf,
+				    size_t count, loff_t *ppos)
+{
+	struct pci_margin_lane *plane = ((struct seq_file *)file->private_data)->private;
+	struct pci_margin_dev *mdev = plane->mdev;
+	struct pci_margin_rx_info *info;
+	int val, ret;
+	u8 step, dir;
+	u16 sts;
+	u8 caps;
+
+	ret = kstrtoint_from_user(user_buf, count, 0, &val);
+	if (ret)
+		return ret;
+
+	if (val > LMR_MAX_TIMING_STEP || val < -LMR_MAX_TIMING_STEP)
+		return -EINVAL;
+
+	mutex_lock(&mdev->lock);
+	if (!mdev->enabled) {
+		ret = -EACCES;
+		goto out;
+	}
+
+	if (val == 0) {
+		ret = pci_lmr_demargin_lane(plane);
+		goto out;
+	}
+
+	ret = pci_lmr_cache_rx_info(plane, plane->rx);
+	if (ret)
+		goto out;
+
+	info = &plane->rx_info[plane->rx];
+	caps = info->caps;
+
+	if (val < 0) {
+		step = -val;
+		if (!(caps & LMR_CAP_IND_LEFT_RIGHT_TIMING)) {
+			/* Symmetric margining requires dir=1 per spec */
+			ret = -EINVAL;
+			goto out;
+		}
+		dir = LMR_STEP_DIR_DECREASE;
+	} else {
+		step = val;
+		dir = LMR_STEP_DIR_INCREASE;
+	}
+
+	if (step > info->num_timing_steps) {
+		ret = -EINVAL;
+		goto out;
+	}
+
+	ret = pci_lmr_run_cmd(mdev->dev, plane->lane, plane->rx,
+			      LMR_TYPE_TIMING, 0,
+			      (step & LMR_TIMING_STEP_MASK) | (dir << LMR_TIMING_DIR_SHIFT), &sts);
+	if (ret)
+		goto out;
+
+	/* Record actual step count applied by hardware */
+	step = FIELD_GET(PCI_LMR_LANE_STS_PAYLOAD, sts) & LMR_TIMING_STEP_MASK;
+	plane->timing_val = (dir == LMR_STEP_DIR_DECREASE) ? -step : step;
+
+out:
+	mutex_unlock(&mdev->lock);
+	return ret ? ret : count;
+}
+
+static int margin_lane_timing_show(struct seq_file *s, void *v)
+{
+	struct pci_margin_lane *plane = s->private;
+	int timing_val;
+
+	mutex_lock(&plane->mdev->lock);
+	timing_val = plane->timing_val;
+	mutex_unlock(&plane->mdev->lock);
+
+	seq_printf(s, "%d\n", timing_val);
+	return 0;
+}
+
+static int margin_lane_timing_open(struct inode *inode, struct file *file)
+{
+	return single_open(file, margin_lane_timing_show, inode->i_private);
+}
+
+static const struct file_operations margin_lane_timing_fops = {
+	.open = margin_lane_timing_open,
+	.read = seq_read,
+	.write = margin_lane_timing_write,
+	.llseek = seq_lseek,
+	.release = single_release,
+};
+
+static ssize_t margin_lane_voltage_write(struct file *file, const char __user *user_buf,
+				     size_t count, loff_t *ppos)
+{
+	struct pci_margin_lane *plane = ((struct seq_file *)file->private_data)->private;
+	struct pci_margin_dev *mdev = plane->mdev;
+	struct pci_margin_rx_info *info;
+	int val, ret;
+	u8 step, dir;
+	u16 sts;
+	u8 caps;
+
+	ret = kstrtoint_from_user(user_buf, count, 0, &val);
+	if (ret)
+		return ret;
+
+	if (val > LMR_MAX_VOLTAGE_STEP || val < -LMR_MAX_VOLTAGE_STEP)
+		return -EINVAL;
+
+	mutex_lock(&mdev->lock);
+	if (!mdev->enabled) {
+		ret = -EACCES;
+		goto out;
+	}
+
+	if (val == 0) {
+		ret = pci_lmr_demargin_lane(plane);
+		goto out;
+	}
+
+	ret = pci_lmr_cache_rx_info(plane, plane->rx);
+	if (ret)
+		goto out;
+
+	info = &plane->rx_info[plane->rx];
+	caps = info->caps;
+
+	if (val < 0) {
+		step = -val;
+		if (!(caps & LMR_CAP_IND_UP_DOWN_VOLTAGE)) {
+			/* Symmetric voltage margining requires dir=1 per spec */
+			ret = -EINVAL;
+			goto out;
+		}
+		dir = 0;
+	} else {
+		step = val;
+		dir = 1;
+	}
+
+	if (step > info->num_voltage_steps) {
+		ret = -EINVAL;
+		goto out;
+	}
+
+	ret = pci_lmr_run_cmd(mdev->dev, plane->lane, plane->rx,
+			      LMR_TYPE_VOLTAGE, 0,
+			      (step & LMR_VOLTAGE_STEP_MASK) |
+			      (dir << LMR_VOLTAGE_DIR_SHIFT), &sts);
+	if (ret)
+		goto out;
+
+	step = FIELD_GET(PCI_LMR_LANE_STS_PAYLOAD, sts) & LMR_VOLTAGE_STEP_MASK;
+	plane->voltage_val = (dir == 0) ? -step : step;
+
+out:
+	mutex_unlock(&mdev->lock);
+	return ret ? ret : count;
+}
+
+static int margin_lane_voltage_show(struct seq_file *s, void *v)
+{
+	struct pci_margin_lane *plane = s->private;
+	int voltage_val;
+
+	mutex_lock(&plane->mdev->lock);
+	voltage_val = plane->voltage_val;
+	mutex_unlock(&plane->mdev->lock);
+
+	seq_printf(s, "%d\n", voltage_val);
+	return 0;
+}
+
+static int margin_lane_voltage_open(struct inode *inode, struct file *file)
+{
+	return single_open(file, margin_lane_voltage_show, inode->i_private);
+}
+
+static const struct file_operations margin_lane_voltage_fops = {
+	.open = margin_lane_voltage_open,
+	.read = seq_read,
+	.write = margin_lane_voltage_write,
+	.llseek = seq_lseek,
+	.release = single_release,
+};
+
+static void pci_margin_debugfs_init(struct pci_margin_dev *mdev)
+{
+	struct pci_dev *dev = mdev->dev;
+	struct dentry *parent;
+	char dirname[64];
+	int i;
+
+	parent = get_pci_debugfs_root();
+	snprintf(dirname, sizeof(dirname), "pcie_lmr_%s", dev_name(&dev->dev));
+	mdev->debugfs = debugfs_create_dir(dirname, parent);
+	if (!mdev->debugfs)
+		return;
+
+	debugfs_create_file("capabilities", 0444, mdev->debugfs, mdev, &margin_caps_fops);
+	debugfs_create_file("port_status", 0444, mdev->debugfs, mdev, &margin_port_status_fops);
+	debugfs_create_file("enable", 0644, mdev->debugfs, mdev, &margin_enable_fops);
+
+	for (i = 0; i < mdev->num_lanes; i++) {
+		struct pci_margin_lane *plane = &mdev->lanes[i];
+		struct dentry *lane_dir;
+		char lane_name[16];
+
+		snprintf(lane_name, sizeof(lane_name), "lane%d", i);
+		lane_dir = debugfs_create_dir(lane_name, mdev->debugfs);
+		if (!lane_dir)
+			continue;
+
+		debugfs_create_file("receiver", 0644, lane_dir, plane, &margin_lane_receiver_fops);
+		debugfs_create_file("caps", 0444, lane_dir, plane, &margin_lane_caps_fops);
+		debugfs_create_file("num_timing_steps", 0444, lane_dir, plane,
+				    &margin_lane_timing_steps_fops);
+		debugfs_create_file("num_voltage_steps", 0444, lane_dir, plane,
+				    &margin_lane_voltage_steps_fops);
+		debugfs_create_file("margin_timing", 0644, lane_dir, plane,
+				    &margin_lane_timing_fops);
+		debugfs_create_file("margin_voltage", 0644, lane_dir, plane,
+				    &margin_lane_voltage_fops);
+	}
+}
+
+static void pci_margin_debugfs_remove(struct pci_margin_dev *mdev)
+{
+	debugfs_remove_recursive(mdev->debugfs);
+}
+
+#else
+static inline void pci_margin_debugfs_init(struct pci_margin_dev *mdev) { }
+static inline void pci_margin_debugfs_remove(struct pci_margin_dev *mdev) { }
+#endif
+
+void pci_lmr_init(struct pci_dev *dev)
+{
+	struct pci_margin_dev *mdev;
+	enum pci_bus_speed speed;
+	u16 lmr;
+	u16 lnkcap;
+	int i;
+
+	if (!pci_is_pcie(dev))
+		return;
+
+	speed = pcie_get_speed_cap(dev);
+
+	lmr = pci_find_ext_capability(dev, PCI_EXT_CAP_ID_LMR);
+	if (!lmr) {
+		if (speed == PCIE_SPEED_64_0GT)
+			pci_warn(dev,
+				 "Missing Lane Margining at Receiver Capability (mandatory for Gen6+)\n");
+		else if (speed >= PCIE_SPEED_16_0GT && speed != PCI_SPEED_UNKNOWN)
+			pci_dbg(dev,
+				"Optional Lane Margining at Receiver Capability not found\n");
+		return;
+	}
+
+	if (speed < PCIE_SPEED_16_0GT && speed != PCI_SPEED_UNKNOWN)
+		return;
+
+	dev->lmr_cap = lmr;
+
+	mdev = kzalloc_obj(*mdev, GFP_KERNEL);
+	if (!mdev)
+		return;
+
+	mdev->dev = dev;
+	mdev->cap = lmr;
+	mutex_init(&mdev->lock);
+
+	pcie_capability_read_word(dev, PCI_EXP_LNKCAP, &lnkcap);
+	mdev->num_lanes = FIELD_GET(PCI_EXP_LNKCAP_MLW, lnkcap);
+
+	if (mdev->num_lanes == 0 || mdev->num_lanes > LMR_MAX_LANES) {
+		pci_warn(dev, "Invalid link width %d for LMR\n", mdev->num_lanes);
+		goto err_free_mdev;
+	}
+
+	mdev->lanes = kcalloc(mdev->num_lanes, sizeof(*mdev->lanes), GFP_KERNEL);
+	if (!mdev->lanes)
+		goto err_free_mdev;
+
+	for (i = 0; i < mdev->num_lanes; i++) {
+		mdev->lanes[i].mdev = mdev;
+		mdev->lanes[i].lane = i;
+		mdev->lanes[i].rx = LMR_RX_LOCAL;
+	}
+
+	pci_margin_debugfs_init(mdev);
+
+	dev->lmr = mdev;
+
+	pci_info(dev, "Lane Margining at Receiver (Gen%u) Capability detected\n",
+		 speed == PCIE_SPEED_64_0GT ? LMR_GEN6 :
+		 speed == PCIE_SPEED_32_0GT ? LMR_GEN5 :
+		 LMR_GEN4);
+	return;
+
+err_free_mdev:
+	mutex_destroy(&mdev->lock);
+	kfree(mdev);
+}
+
+void pci_lmr_exit(struct pci_dev *dev)
+{
+	struct pci_margin_dev *mdev = dev->lmr;
+	int i;
+
+	if (!mdev)
+		return;
+
+	mutex_lock(&mdev->lock);
+	if (mdev->enabled) {
+		for (i = 0; i < mdev->num_lanes; i++)
+			pci_lmr_demargin_lane(&mdev->lanes[i]);
+	}
+	mutex_unlock(&mdev->lock);
+
+	pci_margin_debugfs_remove(mdev);
+	mutex_destroy(&mdev->lock);
+	kfree(mdev->lanes);
+	kfree(mdev);
+	dev->lmr = NULL;
+}
+
+void pci_suspend_lmr(struct pci_dev *dev)
+{
+	struct pci_margin_dev *mdev = dev->lmr;
+	int i;
+	u16 sts;
+
+	if (!mdev)
+		return;
+
+	mutex_lock(&mdev->lock);
+	if (mdev->enabled) {
+		for (i = 0; i < mdev->num_lanes; i++)
+			pci_lmr_demargin_lane(&mdev->lanes[i]);
+
+		if (pci_read_config_word(dev, mdev->cap + PCI_LMR_PORT_STS, &sts) == 0) {
+			sts &= ~PCI_LMR_PORT_STS_SW_READY;
+			pci_write_config_word(dev, mdev->cap + PCI_LMR_PORT_STS, sts);
+		}
+		mdev->enabled = false;
+	}
+	mutex_unlock(&mdev->lock);
+}
diff --git a/drivers/pci/probe.c b/drivers/pci/probe.c
index dd0abbc63e18..352b95568ebf 100644
--- a/drivers/pci/probe.c
+++ b/drivers/pci/probe.c
@@ -2666,6 +2666,7 @@ static void pci_init_capabilities(struct pci_dev *dev)
 	pci_pasid_init(dev);		/* Process Address Space ID */
 	pci_acs_init(dev);		/* Access Control Services */
 	pci_ptm_init(dev);		/* Precision Time Measurement */
+	pci_lmr_init(dev);		/* Lane Margining at Receiver */
 	pci_aer_init(dev);		/* Advanced Error Reporting */
 	pci_dpc_init(dev);		/* Downstream Port Containment */
 	pci_rcec_init(dev);		/* Root Complex Event Collector */
diff --git a/drivers/pci/remove.c b/drivers/pci/remove.c
index d8bffa21498a..6fba29040e44 100644
--- a/drivers/pci/remove.c
+++ b/drivers/pci/remove.c
@@ -36,6 +36,7 @@ static void pci_destroy_dev(struct pci_dev *dev)
 
 	pci_doe_sysfs_teardown(dev);
 	pci_npem_remove(dev);
+	pci_lmr_exit(dev);
 
 	/*
 	 * While device is in D0 drop the device from TSM link operations
diff --git a/include/linux/pci.h b/include/linux/pci.h
index 64b308b6e61c..ef1275f4c5b6 100644
--- a/include/linux/pci.h
+++ b/include/linux/pci.h
@@ -349,6 +349,8 @@ struct rcec_ea;
  *			number resources to allow for hierarchy expansion.
  * @is_pciehp:		PCIe Hot-Plug Capable bridge.
  */
+struct pci_margin_dev;
+
 struct pci_dev {
 	struct list_head bus_list;	/* Node in per-bus list */
 	struct pci_bus	*bus;		/* Bus this device is on */
@@ -528,6 +530,10 @@ struct pci_dev {
 	atomic_t	ptm_enable_cnt;
 	u8		ptm_granularity;
 #endif
+#ifdef CONFIG_PCIE_LMR
+	u16			lmr_cap;	/* Lane Margining Capability */
+	struct pci_margin_dev	*lmr;
+#endif
 #ifdef CONFIG_PCI_MSI
 	void __iomem	*msix_base;
 	raw_spinlock_t	msi_lock;
diff --git a/include/uapi/linux/pci_regs.h b/include/uapi/linux/pci_regs.h
index facaa324bd86..90cbe310e62f 100644
--- a/include/uapi/linux/pci_regs.h
+++ b/include/uapi/linux/pci_regs.h
@@ -757,6 +757,7 @@
 #define PCI_EXT_CAP_ID_VF_REBAR 0x24	/* VF Resizable BAR */
 #define PCI_EXT_CAP_ID_DLF	0x25	/* Data Link Feature */
 #define PCI_EXT_CAP_ID_PL_16GT	0x26	/* Physical Layer 16.0 GT/s */
+#define PCI_EXT_CAP_ID_LMR	0x27	/* Lane Margining at Receiver */
 #define PCI_EXT_CAP_ID_NPEM	0x29	/* Native PCIe Enclosure Management */
 #define PCI_EXT_CAP_ID_PL_32GT  0x2A    /* Physical Layer 32.0 GT/s */
 #define PCI_EXT_CAP_ID_DOE	0x2E	/* Data Object Exchange */
@@ -1181,6 +1182,23 @@
 #define  PCI_PL_16GT_LE_CTRL_USP_TX_PRESET_MASK		0x000000F0
 #define  PCI_PL_16GT_LE_CTRL_USP_TX_PRESET_SHIFT	4
 
+/* Lane Margining at Receiver */
+#define PCI_LMR_PORT_CAP		0x04	/* Margining Port Capabilities */
+#define  PCI_LMR_PORT_CAP_USES_SW_READY	0x0001	/* Margining Uses Software Ready */
+#define PCI_LMR_PORT_STS		0x06	/* Margining Port Status */
+#define  PCI_LMR_PORT_STS_MARGIN_READY	0x0001	/* Margining Ready */
+#define  PCI_LMR_PORT_STS_SW_READY	0x0002	/* Margining SW Ready */
+#define PCI_LMR_LANE_CTRL		0x08	/* Margining Lane Control */
+#define  PCI_LMR_LANE_CTRL_RX_NUM	0x0007	/* Receiver Number */
+#define  PCI_LMR_LANE_CTRL_MTYPE	0x0038	/* Margining Type */
+#define  PCI_LMR_LANE_CTRL_USAGE	0x0040	/* Margining Usage Model */
+#define  PCI_LMR_LANE_CTRL_PAYLOAD	0xFF00	/* Margining Payload */
+#define PCI_LMR_LANE_STS		0x0A	/* Margining Lane Status */
+#define  PCI_LMR_LANE_STS_RX_NUM	0x0007	/* Receiver Number */
+#define  PCI_LMR_LANE_STS_MTYPE		0x0038	/* Margining Type */
+#define  PCI_LMR_LANE_STS_USAGE		0x0040	/* Margining Usage Model */
+#define  PCI_LMR_LANE_STS_PAYLOAD	0xFF00	/* Margining Payload */
+
 /* Physical Layer 32.0 GT/s */
 #define PCI_PL_32GT_LE_CTRL	0x20	/* Lane Equalization Control Register */
 
diff --git a/tools/testing/selftests/Makefile b/tools/testing/selftests/Makefile
index 8a4b6ddc68df..6990d999388a 100644
--- a/tools/testing/selftests/Makefile
+++ b/tools/testing/selftests/Makefile
@@ -91,6 +91,7 @@ TARGETS += net/tcp_ao
 TARGETS += nolibc
 TARGETS += pci_endpoint
 TARGETS += pcie_bwctrl
+TARGETS += pcie_lmt
 TARGETS += perf_events
 TARGETS += pidfd
 TARGETS += pid_namespace
diff --git a/tools/testing/selftests/pcie_lmt/Makefile b/tools/testing/selftests/pcie_lmt/Makefile
new file mode 100644
index 000000000000..36ac85937d78
--- /dev/null
+++ b/tools/testing/selftests/pcie_lmt/Makefile
@@ -0,0 +1,3 @@
+# SPDX-License-Identifier: GPL-2.0
+TEST_PROGS = pcie_lmt.sh
+include ../lib.mk
diff --git a/tools/testing/selftests/pcie_lmt/pcie_lmt.sh b/tools/testing/selftests/pcie_lmt/pcie_lmt.sh
new file mode 100755
index 000000000000..22c00c2b8956
--- /dev/null
+++ b/tools/testing/selftests/pcie_lmt/pcie_lmt.sh
@@ -0,0 +1,105 @@
+#!/bin/bash
+# SPDX-License-Identifier: GPL-2.0
+#
+# Copyright (C) 2026 Google LLC
+# Author: Priyank Rathod <rathodpriyank@google.com>
+#
+# Kselftest for PCIe Lane Margining at Receiver (LMR / LMT)
+# Tests the debugfs interface exposed by drivers/pci/pcie/margin.c
+# (/sys/kernel/debug/pci/pcie_lmr_<pci_dev_name>/)
+
+set -e
+
+TESTNAME="pcie_lmt"
+
+# Kselftest framework requirement - SKIP code is 4.
+ksft_skip=4
+retval=0
+skipmsg="skip all tests:"
+
+if [ $UID != 0 ]; then
+	echo "$skipmsg must be run as root" >&2
+	exit $ksft_skip
+fi
+
+DEBUGFS=$(mount -t debugfs | head -1 | awk '{ print $3 }')
+if [ -z "$DEBUGFS" ]; then
+	if [ -d "/sys/kernel/debug" ]; then
+		DEBUGFS="/sys/kernel/debug"
+	else
+		echo "$skipmsg debugfs is not mounted" >&2
+		exit $ksft_skip
+	fi
+fi
+
+if [ ! -d "$DEBUGFS/pci" ]; then
+	# Allow searching debugfs root or pci directory
+	:
+fi
+
+LMR_DEVS=$(ls -d $DEBUGFS/pci/pcie_lmr_* $DEBUGFS/pcie_lmr_* 2>/dev/null || true)
+if [ -z "$LMR_DEVS" ]; then
+	echo "$skipmsg no PCIe LMR devices found in $DEBUGFS/" >&2
+	exit $ksft_skip
+fi
+
+cleanup_dev()
+{
+	local dev="$1"
+	echo 0 > "$dev/enable" 2>/dev/null || true
+}
+
+echo "$TESTNAME: testing PCIe LMR debugfs entries"
+
+for dev in $LMR_DEVS; do
+	dev_name=$(basename "$dev")
+	echo "$TESTNAME: probing device $dev_name"
+
+	if [ ! -r "$dev/capabilities" ] || [ ! -r "$dev/port_status" ] ||
+	   [ ! -r "$dev/enable" ] || [ ! -w "$dev/enable" ]; then
+		echo "$TESTNAME: $dev_name missing mandatory root attributes"
+		retval=1
+		continue
+	fi
+
+	caps=$(cat "$dev/capabilities")
+	status=$(cat "$dev/port_status")
+	echo "  $dev_name: capabilities read OK"
+	echo "  $dev_name: port_status read OK"
+
+	trap 'cleanup_dev "$dev"' EXIT
+
+	if ! echo 1 > "$dev/enable" 2>/dev/null; then
+		echo "  $dev_name: margining not ready by hardware (skipping active lanes)"
+		continue
+	fi
+
+	echo "  $dev_name: margining enabled OK"
+
+	for lane_dir in $(ls -d "$dev"/lane* 2>/dev/null || true); do
+		lane=$(basename "$lane_dir")
+		echo "  $dev_name: testing $lane"
+
+		# Test setting receiver (Rx 0 is always local receiver)
+		echo 0 > "$lane_dir/receiver"
+		cat "$lane_dir/caps" > /dev/null
+		cat "$lane_dir/num_timing_steps" > /dev/null
+		cat "$lane_dir/num_voltage_steps" > /dev/null
+
+		# Test resetting timing and voltage margin
+		echo 0 > "$lane_dir/margin_timing"
+		echo 0 > "$lane_dir/margin_voltage"
+	done
+
+	echo 0 > "$dev/enable"
+	trap - EXIT
+	echo "  $dev_name: margining disabled OK"
+done
+
+if [ $retval -eq 0 ]; then
+	echo "$TESTNAME [PASS]"
+else
+	echo "$TESTNAME [FAIL]"
+fi
+
+exit $retval

---
base-commit: 0f23d56f17fdfc7db69d51f64c8b91bbab947aa9
change-id: 20260818-pcie-lmt-3044d586aaec

Best regards,
-- 
Priyank Rathod <rathodpriyank@google.com>

^ permalink raw reply related	[flat|nested] 9+ messages in thread

* Re: [PATCH v3] PCI/pcie: Add PCIe Lane Margining at Receiver (LMR) support
  2026-08-18 20:49 ` [PATCH v3] " Priyank Rathod
@ 2026-08-18 21:00   ` sashiko-bot
  0 siblings, 0 replies; 9+ messages in thread
From: sashiko-bot @ 2026-08-18 21:00 UTC (permalink / raw)
  To: Priyank Rathod; +Cc: linux-pci

Thank you for your contribution! Sashiko AI review found 8 potential issue(s) to consider:
- [High] Enabling Lane Margining permanently disables ASPM L0s/L1 without a restoration path, resulting in a persistent power regression.
- [High] The driver determines the number of lanes using Maximum Link Width instead of Negotiated Link Width, leading to massive command timeouts on inactive lanes.
- [Medium] Ignoring the return value of `pci_lmr_demargin_lane()` causes software state desynchronization and leaked hardware margins.
- [Medium] Incomplete LMR state teardown in `pci_lmr_exit()` fails to clear `SW_READY`, leaking hardware state.
- [Medium] The lazy initialization of `pci_debugfs_root_dir` is not thread-safe, allowing concurrent device probes to corrupt the root dentry pointer.
- [Medium] Missing runtime PM protection allows the device to enter D3hot while margining is active, destroying the hardware margining state.
- [Low] Enabling LMR silently clobbers user-selected lane receivers, forcing them back to LMR_RX_LOCAL.
- [Low] Incorrect error check for debugfs_create_dir() return value in pci_margin_debugfs_init().
--

--- Patch [1]: [PATCH v3] PCI/pcie: Add PCIe Lane Margining at Receiver (LMR) support ---
commit 8efc0096cd4866c98886e7abe624d739ed9f545f
Author: Priyank Rathod <rathodpriyank@google.com>

PCI/pcie: Add PCIe Lane Margining at Receiver (LMR) support

This patch adds support for PCIe Lane Margining at Receiver (LMR) per the PCIe
6.0 base specification. It introduces driver structures, debugfs exposure for
manual margining configuration, and hooks into the device probe and remove
paths.

> diff --git a/drivers/pci/pcie/margin.c b/drivers/pci/pcie/margin.c
> new file mode 100644
> --- /dev/null
> +++ b/drivers/pci/pcie/margin.c
> @@ -0,0 +1,890 @@

[ ... ]

> +static struct dentry *get_pci_debugfs_root(void)
> +{
> +	if (!pci_debugfs_root_dir)
> +		pci_debugfs_root_dir = debugfs_lookup("pci", NULL);
> +	if (!pci_debugfs_root_dir)
> +		pci_debugfs_root_dir = debugfs_create_dir("pci", NULL);
> +	return pci_debugfs_root_dir;
> +}

[Severity: Medium]
Could this concurrent lockless check corrupt the global debugfs root pointer?

If two threads concurrently probe devices, both might see a NULL pointer and
attempt to create the directory, leading one to overwrite the global
variable with an error pointer from debugfs_create_dir().

[ ... ]

> +static int pci_lmr_cache_rx_info(struct pci_margin_lane *plane, u8 rx)
> +{
> +	struct pci_margin_rx_info *info = &plane->rx_info[rx];
> +	u16 sts;
> +	int ret;
> +
> +	if (info->caps_cached)
> +		return 0;
> +
> +	/* Issuing REPORT_CAPS aborts any active margin per PCIe spec */
> +	pci_lmr_demargin_lane(plane);
> +
> +	ret = pci_lmr_run_cmd(plane->mdev->dev, plane->lane, rx,
> +			      LMR_TYPE_REPORT_CAPS, 0, 0, &sts);

[Severity: Medium]
Should the return value of pci_lmr_demargin_lane() be checked here?

If the demargining command fails due to a timeout, proceeding to issue
REPORT_CAPS resets the hardware state without software properly handling the
error, which might cause the hardware and software states to fall out of sync.

[ ... ]

> +static ssize_t margin_enable_write(struct file *file, const char __user *user_buf,
> +			       size_t count, loff_t *ppos)
> +{

[ ... ]

> +	if (enable) {
> +		if (pci_read_config_word(dev, mdev->cap + PCI_LMR_PORT_CAP, &cap)) {
> +			ret = -EIO;
> +			goto out;
> +		}
> +
> +		/*
> +		 * PCIe Base Spec r6.0: Link must be maintained in L0 during
> +		 * active margining. Disable ASPM L0s/L1.
> +		 */
> +		pci_disable_link_state(dev, PCIE_LINK_STATE_L0S | PCIE_LINK_STATE_L1);

[Severity: High]
Does pci_disable_link_state() permanently disable ASPM for this device?

There does not appear to be a restoration path when the enable flag is toggled
off. This could result in a persistent power efficiency regression for the
system until the next reboot.

[Severity: Medium]
Is a runtime PM reference needed here, such as pm_runtime_get_sync()?

Without taking a runtime PM reference, the PM core could transition an idle
device to D3hot. This would drop the link from L0, violating the active
margining state machine requirements and potentially destroying the hardware
margining state.

> +
> +		if (cap & PCI_LMR_PORT_CAP_USES_SW_READY) {

[ ... ]

> +		/* Cache capabilities for local receiver on all lanes */
> +		for (i = 0; i < mdev->num_lanes; i++) {
> +			mdev->lanes[i].rx = LMR_RX_LOCAL;
> +			pci_lmr_cache_rx_info(&mdev->lanes[i], LMR_RX_LOCAL);
> +		}
> +		mdev->enabled = true;

[Severity: Low]
Does enabling margining intentionally clobber user-selected lane receivers?

If a user configures specific receivers for lanes via debugfs while margining
is disabled, that configuration will be silently overwritten and forced back
to LMR_RX_LOCAL.

[ ... ]

> +static ssize_t margin_lane_receiver_write(struct file *file, const char __user *user_buf,
> +				      size_t count, loff_t *ppos)
> +{

[ ... ]

> +	if (mdev->enabled) {
> +		/* Demargin previous receiver per single-receiver spec rule */
> +		pci_lmr_demargin_lane(plane);
> +		ret = pci_lmr_cache_rx_info(plane, rx);
> +		if (ret)
> +			goto out;
> +	}
> +
> +	plane->rx = rx;

[Severity: Medium]
What happens if pci_lmr_demargin_lane() fails here?

If the error is ignored, plane->rx is updated to the new receiver anyway. This
could leave the previous hardware receiver permanently margined while software
variables lose track of it.

[ ... ]

> +static void pci_margin_debugfs_init(struct pci_margin_dev *mdev)
> +{
> +	struct pci_dev *dev = mdev->dev;
> +	struct dentry *parent;
> +	char dirname[64];
> +	int i;
> +
> +	parent = get_pci_debugfs_root();
> +	snprintf(dirname, sizeof(dirname), "pcie_lmr_%s", dev_name(&dev->dev));
> +	mdev->debugfs = debugfs_create_dir(dirname, parent);
> +	if (!mdev->debugfs)
> +		return;

[Severity: Low]
Does debugfs_create_dir() return an error pointer rather than NULL on failure?

Checking for NULL here will evaluate to false on an error pointer, causing the
driver to proceed with an invalid debugfs dentry instead of cleanly aborting.

[ ... ]

> +void pci_lmr_init(struct pci_dev *dev)
> +{
> +	struct pci_margin_dev *mdev;
> +	enum pci_bus_speed speed;
> +	u16 lmr;
> +	u16 lnkcap;
> +	int i;

[ ... ]

> +	mutex_init(&mdev->lock);
> +
> +	pcie_capability_read_word(dev, PCI_EXP_LNKCAP, &lnkcap);
> +	mdev->num_lanes = FIELD_GET(PCI_EXP_LNKCAP_MLW, lnkcap);
> +
> +	if (mdev->num_lanes == 0 || mdev->num_lanes > LMR_MAX_LANES) {
> +		pci_warn(dev, "Invalid link width %d for LMR\n", mdev->num_lanes);
> +		goto err_free_mdev;
> +	}

[Severity: High]
Should this use Negotiated Link Width (PCI_EXP_LNKSTA_NLW) instead of Maximum
Link Width?

If a device like a x16 card is inserted into a slot with fewer active lanes,
the maximum link width will overestimate the active lanes. When margining is
enabled, commands sent to the inactive lanes might time out, causing massive
stalls during interaction.

[ ... ]

> +void pci_lmr_exit(struct pci_dev *dev)
> +{
> +	struct pci_margin_dev *mdev = dev->lmr;
> +	int i;
> +
> +	if (!mdev)
> +		return;
> +
> +	mutex_lock(&mdev->lock);
> +	if (mdev->enabled) {
> +		for (i = 0; i < mdev->num_lanes; i++)
> +			pci_lmr_demargin_lane(&mdev->lanes[i]);
> +	}
> +	mutex_unlock(&mdev->lock);

[Severity: Medium]
Should this teardown path also clear the PCI_LMR_PORT_STS_SW_READY bit and
set mdev->enabled to false?

Unlike the suspend path, this function leaves SW_READY asserted in hardware.
During device unbind or teardown, this abandons the hardware in an active
margining state expectation.

-- 
Sashiko AI review · https://sashiko.dev/#/patchset/20260818-pcie-lmt-v3-1-d5df983a5d53@google.com?part=1

^ permalink raw reply	[flat|nested] 9+ messages in thread

* [PATCH v4] PCI/pcie: Add PCIe Lane Margining at Receiver (LMR) support
  2026-08-18 18:57 [PATCH] PCI/pcie: Add PCIe Lane Margining at Receiver (LMR) support Priyank Rathod
                   ` (2 preceding siblings ...)
  2026-08-18 20:49 ` [PATCH v3] " Priyank Rathod
@ 2026-08-18 21:20 ` Priyank Rathod
  2026-08-18 21:32   ` sashiko-bot
  2026-08-18 22:29   ` Bjorn Helgaas
  3 siblings, 2 replies; 9+ messages in thread
From: Priyank Rathod @ 2026-08-18 21:20 UTC (permalink / raw)
  To: Bjorn Helgaas, Shuah Khan
  Cc: linux-kernel, linux-pci, linux-kselftest, Priyank Rathod

Per PCIe Base Specification r6.0, sec 8.4.4 ("Lane Margining at
Receiver"), PCIe devices operating at 16.0 GT/s (Gen 4) or higher data
rates support the Lane Margining at Receiver Extended Capability
(ID 0x27), and it is mandatory for receivers operating at 64.0 GT/s
(Gen 6) or higher data rates. Lane Margining allows software to
evaluate high-speed link margins by measuring timing and voltage steps
for each individual physical lane and receiver.

Add driver and debugfs support for PCIe Lane Margining at Receiver:

  - Add Lane Margining at Receiver Extended Capability register
    definitions (PCI_EXT_CAP_ID_LMR, PCI_LMR_PORT_CAP, PCI_LMR_PORT_STS,
    PCI_LMR_LANE_CTRL, PCI_LMR_LANE_STS) to <uapi/linux/pci_regs.h>.
  - Add Kconfig option CONFIG_PCIE_LMR (under drivers/pci/pcie/Kconfig)
    dependent on DEBUG_FS.
  - Implement drivers/pci/pcie/margin.c to probe the capability on Gen4+
    links and expose per-device debugfs entries under:
      /sys/kernel/debug/pci/pcie_lmr_<pci_dev_name>/
    providing control over margining enablement, receiver selection, and
    execution of timing/voltage margin step commands. Distinguish
    between missing mandatory LMR capability on Gen6+ vs optional on
    Gen4/Gen5.
  - Hook pci_lmr_init() into pci_init_capabilities() during device probe
    in drivers/pci/probe.c and pci_lmr_exit() into drivers/pci/remove.c.
  - Add kselftest script under tools/testing/selftests/pcie_lmt/pcie_lmt.sh
    to test debugfs capability reads, enablement, and stepping.

Signed-off-by: Priyank Rathod <rathodpriyank@google.com>
---
Per PCIe Base Specification r6.0, section 8.4.4 ("Lane Margining at Receiver"),
PCIe devices operating at 16.0 GT/s (Gen 4) or higher data rates support the
Lane Margining at Receiver Extended Capability (ID 0x27), and it is mandatory
for receivers operating at 64.0 GT/s (Gen 6) or higher data rates.

Lane Margining allows system software to evaluate high-speed link signal
integrity and margins by measuring timing and voltage steps for each physical
lane and receiver independently.

This series introduces kernel driver support, debugfs controls, and a
kselftest automation script for PCIe Lane Margining at Receiver (LMR/LMT).

==============================================================================
1. How to Enable & Configure
==============================================================================
Enable the Kconfig option under PCI support:
  CONFIG_PCIE_LMR=y (or =m)
  (Depends on CONFIG_PCI and CONFIG_DEBUG_FS)

Upon boot or device hotplug on Gen4+ links (>= 16.0 GT/s), the driver probes
Extended Capability ID 0x27 and exposes per-device debugfs interfaces:
  /sys/kernel/debug/pci/pcie_lmr_<domain>:<bus>:<dev>.<func>/

==============================================================================
2. How to Use the Debugfs Interface (Manual Margining)
==============================================================================
Inspect device-wide margining capabilities and port status:
  # Inspect root device LMR capabilities & status
  cat /sys/kernel/debug/pci/pcie_lmr_0000:01:00.0/capabilities
  cat /sys/kernel/debug/pci/pcie_lmr_0000:01:00.0/port_status

Enable active Lane Margining on the device:
  # Enable Lane Margining state machine
  echo 1 > /sys/kernel/debug/pci/pcie_lmr_0000:01:00.0/enable

Inspect and step individual lanes (e.g. lane0):
  # Select target receiver (0 = local receiver, 1..6 = retimers/link partners)
  echo 0 > /sys/kernel/debug/pci/pcie_lmr_0000:01:00.0/lane0/receiver

  # Check available timing and voltage steps for this receiver
  cat /sys/kernel/debug/pci/pcie_lmr_0000:01:00.0/lane0/caps
  cat /sys/kernel/debug/pci/pcie_lmr_0000:01:00.0/lane0/num_timing_steps
  cat /sys/kernel/debug/pci/pcie_lmr_0000:01:00.0/lane0/num_voltage_steps

  # Step timing margin or voltage margin offset
  echo 2 > /sys/kernel/debug/pci/pcie_lmr_0000:01:00.0/lane0/margin_timing
  echo 1 > /sys/kernel/debug/pci/pcie_lmr_0000:01:00.0/lane0/margin_voltage

  # Reset margin offset back to nominal (0)
  echo 0 > /sys/kernel/debug/pci/pcie_lmr_0000:01:00.0/lane0/margin_timing
  echo 0 > /sys/kernel/debug/pci/pcie_lmr_0000:01:00.0/lane0/margin_voltage

Disable Lane Margining when finished:
  echo 0 > /sys/kernel/debug/pci/pcie_lmr_0000:01:00.0/enable

==============================================================================
3. How to Run Automated Kselftests Using the Test Script
==============================================================================
An automated kselftest script is included to test capability reads, receiver
selection, and margining commands across all enumerated LMR devices:

  # Run directly as root
  sudo ./tools/testing/selftests/pcie_lmt/pcie_lmt.sh

Or run via the kselftest Makefile harness:
  make -C tools/testing/selftests TARGETS=pcie_lmt run_tests

Sample script output on an LMR-capable device:
  pcie_lmt: testing PCIe LMR debugfs entries
  pcie_lmt: probing device pcie_lmr_0000:01:00.0
    pcie_lmr_0000:01:00.0: capabilities read OK
    pcie_lmr_0000:01:00.0: port_status read OK
    pcie_lmr_0000:01:00.0: margining enabled OK
    pcie_lmr_0000:01:00.0: testing lane0
    pcie_lmr_0000:01:00.0: testing lane1
    pcie_lmr_0000:01:00.0: margining disabled OK
  pcie_lmt [PASS]

To: Bjorn Helgaas <bhelgaas@google.com>
To: Shuah Khan <shuah@kernel.org>
Cc: linux-kernel@vger.kernel.org
Cc: linux-pci@vger.kernel.org
Cc: linux-kselftest@vger.kernel.org
Signed-off-by: Priyank Rathod <rathodpriyank@google.com>

Changes in v4:
  - Added Sample Multiple Receivers (Bit 5) concurrency verification in margin_lane_timing_write() and margin_lane_voltage_write() per PCIe r6.0 sec 8.4.4, returning -EBUSY if another lane on the same receiver is already margined when simultaneous lane margining is not supported.
  - Added active operating link speed verification (PCI_EXP_LNKSTA_CLS >= 16.0 GT/s) in margin_enable_write() before enabling LMR, as LMR commands are physically undefined on links operating at Gen1/Gen2/Gen3 speeds.
  - Added fast-path hardware NAK detection in pci_lmr_run_cmd() to return -EOPNOTSUPP immediately if a receiver echoes MTYPE == NO_CMD (0x7) after command issuance rather than waiting 150ms for a timeout.
  - Added pci_reset_lmr() hooked into __pci_reset_function_locked() to synchronize software state and demargin on FLR or Secondary Bus Reset.
  - Comprehensive NULL pointer checks and array/lane/receiver bounds checks added across all internal helpers and debugfs write handlers.

Changes in v2:
  - Fixed NO_CMD (0x7) clearing in pci_lmr_run_cmd() before issuing new commands per PCIe r6.0 sec 8.4.4.
  - Protected plane->rx updates with mdev->lock in margin_lane_receiver_write().
  - Corrected Margining Port Capabilities bit definition to PCI_LMR_PORT_CAP_USES_SW_READY (0x0001) in <uapi/linux/pci_regs.h>.
  - Updated kselftest script (pcie_lmt.sh) to locate LMR debugfs entries.
  - Validated integer bounds against LMR_MAX_TIMING_STEP / LMR_MAX_VOLTAGE_STEP before narrowing u8 cast.
  - Moved mdev->enabled checks inside mutex_lock(&mdev->lock) to eliminate TOCTOU races.
  - Checked return values of all pci_read_config_word() calls, propagating -EIO on failure.
  - Eliminated dead store of cap in margin_enable_write().
  - Explicitly checked speed == PCIE_SPEED_64_0GT in pci_lmr_init() to avoid misidentifying PCI_SPEED_UNKNOWN (0xFF) as Gen6.
---
 drivers/pci/pci-driver.c                     |   1 +
 drivers/pci/pci.c                            |   4 +-
 drivers/pci/pci.h                            |  12 +
 drivers/pci/pcie/Kconfig                     |  12 +
 drivers/pci/pcie/Makefile                    |   1 +
 drivers/pci/pcie/margin.c                    | 957 +++++++++++++++++++++++++++
 drivers/pci/probe.c                          |   1 +
 drivers/pci/remove.c                         |   1 +
 include/linux/pci.h                          |   6 +
 include/uapi/linux/pci_regs.h                |  18 +
 tools/testing/selftests/Makefile             |   1 +
 tools/testing/selftests/pcie_lmt/Makefile    |   3 +
 tools/testing/selftests/pcie_lmt/pcie_lmt.sh | 105 +++
 13 files changed, 1121 insertions(+), 1 deletion(-)

diff --git a/drivers/pci/pci-driver.c b/drivers/pci/pci-driver.c
index f36778e62ac1..17544a7023fc 100644
--- a/drivers/pci/pci-driver.c
+++ b/drivers/pci/pci-driver.c
@@ -821,6 +821,7 @@ static int pci_pm_suspend(struct device *dev)
 	 * since Coffee Lake, to enter a lower-power PM state.
 	 */
 	pci_suspend_ptm(pci_dev);
+	pci_suspend_lmr(pci_dev);
 
 	if (pci_has_legacy_pm_support(pci_dev))
 		return pci_legacy_suspend(dev, PMSG_SUSPEND);
diff --git a/drivers/pci/pci.c b/drivers/pci/pci.c
index 77b17b13ee61..dc9724cb7b4d 100644
--- a/drivers/pci/pci.c
+++ b/drivers/pci/pci.c
@@ -5145,8 +5145,10 @@ int __pci_reset_function_locked(struct pci_dev *dev)
 		method = &pci_reset_fn_methods[m];
 		pci_dbg(dev, "reset via %s\n", method->name);
 		rc = method->reset_fn(dev, PCI_RESET_DO_RESET);
-		if (!rc)
+		if (!rc) {
+			pci_reset_lmr(dev);
 			return 0;
+		}
 
 		pci_dbg(dev, "%s failed with %d\n", method->name, rc);
 		if (rc != -ENOTTY)
diff --git a/drivers/pci/pci.h b/drivers/pci/pci.h
index 4469e1a77f3c..6322a81f9e50 100644
--- a/drivers/pci/pci.h
+++ b/drivers/pci/pci.h
@@ -1023,6 +1023,18 @@ static inline void pci_no_tph(void) { }
 static inline void pci_tph_init(struct pci_dev *dev) { }
 #endif
 
+#ifdef CONFIG_PCIE_LMR
+void pci_lmr_init(struct pci_dev *dev);
+void pci_lmr_exit(struct pci_dev *dev);
+void pci_suspend_lmr(struct pci_dev *dev);
+void pci_reset_lmr(struct pci_dev *dev);
+#else
+static inline void pci_lmr_init(struct pci_dev *dev) { }
+static inline void pci_lmr_exit(struct pci_dev *dev) { }
+static inline void pci_suspend_lmr(struct pci_dev *dev) { }
+static inline void pci_reset_lmr(struct pci_dev *dev) { }
+#endif
+
 #ifdef CONFIG_PCIE_PTM
 void pci_ptm_init(struct pci_dev *dev);
 void pci_save_ptm_state(struct pci_dev *dev);
diff --git a/drivers/pci/pcie/Kconfig b/drivers/pci/pcie/Kconfig
index 207c2deae35f..3b021ca2fe84 100644
--- a/drivers/pci/pcie/Kconfig
+++ b/drivers/pci/pcie/Kconfig
@@ -137,6 +137,18 @@ config PCIE_PTM
 	  This is only useful if you have devices that support PTM, but it
 	  is safe to enable even if you don't.
 
+config PCIE_LMR
+	bool "PCI Express Lane Margining at Receiver Support"
+	depends on DEBUG_FS
+	help
+	  This enables the PCI Express Lane Margining at Receiver support.
+	  Lane Margining allows software to determine the voltage and
+	  timing margin of each lane on a PCIe link (16.0 GT/s and above).
+	  The margining data is exposed via debugfs.
+
+	  This is only useful if you have devices that support lane
+	  margining, but it is safe to enable even if you don't.
+
 config PCIE_EDR
 	bool "PCI Express Error Disconnect Recover support"
 	depends on PCIE_DPC && ACPI
diff --git a/drivers/pci/pcie/Makefile b/drivers/pci/pcie/Makefile
index b0b43a18c304..aac45ae0402e 100644
--- a/drivers/pci/pcie/Makefile
+++ b/drivers/pci/pcie/Makefile
@@ -13,4 +13,5 @@ obj-$(CONFIG_PCIEAER_INJECT)	+= aer_inject.o
 obj-$(CONFIG_PCIE_PME)		+= pme.o
 obj-$(CONFIG_PCIE_DPC)		+= dpc.o
 obj-$(CONFIG_PCIE_PTM)		+= ptm.o
+obj-$(CONFIG_PCIE_LMR)		+= margin.o
 obj-$(CONFIG_PCIE_EDR)		+= edr.o
diff --git a/drivers/pci/pcie/margin.c b/drivers/pci/pcie/margin.c
new file mode 100644
index 000000000000..e9e195744d5a
--- /dev/null
+++ b/drivers/pci/pcie/margin.c
@@ -0,0 +1,957 @@
+// SPDX-License-Identifier: GPL-2.0
+/*
+ * PCI Express Lane Margining at Receiver
+ *
+ * Copyright (C) 2026 Google LLC
+ * Author: Priyank Rathod <rathodpriyank@google.com>
+ *
+ * Lane Margining at Receiver (PCIe Base Specification r6.0, sec 8.4.4)
+ * allows system software to determine the voltage and timing margins of
+ * each physical lane on a PCIe link. The Extended Capability (ID 0x27)
+ * is available for receivers operating at 16.0 GT/s (Gen4) or higher data
+ * rates, and is mandatory for receivers operating at 64.0 GT/s (Gen6) or
+ * higher data rates.
+ *
+ * This driver implements:
+ *   - Probing Extended Capability ID 0x27 and Margining Port Capabilities.
+ *   - Managing ASPM L0s/L1 link states during active margining per spec.
+ *   - PCIe r6.0 NO_CMD (0x7) clearing handshake per receiver and lane.
+ *   - Caching receiver capabilities & step counts to avoid DEMARGIN side-effects.
+ *   - Handling Symmetric vs Independent Left/Right & Up/Down margin steps.
+ *   - Exposing per-device debugfs interfaces under /sys/kernel/debug/pci/.
+ */
+
+#include <linux/pci.h>
+#include <linux/slab.h>
+#include <linux/mutex.h>
+#include <linux/delay.h>
+#include <linux/jiffies.h>
+#include <linux/seq_file.h>
+#include <linux/bitfield.h>
+#include <linux/debugfs.h>
+
+#include "../pci.h"
+
+/* Margin type encodings per PCIe Base Spec r6.0 sec 8.4.4 */
+#define LMR_TYPE_DEMARGIN               0x0
+#define LMR_TYPE_REPORT_CAPS            0x1
+#define LMR_TYPE_REPORT_VOLTAGE_STEPS   0x2
+#define LMR_TYPE_REPORT_TIMING_STEPS    0x3
+#define LMR_TYPE_TIMING                 0x4
+#define LMR_TYPE_VOLTAGE                0x5
+#define LMR_TYPE_NO_CMD                 0x7
+
+/* LMR command timing parameters */
+#define LMR_CMD_TIMEOUT_MS              150
+#define LMR_CMD_SLEEP_MIN_US            100
+#define LMR_CMD_SLEEP_MAX_US            250
+#define LMR_ENABLE_TIMEOUT_MS           150
+#define LMR_ENABLE_SLEEP_MIN_US         1000
+#define LMR_ENABLE_SLEEP_MAX_US         2000
+
+/* LMR limits */
+#define LMR_MAX_LANES                   32
+#define LMR_MAX_RX_NUM                  6
+#define LMR_MAX_TIMING_STEP             63
+#define LMR_MAX_VOLTAGE_STEP            127
+
+/* LMR PCIe generation numbers */
+#define LMR_GEN6                        6
+#define LMR_GEN5                        5
+#define LMR_GEN4                        4
+
+/* LMR lane register stride */
+#define LMR_LANE_REG_STRIDE             4
+
+/* LMR receivers and directions */
+#define LMR_RX_LOCAL                    0
+#define LMR_STEP_DIR_INCREASE           1
+#define LMR_STEP_DIR_DECREASE           0
+
+/* LMR step & direction encoding masks */
+#define LMR_STEPS_MASK                  0x7F
+#define LMR_TIMING_STEP_MASK            0x3F
+#define LMR_TIMING_DIR_SHIFT            6
+#define LMR_VOLTAGE_STEP_MASK           0x7F
+#define LMR_VOLTAGE_DIR_SHIFT           7
+
+/* LMR capabilities report bit fields */
+#define LMR_CAP_MARGIN_HV               BIT(0)
+#define LMR_CAP_MARGIN_EV_IV            BIT(1)
+#define LMR_CAP_IND_LEFT_RIGHT_TIMING   BIT(2)
+#define LMR_CAP_IND_UP_DOWN_VOLTAGE     BIT(3)
+#define LMR_CAP_ERROR_SAMPLER           BIT(4)
+#define LMR_CAP_SAMPLE_MULTIPLE_RX      BIT(5)
+
+struct pci_margin_rx_info {
+	bool caps_cached;
+	u8 caps;
+	u8 num_timing_steps;
+	u8 num_voltage_steps;
+};
+
+struct pci_margin_lane {
+	struct pci_margin_dev *mdev;
+	int lane;
+	u8 rx;
+	int timing_val;
+	int voltage_val;
+	struct pci_margin_rx_info rx_info[LMR_MAX_RX_NUM + 1];
+};
+
+struct pci_margin_dev {
+	struct pci_dev *dev;
+	u16 cap;
+	struct dentry *debugfs;
+	struct mutex lock;
+	int num_lanes;
+	struct pci_margin_lane *lanes;
+	bool enabled;
+};
+
+#if IS_ENABLED(CONFIG_DEBUG_FS)
+static struct dentry *pci_debugfs_root_dir;
+
+static struct dentry *get_pci_debugfs_root(void)
+{
+	if (!pci_debugfs_root_dir)
+		pci_debugfs_root_dir = debugfs_lookup("pci", NULL);
+	if (!pci_debugfs_root_dir)
+		pci_debugfs_root_dir = debugfs_create_dir("pci", NULL);
+	return pci_debugfs_root_dir;
+}
+#endif
+
+/*
+ * pci_lmr_run_cmd() - Issue LMR command to Lane Control and wait for Status.
+ * Must be called with mdev->lock held.
+ */
+static int pci_lmr_run_cmd(struct pci_dev *dev, int lane, u8 rx, u8 type,
+			   u8 usage, u8 payload, u16 *status_val)
+{
+	u16 lmr, ctrl_offset, sts_offset;
+	u16 ctrl, sts;
+	unsigned long timeout;
+
+	if (!dev || !dev->lmr || lane < 0 || lane >= dev->lmr->num_lanes ||
+	    rx > LMR_MAX_RX_NUM)
+		return -EINVAL;
+
+	lmr = dev->lmr_cap;
+	ctrl_offset = lmr + PCI_LMR_LANE_CTRL + LMR_LANE_REG_STRIDE * lane;
+	sts_offset = lmr + PCI_LMR_LANE_STS + LMR_LANE_REG_STRIDE * lane;
+
+	/*
+	 * Per PCIe Base Spec r6.0 sec 8.4.4, software must issue NO_CMD (0x7)
+	 * targeting the specific receiver (rx) to clear MTYPE in Lane Status
+	 * before issuing a subsequent command.
+	 */
+	if (type != LMR_TYPE_NO_CMD) {
+		ctrl = FIELD_PREP(PCI_LMR_LANE_CTRL_RX_NUM, rx) |
+		       FIELD_PREP(PCI_LMR_LANE_CTRL_MTYPE, LMR_TYPE_NO_CMD) |
+		       FIELD_PREP(PCI_LMR_LANE_CTRL_USAGE, 0) |
+		       FIELD_PREP(PCI_LMR_LANE_CTRL_PAYLOAD, 0);
+
+		pci_write_config_word(dev, ctrl_offset, ctrl);
+
+		timeout = jiffies + msecs_to_jiffies(LMR_CMD_TIMEOUT_MS);
+		while (1) {
+			if (pci_read_config_word(dev, sts_offset, &sts))
+				return -EIO;
+			if (sts == 0xFFFF)
+				return -ENODEV;
+			if (FIELD_GET(PCI_LMR_LANE_STS_MTYPE, sts) == LMR_TYPE_NO_CMD &&
+			    FIELD_GET(PCI_LMR_LANE_STS_RX_NUM, sts) == rx)
+				break;
+			if (time_after(jiffies, timeout))
+				return -ETIMEDOUT;
+			usleep_range(LMR_CMD_SLEEP_MIN_US, LMR_CMD_SLEEP_MAX_US);
+		}
+	}
+
+	ctrl = FIELD_PREP(PCI_LMR_LANE_CTRL_RX_NUM, rx) |
+	       FIELD_PREP(PCI_LMR_LANE_CTRL_MTYPE, type) |
+	       FIELD_PREP(PCI_LMR_LANE_CTRL_USAGE, usage) |
+	       FIELD_PREP(PCI_LMR_LANE_CTRL_PAYLOAD, payload);
+
+	pci_write_config_word(dev, ctrl_offset, ctrl);
+
+	timeout = jiffies + msecs_to_jiffies(LMR_CMD_TIMEOUT_MS);
+	while (1) {
+		if (pci_read_config_word(dev, sts_offset, &sts))
+			return -EIO;
+		if (sts == 0xFFFF)
+			return -ENODEV;
+
+		if (FIELD_GET(PCI_LMR_LANE_STS_MTYPE, sts) == type &&
+		    FIELD_GET(PCI_LMR_LANE_STS_RX_NUM, sts) == rx) {
+			if (status_val)
+				*status_val = sts;
+			return 0;
+		}
+
+		/*
+		 * Per PCIe Base Spec r6.0 sec 8.4.4, if receiver echoes NO_CMD (0x7)
+		 * after a command was sent, it indicates command rejection/NAK.
+		 */
+		if (FIELD_GET(PCI_LMR_LANE_STS_MTYPE, sts) == LMR_TYPE_NO_CMD &&
+		    FIELD_GET(PCI_LMR_LANE_STS_RX_NUM, sts) == rx)
+			return -EOPNOTSUPP;
+
+		if (time_after(jiffies, timeout))
+			break;
+
+		usleep_range(LMR_CMD_SLEEP_MIN_US, LMR_CMD_SLEEP_MAX_US);
+	}
+
+	return -ETIMEDOUT;
+}
+
+static int pci_lmr_demargin_lane(struct pci_margin_lane *plane)
+{
+	u16 sts;
+	int ret;
+
+	if (!plane || !plane->mdev)
+		return -EINVAL;
+
+	if (plane->timing_val == 0 && plane->voltage_val == 0)
+		return 0;
+
+	ret = pci_lmr_run_cmd(plane->mdev->dev, plane->lane, plane->rx,
+			      LMR_TYPE_DEMARGIN, 0, 0, &sts);
+	if (!ret) {
+		plane->timing_val = 0;
+		plane->voltage_val = 0;
+	}
+	return ret;
+}
+
+static int pci_lmr_cache_rx_info(struct pci_margin_lane *plane, u8 rx)
+{
+	struct pci_margin_rx_info *info;
+	u16 sts;
+	int ret;
+
+	if (!plane || rx > LMR_MAX_RX_NUM)
+		return -EINVAL;
+
+	info = &plane->rx_info[rx];
+
+	if (info->caps_cached)
+		return 0;
+
+	/* Issuing REPORT_CAPS aborts any active margin per PCIe spec */
+	pci_lmr_demargin_lane(plane);
+
+	ret = pci_lmr_run_cmd(plane->mdev->dev, plane->lane, rx,
+			      LMR_TYPE_REPORT_CAPS, 0, 0, &sts);
+	if (ret)
+		return ret;
+	info->caps = FIELD_GET(PCI_LMR_LANE_STS_PAYLOAD, sts);
+
+	ret = pci_lmr_run_cmd(plane->mdev->dev, plane->lane, rx,
+			      LMR_TYPE_REPORT_TIMING_STEPS, 0, 0, &sts);
+	if (ret)
+		return ret;
+	info->num_timing_steps = FIELD_GET(PCI_LMR_LANE_STS_PAYLOAD, sts) & LMR_STEPS_MASK;
+
+	ret = pci_lmr_run_cmd(plane->mdev->dev, plane->lane, rx,
+			      LMR_TYPE_REPORT_VOLTAGE_STEPS, 0, 0, &sts);
+	if (ret)
+		return ret;
+	info->num_voltage_steps = FIELD_GET(PCI_LMR_LANE_STS_PAYLOAD, sts) & LMR_STEPS_MASK;
+
+	info->caps_cached = true;
+	return 0;
+}
+
+#if IS_ENABLED(CONFIG_DEBUG_FS)
+
+static int margin_caps_show(struct seq_file *s, void *v)
+{
+	struct pci_margin_dev *mdev = s->private;
+	struct pci_dev *dev = mdev->dev;
+	u16 cap;
+
+	if (pci_read_config_word(dev, mdev->cap + PCI_LMR_PORT_CAP, &cap))
+		return -EIO;
+	seq_printf(s, "Port Capabilities: %#06x\n", cap);
+	seq_printf(s, "  Uses SW Ready: %s\n",
+		   (cap & PCI_LMR_PORT_CAP_USES_SW_READY) ? "yes" : "no");
+	return 0;
+}
+DEFINE_SHOW_ATTRIBUTE(margin_caps);
+
+static int margin_port_status_show(struct seq_file *s, void *v)
+{
+	struct pci_margin_dev *mdev = s->private;
+	struct pci_dev *dev = mdev->dev;
+	u16 sts;
+
+	if (pci_read_config_word(dev, mdev->cap + PCI_LMR_PORT_STS, &sts))
+		return -EIO;
+	seq_printf(s, "Port Status: %#06x\n", sts);
+	seq_printf(s, "  Margining Ready: %s\n",
+		   (sts & PCI_LMR_PORT_STS_MARGIN_READY) ? "yes" : "no");
+	seq_printf(s, "  SW Ready: %s\n", (sts & PCI_LMR_PORT_STS_SW_READY) ? "yes" : "no");
+	return 0;
+}
+DEFINE_SHOW_ATTRIBUTE(margin_port_status);
+
+static int margin_enable_show(struct seq_file *s, void *v)
+{
+	struct pci_margin_dev *mdev = s->private;
+	bool enabled;
+
+	mutex_lock(&mdev->lock);
+	enabled = mdev->enabled;
+	mutex_unlock(&mdev->lock);
+
+	seq_printf(s, "%d\n", enabled);
+	return 0;
+}
+
+static ssize_t margin_enable_write(struct file *file, const char __user *user_buf,
+			       size_t count, loff_t *ppos)
+{
+	struct pci_margin_dev *mdev = ((struct seq_file *)file->private_data)->private;
+	struct pci_dev *dev = mdev->dev;
+	unsigned long timeout;
+	bool enable;
+	int ret, i;
+	u16 sts, cap;
+
+	ret = kstrtobool_from_user(user_buf, count, &enable);
+	if (ret)
+		return ret;
+
+	mutex_lock(&mdev->lock);
+
+	if (mdev->enabled == enable)
+		goto out;
+
+	if (enable) {
+		u16 lnksta;
+
+		/* PCIe r6.0 sec 8.4.4: LMR is physically undefined below 16.0 GT/s (Gen4) */
+		pcie_capability_read_word(dev, PCI_EXP_LNKSTA, &lnksta);
+		if ((lnksta & PCI_EXP_LNKSTA_CLS) < PCI_EXP_LNKSTA_CLS_16_0GB) {
+			ret = -EOPNOTSUPP;
+			goto out;
+		}
+
+		if (pci_read_config_word(dev, mdev->cap + PCI_LMR_PORT_CAP, &cap)) {
+			ret = -EIO;
+			goto out;
+		}
+
+		/*
+		 * PCIe Base Spec r6.0: Link must be maintained in L0 during
+		 * active margining. Disable ASPM L0s/L1.
+		 */
+		pci_disable_link_state(dev, PCIE_LINK_STATE_L0S | PCIE_LINK_STATE_L1);
+
+		if (cap & PCI_LMR_PORT_CAP_USES_SW_READY) {
+			if (pci_read_config_word(dev, mdev->cap + PCI_LMR_PORT_STS, &sts)) {
+				ret = -EIO;
+				goto out;
+			}
+			sts |= PCI_LMR_PORT_STS_SW_READY;
+			pci_write_config_word(dev, mdev->cap + PCI_LMR_PORT_STS, sts);
+		}
+
+		timeout = jiffies + msecs_to_jiffies(LMR_ENABLE_TIMEOUT_MS);
+		while (1) {
+			if (pci_read_config_word(dev, mdev->cap + PCI_LMR_PORT_STS, &sts)) {
+				ret = -EIO;
+				goto out;
+			}
+			if (sts == 0xFFFF) {
+				ret = -ENODEV;
+				goto out;
+			}
+			if (sts & PCI_LMR_PORT_STS_MARGIN_READY)
+				break;
+			if (time_after(jiffies, timeout)) {
+				ret = -ETIMEDOUT;
+				if (cap & PCI_LMR_PORT_CAP_USES_SW_READY) {
+					sts &= ~PCI_LMR_PORT_STS_SW_READY;
+					pci_write_config_word(dev,
+							      mdev->cap + PCI_LMR_PORT_STS,
+							      sts);
+				}
+				goto out;
+			}
+			usleep_range(LMR_ENABLE_SLEEP_MIN_US, LMR_ENABLE_SLEEP_MAX_US);
+		}
+
+		/* Cache capabilities for local receiver on all lanes */
+		for (i = 0; i < mdev->num_lanes; i++) {
+			mdev->lanes[i].rx = LMR_RX_LOCAL;
+			pci_lmr_cache_rx_info(&mdev->lanes[i], LMR_RX_LOCAL);
+		}
+		mdev->enabled = true;
+	} else {
+		/* Demargin all lanes before clearing SW_READY */
+		for (i = 0; i < mdev->num_lanes; i++)
+			pci_lmr_demargin_lane(&mdev->lanes[i]);
+
+		if (pci_read_config_word(dev, mdev->cap + PCI_LMR_PORT_STS, &sts) == 0) {
+			sts &= ~PCI_LMR_PORT_STS_SW_READY;
+			pci_write_config_word(dev, mdev->cap + PCI_LMR_PORT_STS, sts);
+		}
+		mdev->enabled = false;
+	}
+
+out:
+	mutex_unlock(&mdev->lock);
+	return ret ? ret : count;
+}
+
+static int margin_enable_open(struct inode *inode, struct file *file)
+{
+	return single_open(file, margin_enable_show, inode->i_private);
+}
+
+static const struct file_operations margin_enable_fops = {
+	.open = margin_enable_open,
+	.read = seq_read,
+	.write = margin_enable_write,
+	.llseek = seq_lseek,
+	.release = single_release,
+};
+
+static int margin_lane_receiver_show(struct seq_file *s, void *v)
+{
+	struct pci_margin_lane *plane = s->private;
+	int rx;
+
+	mutex_lock(&plane->mdev->lock);
+	rx = plane->rx;
+	mutex_unlock(&plane->mdev->lock);
+
+	seq_printf(s, "%d\n", rx);
+	return 0;
+}
+
+static ssize_t margin_lane_receiver_write(struct file *file, const char __user *user_buf,
+				      size_t count, loff_t *ppos)
+{
+	struct pci_margin_lane *plane = ((struct seq_file *)file->private_data)->private;
+	struct pci_margin_dev *mdev = plane->mdev;
+	int ret;
+	int rx;
+
+	ret = kstrtoint_from_user(user_buf, count, 0, &rx);
+	if (ret)
+		return ret;
+
+	if (rx < 0 || rx > LMR_MAX_RX_NUM)
+		return -EINVAL;
+
+	mutex_lock(&mdev->lock);
+	if (plane->rx == rx)
+		goto out;
+
+	if (mdev->enabled) {
+		/* Demargin previous receiver per single-receiver spec rule */
+		pci_lmr_demargin_lane(plane);
+		ret = pci_lmr_cache_rx_info(plane, rx);
+		if (ret)
+			goto out;
+	}
+
+	plane->rx = rx;
+out:
+	mutex_unlock(&mdev->lock);
+	return ret ? ret : count;
+}
+
+static int margin_lane_receiver_open(struct inode *inode, struct file *file)
+{
+	return single_open(file, margin_lane_receiver_show, inode->i_private);
+}
+
+static const struct file_operations margin_lane_receiver_fops = {
+	.open = margin_lane_receiver_open,
+	.read = seq_read,
+	.write = margin_lane_receiver_write,
+	.llseek = seq_lseek,
+	.release = single_release,
+};
+
+static int margin_lane_caps_show(struct seq_file *s, void *v)
+{
+	struct pci_margin_lane *plane = s->private;
+	struct pci_margin_dev *mdev = plane->mdev;
+	struct pci_margin_rx_info *info;
+	int ret = 0;
+	u8 val;
+
+	mutex_lock(&mdev->lock);
+	if (!mdev->enabled) {
+		ret = -EACCES;
+		goto out;
+	}
+
+	ret = pci_lmr_cache_rx_info(plane, plane->rx);
+	if (ret)
+		goto out;
+
+	info = &plane->rx_info[plane->rx];
+	val = info->caps;
+	seq_printf(s, "Lane %d Rx %d Capabilities: %#02x\n", plane->lane, plane->rx, val);
+	seq_printf(s, "  Margining H/V: %s\n", (val & LMR_CAP_MARGIN_HV) ? "both" : "either");
+	seq_printf(s, "  Margining eV/iV: %s\n",
+		   (val & LMR_CAP_MARGIN_EV_IV) ? "both (close & open)" : "one (close only)");
+	seq_printf(s, "  Left/Right: %s\n", (val & LMR_CAP_IND_LEFT_RIGHT_TIMING) ? "both" : "one");
+	seq_printf(s, "  Up/Down: %s\n", (val & LMR_CAP_IND_UP_DOWN_VOLTAGE) ? "both" : "one");
+	seq_printf(s, "  Error Sampler: %s\n",
+		   (val & LMR_CAP_ERROR_SAMPLER) ? "yes" : "no (main sampler)");
+	seq_printf(s, "  Sample Multiple Receivers: %s\n",
+		   (val & LMR_CAP_SAMPLE_MULTIPLE_RX) ? "yes" : "no");
+out:
+	mutex_unlock(&mdev->lock);
+	return ret;
+}
+DEFINE_SHOW_ATTRIBUTE(margin_lane_caps);
+
+static int margin_lane_timing_steps_show(struct seq_file *s, void *v)
+{
+	struct pci_margin_lane *plane = s->private;
+	struct pci_margin_dev *mdev = plane->mdev;
+	struct pci_margin_rx_info *info;
+	int ret = 0;
+
+	mutex_lock(&mdev->lock);
+	if (!mdev->enabled) {
+		ret = -EACCES;
+		goto out;
+	}
+
+	ret = pci_lmr_cache_rx_info(plane, plane->rx);
+	if (!ret) {
+		info = &plane->rx_info[plane->rx];
+		seq_printf(s, "%d\n", info->num_timing_steps);
+	}
+out:
+	mutex_unlock(&mdev->lock);
+	return ret;
+}
+DEFINE_SHOW_ATTRIBUTE(margin_lane_timing_steps);
+
+static int margin_lane_voltage_steps_show(struct seq_file *s, void *v)
+{
+	struct pci_margin_lane *plane = s->private;
+	struct pci_margin_dev *mdev = plane->mdev;
+	struct pci_margin_rx_info *info;
+	int ret = 0;
+
+	mutex_lock(&mdev->lock);
+	if (!mdev->enabled) {
+		ret = -EACCES;
+		goto out;
+	}
+
+	ret = pci_lmr_cache_rx_info(plane, plane->rx);
+	if (!ret) {
+		info = &plane->rx_info[plane->rx];
+		seq_printf(s, "%d\n", info->num_voltage_steps);
+	}
+out:
+	mutex_unlock(&mdev->lock);
+	return ret;
+}
+DEFINE_SHOW_ATTRIBUTE(margin_lane_voltage_steps);
+
+static bool pci_lmr_check_sample_multiple_rx(struct pci_margin_dev *mdev,
+					     struct pci_margin_lane *plane)
+{
+	struct pci_margin_rx_info *info = &plane->rx_info[plane->rx];
+	int i;
+
+	if (info->caps & LMR_CAP_SAMPLE_MULTIPLE_RX)
+		return true;
+
+	for (i = 0; i < mdev->num_lanes; i++) {
+		struct pci_margin_lane *other = &mdev->lanes[i];
+
+		if (i == plane->lane)
+			continue;
+		if (other->rx == plane->rx &&
+		    (other->timing_val != 0 || other->voltage_val != 0))
+			return false;
+	}
+	return true;
+}
+
+static ssize_t margin_lane_timing_write(struct file *file, const char __user *user_buf,
+				    size_t count, loff_t *ppos)
+{
+	struct pci_margin_lane *plane = ((struct seq_file *)file->private_data)->private;
+	struct pci_margin_dev *mdev = plane->mdev;
+	struct pci_margin_rx_info *info;
+	int val, ret;
+	u8 step, dir;
+	u16 sts;
+	u8 caps;
+
+	ret = kstrtoint_from_user(user_buf, count, 0, &val);
+	if (ret)
+		return ret;
+
+	if (val > LMR_MAX_TIMING_STEP || val < -LMR_MAX_TIMING_STEP)
+		return -EINVAL;
+
+	mutex_lock(&mdev->lock);
+	if (!mdev->enabled) {
+		ret = -EACCES;
+		goto out;
+	}
+
+	if (val == 0) {
+		ret = pci_lmr_demargin_lane(plane);
+		goto out;
+	}
+
+	ret = pci_lmr_cache_rx_info(plane, plane->rx);
+	if (ret)
+		goto out;
+
+	if (!pci_lmr_check_sample_multiple_rx(mdev, plane)) {
+		ret = -EBUSY;
+		goto out;
+	}
+
+	info = &plane->rx_info[plane->rx];
+	caps = info->caps;
+
+	if (val < 0) {
+		step = -val;
+		if (!(caps & LMR_CAP_IND_LEFT_RIGHT_TIMING)) {
+			/* Symmetric margining requires dir=1 per spec */
+			ret = -EINVAL;
+			goto out;
+		}
+		dir = LMR_STEP_DIR_DECREASE;
+	} else {
+		step = val;
+		dir = LMR_STEP_DIR_INCREASE;
+	}
+
+	if (step > info->num_timing_steps) {
+		ret = -EINVAL;
+		goto out;
+	}
+
+	ret = pci_lmr_run_cmd(mdev->dev, plane->lane, plane->rx,
+			      LMR_TYPE_TIMING, 0,
+			      (step & LMR_TIMING_STEP_MASK) | (dir << LMR_TIMING_DIR_SHIFT), &sts);
+	if (ret)
+		goto out;
+
+	/* Record actual step count applied by hardware */
+	step = FIELD_GET(PCI_LMR_LANE_STS_PAYLOAD, sts) & LMR_TIMING_STEP_MASK;
+	plane->timing_val = (dir == LMR_STEP_DIR_DECREASE) ? -step : step;
+
+out:
+	mutex_unlock(&mdev->lock);
+	return ret ? ret : count;
+}
+
+static int margin_lane_timing_show(struct seq_file *s, void *v)
+{
+	struct pci_margin_lane *plane = s->private;
+	int timing_val;
+
+	mutex_lock(&plane->mdev->lock);
+	timing_val = plane->timing_val;
+	mutex_unlock(&plane->mdev->lock);
+
+	seq_printf(s, "%d\n", timing_val);
+	return 0;
+}
+
+static int margin_lane_timing_open(struct inode *inode, struct file *file)
+{
+	return single_open(file, margin_lane_timing_show, inode->i_private);
+}
+
+static const struct file_operations margin_lane_timing_fops = {
+	.open = margin_lane_timing_open,
+	.read = seq_read,
+	.write = margin_lane_timing_write,
+	.llseek = seq_lseek,
+	.release = single_release,
+};
+
+static ssize_t margin_lane_voltage_write(struct file *file, const char __user *user_buf,
+				     size_t count, loff_t *ppos)
+{
+	struct pci_margin_lane *plane = ((struct seq_file *)file->private_data)->private;
+	struct pci_margin_dev *mdev = plane->mdev;
+	struct pci_margin_rx_info *info;
+	int val, ret;
+	u8 step, dir;
+	u16 sts;
+	u8 caps;
+
+	ret = kstrtoint_from_user(user_buf, count, 0, &val);
+	if (ret)
+		return ret;
+
+	if (val > LMR_MAX_VOLTAGE_STEP || val < -LMR_MAX_VOLTAGE_STEP)
+		return -EINVAL;
+
+	mutex_lock(&mdev->lock);
+	if (!mdev->enabled) {
+		ret = -EACCES;
+		goto out;
+	}
+
+	if (val == 0) {
+		ret = pci_lmr_demargin_lane(plane);
+		goto out;
+	}
+
+	ret = pci_lmr_cache_rx_info(plane, plane->rx);
+	if (ret)
+		goto out;
+
+	if (!pci_lmr_check_sample_multiple_rx(mdev, plane)) {
+		ret = -EBUSY;
+		goto out;
+	}
+
+	info = &plane->rx_info[plane->rx];
+	caps = info->caps;
+
+	if (val < 0) {
+		step = -val;
+		if (!(caps & LMR_CAP_IND_UP_DOWN_VOLTAGE)) {
+			/* Symmetric voltage margining requires dir=1 per spec */
+			ret = -EINVAL;
+			goto out;
+		}
+		dir = 0;
+	} else {
+		step = val;
+		dir = 1;
+	}
+
+	if (step > info->num_voltage_steps) {
+		ret = -EINVAL;
+		goto out;
+	}
+
+	ret = pci_lmr_run_cmd(mdev->dev, plane->lane, plane->rx,
+			      LMR_TYPE_VOLTAGE, 0,
+			      (step & LMR_VOLTAGE_STEP_MASK) |
+			      (dir << LMR_VOLTAGE_DIR_SHIFT), &sts);
+	if (ret)
+		goto out;
+
+	step = FIELD_GET(PCI_LMR_LANE_STS_PAYLOAD, sts) & LMR_VOLTAGE_STEP_MASK;
+	plane->voltage_val = (dir == 0) ? -step : step;
+
+out:
+	mutex_unlock(&mdev->lock);
+	return ret ? ret : count;
+}
+
+static int margin_lane_voltage_show(struct seq_file *s, void *v)
+{
+	struct pci_margin_lane *plane = s->private;
+	int voltage_val;
+
+	mutex_lock(&plane->mdev->lock);
+	voltage_val = plane->voltage_val;
+	mutex_unlock(&plane->mdev->lock);
+
+	seq_printf(s, "%d\n", voltage_val);
+	return 0;
+}
+
+static int margin_lane_voltage_open(struct inode *inode, struct file *file)
+{
+	return single_open(file, margin_lane_voltage_show, inode->i_private);
+}
+
+static const struct file_operations margin_lane_voltage_fops = {
+	.open = margin_lane_voltage_open,
+	.read = seq_read,
+	.write = margin_lane_voltage_write,
+	.llseek = seq_lseek,
+	.release = single_release,
+};
+
+static void pci_margin_debugfs_init(struct pci_margin_dev *mdev)
+{
+	struct pci_dev *dev = mdev->dev;
+	struct dentry *parent;
+	char dirname[64];
+	int i;
+
+	parent = get_pci_debugfs_root();
+	snprintf(dirname, sizeof(dirname), "pcie_lmr_%s", dev_name(&dev->dev));
+	mdev->debugfs = debugfs_create_dir(dirname, parent);
+	if (!mdev->debugfs)
+		return;
+
+	debugfs_create_file("capabilities", 0444, mdev->debugfs, mdev, &margin_caps_fops);
+	debugfs_create_file("port_status", 0444, mdev->debugfs, mdev, &margin_port_status_fops);
+	debugfs_create_file("enable", 0644, mdev->debugfs, mdev, &margin_enable_fops);
+
+	for (i = 0; i < mdev->num_lanes; i++) {
+		struct pci_margin_lane *plane = &mdev->lanes[i];
+		struct dentry *lane_dir;
+		char lane_name[16];
+
+		snprintf(lane_name, sizeof(lane_name), "lane%d", i);
+		lane_dir = debugfs_create_dir(lane_name, mdev->debugfs);
+		if (!lane_dir)
+			continue;
+
+		debugfs_create_file("receiver", 0644, lane_dir, plane, &margin_lane_receiver_fops);
+		debugfs_create_file("caps", 0444, lane_dir, plane, &margin_lane_caps_fops);
+		debugfs_create_file("num_timing_steps", 0444, lane_dir, plane,
+				    &margin_lane_timing_steps_fops);
+		debugfs_create_file("num_voltage_steps", 0444, lane_dir, plane,
+				    &margin_lane_voltage_steps_fops);
+		debugfs_create_file("margin_timing", 0644, lane_dir, plane,
+				    &margin_lane_timing_fops);
+		debugfs_create_file("margin_voltage", 0644, lane_dir, plane,
+				    &margin_lane_voltage_fops);
+	}
+}
+
+static void pci_margin_debugfs_remove(struct pci_margin_dev *mdev)
+{
+	debugfs_remove_recursive(mdev->debugfs);
+}
+
+#else
+static inline void pci_margin_debugfs_init(struct pci_margin_dev *mdev) { }
+static inline void pci_margin_debugfs_remove(struct pci_margin_dev *mdev) { }
+#endif
+
+void pci_lmr_init(struct pci_dev *dev)
+{
+	struct pci_margin_dev *mdev;
+	enum pci_bus_speed speed;
+	u16 lmr;
+	u16 lnkcap;
+	int i;
+
+	if (!pci_is_pcie(dev))
+		return;
+
+	speed = pcie_get_speed_cap(dev);
+
+	lmr = pci_find_ext_capability(dev, PCI_EXT_CAP_ID_LMR);
+	if (!lmr) {
+		if (speed == PCIE_SPEED_64_0GT)
+			pci_warn(dev,
+				 "Missing Lane Margining at Receiver Capability (mandatory for Gen6+)\n");
+		else if (speed >= PCIE_SPEED_16_0GT && speed != PCI_SPEED_UNKNOWN)
+			pci_dbg(dev,
+				"Optional Lane Margining at Receiver Capability not found\n");
+		return;
+	}
+
+	if (speed < PCIE_SPEED_16_0GT && speed != PCI_SPEED_UNKNOWN)
+		return;
+
+	dev->lmr_cap = lmr;
+
+	mdev = kzalloc_obj(*mdev, GFP_KERNEL);
+	if (!mdev)
+		return;
+
+	mdev->dev = dev;
+	mdev->cap = lmr;
+	mutex_init(&mdev->lock);
+
+	pcie_capability_read_word(dev, PCI_EXP_LNKCAP, &lnkcap);
+	mdev->num_lanes = FIELD_GET(PCI_EXP_LNKCAP_MLW, lnkcap);
+
+	if (mdev->num_lanes == 0 || mdev->num_lanes > LMR_MAX_LANES) {
+		pci_warn(dev, "Invalid link width %d for LMR\n", mdev->num_lanes);
+		goto err_free_mdev;
+	}
+
+	mdev->lanes = kcalloc(mdev->num_lanes, sizeof(*mdev->lanes), GFP_KERNEL);
+	if (!mdev->lanes)
+		goto err_free_mdev;
+
+	for (i = 0; i < mdev->num_lanes; i++) {
+		mdev->lanes[i].mdev = mdev;
+		mdev->lanes[i].lane = i;
+		mdev->lanes[i].rx = LMR_RX_LOCAL;
+	}
+
+	pci_margin_debugfs_init(mdev);
+
+	dev->lmr = mdev;
+
+	pci_info(dev, "Lane Margining at Receiver (Gen%u) Capability detected\n",
+		 speed == PCIE_SPEED_64_0GT ? LMR_GEN6 :
+		 speed == PCIE_SPEED_32_0GT ? LMR_GEN5 :
+		 LMR_GEN4);
+	return;
+
+err_free_mdev:
+	mutex_destroy(&mdev->lock);
+	kfree(mdev);
+}
+
+void pci_lmr_exit(struct pci_dev *dev)
+{
+	struct pci_margin_dev *mdev = dev->lmr;
+	int i;
+
+	if (!mdev)
+		return;
+
+	mutex_lock(&mdev->lock);
+	if (mdev->enabled) {
+		for (i = 0; i < mdev->num_lanes; i++)
+			pci_lmr_demargin_lane(&mdev->lanes[i]);
+	}
+	mutex_unlock(&mdev->lock);
+
+	pci_margin_debugfs_remove(mdev);
+	mutex_destroy(&mdev->lock);
+	kfree(mdev->lanes);
+	kfree(mdev);
+	dev->lmr = NULL;
+}
+
+void pci_suspend_lmr(struct pci_dev *dev)
+{
+	struct pci_margin_dev *mdev = dev->lmr;
+	int i;
+	u16 sts;
+
+	if (!mdev)
+		return;
+
+	mutex_lock(&mdev->lock);
+	if (mdev->enabled) {
+		for (i = 0; i < mdev->num_lanes; i++)
+			pci_lmr_demargin_lane(&mdev->lanes[i]);
+
+		if (pci_read_config_word(dev, mdev->cap + PCI_LMR_PORT_STS, &sts) == 0) {
+			sts &= ~PCI_LMR_PORT_STS_SW_READY;
+			pci_write_config_word(dev, mdev->cap + PCI_LMR_PORT_STS, sts);
+		}
+		mdev->enabled = false;
+	}
+	mutex_unlock(&mdev->lock);
+}
+
+void pci_reset_lmr(struct pci_dev *dev)
+{
+	pci_suspend_lmr(dev);
+}
diff --git a/drivers/pci/probe.c b/drivers/pci/probe.c
index dd0abbc63e18..352b95568ebf 100644
--- a/drivers/pci/probe.c
+++ b/drivers/pci/probe.c
@@ -2666,6 +2666,7 @@ static void pci_init_capabilities(struct pci_dev *dev)
 	pci_pasid_init(dev);		/* Process Address Space ID */
 	pci_acs_init(dev);		/* Access Control Services */
 	pci_ptm_init(dev);		/* Precision Time Measurement */
+	pci_lmr_init(dev);		/* Lane Margining at Receiver */
 	pci_aer_init(dev);		/* Advanced Error Reporting */
 	pci_dpc_init(dev);		/* Downstream Port Containment */
 	pci_rcec_init(dev);		/* Root Complex Event Collector */
diff --git a/drivers/pci/remove.c b/drivers/pci/remove.c
index d8bffa21498a..6fba29040e44 100644
--- a/drivers/pci/remove.c
+++ b/drivers/pci/remove.c
@@ -36,6 +36,7 @@ static void pci_destroy_dev(struct pci_dev *dev)
 
 	pci_doe_sysfs_teardown(dev);
 	pci_npem_remove(dev);
+	pci_lmr_exit(dev);
 
 	/*
 	 * While device is in D0 drop the device from TSM link operations
diff --git a/include/linux/pci.h b/include/linux/pci.h
index 64b308b6e61c..ef1275f4c5b6 100644
--- a/include/linux/pci.h
+++ b/include/linux/pci.h
@@ -349,6 +349,8 @@ struct rcec_ea;
  *			number resources to allow for hierarchy expansion.
  * @is_pciehp:		PCIe Hot-Plug Capable bridge.
  */
+struct pci_margin_dev;
+
 struct pci_dev {
 	struct list_head bus_list;	/* Node in per-bus list */
 	struct pci_bus	*bus;		/* Bus this device is on */
@@ -528,6 +530,10 @@ struct pci_dev {
 	atomic_t	ptm_enable_cnt;
 	u8		ptm_granularity;
 #endif
+#ifdef CONFIG_PCIE_LMR
+	u16			lmr_cap;	/* Lane Margining Capability */
+	struct pci_margin_dev	*lmr;
+#endif
 #ifdef CONFIG_PCI_MSI
 	void __iomem	*msix_base;
 	raw_spinlock_t	msi_lock;
diff --git a/include/uapi/linux/pci_regs.h b/include/uapi/linux/pci_regs.h
index facaa324bd86..90cbe310e62f 100644
--- a/include/uapi/linux/pci_regs.h
+++ b/include/uapi/linux/pci_regs.h
@@ -757,6 +757,7 @@
 #define PCI_EXT_CAP_ID_VF_REBAR 0x24	/* VF Resizable BAR */
 #define PCI_EXT_CAP_ID_DLF	0x25	/* Data Link Feature */
 #define PCI_EXT_CAP_ID_PL_16GT	0x26	/* Physical Layer 16.0 GT/s */
+#define PCI_EXT_CAP_ID_LMR	0x27	/* Lane Margining at Receiver */
 #define PCI_EXT_CAP_ID_NPEM	0x29	/* Native PCIe Enclosure Management */
 #define PCI_EXT_CAP_ID_PL_32GT  0x2A    /* Physical Layer 32.0 GT/s */
 #define PCI_EXT_CAP_ID_DOE	0x2E	/* Data Object Exchange */
@@ -1181,6 +1182,23 @@
 #define  PCI_PL_16GT_LE_CTRL_USP_TX_PRESET_MASK		0x000000F0
 #define  PCI_PL_16GT_LE_CTRL_USP_TX_PRESET_SHIFT	4
 
+/* Lane Margining at Receiver */
+#define PCI_LMR_PORT_CAP		0x04	/* Margining Port Capabilities */
+#define  PCI_LMR_PORT_CAP_USES_SW_READY	0x0001	/* Margining Uses Software Ready */
+#define PCI_LMR_PORT_STS		0x06	/* Margining Port Status */
+#define  PCI_LMR_PORT_STS_MARGIN_READY	0x0001	/* Margining Ready */
+#define  PCI_LMR_PORT_STS_SW_READY	0x0002	/* Margining SW Ready */
+#define PCI_LMR_LANE_CTRL		0x08	/* Margining Lane Control */
+#define  PCI_LMR_LANE_CTRL_RX_NUM	0x0007	/* Receiver Number */
+#define  PCI_LMR_LANE_CTRL_MTYPE	0x0038	/* Margining Type */
+#define  PCI_LMR_LANE_CTRL_USAGE	0x0040	/* Margining Usage Model */
+#define  PCI_LMR_LANE_CTRL_PAYLOAD	0xFF00	/* Margining Payload */
+#define PCI_LMR_LANE_STS		0x0A	/* Margining Lane Status */
+#define  PCI_LMR_LANE_STS_RX_NUM	0x0007	/* Receiver Number */
+#define  PCI_LMR_LANE_STS_MTYPE		0x0038	/* Margining Type */
+#define  PCI_LMR_LANE_STS_USAGE		0x0040	/* Margining Usage Model */
+#define  PCI_LMR_LANE_STS_PAYLOAD	0xFF00	/* Margining Payload */
+
 /* Physical Layer 32.0 GT/s */
 #define PCI_PL_32GT_LE_CTRL	0x20	/* Lane Equalization Control Register */
 
diff --git a/tools/testing/selftests/Makefile b/tools/testing/selftests/Makefile
index 8a4b6ddc68df..6990d999388a 100644
--- a/tools/testing/selftests/Makefile
+++ b/tools/testing/selftests/Makefile
@@ -91,6 +91,7 @@ TARGETS += net/tcp_ao
 TARGETS += nolibc
 TARGETS += pci_endpoint
 TARGETS += pcie_bwctrl
+TARGETS += pcie_lmt
 TARGETS += perf_events
 TARGETS += pidfd
 TARGETS += pid_namespace
diff --git a/tools/testing/selftests/pcie_lmt/Makefile b/tools/testing/selftests/pcie_lmt/Makefile
new file mode 100644
index 000000000000..36ac85937d78
--- /dev/null
+++ b/tools/testing/selftests/pcie_lmt/Makefile
@@ -0,0 +1,3 @@
+# SPDX-License-Identifier: GPL-2.0
+TEST_PROGS = pcie_lmt.sh
+include ../lib.mk
diff --git a/tools/testing/selftests/pcie_lmt/pcie_lmt.sh b/tools/testing/selftests/pcie_lmt/pcie_lmt.sh
new file mode 100755
index 000000000000..22c00c2b8956
--- /dev/null
+++ b/tools/testing/selftests/pcie_lmt/pcie_lmt.sh
@@ -0,0 +1,105 @@
+#!/bin/bash
+# SPDX-License-Identifier: GPL-2.0
+#
+# Copyright (C) 2026 Google LLC
+# Author: Priyank Rathod <rathodpriyank@google.com>
+#
+# Kselftest for PCIe Lane Margining at Receiver (LMR / LMT)
+# Tests the debugfs interface exposed by drivers/pci/pcie/margin.c
+# (/sys/kernel/debug/pci/pcie_lmr_<pci_dev_name>/)
+
+set -e
+
+TESTNAME="pcie_lmt"
+
+# Kselftest framework requirement - SKIP code is 4.
+ksft_skip=4
+retval=0
+skipmsg="skip all tests:"
+
+if [ $UID != 0 ]; then
+	echo "$skipmsg must be run as root" >&2
+	exit $ksft_skip
+fi
+
+DEBUGFS=$(mount -t debugfs | head -1 | awk '{ print $3 }')
+if [ -z "$DEBUGFS" ]; then
+	if [ -d "/sys/kernel/debug" ]; then
+		DEBUGFS="/sys/kernel/debug"
+	else
+		echo "$skipmsg debugfs is not mounted" >&2
+		exit $ksft_skip
+	fi
+fi
+
+if [ ! -d "$DEBUGFS/pci" ]; then
+	# Allow searching debugfs root or pci directory
+	:
+fi
+
+LMR_DEVS=$(ls -d $DEBUGFS/pci/pcie_lmr_* $DEBUGFS/pcie_lmr_* 2>/dev/null || true)
+if [ -z "$LMR_DEVS" ]; then
+	echo "$skipmsg no PCIe LMR devices found in $DEBUGFS/" >&2
+	exit $ksft_skip
+fi
+
+cleanup_dev()
+{
+	local dev="$1"
+	echo 0 > "$dev/enable" 2>/dev/null || true
+}
+
+echo "$TESTNAME: testing PCIe LMR debugfs entries"
+
+for dev in $LMR_DEVS; do
+	dev_name=$(basename "$dev")
+	echo "$TESTNAME: probing device $dev_name"
+
+	if [ ! -r "$dev/capabilities" ] || [ ! -r "$dev/port_status" ] ||
+	   [ ! -r "$dev/enable" ] || [ ! -w "$dev/enable" ]; then
+		echo "$TESTNAME: $dev_name missing mandatory root attributes"
+		retval=1
+		continue
+	fi
+
+	caps=$(cat "$dev/capabilities")
+	status=$(cat "$dev/port_status")
+	echo "  $dev_name: capabilities read OK"
+	echo "  $dev_name: port_status read OK"
+
+	trap 'cleanup_dev "$dev"' EXIT
+
+	if ! echo 1 > "$dev/enable" 2>/dev/null; then
+		echo "  $dev_name: margining not ready by hardware (skipping active lanes)"
+		continue
+	fi
+
+	echo "  $dev_name: margining enabled OK"
+
+	for lane_dir in $(ls -d "$dev"/lane* 2>/dev/null || true); do
+		lane=$(basename "$lane_dir")
+		echo "  $dev_name: testing $lane"
+
+		# Test setting receiver (Rx 0 is always local receiver)
+		echo 0 > "$lane_dir/receiver"
+		cat "$lane_dir/caps" > /dev/null
+		cat "$lane_dir/num_timing_steps" > /dev/null
+		cat "$lane_dir/num_voltage_steps" > /dev/null
+
+		# Test resetting timing and voltage margin
+		echo 0 > "$lane_dir/margin_timing"
+		echo 0 > "$lane_dir/margin_voltage"
+	done
+
+	echo 0 > "$dev/enable"
+	trap - EXIT
+	echo "  $dev_name: margining disabled OK"
+done
+
+if [ $retval -eq 0 ]; then
+	echo "$TESTNAME [PASS]"
+else
+	echo "$TESTNAME [FAIL]"
+fi
+
+exit $retval

---
base-commit: 0f23d56f17fdfc7db69d51f64c8b91bbab947aa9
change-id: 20260818-pcie-lmt-3044d586aaec

Best regards,
-- 
Priyank Rathod <rathodpriyank@google.com>

^ permalink raw reply related	[flat|nested] 9+ messages in thread

* Re: [PATCH v4] PCI/pcie: Add PCIe Lane Margining at Receiver (LMR) support
  2026-08-18 21:20 ` [PATCH v4] " Priyank Rathod
@ 2026-08-18 21:32   ` sashiko-bot
  2026-08-18 22:29   ` Bjorn Helgaas
  1 sibling, 0 replies; 9+ messages in thread
From: sashiko-bot @ 2026-08-18 21:32 UTC (permalink / raw)
  To: Priyank Rathod; +Cc: linux-pci

Thank you for your contribution! Sashiko AI review found 7 potential issue(s) to consider:
- [High] Fast-path NAK detection instantly fails valid LMR commands due to stale NO_CMD status.
- [High] Teardown race in pci_lmr_exit() between hardware demargining and debugfs interface removal allows margining to be re-enabled during device destruction.
- [Medium] pci_reset_lmr() issues DEMARGIN commands after hardware reset, causing massive synchronous timeouts and kernel stalls.
- [Medium] Concurrent lockless initialization of pci_debugfs_root_dir can corrupt the global pointer and break LMR debugfs system-wide.
- [Medium] Software state loses sync with hardware if margin commands time out, skipping subsequent demargin operations.
- [Medium] Ignoring pci_lmr_demargin_lane() failure in margin_lane_receiver_write() causes state confusion and hardware state leak.
- [Medium] Iterating over the Maximum Link Width (MLW) instead of Negotiated Link Width (NLW) causes multi-second timeouts for inactive lanes.
--

--- Patch [1]: [PATCH v4] PCI/pcie: Add PCIe Lane Margining at Receiver (LMR) support ---
commit 08906bfa6b5ee5f4a64cb0f08b667e3fc86991d7
Author: Priyank Rathod <rathodpriyank@google.com>

PCI/pcie: Add PCIe Lane Margining at Receiver (LMR) support

Adds PCIe Lane Margining at Receiver (LMR) support, probing Extended Capability
ID 0x27 on Gen4+ links. It exposes debugfs interfaces for margining enablement,
receiver selection, and execution of timing/voltage margin step commands.

> diff --git a/drivers/pci/pci.c b/drivers/pci/pci.c
> index 77b17b13ee615..dc9724cb7b4d8 100644
> --- a/drivers/pci/pci.c
> +++ b/drivers/pci/pci.c
> @@ -5145,8 +5145,10 @@ int __pci_reset_function_locked(struct pci_dev *dev)
>  		method = &pci_reset_fn_methods[m];
>  		pci_dbg(dev, "reset via %s\n", method->name);
>  		rc = method->reset_fn(dev, PCI_RESET_DO_RESET);
> -		if (!rc)
> +		if (!rc) {
> +			pci_reset_lmr(dev);

[Severity: Medium]
Since this hook is called after the hardware reset has already wiped the
device state, won't this cause timeouts?

The pci_reset_lmr() function aliases to pci_suspend_lmr(), which
unconditionally loops over all margined lanes and issues DEMARGIN writes.
Because the device was just reset, its Margining SW Ready bit is 0,
causing the hardware to ignore the commands and the driver to stall
while waiting for the status register to update.

>  			return 0;
> +		}
>  
>  		pci_dbg(dev, "%s failed with %d\n", method->name, rc);

[ ... ]

> diff --git a/drivers/pci/pcie/margin.c b/drivers/pci/pcie/margin.c
> new file mode 100644
> index 0000000000000..e9e195744d5ad
> --- /dev/null
> +++ b/drivers/pci/pcie/margin.c

[ ... ]

> +static struct dentry *get_pci_debugfs_root(void)
> +{
> +	if (!pci_debugfs_root_dir)
> +		pci_debugfs_root_dir = debugfs_lookup("pci", NULL);
> +	if (!pci_debugfs_root_dir)
> +		pci_debugfs_root_dir = debugfs_create_dir("pci", NULL);

[Severity: Medium]
Could this lockless initialization race during concurrent probing?

If two threads enter simultaneously, both might see a NULL pointer and
attempt to create the directory. The second thread could receive
ERR_PTR(-EEXIST) and overwrite the valid pointer, breaking the debugfs
functionality system-wide for future LMR devices.

> +	return pci_debugfs_root_dir;
> +}

[ ... ]

> +static int pci_lmr_run_cmd(struct pci_dev *dev, int lane, u8 rx, u8 type,
> +			   u8 usage, u8 payload, u16 *status_val)
> +{

[ ... ]

> +		/*
> +		 * Per PCIe Base Spec r6.0 sec 8.4.4, if receiver echoes NO_CMD (0x7)
> +		 * after a command was sent, it indicates command rejection/NAK.
> +		 */
> +		if (FIELD_GET(PCI_LMR_LANE_STS_MTYPE, sts) == LMR_TYPE_NO_CMD &&
> +		    FIELD_GET(PCI_LMR_LANE_STS_RX_NUM, sts) == rx)
> +			return -EOPNOTSUPP;

[Severity: High]
Does this instantly abort valid commands due to a stale NO_CMD status?

The driver successfully issues a NO_CMD handshake earlier, leaving the
hardware status register with MTYPE = 0x7. It then issues the real command
and immediately polls the status register. Because hardware updates are
asynchronous, the first read likely returns the stale NO_CMD state, which
this check misinterprets as an immediate command rejection.

> +
> +		if (time_after(jiffies, timeout))
> +			break;

[ ... ]

> +static int pci_lmr_demargin_lane(struct pci_margin_lane *plane)
> +{
> +	u16 sts;
> +	int ret;
> +
> +	if (!plane || !plane->mdev)
> +		return -EINVAL;
> +
> +	if (plane->timing_val == 0 && plane->voltage_val == 0)
> +		return 0;

[Severity: Medium]
What happens if a previous margin command timed out before updating the
cached state?

If a prior command returns -ETIMEDOUT, the driver skips updating
plane->timing_val even if the hardware actually applied the margin. When
demargining is requested later, this check short-circuits because
timing_val is 0, completely skipping the hardware DEMARGIN command and
leaving the physical lane permanently margined.

> +
> +	ret = pci_lmr_run_cmd(plane->mdev->dev, plane->lane, plane->rx,
> +			      LMR_TYPE_DEMARGIN, 0, 0, &sts);

[ ... ]

> +static ssize_t margin_enable_write(struct file *file, const char __user *user_buf,
> +			       size_t count, loff_t *ppos)
> +{

[ ... ]

> +		/* Cache capabilities for local receiver on all lanes */
> +		for (i = 0; i < mdev->num_lanes; i++) {
> +			mdev->lanes[i].rx = LMR_RX_LOCAL;
> +			pci_lmr_cache_rx_info(&mdev->lanes[i], LMR_RX_LOCAL);
> +		}

[Severity: Medium]
Will this loop over physically inactive lanes if the negotiated link width
is less than the maximum link width?

If the device has downgraded links (for instance, an x16 card in an x4 slot),
these commands will silently time out for every inactive lane. Could we use
the Negotiated Link Width (PCI_EXP_LNKSTA_NLW) instead to avoid a multi-second
stall?

> +		mdev->enabled = true;

[ ... ]

> +static ssize_t margin_lane_receiver_write(struct file *file, const char __user *user_buf,
> +				      size_t count, loff_t *ppos)
> +{

[ ... ]

> +	if (mdev->enabled) {
> +		/* Demargin previous receiver per single-receiver spec rule */
> +		pci_lmr_demargin_lane(plane);
> +		ret = pci_lmr_cache_rx_info(plane, rx);
> +		if (ret)
> +			goto out;
> +	}
> +
> +	plane->rx = rx;

[Severity: Medium]
Is it safe to ignore the return value of pci_lmr_demargin_lane() here?

If the demargin command fails (e.g., via NAK or timeout), the active
timing_val and voltage_val are not cleared, but plane->rx is unconditionally
updated. This causes the new receiver to incorrectly inherit the margin state
of the old receiver, leaving the old receiver permanently margined in
hardware.

> +out:
> +	mutex_unlock(&mdev->lock);

[ ... ]

> +static ssize_t margin_lane_timing_write(struct file *file, const char __user *user_buf,
> +				    size_t count, loff_t *ppos)
> +{

[ ... ]

> +	ret = pci_lmr_run_cmd(mdev->dev, plane->lane, plane->rx,
> +			      LMR_TYPE_TIMING, 0,
> +			      (step & LMR_TIMING_STEP_MASK) | (dir << LMR_TIMING_DIR_SHIFT), &sts);
> +	if (ret)
> +		goto out;

[Severity: Medium]
When pci_lmr_run_cmd() times out, it returns without updating the cached
state. This leads to the software state losing sync with the hardware,
preventing subsequent demargin operations from executing successfully.

> +
> +	/* Record actual step count applied by hardware */
> +	step = FIELD_GET(PCI_LMR_LANE_STS_PAYLOAD, sts) & LMR_TIMING_STEP_MASK;
> +	plane->timing_val = (dir == LMR_STEP_DIR_DECREASE) ? -step : step;

[ ... ]

> +void pci_lmr_init(struct pci_dev *dev)
> +{

[ ... ]

> +	pcie_capability_read_word(dev, PCI_EXP_LNKCAP, &lnkcap);
> +	mdev->num_lanes = FIELD_GET(PCI_EXP_LNKCAP_MLW, lnkcap);

[Severity: Medium]
Does this allocate the array and set bounds based on the Maximum Link Width
instead of the active Negotiated Link Width?

If the active link is narrower than the capability, the driver will later
attempt to issue I/O commands to disconnected lanes, which will dependably
time out.

> +
> +	if (mdev->num_lanes == 0 || mdev->num_lanes > LMR_MAX_LANES) {

[ ... ]

> +void pci_lmr_exit(struct pci_dev *dev)
> +{
> +	struct pci_margin_dev *mdev = dev->lmr;
> +	int i;
> +
> +	if (!mdev)
> +		return;
> +
> +	mutex_lock(&mdev->lock);
> +	if (mdev->enabled) {
> +		for (i = 0; i < mdev->num_lanes; i++)
> +			pci_lmr_demargin_lane(&mdev->lanes[i]);
> +	}
> +	mutex_unlock(&mdev->lock);
> +
> +	pci_margin_debugfs_remove(mdev);

[Severity: High]
Does releasing the lock before disabling the state create a race condition
with debugfs writers?

Because mdev->enabled is not set to false and the SW_READY bit isn't
cleared, a concurrent debugfs writer blocked on mdev->lock could wake up
right after the unlock and issue new margining commands to the hardware
just before the driver tears down the structures.

> +	mutex_destroy(&mdev->lock);
> +	kfree(mdev->lanes);

-- 
Sashiko AI review · https://sashiko.dev/#/patchset/20260818-pcie-lmt-v4-1-eb17a6d755fa@google.com?part=1

^ permalink raw reply	[flat|nested] 9+ messages in thread

* Re: [PATCH v4] PCI/pcie: Add PCIe Lane Margining at Receiver (LMR) support
  2026-08-18 21:20 ` [PATCH v4] " Priyank Rathod
  2026-08-18 21:32   ` sashiko-bot
@ 2026-08-18 22:29   ` Bjorn Helgaas
  1 sibling, 0 replies; 9+ messages in thread
From: Bjorn Helgaas @ 2026-08-18 22:29 UTC (permalink / raw)
  To: Priyank Rathod
  Cc: Bjorn Helgaas, Shuah Khan, linux-kernel, linux-pci,
	linux-kselftest

On Tue, Aug 18, 2026 at 09:20:24PM +0000, Priyank Rathod wrote:
> Per PCIe Base Specification r6.0, sec 8.4.4 ("Lane Margining at
> Receiver"), PCIe devices operating at 16.0 GT/s (Gen 4) or higher data
> rates support the Lane Margining at Receiver Extended Capability
> (ID 0x27), and it is mandatory for receivers operating at 64.0 GT/s
> (Gen 6) or higher data rates. Lane Margining allows software to
> evaluate high-speed link margins by measuring timing and voltage steps
> for each individual physical lane and receiver.

  Aug 18 13:57 Priyank Rathod  [PATCH] PCI/pcie: Add PCIe Lane ...
  Aug 18 14:26 Priyank Rathod  ├─>[PATCH v2] PCI/pcie: Add PCIe Lane ...
  Aug 18 15:49 Priyank Rathod  ├─>[PATCH v3] PCI/pcie: Add PCIe Lane ...
  Aug 18 16:20 Priyank Rathod  └─>[PATCH v4] PCI/pcie: Add PCIe Lane ...

No need to rush:
https://git.kernel.org/pub/scm/linux/kernel/git/torvalds/linux.git/tree/Documentation/process/submitting-patches.rst?id=v7.2#n361

The v7.3 merge window is open now, so this is likely v7.4 material.

^ permalink raw reply	[flat|nested] 9+ messages in thread

end of thread, other threads:[~2026-08-18 22:29 UTC | newest]

Thread overview: 9+ messages (download: mbox.gz follow: Atom feed
-- links below jump to the message on this page --
2026-08-18 18:57 [PATCH] PCI/pcie: Add PCIe Lane Margining at Receiver (LMR) support Priyank Rathod
2026-08-18 19:10 ` sashiko-bot
2026-08-18 19:26 ` [PATCH v2] " Priyank Rathod
2026-08-18 20:00   ` sashiko-bot
2026-08-18 20:49 ` [PATCH v3] " Priyank Rathod
2026-08-18 21:00   ` sashiko-bot
2026-08-18 21:20 ` [PATCH v4] " Priyank Rathod
2026-08-18 21:32   ` sashiko-bot
2026-08-18 22:29   ` Bjorn Helgaas

This is an external index of several public inboxes,
see mirroring instructions on how to clone and mirror
all data and code used by this external index.