RE: [PATCH v3 06/11] cxl: Add register-virtualization helpers for vfio Type-2 passthrough
Manish Honap <[email protected]>
| Newsgroups | org.kernel.vger.linux-cxl,org.kernel.vger.kvm,org.kernel.vger.linux-doc,org.kernel.vger.linux-kernel,org.kernel.vger.linux-kselftest |
|---|---|
| Message-ID | <IA1PR12MB903005CC9C3E45A99CBD3447BDFA2@IA1PR12MB9030.namprd12.prod.outlook.com> |
> -----Original Message----- > From: Dan Williams (nvidia) <[email protected]> > Sent: 11 July 2026 03:27 > To: Manish Honap <[email protected]>; [email protected]; [email protected]; > [email protected]; [email protected]; [email protected]; Ankit Agrawal > <[email protected]>; [email protected]; > [email protected]; [email protected]; [email protected]; > [email protected]; [email protected] > Cc: Neo Jia <[email protected]>; Krishnakant Jaju <[email protected]>; > Vikram Sethi <[email protected]>; Zhi Wang <[email protected]>; Manish > Honap <[email protected]>; [email protected]; linux- > [email protected]; [email protected]; linux- > [email protected]; [email protected] > Subject: Re: [PATCH v3 06/11] cxl: Add register-virtualization helpers > for vfio Type-2 passthrough > > External email: Use caution opening links or attachments > > > mhonap@ wrote: > > From: Manish Honap <[email protected]> > > > > vfio-pci needs the CXL Device DVSEC body, the HDM Decoder Capability > > block, and the CXL.cache/mem cap-array prefix to be virtualized toward > > a KVM guest in a CXL-spec-compliant way. > > > > Introduce a narrow helper API owned by cxl-core: > > > > struct cxl_passthrough * > > devm_cxl_passthrough_create(struct device *dev, > > struct cxl_dev_state *cxlds); > > > > int cxl_passthrough_dvsec_rw(struct cxl_passthrough *p, u32 off, > > u32 *val, size_t sz, bool write); > > int cxl_passthrough_hdm_rw(struct cxl_passthrough *p, u32 off, > > u32 *val, bool write); > > int cxl_passthrough_cm_rw(struct cxl_passthrough *p, u32 off, > > u32 *val, bool write); > > > > Each helper takes a per-device mutex covering the DVSEC + HDM shadows > > (the CM cap-array snapshot is immutable after create) and dispatches > > by offset to a hand-written write handler against CXL r4.0 §8.1.3 > > (DVSEC: LOCK is RWO, CONTROL/CONTROL2 are RWL gated on CONFIG_LOCK, > > STATUS/STATUS2 are RW1C, RANGE1 is HwInit, RANGE2 is RsvdZ) and > > §8.2.4.20 (HDM: GLOBAL_CTRL RW, decoder CTRL implements > > COMMIT/COMMITTED, decoder BASE/SIZE RWL gated on COMMITTED or > > LOCK_ON_COMMIT, cap header HwInit). > > > > Writes to the CM cap-array are silently discarded because the > > cap-array headers are RO per CXL r4.0 §8.2.4; the write parameter is > > kept on the rw API to make the drop policy explicit at the call site. > > > > The shadows are snapshotted at create time: the DVSEC body from PCI > > config space dword-at-a-time, the CM cap-array and HDM block from the > > cxl-core MMIO mapping at cxlds->reg_map.base. This preserves > > firmware-committed values so the guest reads what the host BIOS > > committed, while writes update the shadow per the per-field write > > semantics above. > > > > The file is gated by the hidden Kconfig CXL_VFIO_PASSTHROUGH so the > > passthrough code stays out of cxl_core when no vfio consumer is > configured. > > > > Scope: firmware-committed, single-decoder, no-interleave Type-2 > > passthrough. Multi-decoder, interleave, and hotplug are out-of-scope > > and rejected at create time (-EOPNOTSUPP for hdm_count != 1). > > Hi Manish, many style comments below, but one hard concern to highlight > is that some component capability registers require 64-bit access. > > > > > Signed-off-by: Manish Honap <[email protected]> > > --- > > drivers/cxl/Kconfig | 7 + > > drivers/cxl/core/Makefile | 1 + > > drivers/cxl/core/passthrough.c | 590 > > +++++++++++++++++++++++++++++++++ > > Likely this should be called vfio.c because nothing in this file is > relevant to the rest of the CXL core. However, it is not even clear > there is value in it living in drivers/cxl/. > > A lot of this code is a VFIO backend implemnentation detail, not > anything that CXL core needs. It also seems to have little if any > dependency on any core symbols. Does it really belong in > drivers/cxl/core/? okay, agreed on the direction. For v4 I'll move this out of drivers/cxl/core/ into the vfio-cxl backend under drivers/vfio/pci/cxl/, which resolves several of these at once: - the file/struct/header get vfio-oriented names - EXPORT_SYMBOL_NS_GPL("CXL") exports go away Any spec-defined register defines that must be shared will move to include/cxl/ rather than being duplicated. > > > include/cxl/passthrough.h | 121 +++++++ > > 4 files changed, 719 insertions(+) > > create mode 100644 drivers/cxl/core/passthrough.c create mode 100644 > > include/cxl/passthrough.h > > > > diff --git a/drivers/cxl/Kconfig b/drivers/cxl/Kconfig index > > 80aeb0d556bd..7c874d486a9c 100644 > > --- a/drivers/cxl/Kconfig > > +++ b/drivers/cxl/Kconfig > > @@ -19,6 +19,13 @@ menuconfig CXL_BUS > > > > if CXL_BUS > > > > +config CXL_VFIO_PASSTHROUGH > > + bool > > + # Hidden symbol selected by VFIO_PCI_CXL to pull > > + # drivers/cxl/core/passthrough.c into cxl_core when a vfio > > + # Type-2 passthrough consumer is configured. Keep silent: no > > + # help text, no default, no user-visible prompt. > > No need for a paragraph of documentation to explain why this prompt has > no documention. If probably just be called CXL_VFIO. okay. I will name the Kconfig symbol to CXL_VFIO and remove the paragraph. > > > + > > config CXL_PCI > > tristate "PCI manageability" > > default CXL_BUS > > diff --git a/drivers/cxl/core/Makefile b/drivers/cxl/core/Makefile > > index ce7213818d3c..0cc80bd35a88 100644 > > --- a/drivers/cxl/core/Makefile > > +++ b/drivers/cxl/core/Makefile > > @@ -22,3 +22,4 @@ cxl_core-$(CONFIG_CXL_EDAC_MEM_FEATURES) += edac.o > > cxl_core-$(CONFIG_CXL_RAS) += ras.o > > cxl_core-$(CONFIG_CXL_RAS) += ras_rch.o > > cxl_core-$(CONFIG_CXL_ATL) += atl.o > > +cxl_core-$(CONFIG_CXL_VFIO_PASSTHROUGH) += passthrough.o > > diff --git a/drivers/cxl/core/passthrough.c > > b/drivers/cxl/core/passthrough.c new file mode 100644 index > > 000000000000..b89829586024 > > --- /dev/null > > +++ b/drivers/cxl/core/passthrough.c > > @@ -0,0 +1,590 @@ > > +// SPDX-License-Identifier: GPL-2.0-only > > +/* Copyright(c) 2026 NVIDIA Corporation. All rights reserved. > > + * > > + * vfio-pci Type-2 device passthrough — CXL register virtualization. > > + * > > + * Owns the CXL spec-defined virtualization semantics for the > > + * - CXL Device DVSEC capability body (CXL r4.0 §8.1.3) > > + * - HDM Decoder Capability block (CXL r4.0 §8.2.4.20) > > + * - CXL.cache/mem (CM) cap-array (CXL r4.0 §8.2.4) > > + * > > + * vfio-pci is the only caller. This file is NOT a generic emulation > > + * framework: every register the guest may touch has a hand-written > > + * write handler against the spec. Reads serve from a shadow > > + * snapshotted at create time; writes update the shadow per the spec > > + * attribute mode for that field. > > + * > > + * Scope: firmware-committed, single-decoder, no-interleave Type-2 > > + * passthrough. Multi-decoder, interleave, and hotplug are > > + * out-of-scope and rejected at create time. > > + */ > > + > > +#include <linux/bitfield.h> > > +#include <linux/bitops.h> > > +#include <linux/cleanup.h> > > +#include <linux/device.h> > > +#include <linux/export.h> > > +#include <linux/io.h> > > +#include <linux/mutex.h> > > +#include <linux/pci.h> > > +#include <linux/pci_ids.h> > > +#include <linux/pci_regs.h> > > +#include <linux/slab.h> > > +#include <linux/types.h> > > +#include <linux/unaligned.h> > > + > > +#include <uapi/cxl/cxl_regs.h> > > + > > +#include <cxlpci.h> > > +#include <cxlmem.h> > > +#include <cxl/cxl.h> > > +#include <cxl/passthrough.h> > > + > > +#include "core.h" > > + > > +/* DVSEC CXL Device body offsets — relative to DVSEC capability > start. > > + * Body begins at PCI_DVSEC_CXL_CAP (0x0a); preceding bytes are the > > +PCI > > + * ext-cap header and DVSEC headers handled by the generic vfio > > + * perm-bits path. > > + */ > > +#define DVSEC_OFF_CAPABILITY PCI_DVSEC_CXL_CAP /* 0x0a, > u16 */ > > +#define DVSEC_OFF_CONTROL PCI_DVSEC_CXL_CTRL /* 0x0c, > u16 */ > > +#define DVSEC_OFF_STATUS 0x0e /* u16 > */ > > +#define DVSEC_OFF_CONTROL2 0x10 /* u16 > */ > > +#define DVSEC_OFF_STATUS2 0x12 /* u16 > */ > > +#define DVSEC_OFF_LOCK 0x14 > /* u16 */ > > +#define DVSEC_OFF_RANGE1_SIZE_HI 0x18 /* u32 > */ > > +#define DVSEC_OFF_RANGE1_SIZE_LO 0x1c > > +#define DVSEC_OFF_RANGE1_BASE_HI 0x20 > > +#define DVSEC_OFF_RANGE1_BASE_LO 0x24 > > +#define DVSEC_OFF_RANGE2_SIZE_HI 0x28 > > +#define DVSEC_OFF_RANGE2_SIZE_LO 0x2c > > +#define DVSEC_OFF_RANGE2_BASE_HI 0x30 > > +#define DVSEC_OFF_RANGE2_BASE_LO 0x34 > > +#define DVSEC_BODY_END 0x38 > > Please do not duplicate the existing definitions in > include/uapi/linux/pci_regs.h. > > > + > > +#define DVSEC_LOCK_CONFIG_LOCK BIT(0) > > > + > > +/* HDM Decoder Capability block offsets — relative to HDM block base. > > + * Decoder N register set starts at 0x10 + N * 0x20. > > + */ > > +#define HDM_OFF_CAP_HEADER 0x00 > > +#define HDM_OFF_GLOBAL_CTRL 0x04 > > +#define HDM_DEC_BASE 0x10 > > +#define HDM_DEC_STRIDE 0x20 > > +#define HDM_DEC_OFF_BASE_LO(n) (HDM_DEC_BASE + (n) * > HDM_DEC_STRIDE + 0x00) > > +#define HDM_DEC_OFF_BASE_HI(n) (HDM_DEC_BASE + (n) * > HDM_DEC_STRIDE + 0x04) > > +#define HDM_DEC_OFF_SIZE_LO(n) (HDM_DEC_BASE + (n) * > HDM_DEC_STRIDE + 0x08) > > +#define HDM_DEC_OFF_SIZE_HI(n) (HDM_DEC_BASE + (n) * > HDM_DEC_STRIDE + 0x0c) > > +#define HDM_DEC_OFF_CTRL(n) (HDM_DEC_BASE + (n) * > HDM_DEC_STRIDE + 0x10) > > + > > +/* HDM Decoder CTRL bits per CXL r4.0 §8.2.4.20.5. */ > > +#define HDM_CTRL_LOCK_ON_COMMIT BIT(8) > > +#define HDM_CTRL_COMMIT BIT(9) > > +#define HDM_CTRL_COMMITTED BIT(10) > > +#define HDM_CTRL_ERR_NOT_COMMITTED BIT(11) > > Please do not duplicate the definitions in drivers/cxl/cxl.h? > > If you move this file to drivers/vfio/ then also move the definitions to > include/cxl/. okay; I will fix both these duplicates as suggested. > > > +struct cxl_passthrough { > > This structure should be called cxl_vfio so that any grep for "vfio" in > drivers/cxl/ brings up this code. Otherwise "passthrough" is not a term > I would expect someone to understand is immediately vfio related. Even > drivers/vfio/ barely uses the term. okay, agreed. > > > + struct cxl_dev_state *cxlds; > > + > > + /* DVSEC body shadow. Byte-indexed by (off - > PCI_DVSEC_CXL_CAP). > > + * Allocated rounded up to a dword so dword reads at the tail > > + * never overrun. > > + */ > > + u8 *dvsec_shadow; > > + u16 dvsec_size; /* full DVSEC cap length, incl. > headers */ > > + bool dvsec_config_locked; > > + > > + /* HDM block shadow. Byte-indexed; size = hdm_reg_size. */ > > + u8 *hdm_shadow; > > + resource_size_t hdm_reg_size; > > + > > + /* CM cap-array snapshot. Dword-indexed by (off / 4) where off > > + * is the byte offset from CXL_CM_OFFSET. Read-only after > create. > > + */ > > + __le32 *cm_snapshot; > > + size_t cm_snapshot_dwords; > > + > > + /* Covers dvsec_shadow + dvsec_config_locked + hdm_shadow. > > + * cm_snapshot is immutable after create; no lock needed. Leaf- > > + * level: no entry point holding this mutex calls into cxl-bus > or > > + * vfio. > > + */ > > + struct mutex lock; > > +}; > > + > > +/* ------------------------------------------------------------------ > */ > > +/* Snapshot helpers > */ > > +/* ------------------------------------------------------------------ > > +*/ > > + > > +/* Read the DVSEC body bytes [PCI_DVSEC_CXL_CAP, dvsec_size) from PCI > > + * config space into the shadow. > > + * > > + * The body starts at PCI_DVSEC_CXL_CAP (0x0a), which is word-aligned > > +but > > + * NOT dword-aligned, and CXL r4.0 §8.1.3 places six 16-bit > > +descriptors > > + * (CAPABILITY through LOCK) at offsets 0x0a..0x14 before any 32-bit > > + * field. Strict-alignment PCIe host bridges (e.g. ARM64 ECAM) > > +reject > > + * misaligned dword config accesses with PCIBIOS_BAD_REGISTER_NUMBER; > > + * snapshot at the natural granularity of the body's 16-bit > > +descriptors > > + * (2-byte stride) so every offset in the range is naturally aligned. > > + */ > > +static int snapshot_dvsec_body(struct cxl_passthrough *p) { > > + struct pci_dev *pdev = to_pci_dev(p->cxlds->dev); > > + u16 dvsec = p->cxlds->cxl_dvsec; > > + u16 off; > > + u16 word; > > + int rc; > > + > > + for (off = PCI_DVSEC_CXL_CAP; off < p->dvsec_size; off += 2) { > > + rc = pci_read_config_word(pdev, dvsec + off, &word); > > + if (rc) > > + return -EIO; > > + put_unaligned_le16(word, p->dvsec_shadow + > > + (off - PCI_DVSEC_CXL_CAP)); > > + } > > + return 0; > > +} > > + > > +/* Read the CM cap-array prefix [CXL_CM_OFFSET, hdm_reg_offset) from > > + * MMIO into cm_snapshot, and the HDM block [hdm_reg_offset, > > + * hdm_reg_offset + hdm_reg_size) into hdm_shadow. > > + * > > + * @base is a short-lived kva for the component register block, > > + * established by the caller via ioremap() against cxlds- > >reg_map.resource. > > + * cxl_setup_regs() drops its own ioremap (clears reg_map.base) after > > +the > > + * cap-array probe completes, so this function cannot rely on > > + * cxlds->reg_map.base being valid; the caller passes a fresh mapping > > + * here and releases it once snapshot data has been copied into the > > + * in-memory shadows. > > This commentary is point in time research about how the CXL core > behaves. I do not see a reason for devm_cxl_vfio_create() to document > the semantics of cxl_setup_regs(). > > If you want to add documentation that explains how cxl_setup_regs() > works that belongs on cxl_setup_regs() directly. > > Lastly cxl_setup_regs() is a convenience function for the cxl_pci driver > that wants to parse the component register block into per capability > sparse mappings. The only functionality this implementation wants is the > base resource address, but that might change with the next comment. > > > + */ > > +static void snapshot_cm_and_hdm(struct cxl_passthrough *p, > > + void __iomem *base, > > + resource_size_t hdm_off) { > > + size_t i; > > + > > + for (i = 0; i < p->cm_snapshot_dwords; i++) > > + p->cm_snapshot[i] = cpu_to_le32(readl(base + > CXL_CM_OFFSET + > > + i * 4)); > > Unfortunately there are some registers that are 64-bit only registers. > > The spec says: "A 64-bit register shall be accessed as an 8-byte > quantity. Partial reads are not permitted." > > That means if a device has a link capability structure, a CHMU, or a > CPMU then this implementation will violate that requirement. Maybe the > implemetnation indeed needs to limit the component register blocks that > are supported. okay, agreed. For v4 I'll constrain the supported set: at acquire time, walk the CM cap-array and reject (-EOPNOTSUPP) any block that contains a capability requiring 64-bit access (link cap / CHMU / CPMU), which is consistent with the already-narrow single-decoder, no-interleave, fw-precommitted scope. Adding true 8-byte access to both the emu table and the COMP_REGS region is possible later but I'd rather not widen scope in this series. > > > + > > + for (i = 0; i < p->hdm_reg_size / 4; i++) > > + put_unaligned_le32(readl(base + hdm_off + i * 4), > > + p->hdm_shadow + i * 4); } > > + > > +/* ------------------------------------------------------------------ > */ > > +/* devres > */ > > +/* ------------------------------------------------------------------ > > +*/ > > + > > +static void cxl_passthrough_release(struct device *dev, void *res) { > > + struct cxl_passthrough *p = *(struct cxl_passthrough **)res; > > + > > + kfree(p->dvsec_shadow); > > + kfree(p->hdm_shadow); > > + kfree(p->cm_snapshot); > > + mutex_destroy(&p->lock); > > + kfree(p); > > +} > > + > > +struct cxl_passthrough * > > +devm_cxl_passthrough_create(struct device *dev, struct cxl_dev_state > > +*cxlds) { > > + struct cxl_passthrough **dres; > > + struct cxl_passthrough *p; > > + struct pci_dev *pdev; > > + resource_size_t hdm_off, hdm_size; > > + size_t dvsec_shadow_size; > > + u8 hdm_count; > > + u32 hdr; > > + int rc; > > + > > + /* > > + * cxl_setup_regs() releases its short-lived ioremap before > returning, > > + * so reg_map.base is NULL by the time we run. Validate the > persistent > > + * fields (resource address and size) instead; the local ioremap > > + * established further below covers the snapshot reads. > > + */ > > This is the cxl_setup_regs() documentation again. > > > > + if (!dev || !cxlds || !cxlds->dev || !cxlds->cxl_dvsec || > > + !cxlds->reg_map.resource || !cxlds->reg_map.max_size) > > + return ERR_PTR(-EINVAL); > > + > > + pdev = to_pci_dev(cxlds->dev); > > + > > + rc = cxl_get_hdm_info(cxlds, &hdm_count, &hdm_off, &hdm_size); > > + if (rc) > > + return ERR_PTR(rc); > > + if (hdm_count != 1 || !hdm_size || hdm_off <= CXL_CM_OFFSET || > > + !IS_ALIGNED(hdm_size, 4)) > > + return ERR_PTR(-EOPNOTSUPP); > > + > > + p = kzalloc_obj(*p, GFP_KERNEL); > > + if (!p) > > + return ERR_PTR(-ENOMEM); > > + > > + mutex_init(&p->lock); > > + p->cxlds = cxlds; > > + p->hdm_reg_size = hdm_size; > > + > > + /* DVSEC body length from PCI ext-cap header. */ > > + rc = pci_read_config_dword(pdev, cxlds->cxl_dvsec + > PCI_DVSEC_HEADER1, > > + &hdr); > > + if (rc) { > > + rc = -EIO; > > + goto err; > > + } > > + p->dvsec_size = PCI_DVSEC_HEADER1_LEN(hdr); > > + if (p->dvsec_size < DVSEC_BODY_END) { > > + rc = -EINVAL; > > + goto err; > > + } > > + > > + dvsec_shadow_size = round_up(p->dvsec_size - PCI_DVSEC_CXL_CAP, > 4); > > + p->dvsec_shadow = kzalloc(dvsec_shadow_size, GFP_KERNEL); > > + if (!p->dvsec_shadow) { > > + rc = -ENOMEM; > > + goto err; > > + } > > + > > + p->cm_snapshot_dwords = (hdm_off - CXL_CM_OFFSET) / 4; > > + p->cm_snapshot = kcalloc(p->cm_snapshot_dwords, sizeof(__le32), > > + GFP_KERNEL); > > + if (!p->cm_snapshot) { > > + rc = -ENOMEM; > > + goto err; > > + } > > + > > + p->hdm_shadow = kzalloc(hdm_size, GFP_KERNEL); > > + if (!p->hdm_shadow) { > > + rc = -ENOMEM; > > + goto err; > > + } > > + > > + rc = snapshot_dvsec_body(p); > > + if (rc) > > + goto err; > > + > > + { > > + void __iomem *base; > > Just declare this at the top and skip the new scope, or just make this a > helper function. okay. I will change this. > > > + > > + /* > > + * Bind-time-only ioremap. cxl_setup_regs() has already > > + * released the cxl-core ioremap (see comment on the > entry > > + * gate). Take a fresh, short-lived mapping for the > > + * snapshot, then release it; all subsequent reads serve > > + * from the in-memory shadows. > > + */ > > More redundant explanation of the cxl_setup_pci_regs() semantics. > > > + base = ioremap(cxlds->reg_map.resource, > > + cxlds->reg_map.max_size); > > + if (!base) { > > + rc = -ENOMEM; > > + goto err; > > + } > > + snapshot_cm_and_hdm(p, base, hdm_off); > > + iounmap(base); > > + } > > + > > + dres = devres_alloc(cxl_passthrough_release, sizeof(*dres), > > + GFP_KERNEL); > > + if (!dres) { > > + rc = -ENOMEM; > > + goto err; > > + } > > + *dres = p; > > + devres_add(dev, dres); > > This is just: > > rc = devm_add_action_or_reset(dev, cxl_passthrough_release, p); if (rc) > return ERR_PTR(rc); > okay, I will update this. > > + return p; > > + > > +err: > > + kfree(p->dvsec_shadow); > > + kfree(p->cm_snapshot); > > + kfree(p->hdm_shadow); > > + mutex_destroy(&p->lock); > > + kfree(p); > > + return ERR_PTR(rc); > > +} > > +EXPORT_SYMBOL_NS_GPL(devm_cxl_passthrough_create, "CXL"); > > + > > +/* ------------------------------------------------------------------ > */ > > +/* DVSEC write semantics > */ > > +/* ------------------------------------------------------------------ > > +*/ > > + > > +static u16 dvsec_shadow_get_u16(struct cxl_passthrough *p, u16 off) { > > + return get_unaligned_le16(p->dvsec_shadow + (off - > > +PCI_DVSEC_CXL_CAP)); } > > + > > +static void dvsec_shadow_set_u16(struct cxl_passthrough *p, u16 off, > > +u16 val) { > > + put_unaligned_le16(val, p->dvsec_shadow + (off - > > +PCI_DVSEC_CXL_CAP)); } > > + > > +/* Apply a write to a single DVSEC field at @off, with the field's > > + * native width (2 for descriptors, 4 for RANGE entries). @width is > > + * the field's spec width; @new is the merged value to apply. Caller > > + * holds p->lock. > > + */ > > +static void dvsec_apply_write(struct cxl_passthrough *p, u16 off, > size_t width, > > + u32 new) > > +{ > > + u16 cur16; > > + > > + switch (off) { > > + case DVSEC_OFF_CAPABILITY: > > + /* HwInit — drop. */ > > + return; > > + case DVSEC_OFF_CONTROL: > > + case DVSEC_OFF_CONTROL2: > > + /* RWL — gated on CONFIG_LOCK. */ > > + if (p->dvsec_config_locked) > > + return; > > + dvsec_shadow_set_u16(p, off, (u16)new); > > + return; > > + case DVSEC_OFF_STATUS: > > + case DVSEC_OFF_STATUS2: > > + /* RW1C — clear bits where the guest wrote 1. */ > > + cur16 = dvsec_shadow_get_u16(p, off); > > + dvsec_shadow_set_u16(p, off, cur16 & ~(u16)new); > > + return; > > + case DVSEC_OFF_LOCK: > > + /* RWO — first 1-write latches CONFIG_LOCK; subsequent > > + * writes are ignored. > > + */ > > + cur16 = dvsec_shadow_get_u16(p, off); > > + if (cur16 & DVSEC_LOCK_CONFIG_LOCK) > > + return; > > + if (new & DVSEC_LOCK_CONFIG_LOCK) { > > + dvsec_shadow_set_u16(p, off, > > + cur16 | > DVSEC_LOCK_CONFIG_LOCK); > > + p->dvsec_config_locked = true; > > + } > > + return; > > + case DVSEC_OFF_RANGE1_SIZE_HI: > > + case DVSEC_OFF_RANGE1_SIZE_LO: > > + case DVSEC_OFF_RANGE1_BASE_HI: > > + case DVSEC_OFF_RANGE1_BASE_LO: > > + /* HwInit — drop. */ > > + return; > > + case DVSEC_OFF_RANGE2_SIZE_HI: > > + case DVSEC_OFF_RANGE2_SIZE_LO: > > + case DVSEC_OFF_RANGE2_BASE_HI: > > + case DVSEC_OFF_RANGE2_BASE_LO: > > + /* RsvdZ — drop. */ > > + return; > > + default: > > + /* Reserved offsets inside the modelled body: drop. */ > > + (void)width; > > + return; > > + } > > +} > > + > > +/* Map a byte offset @off inside the DVSEC body to the natural-width > > + * field that contains it: returns the field's base offset (16-bit > > + * aligned for descriptors, 32-bit aligned for RANGE entries) and > width. > > + * Returns false if @off lies outside any modelled field. > > + */ > > +static bool dvsec_field_at(u16 off, u16 *field_off, size_t *width) { > > + if (off >= DVSEC_OFF_CAPABILITY && off < > DVSEC_OFF_RANGE1_SIZE_HI) { > > + *field_off = ALIGN_DOWN(off, 2); > > + *width = 2; > > + return true; > > + } > > + if (off >= DVSEC_OFF_RANGE1_SIZE_HI && off < DVSEC_BODY_END) { > > + *field_off = ALIGN_DOWN(off, 4); > > + *width = 4; > > + return true; > > + } > > + return false; > > +} > > + > > +int cxl_passthrough_dvsec_rw(struct cxl_passthrough *p, u32 off, u32 > *val, > > + size_t sz, bool write) { > > + u8 *shadow; > > + u16 field_off; > > + size_t field_width; > > + u32 cur, merged; > > + u32 sub_shift; > > + u32 width_mask; > > + > > + if (!p || !val) > > + return -EINVAL; > > + if (sz != 1 && sz != 2 && sz != 4) > > + return -EINVAL; > > + if (off < PCI_DVSEC_CXL_CAP || off + sz > p->dvsec_size) > > + return -EINVAL; > > + > > + guard(mutex)(&p->lock); > > + > > + shadow = p->dvsec_shadow + (off - PCI_DVSEC_CXL_CAP); > > + > > + if (!write) { > > + switch (sz) { > > + case 1: > > + *val = *shadow; > > + break; > > + case 2: > > + *val = get_unaligned_le16(shadow); > > + break; > > + case 4: > > + *val = get_unaligned_le32(shadow); > > + break; > > + } > > + return 0; > > + } > > + > > + if (!dvsec_field_at(off, &field_off, &field_width)) > > + return 0; /* outside any modelled field: drop */ > > + > > + /* Read-modify-merge the field at its natural width. */ > > + if (field_width == 2) > > + cur = dvsec_shadow_get_u16(p, field_off); > > + else > > + cur = get_unaligned_le32(p->dvsec_shadow + > > + (field_off - > > + PCI_DVSEC_CXL_CAP)); > > + > > + width_mask = (sz == 4) ? 0xffffffff : (sz == 2 ? 0xffff : 0xff); > > + sub_shift = (off - field_off) * 8; > > + merged = cur & ~(width_mask << sub_shift); > > + merged |= (*val & width_mask) << sub_shift; > > + > > + dvsec_apply_write(p, field_off, field_width, merged); > > + return 0; > > +} > > +EXPORT_SYMBOL_NS_GPL(cxl_passthrough_dvsec_rw, "CXL"); > > Hmm, no other "CXL" namespace consumer would ever care about this > mechanism. The export goes away if this file moves drivers/vfio/. If > there is a good argument to stay in drivers/cxl/ then it should be > EXPORT_SYMBOL_FOR_MODULES. okay, I will update this part and move this file to drivers/vfio/ > > > + > > +/* ------------------------------------------------------------------ > */ > > +/* HDM write semantics > */ > > +/* ------------------------------------------------------------------ > > +*/ > > + > > +static u32 hdm_shadow_get(struct cxl_passthrough *p, u32 off) { > > + return get_unaligned_le32(p->hdm_shadow + off); } > > + > > +static void hdm_shadow_set(struct cxl_passthrough *p, u32 off, u32 > > +val) { > > + put_unaligned_le32(val, p->hdm_shadow + off); } > > + > > +/* Decoder index for a per-decoder register offset. */ static u32 > > +hdm_decoder_of(u32 off) { > > + return (off - HDM_DEC_BASE) / HDM_DEC_STRIDE; } > > + > > +static u32 hdm_decoder_field(u32 off) { > > + return (off - HDM_DEC_BASE) % HDM_DEC_STRIDE; } > > + > > +static void hdm_decoder_ctrl_write(struct cxl_passthrough *p, u32 > > +off, u32 val) { > > + u32 cur = hdm_shadow_get(p, off); > > + u32 next; > > + > > + /* Once COMMITTED, only the COMMIT toggle is honoured. > Releasing > > + * COMMIT clears COMMITTED and Lock-on-Commit per CXL r4.0 > > + * §8.2.4.20.5. > > + */ > > + if (cur & HDM_CTRL_COMMITTED) { > > + next = (cur & ~HDM_CTRL_COMMIT) | (val & > HDM_CTRL_COMMIT); > > + if (!(val & HDM_CTRL_COMMIT)) { > > + next &= ~HDM_CTRL_COMMITTED; > > + next &= ~HDM_CTRL_LOCK_ON_COMMIT; > > No, when the decoder is locked its commit bit becomes read-only. Only > reset can clear lock-on-commit. > okay, I will update this behaviour in v4: When LOCK_ON_COMMIT is set in the shadow: - Writes that attempt to clear COMMITTED are dropped. - COMMIT becomes read-only (writes to COMMIT=0 are dropped). - LOCK_ON_COMMIT clears only when the device reset path refreshes the shadow from hardware. > > > + } > > + hdm_shadow_set(p, off, next); > > + return; > > + } > > + > > + next = val & ~(HDM_CTRL_COMMITTED | HDM_CTRL_ERR_NOT_COMMITTED); > > + if (val & HDM_CTRL_COMMIT) > > + next |= HDM_CTRL_COMMITTED; > > + hdm_shadow_set(p, off, next); > > +} > > + > > +static void hdm_decoder_basesize_write(struct cxl_passthrough *p, u32 > off, > > + u32 val) { > > + u32 n = hdm_decoder_of(off); > > + u32 ctrl = hdm_shadow_get(p, HDM_DEC_OFF_CTRL(n)); > > + > > + /* RWL — BASE/SIZE locked when the decoder is committed or > > + * lock-on-commit has been latched. > > + */ > > + if (ctrl & (HDM_CTRL_COMMITTED | HDM_CTRL_LOCK_ON_COMMIT)) > > + return; > > + hdm_shadow_set(p, off, val); > > +} > > + > > +int cxl_passthrough_hdm_rw(struct cxl_passthrough *p, u32 off, u32 > *val, > > + bool write) > > +{ > > + u32 field; > > + > > + if (!p || !val) > > + return -EINVAL; > > + if (!IS_ALIGNED(off, 4) || off + 4 > p->hdm_reg_size) > > + return -EINVAL; > > + > > + guard(mutex)(&p->lock); > > + > > + if (!write) { > > + *val = hdm_shadow_get(p, off); > > + return 0; > > + } > > + > > + switch (off) { > > + case HDM_OFF_CAP_HEADER: > > + /* HwInit — drop. */ > > + return 0; > > + case HDM_OFF_GLOBAL_CTRL: > > + /* RW — shadow. */ > > + hdm_shadow_set(p, off, *val); > > + return 0; > > + } > > + > > + if (off < HDM_DEC_BASE) > > + return 0; /* gap before per-decoder regs: drop */ > > + > > + field = hdm_decoder_field(off); > > + switch (field) { > > + case 0x00: case 0x04: /* BASE_LO / BASE_HI */ > > + case 0x08: case 0x0c: /* SIZE_LO / SIZE_HI */ > > + hdm_decoder_basesize_write(p, off, *val); > > + return 0; > > + case 0x10: /* CTRL */ > > + hdm_decoder_ctrl_write(p, off, *val); > > + return 0; > > + default: > > + /* TARGET_LIST_{LO,HI} and other per-decoder bytes are > > + * accepted as plain RW shadow for the firmware- > committed > > + * scope; multi-decoder / interleave behaviour is > > + * out-of-scope. > > + */ > > + hdm_shadow_set(p, off, *val); > > + return 0; > > + } > > +} > > +EXPORT_SYMBOL_NS_GPL(cxl_passthrough_hdm_rw, "CXL"); > > > > + > > +/* ------------------------------------------------------------------ > */ > > +/* CM cap-array snapshot > */ > > +/* ------------------------------------------------------------------ > > +*/ > > + > > +int cxl_passthrough_cm_rw(struct cxl_passthrough *p, u32 off, u32 > *val, > > + bool write) > > +{ > > + if (!p || !val) > > + return -EINVAL; > > + if (!IS_ALIGNED(off, 4) || off / 4 >= p->cm_snapshot_dwords) > > + return -EINVAL; > > + > > + if (write) > > + return 0; /* cap-array headers are RO; drop. */ > > + > > + *val = le32_to_cpu(p->cm_snapshot[off / 4]); > > + return 0; > > +} > > +EXPORT_SYMBOL_NS_GPL(cxl_passthrough_cm_rw, "CXL"); > > diff --git a/include/cxl/passthrough.h b/include/cxl/passthrough.h new > > file mode 100644 index 000000000000..43214b0d34f6 > > --- /dev/null > > +++ b/include/cxl/passthrough.h > > This file becomes private to drivers/vfio if it moves. > > Otherwise it needs s/passthrough/vfio/ as mentioned earlier. Agreed, I will update this in v4. > > > @@ -0,0 +1,121 @@ > > +/* SPDX-License-Identifier: GPL-2.0-only */ > > +/* Copyright(c) 2026 NVIDIA Corporation. All rights reserved. > > + * > > + * CXL register virtualization helpers for vfio-pci Type-2 > passthrough. > > + * > > + * See Documentation/driver-api/vfio-pci-cxl.rst for the ownership > > + * contract. In short: cxl-core owns the per-device DVSEC body, HDM > > + * Decoder block, and CM cap-array shadows; vfio-pci is a transport > > + * that forwards guest reads and writes through the helpers below. > > + * > > + * The helpers are not a generic emulation framework. Each register > > + * is hand-coded against CXL r4.0 §8.1.3 and §8.2.4.20. Adding a new > > + * field is "add a case", not "add a mode". > > + */ > > +#ifndef __CXL_PASSTHROUGH_H__ > > +#define __CXL_PASSTHROUGH_H__ > > + > > +#include <linux/types.h> > > + > > +struct cxl_dev_state; > > +struct cxl_passthrough; > > +struct device; > > + > > +/** > > + * devm_cxl_passthrough_create - snapshot a Type-2 device's DVSEC + > HDM + > > + * CM cap-array shadows and return the opaque handle the rw helpers > > + * operate on. > > + * > > + * @dev: device whose devres lifetime bounds the returned handle. > > + * @cxlds: CXL device state with cxlds->cxl_dvsec populated and > > + * cxlds->reg_map.resource and cxlds->reg_map.max_size > describing > > + * the component register block. cxlds->reg_map.base is NOT > > + * required; cxl_pci_setup_regs() releases its short-lived > > + * ioremap before returning, so this helper takes a local > > + * bind-time ioremap against cxlds->reg_map.resource for the > > + * duration of the snapshot. > > More out of place cxl_pci_setup_regs() documentation... okay, I will resolve this part. > > > + * > > + * On success the returned handle is bound to @dev's devres so unwind > > + * happens automatically when @dev is unbound. The handle must not > be > > + * freed by the caller. > > + * > > + * Return: a valid &struct cxl_passthrough on success, ERR_PTR(- > errno) > > + * on failure. > > + */ > > +struct cxl_passthrough * > > +devm_cxl_passthrough_create(struct device *dev, struct cxl_dev_state > *cxlds); > > + > > +/** > > + * cxl_passthrough_dvsec_rw - read or write the CXL Device DVSEC body > shadow. > > + * > > + * @p: handle from devm_cxl_passthrough_create(). > > + * @off: byte offset from the start of the DVSEC capability. Must be > > + * >= PCI_DVSEC_CXL_CAP and (off + sz) must lie inside the DVSEC. > > + * Accesses to the PCI ext-cap header bytes (off < > PCI_DVSEC_CXL_CAP) > > + * are the caller's responsibility; they belong on the generic > > + * perm-bits path, not here. > > + * @val: pointer to a u32 holding the read result or the write value. > > + * The low @sz bytes of *val are the payload; upper bytes ignored > > + * for writes and zero for reads. > > + * @sz: 1, 2, or 4. Other values return -EINVAL. > > + * @write: false for read, true for write. > > + * > > + * Reads serve from the shadow. Writes update the shadow per the > spec > > + * attribute mode for the addressed field (LOCK is RWO, > CONTROL/CONTROL2 > > + * are RWL gated on CONFIG_LOCK, STATUS/STATUS2 are RW1C, RANGE1/2 > are > > + * HwInit, Reserved/RsvdZ silently consumed). > > + * > > + * Known limitation: a 4-byte write whose @off straddles a 16-bit > DVSEC > > + * field boundary (CONTROL/STATUS at 0x0c/0x0e, CONTROL2/STATUS2 at > > + * 0x10/0x12) applies only the field containing the first byte of the > > + * access; the adjacent 16-bit field is not updated by the same > write. > > + * Standard CXL register-access patterns issue separate 2-byte > accesses > > + * to CONTROL, STATUS, CONTROL2 and STATUS2, so this corner case is > > + * documented rather than handled. > > + * > > + * Return: 0 on success; -EINVAL on out-of-range or bad size. > > + */ > > +int cxl_passthrough_dvsec_rw(struct cxl_passthrough *p, u32 off, u32 > *val, > > + size_t sz, bool write); > > + > > +/** > > + * cxl_passthrough_hdm_rw - read or write the HDM Decoder block > shadow. > > + * > > + * @p: handle from devm_cxl_passthrough_create(). > > + * @off: byte offset from the HDM block base; must be 4-byte aligned > and > > + * (off + 4) <= hdm_reg_size. Sub-dword access is not supported > on > > + * HDM registers per CXL r4.0 §8.2.4. > > + * @val: pointer to a u32 holding the read result or the write value. > > + * @write: false for read, true for write. > > + * > > + * Reads serve from the shadow. Writes implement the per-decoder > > + * COMMIT/COMMITTED handshake (CTRL) and the RWL gating on BASE/SIZE > > + * imposed by COMMITTED|LOCK_ON_COMMIT. GLOBAL_CTRL is RW; the cap > > + * header is HwInit (writes dropped); other offsets in the per- > decoder > > + * stride are RW shadow. > > + * > > + * Return: 0 on success; -EINVAL on misalignment or out-of-range. > > + */ > > +int cxl_passthrough_hdm_rw(struct cxl_passthrough *p, u32 off, u32 > *val, > > + bool write); > > + > > +/** > > + * cxl_passthrough_cm_rw - read or write the CXL.cache/mem cap-array > snapshot. > > + * > > + * @p: handle from devm_cxl_passthrough_create(). > > + * @off: byte offset from CXL_CM_OFFSET (the start of the CM cap- > array > > + * header in the component register block); must be 4-byte aligned > > + * and (off + 4) <= cm_snapshot_size. > > + * @val: pointer to a u32 holding the read result; ignored on write. > > + * @write: false for read. Writes to the cap-array are silently > dropped > > + * (the array headers are RO per CXL r4.0 §8.2.4); the @write > > + * parameter is present only to keep the API symmetric with the > > + * other rw helpers and to make the drop policy explicit at the > > + * call site. > > + * > > + * Return: 0 on success; -EINVAL on misalignment or out-of-range. > > + */ > > +int cxl_passthrough_cm_rw(struct cxl_passthrough *p, u32 off, u32 > *val, > > + bool write); > > + > > +#endif /* __CXL_PASSTHROUGH_H__ */ > > -- > > 2.25.1 >