* [RFC PATCH 1/5] cxl/mbox: Add Get Poison List snapshot support
2026-10-10 16:20 [RFC PATCH 0/5] cxl, mm: Keep device-reported poison out of the page allocator Shaikh Kamaluddin
@ 2026-10-10 16:20 ` Shaikh Kamaluddin
2026-10-10 16:20 ` [RFC PATCH 2/5] cxl/region: Factor decoder poison-list retrieval Shaikh Kamaluddin
` (3 subsequent siblings)
4 siblings, 0 replies; 6+ messages in thread
From: Shaikh Kamaluddin @ 2026-10-10 16:20 UTC (permalink / raw)
To: Davidlohr Bueso, Jonathan Cameron, Dave Jiang, Alison Schofield,
Vishal Verma, Dan Williams, Miaohe Lin, Andrew Morton,
Vlastimil Babka, Breno Leitao
Cc: Ira Weiny, Li Ming, Richard Cheng, Naoya Horiguchi,
Suren Baghdasaryan, Michal Hocko, Brendan Jackman,
Johannes Weiner, Zi Yan, linux-cxl, linux-kernel, linux-mm,
David Hildenbrand, Oscar Salvador, Kiryl Shutsemau, Harry Yoo,
Shaikh Kamaluddin
cxl_mem_get_poison() uses a device-owned output buffer for each Get
Poison List response. It traces the records before issuing the next
command, which overwrites that buffer when the device sets the More
Media Error Records flag. A caller therefore cannot inspect the complete
set of collected records after the function returns.
Add a caller-owned snapshot and copy each validated mailbox response
into it. Preserve device overflow and scan-in-progress status, and
indicate when the host stops collection before retrieving all available
records.
Keep cxl_mem_get_poison() as the existing trace-only wrapper so its
current callers, including trigger_poison_list, retain their behavior.
Signed-off-by: Shaikh Kamaluddin <shaikhkamal2012@gmail.com>
---
drivers/cxl/core/mbox.c | 137 ++++++++++++++++++++++++++++++++++++++--
drivers/cxl/cxlmem.h | 25 ++++++++
2 files changed, 156 insertions(+), 6 deletions(-)
diff --git a/drivers/cxl/core/mbox.c b/drivers/cxl/core/mbox.c
index 55828a836c01..2752b46ffee2 100644
--- a/drivers/cxl/core/mbox.c
+++ b/drivers/cxl/core/mbox.c
@@ -1376,14 +1376,91 @@ int cxl_set_timestamp(struct cxl_memdev_state *mds)
}
EXPORT_SYMBOL_NS_GPL(cxl_set_timestamp, "CXL");
-int cxl_mem_get_poison(struct cxl_memdev *cxlmd, u64 offset, u64 len,
- struct cxl_region *cxlr)
+int cxl_poison_snapshot_init(struct cxl_memdev *cxlmd,
+ struct cxl_poison_snapshot *snapshot)
+{
+ struct cxl_memdev_state *mds = to_cxl_memdev_state(cxlmd->cxlds);
+ u32 capacity = mds->poison.max_errors;
+
+ if (!snapshot)
+ return -EINVAL;
+ memset(snapshot, 0, sizeof(*snapshot));
+
+ if (!test_bit(CXL_POISON_ENABLED_LIST, mds->poison.enabled_cmds))
+ return -EOPNOTSUPP;
+
+ if (!capacity)
+ return -EPROTO;
+
+ snapshot->records = kvmalloc_array(mds->poison.max_errors,
+ sizeof(*snapshot->records),
+ GFP_KERNEL);
+ if (!snapshot->records)
+ return -ENOMEM;
+
+ snapshot->capacity = capacity;
+
+ return 0;
+}
+
+void cxl_poison_snapshot_destroy(struct cxl_poison_snapshot *snapshot)
+{
+ if (!snapshot)
+ return;
+
+ kvfree(snapshot->records);
+ memset(snapshot, 0, sizeof(*snapshot));
+}
+
+static int cxl_poison_snapshot_append(struct cxl_poison_snapshot *snapshot,
+ struct cxl_poison_record *records,
+ u32 count)
+{
+ u32 available;
+
+ /*
+ * Existing cxl_mem_get_poison() callers request tracing only and
+ * pass no snapshot.
+ */
+
+ if (!snapshot)
+ return 0;
+
+ if (!snapshot->records)
+ return -EINVAL;
+
+ if (snapshot->nr_records > snapshot->capacity)
+ return -EOVERFLOW;
+
+ if (!count)
+ return 0;
+
+ if (!records)
+ return -EINVAL;
+
+ available = snapshot->capacity - snapshot->nr_records;
+ if (count > available) {
+ snapshot->truncated = true;
+ return -ENOSPC;
+ }
+
+ memcpy(&snapshot->records[snapshot->nr_records], records,
+ count * sizeof(*records));
+ snapshot->nr_records += count;
+
+ return 0;
+}
+
+static int cxl_mem_get_poison_common(struct cxl_memdev *cxlmd, u64 offset,
+ u64 len, struct cxl_region *cxlr,
+ struct cxl_poison_snapshot *snapshot)
{
struct cxl_memdev_state *mds = to_cxl_memdev_state(cxlmd->cxlds);
struct cxl_mailbox *cxl_mbox = &cxlmd->cxlds->cxl_mbox;
struct cxl_mbox_poison_out *po;
struct cxl_mbox_poison_in pi;
- int nr_records = 0;
+ u32 nr_records = 0;
+ u32 count;
int rc;
ACQUIRE(mutex_intr, lock)(&mds->poison.mutex);
@@ -1408,29 +1485,77 @@ int cxl_mem_get_poison(struct cxl_memdev *cxlmd, u64 offset, u64 len,
if (rc)
break;
- if (!le16_to_cpu(po->count)) {
+ count = le16_to_cpu(po->count);
+ if (!count) {
dev_dbg(&cxlmd->dev, "Poison empty payload!\n");
+
+ if (po->flags & CXL_POISON_FLAG_MORE) {
+ if (snapshot)
+ snapshot->truncated = true;
+ rc = -EPROTO;
+ }
+
+ break;
+ }
+
+ if (struct_size(po, record, count) > mbox_cmd.size_out) {
+ dev_err(&cxlmd->dev,
+ "invalid poison record count: %u\n", count);
+ rc = -EPROTO;
break;
}
- for (int i = 0; i < le16_to_cpu(po->count); i++)
+ for (u32 i = 0; i < count; i++)
trace_cxl_poison(cxlmd, cxlr, &po->record[i],
po->flags, po->overflow_ts,
CXL_POISON_TRACE_LIST);
+ if (snapshot && (po->flags & CXL_POISON_FLAG_OVERFLOW)) {
+ snapshot->device_flags |= CXL_POISON_FLAG_OVERFLOW;
+ snapshot->overflow_ts = le64_to_cpu(po->overflow_ts);
+ }
+
+ if (snapshot && (po->flags & CXL_POISON_FLAG_SCANNING))
+ snapshot->device_flags |= CXL_POISON_FLAG_SCANNING;
+
+ rc = cxl_poison_snapshot_append(snapshot, po->record, count);
+ if (rc)
+ break;
+
/* Protect against an uncleared _FLAG_MORE */
- nr_records = nr_records + le16_to_cpu(po->count);
+ nr_records += count;
if (nr_records >= mds->poison.max_errors) {
dev_dbg(&cxlmd->dev, "Max Error Records reached: %d\n",
nr_records);
+ if (snapshot && (po->flags & CXL_POISON_FLAG_MORE))
+ snapshot->truncated = true;
+
break;
}
} while (po->flags & CXL_POISON_FLAG_MORE);
return rc;
}
+
+int cxl_mem_get_poison(struct cxl_memdev *cxlmd, u64 offset, u64 len,
+ struct cxl_region *cxlr)
+{
+ return cxl_mem_get_poison_common(cxlmd, offset, len, cxlr, NULL);
+}
EXPORT_SYMBOL_NS_GPL(cxl_mem_get_poison, "CXL");
+int cxl_mem_get_poison_snapshot(struct cxl_memdev *cxlmd, u64 offset, u64 len,
+ struct cxl_region *cxlr,
+ struct cxl_poison_snapshot *snapshot)
+{
+ if (!snapshot || !snapshot->records || !snapshot->capacity)
+ return -EINVAL;
+
+ if (snapshot->nr_records > snapshot->capacity)
+ return -EOVERFLOW;
+
+ return cxl_mem_get_poison_common(cxlmd, offset, len, cxlr, snapshot);
+}
static void free_poison_buf(void *buf)
{
kvfree(buf);
diff --git a/drivers/cxl/cxlmem.h b/drivers/cxl/cxlmem.h
index c401e3a1af06..1d6f8d958bd2 100644
--- a/drivers/cxl/cxlmem.h
+++ b/drivers/cxl/cxlmem.h
@@ -812,6 +812,31 @@ 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,
struct cxl_region *cxlr);
+
+/**
+ * struct cxl_poison_snapshot - Aggregate Get Poison List result
+ * @records: Copied Media Error Records from all mailbox responses
+ * @nr_records: Number of valid records stored in @records
+ * @capacity: Maximum number of records that @records can hold
+ * @overflow_ts: Timestamp associated with a reported device overflow
+ * @device_flags: Persistent device status observed during collection
+ * @truncated: Host stopped before collecting all available records
+ */
+struct cxl_poison_snapshot {
+ struct cxl_poison_record *records;
+ u32 nr_records;
+ u32 capacity;
+ u64 overflow_ts;
+ u8 device_flags;
+ bool truncated;
+};
+
+int cxl_poison_snapshot_init(struct cxl_memdev *cxlmd,
+ struct cxl_poison_snapshot *snapshot);
+void cxl_poison_snapshot_destroy(struct cxl_poison_snapshot *snapshot);
+int cxl_mem_get_poison_snapshot(struct cxl_memdev *cxlmd, u64 offset, u64 len,
+ struct cxl_region *cxlr,
+ struct cxl_poison_snapshot *snapshot);
int cxl_trigger_poison_list(struct cxl_memdev *cxlmd);
int cxl_inject_poison(struct cxl_memdev *cxlmd, u64 dpa);
int cxl_clear_poison(struct cxl_memdev *cxlmd, u64 dpa);
--
2.43.0
^ permalink raw reply [flat|nested] 6+ messages in thread* [RFC PATCH 2/5] cxl/region: Factor decoder poison-list retrieval
2026-10-10 16:20 [RFC PATCH 0/5] cxl, mm: Keep device-reported poison out of the page allocator Shaikh Kamaluddin
2026-10-10 16:20 ` [RFC PATCH 1/5] cxl/mbox: Add Get Poison List snapshot support Shaikh Kamaluddin
@ 2026-10-10 16:20 ` Shaikh Kamaluddin
2026-10-10 16:20 ` [RFC PATCH 3/5] mm/memory-failure: Add a pre-online poison registry Shaikh Kamaluddin
` (2 subsequent siblings)
4 siblings, 0 replies; 6+ messages in thread
From: Shaikh Kamaluddin @ 2026-10-10 16:20 UTC (permalink / raw)
To: Davidlohr Bueso, Jonathan Cameron, Dave Jiang, Alison Schofield,
Vishal Verma, Dan Williams, Miaohe Lin, Andrew Morton,
Vlastimil Babka, Breno Leitao
Cc: Ira Weiny, Li Ming, Richard Cheng, Naoya Horiguchi,
Suren Baghdasaryan, Michal Hocko, Brendan Jackman,
Johannes Weiner, Zi Yan, linux-cxl, linux-kernel, linux-mm,
David Hildenbrand, Oscar Salvador, Kiryl Shutsemau, Harry Yoo,
Shaikh Kamaluddin
poison_by_decoder() performs two different operations: it queries an
unmapped DPA gap preceding an endpoint decoder, then queries the DPA
resource assigned to that decoder and its region.
Factor the second operation into cxl_get_poison_by_decoder(). Give the
helper an optional snapshot so a later region-probe caller can retain
records from exactly one mapped decoder. Keep the existing endpoint
walk, skipped-DPA queries, partition-tail queries, -EFAULT handling, and
trace behavior unchanged.
Signed-off-by: Shaikh Kamaluddin <shaikhkamal2012@gmail.com>
---
drivers/cxl/core/region.c | 41 ++++++++++++++++++++++++++++++++++-----
1 file changed, 36 insertions(+), 5 deletions(-)
diff --git a/drivers/cxl/core/region.c b/drivers/cxl/core/region.c
index 75b8092e6dc2..c72524923b38 100644
--- a/drivers/cxl/core/region.c
+++ b/drivers/cxl/core/region.c
@@ -2940,6 +2940,40 @@ static int cxl_get_poison_unmapped(struct cxl_memdev *cxlmd,
return rc;
}
+enum cxl_poison_query_policy {
+ CXL_POISON_QUERY_DIAGNOSTIC,
+ CXL_POISON_QUERY_PREONLINE,
+};
+
+static int cxl_get_poison_by_decoder(struct cxl_endpoint_decoder *cxled,
+ struct cxl_poison_snapshot *snapshot,
+ enum cxl_poison_query_policy policy)
+{
+ struct cxl_memdev *cxlmd = cxled_to_memdev(cxled);
+ struct cxl_dev_state *cxlds = cxlmd->cxlds;
+ enum cxl_partition_mode mode;
+ u64 offset, length;
+ int rc;
+
+ if (!cxled->dpa_res)
+ return 0;
+
+ mode = cxlds->part[cxled->part].mode;
+ offset = cxled->dpa_res->start;
+ length = resource_size(cxled->dpa_res);
+ if (snapshot)
+ rc = cxl_mem_get_poison_snapshot(cxlmd, offset, length,
+ cxled->cxld.region, snapshot);
+ else
+ rc = cxl_mem_get_poison(cxlmd, offset, length,
+ cxled->cxld.region);
+
+ if (rc == -EFAULT && mode == CXL_PARTMODE_RAM &&
+ policy == CXL_POISON_QUERY_DIAGNOSTIC)
+ rc = 0;
+ return rc;
+}
+
static int poison_by_decoder(struct device *dev, void *arg)
{
struct cxl_poison_context *ctx = arg;
@@ -2971,11 +3005,8 @@ static int poison_by_decoder(struct device *dev, void *arg)
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;
+ rc = cxl_get_poison_by_decoder(cxled, NULL,
+ CXL_POISON_QUERY_DIAGNOSTIC);
if (rc)
return rc;
--
2.43.0
^ permalink raw reply [flat|nested] 6+ messages in thread* [RFC PATCH 3/5] mm/memory-failure: Add a pre-online poison registry
2026-10-10 16:20 [RFC PATCH 0/5] cxl, mm: Keep device-reported poison out of the page allocator Shaikh Kamaluddin
2026-10-10 16:20 ` [RFC PATCH 1/5] cxl/mbox: Add Get Poison List snapshot support Shaikh Kamaluddin
2026-10-10 16:20 ` [RFC PATCH 2/5] cxl/region: Factor decoder poison-list retrieval Shaikh Kamaluddin
@ 2026-10-10 16:20 ` Shaikh Kamaluddin
2026-10-10 16:20 ` [RFC PATCH 4/5] mm/page_alloc: Keep pre-online poison out of the buddy allocator Shaikh Kamaluddin
2026-10-10 16:20 ` [RFC PATCH 5/5] cxl/region: Register device poison before exposing RAM Shaikh Kamaluddin
4 siblings, 0 replies; 6+ messages in thread
From: Shaikh Kamaluddin @ 2026-10-10 16:20 UTC (permalink / raw)
To: Davidlohr Bueso, Jonathan Cameron, Dave Jiang, Alison Schofield,
Vishal Verma, Dan Williams, Miaohe Lin, Andrew Morton,
Vlastimil Babka, Breno Leitao
Cc: Ira Weiny, Li Ming, Richard Cheng, Naoya Horiguchi,
Suren Baghdasaryan, Michal Hocko, Brendan Jackman,
Johannes Weiner, Zi Yan, linux-cxl, linux-kernel, linux-mm,
David Hildenbrand, Oscar Salvador, Kiryl Shutsemau, Harry Yoo,
Shaikh Kamaluddin
A frame can be known bad before its backing memory is onlined. A CXL
Type-3 device retains poison as DPA-based Media Error Records and can
report those records through GET_POISON_LIST after a reboot, kexec, or
device hotplug.
CXL poison can be retrieved while a region is being prepared, before
dax/kmem calls add_memory_driver_managed() for the range. At that point
there is no initialized struct page on which memory_failure() can
operate. The poison information must therefore be retained until the
corresponding pages pass through memory initialization.
Add a registry of sorted, nonoverlapping poisoned PFN ranges. Each
registration represents one owner and its independently managed memory.
For example, a CXL region registers the poisoned PFN ranges translated
from its device poison records and receives an opaque handle for that
range set. A second CXL region receives a separate handle for its own
ranges.
The owner uses its handle to remove exactly its registration after the
associated memory can no longer enter the allocator. Removing one CXL
region's registration does not affect poison ranges registered by
another region.
Keep each registered range set immutable and protect the registration
list with RCU. This keeps allocator-side lookups lockless while allowing
a registration to be removed safely after an RCU grace period. A later
patch will consult the registry before initialized pages are released
to the buddy allocator.
Signed-off-by: Shaikh Kamaluddin <shaikhkamal2012@gmail.com>
---
include/linux/memory-failure.h | 92 +++++++++++++++++
mm/memory-failure.c | 175 +++++++++++++++++++++++++++++++++
2 files changed, 267 insertions(+)
diff --git a/include/linux/memory-failure.h b/include/linux/memory-failure.h
index d333dcdbeae7..ebb988c2fe12 100644
--- a/include/linux/memory-failure.h
+++ b/include/linux/memory-failure.h
@@ -11,9 +11,73 @@ struct pfn_address_space {
unsigned long pfn, pgoff_t *pgoff);
};
+struct preonline_hwpoison;
+
+/**
+ * struct preonline_hwpoison_range - Pre-online poisoned PFN range
+ * @start_pfn: First PFN in the range
+ * @nr_pages: Number of consecutive PFNs in the range
+ */
+struct preonline_hwpoison_range {
+ unsigned long start_pfn;
+ unsigned long nr_pages;
+};
+
#ifdef CONFIG_MEMORY_FAILURE
int register_pfn_address_space(struct pfn_address_space *pfn_space);
void unregister_pfn_address_space(struct pfn_address_space *pfn_space);
+
+/**
+ * preonline_hwpoison_register - Register poison before memory is onlined
+ * @ranges: Sorted, nonoverlapping PFN ranges in ascending start PFN order.
+ * Adjacent ranges are accepted, but callers should coalesce them.
+ * The array is copied and need not outlive this call.
+ * @nr_ranges: Number of entries in @ranges. Must be nonzero.
+ * @handle: Registration handle returned on success. Set to NULL on failure.
+ *
+ * Register PFN ranges that must be withheld when their backing memory is
+ * initialized. Pass the returned handle to
+ * preonline_hwpoison_unregister() only after the memory can no longer enter
+ * the allocator.
+ *
+ * Return: 0 on success, -EINVAL if @handle is NULL or @ranges is empty,
+ * unsorted, overlapping, or contains a zero-length entry, -EOVERFLOW on PFN
+ * wraparound, and -ENOMEM on allocation failure.
+ */
+int preonline_hwpoison_register(const struct preonline_hwpoison_range *ranges,
+ unsigned int nr_ranges,
+ struct preonline_hwpoison **handle);
+
+/**
+ * preonline_hwpoison_unregister - Remove a pre-online poison registration
+ * @handle: Handle returned by preonline_hwpoison_register(), or NULL
+ *
+ * Remove @handle from future pre-online poison lookups. The caller must first
+ * ensure that the associated memory can no longer be initialized or handed
+ * to the page allocator.
+ *
+ * Pages already marked as hwpoison remain marked.
+ */
+void preonline_hwpoison_unregister(struct preonline_hwpoison *handle);
+
+/**
+ * preonline_hwpoison_intersects - Does a PFN range cover a known-bad frame?
+ * @start_pfn: First PFN of the range to test
+ * @nr_pages: Length of the range in frames
+ *
+ * Return: %true if any registered range overlaps [@start_pfn, @start_pfn +
+ * @nr_pages), %false otherwise.
+ */
+bool preonline_hwpoison_intersects(unsigned long start_pfn,
+ unsigned long nr_pages);
+
+/**
+ * preonline_hwpoison_contains - Is a single frame known bad?
+ * @pfn: Frame to test
+ *
+ * Return: %true if @pfn falls in a registered range.
+ */
+bool preonline_hwpoison_contains(unsigned long pfn);
#else
static inline int register_pfn_address_space(struct pfn_address_space *pfn_space)
{
@@ -23,6 +87,34 @@ static inline int register_pfn_address_space(struct pfn_address_space *pfn_space
static inline void unregister_pfn_address_space(struct pfn_address_space *pfn_space)
{
}
+
+static inline int
+preonline_hwpoison_register(const struct preonline_hwpoison_range *ranges,
+ unsigned int nr_ranges,
+ struct preonline_hwpoison **handle)
+{
+ if (handle)
+ *handle = NULL;
+
+ return -EOPNOTSUPP;
+}
+
+static inline void
+preonline_hwpoison_unregister(struct preonline_hwpoison *handle)
+{
+}
+
+static inline bool preonline_hwpoison_intersects(unsigned long start_pfn,
+ unsigned long nr_pages)
+{
+ return false;
+}
+
+static inline bool preonline_hwpoison_contains(unsigned long pfn)
+{
+ return false;
+}
+
#endif /* CONFIG_MEMORY_FAILURE */
#endif /* _LINUX_MEMORY_FAILURE_H */
diff --git a/mm/memory-failure.c b/mm/memory-failure.c
index 60e968243470..e89b400c3fa2 100644
--- a/mm/memory-failure.c
+++ b/mm/memory-failure.c
@@ -2291,6 +2291,181 @@ static void kill_procs_now(struct page *p, unsigned long pfn, int flags,
kill_procs(&tokill, true, pfn, flags);
}
+/**
+ * struct preonline_hwpoison - Owner of pre-online poisoned PFN ranges
+ * @list: Entry in the global owner list
+ * @rcu: Used to defer the free past a grace period
+ * @nr_ranges: Number of ranges in @ranges
+ * @ranges: Sorted, nonoverlapping PFN ranges
+ *
+ * The ranges are immutable after the object is added to the owner list, which is
+ * what lets readers walk them under RCU with no lock.
+ */
+struct preonline_hwpoison {
+ struct list_head list;
+ struct rcu_head rcu;
+ unsigned int nr_ranges;
+ struct preonline_hwpoison_range ranges[];
+};
+
+static LIST_HEAD(preonline_hwpoison_owners);
+/* Serialises writers only. Readers use RCU. */
+static DEFINE_MUTEX(preonline_hwpoison_lock);
+
+static int
+preonline_hwpoison_validate(const struct preonline_hwpoison_range *ranges,
+ unsigned int nr_ranges)
+{
+ unsigned long previous_end = 0;
+
+ if (!ranges || !nr_ranges)
+ return -EINVAL;
+
+ for (unsigned int i = 0; i < nr_ranges; i++) {
+ unsigned long end;
+
+ if (!ranges[i].nr_pages)
+ return -EINVAL;
+
+ if (check_add_overflow(ranges[i].start_pfn,
+ ranges[i].nr_pages - 1, &end))
+ return -EOVERFLOW;
+
+ /*
+ * Require sorted, nonoverlapping ranges. Adjacent ranges
+ * remain valid, although callers may coalesce them to reduce
+ * storage and lookup overhead.
+ */
+ if (i && ranges[i].start_pfn <= previous_end)
+ return -EINVAL;
+
+ previous_end = end;
+ }
+
+ return 0;
+}
+
+int preonline_hwpoison_register(const struct preonline_hwpoison_range *ranges,
+ unsigned int nr_ranges,
+ struct preonline_hwpoison **handle)
+{
+ struct preonline_hwpoison *owner;
+ int rc;
+
+ if (!handle)
+ return -EINVAL;
+
+ *handle = NULL;
+
+ rc = preonline_hwpoison_validate(ranges, nr_ranges);
+ if (rc)
+ return rc;
+
+ /*
+ * The range array may be larger than a page, so do not require
+ * physically contiguous memory.
+ */
+ owner = kvmalloc(struct_size(owner, ranges, nr_ranges), GFP_KERNEL);
+ if (!owner)
+ return -ENOMEM;
+
+ owner->nr_ranges = nr_ranges;
+ memcpy(owner->ranges, ranges,
+ flex_array_size(owner, ranges, nr_ranges));
+
+ /*
+ * list_add_tail_rcu() publishes with a release barrier, so a reader
+ * that observes the entry also observes the fully initialised
+ * ranges[] above.
+ */
+ scoped_guard(mutex, &preonline_hwpoison_lock) {
+ list_add_tail_rcu(&owner->list, &preonline_hwpoison_owners);
+ }
+
+ *handle = owner;
+
+ return 0;
+}
+EXPORT_SYMBOL_GPL(preonline_hwpoison_register);
+
+void preonline_hwpoison_unregister(struct preonline_hwpoison *owner)
+{
+ if (!owner)
+ return;
+
+ scoped_guard(mutex, &preonline_hwpoison_lock) {
+ list_del_rcu(&owner->list);
+ }
+
+ /*
+ * A reader may still be walking this entry. list_del_rcu() leaves its
+ * forward pointer intact so that walk completes into the live list;
+ * the free waits for a grace period.
+ */
+ kvfree_rcu(owner, rcu);
+}
+EXPORT_SYMBOL_GPL(preonline_hwpoison_unregister);
+
+static bool preonline_owner_intersects(const struct preonline_hwpoison *owner,
+ unsigned long start_pfn,
+ unsigned long end_pfn)
+{
+ unsigned int low = 0;
+ unsigned int high = owner->nr_ranges;
+
+ /*
+ * Find the first range whose inclusive end is greater than or equal
+ * to start_pfn.
+ */
+ while (low < high) {
+ const struct preonline_hwpoison_range *range;
+ unsigned long range_end;
+ unsigned int mid;
+
+ mid = low + (high - low) / 2;
+ range = &owner->ranges[mid];
+ range_end = range->start_pfn + range->nr_pages - 1;
+
+ if (range_end < start_pfn)
+ low = mid + 1;
+ else
+ high = mid;
+ }
+
+ if (low == owner->nr_ranges)
+ return false;
+
+ return owner->ranges[low].start_pfn <= end_pfn;
+}
+
+bool preonline_hwpoison_intersects(unsigned long start_pfn,
+ unsigned long nr_pages)
+{
+ struct preonline_hwpoison *owner;
+ unsigned long end_pfn;
+
+ if (!nr_pages)
+ return false;
+
+ /* Fail safe: withhold a range we cannot reason about. */
+ if (WARN_ON_ONCE(check_add_overflow(start_pfn, nr_pages - 1, &end_pfn)))
+ return true;
+
+ guard(rcu)();
+
+ list_for_each_entry_rcu(owner, &preonline_hwpoison_owners, list) {
+ if (preonline_owner_intersects(owner, start_pfn, end_pfn))
+ return true;
+ }
+
+ return false;
+}
+
+bool preonline_hwpoison_contains(unsigned long pfn)
+{
+ return preonline_hwpoison_intersects(pfn, 1);
+}
+
int register_pfn_address_space(struct pfn_address_space *pfn_space)
{
guard(mutex)(&pfn_space_lock);
--
2.43.0
^ permalink raw reply [flat|nested] 6+ messages in thread* [RFC PATCH 4/5] mm/page_alloc: Keep pre-online poison out of the buddy allocator
2026-10-10 16:20 [RFC PATCH 0/5] cxl, mm: Keep device-reported poison out of the page allocator Shaikh Kamaluddin
` (2 preceding siblings ...)
2026-10-10 16:20 ` [RFC PATCH 3/5] mm/memory-failure: Add a pre-online poison registry Shaikh Kamaluddin
@ 2026-10-10 16:20 ` Shaikh Kamaluddin
2026-10-10 16:20 ` [RFC PATCH 5/5] cxl/region: Register device poison before exposing RAM Shaikh Kamaluddin
4 siblings, 0 replies; 6+ messages in thread
From: Shaikh Kamaluddin @ 2026-10-10 16:20 UTC (permalink / raw)
To: Davidlohr Bueso, Jonathan Cameron, Dave Jiang, Alison Schofield,
Vishal Verma, Dan Williams, Miaohe Lin, Andrew Morton,
Vlastimil Babka, Breno Leitao
Cc: Ira Weiny, Li Ming, Richard Cheng, Naoya Horiguchi,
Suren Baghdasaryan, Michal Hocko, Brendan Jackman,
Johannes Weiner, Zi Yan, linux-cxl, linux-kernel, linux-mm,
David Hildenbrand, Oscar Salvador, Kiryl Shutsemau, Harry Yoo,
Shaikh Kamaluddin
The inherited-poison path checks initialized blocks before releasing
them to the buddy allocator. CXL device poison discovered before
hot-added memory is onlined is recorded separately in the pre-online
poison registry.
Extend __free_pages_core() to send a block to free_poisoned_block() when
it intersects either poison source. In free_poisoned_block(), withhold a
page when its PFN is registered as pre-online poison or its physical
address is present in the inherited-poison table.
Pages identified by either source continue through
hwpoison_boot_page(), while clean pages are released through
accept_and_free_block().
Signed-off-by: Shaikh Kamaluddin <shaikhkamal2012@gmail.com>
---
mm/page_alloc.c | 8 ++++++--
1 file changed, 6 insertions(+), 2 deletions(-)
diff --git a/mm/page_alloc.c b/mm/page_alloc.c
index cca67a2702a4..57843df01b2a 100644
--- a/mm/page_alloc.c
+++ b/mm/page_alloc.c
@@ -33,6 +33,7 @@
#include <linux/cpuset.h>
#include <linux/folio_batch.h>
#include <linux/memory_hotplug.h>
+#include <linux/memory-failure.h>
#include <linux/nodemask.h>
#include <linux/vmstat.h>
#include <linux/fault-inject.h>
@@ -1601,12 +1602,14 @@ static void __meminit free_poisoned_block(struct page *page, unsigned int order,
enum meminit_context context)
{
unsigned long i, nr_pages = 1UL << order;
+ unsigned long pfn = page_to_pfn(page);
for (i = 0; i < nr_pages; i++) {
struct page *p = page + i;
phys_addr_t phys = page_to_phys(p);
- if (range_contains_poisoned_memory(phys, PAGE_SIZE)) {
+ if (range_contains_poisoned_memory(phys, PAGE_SIZE) ||
+ preonline_hwpoison_contains(pfn + i)) {
hwpoison_boot_page(p, context);
continue;
}
@@ -1652,7 +1655,8 @@ void __meminit __free_pages_core(struct page *page, unsigned int order,
/* First: a block parked by __free_unaccepted() never returns here. */
if (range_contains_poisoned_memory(page_to_phys(page),
- PAGE_SIZE << order)) {
+ PAGE_SIZE << order) ||
+ preonline_hwpoison_intersects(page_to_pfn(page), nr_pages)) {
free_poisoned_block(page, order, context);
return;
}
--
2.43.0
^ permalink raw reply [flat|nested] 6+ messages in thread* [RFC PATCH 5/5] cxl/region: Register device poison before exposing RAM
2026-10-10 16:20 [RFC PATCH 0/5] cxl, mm: Keep device-reported poison out of the page allocator Shaikh Kamaluddin
` (3 preceding siblings ...)
2026-10-10 16:20 ` [RFC PATCH 4/5] mm/page_alloc: Keep pre-online poison out of the buddy allocator Shaikh Kamaluddin
@ 2026-10-10 16:20 ` Shaikh Kamaluddin
4 siblings, 0 replies; 6+ messages in thread
From: Shaikh Kamaluddin @ 2026-10-10 16:20 UTC (permalink / raw)
To: Davidlohr Bueso, Jonathan Cameron, Dave Jiang, Alison Schofield,
Vishal Verma, Dan Williams, Miaohe Lin, Andrew Morton,
Vlastimil Babka, Breno Leitao
Cc: Ira Weiny, Li Ming, Richard Cheng, Naoya Horiguchi,
Suren Baghdasaryan, Michal Hocko, Brendan Jackman,
Johannes Weiner, Zi Yan, linux-cxl, linux-kernel, linux-mm,
David Hildenbrand, Oscar Salvador, Kiryl Shutsemau, Harry Yoo,
Shaikh Kamaluddin
The trigger_poison_list interface may retrieve poison after CXL memory
is already online, which is too late to prevent known-bad frames from
entering the page allocator.
During RAM-region probe, retrieve a poison-list snapshot for each
participating endpoint decoder. Validate each record against the
decoder's assigned DPA range, translate its poisoned DPA units to HPA
and PFN, and collect the affected PFNs in an xarray. Coalesce consecutive
PFNs and register the resulting ranges with the pre-online hwpoison
registry before creating the DAX region.
When Get Poison List is supported, abort region probe if the list is
incomplete, malformed, or cannot be translated. If volatile poison-list
retrieval is unsupported, preserve the existing behavior and warn that
poison cannot be withheld before memory onlining.
Signed-off-by: Shaikh Kamaluddin <shaikhkamal2012@gmail.com>
---
drivers/cxl/core/region.c | 322 ++++++++++++++++++++++++++++++++++++++
1 file changed, 322 insertions(+)
diff --git a/drivers/cxl/core/region.c b/drivers/cxl/core/region.c
index c72524923b38..e3ffa77927df 100644
--- a/drivers/cxl/core/region.c
+++ b/drivers/cxl/core/region.c
@@ -14,6 +14,7 @@
#include <linux/string_choices.h>
#include <cxlmem.h>
#include <cxl.h>
+#include <linux/memory-failure.h>
#include "core.h"
#include "mce.h"
@@ -3038,6 +3039,323 @@ int cxl_get_poison_by_endpoint(struct cxl_port *port)
return rc;
}
+/*
+ * Device-reported poison is gathered at RAM-region probe, before the range
+ * reaches dax/kmem, and registered with the pre-online registry so the
+ * frames it covers are withheld when the memory is onlined.
+ */
+static int cxl_poison_store_pfn(struct xarray *pfns, u64 hpa)
+{
+ return xa_err(xa_store(pfns, PHYS_PFN(hpa), xa_mk_value(1),
+ GFP_KERNEL));
+}
+
+static int cxl_poison_store_hpa(struct cxl_region *cxlr, struct xarray *pfns,
+ u64 hpa)
+{
+ struct cxl_region_params *p = &cxlr->params;
+ int rc;
+
+ /* Outside this region: not ours to withhold. */
+ if (!cxl_resource_contains_addr(p->res, hpa))
+ return -ERANGE;
+
+ rc = cxl_poison_store_pfn(pfns, hpa);
+ if (rc || !p->cache_size)
+ return rc;
+
+ /*
+ * An extended linear cache makes the same DPA visible a second time
+ * at hpa - cache_size. Match the translation used by the
+ * cxl_general_media and cxl_dram trace events.
+ */
+ if (WARN_ON_ONCE(hpa < p->res->start + p->cache_size))
+ return -ERANGE;
+
+ return cxl_poison_store_pfn(pfns, hpa - p->cache_size);
+}
+
+static int cxl_poison_record_to_pfns(struct cxl_region *cxlr,
+ struct cxl_memdev *cxlmd,
+ const struct cxl_poison_record *record,
+ struct xarray *pfns)
+{
+ struct cxl_region_params *p = &cxlr->params;
+ u64 raw = le64_to_cpu(record->address);
+ u64 units = le32_to_cpu(record->length);
+ u64 dpa = raw & CXL_POISON_START_MASK;
+ u8 source = raw & CXL_POISON_SOURCE_MASK;
+ u64 length, end, step, cur;
+
+ if (!units ||
+ check_mul_overflow(units, (u64)CXL_POISON_LEN_MULT, &length) ||
+ check_add_overflow(dpa, length, &end))
+ return -EPROTO;
+
+ /*
+ * A corrected error never reaches the poison list, so every record
+ * is uncorrectable. Only a failure of the device's own media is
+ * withheld: External poison was written by the host into healthy
+ * media and a rewrite or Clear Poison recovers it, and Injected
+ * poison is a test artifact. Withholding either would make a
+ * recoverable state permanent.
+ */
+ if (source != CXL_POISON_SOURCE_INTERNAL) {
+ dev_dbg(&cxlr->dev, "%s: dpa %#llx source %u not withheld\n",
+ dev_name(&cxlmd->dev), dpa, source);
+ return 0;
+ }
+
+ /*
+ * Translate once per unit of HPA contiguity rather than per 64
+ * bytes. Consecutive DPAs within an interleave granule map to
+ * consecutive HPAs, and a granule no larger than a page lies within
+ * one page, so stepping by min(granule, PAGE_SIZE) from an aligned
+ * start reaches every page the record covers.
+ */
+ step = clamp_t(u64, p->interleave_granularity, CXL_POISON_LEN_MULT,
+ PAGE_SIZE);
+
+ for (cur = ALIGN_DOWN(dpa, step); cur < end; cur += step) {
+ u64 hpa = cxl_dpa_to_hpa(cxlr, cxlmd, max(cur, dpa));
+ int rc;
+
+ /* Not mapped by this region; nothing here to withhold. */
+ if (hpa == ULLONG_MAX)
+ continue;
+
+ rc = cxl_poison_store_hpa(cxlr, pfns, hpa);
+ if (rc)
+ return rc;
+ }
+
+ return 0;
+}
+
+static int cxl_region_collect_decoder_poison(struct cxl_region *cxlr,
+ struct cxl_endpoint_decoder *cxled,
+ struct xarray *pfns)
+{
+ struct cxl_memdev *cxlmd = cxled_to_memdev(cxled);
+ struct cxl_poison_snapshot snapshot;
+ int rc;
+
+ rc = cxl_poison_snapshot_init(cxlmd, &snapshot);
+ if (rc == -EOPNOTSUPP) {
+ dev_info(&cxlr->dev,
+ "%s: no Get Poison List support, nothing withheld\n",
+ dev_name(&cxlmd->dev));
+ return 0;
+ }
+ if (rc)
+ return rc;
+
+ rc = cxl_get_poison_by_decoder(cxled, &snapshot,
+ CXL_POISON_QUERY_PREONLINE);
+ if (rc == -EFAULT) {
+ /*
+ * The device keeps no poison list for volatile ranges, which
+ * CXL r3.2 8.2.9.8.4.1 permits. Online the region as today.
+ */
+ dev_info(&cxlr->dev,
+ "%s: no volatile poison list, nothing withheld\n",
+ dev_name(&cxlmd->dev));
+ rc = 0;
+ goto out;
+ }
+ if (rc)
+ goto out;
+
+ /*
+ * Withholding from a partial list would online the region on the
+ * belief that every bad frame had been named.
+ */
+ if (snapshot.truncated ||
+ (snapshot.device_flags & CXL_POISON_FLAG_OVERFLOW)) {
+ dev_err(&cxlr->dev, "%s: poison list incomplete\n",
+ dev_name(&cxlmd->dev));
+ rc = -EOVERFLOW;
+ goto out;
+ }
+ if (snapshot.device_flags & CXL_POISON_FLAG_SCANNING) {
+ dev_err(&cxlr->dev, "%s: media scan in progress\n",
+ dev_name(&cxlmd->dev));
+ rc = -EBUSY;
+ goto out;
+ }
+
+ for (u32 i = 0; i < snapshot.nr_records; i++) {
+ rc = cxl_poison_record_to_pfns(cxlr, cxlmd,
+ &snapshot.records[i], pfns);
+ if (rc)
+ break;
+ }
+out:
+ cxl_poison_snapshot_destroy(&snapshot);
+ return rc;
+}
+
+static unsigned int cxl_count_poison_ranges(struct xarray *pfns)
+{
+ unsigned long index, previous = 0;
+ unsigned int nr = 0;
+ void *entry;
+ bool first = true;
+
+ xa_for_each(pfns, index, entry) {
+ if (first || index != previous + 1)
+ nr++;
+ first = false;
+ previous = index;
+ }
+ return nr;
+}
+
+/*
+ * The xarray holds each PFN once and xa_for_each() walks them in ascending
+ * order, so runs of consecutive indices are exactly the sorted,
+ * nonoverlapping ranges preonline_hwpoison_register() requires.
+ */
+static int cxl_pfns_to_ranges(struct xarray *pfns,
+ struct preonline_hwpoison_range **ranges_out,
+ unsigned int *nr_ranges_out)
+{
+ struct preonline_hwpoison_range *ranges;
+ unsigned long index, previous = 0, run_start = 0;
+ unsigned int nr_ranges, nr = 0;
+ void *entry;
+ bool first = true;
+
+ nr_ranges = cxl_count_poison_ranges(pfns);
+ if (!nr_ranges) {
+ *ranges_out = NULL;
+ *nr_ranges_out = 0;
+ return 0;
+ }
+
+ ranges = kvmalloc_array(nr_ranges, sizeof(*ranges), GFP_KERNEL);
+ if (!ranges)
+ return -ENOMEM;
+
+ xa_for_each(pfns, index, entry) {
+ if (first) {
+ run_start = index;
+ previous = index;
+ first = false;
+ continue;
+ }
+ if (index == previous + 1) {
+ previous = index;
+ continue;
+ }
+ ranges[nr++] = (struct preonline_hwpoison_range) {
+ .start_pfn = run_start,
+ .nr_pages = previous - run_start + 1,
+ };
+ run_start = index;
+ previous = index;
+ }
+ ranges[nr++] = (struct preonline_hwpoison_range){
+ .start_pfn = run_start,
+ .nr_pages = previous - run_start + 1,
+ };
+
+ WARN_ON_ONCE(nr != nr_ranges);
+ *ranges_out = ranges;
+ *nr_ranges_out = nr;
+ return 0;
+}
+
+static void cxl_unregister_device_poison(void *data)
+{
+ preonline_hwpoison_unregister(data);
+}
+
+static int cxl_region_register_device_poison(struct cxl_region *cxlr)
+{
+ struct cxl_region_params *p = &cxlr->params;
+ struct preonline_hwpoison_range *ranges = NULL;
+ struct preonline_hwpoison *handle;
+ unsigned int nr_ranges = 0;
+ unsigned long nr_frames = 0;
+ struct xarray pfns;
+ int rc = 0;
+
+ lockdep_assert_held(&cxl_rwsem.region);
+ lockdep_assert_held(&cxl_rwsem.dpa);
+
+ /* The region may have been decommitted while the lock was dropped. */
+ if (p->state < CXL_CONFIG_COMMIT)
+ return -ENXIO;
+
+ if (test_bit(CXL_REGION_F_NORMALIZED_ADDRESSING, &cxlr->flags)) {
+ dev_info(&cxlr->dev,
+ "normalized addressing, poison not translated\n");
+ return 0;
+ }
+
+ xa_init(&pfns);
+
+ for (int i = 0; i < p->nr_targets; i++) {
+ struct cxl_endpoint_decoder *cxled = p->targets[i];
+
+ if (!cxled->dpa_res || !resource_size(cxled->dpa_res))
+ continue;
+ rc = cxl_region_collect_decoder_poison(cxlr, cxled, &pfns);
+ if (rc)
+ goto out;
+ }
+
+ if (xa_empty(&pfns))
+ goto out;
+
+ rc = cxl_pfns_to_ranges(&pfns, &ranges, &nr_ranges);
+ if (rc)
+ goto out;
+
+ rc = preonline_hwpoison_register(ranges, nr_ranges, &handle);
+ if (rc == -EOPNOTSUPP) {
+ /* No poison tracking in this kernel; nothing to withhold. */
+ rc = 0;
+ goto out;
+ }
+ if (rc)
+ goto out;
+
+ for (unsigned int i = 0; i < nr_ranges; i++)
+ nr_frames += ranges[i].nr_pages;
+ dev_warn(&cxlr->dev,
+ "withholding %lu frame(s) reported poisoned by the device\n",
+ nr_frames);
+
+ rc = devm_add_action_or_reset(&cxlr->dev, cxl_unregister_device_poison,
+ handle);
+out:
+ kvfree(ranges);
+ xa_destroy(&pfns);
+ return rc;
+}
+
+static int cxl_region_scan_device_poison(struct cxl_region *cxlr)
+{
+ int rc;
+
+ /* Nothing would consume the result. */
+ if (!IS_ENABLED(CONFIG_MEMORY_FAILURE))
+ return 0;
+
+ ACQUIRE(rwsem_read_intr, region_rwsem)(&cxl_rwsem.region);
+ rc = ACQUIRE_ERR(rwsem_read_intr, ®ion_rwsem);
+ if (rc)
+ return rc;
+ ACQUIRE(rwsem_read_intr, dpa_rwsem)(&cxl_rwsem.dpa);
+ rc = ACQUIRE_ERR(rwsem_read_intr, &dpa_rwsem);
+ if (rc)
+ return rc;
+
+ return cxl_region_register_device_poison(cxlr);
+}
+
struct cxl_dpa_to_region_context {
struct cxl_region *cxlr;
u64 dpa;
@@ -4262,6 +4580,10 @@ static int cxl_region_probe(struct device *dev)
p->res->start, p->res->end, cxlr,
is_system_ram) > 0)
return 0;
+ rc = cxl_region_scan_device_poison(cxlr);
+ if (rc)
+ return rc;
+
return devm_cxl_add_dax_region(cxlr);
default:
dev_dbg(&cxlr->dev, "unsupported region mode: %d\n",
--
2.43.0
^ permalink raw reply [flat|nested] 6+ messages in thread