From: Manish Honap <[email protected]>

Register the HDM region as a CXL VFIO region so the guest can map
the device memory.

CXL.mem is coherent memory, so the guest and KVM need a write-back
mapping. Claim the host physical range exclusively at bind so a
firmware-owned cacheable alias cannot fault the host once the range is
mapped write-back.

The region is advertised mmap-able, so a fd holder can fault its
struct-page-less device memory in from the host CPU. A memory error on
that range would otherwise escalate to a host SError. Register the range
with memory_failure() via register_pfn_address_space() so such an error
is contained to unmapping the range and a SIGBUS to the fd holder.
register_pfn_address_space() returns -EOPNOTSUPP when
CONFIG_MEMORY_FAILURE is off, in which case the region is exposed
without containment.

A CXL Type-2 guest also reads the HDM decoder registers to learn the
HDM region it was handed. Those registers live in the component BAR that
vfio-pci owns, so map the decoder block at bind and copy it into a
per-open shadow, then expose a read-only region backed by that shadow.
The block location comes from the pdev->hdm enumeration cache rather
than the CXL core register map.

The guest programs a GPA into the decoder while the host resolves the
HPA, so it must never reach the physical registers. Serving reads from
the shadow keeps the two apart; write emulation follows in a later
change.

Signed-off-by: Manish Honap <[email protected]>
---
 drivers/vfio/pci/cxl/vfio_cxl_core.c | 347 ++++++++++++++++++++++++++-
 drivers/vfio/pci/vfio_pci_core.c     |  17 ++
 include/linux/vfio_pci_core.h        |   1 +
 include/uapi/linux/vfio.h            |   3 +
 4 files changed, 367 insertions(+), 1 deletion(-)

diff --git a/drivers/vfio/pci/cxl/vfio_cxl_core.c 
b/drivers/vfio/pci/cxl/vfio_cxl_core.c
index 9fed909cb9d3..07047646edde 100644
--- a/drivers/vfio/pci/cxl/vfio_cxl_core.c
+++ b/drivers/vfio/pci/cxl/vfio_cxl_core.c
@@ -5,10 +5,14 @@
  * Copyright (c) 2026 NVIDIA Corporation & Affiliates
  */
 
+#include <linux/cleanup.h>
+#include <linux/memory-failure.h>
+#include <linux/mm.h>
 #include <linux/module.h>
 #include <linux/pci.h>
 #include <linux/range.h>
 #include <linux/slab.h>
+#include <linux/uaccess.h>
 #include <linux/vfio_pci_core.h>
 #include <cxl/cxl.h>
 #include <cxl/pci.h>
@@ -18,21 +22,250 @@
  * @cxlds: CXL device state; kept first for devm_cxl_dev_state_create()
  * @cxlmd: memory device joined to the CXL topology at bind
  * @hpa_range: host physical range of the HDM region
+ * @dpa_pfn_space: HDM-region pfn range registered with memory_failure()
  * @dvsec: CXL device DVSEC config-space offset
  * @dvsec_len: length of the DVSEC body
  * @dvsec_dwords: dword count of @dvsec_shadow
  * @dvsec_shadow: guest view of the CXL DVSEC body, sampled at open
+ * @hdm_regs: mapped HDM decoder registers, source for the open-time snapshot
+ * @hdm_len: length of the HDM decoder register block
+ * @hdm_shadow: guest view of the HDM decoder registers, sampled at open
+ * @hdm_region_idx: vdev->region[] index of the HDM region
+ * @hdm_valid: true when the decoder is in a known-good restored state and host
+ *            CPU access to the HDM range is safe; gated under memory_lock
  */
 struct vfio_cxl_state {
        struct cxl_dev_state cxlds;
        struct cxl_memdev *cxlmd;
        struct range hpa_range;
+       struct pfn_address_space dpa_pfn_space;
        u16 dvsec;
        u32 dvsec_len;
        u32 dvsec_dwords;
+
        u32 *dvsec_shadow __counted_by_ptr(dvsec_dwords);
+       void __iomem *hdm_regs;
+       u32 hdm_len;
+       u32 hdm_dwords;
+
+       __le32 *hdm_shadow __counted_by_ptr(hdm_dwords);
+       int hdm_region_idx;
+       bool hdm_valid;
+};
+
+static unsigned long vfio_cxl_mem_pgoff(struct vm_area_struct *vma,
+                                       unsigned long addr)
+{
+       unsigned long mask = (1U << (VFIO_PCI_OFFSET_SHIFT - PAGE_SHIFT)) - 1;
+
+       return (vma->vm_pgoff & mask) + ((addr - vma->vm_start) >> PAGE_SHIFT);
+}
+
+static vm_fault_t vfio_cxl_mem_huge_fault(struct vm_fault *vmf,
+                                         unsigned int order)
+{
+       struct vm_area_struct *vma = vmf->vma;
+       struct vfio_pci_core_device *vdev = vma->vm_private_data;
+       struct vfio_cxl_state *cxl = vdev->cxl;
+       unsigned long addr = ALIGN_DOWN(vmf->address, PAGE_SIZE << order);
+       unsigned long pfn = PHYS_PFN(cxl->hpa_range.start) +
+                           vfio_cxl_mem_pgoff(vma, addr);
+       vm_fault_t ret = VM_FAULT_FALLBACK;
+
+       if (is_aligned_for_order(vma, addr, pfn, order)) {
+               scoped_guard(rwsem_read, &vdev->memory_lock) {
+                       /*
+                        * A reset or D3 transition takes memory_lock for write,
+                        * revokes this mapping and clears the decoder. Do not
+                        * insert a PFN for a decoder that is not in a 
known-good
+                        * state, or the host CPU could reach a disabled 
decoder.
+                        * vfio_pci_vmf_insert_pfn() adds the Memory-Space gate:
+                        * an HDM access while the device has Memory-Space 
disabled
+                        * aborts on the fabric as a fatal host SError, so it 
must
+                        * not be faulted in until the guest re-enables it.
+                        */
+                       if (cxl->hdm_valid)
+                               ret = vfio_pci_vmf_insert_pfn(vdev, vmf, pfn,
+                                                             order);
+                       else
+                               ret = VM_FAULT_SIGBUS;
+               }
+       }
+
+       return ret;
+}
+
+static vm_fault_t vfio_cxl_mem_fault(struct vm_fault *vmf)
+{
+       return vfio_cxl_mem_huge_fault(vmf, 0);
+}
+
+static const struct vm_operations_struct vfio_cxl_mem_vm_ops = {
+       .fault = vfio_cxl_mem_fault,
+#ifdef CONFIG_ARCH_SUPPORTS_HUGE_PFNMAP
+       .huge_fault = vfio_cxl_mem_huge_fault,
+#endif
+};
+
+static int vfio_cxl_mem_mmap(struct vfio_pci_core_device *vdev,
+                            struct vfio_pci_region *region,
+                            struct vm_area_struct *vma)
+{
+       unsigned long mask = (1U << (VFIO_PCI_OFFSET_SHIFT - PAGE_SHIFT)) - 1;
+       u64 req_start = (vma->vm_pgoff & mask) << PAGE_SHIFT;
+       u64 req_len = vma->vm_end - vma->vm_start;
+
+       if (req_start + req_len > region->size)
+               return -EINVAL;
+
+       /*
+        * CXL.mem is coherent memory, so leave the mapping write-back 
cacheable;
+        * a device or non-cached mapping would break the coherence the guest 
and
+        * KVM depend on. The host physical range is claimed exclusively at 
bind,
+        * so no conflicting cacheable alias remains.
+        */
+       vm_flags_set(vma, VM_IO | VM_PFNMAP | VM_DONTEXPAND | VM_DONTDUMP);
+       vma->vm_ops = &vfio_cxl_mem_vm_ops;
+       vma->vm_private_data = vdev;
+
+       return 0;
+}
+
+static ssize_t vfio_cxl_mem_rw(struct vfio_pci_core_device *vdev,
+                              char __user *buf, size_t count, loff_t *ppos,
+                              bool iswrite)
+{
+       /*
+        * The HDM region advertises READ and WRITE so a VMM can derive an
+        * accessible mmap protection for it, but fd read/write is not 
supported.
+        * The only host-side way to reach the range for a copy is a kernel
+        * mapping of the CXL.mem host physical address (memremap, which reuses
+        * the linear map for this RAM-backed range), and a CPU access through
+        * that mapping aborts on the fabric as a fatal host SError, unlike the
+        * guest-facing mmap fault path which maps the pfn directly. Reject the
+        * transfer rather than fault the host; a consumer mmaps the region and
+        * accesses it that way.
+        */
+       return -EIO;
+}
+
+/*
+ * The CXL regions carry no per-region state (region->data is the shared,
+ * devm-managed vfio_cxl_state), so releasing a region is a no-op. The hook is
+ * still required: vfio_pci_core_disable() calls region->ops->release() for
+ * every region without a NULL check.
+ */
+static void vfio_cxl_region_release(struct vfio_pci_core_device *vdev,
+                                   struct vfio_pci_region *region)
+{
+}
+
+static const struct vfio_pci_regops vfio_cxl_mem_regops = {
+       .rw = vfio_cxl_mem_rw,
+       .mmap = vfio_cxl_mem_mmap,
+       .release = vfio_cxl_region_release,
+};
+
+/*
+ * Map a poisoned HDM-region pfn back to the file offset of each user mapping 
so
+ * memory_failure() can unmap it and signal the fd holder. The region is a
+ * single linear range at hpa_range.start; recover the per-vma file offset the
+ * same way the fault handler derived the pfn.
+ */
+static int vfio_cxl_pfn_to_vma_pgoff(struct vm_area_struct *vma,
+                                    unsigned long pfn, pgoff_t *pgoff)
+{
+       struct vfio_pci_core_device *vdev;
+       struct vfio_cxl_state *cxl;
+       pgoff_t vma_off, pfn_off;
+       unsigned long start_pfn;
+
+       if (vma->vm_ops != &vfio_cxl_mem_vm_ops)
+               return -ENOENT;
+
+       vdev = vma->vm_private_data;
+       cxl = vdev->cxl;
+
+       start_pfn = PHYS_PFN(cxl->hpa_range.start);
+       if (pfn < start_pfn ||
+           pfn >= start_pfn + (range_len(&cxl->hpa_range) >> PAGE_SHIFT))
+               return -EFAULT;
+
+       pfn_off = pfn - start_pfn;
+       vma_off = vma->vm_pgoff &
+                 ((1UL << (VFIO_PCI_OFFSET_SHIFT - PAGE_SHIFT)) - 1);
+       /* Skip VMAs that do not map the pfn, e.g. a partial mmap of the 
region. */
+       if (pfn_off < vma_off || pfn_off - vma_off >= vma_pages(vma))
+               return -EFAULT;
+
+       *pgoff = vma->vm_pgoff + (pfn_off - vma_off);
+       return 0;
+}
+
+/*
+ * The HDM region is struct-page-less device memory, so a memory error on it
+ * cannot be routed through the normal page path. Register the range with
+ * memory_failure() so such an error is contained to unmapping the range and a
+ * SIGBUS to the fd holder instead of escalating to a host SError.
+ */
+static int vfio_cxl_register_pfn_space(struct vfio_pci_core_device *vdev)
+{
+       struct vfio_cxl_state *cxl = vdev->cxl;
+       unsigned long start_pfn = PHYS_PFN(cxl->hpa_range.start);
+
+       cxl->dpa_pfn_space.node.start = start_pfn;
+       cxl->dpa_pfn_space.node.last =
+               start_pfn + (range_len(&cxl->hpa_range) >> PAGE_SHIFT) - 1;
+       cxl->dpa_pfn_space.mapping = vdev->vdev.inode->i_mapping;
+       cxl->dpa_pfn_space.pfn_to_vma_pgoff = vfio_cxl_pfn_to_vma_pgoff;
+
+       return register_pfn_address_space(&cxl->dpa_pfn_space);
+}
+
+static ssize_t vfio_cxl_comp_rw(struct vfio_pci_core_device *vdev,
+                               char __user *buf, size_t count, loff_t *ppos,
+                               bool iswrite)
+{
+       struct vfio_cxl_state *cxl = vdev->cxl;
+       loff_t pos = *ppos & VFIO_PCI_OFFSET_MASK;
+
+       /*
+        * The guest programs a GPA into this decoder and the host resolves the
+        * HPA, so the guest never drives the physical decoder. Reads come from
+        * the open-time snapshot; write emulation lands in a later change.
+        */
+       if (iswrite)
+               return -EINVAL;
+
+       if (pos >= cxl->hdm_len)
+               return -EINVAL;
+
+       count = min_t(size_t, count, cxl->hdm_len - pos);
+       /*
+        * The shadow mirrors the physical decoder, so BASE_LOW/HIGH carry the
+        * host HPA. That is visible only to the trusted VMM holding the fd; the
+        * VMM virtualizes the base so the guest sees its own GPA and never the
+        * host address.
+        */
+       if (copy_to_user(buf, (u8 *)cxl->hdm_shadow + pos, count))
+               return -EFAULT;
+
+       *ppos += count;
+       return count;
+}
+
+static const struct vfio_pci_regops vfio_cxl_comp_regops = {
+       .rw = vfio_cxl_comp_rw,
+       .release = vfio_cxl_region_release,
 };
 
+static void vfio_cxl_release_hpa(void *data)
+{
+       struct vfio_cxl_state *cxl = data;
+
+       release_mem_region(cxl->hpa_range.start, range_len(&cxl->hpa_range));
+}
+
 static int vfio_cxl_init_device(struct vfio_pci_core_device *vdev)
 {
        struct pci_dev *pdev = vdev->pdev;
@@ -92,6 +325,20 @@ static int vfio_cxl_init_device(struct vfio_pci_core_device 
*vdev)
        if (ret)
                return ret;
 
+       /*
+        * Map the HDM decoder registers to sample their programming at open.
+        * The block location comes from the enumeration cache in pdev->hdm, so
+        * this does not reach into the CXL core register map. vfio-pci owns the
+        * BAR, so map without claiming the sub-block.
+        */
+       cxl->hdm_regs = devm_ioremap(&pdev->dev,
+                                    pci_resource_start(pdev, 
pdev->hdm->hdm_bar) +
+                                    pdev->hdm->hdm_offset, 
pdev->hdm->hdm_size);
+       if (!cxl->hdm_regs)
+               return -ENOMEM;
+
+       cxl->hdm_len = pdev->hdm->hdm_size;
+
        ret = cxl_set_capacity(&cxl->cxlds, hdm_size);
        if (ret)
                return ret;
@@ -100,9 +347,30 @@ static int vfio_cxl_init_device(struct 
vfio_pci_core_device *vdev)
        if (IS_ERR(cxlmd))
                return PTR_ERR(cxlmd);
 
+       /*
+        * Own the resolved host physical range outright, and exclusively: mark
+        * it IORESOURCE_EXCLUSIVE so /dev/mem cannot map a conflicting alias 
even
+        * on an IO_STRICT_DEVMEM=n kernel. Firmware that left it as System RAM
+        * would otherwise keep a cacheable alias that faults the host once the
+        * guest maps the range write-back. There is no devm form of the 
exclusive
+        * request, so pair it with a devm release action.
+        */
+       if (!request_mem_region_exclusive(cxl->hpa_range.start,
+                                         range_len(&cxl->hpa_range),
+                                         "vfio-cxl-hdm"))
+               return -EBUSY;
+       ret = devm_add_action_or_reset(&pdev->dev, vfio_cxl_release_hpa, cxl);
+       if (ret)
+               return ret;
+
        cxl->cxlmd = cxlmd;
        vdev->cxl = cxl;
 
+       /*
+        * The VFIO regions and the poison-containment pfn space are set up in
+        * open_device(): vfio_pci_core_disable() tears down all dynamic 
regions on
+        * close, so they must be created per open rather than once at bind.
+        */
        return 0;
 }
 
@@ -114,9 +382,11 @@ static void vfio_cxl_release_device(struct 
vfio_pci_core_device *vdev)
 static int vfio_cxl_open_device(struct vfio_pci_core_device *vdev)
 {
        struct vfio_cxl_state *cxl = vdev->cxl;
+       void __iomem *hdm = cxl->hdm_regs;
        struct pci_dev *pdev = vdev->pdev;
+       __le32 *hdm_shadow;
        u32 hdr, *shadow;
-       int i, dwords;
+       int i, dwords, ret;
 
        /*
         * Sample the DVSEC body now rather than at bind: a low-power
@@ -137,13 +407,88 @@ static int vfio_cxl_open_device(struct 
vfio_pci_core_device *vdev)
        cxl->dvsec_dwords = dwords;
        cxl->dvsec_shadow = shadow;
 
+       dwords = cxl->hdm_len / sizeof(u32);
+       hdm_shadow = kcalloc(dwords, sizeof(__le32), GFP_KERNEL);
+       if (!hdm_shadow) {
+               kfree(shadow);
+               cxl->dvsec_shadow = NULL;
+               return -ENOMEM;
+       }
+
+       for (i = 0; i < dwords; i++)
+               hdm_shadow[i] = cpu_to_le32(readl(hdm + i * sizeof(u32)));
+
+       cxl->hdm_dwords = dwords;
+       cxl->hdm_shadow = hdm_shadow;
+
+       /*
+        * vfio_pci_core_disable() frees all dynamic regions on close, so 
register
+        * them here (per open) rather than at bind. A failed first-open never
+        * reaches close_device(), so unwind on error.
+        *
+        * Advertise READ and WRITE alongside MMAP: a VMM derives the mmap
+        * protection from these flags, so without them the HDM memory is mapped
+        * PROT_NONE and a guest access faults (KVM cannot back the mapping). 
The
+        * flags describe the mmap protection only; fd read/write returns -EIO,
+        * because a host CPU read through a kernel mapping of the coherent
+        * CXL.mem range aborts on the fabric (see vfio_cxl_mem_rw()).
+        */
+       ret = vfio_pci_core_register_dev_region(vdev, VFIO_REGION_TYPE_CXL,
+                                               VFIO_REGION_SUBTYPE_CXL_MEM,
+                                               &vfio_cxl_mem_regops,
+                                               range_len(&cxl->hpa_range),
+                                               VFIO_REGION_INFO_FLAG_READ |
+                                               VFIO_REGION_INFO_FLAG_WRITE |
+                                               VFIO_REGION_INFO_FLAG_MMAP, 
cxl);
+       if (ret)
+               goto err_free_shadows;
+
+       ret = vfio_pci_core_register_dev_region(vdev, VFIO_REGION_TYPE_CXL,
+                                               
VFIO_REGION_SUBTYPE_CXL_COMP_REGS,
+                                               &vfio_cxl_comp_regops, 
cxl->hdm_len,
+                                               VFIO_REGION_INFO_FLAG_READ, 
cxl);
+       if (ret)
+               goto err_unregister_hdm;
+
+       /*
+        * The HDM region is advertised mmap-able, so a fd holder can fault its
+        * struct-page-less device memory in from the host CPU. Register it with
+        * memory_failure() to contain a memory error. -EOPNOTSUPP means
+        * CONFIG_MEMORY_FAILURE is off, so run without containment.
+        */
+       ret = vfio_cxl_register_pfn_space(vdev);
+       if (ret && ret != -EOPNOTSUPP)
+               goto err_unregister_comp;
+
+       /*
+        * The decoder is firmware-committed and the shadow now mirrors it, so
+        * host access to the HDM range is safe. Open the access gate; reset and
+        * power transitions clear it until the decoder is restored.
+        */
+       cxl->hdm_valid = true;
+
        return 0;
+
+err_unregister_comp:
+       vfio_pci_core_unregister_dev_region(vdev);
+err_unregister_hdm:
+       vfio_pci_core_unregister_dev_region(vdev);
+err_free_shadows:
+       kfree(cxl->hdm_shadow);
+       cxl->hdm_shadow = NULL;
+       kfree(cxl->dvsec_shadow);
+       cxl->dvsec_shadow = NULL;
+       return ret;
 }
 
 static void vfio_cxl_close_device(struct vfio_pci_core_device *vdev)
 {
        struct vfio_cxl_state *cxl = vdev->cxl;
 
+       cxl->hdm_valid = false;
+       unregister_pfn_address_space(&cxl->dpa_pfn_space);
+       kfree(cxl->hdm_shadow);
+       cxl->hdm_shadow = NULL;
        kfree(cxl->dvsec_shadow);
        cxl->dvsec_shadow = NULL;
 }
diff --git a/drivers/vfio/pci/vfio_pci_core.c b/drivers/vfio/pci/vfio_pci_core.c
index 470730cdc88b..6f6ebcce86a1 100644
--- a/drivers/vfio/pci/vfio_pci_core.c
+++ b/drivers/vfio/pci/vfio_pci_core.c
@@ -1123,6 +1123,23 @@ int vfio_pci_core_register_dev_region(struct 
vfio_pci_core_device *vdev,
 }
 EXPORT_SYMBOL_GPL(vfio_pci_core_register_dev_region);
 
+/*
+ * Unregister the most recently registered dynamic region. Used to unwind a
+ * partially built region set on an open-time error; regions are otherwise
+ * released together in vfio_pci_core_disable().
+ */
+void vfio_pci_core_unregister_dev_region(struct vfio_pci_core_device *vdev)
+{
+       struct vfio_pci_region *region;
+
+       if (WARN_ON(!vdev->num_regions))
+               return;
+
+       region = &vdev->region[--vdev->num_regions];
+       region->ops->release(vdev, region);
+}
+EXPORT_SYMBOL_GPL(vfio_pci_core_unregister_dev_region);
+
 static int vfio_pci_info_atomic_cap(struct vfio_pci_core_device *vdev,
                                    struct vfio_info_cap *caps)
 {
diff --git a/include/linux/vfio_pci_core.h b/include/linux/vfio_pci_core.h
index b9202cb29d96..294e95b5e881 100644
--- a/include/linux/vfio_pci_core.h
+++ b/include/linux/vfio_pci_core.h
@@ -187,6 +187,7 @@ int vfio_pci_core_register_dev_region(struct 
vfio_pci_core_device *vdev,
                                      unsigned int type, unsigned int subtype,
                                      const struct vfio_pci_regops *ops,
                                      size_t size, u32 flags, void *data);
+void vfio_pci_core_unregister_dev_region(struct vfio_pci_core_device *vdev);
 void vfio_pci_core_set_mmap_exclude(struct vfio_pci_core_device *vdev, int bar,
                                    u64 start, u64 len);
 void vfio_pci_core_close_device(struct vfio_device *core_vdev);
diff --git a/include/uapi/linux/vfio.h b/include/uapi/linux/vfio.h
index 540e62312671..d5b8106625bf 100644
--- a/include/uapi/linux/vfio.h
+++ b/include/uapi/linux/vfio.h
@@ -377,6 +377,9 @@ struct vfio_region_info_cap_type {
 /* sub-types for VFIO_REGION_TYPE_CXL */
 /* CXL.mem HDM region of a Type-2 device, mmap-able */
 #define VFIO_REGION_SUBTYPE_CXL_MEM            (1)
+/* CXL HDM decoder registers, trapped so the guest programs a GPA it never 
owns */
+#define VFIO_REGION_SUBTYPE_CXL_COMP_REGS      (2)
+/* Subtypes 3+ are reserved for future trapped CXL surfaces, e.g. RAS. */
 
 /**
  * struct vfio_region_gfx_edid - EDID region layout.
-- 
2.25.1


Reply via email to