* [PATCH v2 01/13] cxl/pci: Make the HDM and Mem_Enable writes callable from CXL
2026-08-25 2:26 [PATCH v2 00/13] Make SBR work for CXL Downstream Ports Fabio M. De Francesco
@ 2026-08-25 2:26 ` Fabio M. De Francesco
2026-08-25 2:26 ` [PATCH v2 02/13] cxl/hdm: Add function to restore one HDM decoder Fabio M. De Francesco
` (11 subsequent siblings)
12 siblings, 0 replies; 15+ messages in thread
From: Fabio M. De Francesco @ 2026-08-25 2:26 UTC (permalink / raw)
To: linux-cxl
Cc: David Hildenbrand, Oscar Salvador, Greg Kroah-Hartman,
Rafael J . Wysocki, Danilo Krummrich, Davidlohr Bueso,
Jonathan Cameron, Dave Jiang, Alison Schofield, Vishal Verma,
Ira Weiny, Li Ming, Bjorn Helgaas, Mahesh J Salgaonkar,
Oliver O'Halloran, Andrew Morton, Lorenzo Stoakes,
Liam R . Howlett, Vlastimil Babka, Mike Rapoport,
Suren Baghdasaryan, Michal Hocko, linux-kernel, linux-mm,
driver-core, linux-pci, linuxppc-dev, Fabio M. De Francesco
Split the Global Control write out of devm_cxl_enable_hdm() as
cxl_enable_hdm() and drop the static from cxl_set_mem_enable(), so both
are reachable from the rest of cxl_core. Declare them in core.h.
Signed-off-by: Fabio M. De Francesco <fabio.m.de.francesco@linux.intel.com>
---
drivers/cxl/core/core.h | 2 ++
drivers/cxl/core/pci.c | 19 ++++++++++++++-----
2 files changed, 16 insertions(+), 5 deletions(-)
diff --git a/drivers/cxl/core/core.h b/drivers/cxl/core/core.h
index 07555ae63859..4dc324f019ab 100644
--- a/drivers/cxl/core/core.h
+++ b/drivers/cxl/core/core.h
@@ -213,6 +213,8 @@ int cxl_gpf_port_setup(struct cxl_dport *dport);
struct cxl_hdm;
int cxl_hdm_decode_init(struct cxl_dev_state *cxlds, struct cxl_hdm *cxlhdm,
struct cxl_endpoint_dvsec_info *info);
+void cxl_enable_hdm(struct cxl_hdm *cxlhdm, u32 global_ctrl);
+int cxl_set_mem_enable(struct cxl_dev_state *cxlds, u16 val);
int cxl_port_get_possible_dports(struct cxl_port *port);
#ifdef CONFIG_CXL_FEATURES
diff --git a/drivers/cxl/core/pci.c b/drivers/cxl/core/pci.c
index e4338fd7e01b..a7a2b84293e9 100644
--- a/drivers/cxl/core/pci.c
+++ b/drivers/cxl/core/pci.c
@@ -179,7 +179,7 @@ int cxl_await_media_ready(struct cxl_dev_state *cxlds)
}
EXPORT_SYMBOL_NS_GPL(cxl_await_media_ready, "CXL");
-static int cxl_set_mem_enable(struct cxl_dev_state *cxlds, u16 val)
+int cxl_set_mem_enable(struct cxl_dev_state *cxlds, u16 val)
{
struct pci_dev *pdev = to_pci_dev(cxlds->dev);
int d = cxlds->cxl_dvsec;
@@ -247,14 +247,23 @@ static void disable_hdm(void *_cxlhdm)
hdm + CXL_HDM_DECODER_CTRL_OFFSET);
}
-static int devm_cxl_enable_hdm(struct device *host, struct cxl_hdm *cxlhdm)
+/*
+ * @global_ctrl is the CXL HDM Decoder Global Control value to enable decode in.
+ * A caller restoring decode after a reset passes the value it saved, so the
+ * fields the driver does not model are not left at their reset defaults.
+ */
+void cxl_enable_hdm(struct cxl_hdm *cxlhdm, u32 global_ctrl)
{
void __iomem *hdm = cxlhdm->regs.hdm_decoder;
- u32 global_ctrl;
- global_ctrl = readl(hdm + CXL_HDM_DECODER_CTRL_OFFSET);
writel(global_ctrl | CXL_HDM_DECODER_ENABLE,
hdm + CXL_HDM_DECODER_CTRL_OFFSET);
+}
+
+static int devm_cxl_enable_hdm(struct device *host, struct cxl_hdm *cxlhdm,
+ u32 global_ctrl)
+{
+ cxl_enable_hdm(cxlhdm, global_ctrl);
return devm_add_action_or_reset(host, disable_hdm, cxlhdm);
}
@@ -398,7 +407,7 @@ int cxl_hdm_decode_init(struct cxl_dev_state *cxlds, struct cxl_hdm *cxlhdm,
* enable and use the HDM Decoder Capability registers.
*/
if (!info->mem_enabled) {
- rc = devm_cxl_enable_hdm(&port->dev, cxlhdm);
+ rc = devm_cxl_enable_hdm(&port->dev, cxlhdm, global_ctrl);
if (rc)
return rc;
--
2.55.0
^ permalink raw reply related [flat|nested] 15+ messages in thread* [PATCH v2 02/13] cxl/hdm: Add function to restore one HDM decoder
2026-08-25 2:26 [PATCH v2 00/13] Make SBR work for CXL Downstream Ports Fabio M. De Francesco
2026-08-25 2:26 ` [PATCH v2 01/13] cxl/pci: Make the HDM and Mem_Enable writes callable from CXL Fabio M. De Francesco
@ 2026-08-25 2:26 ` Fabio M. De Francesco
2026-08-25 2:26 ` [PATCH v2 03/13] cxl/hdm: Add function to restore CXL.mem decode Fabio M. De Francesco
` (10 subsequent siblings)
12 siblings, 0 replies; 15+ messages in thread
From: Fabio M. De Francesco @ 2026-08-25 2:26 UTC (permalink / raw)
To: linux-cxl
Cc: David Hildenbrand, Oscar Salvador, Greg Kroah-Hartman,
Rafael J . Wysocki, Danilo Krummrich, Davidlohr Bueso,
Jonathan Cameron, Dave Jiang, Alison Schofield, Vishal Verma,
Ira Weiny, Li Ming, Bjorn Helgaas, Mahesh J Salgaonkar,
Oliver O'Halloran, Andrew Morton, Lorenzo Stoakes,
Liam R . Howlett, Vlastimil Babka, Mike Rapoport,
Suren Baghdasaryan, Michal Hocko, linux-kernel, linux-mm,
driver-core, linux-pci, linuxppc-dev, Fabio M. De Francesco
Add cxl_decoder_recommit() to restore one HDM decoder that the driver
holds as committed but hardware no longer reports Committed, rewriting the
cached settings and committing it again.
Restoring the decoders below a reset Port means reprogramming each one
along the path to every affected endpoint. cxl_decoder_recommit() does that
for a single decoder, so it will be reused later for the full path.
Signed-off-by: Fabio M. De Francesco <fabio.m.de.francesco@linux.intel.com>
---
drivers/cxl/core/core.h | 1 +
drivers/cxl/core/hdm.c | 71 +++++++++++++++++++++++++++++++++++++++++
2 files changed, 72 insertions(+)
diff --git a/drivers/cxl/core/core.h b/drivers/cxl/core/core.h
index 4dc324f019ab..6d536fe7b446 100644
--- a/drivers/cxl/core/core.h
+++ b/drivers/cxl/core/core.h
@@ -215,6 +215,7 @@ int cxl_hdm_decode_init(struct cxl_dev_state *cxlds, struct cxl_hdm *cxlhdm,
struct cxl_endpoint_dvsec_info *info);
void cxl_enable_hdm(struct cxl_hdm *cxlhdm, u32 global_ctrl);
int cxl_set_mem_enable(struct cxl_dev_state *cxlds, u16 val);
+int cxl_decoder_recommit(struct cxl_decoder *cxld, u32 ctrl);
int cxl_port_get_possible_dports(struct cxl_port *port);
#ifdef CONFIG_CXL_FEATURES
diff --git a/drivers/cxl/core/hdm.c b/drivers/cxl/core/hdm.c
index 0c80b76a5f9b..d55d764895f8 100644
--- a/drivers/cxl/core/hdm.c
+++ b/drivers/cxl/core/hdm.c
@@ -849,6 +849,77 @@ static int cxl_decoder_commit(struct cxl_decoder *cxld)
return 0;
}
+/**
+ * cxl_decoder_recommit - reprogram @cxld's HDM decoder registers and commit
+ * @cxld: decoder to reprogram from its cached settings
+ * @ctrl: CXL HDM Decoder n Control value to restore under the cached settings
+ *
+ * A reset of an upstream link clears the HDM decoder registers of every
+ * component below it, dropping Committed while the driver still holds the
+ * settings that were in effect. Restore those settings and commit.
+ *
+ * setup_hw_decoder() rewrites only Interleave Granularity, Interleave Ways and
+ * Target Range Type, so the rest of the control register would come from a read
+ * of the reset defaults. Per CXL r4.0 sec 8.2.4.20.7 Table 8-123 that register
+ * also holds BI, UIO, Upstream Interleave Granularity, Upstream Interleave Ways
+ * and Lock On Commit, none of which the driver models, and sec 8.2.4.20.12 makes
+ * device operation undefined if a device that requires BI is committed without
+ * it. Write @ctrl first so those fields are in place, with Commit masked off
+ * until setup_hw_decoder() has written the range.
+ *
+ * A decoder that hardware still reports Committed kept its programming across
+ * the reset and needs no work. A decoder with no ->commit is driven through the
+ * DVSEC ranges or is a passthrough decoder, and has no registers to program.
+ *
+ * Section 8.2.4.20.13 requires the traffic targeting @cxld to be quiesced while
+ * it is reprogrammed, which the caller owns. @cxld decodes nothing until the
+ * commit completes.
+ *
+ * Return: 0 on success or if @cxld needs no reprogramming, negative errno if the
+ * commit times out or if the hardware reports a commit error.
+ */
+int cxl_decoder_recommit(struct cxl_decoder *cxld, u32 ctrl)
+{
+ struct cxl_port *port = to_cxl_port(cxld->dev.parent);
+ struct cxl_hdm *cxlhdm = dev_get_drvdata(&port->dev);
+ void __iomem *hdm = cxlhdm->regs.hdm_decoder;
+ u32 hw_ctrl;
+ int rc;
+
+ if ((cxld->flags & CXL_DECODER_F_ENABLE) == 0)
+ return 0;
+
+ if (!cxld->commit)
+ return 0;
+
+ hw_ctrl = readl(hdm + CXL_HDM_DECODER0_CTRL_OFFSET(cxld->id));
+ if (FIELD_GET(CXL_HDM_DECODER0_CTRL_COMMITTED, hw_ctrl)) {
+ dev_dbg(&cxld->dev, "%s: still committed, no reprogram needed\n",
+ __func__);
+ return 0;
+ }
+
+ writel(ctrl & ~(CXL_HDM_DECODER0_CTRL_COMMIT |
+ CXL_HDM_DECODER0_CTRL_COMMITTED |
+ CXL_HDM_DECODER0_CTRL_COMMIT_ERROR),
+ hdm + CXL_HDM_DECODER0_CTRL_OFFSET(cxld->id));
+
+ scoped_guard(rwsem_read, &cxl_rwsem.dpa)
+ setup_hw_decoder(cxld, hdm);
+
+ rc = cxld_await_commit(hdm, cxld->id);
+ if (rc) {
+ dev_warn(&cxld->dev, "%s: failed to commit decoder: %d\n",
+ __func__, rc);
+ return rc;
+ }
+
+ dev_dbg(&cxld->dev, "%s: reprogrammed HPA %#llx-%#llx\n",
+ __func__, cxld->hpa_range.start, cxld->hpa_range.end);
+
+ return 0;
+}
+
static int commit_reap(struct device *dev, void *data)
{
struct cxl_port *port = to_cxl_port(dev->parent);
--
2.55.0
^ permalink raw reply related [flat|nested] 15+ messages in thread* [PATCH v2 03/13] cxl/hdm: Add function to restore CXL.mem decode
2026-08-25 2:26 [PATCH v2 00/13] Make SBR work for CXL Downstream Ports Fabio M. De Francesco
2026-08-25 2:26 ` [PATCH v2 01/13] cxl/pci: Make the HDM and Mem_Enable writes callable from CXL Fabio M. De Francesco
2026-08-25 2:26 ` [PATCH v2 02/13] cxl/hdm: Add function to restore one HDM decoder Fabio M. De Francesco
@ 2026-08-25 2:26 ` Fabio M. De Francesco
2026-08-25 2:26 ` [PATCH v2 04/13] cxl/hdm: Reprogram the HDM Decoders below a CXL Port Fabio M. De Francesco
` (9 subsequent siblings)
12 siblings, 0 replies; 15+ messages in thread
From: Fabio M. De Francesco @ 2026-08-25 2:26 UTC (permalink / raw)
To: linux-cxl
Cc: David Hildenbrand, Oscar Salvador, Greg Kroah-Hartman,
Rafael J . Wysocki, Danilo Krummrich, Davidlohr Bueso,
Jonathan Cameron, Dave Jiang, Alison Schofield, Vishal Verma,
Ira Weiny, Li Ming, Bjorn Helgaas, Mahesh J Salgaonkar,
Oliver O'Halloran, Andrew Morton, Lorenzo Stoakes,
Liam R . Howlett, Vlastimil Babka, Mike Rapoport,
Suren Baghdasaryan, Michal Hocko, linux-kernel, linux-mm,
driver-core, linux-pci, linuxppc-dev, Fabio M. De Francesco
A reset of a CXL Downstream Port clears HDM Decoder Enable in the CXL HDM
Decoder Global Control register of every endpoint below it, and Mem_Enable
in their DVSEC CXL Control register. Per CXL r4.0 sec 8.2.4.20.2
Table 8-118 a device decodes CXL.mem through the DVSEC range registers
while HDM Decoder Enable is clear, so a decoder committed in that state
does not establish the route, and per sec 8.1.3.2 Table 8-6 a component
with Mem_Enable clear may silently drop all CXL.mem transactions.
Add cxl_endpoint_enable_hdm_decode() to put an endpoint back in HDM decode
before its decoders are committed again. It takes the Global Control value
to enable decode in, since that register also holds Poison On Decode Error
Enable, which the driver does not model and which a read taken after the
reset returns at its default.
Leave alone an endpoint that decodes through the DVSEC ranges rather than
the HDM decoders, either because it has no HDM decoder registers or
because its decoders are emulated from those ranges. Setting HDM Decoder
Enable on such a device moves it off the ranges it is decoding through and
onto decoders that are locked against reprogramming, so it would decode
nothing.
Signed-off-by: Fabio M. De Francesco <fabio.m.de.francesco@linux.intel.com>
---
drivers/cxl/core/core.h | 1 +
drivers/cxl/core/hdm.c | 52 +++++++++++++++++++++++++++++++++++++++++
2 files changed, 53 insertions(+)
diff --git a/drivers/cxl/core/core.h b/drivers/cxl/core/core.h
index 6d536fe7b446..918d48f4422e 100644
--- a/drivers/cxl/core/core.h
+++ b/drivers/cxl/core/core.h
@@ -216,6 +216,7 @@ int cxl_hdm_decode_init(struct cxl_dev_state *cxlds, struct cxl_hdm *cxlhdm,
void cxl_enable_hdm(struct cxl_hdm *cxlhdm, u32 global_ctrl);
int cxl_set_mem_enable(struct cxl_dev_state *cxlds, u16 val);
int cxl_decoder_recommit(struct cxl_decoder *cxld, u32 ctrl);
+int cxl_endpoint_enable_hdm_decode(struct cxl_memdev *cxlmd, u32 global_ctrl);
int cxl_port_get_possible_dports(struct cxl_port *port);
#ifdef CONFIG_CXL_FEATURES
diff --git a/drivers/cxl/core/hdm.c b/drivers/cxl/core/hdm.c
index d55d764895f8..7c898c60f60f 100644
--- a/drivers/cxl/core/hdm.c
+++ b/drivers/cxl/core/hdm.c
@@ -920,6 +920,58 @@ int cxl_decoder_recommit(struct cxl_decoder *cxld, u32 ctrl)
return 0;
}
+static int __cxl_endpoint_decoder_is_emulated(struct device *dev, void *data)
+{
+ if (!is_endpoint_decoder(dev))
+ return 0;
+
+ return !to_cxl_decoder(dev)->commit;
+}
+
+/* Only the DVSEC setup path leaves ->commit NULL. */
+static bool cxl_endpoint_decoders_are_emulated(struct cxl_port *endpoint)
+{
+ return device_for_each_child(&endpoint->dev, NULL,
+ __cxl_endpoint_decoder_is_emulated);
+}
+
+/*
+ * Restore CXL.mem decode on @cxlmd before any of its decoders is committed. A
+ * reset clears the endpoint's HDM Decoder Global Control and the DVSEC CXL
+ * Control, and per CXL r4.0 sec 8.2.4.20.2 Table 8-118 a device decodes CXL.mem
+ * with the DVSEC range registers while HDM Decoder Enable is clear. Committing
+ * a decoder in that state does not establish the route. An endpoint with no HDM
+ * decoder registers is driven through the DVSEC ranges and has nothing to
+ * enable. So is an endpoint whose decoders are emulated from those ranges, and
+ * setting HDM Decoder Enable there would switch it to decoders locked against
+ * reprogramming.
+ *
+ * @global_ctrl is the Global Control value to enable decode in. That register
+ * also holds Poison On Decode Error Enable, which the driver does not model, so
+ * the caller supplies the value it saved rather than one read back after the
+ * reset.
+ */
+int cxl_endpoint_enable_hdm_decode(struct cxl_memdev *cxlmd, u32 global_ctrl)
+{
+ struct cxl_port *endpoint = cxlmd->endpoint;
+ struct cxl_hdm *cxlhdm = dev_get_drvdata(&endpoint->dev);
+ int rc;
+
+ if (!cxlhdm || !cxlhdm->regs.hdm_decoder)
+ return 0;
+
+ if (cxl_endpoint_decoders_are_emulated(endpoint))
+ return 0;
+
+ cxl_enable_hdm(cxlhdm, global_ctrl);
+
+ rc = cxl_set_mem_enable(cxlmd->cxlds, PCI_DVSEC_CXL_MEM_ENABLE);
+ if (rc < 0)
+ return rc;
+
+ return 0;
+}
+
static int commit_reap(struct device *dev, void *data)
{
struct cxl_port *port = to_cxl_port(dev->parent);
--
2.55.0
^ permalink raw reply related [flat|nested] 15+ messages in thread* [PATCH v2 04/13] cxl/hdm: Reprogram the HDM Decoders below a CXL Port
2026-08-25 2:26 [PATCH v2 00/13] Make SBR work for CXL Downstream Ports Fabio M. De Francesco
` (2 preceding siblings ...)
2026-08-25 2:26 ` [PATCH v2 03/13] cxl/hdm: Add function to restore CXL.mem decode Fabio M. De Francesco
@ 2026-08-25 2:26 ` Fabio M. De Francesco
2026-08-25 2:26 ` [PATCH v2 05/13] cxl/core: Restore the HDM decoders below DPort Fabio M. De Francesco
` (8 subsequent siblings)
12 siblings, 0 replies; 15+ messages in thread
From: Fabio M. De Francesco @ 2026-08-25 2:26 UTC (permalink / raw)
To: linux-cxl
Cc: David Hildenbrand, Oscar Salvador, Greg Kroah-Hartman,
Rafael J . Wysocki, Danilo Krummrich, Davidlohr Bueso,
Jonathan Cameron, Dave Jiang, Alison Schofield, Vishal Verma,
Ira Weiny, Li Ming, Bjorn Helgaas, Mahesh J Salgaonkar,
Oliver O'Halloran, Andrew Morton, Lorenzo Stoakes,
Liam R . Howlett, Vlastimil Babka, Mike Rapoport,
Suren Baghdasaryan, Michal Hocko, linux-kernel, linux-mm,
driver-core, linux-pci, linuxppc-dev, Fabio M. De Francesco
A reset of a CXL Port clears the HDM Decoders of every component below it,
so the decode has to be put back for every endpoint the Port routes to.
setup_hw_decoder() writes Interleave Granularity, Interleave Ways and
Target Range Type. Restoring a decoder needs the rest of its Control
register too, and the port's HDM Decoder Global Control register, so read
both before the reset and write them back after it.
Add cxl_port_recommit_decoders() to restore the decode of every endpoint
below a CXL Port, and cxl_port_save_hdm_state() to record those registers
beforehand, one struct cxl_hdm_state per port.
Commit each path from the endpoint upward, so a decoder starts claiming its
range only after the path below it is programmed. Within a port, commit the
decoders in instance order, which CXL r4.0 sec 8.2.4.20.13 requires during
reprogramming.
Signed-off-by: Fabio M. De Francesco <fabio.m.de.francesco@linux.intel.com>
---
drivers/cxl/core/core.h | 22 +++-
drivers/cxl/core/hdm.c | 222 +++++++++++++++++++++++++++++++++++++++-
2 files changed, 240 insertions(+), 4 deletions(-)
diff --git a/drivers/cxl/core/core.h b/drivers/cxl/core/core.h
index 918d48f4422e..d23f0bbd7bf0 100644
--- a/drivers/cxl/core/core.h
+++ b/drivers/cxl/core/core.h
@@ -215,8 +215,26 @@ int cxl_hdm_decode_init(struct cxl_dev_state *cxlds, struct cxl_hdm *cxlhdm,
struct cxl_endpoint_dvsec_info *info);
void cxl_enable_hdm(struct cxl_hdm *cxlhdm, u32 global_ctrl);
int cxl_set_mem_enable(struct cxl_dev_state *cxlds, u16 val);
-int cxl_decoder_recommit(struct cxl_decoder *cxld, u32 ctrl);
-int cxl_endpoint_enable_hdm_decode(struct cxl_memdev *cxlmd, u32 global_ctrl);
+/**
+ * struct cxl_hdm_state - one CXL port's HDM decoder programming, saved
+ * @global_ctrl: CXL HDM Decoder Global Control
+ * @nr_ctrl: number of entries in @ctrl
+ * @ctrl: CXL HDM Decoder n Control, indexed by decoder id
+ *
+ * Holds the fields of those two registers that the driver does not model, read
+ * before a reset and written back after it. Instances are held in an xarray
+ * keyed by the &struct cxl_port they were read from.
+ */
+struct cxl_hdm_state {
+ u32 global_ctrl;
+ int nr_ctrl;
+ u32 ctrl[];
+};
+
+int cxl_port_save_hdm_state(struct cxl_port *port, struct xarray *hdm_state);
+void cxl_port_put_hdm_state(struct xarray *hdm_state);
+int cxl_port_recommit_decoders(struct cxl_port *port,
+ struct xarray *hdm_state);
int cxl_port_get_possible_dports(struct cxl_port *port);
#ifdef CONFIG_CXL_FEATURES
diff --git a/drivers/cxl/core/hdm.c b/drivers/cxl/core/hdm.c
index 7c898c60f60f..9839d2592d20 100644
--- a/drivers/cxl/core/hdm.c
+++ b/drivers/cxl/core/hdm.c
@@ -878,7 +878,7 @@ static int cxl_decoder_commit(struct cxl_decoder *cxld)
* Return: 0 on success or if @cxld needs no reprogramming, negative errno if the
* commit times out or if the hardware reports a commit error.
*/
-int cxl_decoder_recommit(struct cxl_decoder *cxld, u32 ctrl)
+static int cxl_decoder_recommit(struct cxl_decoder *cxld, u32 ctrl)
{
struct cxl_port *port = to_cxl_port(cxld->dev.parent);
struct cxl_hdm *cxlhdm = dev_get_drvdata(&port->dev);
@@ -935,6 +935,42 @@ static bool cxl_endpoint_decoders_are_emulated(struct cxl_port *endpoint)
__cxl_endpoint_decoder_is_emulated);
}
+struct cxl_recommit_ctx {
+ const struct cxl_hdm_state *state;
+ int *first_rc;
+};
+
+static int __cxl_port_recommit_decoder(struct device *dev, void *data)
+{
+ struct cxl_recommit_ctx *ctx = data;
+ struct cxl_decoder *cxld;
+ int rc;
+
+ if (!(is_switch_decoder(dev) || is_endpoint_decoder(dev)))
+ return 0;
+
+ cxld = to_cxl_decoder(dev);
+
+ if (cxld->id >= ctx->state->nr_ctrl) {
+ dev_warn(&cxld->dev, "%s: no saved control register\n",
+ __func__);
+ if (!*ctx->first_rc)
+ *ctx->first_rc = -ENODATA;
+ return 0;
+ }
+
+ /*
+ * Reprogram every decoder the walk reaches. Stopping at the first
+ * failure would leave the rest of the path decoding nothing, so record
+ * the first error and continue.
+ */
+ rc = cxl_decoder_recommit(cxld, ctx->state->ctrl[cxld->id]);
+ if (rc && !*ctx->first_rc)
+ *ctx->first_rc = rc;
+
+ return 0;
+}
+
/*
* Restore CXL.mem decode on @cxlmd before any of its decoders is committed. A
* reset clears the endpoint's HDM Decoder Global Control and the DVSEC CXL
@@ -951,7 +987,8 @@ static bool cxl_endpoint_decoders_are_emulated(struct cxl_port *endpoint)
* the caller supplies the value it saved rather than one read back after the
* reset.
*/
-int cxl_endpoint_enable_hdm_decode(struct cxl_memdev *cxlmd, u32 global_ctrl)
+static int cxl_endpoint_enable_hdm_decode(struct cxl_memdev *cxlmd,
+ u32 global_ctrl)
{
struct cxl_port *endpoint = cxlmd->endpoint;
struct cxl_hdm *cxlhdm = dev_get_drvdata(&endpoint->dev);
@@ -972,6 +1009,187 @@ int cxl_endpoint_enable_hdm_decode(struct cxl_memdev *cxlmd, u32 global_ctrl)
return 0;
}
+/**
+ * cxl_port_recommit_decoders - reprogram the HDM decoders below @port
+ * @port: CXL port whose downstream decoders to reprogram
+ * @hdm_state: saved &struct cxl_hdm_state per port, keyed by &struct cxl_port
+ *
+ * Reprogram the HDM decoders below @port that lost their programming. Every
+ * endpoint beneath @port is restored along its whole path, from the endpoint up
+ * to the last port below @port. A decoder that hardware still reports committed
+ * is left untouched.
+ *
+ * Per CXL r4.0 sec 8.2.4.20.13 decoder m must be committed before decoder m+1
+ * while reprogramming, so let device_for_each_child() visit each port's decoders
+ * in instance order. Each path is walked from the endpoint upward, the order
+ * cxl_region_decode_commit() uses.
+ *
+ * The endpoints are reprogrammed one after another, so an interleaved HPA range
+ * decodes through only part of its interleave set until the last member is
+ * done. Per CXL r4.0 sec 8.2.4.20.13 software owns quiescing the traffic that
+ * targets a decoder being reprogrammed: a read that no decoder positively
+ * decodes returns all 1s or poison, and per Table 8-118 such a write is
+ * dropped. Nothing here can detect a stray access, so the caller carries that
+ * duty.
+ *
+ * Context: caller must hold @cxl_rwsem.region to keep the topology and the
+ * switch decoder target lists stable across the walk, and must have quiesced
+ * every access to the HPA ranges decoded below @port.
+ *
+ * A port with no entry in @hdm_state was not saved, so its decoders are left
+ * alone rather than committed with whatever the reset left in the fields the
+ * driver does not model.
+ *
+ * Return: 0 on success, negative errno of the first decoder that failed or
+ * -ENODATA if a port on the path has no saved state.
+ */
+int cxl_port_recommit_decoders(struct cxl_port *port, struct xarray *hdm_state)
+{
+ struct cxl_ep *port_ep;
+ unsigned long index;
+ int first_rc = 0;
+
+ lockdep_assert_held(&cxl_rwsem.region);
+
+ xa_for_each(&port->endpoints, index, port_ep) {
+ struct cxl_memdev *cxlmd = to_cxl_memdev(port_ep->ep);
+ struct cxl_hdm_state *state;
+ struct cxl_port *iter;
+ int rc;
+
+ if (IS_ERR_OR_NULL(cxlmd->endpoint))
+ continue;
+
+ state = xa_load(hdm_state, (unsigned long)cxlmd->endpoint);
+ if (!state) {
+ dev_warn(&cxlmd->dev, "%s: no saved HDM state\n",
+ __func__);
+ if (!first_rc)
+ first_rc = -ENODATA;
+ continue;
+ }
+
+ rc = cxl_endpoint_enable_hdm_decode(cxlmd, state->global_ctrl);
+ if (rc) {
+ dev_warn(&cxlmd->dev,
+ "%s: failed to enable HDM decode: %d\n",
+ __func__, rc);
+ if (!first_rc)
+ first_rc = rc;
+ continue;
+ }
+
+ /*
+ * Walk from the endpoint up to @port so a decoder is committed
+ * only after the decoder it routes to. @port is the last parent
+ * visited by the walk, and it is excluded.
+ */
+ for (iter = cxlmd->endpoint; iter && iter != port;
+ iter = parent_port_of(iter)) {
+ struct cxl_recommit_ctx ctx = {
+ .state = xa_load(hdm_state, (unsigned long)iter),
+ .first_rc = &first_rc,
+ };
+
+ if (!ctx.state) {
+ dev_warn(&iter->dev, "%s: no saved HDM state\n",
+ __func__);
+ if (!first_rc)
+ first_rc = -ENODATA;
+ continue;
+ }
+
+ device_for_each_child(&iter->dev, &ctx,
+ __cxl_port_recommit_decoder);
+ }
+ }
+
+ return first_rc;
+}
+
+/**
+ * cxl_port_save_hdm_state - record the HDM decoder control registers below @port
+ * @port: CXL port whose downstream decoders to record
+ * @hdm_state: xarray to fill, one entry per port, keyed by &struct cxl_port
+ *
+ * Read the CXL HDM Decoder Global Control and every CXL HDM Decoder n Control
+ * register of the ports below @port. Those hold the fields
+ * cxl_port_recommit_decoders() cannot rebuild from the driver's cached settings,
+ * so they have to be read while the registers still hold them.
+ *
+ * The set of ports is the same one cxl_port_recommit_decoders() walks. A port
+ * with no HDM decoder registers has nothing to record and gets no entry.
+ *
+ * Context: caller must hold @cxl_rwsem.region.
+ *
+ * Return: 0 on success, negative errno if an entry cannot be allocated or
+ * inserted.
+ */
+int cxl_port_save_hdm_state(struct cxl_port *port, struct xarray *hdm_state)
+{
+ struct cxl_ep *port_ep;
+ unsigned long index;
+
+ lockdep_assert_held(&cxl_rwsem.region);
+
+ xa_for_each(&port->endpoints, index, port_ep) {
+ struct cxl_memdev *cxlmd = to_cxl_memdev(port_ep->ep);
+ struct cxl_port *iter;
+
+ if (IS_ERR_OR_NULL(cxlmd->endpoint))
+ continue;
+
+ for (iter = cxlmd->endpoint; iter && iter != port;
+ iter = parent_port_of(iter)) {
+ struct cxl_hdm *cxlhdm = dev_get_drvdata(&iter->dev);
+ struct cxl_hdm_state *state;
+ void __iomem *hdm;
+ int rc;
+
+ if (xa_load(hdm_state, (unsigned long)iter))
+ continue;
+
+ if (!cxlhdm || !cxlhdm->regs.hdm_decoder)
+ continue;
+
+ hdm = cxlhdm->regs.hdm_decoder;
+ state = kzalloc_flex(*state, ctrl,
+ cxlhdm->decoder_count);
+ if (!state)
+ return -ENOMEM;
+
+ state->global_ctrl = readl(hdm + CXL_HDM_DECODER_CTRL_OFFSET);
+ state->nr_ctrl = cxlhdm->decoder_count;
+ for (int i = 0; i < state->nr_ctrl; i++)
+ state->ctrl[i] =
+ readl(hdm + CXL_HDM_DECODER0_CTRL_OFFSET(i));
+
+ rc = xa_insert(hdm_state, (unsigned long)iter, state,
+ GFP_KERNEL);
+ if (rc) {
+ kfree(state);
+ return rc;
+ }
+ }
+ }
+
+ return 0;
+}
+
+/**
+ * cxl_port_put_hdm_state - release a set filled by cxl_port_save_hdm_state()
+ * @hdm_state: xarray to empty
+ */
+void cxl_port_put_hdm_state(struct xarray *hdm_state)
+{
+ struct cxl_hdm_state *state;
+ unsigned long index;
+
+ xa_for_each(hdm_state, index, state)
+ kfree(state);
+ xa_destroy(hdm_state);
+}
+
static int commit_reap(struct device *dev, void *data)
{
struct cxl_port *port = to_cxl_port(dev->parent);
--
2.55.0
^ permalink raw reply related [flat|nested] 15+ messages in thread* [PATCH v2 05/13] cxl/core: Restore the HDM decoders below DPort
2026-08-25 2:26 [PATCH v2 00/13] Make SBR work for CXL Downstream Ports Fabio M. De Francesco
` (3 preceding siblings ...)
2026-08-25 2:26 ` [PATCH v2 04/13] cxl/hdm: Reprogram the HDM Decoders below a CXL Port Fabio M. De Francesco
@ 2026-08-25 2:26 ` Fabio M. De Francesco
2026-08-25 2:26 ` [PATCH v2 06/13] drivers/base/memory: Add cxl_offline_memory() to offline a physical range Fabio M. De Francesco
` (7 subsequent siblings)
12 siblings, 0 replies; 15+ messages in thread
From: Fabio M. De Francesco @ 2026-08-25 2:26 UTC (permalink / raw)
To: linux-cxl
Cc: David Hildenbrand, Oscar Salvador, Greg Kroah-Hartman,
Rafael J . Wysocki, Danilo Krummrich, Davidlohr Bueso,
Jonathan Cameron, Dave Jiang, Alison Schofield, Vishal Verma,
Ira Weiny, Li Ming, Bjorn Helgaas, Mahesh J Salgaonkar,
Oliver O'Halloran, Andrew Morton, Lorenzo Stoakes,
Liam R . Howlett, Vlastimil Babka, Mike Rapoport,
Suren Baghdasaryan, Michal Hocko, linux-kernel, linux-mm,
driver-core, linux-pci, linuxppc-dev, Fabio M. De Francesco
A Secondary Bus Reset clears the HDM Decoder registers of every CXL
component below the Port, so the decoders have to be reprogrammed from the
settings the driver holds before anything reaches the region again.
Add cxl_sbr_recommit_decoders() to find the CXL port that owns the
Downstream Port and reprogram the decoders below it. It takes
cxl_rwsem.region for read across the walk: the walk reads the endpoint set
below the Port, each endpoint's parent chain and the switch decoder target
lists, all of which change only under that lock held for write.
The driver's cached settings do not cover the whole of either control
register, so the saved state that cxl_port_recommit_decoders() needs is
passed through as well, an xarray of struct cxl_hdm_state keyed by
struct cxl_port and read before the reset.
Put it in a new dport_sbr.c, which collects what a Secondary Bus Reset of a
CXL Downstream Port needs from the CXL core. find_cxl_port() loses its
static since the lookup now has a caller outside port.c.
It has no caller yet. It is used on the way out of the reset, once the
regions routed through the Port are back.
Signed-off-by: Fabio M. De Francesco <fabio.m.de.francesco@linux.intel.com>
---
drivers/cxl/core/Makefile | 2 +-
drivers/cxl/core/core.h | 6 ++++++
drivers/cxl/core/dport_sbr.c | 36 ++++++++++++++++++++++++++++++++++++
drivers/cxl/core/port.c | 4 ++--
tools/testing/cxl/Kbuild | 2 +-
5 files changed, 46 insertions(+), 4 deletions(-)
create mode 100644 drivers/cxl/core/dport_sbr.c
diff --git a/drivers/cxl/core/Makefile b/drivers/cxl/core/Makefile
index ce7213818d3c..4d4538182d81 100644
--- a/drivers/cxl/core/Makefile
+++ b/drivers/cxl/core/Makefile
@@ -15,7 +15,7 @@ cxl_core-y += hdm.o
cxl_core-y += pmu.o
cxl_core-y += cdat.o
cxl_core-$(CONFIG_TRACING) += trace.o
-cxl_core-$(CONFIG_CXL_REGION) += region.o region_pmem.o region_dax.o
+cxl_core-$(CONFIG_CXL_REGION) += region.o region_pmem.o region_dax.o dport_sbr.o
cxl_core-$(CONFIG_CXL_MCE) += mce.o
cxl_core-$(CONFIG_CXL_FEATURES) += features.o
cxl_core-$(CONFIG_CXL_EDAC_MEM_FEATURES) += edac.o
diff --git a/drivers/cxl/core/core.h b/drivers/cxl/core/core.h
index d23f0bbd7bf0..a8b9d7990d32 100644
--- a/drivers/cxl/core/core.h
+++ b/drivers/cxl/core/core.h
@@ -13,6 +13,9 @@ extern const struct device_type cxl_pmu_type;
extern struct attribute_group cxl_base_attribute_group;
+struct cxl_port *find_cxl_port(struct device *dport_dev,
+ struct cxl_dport **dport);
+
enum cxl_detach_mode {
DETACH_ONLY,
DETACH_INVALIDATE,
@@ -53,6 +56,9 @@ u64 cxl_dpa_to_hpa(struct cxl_region *cxlr, const struct cxl_memdev *cxlmd,
int devm_cxl_add_dax_region(struct cxl_region *cxlr);
int devm_cxl_add_pmem_region(struct cxl_region *cxlr);
void kill_regions(struct cxl_root_decoder *cxlrd);
+struct pci_dev;
+void cxl_sbr_recommit_decoders(struct pci_dev *dport_pci,
+ struct xarray *hdm_state);
#else
static inline u64 cxl_dpa_to_hpa(struct cxl_region *cxlr,
diff --git a/drivers/cxl/core/dport_sbr.c b/drivers/cxl/core/dport_sbr.c
new file mode 100644
index 000000000000..55c35386d0cb
--- /dev/null
+++ b/drivers/cxl/core/dport_sbr.c
@@ -0,0 +1,36 @@
+// SPDX-License-Identifier: GPL-2.0-only
+/* Copyright(c) 2026 Intel Corporation. */
+
+#include <linux/device.h>
+#include <linux/pci.h>
+#include <cxl.h>
+#include "core.h"
+
+/*
+ * The reset cleared the HDM Decoder registers of every CXL component below
+ * @dport_pci, so restore them from the settings the driver holds and from
+ * @hdm_state, the register fields the driver does not model, saved before the
+ * reset. Takes cxl_rwsem.region for read, which cxl_port_recommit_decoders()
+ * requires. The caller has already disabled the regions, so nothing reaches the
+ * decoders being reprogrammed.
+ */
+void cxl_sbr_recommit_decoders(struct pci_dev *dport_pci,
+ struct xarray *hdm_state)
+{
+ struct cxl_dport *dport;
+ int rc;
+
+ struct cxl_port *port __free(put_cxl_port) =
+ find_cxl_port(&dport_pci->dev, &dport);
+ if (!port) {
+ pci_dbg(dport_pci, "no CXL port owns this Downstream Port\n");
+ return;
+ }
+
+ pci_dbg(dport_pci, "restoring HDM decode below %s\n", dev_name(&port->dev));
+
+ guard(rwsem_read)(&cxl_rwsem.region);
+ rc = cxl_port_recommit_decoders(port, hdm_state);
+ if (rc)
+ pci_warn(dport_pci, "HDM decode restore failed: %d\n", rc);
+}
diff --git a/drivers/cxl/core/port.c b/drivers/cxl/core/port.c
index 1215ee4f4035..66a6d513843e 100644
--- a/drivers/cxl/core/port.c
+++ b/drivers/cxl/core/port.c
@@ -1392,8 +1392,8 @@ static struct cxl_port *__find_cxl_port(struct cxl_find_port_ctx *ctx)
return NULL;
}
-static struct cxl_port *find_cxl_port(struct device *dport_dev,
- struct cxl_dport **dport)
+struct cxl_port *find_cxl_port(struct device *dport_dev,
+ struct cxl_dport **dport)
{
struct cxl_find_port_ctx ctx = {
.dport_dev = dport_dev,
diff --git a/tools/testing/cxl/Kbuild b/tools/testing/cxl/Kbuild
index 2be1df80fcc9..f18d41b299fe 100644
--- a/tools/testing/cxl/Kbuild
+++ b/tools/testing/cxl/Kbuild
@@ -63,7 +63,7 @@ cxl_core-y += $(CXL_CORE_SRC)/hdm.o
cxl_core-y += $(CXL_CORE_SRC)/pmu.o
cxl_core-y += $(CXL_CORE_SRC)/cdat.o
cxl_core-$(CONFIG_TRACING) += $(CXL_CORE_SRC)/trace.o
-cxl_core-$(CONFIG_CXL_REGION) += $(CXL_CORE_SRC)/region.o $(CXL_CORE_SRC)/region_pmem.o $(CXL_CORE_SRC)/region_dax.o
+cxl_core-$(CONFIG_CXL_REGION) += $(CXL_CORE_SRC)/region.o $(CXL_CORE_SRC)/region_pmem.o $(CXL_CORE_SRC)/region_dax.o $(CXL_CORE_SRC)/dport_sbr.o
cxl_core-$(CONFIG_CXL_MCE) += $(CXL_CORE_SRC)/mce.o
cxl_core-$(CONFIG_CXL_FEATURES) += $(CXL_CORE_SRC)/features.o
cxl_core-$(CONFIG_CXL_EDAC_MEM_FEATURES) += $(CXL_CORE_SRC)/edac.o
--
2.55.0
^ permalink raw reply related [flat|nested] 15+ messages in thread* [PATCH v2 06/13] drivers/base/memory: Add cxl_offline_memory() to offline a physical range
2026-08-25 2:26 [PATCH v2 00/13] Make SBR work for CXL Downstream Ports Fabio M. De Francesco
` (4 preceding siblings ...)
2026-08-25 2:26 ` [PATCH v2 05/13] cxl/core: Restore the HDM decoders below DPort Fabio M. De Francesco
@ 2026-08-25 2:26 ` Fabio M. De Francesco
2026-08-25 2:26 ` [PATCH v2 07/13] cxl/core: Add region disable and enable for a DPort SBR Fabio M. De Francesco
` (6 subsequent siblings)
12 siblings, 0 replies; 15+ messages in thread
From: Fabio M. De Francesco @ 2026-08-25 2:26 UTC (permalink / raw)
To: linux-cxl
Cc: David Hildenbrand, Oscar Salvador, Greg Kroah-Hartman,
Rafael J . Wysocki, Danilo Krummrich, Davidlohr Bueso,
Jonathan Cameron, Dave Jiang, Alison Schofield, Vishal Verma,
Ira Weiny, Li Ming, Bjorn Helgaas, Mahesh J Salgaonkar,
Oliver O'Halloran, Andrew Morton, Lorenzo Stoakes,
Liam R . Howlett, Vlastimil Babka, Mike Rapoport,
Suren Baghdasaryan, Michal Hocko, linux-kernel, linux-mm,
driver-core, linux-pci, linuxppc-dev, Fabio M. De Francesco
A CXL region whose capacity is online as System RAM must be taken
offline before a Secondary Bus Reset of the CXL Downstream Port it is
reached through.
Add cxl_offline_memory() to offline the memory blocks spanning a
physical range, exported in the CXL_MHP namespace since the CXL core is
its only caller.
Signed-off-by: Fabio M. De Francesco <fabio.m.de.francesco@linux.intel.com>
---
drivers/base/memory.c | 36 ++++++++++++++++++++++++++++++++++++
include/linux/memory.h | 5 +++++
2 files changed, 41 insertions(+)
diff --git a/drivers/base/memory.c b/drivers/base/memory.c
index bcfe2d9f4adb..ce89b251d34a 100644
--- a/drivers/base/memory.c
+++ b/drivers/base/memory.c
@@ -1018,6 +1018,42 @@ int walk_memory_blocks(unsigned long start, unsigned long size,
return ret;
}
+static int cxl_offline_memory_block(struct memory_block *mem, void *arg)
+{
+ int rc = device_offline(&mem->dev);
+
+ /* device_offline() returns a positive value when already offline. */
+ if (rc > 0)
+ return 0;
+
+ return rc;
+}
+
+/**
+ * cxl_offline_memory - offline the memory blocks spanning a physical range
+ * @start: start of the range, memory-block aligned
+ * @size: size of the range, a multiple of the memory block size
+ *
+ * Offline every memory block in [start, start + size). The blocks are offlined
+ * but not removed, so the range can be brought back online afterward. The
+ * caller owns the range and this performs no validation on it.
+ *
+ * Return: 0 on success, negative errno if a block cannot be offlined.
+ *
+ * Context: process context. Sleeps and takes the memory hotplug lock.
+ */
+int cxl_offline_memory(u64 start, u64 size)
+{
+ int rc;
+
+ lock_device_hotplug();
+ rc = walk_memory_blocks(start, size, NULL, cxl_offline_memory_block);
+ unlock_device_hotplug();
+
+ return rc;
+}
+EXPORT_SYMBOL_NS_GPL(cxl_offline_memory, "CXL_MHP");
+
struct for_each_memory_block_cb_data {
walk_memory_blocks_func_t func;
void *arg;
diff --git a/include/linux/memory.h b/include/linux/memory.h
index 463dc02f6cff..5dc0b96fb004 100644
--- a/include/linux/memory.h
+++ b/include/linux/memory.h
@@ -149,6 +149,10 @@ static inline unsigned long memory_block_advised_max_size(void)
{
return 0;
}
+static inline int cxl_offline_memory(u64 start, u64 size)
+{
+ return 0;
+}
#else /* CONFIG_MEMORY_HOTPLUG */
extern int register_memory_notifier(struct notifier_block *nb);
extern void unregister_memory_notifier(struct notifier_block *nb);
@@ -167,6 +171,7 @@ typedef int (*walk_memory_blocks_func_t)(struct memory_block *, void *);
extern int walk_memory_blocks(unsigned long start, unsigned long size,
void *arg, walk_memory_blocks_func_t func);
extern int for_each_memory_block(void *arg, walk_memory_blocks_func_t func);
+int cxl_offline_memory(u64 start, u64 size);
extern int memory_group_register_static(int nid, unsigned long max_pages);
extern int memory_group_register_dynamic(int nid, unsigned long unit_pages);
--
2.55.0
^ permalink raw reply related [flat|nested] 15+ messages in thread* [PATCH v2 07/13] cxl/core: Add region disable and enable for a DPort SBR
2026-08-25 2:26 [PATCH v2 00/13] Make SBR work for CXL Downstream Ports Fabio M. De Francesco
` (5 preceding siblings ...)
2026-08-25 2:26 ` [PATCH v2 06/13] drivers/base/memory: Add cxl_offline_memory() to offline a physical range Fabio M. De Francesco
@ 2026-08-25 2:26 ` Fabio M. De Francesco
2026-08-25 2:26 ` [PATCH v2 08/13] cxl/core: Collect the regions routed through a DPort Fabio M. De Francesco
` (5 subsequent siblings)
12 siblings, 0 replies; 15+ messages in thread
From: Fabio M. De Francesco @ 2026-08-25 2:26 UTC (permalink / raw)
To: linux-cxl
Cc: David Hildenbrand, Oscar Salvador, Greg Kroah-Hartman,
Rafael J . Wysocki, Danilo Krummrich, Davidlohr Bueso,
Jonathan Cameron, Dave Jiang, Alison Schofield, Vishal Verma,
Ira Weiny, Li Ming, Bjorn Helgaas, Mahesh J Salgaonkar,
Oliver O'Halloran, Andrew Morton, Lorenzo Stoakes,
Liam R . Howlett, Vlastimil Babka, Mike Rapoport,
Suren Baghdasaryan, Michal Hocko, linux-kernel, linux-mm,
driver-core, linux-pci, linuxppc-dev, Fabio M. De Francesco
A Secondary Bus Reset of a CXL Downstream Port removes the downstream
component from the bus, so a CXL region reached through that Port has to
be disabled while the reset runs.
Add cxl_region_disable() to offline memory, invalidate the CPU caches
for its range and release its driver, and cxl_region_enable() to
re-attach the driver afterwards.
Neither function has a caller yet. Both are driven by the reset once the
set of regions routed through the Port is known.
Signed-off-by: Fabio M. De Francesco <fabio.m.de.francesco@linux.intel.com>
---
drivers/cxl/core/core.h | 3 ++
drivers/cxl/core/dport_sbr.c | 79 ++++++++++++++++++++++++++++++++++++
drivers/cxl/core/region.c | 3 +-
3 files changed, 84 insertions(+), 1 deletion(-)
diff --git a/drivers/cxl/core/core.h b/drivers/cxl/core/core.h
index a8b9d7990d32..ac55f9e8160e 100644
--- a/drivers/cxl/core/core.h
+++ b/drivers/cxl/core/core.h
@@ -56,6 +56,9 @@ u64 cxl_dpa_to_hpa(struct cxl_region *cxlr, const struct cxl_memdev *cxlmd,
int devm_cxl_add_dax_region(struct cxl_region *cxlr);
int devm_cxl_add_pmem_region(struct cxl_region *cxlr);
void kill_regions(struct cxl_root_decoder *cxlrd);
+int cxl_region_invalidate_memregion(struct cxl_region *cxlr);
+int cxl_region_disable(struct cxl_region *cxlr);
+void cxl_region_enable(struct cxl_region *cxlr);
struct pci_dev;
void cxl_sbr_recommit_decoders(struct pci_dev *dport_pci,
struct xarray *hdm_state);
diff --git a/drivers/cxl/core/dport_sbr.c b/drivers/cxl/core/dport_sbr.c
index 55c35386d0cb..0f398ab0d3a4 100644
--- a/drivers/cxl/core/dport_sbr.c
+++ b/drivers/cxl/core/dport_sbr.c
@@ -1,11 +1,90 @@
// SPDX-License-Identifier: GPL-2.0-only
/* Copyright(c) 2026 Intel Corporation. */
+#include <linux/memregion.h>
+#include <linux/memory_hotplug.h>
+#include <linux/memory.h>
#include <linux/device.h>
#include <linux/pci.h>
#include <cxl.h>
#include "core.h"
+/*
+ * cxl_region_disable - make a region inactive ahead of a Secondary Bus Reset
+ * @cxlr: region routed through the CXL Downstream Port being reset
+ *
+ * Offline the memory blocks the region owns and unbind its driver. An SBR
+ * zeroes the downstream bus number, so a region left live as System RAM would
+ * be accessed while the device is in reset. On offline failure return the error
+ * so the caller aborts the reset; the memory is never force-removed.
+ *
+ * Context: process context. Offlining and driver unbind sleep and take the
+ * memory hotplug lock, so this cannot run in atomic context.
+ */
+int cxl_region_disable(struct cxl_region *cxlr)
+{
+ struct cxl_region_params *p = &cxlr->params;
+ unsigned long block_size;
+ u64 start, end;
+ int rc;
+
+ /*
+ * Per CXL r4.0 sec 9.13.1 an Interleave Set has a Base HPA and a Size
+ * that are multiples of 256 MB, while a memory block spans up to 2 GB.
+ * A block overlapping either end of the range therefore also covers
+ * memory outside this region, so round the range inward to block
+ * granularity as dax_kmem did when it onlined the range. Offlining a
+ * straddling block would migrate pages that the reset does not affect.
+ */
+ block_size = memory_block_size_bytes();
+ start = ALIGN(p->res->start, block_size);
+ end = ALIGN_DOWN(p->res->end + 1, block_size);
+ if (start >= end) {
+ dev_dbg(&cxlr->dev, "%s: HPA %pr spans no whole memory block, no System RAM to offline\n",
+ __func__, p->res);
+ } else {
+ rc = cxl_offline_memory(start, end - start);
+ if (rc) {
+ dev_warn(&cxlr->dev, "offline System RAM failed before reset: %d\n",
+ rc);
+ return rc;
+ }
+ }
+
+ rc = cxl_region_invalidate_memregion(cxlr);
+ if (rc) {
+ dev_warn(&cxlr->dev, "CPU cache invalidate failed before reset: %d\n",
+ rc);
+ return rc;
+ }
+
+ device_release_driver(&cxlr->dev);
+ dev_dbg(&cxlr->dev, "%s: System RAM offline, region disabled before reset, HPA %pr\n",
+ __func__, p->res);
+
+ return 0;
+}
+
+/*
+ * cxl_region_enable - restore a region after a Secondary Bus Reset
+ * @cxlr: region disabled by cxl_region_disable() before the reset
+ *
+ * Rebind the region driver. The System RAM is left offline; bringing it back
+ * online is a separate administrative step.
+ */
+void cxl_region_enable(struct cxl_region *cxlr)
+{
+ struct cxl_region_params *p = &cxlr->params;
+
+ if (device_attach(&cxlr->dev) < 0) {
+ dev_dbg(&cxlr->dev, "driver re-attach failed after reset\n");
+ return;
+ }
+
+ dev_dbg(&cxlr->dev, "%s: region re-enabled after reset, HPA %pr, IW %d, IG %d\n",
+ __func__, p->res, p->interleave_ways, p->interleave_granularity);
+}
+
/*
* The reset cleared the HDM Decoder registers of every CXL component below
* @dport_pci, so restore them from the settings the driver holds and from
diff --git a/drivers/cxl/core/region.c b/drivers/cxl/core/region.c
index 1e211542b6b6..fc0bec991a69 100644
--- a/drivers/cxl/core/region.c
+++ b/drivers/cxl/core/region.c
@@ -222,7 +222,7 @@ static struct cxl_region_ref *cxl_rr_load(struct cxl_port *port,
return xa_load(&port->regions, (unsigned long)cxlr);
}
-static int cxl_region_invalidate_memregion(struct cxl_region *cxlr)
+int cxl_region_invalidate_memregion(struct cxl_region *cxlr)
{
if (!cpu_cache_has_invalidate_memregion()) {
if (IS_ENABLED(CONFIG_CXL_REGION_INVALIDATION_TEST)) {
@@ -4273,4 +4273,5 @@ void cxl_region_exit(void)
MODULE_IMPORT_NS("CXL");
MODULE_IMPORT_NS("DEVMEM");
+MODULE_IMPORT_NS("CXL_MHP");
MODULE_ALIAS_CXL(CXL_DEVICE_REGION);
--
2.55.0
^ permalink raw reply related [flat|nested] 15+ messages in thread* [PATCH v2 08/13] cxl/core: Collect the regions routed through a DPort
2026-08-25 2:26 [PATCH v2 00/13] Make SBR work for CXL Downstream Ports Fabio M. De Francesco
` (6 preceding siblings ...)
2026-08-25 2:26 ` [PATCH v2 07/13] cxl/core: Add region disable and enable for a DPort SBR Fabio M. De Francesco
@ 2026-08-25 2:26 ` Fabio M. De Francesco
2026-08-25 2:26 ` [PATCH v2 09/13] PCI/CXL: Disable and re-enable CXL regions Fabio M. De Francesco
` (4 subsequent siblings)
12 siblings, 0 replies; 15+ messages in thread
From: Fabio M. De Francesco @ 2026-08-25 2:26 UTC (permalink / raw)
To: linux-cxl
Cc: David Hildenbrand, Oscar Salvador, Greg Kroah-Hartman,
Rafael J . Wysocki, Danilo Krummrich, Davidlohr Bueso,
Jonathan Cameron, Dave Jiang, Alison Schofield, Vishal Verma,
Ira Weiny, Li Ming, Bjorn Helgaas, Mahesh J Salgaonkar,
Oliver O'Halloran, Andrew Morton, Lorenzo Stoakes,
Liam R . Howlett, Vlastimil Babka, Mike Rapoport,
Suren Baghdasaryan, Michal Hocko, linux-kernel, linux-mm,
driver-core, linux-pci, linuxppc-dev, Fabio M. De Francesco
Disabling the CXL regions ahead of a Secondary Bus Reset needs the set of
regions with a member endpoint below the Port being reset.
Add cxl_sbr_collect_regions() to walk the CXL port that owns the Downstream
Port and return every region with an endpoint below it, and
cxl_sbr_put_regions() to release the set.
Signed-off-by: Fabio M. De Francesco <fabio.m.de.francesco@linux.intel.com>
---
drivers/cxl/core/core.h | 2 ++
drivers/cxl/core/dport_sbr.c | 66 ++++++++++++++++++++++++++++++++++++
2 files changed, 68 insertions(+)
diff --git a/drivers/cxl/core/core.h b/drivers/cxl/core/core.h
index ac55f9e8160e..077a2af9cf0c 100644
--- a/drivers/cxl/core/core.h
+++ b/drivers/cxl/core/core.h
@@ -60,6 +60,8 @@ int cxl_region_invalidate_memregion(struct cxl_region *cxlr);
int cxl_region_disable(struct cxl_region *cxlr);
void cxl_region_enable(struct cxl_region *cxlr);
struct pci_dev;
+int cxl_sbr_collect_regions(struct pci_dev *dport_pci, struct xarray *regions);
+void cxl_sbr_put_regions(struct xarray *regions);
void cxl_sbr_recommit_decoders(struct pci_dev *dport_pci,
struct xarray *hdm_state);
diff --git a/drivers/cxl/core/dport_sbr.c b/drivers/cxl/core/dport_sbr.c
index 0f398ab0d3a4..233499bc1fad 100644
--- a/drivers/cxl/core/dport_sbr.c
+++ b/drivers/cxl/core/dport_sbr.c
@@ -85,6 +85,72 @@ void cxl_region_enable(struct cxl_region *cxlr)
__func__, p->res, p->interleave_ways, p->interleave_granularity);
}
+/*
+ * Collect the regions with a member endpoint routed through @dport_pci, the
+ * CXL Downstream Port about to be reset. cxl_rwsem.region keeps the topology
+ * stable for the duration of the walk only. Each collected region is pinned
+ * with get_device() so the object survives after the lock is dropped, since
+ * cxl_region_disable()/cxl_region_enable() run with the rwsem released (they
+ * unbind and rebind the region driver). Hence snapshot the set first.
+ */
+int cxl_sbr_collect_regions(struct pci_dev *dport_pci,
+ struct xarray *regions)
+{
+ struct cxl_region_ref *cxl_rr;
+ struct cxl_dport *dport;
+ unsigned long index;
+ int count = 0;
+ int rc;
+
+ struct cxl_port *port __free(put_cxl_port) =
+ find_cxl_port(&dport_pci->dev, &dport);
+ if (!port) {
+ pci_dbg(dport_pci, "no CXL port found for reset dport\n");
+ return 0;
+ }
+
+ guard(rwsem_read)(&cxl_rwsem.region);
+ xa_for_each(&port->regions, index, cxl_rr) {
+ struct cxl_region *cxlr = cxl_rr->region;
+ struct cxl_ep *ep;
+ unsigned long ep_index;
+
+ /* Skip unless a region endpoint sits below the reset dport. */
+ xa_for_each(&cxl_rr->endpoints, ep_index, ep)
+ if (ep->dport == dport)
+ break;
+ if (!ep) {
+ dev_dbg(&cxlr->dev, "%s: no endpoint below %s, region excluded\n",
+ __func__, dev_name(dport->dport_dev));
+ continue;
+ }
+
+ get_device(&cxlr->dev);
+ rc = xa_insert(regions, (unsigned long)cxlr, cxlr, GFP_KERNEL);
+ if (rc) {
+ put_device(&cxlr->dev);
+ return rc;
+ }
+ dev_dbg(&cxlr->dev, "%s: endpoint below %s, region collected\n",
+ __func__, dev_name(dport->dport_dev));
+ count++;
+ }
+
+ dev_dbg(&port->dev, "%d region(s) routed through %s\n", count,
+ dev_name(dport->dport_dev));
+ return 0;
+}
+
+void cxl_sbr_put_regions(struct xarray *regions)
+{
+ struct cxl_region *cxlr;
+ unsigned long index;
+
+ xa_for_each(regions, index, cxlr)
+ put_device(&cxlr->dev);
+ xa_destroy(regions);
+}
+
/*
* The reset cleared the HDM Decoder registers of every CXL component below
* @dport_pci, so restore them from the settings the driver holds and from
--
2.55.0
^ permalink raw reply related [flat|nested] 15+ messages in thread* [PATCH v2 09/13] PCI/CXL: Disable and re-enable CXL regions
2026-08-25 2:26 [PATCH v2 00/13] Make SBR work for CXL Downstream Ports Fabio M. De Francesco
` (7 preceding siblings ...)
2026-08-25 2:26 ` [PATCH v2 08/13] cxl/core: Collect the regions routed through a DPort Fabio M. De Francesco
@ 2026-08-25 2:26 ` Fabio M. De Francesco
2026-08-25 2:26 ` [PATCH v2 10/13] PCI/CXL: Clear ACS SV across an SBR of a CXL DPort Fabio M. De Francesco
` (3 subsequent siblings)
12 siblings, 0 replies; 15+ messages in thread
From: Fabio M. De Francesco @ 2026-08-25 2:26 UTC (permalink / raw)
To: linux-cxl
Cc: David Hildenbrand, Oscar Salvador, Greg Kroah-Hartman,
Rafael J . Wysocki, Danilo Krummrich, Davidlohr Bueso,
Jonathan Cameron, Dave Jiang, Alison Schofield, Vishal Verma,
Ira Weiny, Li Ming, Bjorn Helgaas, Mahesh J Salgaonkar,
Oliver O'Halloran, Andrew Morton, Lorenzo Stoakes,
Liam R . Howlett, Vlastimil Babka, Mike Rapoport,
Suren Baghdasaryan, Michal Hocko, linux-kernel, linux-mm,
driver-core, linux-pci, linuxppc-dev, Fabio M. De Francesco
A Secondary Bus Reset of a CXL Downstream Port needs every CXL region
routed through that Port disabled for the duration.
Add struct pci_cxl_sbr_region_ops, holding a disable_regions() and an
enable_regions() callback, and pci_cxl_set_sbr_region_ops() for the CXL
core to register them. The pointer is NULL whenever the CXL region code
is absent, either not built or built as a module that is not loaded.
Add cxl_sbr_disable_regions(), which collects the regions with a member
endpoint below the Port and disables each one. If any fails, re-enable
the whole collected set and return the error, so the PCI core aborts the
reset before touching hardware; re-enabling a region left untouched is a
no-op, which also recovers the one that failed midway.
Add cxl_sbr_enable_regions() for the other side. It restores the HDM
decoders below the Port before it re-attaches any region driver, since a
region cannot serve memory through decoders that are not programmed.
cxl_sbr_disable_regions() records the decoder registers before it
touches anything and cxl_sbr_enable_regions() hands them to the restore
and releases them. They are held in an xarray indexed by the Downstream
Port's struct pci_dev, so resets of different Ports do not share an
entry, and they are released on the abort path too, where
enable_regions() never runs.
Register both ops from cxl_region_init() and clear the pointer in
cxl_region_exit().
Signed-off-by: Fabio M. De Francesco <fabio.m.de.francesco@linux.intel.com>
---
drivers/cxl/core/core.h | 9 +--
drivers/cxl/core/dport_sbr.c | 152 +++++++++++++++++++++++++++++++++--
drivers/cxl/core/region.c | 3 +
drivers/pci/pci.c | 13 +++
include/linux/pci.h | 12 +++
5 files changed, 175 insertions(+), 14 deletions(-)
diff --git a/drivers/cxl/core/core.h b/drivers/cxl/core/core.h
index 077a2af9cf0c..b250fa346184 100644
--- a/drivers/cxl/core/core.h
+++ b/drivers/cxl/core/core.h
@@ -57,13 +57,8 @@ int devm_cxl_add_dax_region(struct cxl_region *cxlr);
int devm_cxl_add_pmem_region(struct cxl_region *cxlr);
void kill_regions(struct cxl_root_decoder *cxlrd);
int cxl_region_invalidate_memregion(struct cxl_region *cxlr);
-int cxl_region_disable(struct cxl_region *cxlr);
-void cxl_region_enable(struct cxl_region *cxlr);
-struct pci_dev;
-int cxl_sbr_collect_regions(struct pci_dev *dport_pci, struct xarray *regions);
-void cxl_sbr_put_regions(struct xarray *regions);
-void cxl_sbr_recommit_decoders(struct pci_dev *dport_pci,
- struct xarray *hdm_state);
+struct pci_cxl_sbr_region_ops;
+extern const struct pci_cxl_sbr_region_ops cxl_sbr_region_ops;
#else
static inline u64 cxl_dpa_to_hpa(struct cxl_region *cxlr,
diff --git a/drivers/cxl/core/dport_sbr.c b/drivers/cxl/core/dport_sbr.c
index 233499bc1fad..2b6f840e22a7 100644
--- a/drivers/cxl/core/dport_sbr.c
+++ b/drivers/cxl/core/dport_sbr.c
@@ -21,7 +21,7 @@
* Context: process context. Offlining and driver unbind sleep and take the
* memory hotplug lock, so this cannot run in atomic context.
*/
-int cxl_region_disable(struct cxl_region *cxlr)
+static int cxl_region_disable(struct cxl_region *cxlr)
{
struct cxl_region_params *p = &cxlr->params;
unsigned long block_size;
@@ -72,7 +72,7 @@ int cxl_region_disable(struct cxl_region *cxlr)
* Rebind the region driver. The System RAM is left offline; bringing it back
* online is a separate administrative step.
*/
-void cxl_region_enable(struct cxl_region *cxlr)
+static void cxl_region_enable(struct cxl_region *cxlr)
{
struct cxl_region_params *p = &cxlr->params;
@@ -93,8 +93,8 @@ void cxl_region_enable(struct cxl_region *cxlr)
* cxl_region_disable()/cxl_region_enable() run with the rwsem released (they
* unbind and rebind the region driver). Hence snapshot the set first.
*/
-int cxl_sbr_collect_regions(struct pci_dev *dport_pci,
- struct xarray *regions)
+static int cxl_sbr_collect_regions(struct pci_dev *dport_pci,
+ struct xarray *regions)
{
struct cxl_region_ref *cxl_rr;
struct cxl_dport *dport;
@@ -141,7 +141,7 @@ int cxl_sbr_collect_regions(struct pci_dev *dport_pci,
return 0;
}
-void cxl_sbr_put_regions(struct xarray *regions)
+static void cxl_sbr_put_regions(struct xarray *regions)
{
struct cxl_region *cxlr;
unsigned long index;
@@ -159,8 +159,8 @@ void cxl_sbr_put_regions(struct xarray *regions)
* requires. The caller has already disabled the regions, so nothing reaches the
* decoders being reprogrammed.
*/
-void cxl_sbr_recommit_decoders(struct pci_dev *dport_pci,
- struct xarray *hdm_state)
+static void cxl_sbr_recommit_decoders(struct pci_dev *dport_pci,
+ struct xarray *hdm_state)
{
struct cxl_dport *dport;
int rc;
@@ -179,3 +179,141 @@ void cxl_sbr_recommit_decoders(struct pci_dev *dport_pci,
if (rc)
pci_warn(dport_pci, "HDM decode restore failed: %d\n", rc);
}
+
+/*
+ * The HDM decoder control registers the reset is about to clear, held from the
+ * disable to the enable of one Downstream Port and indexed by that Port's
+ * struct pci_dev, so resets of different Ports do not share an entry.
+ */
+static DEFINE_XARRAY(cxl_sbr_hdm_state);
+
+static void cxl_sbr_drop_hdm_state(struct pci_dev *dport_pci)
+{
+ struct xarray *hdm_state;
+
+ hdm_state = xa_erase(&cxl_sbr_hdm_state, (unsigned long)dport_pci);
+ if (!hdm_state)
+ return;
+
+ cxl_port_put_hdm_state(hdm_state);
+ kfree(hdm_state);
+}
+
+/*
+ * Record the control registers of every port below @dport_pci before the reset
+ * clears them. cxl_sbr_enable_regions() consumes the set and drops it.
+ */
+static int cxl_sbr_save_hdm_state(struct pci_dev *dport_pci)
+{
+ struct xarray *hdm_state;
+ struct cxl_dport *dport;
+ int rc;
+
+ struct cxl_port *port __free(put_cxl_port) =
+ find_cxl_port(&dport_pci->dev, &dport);
+ if (!port)
+ return 0;
+
+ hdm_state = kzalloc_obj(*hdm_state);
+ if (!hdm_state)
+ return -ENOMEM;
+
+ xa_init(hdm_state);
+
+ scoped_guard(rwsem_read, &cxl_rwsem.region)
+ rc = cxl_port_save_hdm_state(port, hdm_state);
+
+ if (!rc)
+ rc = xa_insert(&cxl_sbr_hdm_state, (unsigned long)dport_pci,
+ hdm_state, GFP_KERNEL);
+ if (rc) {
+ cxl_port_put_hdm_state(hdm_state);
+ kfree(hdm_state);
+ return rc;
+ }
+
+ return 0;
+}
+
+/*
+ * Disable the regions routed through the Downstream Port being reset. On
+ * failure re-enable the regions already disabled and return the error so the
+ * PCI core aborts the reset with the topology unchanged.
+ */
+static int cxl_sbr_disable_regions(struct pci_dev *dport_pci)
+{
+ struct cxl_region *cxlr;
+ struct xarray regions;
+ unsigned long index;
+ int rc;
+
+ rc = cxl_sbr_save_hdm_state(dport_pci);
+ if (rc)
+ return rc;
+
+ xa_init(®ions);
+
+ rc = cxl_sbr_collect_regions(dport_pci, ®ions);
+ if (rc)
+ goto out;
+
+ xa_for_each(®ions, index, cxlr) {
+ rc = cxl_region_disable(cxlr);
+ if (rc)
+ break;
+ }
+
+ /*
+ * On failure restore every collected region and return the error so the
+ * PCI core aborts the reset before touching the hardware. Re-enabling a
+ * region left untouched is a no-op, so enabling the whole set also
+ * recovers the region whose offline failed midway.
+ */
+ if (rc) {
+ dev_dbg(&dport_pci->dev, "%s: disable failed (%d), re-enabling collected regions and aborting reset\n",
+ __func__, rc);
+ xa_for_each(®ions, index, cxlr)
+ cxl_region_enable(cxlr);
+ }
+
+out:
+ cxl_sbr_put_regions(®ions);
+ /* No enable_regions() call follows an aborted reset, so drop the set. */
+ if (rc)
+ cxl_sbr_drop_hdm_state(dport_pci);
+ return rc;
+}
+
+/*
+ * Re-enable the regions disabled by cxl_sbr_disable_regions(). Restore the HDM
+ * decode first: a region cannot serve memory through decoders that are not
+ * programmed, so its driver must not re-attach before they are.
+ */
+static void cxl_sbr_enable_regions(struct pci_dev *dport_pci)
+{
+ struct xarray *hdm_state;
+ struct cxl_region *cxlr;
+ struct xarray regions;
+ unsigned long index;
+
+ xa_init(®ions);
+
+ cxl_sbr_collect_regions(dport_pci, ®ions);
+
+ hdm_state = xa_load(&cxl_sbr_hdm_state, (unsigned long)dport_pci);
+ if (hdm_state)
+ cxl_sbr_recommit_decoders(dport_pci, hdm_state);
+ else
+ pci_warn(dport_pci, "no saved HDM state, decode not restored\n");
+
+ xa_for_each(®ions, index, cxlr)
+ cxl_region_enable(cxlr);
+
+ cxl_sbr_put_regions(®ions);
+ cxl_sbr_drop_hdm_state(dport_pci);
+}
+
+const struct pci_cxl_sbr_region_ops cxl_sbr_region_ops = {
+ .disable_regions = cxl_sbr_disable_regions,
+ .enable_regions = cxl_sbr_enable_regions,
+};
diff --git a/drivers/cxl/core/region.c b/drivers/cxl/core/region.c
index fc0bec991a69..d1dd4924fba1 100644
--- a/drivers/cxl/core/region.c
+++ b/drivers/cxl/core/region.c
@@ -12,6 +12,7 @@
#include <linux/idr.h>
#include <linux/memory-tiers.h>
#include <linux/string_choices.h>
+#include <linux/pci.h>
#include <cxlmem.h>
#include <cxl.h>
#include "core.h"
@@ -4263,12 +4264,14 @@ static struct cxl_driver cxl_region_driver = {
int cxl_region_init(void)
{
+ pci_cxl_set_sbr_region_ops(&cxl_sbr_region_ops);
return cxl_driver_register(&cxl_region_driver);
}
void cxl_region_exit(void)
{
cxl_driver_unregister(&cxl_region_driver);
+ pci_cxl_set_sbr_region_ops(NULL);
}
MODULE_IMPORT_NS("CXL");
diff --git a/drivers/pci/pci.c b/drivers/pci/pci.c
index 77b17b13ee61..417b6b44473e 100644
--- a/drivers/pci/pci.c
+++ b/drivers/pci/pci.c
@@ -4844,6 +4844,19 @@ void __weak pcibios_reset_secondary_bus(struct pci_dev *dev)
pci_reset_secondary_bus(dev);
}
+/*
+ * Registered by the CXL core to disable and re-enable the regions mapped
+ * through a CXL Downstream Port across a Secondary Bus Reset. NULL whenever
+ * the CXL region code is absent: not built, or built as a module not loaded.
+ */
+static const struct pci_cxl_sbr_region_ops *cxl_sbr_region_ops;
+
+void pci_cxl_set_sbr_region_ops(const struct pci_cxl_sbr_region_ops *ops)
+{
+ cxl_sbr_region_ops = ops;
+}
+EXPORT_SYMBOL_GPL(pci_cxl_set_sbr_region_ops);
+
/**
* pci_bridge_secondary_bus_reset - Reset the secondary bus on a PCI bridge.
* @dev: Bridge device
diff --git a/include/linux/pci.h b/include/linux/pci.h
index 64b308b6e61c..2feb0e355305 100644
--- a/include/linux/pci.h
+++ b/include/linux/pci.h
@@ -1606,6 +1606,18 @@ int devm_request_pci_bus_resources(struct device *dev,
/* Temporary until new and working PCI SBR API in place */
int pci_bridge_secondary_bus_reset(struct pci_dev *dev);
+/**
+ * struct pci_cxl_sbr_region_ops - CXL region callbacks for a bus reset
+ * @disable_regions: disable the regions below @dport, 0 or errno
+ * @enable_regions: re-enable the regions below @dport
+ */
+struct pci_cxl_sbr_region_ops {
+ int (*disable_regions)(struct pci_dev *dport);
+ void (*enable_regions)(struct pci_dev *dport);
+};
+
+void pci_cxl_set_sbr_region_ops(const struct pci_cxl_sbr_region_ops *ops);
+
#define __pci_bus_for_each_res0(bus, res, ...) \
for (unsigned int __b = 0; \
(res = pci_bus_resource_n(bus, __b)) || __b < PCI_BRIDGE_RESOURCE_NUM; \
--
2.55.0
^ permalink raw reply related [flat|nested] 15+ messages in thread* [PATCH v2 10/13] PCI/CXL: Clear ACS SV across an SBR of a CXL DPort
2026-08-25 2:26 [PATCH v2 00/13] Make SBR work for CXL Downstream Ports Fabio M. De Francesco
` (8 preceding siblings ...)
2026-08-25 2:26 ` [PATCH v2 09/13] PCI/CXL: Disable and re-enable CXL regions Fabio M. De Francesco
@ 2026-08-25 2:26 ` Fabio M. De Francesco
2026-08-26 8:47 ` Richard Cheng
2026-08-25 2:26 ` [PATCH v2 11/13] PCI/DPC: Unbind regions for DPC recovery Fabio M. De Francesco
` (2 subsequent siblings)
12 siblings, 1 reply; 15+ messages in thread
From: Fabio M. De Francesco @ 2026-08-25 2:26 UTC (permalink / raw)
To: linux-cxl
Cc: David Hildenbrand, Oscar Salvador, Greg Kroah-Hartman,
Rafael J . Wysocki, Danilo Krummrich, Davidlohr Bueso,
Jonathan Cameron, Dave Jiang, Alison Schofield, Vishal Verma,
Ira Weiny, Li Ming, Bjorn Helgaas, Mahesh J Salgaonkar,
Oliver O'Halloran, Andrew Morton, Lorenzo Stoakes,
Liam R . Howlett, Vlastimil Babka, Mike Rapoport,
Suren Baghdasaryan, Michal Hocko, linux-kernel, linux-mm,
driver-core, linux-pci, linuxppc-dev, Fabio M. De Francesco
A Secondary Bus Reset (SBR) of a CXL Downstream Port clears the
Downstream Component's (DPort) captured Bus Number. CXL r4.0 sec 8.1.5.1
notes that if a Downstream Component issues PM Init messages to a DPort
with Access Control Services (ACS) Source Validation (SV) bit enabled,
PM Initialization may then fail to complete, and that a further SBR
alone does not recover it. PCIe r7.0 sec 6.12.1.1 makes that rejection
an error that is a reported as ACS Violation.
Reuse the recovery sequence described in sec 8.1.5.1 but make it
preventive, so that PM Init don't fail and don't need to be recovered.
Signed-off-by: Fabio M. De Francesco <fabio.m.de.francesco@linux.intel.com>
---
drivers/pci/pci.c | 177 ++++++++++++++++++++++++++++++++--
include/uapi/linux/pci_regs.h | 2 +
2 files changed, 172 insertions(+), 7 deletions(-)
diff --git a/drivers/pci/pci.c b/drivers/pci/pci.c
index 417b6b44473e..f3781d1e8f6e 100644
--- a/drivers/pci/pci.c
+++ b/drivers/pci/pci.c
@@ -23,6 +23,7 @@
#include <linux/module.h>
#include <linux/spinlock.h>
#include <linux/string.h>
+#include <linux/string_choices.h>
#include <linux/log2.h>
#include <linux/logic_pio.h>
#include <linux/device.h>
@@ -4857,21 +4858,189 @@ void pci_cxl_set_sbr_region_ops(const struct pci_cxl_sbr_region_ops *ops)
}
EXPORT_SYMBOL_GPL(pci_cxl_set_sbr_region_ops);
+struct cxl_sbr_ctx {
+ u16 port_ctl;
+ u16 acs_ctrl;
+ u16 command;
+};
+
+static bool is_cxl_dport(struct pci_dev *dev)
+{
+ return pcie_is_cxl(dev) && pcie_downstream_port(dev);
+}
+
+static u16 cxl_port_dvsec(struct pci_dev *dev)
+{
+ return pci_find_dvsec_capability(dev, PCI_VENDOR_ID_CXL,
+ PCI_DVSEC_CXL_PORT);
+}
+
+static int cxl_sbr_prepare(struct pci_dev *bridge, u16 dvsec,
+ struct cxl_sbr_ctx *ctx)
+{
+ int rc;
+
+ /* Abort before touching hardware if the regions cannot be disabled. */
+ if (cxl_sbr_region_ops) {
+ rc = cxl_sbr_region_ops->disable_regions(bridge);
+ if (rc)
+ return rc;
+ }
+
+ /* CXL r4.0 sec 8.1.5.2, Table 8-32: set Unmask SBR so the Port issues Hot Reset. */
+ pci_read_config_word(bridge, dvsec + PCI_DVSEC_CXL_PORT_CTL, &ctx->port_ctl);
+ pci_write_config_word(bridge, dvsec + PCI_DVSEC_CXL_PORT_CTL,
+ ctx->port_ctl | PCI_DVSEC_CXL_PORT_CTL_UNMASK_SBR);
+
+ pci_read_config_word(bridge, PCI_COMMAND, &ctx->command);
+ pci_clear_master(bridge);
+
+ /* CXL r4.0 sec 8.1.5.1: Disable ACS SV bit before SBR */
+ if (bridge->acs_cap) {
+ pci_read_config_word(bridge, bridge->acs_cap + PCI_ACS_CTRL, &ctx->acs_ctrl);
+ pci_dbg(bridge, "%s: ACS SV %s\n", __func__,
+ str_enabled_disabled(ctx->acs_ctrl & PCI_ACS_SV));
+ pci_write_config_word(bridge, bridge->acs_cap + PCI_ACS_CTRL,
+ ctx->acs_ctrl & ~PCI_ACS_SV);
+ }
+
+ return 0;
+}
+
+/*
+ * CXL r4.0 sec 8.1.5.1, Table 8-31: the Port sets PM Init Complete within
+ * 100 ms of link-up. Restoring ACS Source Validation before then makes the
+ * Port reject the downstream Component's Requester-Bus-0 IP2PM message, so
+ * poll for completion before restoring config.
+ */
+static bool cxl_port_pm_init_is_complete(struct pci_dev *bridge, u16 dvsec)
+{
+ unsigned long start = jiffies;
+ unsigned long timeout = start + msecs_to_jiffies(100);
+ u16 status;
+
+ do {
+ pci_read_config_word(bridge,
+ dvsec + PCI_DVSEC_CXL_PORT_EXT_STATUS,
+ &status);
+ if (!PCI_POSSIBLE_ERROR(status) &&
+ (status & PCI_DVSEC_CXL_PORT_EXT_STATUS_PM_INIT_COMP)) {
+ pci_dbg(bridge, "%s: PM Init Complete set after %u ms, ext status %#06x\n",
+ __func__, jiffies_to_msecs(jiffies - start), status);
+ return true;
+ }
+ msleep(10);
+ } while (time_before(jiffies, timeout));
+
+ pci_warn(bridge, "%s: PM Init Complete not set after %u ms, ext status %#06x\n",
+ __func__, jiffies_to_msecs(jiffies - start), status);
+
+ return false;
+}
+
+static int cxl_sbr_restore_config_space(struct pci_dev *dev, void *userdata)
+{
+ pci_restore_config_space(dev);
+ pci_dbg(dev, "%s: config space restored\n", __func__);
+
+ return 0;
+}
+
+/*
+ * The CXL region ops that run next read the HDM Decoders through a Base Address
+ * Register the reset returned to its initialization value, so restore the
+ * header of every device below @bridge first. Restoring also re-captures each
+ * Bus Number before the Port's ACS Source Validation comes back: a device that
+ * has completed no Type 0 Configuration Write since the reset sources Requests
+ * with Bus 0, which the Port rejects as an ACS Violation.
+ *
+ * Only the header is restored. The capability state each caller saved is its own
+ * to replay, and the ->reset_done() callbacks pci_dev_restore() invokes must
+ * fire once, from the caller that owns the reset.
+ */
+static void cxl_sbr_restore_subordinate(struct pci_dev *bridge)
+{
+ if (!bridge->subordinate)
+ return;
+
+ /* Parents before children: a child answers once its parent forwards. */
+ pci_walk_bus(bridge->subordinate, cxl_sbr_restore_config_space, NULL);
+}
+
+static void cxl_sbr_complete(struct pci_dev *bridge, u16 dvsec,
+ const struct cxl_sbr_ctx *ctx)
+{
+ u16 val;
+
+ /* CXL r4.0 sec 8.1.5.1: wait for PM Init before restoring ACS SV. */
+ if (!cxl_port_pm_init_is_complete(bridge, dvsec))
+ pci_warn(bridge,
+ "restoring ACS Source Validation before PM Init complete; Port may reject the Component's bus 0 traffic\n");
+
+ cxl_sbr_restore_subordinate(bridge);
+
+ /* CXL r4.0 sec 8.1.5.1: Re-enable ACS SV bit after SBR if it was enabled before */
+ if (bridge->acs_cap && (ctx->acs_ctrl & PCI_ACS_SV)) {
+ pci_read_config_word(bridge, bridge->acs_cap + PCI_ACS_CTRL, &val);
+ pci_write_config_word(bridge, bridge->acs_cap + PCI_ACS_CTRL,
+ val | PCI_ACS_SV);
+ pci_dbg(bridge, "%s: ACS SV bit set\n", __func__);
+ } else {
+ pci_dbg(bridge, "%s: ACS SV bit not set (was not enabled before the SBR)\n",
+ __func__);
+ }
+
+ if (ctx->command & PCI_COMMAND_MASTER)
+ pci_set_master(bridge);
+
+ if (!(ctx->port_ctl & PCI_DVSEC_CXL_PORT_CTL_UNMASK_SBR)) {
+ pci_read_config_word(bridge, dvsec + PCI_DVSEC_CXL_PORT_CTL, &val);
+ pci_write_config_word(bridge, dvsec + PCI_DVSEC_CXL_PORT_CTL,
+ val & ~PCI_DVSEC_CXL_PORT_CTL_UNMASK_SBR);
+ }
+
+ if (cxl_sbr_region_ops)
+ cxl_sbr_region_ops->enable_regions(bridge);
+}
+
/**
* pci_bridge_secondary_bus_reset - Reset the secondary bus on a PCI bridge.
* @dev: Bridge device
*
* Use the bridge control register to assert reset on the secondary bus.
* Devices on the secondary bus are left in power-on state.
+ *
+ * When @dev is a CXL Downstream Port, clear ACS Source Validation and Bus
+ * Master Enable across the reset, per the workaround in CXL r4.0 sec 8.1.5.1,
+ * so that Port Power Management Initialization completes at link-up. The
+ * bits stay cleared until the secondary bus is back, then are restored.
*/
int pci_bridge_secondary_bus_reset(struct pci_dev *dev)
{
+ struct cxl_sbr_ctx ctx = {};
+ u16 dvsec = 0;
+ int rc;
+
if (!dev->block_cfg_access)
pci_warn_once(dev, "unlocked secondary bus reset via: %pS\n",
__builtin_return_address(0));
+
+ if (is_cxl_dport(dev))
+ dvsec = cxl_port_dvsec(dev);
+ if (dvsec) {
+ rc = cxl_sbr_prepare(dev, dvsec, &ctx);
+ if (rc)
+ return rc;
+ }
+
pcibios_reset_secondary_bus(dev);
- return pci_bridge_wait_for_secondary_bus(dev, "bus reset");
+ rc = pci_bridge_wait_for_secondary_bus(dev, "bus reset");
+
+ if (dvsec)
+ cxl_sbr_complete(dev, dvsec, &ctx);
+
+ return rc;
}
EXPORT_SYMBOL_GPL(pci_bridge_secondary_bus_reset);
@@ -4917,12 +5086,6 @@ static int pci_dev_reset_slot_function(struct pci_dev *dev, bool probe)
return pci_reset_hotplug_slot(dev->slot->hotplug, probe);
}
-static u16 cxl_port_dvsec(struct pci_dev *dev)
-{
- return pci_find_dvsec_capability(dev, PCI_VENDOR_ID_CXL,
- PCI_DVSEC_CXL_PORT);
-}
-
static bool cxl_sbr_masked(struct pci_dev *dev)
{
u16 dvsec, reg;
diff --git a/include/uapi/linux/pci_regs.h b/include/uapi/linux/pci_regs.h
index facaa324bd86..0eaa34db93ce 100644
--- a/include/uapi/linux/pci_regs.h
+++ b/include/uapi/linux/pci_regs.h
@@ -1371,6 +1371,8 @@
/* CXL r4.0, 8.1.5: Extensions DVSEC for Ports */
#define PCI_DVSEC_CXL_PORT 3
+#define PCI_DVSEC_CXL_PORT_EXT_STATUS 0x0a
+#define PCI_DVSEC_CXL_PORT_EXT_STATUS_PM_INIT_COMP 0x00000001
#define PCI_DVSEC_CXL_PORT_CTL 0x0c
#define PCI_DVSEC_CXL_PORT_CTL_UNMASK_SBR 0x00000001
--
2.55.0
^ permalink raw reply related [flat|nested] 15+ messages in thread* Re: [PATCH v2 10/13] PCI/CXL: Clear ACS SV across an SBR of a CXL DPort
2026-08-25 2:26 ` [PATCH v2 10/13] PCI/CXL: Clear ACS SV across an SBR of a CXL DPort Fabio M. De Francesco
@ 2026-08-26 8:47 ` Richard Cheng
0 siblings, 0 replies; 15+ messages in thread
From: Richard Cheng @ 2026-08-26 8:47 UTC (permalink / raw)
To: Fabio M. De Francesco
Cc: linux-cxl, David Hildenbrand, Oscar Salvador, Greg Kroah-Hartman,
Rafael J . Wysocki, Danilo Krummrich, Davidlohr Bueso,
Jonathan Cameron, Dave Jiang, Alison Schofield, Vishal Verma,
Ira Weiny, Li Ming, Bjorn Helgaas, Mahesh J Salgaonkar,
Oliver O'Halloran, Andrew Morton, Lorenzo Stoakes,
Liam R . Howlett, Vlastimil Babka, Mike Rapoport,
Suren Baghdasaryan, Michal Hocko, linux-kernel, linux-mm,
driver-core, linux-pci, linuxppc-dev
On Tue, Aug 25, 2026 at 04:26:25AM +0800, Fabio M. De Francesco wrote:
> A Secondary Bus Reset (SBR) of a CXL Downstream Port clears the
> Downstream Component's (DPort) captured Bus Number. CXL r4.0 sec 8.1.5.1
> notes that if a Downstream Component issues PM Init messages to a DPort
> with Access Control Services (ACS) Source Validation (SV) bit enabled,
> PM Initialization may then fail to complete, and that a further SBR
> alone does not recover it. PCIe r7.0 sec 6.12.1.1 makes that rejection
> an error that is a reported as ACS Violation.
>
> Reuse the recovery sequence described in sec 8.1.5.1 but make it
> preventive, so that PM Init don't fail and don't need to be recovered.
>
> Signed-off-by: Fabio M. De Francesco <fabio.m.de.francesco@linux.intel.com>
> ---
> drivers/pci/pci.c | 177 ++++++++++++++++++++++++++++++++--
> include/uapi/linux/pci_regs.h | 2 +
> 2 files changed, 172 insertions(+), 7 deletions(-)
>
> diff --git a/drivers/pci/pci.c b/drivers/pci/pci.c
> index 417b6b44473e..f3781d1e8f6e 100644
> --- a/drivers/pci/pci.c
> +++ b/drivers/pci/pci.c
> @@ -23,6 +23,7 @@
> #include <linux/module.h>
> #include <linux/spinlock.h>
> #include <linux/string.h>
> +#include <linux/string_choices.h>
> #include <linux/log2.h>
> #include <linux/logic_pio.h>
> #include <linux/device.h>
> @@ -4857,21 +4858,189 @@ void pci_cxl_set_sbr_region_ops(const struct pci_cxl_sbr_region_ops *ops)
> }
> EXPORT_SYMBOL_GPL(pci_cxl_set_sbr_region_ops);
>
> +struct cxl_sbr_ctx {
> + u16 port_ctl;
> + u16 acs_ctrl;
> + u16 command;
> +};
> +
> +static bool is_cxl_dport(struct pci_dev *dev)
> +{
> + return pcie_is_cxl(dev) && pcie_downstream_port(dev);
> +}
> +
> +static u16 cxl_port_dvsec(struct pci_dev *dev)
> +{
> + return pci_find_dvsec_capability(dev, PCI_VENDOR_ID_CXL,
> + PCI_DVSEC_CXL_PORT);
> +}
> +
> +static int cxl_sbr_prepare(struct pci_dev *bridge, u16 dvsec,
> + struct cxl_sbr_ctx *ctx)
> +{
> + int rc;
> +
> + /* Abort before touching hardware if the regions cannot be disabled. */
> + if (cxl_sbr_region_ops) {
> + rc = cxl_sbr_region_ops->disable_regions(bridge);
> + if (rc)
> + return rc;
> + }
> +
> + /* CXL r4.0 sec 8.1.5.2, Table 8-32: set Unmask SBR so the Port issues Hot Reset. */
> + pci_read_config_word(bridge, dvsec + PCI_DVSEC_CXL_PORT_CTL, &ctx->port_ctl);
> + pci_write_config_word(bridge, dvsec + PCI_DVSEC_CXL_PORT_CTL,
> + ctx->port_ctl | PCI_DVSEC_CXL_PORT_CTL_UNMASK_SBR);
> +
> + pci_read_config_word(bridge, PCI_COMMAND, &ctx->command);
> + pci_clear_master(bridge);
> +
> + /* CXL r4.0 sec 8.1.5.1: Disable ACS SV bit before SBR */
> + if (bridge->acs_cap) {
> + pci_read_config_word(bridge, bridge->acs_cap + PCI_ACS_CTRL, &ctx->acs_ctrl);
> + pci_dbg(bridge, "%s: ACS SV %s\n", __func__,
> + str_enabled_disabled(ctx->acs_ctrl & PCI_ACS_SV));
> + pci_write_config_word(bridge, bridge->acs_cap + PCI_ACS_CTRL,
> + ctx->acs_ctrl & ~PCI_ACS_SV);
> + }
> +
> + return 0;
> +}
> +
> +/*
> + * CXL r4.0 sec 8.1.5.1, Table 8-31: the Port sets PM Init Complete within
> + * 100 ms of link-up. Restoring ACS Source Validation before then makes the
> + * Port reject the downstream Component's Requester-Bus-0 IP2PM message, so
> + * poll for completion before restoring config.
> + */
> +static bool cxl_port_pm_init_is_complete(struct pci_dev *bridge, u16 dvsec)
> +{
> + unsigned long start = jiffies;
> + unsigned long timeout = start + msecs_to_jiffies(100);
> + u16 status;
> +
> + do {
> + pci_read_config_word(bridge,
> + dvsec + PCI_DVSEC_CXL_PORT_EXT_STATUS,
> + &status);
> + if (!PCI_POSSIBLE_ERROR(status) &&
> + (status & PCI_DVSEC_CXL_PORT_EXT_STATUS_PM_INIT_COMP)) {
> + pci_dbg(bridge, "%s: PM Init Complete set after %u ms, ext status %#06x\n",
> + __func__, jiffies_to_msecs(jiffies - start), status);
> + return true;
> + }
> + msleep(10);
> + } while (time_before(jiffies, timeout));
> +
> + pci_warn(bridge, "%s: PM Init Complete not set after %u ms, ext status %#06x\n",
> + __func__, jiffies_to_msecs(jiffies - start), status);
> +
> + return false;
> +}
> +
> +static int cxl_sbr_restore_config_space(struct pci_dev *dev, void *userdata)
> +{
> + pci_restore_config_space(dev);
> + pci_dbg(dev, "%s: config space restored\n", __func__);
> +
> + return 0;
> +}
> +
> +/*
> + * The CXL region ops that run next read the HDM Decoders through a Base Address
> + * Register the reset returned to its initialization value, so restore the
> + * header of every device below @bridge first. Restoring also re-captures each
> + * Bus Number before the Port's ACS Source Validation comes back: a device that
> + * has completed no Type 0 Configuration Write since the reset sources Requests
> + * with Bus 0, which the Port rejects as an ACS Violation.
> + *
> + * Only the header is restored. The capability state each caller saved is its own
> + * to replay, and the ->reset_done() callbacks pci_dev_restore() invokes must
> + * fire once, from the caller that owns the reset.
> + */
> +static void cxl_sbr_restore_subordinate(struct pci_dev *bridge)
> +{
> + if (!bridge->subordinate)
> + return;
> +
> + /* Parents before children: a child answers once its parent forwards. */
> + pci_walk_bus(bridge->subordinate, cxl_sbr_restore_config_space, NULL);
> +}
> +
> +static void cxl_sbr_complete(struct pci_dev *bridge, u16 dvsec,
> + const struct cxl_sbr_ctx *ctx)
> +{
> + u16 val;
> +
> + /* CXL r4.0 sec 8.1.5.1: wait for PM Init before restoring ACS SV. */
> + if (!cxl_port_pm_init_is_complete(bridge, dvsec))
> + pci_warn(bridge,
> + "restoring ACS Source Validation before PM Init complete; Port may reject the Component's bus 0 traffic\n");
> +
> + cxl_sbr_restore_subordinate(bridge);
> +
> + /* CXL r4.0 sec 8.1.5.1: Re-enable ACS SV bit after SBR if it was enabled before */
> + if (bridge->acs_cap && (ctx->acs_ctrl & PCI_ACS_SV)) {
> + pci_read_config_word(bridge, bridge->acs_cap + PCI_ACS_CTRL, &val);
> + pci_write_config_word(bridge, bridge->acs_cap + PCI_ACS_CTRL,
> + val | PCI_ACS_SV);
> + pci_dbg(bridge, "%s: ACS SV bit set\n", __func__);
> + } else {
> + pci_dbg(bridge, "%s: ACS SV bit not set (was not enabled before the SBR)\n",
> + __func__);
> + }
> +
> + if (ctx->command & PCI_COMMAND_MASTER)
> + pci_set_master(bridge);
> +
> + if (!(ctx->port_ctl & PCI_DVSEC_CXL_PORT_CTL_UNMASK_SBR)) {
> + pci_read_config_word(bridge, dvsec + PCI_DVSEC_CXL_PORT_CTL, &val);
> + pci_write_config_word(bridge, dvsec + PCI_DVSEC_CXL_PORT_CTL,
> + val & ~PCI_DVSEC_CXL_PORT_CTL_UNMASK_SBR);
> + }
> +
> + if (cxl_sbr_region_ops)
> + cxl_sbr_region_ops->enable_regions(bridge);
> +}
> +
> /**
> * pci_bridge_secondary_bus_reset - Reset the secondary bus on a PCI bridge.
> * @dev: Bridge device
> *
> * Use the bridge control register to assert reset on the secondary bus.
> * Devices on the secondary bus are left in power-on state.
> + *
> + * When @dev is a CXL Downstream Port, clear ACS Source Validation and Bus
> + * Master Enable across the reset, per the workaround in CXL r4.0 sec 8.1.5.1,
> + * so that Port Power Management Initialization completes at link-up. The
> + * bits stay cleared until the secondary bus is back, then are restored.
> */
> int pci_bridge_secondary_bus_reset(struct pci_dev *dev)
> {
> + struct cxl_sbr_ctx ctx = {};
> + u16 dvsec = 0;
> + int rc;
> +
> if (!dev->block_cfg_access)
> pci_warn_once(dev, "unlocked secondary bus reset via: %pS\n",
> __builtin_return_address(0));
> +
> + if (is_cxl_dport(dev))
> + dvsec = cxl_port_dvsec(dev);
> + if (dvsec) {
> + rc = cxl_sbr_prepare(dev, dvsec, &ctx);
> + if (rc)
> + return rc;
> + }
> +
> pcibios_reset_secondary_bus(dev);
>
> - return pci_bridge_wait_for_secondary_bus(dev, "bus reset");
> + rc = pci_bridge_wait_for_secondary_bus(dev, "bus reset");
> +
> + if (dvsec)
> + cxl_sbr_complete(dev, dvsec, &ctx);
Hi Fabio,
I wonder what happens if pci_bridge_wait_for_secondary_bus() fails here?
Maybe failures before/after SBR should be handled differenly ?
Before SBR, it's safe to undo preparation. After SBR, the HW state may be unknown,
in that case, enable_regions() must not reattach regions unless PCI state and all HDM
decoders were restored successfully.
Best regards,
Richard Cheng
> +
> + return rc;
> }
> EXPORT_SYMBOL_GPL(pci_bridge_secondary_bus_reset);
>
> @@ -4917,12 +5086,6 @@ static int pci_dev_reset_slot_function(struct pci_dev *dev, bool probe)
> return pci_reset_hotplug_slot(dev->slot->hotplug, probe);
> }
>
> -static u16 cxl_port_dvsec(struct pci_dev *dev)
> -{
> - return pci_find_dvsec_capability(dev, PCI_VENDOR_ID_CXL,
> - PCI_DVSEC_CXL_PORT);
> -}
> -
> static bool cxl_sbr_masked(struct pci_dev *dev)
> {
> u16 dvsec, reg;
> diff --git a/include/uapi/linux/pci_regs.h b/include/uapi/linux/pci_regs.h
> index facaa324bd86..0eaa34db93ce 100644
> --- a/include/uapi/linux/pci_regs.h
> +++ b/include/uapi/linux/pci_regs.h
> @@ -1371,6 +1371,8 @@
>
> /* CXL r4.0, 8.1.5: Extensions DVSEC for Ports */
> #define PCI_DVSEC_CXL_PORT 3
> +#define PCI_DVSEC_CXL_PORT_EXT_STATUS 0x0a
> +#define PCI_DVSEC_CXL_PORT_EXT_STATUS_PM_INIT_COMP 0x00000001
> #define PCI_DVSEC_CXL_PORT_CTL 0x0c
> #define PCI_DVSEC_CXL_PORT_CTL_UNMASK_SBR 0x00000001
>
> --
> 2.55.0
>
>
^ permalink raw reply [flat|nested] 15+ messages in thread
* [PATCH v2 11/13] PCI/DPC: Unbind regions for DPC recovery
2026-08-25 2:26 [PATCH v2 00/13] Make SBR work for CXL Downstream Ports Fabio M. De Francesco
` (9 preceding siblings ...)
2026-08-25 2:26 ` [PATCH v2 10/13] PCI/CXL: Clear ACS SV across an SBR of a CXL DPort Fabio M. De Francesco
@ 2026-08-25 2:26 ` Fabio M. De Francesco
2026-08-25 2:26 ` [PATCH v2 12/13] PCI/CXL: Add a sysfs entry to unmask SBR Fabio M. De Francesco
2026-08-25 2:26 ` [PATCH v2 13/13] PCI/CXL: Refuse an SBR of a CXL DPort unless authorized Fabio M. De Francesco
12 siblings, 0 replies; 15+ messages in thread
From: Fabio M. De Francesco @ 2026-08-25 2:26 UTC (permalink / raw)
To: linux-cxl
Cc: David Hildenbrand, Oscar Salvador, Greg Kroah-Hartman,
Rafael J . Wysocki, Danilo Krummrich, Davidlohr Bueso,
Jonathan Cameron, Dave Jiang, Alison Schofield, Vishal Verma,
Ira Weiny, Li Ming, Bjorn Helgaas, Mahesh J Salgaonkar,
Oliver O'Halloran, Andrew Morton, Lorenzo Stoakes,
Liam R . Howlett, Vlastimil Babka, Mike Rapoport,
Suren Baghdasaryan, Michal Hocko, linux-kernel, linux-mm,
driver-core, linux-pci, linuxppc-dev, Fabio M. De Francesco
CXL r4.0 sec 8.1.5.1 lists Downstream Port Containment among the
conditions that reset the Downstream Component's bus number. DPC
recovery needs to wait for the link to come back up, but if Access
Control Services Source Validation bit is enables PM Initialization will
fail to complete.
Have dpc_reset_link() reset the secondary bus of a CXL Downstream Port.
Split pci_bridge_secondary_bus_reset() so the reset takes an action
saying what to do with the CXL regions routed through the Port, and pass
the action that unbinds them without offlining their memory. Offlining
migrates the memory that the memdev back and on a contained link are not
addressable. Don't release the memdev driver in cxl_error_detected().
Signed-off-by: Fabio M. De Francesco <fabio.m.de.francesco@linux.intel.com>
---
drivers/cxl/core/dport_sbr.c | 57 ++++++++++++++++++++++++++++++++-
drivers/cxl/core/ras.c | 10 ++++++
drivers/pci/pci.c | 62 ++++++++++++++++++++++++++----------
drivers/pci/pci.h | 15 +++++++++
drivers/pci/pcie/dpc.c | 46 +++++++++++++++++++++++++-
include/linux/aer.h | 9 ++++++
include/linux/pci.h | 3 ++
7 files changed, 183 insertions(+), 19 deletions(-)
diff --git a/drivers/cxl/core/dport_sbr.c b/drivers/cxl/core/dport_sbr.c
index 2b6f840e22a7..823b63012f45 100644
--- a/drivers/cxl/core/dport_sbr.c
+++ b/drivers/cxl/core/dport_sbr.c
@@ -9,6 +9,30 @@
#include <cxl.h>
#include "core.h"
+/*
+ * cxl_region_unbind - take a region out of service ahead of a reset
+ * @cxlr: region routed through the CXL Downstream Port being reset
+ *
+ * Unbind the region driver, which tears down everything built on the region:
+ * the dax region device, its dax device and the driver bound to it. An SBR
+ * zeroes the downstream bus number, so a region left bound would decode to a
+ * device in reset.
+ *
+ * The memory the region hosts is left as it is. A caller that reaches a live
+ * device offlines it first; see cxl_region_disable().
+ *
+ * Context: process context. Driver unbind sleeps, so this cannot run in atomic
+ * context.
+ */
+static void cxl_region_unbind(struct cxl_region *cxlr)
+{
+ struct cxl_region_params *p = &cxlr->params;
+
+ device_release_driver(&cxlr->dev);
+ dev_dbg(&cxlr->dev, "%s: region unbound before reset, HPA %pr\n",
+ __func__, p->res);
+}
+
/*
* cxl_region_disable - make a region inactive ahead of a Secondary Bus Reset
* @cxlr: region routed through the CXL Downstream Port being reset
@@ -58,7 +82,7 @@ static int cxl_region_disable(struct cxl_region *cxlr)
return rc;
}
- device_release_driver(&cxlr->dev);
+ cxl_region_unbind(cxlr);
dev_dbg(&cxlr->dev, "%s: System RAM offline, region disabled before reset, HPA %pr\n",
__func__, p->res);
@@ -284,6 +308,36 @@ static int cxl_sbr_disable_regions(struct pci_dev *dport_pci)
return rc;
}
+/*
+ * Unbind the regions routed through the Downstream Port being reset, leaving
+ * their memory online. Used on the DPC recovery path, where dpc_reset_link()
+ * clears DPC Trigger Status and enters the reset without waiting for the link,
+ * so the device may still be unreachable and the page migration that an offline
+ * performs would have no device to read from.
+ *
+ * Unbinding cannot fail, so unlike cxl_sbr_disable_regions() this never aborts
+ * the reset. The memory stays online across the reset with no region decoding
+ * it; cxl_sbr_enable_regions() reprograms the decoders on the way out.
+ */
+static void cxl_sbr_unbind_regions(struct pci_dev *dport_pci)
+{
+ struct cxl_region *cxlr;
+ struct xarray regions;
+ unsigned long index;
+
+ if (cxl_sbr_save_hdm_state(dport_pci))
+ pci_warn(dport_pci, "HDM state not saved, decode will not be restored\n");
+
+ xa_init(®ions);
+
+ cxl_sbr_collect_regions(dport_pci, ®ions);
+
+ xa_for_each(®ions, index, cxlr)
+ cxl_region_unbind(cxlr);
+
+ cxl_sbr_put_regions(®ions);
+}
+
/*
* Re-enable the regions disabled by cxl_sbr_disable_regions(). Restore the HDM
* decode first: a region cannot serve memory through decoders that are not
@@ -315,5 +369,6 @@ static void cxl_sbr_enable_regions(struct pci_dev *dport_pci)
const struct pci_cxl_sbr_region_ops cxl_sbr_region_ops = {
.disable_regions = cxl_sbr_disable_regions,
+ .unbind_regions = cxl_sbr_unbind_regions,
.enable_regions = cxl_sbr_enable_regions,
};
diff --git a/drivers/cxl/core/ras.c b/drivers/cxl/core/ras.c
index 99fb00949c2f..568a906f3b50 100644
--- a/drivers/cxl/core/ras.c
+++ b/drivers/cxl/core/ras.c
@@ -323,6 +323,16 @@ pci_ers_result_t cxl_error_detected(struct pci_dev *pdev,
}
return PCI_ERS_RESULT_CAN_RECOVER;
case pci_channel_io_frozen:
+ /*
+ * A Port on the path in DPC means dpc_reset_link() is about to
+ * reset the link, and that path takes the CXL regions out of
+ * service and restores the HDM decode itself. Keep the memdev
+ * driver bound so the endpoint and its decoders are still there
+ * to restore.
+ */
+ if (pci_dpc_containment_active(pdev))
+ return PCI_ERS_RESULT_NEED_RESET;
+
dev_warn(&pdev->dev,
"%s: frozen state error detected, disable CXL.mem\n",
dev_name(dev));
diff --git a/drivers/pci/pci.c b/drivers/pci/pci.c
index f3781d1e8f6e..eedd516f8484 100644
--- a/drivers/pci/pci.c
+++ b/drivers/pci/pci.c
@@ -4864,24 +4864,37 @@ struct cxl_sbr_ctx {
u16 command;
};
-static bool is_cxl_dport(struct pci_dev *dev)
+bool is_cxl_dport(struct pci_dev *dev)
{
return pcie_is_cxl(dev) && pcie_downstream_port(dev);
}
-static u16 cxl_port_dvsec(struct pci_dev *dev)
+u16 cxl_port_dvsec(struct pci_dev *dev)
{
return pci_find_dvsec_capability(dev, PCI_VENDOR_ID_CXL,
PCI_DVSEC_CXL_PORT);
}
static int cxl_sbr_prepare(struct pci_dev *bridge, u16 dvsec,
- struct cxl_sbr_ctx *ctx)
+ struct cxl_sbr_ctx *ctx,
+ enum cxl_sbr_region_action action)
{
int rc;
- /* Abort before touching hardware if the regions cannot be disabled. */
- if (cxl_sbr_region_ops) {
+ /*
+ * CXL_SBR_UNBIND: the link is already down, so offlining the regions'
+ * memory would take the reads that page migration performs as a machine
+ * check. Per PCIe r7.0 sec 2.9.3 the Port answers a Non-Posted Request
+ * with an Unsupported Request or Completer Abort completion while it is
+ * in DPC. Unbinding never fails, so the reset always goes ahead.
+ *
+ * CXL_SBR_OFFLINE_AND_UNBIND: the device is reachable, so offline the
+ * memory first and abort the reset before touching hardware if that
+ * fails.
+ */
+ if (cxl_sbr_region_ops && action == CXL_SBR_UNBIND) {
+ cxl_sbr_region_ops->unbind_regions(bridge);
+ } else if (cxl_sbr_region_ops) {
rc = cxl_sbr_region_ops->disable_regions(bridge);
if (rc)
return rc;
@@ -5003,19 +5016,17 @@ static void cxl_sbr_complete(struct pci_dev *bridge, u16 dvsec,
cxl_sbr_region_ops->enable_regions(bridge);
}
-/**
- * pci_bridge_secondary_bus_reset - Reset the secondary bus on a PCI bridge.
- * @dev: Bridge device
- *
- * Use the bridge control register to assert reset on the secondary bus.
- * Devices on the secondary bus are left in power-on state.
+/*
+ * __pci_bridge_secondary_bus_reset - assert Secondary Bus Reset on a bridge
+ * @dev: bridge device
+ * @action: what to do with the CXL regions reached through @dev
*
- * When @dev is a CXL Downstream Port, clear ACS Source Validation and Bus
- * Master Enable across the reset, per the workaround in CXL r4.0 sec 8.1.5.1,
- * so that Port Power Management Initialization completes at link-up. The
- * bits stay cleared until the secondary bus is back, then are restored.
+ * See pci_bridge_secondary_bus_reset(). Pass CXL_SBR_UNBIND when the link is
+ * already down, which leaves the regions' memory online because offlining it
+ * needs a reachable device.
*/
-int pci_bridge_secondary_bus_reset(struct pci_dev *dev)
+int __pci_bridge_secondary_bus_reset(struct pci_dev *dev,
+ enum cxl_sbr_region_action action)
{
struct cxl_sbr_ctx ctx = {};
u16 dvsec = 0;
@@ -5028,7 +5039,7 @@ int pci_bridge_secondary_bus_reset(struct pci_dev *dev)
if (is_cxl_dport(dev))
dvsec = cxl_port_dvsec(dev);
if (dvsec) {
- rc = cxl_sbr_prepare(dev, dvsec, &ctx);
+ rc = cxl_sbr_prepare(dev, dvsec, &ctx, action);
if (rc)
return rc;
}
@@ -5042,6 +5053,23 @@ int pci_bridge_secondary_bus_reset(struct pci_dev *dev)
return rc;
}
+
+/**
+ * pci_bridge_secondary_bus_reset - Reset the secondary bus on a PCI bridge.
+ * @dev: Bridge device
+ *
+ * Use the bridge control register to assert reset on the secondary bus.
+ * Devices on the secondary bus are left in power-on state.
+ *
+ * When @dev is a CXL Downstream Port, clear ACS Source Validation and Bus
+ * Master Enable across the reset, per the workaround in CXL r4.0 sec 8.1.5.1,
+ * so that Port Power Management Initialization completes at link-up. The
+ * bits stay cleared until the secondary bus is back, then are restored.
+ */
+int pci_bridge_secondary_bus_reset(struct pci_dev *dev)
+{
+ return __pci_bridge_secondary_bus_reset(dev, CXL_SBR_OFFLINE_AND_UNBIND);
+}
EXPORT_SYMBOL_GPL(pci_bridge_secondary_bus_reset);
static int pci_parent_bus_reset(struct pci_dev *dev, bool probe)
diff --git a/drivers/pci/pci.h b/drivers/pci/pci.h
index 4469e1a77f3c..b6d873b077ed 100644
--- a/drivers/pci/pci.h
+++ b/drivers/pci/pci.h
@@ -233,6 +233,21 @@ int pci_mmap_fits(struct pci_dev *pdev, int resno, struct vm_area_struct *vmai,
bool pci_reset_supported(struct pci_dev *dev);
void pci_init_reset_methods(struct pci_dev *dev);
int pci_bridge_secondary_bus_reset(struct pci_dev *dev);
+
+/*
+ * What to do with the CXL regions reached through a Downstream Port before it
+ * is reset. Offlining their memory needs a reachable device, so a Port whose
+ * link is already down only unbinds and leaves the memory online.
+ */
+enum cxl_sbr_region_action {
+ CXL_SBR_OFFLINE_AND_UNBIND,
+ CXL_SBR_UNBIND,
+};
+
+int __pci_bridge_secondary_bus_reset(struct pci_dev *dev,
+ enum cxl_sbr_region_action action);
+bool is_cxl_dport(struct pci_dev *dev);
+u16 cxl_port_dvsec(struct pci_dev *dev);
int pci_bus_error_reset(struct pci_dev *dev);
int pci_try_reset_bridge(struct pci_dev *bridge);
diff --git a/drivers/pci/pcie/dpc.c b/drivers/pci/pcie/dpc.c
index 2b779bd1d861..ad74086bf82c 100644
--- a/drivers/pci/pcie/dpc.c
+++ b/drivers/pci/pcie/dpc.c
@@ -127,6 +127,44 @@ bool pci_dpc_recovered(struct pci_dev *pdev)
}
#endif /* CONFIG_HOTPLUG_PCI_PCIE */
+/**
+ * pci_dpc_containment_active - whether a Port above @pdev is contained by DPC
+ * @pdev: PCI device below the Port
+ *
+ * Per PCIe r7.0 sec 2.9.3 the Port's LTSSM stays in the Disabled state while
+ * DPC Trigger Status is set, and dpc_reset_link() clears that bit only after
+ * pcie_do_recovery() has broadcast error_detected. A ->error_detected()
+ * callback can therefore use this to tell a DPC containment from any other
+ * frozen-channel error, and to know that the link is about to be reset.
+ *
+ * The Port that triggered is on the path to @pdev, because the broadcast walks
+ * that Port's subordinate bus, so test every bridge above @pdev.
+ *
+ * Return: true if a Port on the path to @pdev has DPC Trigger Status set.
+ */
+bool pci_dpc_containment_active(struct pci_dev *pdev)
+{
+ struct pci_dev *bridge;
+
+ for (bridge = pci_upstream_bridge(pdev); bridge;
+ bridge = pci_upstream_bridge(bridge)) {
+ u16 status;
+
+ if (!bridge->dpc_cap)
+ continue;
+
+ pci_read_config_word(bridge,
+ bridge->dpc_cap + PCI_EXP_DPC_STATUS,
+ &status);
+ if (!PCI_POSSIBLE_ERROR(status) &&
+ (status & PCI_EXP_DPC_STATUS_TRIGGER))
+ return true;
+ }
+
+ return false;
+}
+EXPORT_SYMBOL_GPL(pci_dpc_containment_active);
+
static int dpc_wait_rp_inactive(struct pci_dev *pdev)
{
unsigned long timeout = jiffies + HZ;
@@ -149,6 +187,7 @@ pci_ers_result_t dpc_reset_link(struct pci_dev *pdev)
{
pci_ers_result_t ret;
u16 cap;
+ int rc;
set_bit(PCI_DPC_RECOVERING, &pdev->priv_flags);
@@ -174,7 +213,12 @@ pci_ers_result_t dpc_reset_link(struct pci_dev *pdev)
pci_write_config_word(pdev, cap + PCI_EXP_DPC_STATUS,
PCI_EXP_DPC_STATUS_TRIGGER);
- if (pci_bridge_wait_for_secondary_bus(pdev, "DPC")) {
+ if (is_cxl_dport(pdev) && cxl_port_dvsec(pdev))
+ rc = __pci_bridge_secondary_bus_reset(pdev, CXL_SBR_UNBIND);
+ else
+ rc = pci_bridge_wait_for_secondary_bus(pdev, "DPC");
+
+ if (rc) {
clear_bit(PCI_DPC_RECOVERED, &pdev->priv_flags);
ret = PCI_ERS_RESULT_DISCONNECT;
} else {
diff --git a/include/linux/aer.h b/include/linux/aer.h
index df0f5c382286..7ac029f01c25 100644
--- a/include/linux/aer.h
+++ b/include/linux/aer.h
@@ -66,6 +66,15 @@ static inline int pcie_aer_is_native(struct pci_dev *dev) { return 0; }
static inline void pci_aer_unmask_internal_errors(struct pci_dev *dev) { }
#endif
+#if defined(CONFIG_PCIE_DPC)
+bool pci_dpc_containment_active(struct pci_dev *pdev);
+#else
+static inline bool pci_dpc_containment_active(struct pci_dev *pdev)
+{
+ return false;
+}
+#endif
+
void pci_print_aer(struct pci_dev *dev, int aer_severity,
struct aer_capability_regs *aer);
int cper_severity_to_aer(int cper_severity);
diff --git a/include/linux/pci.h b/include/linux/pci.h
index 2feb0e355305..0d9832ce6f3d 100644
--- a/include/linux/pci.h
+++ b/include/linux/pci.h
@@ -1609,10 +1609,13 @@ int pci_bridge_secondary_bus_reset(struct pci_dev *dev);
/**
* struct pci_cxl_sbr_region_ops - CXL region callbacks for a bus reset
* @disable_regions: disable the regions below @dport, 0 or errno
+ * @unbind_regions: unbind the drivers of the regions below @dport, leaving
+ * their memory online, for a link already contained
* @enable_regions: re-enable the regions below @dport
*/
struct pci_cxl_sbr_region_ops {
int (*disable_regions)(struct pci_dev *dport);
+ void (*unbind_regions)(struct pci_dev *dport);
void (*enable_regions)(struct pci_dev *dport);
};
--
2.55.0
^ permalink raw reply related [flat|nested] 15+ messages in thread* [PATCH v2 12/13] PCI/CXL: Add a sysfs entry to unmask SBR
2026-08-25 2:26 [PATCH v2 00/13] Make SBR work for CXL Downstream Ports Fabio M. De Francesco
` (10 preceding siblings ...)
2026-08-25 2:26 ` [PATCH v2 11/13] PCI/DPC: Unbind regions for DPC recovery Fabio M. De Francesco
@ 2026-08-25 2:26 ` Fabio M. De Francesco
2026-08-25 2:26 ` [PATCH v2 13/13] PCI/CXL: Refuse an SBR of a CXL DPort unless authorized Fabio M. De Francesco
12 siblings, 0 replies; 15+ messages in thread
From: Fabio M. De Francesco @ 2026-08-25 2:26 UTC (permalink / raw)
To: linux-cxl
Cc: David Hildenbrand, Oscar Salvador, Greg Kroah-Hartman,
Rafael J . Wysocki, Danilo Krummrich, Davidlohr Bueso,
Jonathan Cameron, Dave Jiang, Alison Schofield, Vishal Verma,
Ira Weiny, Li Ming, Bjorn Helgaas, Mahesh J Salgaonkar,
Oliver O'Halloran, Andrew Morton, Lorenzo Stoakes,
Liam R . Howlett, Vlastimil Babka, Mike Rapoport,
Suren Baghdasaryan, Michal Hocko, linux-kernel, linux-mm,
driver-core, linux-pci, linuxppc-dev, Fabio M. De Francesco
A Secondary Bus Reset of a CXL Downstream Port only reaches the component
below it when the Port's Unmask SBR bit is set. CXL r4.0 sec 8.1.5.2
Table 8-32: "When cleared to 0 (default), the SBR bit in this Port's
Bridge Control register has no effect." A kernel that unmasks it on its own initiative overrules
that for every path reaching pci_bridge_secondary_bus_reset() SBR.
Add a cxl_unmask_sbr attribute beside reset_subordinate in the Port's
sysfs directory to allow kernel triggered SBR of CXL DPorts.
Signed-off-by: Fabio M. De Francesco <fabio.m.de.francesco@linux.intel.com>
---
Documentation/ABI/testing/sysfs-bus-pci | 16 +++++++++++++
drivers/pci/pci-sysfs.c | 31 +++++++++++++++++++++++++
include/linux/pci.h | 1 +
3 files changed, 48 insertions(+)
diff --git a/Documentation/ABI/testing/sysfs-bus-pci b/Documentation/ABI/testing/sysfs-bus-pci
index b767db2c52cb..7b0314da6f4d 100644
--- a/Documentation/ABI/testing/sysfs-bus-pci
+++ b/Documentation/ABI/testing/sysfs-bus-pci
@@ -174,6 +174,22 @@ Description:
similiar to writing 1 to their individual "reset" file, so use
with caution.
+What: /sys/bus/pci/devices/.../cxl_unmask_sbr
+Date: August 2026
+Contact: linux-pci@vger.kernel.org
+Description:
+ This is visible only for a CXL Downstream Port, that is a Root
+ Port or a Downstream Switch Port that publishes the CXL
+ Extensions DVSEC for Ports. Such a Port ignores the Secondary
+ Bus Reset bit in its Bridge Control register unless its
+ "Unmask SBR" bit is set, and system firmware leaves that bit
+ clear by default.
+
+ Writing 1 allows the kernel to set "Unmask SBR" while it
+ resets the Port's secondary bus, so that the reset reaches the
+ component below the Port. While this reads 0, a bus reset of
+ the Port fails instead.
+
What: /sys/bus/pci/devices/.../vpd
Date: February 2008
Contact: Ben Hutchings <bwh@kernel.org>
diff --git a/drivers/pci/pci-sysfs.c b/drivers/pci/pci-sysfs.c
index 5ec0b245a69b..ba1cae6ab4fa 100644
--- a/drivers/pci/pci-sysfs.c
+++ b/drivers/pci/pci-sysfs.c
@@ -573,6 +573,33 @@ static ssize_t reset_subordinate_store(struct device *dev,
}
static DEVICE_ATTR_WO(reset_subordinate);
+static ssize_t cxl_unmask_sbr_show(struct device *dev,
+ struct device_attribute *attr, char *buf)
+{
+ struct pci_dev *pdev = to_pci_dev(dev);
+
+ return sysfs_emit(buf, "%u\n", pdev->cxl_unmask_sbr);
+}
+
+static ssize_t cxl_unmask_sbr_store(struct device *dev,
+ struct device_attribute *attr,
+ const char *buf, size_t count)
+{
+ struct pci_dev *pdev = to_pci_dev(dev);
+ unsigned long val;
+
+ if (!capable(CAP_SYS_ADMIN))
+ return -EPERM;
+
+ if (kstrtoul(buf, 0, &val) < 0)
+ return -EINVAL;
+
+ pdev->cxl_unmask_sbr = !!val;
+
+ return count;
+}
+static DEVICE_ATTR_RW(cxl_unmask_sbr);
+
#if defined(CONFIG_PM) && defined(CONFIG_ACPI)
static ssize_t d3cold_allowed_store(struct device *dev,
struct device_attribute *attr,
@@ -650,6 +677,7 @@ static struct attribute *pci_bridge_attrs[] = {
&dev_attr_subordinate_bus_number.attr,
&dev_attr_secondary_bus_number.attr,
&dev_attr_reset_subordinate.attr,
+ &dev_attr_cxl_unmask_sbr.attr,
NULL,
};
@@ -1824,6 +1852,9 @@ static umode_t pci_bridge_attrs_are_visible(struct kobject *kobj,
struct device *dev = kobj_to_dev(kobj);
struct pci_dev *pdev = to_pci_dev(dev);
+ if (a == &dev_attr_cxl_unmask_sbr.attr && !is_cxl_dport(pdev))
+ return 0;
+
if (pci_is_bridge(pdev))
return a->mode;
diff --git a/include/linux/pci.h b/include/linux/pci.h
index 0d9832ce6f3d..bc0f652eff80 100644
--- a/include/linux/pci.h
+++ b/include/linux/pci.h
@@ -485,6 +485,7 @@ struct pci_dev {
unsigned int shpc_managed:1; /* SHPC owned by shpchp */
unsigned int is_thunderbolt:1; /* Thunderbolt controller */
unsigned int is_cxl:1; /* Compute Express Link (CXL) */
+ unsigned int cxl_unmask_sbr:1; /* SBR unmask allowed by user */
/*
* Devices marked being untrusted are the ones that can potentially
* execute DMA attacks and similar. They are typically connected
--
2.55.0
^ permalink raw reply related [flat|nested] 15+ messages in thread* [PATCH v2 13/13] PCI/CXL: Refuse an SBR of a CXL DPort unless authorized
2026-08-25 2:26 [PATCH v2 00/13] Make SBR work for CXL Downstream Ports Fabio M. De Francesco
` (11 preceding siblings ...)
2026-08-25 2:26 ` [PATCH v2 12/13] PCI/CXL: Add a sysfs entry to unmask SBR Fabio M. De Francesco
@ 2026-08-25 2:26 ` Fabio M. De Francesco
12 siblings, 0 replies; 15+ messages in thread
From: Fabio M. De Francesco @ 2026-08-25 2:26 UTC (permalink / raw)
To: linux-cxl
Cc: David Hildenbrand, Oscar Salvador, Greg Kroah-Hartman,
Rafael J . Wysocki, Danilo Krummrich, Davidlohr Bueso,
Jonathan Cameron, Dave Jiang, Alison Schofield, Vishal Verma,
Ira Weiny, Li Ming, Bjorn Helgaas, Mahesh J Salgaonkar,
Oliver O'Halloran, Andrew Morton, Lorenzo Stoakes,
Liam R . Howlett, Vlastimil Babka, Mike Rapoport,
Suren Baghdasaryan, Michal Hocko, linux-kernel, linux-mm,
driver-core, linux-pci, linuxppc-dev, Fabio M. De Francesco
CXL r4.0 sec 8.1.5.2 Table 8-32 describes a bit that makes the SBR bit
in Bridge Control take effect at all, so a Port left at the firmware
default is one the reset must not silently unmask.
Add cxl_sbr_allowed() to answer if SBR is allowed: check if a Port whose
Unmask SBR is already set needs no consent, as it happens with cxl_bus
reset.
Signed-off-by: Fabio M. De Francesco <fabio.m.de.francesco@linux.intel.com>
---
drivers/pci/pci.c | 26 ++++++++++++++++++++++++++
drivers/pci/pci.h | 1 +
2 files changed, 27 insertions(+)
diff --git a/drivers/pci/pci.c b/drivers/pci/pci.c
index eedd516f8484..08873ea3957b 100644
--- a/drivers/pci/pci.c
+++ b/drivers/pci/pci.c
@@ -4881,6 +4881,11 @@ static int cxl_sbr_prepare(struct pci_dev *bridge, u16 dvsec,
{
int rc;
+ if (action == CXL_SBR_OFFLINE_AND_UNBIND && !cxl_sbr_allowed(bridge)) {
+ pci_info(bridge, "SBR masked, write 1 to cxl_unmask_sbr to allow a bus reset\n");
+ return -ENOTTY;
+ }
+
/*
* CXL_SBR_UNBIND: the link is already down, so offlining the regions'
* memory would take the reads that page migration performs as a machine
@@ -5138,6 +5143,27 @@ static bool cxl_sbr_masked(struct pci_dev *dev)
return true;
}
+/*
+ * cxl_sbr_allowed - whether an SBR of a CXL Downstream Port may go ahead
+ * @dev: Downstream Port to test
+ *
+ * Per CXL r4.0 sec 8.1.5.2 Table 8-32 the SBR bit in a CXL Port's Bridge
+ * Control register has no effect while the Port's Unmask SBR bit is clear, and
+ * sec 9.12.3 says System Firmware may leave it clear "to prevent CXL-unaware
+ * PCIe software from resetting the device and the link". A Port that already
+ * has it set needs no further permission; otherwise unmasking it takes the
+ * administrator's consent, given by writing 1 to the Port's cxl_unmask_sbr.
+ *
+ * Return: true if the reset paths may unmask and generate an SBR of @dev.
+ */
+bool cxl_sbr_allowed(struct pci_dev *dev)
+{
+ if (!cxl_port_dvsec(dev))
+ return false;
+
+ return dev->cxl_unmask_sbr || !cxl_sbr_masked(dev);
+}
+
static int pci_reset_bus_function(struct pci_dev *dev, bool probe)
{
struct pci_dev *bridge = pci_upstream_bridge(dev);
diff --git a/drivers/pci/pci.h b/drivers/pci/pci.h
index b6d873b077ed..bea05acc58f0 100644
--- a/drivers/pci/pci.h
+++ b/drivers/pci/pci.h
@@ -248,6 +248,7 @@ int __pci_bridge_secondary_bus_reset(struct pci_dev *dev,
enum cxl_sbr_region_action action);
bool is_cxl_dport(struct pci_dev *dev);
u16 cxl_port_dvsec(struct pci_dev *dev);
+bool cxl_sbr_allowed(struct pci_dev *dev);
int pci_bus_error_reset(struct pci_dev *dev);
int pci_try_reset_bridge(struct pci_dev *bridge);
--
2.55.0
^ permalink raw reply related [flat|nested] 15+ messages in thread