From 2c73cda8908f1275b0ecf81ae540a3bc2a90fc30 Mon Sep 17 00:00:00 2001 From: Richard Cheng Date: Tue, 21 Jul 2026 16:40:09 +0800 Subject: [PATCH 01/49] tools/testing/cxl: Don't wrap cxl_core's own exported symbols Building cxl_test with LLVM=1 (ld.lld) fails in modpost: """ cxl_core: .export_symbol section references '__wrap_...', but it does not seem to be an export symbol """ cxl_test --wraps 6 symbols that cxl_core both defines and exports. GNU ld only wraps undefined references, but ld.lld also redirects references to defined symbols, so cxl_core's own EXPORT_SYMBOL entries get rewritten to __wrap_* and modpost rejects them. cxl_core never calls these symbols, so wrap them only in the caller modules via LDFLAGS_.o instead of the global ldflags-y. Signed-off-by: Richard Cheng Reviewed-by: Dave Jiang Reviewed-by: Alison Schofield Tested-by: Shaikh Kamaluddin Link: https://patch.msgid.link/20260721084009.38100-1-icheng@nvidia.com Signed-off-by: Dave Jiang --- tools/testing/cxl/Kbuild | 16 ++++++++++------ 1 file changed, 10 insertions(+), 6 deletions(-) diff --git a/tools/testing/cxl/Kbuild b/tools/testing/cxl/Kbuild index 2be1df80fcc9..e49ce644b4b1 100644 --- a/tools/testing/cxl/Kbuild +++ b/tools/testing/cxl/Kbuild @@ -4,16 +4,20 @@ ldflags-y += --wrap=is_acpi_device_node ldflags-y += --wrap=acpi_evaluate_integer ldflags-y += --wrap=acpi_pci_find_root ldflags-y += --wrap=nvdimm_bus_register -ldflags-y += --wrap=cxl_await_media_ready -ldflags-y += --wrap=devm_cxl_add_rch_dport -ldflags-y += --wrap=cxl_endpoint_parse_cdat -ldflags-y += --wrap=devm_cxl_endpoint_decoders_setup ldflags-y += --wrap=hmat_get_extended_linear_cache_size -ldflags-y += --wrap=devm_cxl_add_dport_by_dev -ldflags-y += --wrap=devm_cxl_switch_port_decoders_setup ldflags-y += --wrap=walk_hmem_resources ldflags-y += --wrap=region_intersects ldflags-y += --wrap=region_intersects_soft_reserve +wrap_cxl_core := --wrap=cxl_await_media_ready +wrap_cxl_core += --wrap=devm_cxl_add_rch_dport +wrap_cxl_core += --wrap=devm_cxl_add_dport_by_dev +wrap_cxl_core += --wrap=devm_cxl_switch_port_decoders_setup +wrap_cxl_core += --wrap=devm_cxl_endpoint_decoders_setup +wrap_cxl_core += --wrap=cxl_endpoint_parse_cdat +LDFLAGS_cxl_acpi.o += $(wrap_cxl_core) +LDFLAGS_cxl_pmem.o += $(wrap_cxl_core) +LDFLAGS_cxl_port.o += $(wrap_cxl_core) +LDFLAGS_cxl_mem.o += $(wrap_cxl_core) DRIVERS := ../../../drivers DAX_HMEM_SRC := $(DRIVERS)/dax/hmem From 04a7b330c8b60dfff200d9a32dd6615f97b59d07 Mon Sep 17 00:00:00 2001 From: Shaikh Kamaluddin Date: Mon, 24 Aug 2026 19:19:57 +0530 Subject: [PATCH 02/49] cxl/mce: Avoid alias page retirement for corrected errors cxl_handle_mce() offlines the aliased page of an Extended Linear Cache (ELC) region for any MCE with a usable address in the region. This includes corrected errors, needlessly reducing usable memory. Skip ELC alias retirement when mce_is_correctable() identifies the reported error as corrected. Uncorrected errors with a usable address in the ELC region continue to retire the aliased page as before. mce_usable_address() already implements the per-vendor checks that determine whether the reported address is usable, so no separate memory-error classification is needed. On AMD, a corrected legacy bank 4 DRAM ECC error (XEC 8) reports a usable address and retired the aliased page before this change. Tested under QEMU with Intel Skylake-Server and AMD EPYC-Milan models. Fixes: 516e5bd0b6bf ("cxl: Add mce notifier to emit aliased address for extended linear cache") Signed-off-by: Shaikh Kamaluddin Reviewed-by: Alison Schofield Reviewed-by: Dave Jiang Link: https://patch.msgid.link/20260824134957.11085-1-shaikhkamal2012@gmail.com Signed-off-by: Dave Jiang --- drivers/cxl/core/mce.c | 8 +++++++- 1 file changed, 7 insertions(+), 1 deletion(-) diff --git a/drivers/cxl/core/mce.c b/drivers/cxl/core/mce.c index 65fed913b221..3ac6802e750d 100644 --- a/drivers/cxl/core/mce.c +++ b/drivers/cxl/core/mce.c @@ -18,7 +18,13 @@ static int cxl_handle_mce(struct notifier_block *nb, unsigned long val, u64 spa, spa_alias; unsigned long pfn; - if (!mce || !mce_usable_address(mce)) + if (!mce) + return NOTIFY_DONE; + + if (mce_is_correctable(mce)) + return NOTIFY_DONE; + + if (!mce_usable_address(mce)) return NOTIFY_DONE; spa = mce->addr & MCI_ADDR_PHYSADDR; From b212e37113f82bfb018e045adfa28dadc3e1bd6d Mon Sep 17 00:00:00 2001 From: Alison Schofield Date: Fri, 21 Aug 2026 15:25:14 -0700 Subject: [PATCH 03/49] cxl/region: Guard against a missing peer mapping When two endpoints share a dport, both must be mapped by the port. sashiko-bot reports that a missing peer mapping could result in a NULL pointer dereference during region creation. No caller can reach this today. Guard it anyway so a future violation of the mapping requirement fails region creation rather than causing an oops. Reported-by: sashiko-bot@kernel.org Link: https://sashiko.dev/#/patchset/cover.1787255388.git.alison.schofield%40intel.com?part=3 Assisted-by: Claude:claude-opus-5 Signed-off-by: Alison Schofield Reviewed-by: Richard Cheng Reviewed-by: Dave Jiang Link: https://patch.msgid.link/20260821222515.2675707-1-alison.schofield@intel.com Signed-off-by: Dave Jiang --- drivers/cxl/core/region.c | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/drivers/cxl/core/region.c b/drivers/cxl/core/region.c index 27e63e6dab7c..75b8092e6dc2 100644 --- a/drivers/cxl/core/region.c +++ b/drivers/cxl/core/region.c @@ -1367,7 +1367,8 @@ static int check_last_peer(struct cxl_endpoint_decoder *cxled, /* * If this position wants to share a dport with the last endpoint mapped * then that endpoint, at index 'position - distance', must also be - * mapped by this dport. + * mapped by this dport. An endpoint that this port does not map at all + * fails that requirement. */ if (pos < distance) { dev_dbg(&cxlr->dev, "%s:%s: cannot host %s:%s at %d\n", @@ -1378,7 +1379,7 @@ static int check_last_peer(struct cxl_endpoint_decoder *cxled, cxled_peer = p->targets[pos - distance]; cxlmd_peer = cxled_to_memdev(cxled_peer); ep_peer = cxl_ep_load(port, cxlmd_peer); - if (ep->dport != ep_peer->dport) { + if (!ep_peer || ep->dport != ep_peer->dport) { dev_dbg(&cxlr->dev, "%s:%s: %s:%s pos %d mismatched peer %s:%s\n", dev_name(port->uport_dev), dev_name(&port->dev), From 6f057dea152574dd044382fd0ba5ed19e3ecba25 Mon Sep 17 00:00:00 2001 From: Davidlohr Bueso Date: Mon, 31 Aug 2026 10:51:19 -0700 Subject: [PATCH 04/49] cxl/test: Map mock device nodes to an online node cxl_type3_mem_init() and cxl_type2_mem_init() assign alternating NUMA nodes to the mock memdevs to model multi-node topologies. On a single-node host node 1 has no pgdat, and the first node-aware allocation in an odd device's probe faults on a NULL zonelist: Oops: general protection fault, probably for non-canonical address 0xdffffc00000006e3 KASAN: probably user-memory-access in range [0x0000000000003718-0x000000000000371f] Workqueue: async async_run_entry_fn RIP: __alloc_frozen_pages_noprof+0x288 new_slab ___slab_alloc __kmalloc_node_track_caller_noprof devm_kmalloc cxl_mock_mem_probe Map the requested node to an online one, as dax_hmem already does for its target node. Fixes: cf1f6877b088 ("cxl/memdev: Add numa_node attribute") Signed-off-by: Davidlohr Bueso Reviewed-by: Dave Jiang Reviewed-by: Alison Schofield Tested-by: Alison Schofield Tested-by: Shaikh Kamaluddin Reviewed-by: Gregory Price (Meta) Link: https://patch.msgid.link/20260831175119.3247915-1-dave@stgolabs.net Signed-off-by: Dave Jiang --- tools/testing/cxl/test/cxl.c | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/tools/testing/cxl/test/cxl.c b/tools/testing/cxl/test/cxl.c index 62bd92b3be45..221be4addb0e 100644 --- a/tools/testing/cxl/test/cxl.c +++ b/tools/testing/cxl/test/cxl.c @@ -1945,7 +1945,7 @@ static int cxl_type2_mem_init(void) goto err_mem; } pdev->dev.parent = &dport->dev; - set_dev_node(&pdev->dev, i % 2); + set_dev_node(&pdev->dev, numa_map_to_online_node(i % 2)); rc = cxl_mock_platform_device_add(pdev, &cxl_mem[i]); if (rc) @@ -1974,7 +1974,7 @@ static int cxl_type3_mem_init(void) goto err_mem; } pdev->dev.parent = &dport->dev; - set_dev_node(&pdev->dev, i % 2); + set_dev_node(&pdev->dev, numa_map_to_online_node(i % 2)); rc = cxl_mock_platform_device_add(pdev, &cxl_mem[i]); if (rc) @@ -1991,7 +1991,7 @@ static int cxl_type3_mem_init(void) goto err_single; } pdev->dev.parent = &dport->dev; - set_dev_node(&pdev->dev, i % 2); + set_dev_node(&pdev->dev, numa_map_to_online_node(i % 2)); rc = cxl_mock_platform_device_add(pdev, &cxl_mem_single[i]); if (rc) @@ -2009,7 +2009,7 @@ static int cxl_type3_mem_init(void) goto err_rcd; } pdev->dev.parent = &rch->dev; - set_dev_node(&pdev->dev, i % 2); + set_dev_node(&pdev->dev, numa_map_to_online_node(i % 2)); rc = cxl_mock_platform_device_add(pdev, &cxl_rcd[i]); if (rc) From 977eaad352bad4c4aac7cd15683800196c2ead57 Mon Sep 17 00:00:00 2001 From: Dave Jiang Date: Fri, 28 Aug 2026 14:57:26 -0700 Subject: [PATCH 05/49] cxl/ras: Pass the PCI device's struct device to match_memdev_by_parent() cxl_cper_handle_prot_err() hands bus_find_device() the 'struct pci_dev' where match_memdev_by_parent() expects the embedded 'struct device'. The comparison never matches. The lookup always fails and CXL endpoint protocol errors reported via CPER are dropped without a trace event, leaving only the root port, downstream and upstream cases reporting. Pass &pdev->dev, and rename the callback parameter to match what it is. Fixes: 3c70ec71abda ("cxl/ras: Fix CPER handler device confusion") Reviewed-by: Alison Schofield Reviewed-by: Terry Bowman Link: https://patch.msgid.link/20260828215726.1077498-1-dave.jiang@intel.com Signed-off-by: Dave Jiang --- drivers/cxl/core/ras.c | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/drivers/cxl/core/ras.c b/drivers/cxl/core/ras.c index e307361bb39e..0759fc9b07de 100644 --- a/drivers/cxl/core/ras.c +++ b/drivers/cxl/core/ras.c @@ -70,9 +70,9 @@ cxl_cper_trace_uncorr_prot_err(struct cxl_memdev *cxlmd, trace_cxl_aer_uncorrectable_error(cxlmd, status, fe, hl); } -static int match_memdev_by_parent(struct device *dev, const void *uport) +static int match_memdev_by_parent(struct device *dev, const void *parent) { - if (is_cxl_memdev(dev) && dev->parent == uport) + if (is_cxl_memdev(dev) && dev->parent == parent) return 1; return 0; } @@ -108,7 +108,7 @@ void cxl_cper_handle_prot_err(struct cxl_cper_prot_err_work_data *data) return; struct device *mem_dev __free(put_device) = bus_find_device( - &cxl_bus_type, NULL, pdev, match_memdev_by_parent); + &cxl_bus_type, NULL, &pdev->dev, match_memdev_by_parent); if (!mem_dev) return; From f0f38b3d0730a780fc83c202f9d17e605c9f15f2 Mon Sep 17 00:00:00 2001 From: Manuel Ebner Date: Thu, 3 Sep 2026 11:54:45 +0200 Subject: [PATCH 06/49] cxl: docs/platform/acpi: Fix brackets Add '}' and fix indentation. Signed-off-by: Manuel Ebner Reviewed-by: Randy Dunlap Link: https://patch.msgid.link/20260903095445.729419-2-manuelebnerli@mailbox.org Signed-off-by: Dave Jiang --- Documentation/driver-api/cxl/platform/acpi/dsdt.rst | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/Documentation/driver-api/cxl/platform/acpi/dsdt.rst b/Documentation/driver-api/cxl/platform/acpi/dsdt.rst index b4583b01d67d..73b23d82aff0 100644 --- a/Documentation/driver-api/cxl/platform/acpi/dsdt.rst +++ b/Documentation/driver-api/cxl/platform/acpi/dsdt.rst @@ -25,4 +25,5 @@ Example Compute Express Link Host Bridge :: ... Name (_UID, 0x05) // _UID: Unique ID ... - } + } + } From 6495547d643d69506deaa983200400ab6fce7ec7 Mon Sep 17 00:00:00 2001 From: Dave Jiang Date: Wed, 2 Sep 2026 16:53:48 -0700 Subject: [PATCH 07/49] MAINTAINERS: Add Richard Cheng to CXL subsystem as a Reviewer Add Richard as a reviewer for CXL subsystem. Cc: Davidlohr Bueso Cc: Jonathan Cameron Cc: Alison Schofield Cc: Vishal Verma Cc: Richard Cheng Acked-by: Richard Cheng Acked-by: Jonathan Cameron Reviewed-by: Alison Schofield Acked-by: Davidlohr Bueso Link: https://patch.msgid.link/20260902235348.3563444-1-dave.jiang@intel.com Signed-off-by: Dave Jiang --- MAINTAINERS | 1 + 1 file changed, 1 insertion(+) diff --git a/MAINTAINERS b/MAINTAINERS index 3a19da74d00c..25d61ce9506a 100644 --- a/MAINTAINERS +++ b/MAINTAINERS @@ -6567,6 +6567,7 @@ M: Vishal Verma M: Dan Williams R: Ira Weiny R: Li Ming +R: Richard Cheng L: linux-cxl@vger.kernel.org S: Maintained F: Documentation/driver-api/cxl From 3eb3376944ace574bf7ae299e944a2a3c30c93ba Mon Sep 17 00:00:00 2001 From: Richard Cheng Date: Wed, 2 Sep 2026 13:38:33 +0800 Subject: [PATCH 08/49] cxl/features: Reject feature offset that overflows 16-bit field cxl_get_feature() and cxl_set_feature() build each mailbox command's offset from the starting offset plus the amount of data already transferred, then store it in a 16-bit field. A user-controlled fwctl offset and transfer size can exceed the feature extent, allowing a later offset to be truncated by cpu_to_le16() and target the wrong feature data. Reject requests whose transfer size exceeds the remaining 16-bit feature range. Express the check as "size > U16_MAX - offset" so the validation itself cannot wrap on 32-bit systems. Change cxl_get_feature() to return ssize_t so invalid input and mailbox failures are reported as negative errno rather than being conflated with a zero-byte result. Update the EDAC callers to handle negative results. Keep fwctl behavior unchanged by translating helper failures to the same header-only RPC response carrying the CXL mailbox return code. Fixes: 5e5ac21f629d ("cxl/mbox: Add GET_FEATURE mailbox command") Fixes: 14d502cc2718 ("cxl/mbox: Add SET_FEATURE mailbox command") Reviewed-by: Dave Jiang Signed-off-by: Richard Cheng Reviewed-by: Alison Schofield Link: https://patch.msgid.link/20260902053839.25595-2-icheng@nvidia.com Signed-off-by: Dave Jiang --- drivers/cxl/core/core.h | 8 ++++---- drivers/cxl/core/edac.c | 20 +++++++++++++++----- drivers/cxl/core/features.c | 28 ++++++++++++++++++---------- 3 files changed, 37 insertions(+), 19 deletions(-) diff --git a/drivers/cxl/core/core.h b/drivers/cxl/core/core.h index 35eaf636adc9..bb380ec6daeb 100644 --- a/drivers/cxl/core/core.h +++ b/drivers/cxl/core/core.h @@ -217,10 +217,10 @@ int cxl_port_get_possible_dports(struct cxl_port *port); #ifdef CONFIG_CXL_FEATURES struct cxl_feat_entry * cxl_feature_info(struct cxl_features_state *cxlfs, const uuid_t *uuid); -size_t cxl_get_feature(struct cxl_mailbox *cxl_mbox, const uuid_t *feat_uuid, - enum cxl_get_feat_selection selection, - void *feat_out, size_t feat_out_size, u16 offset, - u16 *return_code); +ssize_t cxl_get_feature(struct cxl_mailbox *cxl_mbox, const uuid_t *feat_uuid, + enum cxl_get_feat_selection selection, + void *feat_out, size_t feat_out_size, u16 offset, + u16 *return_code); int cxl_set_feature(struct cxl_mailbox *cxl_mbox, const uuid_t *feat_uuid, u8 feat_version, const void *feat_data, size_t feat_data_size, u32 feat_flag, u16 offset, diff --git a/drivers/cxl/core/edac.c b/drivers/cxl/core/edac.c index b321971fef58..f1df4b5cfe5b 100644 --- a/drivers/cxl/core/edac.c +++ b/drivers/cxl/core/edac.c @@ -78,7 +78,7 @@ static int cxl_mem_scrub_get_attrbs(struct cxl_mailbox *cxl_mbox, u8 *cap, u16 *cycle, u8 *flags, u8 *min_cycle) { size_t rd_data_size = sizeof(struct cxl_scrub_rd_attrbs); - size_t data_size; + ssize_t data_size; struct cxl_scrub_rd_attrbs *rd_attrbs __free(kfree) = kzalloc(rd_data_size, GFP_KERNEL); if (!rd_attrbs) @@ -87,6 +87,8 @@ static int cxl_mem_scrub_get_attrbs(struct cxl_mailbox *cxl_mbox, u8 *cap, data_size = cxl_get_feature(cxl_mbox, &CXL_FEAT_PATROL_SCRUB_UUID, CXL_GET_FEAT_SEL_CURRENT_VALUE, rd_attrbs, rd_data_size, 0, NULL); + if (data_size < 0) + return data_size; if (!data_size) return -EIO; @@ -551,7 +553,7 @@ static int cxl_mem_ecs_get_attrbs(struct device *dev, struct cxl_mailbox *cxl_mbox = &cxlmd->cxlds->cxl_mbox; struct cxl_ecs_fru_rd_attrbs *fru_rd_attrbs; size_t rd_data_size; - size_t data_size; + ssize_t data_size; rd_data_size = cxl_ecs_ctx->get_feat_size; @@ -563,6 +565,8 @@ static int cxl_mem_ecs_get_attrbs(struct device *dev, data_size = cxl_get_feature(cxl_mbox, &CXL_FEAT_ECS_UUID, CXL_GET_FEAT_SEL_CURRENT_VALUE, rd_attrbs, rd_data_size, 0, NULL); + if (data_size < 0) + return data_size; if (!data_size) return -EIO; @@ -583,7 +587,7 @@ static int cxl_mem_ecs_set_attrbs(struct device *dev, struct cxl_ecs_fru_wr_attrbs *fru_wr_attrbs; size_t rd_data_size, wr_data_size; u16 num_media_frus, count; - size_t data_size; + ssize_t data_size; num_media_frus = cxl_ecs_ctx->num_media_frus; rd_data_size = cxl_ecs_ctx->get_feat_size; @@ -596,6 +600,8 @@ static int cxl_mem_ecs_set_attrbs(struct device *dev, data_size = cxl_get_feature(cxl_mbox, &CXL_FEAT_ECS_UUID, CXL_GET_FEAT_SEL_CURRENT_VALUE, rd_attrbs, rd_data_size, 0, NULL); + if (data_size < 0) + return data_size; if (!data_size) return -EIO; @@ -1264,7 +1270,7 @@ cxl_mem_sparing_get_attrbs(struct cxl_mem_sparing_context *cxl_sparing_ctx) struct cxl_memdev *cxlmd = cxl_sparing_ctx->cxlmd; struct cxl_mailbox *cxl_mbox = &cxlmd->cxlds->cxl_mbox; u16 restriction_flags; - size_t data_size; + ssize_t data_size; u16 return_code; struct cxl_memdev_sparing_rd_attrbs *rd_attrbs __free(kfree) = kzalloc(rd_data_size, GFP_KERNEL); @@ -1274,6 +1280,8 @@ cxl_mem_sparing_get_attrbs(struct cxl_mem_sparing_context *cxl_sparing_ctx) data_size = cxl_get_feature(cxl_mbox, &cxl_sparing_ctx->repair_uuid, CXL_GET_FEAT_SEL_CURRENT_VALUE, rd_attrbs, rd_data_size, 0, &return_code); + if (data_size < 0) + return data_size; if (!data_size) return -EIO; @@ -1750,7 +1758,7 @@ static int cxl_mem_ppr_get_attrbs(struct cxl_ppr_context *cxl_ppr_ctx) struct cxl_memdev *cxlmd = cxl_ppr_ctx->cxlmd; struct cxl_mailbox *cxl_mbox = &cxlmd->cxlds->cxl_mbox; u16 restriction_flags; - size_t data_size; + ssize_t data_size; u16 return_code; struct cxl_memdev_ppr_rd_attrbs *rd_attrbs __free(kfree) = @@ -1761,6 +1769,8 @@ static int cxl_mem_ppr_get_attrbs(struct cxl_ppr_context *cxl_ppr_ctx) data_size = cxl_get_feature(cxl_mbox, &cxl_ppr_ctx->repair_uuid, CXL_GET_FEAT_SEL_CURRENT_VALUE, rd_attrbs, rd_data_size, 0, &return_code); + if (data_size < 0) + return data_size; if (!data_size) return -EIO; diff --git a/drivers/cxl/core/features.c b/drivers/cxl/core/features.c index ba6d2a5acb74..8d44ce829497 100644 --- a/drivers/cxl/core/features.c +++ b/drivers/cxl/core/features.c @@ -220,10 +220,10 @@ int devm_cxl_setup_features(struct cxl_dev_state *cxlds) } EXPORT_SYMBOL_NS_GPL(devm_cxl_setup_features, "CXL"); -size_t cxl_get_feature(struct cxl_mailbox *cxl_mbox, const uuid_t *feat_uuid, - enum cxl_get_feat_selection selection, - void *feat_out, size_t feat_out_size, u16 offset, - u16 *return_code) +ssize_t cxl_get_feature(struct cxl_mailbox *cxl_mbox, const uuid_t *feat_uuid, + enum cxl_get_feat_selection selection, + void *feat_out, size_t feat_out_size, u16 offset, + u16 *return_code) { size_t data_to_rd_size; struct cxl_mbox_get_feat_in pi; @@ -235,7 +235,10 @@ size_t cxl_get_feature(struct cxl_mailbox *cxl_mbox, const uuid_t *feat_uuid, *return_code = CXL_MBOX_CMD_RC_INPUT; if (!feat_out || !feat_out_size) - return 0; + return -EINVAL; + + if (feat_out_size > U16_MAX - offset) + return -EINVAL; uuid_copy(&pi.uuid, feat_uuid); pi.selection = selection; @@ -259,7 +262,7 @@ size_t cxl_get_feature(struct cxl_mailbox *cxl_mbox, const uuid_t *feat_uuid, if (rc < 0 || !mbox_cmd.size_out) { if (return_code) *return_code = mbox_cmd.return_code; - return 0; + return rc < 0 ? rc : -EIO; } data_rcvd_size += mbox_cmd.size_out; } while (data_rcvd_size < feat_out_size); @@ -288,6 +291,9 @@ int cxl_set_feature(struct cxl_mailbox *cxl_mbox, if (return_code) *return_code = CXL_MBOX_CMD_RC_INPUT; + if (feat_data_size > U16_MAX - offset) + return -EINVAL; + struct cxl_mbox_set_feat_in *pi __free(kfree) = kzalloc(cxl_mbox->payload_size, GFP_KERNEL); if (!pi) @@ -462,6 +468,7 @@ static void *cxlctl_get_feature(struct cxl_features_state *cxlfs, const struct cxl_mbox_get_feat_in *feat_in; u16 offset, count, return_code; size_t out_size = *out_len; + ssize_t data_size; if (rpc_in->op_size != sizeof(*feat_in)) return ERR_PTR(-EINVAL); @@ -482,16 +489,17 @@ static void *cxlctl_get_feature(struct cxl_features_state *cxlfs, if (!rpc_out) return ERR_PTR(-ENOMEM); - out_size = cxl_get_feature(cxl_mbox, &feat_in->uuid, - feat_in->selection, rpc_out->payload, - count, offset, &return_code); + data_size = cxl_get_feature(cxl_mbox, &feat_in->uuid, + feat_in->selection, rpc_out->payload, + count, offset, &return_code); *out_len = sizeof(struct fwctl_rpc_cxl_out); - if (!out_size) { + if (data_size <= 0) { rpc_out->size = 0; rpc_out->retval = return_code; return no_free_ptr(rpc_out); } + out_size = data_size; rpc_out->size = out_size; rpc_out->retval = CXL_MBOX_CMD_RC_SUCCESS; *out_len += out_size; From 51493ff66ad6b1c8e904cf3ab8c69e9c59aaba0f Mon Sep 17 00:00:00 2001 From: Richard Cheng Date: Wed, 2 Sep 2026 13:38:39 +0800 Subject: [PATCH 09/49] cxl/fwctl: Propagate feature RPC delivery errors FWCTL_RPC requires delivery failures to be returned as ioctl errors, while device errors are reported in the output. Get and Set Feature instead converted all failures into normal responses, sometimes with a SUCCESS device status. Initialize the return code to SUCCESS. When the helper fails without a device error code, return its errno. Continue reporting actual device errors through rpc_out->retval. CXL permits Get Feature to return a nonzero short payload when Offset + Count runs past the end of the Feature. cxl_internal_send_cmd() reports that response as -EIO, so preserve the returned bytes as a successful partial transfer. Fixed-format EDAC callers still require complete attribute structures, so reject partial payloads before consuming them. Map an unexpected zero-length result with a SUCCESS device status to -EIO. Fixes: 5908f3ed6dc2 ("cxl: Add support to handle user feature commands for get feature") Fixes: eb5dfcb9e36d ("cxl: Add support to handle user feature commands for set feature") Signed-off-by: Richard Cheng Reviewed-by: Alison Schofield Link: https://patch.msgid.link/20260902053839.25595-8-icheng@nvidia.com Signed-off-by: Dave Jiang --- drivers/cxl/core/edac.c | 10 +++++----- drivers/cxl/core/features.c | 28 +++++++++++++++++++--------- 2 files changed, 24 insertions(+), 14 deletions(-) diff --git a/drivers/cxl/core/edac.c b/drivers/cxl/core/edac.c index f1df4b5cfe5b..34b81e8dfbbb 100644 --- a/drivers/cxl/core/edac.c +++ b/drivers/cxl/core/edac.c @@ -89,7 +89,7 @@ static int cxl_mem_scrub_get_attrbs(struct cxl_mailbox *cxl_mbox, u8 *cap, rd_data_size, 0, NULL); if (data_size < 0) return data_size; - if (!data_size) + if ((size_t)data_size != rd_data_size) return -EIO; *cap = rd_attrbs->scrub_cycle_cap; @@ -567,7 +567,7 @@ static int cxl_mem_ecs_get_attrbs(struct device *dev, rd_data_size, 0, NULL); if (data_size < 0) return data_size; - if (!data_size) + if ((size_t)data_size != rd_data_size) return -EIO; fru_rd_attrbs = rd_attrbs->fru_attrbs; @@ -602,7 +602,7 @@ static int cxl_mem_ecs_set_attrbs(struct device *dev, rd_data_size, 0, NULL); if (data_size < 0) return data_size; - if (!data_size) + if ((size_t)data_size != rd_data_size) return -EIO; struct cxl_ecs_wr_attrbs *wr_attrbs __free(kvfree) = @@ -1282,7 +1282,7 @@ cxl_mem_sparing_get_attrbs(struct cxl_mem_sparing_context *cxl_sparing_ctx) rd_data_size, 0, &return_code); if (data_size < 0) return data_size; - if (!data_size) + if ((size_t)data_size != rd_data_size) return -EIO; cxl_sparing_ctx->op_class = rd_attrbs->hdr.op_class; @@ -1771,7 +1771,7 @@ static int cxl_mem_ppr_get_attrbs(struct cxl_ppr_context *cxl_ppr_ctx) rd_data_size, 0, &return_code); if (data_size < 0) return data_size; - if (!data_size) + if ((size_t)data_size != rd_data_size) return -EIO; cxl_ppr_ctx->op_class = rd_attrbs->hdr.op_class; diff --git a/drivers/cxl/core/features.c b/drivers/cxl/core/features.c index 8d44ce829497..95f47193fb61 100644 --- a/drivers/cxl/core/features.c +++ b/drivers/cxl/core/features.c @@ -232,7 +232,7 @@ ssize_t cxl_get_feature(struct cxl_mailbox *cxl_mbox, const uuid_t *feat_uuid, int rc; if (return_code) - *return_code = CXL_MBOX_CMD_RC_INPUT; + *return_code = CXL_MBOX_CMD_RC_SUCCESS; if (!feat_out || !feat_out_size) return -EINVAL; @@ -259,6 +259,17 @@ ssize_t cxl_get_feature(struct cxl_mailbox *cxl_mbox, const uuid_t *feat_uuid, .min_out = data_to_rd_size, }; rc = cxl_internal_send_cmd(cxl_mbox, &mbox_cmd); + /* + * Per CXL r4.0 8.2.10.6.2, when Offset + Count runs past the + * end of the Feature the device returns only the bytes up to + * the Feature size. cxl_internal_send_cmd() reports that as + * -EIO with a short payload, so stop and return what arrived. + */ + if (rc == -EIO && mbox_cmd.size_out && + mbox_cmd.size_out < data_to_rd_size) { + data_rcvd_size += mbox_cmd.size_out; + break; + } if (rc < 0 || !mbox_cmd.size_out) { if (return_code) *return_code = mbox_cmd.return_code; @@ -267,9 +278,6 @@ ssize_t cxl_get_feature(struct cxl_mailbox *cxl_mbox, const uuid_t *feat_uuid, data_rcvd_size += mbox_cmd.size_out; } while (data_rcvd_size < feat_out_size); - if (return_code) - *return_code = CXL_MBOX_CMD_RC_SUCCESS; - return data_rcvd_size; } @@ -289,7 +297,7 @@ int cxl_set_feature(struct cxl_mailbox *cxl_mbox, size_t hdr_size; if (return_code) - *return_code = CXL_MBOX_CMD_RC_INPUT; + *return_code = CXL_MBOX_CMD_RC_SUCCESS; if (feat_data_size > U16_MAX - offset) return -EINVAL; @@ -340,11 +348,8 @@ int cxl_set_feature(struct cxl_mailbox *cxl_mbox, } data_sent_size += data_in_size; - if (data_sent_size >= feat_data_size) { - if (return_code) - *return_code = CXL_MBOX_CMD_RC_SUCCESS; + if (data_sent_size >= feat_data_size) return 0; - } if ((feat_data_size - data_sent_size) <= (cxl_mbox->payload_size - hdr_size)) { data_in_size = feat_data_size - data_sent_size; @@ -492,6 +497,9 @@ static void *cxlctl_get_feature(struct cxl_features_state *cxlfs, data_size = cxl_get_feature(cxl_mbox, &feat_in->uuid, feat_in->selection, rpc_out->payload, count, offset, &return_code); + if (data_size <= 0 && + return_code == CXL_MBOX_CMD_RC_SUCCESS) + return ERR_PTR(data_size ?: -EIO); *out_len = sizeof(struct fwctl_rpc_cxl_out); if (data_size <= 0) { rpc_out->size = 0; @@ -544,6 +552,8 @@ static void *cxlctl_set_feature(struct cxl_features_state *cxlfs, rc = cxl_set_feature(cxl_mbox, &feat_in->uuid, feat_in->version, feat_in->feat_data, data_size, flags, offset, &return_code); + if (rc && return_code == CXL_MBOX_CMD_RC_SUCCESS) + return ERR_PTR(rc); *out_len = sizeof(*rpc_out); if (rc) { rpc_out->retval = return_code; From dc96f35358a14c75f1e931b1c2cb743187735f8b Mon Sep 17 00:00:00 2001 From: Junjie Cao Date: Thu, 10 Sep 2026 17:38:58 +0800 Subject: [PATCH 10/49] cxl/regs: Reject register blocks in an unassigned BAR cxl_decode_regblock() only checks that the Register Locator offset fits pci_resource_len(). A BAR the PCI core could not place is reset to zero start, end and flags while config space keeps the firmware value, so a zero offset passes and the block is mapped at physical address 0. On x86 ioremap() of the reserved low megabyte succeeds, the component register header does not match, and a switch port fails with "HDM decoder capability not found", pointing at the HDM decoders rather than at the BAR. Reject a BAR with no length or still unassigned, next to the existing size check. Skip empty Register Locator entries first: they decode as BAR0 offset 0 and would trip the check, and nothing looks them up. Seen with edk2-stable202602 (f6489621b8ae, reverted in stable202605), which places 64-bit non-prefetchable BARs behind a bridge in the prefetchable window. Linux refuses to claim them and the switch upstream and downstream port BAR0s find no room in the 32-bit window. QEMU q35 with a CXL switch, Fedora 43 edk2-ovmf-20260213, before: pci 0000:0d:00.0: BAR 0 [mem size 0x00010000 64bit]: can't assign; no space cxl_port port2: HDM decoder capability not found after: pcieport 0000:0d:00.0: BAR0: not assigned (type: 1) cxl_port port2: No component registers mapped cxl_port port2: Failed to map HDM decoder capability Link: https://github.com/tianocore/edk2/issues/13104 Signed-off-by: Junjie Cao Reviewed-by: Li Ming Reviewed-by: Dave Jiang Reviewed-by: Jonathan Cameron Link: https://patch.msgid.link/20260910093858.535969-1-junjie.cao@intel.com Signed-off-by: Dave Jiang --- drivers/cxl/core/regs.c | 14 ++++++++++++++ 1 file changed, 14 insertions(+) diff --git a/drivers/cxl/core/regs.c b/drivers/cxl/core/regs.c index 20c2d9fbcfe7..41416fa2ce4a 100644 --- a/drivers/cxl/core/regs.c +++ b/drivers/cxl/core/regs.c @@ -277,6 +277,20 @@ static bool cxl_decode_regblock(struct pci_dev *pdev, u32 reg_lo, u32 reg_hi, u64 offset = ((u64)reg_hi << 32) | (reg_lo & PCI_DVSEC_CXL_REG_LOCATOR_BLOCK_OFF_LOW); + if (reg_type == CXL_REGLOC_RBI_EMPTY) + return false; + + /* + * A BAR the PCI core could not place is reset to zero; decoding it + * would map the block at physical address 0. + */ + if (!pci_resource_len(pdev, bar) || + (pci_resource_flags(pdev, bar) & IORESOURCE_UNSET)) { + dev_warn(&pdev->dev, "BAR%d: not assigned (type: %d)\n", bar, + reg_type); + return false; + } + if (offset > pci_resource_len(pdev, bar)) { dev_warn(&pdev->dev, "BAR%d: %pr: too small (offset: %pa, type: %d)\n", bar, From 2dfb4796d19b06c47f12dc358023bc00d30e500f Mon Sep 17 00:00:00 2001 From: Karl Mehltretter Date: Sat, 12 Sep 2026 00:17:48 +0200 Subject: [PATCH 11/49] cxl/region: Fix the return value in devm_cxl_add_region() kernel-doc devm_cxl_add_region() returns the new struct cxl_region, or an ERR_PTR() on failure, but its kernel-doc says "0 if the region was added to the @cxlrd, else returns negative error code", which was never true. Describe the pointer. [ dj: Dropped Fixes tag as it's a comment fix and not code fix. ] Assisted-by: LLM Signed-off-by: Karl Mehltretter Reviewed-by: Jonathan Cameron Link: https://patch.msgid.link/20260911221748.7212-1-kmehltretter@gmail.com Signed-off-by: Dave Jiang --- drivers/cxl/core/region.c | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/drivers/cxl/core/region.c b/drivers/cxl/core/region.c index 75b8092e6dc2..4f6069451fed 100644 --- a/drivers/cxl/core/region.c +++ b/drivers/cxl/core/region.c @@ -2728,7 +2728,7 @@ void kill_regions(struct cxl_root_decoder *cxlrd) * This is the second step of region initialization. Regions exist within an * address space which is mapped by a @cxlrd. * - * Return: 0 if the region was added to the @cxlrd, else returns negative error + * Return: the new region if it was added to the @cxlrd, else an ERR_PTR() with the error * code. The region will be named "regionZ" where Z is the unique region number. */ static struct cxl_region *devm_cxl_add_region(struct cxl_root_decoder *cxlrd, From 057dc2892d1f69f457119f0080e2acacfcc9a9d0 Mon Sep 17 00:00:00 2001 From: Richard Cheng Date: Mon, 14 Sep 2026 17:08:56 +0800 Subject: [PATCH 12/49] cxl/region: Simplify poison_by_decoder() error handling "rc" carries both an error code and the loop control signal for device_for_each_child(), so returning it bare is misleading, the early guards mean "keep walking", not "no error". Zeroing "rc" to forgive an -EFAULT on a RAM partition adds to that by discarding what the device actually returned. Return a literal 0 where the walk should continue, and test the forgiven case directly instead of rewriting "rc". Give that test a name, poison_efault_forgiven(), so cxl_get_poison_unmapped() and poison_by_decoder() spell the same rule the same way. No functional change. Suggested-by: Jonathan Cameron Signed-off-by: Richard Cheng Reviewed-by: Alison Schofield Link: https://patch.msgid.link/20260914090858.19181-2-icheng@nvidia.com Signed-off-by: Dave Jiang --- drivers/cxl/core/region.c | 26 ++++++++++++++++---------- 1 file changed, 16 insertions(+), 10 deletions(-) diff --git a/drivers/cxl/core/region.c b/drivers/cxl/core/region.c index 27e63e6dab7c..10abc073103b 100644 --- a/drivers/cxl/core/region.c +++ b/drivers/cxl/core/region.c @@ -2902,6 +2902,16 @@ struct cxl_poison_context { u64 offset; }; +/* + * A device may answer a Get Poison List request with "physical address + * specified is invalid" (-EFAULT). That answer is tolerated for a RAM + * partition and the poison walk continues. + */ +static inline bool poison_efault_forgiven(int rc, enum cxl_partition_mode mode) +{ + return rc == -EFAULT && mode == CXL_PARTMODE_RAM; +} + static int cxl_get_poison_unmapped(struct cxl_memdev *cxlmd, struct cxl_poison_context *ctx) { @@ -2930,7 +2940,7 @@ static int cxl_get_poison_unmapped(struct cxl_memdev *cxlmd, if (!length) break; rc = cxl_mem_get_poison(cxlmd, offset, length, NULL); - if (rc == -EFAULT && cxlds->part[i].mode == CXL_PARTMODE_RAM) + if (poison_efault_forgiven(rc, cxlds->part[i].mode)) continue; if (rc) break; @@ -2947,14 +2957,14 @@ static int poison_by_decoder(struct device *dev, void *arg) struct cxl_dev_state *cxlds; struct cxl_memdev *cxlmd; u64 offset, length; - int rc = 0; + int rc; if (!is_endpoint_decoder(dev)) - return rc; + return 0; cxled = to_cxl_endpoint_decoder(dev); if (!cxled->dpa_res) - return rc; + return 0; cxlmd = cxled_to_memdev(cxled); cxlds = cxlmd->cxlds; @@ -2964,18 +2974,14 @@ static int poison_by_decoder(struct device *dev, void *arg) offset = cxled->dpa_res->start - cxled->skip; length = cxled->skip; rc = cxl_mem_get_poison(cxlmd, offset, length, NULL); - if (rc == -EFAULT && mode == CXL_PARTMODE_RAM) - rc = 0; - if (rc) + if (rc && !poison_efault_forgiven(rc, mode)) return rc; } offset = cxled->dpa_res->start; length = cxled->dpa_res->end - offset + 1; rc = cxl_mem_get_poison(cxlmd, offset, length, cxled->cxld.region); - if (rc == -EFAULT && mode == CXL_PARTMODE_RAM) - rc = 0; - if (rc) + if (rc && !poison_efault_forgiven(rc, mode)) return rc; /* Iterate until commit_end is reached */ From 00f9ef9ad1cee60541ced2ec35f3c8182c4cb2ec Mon Sep 17 00:00:00 2001 From: Richard Cheng Date: Mon, 14 Sep 2026 17:08:57 +0800 Subject: [PATCH 13/49] cxl/hdm: Allow zero sized HDM decoders MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit CXL r4.0 §8.2.4.20.12 ("Committing Decoder Programming") and §14.13.10 ("CXL HDM Decoder Zero Size Commit") permit committing an HDM decoder with size 0. BIOS may commit and lock such decoders so the OS cannot program regions through them, this is a design choice rather than a spec requirement. The kernel rejected these with -ENXIO during port enumeration and aborted the whole port, so affected systems showed nothing under "cxl list". Treat empty decoders as first class reservations. Back them with a separately allocated resource, since the resource tree cannot represent an empty range, and keep the skip and hdm_end accounting intact. Exclude empty decoders from region assembly and avoid zero-length poison queries. Suggested-by: Dan Williams Signed-off-by: Vishal Aslot Signed-off-by: Richard Cheng Reviewed-by: Dan Williams Reviewed-by: Dave Jiang Reviewed-by: Jonathan Cameron Reviewed-by: Alison Schofield Link: https://patch.msgid.link/20260914090858.19181-3-icheng@nvidia.com Signed-off-by: Dave Jiang --- drivers/cxl/core/hdm.c | 58 +++++++++++++++++++++++++++------------ drivers/cxl/core/mbox.c | 3 ++ drivers/cxl/core/region.c | 45 ++++++++++++++++++++---------- drivers/cxl/cxl.h | 10 +++++++ drivers/cxl/port.c | 3 ++ 5 files changed, 86 insertions(+), 33 deletions(-) diff --git a/drivers/cxl/core/hdm.c b/drivers/cxl/core/hdm.c index 0c80b76a5f9b..39fe283cbc74 100644 --- a/drivers/cxl/core/hdm.c +++ b/drivers/cxl/core/hdm.c @@ -240,6 +240,18 @@ static resource_size_t __adjust_skip(struct cxl_dev_state *cxlds, } #define release_skip(c, b, l) __adjust_skip((c), (b), (l), NULL) +static void cxl_dpa_release_region(struct resource *parent, + struct resource *res) +{ + /* zero sized decoders are not tracked in the resource tree */ + if (resource_size(res) == 0) { + kfree(res); + return; + } + + __release_region(parent, res->start, resource_size(res)); +} + /* * Must be called in a context that synchronizes against this decoder's * port ->remove() callback (like an endpoint decoder sysfs attribute) @@ -256,7 +268,7 @@ static void __cxl_dpa_release(struct cxl_endpoint_decoder *cxled) /* save @skip_start, before @res is released */ skip_start = res->start - cxled->skip; - __release_region(&cxlds->dpa_res, res->start, resource_size(res)); + cxl_dpa_release_region(&cxlds->dpa_res, res); if (cxled->skip) release_skip(cxlds, skip_start, cxled->skip); cxled->skip = 0; @@ -336,6 +348,27 @@ static int request_skip(struct cxl_dev_state *cxlds, return -EBUSY; } +static struct resource *cxl_dpa_request_region(struct resource *parent, + resource_size_t start, + resource_size_t n, + const char *name) +{ + struct resource *res; + + if (!n) { + res = kmalloc_obj(*res); + if (!res) + return ERR_PTR(-ENOMEM); + + *res = DEFINE_RES_NAMED(start, 0, name, IORESOURCE_MEM); + + return res; + } + + res = __request_region(parent, start, n, name, 0); + return res ?: ERR_PTR(-EBUSY); +} + static int __cxl_dpa_reserve(struct cxl_endpoint_decoder *cxled, resource_size_t base, resource_size_t len, resource_size_t skipped) @@ -349,12 +382,6 @@ static int __cxl_dpa_reserve(struct cxl_endpoint_decoder *cxled, lockdep_assert_held_write(&cxl_rwsem.dpa); - if (!len) { - dev_warn(dev, "decoder%d.%d: empty reservation attempted\n", - port->id, cxled->cxld.id); - return -EINVAL; - } - if (cxled->dpa_res) { dev_dbg(dev, "decoder%d.%d: existing allocation %pr assigned\n", port->id, cxled->cxld.id, cxled->dpa_res); @@ -378,14 +405,14 @@ static int __cxl_dpa_reserve(struct cxl_endpoint_decoder *cxled, if (rc) return rc; } - res = __request_region(&cxlds->dpa_res, base, len, - dev_name(&cxled->cxld.dev), 0); - if (!res) { + res = cxl_dpa_request_region(&cxlds->dpa_res, base, len, + dev_name(&cxled->cxld.dev)); + if (IS_ERR(res)) { dev_dbg(dev, "decoder%d.%d: failed to reserve allocation\n", port->id, cxled->cxld.id); if (skipped) release_skip(cxlds, base - skipped, skipped); - return -EBUSY; + return PTR_ERR(res); } cxled->dpa_res = res; cxled->skip = skipped; @@ -402,7 +429,8 @@ static int __cxl_dpa_reserve(struct cxl_endpoint_decoder *cxled, break; } - if (cxled->part < 0) + /* Empty decoders may not be contained by a partition boundary */ + if (cxled->part < 0 && resource_size(res)) dev_warn(dev, "decoder%d.%d: %pr does not map any partition\n", port->id, cxled->cxld.id, res); @@ -1031,12 +1059,6 @@ static int init_hdm_decoder(struct cxl_port *port, struct cxl_decoder *cxld, return -ENXIO; } - if (size == 0) { - dev_warn(&port->dev, - "decoder%d.%d: Committed with zero size\n", - port->id, cxld->id); - return -ENXIO; - } port->commit_end = cxld->id; } else { if (cxled) { diff --git a/drivers/cxl/core/mbox.c b/drivers/cxl/core/mbox.c index 55828a836c01..1a2553332801 100644 --- a/drivers/cxl/core/mbox.c +++ b/drivers/cxl/core/mbox.c @@ -1386,6 +1386,9 @@ int cxl_mem_get_poison(struct cxl_memdev *cxlmd, u64 offset, u64 len, int nr_records = 0; int rc; + if (!len) + return 0; + ACQUIRE(mutex_intr, lock)(&mds->poison.mutex); if ((rc = ACQUIRE_ERR(mutex_intr, &lock))) return rc; diff --git a/drivers/cxl/core/region.c b/drivers/cxl/core/region.c index 10abc073103b..bb51e67f25f5 100644 --- a/drivers/cxl/core/region.c +++ b/drivers/cxl/core/region.c @@ -2112,7 +2112,7 @@ static int cxl_region_attach(struct cxl_region *cxlr, return -ENXIO; } - if (!cxled->dpa_res) { + if (cxled_empty(cxled)) { dev_dbg(&cxlr->dev, "%s:%s: missing DPA allocation.\n", dev_name(&cxlmd->dev), dev_name(&cxled->cxld.dev)); return -ENXIO; @@ -2966,24 +2966,31 @@ static int poison_by_decoder(struct device *dev, void *arg) if (!cxled->dpa_res) return 0; - cxlmd = cxled_to_memdev(cxled); - cxlds = cxlmd->cxlds; - mode = cxlds->part[cxled->part].mode; + /* + * Handle the degenerate case of a device with only empty decoders. An + * empty decoder can still map a non-zero skip range, so advance the + * walk to commit_end either way. + */ + if (cxled->part >= 0) { + cxlmd = cxled_to_memdev(cxled); + cxlds = cxlmd->cxlds; + mode = cxlds->part[cxled->part].mode; - if (cxled->skip) { - offset = cxled->dpa_res->start - cxled->skip; - length = cxled->skip; - rc = cxl_mem_get_poison(cxlmd, offset, length, NULL); + if (cxled->skip) { + offset = cxled->dpa_res->start - cxled->skip; + length = cxled->skip; + rc = cxl_mem_get_poison(cxlmd, offset, length, NULL); + if (rc && !poison_efault_forgiven(rc, mode)) + return rc; + } + + offset = cxled->dpa_res->start; + length = cxled->dpa_res->end - offset + 1; + rc = cxl_mem_get_poison(cxlmd, offset, length, cxled->cxld.region); if (rc && !poison_efault_forgiven(rc, mode)) return rc; } - offset = cxled->dpa_res->start; - length = cxled->dpa_res->end - offset + 1; - rc = cxl_mem_get_poison(cxlmd, offset, length, cxled->cxld.region); - if (rc && !poison_efault_forgiven(rc, mode)) - return rc; - /* Iterate until commit_end is reached */ if (cxled->cxld.id == ctx->port->commit_end) { ctx->offset = cxled->dpa_res->end + 1; @@ -3005,9 +3012,17 @@ int cxl_get_poison_by_endpoint(struct cxl_port *port) }; rc = device_for_each_child(&port->dev, &ctx, poison_by_decoder); - if (rc == 1) + if (rc == 1) { + /* + * No decoder with a sized DPA reservation was walked + * (every committed decoder is zero-size): scan all + * partitions in full. + */ + if (ctx.part < 0) + ctx.part = 0; rc = cxl_get_poison_unmapped(to_cxl_memdev(port->uport_dev), &ctx); + } return rc; } diff --git a/drivers/cxl/cxl.h b/drivers/cxl/cxl.h index cab8ce39f465..3ef0810ab86b 100644 --- a/drivers/cxl/cxl.h +++ b/drivers/cxl/cxl.h @@ -336,6 +336,16 @@ struct cxl_endpoint_decoder { int pos; }; +/* + * The common case is decoders with no reservation, but also handle + * decoders with a zero-sized reservation that firmware may install for + * security lockdown purposes. + */ +static inline bool cxled_empty(struct cxl_endpoint_decoder *cxled) +{ + return !cxled->dpa_res || !resource_size(cxled->dpa_res); +} + /** * struct cxl_switch_decoder - Switch specific CXL HDM Decoder * @cxld: base cxl_decoder object diff --git a/drivers/cxl/port.c b/drivers/cxl/port.c index 99cf77b6b699..c12fd0b89883 100644 --- a/drivers/cxl/port.c +++ b/drivers/cxl/port.c @@ -46,6 +46,9 @@ static int discover_region(struct device *dev, void *unused) if (cxled->state != CXL_DECODER_STATE_AUTO) return 0; + if (cxled_empty(cxled)) + return 0; + /* * Region enumeration is opportunistic, if this add-event fails, * continue to the next endpoint decoder. From fef22d37d47043b63e4007f2f5fe762fa21fae0c Mon Sep 17 00:00:00 2001 From: Richard Cheng Date: Mon, 14 Sep 2026 17:08:58 +0800 Subject: [PATCH 14/49] tools/testing/cxl: Enable zero sized decoders under hb0 The kernel now allows committed zero-size HDM decoders so BIOS can lock empty decoders; cxl_test needs to exercise that path. Add a mock_zero_size_decoders module parameter (default off). When set, the special endpoints under host-bridge0 (cxl_mem.0 and cxl_mem.4) commit decoders 1 and 2 as zero-size + locked above the decoder[0] auto-region, mirrored on the parent switch and host bridge. The mocks take a real zero-size DPA reservation, like enumeration of real hardware, so commit_end lands on a zero-size decoder and the reservation, poison-by-endpoint, and teardown paths all run. Signed-off-by: Vishal Aslot Signed-off-by: Richard Cheng Reviewed-by: Dave Jiang Reviewed-by: Jonathan Cameron Reviewed-by: Alison Schofield Reviewed-by: Jonathan Cameron Link: https://patch.msgid.link/20260914090858.19181-4-icheng@nvidia.com Signed-off-by: Dave Jiang --- tools/testing/cxl/test/cxl.c | 109 ++++++++++++++++++++++++++++++----- 1 file changed, 94 insertions(+), 15 deletions(-) diff --git a/tools/testing/cxl/test/cxl.c b/tools/testing/cxl/test/cxl.c index 62bd92b3be45..2921f1111870 100644 --- a/tools/testing/cxl/test/cxl.c +++ b/tools/testing/cxl/test/cxl.c @@ -18,6 +18,7 @@ static int interleave_arithmetic; static bool extended_linear_cache; static bool fail_autoassemble; static bool type2_test; +static bool mock_zero_size_decoders; #define FAKE_QTG_ID 42 @@ -872,14 +873,13 @@ static int cxld_registry_restore(struct cxl_decoder *cxld, cxld_copy(cxld, &td->cxled.cxld); cxled->state = td->cxled.state; cxled->skip = td->cxled.skip; - if (range_len(&td->dpa_range)) { - rc = devm_cxl_dpa_reserve(cxled, td->dpa_range.start, - range_len(&td->dpa_range), - td->cxled.skip); - if (rc) { - init_disabled_mock_decoder(cxld); - return rc; - } + /* enabled endpoint decoders hold a reservation, sized or not */ + rc = devm_cxl_dpa_reserve(cxled, td->dpa_range.start, + range_len(&td->dpa_range), + td->cxled.skip); + if (rc) { + init_disabled_mock_decoder(cxld); + return rc; } port->commit_end = cxld->id; } @@ -1072,16 +1072,49 @@ static void default_mock_decoder(struct cxl_decoder *cxld) WARN_ON_ONCE(!cxld_registry_new(cxld)); } -static int first_decoder(struct device *dev, const void *data) +static int match_decoder_by_index(struct device *dev, const void *data) { + int target_id = *(const int *)data; struct cxl_decoder *cxld; if (!is_switch_decoder(dev)) return 0; cxld = to_cxl_decoder(dev); - if (cxld->id == 0) - return 1; - return 0; + return cxld->id == target_id; +} + +/* + * Mock a committed, locked, empty decoder + * (CXL r4.0 8.2.4.20.12). Gated by the mock_zero_size_decoders module + * param so the default cxl_test topology, shared by the region test + * suite, is left undisturbed. + */ +static void size_zero_mock_decoder_ep(struct cxl_decoder *cxld, u64 base) +{ + struct cxl_endpoint_decoder *cxled = to_cxl_endpoint_decoder(&cxld->dev); + + cxld->hpa_range = DEFINE_RANGE(base, base - 1); + cxld->interleave_ways = 2; + cxld->interleave_granularity = 4096; + cxld->target_type = CXL_DECODER_HOSTONLYMEM; + cxld->flags = CXL_DECODER_F_ENABLE | CXL_DECODER_F_LOCK; + cxled->state = CXL_DECODER_STATE_AUTO; + /* decoder[0] reserved [0, size/2), empty decoders sit at that watermark */ + devm_cxl_dpa_reserve(cxled, mock_auto_region_size / 2, 0, 0); + cxld->commit = mock_decoder_commit; + cxld->reset = mock_decoder_reset; +} + +static void size_zero_mock_decoder_sw(struct cxl_decoder *cxld, u64 base, + int level) +{ + cxld->flags = CXL_DECODER_F_ENABLE | CXL_DECODER_F_LOCK; + cxld->target_type = CXL_DECODER_HOSTONLYMEM; + cxld->interleave_ways = level == 0 ? 2 : 1; + cxld->interleave_granularity = 4096; + cxld->hpa_range = DEFINE_RANGE(base, base - 1); + cxld->commit = mock_decoder_commit; + cxld->reset = mock_decoder_reset; } enum cxld_init_type { @@ -1114,7 +1147,7 @@ static enum cxld_init_type get_decoder_init_type(struct cxl_decoder *cxld, * See 'cxl list -BMPu -m cxl_mem.0,cxl_mem.4' */ if (!is_endpoint_decoder(&cxld->dev) || !hb0 || pdev->id % 4 || - pdev->id > 4 || cxld->id > 0) + pdev->id > 4 || cxld->id > (mock_zero_size_decoders ? 2 : 0)) return MOCK_DECODER_INIT_DEFAULT; return type2_test ? MOCK_DECODER_INIT_TYPE2_AUTO : @@ -1147,6 +1180,7 @@ static void mock_init_hdm_type2_cxled(struct cxl_endpoint_decoder *cxled, struct cxl_port *root_port; struct device *dev; u64 base; + int id = 0; base = window->base_hpa; cxld->hpa_range = (struct range) { @@ -1172,7 +1206,7 @@ static void mock_init_hdm_type2_cxled(struct cxl_endpoint_decoder *cxled, */ dport = port->parent_dport; root_port = dport->port; - dev = device_find_child(&root_port->dev, NULL, first_decoder); + dev = device_find_child(&root_port->dev, &id, match_decoder_by_index); /* * Ancestor ports are guaranteed to be enumerated before * @port, and all ports have at least one decoder. @@ -1232,6 +1266,20 @@ static void mock_init_hdm_type3_cxled(struct cxl_endpoint_decoder *cxled, base = window->base_hpa; if (extended_linear_cache) base += mock_auto_region_size; + + /* + * With mock_zero_size_decoders, decoders 1 and 2 of the special + * endpoints mock committed, locked, empty decoders above the + * decoder[0] auto-region (CXL r4.0 8.2.4.20.12). commit_end then + * points at a zero-size decoder, exercising the zero-size + * reservation and poison-by-endpoint code paths. + */ + if (cxld->id == 1 || cxld->id == 2) { + size_zero_mock_decoder_ep(cxld, base); + port->commit_end = cxld->id; + WARN_ON_ONCE(!cxld_registry_new(cxld)); + return; + } cxld->hpa_range = (struct range) { .start = base, .end = base + mock_auto_region_size - 1, @@ -1255,9 +1303,11 @@ static void mock_init_hdm_type3_cxled(struct cxl_endpoint_decoder *cxled, */ iter = port; for (i = 0; i < 2; i++) { + int id = 0; + dport = iter->parent_dport; iter = dport->port; - dev = device_find_child(&iter->dev, NULL, first_decoder); + dev = device_find_child(&iter->dev, &id, match_decoder_by_index); /* * Ancestor ports are guaranteed to be enumerated before * @port, and all ports have at least one decoder. @@ -1307,6 +1357,26 @@ static void mock_init_hdm_type3_cxled(struct cxl_endpoint_decoder *cxled, cxld_registry_update(cxld); put_device(dev); + + if (!mock_zero_size_decoders) + continue; + + /* + * Mirror the endpoint: commit the next two switch decoders + * as zero-size + locked so the empty-decoder layout extends + * end-to-end through the switch and host bridge. + */ + for (id = 1; id <= 2; id++) { + dev = device_find_child(&iter->dev, &id, + match_decoder_by_index); + if (WARN_ON(!dev)) + continue; + cxld = to_cxl_decoder(dev); + size_zero_mock_decoder_sw(cxld, base, i); + iter->commit_end = id; + cxld_registry_update(cxld); + put_device(dev); + } } } @@ -2076,6 +2146,12 @@ static bool __init have_multiple_modparms(void) count++; if (type2_test) count++; + if (mock_zero_size_decoders) + count++; + + /* Zero-size decoders require decoder[0] to preserve commit order. */ + if (fail_autoassemble && mock_zero_size_decoders) + return true; return count > 1; } @@ -2484,6 +2560,9 @@ module_param(fail_autoassemble, bool, 0444); MODULE_PARM_DESC(fail_autoassemble, "Simulate missing member of an auto-region"); module_param(type2_test, bool, 0444); MODULE_PARM_DESC(type2_test, "Enable type 2 support testing"); +module_param(mock_zero_size_decoders, bool, 0444); +MODULE_PARM_DESC(mock_zero_size_decoders, + "Mock committed, locked, empty decoders under host-bridge0"); module_init(cxl_test_init); module_exit(cxl_test_exit); MODULE_LICENSE("GPL v2"); From 3696326ca14cf018788d64232a068fdf26b1ff31 Mon Sep 17 00:00:00 2001 From: Alison Schofield Date: Fri, 7 Aug 2026 17:05:48 -0700 Subject: [PATCH 15/49] cxl/hdm: Reject switch decoder interleave ways that overflow targets Switch decoder enumeration validates that the interleave ways encoding is legal, but not that the resulting number of ways fits the available targets. This can overrun the target array (target_list.target_id[]) during enumeration of 12 or 16 way interleaves. Bound the target count where it is established rather than at each use. Reject an HDM capability whose reported target count is zero or exceeds the target list register capacity, and reject a zero target switch decoder allocation in cxl_switch_decoder_init(). Enumeration is then left with a single check: reject a decoder whose interleave ways exceed the targets it has. Add a clarifying comment that target_count is a direct count, not 0-based like decoder_count. Link: https://sashiko.dev/#/patchset/20260711180755.1779002-1-alison.schofield@intel.com?part=1 Fixes: d17d0540a0db ("cxl/core/hdm: Add CXL standard decoder enumeration to the core") Tested-by: Davidlohr Bueso Reviewed-by: Davidlohr Bueso Reviewed-by: Richard Cheng Signed-off-by: Alison Schofield Reviewed-by: Li Ming Link: https://patch.msgid.link/af094094813d7f51d86cbf608936270b7d85526e.1786143520.git.alison.schofield@intel.com Signed-off-by: Dave Jiang --- drivers/cxl/core/hdm.c | 24 ++++++++++++++++++++++++ drivers/cxl/core/port.c | 2 +- drivers/cxl/cxl.h | 2 ++ 3 files changed, 27 insertions(+), 1 deletion(-) diff --git a/drivers/cxl/core/hdm.c b/drivers/cxl/core/hdm.c index 0c80b76a5f9b..c394b3d54d36 100644 --- a/drivers/cxl/core/hdm.c +++ b/drivers/cxl/core/hdm.c @@ -76,6 +76,8 @@ static void parse_hdm_decoder_caps(struct cxl_hdm *cxlhdm) hdm_cap = readl(cxlhdm->regs.hdm_decoder + CXL_HDM_DECODER_CAP_OFFSET); cxlhdm->decoder_count = cxl_hdm_decoder_count(hdm_cap); + + /* target_count is a direct count (1h..8h), not 0-based like decoder_count */ cxlhdm->target_count = FIELD_GET(CXL_HDM_DECODER_TARGET_COUNT_MASK, hdm_cap); if (FIELD_GET(CXL_HDM_DECODER_INTERLEAVE_11_8, hdm_cap)) @@ -1084,6 +1086,16 @@ static int init_hdm_decoder(struct cxl_port *port, struct cxl_decoder *cxld, cxld->interleave_ways, cxld->interleave_granularity); if (!cxled) { + struct cxl_switch_decoder *cxlsd = to_cxl_switch_decoder(&cxld->dev); + + if (cxld->interleave_ways > cxlsd->nr_targets) { + dev_err(&port->dev, + "decoder%d.%d: interleave ways: %d exceeds targets: %d\n", + port->id, cxld->id, cxld->interleave_ways, + cxlsd->nr_targets); + return -ENXIO; + } + lo = readl(hdm + CXL_HDM_DECODER0_TL_LOW(which)); hi = readl(hdm + CXL_HDM_DECODER0_TL_HIGH(which)); target_list.value = (hi << 32) + lo; @@ -1161,6 +1173,18 @@ static int devm_cxl_enumerate_decoders(struct cxl_hdm *cxlhdm, int i; u64 dpa_base = 0; + /* + * Per CXL 4.0 8.2.4.20.1 Target Count is the number of target ports + * per decoder, max 8. Endpoint decoders have no targets. + */ + if (!is_cxl_endpoint(port) && + (cxlhdm->target_count < 1 || + cxlhdm->target_count > CXL_HDM_DECODER0_TL_TARGETS)) { + dev_err(&port->dev, "Invalid decoder target count: %u\n", + cxlhdm->target_count); + return -ENXIO; + } + cxl_settle_decoders(cxlhdm); for (i = 0; i < cxlhdm->decoder_count; i++) { diff --git a/drivers/cxl/core/port.c b/drivers/cxl/core/port.c index 625e4aa427db..04a000f6bf89 100644 --- a/drivers/cxl/core/port.c +++ b/drivers/cxl/core/port.c @@ -1987,7 +1987,7 @@ static int cxl_switch_decoder_init(struct cxl_port *port, struct cxl_switch_decoder *cxlsd, int nr_targets) { - if (nr_targets > CXL_DECODER_MAX_INTERLEAVE) + if (nr_targets < 1 || nr_targets > CXL_DECODER_MAX_INTERLEAVE) return -EINVAL; cxlsd->nr_targets = nr_targets; diff --git a/drivers/cxl/cxl.h b/drivers/cxl/cxl.h index cab8ce39f465..b6853bc8fd81 100644 --- a/drivers/cxl/cxl.h +++ b/drivers/cxl/cxl.h @@ -67,6 +67,8 @@ extern const struct nvdimm_security_ops *cxl_security_ops; #define CXL_HDM_DECODER0_CTRL_HOSTONLY BIT(12) #define CXL_HDM_DECODER0_TL_LOW(i) (0x20 * (i) + 0x24) #define CXL_HDM_DECODER0_TL_HIGH(i) (0x20 * (i) + 0x28) +/* Target list capacity, i.e. max interleave ways for a non-endpoint decoder */ +#define CXL_HDM_DECODER0_TL_TARGETS 8 #define CXL_HDM_DECODER0_SKIP_LOW(i) CXL_HDM_DECODER0_TL_LOW(i) #define CXL_HDM_DECODER0_SKIP_HIGH(i) CXL_HDM_DECODER0_TL_HIGH(i) From 3f846646b3d3b5699139e686e1107d231a725860 Mon Sep 17 00:00:00 2001 From: Alison Schofield Date: Fri, 7 Aug 2026 17:05:49 -0700 Subject: [PATCH 16/49] cxl/hdm: Make switch decoder target parsing endian-safe Switch decoder target IDs are stored one per byte, starting with target 0 in the least significant byte. The current code accesses those bytes in memory order, which only matches register order on little-endian hosts. On big-endian hosts, the target IDs are reversed and the decoder is associated with the wrong downstream ports. Place the register values in a byte array in little-endian order and index the target IDs from there, so the target mapping is independent of host endianness. Fixes: d17d0540a0db ("cxl/core/hdm: Add CXL standard decoder enumeration to the core") Signed-off-by: Alison Schofield Reviewed-by: Li Ming Link: https://patch.msgid.link/b0526825b3c3ffdb78837563e178d9aa27b6bbb8.1786143520.git.alison.schofield@intel.com Signed-off-by: Dave Jiang --- drivers/cxl/core/hdm.c | 15 +++++++-------- 1 file changed, 7 insertions(+), 8 deletions(-) diff --git a/drivers/cxl/core/hdm.c b/drivers/cxl/core/hdm.c index c394b3d54d36..fc22d515b638 100644 --- a/drivers/cxl/core/hdm.c +++ b/drivers/cxl/core/hdm.c @@ -1,6 +1,7 @@ // SPDX-License-Identifier: GPL-2.0-only /* Copyright(c) 2022 Intel Corporation. All rights reserved. */ #include +#include #include #include @@ -975,15 +976,12 @@ static int init_hdm_decoder(struct cxl_port *port, struct cxl_decoder *cxld, u64 *dpa_base, struct cxl_endpoint_dvsec_info *info) { struct cxl_endpoint_decoder *cxled = NULL; + u8 target_id[CXL_HDM_DECODER0_TL_TARGETS]; u64 size, base, skip, dpa_size, lo, hi; bool committed; u32 remainder; int i, rc; u32 ctrl; - union { - u64 value; - unsigned char target_id[8]; - } target_list; if (should_emulate_decoders(info)) return cxl_setup_hdm_decoder_from_dvsec(port, cxld, dpa_base, @@ -1096,11 +1094,12 @@ static int init_hdm_decoder(struct cxl_port *port, struct cxl_decoder *cxld, return -ENXIO; } - lo = readl(hdm + CXL_HDM_DECODER0_TL_LOW(which)); - hi = readl(hdm + CXL_HDM_DECODER0_TL_HIGH(which)); - target_list.value = (hi << 32) + lo; + put_unaligned_le32(readl(hdm + CXL_HDM_DECODER0_TL_LOW(which)), + &target_id[0]); + put_unaligned_le32(readl(hdm + CXL_HDM_DECODER0_TL_HIGH(which)), + &target_id[4]); for (i = 0; i < cxld->interleave_ways; i++) - cxld->target_map[i] = target_list.target_id[i]; + cxld->target_map[i] = target_id[i]; return 0; } From 7dae18c39e99813461db6ef3fc13f077452109ce Mon Sep 17 00:00:00 2001 From: Alison Schofield Date: Fri, 7 Aug 2026 17:05:50 -0700 Subject: [PATCH 17/49] cxl/hdm: Restore commit_end when decoder enumeration fails commit_end tracks the highest committed decoder on a port. It gets advanced before decoder enumeration is complete, so a later failure leaves it pointing at a decoder that was never added. The next probe then rejects decoder0 as out of order and enumeration fails. Reset commit_end to the uncommitted baseline once the decoders are torn down, so a subsequent probe rebuilds it from scratch. Register the reset before the decoders are added so that on unwind it runs after every decoder has been unregistered. This prevents commit_end from dropping below the highest committed decoder still registered and exposing an inconsistent value to a concurrent reader. Protect the commit_end advance in the DVSEC emulation path against concurrent readers, matching the register-programmed path. Fixes: 176baefb2eb5 ("cxl/hdm: Commit decoder state to hardware") Fixes: b777e9bec960 ("cxl/hdm: Emulate HDM decoder from DVSEC range registers") Reviewed-by: Jonathan Cameron Reviewed-by: Dave Jiang Reviewed-by: Li Ming Reviewed-by: Richard Cheng Signed-off-by: Alison Schofield Link: https://patch.msgid.link/4802c5abc906b858a4754bfc4fa3e4c2c455d0c3.1786143520.git.alison.schofield@intel.com Signed-off-by: Dave Jiang --- drivers/cxl/core/hdm.c | 23 ++++++++++++++++++++--- 1 file changed, 20 insertions(+), 3 deletions(-) diff --git a/drivers/cxl/core/hdm.c b/drivers/cxl/core/hdm.c index fc22d515b638..cb24901dd289 100644 --- a/drivers/cxl/core/hdm.c +++ b/drivers/cxl/core/hdm.c @@ -956,7 +956,8 @@ static int cxl_setup_hdm_decoder_from_dvsec( * change the range registers at run time. */ cxld->flags |= CXL_DECODER_F_ENABLE | CXL_DECODER_F_LOCK; - port->commit_end = cxld->id; + scoped_guard(rwsem_write, &cxl_rwsem.region) + port->commit_end = cxld->id; rc = devm_cxl_dpa_reserve(cxled, *dpa_base, len, 0); if (rc) { @@ -1159,6 +1160,14 @@ static void cxl_settle_decoders(struct cxl_hdm *cxlhdm) msleep(20); } +static void cxl_reset_commit_end(void *data) +{ + struct cxl_port *port = data; + + guard(rwsem_write)(&cxl_rwsem.region); + port->commit_end = -1; +} + /** * devm_cxl_enumerate_decoders - add decoder objects per HDM register set * @cxlhdm: Structure to populate with HDM capabilities @@ -1169,8 +1178,8 @@ static int devm_cxl_enumerate_decoders(struct cxl_hdm *cxlhdm, { void __iomem *hdm = cxlhdm->regs.hdm_decoder; struct cxl_port *port = cxlhdm->port; - int i; u64 dpa_base = 0; + int i, rc; /* * Per CXL 4.0 8.2.4.20.1 Target Count is the number of target ports @@ -1186,8 +1195,16 @@ static int devm_cxl_enumerate_decoders(struct cxl_hdm *cxlhdm, cxl_settle_decoders(cxlhdm); + /* + * Reset commit_end after all decoders have been torn down so a + * subsequent probe rebuilds it from scratch. + */ + rc = devm_add_action(&port->dev, cxl_reset_commit_end, port); + if (rc) + return rc; + for (i = 0; i < cxlhdm->decoder_count; i++) { - int rc, target_count = cxlhdm->target_count; + int target_count = cxlhdm->target_count; struct cxl_decoder *cxld; if (is_cxl_endpoint(port)) { From 4e42c398af136d62fac8c19c9f410ec83a8dd34a Mon Sep 17 00:00:00 2001 From: Alison Schofield Date: Fri, 7 Aug 2026 17:05:51 -0700 Subject: [PATCH 18/49] cxl/port: Bound switch decoder target array access by nr_targets Switch decoder target lookups walk the target array using interleave ways as the bound. The array is allocated with nr_targets entries, so a decoder whose interleave ways exceeds nr_targets reads and writes past the end of it. That happens during dport activation, during decoder target population, and when userspace reads the target_list attribute. Bounding by nr_targets alone is not correct either. Commit d6488fee6647 ("cxl/port: Fix decoder initialization when nr_targets > interleave_ways") moved target population to interleave ways precisely so that targets a decoder does not use are left unpopulated. Neither field alone is a safe bound, so add a helper that returns the minimum of the two and use it at every walk of the target array. Valid configurations keep interleave ways within nr_targets, so this is not a behavior change for them. Fixes: 40ba17afdfab ("cxl/acpi: Introduce cxl_decoder objects") Fixes: d6488fee6647 ("cxl/port: Fix decoder initialization when nr_targets > interleave_ways") Signed-off-by: Alison Schofield Reviewed-by: Li Ming Link: https://patch.msgid.link/d7577652cc6d414e1de0ee4045d4d6b2808172ce.1786143520.git.alison.schofield@intel.com Signed-off-by: Dave Jiang --- drivers/cxl/core/port.c | 25 ++++++++++++++++++------- 1 file changed, 18 insertions(+), 7 deletions(-) diff --git a/drivers/cxl/core/port.c b/drivers/cxl/core/port.c index 04a000f6bf89..5baa367e7913 100644 --- a/drivers/cxl/core/port.c +++ b/drivers/cxl/core/port.c @@ -148,20 +148,29 @@ static ssize_t target_type_show(struct device *dev, } static DEVICE_ATTR_RO(target_type); +/* + * Interleave ways selects how many targets a decoder uses, but the target + * array is only nr_targets long. Bound array access by both. + */ +static int cxlsd_nr_used_targets(struct cxl_switch_decoder *cxlsd) +{ + return min(cxlsd->cxld.interleave_ways, cxlsd->nr_targets); +} + static ssize_t emit_target_list(struct cxl_switch_decoder *cxlsd, char *buf) { - struct cxl_decoder *cxld = &cxlsd->cxld; + int nr_used = cxlsd_nr_used_targets(cxlsd); ssize_t offset = 0; int i, rc = 0; - for (i = 0; i < cxld->interleave_ways; i++) { + for (i = 0; i < nr_used; i++) { struct cxl_dport *dport = cxlsd->target[i]; struct cxl_dport *next = NULL; if (!dport) break; - if (i + 1 < cxld->interleave_ways) + if (i + 1 < nr_used) next = cxlsd->target[i + 1]; rc = sysfs_emit_at(buf, offset, "%d%s", dport->port_id, next ? "," : ""); @@ -1611,7 +1620,7 @@ static int update_decoder_targets(struct device *dev, void *data) struct cxl_dport *dport = data; struct cxl_switch_decoder *cxlsd; struct cxl_decoder *cxld; - int i; + int i, nr_used; if (!is_switch_decoder(dev)) return 0; @@ -1619,8 +1628,9 @@ static int update_decoder_targets(struct device *dev, void *data) cxlsd = to_cxl_switch_decoder(dev); cxld = &cxlsd->cxld; guard(rwsem_write)(&cxl_rwsem.region); + nr_used = cxlsd_nr_used_targets(cxlsd); - for (i = 0; i < cxld->interleave_ways; i++) { + for (i = 0; i < nr_used; i++) { if (cxld->target_map[i] == dport->port_id) { cxlsd->target[i] = dport; dev_dbg(dev, "dport%d found in target list, index %d\n", @@ -1918,7 +1928,7 @@ static int decoder_populate_targets(struct cxl_switch_decoder *cxlsd, struct cxl_port *port) { struct cxl_decoder *cxld = &cxlsd->cxld; - int i; + int i, nr_used; device_lock_assert(&port->dev); @@ -1926,7 +1936,8 @@ static int decoder_populate_targets(struct cxl_switch_decoder *cxlsd, return 0; guard(rwsem_write)(&cxl_rwsem.region); - for (i = 0; i < cxlsd->cxld.interleave_ways; i++) { + nr_used = cxlsd_nr_used_targets(cxlsd); + for (i = 0; i < nr_used; i++) { struct cxl_dport *dport = find_dport(port, cxld->target_map[i]); if (!dport) { From 17ff3a9fd8669ea53fe83a7013871f30214d5537 Mon Sep 17 00:00:00 2001 From: Guixin Liu Date: Mon, 31 Aug 2026 19:04:49 +0800 Subject: [PATCH 19/49] cxl/pci: Skip reset detection for DVSEC emulated decoders HDM decoders are emulated from the DVSEC range registers in two cases: (a) the component registers expose no HDM decoder capability, or (b) the capability is present but the DVSEC ranges were the ones in use at driver load. After an FLR or SBR, __cxl_endpoint_decoder_reset_detected() reads the HDM decoder Committed bit for every decoder marked enabled, emulated ones included. In case (a) regs.hdm_decoder is NULL and the read oopses. In case (b) the Committed bit was never set, so a reset gets reported that never happened. Use the absence of cxld->commit to elide the check for emulated decoders. Case (b) tested under QEMU: a reset on an endpoint driven down the DVSEC emulation path no longer reports a reset or strips the decoder flags. Fixes: 934edcd436dc ("cxl: Add post-reset warning if reset results in loss of previously committed HDM decoders") Signed-off-by: Guixin Liu Reviewed-by: Jonathan Cameron Link: https://lore.kernel.org/linux-cxl/20260811113608.2815625-1-kanie@linux.alibaba.com/ Link: https://patch.msgid.link/20260831110449.719086-1-kanie@linux.alibaba.com Signed-off-by: Dave Jiang --- drivers/cxl/core/pci.c | 9 +++++++++ 1 file changed, 9 insertions(+) diff --git a/drivers/cxl/core/pci.c b/drivers/cxl/core/pci.c index 9d807c1a002c..43b9b7afff29 100644 --- a/drivers/cxl/core/pci.c +++ b/drivers/cxl/core/pci.c @@ -683,6 +683,15 @@ static int __cxl_endpoint_decoder_reset_detected(struct device *dev, void *data) if ((cxld->flags & CXL_DECODER_F_ENABLE) == 0) return 0; + /* + * Decoders emulated from the DVSEC range registers have no commit + * callback and no HDM decoder registers to consult. + */ + if (!cxld->commit) { + dev_warn(dev, "DVSEC emulated decode may have been cleared by reset\n"); + return 0; + } + cxlhdm = dev_get_drvdata(&port->dev); hdm = cxlhdm->regs.hdm_decoder; ctrl = readl(hdm + CXL_HDM_DECODER0_CTRL_OFFSET(cxld->id)); From c1031c2474f73261d8d5e4deef2b5dc20fb964f5 Mon Sep 17 00:00:00 2001 From: Ashok Raj Date: Fri, 11 Sep 2026 14:53:04 -0700 Subject: [PATCH 20/49] cxl/features: Ensure that count is set before access to ent[] __counted_by() get_supported_features() assigns entries->num_features only after the memcpy() loop that fills entries->ent[] has already run. Since ent[] is __counted_by(num_features), the compiler's bounds instrumentation sees a 0-length array during that loop and FORTIFY_SOURCE trips on the memcpy. Set @num_features right after allocation, before any write into ent[], so the bound is correct for the whole lifetime of the array. This was found via a fortify panic: memcpy: detected buffer overflow: 384 byte write of buffer size 0 WARNING: lib/string_helpers.c:1035 at __fortify_report+0x54/0xa0 kernel BUG at lib/string_helpers.c:1043! Call trace: __fortify_panic+0x10/0x18 get_supported_features.isra.0+0x4a0/0x4d0 [cxl_core] devm_cxl_setup_features+0x84/0x120 [cxl_core] cxl_pci_probe+0x254/0x5e0 [cxl_pci] Same class of bug, same fix shape as commit 6c9d2e87df40 ("cxl/fwctl: Fix __fortify_panic"), which fixed the analogous issue in cxlctl_get_supported_features() but missed this one. [ dj: Fixed up title typo per Jonathan. ] Fixes: f0e6a2329bf9 ("cxl: Add Get Supported Features command for kernel usage") Signed-off-by: Ashok Raj Reviewed-by: Dave Jiang Reviewed-by: Jonathan Cameron Link: https://patch.msgid.link/20260911215304.1816467-1-ashok.raj@oss.qualcomm.com Signed-off-by: Dave Jiang --- drivers/cxl/core/features.c | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/drivers/cxl/core/features.c b/drivers/cxl/core/features.c index 95f47193fb61..8024c2dcae1d 100644 --- a/drivers/cxl/core/features.c +++ b/drivers/cxl/core/features.c @@ -97,6 +97,7 @@ get_supported_features(struct cxl_features_state *cxlfs) kvmalloc_flex(*entries, ent, count); if (!entries) return NULL; + entries->num_features = count; struct cxl_mbox_get_sup_feats_out *mbox_out __free(kvfree) = kvmalloc(cxl_mbox->payload_size, GFP_KERNEL); @@ -174,7 +175,6 @@ get_supported_features(struct cxl_features_state *cxlfs) start += num_entries; } while (remain_feats); - entries->num_features = count; entries->num_user_features = user_feats; return no_free_ptr(entries); From 97b74de26c81885a6447f5fe87641a2a136f8c7c Mon Sep 17 00:00:00 2001 From: Guixin Liu Date: Mon, 31 Aug 2026 17:22:15 +0800 Subject: [PATCH 21/49] cxl/cdat: Fix uninitialized stack use in bandwidth gathering cxl_endpoint_gather_bandwidth() and cxl_switch_gather_bandwidth() declare access_coordinate arrays on the stack and rely on their helpers to fill them. The helpers assign only the bandwidth members, and the combine step assigns even those only when both inputs are non-zero. The latency members are summed from stack residue on every call, and the bandwidth members keep that residue into the region access coordinate sysfs attributes when the endpoint CDAT reports no bandwidth. Zero initialize the arrays. Zero is the value this code already uses for "not reported", so an unset member now reads back as unknown rather than as a plausible number. Found by code inspection. Tested on a QEMU CXL topology with two endpoints sharing a switch upstream link and HMAT generic-port coordinates for the host bridge, which the calculation requires to run at all: with temporary printk at the combine sites, the unpatched kernel summed 0xfefefefe, the CONFIG_INIT_STACK_ALL_PATTERN stack filler, into the latency members, while the patched kernel reports the CDAT latency values there. Fixes: a5ab0de0ebaa ("cxl: Calculate region bandwidth of targets with shared upstream link") Signed-off-by: Guixin Liu Reviewed-by: Richard Cheng Reviewed-by: Dave Jiang Reviewed-by: Jonathan Cameron Link: https://patch.msgid.link/20260831092216.540644-2-kanie@linux.alibaba.com Signed-off-by: Dave Jiang --- drivers/cxl/core/cdat.c | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/drivers/cxl/core/cdat.c b/drivers/cxl/core/cdat.c index 5c9f07262513..df9261ba84e5 100644 --- a/drivers/cxl/core/cdat.c +++ b/drivers/cxl/core/cdat.c @@ -633,9 +633,9 @@ static int cxl_endpoint_gather_bandwidth(struct cxl_region *cxlr, struct cxl_port *endpoint = to_cxl_port(cxled->cxld.dev.parent); struct cxl_port *parent_port = to_cxl_port(endpoint->dev.parent); struct cxl_port *gp_port = to_cxl_port(parent_port->dev.parent); - struct access_coordinate pci_coord[ACCESS_COORDINATE_MAX]; - struct access_coordinate sw_coord[ACCESS_COORDINATE_MAX]; - struct access_coordinate ep_coord[ACCESS_COORDINATE_MAX]; + struct access_coordinate pci_coord[ACCESS_COORDINATE_MAX] = { }; + struct access_coordinate sw_coord[ACCESS_COORDINATE_MAX] = { }; + struct access_coordinate ep_coord[ACCESS_COORDINATE_MAX] = { }; struct cxl_memdev *cxlmd = cxled_to_memdev(cxled); struct cxl_dev_state *cxlds = cxlmd->cxlds; struct pci_dev *pdev = to_pci_dev(cxlds->dev); @@ -757,7 +757,7 @@ static struct xarray *cxl_switch_gather_bandwidth(struct cxl_region *cxlr, { struct xarray *res_xa __free(free_perf_xa) = kzalloc_obj(*res_xa); - struct access_coordinate coords[ACCESS_COORDINATE_MAX]; + struct access_coordinate coords[ACCESS_COORDINATE_MAX] = { }; struct cxl_perf_ctx *ctx, *us_ctx; unsigned long index, us_index; int dev_count = 0; From 58b2b1ef28024093063de536c05aaea792226e24 Mon Sep 17 00:00:00 2001 From: Guixin Liu Date: Mon, 31 Aug 2026 17:22:16 +0800 Subject: [PATCH 22/49] cxl/port: Fix uninitialized coordinates reported for RCDs cxl_endpoint_get_perf_coordinates() returns success for a Restricted CXL Device without calculating coordinates, so the caller's output array is left uninitialized. Callers treat it as valid and can expose the stack residue as access coordinates. Initialize the coordinates to zero before returning for an RCD. Zeroing in the helper rather than at the caller keeps the @coord output contract the exported function documents. Reported by the Sashiko review bot. Fixes: 5d211c709059 ("cxl: Fix cxl_endpoint_get_perf_coordinate() support for RCH") Signed-off-by: Guixin Liu Reviewed-by: Richard Cheng Reviewed-by: Jonathan Cameron Link: https://patch.msgid.link/20260831092216.540644-3-kanie@linux.alibaba.com Signed-off-by: Dave Jiang --- drivers/cxl/core/port.c | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/drivers/cxl/core/port.c b/drivers/cxl/core/port.c index 625e4aa427db..fea43a92744c 100644 --- a/drivers/cxl/core/port.c +++ b/drivers/cxl/core/port.c @@ -2394,8 +2394,10 @@ int cxl_endpoint_get_perf_coordinates(struct cxl_port *port, * Skip calculation for RCD. Expectation is HMAT already covers RCD case * since RCH does not support hotplug. */ - if (cxlmd->cxlds->rcd) + if (cxlmd->cxlds->rcd) { + memset(coord, 0, sizeof(*coord) * ACCESS_COORDINATE_MAX); return 0; + } /* * Exit the loop when the parent port of the current iter port is cxl From 0de71a1ab211694be7e88d56fc35c9bf72078ad1 Mon Sep 17 00:00:00 2001 From: Guixin Liu Date: Mon, 14 Sep 2026 20:18:43 +0800 Subject: [PATCH 23/49] cxl/core: Fix dport use-after-free via the einj_inject debugfs file The per-dport einj_inject debugfs file retains a pointer to the 'struct cxl_dport', but its lifetime is not tied to the dport. Unbinding the dport host frees the dport and leaves einj_inject behind, so writing the file dereferences freed memory. The stale directory also prevents recreation on rebind. Remove the per-dport debugfs directory when the dport host is released. Found by code inspection, then reproduced on a QEMU CXL topology with KASAN (the einj_cxl_is_initialized() guard had to be forced open, as QEMU emits no EINJ table). Unpatched, writing the leftover file after unbind gives a KASAN slab-use-after-free report in cxl_einj_inject(), and rebind hits "already exists in 'cxl'". Patched, the directory goes away with the dport and rebind recreates a working einj_inject. Fixes: 8039804cfa73 ("cxl/core: Add CXL EINJ debugfs files") Signed-off-by: Guixin Liu Reviewed-by: Richard Cheng Reviewed-by: Li Ming Reviewed-by: Alison Schofield Link: https://patch.msgid.link/20260914121843.718493-1-kanie@linux.alibaba.com Signed-off-by: Dave Jiang --- drivers/cxl/core/port.c | 7 +++++++ 1 file changed, 7 insertions(+) diff --git a/drivers/cxl/core/port.c b/drivers/cxl/core/port.c index fea43a92744c..8d715739995b 100644 --- a/drivers/cxl/core/port.c +++ b/drivers/cxl/core/port.c @@ -814,6 +814,11 @@ static int cxl_einj_inject(void *data, u64 type) DEFINE_DEBUGFS_ATTRIBUTE(cxl_einj_inject_fops, NULL, cxl_einj_inject, "0x%llx\n"); +static void remove_debugfs(void *dentry) +{ + debugfs_remove_recursive(dentry); +} + static void cxl_debugfs_create_dport_dir(struct cxl_dport *dport) { struct cxl_port *parent = parent_port_of(dport->port); @@ -832,6 +837,8 @@ static void cxl_debugfs_create_dport_dir(struct cxl_dport *dport) dir = cxl_debugfs_create_dir(dev_name(dport->dport_dev)); + devm_add_action_or_reset(dport_to_host(dport), remove_debugfs, dir); + debugfs_create_file("einj_inject", 0200, dir, dport, &cxl_einj_inject_fops); } From bb26797c08d408ea7e8d3625045f0fe28714a425 Mon Sep 17 00:00:00 2001 From: Guixin Liu Date: Mon, 31 Aug 2026 14:11:20 +0800 Subject: [PATCH 24/49] cxl/region: Unregister the pmem region when the bridge is unbound Removing cxl_acpi unbinds the nvdimm bridge before the regions it serves. A region probe can therefore register its cxl_pmem_region and only then find the bridge unbound, which leaves the new device with nothing to remove it. The orphan outlives the teardown and pins its region and its memdevs, so a later cxl_acpi bind renumbers the root port, ports, endpoints, decoders and the memdev. Unregister the cxl_pmem_region when the bridge is unbound. Found by code inspection. Racing a cxl_region bind against a cxl_acpi unbind leaked a pmem_region device on 12 of 12 attempts, and none with this patch. Fixes: f17b558d6663 ("cxl/pmem: Refactor nvdimm device registration, delete the workqueue") Signed-off-by: Guixin Liu Reviewed-by: Richard Cheng Reviewed-by: Li Ming Reviewed-by: Dave Jiang Link: https://patch.msgid.link/20260831061120.200790-1-kanie@linux.alibaba.com Signed-off-by: Dave Jiang --- drivers/cxl/core/region_pmem.c | 6 ++++-- 1 file changed, 4 insertions(+), 2 deletions(-) diff --git a/drivers/cxl/core/region_pmem.c b/drivers/cxl/core/region_pmem.c index 23d97e3d78b6..7ab1373a95e0 100644 --- a/drivers/cxl/core/region_pmem.c +++ b/drivers/cxl/core/region_pmem.c @@ -168,12 +168,14 @@ int devm_cxl_add_pmem_region(struct cxl_region *cxlr) dev_name(dev)); scoped_guard(device, &cxl_nvb->dev) { - if (cxl_nvb->dev.driver) + if (cxl_nvb->dev.driver) { rc = devm_add_action_or_reset(&cxl_nvb->dev, cxlr_pmem_unregister, cxlr_pmem); - else + } else { rc = -ENXIO; + cxlr_pmem_unregister(cxlr_pmem); + } } if (rc) From ef71014055f7b4959e4cb023ca8a18258cb5641c Mon Sep 17 00:00:00 2001 From: Guixin Liu Date: Thu, 17 Sep 2026 10:46:32 +0800 Subject: [PATCH 25/49] cxl/region: Create node access attributes for CFMWS-only NUMA nodes A NUMA node that only backs a CXL fixed memory window and is not enumerated by SRAT has no memory_target in the HMAT code, so hmat_callback() exits early at find_mem_target() and never creates the node's accessN attributes: /sys/devices/system/node/nodeX/accessN/initiators/* stays missing. Commit debdce20c4f2 ("cxl/region: Deal with numa nodes not enumerated by SRAT") had the CXL region notifier call node_set_perf_attrs() directly for such nodes, but commit 2e454fb8056d ("cxl, acpi/hmat: Update CXL access coordinates directly instead of through HMAT") replaced it with node_update_perf_attrs(), which only updates attributes that already exist and thus does nothing for these nodes. Create the attributes with node_set_perf_attrs() when the node is not backed by a real SRAT pxm. Tested on a QEMU CXL topology with a CFMWS window not described by SRAT or HMAT: node1/access0/initiators/{read,write}_{bandwidth,latency} appear with the calculated coordinates once the region's first memory block comes online. Fixes: 2e454fb8056d ("cxl, acpi/hmat: Update CXL access coordinates directly instead of through HMAT") Reviewed-by: Jonathan Cameron Signed-off-by: Guixin Liu Reviewed-by: Gregory Price (Meta) Reviewed-by: Alison Schofield Link: https://patch.msgid.link/20260917024632.1397380-1-kanie@linux.alibaba.com Signed-off-by: Dave Jiang --- drivers/cxl/core/region.c | 15 ++++++++++++++- 1 file changed, 14 insertions(+), 1 deletion(-) diff --git a/drivers/cxl/core/region.c b/drivers/cxl/core/region.c index 27e63e6dab7c..9a5fd3da539d 100644 --- a/drivers/cxl/core/region.c +++ b/drivers/cxl/core/region.c @@ -1,5 +1,6 @@ // SPDX-License-Identifier: GPL-2.0-only /* Copyright(c) 2022 Intel Corporation. All rights reserved. */ +#include #include #include #include @@ -2630,7 +2631,19 @@ static bool cxl_region_update_coordinates(struct cxl_region *cxlr, int nid) for (int i = 0; i < ACCESS_COORDINATE_MAX; i++) { if (cxlr->coord[i].read_bandwidth) { - node_update_perf_attrs(nid, &cxlr->coord[i], i); + /* + * A CFMWS-only node (not backed by a real SRAT pxm) + * has no HMAT memory_target, so hmat_callback() + * never created the access attributes for it: + * create them here instead of only updating them. + * This is called at most once per node (see + * nodemask_region_seen), so the attributes are not + * created twice. + */ + if (!acpi_node_backed_by_real_pxm(nid)) + node_set_perf_attrs(nid, &cxlr->coord[i], i); + else + node_update_perf_attrs(nid, &cxlr->coord[i], i); cset++; } } From a8c458611a7daf67ffd9088088aeb28626135a92 Mon Sep 17 00:00:00 2001 From: Shaikh Kamaluddin Date: Sat, 12 Sep 2026 15:08:15 +0530 Subject: [PATCH 26/49] cxl/events: Return IRQ_NONE when no event is pending CXL event interrupts may share an MSI/MSI-X vector with other event logs or device features. Consequently, cxl_event_thread() is registered with IRQF_SHARED and must determine whether the event-log facility claimed the interrupt. The handler masks the Device Event Status register to the event logs supported by the driver. However, when no supported status bit is set, it exits the processing loop and still returns IRQ_HANDLED. This incorrectly reports that the interrupt was claimed and prevents the generic spurious-interrupt detector from accounting it as unhandled. Track whether the CXL event handler claimed the interrupt and return IRQ_NONE when no supported event is detected. If no handler sharing the vector claims a sustained interrupt storm, the generic spurious- interrupt detector can eventually identify and disable the affected IRQ. Signed-off-by: Shaikh Kamaluddin Reviewed-by: Alison Schofield Reviewed-by: Jonathan Cameron Reviewed-by: Anisa Su Reviewed-by: Li Ming Link: https://patch.msgid.link/20260912093815.14455-1-shaikhkamal2012@gmail.com Signed-off-by: Dave Jiang --- drivers/cxl/pci.c | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/drivers/cxl/pci.c b/drivers/cxl/pci.c index c7c91e8dc51d..8b560cae91f2 100644 --- a/drivers/cxl/pci.c +++ b/drivers/cxl/pci.c @@ -515,6 +515,7 @@ static irqreturn_t cxl_event_thread(int irq, void *id) struct cxl_dev_id *dev_id = id; struct cxl_dev_state *cxlds = dev_id->cxlds; struct cxl_memdev_state *mds = to_cxl_memdev_state(cxlds); + bool handled = false; u32 status; do { @@ -527,11 +528,13 @@ static irqreturn_t cxl_event_thread(int irq, void *id) status &= CXLDEV_EVENT_STATUS_ALL; if (!status) break; + + handled = true; cxl_mem_get_event_records(mds, status); cond_resched(); } while (status); - return IRQ_HANDLED; + return handled ? IRQ_HANDLED : IRQ_NONE; } static int cxl_event_req_irq(struct cxl_dev_state *cxlds, u8 setting) From b25af6afd2f26b455108bab03dfeba52378e8dd2 Mon Sep 17 00:00:00 2001 From: Guixin Liu Date: Tue, 15 Sep 2026 14:01:38 +0800 Subject: [PATCH 27/49] cxl/core: Skip einj_inject creation when devm_add_action_or_reset() fails cxl_debugfs_create_dport_dir() ignores the devm_add_action_or_reset() result and creates the einj_inject file below @dir unconditionally. When the devres allocation fails, remove_debugfs() has already run before devm_add_action_or_reset() returns, i.e. debugfs_remove() has released @dir. The subsequent debugfs_create_file() would then use that dentry as its parent although its final reference was dropped in the removal and its memory is subject to call_rcu() delayed freeing. Skip the file creation in that case, consistent with how the rest of the helper tolerates debugfs failures. Fixes: 9185b1a3043c ("cxl/core: Fix dport use-after-free via the einj_inject debugfs file") Signed-off-by: Guixin Liu Reviewed-by: Li Ming Reviewed-by: Jonathan Cameron Link: https://patch.msgid.link/20260915060138.2007974-1-kanie@linux.alibaba.com Signed-off-by: Dave Jiang --- drivers/cxl/core/port.c | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/drivers/cxl/core/port.c b/drivers/cxl/core/port.c index 8d715739995b..95c0be57ed53 100644 --- a/drivers/cxl/core/port.c +++ b/drivers/cxl/core/port.c @@ -837,7 +837,8 @@ static void cxl_debugfs_create_dport_dir(struct cxl_dport *dport) dir = cxl_debugfs_create_dir(dev_name(dport->dport_dev)); - devm_add_action_or_reset(dport_to_host(dport), remove_debugfs, dir); + if (devm_add_action_or_reset(dport_to_host(dport), remove_debugfs, dir)) + return; debugfs_create_file("einj_inject", 0200, dir, dport, &cxl_einj_inject_fops); From 18f26958544783b0f30210c41fc0c0da11b8d9a4 Mon Sep 17 00:00:00 2001 From: Guixin Liu Date: Wed, 16 Sep 2026 10:54:52 +0800 Subject: [PATCH 28/49] driver core: Add conditional guard support for device_trylock() Introduce a conditional guard version of device_trylock() for scenarios that must not block on the device lock. device_trylock() returns 1 on success like mutex_trylock(), so the default binary condition applies and ACQUIRE_ERR() reports -EBUSY on contention. Suggested-by: Shaikh Kamaluddin Reviewed-by: Jonathan Cameron Signed-off-by: Guixin Liu Acked-by: Greg Kroah-Hartman Reviewed-by: Dave Jiang Link: https://patch.msgid.link/20260916025453.3532614-2-kanie@linux.alibaba.com Signed-off-by: Dave Jiang --- include/linux/device.h | 1 + 1 file changed, 1 insertion(+) diff --git a/include/linux/device.h b/include/linux/device.h index aee79fd6b32b..a8d6cf76d137 100644 --- a/include/linux/device.h +++ b/include/linux/device.h @@ -1121,6 +1121,7 @@ static inline void device_unlock(struct device *dev) DEFINE_GUARD(device, struct device *, device_lock(_T), device_unlock(_T)) DEFINE_GUARD_COND(device, _intr, device_lock_interruptible(_T), _RET == 0) +DEFINE_GUARD_COND(device, _try, device_trylock(_T)) static inline void device_lock_assert(struct device *dev) { From 391bc7ab9be6635e77a205591b7d30db797f1a0e Mon Sep 17 00:00:00 2001 From: Guixin Liu Date: Wed, 16 Sep 2026 10:54:53 +0800 Subject: [PATCH 29/49] cxl/memdev: Fix deadlock between poison debugfs and cxl_mem unbind The poison debugfs handlers take the memdev device lock so that the region lookup sees a stable cxlmd->dev.driver. debugfs holds a reference on the file across the handler, and cxl_mem unbind removes that file while holding the very same device lock, so a handler that waits for the lock deadlocks against a concurrent unbind. The trigger is any teardown that detaches cxl_mem from the memdev while a poison write is in flight: unbinding or unloading cxl_mem or cxl_pci, or removing the endpoint PCI device, including hot-remove. Both tasks then hang. The unbind side is uninterruptible, and it also blocks the memdev detach work, which runs on an ordered workqueue and so stalls every other CXL bus work item. Take the lock with the trylock guard and return -EBUSY instead of waiting. An unbind that wins the race removes the file first and the write fails with -ENOENT. The poison inject and clear files are a debug ABI for expert users, mostly device vendors, to test the poison capabilities of their devices. Mixing a poison write with cxl_mem teardown is not a supported use of the interface, but when it happens anyway the cost should be a failed write, not a hung kernel. Found by code inspection. Reproduced by writing inject_poison in a loop while unbinding and rebinding cxl_mem, and confirmed fixed by the same test. Also reproduced the same deadlock with those writes racing a pciehp hot-remove of the memdev; with this patch the hot-remove flow completes normally. Fixes: 574eda81d0a7 ("cxl/memdev: Hold memdev lock during memdev poison injection/clear") Suggested-by: Shaikh Kamaluddin Reviewed-by: Jonathan Cameron Signed-off-by: Guixin Liu Reviewed-by: Dave Jiang Link: https://patch.msgid.link/20260916025453.3532614-3-kanie@linux.alibaba.com Signed-off-by: Dave Jiang --- drivers/cxl/mem.c | 14 ++++++++++---- 1 file changed, 10 insertions(+), 4 deletions(-) diff --git a/drivers/cxl/mem.c b/drivers/cxl/mem.c index 798e5c369cfc..3959ec963026 100644 --- a/drivers/cxl/mem.c +++ b/drivers/cxl/mem.c @@ -50,8 +50,13 @@ static int cxl_debugfs_poison_inject(void *data, u64 dpa) struct cxl_memdev *cxlmd = data; int rc; - ACQUIRE(device_intr, devlock)(&cxlmd->dev); - if ((rc = ACQUIRE_ERR(device_intr, &devlock))) + /* + * Never wait for this lock: the debugfs proxy holds a file reference + * across the callback and unbind removes the file under the same + * device lock, so waiting here deadlocks against unbind. + */ + ACQUIRE(device_try, devlock)(&cxlmd->dev); + if ((rc = ACQUIRE_ERR(device_try, &devlock))) return rc; return cxl_inject_poison(cxlmd, dpa); @@ -65,8 +70,9 @@ static int cxl_debugfs_poison_clear(void *data, u64 dpa) struct cxl_memdev *cxlmd = data; int rc; - ACQUIRE(device_intr, devlock)(&cxlmd->dev); - if ((rc = ACQUIRE_ERR(device_intr, &devlock))) + /* Never wait, per the inject path above. */ + ACQUIRE(device_try, devlock)(&cxlmd->dev); + if ((rc = ACQUIRE_ERR(device_try, &devlock))) return rc; return cxl_clear_poison(cxlmd, dpa); From 371c2ad1624283b6c7a62a28199f5161d388a738 Mon Sep 17 00:00:00 2001 From: Jiale Yao Date: Sat, 26 Sep 2026 15:16:05 +0800 Subject: [PATCH 30/49] cxl/acpi: Check ACPI companion before use Platform drivers can be forced to match devices outside their ID tables through driver_override. cxl_acpi_probe() assumes that every bound device has an ACPI companion and dereferences adev->dev.bus without checking the result of ACPI_COMPANION(). Force-binding cxl_acpi to a platform device without a companion therefore causes a NULL pointer dereference. This was reproduced by setting the driver override for the pcspkr platform device to cxl_acpi and binding it through sysfs: BUG: kernel NULL pointer dereference, address: 0000000000000280 #PF: supervisor read access in kernel mode RIP: cxl_acpi_probe+0xf4/0x220 Call Trace: platform_probe+0x4d/0x80 really_probe+0x106/0x370 device_driver_attach+0x4c/0xa0 bind_store+0xd0/0x100 Commit 2b3a5dabe89e ("platform/surface: acpi-notify: Check ACPI companion before use") fixed the same force-binding issue in another platform driver. Check the companion before setting up the CXL root and return -ENODEV when it is absent. Fixes: 7d4b5ca2e2cb ("cxl/acpi: Add downstream port data to cxl_port instances") Cc: stable@vger.kernel.org Signed-off-by: Jiale Yao Reviewed-by: Jonathan Cameron Link: https://patch.msgid.link/20260926071605.3013893-1-yaojiale02@163.com Signed-off-by: Dave Jiang --- drivers/cxl/acpi.c | 10 +++++++--- 1 file changed, 7 insertions(+), 3 deletions(-) diff --git a/drivers/cxl/acpi.c b/drivers/cxl/acpi.c index 3b818adbd38b..fb09a5ff48c1 100644 --- a/drivers/cxl/acpi.c +++ b/drivers/cxl/acpi.c @@ -885,13 +885,17 @@ static int pair_cxl_resource(struct device *dev, void *data) static int cxl_acpi_probe(struct platform_device *pdev) { - int rc; + struct cxl_cfmws_context ctx; + struct acpi_device *adev; struct resource *cxl_res; struct cxl_root *cxl_root; struct cxl_port *root_port; struct device *host = &pdev->dev; - struct acpi_device *adev = ACPI_COMPANION(host); - struct cxl_cfmws_context ctx; + int rc; + + adev = ACPI_COMPANION(host); + if (!adev) + return -ENODEV; device_lock_set_class(&pdev->dev, &cxl_root_key); rc = devm_add_action_or_reset(&pdev->dev, cxl_acpi_lock_reset_class, From 682d3772be807d1269fc0df071b93e86374f1d18 Mon Sep 17 00:00:00 2001 From: Alison Schofield Date: Thu, 3 Sep 2026 16:23:44 -0700 Subject: [PATCH 31/49] cxl/region: Warn on user region position mismatch User region creation includes a self-test that checks the assigned endpoint positions against the position calculation used by auto region creation. The result, matching or not, is reported with dev_dbg(). Promote that to a dev_warn() that fires only on a mismatch, so bugs in the auto region position calculation are easier to catch as that calculation is extended to support more region configurations. The matching case no longer prints, so dynamic debug on region.c no longer confirms that the self-test ran. Signed-off-by: Alison Schofield Reviewed-by: Jonathan Cameron Reviewed-by: Robert Richter Link: https://patch.msgid.link/1d662e3ea78ec8b05fceb500f04e69ee9f6d4a72.1788475206.git.alison.schofield@intel.com Signed-off-by: Dave Jiang --- drivers/cxl/core/region.c | 9 +++++---- 1 file changed, 5 insertions(+), 4 deletions(-) diff --git a/drivers/cxl/core/region.c b/drivers/cxl/core/region.c index 4f6069451fed..0f6a32e83601 100644 --- a/drivers/cxl/core/region.c +++ b/drivers/cxl/core/region.c @@ -2216,10 +2216,11 @@ static int cxl_region_attach(struct cxl_region *cxlr, int test_pos; test_pos = cxl_calc_interleave_pos(target, &cxlr->hpa_range); - dev_dbg(&target->cxld.dev, - "Test cxl_calc_interleave_pos(): %s test_pos:%d target->pos:%d\n", - (test_pos == target->pos) ? "success" : "fail", - test_pos, target->pos); + if (test_pos != target->pos) + dev_warn(&cxlr->dev, + "%s: position mismatch: calculated:%d assigned:%d\n", + dev_name(&target->cxld.dev), test_pos, + target->pos); } return 0; From 019560d23450a320f4060ff235563a5277ea8033 Mon Sep 17 00:00:00 2001 From: Dave Jiang Date: Thu, 1 Oct 2026 08:47:42 -0700 Subject: [PATCH 32/49] cxl/port: Clear cached dport pointers when a dport is removed Switch decoders cache dport pointers in cxlsd->target[], and nothing clears them when a dport is freed. Readers then dereference freed memory. KASAN caught it under a cxl_test load/unload loop with concurrent sysfs reads (abbreviated). Clear the matching slots from cxl_dport_remove(), walking the port's decoders the way cxl_port_update_decoder_targets() does on the add side. Scan all nr_targets slots, the target[] allocation size, and clear every hit. Fixes: 8330671c57c7 ("cxl: Add helper to delete dport") Cc: stable@vger.kernel.org Assisted-by: LLM Reviewed-by: Jonathan Cameron Reviewed-by: Li Ming Reviewed-by: Gregory Price (Meta) Reviewed-by: Alison Schofield Link: https://patch.msgid.link/20261001154744.1095902-2-dave.jiang@intel.com Signed-off-by: Dave Jiang --- drivers/cxl/core/port.c | 29 +++++++++++++++++++++++++++++ 1 file changed, 29 insertions(+) diff --git a/drivers/cxl/core/port.c b/drivers/cxl/core/port.c index 95c0be57ed53..ecad766737aa 100644 --- a/drivers/cxl/core/port.c +++ b/drivers/cxl/core/port.c @@ -1097,11 +1097,40 @@ static void cond_cxl_root_unlock(struct cxl_port *port) device_unlock(&port->dev); } +static int clear_decoder_target(struct device *dev, void *data) +{ + struct cxl_dport *dport = data; + struct cxl_switch_decoder *cxlsd; + + if (!is_switch_decoder(dev)) + return 0; + + cxlsd = to_cxl_switch_decoder(dev); + guard(rwsem_write)(&cxl_rwsem.region); + + /* + * A dport can occupy more than one position of an interleave, so + * scan the whole target list rather than stopping at the first hit. + */ + for (int i = 0; i < cxlsd->nr_targets; i++) { + if (cxlsd->target[i] != dport) + continue; + cxlsd->target[i] = NULL; + dev_dbg(dev, "dport%d removed from target list, index %d\n", + dport->port_id, i); + } + + return 0; +} + static void cxl_dport_remove(void *data) { struct cxl_dport *dport = data; struct cxl_port *port = dport->port; + /* counterpart of cxl_port_update_decoder_targets() */ + device_for_each_child(&port->dev, dport, clear_decoder_target); + port->nr_dports--; xa_erase(&port->dports, (unsigned long) dport->dport_dev); put_device(dport->dport_dev); From b72a03ebd9586d59992ecaa6ceed11ea96835734 Mon Sep 17 00:00:00 2001 From: Dave Jiang Date: Thu, 1 Oct 2026 08:47:43 -0700 Subject: [PATCH 33/49] cxl/core: Hold the dport host lock across dport lookup and use Callers of cxl_pci_find_port() and cxl_mem_find_port() can be handed a 'struct cxl_dport' that is already freed, and all four dereference it without the lock that would prevent that. A dport is kzalloc()'d into a devres group on its host device, so it dies when that host unbinds. The host is &port->dev for a switch port and port->uport_dev for the root port. Both helpers return the dport through an output parameter. The caller's port reference pins struct cxl_port, not the dport allocation, and the lookup itself is a bare xa_load() under no lock. Add cxl_port_dport_host() to name the device that pins a dport, and cxl_pci_find_dport() / cxl_mem_find_dport() to look one up with that device's lock held and asserted. Convert the four callers to use them and bail if the dport is gone. cxl_port_add_dport() already follows this rule. cxl_mem_probe() chose its endpoint devm host from dport->rch, which meant dereferencing the dport before taking any lock. Use cxl_port_dport_host(): rch dports only ever land on the root port, so it selects the same device without needing the dport. With every caller converted, the dport output parameter of cxl_pci_find_port() and cxl_mem_find_port() is unused. Drop it. Fixes: 733b57f262b0 ("cxl/pci: Early setup RCH dport component registers from RCRB") Assisted-by: LLM Reviewed-by: Li Ming Reviewed-by: Alison Schofield Link: https://patch.msgid.link/20261001154744.1095902-3-dave.jiang@intel.com Signed-off-by: Dave Jiang --- drivers/cxl/core/core.h | 6 +--- drivers/cxl/core/pci.c | 8 ++++- drivers/cxl/core/port.c | 62 ++++++++++++++++++++++++++------------ drivers/cxl/core/ras_rch.c | 12 +++++++- drivers/cxl/cxl.h | 22 +++++++++++--- drivers/cxl/mem.c | 14 ++++++--- drivers/cxl/pci.c | 11 ++++--- 7 files changed, 95 insertions(+), 40 deletions(-) diff --git a/drivers/cxl/core/core.h b/drivers/cxl/core/core.h index bb380ec6daeb..79918fde1575 100644 --- a/drivers/cxl/core/core.h +++ b/drivers/cxl/core/core.h @@ -178,11 +178,7 @@ static inline struct device *port_to_host(struct cxl_port *port) static inline struct device *dport_to_host(struct cxl_dport *dport) { - struct cxl_port *port = dport->port; - - if (is_cxl_root(port)) - return port->uport_dev; - return &port->dev; + return cxl_port_dport_host(dport->port); } #ifdef CONFIG_CXL_RAS void cxl_ras_init(void); diff --git a/drivers/cxl/core/pci.c b/drivers/cxl/core/pci.c index 43b9b7afff29..4af165a436d2 100644 --- a/drivers/cxl/core/pci.c +++ b/drivers/cxl/core/pci.c @@ -743,10 +743,16 @@ int cxl_pci_setup_regs(struct pci_dev *pdev, enum cxl_regloc_type type, if (rc && type == CXL_REGLOC_RBI_COMPONENT && is_cxl_restricted(pdev)) { struct cxl_dport *dport; struct cxl_port *port __free(put_cxl_port) = - cxl_pci_find_port(pdev, &dport); + cxl_pci_find_port(pdev); if (!port) return -EPROBE_DEFER; + guard(device)(cxl_port_dport_host(port)); + + dport = cxl_pci_find_dport(pdev, port); + if (!dport) + return -EPROBE_DEFER; + rc = cxl_rcrb_get_comp_regs(pdev, map, dport); if (rc) return rc; diff --git a/drivers/cxl/core/port.c b/drivers/cxl/core/port.c index ecad766737aa..979dffdccef0 100644 --- a/drivers/cxl/core/port.c +++ b/drivers/cxl/core/port.c @@ -1395,13 +1395,11 @@ static int cxl_add_ep(struct cxl_dport *dport, struct device *ep_dev) struct cxl_find_port_ctx { const struct device *dport_dev; const struct cxl_port *parent_port; - struct cxl_dport **dport; }; static int match_port_by_dport(struct device *dev, const void *data) { const struct cxl_find_port_ctx *ctx = data; - struct cxl_dport *dport; struct cxl_port *port; if (!is_cxl_port(dev)) @@ -1410,10 +1408,7 @@ static int match_port_by_dport(struct device *dev, const void *data) return 0; port = to_cxl_port(dev); - dport = cxl_find_dport_by_dev(port, ctx->dport_dev); - if (ctx->dport) - *ctx->dport = dport; - return dport != NULL; + return !!cxl_find_dport_by_dev(port, ctx->dport_dev); } static struct cxl_port *__find_cxl_port_by_dport(struct cxl_find_port_ctx *ctx) @@ -1432,22 +1427,21 @@ static struct cxl_port *__find_cxl_port_by_dport(struct cxl_find_port_ctx *ctx) /** * find_cxl_port_by_dport - find a cxl_port by one of its targets * @dport_dev: device representing the dport target - * @dport: optional output of the 'struct cxl_dport' companion of the @dport_dev * * Return a 'struct cxl_port' with an elevated reference if found. Use * __free(put_cxl_port) to release. + * + * The port reference does not pin the dport, which is a devm allocation of + * cxl_port_dport_host(). Use cxl_pci_find_dport() or cxl_mem_find_dport() + * under that device's lock to get it. */ -static struct cxl_port *find_cxl_port_by_dport(struct device *dport_dev, - struct cxl_dport **dport) +static struct cxl_port *find_cxl_port_by_dport(struct device *dport_dev) { struct cxl_find_port_ctx ctx = { .dport_dev = dport_dev, - .dport = dport, }; - struct cxl_port *port; - port = __find_cxl_port_by_dport(&ctx); - return port; + return __find_cxl_port_by_dport(&ctx); } /* @@ -1937,20 +1931,50 @@ int devm_cxl_enumerate_ports(struct cxl_memdev *cxlmd) } EXPORT_SYMBOL_NS_GPL(devm_cxl_enumerate_ports, "CXL"); -struct cxl_port *cxl_pci_find_port(struct pci_dev *pdev, - struct cxl_dport **dport) +struct cxl_port *cxl_pci_find_port(struct pci_dev *pdev) { - return find_cxl_port_by_dport(pdev->dev.parent, dport); + return find_cxl_port_by_dport(pdev->dev.parent); } EXPORT_SYMBOL_NS_GPL(cxl_pci_find_port, "CXL"); -struct cxl_port *cxl_mem_find_port(struct cxl_memdev *cxlmd, - struct cxl_dport **dport) +struct cxl_port *cxl_mem_find_port(struct cxl_memdev *cxlmd) { - return find_cxl_port_by_dport(grandparent(&cxlmd->dev), dport); + return find_cxl_port_by_dport(grandparent(&cxlmd->dev)); } EXPORT_SYMBOL_NS_GPL(cxl_mem_find_port, "CXL"); +/** + * cxl_pci_find_dport - find the dport of @port that @pdev is below + * @pdev: PCI device below the dport + * @port: port to search, typically from cxl_pci_find_port() + * + * Context: Caller must hold the cxl_port_dport_host() lock of @port, and + * must not use the result after dropping it. + */ +struct cxl_dport *cxl_pci_find_dport(struct pci_dev *pdev, + struct cxl_port *port) +{ + device_lock_assert(cxl_port_dport_host(port)); + return cxl_find_dport_by_dev(port, pdev->dev.parent); +} +EXPORT_SYMBOL_NS_GPL(cxl_pci_find_dport, "CXL"); + +/** + * cxl_mem_find_dport - find the dport of @port that @cxlmd is below + * @cxlmd: memdev below the dport + * @port: port to search, typically from cxl_mem_find_port() + * + * Context: Caller must hold the cxl_port_dport_host() lock of @port, and + * must not use the result after dropping it. + */ +struct cxl_dport *cxl_mem_find_dport(struct cxl_memdev *cxlmd, + struct cxl_port *port) +{ + device_lock_assert(cxl_port_dport_host(port)); + return cxl_find_dport_by_dev(port, grandparent(&cxlmd->dev)); +} +EXPORT_SYMBOL_NS_GPL(cxl_mem_find_dport, "CXL"); + static int decoder_populate_targets(struct cxl_switch_decoder *cxlsd, struct cxl_port *port) { diff --git a/drivers/cxl/core/ras_rch.c b/drivers/cxl/core/ras_rch.c index e0e01aa5eba6..3c6aae26deed 100644 --- a/drivers/cxl/core/ras_rch.c +++ b/drivers/cxl/core/ras_rch.c @@ -118,10 +118,20 @@ void cxl_handle_rdport_errors(struct cxl_dev_state *cxlds) int severity; struct cxl_port *port __free(put_cxl_port) = - cxl_pci_find_port(pdev, &dport); + cxl_pci_find_port(pdev); if (!port) return; + /* + * The dport may have been freed since the lookup above, so take the + * lock that pins it and find it again. + */ + guard(device)(cxl_port_dport_host(port)); + + dport = cxl_pci_find_dport(pdev, port); + if (!dport) + return; + if (!cxl_rch_get_aer_info(dport->regs.dport_aer, &aer_regs)) return; diff --git a/drivers/cxl/cxl.h b/drivers/cxl/cxl.h index cab8ce39f465..cd636d014872 100644 --- a/drivers/cxl/cxl.h +++ b/drivers/cxl/cxl.h @@ -701,6 +701,18 @@ static inline bool is_cxl_root(struct cxl_port *port) return port->uport_dev == port->dev.parent; } +/* + * Return the owner device of the devm allocations of @port's dports. They + * are freed when that device unbinds, so hold device_lock() on the returned + * device across any lookup and use of a 'struct cxl_dport'. + */ +static inline struct device *cxl_port_dport_host(struct cxl_port *port) +{ + if (is_cxl_root(port)) + return port->uport_dev; + return &port->dev; +} + /* Address translation functions exported to cxl_translate test module only */ int cxl_validate_translation_params(u8 eiw, u16 eig, int pos); u64 cxl_calculate_hpa_offset(u64 dpa_offset, int pos, u8 eiw, u16 eig); @@ -747,10 +759,12 @@ DEFINE_FREE(put_cxl_dax_region, struct cxl_dax_region *, if (!IS_ERR_OR_NULL(_T) int devm_cxl_enumerate_ports(struct cxl_memdev *cxlmd); void cxl_bus_rescan(void); void cxl_bus_drain(void); -struct cxl_port *cxl_pci_find_port(struct pci_dev *pdev, - struct cxl_dport **dport); -struct cxl_port *cxl_mem_find_port(struct cxl_memdev *cxlmd, - struct cxl_dport **dport); +struct cxl_dport *cxl_pci_find_dport(struct pci_dev *pdev, + struct cxl_port *port); +struct cxl_dport *cxl_mem_find_dport(struct cxl_memdev *cxlmd, + struct cxl_port *port); +struct cxl_port *cxl_pci_find_port(struct pci_dev *pdev); +struct cxl_port *cxl_mem_find_port(struct cxl_memdev *cxlmd); bool schedule_cxl_memdev_detach(struct cxl_memdev *cxlmd); struct cxl_dport *devm_cxl_add_dport(struct cxl_port *port, diff --git a/drivers/cxl/mem.c b/drivers/cxl/mem.c index 3959ec963026..493c4071245d 100644 --- a/drivers/cxl/mem.c +++ b/drivers/cxl/mem.c @@ -139,7 +139,7 @@ static int cxl_mem_probe(struct device *dev) return rc; struct cxl_port *parent_port __free(put_cxl_port) = - cxl_mem_find_port(cxlmd, &dport); + cxl_mem_find_port(cxlmd); if (!parent_port) { dev_err(dev, "CXL port topology not found\n"); return -ENXIO; @@ -154,10 +154,7 @@ static int cxl_mem_probe(struct device *dev) } } - if (dport->rch) - endpoint_parent = parent_port->uport_dev; - else - endpoint_parent = &parent_port->dev; + endpoint_parent = cxl_port_dport_host(parent_port); scoped_guard(device, endpoint_parent) { if (!endpoint_parent->driver) { @@ -166,6 +163,13 @@ static int cxl_mem_probe(struct device *dev) return -ENXIO; } + /* @dport is only valid while @endpoint_parent stays bound */ + dport = cxl_mem_find_dport(cxlmd, parent_port); + if (!dport) { + dev_err(dev, "CXL topology dport not found\n"); + return -ENXIO; + } + rc = devm_cxl_add_endpoint(endpoint_parent, cxlmd, dport); if (rc) return rc; diff --git a/drivers/cxl/pci.c b/drivers/cxl/pci.c index 8b560cae91f2..44ae1a20bbbf 100644 --- a/drivers/cxl/pci.c +++ b/drivers/cxl/pci.c @@ -709,22 +709,23 @@ static ssize_t rcd_pcie_cap_emit(struct device *dev, u16 offset, char *buf, size struct device *root_dev; struct cxl_dport *dport; struct cxl_port *root __free(put_cxl_port) = - cxl_mem_find_port(cxlmd, &dport); + cxl_mem_find_port(cxlmd); if (!root) return -ENXIO; - root_dev = root->uport_dev; + root_dev = cxl_port_dport_host(root); if (!root_dev) return -ENXIO; - if (!dport->regs.rcd_pcie_cap) - return -ENXIO; - guard(device)(root_dev); if (!root_dev->driver) return -ENXIO; + dport = cxl_mem_find_dport(cxlmd, root); + if (!dport || !dport->regs.rcd_pcie_cap) + return -ENXIO; + switch (width) { case 2: return sysfs_emit(buf, "%#x\n", From bcda00c78d075340fe30cb6c980507fac711e7d1 Mon Sep 17 00:00:00 2001 From: Dave Jiang Date: Thu, 1 Oct 2026 08:47:44 -0700 Subject: [PATCH 34/49] cxl/port: Attach endpoints to a dport under the lock that pins it devm_cxl_enumerate_ports() and add_port_attach_ep() look up or create a dport under its port's device lock, drop the lock, then pass the dport to cxl_add_ep(). add_ep() reads dport->port before taking the lock, and cxl_gpf_port_setup() reads and writes the dport with no lock held. A concurrent cxl_detach_ep() for another memdev below the same port can free the dport in that window: it calls del_dports() under the port lock when the last endpoint leaves. The enumerating memdev then dereferences freed memory. Attach the endpoint and set up GPF while the port lock from the lookup is still held. Make add_ep() assert that lock instead of taking it. Under the lock, a dport found in port->dports is still live, since removal erases it from the xarray before freeing it. [ dj: Added kdoc update from Alison. ] Fixes: de516b40116e ("cxl/port: Record dport in endpoint references") Reported-by: Li Ming Closes: https://lore.kernel.org/linux-cxl/0c68324c-cfc7-46b2-9344-24ae40e88923@zohomail.com/ Assisted-by: LLM Reviewed-by: Li Ming Reviewed-by: Alison Schofield Link: https://patch.msgid.link/20261001154744.1095902-4-dave.jiang@intel.com Signed-off-by: Dave Jiang --- drivers/cxl/core/port.c | 117 ++++++++++++++++++++++------------------ 1 file changed, 64 insertions(+), 53 deletions(-) diff --git a/drivers/cxl/core/port.c b/drivers/cxl/core/port.c index 979dffdccef0..a7bfe320c802 100644 --- a/drivers/cxl/core/port.c +++ b/drivers/cxl/core/port.c @@ -1357,7 +1357,7 @@ static int add_ep(struct cxl_ep *new) { struct cxl_port *port = new->dport->port; - guard(device)(&port->dev); + device_lock_assert(&port->dev); if (port->dead) return -ENXIO; @@ -1373,6 +1373,11 @@ static int add_ep(struct cxl_ep *new) * Intermediate CXL ports are scanned based on the arrival of endpoints. * When those endpoints depart the port can be destroyed once all * endpoints that care about that port have been removed. + * + * Context: Caller must hold the device lock of @dport->port, which pins + * @dport, across the lookup of @dport and this call. @dport->port + * is never the CXL root port, whose dports are pinned by its + * uport_dev instead. */ static int cxl_add_ep(struct cxl_dport *dport, struct device *ep_dev) { @@ -1703,13 +1708,15 @@ static struct cxl_dport *probe_dport(struct cxl_port *port, return drv->add_dport(port, dport_dev); } -static struct cxl_dport *devm_cxl_create_port(struct device *ep_dev, - struct cxl_port *parent_port, - struct cxl_dport *parent_dport, - struct device *uport_dev, - struct device *dport_dev) +static int devm_cxl_create_port(struct device *ep_dev, + struct cxl_port *parent_port, + struct cxl_dport *parent_dport, + struct device *uport_dev, + struct device *dport_dev) { resource_size_t component_reg_phys; + struct cxl_dport *dport; + int rc; device_lock_assert(&parent_port->dev); if (!parent_port->dev.driver) { @@ -1726,7 +1733,7 @@ static struct cxl_dport *devm_cxl_create_port(struct device *ep_dev, port = devm_cxl_add_port(&parent_port->dev, uport_dev, component_reg_phys, parent_dport); if (IS_ERR(port)) - return ERR_CAST(port); + return PTR_ERR(port); /* * retry to make sure a port is found. a port device @@ -1734,7 +1741,7 @@ static struct cxl_dport *devm_cxl_create_port(struct device *ep_dev, */ port = find_cxl_port_by_uport(uport_dev); if (!port) - return ERR_PTR(-ENODEV); + return -ENODEV; dev_dbg(ep_dev, "created port %s:%s\n", dev_name(&port->dev), dev_name(port->uport_dev)); @@ -1743,11 +1750,25 @@ static struct cxl_dport *devm_cxl_create_port(struct device *ep_dev, * Port was created before right before this function is * called. Signal the caller to deal with it. */ - return ERR_PTR(-EAGAIN); + return -EAGAIN; } + /* @dport is only valid while @port's lock is held */ guard(device)(&port->dev); - return probe_dport(port, dport_dev); + dport = probe_dport(port, dport_dev); + if (IS_ERR(dport)) + return PTR_ERR(dport); + + rc = cxl_add_ep(dport, ep_dev); + if (rc == -EBUSY) { + /* + * "can't" happen, but this error code means + * something to the caller, so translate it. + */ + rc = -ENXIO; + } + + return rc; } static int add_port_attach_ep(struct cxl_memdev *cxlmd, @@ -1755,7 +1776,7 @@ static int add_port_attach_ep(struct cxl_memdev *cxlmd, struct device *dport_dev) { struct device *dparent = grandparent(dport_dev); - struct cxl_dport *dport, *parent_dport; + struct cxl_dport *parent_dport; int rc; if (is_cxl_host_bridge(dparent)) { @@ -1784,33 +1805,23 @@ static int add_port_attach_ep(struct cxl_memdev *cxlmd, return PTR_ERR(parent_dport); } - dport = devm_cxl_create_port(&cxlmd->dev, parent_port, - parent_dport, uport_dev, - dport_dev); - if (IS_ERR(dport)) { - /* Port or dport already exists, restart iteration */ - if (PTR_ERR(dport) == -EAGAIN || PTR_ERR(dport) == -EBUSY) - return 0; - return PTR_ERR(dport); - } + rc = devm_cxl_create_port(&cxlmd->dev, parent_port, + parent_dport, uport_dev, dport_dev); } - rc = cxl_add_ep(dport, &cxlmd->dev); - if (rc == -EBUSY) { - /* - * "can't" happen, but this error code means - * something to the caller, so translate it. - */ - rc = -ENXIO; - } + /* Port or dport already exists, restart iteration */ + if (rc == -EAGAIN || rc == -EBUSY) + return 0; return rc; } -static struct cxl_dport *find_or_add_dport(struct cxl_port *port, - struct device *dport_dev) +static int find_or_add_dport_attach_ep(struct cxl_memdev *cxlmd, + struct cxl_port *port, + struct device *dport_dev) { struct cxl_dport *dport; + int rc; /* * The port is already visible in CXL hierarchy, but it may still @@ -1826,13 +1837,28 @@ static struct cxl_dport *find_or_add_dport(struct cxl_port *port, if (!dport) { dport = probe_dport(port, dport_dev); if (IS_ERR(dport)) - return dport; + return PTR_ERR(dport); /* New dport added, restart iteration */ - return ERR_PTR(-EAGAIN); + return -EAGAIN; } - return dport; + /* @dport is only valid while @port's lock is held */ + rc = cxl_add_ep(dport, &cxlmd->dev); + + /* + * If the endpoint already exists in the port's list, + * that's ok, it was added on a previous pass. + * Otherwise, retry in add_port_attach_ep() after taking + * the parent_port lock as the current port may be being + * reaped. + */ + if (rc && rc != -EBUSY) + return rc; + + cxl_gpf_port_setup(dport); + + return 0; } int devm_cxl_enumerate_ports(struct cxl_memdev *cxlmd) @@ -1861,7 +1887,6 @@ int devm_cxl_enumerate_ports(struct cxl_memdev *cxlmd) for (iter = dev; iter; iter = grandparent(iter)) { struct device *dport_dev = grandparent(iter); struct device *uport_dev; - struct cxl_dport *dport; if (is_cxl_host_bridge(dport_dev)) return 0; @@ -1888,27 +1913,13 @@ int devm_cxl_enumerate_ports(struct cxl_memdev *cxlmd) * RP port enumerated by cxl_acpi without dport will * have the dport added here. */ - dport = find_or_add_dport(port, dport_dev); - if (IS_ERR(dport)) { - if (PTR_ERR(dport) == -EAGAIN) - goto retry; - return PTR_ERR(dport); - } - - rc = cxl_add_ep(dport, &cxlmd->dev); - - /* - * If the endpoint already exists in the port's list, - * that's ok, it was added on a previous pass. - * Otherwise, retry in add_port_attach_ep() after taking - * the parent_port lock as the current port may be being - * reaped. - */ - if (rc && rc != -EBUSY) + rc = find_or_add_dport_attach_ep(cxlmd, port, + dport_dev); + if (rc == -EAGAIN) + goto retry; + if (rc) return rc; - cxl_gpf_port_setup(dport); - /* Any more ports to add between this one and the root? */ if (!dev_is_cxl_root_child(&port->dev)) continue; From d2654fa0d841fa36ffa76577f83a2810d643ae19 Mon Sep 17 00:00:00 2001 From: Guixin Liu Date: Thu, 8 Oct 2026 11:46:12 +0800 Subject: [PATCH 35/49] cxl/pci: Use the Device GPF DVSEC for restricted endpoints cxl_gpf_get_dvsec() picks between the Port and the Device GPF DVSEC from the PCIe type of the given device, and only a plain endpoint counts as a device. A restricted CXL device is an endpoint too, but it falls into the port case and is probed for the Port GPF DVSEC instead of the Device GPF DVSEC. On an RCD, dirty shutdown tracking therefore never arms: the DVSEC lookup in cxl_nvdimm_arm_dirty_shutdown_tracking() fails, the dirty shutdown count stays invalid, and the warning complains about a missing Port GPF DVSEC on a device. Treat restricted endpoints as devices. Fixes: 36aace15d9bd ("cxl/pci: Drop the parameter is_port of cxl_gpf_get_dvsec()") Cc: stable@vger.kernel.org Assisted-by: LLM Signed-off-by: Guixin Liu Reviewed-by: Jonathan Cameron Reviewed-by: Dave Jiang Reviewed-by: Alison Schofield Link: https://patch.msgid.link/20261008034619.28059-2-kanie@linux.alibaba.com Signed-off-by: Dave Jiang --- drivers/cxl/core/pci.c | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/drivers/cxl/core/pci.c b/drivers/cxl/core/pci.c index 4af165a436d2..5667b4c5a6f3 100644 --- a/drivers/cxl/core/pci.c +++ b/drivers/cxl/core/pci.c @@ -819,7 +819,8 @@ u16 cxl_gpf_get_dvsec(struct device *dev) return 0; pdev = to_pci_dev(dev); - if (pci_pcie_type(pdev) == PCI_EXP_TYPE_ENDPOINT) + if (pci_pcie_type(pdev) == PCI_EXP_TYPE_ENDPOINT || + is_cxl_restricted(pdev)) is_port = false; dvsec = pci_find_dvsec_capability(pdev, PCI_VENDOR_ID_CXL, From ec2e30a682945616d194d7999cf96606b7ee5a88 Mon Sep 17 00:00:00 2001 From: Guixin Liu Date: Thu, 8 Oct 2026 11:46:13 +0800 Subject: [PATCH 36/49] cxl/pci: Program the Port GPF timeouts before caching the DVSEC cxl_gpf_port_setup() caches the Port GPF DVSEC offset in the dport before programming the phase timeouts, and ignores the return value of both update_gpf_port_dvsec() calls: the function always reports success. A failing config write is therefore not only silent, it is also permanent: the timeouts stay at the hardware defaults instead of the maximum flush window the kernel is asking for, and the cached offset keeps any later endpoint attach from retrying the update. Propagate the errors and cache the offset only after both phases are programmed, so the next endpoint attach retries the setup. Fixes: 6af941db6a60 ("cxl/pci: Update Port GPF timeout only when the first EP attaching") Cc: stable@vger.kernel.org Assisted-by: LLM Signed-off-by: Guixin Liu Reviewed-by: Jonathan Cameron Reviewed-by: Dave Jiang Reviewed-by: Alison Schofield Link: https://patch.msgid.link/20261008034619.28059-3-kanie@linux.alibaba.com Signed-off-by: Dave Jiang --- drivers/cxl/core/pci.c | 30 +++++++++++++++++++++--------- 1 file changed, 21 insertions(+), 9 deletions(-) diff --git a/drivers/cxl/core/pci.c b/drivers/cxl/core/pci.c index 5667b4c5a6f3..2be3ba63e2d0 100644 --- a/drivers/cxl/core/pci.c +++ b/drivers/cxl/core/pci.c @@ -855,7 +855,7 @@ static int update_gpf_port_dvsec(struct pci_dev *pdev, int dvsec, int phase) rc = pci_read_config_word(pdev, dvsec + offset, &ctrl); if (rc) - return rc; + return pcibios_err_to_errno(rc); if (FIELD_GET(base, ctrl) == GPF_TIMEOUT_BASE_MAX && FIELD_GET(scale, ctrl) == GPF_TIMEOUT_SCALE_MAX) @@ -865,11 +865,17 @@ static int update_gpf_port_dvsec(struct pci_dev *pdev, int dvsec, int phase) ctrl |= FIELD_PREP(scale, GPF_TIMEOUT_SCALE_MAX); rc = pci_write_config_word(pdev, dvsec + offset, ctrl); - if (!rc) - pci_dbg(pdev, "Port GPF phase %d timeout: %d0 secs\n", - phase, GPF_TIMEOUT_BASE_MAX); + if (rc) { + rc = pcibios_err_to_errno(rc); + pci_warn(pdev, "Port GPF phase %d timeout write failed: %d\n", + phase, rc); + return rc; + } - return rc; + pci_dbg(pdev, "Port GPF phase %d timeout: %d0 secs\n", + phase, GPF_TIMEOUT_BASE_MAX); + + return 0; } int cxl_gpf_port_setup(struct cxl_dport *dport) @@ -879,16 +885,22 @@ int cxl_gpf_port_setup(struct cxl_dport *dport) if (!dport->gpf_dvsec) { struct pci_dev *pdev; - int dvsec; + int dvsec, rc; dvsec = cxl_gpf_get_dvsec(dport->dport_dev); if (!dvsec) return -EINVAL; - dport->gpf_dvsec = dvsec; pdev = to_pci_dev(dport->dport_dev); - update_gpf_port_dvsec(pdev, dport->gpf_dvsec, 1); - update_gpf_port_dvsec(pdev, dport->gpf_dvsec, 2); + rc = update_gpf_port_dvsec(pdev, dvsec, 1); + if (rc) + return rc; + + rc = update_gpf_port_dvsec(pdev, dvsec, 2); + if (rc) + return rc; + + dport->gpf_dvsec = dvsec; } return 0; From b5d14009bdb1973d54a6ea111a660b3dae115be5 Mon Sep 17 00:00:00 2001 From: Guixin Liu Date: Thu, 8 Oct 2026 11:46:14 +0800 Subject: [PATCH 37/49] cxl/pci: Update only the Port GPF timeout fields update_gpf_port_dvsec() rebuilds the phase control register from just the timeout base and scale, so the rest of the register is cleared by the write. The other bits are reserved today and nothing is lost, but the full-register rewrite would clobber any field the spec defines there later. Read-modify-write the register and change only the two timeout fields. Fixes: a52b6a2c1c99 ("cxl/pci: Support Global Persistent Flush (GPF)") Assisted-by: LLM Signed-off-by: Guixin Liu Reviewed-by: Dave Jiang Reviewed-by: Alison Schofield Link: https://patch.msgid.link/20261008034619.28059-4-kanie@linux.alibaba.com Signed-off-by: Dave Jiang --- drivers/cxl/core/pci.c | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/drivers/cxl/core/pci.c b/drivers/cxl/core/pci.c index 2be3ba63e2d0..713a75e68bb9 100644 --- a/drivers/cxl/core/pci.c +++ b/drivers/cxl/core/pci.c @@ -861,8 +861,8 @@ static int update_gpf_port_dvsec(struct pci_dev *pdev, int dvsec, int phase) FIELD_GET(scale, ctrl) == GPF_TIMEOUT_SCALE_MAX) return 0; - ctrl = FIELD_PREP(base, GPF_TIMEOUT_BASE_MAX); - ctrl |= FIELD_PREP(scale, GPF_TIMEOUT_SCALE_MAX); + FIELD_MODIFY(base, &ctrl, GPF_TIMEOUT_BASE_MAX); + FIELD_MODIFY(scale, &ctrl, GPF_TIMEOUT_SCALE_MAX); rc = pci_write_config_word(pdev, dvsec + offset, ctrl); if (rc) { From 16440d6e4bf1175cda83438d9f9a8ae84bfcf0ee Mon Sep 17 00:00:00 2001 From: Koichiro Den Date: Fri, 9 Oct 2026 13:54:50 +0900 Subject: [PATCH 38/49] cxl/port: Convert PCIe link latency to nanoseconds Commit 51293c565cf4 ("cxl: Fix incorrect region perf data calculation") converted CDAT latencies to nanoseconds and removed the conversion in cxl_region_perf_data_calculate(), but left PCIe link latencies in picoseconds. This inflates the FlitLatency contribution to region and node latencies by a factor of 1000, exposing incorrect values in sysfs. The resulting abstract distances can incorrectly reverse the memory tier ordering of nodes. Convert the FlitLatency contribution to nanoseconds before adding it to the access coordinates, rounding up as cdat_normalize() does. Fixes: 51293c565cf4 ("cxl: Fix incorrect region perf data calculation") Cc: stable@vger.kernel.org Signed-off-by: Koichiro Den Reviewed-by: Alison Schofield Reviewed-by: Dave Jiang Link: https://patch.msgid.link/20261009045452.1107921-2-den@valinux.co.jp Signed-off-by: Dave Jiang --- drivers/cxl/core/port.c | 3 +++ 1 file changed, 3 insertions(+) diff --git a/drivers/cxl/core/port.c b/drivers/cxl/core/port.c index a7bfe320c802..376d1d720709 100644 --- a/drivers/cxl/core/port.c +++ b/drivers/cxl/core/port.c @@ -2392,6 +2392,9 @@ EXPORT_SYMBOL_NS_GPL(schedule_cxl_memdev_detach, "CXL"); static void add_latency(struct access_coordinate *c, long latency) { + /* Convert link latency from picoseconds to nanoseconds. */ + latency = DIV_ROUND_UP(latency, 1000); + for (int i = 0; i < ACCESS_COORDINATE_MAX; i++) { c[i].write_latency += latency; c[i].read_latency += latency; From db4503d4575cdd2fad7e8a13a8a8ffcbc86d152d Mon Sep 17 00:00:00 2001 From: Koichiro Den Date: Fri, 9 Oct 2026 13:54:51 +0900 Subject: [PATCH 39/49] cxl: Account for link width in latency calculation cxl_pci_get_latency() derives bandwidth from the per-lane speed returned by pcie_link_speed_mbps(), ignoring the negotiated link width. This overestimates the FlitLatency contribution for multi-lane links. Sections 2.11.3 and 2.11.4 of the Intel CXL Memory Device Software Guide describe the link bandwidth as the product of the negotiated speed and width. Replace pcie_link_speed_mbps() with pcie_link_bandwidth_mbps() and migrate both CXL callers (the only users of the API). The new helper obtains speed and width from a single Link Status read. The CXL bandwidth calculation no longer needs a separate width read and multiplication. While at it, make error handling more robust by converting PCI config read errors to negative errno values and checking for zero bandwidth before calculating latency. Fixes: 4d07a05397c8 ("cxl: Calculate and store PCI link latency for the downstream ports") Cc: stable@vger.kernel.org Signed-off-by: Koichiro Den Reviewed-by: Alison Schofield Acked-by: Bjorn Helgaas # pci/pci.c Reviewed-by: Dave Jiang Link: https://patch.msgid.link/20261009045452.1107921-3-den@valinux.co.jp Signed-off-by: Dave Jiang --- drivers/cxl/core/pci.c | 20 +++++++------------- drivers/pci/pci.c | 23 ++++++++++++++++++----- include/linux/pci.h | 2 +- 3 files changed, 26 insertions(+), 19 deletions(-) diff --git a/drivers/cxl/core/pci.c b/drivers/cxl/core/pci.c index 713a75e68bb9..125abb36df7f 100644 --- a/drivers/cxl/core/pci.c +++ b/drivers/cxl/core/pci.c @@ -660,8 +660,8 @@ long cxl_pci_get_latency(struct pci_dev *pdev) { long bw; - bw = pcie_link_speed_mbps(pdev); - if (bw < 0) + bw = pcie_link_bandwidth_mbps(pdev); + if (bw <= 0) return 0; bw /= BITS_PER_BYTE; @@ -771,18 +771,12 @@ EXPORT_SYMBOL_NS_GPL(cxl_pci_setup_regs, "CXL"); int cxl_pci_get_bandwidth(struct pci_dev *pdev, struct access_coordinate *c) { - int speed, bw; - u16 lnksta; - u32 width; + int bw; - speed = pcie_link_speed_mbps(pdev); - if (speed < 0) - return speed; - speed /= BITS_PER_BYTE; - - pcie_capability_read_word(pdev, PCI_EXP_LNKSTA, &lnksta); - width = FIELD_GET(PCI_EXP_LNKSTA_NLW, lnksta); - bw = speed * width; + bw = pcie_link_bandwidth_mbps(pdev); + if (bw < 0) + return bw; + bw /= BITS_PER_BYTE; for (int i = 0; i < ACCESS_COORDINATE_MAX; i++) { c[i].read_bandwidth = bw; diff --git a/drivers/pci/pci.c b/drivers/pci/pci.c index b2879a6be5f8..aa3d180de56e 100644 --- a/drivers/pci/pci.c +++ b/drivers/pci/pci.c @@ -5980,18 +5980,31 @@ static enum pci_bus_speed to_pcie_link_speed(u16 lnksta) return pcie_link_speed[FIELD_GET(PCI_EXP_LNKSTA_CLS, lnksta)]; } -int pcie_link_speed_mbps(struct pci_dev *pdev) +/** + * pcie_link_bandwidth_mbps - get the current bandwidth of a single PCIe link + * @pdev: PCI device to query + * + * The bandwidth includes all negotiated lanes and does not account for + * encoding overhead. A negotiated width of zero gives zero bandwidth. + * + * Return: Bandwidth in Mb/s, or a negative errno on failure. + */ +int pcie_link_bandwidth_mbps(struct pci_dev *pdev) { u16 lnksta; - int err; + int speed, err; err = pcie_capability_read_word(pdev, PCI_EXP_LNKSTA, &lnksta); if (err) - return err; + return pcibios_err_to_errno(err); - return pcie_dev_speed_mbps(to_pcie_link_speed(lnksta)); + speed = pcie_dev_speed_mbps(to_pcie_link_speed(lnksta)); + if (speed < 0) + return speed; + + return speed * FIELD_GET(PCI_EXP_LNKSTA_NLW, lnksta); } -EXPORT_SYMBOL(pcie_link_speed_mbps); +EXPORT_SYMBOL(pcie_link_bandwidth_mbps); /** * pcie_bandwidth_available - determine minimum link settings of a PCIe diff --git a/include/linux/pci.h b/include/linux/pci.h index d31a8d107b1e..bbfb0b98ff46 100644 --- a/include/linux/pci.h +++ b/include/linux/pci.h @@ -1477,7 +1477,7 @@ int pcie_set_mps(struct pci_dev *dev, int mps); u32 pcie_bandwidth_available(struct pci_dev *dev, struct pci_dev **limiting_dev, enum pci_bus_speed *speed, enum pcie_link_width *width); -int pcie_link_speed_mbps(struct pci_dev *pdev); +int pcie_link_bandwidth_mbps(struct pci_dev *pdev); void pcie_print_link_status(struct pci_dev *dev); int pcie_reset_flr(struct pci_dev *dev, bool probe); int pcie_flr(struct pci_dev *dev); From 2658c66a5efb5f30f5ced708a5de831845b43f6d Mon Sep 17 00:00:00 2001 From: Koichiro Den Date: Fri, 9 Oct 2026 13:54:52 +0900 Subject: [PATCH 40/49] cxl/acpi: Convert QTG _DSM latencies to picoseconds The QTG _DSM expects read and write latencies in picoseconds, as specified in CXL 3.0, Table 9-30. The code base originally kept the latencies passed to the QTG _DSM in picoseconds in effect. Commit 51293c565cf4 ("cxl: Fix incorrect region perf data calculation") normalized CDAT and host bridge latencies to nanoseconds, but did not add a conversion back to picoseconds when constructing the _DSM input. Firmware can therefore select QTG IDs using understated latencies. Convert the latencies to picoseconds when constructing the _DSM input package. Multiply in 64 bits and reject latencies that cannot fit in the DWORD inputs defined by the specification. Fixes: 51293c565cf4 ("cxl: Fix incorrect region perf data calculation") Cc: stable@vger.kernel.org Signed-off-by: Koichiro Den Reviewed-by: Alison Schofield Reviewed-by: Dave Jiang Link: https://patch.msgid.link/20261009045452.1107921-4-den@valinux.co.jp Signed-off-by: Dave Jiang --- drivers/cxl/acpi.c | 10 ++++++++-- 1 file changed, 8 insertions(+), 2 deletions(-) diff --git a/drivers/cxl/acpi.c b/drivers/cxl/acpi.c index fb09a5ff48c1..b7ce6b59c680 100644 --- a/drivers/cxl/acpi.c +++ b/drivers/cxl/acpi.c @@ -231,9 +231,10 @@ cxl_acpi_evaluate_qtg_dsm(acpi_handle handle, struct access_coordinate *coord, int entries, int *qos_class) { union acpi_object *out_obj, *out_buf, *obj; + /* QTG _DSM expects latencies in picoseconds. */ union acpi_object in_array[4] = { - [0].integer = { ACPI_TYPE_INTEGER, coord->read_latency }, - [1].integer = { ACPI_TYPE_INTEGER, coord->write_latency }, + [0].integer = { ACPI_TYPE_INTEGER, coord->read_latency * 1000ULL }, + [1].integer = { ACPI_TYPE_INTEGER, coord->write_latency * 1000ULL }, [2].integer = { ACPI_TYPE_INTEGER, coord->read_bandwidth }, [3].integer = { ACPI_TYPE_INTEGER, coord->write_bandwidth }, }; @@ -251,6 +252,11 @@ cxl_acpi_evaluate_qtg_dsm(acpi_handle handle, struct access_coordinate *coord, if (!entries) return -EINVAL; + /* Latency inputs must fit in the DWORD fields defined by the spec. */ + if (coord->read_latency > U32_MAX / 1000 || + coord->write_latency > U32_MAX / 1000) + return -ERANGE; + out_obj = acpi_evaluate_dsm(handle, &acpi_cxl_qtg_id_guid, 1, 1, &in_obj); if (!out_obj) return -ENXIO; From 13797539ae78e8bc242d28733100060f351ee49b Mon Sep 17 00:00:00 2001 From: Ira Weiny Date: Sun, 11 Oct 2026 07:54:52 +0900 Subject: [PATCH 41/49] cxl/mbox: Flag support for Dynamic Capacity Devices (DCD) Per the CXL 4.0 specification software must check the Command Effects Log (CEL) for dynamic capacity command support. Detect support for the DCD commands while reading the CEL, including: Get DC Config Get DC Extent List Add DC Response Release DC Leave mds->dcd_supported false. Dynamic capacity is only usable once the driver can handle Add and Release Capacity events. Detect and log the capability here and let the commit which adds DCD event handling set the flag. Based on an original patch by Navneet Singh. Signed-off-by: Ira Weiny Signed-off-by: Anisa Su Tested-by: Wonjae Lee Tested-by: Junhee Park Tested-by: Heesoo Kim Reviewed-by: Dave Jiang Reviewed-by: Gregory Price (Meta) Reviewed-by: Davidlohr Bueso Reviewed-by: Jonathan Cameron Reviewed-by: Richard Cheng Reviewed-by: Alison Schofield Tested-by: Alison Schofield Link: https://patch.msgid.link/20261010225852.10727-2-anisa.su@samsung.com Signed-off-by: Dave Jiang --- drivers/cxl/core/mbox.c | 39 +++++++++++++++++++++++++++++++++++++++ drivers/cxl/cxlmem.h | 20 ++++++++++++++++++++ 2 files changed, 59 insertions(+) diff --git a/drivers/cxl/core/mbox.c b/drivers/cxl/core/mbox.c index 55828a836c01..199bb986d674 100644 --- a/drivers/cxl/core/mbox.c +++ b/drivers/cxl/core/mbox.c @@ -171,6 +171,33 @@ static void cxl_set_security_cmd_enabled(struct cxl_security_state *security, } } +static bool cxl_is_dcd_command(u16 opcode) +{ +#define CXL_MBOX_OP_DCD_CMDS 0x48 + + return (opcode >> 8) == CXL_MBOX_OP_DCD_CMDS; +} + +static void cxl_set_dcd_cmd_enabled(u16 opcode, unsigned long *cmd_mask) +{ + switch (opcode) { + case CXL_MBOX_OP_GET_DC_CONFIG: + set_bit(CXL_DCD_ENABLED_GET_CONFIG, cmd_mask); + break; + case CXL_MBOX_OP_GET_DC_EXTENT_LIST: + set_bit(CXL_DCD_ENABLED_GET_EXTENT_LIST, cmd_mask); + break; + case CXL_MBOX_OP_ADD_DC_RESPONSE: + set_bit(CXL_DCD_ENABLED_ADD_RESPONSE, cmd_mask); + break; + case CXL_MBOX_OP_RELEASE_DC: + set_bit(CXL_DCD_ENABLED_RELEASE, cmd_mask); + break; + default: + break; + } +} + static bool cxl_is_poison_command(u16 opcode) { #define CXL_MBOX_OP_POISON_CMDS 0x43 @@ -759,6 +786,7 @@ static void cxl_walk_cel(struct cxl_memdev_state *mds, size_t size, u8 *cel) struct cxl_mailbox *cxl_mbox = &mds->cxlds.cxl_mbox; struct cxl_cel_entry *cel_entry; const int cel_entries = size / sizeof(*cel_entry); + DECLARE_BITMAP(dcd_cmds, CXL_DCD_ENABLED_MAX) = {}; struct device *dev = mds->cxlds.dev; int i, ro_cmds = 0, wr_cmds = 0; @@ -787,11 +815,22 @@ static void cxl_walk_cel(struct cxl_memdev_state *mds, size_t size, u8 *cel) enabled++; } + if (cxl_is_dcd_command(opcode)) { + cxl_set_dcd_cmd_enabled(opcode, dcd_cmds); + enabled++; + } + dev_dbg(dev, "Opcode 0x%04x %s\n", opcode, enabled ? "enabled" : "unsupported by driver"); } set_features_cap(cxl_mbox, ro_cmds, wr_cmds); + /* + * Disabled until event handling implemented. + */ + if (bitmap_full(dcd_cmds, CXL_DCD_ENABLED_MAX)) + dev_dbg(dev, "Device supports DCD; capability disabled\n"); + mds->dcd_supported = false; } static struct cxl_mbox_get_supported_logs *cxl_get_gsl(struct cxl_memdev_state *mds) diff --git a/drivers/cxl/cxlmem.h b/drivers/cxl/cxlmem.h index c401e3a1af06..0c8e23276dc6 100644 --- a/drivers/cxl/cxlmem.h +++ b/drivers/cxl/cxlmem.h @@ -252,6 +252,20 @@ struct cxl_event_state { struct mutex log_lock; }; +/* + * CXL r4.0 Section 8.2.10.9 - Memory Device Command Sets. See Table 8-308. + * + * The 48h Command Set (Opcodes 4800h - 4803h) defines the device-enabled DCD + * commands. + */ +enum dcd_cmd_enabled_bits { + CXL_DCD_ENABLED_GET_CONFIG, + CXL_DCD_ENABLED_GET_EXTENT_LIST, + CXL_DCD_ENABLED_ADD_RESPONSE, + CXL_DCD_ENABLED_RELEASE, + CXL_DCD_ENABLED_MAX +}; + /* Device enabled poison commands */ enum poison_cmd_enabled_bits { CXL_POISON_ENABLED_LIST, @@ -427,6 +441,7 @@ static inline struct cxl_dev_state *mbox_to_cxlds(struct cxl_mailbox *cxl_mbox) * @partition_align_bytes: alignment size for partition-able capacity * @active_volatile_bytes: sum of hard + soft volatile * @active_persistent_bytes: sum of hard + soft persistent + * @dcd_supported: all DCD commands are supported * @event: event log driver state * @poison: poison driver state info * @security: security driver state info @@ -445,6 +460,7 @@ struct cxl_memdev_state { u64 partition_align_bytes; u64 active_volatile_bytes; u64 active_persistent_bytes; + bool dcd_supported; struct cxl_event_state event; struct cxl_poison_state poison; @@ -505,6 +521,10 @@ enum cxl_opcode { CXL_MBOX_OP_UNLOCK = 0x4503, CXL_MBOX_OP_FREEZE_SECURITY = 0x4504, CXL_MBOX_OP_PASSPHRASE_SECURE_ERASE = 0x4505, + CXL_MBOX_OP_GET_DC_CONFIG = 0x4800, + CXL_MBOX_OP_GET_DC_EXTENT_LIST = 0x4801, + CXL_MBOX_OP_ADD_DC_RESPONSE = 0x4802, + CXL_MBOX_OP_RELEASE_DC = 0x4803, CXL_MBOX_OP_MAX = 0x10000 }; From b5d93fa1b7ac3f1173dfb9ba32fe6143b22fddf7 Mon Sep 17 00:00:00 2001 From: Ira Weiny Date: Sun, 11 Oct 2026 07:54:53 +0900 Subject: [PATCH 42/49] cxl/mem: Read dynamic capacity configuration from the device Devices which support Dynamic Capacity (DC) are configured via mailbox commands. CXL r4.0 section 9.13.3 describes the use of the Get DC Configuration command in order to obtain DCD partition characteristics. Implement the DC mailbox commands as specified in CXL 4.0 section 8.2.10.9.9 (opcodes 48XXh) to read and store the DCD configuration information. Disable DCD if an invalid configuration is found. Initial enablement for Linux only supports one dynamic capacity partition. Read all the partitions the device reports but validate only the first, and configure it as 'dynamic ram'. The specification requires that volatile capacity starts at DPA 0 and pmem starts at the DPA immediately following it, but DC partitions only need to be 256MB aligned according to CXL r4.0 section 8.2.10.9.9.1 Table 8-347. So a device could leave a gap between ram/pmem (static) capacity and its first DC partition, or between one DC partition and the next. However, Linux chooses not to support gaps between static and dynamic capacity, and requires the first DC partition to begin at the DPA immediately following static capacity. Based on an original patch by Navneet Singh. [ dj: Fixed up output line wrapping. (Jonathan) ] [ dj: Fixed up cleanup declaration. (Jonathan) ] [ dj: Remove start_partition in cxl_get_dc_config. (Jonathan) ] Signed-off-by: Ira Weiny Co-developed-by: Anisa Su Signed-off-by: Anisa Su Tested-by: Wonjae Lee Tested-by: Junhee Park Tested-by: Heesoo Kim Reviewed-by: Jonathan Cameron Link: https://patch.msgid.link/20261010225852.10727-3-anisa.su@samsung.com Signed-off-by: Dave Jiang --- drivers/cxl/core/hdm.c | 13 ++- drivers/cxl/core/mbox.c | 235 ++++++++++++++++++++++++++++++++++++++ drivers/cxl/core/memdev.c | 53 +++++++++ drivers/cxl/cxlmem.h | 55 +++++++++ drivers/cxl/pci.c | 6 + include/cxl/cxl.h | 3 +- 6 files changed, 363 insertions(+), 2 deletions(-) diff --git a/drivers/cxl/core/hdm.c b/drivers/cxl/core/hdm.c index 0c80b76a5f9b..5fa2177cefcc 100644 --- a/drivers/cxl/core/hdm.c +++ b/drivers/cxl/core/hdm.c @@ -446,6 +446,8 @@ static const char *cxl_mode_name(enum cxl_partition_mode mode) return "ram"; case CXL_PARTMODE_PMEM: return "pmem"; + case CXL_PARTMODE_DYNAMIC_RAM: + return "dynamic_ram"; default: return ""; }; @@ -455,6 +457,7 @@ static const char *cxl_mode_name(enum cxl_partition_mode mode) int cxl_dpa_setup(struct cxl_dev_state *cxlds, const struct cxl_dpa_info *info) { struct device *dev = cxlds->dev; + int dc_nth = 0; guard(rwsem_write)(&cxl_rwsem.dpa); @@ -471,8 +474,16 @@ int cxl_dpa_setup(struct cxl_dev_state *cxlds, const struct cxl_dpa_info *info) for (int i = 0; i < info->nr_partitions; i++) { const struct cxl_dpa_part_info *part = &info->part[i]; + const char *name = cxl_mode_name(part->mode); int rc; + if (part->mode == CXL_PARTMODE_DYNAMIC_RAM) { + name = devm_kasprintf(dev, GFP_KERNEL, "%s_%d", name, + ++dc_nth); + if (!name) + return -ENOMEM; + } + cxlds->part[i].perf.qos_class = CXL_QOS_CLASS_INVALID; cxlds->part[i].mode = part->mode; @@ -485,7 +496,7 @@ int cxl_dpa_setup(struct cxl_dev_state *cxlds, const struct cxl_dpa_info *info) } rc = add_dpa_res(dev, &cxlds->dpa_res, &cxlds->part[i].res, part->range.start, range_len(&part->range), - cxl_mode_name(part->mode)); + name); if (rc) return rc; cxlds->nr_partitions++; diff --git a/drivers/cxl/core/mbox.c b/drivers/cxl/core/mbox.c index 199bb986d674..45c8c112c09c 100644 --- a/drivers/cxl/core/mbox.c +++ b/drivers/cxl/core/mbox.c @@ -1349,6 +1349,241 @@ int cxl_mem_sanitize(struct cxl_memdev *cxlmd, u16 cmd) return -EBUSY; } +/* + * A partition unavailable for Dynamic Capacity reports zeros for decode_length, + * length, and block_size. + */ +static bool cxl_dc_partition_unavailable(struct cxl_dc_partition *dev_part) +{ + return !le64_to_cpu(dev_part->decode_length) && + !le64_to_cpu(dev_part->length) && + !le64_to_cpu(dev_part->block_size); +} + +static int cxl_dc_check(struct device *dev, struct cxl_dc_partition_info *part, + struct cxl_dc_partition *dev_part) +{ + u64 decode_len = le64_to_cpu(dev_part->decode_length); + u64 blk_size = le64_to_cpu(dev_part->block_size); + u64 len = le64_to_cpu(dev_part->length); + u64 size; + + /* + * Not an error; leave the entry empty. A partially zeroed partition + * is rejected by the checks below. CXL r4.0 Table 8-347. + */ + if (cxl_dc_partition_unavailable(dev_part)) { + *part = (struct cxl_dc_partition_info) { }; + dev_dbg(dev, "Partition 0 unavailable for DC\n"); + return 0; + } + + /* + * Decode length is a count of 256M units, so a device reporting 2^36 + * or more wraps. + */ + if (check_mul_overflow(decode_len, (u64)CXL_CAPACITY_MULTIPLIER, &size)) { + dev_err(dev, "DC partition 0 decode length %#llx overflows\n", + decode_len); + return -EINVAL; + } + + *part = (struct cxl_dc_partition_info) { + .start = le64_to_cpu(dev_part->base), + .size = size, + }; + + /* + * Block size is a power of 2 and a multiple of 40h. is_power_of_2() + * takes an unsigned long, which truncates blk_size on 32 bit. + */ + if (blk_size == 0 || (blk_size & (blk_size - 1)) || + blk_size % CXL_DCD_BLOCK_LINE_SIZE) { + dev_err(dev, "DC partition 0 invalid block size %#llx\n", blk_size); + return -EINVAL; + } + + if (part->size == 0) { + dev_err(dev, "DC partition 0 zero decode length\n"); + return -EINVAL; + } + + if (len == 0) { + dev_err(dev, "DC partition 0 zero length\n"); + return -EINVAL; + } + + if (len > part->size) { + dev_err(dev, "DC partition 0 length %#llx exceeds decode length %#llx\n", + len, part->size); + return -EINVAL; + } + + if (!IS_ALIGNED(len, blk_size)) { + dev_err(dev, "DC partition 0 length %#llx not a multiple of block size %#llx\n", + len, blk_size); + return -EINVAL; + } + + if (!IS_ALIGNED(part->start, SZ_256M)) { + dev_err(dev, "DC partition 0 base %#llx not aligned to 256M\n", + part->start); + return -EINVAL; + } + + dev_dbg(dev, "DC partition 0 start %#llx size %#llx blk_size: %#llx\n", + part->start, part->size, blk_size); + + return 0; +} + +/* Returns the number of partitions in dc_resp or -ERRNO */ +static int cxl_get_dc_config(struct cxl_mailbox *mbox, u8 start_partition, + u8 partition_count, + struct cxl_mbox_get_dc_config_out *dc_resp, + size_t dc_resp_size) +{ + struct cxl_mbox_get_dc_config_in get_dc = (struct cxl_mbox_get_dc_config_in) { + .partition_count = partition_count, + .start_partition_index = start_partition, + }; + struct cxl_mbox_cmd mbox_cmd = (struct cxl_mbox_cmd) { + .opcode = CXL_MBOX_OP_GET_DC_CONFIG, + .payload_in = &get_dc, + .size_in = sizeof(get_dc), + .size_out = dc_resp_size, + .payload_out = dc_resp, + /* The device must return at least the fixed header */ + .min_out = sizeof(*dc_resp), + }; + size_t expected_sz; + int rc; + + rc = cxl_internal_send_cmd(mbox, &mbox_cmd); + if (rc < 0) + return rc; + + if (dc_resp->partitions_returned > partition_count) { + dev_err(mbox->host, "Device returned %u partitions, requested %u\n", + dc_resp->partitions_returned, partition_count); + return -EIO; + } + + /* The trailing extent/tag counts (CXL r4.0 Table 8-346) are not read */ + expected_sz = struct_size(dc_resp, partition, + dc_resp->partitions_returned); + + if (mbox_cmd.size_out < expected_sz) { + dev_err(mbox->host, + "Payload size %zu less than expected %zu for %u partitions\n", + mbox_cmd.size_out, expected_sz, + dc_resp->partitions_returned); + return -EIO; + } + + dev_dbg(mbox->host, "Read %d/%d DC partitions\n", + dc_resp->partitions_returned, dc_resp->avail_partition_count); + return dc_resp->partitions_returned; +} + +/** + * cxl_dev_dc_identify() - Reads the dynamic capacity information from the + * device. + * @mbox: Mailbox to query + * @dc_info: The dynamic partition information to return + * + * Read every partition the device reports, but validate only the first: + * Linux maps partition 0 and nothing else, so a defect in capacity the + * driver never touches is not a reason to refuse the device dynamic + * capacity. The remaining partitions are read but not recorded. + * + * Return: 0 if identify was executed successfully, -ERRNO on error. + * On error, @dc_info is left unchanged. + */ +int cxl_dev_dc_identify(struct cxl_mailbox *mbox, + struct cxl_dc_partition_info *dc_info) +{ + struct cxl_dc_partition_info partitions[CXL_MAX_DC_PARTITIONS] = { }; + struct device *dev = mbox->host; + u8 avail_partitions = 0; + u8 num_partitions; + u8 partition_count; + size_t dc_resp_size; + + /* + * Bound requested number of partitions by mailbox payload size. The + * 256 byte spec minimum, verified in cxl_pci_setup_mailbox(), keeps + * the subtraction below from underflowing. + */ + partition_count = min(CXL_MAX_DC_PARTITIONS, + (mbox->payload_size - + sizeof(struct cxl_mbox_get_dc_config_out) - + sizeof(struct cxl_mbox_get_dc_config_tail)) / + sizeof(struct cxl_dc_partition)); + dc_resp_size = struct_size_t(struct cxl_mbox_get_dc_config_out, + partition, partition_count) + + sizeof(struct cxl_mbox_get_dc_config_tail); + + struct cxl_mbox_get_dc_config_out *dc_resp __free(kfree) = + kmalloc(dc_resp_size, GFP_KERNEL); + if (!dc_resp) + return -ENOMEM; + + num_partitions = 0; + do { + int returned, rc; + + returned = cxl_get_dc_config(mbox, num_partitions, + partition_count, dc_resp, + dc_resp_size); + if (returned < 0) { + dev_err(dev, "Failed to get DC config: %d\n", returned); + return returned; + } + + if (returned == 0) { + dev_err(dev, + "Device reported %u partitions available but returned none at index %u\n", + dc_resp->avail_partition_count, num_partitions); + return -EIO; + } + + /* A DCD reports at least one partition (CXL r4.0 Table 8-346) */ + if (dc_resp->avail_partition_count == 0) { + dev_err(dev, "Device reported no DC partitions\n"); + return -EIO; + } + + avail_partitions = min(dc_resp->avail_partition_count, + CXL_MAX_DC_PARTITIONS); + + /* + * Only partition 0 is mapped, so it is the only one checked. + * The rest are read to complete the enumeration and dropped. + */ + if (num_partitions == 0) { + rc = cxl_dc_check(dev, &partitions[0], + &dc_resp->partition[0]); + if (rc) + return rc; + } + + num_partitions += returned; + + } while (num_partitions < avail_partitions); + + /* Linux only supports the 1st partition; nothing to do if it is unavailable */ + if (partitions[0].size == 0) + return -ENODEV; + + *dc_info = partitions[0]; + dev_dbg(dev, "Returning partition 0 %#llx size %#llx\n", + dc_info->start, dc_info->size); + + return 0; +} +EXPORT_SYMBOL_NS_GPL(cxl_dev_dc_identify, "CXL"); + int cxl_get_dirty_count(struct cxl_memdev_state *mds, u32 *count) { struct cxl_mailbox *cxl_mbox = &mds->cxlds.cxl_mbox; diff --git a/drivers/cxl/core/memdev.c b/drivers/cxl/core/memdev.c index b3419df586b9..469767f1de70 100644 --- a/drivers/cxl/core/memdev.c +++ b/drivers/cxl/core/memdev.c @@ -643,6 +643,59 @@ int cxl_mem_dpa_fetch(struct cxl_memdev_state *mds, struct cxl_dpa_info *info) } EXPORT_SYMBOL_NS_GPL(cxl_mem_dpa_fetch, "CXL"); +int cxl_configure_dcd(struct cxl_memdev_state *mds, struct cxl_dpa_info *info) +{ + struct cxl_dc_partition_info dc_info = { }; + struct device *dev = mds->cxlds.dev; + u64 static_end = 0; + u64 dc_end; + int rc; + + if (!mds->cxlds.media_ready) + return 0; + + rc = cxl_dev_dc_identify(&mds->cxlds.cxl_mbox, &dc_info); + if (rc) { + dev_warn(dev, + "Failed to read Dynamic Capacity config: %d\n", rc); + return rc; + } + + if (info->nr_partitions) + static_end = info->part[info->nr_partitions - 1].range.end + 1; + + if (dc_info.start < static_end) { + dev_err(dev, + "DC partition 0 base %#llx overlaps static capacity ending at %#llx\n", + dc_info.start, static_end); + return -EINVAL; + } + + /* A gap between static capacity and the DC partition is not supported */ + if (dc_info.start > static_end) { + dev_warn(dev, + "DC partition 0 base %#llx leaves a gap from static capacity ending at %#llx\n", + dc_info.start, static_end); + return -EOPNOTSUPP; + } + + if (check_add_overflow(dc_info.start, dc_info.size, &dc_end)) { + dev_err(dev, + "DC partition 0 size %#llx overflows DPA space at base %#llx\n", + dc_info.size, dc_info.start); + return -EINVAL; + } + + /* The DPA space must reach the end of the DC partition */ + if (dc_end > info->size) + info->size = dc_end; + dev_dbg(dev, "Adding dynamic ram partition; %#llx size %#llx\n", + dc_info.start, dc_info.size); + add_part(info, dc_info.start, dc_info.size, CXL_PARTMODE_DYNAMIC_RAM); + + return 0; +} +EXPORT_SYMBOL_NS_GPL(cxl_configure_dcd, "CXL"); /** * cxl_set_capacity: initialize dpa by a driver without a mailbox. diff --git a/drivers/cxl/cxlmem.h b/drivers/cxl/cxlmem.h index 0c8e23276dc6..4fac65dbb808 100644 --- a/drivers/cxl/cxlmem.h +++ b/drivers/cxl/cxlmem.h @@ -407,6 +407,8 @@ struct cxl_security_state { struct kernfs_node *sanitize_node; }; +#define CXL_MAX_DC_PARTITIONS 8 + static inline resource_size_t cxl_pmem_size(struct cxl_dev_state *cxlds) { /* @@ -689,6 +691,39 @@ struct cxl_mbox_set_shutdown_state_in { u8 state; } __packed; +/* See CXL r4.0 Table 8-345 get dynamic capacity config Input Payload */ +struct cxl_mbox_get_dc_config_in { + u8 partition_count; + u8 start_partition_index; +} __packed; + +/* See CXL r4.0 Table 8-346 get dynamic capacity config Output Payload */ +struct cxl_mbox_get_dc_config_out { + u8 avail_partition_count; + u8 partitions_returned; + u8 rsvd[6]; + /* See CXL r4.0 Table 8-347 */ + struct cxl_dc_partition { + __le64 base; + __le64 decode_length; + __le64 length; + __le64 block_size; + __le32 dsmad_handle; + u8 flags; + u8 rsvd[3]; + } __packed partition[] __counted_by(partitions_returned); + /* Trailing extent/tag count fields unused */ +} __packed; + +/* Trailing counts; cannot be a member after the flex array above */ +struct cxl_mbox_get_dc_config_tail { + __le32 num_extents_supported; + __le32 num_extents_available; + __le32 num_tags_supported; + __le32 num_tags_available; +} __packed; +#define CXL_DCD_BLOCK_LINE_SIZE 0x40 + /* Set Timestamp CXL 3.0 Spec 8.2.9.4.2 */ struct cxl_mbox_set_timestamp_in { __le64 timestamp; @@ -812,9 +847,18 @@ enum { int cxl_internal_send_cmd(struct cxl_mailbox *cxl_mbox, struct cxl_mbox_cmd *cmd); int cxl_dev_state_identify(struct cxl_memdev_state *mds); + +struct cxl_dc_partition_info { + u64 start; + u64 size; +}; + +int cxl_dev_dc_identify(struct cxl_mailbox *mbox, + struct cxl_dc_partition_info *dc_info); int cxl_await_media_ready(struct cxl_dev_state *cxlds); int cxl_enumerate_cmds(struct cxl_memdev_state *mds); int cxl_mem_dpa_fetch(struct cxl_memdev_state *mds, struct cxl_dpa_info *info); +int cxl_configure_dcd(struct cxl_memdev_state *mds, struct cxl_dpa_info *info); struct cxl_memdev_state *cxl_memdev_state_create(struct device *dev, u64 serial, u16 dvsec); void set_exclusive_cxl_commands(struct cxl_memdev_state *mds, @@ -828,6 +872,17 @@ void cxl_event_trace_record(struct cxl_memdev *cxlmd, const uuid_t *uuid, union cxl_event *evt); int cxl_get_dirty_count(struct cxl_memdev_state *mds, u32 *count); int cxl_arm_dirty_shutdown(struct cxl_memdev_state *mds); + +static inline bool cxl_dcd_supported(struct cxl_memdev_state *mds) +{ + return mds->dcd_supported; +} + +static inline void cxl_disable_dcd(struct cxl_memdev_state *mds) +{ + mds->dcd_supported = false; +} + int cxl_set_timestamp(struct cxl_memdev_state *mds); int cxl_poison_state_init(struct cxl_memdev_state *mds); int cxl_mem_get_poison(struct cxl_memdev *cxlmd, u64 offset, u64 len, diff --git a/drivers/cxl/pci.c b/drivers/cxl/pci.c index c7c91e8dc51d..2fe9db4b3d86 100644 --- a/drivers/cxl/pci.c +++ b/drivers/cxl/pci.c @@ -868,6 +868,12 @@ static int cxl_pci_probe(struct pci_dev *pdev, const struct pci_device_id *id) if (rc) return rc; + if (cxl_dcd_supported(mds)) { + rc = cxl_configure_dcd(mds, &range_info); + if (rc) + cxl_disable_dcd(mds); + } + rc = cxl_dpa_setup(cxlds, &range_info); if (rc) return rc; diff --git a/include/cxl/cxl.h b/include/cxl/cxl.h index 802b143de83d..e07360849344 100644 --- a/include/cxl/cxl.h +++ b/include/cxl/cxl.h @@ -133,6 +133,7 @@ struct cxl_dpa_perf { enum cxl_partition_mode { CXL_PARTMODE_RAM, CXL_PARTMODE_PMEM, + CXL_PARTMODE_DYNAMIC_RAM, }; /** @@ -147,7 +148,7 @@ struct cxl_dpa_partition { enum cxl_partition_mode mode; }; -#define CXL_NR_PARTITIONS_MAX 2 +#define CXL_NR_PARTITIONS_MAX 3 /** * struct cxl_dev_state - The driver device state From a7d2b5e9c4f51a17912c1a4470a24e038c944067 Mon Sep 17 00:00:00 2001 From: Ira Weiny Date: Sun, 11 Oct 2026 07:54:54 +0900 Subject: [PATCH 43/49] cxl/cdat: Gather DSMAS data for DCD partitions Additional DCD partition (AKA region) information is contained in the DSMAS CDAT tables, including performance, shareable and read-only attributes. Match DCD partitions with DSMAS tables and store the meta data. A DCD shall describe its DC partition in a DSMAS entry (CXL r4.0 9.13.3), so if the CDAT is absent, fails to parse, or has no entry matching the DC partition, disable DCD rather than run with unknown attributes. DSLBIS is optional, but cxl_cdat_endpoint_process() treated zero DSLBIS entries as a parse failure, which would now disable DCD on a device with a valid DSMAS. Accept an empty DSLBIS, and skip DSMAS entries with no DSLBIS coordinates. Signed-off-by: Ira Weiny Co-developed-by: Anisa Su Signed-off-by: Anisa Su Tested-by: Wonjae Lee Tested-by: Junhee Park Tested-by: Heesoo Kim Reviewed-by: Alison Schofield Reviewed-by: Jonathan Cameron Link: https://patch.msgid.link/20261010225852.10727-4-anisa.su@samsung.com Signed-off-by: Dave Jiang --- drivers/cxl/core/cdat.c | 99 ++++++++++++++++++++++++++++++++++----- drivers/cxl/core/hdm.c | 1 + drivers/cxl/core/mbox.c | 9 ++++ drivers/cxl/core/memdev.c | 17 ++++--- drivers/cxl/cxlmem.h | 11 +++++ include/cxl/cxl.h | 6 +++ 6 files changed, 124 insertions(+), 19 deletions(-) diff --git a/drivers/cxl/core/cdat.c b/drivers/cxl/core/cdat.c index 5c9f07262513..ef04af813dbb 100644 --- a/drivers/cxl/core/cdat.c +++ b/drivers/cxl/core/cdat.c @@ -17,6 +17,8 @@ struct dsmas_entry { struct access_coordinate cdat_coord[ACCESS_COORDINATE_MAX]; int entries; int qos_class; + bool shareable; + bool read_only; }; static u32 cdat_normalize(u16 entry, u64 base, u8 type) @@ -73,7 +75,13 @@ static int cdat_dsmas_handler(union acpi_subtable_headers *header, void *arg, if (!dent) return -ENOMEM; + /* Stays invalid unless the QTG lookup assigns a class */ + dent->qos_class = CXL_QOS_CLASS_INVALID; dent->handle = dsmas->dsmad_handle; + /* Shareable is CDAT 1.03 and later, DSMAS Flags bit 3 */ + dent->shareable = dsmas->flags & ACPI_CDAT_DSMAS_SHAREABLE; + /* Read Only is CDAT 1.04 and later, DSMAS Flags bit 6 */ + dent->read_only = dsmas->flags & ACPI_CDAT_DSMAS_READ_ONLY; dent->dpa_range.start = le64_to_cpu((__force __le64)dsmas->dpa_base_address); dent->dpa_range.end = le64_to_cpu((__force __le64)dsmas->dpa_base_address) + le64_to_cpu((__force __le64)dsmas->dpa_length) - 1; @@ -188,9 +196,10 @@ static int cxl_cdat_endpoint_process(struct cxl_port *port, if (rc) return rc; + /* DSLBIS is optional; zero entries is not an error */ rc = cdat_table_parse(ACPI_CDAT_TYPE_DSLBIS, cdat_dslbis_handler, dsmas_xa, port->cdat.table, port->cdat.length); - return cdat_table_parse_output(rc); + return rc < 0 ? rc : 0; } static int cxl_port_perf_data_calculate(struct cxl_port *port, @@ -219,6 +228,13 @@ static int cxl_port_perf_data_calculate(struct cxl_port *port, xa_for_each(dsmas_xa, index, dent) { int qos_class; + /* No DSLBIS for this DSMAS; nothing to hand the QTG _DSM */ + if (!dent->cdat_coord[ACCESS_COORDINATE_CPU].read_bandwidth && + !dent->cdat_coord[ACCESS_COORDINATE_CPU].write_bandwidth && + !dent->cdat_coord[ACCESS_COORDINATE_CPU].read_latency && + !dent->cdat_coord[ACCESS_COORDINATE_CPU].write_latency) + continue; + cxl_coordinates_combine(dent->coord, dent->cdat_coord, ep_c); dent->entries = 1; rc = cxl_root->ops.qos_class(cxl_root, @@ -255,35 +271,90 @@ static void update_perf_entry(struct device *dev, struct dsmas_entry *dent, dent->coord[ACCESS_COORDINATE_CPU].write_latency); } -static void cxl_memdev_set_qos_class(struct cxl_dev_state *cxlds, - struct xarray *dsmas_xa) +/* + * A DCD shall describe its DC partition in a DSMAS entry (CXL r4.0 9.13.3). + * Without one the partition's attributes are unknown, so disable DCD. + */ +static void cxl_dcd_dsmas_missing(struct cxl_dev_state *cxlds, const char *why) +{ + struct cxl_memdev_state *mds; + + if (cxlds->type != CXL_DEVTYPE_CLASSMEM) + return; + + mds = to_cxl_memdev_state(cxlds); + if (!cxl_dcd_supported(mds) || + !cxl_part_size(cxlds, CXL_PARTMODE_DYNAMIC_RAM)) + return; + + dev_warn(cxlds->dev, "DC partition has no valid DSMAS entry (%s), disabling DCD\n", + why); + cxl_disable_dcd(mds); +} + +enum cxl_dsmas_apply { + CXL_DSMAS_APPLY_FLAGS, /* shareable etc.; no QTG data */ + CXL_DSMAS_APPLY_FLAGS_PERF, /* perf coordinates and qos_class */ +}; + +/* + * DSMAS flags such as shareable are properties of the CDAT entry and are + * applied regardless; perf data is applied only if the QTG lookup produced + * recommendations. + */ +static void cxl_memdev_apply_dsmas(struct cxl_dev_state *cxlds, + struct xarray *dsmas_xa, + enum cxl_dsmas_apply attrs) { struct device *dev = cxlds->dev; struct dsmas_entry *dent; + bool dc_matched = false; unsigned long index; xa_for_each(dsmas_xa, index, dent) { bool found = false; for (int i = 0; i < cxlds->nr_partitions; i++) { - struct resource *res = &cxlds->part[i].res; + struct cxl_dpa_partition *part = &cxlds->part[i]; struct range range = { - .start = res->start, - .end = res->end, + .start = part->res.start, + .end = part->res.end, }; - if (range_contains(&range, &dent->dpa_range)) { - update_perf_entry(dev, dent, - &cxlds->part[i].perf); - found = true; + if (!range_contains(&range, &dent->dpa_range)) + continue; + + found = true; + /* + * part->handle is from Get DC Config, dent->handle + * from the CDAT DSMAS entry. + */ + if (part->mode == CXL_PARTMODE_DYNAMIC_RAM && + dent->handle != part->handle) { + dev_warn(dev, + "DSMAD handle mismatch: %pra has %u, DSMAS %pra has %u\n", + &range, part->handle, + &dent->dpa_range, dent->handle); break; } + + if (attrs == CXL_DSMAS_APPLY_FLAGS_PERF) + update_perf_entry(dev, dent, &part->perf); + if (part->mode == CXL_PARTMODE_DYNAMIC_RAM) { + part->shareable = dent->shareable; + part->read_only = dent->read_only; + dc_matched = true; + } + break; } if (!found) dev_dbg(dev, "no partition for dsmas dpa: %pra\n", &dent->dpa_range); } + + if (!dc_matched) + cxl_dcd_dsmas_missing(cxlds, "no matching entry"); } static int match_cxlrd_qos_class(struct device *dev, void *data) @@ -409,22 +480,26 @@ void cxl_endpoint_parse_cdat(struct cxl_port *port) int rc; xa_init(&__dsmas_xa); - if (!port->cdat.table) + if (!port->cdat.table) { + cxl_dcd_dsmas_missing(cxlds, "no CDAT"); return; + } rc = cxl_cdat_endpoint_process(port, dsmas_xa); if (rc < 0) { dev_dbg(&port->dev, "Failed to parse CDAT: %d\n", rc); + cxl_dcd_dsmas_missing(cxlds, "CDAT parse failed"); return; } rc = cxl_port_perf_data_calculate(port, dsmas_xa); if (rc) { dev_dbg(&port->dev, "Failed to do perf coord calculations.\n"); + cxl_memdev_apply_dsmas(cxlds, dsmas_xa, CXL_DSMAS_APPLY_FLAGS); return; } - cxl_memdev_set_qos_class(cxlds, dsmas_xa); + cxl_memdev_apply_dsmas(cxlds, dsmas_xa, CXL_DSMAS_APPLY_FLAGS_PERF); cxl_qos_class_verify(cxlmd); cxl_memdev_update_perf(cxlmd); } diff --git a/drivers/cxl/core/hdm.c b/drivers/cxl/core/hdm.c index 5fa2177cefcc..379540246333 100644 --- a/drivers/cxl/core/hdm.c +++ b/drivers/cxl/core/hdm.c @@ -486,6 +486,7 @@ int cxl_dpa_setup(struct cxl_dev_state *cxlds, const struct cxl_dpa_info *info) cxlds->part[i].perf.qos_class = CXL_QOS_CLASS_INVALID; cxlds->part[i].mode = part->mode; + cxlds->part[i].handle = part->handle; /* Require ordered + contiguous partitions */ if (i) { diff --git a/drivers/cxl/core/mbox.c b/drivers/cxl/core/mbox.c index 45c8c112c09c..2b35361e2413 100644 --- a/drivers/cxl/core/mbox.c +++ b/drivers/cxl/core/mbox.c @@ -1366,6 +1366,7 @@ static int cxl_dc_check(struct device *dev, struct cxl_dc_partition_info *part, u64 decode_len = le64_to_cpu(dev_part->decode_length); u64 blk_size = le64_to_cpu(dev_part->block_size); u64 len = le64_to_cpu(dev_part->length); + u32 handle = le32_to_cpu(dev_part->dsmad_handle); u64 size; /* @@ -1378,6 +1379,13 @@ static int cxl_dc_check(struct device *dev, struct cxl_dc_partition_info *part, return 0; } + /* The CDAT DSMAD handle this refers to is 8 bits */ + if (handle > U8_MAX) { + dev_warn(dev, "DSMAD handle 0x%x exceeds the 8 bit CDAT DSMAD handle\n", + handle); + return -EINVAL; + } + /* * Decode length is a count of 256M units, so a device reporting 2^36 * or more wraps. @@ -1391,6 +1399,7 @@ static int cxl_dc_check(struct device *dev, struct cxl_dc_partition_info *part, *part = (struct cxl_dc_partition_info) { .start = le64_to_cpu(dev_part->base), .size = size, + .handle = handle, }; /* diff --git a/drivers/cxl/core/memdev.c b/drivers/cxl/core/memdev.c index 469767f1de70..71493231f5e5 100644 --- a/drivers/cxl/core/memdev.c +++ b/drivers/cxl/core/memdev.c @@ -594,7 +594,8 @@ bool is_cxl_memdev(const struct device *dev) } EXPORT_SYMBOL_NS_GPL(is_cxl_memdev, "CXL"); -static void add_part(struct cxl_dpa_info *info, u64 start, u64 size, enum cxl_partition_mode mode) +static void add_part(struct cxl_dpa_info *info, u64 start, u64 size, + enum cxl_partition_mode mode, u8 handle) { int i = info->nr_partitions; @@ -606,6 +607,7 @@ static void add_part(struct cxl_dpa_info *info, u64 start, u64 size, enum cxl_pa .end = start + size - 1, }; info->part[i].mode = mode; + info->part[i].handle = handle; info->nr_partitions++; } @@ -623,9 +625,9 @@ int cxl_mem_dpa_fetch(struct cxl_memdev_state *mds, struct cxl_dpa_info *info) info->size = mds->total_bytes; if (mds->partition_align_bytes == 0) { - add_part(info, 0, mds->volatile_only_bytes, CXL_PARTMODE_RAM); + add_part(info, 0, mds->volatile_only_bytes, CXL_PARTMODE_RAM, 0); add_part(info, mds->volatile_only_bytes, - mds->persistent_only_bytes, CXL_PARTMODE_PMEM); + mds->persistent_only_bytes, CXL_PARTMODE_PMEM, 0); return 0; } @@ -635,9 +637,9 @@ int cxl_mem_dpa_fetch(struct cxl_memdev_state *mds, struct cxl_dpa_info *info) return rc; } - add_part(info, 0, mds->active_volatile_bytes, CXL_PARTMODE_RAM); + add_part(info, 0, mds->active_volatile_bytes, CXL_PARTMODE_RAM, 0); add_part(info, mds->active_volatile_bytes, mds->active_persistent_bytes, - CXL_PARTMODE_PMEM); + CXL_PARTMODE_PMEM, 0); return 0; } @@ -691,7 +693,8 @@ int cxl_configure_dcd(struct cxl_memdev_state *mds, struct cxl_dpa_info *info) info->size = dc_end; dev_dbg(dev, "Adding dynamic ram partition; %#llx size %#llx\n", dc_info.start, dc_info.size); - add_part(info, dc_info.start, dc_info.size, CXL_PARTMODE_DYNAMIC_RAM); + add_part(info, dc_info.start, dc_info.size, CXL_PARTMODE_DYNAMIC_RAM, + dc_info.handle); return 0; } @@ -709,7 +712,7 @@ int cxl_set_capacity(struct cxl_dev_state *cxlds, u64 capacity) .size = capacity, }; - add_part(&range_info, 0, capacity, CXL_PARTMODE_RAM); + add_part(&range_info, 0, capacity, CXL_PARTMODE_RAM, 0); return cxl_dpa_setup(cxlds, &range_info); } EXPORT_SYMBOL_NS_GPL(cxl_set_capacity, "CXL"); diff --git a/drivers/cxl/cxlmem.h b/drivers/cxl/cxlmem.h index 4fac65dbb808..b539b5a5c829 100644 --- a/drivers/cxl/cxlmem.h +++ b/drivers/cxl/cxlmem.h @@ -140,6 +140,7 @@ struct cxl_dpa_info { struct cxl_dpa_part_info { struct range range; enum cxl_partition_mode mode; + u8 handle; } part[CXL_NR_PARTITIONS_MAX]; int nr_partitions; }; @@ -421,6 +422,15 @@ static inline resource_size_t cxl_pmem_size(struct cxl_dev_state *cxlds) return 0; } +static inline resource_size_t cxl_part_size(struct cxl_dev_state *cxlds, + enum cxl_partition_mode mode) +{ + for (int i = 0; i < cxlds->nr_partitions; i++) + if (cxlds->part[i].mode == mode) + return resource_size(&cxlds->part[i].res); + return 0; +} + static inline struct cxl_dev_state *mbox_to_cxlds(struct cxl_mailbox *cxl_mbox) { return dev_get_drvdata(cxl_mbox->host); @@ -851,6 +861,7 @@ int cxl_dev_state_identify(struct cxl_memdev_state *mds); struct cxl_dc_partition_info { u64 start; u64 size; + u8 handle; }; int cxl_dev_dc_identify(struct cxl_mailbox *mbox, diff --git a/include/cxl/cxl.h b/include/cxl/cxl.h index e07360849344..728decda57fd 100644 --- a/include/cxl/cxl.h +++ b/include/cxl/cxl.h @@ -141,11 +141,17 @@ enum cxl_partition_mode { * @res: shortcut to the partition in the DPA resource tree (cxlds->dpa_res) * @perf: performance attributes of the partition from CDAT * @mode: operation mode for the DPA capacity, e.g. ram, pmem, dynamic... + * @handle: DSMAS handle that represents this partition + * @shareable: Is the partition shareable (from its CDAT DSMAS entry) + * @read_only: Is the partition read-only (from its CDAT DSMAS entry) */ struct cxl_dpa_partition { struct resource res; struct cxl_dpa_perf perf; enum cxl_partition_mode mode; + u8 handle; + bool shareable; + bool read_only; }; #define CXL_NR_PARTITIONS_MAX 3 From f9c672229ca8a8d272c5e5c8ea3105238d558cb0 Mon Sep 17 00:00:00 2001 From: Ira Weiny Date: Sun, 11 Oct 2026 07:54:55 +0900 Subject: [PATCH 44/49] cxl/events: Split event msgnum configuration from irq setup Dynamic Capacity Devices (DCD) require event interrupts to process memory addition or removal. BIOS may have control over non-DCD event processing. DCD interrupt configuration needs to be separate from memory event interrupt configuration. Split cxl_event_config_msgnums() from irq setup in preparation for separate DCD interrupts configuration. Signed-off-by: Ira Weiny Signed-off-by: Anisa Su Tested-by: Wonjae Lee Tested-by: Junhee Park Tested-by: Heesoo Kim Reviewed-by: Jonathan Cameron Reviewed-by: Fan Ni Reviewed-by: Dave Jiang Reviewed-by: Li Ming Reviewed-by: Alison Schofield Reviewed-by: Davidlohr Bueso Reviewed-by: Richard Cheng Tested-by: Alison Schofield Link: https://patch.msgid.link/20261010225852.10727-5-anisa.su@samsung.com Signed-off-by: Dave Jiang --- drivers/cxl/pci.c | 22 +++++++++++----------- 1 file changed, 11 insertions(+), 11 deletions(-) diff --git a/drivers/cxl/pci.c b/drivers/cxl/pci.c index 2fe9db4b3d86..4867657f5526 100644 --- a/drivers/cxl/pci.c +++ b/drivers/cxl/pci.c @@ -600,35 +600,31 @@ static int cxl_event_config_msgnums(struct cxl_memdev_state *mds, return cxl_event_get_int_policy(mds, policy); } -static int cxl_event_irqsetup(struct cxl_memdev_state *mds) +static int cxl_event_irqsetup(struct cxl_memdev_state *mds, + struct cxl_event_interrupt_policy *policy) { struct cxl_dev_state *cxlds = &mds->cxlds; - struct cxl_event_interrupt_policy policy; int rc; - rc = cxl_event_config_msgnums(mds, &policy); - if (rc) - return rc; - - rc = cxl_event_req_irq(cxlds, policy.info_settings); + rc = cxl_event_req_irq(cxlds, policy->info_settings); if (rc) { dev_err(cxlds->dev, "Failed to get interrupt for event Info log\n"); return rc; } - rc = cxl_event_req_irq(cxlds, policy.warn_settings); + rc = cxl_event_req_irq(cxlds, policy->warn_settings); if (rc) { dev_err(cxlds->dev, "Failed to get interrupt for event Warn log\n"); return rc; } - rc = cxl_event_req_irq(cxlds, policy.failure_settings); + rc = cxl_event_req_irq(cxlds, policy->failure_settings); if (rc) { dev_err(cxlds->dev, "Failed to get interrupt for event Failure log\n"); return rc; } - rc = cxl_event_req_irq(cxlds, policy.fatal_settings); + rc = cxl_event_req_irq(cxlds, policy->fatal_settings); if (rc) { dev_err(cxlds->dev, "Failed to get interrupt for event Fatal log\n"); return rc; @@ -675,11 +671,15 @@ static int cxl_event_config(struct pci_host_bridge *host_bridge, return -EBUSY; } + rc = cxl_event_config_msgnums(mds, &policy); + if (rc) + return rc; + rc = cxl_mem_alloc_event_buf(mds); if (rc) return rc; - rc = cxl_event_irqsetup(mds); + rc = cxl_event_irqsetup(mds, &policy); if (rc) return rc; From 726ac97330a47642dca3b6322d59f038634cdc19 Mon Sep 17 00:00:00 2001 From: Ira Weiny Date: Sun, 11 Oct 2026 07:54:56 +0900 Subject: [PATCH 45/49] cxl/pci: Factor out interrupt policy check Dynamic Capacity Devices (DCD) require event interrupts to process memory addition or removal. BIOS may have control over non-DCD event processing. DCD interrupt configuration needs to be separate from memory event interrupt configuration. Factor out check for firmware ownership of memory event logs. Link: https://lore.kernel.org/all/663922b475e50_d54d72945b@dwillia2-xfh.jf.intel.com.notmuch/ [1] Suggested-by: Dan Williams Signed-off-by: Ira Weiny Signed-off-by: Anisa Su Tested-by: Wonjae Lee Tested-by: Junhee Park Tested-by: Heesoo Kim Reviewed-by: Dave Jiang Reviewed-by: Jonathan Cameron Reviewed-by: Fan Ni Reviewed-by: Li Ming Reviewed-by: Davidlohr Bueso Reviewed-by: Alison Schofield Reviewed-by: Richard Cheng Tested-by: Alison Schofield Link: https://patch.msgid.link/20261010225852.10727-6-anisa.su@samsung.com Signed-off-by: Dave Jiang --- drivers/cxl/pci.c | 13 +++++++++---- 1 file changed, 9 insertions(+), 4 deletions(-) diff --git a/drivers/cxl/pci.c b/drivers/cxl/pci.c index 4867657f5526..7b754af2168d 100644 --- a/drivers/cxl/pci.c +++ b/drivers/cxl/pci.c @@ -640,6 +640,14 @@ static bool cxl_event_int_is_fw(u8 setting) return mode == CXL_INT_FW; } +static bool cxl_event_fw_owns_mem_logs(struct cxl_event_interrupt_policy *policy) +{ + return cxl_event_int_is_fw(policy->info_settings) || + cxl_event_int_is_fw(policy->warn_settings) || + cxl_event_int_is_fw(policy->failure_settings) || + cxl_event_int_is_fw(policy->fatal_settings); +} + static int cxl_event_config(struct pci_host_bridge *host_bridge, struct cxl_memdev_state *mds, bool irq_avail) { @@ -662,10 +670,7 @@ static int cxl_event_config(struct pci_host_bridge *host_bridge, if (rc) return rc; - if (cxl_event_int_is_fw(policy.info_settings) || - cxl_event_int_is_fw(policy.warn_settings) || - cxl_event_int_is_fw(policy.failure_settings) || - cxl_event_int_is_fw(policy.fatal_settings)) { + if (cxl_event_fw_owns_mem_logs(&policy)) { dev_err(mds->cxlds.dev, "FW still in control of Event Logs despite _OSC settings\n"); return -EBUSY; From 9c45dd3684a51ef93afe326b33a16551ee9a74e5 Mon Sep 17 00:00:00 2001 From: Ira Weiny Date: Sun, 11 Oct 2026 07:54:57 +0900 Subject: [PATCH 46/49] cxl/mem: Configure dynamic capacity interrupts Dynamic Capacity Devices (DCD) support extent change notifications through the event log mechanism. The interrupt mailbox commands were extended in CXL 3.1 to support these notifications. Firmware can't configure DCD events to be FW controlled but can retain control of memory events. Configure DCD event log interrupts on devices supporting dynamic capacity. Disable DCD if interrupts are not supported. The DCD event log is not drained here. cxl_event_drain_mask() reports only the logs the driver can service, and the DCD bit is added by the patch introducing DCD event handling. Until then a DCD interrupt wakes the event thread, which finds no log it owns and returns. Care is taken to preserve the interrupt policy set by the FW if FW first has been selected by the BIOS. Based on an original patch by Navneet Singh. Signed-off-by: Ira Weiny Co-developed-by: Anisa Su Signed-off-by: Anisa Su Tested-by: Wonjae Lee Tested-by: Junhee Park Tested-by: Heesoo Kim Reviewed-by: Jonathan Cameron Link: https://patch.msgid.link/20261010225852.10727-7-anisa.su@samsung.com Signed-off-by: Dave Jiang --- drivers/cxl/cxl.h | 14 ++- drivers/cxl/cxlmem.h | 2 + drivers/cxl/pci.c | 188 ++++++++++++++++++++++++++++------- tools/testing/cxl/test/mem.c | 2 +- 4 files changed, 163 insertions(+), 43 deletions(-) diff --git a/drivers/cxl/cxl.h b/drivers/cxl/cxl.h index cab8ce39f465..15356d25369d 100644 --- a/drivers/cxl/cxl.h +++ b/drivers/cxl/cxl.h @@ -192,11 +192,15 @@ static inline int ways_to_eiw(unsigned int ways, u8 *eiw) #define CXLDEV_EVENT_STATUS_WARN BIT(1) #define CXLDEV_EVENT_STATUS_FAIL BIT(2) #define CXLDEV_EVENT_STATUS_FATAL BIT(3) - -#define CXLDEV_EVENT_STATUS_ALL (CXLDEV_EVENT_STATUS_INFO | \ - CXLDEV_EVENT_STATUS_WARN | \ - CXLDEV_EVENT_STATUS_FAIL | \ - CXLDEV_EVENT_STATUS_FATAL) +#define CXLDEV_EVENT_STATUS_DCD BIT(4) +/* + * Logs whose ownership follows native_cxl_error; the DCD log is always + * OS owned. + */ +#define CXLDEV_EVENT_STATUS_STANDARD_LOGS (CXLDEV_EVENT_STATUS_INFO | \ + CXLDEV_EVENT_STATUS_WARN | \ + CXLDEV_EVENT_STATUS_FAIL | \ + CXLDEV_EVENT_STATUS_FATAL) /* CXL rev 3.0 section 8.2.9.2.4; Table 8-52 */ #define CXLDEV_EVENT_INT_MODE_MASK GENMASK(1, 0) diff --git a/drivers/cxl/cxlmem.h b/drivers/cxl/cxlmem.h index b539b5a5c829..bac7e1ac715f 100644 --- a/drivers/cxl/cxlmem.h +++ b/drivers/cxl/cxlmem.h @@ -240,7 +240,9 @@ struct cxl_event_interrupt_policy { u8 warn_settings; u8 failure_settings; u8 fatal_settings; + u8 dcd_settings; } __packed; +#define CXL_EVENT_INT_POLICY_BASE_SIZE 4 /* info, warn, failure, fatal */ /** * struct cxl_event_state - Event log driver state diff --git a/drivers/cxl/pci.c b/drivers/cxl/pci.c index 7b754af2168d..1d94e9ebb1a2 100644 --- a/drivers/cxl/pci.c +++ b/drivers/cxl/pci.c @@ -510,11 +510,22 @@ static bool cxl_alloc_irq_vectors(struct pci_dev *pdev) return true; } +/* Event logs the driver drains: standard logs when native_cxl */ +static u32 cxl_event_drain_mask(struct pci_host_bridge *host_bridge) +{ + if (host_bridge->native_cxl_error) + return CXLDEV_EVENT_STATUS_STANDARD_LOGS; + return 0; +} + static irqreturn_t cxl_event_thread(int irq, void *id) { struct cxl_dev_id *dev_id = id; struct cxl_dev_state *cxlds = dev_id->cxlds; struct cxl_memdev_state *mds = to_cxl_memdev_state(cxlds); + struct pci_host_bridge *host_bridge = + pci_find_host_bridge(to_pci_dev(cxlds->dev)->bus); + u32 mask = cxl_event_drain_mask(host_bridge); u32 status; do { @@ -523,8 +534,8 @@ static irqreturn_t cxl_event_thread(int irq, void *id) * ignore the reserved upper 32 bits */ status = readl(cxlds->regs.status + CXLDEV_DEV_EVENT_STATUS_OFFSET); - /* Ignore logs unknown to the driver */ - status &= CXLDEV_EVENT_STATUS_ALL; + /* Ignore logs unknown to the driver or owned by BIOS */ + status &= mask; if (!status) break; cxl_mem_get_event_records(mds, status); @@ -551,53 +562,81 @@ static int cxl_event_req_irq(struct cxl_dev_state *cxlds, u8 setting) } static int cxl_event_get_int_policy(struct cxl_memdev_state *mds, - struct cxl_event_interrupt_policy *policy) + struct cxl_event_interrupt_policy *policy, + size_t *policy_size) { struct cxl_mailbox *cxl_mbox = &mds->cxlds.cxl_mbox; struct cxl_mbox_cmd mbox_cmd = { .opcode = CXL_MBOX_OP_GET_EVT_INT_POLICY, .payload_out = policy, .size_out = sizeof(*policy), + /* CXL 2.0 firmware omits dcd_settings; accept the shorter reply */ + .min_out = CXL_EVENT_INT_POLICY_BASE_SIZE, + }; + int rc; + + rc = cxl_internal_send_cmd(cxl_mbox, &mbox_cmd); + if (rc < 0) { + dev_err(mds->cxlds.dev, + "Failed to get event interrupt policy : %d", rc); + return rc; + } + + if (policy_size) + *policy_size = mbox_cmd.size_out; + return rc; +} + +static int cxl_event_set_int_policy(struct cxl_memdev_state *mds, + struct cxl_event_interrupt_policy *policy, + size_t policy_size) +{ + struct cxl_mailbox *cxl_mbox = &mds->cxlds.cxl_mbox; + struct cxl_mbox_cmd mbox_cmd = { + .opcode = CXL_MBOX_OP_SET_EVT_INT_POLICY, + .payload_in = policy, + .size_in = policy_size, }; int rc; rc = cxl_internal_send_cmd(cxl_mbox, &mbox_cmd); if (rc < 0) - dev_err(mds->cxlds.dev, - "Failed to get event interrupt policy : %d", rc); - + dev_err(mds->cxlds.dev, "Failed to set event interrupt policy : %d", + rc); return rc; } static int cxl_event_config_msgnums(struct cxl_memdev_state *mds, - struct cxl_event_interrupt_policy *policy) + struct cxl_event_interrupt_policy *policy, + bool native_cxl, size_t policy_size) { - struct cxl_mailbox *cxl_mbox = &mds->cxlds.cxl_mbox; - struct cxl_mbox_cmd mbox_cmd; int rc; - *policy = (struct cxl_event_interrupt_policy) { - .info_settings = CXL_INT_MSI_MSIX, - .warn_settings = CXL_INT_MSI_MSIX, - .failure_settings = CXL_INT_MSI_MSIX, - .fatal_settings = CXL_INT_MSI_MSIX, - }; - - mbox_cmd = (struct cxl_mbox_cmd) { - .opcode = CXL_MBOX_OP_SET_EVT_INT_POLICY, - .payload_in = policy, - .size_in = sizeof(*policy), - }; - - rc = cxl_internal_send_cmd(cxl_mbox, &mbox_cmd); - if (rc < 0) { - dev_err(mds->cxlds.dev, "Failed to set event interrupt policy : %d", - rc); - return rc; + /* memory event policy is left if FW has control */ + if (native_cxl) { + *policy = (struct cxl_event_interrupt_policy) { + .info_settings = CXL_INT_MSI_MSIX, + .warn_settings = CXL_INT_MSI_MSIX, + .failure_settings = CXL_INT_MSI_MSIX, + .fatal_settings = CXL_INT_MSI_MSIX, + .dcd_settings = 0, + }; } + /* + * A CXL 3.0+ device can carry dcd_settings field without DCD command + * support, so size the request by the device's policy_size and only + * enable the DCD interrupt when DCD commands are supported. + */ + if (cxl_dcd_supported(mds)) + policy->dcd_settings = CXL_INT_MSI_MSIX; + + rc = cxl_event_set_int_policy(mds, policy, policy_size); + if (rc) + return rc; + /* Retrieve final interrupt settings */ - return cxl_event_get_int_policy(mds, policy); + return cxl_event_get_int_policy(mds, policy, NULL); } static int cxl_event_irqsetup(struct cxl_memdev_state *mds, @@ -633,6 +672,37 @@ static int cxl_event_irqsetup(struct cxl_memdev_state *mds, return 0; } +static int cxl_irqsetup(struct cxl_memdev_state *mds, + struct cxl_event_interrupt_policy *policy, + bool native_cxl, size_t policy_size) +{ + struct cxl_dev_state *cxlds = &mds->cxlds; + int rc; + + if (native_cxl) { + rc = cxl_event_irqsetup(mds, policy); + if (rc) + return rc; + } + + if (cxl_dcd_supported(mds)) { + rc = cxl_event_req_irq(cxlds, policy->dcd_settings); + if (rc) { + dev_err(cxlds->dev, "Failed to get interrupt for DCD event log\n"); + cxl_disable_dcd(mds); + /* No handler will service the DCD vector; tell the device */ + policy->dcd_settings = CXL_INT_NONE; + rc = cxl_event_set_int_policy(mds, policy, policy_size); + if (rc) + dev_warn(cxlds->dev, + "DCD disabled but its interrupt policy could not be cleared: %d\n", + rc); + } + } + + return 0; +} + static bool cxl_event_int_is_fw(u8 setting) { u8 mode = FIELD_GET(CXLDEV_EVENT_INT_MODE_MASK, setting); @@ -651,46 +721,90 @@ static bool cxl_event_fw_owns_mem_logs(struct cxl_event_interrupt_policy *policy static int cxl_event_config(struct pci_host_bridge *host_bridge, struct cxl_memdev_state *mds, bool irq_avail) { - struct cxl_event_interrupt_policy policy; + struct cxl_event_interrupt_policy policy = { 0 }; + bool native_cxl = host_bridge->native_cxl_error; + size_t policy_size; + u32 mask; int rc; /* * When BIOS maintains CXL error reporting control, it will process * event records. Only one agent can do so. + * + * If BIOS has control of events and DCD is not supported skip event + * configuration. */ - if (!host_bridge->native_cxl_error) + if (!native_cxl && !cxl_dcd_supported(mds)) return 0; if (!irq_avail) { dev_info(mds->cxlds.dev, "No interrupt support, disable event processing.\n"); + if (cxl_dcd_supported(mds)) { + dev_info(mds->cxlds.dev, "DCD requires interrupts, disable DCD\n"); + cxl_disable_dcd(mds); + } return 0; } - rc = cxl_event_get_int_policy(mds, &policy); + rc = cxl_event_get_int_policy(mds, &policy, &policy_size); if (rc) - return rc; + goto err; - if (cxl_event_fw_owns_mem_logs(&policy)) { + /* + * dcd_settings cannot be sent to a device whose policy is too short + * to hold it, so the device would never accept the setting + * cxl_irqsetup() arms the DCD interrupt with. + */ + if (cxl_dcd_supported(mds) && policy_size < sizeof(policy)) { + dev_warn(mds->cxlds.dev, + "DCD supported but interrupt policy is only %zu bytes\n", + policy_size); + cxl_disable_dcd(mds); + /* DCD was the only reason to be here */ + if (!native_cxl) + return 0; + } + + if (native_cxl && cxl_event_fw_owns_mem_logs(&policy)) { dev_err(mds->cxlds.dev, "FW still in control of Event Logs despite _OSC settings\n"); return -EBUSY; } - rc = cxl_event_config_msgnums(mds, &policy); + rc = cxl_event_config_msgnums(mds, &policy, native_cxl, policy_size); if (rc) - return rc; + goto err; rc = cxl_mem_alloc_event_buf(mds); if (rc) return rc; - rc = cxl_event_irqsetup(mds, &policy); + rc = cxl_irqsetup(mds, &policy, native_cxl, policy_size); if (rc) return rc; - cxl_mem_get_event_records(mds, CXLDEV_EVENT_STATUS_ALL); + mask = cxl_event_drain_mask(host_bridge); + if (mask) + cxl_mem_get_event_records(mds, mask); + + dev_dbg(mds->cxlds.dev, "Event config : %s DCD %s\n", + native_cxl ? "OS" : "BIOS", + cxl_dcd_supported(mds) ? "supported" : "not supported"); return 0; + +err: + /* + * Failed to Get/Set interrupt policy for DCD while BIOS owns the + * other logs. Disable DCD and continue. + */ + if (!native_cxl) { + dev_warn(mds->cxlds.dev, + "Event interrupt policy failed (%d), disabling DCD\n", rc); + cxl_disable_dcd(mds); + return 0; + } + return rc; } static int cxl_pci_type3_init_mailbox(struct cxl_dev_state *cxlds) diff --git a/tools/testing/cxl/test/mem.c b/tools/testing/cxl/test/mem.c index 7b756000a1a6..c740372bdaaa 100644 --- a/tools/testing/cxl/test/mem.c +++ b/tools/testing/cxl/test/mem.c @@ -1818,7 +1818,7 @@ static int cxl_mock_mem_probe(struct platform_device *pdev) if (rc) dev_dbg(dev, "No CXL FWCTL setup\n"); - cxl_mem_get_event_records(mds, CXLDEV_EVENT_STATUS_ALL); + cxl_mem_get_event_records(mds, CXLDEV_EVENT_STATUS_STANDARD_LOGS); cxl_mock_test_feat_init(mdata); return 0; From c801107605f39b5a8632fbc835e0da65a6fb6b51 Mon Sep 17 00:00:00 2001 From: Ira Weiny Date: Sun, 11 Oct 2026 07:54:58 +0900 Subject: [PATCH 47/49] cxl/core: Enforce partition order/simplify partition calls Device partitions share one flat DPA space in a fixed order: static volatile capacity at DPA 0, static persistent capacity immediately above it, and dynamic capacity above all static capacity. The static pair is mandated by CXL r4.0 8.2.10.9.2.1 "Get Partition Info", Table 8-310: the device "shall provide this volatile capacity starting at DPA 0" and "shall provide this persistent capacity starting at the DPA immediately following the volatile capacity". Dynamic capacity has no such rule. Table 8-347 only requires the region base to be 256M aligned, so placing it directly above static capacity is a Linux requirement, enforced by cxl_configure_dcd(). DPA 0 end +---------------+---------------+---------------------------+ | ram | pmem | dynamic_ram_1 | +---------------+---------------+---------------------------+ part[0] part[1] part[2] Signed-off-by: Ira Weiny Co-developed-by: Anisa Su Signed-off-by: Anisa Su Tested-by: Wonjae Lee Tested-by: Junhee Park Tested-by: Heesoo Kim Reviewed-by: Dave Jiang Reviewed-by: Jonathan Cameron Reviewed-by: Alison Schofield Link: https://patch.msgid.link/20261010225852.10727-8-anisa.su@samsung.com Signed-off-by: Dave Jiang --- drivers/cxl/core/hdm.c | 10 ++++++++++ drivers/cxl/core/memdev.c | 32 +++++++++----------------------- drivers/cxl/cxlmem.h | 12 ------------ drivers/cxl/mem.c | 2 +- include/cxl/cxl.h | 20 ++++++++++++++++++++ 5 files changed, 40 insertions(+), 36 deletions(-) diff --git a/drivers/cxl/core/hdm.c b/drivers/cxl/core/hdm.c index 379540246333..ec5742debb52 100644 --- a/drivers/cxl/core/hdm.c +++ b/drivers/cxl/core/hdm.c @@ -470,6 +470,16 @@ int cxl_dpa_setup(struct cxl_dev_state *cxlds, const struct cxl_dpa_info *info) return 0; } + /* Verify partitions are in expected order. */ + for (int i = 1; i < info->nr_partitions; i++) { + if (info->part[i].mode < info->part[i - 1].mode) { + dev_err(dev, "Partition order mismatch: %d (%s) follows %d (%s)\n", + i, cxl_mode_name(info->part[i].mode), + i - 1, cxl_mode_name(info->part[i - 1].mode)); + return -EINVAL; + } + } + cxlds->dpa_res = DEFINE_RES_MEM(0, info->size); for (int i = 0; i < info->nr_partitions; i++) { diff --git a/drivers/cxl/core/memdev.c b/drivers/cxl/core/memdev.c index 71493231f5e5..787988e92965 100644 --- a/drivers/cxl/core/memdev.c +++ b/drivers/cxl/core/memdev.c @@ -77,20 +77,12 @@ static ssize_t label_storage_size_show(struct device *dev, } static DEVICE_ATTR_RO(label_storage_size); -static resource_size_t cxl_ram_size(struct cxl_dev_state *cxlds) -{ - /* Static RAM is only expected at partition 0. */ - if (cxlds->part[0].mode != CXL_PARTMODE_RAM) - return 0; - return resource_size(&cxlds->part[0].res); -} - static ssize_t ram_size_show(struct device *dev, struct device_attribute *attr, char *buf) { struct cxl_memdev *cxlmd = to_cxl_memdev(dev); struct cxl_dev_state *cxlds = cxlmd->cxlds; - unsigned long long len = cxl_ram_size(cxlds); + unsigned long long len = cxl_part_size(cxlds, CXL_PARTMODE_RAM); return sysfs_emit(buf, "%#llx\n", len); } @@ -103,7 +95,7 @@ static ssize_t pmem_size_show(struct device *dev, struct device_attribute *attr, { struct cxl_memdev *cxlmd = to_cxl_memdev(dev); struct cxl_dev_state *cxlds = cxlmd->cxlds; - unsigned long long len = cxl_pmem_size(cxlds); + unsigned long long len = cxl_part_size(cxlds, CXL_PARTMODE_PMEM); return sysfs_emit(buf, "%#llx\n", len); } @@ -426,10 +418,11 @@ static struct attribute *cxl_memdev_attributes[] = { NULL, }; -static struct cxl_dpa_perf *to_pmem_perf(struct cxl_dev_state *cxlds) +static struct cxl_dpa_perf *part_perf(struct cxl_dev_state *cxlds, + enum cxl_partition_mode mode) { for (int i = 0; i < cxlds->nr_partitions; i++) - if (cxlds->part[i].mode == CXL_PARTMODE_PMEM) + if (cxlds->part[i].mode == mode) return &cxlds->part[i].perf; return NULL; } @@ -440,7 +433,7 @@ static ssize_t pmem_qos_class_show(struct device *dev, struct cxl_memdev *cxlmd = to_cxl_memdev(dev); struct cxl_dev_state *cxlds = cxlmd->cxlds; - return sysfs_emit(buf, "%d\n", to_pmem_perf(cxlds)->qos_class); + return sysfs_emit(buf, "%d\n", part_perf(cxlds, CXL_PARTMODE_PMEM)->qos_class); } static struct device_attribute dev_attr_pmem_qos_class = @@ -452,20 +445,13 @@ static struct attribute *cxl_memdev_pmem_attributes[] = { NULL, }; -static struct cxl_dpa_perf *to_ram_perf(struct cxl_dev_state *cxlds) -{ - if (cxlds->part[0].mode != CXL_PARTMODE_RAM) - return NULL; - return &cxlds->part[0].perf; -} - static ssize_t ram_qos_class_show(struct device *dev, struct device_attribute *attr, char *buf) { struct cxl_memdev *cxlmd = to_cxl_memdev(dev); struct cxl_dev_state *cxlds = cxlmd->cxlds; - return sysfs_emit(buf, "%d\n", to_ram_perf(cxlds)->qos_class); + return sysfs_emit(buf, "%d\n", part_perf(cxlds, CXL_PARTMODE_RAM)->qos_class); } static struct device_attribute dev_attr_ram_qos_class = @@ -501,7 +487,7 @@ static umode_t cxl_ram_visible(struct kobject *kobj, struct attribute *a, int n) { struct device *dev = kobj_to_dev(kobj); struct cxl_memdev *cxlmd = to_cxl_memdev(dev); - struct cxl_dpa_perf *perf = to_ram_perf(cxlmd->cxlds); + struct cxl_dpa_perf *perf = part_perf(cxlmd->cxlds, CXL_PARTMODE_RAM); if (a == &dev_attr_ram_qos_class.attr && (!perf || perf->qos_class == CXL_QOS_CLASS_INVALID)) @@ -520,7 +506,7 @@ static umode_t cxl_pmem_visible(struct kobject *kobj, struct attribute *a, int n { struct device *dev = kobj_to_dev(kobj); struct cxl_memdev *cxlmd = to_cxl_memdev(dev); - struct cxl_dpa_perf *perf = to_pmem_perf(cxlmd->cxlds); + struct cxl_dpa_perf *perf = part_perf(cxlmd->cxlds, CXL_PARTMODE_PMEM); if (a == &dev_attr_pmem_qos_class.attr && (!perf || perf->qos_class == CXL_QOS_CLASS_INVALID)) diff --git a/drivers/cxl/cxlmem.h b/drivers/cxl/cxlmem.h index bac7e1ac715f..35268d1c387c 100644 --- a/drivers/cxl/cxlmem.h +++ b/drivers/cxl/cxlmem.h @@ -412,18 +412,6 @@ struct cxl_security_state { #define CXL_MAX_DC_PARTITIONS 8 -static inline resource_size_t cxl_pmem_size(struct cxl_dev_state *cxlds) -{ - /* - * Static PMEM may be at partition index 0 when there is no static RAM - * capacity. - */ - for (int i = 0; i < cxlds->nr_partitions; i++) - if (cxlds->part[i].mode == CXL_PARTMODE_PMEM) - return resource_size(&cxlds->part[i].res); - return 0; -} - static inline resource_size_t cxl_part_size(struct cxl_dev_state *cxlds, enum cxl_partition_mode mode) { diff --git a/drivers/cxl/mem.c b/drivers/cxl/mem.c index 798e5c369cfc..9c4ef401cbad 100644 --- a/drivers/cxl/mem.c +++ b/drivers/cxl/mem.c @@ -139,7 +139,7 @@ static int cxl_mem_probe(struct device *dev) return -ENXIO; } - if (cxl_pmem_size(cxlds) && IS_ENABLED(CONFIG_CXL_PMEM)) { + if (cxl_part_size(cxlds, CXL_PARTMODE_PMEM) && IS_ENABLED(CONFIG_CXL_PMEM)) { rc = devm_cxl_add_nvdimm(dev, parent_port, cxlmd); if (rc) { if (rc == -ENODEV) diff --git a/include/cxl/cxl.h b/include/cxl/cxl.h index 728decda57fd..1c04a7823f64 100644 --- a/include/cxl/cxl.h +++ b/include/cxl/cxl.h @@ -130,6 +130,26 @@ struct cxl_dpa_perf { int qos_class; }; +/* + * Partitions share one flat DPA space in a fixed order: + * + * DPA 0 end + * +---------------+---------------+---------------------------+ + * | ram | pmem | dynamic_ram_1 | + * +---------------+---------------+---------------------------+ + * part[0] part[1] part[2] + * + * The static ram and pmem partition order is mandated by CXL r4.0 + * 8.2.10.9.2.1 "Get Partition Info" (opcode 4100h), Table 8-310: volatile + * capacity starts at DPA 0 and persistent capacity starts at the DPA + * immediately following it. A dynamic partition has no such + * requirement -- 8.2.10.9.9.1 "Get Dynamic Capacity Configuration" + * (opcode 4800h), Table 8-347, only requires the DC Region Base to be + * 256MB aligned -- but Linux follows that precedent and requires it to + * start at the DPA immediately following static capacity: after pmem, + * after ram on a device with no pmem, or at DPA 0 on a device with no + * static capacity at all. cxl_configure_dcd() enforces it. + */ enum cxl_partition_mode { CXL_PARTMODE_RAM, CXL_PARTMODE_PMEM, From e4478834aa09fd28abaaf774a4bcaa4bbe6804e5 Mon Sep 17 00:00:00 2001 From: Anisa Su Date: Sun, 11 Oct 2026 07:54:59 +0900 Subject: [PATCH 48/49] Documentation/cxl: Document DPA partition layout and ordering rules DC Partitions complicate DPA ordering. Add a DPA Partitions section to cxl-driver.rst describing spec-mandated and Linux requirements for the layout. maturity-map.rst breaks the DCD entry into sub-items so the map shows what this series lands: partition enumeration is [1], event handling and DC-backed DAX regions remain [0] until the follow-on series enables them. The parent stays [0]; nothing is usable yet. Suggested-by: Gregory Price Signed-off-by: Anisa Su Tested-by: Wonjae Lee Tested-by: Junhee Park Tested-by: Heesoo Kim Reviewed-by: Jonathan Cameron Reviewed-by: Davidlohr Bueso Reviewed-by: Alison Schofield Reviewed-by: Gregory Price (Meta) Link: https://patch.msgid.link/20261010225852.10727-9-anisa.su@samsung.com Signed-off-by: Dave Jiang --- .../driver-api/cxl/linux/cxl-driver.rst | 35 +++++++++++++++++++ Documentation/driver-api/cxl/maturity-map.rst | 4 +++ 2 files changed, 39 insertions(+) diff --git a/Documentation/driver-api/cxl/linux/cxl-driver.rst b/Documentation/driver-api/cxl/linux/cxl-driver.rst index dd6dd17dc536..212df4fb8d63 100644 --- a/Documentation/driver-api/cxl/linux/cxl-driver.rst +++ b/Documentation/driver-api/cxl/linux/cxl-driver.rst @@ -181,6 +181,41 @@ A Memory Device is a discrete base object that is not a port. While the physical device it belongs to may also host an `endpoint`, the relationship between an `endpoint` and a `memdev` is not captured in sysfs. +DPA Partitions +~~~~~~~~~~~~~~ +A memory device presents its capacity as one flat `Device Physical Address` +(DPA) space divided into `partitions`, which Linux lays out in a fixed +order:: + + DPA 0 end + +---------------+---------------+---------------------------+ + | ram | pmem | dynamic_ram_1 | + +---------------+---------------+---------------------------+ + part[0] part[1] part[2] + +Part of that order is required by the CXL specification and part of it is a +Linux choice. + +The `ram` and `pmem` order is mandated. CXL r4.0 section 8.2.10.9.2.1 "Get +Partition Info" (4100h), Table 8-310, mandates that volatile capacity starts +at DPA 0 and pmem starts at the DPA immediately following it. + +Dynamic Capacity partitions only need to be 256MB aligned according to +CXL r4.0 section 8.2.10.9.9.1 "Get Dynamic Capacity Configuration" +(opcode 4800h), Table 8-347. So a device could leave a gap between ram/pmem +(static) capacity and its first DC partition, or between one DC partition +and the next. + +Linux chooses not to support gaps between static and dynamic capacity: the +first DC partition must begin at the DPA immediately following static +capacity -- after pmem, after ram on a device with no pmem, or at DPA 0 on +a device with no static capacity at all. + +Currently, only one dynamic partition is supported. A device may report up +to eight (CXL r4.0 Table 8-346); Linux configures the first as its +`dynamic_ram_1` partition. Gaps between DC partitions are not checked, since +only the first is used. + Port Relationships ~~~~~~~~~~~~~~~~~~ In our example described above, there are four host bridges attached to the diff --git a/Documentation/driver-api/cxl/maturity-map.rst b/Documentation/driver-api/cxl/maturity-map.rst index 282c1102dd81..fefd31899229 100644 --- a/Documentation/driver-api/cxl/maturity-map.rst +++ b/Documentation/driver-api/cxl/maturity-map.rst @@ -150,6 +150,10 @@ Memory-pooling * [1] Hotplug of LDs (via PCI hotplug) * [0] Dynamic Capacity Device (DCD) Support + * [1] DC partition enumeration / configuration (Get DC Config, CDAT DSMAS, event interrupts) + * [0] Extent add / release event handling + * [0] DC-backed DAX regions + Multi-host sharing ------------------ From b94ba9a1f2d5c57f2eec67d502bb8fdd1e53c87a Mon Sep 17 00:00:00 2001 From: Guixin Liu Date: Tue, 22 Sep 2026 16:22:14 +0800 Subject: [PATCH 49/49] cxl/events: Test CME counter expiry with equality, not bitwise AND The Memory Event Type field in the General Media and DRAM event records is an enumeration, not a bitmask. The validity checks for the advanced programmable CME counter expiration event (05h) test the field with a bitwise AND, so types 01h, 03h, 04h and 06h match it as well. A device reporting one of those types trips WARN_ON_ONCE once the general media or DRAM tracepoint is enabled, which is the case anywhere rasdaemon collects CXL events. Under panic_on_warn=1 that kills the machine. Compare the type field for equality instead. Type 05h records that do violate the spec conditions still warn. Fixes: cd3b36cfc659 ("cxl/events: Add extra validity checks for corrected memory error count in General Media Event Record") Fixes: d8145bb8af5c ("cxl/events: Add extra validity checks for CVME count in DRAM Event Record") Cc: stable@vger.kernel.org Assisted-by: LLM Signed-off-by: Guixin Liu Reviewed-by: Dave Jiang Reviewed-by: Jonathan Cameron Link: https://patch.msgid.link/20260922082214.3526355-1-kanie@linux.alibaba.com Signed-off-by: Dave Jiang --- drivers/cxl/core/mbox.c | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/drivers/cxl/core/mbox.c b/drivers/cxl/core/mbox.c index 55828a836c01..01341f524a93 100644 --- a/drivers/cxl/core/mbox.c +++ b/drivers/cxl/core/mbox.c @@ -942,11 +942,11 @@ void cxl_event_trace_record(struct cxl_memdev *cxlmd, if (evt->gen_media.media_hdr.descriptor & CXL_GMER_EVT_DESC_THRESHOLD_EVENT) - WARN_ON_ONCE((evt->gen_media.media_hdr.type & + WARN_ON_ONCE((evt->gen_media.media_hdr.type == CXL_GMER_MEM_EVT_TYPE_AP_CME_COUNTER_EXPIRE) && !get_unaligned_le24(evt->gen_media.cme_count)); else - WARN_ON_ONCE(evt->gen_media.media_hdr.type & + WARN_ON_ONCE(evt->gen_media.media_hdr.type == CXL_GMER_MEM_EVT_TYPE_AP_CME_COUNTER_EXPIRE); trace_cxl_general_media(cxlmd, type, cxlr, hpa, @@ -957,11 +957,11 @@ void cxl_event_trace_record(struct cxl_memdev *cxlmd, if (evt->dram.media_hdr.descriptor & CXL_GMER_EVT_DESC_THRESHOLD_EVENT) - WARN_ON_ONCE((evt->dram.media_hdr.type & + WARN_ON_ONCE((evt->dram.media_hdr.type == CXL_DER_MEM_EVT_TYPE_AP_CME_COUNTER_EXPIRE) && !get_unaligned_le24(evt->dram.cvme_count)); else - WARN_ON_ONCE(evt->dram.media_hdr.type & + WARN_ON_ONCE(evt->dram.media_hdr.type == CXL_DER_MEM_EVT_TYPE_AP_CME_COUNTER_EXPIRE); trace_cxl_dram(cxlmd, type, cxlr, hpa, hpa_alias,