From 61f4eef68feee5076674c2d36749184afc3c9ffe Mon Sep 17 00:00:00 2001 From: Mohammad Shahid Date: Fri, 3 Jul 2026 20:18:51 +0530 Subject: [PATCH 01/17] nvdimm: nfit: remove redundant NULL check before vfree() vfree() safely handles NULL pointers, so the explicit NULL check before calling vfree() is unnecessary. This issue was reported by ifnullfree.cocci. Signed-off-by: Mohammad Shahid Reviewed-by: Dave Jiang Link: https://patch.msgid.link/20260703144851.80309-1-mdshahid03@gmail.com Signed-off-by: Alison Schofield --- tools/testing/nvdimm/test/nfit.c | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/tools/testing/nvdimm/test/nfit.c b/tools/testing/nvdimm/test/nfit.c index f87e9f251d13..009fe107b0d7 100644 --- a/tools/testing/nvdimm/test/nfit.c +++ b/tools/testing/nvdimm/test/nfit.c @@ -1644,8 +1644,7 @@ static void *__test_alloc(struct nfit_test *t, size_t size, dma_addr_t *dma, err: if (*dma && size >= DIMM_SIZE) gen_pool_free(nfit_pool, *dma, size); - if (buf) - vfree(buf); + vfree(buf); kfree(nfit_res); return NULL; } From f6b2b1ad96c61c9223243097fe3da20dfe50ee69 Mon Sep 17 00:00:00 2001 From: Mohammad Shahid Date: Fri, 3 Jul 2026 19:25:13 +0530 Subject: [PATCH 02/17] nvdimm: ndtest: remove redundant NULL check before vfree() vfree() safely handles NULL pointers, so the explicit NULL check before calling vfree() is unnecessary. This issue was reported by ifnullfree.cocci. Signed-off-by: Mohammad Shahid Reviewed-by: Dave Jiang Link: https://patch.msgid.link/20260703135513.75840-1-mdshahid03@gmail.com Signed-off-by: Alison Schofield --- tools/testing/nvdimm/test/ndtest.c | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/tools/testing/nvdimm/test/ndtest.c b/tools/testing/nvdimm/test/ndtest.c index 8e3b6be53839..2051ad5d4882 100644 --- a/tools/testing/nvdimm/test/ndtest.c +++ b/tools/testing/nvdimm/test/ndtest.c @@ -376,8 +376,7 @@ static void *ndtest_alloc_resource(struct ndtest_priv *p, size_t size, buf_err: if (__dma && size >= DIMM_SIZE) gen_pool_free(ndtest_pool, __dma, size); - if (buf) - vfree(buf); + vfree(buf); kfree(res); return NULL; From 037770686126155eafc44501312989e2837b9659 Mon Sep 17 00:00:00 2001 From: Bryam Vargas Date: Wed, 24 Jun 2026 01:03:45 -0500 Subject: [PATCH 03/17] libnvdimm/labels: Prevent integer overflow in __nd_label_validate() The on-media namespace index field nslot is a u32 read from the DIMM label storage area. __nd_label_validate() bounds it against the config area size, but sizeof_namespace_label() returns unsigned, so the product nslot * label_size is evaluated in 32-bit and wraps modulo 2^32 before the comparison. A crafted nslot passes the bound and is then used as the loop trip count in nd_label_data_init(), whose memset() walks off the end of the config_size buffer: an out-of-bounds write. The field is not trusted -- it comes from the medium, or from userspace via ND_CMD_SET_CONFIG_DATA. Evaluate the product in 64-bit so the bound check is exact; conforming labels are unaffected. The check was safe when introduced by commit 4a826c83db4e ("libnvdimm: namespace indices: read and validate"): it multiplied by sizeof(struct nd_namespace_label), a size_t, so on a 64-bit build the product did not wrap. Commit 564e871aa66f ("libnvdimm, label: add v1.2 nvdimm label definitions") narrowed it to 32 bits when the label size became a runtime value read via sizeof_namespace_label(). Fixes: 564e871aa66f ("libnvdimm, label: add v1.2 nvdimm label definitions") Cc: stable@vger.kernel.org Reviewed-by: Alison Schofield Signed-off-by: Bryam Vargas Link: https://patch.msgid.link/20260624-b4-disp-d8279485-v3-1-cdb6cab28b41@proton.me Signed-off-by: Alison Schofield --- drivers/nvdimm/label.c | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/drivers/nvdimm/label.c b/drivers/nvdimm/label.c index 4218e3ac4a2a..ec12ce72cfe2 100644 --- a/drivers/nvdimm/label.c +++ b/drivers/nvdimm/label.c @@ -202,7 +202,7 @@ static int __nd_label_validate(struct nvdimm_drvdata *ndd) } nslot = __le32_to_cpu(nsindex[i]->nslot); - if (nslot * sizeof_namespace_label(ndd) + if ((u64)nslot * sizeof_namespace_label(ndd) + 2 * sizeof_namespace_index(ndd) > ndd->nsarea.config_size) { dev_dbg(dev, "nsindex%d nslot: %u invalid, config_size: %#x\n", From 18f9124248ed7a9da1c31973b629dceef76a9b0c Mon Sep 17 00:00:00 2001 From: Bryam Vargas Date: Wed, 24 Jun 2026 01:03:46 -0500 Subject: [PATCH 04/17] libnvdimm/labels: Bound the on-media label size before the shift For a v1.2+ index, __nd_label_validate() computes the label size as 1 << (7 + nsindex[i]->labelsize), where labelsize is a u8 read from the label storage medium. A value of 25 or more makes the shift count reach or exceed the width of int -- undefined behavior -- and 24 already shifts into the sign bit. Only 0 (128-byte) and 1 (256-byte) are valid. Reject a labelsize above 1 before the shift. The result was rejected by the following size comparison anyway, so this only removes the undefined shift on a crafted or corrupted medium; conforming labels are unaffected. Fixes: 564e871aa66f ("libnvdimm, label: add v1.2 nvdimm label definitions") Signed-off-by: Bryam Vargas Reviewed-by: Alison Schofield Link: https://patch.msgid.link/20260624-b4-disp-d8279485-v3-2-cdb6cab28b41@proton.me Signed-off-by: Alison Schofield --- drivers/nvdimm/label.c | 15 +++++++++++++-- 1 file changed, 13 insertions(+), 2 deletions(-) diff --git a/drivers/nvdimm/label.c b/drivers/nvdimm/label.c index ec12ce72cfe2..dea2eee86d13 100644 --- a/drivers/nvdimm/label.c +++ b/drivers/nvdimm/label.c @@ -145,10 +145,21 @@ static int __nd_label_validate(struct nvdimm_drvdata *ndd) /* label sizes larger than 128 arrived with v1.2 */ version = __le16_to_cpu(nsindex[i]->major) * 100 + __le16_to_cpu(nsindex[i]->minor); - if (version >= 102) + if (version >= 102) { + /* + * labelsize feeds the shift below; only 0 (128-byte) + * and 1 (256-byte) are valid -- a larger value would + * overflow or exceed the width of int. + */ + if (nsindex[i]->labelsize > 1) { + dev_dbg(dev, "nsindex%d labelsize: %d invalid\n", + i, nsindex[i]->labelsize); + continue; + } labelsize = 1 << (7 + nsindex[i]->labelsize); - else + } else { labelsize = 128; + } if (labelsize != sizeof_namespace_label(ndd)) { dev_dbg(dev, "nsindex%d labelsize %d invalid\n", From 6a1f2e5ed9267ca19187038ac635393c165213ac Mon Sep 17 00:00:00 2001 From: Bryam Vargas Date: Sat, 20 Jun 2026 16:41:31 -0500 Subject: [PATCH 05/17] nvdimm/btt: reject an arena whose nfree is below the lane count The BTT info block's nfree field, the number of reserve free blocks, is read from the medium without validation. btt_freelist_init() and btt_rtt_init() size the per-lane freelist[] and rtt[] arrays by nfree, but the I/O path indexes them by the lane from nd_region_acquire_lane(), which is bounded by nd_region->num_lanes (ND_MAX_LANES), not by nfree. A crafted or foreign arena whose nfree is below the lane count makes freelist[lane]/rtt[lane] run past the allocation: an out-of-bounds write. btt.rst documents the nlanes = min(nfree, num_cpus) invariant, which the code does not currently honor: num_lanes is ND_MAX_LANES regardless of nfree. Reject an arena whose nfree is below num_lanes at discovery, before the per-lane arrays are allocated, enforcing that invariant. Fixes: 5212e11fde4d ("nd_btt: atomic sector updates") Cc: stable@vger.kernel.org Signed-off-by: Bryam Vargas Reviewed-by: Alison Schofield Tested-by: Alison Schofield Link: https://patch.msgid.link/20260620-b4-disp-88b2514b-v1-1-3834e707d232@proton.me Signed-off-by: Alison Schofield --- drivers/nvdimm/btt.c | 8 ++++++++ 1 file changed, 8 insertions(+) diff --git a/drivers/nvdimm/btt.c b/drivers/nvdimm/btt.c index 7e1112960d7f..380e352dc3cc 100644 --- a/drivers/nvdimm/btt.c +++ b/drivers/nvdimm/btt.c @@ -883,6 +883,14 @@ static int discover_arenas(struct btt *btt) arena->external_lba_start = cur_nlba; parse_arena_meta(arena, super, cur_off); + if (arena->nfree < btt->nd_region->num_lanes) { + dev_err(to_dev(arena), + "nfree %u smaller than lane count %d\n", + arena->nfree, btt->nd_region->num_lanes); + ret = -ENODEV; + goto out; + } + ret = log_set_indices(arena); if (ret) { dev_err(to_dev(arena), From 3fc3ebf40b5cf077829d8fc5c66ece5b4c6e66c7 Mon Sep 17 00:00:00 2001 From: John Groves Date: Mon, 15 Jun 2026 16:06:24 +0000 Subject: [PATCH 06/17] dax: fix misleading comment about share/index union in dax_folio_reset_order() The comment in dax_folio_reset_order() claims that DAX maintains an invariant where folio->share != 0 only when folio->mapping == NULL, implying folio->share is zero whenever mapping is non-NULL. This is misleading because folio->share and folio->index are a union -- for non-shared folios with mapping != NULL, reading folio->share returns the file page offset (folio->index), which is typically non-zero. Reword the comment to accurately describe the union aliasing: the assignment clears whichever interpretation of the union word is active (index for non-shared folios, share for shared folios), which is correct because the folio is being released in either case. No functional change -- the code was already correct, only the justification was wrong. Fixes: 59eb73b98ae0b ("dax: Factor out dax_folio_reset_order() helper") Reviewed-by: Jonathan Cameron Reviewed-by: Dave Jiang Reviewed-by: Alison Schofield Signed-off-by: John Groves Link: https://patch.msgid.link/0100019ecc08b8cd-4ee80eeb-1341-4f67-8478-7298129440e9-000000@email.amazonses.com Signed-off-by: Alison Schofield --- fs/dax.c | 12 ++++++------ 1 file changed, 6 insertions(+), 6 deletions(-) diff --git a/fs/dax.c b/fs/dax.c index 6d175cd47a99..df19c9317d10 100644 --- a/fs/dax.c +++ b/fs/dax.c @@ -392,12 +392,12 @@ int dax_folio_reset_order(struct folio *folio) int order = folio_order(folio); /* - * DAX maintains the invariant that folio->share != 0 only when - * folio->mapping == NULL (enforced by dax_folio_make_shared()). - * Equivalently: folio->mapping != NULL implies folio->share == 0. - * Callers ensure share has been decremented to zero before - * calling here, so unconditionally clearing both fields is - * correct. + * Clear the mapping and the index/share union word. folio->share + * and folio->index occupy the same union in struct folio. For + * non-shared folios (mapping != NULL), the union holds folio->index + * (file page offset); for shared folios (mapping == NULL), it holds + * folio->share (reference count). Either way, we are releasing the + * folio and both fields should be zeroed. */ folio->mapping = NULL; folio->share = 0; From e0cb40c3c676786c92ddb7f891a928ce970d6244 Mon Sep 17 00:00:00 2001 From: John Groves Date: Mon, 15 Jun 2026 16:06:32 +0000 Subject: [PATCH 07/17] dax/fsdev: fix multi-range offset in memory_failure handler Fix memory_failure offset calculation for multi-range devices. The old code subtracted ranges[0].range.start from the faulting PFN's physical address, which produces an incorrect (inflated) logical offset when the PFN falls in ranges[1] or beyond due to physical gaps between ranges. Add fsdev_pfn_to_offset() to walk the range list and compute the correct device-linear byte offset relative to ranges[0].start (the device data start) -- the base the holder (xfs, famfs) maps from -- for both static and dynamic devices. V5 walked the pagemap's immutable pgmap->ranges[] instead, to avoid reading the mutable dev_dax->ranges[] from this callback. That had a different problem: it regressed static devices, where pgmap->ranges[0].start can sit data_offset below the data start, so the reported offset came out data_offset too high and the holder would act on the wrong blocks. For dynamic devices the two arrays are identical, so pgmap->ranges[] only ever helped the dynamic case while breaking the static one. Walk dev_dax->ranges[] instead. (Richard Cheng spotted the static regression.) Reading dev_dax->ranges[] here may race a concurrent krealloc() of the range array via sysfs (mapping_store(), under dax_region_rwsem, which this ->memory_failure callback does not hold). That exposure is pre-existing -- the original single-range code read dev_dax->ranges[0] locklessly as well -- so this patch does not make it worse; a proper fix (locking or snapshotting) belongs in a separate change. Fixes: d5406bd458b0a ("dax: add fsdev.c driver for fs-dax on character dax") Reviewed-by: Dave Jiang Reviewed-by: Alison Schofield Signed-off-by: John Groves Reviewed-by: Richard Cheng Link: https://patch.msgid.link/0100019ecc08d74f-ec0d09b8-11e9-4e5b-af48-8c6d382af486-000000@email.amazonses.com Signed-off-by: Alison Schofield --- drivers/dax/fsdev.c | 17 ++++++++++++++++- 1 file changed, 16 insertions(+), 1 deletion(-) diff --git a/drivers/dax/fsdev.c b/drivers/dax/fsdev.c index 188b2526bee4..f315533b299e 100644 --- a/drivers/dax/fsdev.c +++ b/drivers/dax/fsdev.c @@ -135,11 +135,26 @@ static void fsdev_clear_ops(void *data) * The core mm code in free_zone_device_folio() handles the wake_up_var() * directly for this memory type. */ +static u64 fsdev_pfn_to_offset(struct dev_dax *dev_dax, unsigned long pfn) +{ + phys_addr_t phys = PFN_PHYS(pfn); + u64 offset = 0; + + for (int i = 0; i < dev_dax->nr_range; i++) { + struct range *range = &dev_dax->ranges[i].range; + + if (phys >= range->start && phys <= range->end) + return offset + (phys - range->start); + offset += range_len(range); + } + return -1ULL; +} + static int fsdev_pagemap_memory_failure(struct dev_pagemap *pgmap, unsigned long pfn, unsigned long nr_pages, int mf_flags) { struct dev_dax *dev_dax = pgmap->owner; - u64 offset = PFN_PHYS(pfn) - dev_dax->ranges[0].range.start; + u64 offset = fsdev_pfn_to_offset(dev_dax, pfn); u64 len = nr_pages << PAGE_SHIFT; return dax_holder_notify_failure(dev_dax->dax_dev, offset, From e0239229931faf9ca3367e3befcf16f77b2cd45b Mon Sep 17 00:00:00 2001 From: John Groves Date: Mon, 15 Jun 2026 16:06:46 +0000 Subject: [PATCH 08/17] dax/fsdev: clear vmemmap_shift when binding static pgmap Clear pgmap->vmemmap_shift for static DAX devices. When rebinding a static device from device_dax (which may set vmemmap_shift based on alignment) to fsdev_dax, the stale vmemmap_shift persists on the shared pgmap. Explicitly zero it before devm_memremap_pages() so the vmemmap is built for order-0 folios as fsdev requires. Fixes: d5406bd458b0a ("dax: add fsdev.c driver for fs-dax on character dax") Reviewed-by: Dave Jiang Reviewed-by: Alison Schofield Signed-off-by: John Groves Link: https://patch.msgid.link/0100019ecc090eea-7c46f51e-5393-402c-850d-78059bb6d343-000000@email.amazonses.com Signed-off-by: Alison Schofield --- drivers/dax/fsdev.c | 1 + 1 file changed, 1 insertion(+) diff --git a/drivers/dax/fsdev.c b/drivers/dax/fsdev.c index f315533b299e..dbd722ed7ab0 100644 --- a/drivers/dax/fsdev.c +++ b/drivers/dax/fsdev.c @@ -237,6 +237,7 @@ static int fsdev_dax_probe(struct dev_dax *dev_dax) } pgmap = dev_dax->pgmap; + pgmap->vmemmap_shift = 0; } else { size_t pgmap_size; From caf906009d12cb21b934f1c4d17cedc6d469429f Mon Sep 17 00:00:00 2001 From: John Groves Date: Mon, 15 Jun 2026 16:06:53 +0000 Subject: [PATCH 09/17] dax/fsdev: don't leave a dangling dev_dax->pgmap on probe failure After the dynamic path set dev_dax->pgmap, any later probe failure left dev_dax->pgmap dangling: devres frees the devm_kzalloc'd pgmap on probe failure, and subsequent probe attempts would hit the "dynamic-dax with pre-populated page map" check and fail permanently. Factor pgmap acquisition out into fsdev_acquire_pgmap(), and defer the dev_dax->pgmap assignment until probe can no longer fail. A failed probe now never publishes the pointer at all, so there is nothing to unwind. This also matches kill_dev_dax(), which already clears the dynamic pgmap pointer on unbind: dev_dax->pgmap is now non-NULL only while the pgmap is actually valid. Refactor suggested by Dave Jiang. Fixes: d5406bd458b0a ("dax: add fsdev.c driver for fs-dax on character dax") Reviewed-by: Dave Jiang Signed-off-by: John Groves Link: https://patch.msgid.link/0100019ecc092ca1-ffc7a5fd-1252-4be5-882c-fd5efdc102a9-000000@email.amazonses.com Signed-off-by: Alison Schofield --- drivers/dax/fsdev.c | 85 ++++++++++++++++++++++++++++----------------- 1 file changed, 53 insertions(+), 32 deletions(-) diff --git a/drivers/dax/fsdev.c b/drivers/dax/fsdev.c index dbd722ed7ab0..0fd5e1293d72 100644 --- a/drivers/dax/fsdev.c +++ b/drivers/dax/fsdev.c @@ -219,6 +219,48 @@ static const struct file_operations fsdev_fops = { .release = fsdev_release, }; +/* + * Acquire the dev_pagemap for probe: the static (pre-populated) one if + * present, or a devm-allocated one for the dynamic case. Note that + * dev_dax->pgmap is not set here; fsdev_dax_probe() sets it only once + * probe succeeds, so a failed probe never leaves a dangling pointer + * to a devres-freed pgmap. + */ +static struct dev_pagemap *fsdev_acquire_pgmap(struct dev_dax *dev_dax) +{ + struct device *dev = &dev_dax->dev; + struct dev_pagemap *pgmap; + size_t pgmap_size; + + if (static_dev_dax(dev_dax)) { + if (dev_dax->nr_range > 1) { + dev_warn(dev, + "static pgmap / multi-range device conflict\n"); + return ERR_PTR(-EINVAL); + } + + pgmap = dev_dax->pgmap; + pgmap->vmemmap_shift = 0; + return pgmap; + } + + if (dev_dax->pgmap) { + dev_warn(dev, "dynamic-dax with pre-populated page map\n"); + return ERR_PTR(-EINVAL); + } + + pgmap_size = struct_size(pgmap, ranges, dev_dax->nr_range - 1); + pgmap = devm_kzalloc(dev, pgmap_size, GFP_KERNEL); + if (!pgmap) + return ERR_PTR(-ENOMEM); + + pgmap->nr_range = dev_dax->nr_range; + for (int i = 0; i < dev_dax->nr_range; i++) + pgmap->ranges[i] = dev_dax->ranges[i].range; + + return pgmap; +} + static int fsdev_dax_probe(struct dev_dax *dev_dax) { struct dax_device *dax_dev = dev_dax->dax_dev; @@ -230,36 +272,9 @@ static int fsdev_dax_probe(struct dev_dax *dev_dax) void *addr; int rc, i; - if (static_dev_dax(dev_dax)) { - if (dev_dax->nr_range > 1) { - dev_warn(dev, "static pgmap / multi-range device conflict\n"); - return -EINVAL; - } - - pgmap = dev_dax->pgmap; - pgmap->vmemmap_shift = 0; - } else { - size_t pgmap_size; - - if (dev_dax->pgmap) { - dev_warn(dev, "dynamic-dax with pre-populated page map\n"); - return -EINVAL; - } - - pgmap_size = struct_size(pgmap, ranges, dev_dax->nr_range - 1); - pgmap = devm_kzalloc(dev, pgmap_size, GFP_KERNEL); - if (!pgmap) - return -ENOMEM; - - pgmap->nr_range = dev_dax->nr_range; - dev_dax->pgmap = pgmap; - - for (i = 0; i < dev_dax->nr_range; i++) { - struct range *range = &dev_dax->ranges[i].range; - - pgmap->ranges[i] = *range; - } - } + pgmap = fsdev_acquire_pgmap(dev_dax); + if (IS_ERR(pgmap)) + return PTR_ERR(pgmap); for (i = 0; i < dev_dax->nr_range; i++) { struct range *range = &dev_dax->ranges[i].range; @@ -306,7 +321,7 @@ static int fsdev_dax_probe(struct dev_dax *dev_dax) /* Detect whether the data is at a non-zero offset into the memory */ if (pgmap->range.start != dev_dax->ranges[0].range.start) { u64 phys = dev_dax->ranges[0].range.start; - u64 pgmap_phys = dev_dax->pgmap[0].range.start; + u64 pgmap_phys = pgmap[0].range.start; if (!WARN_ON(pgmap_phys > phys)) data_offset = phys - pgmap_phys; @@ -339,7 +354,13 @@ static int fsdev_dax_probe(struct dev_dax *dev_dax) return rc; run_dax(dax_dev); - return devm_add_action_or_reset(dev, fsdev_kill, dev_dax); + rc = devm_add_action_or_reset(dev, fsdev_kill, dev_dax); + if (rc) + return rc; + + /* Probe can no longer fail; expose the pgmap via dev_dax */ + dev_dax->pgmap = pgmap; + return 0; } static struct dax_device_driver fsdev_dax_driver = { From f48884ac31b6bfc99f36b3f207b8c0cbe5d54bd7 Mon Sep 17 00:00:00 2001 From: John Groves Date: Mon, 15 Jun 2026 16:07:01 +0000 Subject: [PATCH 10/17] dax/fsdev: clear pgmap ops and owner on unbind fsdev_dax_probe() sets pgmap->ops = &fsdev_pagemap_ops and pgmap->owner = dev_dax, but nothing ever clears them. For a dynamic device the pgmap is devm-allocated and freed on unbind, so this is harmless. For a static device the pgmap is the shared, long-lived one owned by the dax bus (kill_dev_dax() only NULLs dev_dax->pgmap for the non-static case), and device.c's probe sets only pgmap->type, never clearing ops/owner. So after fsdev unbinds a static device the stale fsdev_pagemap_ops survives on the shared pgmap. If the device is then rebound to device_dax (MEMORY_DEVICE_GENERIC, which installs no ->memory_failure), or the fsdev_dax module is unloaded, a subsequent memory_failure on that pgmap dispatches through the stale -- and possibly freed -- handler. Register a devm action that clears pgmap->ops and pgmap->owner on unbind, symmetric with setting them at probe, so the pgmap carries no fsdev state once fsdev is detached. Suggested-by: Richard Cheng Fixes: d5406bd458b0a ("dax: add fsdev.c driver for fs-dax on character dax") Signed-off-by: John Groves Reviewed-by: Richard Cheng Link: https://patch.msgid.link/0100019ecc094b6e-fc163bde-0396-4a33-909f-fb88e740be27-000000@email.amazonses.com Signed-off-by: Alison Schofield --- drivers/dax/fsdev.c | 22 ++++++++++++++++++++++ 1 file changed, 22 insertions(+) diff --git a/drivers/dax/fsdev.c b/drivers/dax/fsdev.c index 0fd5e1293d72..68a4369562f7 100644 --- a/drivers/dax/fsdev.c +++ b/drivers/dax/fsdev.c @@ -127,6 +127,23 @@ static void fsdev_clear_ops(void *data) dax_set_ops(dev_dax->dax_dev, NULL); } +static void fsdev_clear_pgmap_ops(void *data) +{ + struct dev_pagemap *pgmap = data; + + /* + * fsdev installs pgmap->ops and ->owner at probe. For a static device + * the pgmap is shared and long-lived (owned by the dax bus), so + * leaving fsdev's ops behind on unbind would let a later + * memory_failure -- after rebind to another driver, or after this + * module is unloaded -- dispatch through a stale or freed + * ->memory_failure handler. Clear them so the pgmap carries no fsdev + * state once we are unbound. + */ + pgmap->ops = NULL; + pgmap->owner = NULL; +} + /* * Page map operations for FS-DAX mode * Similar to fsdax_pagemap_ops in drivers/nvdimm/pmem.c @@ -306,6 +323,11 @@ static int fsdev_dax_probe(struct dev_dax *dev_dax) if (IS_ERR(addr)) return PTR_ERR(addr); + /* Drop fsdev's pgmap->ops/owner on unbind so no stale ops survive. */ + rc = devm_add_action_or_reset(dev, fsdev_clear_pgmap_ops, pgmap); + if (rc) + return rc; + /* * Clear any stale compound folio state left over from a previous * driver (e.g., device_dax with vmemmap_shift). Also register this From ff7c73fca793bd5c29a15ba735b0886f62f3a840 Mon Sep 17 00:00:00 2001 From: John Groves Date: Mon, 15 Jun 2026 16:07:10 +0000 Subject: [PATCH 11/17] dax/fsdev: use __va(phys) for kaddr in direct_access Use __va(phys) instead of virt_addr + linear_offset for the kaddr return in __fsdev_dax_direct_access(). The previous code added a device-linear byte offset to virt_addr (which is __va of ranges[0]), but for multi-range devices with physical gaps between ranges, this linear arithmetic crosses the gap and produces a wrong kernel virtual address. Using __va(phys) where phys comes from dax_pgoff_to_phys() is correct for any range layout because the direct map translates each physical address independently. This leaves dev_dax->virt_addr write-only, so remove the field (suggested by Dave Jiang). Fixes: 759455848df0b ("dax: Save the kva from memremap") Reviewed-by: Dave Jiang Reviewed-by: Alison Schofield Signed-off-by: John Groves Link: https://patch.msgid.link/0100019ecc096de8-8bc254a7-d2cc-44b6-82b1-1394fda8bb41-000000@email.amazonses.com Signed-off-by: Alison Schofield --- drivers/dax/dax-private.h | 2 -- drivers/dax/fsdev.c | 8 ++------ 2 files changed, 2 insertions(+), 8 deletions(-) diff --git a/drivers/dax/dax-private.h b/drivers/dax/dax-private.h index 81e4af49e39c..607a53a91f58 100644 --- a/drivers/dax/dax-private.h +++ b/drivers/dax/dax-private.h @@ -69,7 +69,6 @@ struct dev_dax_range { * data while the device is activated in the driver. * @region: parent region * @dax_dev: core dax functionality - * @virt_addr: kva from memremap; used by fsdev_dax * @cached_size: size of daxdev cached by fsdev_dax * @align: alignment of this instance * @target_node: effective numa node if dev_dax memory range is onlined @@ -85,7 +84,6 @@ struct dev_dax_range { struct dev_dax { struct dax_region *region; struct dax_device *dax_dev; - void *virt_addr; u64 cached_size; unsigned int align; int target_node; diff --git a/drivers/dax/fsdev.c b/drivers/dax/fsdev.c index 68a4369562f7..57c589e19b53 100644 --- a/drivers/dax/fsdev.c +++ b/drivers/dax/fsdev.c @@ -51,9 +51,7 @@ static long __fsdev_dax_direct_access(struct dax_device *dax_dev, pgoff_t pgoff, struct dev_dax *dev_dax = dax_get_private(dax_dev); size_t size = nr_pages << PAGE_SHIFT; size_t offset = pgoff << PAGE_SHIFT; - void *virt_addr = dev_dax->virt_addr + offset; phys_addr_t phys; - unsigned long local_pfn; phys = dax_pgoff_to_phys(dev_dax, pgoff, size); if (phys == -1) { @@ -63,11 +61,10 @@ static long __fsdev_dax_direct_access(struct dax_device *dax_dev, pgoff_t pgoff, } if (kaddr) - *kaddr = virt_addr; + *kaddr = __va(phys); - local_pfn = PHYS_PFN(phys); if (pfn) - *pfn = local_pfn; + *pfn = PHYS_PFN(phys); /* * Use cached_size which was computed at probe time. The size cannot @@ -351,7 +348,6 @@ static int fsdev_dax_probe(struct dev_dax *dev_dax) pr_debug("%s: offset detected phys=%llx pgmap_phys=%llx offset=%llx\n", __func__, phys, pgmap_phys, data_offset); } - dev_dax->virt_addr = addr + data_offset; inode = dax_inode(dax_dev); cdev = inode->i_cdev; From 755effecd6fc7d8ff18f09135cb5e3cf98c20d55 Mon Sep 17 00:00:00 2001 From: John Groves Date: Mon, 15 Jun 2026 16:07:21 +0000 Subject: [PATCH 12/17] dax/fsdev: fail probe on invalid pgmap offset Convert the WARN_ON to a fatal error when pgmap_phys > phys. This condition means the remapped region starts after the device's data region, which is an impossible state. Previously the probe continued with data_offset=0, leaving virt_addr silently misaligned. Now probe returns -EINVAL with a diagnostic message. Fixes: 759455848df0b ("dax: Save the kva from memremap") Reviewed-by: Dave Jiang Reviewed-by: Alison Schofield Reviewed-by: Pankaj Gupta Signed-off-by: John Groves Link: https://patch.msgid.link/0100019ecc0999fa-97574544-8b6b-46cf-9f33-423abdbeee7f-000000@email.amazonses.com Signed-off-by: Alison Schofield --- drivers/dax/fsdev.c | 8 ++++++-- 1 file changed, 6 insertions(+), 2 deletions(-) diff --git a/drivers/dax/fsdev.c b/drivers/dax/fsdev.c index 57c589e19b53..d50891d6dc13 100644 --- a/drivers/dax/fsdev.c +++ b/drivers/dax/fsdev.c @@ -342,8 +342,12 @@ static int fsdev_dax_probe(struct dev_dax *dev_dax) u64 phys = dev_dax->ranges[0].range.start; u64 pgmap_phys = pgmap[0].range.start; - if (!WARN_ON(pgmap_phys > phys)) - data_offset = phys - pgmap_phys; + if (pgmap_phys > phys) { + dev_err(dev, "pgmap start %#llx exceeds data start %#llx\n", + pgmap_phys, phys); + return -EINVAL; + } + data_offset = phys - pgmap_phys; pr_debug("%s: offset detected phys=%llx pgmap_phys=%llx offset=%llx\n", __func__, phys, pgmap_phys, data_offset); From 7ae9d15bdcde0f2955ae13b6a95587f9e23b2359 Mon Sep 17 00:00:00 2001 From: John Groves Date: Mon, 15 Jun 2026 16:07:30 +0000 Subject: [PATCH 13/17] dax: read holder_ops once in dax_holder_notify_failure() dax_holder_notify_failure() reads dax_dev->holder_ops twice without READ_ONCE() -- once for the NULL check and once for the indirect notify_failure() call. A concurrent fs_put_dax() can clear holder_ops between the two reads, so the check can observe a non-NULL pointer while the call dereferences NULL. (kill_dax() also clears holder_ops, but only after synchronize_srcu(), so it cannot race a reader that is inside dax_read_lock(); fs_put_dax() does no such synchronization.) Fetch holder_ops once into a local with READ_ONCE() so the NULL check and the indirect call observe the same value. Fixes: 8012b86608552 ("dax: introduce holder for dax_device") Suggested-by: Richard Cheng Reviewed-by: Richard Cheng Signed-off-by: John Groves Link: https://patch.msgid.link/0100019ecc09bb56-5ecc9c6b-35ba-44f8-b112-921b01b34478-000000@email.amazonses.com Signed-off-by: Alison Schofield --- drivers/dax/super.c | 12 ++++++++++-- 1 file changed, 10 insertions(+), 2 deletions(-) diff --git a/drivers/dax/super.c b/drivers/dax/super.c index 25cf99dd9360..433cd431a6c0 100644 --- a/drivers/dax/super.c +++ b/drivers/dax/super.c @@ -303,6 +303,7 @@ EXPORT_SYMBOL_GPL(dax_recovery_write); int dax_holder_notify_failure(struct dax_device *dax_dev, u64 off, u64 len, int mf_flags) { + const struct dax_holder_operations *ops; int rc, id; id = dax_read_lock(); @@ -311,12 +312,19 @@ int dax_holder_notify_failure(struct dax_device *dax_dev, u64 off, goto out; } - if (!dax_dev->holder_ops) { + /* + * Read holder_ops once: a concurrent fs_put_dax() can clear it without + * synchronizing against readers. Without the single fetch the compiler + * could reload between the NULL check and the call and dereference a + * NULL ops. + */ + ops = READ_ONCE(dax_dev->holder_ops); + if (!ops) { rc = -EOPNOTSUPP; goto out; } - rc = dax_dev->holder_ops->notify_failure(dax_dev, off, len, mf_flags); + rc = ops->notify_failure(dax_dev, off, len, mf_flags); out: dax_read_unlock(id); return rc; From 7a6db2eb6d5da9ade0d4802e25bbec3342ae0187 Mon Sep 17 00:00:00 2001 From: John Groves Date: Mon, 15 Jun 2026 16:07:38 +0000 Subject: [PATCH 14/17] dax: fix holder_ops race in fs_put_dax() Clear holder_ops before holder_data so that a concurrent fs_dax_get() cannot have its newly installed holder_ops overwritten. cmpxchg() provides release ordering on weakly-ordered architectures, ensuring the WRITE_ONCE(holder_ops, NULL) store is visible to any CPU that observes the holder_data release. Add a WARN_ON() that fires only when the cmpxchg observes a non-NULL value that is not @holder, i.e. fs_put_dax() called by something that is not the current holder. That is an API contract violation; the WARN_ON() does not prevent the damage but makes the bug visible. A NULL cmpxchg result is deliberately tolerated: kill_dax() clears holder_data while a holder is still attached when a device is removed out from under a mounted filesystem (after delivering MF_MEM_PRE_REMOVE). The holder's subsequent fs_put_dax() - e.g. xfs_free_buftarg() after a forced shutdown - then legitimately finds holder_data already NULL, so warning on that case would turn supported device removal into a splat (or a panic with panic_on_warn). Also add a kerneldoc comment documenting that fs_put_dax() must only be called by the current holder. Fixes: eec38f5d86d27 ("dax: Add fs_dax_get() func to prepare dax for fs-dax usage") Signed-off-by: John Groves Reviewed-by: Alison Schofield Link: https://patch.msgid.link/0100019ecc09dcab-2f4aa175-0b84-4b36-9e54-ebff302ebb0a-000000@email.amazonses.com Signed-off-by: Alison Schofield --- drivers/dax/super.c | 42 +++++++++++++++++++++++++++++++++++++++--- 1 file changed, 39 insertions(+), 3 deletions(-) diff --git a/drivers/dax/super.c b/drivers/dax/super.c index 433cd431a6c0..45f84b0eb909 100644 --- a/drivers/dax/super.c +++ b/drivers/dax/super.c @@ -116,11 +116,47 @@ EXPORT_SYMBOL_GPL(fs_dax_get_by_bdev); #if IS_ENABLED(CONFIG_FS_DAX) +/** + * fs_put_dax() - release holder ownership of a dax_device + * @dax_dev: dax device to release (may be NULL) + * @holder: the holder pointer previously passed to fs_dax_get() or + * fs_dax_get_by_bdev(); must match exactly, as it is used + * in a cmpxchg to atomically release ownership + * + * Must only be called by the current holder. Clears holder_ops before + * holder_data to avoid a race where a concurrent fs_dax_get() could have + * its newly installed holder_ops overwritten. + */ void fs_put_dax(struct dax_device *dax_dev, void *holder) { - if (dax_dev && holder && - cmpxchg(&dax_dev->holder_data, holder, NULL) == holder) - dax_dev->holder_ops = NULL; + if (dax_dev && holder) { + void *prev; + + /* + * Clear holder_ops before releasing holder_data. A concurrent + * dax_holder_notify_failure() that sees NULL ops returns + * -EOPNOTSUPP cleanly. A concurrent fs_dax_get() that acquires + * holder_data after the cmpxchg below is guaranteed to observe + * holder_ops=NULL first (cmpxchg provides release ordering), so + * its subsequent store of new ops will not be overwritten. + */ + WRITE_ONCE(dax_dev->holder_ops, NULL); + prev = cmpxchg(&dax_dev->holder_data, holder, NULL); + + /* + * prev == holder: normal release. + * prev == NULL: already released by kill_dax() when the + * device was removed under a live holder; + * not a bug. + * prev != holder (non-NULL): fs_put_dax() called by something + * that is not the current holder; an API + * contract violation. A lock would be needed + * to guard against this, but we WARN_ON() + * instead since violating the contract is + * a bug. + */ + WARN_ON(prev && prev != holder); + } put_dax(dax_dev); } EXPORT_SYMBOL_GPL(fs_put_dax); From 6191eeb6c70b41f7bc71967055adab5ef93274dc Mon Sep 17 00:00:00 2001 From: John Groves Date: Mon, 15 Jun 2026 16:07:45 +0000 Subject: [PATCH 15/17] dax: fsdev.c minor formatting cleanup Address some comments from Jonathan that were missed in the merged series. Fix line wrapping in fsdev_dax_recovery_write() and fsdev_dax_zero_page_range() signatures. Reviewed-by: Dave Jiang Reviewed-by: Alison Schofield Signed-off-by: John Groves Link: https://patch.msgid.link/0100019ecc09f607-b558c192-72fc-4c2d-9f64-3b82796e7dd4-000000@email.amazonses.com Signed-off-by: Alison Schofield --- drivers/dax/fsdev.c | 15 ++++++++------- 1 file changed, 8 insertions(+), 7 deletions(-) diff --git a/drivers/dax/fsdev.c b/drivers/dax/fsdev.c index d50891d6dc13..598604bf5ac5 100644 --- a/drivers/dax/fsdev.c +++ b/drivers/dax/fsdev.c @@ -45,8 +45,8 @@ static void fsdev_write_dax(void *addr, struct page *page, } static long __fsdev_dax_direct_access(struct dax_device *dax_dev, pgoff_t pgoff, - long nr_pages, enum dax_access_mode mode, void **kaddr, - unsigned long *pfn) + long nr_pages, enum dax_access_mode mode, void **kaddr, + unsigned long *pfn) { struct dev_dax *dev_dax = dax_get_private(dax_dev); size_t size = nr_pages << PAGE_SHIFT; @@ -80,7 +80,8 @@ static int fsdev_dax_zero_page_range(struct dax_device *dax_dev, long rc; WARN_ONCE(nr_pages > 1, "%s: nr_pages > 1\n", __func__); - rc = __fsdev_dax_direct_access(dax_dev, pgoff, 1, DAX_ACCESS, &kaddr, NULL); + rc = __fsdev_dax_direct_access(dax_dev, pgoff, 1, DAX_ACCESS, + &kaddr, NULL); if (rc < 0) return rc; fsdev_write_dax(kaddr, ZERO_PAGE(0), 0, PAGE_SIZE); @@ -88,15 +89,15 @@ static int fsdev_dax_zero_page_range(struct dax_device *dax_dev, } static long fsdev_dax_direct_access(struct dax_device *dax_dev, - pgoff_t pgoff, long nr_pages, enum dax_access_mode mode, - void **kaddr, unsigned long *pfn) + pgoff_t pgoff, long nr_pages, enum dax_access_mode mode, + void **kaddr, unsigned long *pfn) { return __fsdev_dax_direct_access(dax_dev, pgoff, nr_pages, mode, kaddr, pfn); } -static size_t fsdev_dax_recovery_write(struct dax_device *dax_dev, pgoff_t pgoff, - void *addr, size_t bytes, struct iov_iter *i) +static size_t fsdev_dax_recovery_write(struct dax_device *dax_dev, + pgoff_t pgoff, void *addr, size_t bytes, struct iov_iter *i) { return _copy_from_iter_flushcache(addr, bytes, i); } From 1c454cec28d6ef9f04c32b10d268e699e09573a4 Mon Sep 17 00:00:00 2001 From: Randy Dunlap Date: Tue, 21 Jul 2026 10:21:31 -0700 Subject: [PATCH 16/17] libnvdimm: nd.h: clean up kernel-doc warnings Add missing struct member, function parameters, and function return values to eliminate all kernel-doc warnings: Warning: include/linux/nd.h:124 struct member 'common' not described in 'nd_namespace_io' Warning: include/linux/nd.h:124 Excess struct member 'dev' description in 'nd_namespace_io' Warning: include/linux/nd.h:166 function parameter 'flags' not described in 'nvdimm_read_bytes' Warning: include/linux/nd.h:166 No description found for return value of 'nvdimm_read_bytes' Warning: include/linux/nd.h:185 function parameter 'flags' not described in 'nvdimm_write_bytes' Warning: include/linux/nd.h:185 No description found for return value of 'nvdimm_write_bytes' Signed-off-by: Randy Dunlap Reviewed-by: Dave Jiang Link: https://patch.msgid.link/20260721172132.3718892-1-rdunlap@infradead.org Signed-off-by: Alison Schofield --- include/linux/nd.h | 8 +++++++- 1 file changed, 7 insertions(+), 1 deletion(-) diff --git a/include/linux/nd.h b/include/linux/nd.h index fa099e295f78..62988000e7a7 100644 --- a/include/linux/nd.h +++ b/include/linux/nd.h @@ -110,7 +110,7 @@ static inline struct nd_namespace_common *to_ndns(struct device *dev) /** * struct nd_namespace_io - device representation of a persistent memory range - * @dev: namespace device created by the nd region driver + * @common: namespace device core infrastructure created by the nd region driver * @res: struct resource conversion of a NFIT SPA table * @size: cached resource_size(@res) for fast path size checks * @addr: virtual address to access the namespace range @@ -158,8 +158,11 @@ static inline struct nd_namespace_pmem *to_nd_namespace_pmem(const struct device * @offset: namespace-relative starting offset * @buf: buffer to fill * @size: transfer length + * @flags: process (0) or atomic (1) context * * @buf is up-to-date upon return from this routine. + * + * Returns: %0 on success or a negative error code on failure */ static inline int nvdimm_read_bytes(struct nd_namespace_common *ndns, resource_size_t offset, void *buf, size_t size, @@ -174,11 +177,14 @@ static inline int nvdimm_read_bytes(struct nd_namespace_common *ndns, * @offset: namespace-relative starting offset * @buf: buffer to drain * @size: transfer length + * @flags: process (0) or atomic (1) context * * NVDIMM Namepaces disks do not implement sectors internally. Depending on * the @ndns, the contents of @buf may be in cpu cache, platform buffers, * or on backing memory media upon return from this routine. Flushing * to media is handled internal to the @ndns driver, if at all. + * + * Returns: %0 on success or a negative error code on failure */ static inline int nvdimm_write_bytes(struct nd_namespace_common *ndns, resource_size_t offset, void *buf, size_t size, From e99cb3ecd8334ca21e01ff9a79a916693f58f1fb Mon Sep 17 00:00:00 2001 From: Randy Dunlap Date: Tue, 21 Jul 2026 10:21:32 -0700 Subject: [PATCH 17/17] nvdimm-btt: clean up kernel-doc warnings Add one missing kernel-doc description and drop one that shouldn't be here in order to avoid kernel-doc warnings: Warning: ./drivers/nvdimm/btt.h:232 struct member 'nd_region' not described in 'btt' Warning: ./drivers/nvdimm/btt.h:232 Excess struct member 'lanes' description in 'btt' Signed-off-by: Randy Dunlap Reviewed-by: Dave Jiang Link: https://patch.msgid.link/20260721172132.3718892-2-rdunlap@infradead.org Signed-off-by: Alison Schofield --- drivers/nvdimm/btt.h | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/drivers/nvdimm/btt.h b/drivers/nvdimm/btt.h index 0c76c0333f6e..1ed245c18250 100644 --- a/drivers/nvdimm/btt.h +++ b/drivers/nvdimm/btt.h @@ -210,7 +210,7 @@ struct badblocks; * @lbasize: LBA size as requested and presented to upper layers. * This is sector_size + size of any metadata. * @sector_size: The Linux sector size - 512 or 4096 - * @lanes: Per-lane spinlocks + * @nd_region: &struct nd_region pointer * @init_lock: Mutex used for the BTT initialization * @init_state: Flag describing the initialization state for the BTT * @num_arenas: Number of arenas in the BTT instance