* [PATCH v4 0/2] cxl/edac: Fix event record lookups for memory repair
@ 2026-11-02 12:32 Jongwon Lee
2026-11-02 12:32 ` [PATCH v4 1/2] cxl/edac: Read cached DRAM event records under the xarray lock Jongwon Lee
2026-11-02 12:32 ` [PATCH v4 2/2] cxl/edac: Mask DPA flags in the event record cache key Jongwon Lee
0 siblings, 2 replies; 3+ messages in thread
From: Jongwon Lee @ 2026-11-02 12:32 UTC (permalink / raw)
To: Davidlohr Bueso, Jonathan Cameron, Dave Jiang, Alison Schofield,
Vishal Verma, Dan Williams
Cc: Ira Weiny, Li Ming, Richard Cheng, linux-cxl, linux-kernel
Patch 1 fixes a use-after-free between the repair path and the event
path. Sashiko's review of v2 pointed out that the repair path reads
cached DRAM event records with no lock that keeps the event path from
freeing them [1]. The race was already there in cxl_find_rec_dram() and
cxl_mem_perform_sparing(). Patch 1 does the DRAM lookup under the xarray
lock and copies the matching record out for the sparing path.
Patch 2 is the DPA flag masking fix from v2. It also does the General
Media lookup under the same lock, since v2 added a read of that record.
I used xa_lock() instead of RCU freeing. The cached records are copies
of the spec-defined event structures, so kfree_rcu() would need a
wrapper struct with an rcu_head, and the lookups only compare a few
fields. Taking device_lock() in the repair path, as the event path does,
looked like it could deadlock against unbind, where the EDAC sysfs
files are removed with device_lock() held.
Changes in v4:
- Patch 1: copy the whole struct cxl_event_dram out instead of only
validity_flags (Richard Cheng)
- Patch 2: make cxl_rec_key() and cxl_rec_not_repairable() static
inline (Richard Cheng)
- Cover letter: describe patch 1 first (Richard Cheng)
Changes in v3:
- New patch 1 for the record lifetime race (Sashiko)
- Patch 2: General Media lookup under the xarray lock, returns bool
Changes in v2:
- Moved the Claude Code note below the --- (Dave Jiang)
v3: https://lore.kernel.org/r/20261101013418.87326-1-ai@linux.com
v2: https://lore.kernel.org/r/20261031161121.15948-1-ai@linux.com
v1: https://lore.kernel.org/r/20261030072721.31970-1-ai@linux.com
Testing, QEMU master (b574eaf) with a CXL type 3 device and a committed
region, on top of d89e1d186e. Debug options: KASAN, PROVE_LOCKING,
PROVE_RAW_LOCK_NESTING, PROVE_RCU, DEBUG_ATOMIC_SLEEP, DEBUG_LIST,
UBSAN_BOUNDS, KMEMLEAK, hung task and soft lockup detection, booted with
preempt=full.
- Repair table: DRAM events at 0x1001, 0x2000 and 0x3002 and General
Media events at 0x5001 and 0x6002, then a repair request on all five
mem_repair devices for eight DPAs. With v4, 0x1000 and 0x2000 succeed
for all five repair types and 0x5000 succeeds for PPR only. 0x3000
and 0x6000 (NOT_REPAIRABLE set), 0x7000 (no record), 0x1001 and
0x3002 are rejected with -EINVAL. Unpatched, 0x1000 and the PPR
request at 0x5000 fail, and 0x1001 and 0x3002 are accepted.
- Race test with a test-only mdelay(20): the host injects DRAM and
General Media events with no gap, repeating some DPAs so that
xa_store() replaces and frees records and adding enough distinct
DPAs to reach the overflow path, while four guest loops request
repairs on all five devices. Unpatched, delay after xa_load() and
after the lookup in the sparing path: KASAN slab-use-after-free in
cxl_do_mem_sparing() after 120 s, a 2-byte read of validity_flags in
a record freed by cxl_store_rec_dram(). v4, delay after both
xa_load() calls inside the locked section and after the lookup in
the sparing path: 600 s, 11070 repair requests, 261973 events, no
report.
- The same race test without the delay: v4 for 1800 s, 56460 repair
requests and 786023 events; unpatched for 900 s, 24150 repair
requests and 301280 events. Neither kernel reported anything.
- Unbind test: two repair loops and the event injection as above for
600 s while a loop unbinds cxl_mem, waits 2 s, binds it and waits
3 s. v4: 78 unbind/bind cycles, 7248 repair requests, no report,
mem0 bound at the end.
- kmemleak scan at the end of each run: no unreferenced objects.
- Builds: W=1 after each patch, sparse, clang W=1 for x86_64 and arm64,
and CONFIG_CXL_EDAC_MEM_REPAIR=n, with no new warnings. The series
applies to cxl/next (cf15f9f236) and builds there.
This series was developed with Claude Code.
[1] https://lore.kernel.org/r/sashiko-outbox-192042@kernel.org
Jongwon Lee (2):
cxl/edac: Read cached DRAM event records under the xarray lock
cxl/edac: Mask DPA flags in the event record cache key
drivers/cxl/core/edac.c | 115 ++++++++++++++++++++++++++--------------
1 file changed, 76 insertions(+), 39 deletions(-)
base-commit: cf15f9f2361dd4002203716b7b878393c0eee4bf
--
2.55.0
^ permalink raw reply [flat|nested] 3+ messages in thread
* [PATCH v4 1/2] cxl/edac: Read cached DRAM event records under the xarray lock
2026-11-02 12:32 [PATCH v4 0/2] cxl/edac: Fix event record lookups for memory repair Jongwon Lee
@ 2026-11-02 12:32 ` Jongwon Lee
2026-11-02 12:32 ` [PATCH v4 2/2] cxl/edac: Mask DPA flags in the event record cache key Jongwon Lee
1 sibling, 0 replies; 3+ messages in thread
From: Jongwon Lee @ 2026-11-02 12:32 UTC (permalink / raw)
To: Davidlohr Bueso, Jonathan Cameron, Dave Jiang, Alison Schofield,
Vishal Verma, Dan Williams
Cc: Ira Weiny, Li Ming, Richard Cheng, linux-cxl, linux-kernel
The event path caches DRAM event records in an xarray and frees a record
when xa_store() replaces it, when it expires and when the cache
overflows. It runs with device_lock() and cxl_rwsem held for read.
The repair path looks records up with cxl_find_rec_dram() while holding
only cxl_rwsem for read, so the two paths can run at the same time.
cxl_find_rec_dram() reads the record after xa_load() returns, and
cxl_mem_perform_sparing() reads validity_flags from the returned record
again later. Both reads can hit a record the event path has just freed.
Move the lookup into __cxl_find_rec_dram(), called with the xarray lock
held, and make cxl_find_rec_dram() return whether a record matched and
copy the matching record into a buffer the caller passes in. The PPR
path only needs the match and passes NULL. No caller keeps a pointer to
the record after the lock is dropped. The event path frees a record
only after xa_store() or xa_erase() has removed it from the xarray, and
both take the same lock.
Fixes: 0b5ccb0de1e2 ("cxl/edac: Support for finding memory operation attributes from the current boot")
Fixes: 588ca944c277 ("cxl/edac: Add CXL memory device memory sparing control feature")
Reported-by: Sashiko <sashiko-bot@kernel.org>
Closes: https://lore.kernel.org/r/sashiko-outbox-192042@kernel.org
Signed-off-by: Jongwon Lee <ai@linux.com>
---
drivers/cxl/core/edac.c | 72 ++++++++++++++++++++++++++++-------------
1 file changed, 49 insertions(+), 23 deletions(-)
diff --git a/drivers/cxl/core/edac.c b/drivers/cxl/core/edac.c
index 34b81e8dfb..c5c8dda20e 100644
--- a/drivers/cxl/core/edac.c
+++ b/drivers/cxl/core/edac.c
@@ -938,18 +938,17 @@ cxl_find_rec_gen_media(struct cxl_memdev *cxlmd,
return NULL;
}
+/* Caller holds the xarray lock. The record may be freed once it is dropped. */
static struct cxl_event_dram *
-cxl_find_rec_dram(struct cxl_memdev *cxlmd,
- struct cxl_mem_repair_attrbs *attrbs)
+__cxl_find_rec_dram(struct xarray *rec_xarray,
+ struct cxl_mem_repair_attrbs *attrbs)
{
- struct cxl_mem_err_rec *array_rec = cxlmd->err_rec_array;
struct cxl_event_dram *rec;
u16 validity_flags;
- if (!array_rec)
- return NULL;
+ lockdep_assert_held(&rec_xarray->xa_lock);
- rec = xa_load(&array_rec->rec_dram, attrbs->dpa);
+ rec = xa_load(rec_xarray, attrbs->dpa);
if (!rec)
return NULL;
@@ -1029,6 +1028,32 @@ cxl_find_rec_dram(struct cxl_memdev *cxlmd,
return NULL;
}
+/*
+ * cxl_store_rec_dram() replaces and frees records while the repair path only
+ * holds cxl_rwsem for read, so look the record up and read it under the
+ * xarray lock, and hand back a copy of the record.
+ */
+static bool cxl_find_rec_dram(struct cxl_memdev *cxlmd,
+ struct cxl_mem_repair_attrbs *attrbs,
+ struct cxl_event_dram *out)
+{
+ struct cxl_mem_err_rec *array_rec = cxlmd->err_rec_array;
+ struct cxl_event_dram *rec;
+ bool found;
+
+ if (!array_rec)
+ return false;
+
+ xa_lock(&array_rec->rec_dram);
+ rec = __cxl_find_rec_dram(&array_rec->rec_dram, attrbs);
+ found = rec;
+ if (rec && out)
+ *out = *rec;
+ xa_unlock(&array_rec->rec_dram);
+
+ return found;
+}
+
#define CXL_MAX_STORAGE_DAYS 10
#define CXL_MAX_STORAGE_TIME_SECS (CXL_MAX_STORAGE_DAYS * 24 * 60 * 60)
@@ -1298,9 +1323,9 @@ cxl_mem_sparing_get_attrbs(struct cxl_mem_sparing_context *cxl_sparing_ctx)
return 0;
}
-static struct cxl_event_dram *
-cxl_mem_get_rec_dram(struct cxl_memdev *cxlmd,
- struct cxl_mem_sparing_context *ctx)
+static bool cxl_mem_get_rec_dram(struct cxl_memdev *cxlmd,
+ struct cxl_mem_sparing_context *ctx,
+ struct cxl_event_dram *rec)
{
struct cxl_mem_repair_attrbs attrbs = { 0 };
@@ -1332,10 +1357,10 @@ cxl_mem_get_rec_dram(struct cxl_memdev *cxlmd,
attrbs.repair_type = CXL_RANK_SPARING;
break;
default:
- return NULL;
+ return false;
}
- return cxl_find_rec_dram(cxlmd, &attrbs);
+ return cxl_find_rec_dram(cxlmd, &attrbs, rec);
}
static int
@@ -1344,7 +1369,8 @@ cxl_mem_perform_sparing(struct device *dev,
{
struct cxl_memdev *cxlmd = cxl_sparing_ctx->cxlmd;
struct cxl_memdev_sparing_in_payload sparing_pi;
- struct cxl_event_dram *rec = NULL;
+ struct cxl_event_dram rec;
+ bool rec_found = false;
u16 validity_flags = 0;
int ret;
@@ -1362,11 +1388,14 @@ cxl_mem_perform_sparing(struct device *dev,
return -EBUSY;
} else {
if (cxl_is_memdev_memory_online(cxlmd)) {
- rec = cxl_mem_get_rec_dram(cxlmd, cxl_sparing_ctx);
- if (!rec)
+ rec_found = cxl_mem_get_rec_dram(cxlmd, cxl_sparing_ctx,
+ &rec);
+ if (!rec_found)
return -EINVAL;
- if (!get_unaligned_le16(rec->media_hdr.validity_flags))
+ validity_flags =
+ get_unaligned_le16(rec.media_hdr.validity_flags);
+ if (!validity_flags)
return -EINVAL;
}
}
@@ -1376,13 +1405,10 @@ cxl_mem_perform_sparing(struct device *dev,
if (cxl_sparing_ctx->persist_mode)
sparing_pi.flags |= CXL_SET_HARD_SPARING(1);
- if (rec)
- validity_flags = get_unaligned_le16(rec->media_hdr.validity_flags);
-
switch (cxl_sparing_ctx->repair_type) {
case EDAC_REPAIR_CACHELINE_SPARING:
sparing_pi.column = cpu_to_le16(cxl_sparing_ctx->column);
- if (!rec || (validity_flags & CXL_DER_VALID_SUB_CHANNEL)) {
+ if (!rec_found || (validity_flags & CXL_DER_VALID_SUB_CHANNEL)) {
sparing_pi.flags |= CXL_SET_SPARING_SUB_CHNL_VALID(1);
sparing_pi.sub_channel = cxl_sparing_ctx->sub_channel;
}
@@ -1399,9 +1425,9 @@ cxl_mem_perform_sparing(struct device *dev,
fallthrough;
default:
sparing_pi.channel = cxl_sparing_ctx->channel;
- if ((rec && (validity_flags & CXL_DER_VALID_NIBBLE)) ||
- (!rec && (!cxl_sparing_ctx->nibble_mask ||
- (cxl_sparing_ctx->nibble_mask & 0xFFFFFF)))) {
+ if ((rec_found && (validity_flags & CXL_DER_VALID_NIBBLE)) ||
+ (!rec_found && (!cxl_sparing_ctx->nibble_mask ||
+ (cxl_sparing_ctx->nibble_mask & 0xFFFFFF)))) {
sparing_pi.flags |= CXL_SET_SPARING_NIB_MASK_VALID(1);
put_unaligned_le24(cxl_sparing_ctx->nibble_mask,
sparing_pi.nibble_mask);
@@ -1814,7 +1840,7 @@ static int cxl_mem_perform_ppr(struct cxl_ppr_context *cxl_ppr_ctx)
attrbs.repair_type = CXL_PPR;
attrbs.dpa = cxl_ppr_ctx->dpa;
attrbs.nibble_mask = cxl_ppr_ctx->nibble_mask;
- if (!cxl_find_rec_dram(cxlmd, &attrbs) &&
+ if (!cxl_find_rec_dram(cxlmd, &attrbs, NULL) &&
!cxl_find_rec_gen_media(cxlmd, &attrbs))
return -EINVAL;
}
--
2.55.0
^ permalink raw reply [flat|nested] 3+ messages in thread* [PATCH v4 2/2] cxl/edac: Mask DPA flags in the event record cache key
2026-11-02 12:32 [PATCH v4 0/2] cxl/edac: Fix event record lookups for memory repair Jongwon Lee
2026-11-02 12:32 ` [PATCH v4 1/2] cxl/edac: Read cached DRAM event records under the xarray lock Jongwon Lee
@ 2026-11-02 12:32 ` Jongwon Lee
1 sibling, 0 replies; 3+ messages in thread
From: Jongwon Lee @ 2026-11-02 12:32 UTC (permalink / raw)
To: Davidlohr Bueso, Jonathan Cameron, Dave Jiang, Alison Schofield,
Vishal Verma, Dan Williams
Cc: Ira Weiny, Li Ming, Richard Cheng, linux-cxl, linux-kernel
The low bits of the DPA in a General Media or DRAM event record are
flags: bit 0 is VOLATILE and bit 1 is NOT_REPAIRABLE. The trace events
report the DPA with CXL_DPA_MASK applied, but cxl_store_rec_gen_media()
and cxl_store_rec_dram() use the raw value as the xarray key.
User space reads the DPA from the trace event and writes it to the
mem_repair dpa attribute. If the record had any flag set, xa_load()
misses it and the repair fails with -EINVAL. The raw key also lets a
repair go through for a record the device marked NOT_REPAIRABLE, if the
DPA is written with bit 1 set.
Key the records by the masked DPA and skip records marked
NOT_REPAIRABLE. cxl_find_rec_gen_media() now reads the record, so do
the lookup under the xarray lock as cxl_find_rec_dram() does, and
return a bool since its only caller tests the result.
Fixes: 0b5ccb0de1e2 ("cxl/edac: Support for finding memory operation attributes from the current boot")
Signed-off-by: Jongwon Lee <ai@linux.com>
---
drivers/cxl/core/edac.c | 43 ++++++++++++++++++++++++++---------------
1 file changed, 27 insertions(+), 16 deletions(-)
diff --git a/drivers/cxl/core/edac.c b/drivers/cxl/core/edac.c
index c5c8dda20e..a5f198b561 100644
--- a/drivers/cxl/core/edac.c
+++ b/drivers/cxl/core/edac.c
@@ -918,24 +918,37 @@ struct cxl_mem_repair_attrbs {
enum cxl_mem_repair_type repair_type;
};
-static struct cxl_event_gen_media *
-cxl_find_rec_gen_media(struct cxl_memdev *cxlmd,
- struct cxl_mem_repair_attrbs *attrbs)
+/*
+ * The low bits of the event record DPA are flags (VOLATILE, NOT_REPAIRABLE).
+ * Records are looked up by the DPA user space writes back, which comes from
+ * the trace event and has the flags masked off, so key them the same way.
+ */
+static inline unsigned long cxl_rec_key(const struct cxl_event_media_hdr *hdr)
+{
+ return le64_to_cpu(hdr->phys_addr) & CXL_DPA_MASK;
+}
+
+static inline bool cxl_rec_not_repairable(const struct cxl_event_media_hdr *hdr)
+{
+ return le64_to_cpu(hdr->phys_addr) & CXL_DPA_NOT_REPAIRABLE;
+}
+
+static bool cxl_find_rec_gen_media(struct cxl_memdev *cxlmd,
+ struct cxl_mem_repair_attrbs *attrbs)
{
struct cxl_mem_err_rec *array_rec = cxlmd->err_rec_array;
struct cxl_event_gen_media *rec;
+ bool found;
- if (!array_rec)
- return NULL;
+ if (!array_rec || attrbs->repair_type != CXL_PPR)
+ return false;
+ xa_lock(&array_rec->rec_gen_media);
rec = xa_load(&array_rec->rec_gen_media, attrbs->dpa);
- if (!rec)
- return NULL;
+ found = rec && !cxl_rec_not_repairable(&rec->media_hdr);
+ xa_unlock(&array_rec->rec_gen_media);
- if (attrbs->repair_type == CXL_PPR)
- return rec;
-
- return NULL;
+ return found;
}
/* Caller holds the xarray lock. The record may be freed once it is dropped. */
@@ -949,7 +962,7 @@ __cxl_find_rec_dram(struct xarray *rec_xarray,
lockdep_assert_held(&rec_xarray->xa_lock);
rec = xa_load(rec_xarray, attrbs->dpa);
- if (!rec)
+ if (!rec || cxl_rec_not_repairable(&rec->media_hdr))
return NULL;
validity_flags = get_unaligned_le16(rec->media_hdr.validity_flags);
@@ -1130,8 +1143,7 @@ int cxl_store_rec_gen_media(struct cxl_memdev *cxlmd, union cxl_event *evt)
return -ENOMEM;
old_rec = xa_store(&array_rec->rec_gen_media,
- le64_to_cpu(rec->media_hdr.phys_addr), rec,
- GFP_KERNEL);
+ cxl_rec_key(&rec->media_hdr), rec, GFP_KERNEL);
if (xa_is_err(old_rec)) {
kfree(rec);
return xa_err(old_rec);
@@ -1160,8 +1172,7 @@ int cxl_store_rec_dram(struct cxl_memdev *cxlmd, union cxl_event *evt)
return -ENOMEM;
old_rec = xa_store(&array_rec->rec_dram,
- le64_to_cpu(rec->media_hdr.phys_addr), rec,
- GFP_KERNEL);
+ cxl_rec_key(&rec->media_hdr), rec, GFP_KERNEL);
if (xa_is_err(old_rec)) {
kfree(rec);
return xa_err(old_rec);
--
2.55.0
^ permalink raw reply [flat|nested] 3+ messages in thread
end of thread, other threads:[~2026-11-02 12:32 UTC | newest]
Thread overview: 3+ messages (download: mbox.gz / follow: Atom feed)
-- links below jump to the message on this page --
2026-11-02 12:32 [PATCH v4 0/2] cxl/edac: Fix event record lookups for memory repair Jongwon Lee
2026-11-02 12:32 ` [PATCH v4 1/2] cxl/edac: Read cached DRAM event records under the xarray lock Jongwon Lee
2026-11-02 12:32 ` [PATCH v4 2/2] cxl/edac: Mask DPA flags in the event record cache key Jongwon Lee
This is a public inbox, see mirroring instructions
for how to clone and mirror all data and code used for this inbox
all inboxes | Powered by JetHome®