All of lore.kernel.org
 help / color / mirror / Atom feed
* [PATCH v2 00/39] [RISC-V] virtual interrupt controller (vAPLIC/vIMSIC) support
@ 2026-08-27 15:20 Oleksii Kurochko
  2026-08-27 15:20 ` [PATCH v2 01/39] xen/riscv: drop pregs from struct cpu_user_regs Oleksii Kurochko
                   ` (38 more replies)
  0 siblings, 39 replies; 244+ messages in thread
From: Oleksii Kurochko @ 2026-08-27 15:20 UTC (permalink / raw)
  To: xen-devel
  Cc: Romain Caritey, Baptiste Le Duc, Zheng Zhang, Oleksii Kurochko,
	Alistair Francis, Connor Davis, Andrew Cooper, Anthony PERARD,
	Michal Orzel, Jan Beulich, Julien Grall, Roger Pau Monné,
	Stefano Stabellini

Hi all,

This series adds the initial virtual interrupt controller (vINTC) support
for RISC-V guests in Xen, based on the Advanced Interrupt Architecture
(AIA): a virtual APLIC (vAPLIC) in MSI mode backed by a virtual IMSIC
(vIMSIC) using hardware guest interrupt files.

Rather than emulating APLIC in direct-delivery mode (which requires
trap-and-emulate for every interrupt and is costly), the series targets
IMSIC from the start. AIA lets a hart implement several "guest interrupt
files" (up to GEILEN), so external interrupts can be delivered to a vCPU
directly by hardware via the VGEIN field of hstatus, without a hypervisor
round-trip. Xen only has to emulate the APLIC MMIO programming interface
and route the guest's intent onto the physical MSI topology; interrupt
delivery itself stays in hardware.

The work breaks down into a few logical blocks:

Preparatory fixes and cleanups (patches 1-6)
  - Drop the unused pregs field of struct cpu_user_regs and bug.h's
    duplicate instruction length helpers.
  - Program hstatus.VSXL explicitly, as decoding a trapped instruction
    depends on the effective XLEN of the guest.
  - csr_read64() as the counterpart of csr_write64(), used for CSR_TIME so
    that get_cycles() no longer truncates the time counter on RV32, and
    UINT64_MAX rather than ULONG_MAX to disable the VS-timer.
  - Request a G-stage flush on vmenter where VMIDs are unavailable, so a
    domain cannot run on the translations left behind by the one which ran
    on that hart before it.

APLIC groundwork and vAPLIC MMIO emulation (patches 7-11)
  - Add the missing APLIC register offsets/masks needed by both the
    physical and virtual APLIC code, rearranging asm/aplic.h in the style
    of x86's asm/msr-index.h (no functional change).
  - A per-domain MMIO handler table modelled on Arm's framework, so
    emulated devices self-register their GPA ranges and the fault path
    stays agnostic via a single try_handle_mmio() entry point.
  - vAPLIC MMIO read/write emulation. Writes are gated by the domain's
    authorised-IRQ bitmap so a guest cannot touch interrupts it does not
    own, and TARGET writes are translated from virtual to physical
    hart/guest-file indices. Delegation (SOURCECFG.D) is not yet
    supported.
  - Build the physical APLIC's target hart index with aplic_hart_field()
    as well, dropping the last in-tree duplicate of the AIA hart index
    formula, and add a helper to test for APLIC MSI mode.

vCPU context switching (patches 12-15)
  - context_switch() and the helpers it needs: save/restore of H/VS CSRs,
    the virtual timer and P2M context, and __context_switch() in assembly.
    The VMID is claimed in p2m_ctxt_switch_to() rather than at the next
    guest entry, which leaves p2m_handle_vmenter() with nothing to do.
  - Save and restore the AIA CSRs a guest can change (vsiselect and
    hviprio{1,2}), gated by hstateen0 where Smstateen is implemented.
  - vintc_ctxt_switch_{from,to}() wrappers over new ctxt_switch_{from,to}
    hooks in struct vintc_ops, called from the context switch path, plus
    the IMSIC implementation of those hooks: it records which pCPU owns a
    vCPU's guest interrupt file, as the pCPU id is part of the MSI
    address.

Trap and instruction emulation infrastructure (patches 16-26)
  - Extend the exception-table format with type/data fields and add
    EX_TYPE_TRAP_INFO so fixups can capture sepc/scause/stval, and look
    the table up for any trap taken in Xen context rather than for illegal
    instructions only, so that the hlv/hlvx sequences reach their fixup.
  - A guest page-fault handler, and trap_redirect() to forward a
    synchronous trap back into the guest's VS-mode handler for the faults
    which can never become an emulated access.
  - Resolve the faulting guest physical address from htval and stval,
    which first needs Shtvala to be detected, and define all four
    INSN_PSEUDO_VS_* values independently of the hypervisor's XLEN, so
    that a fault taken on an implicit VS-stage access is recognized as one
    rather than mistaken for an MMIO trap.
  - riscv_read_guest() (HLV/HLVX) to read guest memory and instructions
    safely, the decoding helpers shared by both access types, and the load
    and store emulation which dispatches the access through
    try_handle_mmio().

vCPU migration between pCPUs (patches 27-34) (introduced here for better context
of VGEIN fumctions usage)
  - arch_move_irqs(), dispatching through a new move_irqs hook in
    struct vintc_ops down to imsic_migrate_vcpu(), and the case where a
    vCPU has no guest interrupt file to move yet.
  - The move of a vCPU's IMSIC guest interrupt file itself, following the
    sequence the AIA spec prescribes: quiesce and save eidelivery/
    eithreshold of the old file, zero the new one, G-stage remap it,
    retarget the domain's APLIC interrupts at it
    (aplic_reconfigure_target()) and fence off straggler MSIs with a
    genmsi barrier, dump the old file's eip/eie arrays to memory, then
    restore that state into the new file and update the vCPU's
    hstatus.VGEIN.

VGEIN allocation and vCPU bring-up (patches 35-39)
  - Per-pCPU VGEIN (guest interrupt file) allocator: a bitmap of the files
    a hart implements (up to GEILEN) with helpers to assign and release
    one, and an owners[] map so a file reported pending in HGEIP can be
    traced back to the vCPU it belongs to.
  - Watch a descheduled vCPU's guest interrupt file through HGEIE, so that
    a guest blocked on an external interrupt is woken up instead of
    waiting for an unrelated event to schedule it again.
  - Stage-2 map a vCPU's physical guest interrupt file to the fixed
    per-vCPU GPA page the guest expects at offset 0.
  - continue_new_vcpu(): switch to the idle vCPU's own stack for the idle
    vCPU, and enter the guest through the new return_to_new_vcpu() path in
    entry.S for a guest one.
  - imsic_vsfile_attach(), called once the pCPU a vCPU will run on is
    known: it assigns a VGEIN, maps the guest interrupt file and records
    the IMSIC state as a consistent unit.

CI tests: https://gitlab.com/xen-project/people/olkur/xen/-/pipelines/2796779115

The series depends on [1].

[1] https://lore.kernel.org/xen-devel/cover.1787836900.git.oleksii.kurochko@gmail.com/T/#t

---
Changes in v2:
 - The series has grown from 17 to 39 patches. vCPU context switching, vCPU
   migration between pCPUs and the vCPU bring-up path (continue_new_vcpu(),
   attaching an IMSIC h/w interrupt file) are now part of it to have better
   context of how things are using, together with the trap-side pieces the
   MMIO emulation depends on (faulting GPA resolution, Shtvala detection,
   instruction decoding).
 - vintc_state_{save,restore}() became vintc_ctxt_switch_{from,to}() and
   vcpu_aia_init() became imsic_vsfile_attach(); "xen/riscv: manage
   IRQ_DISABLED flag in APLIC irq enable/disable callbacks" is no longer part
   of this series. The remaining changes are described in the per-patch
   changelogs.
---

Oleksii Kurochko (39):
  xen/riscv: drop pregs from struct cpu_user_regs
  xen/riscv: drop bug.h's duplicate instruction length helpers
  xen/riscv: set the guest's XLEN explicitly in hstatus.VSXL
  xen/riscv: introduce csr_read64()
  xen/riscv: request a G-stage flush on vmenter when VMIDs are disabled
  xen/riscv: use UINT64_MAX to disable the VS-timer
  xen/riscv: add missing APLIC register offsets, masks to asm/aplic.h
  xen/riscv: introduce device-agnostic MMIO emulation dispatch
  xen/riscv: implement virtual APLIC MMIO emulation
  xen/riscv: build the target hart index via aplic_hart_field()
  xen/riscv: add helper to check APLIC MSI mode
  xen/riscv: implement vCPU context switching
  xen/riscv: save and restore AIA state on vCPU context switch
  xen/riscv: introduce vintc_ctxt_switch_{from,to}()
  xen/riscv: add IMSIC vCPU context switch handlers
  xen/riscv: extend exception tables with type and data fields
  xen/riscv: decouple INSN_PSEUDO_VS_* from the hypervisor's XLEN
  xen/riscv: add guest page fault handling stub
  xen/riscv: implement trap redirection to a guest
  xen/riscv: detect Shtvala
  xen/riscv: resolve the faulting guest physical address
  xen/riscv: add guest memory read helper
  xen/riscv: look up the exception table for any trap taken in Xen
    context
  xen/riscv: add helpers for decoding a trapped load or store
  xen/riscv: add guest load emulation for trapped MMIO accesses
  xen/riscv: add guest store emulation for trapped MMIO accesses
  xen/riscv: introduce arch_move_irqs()
  xen/riscv: handle the case when no vCPU migration is needed
  xen/riscv: introduce aplic_reconfigure_target()
  xen/riscv: prepare new IMSIC VS-file
  xen/riscv: implement APLIC-hart sync barrier for vCPU migration
  xen/riscv: remap interrupts to new IMSIC VS-file
  xen/riscv: dump old interrupt file to memory
  xen/riscv: restore register state in the new IMSIC VS-file
  xen/riscv: add basic VGEIN management for AIA guests
  xen/riscv: wake up a descheduled vCPU on a guest external interrupt
  xen/riscv: map IMSIC interrupt file for vCPUs
  xen/riscv: implement continue_new_vcpu()
  xen/riscv: introduce IMSIC h/w interrupt file attaching to vcpu

 xen/arch/riscv/Makefile                     |   2 +
 xen/arch/riscv/aia.c                        | 193 ++++++
 xen/arch/riscv/aplic-priv.h                 |   3 +
 xen/arch/riscv/aplic.c                      | 235 ++++++-
 xen/arch/riscv/cpufeature.c                 |   1 +
 xen/arch/riscv/domain.c                     | 271 +++++++-
 xen/arch/riscv/emulate.c                    | 578 ++++++++++++++++
 xen/arch/riscv/entry.S                      |  67 ++
 xen/arch/riscv/extable.c                    |  70 +-
 xen/arch/riscv/guestcopy.c                  |  87 +++
 xen/arch/riscv/imsic.c                      | 691 ++++++++++++++++++++
 xen/arch/riscv/include/asm/aia.h            |   7 +
 xen/arch/riscv/include/asm/aplic.h          | 130 +++-
 xen/arch/riscv/include/asm/bug.h            |  19 -
 xen/arch/riscv/include/asm/cpufeature.h     |   1 +
 xen/arch/riscv/include/asm/csr.h            |  24 +
 xen/arch/riscv/include/asm/current.h        |   4 +
 xen/arch/riscv/include/asm/domain.h         |  23 +-
 xen/arch/riscv/include/asm/emulate.h        |  10 +
 xen/arch/riscv/include/asm/extable.h        |  64 +-
 xen/arch/riscv/include/asm/gpr-num.h        |  37 ++
 xen/arch/riscv/include/asm/guest_access.h   |   4 +
 xen/arch/riscv/include/asm/imsic.h          |  31 +
 xen/arch/riscv/include/asm/intc.h           |  12 +
 xen/arch/riscv/include/asm/irq.h            |   5 +-
 xen/arch/riscv/include/asm/mmio.h           |  63 ++
 xen/arch/riscv/include/asm/p2m.h            |   1 -
 xen/arch/riscv/include/asm/processor.h      |  16 +-
 xen/arch/riscv/include/asm/riscv_encoding.h |  34 +-
 xen/arch/riscv/include/asm/system.h         |   4 +
 xen/arch/riscv/include/asm/time.h           |   4 +-
 xen/arch/riscv/include/asm/traps.h          |   9 +
 xen/arch/riscv/include/asm/vaplic.h         |   5 +
 xen/arch/riscv/intc.c                       |  22 +
 xen/arch/riscv/mmio.c                       | 176 +++++
 xen/arch/riscv/p2m.c                        |  55 +-
 xen/arch/riscv/riscv64/asm-offsets.c        |  20 +-
 xen/arch/riscv/stubs.c                      |   5 -
 xen/arch/riscv/time.c                       |   4 +-
 xen/arch/riscv/traps.c                      | 110 +++-
 xen/arch/riscv/vaplic.c                     | 358 +++++++++-
 xen/arch/riscv/vmid.c                       |   4 +-
 xen/include/xen/config.h                    |   1 +
 43 files changed, 3281 insertions(+), 179 deletions(-)
 create mode 100644 xen/arch/riscv/emulate.c
 create mode 100644 xen/arch/riscv/include/asm/emulate.h
 create mode 100644 xen/arch/riscv/include/asm/gpr-num.h
 create mode 100644 xen/arch/riscv/include/asm/mmio.h
 create mode 100644 xen/arch/riscv/mmio.c

-- 
2.55.0



^ permalink raw reply	[flat|nested] 244+ messages in thread

* [PATCH v2 01/39] xen/riscv: drop pregs from struct cpu_user_regs
  2026-08-27 15:20 [PATCH v2 00/39] [RISC-V] virtual interrupt controller (vAPLIC/vIMSIC) support Oleksii Kurochko
@ 2026-08-27 15:20 ` Oleksii Kurochko
  2026-08-31 12:48   ` Baptiste Le Duc
  2026-08-27 15:20 ` [PATCH v2 02/39] xen/riscv: drop bug.h's duplicate instruction length helpers Oleksii Kurochko
                   ` (37 subsequent siblings)
  38 siblings, 1 reply; 244+ messages in thread
From: Oleksii Kurochko @ 2026-08-27 15:20 UTC (permalink / raw)
  To: xen-devel
  Cc: Romain Caritey, Baptiste Le Duc, Zheng Zhang, Oleksii Kurochko,
	Alistair Francis, Connor Davis, Andrew Cooper, Anthony PERARD,
	Michal Orzel, Jan Beulich, Julien Grall, Roger Pau Monné,
	Stefano Stabellini

As nothing is using pregs in upstream or downstream ports of RISC-V drop
it from struct cpu_user_regs. Once it will really be needed re-introduce
it.

Signed-off-by: Oleksii Kurochko <oleksii.kurochko@gmail.com>
---
Changes in v2:
 - New patch
---
---
 xen/arch/riscv/include/asm/processor.h | 2 --
 xen/arch/riscv/riscv64/asm-offsets.c   | 1 -
 2 files changed, 3 deletions(-)

diff --git a/xen/arch/riscv/include/asm/processor.h b/xen/arch/riscv/include/asm/processor.h
index 6b89df4a2d4f..b1745c107100 100644
--- a/xen/arch/riscv/include/asm/processor.h
+++ b/xen/arch/riscv/include/asm/processor.h
@@ -50,8 +50,6 @@ struct cpu_user_regs
     unsigned long sepc;
     unsigned long sstatus;
     unsigned long hstatus;
-    /* pointer to previous stack_cpu_regs */
-    unsigned long pregs;
 };
 
 /* TODO: need to implement */
diff --git a/xen/arch/riscv/riscv64/asm-offsets.c b/xen/arch/riscv/riscv64/asm-offsets.c
index 472cced4f8af..1290b9dbbe82 100644
--- a/xen/arch/riscv/riscv64/asm-offsets.c
+++ b/xen/arch/riscv/riscv64/asm-offsets.c
@@ -50,7 +50,6 @@ void asm_offsets(void)
     OFFSET(CPU_USER_REGS_SEPC, struct cpu_user_regs, sepc);
     OFFSET(CPU_USER_REGS_SSTATUS, struct cpu_user_regs, sstatus);
     OFFSET(CPU_USER_REGS_HSTATUS, struct cpu_user_regs, hstatus);
-    OFFSET(CPU_USER_REGS_PREGS, struct cpu_user_regs, pregs);
     BLANK();
     DEFINE(PCPU_INFO_SIZE, sizeof(struct pcpu_info));
     BLANK();
-- 
2.55.0



^ permalink raw reply related	[flat|nested] 244+ messages in thread

* [PATCH v2 02/39] xen/riscv: drop bug.h's duplicate instruction length helpers
  2026-08-27 15:20 [PATCH v2 00/39] [RISC-V] virtual interrupt controller (vAPLIC/vIMSIC) support Oleksii Kurochko
  2026-08-27 15:20 ` [PATCH v2 01/39] xen/riscv: drop pregs from struct cpu_user_regs Oleksii Kurochko
@ 2026-08-27 15:20 ` Oleksii Kurochko
  2026-08-31 12:48   ` Baptiste Le Duc
  2026-09-01  7:01   ` Jan Beulich
  2026-08-27 15:20 ` [PATCH v2 03/39] xen/riscv: set the guest's XLEN explicitly in hstatus.VSXL Oleksii Kurochko
                   ` (36 subsequent siblings)
  38 siblings, 2 replies; 244+ messages in thread
From: Oleksii Kurochko @ 2026-08-27 15:20 UTC (permalink / raw)
  To: xen-devel
  Cc: Romain Caritey, Baptiste Le Duc, Zheng Zhang, Oleksii Kurochko,
	Alistair Francis, Connor Davis, Andrew Cooper, Anthony PERARD,
	Michal Orzel, Jan Beulich, Julien Grall, Roger Pau Monné,
	Stefano Stabellini

asm/riscv_encoding.h already provides INSN_16BIT_MASK and INSN_LEN(), and
emulate.c uses them, so the tree carried two spellings of the same thing
which could drift apart. COMPRESSED_INSN_MASK never had a user.

No functional change.

Signed-off-by: Oleksii Kurochko <oleksii.kurochko@gmail.com>
---
Changes in v2:
 - new patch.
---
---
 xen/arch/riscv/include/asm/bug.h | 19 -------------------
 xen/arch/riscv/traps.c           |  2 +-
 2 files changed, 1 insertion(+), 20 deletions(-)

diff --git a/xen/arch/riscv/include/asm/bug.h b/xen/arch/riscv/include/asm/bug.h
index e6f286881662..c2cdc2dc2a46 100644
--- a/xen/arch/riscv/include/asm/bug.h
+++ b/xen/arch/riscv/include/asm/bug.h
@@ -13,25 +13,6 @@
 
 #define BUG_INSTR "unimp"
 
-/*
- * The base instruction set has a fixed length of 32-bit naturally aligned
- * instructions.
- *
- * There are extensions of variable length ( where each instruction can be
- * any number of 16-bit parcels in length ).
- *
- * Compressed ISA is used now where the instruction length is 16 bit and
- * 'unimp' instruction, in this case, can be either 16 or 32 bit (
- * depending on if compressed ISA is used or not )
- */
-#define INSN_LENGTH_MASK        _UL(0x3)
-#define INSN_LENGTH_32          _UL(0x3)
-
-#define COMPRESSED_INSN_MASK    _UL(0xffff)
-
-#define GET_INSN_LENGTH(insn)                               \
-    (((insn) & INSN_LENGTH_MASK) == INSN_LENGTH_32 ? 4 : 2) \
-
 #endif /* !__ASSEMBLER__ */
 
 #endif /* ASM__RISCV__BUG_H */
diff --git a/xen/arch/riscv/traps.c b/xen/arch/riscv/traps.c
index d35c013e1399..8530e6fbda0a 100644
--- a/xen/arch/riscv/traps.c
+++ b/xen/arch/riscv/traps.c
@@ -214,7 +214,7 @@ void do_trap(struct cpu_user_regs *cpu_regs)
                 die();
             }
 
-            cpu_regs->sepc += GET_INSN_LENGTH(*(uint16_t *)pc);
+            cpu_regs->sepc += INSN_LEN(*(uint16_t *)pc);
 
             break;
         }
-- 
2.55.0



^ permalink raw reply related	[flat|nested] 244+ messages in thread

* [PATCH v2 03/39] xen/riscv: set the guest's XLEN explicitly in hstatus.VSXL
  2026-08-27 15:20 [PATCH v2 00/39] [RISC-V] virtual interrupt controller (vAPLIC/vIMSIC) support Oleksii Kurochko
  2026-08-27 15:20 ` [PATCH v2 01/39] xen/riscv: drop pregs from struct cpu_user_regs Oleksii Kurochko
  2026-08-27 15:20 ` [PATCH v2 02/39] xen/riscv: drop bug.h's duplicate instruction length helpers Oleksii Kurochko
@ 2026-08-27 15:20 ` Oleksii Kurochko
  2026-08-31 12:48   ` Baptiste Le Duc
  2026-09-01 15:20   ` Jan Beulich
  2026-08-27 15:20 ` [PATCH v2 04/39] xen/riscv: introduce csr_read64() Oleksii Kurochko
                   ` (35 subsequent siblings)
  38 siblings, 2 replies; 244+ messages in thread
From: Oleksii Kurochko @ 2026-08-27 15:20 UTC (permalink / raw)
  To: xen-devel
  Cc: Romain Caritey, Baptiste Le Duc, Zheng Zhang, Oleksii Kurochko,
	Alistair Francis, Connor Davis, Andrew Cooper, Anthony PERARD,
	Michal Orzel, Jan Beulich, Julien Grall, Roger Pau Monné,
	Stefano Stabellini

hstatus.VSXL is WARL, so its reset value is implementation-defined. Xen
supports 64-bit guests only, so program it explicitly instead of relying
on whatever the hardware happens to leave there.

This matters beyond the guest's own view of itself: decoding a trapped
instruction depends on the effective XLEN of the guest, as the encodings
which exist for XLEN=64 only must not be recognized for a 32-bit one.

Signed-off-by: Oleksii Kurochko <oleksii.kurochko@gmail.com>
---
Changes in v2:
 - new patch
---
---
 xen/arch/riscv/domain.c                     | 8 +++++++-
 xen/arch/riscv/include/asm/riscv_encoding.h | 2 ++
 2 files changed, 9 insertions(+), 1 deletion(-)

diff --git a/xen/arch/riscv/domain.c b/xen/arch/riscv/domain.c
index d94652809e36..57c37cb2dfc2 100644
--- a/xen/arch/riscv/domain.c
+++ b/xen/arch/riscv/domain.c
@@ -88,7 +88,13 @@ static void vcpu_csr_init(struct vcpu *v)
 {
     v->arch.hedeleg = HEDELEG_DEFAULT & csr_masks.hedeleg;
 
-    vcpu_guest_cpu_user_regs(v)->hstatus = HSTATUS_SPV | HSTATUS_SPVP;
+    /*
+     * Xen supports 64-bit guests only, so set the guest's XLEN explicitly
+     * rather than leaving it to the WARL behaviour of hstatus.VSXL, which the
+     * decoding of a trapped instruction depends on.
+     */
+    vcpu_guest_cpu_user_regs(v)->hstatus =
+        HSTATUS_SPV | HSTATUS_SPVP | MASK_INSR(HSTATUS_VSXL_64, HSTATUS_VSXL);
 
     v->arch.hideleg = HIDELEG_DEFAULT & csr_masks.hideleg;
 
diff --git a/xen/arch/riscv/include/asm/riscv_encoding.h b/xen/arch/riscv/include/asm/riscv_encoding.h
index 03e186bcdb8c..c63e5e304691 100644
--- a/xen/arch/riscv/include/asm/riscv_encoding.h
+++ b/xen/arch/riscv/include/asm/riscv_encoding.h
@@ -68,6 +68,8 @@
 #if __riscv_xlen == 64
 #define HSTATUS_VSXL			_UL(0x300000000)
 #define HSTATUS_VSXL_SHIFT		32
+#define HSTATUS_VSXL_64			_UL(2)
+#define HSTATUS_VSXL_32			_UL(1)
 #endif
 #define HSTATUS_VTSR			_UL(0x00400000)
 #define HSTATUS_VTW			_UL(0x00200000)
-- 
2.55.0



^ permalink raw reply related	[flat|nested] 244+ messages in thread

* [PATCH v2 04/39] xen/riscv: introduce csr_read64()
  2026-08-27 15:20 [PATCH v2 00/39] [RISC-V] virtual interrupt controller (vAPLIC/vIMSIC) support Oleksii Kurochko
                   ` (2 preceding siblings ...)
  2026-08-27 15:20 ` [PATCH v2 03/39] xen/riscv: set the guest's XLEN explicitly in hstatus.VSXL Oleksii Kurochko
@ 2026-08-27 15:20 ` Oleksii Kurochko
  2026-08-27 15:36   ` Andrew Cooper
  2026-08-27 15:20 ` [PATCH v2 05/39] xen/riscv: request a G-stage flush on vmenter when VMIDs are disabled Oleksii Kurochko
                   ` (34 subsequent siblings)
  38 siblings, 1 reply; 244+ messages in thread
From: Oleksii Kurochko @ 2026-08-27 15:20 UTC (permalink / raw)
  To: xen-devel
  Cc: Romain Caritey, Baptiste Le Duc, Zheng Zhang, Oleksii Kurochko,
	Alistair Francis, Connor Davis, Andrew Cooper, Anthony PERARD,
	Michal Orzel, Jan Beulich, Julien Grall, Roger Pau Monné,
	Stefano Stabellini

csr_write64() already hides the RV32 split of a 64-bit CSR into a low and
a high half; add the read counterpart.

Reading the two halves isn't simply the mirror of writing them. A CSR which
hardware increments can carry from the low half into the high one between
the two reads, so a plain pair of reads can produce a value the CSR never
held. Therefore the high half is re-read and the sequence retried if it
changed in the meantime.

Use it for CSR_TIME, which is exactly such a counter, and widen cycles_t to
uint64_t. Otherwise get_cycles() would still truncate the time counter to
32 bits on RV32.

Fixes: a541ddadec0a ("xen/riscv: introduce time.h")
Signed-off-by: Oleksii Kurochko <oleksii.kurochko@gmail.com>
---
Changes in v2:
 - New patch.
---
---
 xen/arch/riscv/include/asm/csr.h  | 24 ++++++++++++++++++++++++
 xen/arch/riscv/include/asm/time.h |  4 ++--
 2 files changed, 26 insertions(+), 2 deletions(-)

diff --git a/xen/arch/riscv/include/asm/csr.h b/xen/arch/riscv/include/asm/csr.h
index 888d6a2a86d6..a5cdd6f99c8e 100644
--- a/xen/arch/riscv/include/asm/csr.h
+++ b/xen/arch/riscv/include/asm/csr.h
@@ -39,12 +39,36 @@
     csr_write(csr, v_);             \
     csr_write(csr ## H, v_ >> 32);  \
 })
+
+/*
+ * The two halves are read by separate instructions, so a CSR which hardware
+ * increments can carry from the low half into the high one in between,
+ * yielding a value the CSR never held. Re-read the high half and retry the
+ * sequence if it changed.
+ */
+#define csr_read64(csr)                         \
+({                                              \
+    uint32_t hi_, lo_;                          \
+                                                \
+    do {                                        \
+        hi_ = csr_read(csr ## H);               \
+        lo_ = csr_read(csr);                    \
+    } while ( hi_ != csr_read(csr ## H) );      \
+                                                \
+    ((uint64_t)hi_ << 32) | lo_;                \
+})
 #else
 #define csr_write64(csr, val)       \
 ({                                  \
     csr_write(csr, val);            \
     (void)csr ## H;                 \
 })
+
+#define csr_read64(csr)             \
+({                                  \
+    (void)csr ## H;                 \
+    csr_read(csr);                  \
+})
 #endif
 
 #define csr_swap(csr, val)                                      \
diff --git a/xen/arch/riscv/include/asm/time.h b/xen/arch/riscv/include/asm/time.h
index 4d68900151a7..ec771c3fe80f 100644
--- a/xen/arch/riscv/include/asm/time.h
+++ b/xen/arch/riscv/include/asm/time.h
@@ -18,11 +18,11 @@ static inline void force_update_vcpu_system_time(struct vcpu *v)
     BUG_ON("unimplemented");
 }
 
-typedef unsigned long cycles_t;
+typedef uint64_t cycles_t;
 
 static inline cycles_t get_cycles(void)
 {
-    return csr_read(CSR_TIME);
+    return csr_read64(CSR_TIME);
 }
 
 void preinit_xen_time(void);
-- 
2.55.0



^ permalink raw reply related	[flat|nested] 244+ messages in thread

* [PATCH v2 05/39] xen/riscv: request a G-stage flush on vmenter when VMIDs are disabled
  2026-08-27 15:20 [PATCH v2 00/39] [RISC-V] virtual interrupt controller (vAPLIC/vIMSIC) support Oleksii Kurochko
                   ` (3 preceding siblings ...)
  2026-08-27 15:20 ` [PATCH v2 04/39] xen/riscv: introduce csr_read64() Oleksii Kurochko
@ 2026-08-27 15:20 ` Oleksii Kurochko
  2026-08-31 12:48   ` Baptiste Le Duc
  2026-08-27 15:20 ` [PATCH v2 06/39] xen/riscv: use UINT64_MAX to disable the VS-timer Oleksii Kurochko
                   ` (33 subsequent siblings)
  38 siblings, 1 reply; 244+ messages in thread
From: Oleksii Kurochko @ 2026-08-27 15:20 UTC (permalink / raw)
  To: xen-devel
  Cc: Romain Caritey, Baptiste Le Duc, Zheng Zhang, Oleksii Kurochko,
	Alistair Francis, Connor Davis, Andrew Cooper, Anthony PERARD,
	Michal Orzel, Jan Beulich, Julien Grall, Roger Pau Monné,
	Stefano Stabellini

vmid_handle_vmenter() reports that no flush is needed when VMIDs are
unavailable (vmid=off, or hardware with no more than one VMID bit). Every
domain then runs under VMID 0 with nothing flushed in between, so as soon
as a hart runs more than one domain, a domain entered there can use the
G-stage translations left behind by the domain which ran before it.

Adjust the comment in p2m_handle_vmenter() accordingly: skipping the
VS-stage flush no longer relies on an old VMID not being reused, which
doesn't hold when there are no VMIDs to begin with.

While at it, spell the other early return as a bool literal.

Fixes: bff3b9ea4696 ("xen/riscv: introduce VMID allocation and manegement")
Signed-off-by: Oleksii Kurochko <oleksii.kurochko@gmail.com>
---
Changes in v2:
 - New patch.
---
---
 xen/arch/riscv/p2m.c  | 4 +++-
 xen/arch/riscv/vmid.c | 4 ++--
 2 files changed, 5 insertions(+), 3 deletions(-)

diff --git a/xen/arch/riscv/p2m.c b/xen/arch/riscv/p2m.c
index 1cea86512c8c..de25607247a6 100644
--- a/xen/arch/riscv/p2m.c
+++ b/xen/arch/riscv/p2m.c
@@ -1584,7 +1584,9 @@ void p2m_handle_vmenter(void)
     /*
      * There is also no need to flush the VS-stage TLB: even if speculation
      * occurs (VSATP + old HGATP were used), it will use the old VMID, which
-     * won't be reused until need_flush is set to true.
+     * won't be reused until need_flush is set to true. When VMIDs aren't
+     * available there is no old VMID to rely on, but then need_flush is set
+     * on every entry, so the flush above covers that case.
      */
 }
 
diff --git a/xen/arch/riscv/vmid.c b/xen/arch/riscv/vmid.c
index 11c7e9d6d6c8..93714b359534 100644
--- a/xen/arch/riscv/vmid.c
+++ b/xen/arch/riscv/vmid.c
@@ -141,7 +141,7 @@ bool vmid_handle_vmenter(struct vcpu_vmid *vmid)
 
     /* Test if VCPU has valid VMID. */
     if ( read_atomic(&vmid->generation) == data->generation )
-        return 0;
+        return false;
 
     /* If there are no free VMIDs, need to go to a new generation. */
     if ( unlikely(data->next_vmid > data->max_vmid) )
@@ -164,7 +164,7 @@ bool vmid_handle_vmenter(struct vcpu_vmid *vmid)
 
  disabled:
     vmid->vmid = 0;
-    return 0;
+    return true;
 }
 
 /*
-- 
2.55.0



^ permalink raw reply related	[flat|nested] 244+ messages in thread

* [PATCH v2 06/39] xen/riscv: use UINT64_MAX to disable the VS-timer
  2026-08-27 15:20 [PATCH v2 00/39] [RISC-V] virtual interrupt controller (vAPLIC/vIMSIC) support Oleksii Kurochko
                   ` (4 preceding siblings ...)
  2026-08-27 15:20 ` [PATCH v2 05/39] xen/riscv: request a G-stage flush on vmenter when VMIDs are disabled Oleksii Kurochko
@ 2026-08-27 15:20 ` Oleksii Kurochko
  2026-08-31 12:48   ` Baptiste Le Duc
  2026-09-01  7:12   ` Jan Beulich
  2026-08-27 15:20 ` [PATCH v2 07/39] xen/riscv: add missing APLIC register offsets, masks to asm/aplic.h Oleksii Kurochko
                   ` (32 subsequent siblings)
  38 siblings, 2 replies; 244+ messages in thread
From: Oleksii Kurochko @ 2026-08-27 15:20 UTC (permalink / raw)
  To: xen-devel
  Cc: Romain Caritey, Baptiste Le Duc, Zheng Zhang, Oleksii Kurochko,
	Alistair Francis, Connor Davis, Andrew Cooper, Anthony PERARD,
	Michal Orzel, Jan Beulich, Julien Grall, Roger Pau Monné,
	Stefano Stabellini

vstimecmp is a 64-bit CSR independently of XLEN, which is why it is
written with csr_write64(). On RV32 that macro splits the value into
the vstimecmp/vstimecmph pair, so passing ULONG_MAX (0xffffffff there)
writes all ones to the low half and zero to the high half, leaving the
CSR at 0x00000000ffffffff rather than at its maximum. A VS-timer irq
would then become pending as soon as (time + htimedelta) reaches 2^32,
which is exactly what the code is trying to avoid.

Use UINT64_MAX, which matches the width of the CSR. On RV64 it is equal
to ULONG_MAX, so no functional change there.

Fixes: 25e032730690 ("xen/riscv: allow Xen to use SSTC while hiding it from guests")
Signed-off-by: Oleksii Kurochko <oleksii.kurochko@gmail.com>
---
Changes in v2:
 - New patch.
---
---
 xen/arch/riscv/time.c | 4 ++--
 1 file changed, 2 insertions(+), 2 deletions(-)

diff --git a/xen/arch/riscv/time.c b/xen/arch/riscv/time.c
index 602c029641b8..8c25198b4063 100644
--- a/xen/arch/riscv/time.c
+++ b/xen/arch/riscv/time.c
@@ -101,8 +101,8 @@ void __init preinit_xen_time(void)
          * A VS-timer interrupt becomes pending whenever the value of
          * (time + htimedelta) is greater than or equal to vstimecmp CSR.
          * Thereby to avoid spurious VS-timer irqs set vstimecmp CSR to
-         * ULONG_MAX.
+         * UINT64_MAX.
          */
-        csr_write64(CSR_VSTIMECMP, ULONG_MAX);
+        csr_write64(CSR_VSTIMECMP, UINT64_MAX);
     }
 }
-- 
2.55.0



^ permalink raw reply related	[flat|nested] 244+ messages in thread

* [PATCH v2 07/39] xen/riscv: add missing APLIC register offsets, masks to asm/aplic.h
  2026-08-27 15:20 [PATCH v2 00/39] [RISC-V] virtual interrupt controller (vAPLIC/vIMSIC) support Oleksii Kurochko
                   ` (5 preceding siblings ...)
  2026-08-27 15:20 ` [PATCH v2 06/39] xen/riscv: use UINT64_MAX to disable the VS-timer Oleksii Kurochko
@ 2026-08-27 15:20 ` Oleksii Kurochko
  2026-09-01 15:36   ` Baptiste Le Duc
  2026-08-27 15:20 ` [PATCH v2 08/39] xen/riscv: introduce device-agnostic MMIO emulation dispatch Oleksii Kurochko
                   ` (31 subsequent siblings)
  38 siblings, 1 reply; 244+ messages in thread
From: Oleksii Kurochko @ 2026-08-27 15:20 UTC (permalink / raw)
  To: xen-devel
  Cc: Romain Caritey, Baptiste Le Duc, Zheng Zhang, Oleksii Kurochko,
	Alistair Francis, Connor Davis, Andrew Cooper, Anthony PERARD,
	Michal Orzel, Jan Beulich, Julien Grall, Roger Pau Monné,
	Stefano Stabellini

These definitions are required for correct decoding of APLIC MMIO
accesses and target configuration, and will be used by both the
physical and virtual APLIC implementations.

While adding them, rearrange the header in the style of x86's
asm/msr-index.h: a register's offset is immediately followed by the
definitions of that register's fields, with the blocks sorted by
offset.  This makes the relation between a register and its fields
obvious from the layout alone, so no comment is needed to express it.

No functional change is intended by this patch; it only centralises
hardware definitions that were previously missing.

Co-developed-by: Romain Caritey <Romain.Caritey@microchip.com>
Signed-off-by: Oleksii Kurochko <oleksii.kurochko@gmail.com>
---
Changes in v2:
 - Rearrange the whole header the way x86's asm/msr-index.h is laid out:
   put each register offset first and the definitions of its fields
   immediately after it (indented by an extra space), sorted by offset.
 - Describe the convention in a comment at the top of the definitions,
   mirroring the one in asm/msr-index.h.
 - Reflow APLIC_SIZE() to fit the new alignment column.
 - Fix the comment for declaration of member target in aplic_regs[]. It
   should be 0x3004.
 - s/APLIC_REG_OFFSET_MASK/APLIC_CTRL_REGION_OFFSET_MASK
---
---
 xen/arch/riscv/include/asm/aplic.h | 90 ++++++++++++++++++++++++------
 1 file changed, 73 insertions(+), 17 deletions(-)

diff --git a/xen/arch/riscv/include/asm/aplic.h b/xen/arch/riscv/include/asm/aplic.h
index 07318aaac25d..a2af55d54fc0 100644
--- a/xen/arch/riscv/include/asm/aplic.h
+++ b/xen/arch/riscv/include/asm/aplic.h
@@ -15,32 +15,88 @@
 
 #include <asm/imsic.h>
 
+/*
+ * APLIC register offsets and, immediately following each of them, the
+ * definitions of the fields of the respective register:
+ *
+ * #define APLIC_$NAME                      0x$OFFSET
+ * #define  APLIC_$NAME_$FIELD1             ...
+ * #define   APLIC_$NAME_$FIELD1_$VAL       ...
+ * #define  APLIC_$NAME_$FIELD2             ...
+ *
+ * Blocks of related constants are sorted by register offset.
+ */
+
+#define APLIC_CTRL_REGION_OFFSET_MASK       0x3fff
+
+#define APLIC_DOMAINCFG                     0x0000
 /*
  * domaincfg read-only fields (AIA spec):
  *  - bits [31:24] -> read-only 0x80
  *  - bit 7        -> read-only 0
  */
-#define APLIC_DOMAINCFG_RO      (0x80U << 24)
-#define APLIC_DOMAINCFG_IE      BIT(8, U)
-#define APLIC_DOMAINCFG_DM      BIT(2, U)
-#define APLIC_DOMAINCFG_BE      BIT(0, U)
+#define  APLIC_DOMAINCFG_RO             (0x80U << 24)
+#define  APLIC_DOMAINCFG_IE             BIT(8, U)
+#define  APLIC_DOMAINCFG_DM             BIT(2, U)
+#define  APLIC_DOMAINCFG_BE             BIT(0, U)
+
+#define APLIC_SOURCECFG_BASE            0x0004
+#define APLIC_SOURCECFG_LAST            0x0ffc
+/*
+ * sourcecfg[] register fields:
+ *  - bit 10 (D) selects the layout of the remaining bits;
+ *  - D = 1: bits [9:0] hold the Child Index, i.e. the source is delegated
+ *           to a child domain (unsupported by Xen);
+ *  - D = 0: bits [2:0] hold the source mode SM (WARL).
+ */
+#define  APLIC_SOURCECFG_D              BIT(10, U)
+#define  APLIC_SOURCECFG_SM             GENMASK(2, 0)
+#define   APLIC_SOURCECFG_SM_INACTIVE   0x0
+#define   APLIC_SOURCECFG_SM_DETACH     0x1
+/* Bits 0x2 and 0x3 are reserved */
+#define   APLIC_SOURCECFG_SM_EDGE_RISE  0x4
+#define   APLIC_SOURCECFG_SM_EDGE_FALL  0x5
+#define   APLIC_SOURCECFG_SM_LEVEL_HIGH 0x6
+#define   APLIC_SOURCECFG_SM_LEVEL_LOW  0x7
+
+#define APLIC_SMSICFGADDR               0x1bc8
+#define APLIC_SMSICFGADDRH              0x1bcc
+
+#define APLIC_SETIP_BASE                0x1c00
+#define APLIC_SETIP_LAST                0x1c7c
+#define APLIC_SETIPNUM                  0x1cdc
+
+#define APLIC_CLRIP_BASE                0x1d00
+#define APLIC_CLRIP_LAST                0x1d7c
+#define APLIC_CLRIPNUM                  0x1ddc
+
+#define APLIC_SETIE_BASE                0x1e00
+#define APLIC_SETIE_LAST                0x1e7c
+#define APLIC_SETIENUM                  0x1edc
+
+#define APLIC_CLRIE_BASE                0x1f00
+#define APLIC_CLRIE_LAST                0x1f7c
+#define APLIC_CLRIENUM                  0x1fdc
+
+#define APLIC_SETIPNUM_LE               0x2000
 
-#define APLIC_SOURCECFG_SM_INACTIVE     0x0
-#define APLIC_SOURCECFG_SM_DETACH       0x1
-#define APLIC_SOURCECFG_SM_EDGE_RISE    0x4
-#define APLIC_SOURCECFG_SM_EDGE_FALL    0x5
-#define APLIC_SOURCECFG_SM_LEVEL_HIGH   0x6
-#define APLIC_SOURCECFG_SM_LEVEL_LOW    0x7
+#define APLIC_GENMSI                    0x3000
 
-#define APLIC_TARGET_HART_IDX_SHIFT 18
+#define APLIC_TARGET_BASE               0x3004
+#define APLIC_TARGET_LAST               0x3ffc
+#define  APLIC_TARGET_HART_IDX          GENMASK(31, 18)
+#define  APLIC_TARGET_HART_IDX_SHIFT    18
+#define  APLIC_TARGET_GUEST_IDX         GENMASK(17, 12)
+/* Bit 11 is reserved and reads as zero */
+#define  APLIC_TARGET_EIID              GENMASK(10, 0)
 
-#define APLIC_IDC_SIZE          32
+#define APLIC_IDC_SIZE                  32
 
-#define APLIC_MIN_SIZE          0x4000
-#define APLIC_SIZE_ALIGN(x)     ROUNDUP(x, APLIC_MIN_SIZE)
+#define APLIC_MIN_SIZE                  0x4000
+#define APLIC_SIZE_ALIGN(x)             ROUNDUP(x, APLIC_MIN_SIZE)
 
-#define APLIC_SIZE(nr_cpus)     (APLIC_MIN_SIZE + \
-                                 APLIC_SIZE_ALIGN(APLIC_IDC_SIZE * (nr_cpus)))
+#define APLIC_SIZE(nr_cpus) \
+    (APLIC_MIN_SIZE + APLIC_SIZE_ALIGN(APLIC_IDC_SIZE * (nr_cpus)))
 
 struct aplic_regs {
     uint32_t domaincfg;         /* 0x0000 */
@@ -82,7 +138,7 @@ struct aplic_regs {
     uint8_t _reserved11[4088];  /* 0x2008 */
 
     uint32_t genmsi;            /* 0x3000 */
-    uint32_t target[1023];      /* 0x3008 */
+    uint32_t target[1023];      /* 0x3004 */
 };
 
 #endif /* ASM_RISCV_APLIC_H */
-- 
2.55.0



^ permalink raw reply related	[flat|nested] 244+ messages in thread

* [PATCH v2 08/39] xen/riscv: introduce device-agnostic MMIO emulation dispatch
  2026-08-27 15:20 [PATCH v2 00/39] [RISC-V] virtual interrupt controller (vAPLIC/vIMSIC) support Oleksii Kurochko
                   ` (6 preceding siblings ...)
  2026-08-27 15:20 ` [PATCH v2 07/39] xen/riscv: add missing APLIC register offsets, masks to asm/aplic.h Oleksii Kurochko
@ 2026-08-27 15:20 ` Oleksii Kurochko
  2026-09-01 15:36   ` Baptiste Le Duc
  2026-09-09 13:24   ` Jan Beulich
  2026-08-27 15:20 ` [PATCH v2 09/39] xen/riscv: implement virtual APLIC MMIO emulation Oleksii Kurochko
                   ` (30 subsequent siblings)
  38 siblings, 2 replies; 244+ messages in thread
From: Oleksii Kurochko @ 2026-08-27 15:20 UTC (permalink / raw)
  To: xen-devel
  Cc: Romain Caritey, Baptiste Le Duc, Zheng Zhang, Oleksii Kurochko,
	Alistair Francis, Connor Davis, Andrew Cooper, Anthony PERARD,
	Michal Orzel, Jan Beulich, Julien Grall, Roger Pau Monné,
	Stefano Stabellini

RISC-V guests can expose several virtual interrupt controllers at
distinct GPA ranges: vPLIC (hasn't been introduced yet) for legacy machines,
vAPLIC and vIMSIC for AIA-compliant ones (are being introduced in the follow
up patches). Routing MMIO faults via a per-device is_access() check in the
trap handler would couple it to every device it must serve, requiring a
new conditional branch in the fault path each time a new emulated device is
added.

Introduce a per-domain MMIO handler registration table, modeled
after the equivalent ARM framework, so that virtual devices
self-register their GPA ranges and read/write callbacks at domain
creation time. The MMIO fault path delegates to a single
try_handle_mmio() entry point and remains agnostic of which device
owns a particular address.

A subsequent patch wires this into the MMIO fault path in traps.c.

Signed-off-by: Oleksii Kurochko <oleksii.kurochko@gmail.com>
---
Changes in v2:
 - Drop copyright from mmio.c as it will go stale anyway as code moves
   around.
 - Drop the max_count parameter of domain_io_init() (MAX_IO_HANDLER is a
   global boundary) and embed the handler array directly in struct vmmio
   as struct mmio_handler handlers[MAX_IO_HANDLER].  This removes the
   xvzalloc_array() allocation, the max_num_entries field and
   domain_io_free() altogether; domain_io_init() consequently cannot fail
   any longer and now returns void. Note this goes slightly beyond the
   suggested variant in that max_num_entries is dropped as well, since
   ARRAY_SIZE(vmmio->handlers) serves the same purpose.
 - Drop the separate register_t argument of mmio_read_t/mmio_write_t;
   handlers now produce and consume the value through info->data.
   handle_read()/handle_write() are gone as a result, with
   try_handle_mmio() invoking ops->read()/ops->write() directly.
 - Turn mmio_read_t/mmio_write_t into function types rather than
   pointer-to-function types, so that pointer-ness is visible at the use
   sites in struct mmio_handler_ops. Constify the mmio_info_t * of the
   write callback, which has no reason to modify it any more.
 - register_mmio_handler() returns int instead of BUG_ON()ing on a full
   table: -ENOSPC now lets the caller fail domain creation. It also
   validates its inputs, rejecting a NULL ops (or one with a missing
   read/write callback) as well as zero-sized and address-wrapping
   regions with -EINVAL.
 - Guarantee the non-overlap property that cmp_mmio_handler() relies on:
   register_mmio_handler() checks the new region against both neighbours
   of its insertion slot and returns -EEXIST on overlap.
 - Replace the sort() call per registration with an insertion into the
   already sorted array: locate the slot and memmove() the tail up by
   one. sort(), swap_mmio_handler() and <xen/sort.h> are gone.
 - Extend cmp_mmio_handler()'s comment to state that it is a bsearch()
   comparator and to explain the key/elem asymmetry; document why the
   neighbours' addr + size cannot overflow.
 - Fix over-long lines and a mis-indented label.
---
---
 xen/arch/riscv/Makefile             |   1 +
 xen/arch/riscv/domain.c             |   3 +
 xen/arch/riscv/include/asm/domain.h |   3 +
 xen/arch/riscv/include/asm/mmio.h   |  63 ++++++++++
 xen/arch/riscv/mmio.c               | 176 ++++++++++++++++++++++++++++
 5 files changed, 246 insertions(+)
 create mode 100644 xen/arch/riscv/include/asm/mmio.h
 create mode 100644 xen/arch/riscv/mmio.c

diff --git a/xen/arch/riscv/Makefile b/xen/arch/riscv/Makefile
index 3b948c11dd61..ce6410a299a4 100644
--- a/xen/arch/riscv/Makefile
+++ b/xen/arch/riscv/Makefile
@@ -14,6 +14,7 @@ obj-y += intc.o
 obj-y += irq.o
 obj-y += kernel.init.o
 obj-y += mm.o
+obj-y += mmio.o
 obj-y += p2m.o
 obj-y += paging.o
 obj-y += pt.o
diff --git a/xen/arch/riscv/domain.c b/xen/arch/riscv/domain.c
index 57c37cb2dfc2..ec327a5e8a23 100644
--- a/xen/arch/riscv/domain.c
+++ b/xen/arch/riscv/domain.c
@@ -12,6 +12,7 @@
 #include <asm/cpufeature.h>
 #include <asm/csr.h>
 #include <asm/intc.h>
+#include <asm/mmio.h>
 #include <asm/riscv_encoding.h>
 #include <asm/vtimer.h>
 
@@ -316,6 +317,8 @@ int arch_domain_create(struct domain *d,
     if ( (rc = p2m_init(d, config)) != 0)
         goto fail;
 
+    domain_io_init(d);
+
     if ( (rc = domain_vintc_init(d)) )
         goto fail;
 
diff --git a/xen/arch/riscv/include/asm/domain.h b/xen/arch/riscv/include/asm/domain.h
index e035b33ddfdc..15e8fa19685e 100644
--- a/xen/arch/riscv/include/asm/domain.h
+++ b/xen/arch/riscv/include/asm/domain.h
@@ -9,6 +9,7 @@
 
 #include <asm/cpufeature.h>
 #include <asm/guest-layout.h>
+#include <asm/mmio.h>
 #include <asm/p2m.h>
 #include <asm/vtimer.h>
 
@@ -101,6 +102,8 @@ struct arch_domain {
     const unsigned long *isa;
 
     struct vintc *vintc;
+
+    struct vmmio vmmio;
 };
 
 #include <xen/sched.h>
diff --git a/xen/arch/riscv/include/asm/mmio.h b/xen/arch/riscv/include/asm/mmio.h
new file mode 100644
index 000000000000..582969e5351b
--- /dev/null
+++ b/xen/arch/riscv/include/asm/mmio.h
@@ -0,0 +1,63 @@
+/* SPDX-License-Identifier: GPL-2.0-or-later */
+#ifndef RISCV_MMIO_H
+#define RISCV_MMIO_H
+
+#include <xen/lib.h>
+#include <xen/rwlock.h>
+
+struct domain;
+struct vcpu;
+
+#define MAX_IO_HANDLER  16
+
+typedef struct {
+    paddr_t gpa;
+    unsigned int len;  /* access width in bytes (1, 2, 4, 8) */
+    bool is_write;
+    /* store: value to write; load: value read (set by handler) */
+    register_t data;
+} mmio_info_t;
+
+enum io_state
+{
+    IO_ABORT,       /* The IO was handled and led to an abort. */
+    IO_HANDLED,     /* The IO was successfully handled. */
+    IO_UNHANDLED,   /* No handler found for the IO. */
+};
+
+typedef enum io_state (mmio_read_t)(struct vcpu *v, mmio_info_t *info);
+typedef enum io_state (mmio_write_t)(struct vcpu *v, const mmio_info_t *info);
+
+struct mmio_handler_ops {
+    mmio_read_t *read;
+    mmio_write_t *write;
+};
+
+struct mmio_handler {
+    paddr_t addr;
+    paddr_t size;
+    const struct mmio_handler_ops *ops;
+};
+
+struct vmmio {
+    unsigned int num_entries;
+    rwlock_t lock;
+    struct mmio_handler handlers[MAX_IO_HANDLER];
+};
+
+int do_mmio(mmio_info_t *info, paddr_t fault_addr, unsigned int len);
+int register_mmio_handler(struct domain *d,
+                          const struct mmio_handler_ops *ops,
+                          paddr_t addr, paddr_t size);
+void domain_io_init(struct domain *d);
+
+#endif /* RISCV_MMIO_H */
+
+/*
+ * Local variables:
+ * mode: C
+ * c-file-style: "BSD"
+ * c-basic-offset: 4
+ * indent-tabs-mode: nil
+ * End:
+ */
diff --git a/xen/arch/riscv/mmio.c b/xen/arch/riscv/mmio.c
new file mode 100644
index 000000000000..d241ab5ea13d
--- /dev/null
+++ b/xen/arch/riscv/mmio.c
@@ -0,0 +1,176 @@
+/* SPDX-License-Identifier: GPL-2.0-or-later */
+
+#include <xen/bsearch.h>
+#include <xen/lib.h>
+#include <xen/rwlock.h>
+#include <xen/sched.h>
+#include <xen/string.h>
+
+#include <asm/current.h>
+#include <asm/mmio.h>
+
+/*
+ * bsearch() comparator: @key holds the address to look up in its addr field,
+ * @elem is an entry of vmmio->handlers. Relies on the regions not
+ * overlapping, which register_mmio_handler() enforces.
+ */
+static int cmp_mmio_handler(const void *key, const void *elem)
+{
+    const struct mmio_handler *handler0 = key;
+    const struct mmio_handler *handler1 = elem;
+
+    if ( handler0->addr < handler1->addr )
+        return -1;
+
+    if ( handler0->addr >= (handler1->addr + handler1->size) )
+        return 1;
+
+    return 0;
+}
+
+/*
+ * Return a copy of the matching handler rather than a pointer into
+ * vmmio->handlers: a concurrent register_mmio_handler() shifts entries
+ * up to keep the array sorted, so an escaped pointer could refer to a
+ * different (or torn) entry once the lock is dropped. The copy stays
+ * valid as the ops structures are never freed.
+ */
+static bool find_mmio_handler(struct domain *d, paddr_t gpa,
+                              struct mmio_handler *out)
+{
+    struct vmmio *vmmio = &d->arch.vmmio;
+    struct mmio_handler key = { .addr = gpa };
+    const struct mmio_handler *handler;
+
+    read_lock(&vmmio->lock);
+    handler = bsearch(&key, vmmio->handlers, vmmio->num_entries,
+                      sizeof(*handler), cmp_mmio_handler);
+    if ( handler )
+        *out = *handler;
+    read_unlock(&vmmio->lock);
+
+    return handler != NULL;
+}
+
+static enum io_state try_handle_mmio(mmio_info_t *info)
+{
+    struct vcpu *v = current;
+    struct mmio_handler handler = {};
+
+    if ( !find_mmio_handler(v->domain, info->gpa, &handler) )
+        return IO_UNHANDLED;
+
+    if ( info->is_write )
+        return handler.ops->write(v, info);
+    else
+        return handler.ops->read(v, info);
+}
+
+/*
+ * Check alignment and dispatch a decoded MMIO access to a registered
+ * handler. On success (0), info->data holds the read value for loads.
+ *
+ * There is no "retry" outcome to handle: find_mmio_handler() returns a
+ * copy of the matching handler taken under vmmio->lock and the ops
+ * structures are never freed, so the lookup result cannot go stale
+ * between finding the handler and invoking it.
+ */
+int do_mmio(mmio_info_t *info, paddr_t fault_addr, unsigned int len)
+{
+    /* Fault address should be aligned to length of MMIO */
+    if ( fault_addr & (len - 1) )
+        return -EIO;
+
+    info->gpa = fault_addr;
+    info->len = len;
+
+    switch ( try_handle_mmio(info) )
+    {
+    case IO_HANDLED:
+        return 0;
+
+    case IO_ABORT:
+        return -EIO;
+
+    default:
+        return -EOPNOTSUPP;
+    }
+}
+
+int register_mmio_handler(struct domain *d,
+                          const struct mmio_handler_ops *ops,
+                          paddr_t addr, paddr_t size)
+{
+    struct vmmio *vmmio = &d->arch.vmmio;
+    struct mmio_handler *handlers = vmmio->handlers;
+    paddr_t end = addr + size;
+    unsigned int i;
+    int rc = 0;
+    bool overlap;
+
+    if ( !ops || !ops->read || !ops->write || !size || end < addr )
+        return -EINVAL;
+
+    write_lock(&vmmio->lock);
+
+    if ( vmmio->num_entries >= ARRAY_SIZE(vmmio->handlers) )
+    {
+        rc = -ENOSPC;
+        goto out;
+    }
+
+    /*
+     * The array is kept sorted by base address, so rather than appending and
+     * re-sorting, find the slot the new region belongs to and shift the tail
+     * up by one.
+     */
+    for ( i = vmmio->num_entries;
+          i > 0 && handlers[i - 1].addr > addr;
+          i-- )
+        /* Nothing */;
+
+    /*
+     * Regions are required not to overlap; check both neighbours. Their
+     * addr + size cannot overflow, as such regions are rejected above when
+     * they get registered.
+     */
+    overlap = (i > 0 && handlers[i - 1].addr + handlers[i - 1].size > addr) ||
+              (i < vmmio->num_entries && end > handlers[i].addr);
+
+    if ( overlap )
+    {
+        rc = -EEXIST;
+        goto out;
+    }
+
+    memmove(&handlers[i + 1], &handlers[i],
+            (vmmio->num_entries - i) * sizeof(*handlers));
+
+    handlers[i] = (struct mmio_handler){
+        .addr = addr,
+        .size = size,
+        .ops = ops,
+    };
+
+    vmmio->num_entries++;
+
+ out:
+    write_unlock(&vmmio->lock);
+
+    return rc;
+}
+
+void domain_io_init(struct domain *d)
+{
+    rwlock_init(&d->arch.vmmio.lock);
+    d->arch.vmmio.num_entries = 0;
+}
+
+/*
+ * Local variables:
+ * mode: C
+ * c-file-style: "BSD"
+ * c-basic-offset: 4
+ * indent-tabs-mode: nil
+ * End:
+ */
-- 
2.55.0



^ permalink raw reply related	[flat|nested] 244+ messages in thread

* [PATCH v2 09/39] xen/riscv: implement virtual APLIC MMIO emulation
  2026-08-27 15:20 [PATCH v2 00/39] [RISC-V] virtual interrupt controller (vAPLIC/vIMSIC) support Oleksii Kurochko
                   ` (7 preceding siblings ...)
  2026-08-27 15:20 ` [PATCH v2 08/39] xen/riscv: introduce device-agnostic MMIO emulation dispatch Oleksii Kurochko
@ 2026-08-27 15:20 ` Oleksii Kurochko
  2026-09-02 11:51   ` Baptiste Le Duc
                     ` (2 more replies)
  2026-08-27 15:20 ` [PATCH v2 10/39] xen/riscv: build the target hart index via aplic_hart_field() Oleksii Kurochko
                   ` (29 subsequent siblings)
  38 siblings, 3 replies; 244+ messages in thread
From: Oleksii Kurochko @ 2026-08-27 15:20 UTC (permalink / raw)
  To: xen-devel
  Cc: Romain Caritey, Baptiste Le Duc, Zheng Zhang, Oleksii Kurochko,
	Alistair Francis, Connor Davis, Andrew Cooper, Anthony PERARD,
	Michal Orzel, Jan Beulich, Julien Grall, Roger Pau Monné,
	Stefano Stabellini

Guests running under Xen program interrupt routing by writing to APLIC
MMIO registers. Xen must intercept these accesses to enforce interrupt
isolation between domains and to translate guest routing intent into the
underlying physical MSI topology.

Writes are gated by the domain's authorised interrupt bitmap so that a
guest cannot affect interrupts it does not own. TARGET register writes
additionally require translation of the hart and IMSIC guest-file
indices from virtual to physical, as the APLIC uses these fields
directly to compute the MSI delivery address.

Delegation (APLIC_SOURCECFG_D) is not yet supported.

Co-developed-by: Romain Caritey <Romain.Caritey@microchip.com>
Signed-off-by: Oleksii Kurochko <oleksii.kurochko@gmail.com>
---
Changes in v2:
 - Shadow the guest-written target registers in a per-domain array and
   serve reads of APLIC_TARGET_* from it: the hardware register holds the
   value produced by aplic_msi_target_gen() (physical hart field, VS-file
   id), so reading it back would expose the host layout and return
   something the guest never wrote. A non-zero guest index is dropped with
   a one-time warning instead of being echoed back.
 - aplic_hart_field(): take a CPU id instead of a hartid and derive both
   the group and the hart index from msi->base_addr + msi->offset - the
   hart index bits are a part of that offset, so the hartid can't be used
   as the hart index. Add APLIC_xMSICFGADDR_PPN_LHX_{MASK,SHIFT} for that.
 - Document the IMSIC MSI target address layout and the APLIC hart index
   packing above aplic_hart_field(), and correct the corresponding diagram
   in asm/imsic.h.
 - Drop the mask parameter of aplic_hw_read_reg() and apply the
   authorization mask in vaplic_emulate_load() instead.
 - vaplic_emulate_{load,store}(): return bool instead of int, rename v/d to
   curr/currd and add ASSERT(curr == current).
 - Use domain_vcpu() instead of open-coded d->vcpu[] indexing when
   resolving the target hart index.
 - s/APLIC_REG_OFFSET_MASK/APLIC_CTRL_REGION_OFFSET_MASK/.
 - Update store emulation handling for target register to be able to deal
   with target format in both cases (MSI and Direct).
 - Update store emulation handling of domaincfg. There is no need to force
   DM/IE mode here (it will be forced/checked on Xen irq handler side).
 - Rename APLIC_DEFAULT_PRIORITY and move to aplic.h as default prioity
   value is used in vaplic code too now.
---
---
 xen/arch/riscv/aplic-priv.h         |   3 +
 xen/arch/riscv/aplic.c              | 128 +++++++++-
 xen/arch/riscv/include/asm/aplic.h  |  34 +++
 xen/arch/riscv/include/asm/imsic.h  |  13 ++
 xen/arch/riscv/include/asm/vaplic.h |   5 +
 xen/arch/riscv/vaplic.c             | 350 +++++++++++++++++++++++++++-
 6 files changed, 528 insertions(+), 5 deletions(-)

diff --git a/xen/arch/riscv/aplic-priv.h b/xen/arch/riscv/aplic-priv.h
index 35100d3a64fe..b3a1f79c5b76 100644
--- a/xen/arch/riscv/aplic-priv.h
+++ b/xen/arch/riscv/aplic-priv.h
@@ -47,4 +47,7 @@ struct aplic_priv {
  */
 extern unsigned int guest_aplic_num_sources;
 
+uint32_t aplic_msi_target_gen(const struct vcpu *target_vcpu,
+                              uint32_t base_val);
+
 #endif /* ASM_RISCV_APLIC_PRIV_H */
diff --git a/xen/arch/riscv/aplic.c b/xen/arch/riscv/aplic.c
index 3681f0669efb..66ba4986a9ff 100644
--- a/xen/arch/riscv/aplic.c
+++ b/xen/arch/riscv/aplic.c
@@ -16,6 +16,7 @@
 #include <xen/irq.h>
 #include <xen/mm.h>
 #include <xen/sections.h>
+#include <xen/sched.h>
 #include <xen/spinlock.h>
 #include <xen/types.h>
 #include <xen/vmap.h>
@@ -28,8 +29,6 @@
 #include <asm/io.h>
 #include <asm/riscv_encoding.h>
 
-#define APLIC_DEFAULT_PRIORITY  1
-
 static struct aplic_priv aplic = {
     .lock = SPIN_LOCK_UNLOCKED,
 };
@@ -38,6 +37,127 @@ static struct intc_info __ro_after_init aplic_info = {
     .hw_variant = INTC_APLIC,
 };
 
+/*
+ * The arrangement of IMSIC interrupt files in MMIO space follows a topology
+ * defined by the RISC-V AIA specification. An IMSIC group is a set of
+ * interrupt files (e.g., in a cluster or socket) co-located in memory.
+ *
+ * The physical address of an outgoing MSI is calculated by bitwise ORing a
+ * Base Physical Page Number (Base PPN) with the Group Index (g), the Hart
+ * Index (h) and, for a supervisor-level interrupt domain, the Guest Index:
+ *
+ *   ( Base PPN | (g << (HHXS + 12)) | (h << LHXS) | guest ) << 12
+ *
+ * where Base PPN, HHXS, LHXS, HHXW and LHXW come from the {m,s}msiaddrcfg[h]
+ * registers of the interrupt domain that sends the MSI:
+ *
+ * XLEN-1       HHXS+24          LHXS+12          12          0
+ * |            |                |                |           |
+ * ------------------------------------------------------------
+ * |xxxx|   g   |xxxxxxxx|   h   |xxxx|Guest Index|     0     |
+ * ------------------------------------------------------------
+ *
+ * - g: group number.
+ * - h: hart number relative to the group.
+ * - xxxx: remaining Base PPN bits; each gap may be zero-width.
+ * - Guest Index: selects one of the 4 KiB pages right above the hart's own
+ *   supervisor-level file, i.e. it starts at bit 12; LHXS must therefore be
+ *   at least as large as the number of guest index bits.
+ * - Bits 11:0: always zero because IMSIC files are 4 KiB page-aligned.
+ *
+ * For wired interrupts in MSI delivery mode (domaincfg.DM = 1) the APLIC
+ * builds that address itself from the "Hart Index" field (bits 31:18) of the
+ * corresponding target[i] register. That field holds a hart index *number*,
+ * in which both indices are packed adjacently:
+ *
+ * 13          lhxw+hhxw   lhxw       0
+ * |           |           |          |
+ * ------------------------------------
+ * |     0     |Group Index|Hart Index|
+ * ------------------------------------
+ *
+ * - lhxw (Low Hart Index Width): the number of bits used for the hart number
+ *   within a group.
+ * - hhxw (High Hart Index Width): the number of bits used for the group
+ *   number; the remaining bits of the field must be zero.
+ *
+ * The Guest Index isn't a part of it: for a supervisor-level interrupt domain
+ * it has its own field (bits 17:12) in target[i].
+ *
+ * Because there are "xxxx" gaps (Base PPN bits) between the indices in the
+ * physical address (depending on HHXS and LHXS), software must extract the
+ * group and hart components separately and pack them into the APLIC-defined
+ * Hart Index format to ensure correct MSI targeting.
+ */
+static unsigned long aplic_hart_field(unsigned int cpu)
+{
+    const struct imsic_config *imsic = imsic_get_config();
+    const struct imsic_msi *msi = &imsic->msi[cpu];
+    /* Low Hart Index Shift */
+    unsigned int lhxs = imsic->guest_index_bits;
+    /* Low Hart Index Width */
+    unsigned int lhxw = imsic->hart_index_bits;
+    /* High Hart Index Width */
+    unsigned int hhxw = imsic->group_index_bits;
+    /* High Hart Index Shift */
+    unsigned int hhxs =
+        imsic->group_index_shift - APLIC_xMSICFGADDR_PPN_SHIFT * 2;
+    /*
+     * msi->base_addr is the base of the MMIO regset this CPU's interrupt
+     * files live in, and one regset can cover several harts; msi->offset
+     * selects this CPU's block inside it. The hart index bits are part of
+     * that offset, so both indexes have to be derived from the full address.
+     */
+    paddr_t target_addr = msi->base_addr + msi->offset;
+    unsigned long tppn = target_addr >> APLIC_xMSICFGADDR_PPN_SHIFT;
+    unsigned long g =
+        (tppn >> APLIC_xMSICFGADDR_PPN_HHX_SHIFT(hhxs)) &
+        APLIC_xMSICFGADDR_PPN_HHX_MASK(hhxw);
+    unsigned long h =
+        (tppn >> APLIC_xMSICFGADDR_PPN_LHX_SHIFT(lhxs)) &
+        APLIC_xMSICFGADDR_PPN_LHX_MASK(lhxw);
+
+    return (g << lhxw) | h;
+}
+
+uint32_t aplic_msi_target_gen(const struct vcpu *target_vcpu,
+                              uint32_t base_val)
+{
+    unsigned int guest_id = vcpu_guest_file_id(target_vcpu);
+    unsigned long hart_field = aplic_hart_field(target_vcpu->processor);
+
+    base_val &= APLIC_TARGET_EIID;
+    base_val |= MASK_INSR(guest_id, APLIC_TARGET_GUEST_IDX);
+    base_val |= MASK_INSR(hart_field, APLIC_TARGET_HART_IDX);
+
+    return base_val;
+}
+
+uint32_t aplic_hw_read_reg(unsigned int offset)
+{
+    unsigned long flags;
+    uint32_t val;
+
+    ASSERT((offset < aplic.size) && IS_ALIGNED(offset, sizeof(uint32_t)));
+
+    spin_lock_irqsave(&aplic.lock, flags);
+    val = readl((volatile void __iomem *)aplic.regs + offset);
+    spin_unlock_irqrestore(&aplic.lock, flags);
+
+    return val;
+}
+
+void aplic_hw_write_reg(unsigned int offset, uint32_t value)
+{
+    unsigned long flags;
+
+    ASSERT((offset < aplic.size) && IS_ALIGNED(offset, sizeof(uint32_t)));
+
+    spin_lock_irqsave(&aplic.lock, flags);
+    writel(value, (volatile void __iomem *)aplic.regs + offset);
+    spin_unlock_irqrestore(&aplic.lock, flags);
+}
+
 static void __init aplic_init_hw_interrupts(void)
 {
     unsigned int i;
@@ -53,9 +173,9 @@ static void __init aplic_init_hw_interrupts(void)
         /*
          * Low bits of target register contains Interrupt Priority bits which
          * can't be zero according to AIA spec.
-         * Thereby they are initialized to APLIC_DEFAULT_PRIORITY.
+         * Thereby they are initialized to APLIC_TARGET_IPRIO_DEFAULT.
          */
-        writel(APLIC_DEFAULT_PRIORITY, &aplic.regs->target[i]);
+        writel(APLIC_TARGET_IPRIO_DEFAULT, &aplic.regs->target[i]);
     }
 
     writel(APLIC_DOMAINCFG_IE | APLIC_DOMAINCFG_DM, &aplic.regs->domaincfg);
diff --git a/xen/arch/riscv/include/asm/aplic.h b/xen/arch/riscv/include/asm/aplic.h
index a2af55d54fc0..babba386071f 100644
--- a/xen/arch/riscv/include/asm/aplic.h
+++ b/xen/arch/riscv/include/asm/aplic.h
@@ -39,6 +39,13 @@
 #define  APLIC_DOMAINCFG_IE             BIT(8, U)
 #define  APLIC_DOMAINCFG_DM             BIT(2, U)
 #define  APLIC_DOMAINCFG_BE             BIT(0, U)
+/*
+ * The bits a write may change. Everything else, including the read-only zero
+ * bit 7 and the reserved bits, has to read back as zero, and BE is WARL and
+ * hardwired to 0 as Xen is little-endian only.
+ */
+#define  APLIC_DOMAINCFG_WMASK          (APLIC_DOMAINCFG_IE | \
+                                         APLIC_DOMAINCFG_DM)
 
 #define APLIC_SOURCECFG_BASE            0x0004
 #define APLIC_SOURCECFG_LAST            0x0ffc
@@ -89,6 +96,9 @@
 #define  APLIC_TARGET_GUEST_IDX         GENMASK(17, 12)
 /* Bit 11 is reserved and reads as zero */
 #define  APLIC_TARGET_EIID              GENMASK(10, 0)
+/* If target is in DM mode */
+#define  APLIC_TARGET_IPRIO             GENMASK(7, 0)
+#define   APLIC_TARGET_IPRIO_DEFAULT    1U
 
 #define APLIC_IDC_SIZE                  32
 
@@ -98,6 +108,27 @@
 #define APLIC_SIZE(nr_cpus) \
     (APLIC_MIN_SIZE + APLIC_SIZE_ALIGN(APLIC_IDC_SIZE * (nr_cpus)))
 
+/*
+ * Using setip is fine here, as all SET* and CLR* register groups consist of 32
+ * registers and therefore have identical sizes.
+ *
+ * Lowest 2 bits are always zero for SET* and CLR* registers.
+ */
+#define APLIC_SETCLR_OFFSET_MASK \
+    (sizeof_field(struct aplic_regs, setip) - sizeof(uint32_t))
+
+#define APLIC_xMSICFGADDR_PPN_SHIFT IMSIC_MMIO_PAGE_SHIFT
+
+#define APLIC_xMSICFGADDR_PPN_HHX_MASK(hhxw) \
+    (BIT(hhxw, UL) - 1)
+#define APLIC_xMSICFGADDR_PPN_HHX_SHIFT(hhxs) \
+    ((hhxs) + APLIC_xMSICFGADDR_PPN_SHIFT)
+
+#define APLIC_xMSICFGADDR_PPN_LHX_MASK(lhxw) \
+    (BIT(lhxw, UL) - 1)
+#define APLIC_xMSICFGADDR_PPN_LHX_SHIFT(lhxs) \
+    (lhxs)
+
 struct aplic_regs {
     uint32_t domaincfg;         /* 0x0000 */
     uint32_t sourcecfg[1023];   /* 0x0004 */
@@ -141,4 +172,7 @@ struct aplic_regs {
     uint32_t target[1023];      /* 0x3004 */
 };
 
+uint32_t aplic_hw_read_reg(unsigned int offset);
+void aplic_hw_write_reg(unsigned int offset, uint32_t value);
+
 #endif /* ASM_RISCV_APLIC_H */
diff --git a/xen/arch/riscv/include/asm/imsic.h b/xen/arch/riscv/include/asm/imsic.h
index 2425430ed116..93f9e44c7d2c 100644
--- a/xen/arch/riscv/include/asm/imsic.h
+++ b/xen/arch/riscv/include/asm/imsic.h
@@ -40,6 +40,19 @@ struct imsic_config {
     /* Base address */
     paddr_t base_addr;
 
+    /*
+     * MSI Target Address Scheme
+     *
+     * XLEN-1       HHXS+24          LHXS+12          12          0
+     * |            |                |                |           |
+     * ------------------------------------------------------------
+     * |xxxx|   g   |xxxxxxxx|   h   |xxxx|Guest Index|     0     |
+     * ------------------------------------------------------------
+     * - g: group number.
+     * - h: hart number relative to the group.
+     * - xxxx: remaining Base PPN bits; each gap may be zero-width.
+     */
+
     /* Bits representing Guest index, HART index, and Group index */
     unsigned int guest_index_bits;
     unsigned int hart_index_bits;
diff --git a/xen/arch/riscv/include/asm/vaplic.h b/xen/arch/riscv/include/asm/vaplic.h
index 96080bfbc23b..046c604915c4 100644
--- a/xen/arch/riscv/include/asm/vaplic.h
+++ b/xen/arch/riscv/include/asm/vaplic.h
@@ -21,11 +21,16 @@ struct domain;
 
 struct vaplic_regs {
     uint32_t domaincfg;
+
+    uint32_t *target;
 };
 
 struct vaplic {
     struct vintc vintc;
     struct vaplic_regs regs;
+
+    paddr_t regs_start;
+    unsigned int regs_size;
 };
 
 int domain_vaplic_init(struct domain *d);
diff --git a/xen/arch/riscv/vaplic.c b/xen/arch/riscv/vaplic.c
index 14f6e3164a9b..8726f7203d6e 100644
--- a/xen/arch/riscv/vaplic.c
+++ b/xen/arch/riscv/vaplic.c
@@ -17,6 +17,7 @@
 #include <asm/aia.h>
 #include <asm/imsic.h>
 #include <asm/intc.h>
+#include <asm/mmio.h>
 #include <asm/vaplic.h>
 
 #include "aplic-priv.h"
@@ -27,6 +28,279 @@ unsigned int __ro_after_init guest_aplic_num_sources;
 
 #define FDT_VAPLIC_INT_CELLS 2
 
+#define AUTH_IRQ_BIT(d, irqn) \
+    (((irqn) < (d)->arch.vintc->nr_virqs) && \
+     test_bit(irqn, (d)->arch.vintc->used_irqs))
+
+/*
+ * Convert a byte offset (within a SETIP/CLRIP/SETIE/CLRIE register group) to
+ * a 32-bit word index into the used_irqs bitmap. Each word covers 32
+ * interrupt sources. For SOURCECFG and TARGET groups the same division also
+ * yields the interrupt number directly, because those arrays store one 32-bit
+ * register per source.
+ */
+#define regoffset_to_word_idx(reg_val) ((reg_val) / sizeof(uint32_t))
+
+static uint32_t vaplic_target_read(const struct domain *d, unsigned int irqn)
+{
+    const struct vaplic *vaplic = to_vaplic(d);
+
+    /* target[0] doesn't exist so irqn == 0 should be impossible */
+    if ( !irqn || irqn >= vaplic->vintc.nr_virqs )
+        return 0;
+
+    return read_atomic(&vaplic->regs.target[irqn]);
+}
+
+static inline uint32_t generate_auth_mask(const struct domain *currd,
+                                          unsigned int word_idx)
+{
+    unsigned int first_bit = word_idx * sizeof(uint32_t) * BITS_PER_BYTE;
+
+    if ( word_idx >= DIV_ROUND_UP(currd->arch.vintc->nr_virqs,
+                                  sizeof(uint32_t) * BITS_PER_BYTE) )
+    {
+        gdprintk(XENLOG_DEBUG, "incorrect word_idx(%u) is passed\n", word_idx);
+
+        return 0;
+    }
+
+    return currd->arch.vintc->used_irqs[first_bit / BITS_PER_LONG] >>
+           (first_bit % BITS_PER_LONG);
+}
+
+static bool vaplic_emulate_load(const struct vcpu *curr, paddr_t addr,
+                                uint32_t *out)
+{
+    const struct domain *currd = curr->domain;
+    const struct vaplic *vaplic = to_vaplic(currd);
+    const unsigned int offset = addr & APLIC_CTRL_REGION_OFFSET_MASK;
+    uint32_t auth_mask;
+    unsigned int i;
+
+    ASSERT(curr == current);
+
+    switch ( offset )
+    {
+    case APLIC_DOMAINCFG:
+        *out = vaplic->regs.domaincfg;
+
+        return true;
+
+    case APLIC_SETIPNUM:
+    case APLIC_SETIPNUM_LE:
+    case APLIC_CLRIPNUM:
+    case APLIC_SETIENUM:
+    case APLIC_CLRIENUM:
+    case APLIC_CLRIE_BASE ... APLIC_CLRIE_LAST:
+        /*
+         * Based on the RISC-V AIA spec a read of these registers
+         * always returns zero
+         */
+        *out = 0;
+
+        return true;
+
+    case APLIC_SETIP_BASE ... APLIC_SETIP_LAST:
+    case APLIC_CLRIP_BASE ... APLIC_CLRIP_LAST:
+    case APLIC_SETIE_BASE ... APLIC_SETIE_LAST:
+        i = regoffset_to_word_idx(offset & APLIC_SETCLR_OFFSET_MASK);
+        auth_mask = generate_auth_mask(currd, i);
+
+        break;
+
+    case APLIC_TARGET_BASE ... APLIC_TARGET_LAST:
+        /*
+         * As target registers start from 1:
+         *  0x3000 genmsi
+         *  0x3004 target[1]
+         *  0x3008 target[2]
+         *   ...
+         *  0x3FFC target[1023]
+         * It is necessary to calculate an interrupt number by subtracting
+         * APLIC_GENMSI instead of APLIC_TARGET_BASE.
+         */
+        i = regoffset_to_word_idx(offset - APLIC_GENMSI);
+
+        *out = AUTH_IRQ_BIT(currd, i) ? vaplic_target_read(currd, i) : 0;
+
+        return true;
+
+    default:
+        gdprintk(XENLOG_WARNING, "Unhandled APLIC read at offset %#x\n",
+                 offset);
+
+        return false;
+    }
+
+    *out = aplic_hw_read_reg(offset) & auth_mask;
+
+    return true;
+}
+
+static bool vaplic_emulate_store(const struct vcpu *curr, paddr_t addr,
+                                 uint32_t value)
+{
+    const struct domain *currd = curr->domain;
+    unsigned int offset = addr & APLIC_CTRL_REGION_OFFSET_MASK;
+
+    ASSERT(curr == current);
+
+    switch ( offset )
+    {
+    case APLIC_SETIP_BASE ... APLIC_SETIP_LAST:
+    case APLIC_CLRIP_BASE ... APLIC_CLRIP_LAST:
+    case APLIC_SETIE_BASE ... APLIC_SETIE_LAST:
+    case APLIC_CLRIE_BASE ... APLIC_CLRIE_LAST:
+    {
+        unsigned int word_idx =
+            regoffset_to_word_idx(offset & APLIC_SETCLR_OFFSET_MASK);
+
+        value &= generate_auth_mask(currd, word_idx);
+
+        break;
+    }
+
+    case APLIC_SOURCECFG_BASE ... APLIC_SOURCECFG_LAST:
+        if ( value & APLIC_SOURCECFG_D )
+        {
+            dprintk(XENLOG_ERR, "APLIC_SOURCECFG_D isn't supported\n");
+
+            goto fail;
+        }
+
+        /*
+         * As sourcecfg register starts from 1:
+         *   0x0000 domaincfg
+         *   0x0004 sourcecfg[1]
+         *   0x0008 sourcecfg[2]
+         *    ...
+         *   0x0FFC sourcecfg[1023]
+         * It is necessary to calculate an interrupt number by subtracting
+         * APLIC_DOMAINCFG instead of APLIC_SOURCECFG_BASE.
+         */
+        if ( !AUTH_IRQ_BIT(currd,
+                           regoffset_to_word_idx(offset - APLIC_DOMAINCFG)) )
+            /* Interrupt not enabled, ignore it */
+            return true;
+
+        if ( value > APLIC_SOURCECFG_SM_LEVEL_LOW )
+        {
+            gdprintk(XENLOG_ERR,
+                     "value(%#x) is incorrect for sourcecfg register\n",
+                     value);
+
+            return true;
+        }
+
+        break;
+
+    case APLIC_TARGET_BASE ... APLIC_TARGET_LAST:
+    {
+        struct vaplic *vaplic = to_vaplic(currd);
+        struct vcpu *target_vcpu;
+        unsigned int guest_hart_idx = MASK_EXTR(value, APLIC_TARGET_HART_IDX);
+        /*
+         * Look at vaplic_emulate_load() for explanation why APLIC_GENMSI is
+         * subtracted.
+         */
+        unsigned int srcn = regoffset_to_word_idx(offset - APLIC_GENMSI);
+
+        if ( !AUTH_IRQ_BIT(currd, srcn) )
+            /* Interrupt not enabled, ignore it */
+            return true;
+
+        target_vcpu = domain_vcpu(currd, guest_hart_idx);
+
+        if ( !target_vcpu )
+        {
+            dprintk(XENLOG_ERR, "Invalid vCPU id in target register\n");
+
+            /* Ignore such writings */
+            return true;
+        }
+
+        if ( vaplic->regs.domaincfg & APLIC_DOMAINCFG_DM )
+        {
+            /*
+             * A non-zero guest index asks for delivery to an interrupt file of
+             * nested guest. The vIMSIC node has no riscv,guest-index-bits
+             * property, so a guest is told its harts have no guest interrupt
+             * files and the field is read-only zero for them. The write isn't
+             * rejected (that would throw away a valid hart index and EIID);
+             * instead the field is dropped, which is also what
+             * aplic_msi_target_gen() does with it when programming the h/w.
+             */
+            if ( MASK_EXTR(value, APLIC_TARGET_GUEST_IDX) )
+            {
+                printk_once(XENLOG_WARNING
+                            "%pd: vAPLIC target guest index != 0 is unsupported\n",
+                            currd);
+
+                /* Ignore such writes ... */
+                return true;
+            }
+
+            write_atomic(&vaplic->regs.target[srcn], value);
+
+            value = aplic_msi_target_gen(target_vcpu, value);
+        }
+        else
+        {
+            /*
+             * IPRIO is WARL and zero isn't a legal value for it, so normalize
+             * it once: the guest then reads back exactly what it gets.
+             */
+            unsigned int iprio = MASK_EXTR(value, APLIC_TARGET_IPRIO) ?:
+                                 APLIC_TARGET_IPRIO_DEFAULT;
+            unsigned long h = cpuid_to_hartid(guest_hart_idx);
+
+            value = MASK_INSR(guest_hart_idx, APLIC_TARGET_HART_IDX) |
+                    MASK_INSR(iprio, APLIC_TARGET_IPRIO);
+
+            write_atomic(&vaplic->regs.target[srcn], value);
+
+            value = MASK_INSR(h, APLIC_TARGET_HART_IDX) |
+                    MASK_INSR(iprio, APLIC_TARGET_IPRIO);
+        }
+
+        break;
+    }
+
+    case APLIC_SETIPNUM:
+    case APLIC_SETIPNUM_LE:
+    case APLIC_CLRIPNUM:
+    case APLIC_SETIENUM:
+    case APLIC_CLRIENUM:
+        if ( !value || !AUTH_IRQ_BIT(currd, value) )
+            return true;
+
+        break;
+
+    case APLIC_DOMAINCFG:
+    {
+        struct vaplic *vaplic = to_vaplic(currd);
+
+        vaplic->regs.domaincfg = APLIC_DOMAINCFG_RO |
+                                 (value & APLIC_DOMAINCFG_WMASK);
+
+        return true;
+    }
+
+    default:
+    fail:
+        gdprintk(XENLOG_WARNING,
+                 "Unhandled APLIC write at offset %#x (value %#x)\n", offset,
+                 value);
+
+        return false;
+    }
+
+    aplic_hw_write_reg(offset, value);
+
+    return true;
+}
+
 static int __init cf_check vaplic_make_domu_dt_node(struct kernel_info *kinfo)
 {
     struct domain *d = kinfo->bd.d;
@@ -95,6 +369,56 @@ static const struct vintc_init_ops __initconstrel init_ops = {
     .make_domu_dt_node = vaplic_make_domu_dt_node,
 };
 
+static enum io_state cf_check vaplic_mmio_read(struct vcpu *v,
+                                               mmio_info_t *info)
+{
+    uint32_t val;
+
+    ASSERT(v == current);
+
+    if ( info->len != sizeof(uint32_t) ||
+         !IS_ALIGNED(info->gpa, sizeof(uint32_t)) )
+    {
+        gdprintk(XENLOG_DEBUG,
+                 "VAPLIC: unaligned/wrong-width read gpa=%"PRIpaddr" len=%u\n",
+                 info->gpa, info->len);
+        return IO_ABORT;
+    }
+
+    if ( !vaplic_emulate_load(v, info->gpa, &val) )
+        return IO_ABORT;
+
+    /* APLIC registers are 32-bit; zero-extend to the guest register width. */
+    info->data = val;
+
+    return IO_HANDLED;
+}
+
+static enum io_state cf_check vaplic_mmio_write(struct vcpu *v,
+                                                const mmio_info_t *info)
+{
+    ASSERT(v == current);
+
+    if ( info->len != sizeof(uint32_t) ||
+         !IS_ALIGNED(info->gpa, sizeof(uint32_t)) )
+    {
+        gdprintk(XENLOG_DEBUG,
+                 "VAPLIC: unaligned/wrong-width write gpa=%"PRIpaddr" len=%u\n",
+                 info->gpa, info->len);
+        return IO_ABORT;
+    }
+
+    if ( !vaplic_emulate_store(v, info->gpa, info->data) )
+        return IO_ABORT;
+
+    return IO_HANDLED;
+}
+
+static const struct mmio_handler_ops vaplic_mmio_ops = {
+    .read  = vaplic_mmio_read,
+    .write = vaplic_mmio_write,
+};
+
 static const struct vintc_ops vintc_ops = {
     .vcpu_init = vcpu_imsic_init,
     .vcpu_deinit = vcpu_imsic_deinit,
@@ -103,6 +427,7 @@ static const struct vintc_ops vintc_ops = {
 int domain_vaplic_init(struct domain *d)
 {
     struct vaplic *vaplic = xvzalloc(struct vaplic);
+    int rc;
 
     if ( !vaplic )
         return -ENOMEM;
@@ -122,7 +447,29 @@ int domain_vaplic_init(struct domain *d)
      */
     d->arch.vintc->nr_virqs = guest_aplic_num_sources + 1;
 
-    return 0;
+    /* Slot 0 is unused: APLIC source numbering starts at 1 (see used_irqs). */
+    vaplic->regs.target = xvzalloc_array(uint32_t, d->arch.vintc->nr_virqs);
+    if ( !vaplic->regs.target )
+    {
+        d->arch.vintc = NULL;
+        xvfree(vaplic);
+
+        return -ENOMEM;
+    }
+
+    vaplic->regs_start = GUEST_APLIC_S_BASE;
+    vaplic->regs_size = APLIC_SIZE(d->max_vcpus);
+
+    rc = register_mmio_handler(d, &vaplic_mmio_ops,
+                               vaplic->regs_start, vaplic->regs_size);
+    if ( rc )
+    {
+        d->arch.vintc = NULL;
+        xvfree(vaplic->regs.target);
+        xvfree(vaplic);
+    }
+
+    return rc;
 }
 
 void domain_vaplic_deinit(struct domain *d)
@@ -134,5 +481,6 @@ void domain_vaplic_deinit(struct domain *d)
 
     vaplic = to_vaplic(d);
     d->arch.vintc = NULL;
+    xvfree(vaplic->regs.target);
     xvfree(vaplic);
 }
-- 
2.55.0



^ permalink raw reply related	[flat|nested] 244+ messages in thread

* [PATCH v2 10/39] xen/riscv: build the target hart index via aplic_hart_field()
  2026-08-27 15:20 [PATCH v2 00/39] [RISC-V] virtual interrupt controller (vAPLIC/vIMSIC) support Oleksii Kurochko
                   ` (8 preceding siblings ...)
  2026-08-27 15:20 ` [PATCH v2 09/39] xen/riscv: implement virtual APLIC MMIO emulation Oleksii Kurochko
@ 2026-08-27 15:20 ` Oleksii Kurochko
  2026-09-04  8:26   ` Baptiste Le Duc
  2026-09-09 14:52   ` Jan Beulich
  2026-08-27 15:20 ` [PATCH v2 11/39] xen/riscv: add helper to check APLIC MSI mode Oleksii Kurochko
                   ` (28 subsequent siblings)
  38 siblings, 2 replies; 244+ messages in thread
From: Oleksii Kurochko @ 2026-08-27 15:20 UTC (permalink / raw)
  To: xen-devel
  Cc: Romain Caritey, Baptiste Le Duc, Zheng Zhang, Oleksii Kurochko,
	Alistair Francis, Connor Davis, Andrew Cooper, Anthony PERARD,
	Michal Orzel, Jan Beulich, Julien Grall, Roger Pau Monné,
	Stefano Stabellini

aplic_set_irq_affinity() open-coded the packing of the group and hart
indices into the target register, and got two things wrong along the
way:

 - imsic_config.msi[] is indexed by logical CPU id, but the index was
   run through cpuid_to_hartid() first. On any platform where the two
   spaces differ this picks another CPU's interrupt file, or reads past
   the array;

 - the same hart id was then used verbatim as the low hart index, and
   the group index was derived from msi[].base_addr alone. The hart
   index bits live in msi[].offset, the base address only covers the
   MMIO regset, which may hold the files of several harts. Both indices
   have to come out of base_addr + offset.

aplic_hart_field() already extracts them that way, and is what the vAPLIC
target path uses, so call it here as well and insert the result with
MASK_INSR(APLIC_TARGET_HART_IDX) instead of a bare shift, which keeps the
value from spilling out of the 14-bit field. This also drops the last
in-tree duplicate of the AIA hart index formula. So drop defintion of
APLIC_TARGET_HART_IDX_SHIFT.

No functional change on a single-group platform whose hart ids match
their CPU ids and whose IMSIC regset holds one file per hart.

Fixes: d4676a1398bc ("xen/riscv: implementation of aplic and imsic operations")
Signed-off-by: Oleksii Kurochko <oleksii.kurochko@gmail.com>
---
Changes in v2:
 - New patch.
---
---
 xen/arch/riscv/aplic.c             | 30 ++++++------------------------
 xen/arch/riscv/include/asm/aplic.h |  1 -
 2 files changed, 6 insertions(+), 25 deletions(-)

diff --git a/xen/arch/riscv/aplic.c b/xen/arch/riscv/aplic.c
index 66ba4986a9ff..319a954f6f3c 100644
--- a/xen/arch/riscv/aplic.c
+++ b/xen/arch/riscv/aplic.c
@@ -325,9 +325,7 @@ static unsigned int aplic_get_cpu_from_mask(const cpumask_t *cpumask)
 static void cf_check aplic_set_irq_affinity(struct irq_desc *desc, const cpumask_t *mask)
 {
     unsigned int cpu;
-    uint64_t group_index, base_ppn;
-    uint32_t hhxw, lhxw, hhxs, value;
-    const struct imsic_config *imsic = aplic.imsic_cfg;
+    uint32_t value;
 
     /*
      * TODO: Currently, APLIC is supported only with MSI interrupts.
@@ -340,27 +338,11 @@ static void cf_check aplic_set_irq_affinity(struct irq_desc *desc, const cpumask
 
     ASSERT(spin_is_locked(&desc->lock));
 
-    cpu = cpuid_to_hartid(aplic_get_cpu_from_mask(mask));
-    hhxw = imsic->group_index_bits;
-    lhxw = imsic->hart_index_bits;
-    /*
-     * Although this variable is used only once in the calculation of
-     * group_index, and it might seem that hhxs could be defined as:
-     *   hhxs = imsic->group_index_shift - IMSIC_MMIO_PAGE_SHIFT;
-     * and then the addition of IMSIC_MMIO_PAGE_SHIFT could be omitted
-     * when calculating the group index.
-     * It was done intentionally this way to follow the formula from
-     * the AIA specification for calculating the MSI address.
-     */
-    hhxs = imsic->group_index_shift - IMSIC_MMIO_PAGE_SHIFT * 2;
-    base_ppn = imsic->msi[cpu].base_addr >> IMSIC_MMIO_PAGE_SHIFT;
-
-    /* Update hart and EEID in the target register */
-    group_index = (base_ppn >> (hhxs + IMSIC_MMIO_PAGE_SHIFT)) &
-                  (BIT(hhxw, UL) - 1);
-    value = desc->irq;
-    value |= cpu << APLIC_TARGET_HART_IDX_SHIFT;
-    value |= group_index << (lhxw + APLIC_TARGET_HART_IDX_SHIFT);
+    cpu = aplic_get_cpu_from_mask(mask);
+
+    /* Update hart index and EIID in the target register */
+    value = MASK_INSR(aplic_hart_field(cpu), APLIC_TARGET_HART_IDX) |
+            (desc->irq & APLIC_TARGET_EIID);
 
     spin_lock(&aplic.lock);
 
diff --git a/xen/arch/riscv/include/asm/aplic.h b/xen/arch/riscv/include/asm/aplic.h
index babba386071f..d629e1c83887 100644
--- a/xen/arch/riscv/include/asm/aplic.h
+++ b/xen/arch/riscv/include/asm/aplic.h
@@ -92,7 +92,6 @@
 #define APLIC_TARGET_BASE               0x3004
 #define APLIC_TARGET_LAST               0x3ffc
 #define  APLIC_TARGET_HART_IDX          GENMASK(31, 18)
-#define  APLIC_TARGET_HART_IDX_SHIFT    18
 #define  APLIC_TARGET_GUEST_IDX         GENMASK(17, 12)
 /* Bit 11 is reserved and reads as zero */
 #define  APLIC_TARGET_EIID              GENMASK(10, 0)
-- 
2.55.0



^ permalink raw reply related	[flat|nested] 244+ messages in thread

* [PATCH v2 11/39] xen/riscv: add helper to check APLIC MSI mode
  2026-08-27 15:20 [PATCH v2 00/39] [RISC-V] virtual interrupt controller (vAPLIC/vIMSIC) support Oleksii Kurochko
                   ` (9 preceding siblings ...)
  2026-08-27 15:20 ` [PATCH v2 10/39] xen/riscv: build the target hart index via aplic_hart_field() Oleksii Kurochko
@ 2026-08-27 15:20 ` Oleksii Kurochko
  2026-09-04  8:26   ` Baptiste Le Duc
  2026-08-27 15:20 ` [PATCH v2 12/39] xen/riscv: implement vCPU context switching Oleksii Kurochko
                   ` (27 subsequent siblings)
  38 siblings, 1 reply; 244+ messages in thread
From: Oleksii Kurochko @ 2026-08-27 15:20 UTC (permalink / raw)
  To: xen-devel
  Cc: Romain Caritey, Baptiste Le Duc, Zheng Zhang, Oleksii Kurochko,
	Alistair Francis, Connor Davis, Andrew Cooper, Anthony PERARD,
	Michal Orzel, Jan Beulich, Julien Grall, Roger Pau Monné,
	Stefano Stabellini

Use convient helper instead of open-coding the things.

Signed-off-by: Oleksii Kurochko <oleksii.kurochko@gmail.com>
---
Changes in v2:
 - Rename helper.
 - Update the commit message.
---
---
 xen/arch/riscv/aplic.c | 9 +++++++--
 1 file changed, 7 insertions(+), 2 deletions(-)

diff --git a/xen/arch/riscv/aplic.c b/xen/arch/riscv/aplic.c
index 319a954f6f3c..b4c419755ac3 100644
--- a/xen/arch/riscv/aplic.c
+++ b/xen/arch/riscv/aplic.c
@@ -37,6 +37,11 @@ static struct intc_info __ro_after_init aplic_info = {
     .hw_variant = INTC_APLIC,
 };
 
+static bool aplic_msi_mode(void)
+{
+    return readl(&aplic.regs->domaincfg) & APLIC_DOMAINCFG_DM;
+}
+
 /*
  * The arrangement of IMSIC interrupt files in MMIO space follows a topology
  * defined by the RISC-V AIA specification. An IMSIC group is a set of
@@ -250,7 +255,7 @@ static void cf_check aplic_irq_enable(struct irq_desc *desc)
      *       If APLIC without MSI interrupts is required in the future,
      *       this function will need to be updated accordingly.
      */
-    ASSERT(readl(&aplic.regs->domaincfg) & APLIC_DOMAINCFG_DM);
+    ASSERT(aplic_msi_mode());
 
     ASSERT(spin_is_locked(&desc->lock));
 
@@ -281,7 +286,7 @@ static void cf_check aplic_irq_disable(struct irq_desc *desc)
      *       If APLIC without MSI interrupts is required in the future,
      *       this function will need to be updated accordingly.
      */
-    ASSERT(readl(&aplic.regs->domaincfg) & APLIC_DOMAINCFG_DM);
+    ASSERT(aplic_msi_mode());
 
     ASSERT(spin_is_locked(&desc->lock));
 
-- 
2.55.0



^ permalink raw reply related	[flat|nested] 244+ messages in thread

* [PATCH v2 12/39] xen/riscv: implement vCPU context switching
  2026-08-27 15:20 [PATCH v2 00/39] [RISC-V] virtual interrupt controller (vAPLIC/vIMSIC) support Oleksii Kurochko
                   ` (10 preceding siblings ...)
  2026-08-27 15:20 ` [PATCH v2 11/39] xen/riscv: add helper to check APLIC MSI mode Oleksii Kurochko
@ 2026-08-27 15:20 ` Oleksii Kurochko
  2026-09-02 14:42   ` Oleksii Kurochko
                     ` (3 more replies)
  2026-08-27 15:20 ` [PATCH v2 13/39] xen/riscv: save and restore AIA state on vCPU context switch Oleksii Kurochko
                   ` (26 subsequent siblings)
  38 siblings, 4 replies; 244+ messages in thread
From: Oleksii Kurochko @ 2026-08-27 15:20 UTC (permalink / raw)
  To: xen-devel
  Cc: Romain Caritey, Baptiste Le Duc, Zheng Zhang, Oleksii Kurochko,
	Alistair Francis, Connor Davis, Andrew Cooper, Anthony PERARD,
	Michal Orzel, Jan Beulich, Julien Grall, Roger Pau Monné,
	Stefano Stabellini

Implement context_switch() and the helpers it needs: save/restore of
H/VS CSRs, virtual timer and P2M context, and __context_switch() in assembly,
which switches Xen's own callee-saved state (and thereby the stack) from
prev to next. Virtual interrupt controller context switch will be
introduced later.

Add offsets of struct arch_vcpu's xen_saved_context to asm-offsets.c for
use by __context_switch().

henvcfg and htimedelta are 64-bit on both RV32 and RV64, so store them as
uint64_t and use csr_{read,write}64() instead of open-coding accesses to
the high halves.

A hart which drops out of a domain's dirty_cpumask stops being a target
of p2m_tlb_flush() while its TLB may still hold G-stage translations of
that domain, and neither the vCPU which just ran nor any other vCPU of
that domain which ran there earlier has had its VMID invalidated. Move
the hart to a new VMID generation at that point: a VMID number is never
re-used until a full local flush has happened, hence none of those
translations can be reached again.

Claim the VMID in p2m_ctxt_switch_to() rather than at the next guest
entry. VMIDs are a per-hart resource, so the (generation, vmid) pair a
migrating vCPU brings from another hart is meaningless here and may even
match this hart's current generation, leaving the vCPU under a VMID owned
by another domain. ctxt_switch_to() invalidates that pair, but claiming a
replacement only on guest entry is too late: p2m_ctxt_switch_to() has by
then already made HGATP live, and speculation can populate G-stage entries
of the incoming domain under the stale VMID. The local flush for a wrapped
generation moves along with the claim.

That leaves p2m_handle_vmenter() with nothing to do, so drop it together
with its call from check_for_pcpu_work(). A VMID can only be invalidated
while its vCPU isn't running: vmid_flush_vcpu() is called for the vCPU
being switched in, and vmid_flush_hart() runs either from schedule_tail(),
ahead of ctxt_switch_to(), or from the wrap path of vmid_handle_vmenter()
itself. A P2M change on another hart doesn't invalidate it either, as
p2m_tlb_flush() drops the stale entries directly with
sbi_remote_hfence_gvma() instead of retiring the VMIDs which tag them. A
guest therefore always runs under the VMID claimed on its way in, and
there is nothing left for a guest entry hook to notice.

p2m_handle_vmenter() also skipped the HGATP write when the VMID it claimed
was unchanged. That isn't carried over: HGATP holds the G-stage root as
well, and skipping the write is only correct where that root is already
the incoming domain's. On the guest entry path it is, on the context
switch path it is not.

While at it, fix the inclusion order of headers in asm-offsets.c: Xen's
headers go first, then arch specific ones.

Signed-off-by: Oleksii Kurochko <oleksii.kurochko@gmail.com>
---
Changes in v2:
 - New patch.
---
---
 xen/arch/riscv/domain.c              | 167 +++++++++++++++++++++++++++
 xen/arch/riscv/entry.S               |  44 +++++++
 xen/arch/riscv/include/asm/domain.h  |  16 ++-
 xen/arch/riscv/include/asm/p2m.h     |   1 -
 xen/arch/riscv/include/asm/system.h  |   4 +
 xen/arch/riscv/p2m.c                 |  57 ++-------
 xen/arch/riscv/riscv64/asm-offsets.c |  19 ++-
 xen/arch/riscv/stubs.c               |   5 -
 xen/arch/riscv/traps.c               |   2 -
 9 files changed, 258 insertions(+), 57 deletions(-)

diff --git a/xen/arch/riscv/domain.c b/xen/arch/riscv/domain.c
index ec327a5e8a23..91a46d630f44 100644
--- a/xen/arch/riscv/domain.c
+++ b/xen/arch/riscv/domain.c
@@ -11,9 +11,11 @@
 #include <asm/bitops.h>
 #include <asm/cpufeature.h>
 #include <asm/csr.h>
+#include <asm/current.h>
 #include <asm/intc.h>
 #include <asm/mmio.h>
 #include <asm/riscv_encoding.h>
+#include <asm/vmid.h>
 #include <asm/vtimer.h>
 
 struct csr_masks {
@@ -158,6 +160,8 @@ int arch_vcpu_create(struct vcpu *v)
     if ( is_idle_vcpu(v) )
         return 0;
 
+    v->arch.last_cpu = NR_CPUS;
+
     vcpu_csr_init(v);
 
     if ( (rc = vcpu_vtimer_init(v)) )
@@ -329,6 +333,169 @@ int arch_domain_create(struct domain *d,
     return rc;
 }
 
+static void save_csr_regs(struct vcpu *vcpu)
+{
+    /*
+     * There is no need to save these CSRs as only hypervisor writes them in
+     * restore_csr_regs() and guest can't access them so they shouldn't be
+     * stored here. Keep them commented here just for symmetry with the
+     * restore CSRs register part.
+     *
+     * vcpu->arch.hedeleg = csr_read(CSR_HEDELEG);
+     * vcpu->arch.hideleg = csr_read(CSR_HIDELEG);
+     * vcpu->arch.henvcfg = csr_read64(CSR_HENVCFG);
+     * vcpu->arch.hcounteren = csr_read(CSR_HCOUNTEREN);
+     * vcpu->arch.htimedelta = csr_read64(CSR_HTIMEDELTA);
+     *
+     * if ( riscv_isa_extension_available(NULL, RISCV_ISA_EXT_smstateen) )
+     *     vcpu->arch.hstateen0 = csr_read(CSR_HSTATEEN0);
+     */
+
+    vcpu->arch.hvip = csr_read(CSR_HVIP);
+
+    vcpu->arch.vsstatus = csr_read(CSR_VSSTATUS);
+    vcpu->arch.vsie = csr_read(CSR_VSIE);
+    vcpu->arch.vstvec = csr_read(CSR_VSTVEC);
+    vcpu->arch.vsscratch = csr_read(CSR_VSSCRATCH);
+    vcpu->arch.vscause = csr_read(CSR_VSCAUSE);
+    vcpu->arch.vstval = csr_read(CSR_VSTVAL);
+    vcpu->arch.vsepc = csr_read(CSR_VSEPC);
+}
+
+static void restore_csr_regs(struct vcpu *vcpu)
+{
+    csr_write(CSR_HEDELEG, vcpu->arch.hedeleg);
+    csr_write(CSR_HIDELEG, vcpu->arch.hideleg);
+    csr_write(CSR_HVIP, vcpu->arch.hvip);
+    csr_write64(CSR_HENVCFG, vcpu->arch.henvcfg);
+    csr_write(CSR_HCOUNTEREN, vcpu->arch.hcounteren);
+    csr_write64(CSR_HTIMEDELTA, vcpu->arch.htimedelta);
+
+    if ( riscv_isa_extension_available(NULL, RISCV_ISA_EXT_smstateen) )
+        csr_write(CSR_HSTATEEN0, vcpu->arch.hstateen0);
+
+    csr_write(CSR_VSSTATUS, vcpu->arch.vsstatus);
+    csr_write(CSR_VSIE, vcpu->arch.vsie);
+    csr_write(CSR_VSTVEC, vcpu->arch.vstvec);
+    csr_write(CSR_VSSCRATCH, vcpu->arch.vsscratch);
+    csr_write(CSR_VSCAUSE, vcpu->arch.vscause);
+    csr_write(CSR_VSTVAL, vcpu->arch.vstval);
+    csr_write(CSR_VSEPC, vcpu->arch.vsepc);
+}
+
+static void ctxt_switch_from(struct vcpu *p)
+{
+    /*
+     * When the idle VCPU is running, Xen will always stay in hypervisor
+     * mode.
+     * Therefore we don't need to save the context of an idle VCPU.
+     */
+    if ( is_idle_vcpu(p) )
+        return;
+
+    p2m_ctxt_switch_from(p);
+
+    vtimer_ctxt_switch_from(p);
+
+    save_csr_regs(p);
+}
+
+static void ctxt_switch_to(struct vcpu *n)
+{
+    /*
+     * When the idle VCPU is running, Xen will always stay in hypervisor
+     * mode.
+     * Therefore we don't need to restore the context of an idle VCPU.
+     */
+    if ( is_idle_vcpu(n) )
+        return;
+
+    /*
+     * If this vCPU last ran on a different pCPU, invalidate its VMID so
+     * vmid_handle_vmenter() assigns a fresh one from the current pCPU's pool.
+     * Without this, two pCPUs could independently assign the same
+     * (generation, vmid) pair, generation counters start at the same value
+     * on all pCPUs and increment independently, causing TLB contamination.
+     */
+    if ( n->arch.last_cpu != smp_processor_id() )
+        vmid_flush_vcpu(n);
+
+    vtimer_ctxt_switch_to(n);
+
+    restore_csr_regs(n);
+
+    p2m_ctxt_switch_to(n);
+}
+
+static void schedule_tail(struct vcpu *prev)
+{
+    unsigned int cpu = smp_processor_id();
+
+    ASSERT(prev != current);
+
+    ctxt_switch_from(prev);
+
+    /*
+     * Mark this CPU in next domain's dirty cpumasks before calling
+     * ctxt_switch_to(). This avoids a race on things like p2m flushing,
+     * which is synchronised on that function.
+     */
+    if ( prev->domain != current->domain )
+    {
+        cpumask_set_cpu(cpu, current->domain->dirty_cpumask);
+
+        /*
+         * Once this hart drops out of prev's dirty_cpumask it stops being a
+         * target of p2m_tlb_flush(), while its TLB may still hold G-stage
+         * translations of prev's domain: neither the vCPU which just ran nor
+         * any other vCPU of that domain which ran here earlier has had its
+         * VMID invalidated. Move the hart to a new VMID generation so that
+         * none of them can be reached again.
+         *
+         * Switching away from the idle vCPU needs no bump: the idle domain
+         * has no p2m of its own, and whatever G-stage entries this hart may
+         * still hold (or speculatively create while HGATP keeps pointing at
+         * the last guest's p2m) are tagged with a VMID which was already made
+         * stale when that guest was switched out. Skipping the bump here also
+         * avoids burning a generation on every pass through idle.
+         */
+        if ( !is_idle_vcpu(prev) )
+            vmid_flush_hart();
+
+        cpumask_clear_cpu(cpu, prev->domain->dirty_cpumask);
+    }
+    write_atomic(&current->dirty_cpu, cpu);
+
+    ctxt_switch_to(current);
+
+    write_atomic(&prev->dirty_cpu, VCPU_CPU_CLEAN);
+
+    current->arch.last_cpu = cpu;
+
+    /*
+     * sched_context_switched() internally uses a spinlock,
+     * which requires interrupts to be enabled.
+     */
+    local_irq_enable();
+
+    sched_context_switched(prev, current);
+}
+
+void context_switch(struct vcpu *prev, struct vcpu *next)
+{
+    ASSERT(local_irq_is_enabled());
+    ASSERT(prev != next);
+    ASSERT(!vcpu_cpu_dirty(next));
+
+    local_irq_disable();
+
+    set_current(next);
+
+    prev = __context_switch(prev, next);
+
+    schedule_tail(prev);
+}
+
 static void __init __maybe_unused build_assertions(void)
 {
     /*
diff --git a/xen/arch/riscv/entry.S b/xen/arch/riscv/entry.S
index 202a35fb03a8..331446a238d3 100644
--- a/xen/arch/riscv/entry.S
+++ b/xen/arch/riscv/entry.S
@@ -99,3 +99,47 @@ restore_registers:
 
         sret
 END(handle_trap)
+
+/*
+ * struct vcpu *__context_switch(struct vcpu *prev, struct vcpu *next)
+ *
+ * This is called on prev's stack, and returns on next's.
+ *
+ * a0 - prev
+ * a1 - next
+ *
+ * Returns prev in a0
+ */
+FUNC(__context_switch)
+        REG_S   s0, VCPU_XEN_SAVED_CONTEXT_S0(a0)
+        REG_S   s1, VCPU_XEN_SAVED_CONTEXT_S1(a0)
+        REG_S   s2, VCPU_XEN_SAVED_CONTEXT_S2(a0)
+        REG_S   s3, VCPU_XEN_SAVED_CONTEXT_S3(a0)
+        REG_S   s4, VCPU_XEN_SAVED_CONTEXT_S4(a0)
+        REG_S   s5, VCPU_XEN_SAVED_CONTEXT_S5(a0)
+        REG_S   s6, VCPU_XEN_SAVED_CONTEXT_S6(a0)
+        REG_S   s7, VCPU_XEN_SAVED_CONTEXT_S7(a0)
+        REG_S   s8, VCPU_XEN_SAVED_CONTEXT_S8(a0)
+        REG_S   s9, VCPU_XEN_SAVED_CONTEXT_S9(a0)
+        REG_S   s10, VCPU_XEN_SAVED_CONTEXT_S10(a0)
+        REG_S   s11, VCPU_XEN_SAVED_CONTEXT_S11(a0)
+        REG_S   sp, VCPU_XEN_SAVED_CONTEXT_SP(a0)
+        REG_S   ra, VCPU_XEN_SAVED_CONTEXT_RA(a0)
+
+        REG_L   s0, VCPU_XEN_SAVED_CONTEXT_S0(a1)
+        REG_L   s1, VCPU_XEN_SAVED_CONTEXT_S1(a1)
+        REG_L   s2, VCPU_XEN_SAVED_CONTEXT_S2(a1)
+        REG_L   s3, VCPU_XEN_SAVED_CONTEXT_S3(a1)
+        REG_L   s4, VCPU_XEN_SAVED_CONTEXT_S4(a1)
+        REG_L   s5, VCPU_XEN_SAVED_CONTEXT_S5(a1)
+        REG_L   s6, VCPU_XEN_SAVED_CONTEXT_S6(a1)
+        REG_L   s7, VCPU_XEN_SAVED_CONTEXT_S7(a1)
+        REG_L   s8, VCPU_XEN_SAVED_CONTEXT_S8(a1)
+        REG_L   s9, VCPU_XEN_SAVED_CONTEXT_S9(a1)
+        REG_L   s10, VCPU_XEN_SAVED_CONTEXT_S10(a1)
+        REG_L   s11, VCPU_XEN_SAVED_CONTEXT_S11(a1)
+        REG_L   sp, VCPU_XEN_SAVED_CONTEXT_SP(a1)
+        REG_L   ra, VCPU_XEN_SAVED_CONTEXT_RA(a1)
+
+        ret
+END(__context_switch)
diff --git a/xen/arch/riscv/include/asm/domain.h b/xen/arch/riscv/include/asm/domain.h
index 15e8fa19685e..90ed584bb844 100644
--- a/xen/arch/riscv/include/asm/domain.h
+++ b/xen/arch/riscv/include/asm/domain.h
@@ -29,6 +29,12 @@ struct arch_vcpu_io {
 struct arch_vcpu {
     struct vcpu_vmid vmid;
 
+    /*
+     * The last CPU this vCPU ran on. Initialised to NR_CPUS
+     * (never ran).
+     */
+    unsigned int last_cpu;
+
     /*
      * Callee saved registers for Xen's state used to switch from
      * prev's stack to the next's stack during context switch.
@@ -60,11 +66,19 @@ struct arch_vcpu {
     register_t hcounteren;
     register_t hedeleg;
     register_t hideleg;
-    register_t henvcfg;
+    uint64_t   henvcfg;
     register_t hstateen0;
+    uint64_t   htimedelta;
     register_t hvip;
 
     register_t vsatp;
+    register_t vscause;
+    register_t vsepc;
+    register_t vsie;
+    register_t vsscratch;
+    register_t vsstatus;
+    register_t vstval;
+    register_t vstvec;
 
     /*
      * VCPU interrupts
diff --git a/xen/arch/riscv/include/asm/p2m.h b/xen/arch/riscv/include/asm/p2m.h
index 0d1dace1a0d8..9edf78377ee5 100644
--- a/xen/arch/riscv/include/asm/p2m.h
+++ b/xen/arch/riscv/include/asm/p2m.h
@@ -262,7 +262,6 @@ struct page_info *p2m_get_page_from_gfn(struct p2m_domain *p2m, gfn_t gfn,
 
 void p2m_ctxt_switch_from(struct vcpu *p);
 void p2m_ctxt_switch_to(struct vcpu *n);
-void p2m_handle_vmenter(void);
 
 #endif /* ASM__RISCV__P2M_H */
 
diff --git a/xen/arch/riscv/include/asm/system.h b/xen/arch/riscv/include/asm/system.h
index f33af64fd2ec..f5f30a9c8059 100644
--- a/xen/arch/riscv/include/asm/system.h
+++ b/xen/arch/riscv/include/asm/system.h
@@ -76,6 +76,10 @@ static inline bool local_irq_is_enabled(void)
 
 #define arch_fetch_and_add(x, v) __sync_fetch_and_add(x, v)
 
+struct vcpu;
+
+struct vcpu *__context_switch(struct vcpu *prev, struct vcpu *next);
+
 #endif /* __ASSEMBLER__ */
 
 #endif /* ASM__RISCV__SYSTEM_H */
diff --git a/xen/arch/riscv/p2m.c b/xen/arch/riscv/p2m.c
index de25607247a6..1f7a6907525d 100644
--- a/xen/arch/riscv/p2m.c
+++ b/xen/arch/riscv/p2m.c
@@ -1504,13 +1504,12 @@ void p2m_ctxt_switch_from(struct vcpu *p)
      * VMID, world-switch code should zero vsatp, then swap hgatp, then
      * finally write the new vsatp value what will be done in
      * p2m_ctxt_switch_to().
-     * Note, that also HGATP update could happen in p2m_handle_vmenter().
      */
     p->arch.vsatp = csr_swap(CSR_VSATP, 0);
 
     /*
-     * Nothing to do with HGATP as it will be update in p2m_ctxt_switch_to()
-     * or/and in p2m_handle_vmenter().
+     * Nothing to do with HGATP as it will be updated in
+     * p2m_ctxt_switch_to().
      */
 }
 
@@ -1524,15 +1523,21 @@ void p2m_ctxt_switch_from(struct vcpu *p)
 void p2m_ctxt_switch_to(struct vcpu *n)
 {
     struct p2m_domain *p2m = p2m_get_hostp2m(n->domain);
+    bool need_flush;
 
     if ( is_idle_vcpu(n) )
         return;
 
+    need_flush = vmid_handle_vmenter(&n->arch.vmid);
+
     csr_write(CSR_HGATP, construct_hgatp(p2m, n->arch.vmid.vmid));
+
     /*
-     * As VMID is unique per vCPU and just re-used here thereby there is no
-     * need for G-stage TLB flush here.
+     * A VMID isn't re-used until the generation it was issued in wraps, so
+     * a G-stage flush is needed only when vmid_handle_vmenter() says so.
      */
+    if ( unlikely(need_flush) )
+        local_hfence_gvma_all();
 
     csr_write(CSR_VSATP, n->arch.vsatp);
 
@@ -1548,48 +1553,6 @@ void p2m_ctxt_switch_to(struct vcpu *n)
     flush_tlb_guest_local();
 }
 
-void p2m_handle_vmenter(void)
-{
-    struct vcpu *curr = current;
-    struct p2m_domain *p2m = p2m_get_hostp2m(curr->domain);
-    struct vcpu_vmid *p_vmid = &curr->arch.vmid;
-    unsigned short old_vmid, new_vmid;
-    bool need_flush;
-
-    BUG_ON(is_idle_vcpu(curr));
-
-    old_vmid = p_vmid->vmid;
-    need_flush = vmid_handle_vmenter(p_vmid);
-    new_vmid = p_vmid->vmid;
-
-#ifdef P2M_DEBUG
-    printk("%pv: oldvmid(%d) new_vmid(%d), need_flush(%d)\n",
-           curr, old_vmid, new_vmid, need_flush);
-#endif
-
-    /*
-     * There is no need to set VSATP to 0 to stop speculation before updating
-     * HGATP, as VSATP is not modified here.
-     */
-    if ( old_vmid != new_vmid )
-        csr_write(CSR_HGATP, construct_hgatp(p2m, p_vmid->vmid));
-
-    /*
-     * There is also no need to flush G-stage TLB unconditionally as old VMID
-     * won't be reused until need_flush is set to true.
-     */
-    if ( unlikely(need_flush) )
-        local_hfence_gvma_all();
-
-    /*
-     * There is also no need to flush the VS-stage TLB: even if speculation
-     * occurs (VSATP + old HGATP were used), it will use the old VMID, which
-     * won't be reused until need_flush is set to true. When VMIDs aren't
-     * available there is no old VMID to rely on, but then need_flush is set
-     * on every entry, so the flush above covers that case.
-     */
-}
-
 struct page_info *get_page_from_gfn(struct domain *d, unsigned long gfn,
                                     p2m_type_t *t, p2m_query_t q)
 {
diff --git a/xen/arch/riscv/riscv64/asm-offsets.c b/xen/arch/riscv/riscv64/asm-offsets.c
index 1290b9dbbe82..c1be1614ce94 100644
--- a/xen/arch/riscv/riscv64/asm-offsets.c
+++ b/xen/arch/riscv/riscv64/asm-offsets.c
@@ -1,8 +1,10 @@
 #define COMPILE_OFFSETS
 
+#include <xen/sched.h>
+#include <xen/types.h>
+
 #include <asm/current.h>
 #include <asm/processor.h>
-#include <xen/types.h>
 
 #define DEFINE(_sym, _val)                                                 \
     asm volatile ( "\n.ascii\"==>#define " #_sym " %0 /* " #_val " */<==\""\
@@ -53,4 +55,19 @@ void asm_offsets(void)
     BLANK();
     DEFINE(PCPU_INFO_SIZE, sizeof(struct pcpu_info));
     BLANK();
+    OFFSET(VCPU_XEN_SAVED_CONTEXT_S0, struct vcpu, arch.xen_saved_context.s0);
+    OFFSET(VCPU_XEN_SAVED_CONTEXT_S1, struct vcpu, arch.xen_saved_context.s1);
+    OFFSET(VCPU_XEN_SAVED_CONTEXT_S2, struct vcpu, arch.xen_saved_context.s2);
+    OFFSET(VCPU_XEN_SAVED_CONTEXT_S3, struct vcpu, arch.xen_saved_context.s3);
+    OFFSET(VCPU_XEN_SAVED_CONTEXT_S4, struct vcpu, arch.xen_saved_context.s4);
+    OFFSET(VCPU_XEN_SAVED_CONTEXT_S5, struct vcpu, arch.xen_saved_context.s5);
+    OFFSET(VCPU_XEN_SAVED_CONTEXT_S6, struct vcpu, arch.xen_saved_context.s6);
+    OFFSET(VCPU_XEN_SAVED_CONTEXT_S7, struct vcpu, arch.xen_saved_context.s7);
+    OFFSET(VCPU_XEN_SAVED_CONTEXT_S8, struct vcpu, arch.xen_saved_context.s8);
+    OFFSET(VCPU_XEN_SAVED_CONTEXT_S9, struct vcpu, arch.xen_saved_context.s9);
+    OFFSET(VCPU_XEN_SAVED_CONTEXT_S10, struct vcpu, arch.xen_saved_context.s10);
+    OFFSET(VCPU_XEN_SAVED_CONTEXT_S11, struct vcpu, arch.xen_saved_context.s11);
+    OFFSET(VCPU_XEN_SAVED_CONTEXT_SP, struct vcpu, arch.xen_saved_context.sp);
+    OFFSET(VCPU_XEN_SAVED_CONTEXT_RA, struct vcpu, arch.xen_saved_context.ra);
+    BLANK();
 }
diff --git a/xen/arch/riscv/stubs.c b/xen/arch/riscv/stubs.c
index 3a7953593d93..e0febae432b2 100644
--- a/xen/arch/riscv/stubs.c
+++ b/xen/arch/riscv/stubs.c
@@ -81,11 +81,6 @@ void smp_send_state_dump(unsigned int cpu)
 
 DEFINE_PER_CPU(struct vcpu *, curr_vcpu);
 
-void context_switch(struct vcpu *prev, struct vcpu *next)
-{
-    BUG_ON("unimplemented");
-}
-
 void continue_running(struct vcpu *same)
 {
     BUG_ON("unimplemented");
diff --git a/xen/arch/riscv/traps.c b/xen/arch/riscv/traps.c
index 8530e6fbda0a..093d81e2d803 100644
--- a/xen/arch/riscv/traps.c
+++ b/xen/arch/riscv/traps.c
@@ -178,8 +178,6 @@ static void check_for_pcpu_work(void)
     vcpu_sync_interrupts(curr);
 
     vcpu_flush_interrupts(curr);
-
-    p2m_handle_vmenter();
 }
 
 static void timer_interrupt(void)
-- 
2.55.0



^ permalink raw reply related	[flat|nested] 244+ messages in thread

* [PATCH v2 13/39] xen/riscv: save and restore AIA state on vCPU context switch
  2026-08-27 15:20 [PATCH v2 00/39] [RISC-V] virtual interrupt controller (vAPLIC/vIMSIC) support Oleksii Kurochko
                   ` (11 preceding siblings ...)
  2026-08-27 15:20 ` [PATCH v2 12/39] xen/riscv: implement vCPU context switching Oleksii Kurochko
@ 2026-08-27 15:20 ` Oleksii Kurochko
  2026-09-04  9:52   ` Baptiste Le Duc
  2026-08-27 15:20 ` [PATCH v2 14/39] xen/riscv: introduce vintc_ctxt_switch_{from,to}() Oleksii Kurochko
                   ` (25 subsequent siblings)
  38 siblings, 1 reply; 244+ messages in thread
From: Oleksii Kurochko @ 2026-08-27 15:20 UTC (permalink / raw)
  To: xen-devel
  Cc: Romain Caritey, Baptiste Le Duc, Zheng Zhang, Oleksii Kurochko,
	Alistair Francis, Connor Davis, Andrew Cooper, Anthony PERARD,
	Michal Orzel, Jan Beulich, Julien Grall, Roger Pau Monné,
	Stefano Stabellini

vsiselect and hviprio{1,2} are per-hart CSRs which a guest can change, so
they have to be part of the vCPU context:
 - vsiselect is written directly by VS-mode through siselect;
 - hviprio1 and hviprio2 hold the priorities of the local interrupts which
   VS-mode reaches through the iprio array of vsiselect/vsireg, so writes
   the guest performs there land in these CSRs.

Without saving them, one vCPU's selector leaks into another vCPU's vsireg
accesses and one guest's interrupt priorities apply to the next guest which
runs on the same hart.

Whether the CSRs may be touched at all is gated by hstateen0 when Smstateen
is implemented: SVSLCT for vsiselect/vsireg and AIA for the rest of the AIA
state. A bit staying clear in v->arch.hstateen0 means M-mode denied the
access (see vcpu_csr_init()), and in that case the CSR can't be accessed
from HS-mode either, hence the gating helper.

vsie, hviprio1 and hviprio2 are 64-bit registers on both RV32 and RV64, so
store them as uint64_t and use csr_{read,write}64() rather than truncating
them to XLEN.

Signed-off-by: Oleksii Kurochko <oleksii.kurochko@gmail.com>
---
Changes in v2:
 - New  patch.
---
---
 xen/arch/riscv/domain.c             | 44 +++++++++++++++++++++++++++--
 xen/arch/riscv/include/asm/domain.h |  5 +++-
 2 files changed, 46 insertions(+), 3 deletions(-)

diff --git a/xen/arch/riscv/domain.c b/xen/arch/riscv/domain.c
index 91a46d630f44..4afdfb4d09ab 100644
--- a/xen/arch/riscv/domain.c
+++ b/xen/arch/riscv/domain.c
@@ -333,6 +333,28 @@ int arch_domain_create(struct domain *d,
     return rc;
 }
 
+/*
+ * vsiselect and hviprio{1,2} are per-hart, but the guest can change them:
+ * vsiselect directly through siselect, and hviprio{1,2} through the iprio
+ * array which vsiselect/vsireg give VS-mode access to. Hence they are part
+ * of the vCPU context.
+ *
+ * When Smstateen is implemented, hstateen0 gates that access: SVSLCT for
+ * vsiselect/vsireg and AIA for the rest of the AIA state. A bit staying clear
+ * in v->arch.hstateen0 means M-mode denied it (see vcpu_csr_init()), and then
+ * the corresponding CSR can't be accessed from HS-mode either.
+ */
+static bool vcpu_has_aia_state(const struct vcpu *v, register_t hstateen0_bit)
+{
+    if ( !riscv_isa_extension_available(NULL, RISCV_ISA_EXT_ssaia) )
+        return false;
+
+    if ( !riscv_isa_extension_available(NULL, RISCV_ISA_EXT_smstateen) )
+        return true;
+
+    return v->arch.hstateen0 & hstateen0_bit;
+}
+
 static void save_csr_regs(struct vcpu *vcpu)
 {
     /*
@@ -354,12 +376,21 @@ static void save_csr_regs(struct vcpu *vcpu)
     vcpu->arch.hvip = csr_read(CSR_HVIP);
 
     vcpu->arch.vsstatus = csr_read(CSR_VSSTATUS);
-    vcpu->arch.vsie = csr_read(CSR_VSIE);
+    vcpu->arch.vsie = csr_read64(CSR_VSIE);
     vcpu->arch.vstvec = csr_read(CSR_VSTVEC);
     vcpu->arch.vsscratch = csr_read(CSR_VSSCRATCH);
     vcpu->arch.vscause = csr_read(CSR_VSCAUSE);
     vcpu->arch.vstval = csr_read(CSR_VSTVAL);
     vcpu->arch.vsepc = csr_read(CSR_VSEPC);
+
+    if ( vcpu_has_aia_state(vcpu, SMSTATEEN0_SVSLCT) )
+        vcpu->arch.vsiselect = csr_read(CSR_VSISELECT);
+
+    if ( vcpu_has_aia_state(vcpu, SMSTATEEN0_AIA) )
+    {
+        vcpu->arch.hviprio1 = csr_read64(CSR_HVIPRIO1);
+        vcpu->arch.hviprio2 = csr_read64(CSR_HVIPRIO2);
+    }
 }
 
 static void restore_csr_regs(struct vcpu *vcpu)
@@ -375,12 +406,21 @@ static void restore_csr_regs(struct vcpu *vcpu)
         csr_write(CSR_HSTATEEN0, vcpu->arch.hstateen0);
 
     csr_write(CSR_VSSTATUS, vcpu->arch.vsstatus);
-    csr_write(CSR_VSIE, vcpu->arch.vsie);
+    csr_write64(CSR_VSIE, vcpu->arch.vsie);
     csr_write(CSR_VSTVEC, vcpu->arch.vstvec);
     csr_write(CSR_VSSCRATCH, vcpu->arch.vsscratch);
     csr_write(CSR_VSCAUSE, vcpu->arch.vscause);
     csr_write(CSR_VSTVAL, vcpu->arch.vstval);
     csr_write(CSR_VSEPC, vcpu->arch.vsepc);
+
+    if ( vcpu_has_aia_state(vcpu, SMSTATEEN0_SVSLCT) )
+        csr_write(CSR_VSISELECT, vcpu->arch.vsiselect);
+
+    if ( vcpu_has_aia_state(vcpu, SMSTATEEN0_AIA) )
+    {
+        csr_write64(CSR_HVIPRIO1, vcpu->arch.hviprio1);
+        csr_write64(CSR_HVIPRIO2, vcpu->arch.hviprio2);
+    }
 }
 
 static void ctxt_switch_from(struct vcpu *p)
diff --git a/xen/arch/riscv/include/asm/domain.h b/xen/arch/riscv/include/asm/domain.h
index 90ed584bb844..23e301782068 100644
--- a/xen/arch/riscv/include/asm/domain.h
+++ b/xen/arch/riscv/include/asm/domain.h
@@ -70,11 +70,14 @@ struct arch_vcpu {
     register_t hstateen0;
     uint64_t   htimedelta;
     register_t hvip;
+    uint64_t   hviprio1;
+    uint64_t   hviprio2;
 
     register_t vsatp;
     register_t vscause;
     register_t vsepc;
-    register_t vsie;
+    uint64_t   vsie;
+    register_t vsiselect;
     register_t vsscratch;
     register_t vsstatus;
     register_t vstval;
-- 
2.55.0



^ permalink raw reply related	[flat|nested] 244+ messages in thread

* [PATCH v2 14/39] xen/riscv: introduce vintc_ctxt_switch_{from,to}()
  2026-08-27 15:20 [PATCH v2 00/39] [RISC-V] virtual interrupt controller (vAPLIC/vIMSIC) support Oleksii Kurochko
                   ` (12 preceding siblings ...)
  2026-08-27 15:20 ` [PATCH v2 13/39] xen/riscv: save and restore AIA state on vCPU context switch Oleksii Kurochko
@ 2026-08-27 15:20 ` Oleksii Kurochko
  2026-09-04 11:25   ` Baptiste Le Duc
  2026-09-10 14:54   ` Jan Beulich
  2026-08-27 15:20 ` [PATCH v2 15/39] xen/riscv: add IMSIC vCPU context switch handlers Oleksii Kurochko
                   ` (24 subsequent siblings)
  38 siblings, 2 replies; 244+ messages in thread
From: Oleksii Kurochko @ 2026-08-27 15:20 UTC (permalink / raw)
  To: xen-devel
  Cc: Romain Caritey, Baptiste Le Duc, Zheng Zhang, Oleksii Kurochko,
	Alistair Francis, Connor Davis, Andrew Cooper, Anthony PERARD,
	Michal Orzel, Jan Beulich, Julien Grall, Roger Pau Monné,
	Stefano Stabellini

Virtual interrupt controller state must be preserved across vCPU context
switches.

Introduce vintc_ctxt_switch_{from,to}() wrappers around new
ctxt_switch_{from,to}() hooks in struct vintc_ops, and call them from the
context switch path, so that this state can be saved/restored without
knowing which vINTC variant a domain uses.

No vINTC variant implements the hooks yet: the vAPLIC implementation is
added separately.

Signed-off-by: Oleksii Kurochko <oleksii.kurochko@gmail.com>
---
Changes in v2:
 - Update the commit message.
 - s/vintc_state_{save,restore}/vintc_ctxt_switch_{to,from}.
 - s/{re}store_state/ctxt_switch_{to,from} for vintc_ops.
 - s/vcpu/v for vintc_ctxt_switch_{to,from}() arguments.
---
---
 xen/arch/riscv/domain.c           |  4 ++++
 xen/arch/riscv/include/asm/intc.h |  9 +++++++++
 xen/arch/riscv/intc.c             | 14 ++++++++++++++
 3 files changed, 27 insertions(+)

diff --git a/xen/arch/riscv/domain.c b/xen/arch/riscv/domain.c
index 4afdfb4d09ab..2dfe4c2e72ce 100644
--- a/xen/arch/riscv/domain.c
+++ b/xen/arch/riscv/domain.c
@@ -437,6 +437,8 @@ static void ctxt_switch_from(struct vcpu *p)
 
     vtimer_ctxt_switch_from(p);
 
+    vintc_ctxt_switch_from(p);
+
     save_csr_regs(p);
 }
 
@@ -462,6 +464,8 @@ static void ctxt_switch_to(struct vcpu *n)
 
     vtimer_ctxt_switch_to(n);
 
+    vintc_ctxt_switch_to(n);
+
     restore_csr_regs(n);
 
     p2m_ctxt_switch_to(n);
diff --git a/xen/arch/riscv/include/asm/intc.h b/xen/arch/riscv/include/asm/intc.h
index 1bfba7c6155b..62e1410156c7 100644
--- a/xen/arch/riscv/include/asm/intc.h
+++ b/xen/arch/riscv/include/asm/intc.h
@@ -64,6 +64,12 @@ struct vintc_ops {
 
     /* Deinitialize some vINTC-related stuff for a vCPU */
     void (*vcpu_deinit)(struct vcpu *v);
+
+    /* Save vINTC state of the vCPU being switched out */
+    void (*ctxt_switch_from)(struct vcpu *v);
+
+    /* Restore vINTC state of the vCPU being switched in */
+    void (*ctxt_switch_to)(struct vcpu *v);
 };
 
 struct vintc {
@@ -91,4 +97,7 @@ void domain_vintc_deinit(struct domain *d);
 
 int vintc_reserve_virq(const struct domain *d, unsigned int virq);
 
+void vintc_ctxt_switch_from(struct vcpu *v);
+void vintc_ctxt_switch_to(struct vcpu *v);
+
 #endif /* ASM__RISCV__INTERRUPT_CONTOLLER_H */
diff --git a/xen/arch/riscv/intc.c b/xen/arch/riscv/intc.c
index bca83b4f4fa3..9fff501b9c25 100644
--- a/xen/arch/riscv/intc.c
+++ b/xen/arch/riscv/intc.c
@@ -178,3 +178,17 @@ int __overlay_init vintc_reserve_virq(const struct domain *d,
 
     return test_and_set_bit(virq, d->arch.vintc->used_irqs) ? -EEXIST : 0;
 }
+
+void vintc_ctxt_switch_from(struct vcpu *v)
+{
+    const struct vintc_ops *ops = v->domain->arch.vintc->ops;
+
+    ops->ctxt_switch_from(v);
+}
+
+void vintc_ctxt_switch_to(struct vcpu *v)
+{
+    const struct vintc_ops *ops = v->domain->arch.vintc->ops;
+
+    ops->ctxt_switch_to(v);
+}
-- 
2.55.0



^ permalink raw reply related	[flat|nested] 244+ messages in thread

* [PATCH v2 15/39] xen/riscv: add IMSIC vCPU context switch handlers
  2026-08-27 15:20 [PATCH v2 00/39] [RISC-V] virtual interrupt controller (vAPLIC/vIMSIC) support Oleksii Kurochko
                   ` (13 preceding siblings ...)
  2026-08-27 15:20 ` [PATCH v2 14/39] xen/riscv: introduce vintc_ctxt_switch_{from,to}() Oleksii Kurochko
@ 2026-08-27 15:20 ` Oleksii Kurochko
  2026-09-04 11:33   ` Baptiste Le Duc
  2026-09-10 14:57   ` Jan Beulich
  2026-08-27 15:21 ` [PATCH v2 16/39] xen/riscv: extend exception tables with type and data fields Oleksii Kurochko
                   ` (23 subsequent siblings)
  38 siblings, 2 replies; 244+ messages in thread
From: Oleksii Kurochko @ 2026-08-27 15:20 UTC (permalink / raw)
  To: xen-devel
  Cc: Romain Caritey, Baptiste Le Duc, Zheng Zhang, Oleksii Kurochko,
	Alistair Francis, Connor Davis, Andrew Cooper, Anthony PERARD,
	Michal Orzel, Jan Beulich, Julien Grall, Roger Pau Monné,
	Stefano Stabellini

IMSIC state currently needs to track only which physical CPU owns a vCPU's
IMSIC guest interrupt file, as the CPU id is part of the physical address
the file is mapped at.

Add imsic_ctxt_switch_from() to record that CPU when a vCPU is switched
out. A vCPU running on the s/w VS-file has no h/w file bound to a CPU, so
there is nothing to record for it. The recorded value stays unused until
vCPU migration support, which needs it to find the file to move away from,
is added later.

imsic_ctxt_switch_to() has nothing to do: by the time a vCPU is switched
in, VGEIN is already assigned to it and its guest interrupt file is already
mapped. Work is only required once a vCPU can move to a different CPU,
which means recalculating VGEIN and remapping the file; that is handled
separately by the vCPU migration patches.

Install both as the ctxt_switch_{from,to} hooks of struct vintc_ops. MSI
delivery is the only mode Xen supports ( aplic_init() panics on an APLIC
without an "msi-parent" property, and a guest's domaincfg.DM reads back as
a fixed one) so the vAPLIC state to save and restore is always the IMSIC
one and no vAPLIC-level forwarder is needed. Being indirect call targets,
both handlers get cf_check.

Co-developed-by: Romain Caritey <Romain.Caritey@microchip.com>
Signed-off-by: Oleksii Kurochko <oleksii.kurochko@gmail.com>
---
Changes in v2:
 - s/imsic_state_{save,restore}/imsic_ctxt_switch_{from,to}: the old names
   suggested saving and restoring register state, which isn't what these
   functions do.
 - Fix the comment in imsic_ctxt_switch_from(): it explained the
   ->vsfile_cpu sentinel while the code checks ->guest_file_id.
 - Adapt to ->vsfile_cpu holding v->processor instead of a hartid.
 - Add cf_check as both are indirect call targets now.
 - Fold in the vintc_ops hook-up, which was a separate patch in v1. It no
   longer adds vaplic_state_{save,restore}() forwarders: the
   BUG_ON("unimplemented") path in them was unreachable and
   has_msi_support() was an MMIO read done on every context switch.
 - Drop the claim that the not-yet-supported case is guarded by a BUG_ON();
   there is no such BUG_ON().
 - Update the subject accordingly.
---
---
 xen/arch/riscv/imsic.c             | 23 +++++++++++++++++++++++
 xen/arch/riscv/include/asm/imsic.h |  3 +++
 xen/arch/riscv/vaplic.c            |  7 +++++++
 3 files changed, 33 insertions(+)

diff --git a/xen/arch/riscv/imsic.c b/xen/arch/riscv/imsic.c
index ad0a220edac2..3787f270d8e3 100644
--- a/xen/arch/riscv/imsic.c
+++ b/xen/arch/riscv/imsic.c
@@ -20,6 +20,7 @@
 #include <xen/init.h>
 #include <xen/libfdt/libfdt.h>
 #include <xen/macros.h>
+#include <xen/rwlock.h>
 #include <xen/sched.h>
 #include <xen/smp.h>
 #include <xen/spinlock.h>
@@ -342,6 +343,28 @@ static int __init imsic_parse_node(const struct dt_device_node *node,
     return 0;
 }
 
+void cf_check imsic_ctxt_switch_from(struct vcpu *v)
+{
+    struct vimsic_state *imsic_state = v->arch.vimsic_state;
+    unsigned long flags;
+
+    /*
+     * A vCPU using the s/w IMSIC VS-file (guest_file_id == 0) has no h/w
+     * VS-file bound to a physical CPU, so there is no location to record.
+     */
+    if ( !vcpu_guest_file_id(v) )
+        return;
+
+    write_lock_irqsave(&imsic_state->vsfile_lock, flags);
+    imsic_state->vsfile_cpu = v->processor;
+    write_unlock_irqrestore(&imsic_state->vsfile_lock, flags);
+}
+
+void cf_check imsic_ctxt_switch_to(struct vcpu *v)
+{
+    /* Nothing to do */
+}
+
 int cf_check vcpu_imsic_init(struct vcpu *v)
 {
     struct vimsic_state *imsic_state;
diff --git a/xen/arch/riscv/include/asm/imsic.h b/xen/arch/riscv/include/asm/imsic.h
index 93f9e44c7d2c..73129c3c9ea7 100644
--- a/xen/arch/riscv/include/asm/imsic.h
+++ b/xen/arch/riscv/include/asm/imsic.h
@@ -109,4 +109,7 @@ unsigned int vcpu_guest_file_id(const struct vcpu *v);
 
 int vimsic_make_domu_dt_node(struct kernel_info *kinfo, unsigned int *phandle);
 
+void imsic_ctxt_switch_from(struct vcpu *v);
+void imsic_ctxt_switch_to(struct vcpu *v);
+
 #endif /* ASM_RISCV_IMSIC_H */
diff --git a/xen/arch/riscv/vaplic.c b/xen/arch/riscv/vaplic.c
index 8726f7203d6e..6c60fe2baf0c 100644
--- a/xen/arch/riscv/vaplic.c
+++ b/xen/arch/riscv/vaplic.c
@@ -422,6 +422,13 @@ static const struct mmio_handler_ops vaplic_mmio_ops = {
 static const struct vintc_ops vintc_ops = {
     .vcpu_init = vcpu_imsic_init,
     .vcpu_deinit = vcpu_imsic_deinit,
+    /*
+     * MSI delivery is the only supported mode: aplic_init() panics on an
+     * APLIC without an "msi-parent", so the vAPLIC state to save and restore
+     * is always the IMSIC one.
+     */
+    .ctxt_switch_from = imsic_ctxt_switch_from,
+    .ctxt_switch_to = imsic_ctxt_switch_to,
 };
 
 int domain_vaplic_init(struct domain *d)
-- 
2.55.0



^ permalink raw reply related	[flat|nested] 244+ messages in thread

* [PATCH v2 16/39] xen/riscv: extend exception tables with type and data fields
  2026-08-27 15:20 [PATCH v2 00/39] [RISC-V] virtual interrupt controller (vAPLIC/vIMSIC) support Oleksii Kurochko
                   ` (14 preceding siblings ...)
  2026-08-27 15:20 ` [PATCH v2 15/39] xen/riscv: add IMSIC vCPU context switch handlers Oleksii Kurochko
@ 2026-08-27 15:21 ` Oleksii Kurochko
  2026-09-07 15:57   ` Baptiste Le Duc
  2026-09-08 13:44   ` Jan Beulich
  2026-08-27 15:21 ` [PATCH v2 17/39] xen/riscv: decouple INSN_PSEUDO_VS_* from the hypervisor's XLEN Oleksii Kurochko
                   ` (22 subsequent siblings)
  38 siblings, 2 replies; 244+ messages in thread
From: Oleksii Kurochko @ 2026-08-27 15:21 UTC (permalink / raw)
  To: xen-devel
  Cc: Romain Caritey, Baptiste Le Duc, Zheng Zhang, Oleksii Kurochko,
	Alistair Francis, Connor Davis, Andrew Cooper, Anthony PERARD,
	Michal Orzel, Jan Beulich, Julien Grall, Roger Pau Monné,
	Stefano Stabellini

Extend the RISC-V exception table format to include a type and
auxiliary data field.

The existing format only supports simple fixups. Some use cases require
additional context from the fault (e.g. capturing trap information),
which cannot be expressed with the current EX_TYPE_FIXUP entries.

Introduce a generic ASM_EXTABLE_RAW() helper to describe entries with a
handler type and associated data. Reimplement ASM_EXTABLE() in terms of
it using EX_TYPE_FIXUP for compatibility.

Add EX_TYPE_TRAP_INFO to allow handlers to retrieve trap state
(sepc/scause/stval) and pass it to the fixup path. The data field is
used to encode which GPR contains a pointer to a struct trap_info.

Provide ASM_EXTABLE_TRAP_INFO() as a convenience wrapper for this case.

Also add gpr-num.h, providing symbolic GPR numbers for use in assembly
and inline asm. This is derived from Linux 6.16 with minor adjustments such
as using .irp instead of open-coding the same using a set of .equ.

Update the exception handling code to dispatch based on the entry type.

Signed-off-by: Oleksii Kurochko <oleksii.kurochko@gmail.com>
---
Changes in v2:
 - regs_get_gpr(): take a GPR number instead of a byte offset, dropping the
   multiplication at the call site. A non-multiple-of-8 offset is now
   unrepresentable.
 - regs_get_gpr(): ASSERT(num < 32) instead of a range check returning 0,
   which isn't usable as an error indicator. No num == 0 check: reading x0
   is harmless out of context, and a NULL trap_info is caught by the
   existing BUG_ON() where the dereference happens.
 - regs_get_gpr(): index the register frame as an array, constify regs and
   drop the pointless inline.
 - Anchor the first BUILD_BUG_ON() at offsetof(..., zero) == 0 rather than
   at ra, pinning both ends of the x0..x31 range.
 - Drop MAX_REG_OFFSET; asm/processor.h is no longer touched by this patch.
 - ex_handler_trap_info(): use regs->sepc and the cause passed down from
   do_trap() instead of re-reading CSR_SEPC/CSR_SCAUSE; fixup_exception()
   gains a cause parameter. stval stays a CSR read as do_trap() doesn't
   read it.
 - asm/extable.h: revert the .word -> .long change, the extra parens and the
   stray semicolon after .popsection; use .half for the new type and data
   fields to match .word.
 - Make GPR_LIST() the single source of the ABI-name -> register-number
   mapping, and check struct cpu_user_regs against it at build time, so the
   two can no longer diverge.
 - Add the explanatory comment above sturc cpu_user_regs to explain an
   ordering of x0-x31 registers.
---
---
 xen/arch/riscv/extable.c               | 70 +++++++++++++++++++++++++-
 xen/arch/riscv/include/asm/extable.h   | 64 +++++++++++++++--------
 xen/arch/riscv/include/asm/gpr-num.h   | 37 ++++++++++++++
 xen/arch/riscv/include/asm/processor.h | 14 +++++-
 xen/arch/riscv/include/asm/traps.h     |  6 +++
 xen/arch/riscv/traps.c                 |  2 +-
 6 files changed, 169 insertions(+), 24 deletions(-)
 create mode 100644 xen/arch/riscv/include/asm/gpr-num.h

diff --git a/xen/arch/riscv/extable.c b/xen/arch/riscv/extable.c
index 5b89c4278c65..6470198d0117 100644
--- a/xen/arch/riscv/extable.c
+++ b/xen/arch/riscv/extable.c
@@ -6,8 +6,10 @@
 #include <xen/sort.h>
 #include <xen/virtual_region.h>
 
+#include <asm/csr.h>
 #include <asm/extable.h>
 #include <asm/processor.h>
+#include <asm/traps.h>
 
 #define EX_FIELD(ptr, field) ((unsigned long)&(ptr)->field + (ptr)->field)
 
@@ -32,6 +34,12 @@ static void __init cf_check swap_ex(void *a, void *b)
 
     x->fixup = y->fixup + delta;
     y->fixup = tmp.fixup - delta;
+
+    x->type = y->type;
+    y->type = tmp.type;
+
+    x->data = y->data;
+    y->data = tmp.data;
 }
 
 static int cf_check cmp_ex(const void *a, const void *b)
@@ -59,7 +67,49 @@ static void ex_handler_fixup(const struct exception_table_entry *ex,
     regs->sepc = ex_fixup(ex);
 }
 
-bool fixup_exception(struct cpu_user_regs *regs)
+#define CHECK_GPR_INDEX(num, name)                      \
+    BUILD_BUG_ON(offsetof(struct cpu_user_regs, name)   \
+                 != (num) * sizeof(unsigned long));
+
+static unsigned long regs_get_gpr(const struct cpu_user_regs *regs,
+                                  unsigned int num)
+{
+    /*
+     * The GPR number -> struct index mapping below relies on x0..x31 being
+     * laid out at the start of struct cpu_user_regs in architectural order,
+     * matching the register numbers GPR_LIST() hands to the assembler.
+     */
+    GPR_LIST(CHECK_GPR_INDEX)
+
+    ASSERT(num < 32);
+
+    return ((const unsigned long *)regs)[num];
+}
+
+#undef CHECK_GPR_INDEX
+
+static void ex_handler_trap_info(const struct exception_table_entry *ex,
+                                 struct cpu_user_regs *regs,
+                                 unsigned long cause)
+{
+    struct trap_info *trap_info =
+        (struct trap_info *)regs_get_gpr(regs, ex->data);
+
+    BUG_ON(!trap_info);
+
+    /*
+     * Only stval still needs a CSR read: sepc and scause were already
+     * captured by the trap entry path and do_trap() respectively. Latch
+     * trap_info->sepc before regs->sepc is pointed at the fixup code.
+     */
+    trap_info->sepc = regs->sepc;
+    trap_info->scause = cause;
+    trap_info->stval = csr_read(CSR_STVAL);
+
+    regs->sepc = ex_fixup(ex);
+}
+
+bool fixup_exception(struct cpu_user_regs *regs, unsigned long cause)
 {
     unsigned long pc = regs->sepc;
     const struct virtual_region *region = find_text_region(pc);
@@ -77,7 +127,23 @@ bool fixup_exception(struct cpu_user_regs *regs)
     if ( !ex )
         return false;
 
-    ex_handler_fixup(ex, regs);
+    switch ( ex->type )
+    {
+    case EX_TYPE_FIXUP:
+        ex_handler_fixup(ex, regs);
+        break;
+
+    case EX_TYPE_TRAP_INFO:
+        ex_handler_trap_info(ex, regs, cause);
+        break;
+
+    default:
+        printk(XENLOG_ERR
+               "Unsupported exception table entry type %u for pc %#lx\n",
+               ex->type, pc);
+
+        return false;
+    }
 
     return true;
 }
diff --git a/xen/arch/riscv/include/asm/extable.h b/xen/arch/riscv/include/asm/extable.h
index c0128a91818f..7378f86e7eea 100644
--- a/xen/arch/riscv/include/asm/extable.h
+++ b/xen/arch/riscv/include/asm/extable.h
@@ -3,17 +3,24 @@
 #ifndef ASM__RISCV__ASM_EXTABLE_H
 #define ASM__RISCV__ASM_EXTABLE_H
 
+#include <asm/gpr-num.h>
+
+#define EX_TYPE_FIXUP       0
+#define EX_TYPE_TRAP_INFO   1
+
 #ifdef __ASSEMBLER__
 
-#define ASM_EXTABLE(insn, fixup) \
-    .pushsection .ex_table, "a"; \
-    .balign     4;               \
-    .word       (insn) - .;      \
-    .word       (fixup) - .;     \
+#define ASM_EXTABLE_RAW(insn, fixup, type, data)    \
+    .pushsection .ex_table, "a";                    \
+    .balign     4;                                  \
+    .word       (insn) - .;                         \
+    .word       (fixup) - .;                        \
+    .half       (type);                             \
+    .half       (data);                             \
     .popsection
 
-.macro asm_extable, insn, fixup
-    ASM_EXTABLE(\insn, \fixup)
+.macro _asm_extable, insn, fixup
+    ASM_EXTABLE_RAW(\insn, \fixup, EX_TYPE_FIXUP, 0)
 .endm
 
 #else /* __ASSEMBLER__ */
@@ -23,20 +30,36 @@
 
 struct cpu_user_regs;
 
-#define ASM_EXTABLE(insn, fixup)      \
-    ".pushsection .ex_table, \"a\"\n" \
-    ".balign    4\n"                  \
-    ".word      (" #insn " - .)\n"    \
-    ".word      (" #fixup " - .)\n"   \
+#define ASM_EXTABLE_RAW(insn, fixup, type, data)    \
+    ".pushsection .ex_table, \"a\"\n"               \
+    ".balign    4\n"                                \
+    ".word      (" insn ") - .\n"                   \
+    ".word      (" fixup ") - .\n"                  \
+    ".half      (" type ")\n"                       \
+    ".half      (" data ")\n"                       \
     ".popsection\n"
 
+#define ASM_EXTABLE(insn, fixup)    \
+    ASM_EXTABLE_RAW(#insn, #fixup, __stringify(EX_TYPE_FIXUP), "0")
+
+#define EX_TRAP_INFO_REG(gpr)   \
+    "(.L_gpr_num_" #gpr ")"
+
+#define ASM_EXTABLE_TRAP_INFO(insn, fixup, data)                    \
+    DEFINE_ASM_GPR_NUMS                                             \
+    ASM_EXTABLE_RAW(#insn, #fixup, __stringify(EX_TYPE_TRAP_INFO),  \
+                    EX_TRAP_INFO_REG(data))
+
 /*
- * The exception table consists of pairs of relative offsets: the first
- * is the relative offset to an instruction that is allowed to fault,
- * and the second is the relative offset at which the program should
- * continue. No general-purpose registers are modified by the exception
- * handling mechanism itself, so it is up to the fixup code to handle
- * any necessary state cleanup.
+ * Each exception table entry consists of two relative offsets and a
+ * handler description: `insn` is the relative offset to an instruction
+ * that is allowed to fault, `fixup` is the relative offset at which the
+ * program should continue, `type` selects how the exception is handled
+ * (EX_TYPE_*), and `data` holds auxiliary information for the handler
+ * (e.g. for EX_TYPE_TRAP_INFO, the number of the GPR that contains a
+ * pointer to a struct trap_info). No general-purpose registers are
+ * modified by the exception handling mechanism itself, so it is up to
+ * the fixup code to handle any necessary state cleanup.
  *
  * The exception table and fixup code live out of line with the main
  * instruction path. This means when everything is well, we don't even
@@ -45,14 +68,15 @@ struct cpu_user_regs;
  */
 struct exception_table_entry {
     int32_t insn, fixup;
+    uint16_t type, data;
 };
 
 extern struct exception_table_entry __start___ex_table[];
 extern struct exception_table_entry __stop___ex_table[];
 
 void sort_exception_tables(void);
-bool fixup_exception(struct cpu_user_regs *regs);
+bool fixup_exception(struct cpu_user_regs *regs, unsigned long cause);
 
-#endif /* __ASSEMBLY__ */
+#endif /* __ASSEMBLER__ */
 
 #endif /* ASM__RISCV__ASM_EXTABLE_H */
diff --git a/xen/arch/riscv/include/asm/gpr-num.h b/xen/arch/riscv/include/asm/gpr-num.h
new file mode 100644
index 000000000000..3b97a72e6c30
--- /dev/null
+++ b/xen/arch/riscv/include/asm/gpr-num.h
@@ -0,0 +1,37 @@
+/* SPDX-License-Identifier: GPL-2.0-only */
+#ifndef RISCV_GPR_NUM_H
+#define RISCV_GPR_NUM_H
+
+/*
+ * GPRs by ABI name, together with their register number (x0 .. x31).
+ *
+ * This is the single source of truth for the mapping: it generates the
+ * .L_gpr_num_<name> assembler symbols used to turn a register name emitted
+ * by the compiler into a register number, and struct cpu_user_regs is
+ * checked against it at build time (see regs_get_gpr()). Neither list can
+ * therefore be changed without the other.
+ */
+#define GPR_LIST(x)                                 \
+    x(0,  zero) x(1,  ra)  x(2,  sp)  x(3,  gp)     \
+    x(4,  tp)   x(5,  t0)  x(6,  t1)  x(7,  t2)     \
+    x(8,  s0)   x(9,  s1)  x(10, a0)  x(11, a1)     \
+    x(12, a2)   x(13, a3)  x(14, a4)  x(15, a5)     \
+    x(16, a6)   x(17, a7)  x(18, s2)  x(19, s3)     \
+    x(20, s4)   x(21, s5)  x(22, s6)  x(23, s7)     \
+    x(24, s8)   x(25, s9)  x(26, s10) x(27, s11)    \
+    x(28, t3)   x(29, t4)  x(30, t5)  x(31, t6)
+
+#ifdef __ASSEMBLER__
+
+#define GPR_NUM_EQU(num, name)  .equ .L_gpr_num_##name, num;
+GPR_LIST(GPR_NUM_EQU)
+#undef GPR_NUM_EQU
+
+#else /* __ASSEMBLER__ */
+
+#define GPR_NUM_EQU(num, name)  ".equ .L_gpr_num_" #name ", " #num "\n"
+#define DEFINE_ASM_GPR_NUMS     GPR_LIST(GPR_NUM_EQU)
+
+#endif /* __ASSEMBLER__ */
+
+#endif /* RISCV_GPR_NUM_H */
diff --git a/xen/arch/riscv/include/asm/processor.h b/xen/arch/riscv/include/asm/processor.h
index b1745c107100..e7b0f2321a0e 100644
--- a/xen/arch/riscv/include/asm/processor.h
+++ b/xen/arch/riscv/include/asm/processor.h
@@ -12,7 +12,19 @@
 
 #ifndef __ASSEMBLER__
 
-/* On stack VCPU state */
+/*
+ * On stack VCPU state.
+ *
+ * x0..x31 must remain at the start of this structure, in architectural
+ * register-number order: code which resolves a register number to its saved
+ * value indexes this structure directly (instruction emulation via
+ * REG_PTR() from asm/riscv_encoding.h, exception table fixups via
+ * regs_get_gpr()). ->zero therefore has to stay at offset 0 and must always
+ * read as 0, since it supplies the value of x0 when x0 is used as a source
+ * operand. The layout is checked against GPR_LIST() at build time; see
+ * regs_get_gpr() in extable.c. Do not reorder these fields or insert
+ * anything between them.
+ */
 struct cpu_user_regs
 {
     unsigned long zero;
diff --git a/xen/arch/riscv/include/asm/traps.h b/xen/arch/riscv/include/asm/traps.h
index 21fa3c3259b3..8d4ab664bca9 100644
--- a/xen/arch/riscv/include/asm/traps.h
+++ b/xen/arch/riscv/include/asm/traps.h
@@ -7,6 +7,12 @@
 
 #ifndef __ASSEMBLER__
 
+struct trap_info {
+    register_t sepc;
+    register_t scause;
+    register_t stval;
+};
+
 void do_trap(struct cpu_user_regs *cpu_regs);
 void handle_trap(void);
 void trap_init(void);
diff --git a/xen/arch/riscv/traps.c b/xen/arch/riscv/traps.c
index 093d81e2d803..11a6fa1bc942 100644
--- a/xen/arch/riscv/traps.c
+++ b/xen/arch/riscv/traps.c
@@ -217,7 +217,7 @@ void do_trap(struct cpu_user_regs *cpu_regs)
             break;
         }
 
-        if ( fixup_exception(cpu_regs) )
+        if ( fixup_exception(cpu_regs, cause) )
             break;
 
         fallthrough;
-- 
2.55.0



^ permalink raw reply related	[flat|nested] 244+ messages in thread

* [PATCH v2 17/39] xen/riscv: decouple INSN_PSEUDO_VS_* from the hypervisor's XLEN
  2026-08-27 15:20 [PATCH v2 00/39] [RISC-V] virtual interrupt controller (vAPLIC/vIMSIC) support Oleksii Kurochko
                   ` (15 preceding siblings ...)
  2026-08-27 15:21 ` [PATCH v2 16/39] xen/riscv: extend exception tables with type and data fields Oleksii Kurochko
@ 2026-08-27 15:21 ` Oleksii Kurochko
  2026-09-07 15:57   ` Baptiste Le Duc
  2026-08-27 15:21 ` [PATCH v2 18/39] xen/riscv: add guest page fault handling stub Oleksii Kurochko
                   ` (21 subsequent siblings)
  38 siblings, 1 reply; 244+ messages in thread
From: Oleksii Kurochko @ 2026-08-27 15:21 UTC (permalink / raw)
  To: xen-devel
  Cc: Romain Caritey, Baptiste Le Duc, Zheng Zhang, Oleksii Kurochko,
	Alistair Francis, Connor Davis, Andrew Cooper, Anthony PERARD,
	Michal Orzel, Jan Beulich, Julien Grall, Roger Pau Monné,
	Stefano Stabellini

htinst reports a pseudoinstruction when a guest page fault is taken on an
implicit memory access done for VS-stage address translation. Four such
values are defined, differing in the access type (read or write) and in the
access width: 4 bytes (0x2000/0x2020) or 8 bytes (0x3000/0x3020).

That width is the width of a VS-stage PTE, i.e. it follows the guest's
paging mode (4 bytes for Sv32, 8 bytes for Sv39 and wider) and has nothing
to do with the XLEN Xen itself is built for. Selecting just one pair with
where a guest running with VSXL=32 and Sv32 in vsatp produces the 4-byte
forms. Such an htinst would not be recognized as a pseudoinstruction and the
fault would be mistaken for an ordinary MMIO trap: Xen would fetch and
decode whatever instruction sepc happens to point at (unrelated to the
access which faulted) and emulate it against a guest physical address
derived from htval, which for an implicit access holds the address of a
VS-stage PTE rather than of any access the guest performed.

Define all four values unconditionally instead, named after the access width
they encode rather than after the build's XLEN. On RV32 the 8-byte forms
simply never occur, so recognizing them costs nothing.

Dropping the ladder loses no build-time coverage: a build for an XLEN other
than 32 or 64 already fails on the equivalent ladders in asm/asm.h and
asm/config.h, so no replacement #error is needed here. Adding one keyed on
CONFIG_RISCV_* would in any case re-introduce exactly the conflation this
patch removes.

This diverges from the imported version of riscv_encoding.h.

No functional change: the values have no user yet.

Signed-off-by: Oleksii Kurochko <oleksii.kurochko@gmail.com>
---
Changes in v2:
 - New patch.
---
---
 xen/arch/riscv/include/asm/riscv_encoding.h | 16 ++++------------
 1 file changed, 4 insertions(+), 12 deletions(-)

diff --git a/xen/arch/riscv/include/asm/riscv_encoding.h b/xen/arch/riscv/include/asm/riscv_encoding.h
index c63e5e304691..2d2e7e11b3ef 100644
--- a/xen/arch/riscv/include/asm/riscv_encoding.h
+++ b/xen/arch/riscv/include/asm/riscv_encoding.h
@@ -839,25 +839,17 @@
 #define INSN_MASK_FENCE_TSO		0xffffffff
 #define INSN_MATCH_FENCE_TSO		0x8330000f
 
-#if __riscv_xlen == 64
-
 /* 64-bit read for VS-stage address translation (RV64) */
-#define INSN_PSEUDO_VS_LOAD		0x00003000
+#define INSN_PSEUDO_VS_LOAD64		0x00003000
 
 /* 64-bit write for VS-stage address translation (RV64) */
-#define INSN_PSEUDO_VS_STORE	0x00003020
-
-#elif __riscv_xlen == 32
+#define INSN_PSEUDO_VS_STORE64		0x00003020
 
 /* 32-bit read for VS-stage address translation (RV32) */
-#define INSN_PSEUDO_VS_LOAD		0x00002000
+#define INSN_PSEUDO_VS_LOAD32		0x00002000
 
 /* 32-bit write for VS-stage address translation (RV32) */
-#define INSN_PSEUDO_VS_STORE	0x00002020
-
-#else
-#error "Unexpected __riscv_xlen"
-#endif
+#define INSN_PSEUDO_VS_STORE32		0x00002020
 
 #define INSN_16BIT_MASK			0x3
 #define INSN_32BIT_MASK			0x1c
-- 
2.55.0



^ permalink raw reply related	[flat|nested] 244+ messages in thread

* [PATCH v2 18/39] xen/riscv: add guest page fault handling stub
  2026-08-27 15:20 [PATCH v2 00/39] [RISC-V] virtual interrupt controller (vAPLIC/vIMSIC) support Oleksii Kurochko
                   ` (16 preceding siblings ...)
  2026-08-27 15:21 ` [PATCH v2 17/39] xen/riscv: decouple INSN_PSEUDO_VS_* from the hypervisor's XLEN Oleksii Kurochko
@ 2026-08-27 15:21 ` Oleksii Kurochko
  2026-09-07 15:57   ` Baptiste Le Duc
  2026-09-08 14:10   ` Jan Beulich
  2026-08-27 15:21 ` [PATCH v2 19/39] xen/riscv: implement trap redirection to a guest Oleksii Kurochko
                   ` (20 subsequent siblings)
  38 siblings, 2 replies; 244+ messages in thread
From: Oleksii Kurochko @ 2026-08-27 15:21 UTC (permalink / raw)
  To: xen-devel
  Cc: Romain Caritey, Baptiste Le Duc, Zheng Zhang, Oleksii Kurochko,
	Alistair Francis, Connor Davis, Andrew Cooper, Anthony PERARD,
	Michal Orzel, Jan Beulich, Julien Grall, Roger Pau Monné,
	Stefano Stabellini

Add a handler for guest page faults and hook it into the trap path,
providing the trap-side entry point which will later feed the MMIO
dispatch.

This will be used, for example, to trap accesses to APLIC registers so
that a guest can initialize and drive an emulated interrupt controller.

Two of the situations handled here are already decided, as neither can
ever be turned into an emulated access:

 - A fault reported with a pseudoinstruction in htinst was taken on an
   implicit access made for VS-stage address translation, so htval holds
   the address of a VS-stage PTE rather than of anything the guest asked
   for, and the guest physical address behind the original access is not
   known. This is orthogonal to the cause and can accompany any of the
   three, which is why it is checked first. scause keeps reporting the
   type of the original access, and on bare hardware a PTE which cannot
   be read raises an access fault of exactly that type, so reflect one
   back to the guest.

 - A fetch fault means the guest tried to execute from a guest physical
   address which is unmapped or which G-stage does not allow to be
   executed. On bare hardware a fetch from physical memory which does
   not exist, or which may not be executed, raises an instruction access
   fault, so reflect one back too.

Explicit loads and stores are where MMIO emulation will hook in.

Neither of the two paths above consults the p2m first, and neither will
the MMIO one: RISC-V has no populate-on-demand, no paging and no
mem_access, so every guest mapping is established eagerly and a G-stage
fault never denotes a mapping Xen could install to let the faulting
access complete.

Both of the helpers this leans on, resolve_faulting_gpa() and
trap_redirect(), are BUG_ON() placeholders for now, so each of the three
causes currently takes the host down rather than the domain. That is no
worse than before this patch, where the same causes fell through to
do_unexpected_trap() and die(). Implementing the helpers is left to
later patches.

Signed-off-by: Oleksii Kurochko <oleksii.kurochko@gmail.com>
---
Changes in v2:
 - Introduce struct guest_fault.
 - Change the prototypes of emulate_{load,store}() to take a non-const
   struct guest_fault, as emulation has to write the destination
   register and advance sepc.
 - Add handling of pseudoinstructions before the call of
   emulate_{load,store}.
 - Rename get_fault_gpa to resolve_faulting_gpa and change its prototype
   to take struct guest_fault.
 - Add handling of CAUSE_FETCH_GUEST_PAGE_FAULT now.
 - Document why the p2m is not consulted before a fault is injected, and
   add a BUILD_BUG_ON() on CONFIG_VM_EVENT to catch that assumption
   breaking.
 - Print the fault cause in the domain_crash() message rather than
   deriving an access type string which cannot cover every case.
 - Move code to introduced emulate.c instead of having it in traps.c
---
---
 xen/arch/riscv/Makefile              |   1 +
 xen/arch/riscv/emulate.c             | 179 +++++++++++++++++++++++++++
 xen/arch/riscv/include/asm/emulate.h |  10 ++
 xen/arch/riscv/include/asm/traps.h   |   3 +
 xen/arch/riscv/traps.c               |  23 +++-
 5 files changed, 215 insertions(+), 1 deletion(-)
 create mode 100644 xen/arch/riscv/emulate.c
 create mode 100644 xen/arch/riscv/include/asm/emulate.h

diff --git a/xen/arch/riscv/Makefile b/xen/arch/riscv/Makefile
index ce6410a299a4..4a021ee9eb70 100644
--- a/xen/arch/riscv/Makefile
+++ b/xen/arch/riscv/Makefile
@@ -6,6 +6,7 @@ obj-y += domain.o
 obj-y += domain-build.init.o
 obj-$(CONFIG_DOM0LESS_BOOT) += dom0less-build.init.o
 obj-$(CONFIG_EARLY_PRINTK) += early_printk.o
+obj-y += emulate.o
 obj-y += entry.o
 obj-y += extable.o
 obj-y += guestcopy.o
diff --git a/xen/arch/riscv/emulate.c b/xen/arch/riscv/emulate.c
new file mode 100644
index 000000000000..f9da0751049c
--- /dev/null
+++ b/xen/arch/riscv/emulate.c
@@ -0,0 +1,179 @@
+/* SPDX-License-Identifier: GPL-2.0-or-later */
+
+/*
+ * RISC-V instruction emulation for trapped guest accesses
+ */
+
+#include <xen/bug.h>
+#include <xen/errno.h>
+#include <xen/sched.h>
+#include <xen/types.h>
+
+#include <asm/csr.h>
+#include <asm/current.h>
+#include <asm/emulate.h>
+#include <asm/riscv_encoding.h>
+#include <asm/traps.h>
+
+/*
+ * The hardware-reported details of a guest page fault, gathered once by
+ * handle_guest_page_fault() and passed down to the emulation of the faulted
+ * access.
+ */
+struct guest_fault {
+    /* The guest register state as saved on entry to do_trap(). */
+    struct cpu_user_regs *regs;
+    /* scause: a fetch, a load or a store/AMO guest page fault. */
+    unsigned long cause;
+    /*
+     * htinst: the trapped instruction in its transformed form, or one of the
+     * special values (zero, or a pseudoinstruction).
+     */
+    unsigned long htinst;
+    /* htval: as written by hardware; see resolve_faulting_gpa(). */
+    unsigned long htval;
+    /* stval: the guest virtual address of the faulting access. */
+    unsigned long stval;
+    /* The faulting guest physical address, filled by resolve_faulting_gpa(). */
+    paddr_t gpa;
+};
+
+/*
+ * Is @htinst one of the pseudoinstructions reported for a guest page fault
+ * taken on an implicit memory access done for VS-stage address translation?
+ *
+ * All four values are recognized regardless of the hypervisor's XLEN: the
+ * width they encode is that of a VS-stage PTE, i.e. it follows the guest's
+ * paging mode (4 bytes for Sv32, 8 otherwise). On RV32 the 64-bit forms
+ * simply never occur.
+ */
+static bool htinst_is_pseudo(unsigned long htinst)
+{
+    switch ( htinst )
+    {
+    case INSN_PSEUDO_VS_LOAD32:
+    case INSN_PSEUDO_VS_STORE32:
+    case INSN_PSEUDO_VS_LOAD64:
+    case INSN_PSEUDO_VS_STORE64:
+        return true;
+
+    default:
+        return false;
+    }
+}
+
+/* Reconstruct the guest physical address of the access which faulted. */
+static void resolve_faulting_gpa(struct guest_fault *gf)
+{
+    BUG_ON("unimplemented");
+}
+
+static int emulate_load(const struct guest_fault *gf)
+{
+    return -EOPNOTSUPP;
+}
+
+static int emulate_store(struct guest_fault *gf)
+{
+    return -EOPNOTSUPP;
+}
+
+static void inject_access_fault(const struct guest_fault *gf)
+{
+    struct trap_info utrap = {};
+
+    switch ( gf->cause )
+    {
+    case CAUSE_FETCH_GUEST_PAGE_FAULT:
+        utrap.scause = CAUSE_FETCH_ACCESS;
+        break;
+
+    case CAUSE_LOAD_GUEST_PAGE_FAULT:
+        utrap.scause = CAUSE_LOAD_ACCESS;
+        break;
+
+    case CAUSE_STORE_GUEST_PAGE_FAULT:
+        utrap.scause = CAUSE_STORE_ACCESS;
+        break;
+
+    default:
+        domain_crash(current->domain, "Impossible cause (%#lx) in %s?\n",
+                     gf->cause, __func__);
+        return;
+    }
+
+    utrap.sepc = gf->regs->sepc;
+    utrap.stval = gf->stval;
+
+    trap_redirect(&utrap);
+}
+
+void handle_guest_page_fault(struct cpu_user_regs *regs, unsigned long cause)
+{
+    struct guest_fault gf = {
+        .regs = regs,
+        .cause = cause,
+        .htinst = csr_read(CSR_HTINST),
+        .htval = csr_read(CSR_HTVAL),
+        .stval = csr_read(CSR_STVAL),
+        .gpa = INVALID_PADDR,
+    };
+    int rc;
+
+    /*
+     * A guest-page fault may arise due to an implicit memory access during
+     * first-stage (VS-stage) address translation, in which case a guest
+     * physical address written to htval is that of the implicit memory
+     * access that faulted - for example, the address of a VS-level page
+     * table entry that could not be read. (The guest physical address
+     * corresponding to the original virtual address is unknown when
+     * VS-stage translation fails to complete)
+     *
+     * In such cases htinst reports one of the pseudoinstructions recognized
+     * by htinst_is_pseudo(), and the fault requires separate handling (since
+     * G-stage translation failed on an unpopulated/unmapped guest physical
+     * address during a hardware page-table walk). To match bare hardware
+     * behavior, we must inject an access fault of the ORIGINAL access type
+     * (Instruction, Load, or Store/AMO) that initiated the address
+     * translation.
+     */
+    if ( htinst_is_pseudo(gf.htinst) )
+    {
+        inject_access_fault(&gf);
+
+        return;
+    }
+
+    resolve_faulting_gpa(&gf);
+
+    switch ( cause )
+    {
+    case CAUSE_LOAD_GUEST_PAGE_FAULT:
+        rc = emulate_load(&gf);
+        break;
+
+    case CAUSE_STORE_GUEST_PAGE_FAULT:
+        rc = emulate_store(&gf);
+        break;
+
+    case CAUSE_FETCH_GUEST_PAGE_FAULT:
+        /*
+         * Guest is trying to reach unmapped/unpopulated or G-stage PTE doesn't
+         * allow execution (X=0). Generate fetch fault in this case.
+         */
+        inject_access_fault(&gf);
+        rc = 0;
+        break;
+
+    default:
+        rc = -EOPNOTSUPP;
+        ASSERT_UNREACHABLE();
+        break;
+    }
+
+    if ( rc )
+        domain_crash(current->domain,
+                     "%s: unable to handle guest page fault (cause=%#lx) at "
+                     "gpa %#"PRIpaddr"\n",
+                     __func__, cause, gf.gpa);
+}
diff --git a/xen/arch/riscv/include/asm/emulate.h b/xen/arch/riscv/include/asm/emulate.h
new file mode 100644
index 000000000000..59e69ca6794c
--- /dev/null
+++ b/xen/arch/riscv/include/asm/emulate.h
@@ -0,0 +1,10 @@
+/* SPDX-License-Identifier: GPL-2.0-or-later */
+
+#ifndef RISCV_EMULATE_H
+#define RISCV_EMULATE_H
+
+struct cpu_user_regs;
+
+void handle_guest_page_fault(struct cpu_user_regs *regs, unsigned long cause);
+
+#endif /* RISCV_EMULATE_H */
diff --git a/xen/arch/riscv/include/asm/traps.h b/xen/arch/riscv/include/asm/traps.h
index 8d4ab664bca9..38c6423742e0 100644
--- a/xen/arch/riscv/include/asm/traps.h
+++ b/xen/arch/riscv/include/asm/traps.h
@@ -17,6 +17,9 @@ void do_trap(struct cpu_user_regs *cpu_regs);
 void handle_trap(void);
 void trap_init(void);
 
+/* Reflect @trap back to the guest, i.e. enter its VS-mode trap handler. */
+void trap_redirect(const struct trap_info *trap);
+
 #endif /* __ASSEMBLER__ */
 
 #endif /* ASM__RISCV__TRAPS_H */
diff --git a/xen/arch/riscv/traps.c b/xen/arch/riscv/traps.c
index 11a6fa1bc942..9cd37d943be1 100644
--- a/xen/arch/riscv/traps.c
+++ b/xen/arch/riscv/traps.c
@@ -14,6 +14,7 @@
 
 #include <asm/extable.h>
 #include <asm/cpufeature.h>
+#include <asm/emulate.h>
 #include <asm/intc.h>
 #include <asm/processor.h>
 #include <asm/riscv_encoding.h>
@@ -193,6 +194,7 @@ void do_trap(struct cpu_user_regs *cpu_regs)
 {
     register_t pc = cpu_regs->sepc;
     unsigned long cause = csr_read(CSR_SCAUSE);
+    bool from_guest = cpu_regs->hstatus & HSTATUS_SPV;
 
     switch ( cause )
     {
@@ -203,6 +205,19 @@ void do_trap(struct cpu_user_regs *cpu_regs)
         vsbi_handle_ecall(cpu_regs);
         break;
 
+    case CAUSE_FETCH_GUEST_PAGE_FAULT:
+    case CAUSE_LOAD_GUEST_PAGE_FAULT:
+    case CAUSE_STORE_GUEST_PAGE_FAULT:
+        /*
+         * A guest page fault taken in Xen context comes from an hlv/hlvx
+         * access made on a vCPU's behalf and is dealt with by the
+         * fixup_exception() above, so only a guest can get here.
+         */
+        BUG_ON(!from_guest);
+
+        handle_guest_page_fault(cpu_regs, cause);
+        break;
+
     case CAUSE_ILLEGAL_INSTRUCTION:
         if ( do_bug_frame(cpu_regs, pc) >= 0 )
         {
@@ -251,7 +266,7 @@ void do_trap(struct cpu_user_regs *cpu_regs)
         break;
     }
 
-    if ( cpu_regs->hstatus & HSTATUS_SPV )
+    if ( from_guest )
         check_for_pcpu_work();
 }
 
@@ -275,3 +290,9 @@ enum mc_disposition arch_do_multicall_call(struct mc_state *state)
     BUG_ON("unimplemented");
     return mc_continue;
 }
+
+/* Redirect trap to Guest. */
+void trap_redirect(const struct trap_info *trap)
+{
+    BUG_ON("unimplemented");
+}
-- 
2.55.0



^ permalink raw reply related	[flat|nested] 244+ messages in thread

* [PATCH v2 19/39] xen/riscv: implement trap redirection to a guest
  2026-08-27 15:20 [PATCH v2 00/39] [RISC-V] virtual interrupt controller (vAPLIC/vIMSIC) support Oleksii Kurochko
                   ` (17 preceding siblings ...)
  2026-08-27 15:21 ` [PATCH v2 18/39] xen/riscv: add guest page fault handling stub Oleksii Kurochko
@ 2026-08-27 15:21 ` Oleksii Kurochko
  2026-09-07 15:57   ` Baptiste Le Duc
                     ` (2 more replies)
  2026-08-27 15:21 ` [PATCH v2 20/39] xen/riscv: detect Shtvala Oleksii Kurochko
                   ` (19 subsequent siblings)
  38 siblings, 3 replies; 244+ messages in thread
From: Oleksii Kurochko @ 2026-08-27 15:21 UTC (permalink / raw)
  To: xen-devel
  Cc: Romain Caritey, Baptiste Le Duc, Zheng Zhang, Oleksii Kurochko,
	Alistair Francis, Connor Davis, Andrew Cooper, Anthony PERARD,
	Michal Orzel, Jan Beulich, Julien Grall, Roger Pau Monné,
	Stefano Stabellini

Some traps taken by Xen on behalf of a guest can't or shouldn't be handled
by the hypervisor and have to be reflected to the guest's own S-mode trap
handler instead: the access faults which handle_guest_page_fault() injects
for a fault that can never become an emulated access, and, later on, a
fault taken by the hlv/hlvx sequences of riscv_read_guest() while
accessing guest memory on a vCPU's behalf.

Implement trap_redirect(), until now a BUG_ON() placeholder, for that
purpose. It makes the trap appear to the guest as if it had been taken
directly in VS-mode: the trap information is transferred to the guest's
virtual supervisor CSRs and the vCPU is resumed at its exception vector in
supervisor mode, following the trap entry rules of the RISC-V privileged
specification.

Add the STVEC_* definitions needed to tell the BASE and MODE fields of
vstvec apart.

The implementation is based on kvm_riscv_vcpu_trap_redirect() from Linux,
with a few deviations:
 - The function reads and writes physical VS-mode CSRs, so it is only
   meaningful for the currently running vCPU. Instead of taking a
   struct vcpu argument, it always operates on current.
 - The MODE field of vstvec is masked off explicitly when computing the
   exception target PC (exceptions always vector to BASE), rather than
   relying on the hardwired zero bit of sepc to drop it on VM entry.
 - Assertions document the preconditions: the trap must have been taken
   from virtualized mode (hstatus.SPV set), and only synchronous
   exceptions may be redirected - interrupts must be injected via hvip
   instead, so that the hardware performs VS-mode trap entry itself,
   respecting vsstatus.SIE and vectored vstvec dispatch.

Signed-off-by: Oleksii Kurochko <oleksii.kurochko@gmail.com>
---
Changes in v2:
 - Add new defines STVEC_*. The STVEC_MODE_DIRECT/_VECTORED values are
   currently unused and were included because riscv encoding header is a
   spec mirror full of unused encodings.
 - Use STVEC_BASE_MASK instead of open-coding it.
 - Rename riscv_vcpu_trap_redirect() to trap_redirect(): unlike its KVM
   counterpart the function takes no vCPU argument, it implicitly operates
   on current, so "vcpu" in the name describes nothing.
---
 xen/arch/riscv/include/asm/riscv_encoding.h |  6 +++
 xen/arch/riscv/traps.c                      | 50 ++++++++++++++++++++-
 2 files changed, 55 insertions(+), 1 deletion(-)

diff --git a/xen/arch/riscv/include/asm/riscv_encoding.h b/xen/arch/riscv/include/asm/riscv_encoding.h
index 2d2e7e11b3ef..b2071f47587c 100644
--- a/xen/arch/riscv/include/asm/riscv_encoding.h
+++ b/xen/arch/riscv/include/asm/riscv_encoding.h
@@ -109,6 +109,12 @@
 #define SIP_SSIP			MIP_SSIP
 #define SIP_STIP			MIP_STIP
 
+/* stvec/vstvec: MODE is bits [1:0], BASE is bits [XLEN-1:2] */
+#define STVEC_MODE_MASK			_UL(0x3)
+#define STVEC_MODE_DIRECT		_UL(0x0)
+#define STVEC_MODE_VECTORED		_UL(0x1)
+#define STVEC_BASE_MASK			(~STVEC_MODE_MASK)
+
 #define PRV_U				_UL(0)
 #define PRV_S				_UL(1)
 #define PRV_M				_UL(3)
diff --git a/xen/arch/riscv/traps.c b/xen/arch/riscv/traps.c
index 9cd37d943be1..8372f34497ad 100644
--- a/xen/arch/riscv/traps.c
+++ b/xen/arch/riscv/traps.c
@@ -294,5 +294,53 @@ enum mc_disposition arch_do_multicall_call(struct mc_state *state)
 /* Redirect trap to Guest. */
 void trap_redirect(const struct trap_info *trap)
 {
-    BUG_ON("unimplemented");
+    struct cpu_user_regs *regs = vcpu_guest_cpu_user_regs(current);
+    unsigned long vsstatus = csr_read(CSR_VSSTATUS);
+
+    /*
+     * Redirecting a trap makes sense only if the trap was taken from
+     * virtualized mode, i.e. sret is going to return to VS-mode.
+     */
+    ASSERT(regs->hstatus & HSTATUS_SPV);
+
+    /*
+     * Only synchronous exceptions can be redirected. Interrupts must be
+     * injected via hvip instead, so that the hardware itself performs
+     * VS-mode trap entry, respecting vsstatus.SIE and the vectored
+     * dispatch (BASE + 4 * cause) if vstvec is configured so.
+     */
+    ASSERT(!(trap->scause & CAUSE_IRQ_FLAG));
+
+    /* Change Guest SSTATUS.SPP bit */
+    vsstatus &= ~SSTATUS_SPP;
+    if ( regs->sstatus & SSTATUS_SPP )
+        vsstatus |= SSTATUS_SPP;
+
+    /* Change Guest SSTATUS.SPIE bit */
+    vsstatus &= ~SSTATUS_SPIE;
+    if ( vsstatus & SSTATUS_SIE )
+        vsstatus |= SSTATUS_SPIE;
+
+    /* Clear Guest SSTATUS.SIE bit */
+    vsstatus &= ~SSTATUS_SIE;
+
+    /* Update Guest SSTATUS */
+    csr_write(CSR_VSSTATUS, vsstatus);
+
+    /* Update Guest SCAUSE, STVAL, and SEPC */
+    csr_write(CSR_VSCAUSE, trap->scause);
+    csr_write(CSR_VSTVAL, trap->stval);
+    csr_write(CSR_VSEPC, trap->sepc);
+
+    /*
+     * Set Guest PC to Guest exception vector.
+     *
+     * vstvec's MODE field is not part of the address. Exceptions always
+     * target BASE regardless of MODE, so mask it off explicitly instead of
+     * relying on the hardwired zero bit of sepc to drop it.
+     */
+    regs->sepc = csr_read(CSR_VSTVEC) & STVEC_BASE_MASK;
+
+    /* Set Guest privilege mode to supervisor */
+    regs->sstatus |= SSTATUS_SPP;
 }
-- 
2.55.0



^ permalink raw reply related	[flat|nested] 244+ messages in thread

* [PATCH v2 20/39] xen/riscv: detect Shtvala
  2026-08-27 15:20 [PATCH v2 00/39] [RISC-V] virtual interrupt controller (vAPLIC/vIMSIC) support Oleksii Kurochko
                   ` (18 preceding siblings ...)
  2026-08-27 15:21 ` [PATCH v2 19/39] xen/riscv: implement trap redirection to a guest Oleksii Kurochko
@ 2026-08-27 15:21 ` Oleksii Kurochko
  2026-09-07 15:57   ` Baptiste Le Duc
  2026-08-27 15:21 ` [PATCH v2 21/39] xen/riscv: resolve the faulting guest physical address Oleksii Kurochko
                   ` (18 subsequent siblings)
  38 siblings, 1 reply; 244+ messages in thread
From: Oleksii Kurochko @ 2026-08-27 15:21 UTC (permalink / raw)
  To: xen-devel
  Cc: Romain Caritey, Baptiste Le Duc, Zheng Zhang, Oleksii Kurochko,
	Alistair Francis, Connor Davis, Andrew Cooper, Anthony PERARD,
	Michal Orzel, Jan Beulich, Julien Grall, Roger Pau Monné,
	Stefano Stabellini

Shtvala says that htval is written with the faulting guest physical address
on a guest-page fault. The H extension itself allows an implementation to
write htval with either that address or with zero, so where the extension is
absent a zero htval cannot be told apart from a genuine fault on guest
physical address 0-3.

It is not offered to guests. Shtvala describes the HS-mode trap interface,
which a VS-mode guest never sees, and the H extension it belongs to is
already withheld from guests. Its guest-facing counterpart is a separate
extension, Shvstvala.

Signed-off-by: Oleksii Kurochko <oleksii.kurochko@gmail.com>
---
Change in v2:
 - New patch.
---
---
 xen/arch/riscv/cpufeature.c             | 1 +
 xen/arch/riscv/include/asm/cpufeature.h | 1 +
 2 files changed, 2 insertions(+)

diff --git a/xen/arch/riscv/cpufeature.c b/xen/arch/riscv/cpufeature.c
index 4bcbf56cb694..09a06f12318c 100644
--- a/xen/arch/riscv/cpufeature.c
+++ b/xen/arch/riscv/cpufeature.c
@@ -195,6 +195,7 @@ static const struct riscv_isa_ext_entry __initconstrel riscv_isa_ext[] = {
     RISCV_ISA_EXT_ENTRY(zba,          RISCV_ISA_EXT_GUEST_ANY),
     RISCV_ISA_EXT_ENTRY(zbb,          RISCV_ISA_EXT_GUEST_ANY),
     RISCV_ISA_EXT_ENTRY(zbs,          RISCV_ISA_EXT_GUEST_ANY),
+    RISCV_ISA_EXT_ENTRY(shtvala,      RISCV_ISA_EXT_GUEST_NONE),
     RISCV_ISA_EXT_ENTRY(smaia,        RISCV_ISA_EXT_GUEST_ANY),
     RISCV_ISA_EXT_ENTRY(smstateen,    RISCV_ISA_EXT_GUEST_ANY),
     RISCV_ISA_EXT_ENTRY(ssaia,        RISCV_ISA_EXT_GUEST_ANY),
diff --git a/xen/arch/riscv/include/asm/cpufeature.h b/xen/arch/riscv/include/asm/cpufeature.h
index 2973eb13a513..ac8c68007072 100644
--- a/xen/arch/riscv/include/asm/cpufeature.h
+++ b/xen/arch/riscv/include/asm/cpufeature.h
@@ -36,6 +36,7 @@ enum riscv_isa_ext_id {
     RISCV_ISA_EXT_zba,
     RISCV_ISA_EXT_zbb,
     RISCV_ISA_EXT_zbs,
+    RISCV_ISA_EXT_shtvala,
     RISCV_ISA_EXT_smaia,
     RISCV_ISA_EXT_smstateen,
     RISCV_ISA_EXT_ssaia,
-- 
2.55.0



^ permalink raw reply related	[flat|nested] 244+ messages in thread

* [PATCH v2 21/39] xen/riscv: resolve the faulting guest physical address
  2026-08-27 15:20 [PATCH v2 00/39] [RISC-V] virtual interrupt controller (vAPLIC/vIMSIC) support Oleksii Kurochko
                   ` (19 preceding siblings ...)
  2026-08-27 15:21 ` [PATCH v2 20/39] xen/riscv: detect Shtvala Oleksii Kurochko
@ 2026-08-27 15:21 ` Oleksii Kurochko
  2026-09-09 12:04   ` Baptiste Le Duc
  2026-09-10 15:06   ` Jan Beulich
  2026-08-27 15:21 ` [PATCH v2 22/39] xen/riscv: add guest memory read helper Oleksii Kurochko
                   ` (17 subsequent siblings)
  38 siblings, 2 replies; 244+ messages in thread
From: Oleksii Kurochko @ 2026-08-27 15:21 UTC (permalink / raw)
  To: xen-devel
  Cc: Romain Caritey, Baptiste Le Duc, Zheng Zhang, Oleksii Kurochko,
	Alistair Francis, Connor Davis, Andrew Cooper, Anthony PERARD,
	Michal Orzel, Jan Beulich, Julien Grall, Roger Pau Monné,
	Stefano Stabellini

Take the guest physical address from htval and stval: on a guest-page fault
htval holds it shifted right by 2, so that an address wider than XLEN fits,
and stval holds the faulting guest virtual address, whose two least
significant bits are those of the guest physical address. The shift is done
on paddr_t rather than on the raw register: a guest physical address is 34
bits wide on RV32 with Sv32x4, so shifting an XLEN-wide value would drop its
top two bits.

Those two low bits come from stval only for a fault on an explicit access.
Where one is taken on an implicit access made for VS-stage translation htval
holds the address of the VS-stage PTE which could not be read, while stval
still holds the guest virtual address which started the walk, and the low
bits of the address written to htval are zero instead. htinst tells the two
apart, which is what the spec points at it for.

stval needs no check against an ISA extension: a guest-page fault writes it
with the faulting guest virtual address regardless. Sstvala would not be the
right thing to test for either (it covers stval across every trap type
which writes it, a wider guarantee than what is needed here).

htval does need one. The H extension lets an implementation write it with
either the faulting address or zero, so without Shtvala a zero htval cannot
be told apart from a genuine fault on guest physical address 0-3, and the
address has to be recovered by decoding the access and walking the VS-stage
page tables in software instead. That is left as a TODO, and until it is
written such hardware panics rather than acting on an address which may not
be the one which faulted.

Signed-off-by: Oleksii Kurochko <oleksii.kurochko@gmail.com>
---
Changes in v2:
 - New patch.
---
---
 xen/arch/riscv/emulate.c | 23 +++++++++++++++++++++--
 1 file changed, 21 insertions(+), 2 deletions(-)

diff --git a/xen/arch/riscv/emulate.c b/xen/arch/riscv/emulate.c
index f9da0751049c..ff530ef2df74 100644
--- a/xen/arch/riscv/emulate.c
+++ b/xen/arch/riscv/emulate.c
@@ -9,6 +9,7 @@
 #include <xen/sched.h>
 #include <xen/types.h>
 
+#include <asm/cpufeature.h>
 #include <asm/csr.h>
 #include <asm/current.h>
 #include <asm/emulate.h>
@@ -62,10 +63,28 @@ static bool htinst_is_pseudo(unsigned long htinst)
     }
 }
 
-/* Reconstruct the guest physical address of the access which faulted. */
+/* Resolves the guest physical address the access faulted on into @gf->gpa. */
 static void resolve_faulting_gpa(struct guest_fault *gf)
 {
-    BUG_ON("unimplemented");
+    /*
+     * A zero htval is either a genuine fault on guest physical address 0-3, or
+     * an implementation which does not report the address at all; only Shtvala
+     * tells the two apart.
+     *
+     * TODO: where it is absent, recover the address in software rather than
+     * giving up.
+     */
+    if ( !gf->htval &&
+         !riscv_isa_extension_available(NULL, RISCV_ISA_EXT_shtvala) )
+        panic("Shtvala isn't supported by h/w; s/w VS-stage walk required\n");
+
+    /*
+     * htval does not carry the two low bits of the address: for an explicit
+     * access they are those of the faulting guest virtual address in stval,
+     * and for an implicit access made for VS-stage translation they are zero.
+     */
+    gf->gpa = ((paddr_t)gf->htval << 2) |
+              (htinst_is_pseudo(gf->htinst) ? 0 : (gf->stval & 3));
 }
 
 static int emulate_load(const struct guest_fault *gf)
-- 
2.55.0



^ permalink raw reply related	[flat|nested] 244+ messages in thread

* [PATCH v2 22/39] xen/riscv: add guest memory read helper
  2026-08-27 15:20 [PATCH v2 00/39] [RISC-V] virtual interrupt controller (vAPLIC/vIMSIC) support Oleksii Kurochko
                   ` (20 preceding siblings ...)
  2026-08-27 15:21 ` [PATCH v2 21/39] xen/riscv: resolve the faulting guest physical address Oleksii Kurochko
@ 2026-08-27 15:21 ` Oleksii Kurochko
  2026-09-09 12:04   ` Baptiste Le Duc
  2026-09-10 15:28   ` Jan Beulich
  2026-08-27 15:21 ` [PATCH v2 23/39] xen/riscv: look up the exception table for any trap taken in Xen context Oleksii Kurochko
                   ` (16 subsequent siblings)
  38 siblings, 2 replies; 244+ messages in thread
From: Oleksii Kurochko @ 2026-08-27 15:21 UTC (permalink / raw)
  To: xen-devel
  Cc: Romain Caritey, Baptiste Le Duc, Zheng Zhang, Oleksii Kurochko,
	Alistair Francis, Connor Davis, Andrew Cooper, Anthony PERARD,
	Michal Orzel, Jan Beulich, Julien Grall, Roger Pau Monné,
	Stefano Stabellini

Introduce riscv_read_guest() to allow Xen to safely read guest memory
using HLV/HLVX instructions while reliably capturing trap context.

This is required for instruction fetch emulation and MMIO decoding, where
Xen must inspect guest memory that may not be directly accessible and may
fault.

The implementation is based on kvm_riscv_vcpu_unpriv_read() from Linux,
with one deviation: the hlv/hlvx instructions translate the guest address
through the live vsatp/hgatp CSRs, i.e. through the address space of the
currently running vCPU, so the function can only be called safely for
current. Instead of taking a struct vcpu argument, it always operates on
current directly.

Signed-off-by: Oleksii Kurochko <oleksii.kurochko@gmail.com>
---
Changes in v2:
 - Rename riscv_vcpu_unpriv_read() to riscv_read_guest() and make the guest
   address the first parameter. "unprivileged" described how hlv/hlvx perform
   the access rather than what the helper is for, and "unprivileged guest"
   reads as a synonym for DomU although the helper works for any domain.
 - Drop the hstatus save/restore and the local_irq_save() protecting it:
   hstatus already belongs to the vCPU which trapped, as Xen never installs
   a value of its own and does not reschedule before returning to the guest.
   ASSERT() hstatus.SPV in the saved copy instead, which also documents that
   the helper is only usable while handling a trap from a guest.
 - Poison val with ~0UL and make [val] "+&r": the fixup for the first access
   resumes past the loads without writing it, so a caller which checks
   trap->scause is no longer handed an uninitialized value.
 - Describe the trap_info write with a "+m" (*trap) operand instead of a
   "memory" clobber; the exception handler writes that structure and nothing
   else.
 - Combine the two halfwords with slli/or instead of sll/add, and use bnez
   for the instruction length check.
 - Turn the hlv.d/hlv.w selection into #if/#elif with an #error default,
   rather than silently using hlv.w for anything that is not RV64.
 - Document that at most two halfwords are fetched, i.e. that encodings
   wider than 32 bits are unsupported and cannot be completed by calling the
   helper again at guest_addr + 4, since the length check would then be
   applied to a continuation halfword.
---
---
 xen/arch/riscv/guestcopy.c | 87 ++++++++++++++++++++++++++++++++++++++
 1 file changed, 87 insertions(+)

diff --git a/xen/arch/riscv/guestcopy.c b/xen/arch/riscv/guestcopy.c
index 8a89212e0bea..b2327822acaa 100644
--- a/xen/arch/riscv/guestcopy.c
+++ b/xen/arch/riscv/guestcopy.c
@@ -6,6 +6,7 @@
 #include <xen/string.h>
 
 #include <asm/guest_access.h>
+#include <asm/traps.h>
 
 #define COPY_from_guest     0U
 #define COPY_to_guest       BIT(0, U)
@@ -114,3 +115,89 @@ unsigned long copy_to_guest_phys(struct domain *d, paddr_t gpa, void *buf,
     return copy_guest(buf, gpa, len, GPA_INFO(d),
                       COPY_to_guest | COPY_gpa);
 }
+
+/*
+ * Read machine word from guest memory
+ *
+ * @guest_addr: Guest address to read
+ * @read_insn: Flag representing whether we are reading instruction
+ * @trap: Output pointer to trap details if something went wrong during read
+ *
+ * The hlv/hlvx instructions translate guest_addr through the live
+ * vsatp/hgatp CSRs, so the read is only meaningful for the address
+ * space of the currently running vCPU.
+ *
+ * At most two halfwords are fetched when @read_insn is true, i.e. encodings
+ * wider than 32 bits are not supported. Such an encoding cannot be completed
+ * by calling this function again at @guest_addr + 4: the length check is
+ * applied to the first halfword read, which would then be a continuation of
+ * the instruction rather than its opcode. It is up to the caller to reject
+ * anything that is neither a 16- nor a 32-bit encoding.
+ */
+unsigned long riscv_read_guest(unsigned long guest_addr, bool read_insn,
+                               struct trap_info *trap)
+{
+    /*
+     * Poison the result: if the very first access faults, the fixup skips
+     * over the loads without writing it. Callers must check trap->scause.
+     */
+    unsigned long val = ~0UL, tmp;
+
+    /*
+     * hlv/hlvx use hstatus.SPVP for the privilege of the access, and the
+     * live vsatp/hgatp for the translation. Xen never installs a value of
+     * its own in hstatus (it is only saved on trap entry and restored
+     * before sret) and it doesn't reschedule before returning to the
+     * guest, so all three still belong to the vCPU which trapped.
+     *
+     * Check the saved copy rather than the live CSR: a nested trap taken
+     * from HS-mode clears hstatus.SPV in the CSR (but leaves SPVP alone).
+     */
+    ASSERT(vcpu_guest_cpu_user_regs(current)->hstatus & HSTATUS_SPV);
+
+    if ( read_insn )
+    {
+        asm volatile ( "\n"
+            "1: hlvx.hu %[val], (%[addr])\n"
+            ASM_EXTABLE_TRAP_INFO(1b, 3f, %[ti])
+            "   andi %[tmp], %[val], 3\n"
+            "   addi %[tmp], %[tmp], -3\n"
+            "   bnez %[tmp], 3f\n"
+            "   addi %[addr], %[addr], 2\n"
+            "\n"
+            "2: hlvx.hu %[tmp], (%[addr])\n"
+            ASM_EXTABLE_TRAP_INFO(2b, 3f, %[ti])
+            "   slli %[tmp], %[tmp], 16\n"
+            "   or %[val], %[val], %[tmp]\n"
+            "3:\n"
+        : [val] "+&r" (val), [tmp] "=&r" (tmp), [addr] "+&r" (guest_addr),
+          "+m" (*trap)
+        : [ti] "r" (trap) );
+
+        /*
+         * Although HLVX instructions' explicit memory accesses require execute
+         * permissions, they still raise the same exceptions as other load
+         * instructions, rather than raising fetch exceptions instead.
+         */
+        if ( trap->scause == CAUSE_LOAD_PAGE_FAULT )
+            trap->scause = CAUSE_FETCH_PAGE_FAULT;
+    }
+    else
+    {
+        asm volatile ( "\n"
+            "1: "
+#if defined(CONFIG_RISCV_64)
+            "hlv.d %[val], (%[addr])\n"
+#elif defined(CONFIG_RISCV_32)
+            "hlv.w %[val], (%[addr])\n"
+#else
+# error "unsupported RISC-V variant: no hlv for a machine word"
+#endif
+            "2:\n"
+            ASM_EXTABLE_TRAP_INFO(1b, 2b, %[ti])
+        : [val] "+&r" (val), "+m" (*trap)
+        : [addr] "r" (guest_addr), [ti] "r" (trap) );
+    }
+
+    return val;
+}
-- 
2.55.0



^ permalink raw reply related	[flat|nested] 244+ messages in thread

* [PATCH v2 23/39] xen/riscv: look up the exception table for any trap taken in Xen context
  2026-08-27 15:20 [PATCH v2 00/39] [RISC-V] virtual interrupt controller (vAPLIC/vIMSIC) support Oleksii Kurochko
                   ` (21 preceding siblings ...)
  2026-08-27 15:21 ` [PATCH v2 22/39] xen/riscv: add guest memory read helper Oleksii Kurochko
@ 2026-08-27 15:21 ` Oleksii Kurochko
  2026-09-10 15:31   ` Jan Beulich
  2026-09-18  8:44   ` Baptiste Le Duc
  2026-08-27 15:21 ` [PATCH v2 24/39] xen/riscv: add helpers for decoding a trapped load or store Oleksii Kurochko
                   ` (15 subsequent siblings)
  38 siblings, 2 replies; 244+ messages in thread
From: Oleksii Kurochko @ 2026-08-27 15:21 UTC (permalink / raw)
  To: xen-devel
  Cc: Romain Caritey, Baptiste Le Duc, Zheng Zhang, Oleksii Kurochko,
	Alistair Francis, Connor Davis, Andrew Cooper, Anthony PERARD,
	Michal Orzel, Jan Beulich, Julien Grall, Roger Pau Monné,
	Stefano Stabellini

do_trap() consulted the exception table only for CAUSE_ILLEGAL_INSTRUCTION,
which covers csr_read_safe() but not the hlv/hlvx sequences reading guest
memory: those fault with load/store (guest) page fault causes and would
reach do_unexpected_trap() instead of their fixup.

Move the lookup ahead of the cause switch, and gate it on the trap having
been taken in Xen context and not being an interrupt:

- sepc of a trap taken from the guest is a guest VA/PA, which the
  guest can point at an address listed in the exception table; Xen would
  then act on that entry and, for EX_TYPE_TRAP_INFO, write through a
  pointer fully under guest control. Entries are matched by exact address,
  so this needs no more than a numerical collision.

- an interrupt taken at an address listed in the table would otherwise be
  "fixed up" as if the access itself had faulted, silently skipping it and
  handing the caller the interrupt's scause as a fault cause.

Returning early skips check_for_pcpu_work(), which is correct: that only
runs for traps taken from the guest.

With that in place a G-stage fault reaching the switch can no longer have
been caused by an hlv/hlvx covered by an entry, so anything left must have
come from the guest; assert as much.

Cache the "trap came from the guest" test in a local, it is now used four
times.

Signed-off-by: Oleksii Kurochko <oleksii.kurochko@gmail.com>
---
Changes in v2:
 - New patch.
---
---
 xen/arch/riscv/traps.c | 28 ++++++++++++++++++++++++----
 1 file changed, 24 insertions(+), 4 deletions(-)

diff --git a/xen/arch/riscv/traps.c b/xen/arch/riscv/traps.c
index 8372f34497ad..f5f83fce10ba 100644
--- a/xen/arch/riscv/traps.c
+++ b/xen/arch/riscv/traps.c
@@ -196,11 +196,34 @@ void do_trap(struct cpu_user_regs *cpu_regs)
     unsigned long cause = csr_read(CSR_SCAUSE);
     bool from_guest = cpu_regs->hstatus & HSTATUS_SPV;
 
+    /*
+     * A synchronous trap taken in Xen context may come from an access done on
+     * a vCPU's behalf, e.g. the hlv/hlvx sequences in riscv_read_guest(),
+     * or from a probing access like csr_read_safe(). Both are covered by
+     * exception table entries which record the fault details for the caller
+     * and resume execution past the faulting instruction.
+     *
+     * Traps taken from the guest must never be fixed up: sepc is then a guest
+     * address, which the guest could point at an address listed in the
+     * exception table, making Xen act on an entry (and, for EX_TYPE_TRAP_INFO,
+     * write through a pointer) fully under guest control.
+     *
+     * Interrupts must be excluded too: one taken at an address which happens
+     * to be listed in the exception table would otherwise be "fixed up" as if
+     * the access itself had faulted, silently skipping it.
+     *
+     * Returning early skips check_for_pcpu_work() below, which is correct:
+     * that only runs for traps taken from the guest.
+     */
+    if ( !from_guest && !(cause & CAUSE_IRQ_FLAG) &&
+         fixup_exception(cpu_regs, cause) )
+        return;
+
     switch ( cause )
     {
     case CAUSE_VIRTUAL_SUPERVISOR_ECALL:
         /* CAUSE_VIRTUAL_SUPERVISOR_ECALL should come from VS-mode */
-        BUG_ON(!(cpu_regs->hstatus & HSTATUS_SPV));
+        BUG_ON(!from_guest);
 
         vsbi_handle_ecall(cpu_regs);
         break;
@@ -232,9 +255,6 @@ void do_trap(struct cpu_user_regs *cpu_regs)
             break;
         }
 
-        if ( fixup_exception(cpu_regs, cause) )
-            break;
-
         fallthrough;
     default:
         if ( cause & CAUSE_IRQ_FLAG )
-- 
2.55.0



^ permalink raw reply related	[flat|nested] 244+ messages in thread

* [PATCH v2 24/39] xen/riscv: add helpers for decoding a trapped load or store
  2026-08-27 15:20 [PATCH v2 00/39] [RISC-V] virtual interrupt controller (vAPLIC/vIMSIC) support Oleksii Kurochko
                   ` (22 preceding siblings ...)
  2026-08-27 15:21 ` [PATCH v2 23/39] xen/riscv: look up the exception table for any trap taken in Xen context Oleksii Kurochko
@ 2026-08-27 15:21 ` Oleksii Kurochko
  2026-09-14 11:03   ` Jan Beulich
  2026-09-18  8:44   ` Baptiste Le Duc
  2026-08-27 15:21 ` [PATCH v2 25/39] xen/riscv: add guest load emulation for trapped MMIO accesses Oleksii Kurochko
                   ` (14 subsequent siblings)
  38 siblings, 2 replies; 244+ messages in thread
From: Oleksii Kurochko @ 2026-08-27 15:21 UTC (permalink / raw)
  To: xen-devel
  Cc: Romain Caritey, Baptiste Le Duc, Zheng Zhang, Oleksii Kurochko,
	Alistair Francis, Connor Davis, Andrew Cooper, Anthony PERARD,
	Michal Orzel, Jan Beulich, Julien Grall, Roger Pau Monné,
	Stefano Stabellini

emulate_load() and emulate_store() will both need to obtain the
instruction which caused a guest MMIO trap, decode it, and locate the
register operand it names. Add what the two share, ahead of either of
them being implemented: struct decoded_insn, insn_fetch_faulted(),
decode_ldst_insn(), guest_xlen(), guest_gpr() and advance_pc().

The mask/match chain is adapted from Linux's KVM RISC-V implementation.

Nothing calls any of this yet, so tag the functions __maybe_unused to
keep the build going; the tags go away once emulate_load() and
emulate_store() gain their bodies later.

Signed-off-by: Oleksii Kurochko <oleksii.kurochko@gmail.com>
---
How this function could be used can be seen in the next patch.
---
Changes in v2:
 - New patch.
---
---
 xen/arch/riscv/emulate.c                    | 330 ++++++++++++++++++++
 xen/arch/riscv/include/asm/guest_access.h   |   4 +
 xen/arch/riscv/include/asm/riscv_encoding.h |  10 +
 3 files changed, 344 insertions(+)

diff --git a/xen/arch/riscv/emulate.c b/xen/arch/riscv/emulate.c
index ff530ef2df74..81a50643a5ec 100644
--- a/xen/arch/riscv/emulate.c
+++ b/xen/arch/riscv/emulate.c
@@ -5,6 +5,7 @@
  */
 
 #include <xen/bug.h>
+#include <xen/compiler.h>
 #include <xen/errno.h>
 #include <xen/sched.h>
 #include <xen/types.h>
@@ -13,9 +14,29 @@
 #include <asm/csr.h>
 #include <asm/current.h>
 #include <asm/emulate.h>
+#include <asm/guest_access.h>
+#include <asm/processor.h>
 #include <asm/riscv_encoding.h>
 #include <asm/traps.h>
 
+/*
+ * Determine the trapped load or store instruction which caused a guest MMIO
+ * trap.
+ */
+struct decoded_insn {
+    /* The instruction itself, and its length in bytes. */
+    unsigned long insn;
+    unsigned int insn_len;
+    /* Width of the memory access, in bytes. */
+    unsigned int len;
+    /* Number of the register operand: rd for a load, rs2 for a store. */
+    unsigned int reg;
+    /* The access is a store rather than a load. */
+    bool is_write;
+    /* The load zero-extends its result rather than sign-extending it. */
+    bool is_unsigned;
+};
+
 /*
  * The hardware-reported details of a guest page fault, gathered once by
  * handle_guest_page_fault() and passed down to the emulation of the faulted
@@ -39,6 +60,71 @@ struct guest_fault {
     paddr_t gpa;
 };
 
+static bool is_load_guest_page_fault(unsigned long scause)
+{
+    return scause == CAUSE_LOAD_GUEST_PAGE_FAULT;
+}
+
+static __maybe_unused void advance_pc(struct cpu_user_regs *regs,
+                                      unsigned int step)
+{
+    regs->sepc += step;
+}
+
+/*
+ * The effective XLEN of the guest at the point of the trap: hstatus.VSXL for a
+ * trap taken from VS-mode, vsstatus.UXL for one taken from VU-mode.
+ *
+ * VSXL is consulted whichever mode the trap came from, as it also gives the
+ * width of vsstatus itself: where VSXL says 32, that register has no UXL field
+ * to consult and VU-mode is 32-bit as well, there being nothing to configure.
+ *
+ * It is needed to decode a trapped instruction: the encodings which exist only
+ * for XLEN=64 must not be recognized for a 32-bit guest. Besides those simply
+ * being reserved there, the compressed ones are ambiguous: C.LD and C.FLW
+ * share the encoding 0x6000 (mask 0xe003), and likewise C.SD/C.FSW,
+ * C.LDSP/C.FLWSP and C.SDSP/C.FSWSP.
+ *
+ * IS_ENABLED() can't be used here as HSTATUS_VSXL is defined for
+ * __riscv_xlen == 64 only, the field not existing on RV32 in the first place.
+ */
+static __maybe_unused unsigned int guest_xlen(const struct cpu_user_regs *regs)
+{
+#ifdef CONFIG_RISCV_32
+    return 32;
+#else
+    unsigned long xl = MASK_EXTR(regs->hstatus, HSTATUS_VSXL);
+
+    if ( (xl == XLEN_FIELD_64) && !(regs->sstatus & SSTATUS_SPP) )
+        xl = MASK_EXTR(csr_read(CSR_VSSTATUS), SSTATUS64_UXL);
+
+    switch ( xl )
+    {
+    case XLEN_FIELD_32:
+        return 32;
+
+    case XLEN_FIELD_64:
+        return 64;
+
+    default:
+        /*
+         * The field holds nothing else in practice: XLEN_FIELD_128 would mean
+         * RV128, which no implementation provides, and the only value left is
+         * reserved. ASSERT_UNREACHABLE() being debug-only, a width still has
+         * to be answered in release builds.
+         *
+         * Answer 32, that being the safe way to be wrong: the decoder then
+         * fails to recognize the RV64-only encodings and emulation gives up.
+         * Answering 64 for what may well be a 32-bit guest would instead have
+         * it take C.FLW for C.LD and C.FSW for C.SD (see above), i.e. quietly
+         * emulate an access of the wrong width against the wrong register.
+         */
+        ASSERT_UNREACHABLE();
+        return 32;
+    }
+#endif
+}
+
 /*
  * Is @htinst one of the pseudoinstructions reported for a guest page fault
  * taken on an implicit memory access done for VS-stage address translation?
@@ -87,6 +173,250 @@ static void resolve_faulting_gpa(struct guest_fault *gf)
               (htinst_is_pseudo(gf->htinst) ? 0 : (gf->stval & 3));
 }
 
+/*
+ * Where the value of a decoded instruction's register operand is held.
+ *
+ * Relies on x0..x31 being laid out at the start of struct cpu_user_regs in
+ * architectural register-number order; see the comment there.
+ */
+static __maybe_unused unsigned long *guest_gpr(struct cpu_user_regs *regs,
+                                               unsigned int reg)
+{
+    ASSERT(reg < 32);
+
+    return REG_PTR(reg, 0, regs);
+}
+
+/*
+ * Obtain the instruction which caused a guest MMIO trap, filling in
+ * @di->insn and @di->insn_len. It either comes transformed in htinst, or has
+ * to be fetched from guest memory.
+ *
+ * Returns true if the fetch faulted in turn; the resulting trap has then
+ * already been redirected to the guest and there is nothing further for the
+ * caller to do. Where it returns false, @di has been filled in and emulation
+ * is to continue.
+ */
+static bool __maybe_unused insn_fetch_faulted(const struct guest_fault *gf,
+                                              struct decoded_insn *di)
+{
+    unsigned long htinst = gf->htinst;
+
+    /*
+     * A pseudoinstruction says nothing about the instruction the guest was
+     * executing, and comes with a guest physical address which isn't the one
+     * that instruction accessed. handle_guest_page_fault() deals with such a
+     * fault on its own, so no emulation can ever start for one.
+     */
+    ASSERT(!htinst_is_pseudo(htinst));
+
+    if ( htinst & BIT(0, UL) )
+    {
+        /*
+         * Bit[0] == 1 implies trapped instruction value is
+         * transformed instruction or custom instruction.
+         *
+         * The transformation always yields the 32-bit format, with bits[1:0]
+         * holding a marker instead of the original opcode bits: bit[0] set to
+         * flag the transformation, bit[1] clear if the trapped instruction
+         * was a compressed one. Restoring the opcode bits makes the value the
+         * valid 32-bit encoding decode_ldst_insn() matches against. Its
+         * INSN_MASK_C_* cases exist for the branch below, where a compressed
+         * instruction is read from guest memory as is: a trapped one arrives
+         * here already expanded to its 32-bit equivalent, and the opcode bits
+         * just restored keep it from matching those cases anyway.
+         *
+         * The length then cannot come from the value anymore, only from
+         * bit[1]. And only a 16- or a 32-bit instruction is ever reported
+         * this way: the standard load and store instructions the hardware
+         * transforms are all of one of these two lengths, anything else comes
+         * as the zero special value handled below.
+         */
+        di->insn = htinst | INSN_16BIT_MASK;
+        di->insn_len = (htinst & BIT(1, UL)) ? 4 : 2;
+    }
+    else
+    {
+        const struct cpu_user_regs *regs = gf->regs;
+        struct trap_info utrap = {};
+
+        /*
+         * Bit[0] == 0 implies trapped instruction value is
+         * zero or special value. With the pseudoinstructions ruled out
+         * above, only zero is left: the instruction has to be read from
+         * guest memory.
+         */
+
+        di->insn = riscv_read_guest(regs->sepc, true, &utrap);
+        if ( utrap.scause )
+        {
+            /*
+             * If during getting of trapped instruction a fault happen in
+             * G-stage translation then CAUSE_LOAD_GUEST_PAGE_FAULT is
+             * generated. Such faults during this operation is considered as
+             * bus error.
+             */
+            if ( is_load_guest_page_fault(utrap.scause) )
+                utrap.scause = CAUSE_FETCH_ACCESS;
+
+            utrap.sepc = regs->sepc;
+
+            trap_redirect(&utrap);
+
+            return true;
+        }
+
+        /*
+         * riscv_read_guest() fetches at most two halfwords, so a wider
+         * encoding has been read in part only and cannot be decoded here.
+         *
+         * Report an illegal instruction, which is what the guest would have
+         * got for such an encoding anyway: the ISA defines no instruction
+         * wider than 32 bits.
+         */
+        if ( !INSN_IS_16BIT(di->insn) && !INSN_IS_32BIT(di->insn) )
+        {
+            utrap.sepc = regs->sepc;
+            utrap.scause = CAUSE_ILLEGAL_INSTRUCTION;
+            /*
+             * stval is left zero: the spec allows that for an illegal
+             * instruction, and only part of the instruction is in hand.
+             */
+
+            trap_redirect(&utrap);
+
+            return true;
+        }
+
+        di->insn_len = INSN_LEN(di->insn);
+    }
+
+    return false;
+}
+
+/*
+ * Decode the load or store instruction fetched into @di, filling in the
+ * remaining fields of it (@di->insn and @di->insn_len are filled by
+ * insn_fetch_faulted()).
+ *
+ * @xlen is the effective XLEN of the guest, needed as
+ * the encodings which exist for XLEN=64 only must not be recognized for a
+ * 32-bit guest.
+ *
+ * Returns false if the instruction is not a load or store which can be
+ * emulated here.
+ */
+static __maybe_unused bool decode_ldst_insn(struct decoded_insn *di,
+                                            unsigned int xlen)
+{
+    unsigned long insn = di->insn;
+    /* Register fields of the uncompressed forms ... */
+    unsigned int rd = RV_RD(insn);
+    unsigned int rs2 = RV_RS2(insn);
+    /*
+     * ... and of the compressed ones, where the 3-bit field selects one of
+     * x8..x15, while the stack-pointer-relative forms have a full-width one.
+     */
+    unsigned int rs2s = RVC_RS2S(insn);
+    unsigned int rs2c = RVC_RS2(insn);
+
+    di->is_write = false;
+    di->is_unsigned = false;
+    di->reg = rd;
+
+    if ( (insn & INSN_MASK_LB) == INSN_MATCH_LB )
+        di->len = 1;
+    else if ( (insn & INSN_MASK_LBU) == INSN_MATCH_LBU )
+    {
+        di->len = 1;
+        di->is_unsigned = true;
+    }
+    else if ( (insn & INSN_MASK_LH) == INSN_MATCH_LH )
+        di->len = 2;
+    else if ( (insn & INSN_MASK_LHU) == INSN_MATCH_LHU )
+    {
+        di->len = 2;
+        di->is_unsigned = true;
+    }
+    else if ( (insn & INSN_MASK_LW) == INSN_MATCH_LW )
+        di->len = 4;
+    else if ( xlen == 64 && (insn & INSN_MASK_LWU) == INSN_MATCH_LWU )
+    {
+        di->len = 4;
+        di->is_unsigned = true;
+    }
+    else if ( (insn & INSN_MASK_C_LW) == INSN_MATCH_C_LW )
+    {
+        di->len = 4;
+        di->reg = rs2s;
+    }
+    /* c.lwsp and c.ldsp are reserved with rd being x0. */
+    else if ( (insn & INSN_MASK_C_LWSP) == INSN_MATCH_C_LWSP && rd )
+        di->len = 4;
+    else if ( xlen == 64 && (insn & INSN_MASK_LD) == INSN_MATCH_LD )
+        di->len = 8;
+    else if ( xlen == 64 && (insn & INSN_MASK_C_LD) == INSN_MATCH_C_LD )
+    {
+        di->len = 8;
+        di->reg = rs2s;
+    }
+    else if ( xlen == 64 && (insn & INSN_MASK_C_LDSP) == INSN_MATCH_C_LDSP &&
+              rd )
+        di->len = 8;
+    else if ( (insn & INSN_MASK_SB) == INSN_MATCH_SB )
+    {
+        di->len = 1;
+        di->is_write = true;
+        di->reg = rs2;
+    }
+    else if ( (insn & INSN_MASK_SH) == INSN_MATCH_SH )
+    {
+        di->len = 2;
+        di->is_write = true;
+        di->reg = rs2;
+    }
+    else if ( (insn & INSN_MASK_SW) == INSN_MATCH_SW )
+    {
+        di->len = 4;
+        di->is_write = true;
+        di->reg = rs2;
+    }
+    else if ( (insn & INSN_MASK_C_SW) == INSN_MATCH_C_SW )
+    {
+        di->len = 4;
+        di->is_write = true;
+        di->reg = rs2s;
+    }
+    else if ( (insn & INSN_MASK_C_SWSP) == INSN_MATCH_C_SWSP )
+    {
+        di->len = 4;
+        di->is_write = true;
+        di->reg = rs2c;
+    }
+    else if ( xlen == 64 && (insn & INSN_MASK_SD) == INSN_MATCH_SD )
+    {
+        di->len = 8;
+        di->is_write = true;
+        di->reg = rs2;
+    }
+    else if ( xlen == 64 && (insn & INSN_MASK_C_SD) == INSN_MATCH_C_SD )
+    {
+        di->len = 8;
+        di->is_write = true;
+        di->reg = rs2s;
+    }
+    else if ( xlen == 64 && (insn & INSN_MASK_C_SDSP) == INSN_MATCH_C_SDSP )
+    {
+        di->len = 8;
+        di->is_write = true;
+        di->reg = rs2c;
+    }
+    else
+        return false;
+
+    return true;
+}
+
 static int emulate_load(const struct guest_fault *gf)
 {
     return -EOPNOTSUPP;
diff --git a/xen/arch/riscv/include/asm/guest_access.h b/xen/arch/riscv/include/asm/guest_access.h
index 8d679319ded0..39c28dd2ecaa 100644
--- a/xen/arch/riscv/include/asm/guest_access.h
+++ b/xen/arch/riscv/include/asm/guest_access.h
@@ -5,6 +5,7 @@
 #include <xen/types.h>
 
 struct domain;
+struct trap_info;
 
 unsigned long raw_copy_to_guest(void *to, const void *from, unsigned len);
 unsigned long raw_copy_from_guest(void *to, const void *from, unsigned len);
@@ -25,6 +26,9 @@ unsigned long raw_clear_guest(void *to, unsigned int len);
 unsigned long copy_to_guest_phys(struct domain *d, paddr_t gpa, void *buf,
                                  unsigned long len);
 
+unsigned long riscv_read_guest(unsigned long guest_addr, bool read_insn,
+                               struct trap_info *trap);
+
 #endif /* ASM__RISCV__GUEST_ACCESS_H */
 /*
  * Local variables:
diff --git a/xen/arch/riscv/include/asm/riscv_encoding.h b/xen/arch/riscv/include/asm/riscv_encoding.h
index b2071f47587c..656a5fcccb0e 100644
--- a/xen/arch/riscv/include/asm/riscv_encoding.h
+++ b/xen/arch/riscv/include/asm/riscv_encoding.h
@@ -65,6 +65,14 @@
 #define SSTATUS64_UXL			MSTATUS_UXL
 #define SSTATUS64_SD			MSTATUS64_SD
 
+/*
+ * Width encoded by the MXL, SXL, UXL and VSXL fields, all of which share one
+ * encoding. 0 is reserved.
+ */
+#define XLEN_FIELD_32			_UL(1)
+#define XLEN_FIELD_64			_UL(2)
+#define XLEN_FIELD_128			_UL(3)
+
 #if __riscv_xlen == 64
 #define HSTATUS_VSXL			_UL(0x300000000)
 #define HSTATUS_VSXL_SHIFT		32
@@ -896,6 +904,8 @@
 					 (RV_X(x, 7, 2) << 6))
 #define RVC_SDSP_IMM(x)			((RV_X(x, 10, 3) << 3) | \
 					 (RV_X(x, 7, 3) << 6))
+#define RV_RD(insn)			RV_X(insn, SH_RD, 5)
+#define RV_RS2(insn)			RV_X(insn, SH_RS2, 5)
 #define RVC_RS1S(insn)			(8 + RV_X(insn, SH_RD, 3))
 #define RVC_RS2S(insn)			(8 + RV_X(insn, SH_RS2C, 3))
 #define RVC_RS2(insn)			RV_X(insn, SH_RS2C, 5)
-- 
2.55.0



^ permalink raw reply related	[flat|nested] 244+ messages in thread

* [PATCH v2 25/39] xen/riscv: add guest load emulation for trapped MMIO accesses
  2026-08-27 15:20 [PATCH v2 00/39] [RISC-V] virtual interrupt controller (vAPLIC/vIMSIC) support Oleksii Kurochko
                   ` (23 preceding siblings ...)
  2026-08-27 15:21 ` [PATCH v2 24/39] xen/riscv: add helpers for decoding a trapped load or store Oleksii Kurochko
@ 2026-08-27 15:21 ` Oleksii Kurochko
  2026-09-14 11:48   ` Jan Beulich
  2026-09-18  9:16   ` Baptiste Le Duc
  2026-08-27 15:21 ` [PATCH v2 26/39] xen/riscv: add guest store " Oleksii Kurochko
                   ` (13 subsequent siblings)
  38 siblings, 2 replies; 244+ messages in thread
From: Oleksii Kurochko @ 2026-08-27 15:21 UTC (permalink / raw)
  To: xen-devel
  Cc: Romain Caritey, Baptiste Le Duc, Zheng Zhang, Oleksii Kurochko,
	Alistair Francis, Connor Davis, Andrew Cooper, Anthony PERARD,
	Michal Orzel, Jan Beulich, Julien Grall, Roger Pau Monné,
	Stefano Stabellini

Implement emulate_load() on top of the decoding interface introduced by
the previous patch: fetch the trapped instruction, decode it, dispatch
the access to a registered MMIO handler via do_mmio(), write the result
back into the destination register and step over the instruction.

Xen dispatches MMIO synchronously to an in-hypervisor handler, so unlike
KVM RISC-V there is no userspace exit/return step and no equivalent of
the kvm_io_bus_read() / KVM_EXIT_MMIO / kvm_riscv_vcpu_mmio_return()
split; the result is consumed in place.

Sign extension is done here rather than in the handlers: a signed load
is normalized by a shift pair, so a handler need only report the value
it read.

At the moment vINTC is the only backend registered with the MMIO
dispatch, so in practice this only covers vINTC traps. An access which
no handler claims currently crashes the domain; injecting an access
fault into the guest instead is left for later.

Signed-off-by: Oleksii Kurochko <oleksii.kurochko@gmail.com>
---
Changes in v2:
 - Move the emulation code to the new arch/riscv/emulate.c, leaving traps.c
   with trap dispatch only.
 - Split the patch up: struct decoded_insn and the decoding helpers are
   introduced by "xen/riscv: introduce the interface for trapped instruction
   decoding" and filled in by "xen/riscv: implement trapped instruction
   decoding", so only emulate_load() itself is left here.
 - do_mmio() is no longer defined alongside emulate_load(); it now lives in
   mmio.c, next to the dispatch it drives.
 - Don't write the result of a load into x0. SET_RD() wrote rd
   unconditionally, so a load into x0 clobbered regs->zero and broke the
   invariant that it reads as zero when x0 is a source operand elsewhere.
 - Recognize the XLEN=64-only encodings by the guest's effective XLEN
   (guest_xlen()) rather than by Xen's own (CONFIG_RISCV_32). Besides those
   encodings simply being reserved on RV32, the compressed ones are ambiguous
   there: C.LD and C.FLW share the encoding 0x6000 (mask 0xe003), and likewise
   C.SD/C.FSW, C.LDSP/C.FLWSP and C.SDSP/C.FSWSP.
 - Take the faulting address from struct guest_fault, filled in by
   resolve_faulting_gpa(), rather than from get_faulting_gpa().
 - Drop the description of a fault taken while re-reading the trapped
   instruction: that code is now in the patch implementing
   fetch_trapped_insn(), where a G-stage fault is reported to the guest as
   CAUSE_FETCH_ACCESS instead of hitting a BUG_ON().
 - Update the commit message accordingly.
---
---
 xen/arch/riscv/emulate.c | 53 +++++++++++++++++++++++++++++++---------
 1 file changed, 42 insertions(+), 11 deletions(-)

diff --git a/xen/arch/riscv/emulate.c b/xen/arch/riscv/emulate.c
index 81a50643a5ec..e52f2851800c 100644
--- a/xen/arch/riscv/emulate.c
+++ b/xen/arch/riscv/emulate.c
@@ -5,7 +5,6 @@
  */
 
 #include <xen/bug.h>
-#include <xen/compiler.h>
 #include <xen/errno.h>
 #include <xen/sched.h>
 #include <xen/types.h>
@@ -15,6 +14,7 @@
 #include <asm/current.h>
 #include <asm/emulate.h>
 #include <asm/guest_access.h>
+#include <asm/mmio.h>
 #include <asm/processor.h>
 #include <asm/riscv_encoding.h>
 #include <asm/traps.h>
@@ -65,8 +65,7 @@ static bool is_load_guest_page_fault(unsigned long scause)
     return scause == CAUSE_LOAD_GUEST_PAGE_FAULT;
 }
 
-static __maybe_unused void advance_pc(struct cpu_user_regs *regs,
-                                      unsigned int step)
+static void advance_pc(struct cpu_user_regs *regs, unsigned int step)
 {
     regs->sepc += step;
 }
@@ -88,7 +87,7 @@ static __maybe_unused void advance_pc(struct cpu_user_regs *regs,
  * IS_ENABLED() can't be used here as HSTATUS_VSXL is defined for
  * __riscv_xlen == 64 only, the field not existing on RV32 in the first place.
  */
-static __maybe_unused unsigned int guest_xlen(const struct cpu_user_regs *regs)
+static unsigned int guest_xlen(const struct cpu_user_regs *regs)
 {
 #ifdef CONFIG_RISCV_32
     return 32;
@@ -179,8 +178,7 @@ static void resolve_faulting_gpa(struct guest_fault *gf)
  * Relies on x0..x31 being laid out at the start of struct cpu_user_regs in
  * architectural register-number order; see the comment there.
  */
-static __maybe_unused unsigned long *guest_gpr(struct cpu_user_regs *regs,
-                                               unsigned int reg)
+static unsigned long *guest_gpr(struct cpu_user_regs *regs, unsigned int reg)
 {
     ASSERT(reg < 32);
 
@@ -197,8 +195,8 @@ static __maybe_unused unsigned long *guest_gpr(struct cpu_user_regs *regs,
  * caller to do. Where it returns false, @di has been filled in and emulation
  * is to continue.
  */
-static bool __maybe_unused insn_fetch_faulted(const struct guest_fault *gf,
-                                              struct decoded_insn *di)
+static bool insn_fetch_faulted(const struct guest_fault *gf,
+                               struct decoded_insn *di)
 {
     unsigned long htinst = gf->htinst;
 
@@ -306,8 +304,7 @@ static bool __maybe_unused insn_fetch_faulted(const struct guest_fault *gf,
  * Returns false if the instruction is not a load or store which can be
  * emulated here.
  */
-static __maybe_unused bool decode_ldst_insn(struct decoded_insn *di,
-                                            unsigned int xlen)
+static bool decode_ldst_insn(struct decoded_insn *di, unsigned int xlen)
 {
     unsigned long insn = di->insn;
     /* Register fields of the uncompressed forms ... */
@@ -419,7 +416,41 @@ static __maybe_unused bool decode_ldst_insn(struct decoded_insn *di,
 
 static int emulate_load(const struct guest_fault *gf)
 {
-    return -EOPNOTSUPP;
+    struct cpu_user_regs *regs = gf->regs;
+    mmio_info_t info = { .is_write = false };
+    struct decoded_insn di;
+    unsigned int shift = 0;
+    int rc;
+
+    /* A fault taken re-reading the instruction is redirected to the guest. */
+    if ( insn_fetch_faulted(gf, &di) )
+        return 0;
+
+    if ( !decode_ldst_insn(&di, guest_xlen(regs)) || di.is_write )
+        return -EOPNOTSUPP;
+
+    if ( !di.is_unsigned )
+        shift = BITS_PER_BYTE * (sizeof(unsigned long) - di.len);
+
+#ifdef EMULATE_LOAD_DEBUG
+    gdprintk(XENLOG_DEBUG, "pc=%#lx, addr=%#"PRIpaddr", len=%u, shift=%u\n",
+             regs->sepc, gf->gpa, di.len, shift);
+#endif
+
+    rc = do_mmio(&info, gf->gpa, di.len);
+    if ( rc )
+        return rc;
+
+    /*
+     * A load into x0 discards its result: writing regs->zero would break the
+     * invariant that it reads as zero when x0 is a source operand elsewhere.
+     */
+    if ( di.reg )
+        *guest_gpr(regs, di.reg) = (long)(info.data << shift) >> shift;
+
+    advance_pc(regs, di.insn_len);
+
+    return 0;
 }
 
 static int emulate_store(struct guest_fault *gf)
-- 
2.55.0



^ permalink raw reply related	[flat|nested] 244+ messages in thread

* [PATCH v2 26/39] xen/riscv: add guest store emulation for trapped MMIO accesses
  2026-08-27 15:20 [PATCH v2 00/39] [RISC-V] virtual interrupt controller (vAPLIC/vIMSIC) support Oleksii Kurochko
                   ` (24 preceding siblings ...)
  2026-08-27 15:21 ` [PATCH v2 25/39] xen/riscv: add guest load emulation for trapped MMIO accesses Oleksii Kurochko
@ 2026-08-27 15:21 ` Oleksii Kurochko
  2026-09-14 12:01   ` Jan Beulich
  2026-09-18  9:16   ` Baptiste Le Duc
  2026-08-27 15:21 ` [PATCH v2 27/39] xen/riscv: introduce arch_move_irqs() Oleksii Kurochko
                   ` (12 subsequent siblings)
  38 siblings, 2 replies; 244+ messages in thread
From: Oleksii Kurochko @ 2026-08-27 15:21 UTC (permalink / raw)
  To: xen-devel
  Cc: Romain Caritey, Baptiste Le Duc, Zheng Zhang, Oleksii Kurochko,
	Alistair Francis, Connor Davis, Andrew Cooper, Anthony PERARD,
	Michal Orzel, Jan Beulich, Julien Grall, Roger Pau Monné,
	Stefano Stabellini

Extend the guest page fault handler with store emulation to support MMIO
write accesses.

The instruction decode mirrors emulate_load() and, like it, is adapted
from Linux's KVM RISC-V implementation. As with the load path, the
completion is synchronous through try_handle_mmio() rather than KVM's
userspace exit/return split, since Xen's MMIO handlers run in the
hypervisor. Faults taken while re-reading the trapped instruction are
handled by decode_ldst_insn(), shared with the load path.

When a guest store instruction faults, the trapped instruction is decoded
using HTINST or, if unavailable, fetched via unprivileged access. At the
moment only virtual interrupt controller (vINTC) traps are expected to
occur, since it is currently the only backend registered with the MMIO
handler dispatch, so in practice the store is emulated via the vINTC
backend.

Together with load emulation, this completes the basic MMIO handling path
needed for virtual interrupt controller support on RISC-V.

Signed-off-by: Oleksii Kurochko <oleksii.kurochko@gmail.com>
---
Changes in v2:
 - Move the emulation code to the new arch/riscv/emulate.c, leaving traps.c
   with trap dispatch only.
 - Split the patch up: the instruction fetch and the mask/match chain now
   live in "xen/riscv: add helpers for decoding a trapped load or store"
   (struct decoded_insn, insn_fetch_faulted(), decode_ldst_insn(),
   guest_gpr(), advance_pc()), so only emulate_store() itself is left here.
 - Since the decoder is now shared with the load path, reject an encoding
   which is not a store (!di.is_write) explicitly; in v2 the mask/match
   chain was store-only and could not match a load.
 - Recognize the XLEN=64-only encodings by the guest's effective XLEN
   (guest_xlen()) rather than by Xen's own (CONFIG_RISCV_32). Besides those
   encodings simply being reserved on RV32, the compressed ones are ambiguous
   there: C.SD and C.FSW share an encoding, and likewise C.SDSP and C.FSWSP.
 - Read the source register through guest_gpr() instead of the
   GET_RS2()/GET_RS2S()/GET_RS2C() macros; which of the three register
   fields an encoding names is now decided by decode_ldst_insn().
 - Take the faulting address from struct guest_fault, filled in by
   resolve_faulting_gpa(), rather than from the fault_addr parameter.
 - Drop the description of a fault taken while re-reading the trapped
   instruction: that code is now in the patch adding the decoding helpers,
   where a G-stage fault is reported to the guest as CAUSE_FETCH_ACCESS.
 - Update the commit message accordingly.
---
---
 xen/arch/riscv/emulate.c | 23 +++++++++++++++++++++--
 1 file changed, 21 insertions(+), 2 deletions(-)

diff --git a/xen/arch/riscv/emulate.c b/xen/arch/riscv/emulate.c
index e52f2851800c..1535b843525e 100644
--- a/xen/arch/riscv/emulate.c
+++ b/xen/arch/riscv/emulate.c
@@ -453,9 +453,28 @@ static int emulate_load(const struct guest_fault *gf)
     return 0;
 }
 
-static int emulate_store(struct guest_fault *gf)
+static int emulate_store(const struct guest_fault *gf)
 {
-    return -EOPNOTSUPP;
+    struct cpu_user_regs *regs = gf->regs;
+    mmio_info_t info = { .is_write = true };
+    struct decoded_insn di;
+    int rc;
+
+    if ( insn_fetch_faulted(gf, &di) )
+        return 0;
+
+    if ( !decode_ldst_insn(&di, guest_xlen(regs)) || !di.is_write )
+        return -EOPNOTSUPP;
+
+    info.data = *guest_gpr(regs, di.reg);
+
+    rc = do_mmio(&info, gf->gpa, di.len);
+    if ( rc )
+        return rc;
+
+    advance_pc(regs, di.insn_len);
+
+    return 0;
 }
 
 static void inject_access_fault(const struct guest_fault *gf)
-- 
2.55.0



^ permalink raw reply related	[flat|nested] 244+ messages in thread

* [PATCH v2 27/39] xen/riscv: introduce arch_move_irqs()
  2026-08-27 15:20 [PATCH v2 00/39] [RISC-V] virtual interrupt controller (vAPLIC/vIMSIC) support Oleksii Kurochko
                   ` (25 preceding siblings ...)
  2026-08-27 15:21 ` [PATCH v2 26/39] xen/riscv: add guest store " Oleksii Kurochko
@ 2026-08-27 15:21 ` Oleksii Kurochko
  2026-09-14 12:07   ` Jan Beulich
  2026-09-22 17:00   ` Baptiste Le Duc
  2026-08-27 15:21 ` [PATCH v2 28/39] xen/riscv: handle the case when no vCPU migration is needed Oleksii Kurochko
                   ` (11 subsequent siblings)
  38 siblings, 2 replies; 244+ messages in thread
From: Oleksii Kurochko @ 2026-08-27 15:21 UTC (permalink / raw)
  To: xen-devel
  Cc: Romain Caritey, Baptiste Le Duc, Zheng Zhang, Oleksii Kurochko,
	Alistair Francis, Connor Davis, Andrew Cooper, Anthony PERARD,
	Michal Orzel, Jan Beulich, Julien Grall, Roger Pau Monné,
	Stefano Stabellini

When migrating a vCPU between pCPUs the hypervisor must also migrate
the associated virtual interrupt state. arch_move_irqs() is the
per-arch hook called by generic code to trigger that.

Replace the static inline BUG_ON placeholder in asm/irq.h with a real
implementation in intc.c dispatching through a new move_irqs vintc_ops
callback. Wire it up in vAPLIC, which delegates to imsic_migrate_vcpu()
which itself still a stub to be implemented in follow-up patches.

Note that technically ASSERT() in arch_move_irqs() could be skipped as
it will be anyway NULL pointer dereference (and a trap will occur) if
something isn't properly initialized but sometimes it is harder to
find place where NULL pointer derefence happened as it isn't
guaraunted that all necessary registers will be filled with something
useful.
As at the moment I don't find any case when ->move_irqs() could be
skipped, the check that ->move_irq isn't NULL is added to ASSERT()
instead of adding "if ( ...->move_irq) vitnc->ops->move_irqs(v)".

Signed-off-by: Oleksii Kurochko <oleksii.kurochko@gmail.com>
---
Changes in v2:
 - New patch.
---
---
 xen/arch/riscv/imsic.c             | 5 +++++
 xen/arch/riscv/include/asm/imsic.h | 2 ++
 xen/arch/riscv/include/asm/intc.h  | 3 +++
 xen/arch/riscv/include/asm/irq.h   | 5 +----
 xen/arch/riscv/intc.c              | 8 ++++++++
 xen/arch/riscv/vaplic.c            | 1 +
 6 files changed, 20 insertions(+), 4 deletions(-)

diff --git a/xen/arch/riscv/imsic.c b/xen/arch/riscv/imsic.c
index 3787f270d8e3..b0c4a9e2d728 100644
--- a/xen/arch/riscv/imsic.c
+++ b/xen/arch/riscv/imsic.c
@@ -686,3 +686,8 @@ int __init vimsic_make_domu_dt_node(struct kernel_info *kinfo,
 
     return fdt_end_node(fdt);
 }
+
+void imsic_migrate_vcpu(struct vcpu *v)
+{
+    BUG_ON("unimplemented");
+}
diff --git a/xen/arch/riscv/include/asm/imsic.h b/xen/arch/riscv/include/asm/imsic.h
index 73129c3c9ea7..57d8c729ac0d 100644
--- a/xen/arch/riscv/include/asm/imsic.h
+++ b/xen/arch/riscv/include/asm/imsic.h
@@ -112,4 +112,6 @@ int vimsic_make_domu_dt_node(struct kernel_info *kinfo, unsigned int *phandle);
 void imsic_ctxt_switch_from(struct vcpu *v);
 void imsic_ctxt_switch_to(struct vcpu *v);
 
+void imsic_migrate_vcpu(struct vcpu *v);
+
 #endif /* ASM_RISCV_IMSIC_H */
diff --git a/xen/arch/riscv/include/asm/intc.h b/xen/arch/riscv/include/asm/intc.h
index 62e1410156c7..b5ab39aa2b39 100644
--- a/xen/arch/riscv/include/asm/intc.h
+++ b/xen/arch/riscv/include/asm/intc.h
@@ -70,6 +70,9 @@ struct vintc_ops {
 
     /* Restore vINTC state of the vCPU being switched in */
     void (*ctxt_switch_to)(struct vcpu *v);
+
+    /* Move interrupts of vCPU to a different pCPU */
+    void (*move_irqs)(struct vcpu *v);
 };
 
 struct vintc {
diff --git a/xen/arch/riscv/include/asm/irq.h b/xen/arch/riscv/include/asm/irq.h
index 66067747dc0f..314b8ee0e00c 100644
--- a/xen/arch/riscv/include/asm/irq.h
+++ b/xen/arch/riscv/include/asm/irq.h
@@ -41,10 +41,7 @@ struct irq_desc *irq_to_desc(unsigned int irq);
 struct cpu_user_regs;
 struct dt_device_node;
 
-static inline void arch_move_irqs(struct vcpu *v)
-{
-    BUG_ON("unimplemented");
-}
+void arch_move_irqs(struct vcpu *v);
 
 int platform_get_irq(const struct dt_device_node *device, int index);
 
diff --git a/xen/arch/riscv/intc.c b/xen/arch/riscv/intc.c
index 9fff501b9c25..b3a16ae9be67 100644
--- a/xen/arch/riscv/intc.c
+++ b/xen/arch/riscv/intc.c
@@ -192,3 +192,11 @@ void vintc_ctxt_switch_to(struct vcpu *v)
 
     ops->ctxt_switch_to(v);
 }
+
+/* Move vCPU's IRQs from one pCPU to another */
+void arch_move_irqs(struct vcpu *v)
+{
+    const struct vintc_ops *ops = v->domain->arch.vintc->ops;
+
+    ops->move_irqs(v);
+}
diff --git a/xen/arch/riscv/vaplic.c b/xen/arch/riscv/vaplic.c
index 6c60fe2baf0c..0c75ba2fb2fe 100644
--- a/xen/arch/riscv/vaplic.c
+++ b/xen/arch/riscv/vaplic.c
@@ -429,6 +429,7 @@ static const struct vintc_ops vintc_ops = {
      */
     .ctxt_switch_from = imsic_ctxt_switch_from,
     .ctxt_switch_to = imsic_ctxt_switch_to,
+    .move_irqs = imsic_migrate_vcpu,
 };
 
 int domain_vaplic_init(struct domain *d)
-- 
2.55.0



^ permalink raw reply related	[flat|nested] 244+ messages in thread

* [PATCH v2 28/39] xen/riscv: handle the case when no vCPU migration is needed
  2026-08-27 15:20 [PATCH v2 00/39] [RISC-V] virtual interrupt controller (vAPLIC/vIMSIC) support Oleksii Kurochko
                   ` (26 preceding siblings ...)
  2026-08-27 15:21 ` [PATCH v2 27/39] xen/riscv: introduce arch_move_irqs() Oleksii Kurochko
@ 2026-08-27 15:21 ` Oleksii Kurochko
  2026-09-14 12:12   ` Jan Beulich
  2026-08-27 15:21 ` [PATCH v2 29/39] xen/riscv: introduce aplic_reconfigure_target() Oleksii Kurochko
                   ` (10 subsequent siblings)
  38 siblings, 1 reply; 244+ messages in thread
From: Oleksii Kurochko @ 2026-08-27 15:21 UTC (permalink / raw)
  To: xen-devel
  Cc: Romain Caritey, Baptiste Le Duc, Zheng Zhang, Oleksii Kurochko,
	Alistair Francis, Connor Davis, Andrew Cooper, Anthony PERARD,
	Michal Orzel, Jan Beulich, Julien Grall, Roger Pau Monné,
	Stefano Stabellini

The IMSIC vsfile mapping is performed in continue_new_vcpu(), since the
target pCPU must be known at that point. It is therefore possible for
imsic_migrate_vcpu() to be called before continue_new_vcpu() has
executed, in which case v->arch.last_pcpu is NR_CPUS and there is nothing
to migrate.

Add a BUG_ON("unimplemented") placeholder in imsic_migrate_vcpu() to guard
against silent incorrect behaviour or unexpected panics in guest VMs until
the function is fully implemented.

Signed-off-by: Oleksii Kurochko <oleksii.kurochko@gmail.com>
---
Changes in v2:
 - New patch.
---
---
 xen/arch/riscv/imsic.c | 10 ++++++++++
 1 file changed, 10 insertions(+)

diff --git a/xen/arch/riscv/imsic.c b/xen/arch/riscv/imsic.c
index b0c4a9e2d728..ad7fbe708bfd 100644
--- a/xen/arch/riscv/imsic.c
+++ b/xen/arch/riscv/imsic.c
@@ -689,5 +689,15 @@ int __init vimsic_make_domu_dt_node(struct kernel_info *kinfo,
 
 void imsic_migrate_vcpu(struct vcpu *v)
 {
+    /*
+     * The scheduler can mark a freshly created vCPU's unit as migrated and
+     * invoke this before the vCPU has ever run (see the migrated branch in
+     * schedule()). No need to do migration for such vCPUs as they aren't fully
+     * initialized (for example, context_switch() will be called after
+     * imsic_migrate_vcpu()).
+     */
+    if ( v->arch.last_cpu == NR_CPUS )
+        return;
+
     BUG_ON("unimplemented");
 }
-- 
2.55.0



^ permalink raw reply related	[flat|nested] 244+ messages in thread

* [PATCH v2 29/39] xen/riscv: introduce aplic_reconfigure_target()
  2026-08-27 15:20 [PATCH v2 00/39] [RISC-V] virtual interrupt controller (vAPLIC/vIMSIC) support Oleksii Kurochko
                   ` (27 preceding siblings ...)
  2026-08-27 15:21 ` [PATCH v2 28/39] xen/riscv: handle the case when no vCPU migration is needed Oleksii Kurochko
@ 2026-08-27 15:21 ` Oleksii Kurochko
  2026-09-14 12:25   ` Jan Beulich
  2026-08-27 15:21 ` [PATCH v2 30/39] xen/riscv: prepare new IMSIC VS-file Oleksii Kurochko
                   ` (9 subsequent siblings)
  38 siblings, 1 reply; 244+ messages in thread
From: Oleksii Kurochko @ 2026-08-27 15:21 UTC (permalink / raw)
  To: xen-devel
  Cc: Romain Caritey, Baptiste Le Duc, Zheng Zhang, Oleksii Kurochko,
	Alistair Francis, Connor Davis, Andrew Cooper, Anthony PERARD,
	Michal Orzel, Jan Beulich, Julien Grall, Roger Pau Monné,
	Stefano Stabellini

When a vCPU is migrated to a different pCPU, its IMSIC guest interrupt
file changes. Any APLIC interrupt previously configured to deliver an
MSI to the old interrupt file must be retargeted to the new one.

Implement aplic_reconfigure_target() to scan all interrupts allocated
to the domain and update their APLIC TARGET registers accordingly.

Signed-off-by: Oleksii Kurochko <oleksii.kurochko@gmail.com>
---
Changes in v2:
 - New patch.
---
---
 xen/arch/riscv/aplic.c             | 42 ++++++++++++++++++++++++++++++
 xen/arch/riscv/include/asm/aplic.h |  4 +++
 2 files changed, 46 insertions(+)

diff --git a/xen/arch/riscv/aplic.c b/xen/arch/riscv/aplic.c
index b4c419755ac3..0af13f28e467 100644
--- a/xen/arch/riscv/aplic.c
+++ b/xen/arch/riscv/aplic.c
@@ -138,6 +138,48 @@ uint32_t aplic_msi_target_gen(const struct vcpu *target_vcpu,
     return base_val;
 }
 
+void aplic_reconfigure_target(const struct vcpu *v,
+                              unsigned int old_guest_file_id,
+                              unsigned int old_cpu)
+{
+    const struct vintc *vintc = v->domain->arch.vintc;
+    const unsigned long *auth_irq_bmp = vintc->used_irqs;
+    unsigned long old_hart_field = aplic_hart_field(old_cpu);
+    unsigned long flags;
+    unsigned int irqn;
+
+    /* Support only MSI mode at the moment */
+    BUG_ON(!aplic_msi_mode());
+
+    spin_lock_irqsave(&aplic.lock, flags);
+
+    bitmap_for_each ( irqn, auth_irq_bmp, vintc->nr_virqs )
+    {
+        volatile uint32_t __iomem *ptarget;
+        uint32_t target_val;
+        unsigned int guest_index, hart_index;
+
+        if ( !irqn )
+            continue;
+
+        ptarget = &aplic.regs->target[irqn - 1];
+        target_val = readl(ptarget);
+
+        guest_index = MASK_EXTR(target_val, APLIC_TARGET_GUEST_IDX);
+        hart_index = MASK_EXTR(target_val, APLIC_TARGET_HART_IDX);
+
+        if ( (guest_index != old_guest_file_id) ||
+             (hart_index != old_hart_field) )
+            continue;
+
+        target_val = aplic_msi_target_gen(v, target_val);
+
+        writel(target_val, ptarget);
+    }
+
+    spin_unlock_irqrestore(&aplic.lock, flags);
+}
+
 uint32_t aplic_hw_read_reg(unsigned int offset)
 {
     unsigned long flags;
diff --git a/xen/arch/riscv/include/asm/aplic.h b/xen/arch/riscv/include/asm/aplic.h
index d629e1c83887..8564f5954b6b 100644
--- a/xen/arch/riscv/include/asm/aplic.h
+++ b/xen/arch/riscv/include/asm/aplic.h
@@ -174,4 +174,8 @@ struct aplic_regs {
 uint32_t aplic_hw_read_reg(unsigned int offset);
 void aplic_hw_write_reg(unsigned int offset, uint32_t value);
 
+void aplic_reconfigure_target(const struct vcpu *v,
+                              unsigned int old_guest_file_id,
+                              unsigned int old_cpu);
+
 #endif /* ASM_RISCV_APLIC_H */
-- 
2.55.0



^ permalink raw reply related	[flat|nested] 244+ messages in thread

* [PATCH v2 30/39] xen/riscv: prepare new IMSIC VS-file
  2026-08-27 15:20 [PATCH v2 00/39] [RISC-V] virtual interrupt controller (vAPLIC/vIMSIC) support Oleksii Kurochko
                   ` (28 preceding siblings ...)
  2026-08-27 15:21 ` [PATCH v2 29/39] xen/riscv: introduce aplic_reconfigure_target() Oleksii Kurochko
@ 2026-08-27 15:21 ` Oleksii Kurochko
  2026-09-14 13:13   ` Jan Beulich
  2026-09-21 16:15   ` Baptiste Le Duc
  2026-08-27 15:21 ` [PATCH v2 31/39] xen/riscv: implement APLIC-hart sync barrier for vCPU migration Oleksii Kurochko
                   ` (8 subsequent siblings)
  38 siblings, 2 replies; 244+ messages in thread
From: Oleksii Kurochko @ 2026-08-27 15:21 UTC (permalink / raw)
  To: xen-devel
  Cc: Romain Caritey, Baptiste Le Duc, Zheng Zhang, Oleksii Kurochko,
	Alistair Francis, Connor Davis, Andrew Cooper, Anthony PERARD,
	Michal Orzel, Jan Beulich, Julien Grall, Roger Pau Monné,
	Stefano Stabellini

Implement first steps of migration a vCPU to a different guest interrupt file
procedure:
- At the old interrupt file, save to memory the values of registers
  eidelivery and eithreshold, and set eidelivery = 0.
- At the new interrupt file, set eidelivery = 0, and zero all
  implemented interrupt-pending bits (the eip array).

The following steps will be introduced in follow-up patches.

Add a BUG_ON("unimplemented") placeholder in imsic_migrate_vcpu() to guard
against silent incorrect behaviour or unexpected panics in guest VMs until
the function is fully implemented.

vgein_assign() will be introduced later in a separate patch, for not it is
only stub.

Signed-off-by: Oleksii Kurochko <oleksii.kurochko@gmail.com>
---
Changes in v2:
 - New patch.
---
---
 xen/arch/riscv/aia.c             |   9 ++
 xen/arch/riscv/imsic.c           | 159 +++++++++++++++++++++++++++++++
 xen/arch/riscv/include/asm/aia.h |   4 +
 xen/include/xen/config.h         |   1 +
 4 files changed, 173 insertions(+)

diff --git a/xen/arch/riscv/aia.c b/xen/arch/riscv/aia.c
index e31c9c2d24b6..75c82bcfa1b3 100644
--- a/xen/arch/riscv/aia.c
+++ b/xen/arch/riscv/aia.c
@@ -1,8 +1,10 @@
 /* SPDX-License-Identifier: GPL-2.0-only */
 
+#include <xen/bug.h>
 #include <xen/errno.h>
 #include <xen/init.h>
 #include <xen/sections.h>
+#include <xen/sched.h>
 #include <xen/types.h>
 
 #include <asm/cpufeature.h>
@@ -21,3 +23,10 @@ void __init aia_init(void)
 
     _aia_usable = true;
 }
+
+unsigned int vgein_assign(struct vcpu *v)
+{
+    BUG_ON("unimplemented\n");
+
+    return 0;
+}
diff --git a/xen/arch/riscv/imsic.c b/xen/arch/riscv/imsic.c
index ad7fbe708bfd..516f0105352a 100644
--- a/xen/arch/riscv/imsic.c
+++ b/xen/arch/riscv/imsic.c
@@ -26,6 +26,7 @@
 #include <xen/spinlock.h>
 #include <xen/xvmalloc.h>
 
+#include <asm/aia.h>
 #include <asm/imsic.h>
 
 #define IMSIC_HART_SIZE(guest_bits) (BIT(guest_bits, U) * IMSIC_MMIO_PAGE_SZ)
@@ -77,6 +78,64 @@ do {                            \
     csr_clear(CSR_SIREG, v);    \
 } while (0)
 
+#define imsic_vs_csr_write(c, v)    \
+do {                                \
+    csr_write(CSR_VSISELECT, (c));  \
+    csr_write(CSR_VSIREG, (v));     \
+} while ( 0 )
+
+/*
+ * Generic switchcase expansion pyramid.
+ * F is the per-operation leaf macro, ireg is the base register index.
+ * Optional extra args (e.g. an operation and/or a value) are forwarded to F
+ * via __VA_ARGS__.
+ *
+ * imsic_switchcase_break(ireg, op, v) - emit "case ireg: op(ireg,v); break;"
+ * imsic_switchcase_ret(ireg, op, ...) - emit "case ireg: return op(ireg[,v]);"
+ *   The variadic tail is optional so the same leaf works for both read (no v)
+ *   and swap (with v).
+ */
+#define imsic_switchcase_break(ireg, op, v) \
+    case ireg:                              \
+        op(ireg, v);                        \
+        break;
+
+#define imsic_switchcase_ret(ireg, op, ...) \
+    case ireg:                              \
+        return op(ireg, ##__VA_ARGS__);
+
+#define imsic_switchcase_2(F, ireg, ...)    \
+    F(ireg + 0, ##__VA_ARGS__)              \
+    F(ireg + 1, ##__VA_ARGS__)
+#define imsic_switchcase_4(F, ireg, ...)    \
+    imsic_switchcase_2(F, ireg + 0, ##__VA_ARGS__)  \
+    imsic_switchcase_2(F, ireg + 2, ##__VA_ARGS__)
+#define imsic_switchcase_8(F, ireg, ...)    \
+    imsic_switchcase_4(F, ireg + 0, ##__VA_ARGS__)  \
+    imsic_switchcase_4(F, ireg + 4, ##__VA_ARGS__)
+#define imsic_switchcase_16(F, ireg, ...)   \
+    imsic_switchcase_8(F, ireg + 0, ##__VA_ARGS__)  \
+    imsic_switchcase_8(F, ireg + 8, ##__VA_ARGS__)
+#define imsic_switchcase_32(F, ireg, ...)   \
+    imsic_switchcase_16(F, ireg + 0, ##__VA_ARGS__) \
+    imsic_switchcase_16(F, ireg + 16, ##__VA_ARGS__)
+#define imsic_switchcase_64(F, ireg, ...)   \
+    imsic_switchcase_32(F, ireg + 0, ##__VA_ARGS__) \
+    imsic_switchcase_32(F, ireg + 32, ##__VA_ARGS__)
+
+static void imsic_eix_write(unsigned int ireg, unsigned long val)
+{
+    switch ( ireg )
+    {
+    imsic_switchcase_64(imsic_switchcase_break, IMSIC_EIP0,
+                        imsic_vs_csr_write, val)
+    imsic_switchcase_64(imsic_switchcase_break, IMSIC_EIE0,
+                        imsic_vs_csr_write, val)
+    default:
+        ASSERT_UNREACHABLE();
+    }
+}
+
 unsigned int vcpu_guest_file_id(const struct vcpu *v)
 {
     return ACCESS_ONCE(v->arch.vimsic_state->guest_file_id);
@@ -389,6 +448,76 @@ int cf_check vcpu_imsic_init(struct vcpu *v)
     return 0;
 }
 
+/*
+ * Arguments of the imsic_vsfile_local_*() helpers, which are executed by the
+ * pCPU owning the interrupt file, thereby through imsic_call_on_cpu().
+ */
+struct imsic_vsfile_data {
+    unsigned int hgei;
+    unsigned int nr_eix;
+    struct imsic_mrif *mrif;
+};
+
+/*
+ * Execute func() on the pCPU which owns the IMSIC interrupt file func() is
+ * going to work with.
+ *
+ * An IMSIC VS-file is reachable only through hstatus.VGEIN of the hart the
+ * file belongs to, and a guest interrupt file index is meaningless on any
+ * other hart, so such work always has to be done by that very hart.
+ *
+ * The local case runs with IRQs disabled to provide func() with the same
+ * environment it is given when it is called from the function call IPI
+ * handler.
+ */
+static void imsic_call_on_cpu(unsigned int cpu, void (*func)(void *),
+                              void *data)
+{
+    if ( cpu == smp_processor_id() )
+    {
+        unsigned long flags;
+
+        local_irq_save(flags);
+        func(data);
+        local_irq_restore(flags);
+    }
+    else
+        on_selected_cpus(cpumask_of(cpu), func, data, 1);
+}
+
+static void cf_check imsic_vsfile_local_clear(void *data)
+{
+    unsigned int i;
+    const struct imsic_vsfile_data *idata = data;
+    unsigned long new_hstatus, old_hstatus, old_vsiselect;
+
+    /* We can only zero-out if we have a IMSIC VS-file */
+    if ( !idata->hgei )
+        return;
+
+    old_vsiselect = csr_read(CSR_VSISELECT);
+    old_hstatus = csr_read(CSR_HSTATUS);
+    new_hstatus = old_hstatus & ~HSTATUS_VGEIN;
+    new_hstatus |= MASK_INSR(idata->hgei, HSTATUS_VGEIN);
+    csr_write(CSR_HSTATUS, new_hstatus);
+
+    imsic_vs_csr_write(IMSIC_EIDELIVERY, 0);
+    imsic_vs_csr_write(IMSIC_EITHRESHOLD, 0);
+
+    for ( i = 0; i < idata->nr_eix; i++ )
+    {
+        imsic_eix_write(IMSIC_EIP0 + i * 2, 0);
+        imsic_eix_write(IMSIC_EIE0 + i * 2, 0);
+#ifdef CONFIG_RISCV_32
+        imsic_eix_write(IMSIC_EIP0 + i * 2 + 1, 0);
+        imsic_eix_write(IMSIC_EIE0 + i * 2 + 1, 0);
+#endif
+    }
+
+    csr_write(CSR_HSTATUS, old_hstatus);
+    csr_write(CSR_VSISELECT, old_vsiselect);
+}
+
 void cf_check vcpu_imsic_deinit(struct vcpu *v)
 {
     XVFREE(v->arch.vimsic_state);
@@ -689,6 +818,14 @@ int __init vimsic_make_domu_dt_node(struct kernel_info *kinfo,
 
 void imsic_migrate_vcpu(struct vcpu *v)
 {
+    unsigned int new_vsfile_hgei;
+    unsigned int new_vsfile_cpu;
+    unsigned int nr_hw_eix = DIV_ROUND_UP(imsic_cfg.nr_ids + 1,
+                                          BITS_PER_TYPE(uint64_t));
+    struct imsic_vsfile_data vsfile_data = {
+        .nr_eix = nr_hw_eix,
+    };
+
     /*
      * The scheduler can mark a freshly created vCPU's unit as migrated and
      * invoke this before the vCPU has ever run (see the migrated branch in
@@ -699,5 +836,27 @@ void imsic_migrate_vcpu(struct vcpu *v)
     if ( v->arch.last_cpu == NR_CPUS )
         return;
 
+    /*
+     * At this point, all interrupt producers are still using the old IMSIC
+     * VS-file.
+     */
+
+    /*
+     * Latch the pCPU the new interrupt file is taken from: vgein_assign()
+     * allocates it from v->processor's pool of guest interrupt files, and
+     * only that hart can access the file afterwards.
+     */
+    new_vsfile_cpu = v->processor;
+
+    new_vsfile_hgei = vgein_assign(v);
+
+    /* We don't support SW interrupt files at the moment. */
+    BUG_ON(!new_vsfile_hgei);
+
+    vsfile_data.hgei = new_vsfile_hgei;
+
+    /* Zero-out new IMSIC VS-file */
+    imsic_call_on_cpu(new_vsfile_cpu, imsic_vsfile_local_clear, &vsfile_data);
+
     BUG_ON("unimplemented");
 }
diff --git a/xen/arch/riscv/include/asm/aia.h b/xen/arch/riscv/include/asm/aia.h
index aaa4bf91fc75..53a1efb042f8 100644
--- a/xen/arch/riscv/include/asm/aia.h
+++ b/xen/arch/riscv/include/asm/aia.h
@@ -3,8 +3,12 @@
 #ifndef RISCV_AIA_H
 #define RISCV_AIA_H
 
+struct vcpu;
+
 bool aia_usable(void);
 
 void aia_init(void);
 
+unsigned int vgein_assign(struct vcpu *v);
+
 #endif /* RISCV_AIA_H */
diff --git a/xen/include/xen/config.h b/xen/include/xen/config.h
index dddc8e1920fe..0e29976e8203 100644
--- a/xen/include/xen/config.h
+++ b/xen/include/xen/config.h
@@ -100,6 +100,7 @@
 #define BITS_PER_INT    (BITS_PER_BYTE * __SIZEOF_INT__)
 #define BITS_PER_LONG   (BITS_PER_BYTE * BYTES_PER_LONG)
 #define BITS_PER_LLONG  (BITS_PER_BYTE * __SIZEOF_LONG_LONG__)
+#define BITS_PER_TYPE(type) (sizeof(type) * BITS_PER_BYTE)
 
 /* It is assumed that sizeof(void *) == __alignof(void *) */
 #define POINTER_ALIGN   __SIZEOF_POINTER__
-- 
2.55.0



^ permalink raw reply related	[flat|nested] 244+ messages in thread

* [PATCH v2 31/39] xen/riscv: implement APLIC-hart sync barrier for vCPU migration
  2026-08-27 15:20 [PATCH v2 00/39] [RISC-V] virtual interrupt controller (vAPLIC/vIMSIC) support Oleksii Kurochko
                   ` (29 preceding siblings ...)
  2026-08-27 15:21 ` [PATCH v2 30/39] xen/riscv: prepare new IMSIC VS-file Oleksii Kurochko
@ 2026-08-27 15:21 ` Oleksii Kurochko
  2026-09-14 13:27   ` Jan Beulich
  2026-09-22 17:00   ` Baptiste Le Duc
  2026-08-27 15:21 ` [PATCH v2 32/39] xen/riscv: remap interrupts to new IMSIC VS-file Oleksii Kurochko
                   ` (7 subsequent siblings)
  38 siblings, 2 replies; 244+ messages in thread
From: Oleksii Kurochko @ 2026-08-27 15:21 UTC (permalink / raw)
  To: xen-devel
  Cc: Romain Caritey, Baptiste Le Duc, Zheng Zhang, Oleksii Kurochko,
	Alistair Francis, Connor Davis, Andrew Cooper, Anthony PERARD,
	Michal Orzel, Jan Beulich, Julien Grall, Roger Pau Monné,
	Stefano Stabellini

During migration of a virtual hart to a different guest interrupt file,
straggler MSIs from the APLIC could arrive at the old interrupt file
after the switch.

genmsi is used despite not supporting guest interrupt files because the
AIA spec guarantees that all MSIs previously sent from the APLIC to the
same hart are visible at the hart's IMSIC before the extempore MSI from
genmsi becomes visible.

Signed-off-by: Oleksii Kurochko <oleksii.kurochko@gmail.com>
---
Changes in v2:
 - New patch.
---
---
 xen/arch/riscv/aplic.c             | 26 ++++++++++++++++++++++++++
 xen/arch/riscv/imsic.c             | 12 ++++++++++++
 xen/arch/riscv/include/asm/aplic.h |  3 +++
 xen/arch/riscv/include/asm/imsic.h |  8 ++++++++
 4 files changed, 49 insertions(+)

diff --git a/xen/arch/riscv/aplic.c b/xen/arch/riscv/aplic.c
index 0af13f28e467..cb11d6aeaaa9 100644
--- a/xen/arch/riscv/aplic.c
+++ b/xen/arch/riscv/aplic.c
@@ -27,7 +27,9 @@
 #include <asm/imsic.h>
 #include <asm/intc.h>
 #include <asm/io.h>
+#include <asm/processor.h>
 #include <asm/riscv_encoding.h>
+#include <asm/smp.h>
 
 static struct aplic_priv aplic = {
     .lock = SPIN_LOCK_UNLOCKED,
@@ -205,6 +207,30 @@ void aplic_hw_write_reg(unsigned int offset, uint32_t value)
     spin_unlock_irqrestore(&aplic.lock, flags);
 }
 
+/*
+ * As needed, synchronize with all IOMMUs and APLICs to ensure that no
+ * straggler MSIs will arrive at the old interrupt file after this step.
+ */
+void aplic_genmsi_barrier(void)
+{
+    const struct imsic_config *imsic = imsic_get_config();
+    unsigned int cpu = smp_processor_id();
+    unsigned long flags;
+    uint32_t val;
+
+    val = MASK_INSR(aplic_hart_field(cpu), APLIC_TARGET_HART_IDX) |
+          (imsic->sync_id & APLIC_TARGET_EIID);
+
+    spin_lock_irqsave(&aplic.lock, flags);
+
+    writel(val, &aplic.regs->genmsi);
+
+    while ( readl(&aplic.regs->genmsi) & APLIC_GENMSI_BUSY )
+        cpu_relax();
+
+    spin_unlock_irqrestore(&aplic.lock, flags);
+}
+
 static void __init aplic_init_hw_interrupts(void)
 {
     unsigned int i;
diff --git a/xen/arch/riscv/imsic.c b/xen/arch/riscv/imsic.c
index 516f0105352a..5de45949610d 100644
--- a/xen/arch/riscv/imsic.c
+++ b/xen/arch/riscv/imsic.c
@@ -195,6 +195,15 @@ void imsic_irq_enable(unsigned int irq)
      */
     ASSERT(!local_irq_is_enabled());
 
+    if ( irq == imsic_cfg.sync_id )
+    {
+        printk(XENLOG_WARNING
+               "irq%u is reserved for APLIC sync so shouldn't be set by %s\n",
+               irq, __func__);
+
+        return;
+    }
+
     spin_lock(&imsic_cfg.lock);
     /*
      * There is no irq - 1 here (look at aplic_set_irq_type()) because:
@@ -377,6 +386,9 @@ static int __init imsic_parse_node(const struct dt_device_node *node,
         return -ENOENT;
     }
 
+    /* Reserve last identity for APLIC-to-hart synchronization */
+    imsic_cfg.sync_id = imsic_cfg.nr_ids;
+
     /* Compute base address */
     *nr_mmios = 0;
     rc = dt_device_get_address(node, *nr_mmios, &base_addr, NULL);
diff --git a/xen/arch/riscv/include/asm/aplic.h b/xen/arch/riscv/include/asm/aplic.h
index 8564f5954b6b..e4f4dfd241cc 100644
--- a/xen/arch/riscv/include/asm/aplic.h
+++ b/xen/arch/riscv/include/asm/aplic.h
@@ -88,6 +88,7 @@
 #define APLIC_SETIPNUM_LE               0x2000
 
 #define APLIC_GENMSI                    0x3000
+#define APLIC_GENMSI_BUSY               BIT(12, U)
 
 #define APLIC_TARGET_BASE               0x3004
 #define APLIC_TARGET_LAST               0x3ffc
@@ -178,4 +179,6 @@ void aplic_reconfigure_target(const struct vcpu *v,
                               unsigned int old_guest_file_id,
                               unsigned int old_cpu);
 
+void aplic_genmsi_barrier(void);
+
 #endif /* ASM_RISCV_APLIC_H */
diff --git a/xen/arch/riscv/include/asm/imsic.h b/xen/arch/riscv/include/asm/imsic.h
index 57d8c729ac0d..6ea2e4b8ca12 100644
--- a/xen/arch/riscv/include/asm/imsic.h
+++ b/xen/arch/riscv/include/asm/imsic.h
@@ -68,6 +68,14 @@ struct imsic_config {
     /* Number off interrupt identities */
     unsigned int nr_ids;
 
+    /*
+     * Interrupt identity reserved exclusively for APLIC-to-hart
+     * synchronization.
+     *
+     * Must not be allocated to any interrupt source.
+     */
+    unsigned int sync_id;
+
     /* MSI */
     const struct imsic_msi *msi;
 
-- 
2.55.0



^ permalink raw reply related	[flat|nested] 244+ messages in thread

* [PATCH v2 32/39] xen/riscv: remap interrupts to new IMSIC VS-file
  2026-08-27 15:20 [PATCH v2 00/39] [RISC-V] virtual interrupt controller (vAPLIC/vIMSIC) support Oleksii Kurochko
                   ` (30 preceding siblings ...)
  2026-08-27 15:21 ` [PATCH v2 31/39] xen/riscv: implement APLIC-hart sync barrier for vCPU migration Oleksii Kurochko
@ 2026-08-27 15:21 ` Oleksii Kurochko
  2026-09-14 15:02   ` Jan Beulich
                     ` (2 more replies)
  2026-08-27 15:21 ` [PATCH v2 33/39] xen/riscv: dump old interrupt file to memory Oleksii Kurochko
                   ` (6 subsequent siblings)
  38 siblings, 3 replies; 244+ messages in thread
From: Oleksii Kurochko @ 2026-08-27 15:21 UTC (permalink / raw)
  To: xen-devel
  Cc: Romain Caritey, Baptiste Le Duc, Zheng Zhang, Oleksii Kurochko,
	Alistair Francis, Connor Davis, Andrew Cooper, Anthony PERARD,
	Michal Orzel, Jan Beulich, Julien Grall, Roger Pau Monné,
	Stefano Stabellini

After new IMSIC VS-file is zeroed-out it is necessary to do G-stage remaping
fo new IMSIC VS-file. Also, if any interrupts at an APLIC are forwarded by
MSIs to the old interrupt file, reconfigure the APLIC to send them to the
new interrupt file.

Generally it is needed also to modify the relevant translation tables at
all IOMMUs so that MSIs for this virtual interrupt file are now sent to
the new physical interrupt file but it is skipped for now there is no IOMMU
support for RISC-V.

Synchronize with APLIC to ensure that no straggler MSIs will arrive at
the old interrupt file by using of aplic_genmsi_barrier().

Technically there is no need for read_lock_irqsave() and
read_unlock_irqrestore() around reading of ->guest_file_id, as a write
cannot happen in parallel: any update to ->guest_file_id for a vCPU
will happen either in imsic_migrate_vcpu() itself or before the vCPU
first gains control (in continue_new_vcpu()), so there is no concurrent
access to it in imsic_migrate_vcpu(). The lock is added here for
potential future cases.

Keep BUG_ON("unimplemented") placeholder in imsic_migrate_vcpu() to guard
against silent incorrect behaviour or unexpected panics in guest VMs until
the function is fully implemented.

imsic_map_guest_file() and imsic_update_state() are stubs for now and will
be introduced later in a separate patch.

Signed-off-by: Oleksii Kurochko <oleksii.kurochko@gmail.com>
---
Changes in v2:
 - New patch.
---
---
 xen/arch/riscv/imsic.c             | 103 +++++++++++++++++++++++++++++
 xen/arch/riscv/include/asm/imsic.h |   3 +
 2 files changed, 106 insertions(+)

diff --git a/xen/arch/riscv/imsic.c b/xen/arch/riscv/imsic.c
index 5de45949610d..5e9f6995e443 100644
--- a/xen/arch/riscv/imsic.c
+++ b/xen/arch/riscv/imsic.c
@@ -27,6 +27,7 @@
 #include <xen/xvmalloc.h>
 
 #include <asm/aia.h>
+#include <asm/aplic.h>
 #include <asm/imsic.h>
 
 #define IMSIC_HART_SIZE(guest_bits) (BIT(guest_bits, U) * IMSIC_MMIO_PAGE_SZ)
@@ -60,6 +61,12 @@ static unsigned int __ro_after_init guest_num_msis;
 #define IMSIC_DISABLE_EITHRESHOLD   1
 #define IMSIC_ENABLE_EITHRESHOLD    0
 
+#define imsic_csr_read(c)           \
+({                                  \
+    csr_write(CSR_SISELECT, (c));   \
+    csr_read(CSR_SIREG);            \
+})
+
 #define imsic_csr_write(c, v)   \
 do {                            \
     csr_write(CSR_SISELECT, c); \
@@ -141,6 +148,11 @@ unsigned int vcpu_guest_file_id(const struct vcpu *v)
     return ACCESS_ONCE(v->arch.vimsic_state->guest_file_id);
 }
 
+void imsic_update_state(struct vcpu *v, unsigned int guest_file_id)
+{
+    BUG_ON("unimplemented\n");
+}
+
 void __init imsic_ids_local_delivery(bool enable)
 {
     if ( enable )
@@ -242,6 +254,15 @@ void imsic_irq_disable(unsigned int irq)
     spin_unlock(&imsic_cfg.lock);
 }
 
+static bool imsic_local_is_pending(unsigned int id)
+{
+    unsigned long isel =
+        (id / BITS_PER_LONG) * (BITS_PER_LONG / IMSIC_EIPx_BITS) + IMSIC_EIP0;
+    unsigned long bit = BIT(id % BITS_PER_LONG, UL);
+
+    return !!(imsic_csr_read(isel) & bit);
+}
+
 /* Callers aren't intended to changed imsic_cfg so return const. */
 const struct imsic_config *imsic_get_config(void)
 {
@@ -436,6 +457,11 @@ void cf_check imsic_ctxt_switch_to(struct vcpu *v)
     /* Nothing to do */
 }
 
+int imsic_map_guest_file(struct vcpu *v, unsigned int vsfile_id)
+{
+    return -EOPNOTSUPP;
+}
+
 int cf_check vcpu_imsic_init(struct vcpu *v)
 {
     struct vimsic_state *imsic_state;
@@ -497,6 +523,27 @@ static void imsic_call_on_cpu(unsigned int cpu, void (*func)(void *),
         on_selected_cpus(cpumask_of(cpu), func, data, 1);
 }
 
+/*
+ * Ensure that all the MSIs the APLIC has already generated for the hart this
+ * runs on have really reached the hart's IMSIC.
+ *
+ * The barrier is the one described by the AIA specification in
+ * "Synchronizing interactions between a hart and the APLIC": ask the APLIC to
+ * send an MSI to the hart itself and wait until it shows up as pending in the
+ * hart's own interrupt file. As it says nothing about MSIs on their way to
+ * any other hart, it has to be executed by the pCPU owning the interrupt file
+ * the MSIs were being sent to.
+ */
+static void cf_check imsic_aplic_sync(void *data)
+{
+    imsic_local_eix_update(imsic_cfg.sync_id, 1, true, false);
+
+    aplic_genmsi_barrier();
+
+    while ( !imsic_local_is_pending(imsic_cfg.sync_id) )
+        cpu_relax();
+}
+
 static void cf_check imsic_vsfile_local_clear(void *data)
 {
     unsigned int i;
@@ -837,6 +884,10 @@ void imsic_migrate_vcpu(struct vcpu *v)
     struct imsic_vsfile_data vsfile_data = {
         .nr_eix = nr_hw_eix,
     };
+    struct vimsic_state *imsic_state = v->arch.vimsic_state;
+    unsigned long flags;
+    unsigned int old_vsfile_id;
+    unsigned int old_vsfile_cpu;
 
     /*
      * The scheduler can mark a freshly created vCPU's unit as migrated and
@@ -848,8 +899,22 @@ void imsic_migrate_vcpu(struct vcpu *v)
     if ( v->arch.last_cpu == NR_CPUS )
         return;
 
+    read_lock_irqsave(&imsic_state->vsfile_lock, flags);
+    old_vsfile_id = imsic_state->guest_file_id;
+    old_vsfile_cpu = imsic_state->vsfile_cpu;
+    read_unlock_irqrestore(&imsic_state->vsfile_lock, flags);
+
+    /*
+     * We don't support SW interrupt files at the moment. Bail out before
+     * anything is touched, as the old file has no owning pCPU in that case
+     * and there is nothing to retarget the producers away from.
+     */
+    if ( old_vsfile_cpu == NR_CPUS )
+        panic("IMSIC SW-file isn't supported\n");
+
     /*
      * At this point, all interrupt producers are still using the old IMSIC
+     * VS-file so we first move all interrupt producers to the new IMSIC
      * VS-file.
      */
 
@@ -870,5 +935,43 @@ void imsic_migrate_vcpu(struct vcpu *v)
     /* Zero-out new IMSIC VS-file */
     imsic_call_on_cpu(new_vsfile_cpu, imsic_vsfile_local_clear, &vsfile_data);
 
+    /* Update G-stage mapping for the new IMSIC VS-file */
+    if ( imsic_map_guest_file(v, new_vsfile_hgei) )
+    {
+        domain_crash(v->domain, "Migration to hw interrupt file failed\n");
+
+        return;
+    }
+
+    imsic_update_state(v, new_vsfile_hgei);
+
+    /*
+     * TODO: Modify the relevant translation tables at all IOMMUs so that MSIs
+     *       for this virtual interrupt file are now sent to the new physical
+     *       interrupt file.
+     */
+    if ( iommu_enabled )
+        printk_once("IMSIC: IOMMU MSI retargeting is not implemented\n");
+
+    /*
+     * If any interrupts at an APLIC are forwarded by MSIs to the old interrupt
+     * file, reconfigure the APLIC to send them to the new interrupt file.
+     */
+    aplic_reconfigure_target(v, old_vsfile_id, old_vsfile_cpu);
+
+    /*
+     * Synchronizing interactions between a hart and the APLIC.
+     *
+     * The MSIs to be flushed are the ones still on their way to the old
+     * interrupt file, so the barrier has to be done by the pCPU which owns
+     * that file.
+     */
+    imsic_call_on_cpu(old_vsfile_cpu, imsic_aplic_sync, NULL);
+
+    /*
+     * At this point, all interrupt producers have been moved
+     * to the new IMSIC VS-file.
+     */
+
     BUG_ON("unimplemented");
 }
diff --git a/xen/arch/riscv/include/asm/imsic.h b/xen/arch/riscv/include/asm/imsic.h
index 6ea2e4b8ca12..6395b539c52d 100644
--- a/xen/arch/riscv/include/asm/imsic.h
+++ b/xen/arch/riscv/include/asm/imsic.h
@@ -114,12 +114,15 @@ void imsic_ids_local_delivery(bool enable);
 int vcpu_imsic_init(struct vcpu *v);
 void vcpu_imsic_deinit(struct vcpu *v);
 unsigned int vcpu_guest_file_id(const struct vcpu *v);
+void imsic_update_state(struct vcpu *v, unsigned int guest_file_id);
 
 int vimsic_make_domu_dt_node(struct kernel_info *kinfo, unsigned int *phandle);
 
 void imsic_ctxt_switch_from(struct vcpu *v);
 void imsic_ctxt_switch_to(struct vcpu *v);
 
+int imsic_map_guest_file(struct vcpu *v, unsigned int vsfile_id);
+
 void imsic_migrate_vcpu(struct vcpu *v);
 
 #endif /* ASM_RISCV_IMSIC_H */
-- 
2.55.0



^ permalink raw reply related	[flat|nested] 244+ messages in thread

* [PATCH v2 33/39] xen/riscv: dump old interrupt file to memory
  2026-08-27 15:20 [PATCH v2 00/39] [RISC-V] virtual interrupt controller (vAPLIC/vIMSIC) support Oleksii Kurochko
                   ` (31 preceding siblings ...)
  2026-08-27 15:21 ` [PATCH v2 32/39] xen/riscv: remap interrupts to new IMSIC VS-file Oleksii Kurochko
@ 2026-08-27 15:21 ` Oleksii Kurochko
  2026-09-14 15:15   ` Jan Beulich
  2026-09-23 15:15   ` Baptiste Le Duc
  2026-08-27 15:21 ` [PATCH v2 34/39] xen/riscv: restore register state in the new IMSIC VS-file Oleksii Kurochko
                   ` (5 subsequent siblings)
  38 siblings, 2 replies; 244+ messages in thread
From: Oleksii Kurochko @ 2026-08-27 15:21 UTC (permalink / raw)
  To: xen-devel
  Cc: Romain Caritey, Baptiste Le Duc, Zheng Zhang, Oleksii Kurochko,
	Alistair Francis, Connor Davis, Andrew Cooper, Anthony PERARD,
	Michal Orzel, Jan Beulich, Julien Grall, Roger Pau Monné,
	Stefano Stabellini

At the old interrupt file, dump to memory all the eip and eie arrays).
After this step is done, the old interrupt file is no longer in use so
old intrrupt file VGEIN could be released.

Restoring of old interrupt file state will be done in follow-up
patch.

There are cases where it is needed to specify on which cpu it is
necessary to VGEIN should be released so update vgein_release() to
deal with that.

Keep BUG_ON("unimplemented") placeholder in imsic_migrate_vcpu() to guard
against silent incorrect behaviour or unexpected panics in guest VMs until
the function is fully implemented.

vgein_release() is stub for now and will be introduced later.

Signed-off-by: Oleksii Kurochko <oleksii.kurochko@gmail.com>
---
Changes in v2:
 - New patch.
---
---
 xen/arch/riscv/aia.c             |   5 ++
 xen/arch/riscv/imsic.c           | 104 +++++++++++++++++++++++++++++++
 xen/arch/riscv/include/asm/aia.h |   1 +
 3 files changed, 110 insertions(+)

diff --git a/xen/arch/riscv/aia.c b/xen/arch/riscv/aia.c
index 75c82bcfa1b3..be3901ec0cfa 100644
--- a/xen/arch/riscv/aia.c
+++ b/xen/arch/riscv/aia.c
@@ -30,3 +30,8 @@ unsigned int vgein_assign(struct vcpu *v)
 
     return 0;
 }
+
+void vgein_release(struct vcpu *v, unsigned int vgein_id, unsigned int cpu)
+{
+    BUG_ON("unimplemented\n");
+}
diff --git a/xen/arch/riscv/imsic.c b/xen/arch/riscv/imsic.c
index 5e9f6995e443..3cba58e0c1b3 100644
--- a/xen/arch/riscv/imsic.c
+++ b/xen/arch/riscv/imsic.c
@@ -56,6 +56,24 @@ static unsigned int __ro_after_init guest_num_msis;
  */
 #define GUEST_IMSIC_MAX_MSIS 255U
 
+/*
+ * The interrupt identities an IMSIC interrupt file provides are 0 (which is
+ * never valid, but still occupies a bit) up to IMSIC_MAX_ID inclusive, so
+ * IMSIC_MAX_ID + 1 bits have to be covered.
+ */
+#define IMSIC_MAX_EIX DIV_ROUND_UP(IMSIC_MAX_ID + 1, BITS_PER_TYPE(uint64_t))
+
+struct imsic_mrif_eix {
+    unsigned long eip[BITS_PER_TYPE(uint64_t) / BITS_PER_LONG];
+    unsigned long eie[BITS_PER_TYPE(uint64_t) / BITS_PER_LONG];
+};
+
+struct imsic_mrif {
+    struct imsic_mrif_eix eix[IMSIC_MAX_EIX];
+    unsigned long eithreshold;
+    unsigned long eidelivery;
+};
+
 #define IMSIC_DISABLE_EIDELIVERY    0
 #define IMSIC_ENABLE_EIDELIVERY     1
 #define IMSIC_DISABLE_EITHRESHOLD   1
@@ -85,6 +103,15 @@ do {                            \
     csr_clear(CSR_SIREG, v);    \
 } while (0)
 
+#define imsic_vs_csr_swap(c, v)     \
+({                                  \
+    unsigned long r_;               \
+                                    \
+    csr_write(CSR_VSISELECT, (c));  \
+    r_ = csr_swap(CSR_VSIREG, (v)); \
+    r_;                             \
+})
+
 #define imsic_vs_csr_write(c, v)    \
 do {                                \
     csr_write(CSR_VSISELECT, (c));  \
@@ -130,6 +157,21 @@ do {                                \
     imsic_switchcase_32(F, ireg + 0, ##__VA_ARGS__) \
     imsic_switchcase_32(F, ireg + 32, ##__VA_ARGS__)
 
+static unsigned long imsic_eix_swap(unsigned int ireg, unsigned long val)
+{
+    switch ( ireg )
+    {
+    imsic_switchcase_64(imsic_switchcase_ret, IMSIC_EIP0,
+                        imsic_vs_csr_swap, val)
+    imsic_switchcase_64(imsic_switchcase_ret, IMSIC_EIE0,
+                        imsic_vs_csr_swap, val)
+    default:
+        ASSERT_UNREACHABLE();
+    }
+
+    return 0;
+}
+
 static void imsic_eix_write(unsigned int ireg, unsigned long val)
 {
     switch ( ireg )
@@ -577,6 +619,61 @@ static void cf_check imsic_vsfile_local_clear(void *data)
     csr_write(CSR_VSISELECT, old_vsiselect);
 }
 
+static void cf_check imsic_vsfile_local_read_clear(void *data)
+{
+    unsigned int i;
+    struct imsic_mrif_eix *eix;
+    const struct imsic_vsfile_data *idata = data;
+    struct imsic_mrif *mrif = idata->mrif;
+    unsigned long new_hstatus, old_hstatus, old_vsiselect;
+
+    old_vsiselect = csr_read(CSR_VSISELECT);
+    old_hstatus = csr_read(CSR_HSTATUS);
+    new_hstatus = old_hstatus & ~HSTATUS_VGEIN;
+    new_hstatus |= ((unsigned long)idata->hgei) << HSTATUS_VGEIN_SHIFT;
+    csr_write(CSR_HSTATUS, new_hstatus);
+
+    /*
+     * There is no need to use atomic functions version to store
+     * values in MRIF because imsic_vsfile_read_clear() is always called
+     * with pointer to temporary MRIF on stack.
+     */
+
+    mrif->eidelivery = imsic_vs_csr_swap(IMSIC_EIDELIVERY, 0);
+    mrif->eithreshold = imsic_vs_csr_swap(IMSIC_EITHRESHOLD, 0);
+    for ( i = 0; i < idata->nr_eix; i++ )
+    {
+        eix = &mrif->eix[i];
+        eix->eip[0] = imsic_eix_swap(IMSIC_EIP0 + i * 2, 0);
+        eix->eie[0] = imsic_eix_swap(IMSIC_EIE0 + i * 2, 0);
+#ifdef CONFIG_RISCV_32
+        eix->eip[1] = imsic_eix_swap(IMSIC_EIP0 + i * 2 + 1, 0);
+        eix->eie[1] = imsic_eix_swap(IMSIC_EIE0 + i * 2 + 1, 0);
+#endif
+    }
+
+    csr_write(CSR_HSTATUS, old_hstatus);
+    csr_write(CSR_VSISELECT, old_vsiselect);
+}
+
+static void imsic_vsfile_read_clear(unsigned int vsfile_id,
+                                    unsigned int vsfile_cpu,
+                                    unsigned int nr_eix,
+                                    struct imsic_mrif *mrif)
+{
+    struct imsic_vsfile_data idata = {
+        .hgei = vsfile_id,
+        .nr_eix = nr_eix,
+        .mrif = mrif,
+    };
+
+    /* We can only read clear if we have a IMSIC VS-file */
+    if ( vsfile_cpu == NR_CPUS || !vsfile_id )
+        return;
+
+    imsic_call_on_cpu(vsfile_cpu, imsic_vsfile_local_read_clear, &idata);
+}
+
 void cf_check vcpu_imsic_deinit(struct vcpu *v)
 {
     XVFREE(v->arch.vimsic_state);
@@ -888,6 +985,7 @@ void imsic_migrate_vcpu(struct vcpu *v)
     unsigned long flags;
     unsigned int old_vsfile_id;
     unsigned int old_vsfile_cpu;
+    struct imsic_mrif tmrif = { };
 
     /*
      * The scheduler can mark a freshly created vCPU's unit as migrated and
@@ -973,5 +1071,11 @@ void imsic_migrate_vcpu(struct vcpu *v)
      * to the new IMSIC VS-file.
      */
 
+    /* Read and clear register state from old IMSIC VS-file */
+    imsic_vsfile_read_clear(old_vsfile_id, old_vsfile_cpu, nr_hw_eix, &tmrif);
+
+    /* Free-up old IMSIC VS-file */
+    vgein_release(v, old_vsfile_id, old_vsfile_cpu);
+
     BUG_ON("unimplemented");
 }
diff --git a/xen/arch/riscv/include/asm/aia.h b/xen/arch/riscv/include/asm/aia.h
index 53a1efb042f8..8e4eb2f6b14e 100644
--- a/xen/arch/riscv/include/asm/aia.h
+++ b/xen/arch/riscv/include/asm/aia.h
@@ -10,5 +10,6 @@ bool aia_usable(void);
 void aia_init(void);
 
 unsigned int vgein_assign(struct vcpu *v);
+void vgein_release(struct vcpu *v, unsigned int vgein_id, unsigned int cpu);
 
 #endif /* RISCV_AIA_H */
-- 
2.55.0



^ permalink raw reply related	[flat|nested] 244+ messages in thread

* [PATCH v2 34/39] xen/riscv: restore register state in the new IMSIC VS-file
  2026-08-27 15:20 [PATCH v2 00/39] [RISC-V] virtual interrupt controller (vAPLIC/vIMSIC) support Oleksii Kurochko
                   ` (32 preceding siblings ...)
  2026-08-27 15:21 ` [PATCH v2 33/39] xen/riscv: dump old interrupt file to memory Oleksii Kurochko
@ 2026-08-27 15:21 ` Oleksii Kurochko
  2026-09-14 15:21   ` Jan Beulich
  2026-09-23 15:42   ` Baptiste Le Duc
  2026-08-27 15:21 ` [PATCH v2 35/39] xen/riscv: add basic VGEIN management for AIA guests Oleksii Kurochko
                   ` (4 subsequent siblings)
  38 siblings, 2 replies; 244+ messages in thread
From: Oleksii Kurochko @ 2026-08-27 15:21 UTC (permalink / raw)
  To: xen-devel
  Cc: Romain Caritey, Baptiste Le Duc, Zheng Zhang, Oleksii Kurochko,
	Alistair Francis, Connor Davis, Andrew Cooper, Anthony PERARD,
	Michal Orzel, Jan Beulich, Julien Grall, Roger Pau Monné,
	Stefano Stabellini

At this point, all interrupt producers have been moved to the new
IMSIC VS-file so we move register state from the old IMSIC VS/SW-file
to the new IMSIC VS-file.

As new IMSIC VS-file is ready to be used update vCPU's hstatus with
new VGEIN.

As the whole migration procedure is finished add some extra explanatory
comments.

Signed-off-by: Oleksii Kurochko <oleksii.kurochko@gmail.com>
---
Changes in v2:
 - New patch.
---
---
 xen/arch/riscv/imsic.c | 82 ++++++++++++++++++++++++++++++++++++++----
 1 file changed, 76 insertions(+), 6 deletions(-)

diff --git a/xen/arch/riscv/imsic.c b/xen/arch/riscv/imsic.c
index 3cba58e0c1b3..d7b137a1f559 100644
--- a/xen/arch/riscv/imsic.c
+++ b/xen/arch/riscv/imsic.c
@@ -112,6 +112,12 @@ do {                            \
     r_;                             \
 })
 
+#define imsic_vs_csr_set(c, v)      \
+do {                                \
+    csr_write(CSR_VSISELECT, (c));  \
+    csr_set(CSR_VSIREG, (v));       \
+} while ( 0 )
+
 #define imsic_vs_csr_write(c, v)    \
 do {                                \
     csr_write(CSR_VSISELECT, (c));  \
@@ -185,6 +191,19 @@ static void imsic_eix_write(unsigned int ireg, unsigned long val)
     }
 }
 
+static void imsic_eix_set(unsigned int ireg, unsigned long val)
+{
+    switch ( ireg )
+    {
+    imsic_switchcase_64(imsic_switchcase_break, IMSIC_EIP0,
+                        imsic_vs_csr_set, val)
+    imsic_switchcase_64(imsic_switchcase_break, IMSIC_EIE0,
+                        imsic_vs_csr_set, val)
+    default:
+        ASSERT_UNREACHABLE();
+    }
+}
+
 unsigned int vcpu_guest_file_id(const struct vcpu *v)
 {
     return ACCESS_ONCE(v->arch.vimsic_state->guest_file_id);
@@ -630,13 +649,13 @@ static void cf_check imsic_vsfile_local_read_clear(void *data)
     old_vsiselect = csr_read(CSR_VSISELECT);
     old_hstatus = csr_read(CSR_HSTATUS);
     new_hstatus = old_hstatus & ~HSTATUS_VGEIN;
-    new_hstatus |= ((unsigned long)idata->hgei) << HSTATUS_VGEIN_SHIFT;
+    new_hstatus |= MASK_INSR(idata->hgei, HSTATUS_VGEIN);
     csr_write(CSR_HSTATUS, new_hstatus);
 
     /*
-     * There is no need to use atomic functions version to store
-     * values in MRIF because imsic_vsfile_read_clear() is always called
-     * with pointer to temporary MRIF on stack.
+     * No atomic accessors are needed to store the values into the MRIF here,
+     * as imsic_vsfile_read_clear() is always called with a pointer to a
+     * temporary MRIF on the stack.
      */
 
     mrif->eidelivery = imsic_vs_csr_swap(IMSIC_EIDELIVERY, 0);
@@ -972,6 +991,49 @@ int __init vimsic_make_domu_dt_node(struct kernel_info *kinfo,
     return fdt_end_node(fdt);
 }
 
+static void cf_check imsic_vsfile_local_update(void *data)
+{
+    unsigned int i;
+    struct imsic_mrif_eix *eix;
+    const struct imsic_vsfile_data *idata = data;
+    struct imsic_mrif *mrif = idata->mrif;
+    unsigned long new_hstatus, old_hstatus, old_vsiselect;
+
+    /* We can only update if we have a HW IMSIC context */
+    if ( !idata->hgei )
+        return;
+
+    /*
+     * No atomic accessors are needed to read the values out of the MRIF here,
+     * as this is always called with a pointer to a temporary MRIF on the
+     * stack.
+     */
+
+    old_vsiselect = csr_read(CSR_VSISELECT);
+    old_hstatus = csr_read(CSR_HSTATUS);
+    new_hstatus = old_hstatus & ~HSTATUS_VGEIN;
+    new_hstatus |= MASK_INSR(idata->hgei, HSTATUS_VGEIN);
+    csr_write(CSR_HSTATUS, new_hstatus);
+
+    for ( i = 0; i < idata->nr_eix; i++ )
+    {
+        eix = &mrif->eix[i];
+
+        imsic_eix_set(IMSIC_EIP0 + i * 2, eix->eip[0]);
+        imsic_eix_set(IMSIC_EIE0 + i * 2, eix->eie[0]);
+#ifdef CONFIG_RISCV_32
+        imsic_eix_set(IMSIC_EIP0 + i * 2 + 1, eix->eip[1]);
+        imsic_eix_set(IMSIC_EIE0 + i * 2 + 1, eix->eie[1]);
+#endif
+    }
+
+    imsic_vs_csr_write(IMSIC_EITHRESHOLD, mrif->eithreshold);
+    imsic_vs_csr_write(IMSIC_EIDELIVERY, mrif->eidelivery);
+
+    csr_write(CSR_HSTATUS, old_hstatus);
+    csr_write(CSR_VSISELECT, old_vsiselect);
+}
+
 void imsic_migrate_vcpu(struct vcpu *v)
 {
     unsigned int new_vsfile_hgei;
@@ -1068,7 +1130,8 @@ void imsic_migrate_vcpu(struct vcpu *v)
 
     /*
      * At this point, all interrupt producers have been moved
-     * to the new IMSIC VS-file.
+     * to the new IMSIC VS-file so we move register state from
+     * the old IMSIC VS/SW-file to the new IMSIC VS-file.
      */
 
     /* Read and clear register state from old IMSIC VS-file */
@@ -1077,5 +1140,12 @@ void imsic_migrate_vcpu(struct vcpu *v)
     /* Free-up old IMSIC VS-file */
     vgein_release(v, old_vsfile_id, old_vsfile_cpu);
 
-    BUG_ON("unimplemented");
+    /* Restore register state in the new IMSIC VS-file */
+    vsfile_data.mrif = &tmrif;
+    imsic_call_on_cpu(new_vsfile_cpu, imsic_vsfile_local_update, &vsfile_data);
+
+    /* Set VCPU HSTATUS.VGEIN to new IMSIC VS-file */
+    vcpu_guest_cpu_user_regs(v)->hstatus &= ~HSTATUS_VGEIN;
+    vcpu_guest_cpu_user_regs(v)->hstatus |=
+            MASK_INSR(new_vsfile_hgei, HSTATUS_VGEIN);
 }
-- 
2.55.0



^ permalink raw reply related	[flat|nested] 244+ messages in thread

* [PATCH v2 35/39] xen/riscv: add basic VGEIN management for AIA guests
  2026-08-27 15:20 [PATCH v2 00/39] [RISC-V] virtual interrupt controller (vAPLIC/vIMSIC) support Oleksii Kurochko
                   ` (33 preceding siblings ...)
  2026-08-27 15:21 ` [PATCH v2 34/39] xen/riscv: restore register state in the new IMSIC VS-file Oleksii Kurochko
@ 2026-08-27 15:21 ` Oleksii Kurochko
  2026-09-18 12:38   ` Jan Beulich
  2026-09-23 16:06   ` Baptiste Le Duc
  2026-08-27 15:21 ` [PATCH v2 36/39] xen/riscv: wake up a descheduled vCPU on a guest external interrupt Oleksii Kurochko
                   ` (3 subsequent siblings)
  38 siblings, 2 replies; 244+ messages in thread
From: Oleksii Kurochko @ 2026-08-27 15:21 UTC (permalink / raw)
  To: xen-devel
  Cc: Romain Caritey, Baptiste Le Duc, Zheng Zhang, Oleksii Kurochko,
	Alistair Francis, Connor Davis, Andrew Cooper, Anthony PERARD,
	Michal Orzel, Jan Beulich, Julien Grall, Roger Pau Monné,
	Stefano Stabellini

It was decided to add support for IMSIC from the start instead of having APLIC
operate in direct delivery mode, as it requires a trap-and-emulation approach,
which is not optimal from a performance standpoint.

AIA provides a hardware-accelerated mechanism for delivering external
interrupts to domains via "guest interrupt files" located in IMSIC.
A single physical hart can implement multiple such files (up to GEILEN),
allowing several virtual harts to receive interrupts directly from hardware.

Introduce per-CPU tracking of guest interrupt file identifiers (VGEIN)
for systems implementing AIA specification. Each CPU maintains
a bitmap describing which guest interrupt files are currently in use.

Implement helpers to initialize the bitmap based on the number of available
guest interrupt files (GEILEN), assign a VGEIN to a vCPU, and release it
when no longer needed.

Signed-off-by: Oleksii Kurochko <oleksii.kurochko@gmail.com>
---
Also in the next patch there is other context to understand the usage of
spinlock introduced here.
---
Changes in v2:
 - make vgein_init() pCPU agnostic as it is working with CSR which could be
   read only on local pCPU itself.
 - Move introduction of vgein_ctrl->owners[] to separate patch.
 - Add ASSERT() and re-init vgein->bmp with 0.
 - Update the commit message (drop the last sentence as ->hstatus isn't
   filled anymore in in vgein_*() functions).
 - Introduce vgein_deinit().
---
---
 xen/arch/riscv/aia.c | 141 +++++++++++++++++++++++++++++++++++++++++--
 1 file changed, 137 insertions(+), 4 deletions(-)

diff --git a/xen/arch/riscv/aia.c b/xen/arch/riscv/aia.c
index be3901ec0cfa..1aca07c2f70f 100644
--- a/xen/arch/riscv/aia.c
+++ b/xen/arch/riscv/aia.c
@@ -1,13 +1,31 @@
 /* SPDX-License-Identifier: GPL-2.0-only */
 
-#include <xen/bug.h>
+#include <xen/bitops.h>
+#include <xen/cpu.h>
 #include <xen/errno.h>
 #include <xen/init.h>
 #include <xen/sections.h>
 #include <xen/sched.h>
+#include <xen/spinlock.h>
 #include <xen/types.h>
 
+#include <asm/aia.h>
 #include <asm/cpufeature.h>
+#include <asm/csr.h>
+#include <asm/current.h>
+
+struct vgein_ctrl {
+    /* The least-significant bits are implemented first, apart from bit 0 */
+    unsigned long bmp;
+    spinlock_t lock;
+    unsigned int geilen;
+};
+
+/*
+ * VGEIN control structure for each physical CPU to track which VS (guest)
+ * interrupt file IDs are in use.
+ */
+static DEFINE_PER_CPU(struct vgein_ctrl, vgein);
 
 static bool __ro_after_init _aia_usable;
 
@@ -16,22 +34,137 @@ bool aia_usable(void)
     return _aia_usable;
 }
 
+/* HGEIE is a per-hart CSR, so this has to run on the CPU being initialized. */
+static int vgein_init(void)
+{
+    struct vgein_ctrl *vgein = &this_cpu(vgein);
+
+    spin_lock_init(&vgein->lock);
+
+    csr_write(CSR_HGEIE, ~0UL);
+    vgein->geilen = flsl(csr_read(CSR_HGEIE) >> 1);
+    csr_write(CSR_HGEIE, 0);
+
+    vgein->bmp = 0;
+
+    if ( !vgein->geilen )
+        return -EOPNOTSUPP;
+
+    return 0;
+}
+
+static void vgein_deinit(void)
+{
+    csr_write(CSR_HGEIE, 0);
+}
+
+static int cf_check cpu_callback(struct notifier_block *nfb,
+                                 unsigned long action, void *hcpu)
+{
+    unsigned int cpu = (unsigned long)hcpu;
+    int rc = 0;
+
+    switch ( action )
+    {
+    case CPU_STARTING:
+        rc = vgein_init();
+        if ( rc )
+            printk(XENLOG_ERR "AIA: failed to init vgein for CPU%u: %d\n",
+                   cpu, rc);
+        break;
+
+    case CPU_DYING:
+        vgein_deinit();
+        break;
+    }
+
+    return notifier_from_errno(rc);
+}
+
+static struct notifier_block cpu_nfb = {
+    .notifier_call = cpu_callback,
+};
+
 void __init aia_init(void)
 {
+    int rc;
+
     if ( !riscv_isa_extension_available(NULL, RISCV_ISA_EXT_ssaia) )
+    {
+        dprintk(XENLOG_WARNING, "SSAIA isn't present in riscv,isa\n");
         return;
+    }
+
+    if ( (rc = vgein_init()) )
+    {
+        dprintk(XENLOG_ERR, "vgein_init() failed: %d\n", rc);
+        return;
+    }
 
     _aia_usable = true;
+
+    register_cpu_notifier(&cpu_nfb);
 }
 
 unsigned int vgein_assign(struct vcpu *v)
 {
-    BUG_ON("unimplemented\n");
+    unsigned int vgein_id;
+    struct vgein_ctrl *vgein = &per_cpu(vgein, v->processor);
+    unsigned long *bmp = &vgein->bmp;
+    unsigned long flags;
 
-    return 0;
+    if ( !vgein->geilen )
+        return 0;
+
+    spin_lock_irqsave(&vgein->lock, flags);
+    /*
+     * The vgein_id shouldn't be zero, as it will indicate that no guest
+     * external interrupt source is selected for VS-level external interrupts
+     * according to RISC-V privileged spec:
+     *   Hypervisor Status Register (hstatus) in RISC-V privileged spec:
+     *
+     *   The VGEIN (Virtual Guest External Interrupt Number) field selects
+     *   a guest external interrupt source for VS-level external interrupts.
+     *   VGEIN is a WLRL field that must be able to hold values between zero
+     *   and the maximum guest external interrupt number (known as GEILEN),
+     *   inclusive.
+     *   When VGEIN=0, no guest external interrupt source is selected for
+     *   VS-level external interrupts.
+     *
+     * So start to search from bit number 1.
+     */
+    vgein_id = find_next_zero_bit(bmp, vgein->geilen + 1, 1);
+
+    if ( vgein_id > vgein->geilen )
+        vgein_id = 0;
+    else
+        __set_bit(vgein_id, bmp);
+
+    spin_unlock_irqrestore(&vgein->lock, flags);
+
+#ifdef VGEIN_DEBUG
+    gprintk(XENLOG_DEBUG, "%s: %pv: vgein_id(%u), xen_cpu%u_bmp=%#lx\n",
+            __func__, v, vgein_id, v->processor, *bmp);
+#endif
+
+    return vgein_id;
 }
 
 void vgein_release(struct vcpu *v, unsigned int vgein_id, unsigned int cpu)
 {
-    BUG_ON("unimplemented\n");
+    unsigned long flags;
+    struct vgein_ctrl *vgein = &per_cpu(vgein, cpu);
+
+    if ( !vgein_id )
+        return;
+
+    spin_lock_irqsave(&vgein->lock, flags);
+    if ( !__test_and_clear_bit(vgein_id, &vgein->bmp) )
+        ASSERT_UNREACHABLE();
+    spin_unlock_irqrestore(&vgein->lock, flags);
+
+#ifdef VGEIN_DEBUG
+    gprintk(XENLOG_DEBUG, "%s: %pv: vgein_id(%u), xen_cpu%u_bmp=%#lx\n",
+            __func__, v, vgein_id, cpu, vgein->bmp);
+#endif
 }
-- 
2.55.0



^ permalink raw reply related	[flat|nested] 244+ messages in thread

* [PATCH v2 36/39] xen/riscv: wake up a descheduled vCPU on a guest external interrupt
  2026-08-27 15:20 [PATCH v2 00/39] [RISC-V] virtual interrupt controller (vAPLIC/vIMSIC) support Oleksii Kurochko
                   ` (34 preceding siblings ...)
  2026-08-27 15:21 ` [PATCH v2 35/39] xen/riscv: add basic VGEIN management for AIA guests Oleksii Kurochko
@ 2026-08-27 15:21 ` Oleksii Kurochko
  2026-09-18 12:52   ` Jan Beulich
  2026-08-27 15:21 ` [PATCH v2 37/39] xen/riscv: map IMSIC interrupt file for vCPUs Oleksii Kurochko
                   ` (2 subsequent siblings)
  38 siblings, 1 reply; 244+ messages in thread
From: Oleksii Kurochko @ 2026-08-27 15:21 UTC (permalink / raw)
  To: xen-devel
  Cc: Romain Caritey, Baptiste Le Duc, Zheng Zhang, Oleksii Kurochko,
	Alistair Francis, Connor Davis, Andrew Cooper, Anthony PERARD,
	Michal Orzel, Jan Beulich, Julien Grall, Roger Pau Monné,
	Stefano Stabellini

While a vCPU is running, MSIs written to its h/w IMSIC guest interrupt
file are delivered straight to VS-mode. Once the vCPU is descheduled
nobody observes that file anymore, so a guest blocked on such an
interrupt would stay blocked until some unrelated event happens to
schedule it again.

Let Xen observe the file in that window: on deschedule set the vCPU's
bit in HGEIE, which turns an interrupt pending in its VS-file into an
HS-level SGEI, and clear the bit again on schedule-in. HGEIP only
reports a file number, so to get from it back to a vCPU keep an
owners[] map per pCPU, filled by vgein_{assign,release} alongside the
VGEIN bitmap, and kick the vCPU it points at.

v->arch.hie is only initialized here and is written to the CSR later,
on the context switch to the vCPU.

vgein_release() still has no caller: a vCPU going away has to both free
its VGEIN slot and drop the owners[] entry, but there is no vCPU
teardown path to hook it into yet. vgein_deinit() only covers a pCPU
going offline.

Signed-off-by: Oleksii Kurochko <oleksii.kurochko@gmail.com>
---
Changes in v2:
 - New patch.
---
---
 xen/arch/riscv/aia.c                | 52 +++++++++++++++++++++++++--
 xen/arch/riscv/domain.c             |  3 ++
 xen/arch/riscv/imsic.c              | 55 ++++++++++++++++++++++++++++-
 xen/arch/riscv/include/asm/aia.h    |  2 ++
 xen/arch/riscv/include/asm/domain.h |  1 +
 xen/arch/riscv/traps.c              |  7 +++-
 6 files changed, 115 insertions(+), 5 deletions(-)

diff --git a/xen/arch/riscv/aia.c b/xen/arch/riscv/aia.c
index 1aca07c2f70f..9642a9796ead 100644
--- a/xen/arch/riscv/aia.c
+++ b/xen/arch/riscv/aia.c
@@ -18,6 +18,13 @@ struct vgein_ctrl {
     /* The least-significant bits are implemented first, apart from bit 0 */
     unsigned long bmp;
     spinlock_t lock;
+    /*
+     * Guest interrupt file IDs run from 1 to geilen inclusive (0 means that
+     * no guest external interrupt source is selected), and geilen can never
+     * exceed BITS_PER_LONG - 1, so indexing this array by the ID directly
+     * always fits.
+     */
+    struct vcpu *owners[BITS_PER_LONG];
     unsigned int geilen;
 };
 
@@ -62,23 +69,25 @@ static int cf_check cpu_callback(struct notifier_block *nfb,
                                  unsigned long action, void *hcpu)
 {
     unsigned int cpu = (unsigned long)hcpu;
-    int rc = 0;
 
     switch ( action )
     {
     case CPU_STARTING:
-        rc = vgein_init();
+    {
+        int rc = vgein_init();
+
         if ( rc )
             printk(XENLOG_ERR "AIA: failed to init vgein for CPU%u: %d\n",
                    cpu, rc);
         break;
+    }
 
     case CPU_DYING:
         vgein_deinit();
         break;
     }
 
-    return notifier_from_errno(rc);
+    return NOTIFY_DONE;
 }
 
 static struct notifier_block cpu_nfb = {
@@ -138,7 +147,10 @@ unsigned int vgein_assign(struct vcpu *v)
     if ( vgein_id > vgein->geilen )
         vgein_id = 0;
     else
+    {
         __set_bit(vgein_id, bmp);
+        vgein->owners[vgein_id] = v;
+    }
 
     spin_unlock_irqrestore(&vgein->lock, flags);
 
@@ -161,6 +173,7 @@ void vgein_release(struct vcpu *v, unsigned int vgein_id, unsigned int cpu)
     spin_lock_irqsave(&vgein->lock, flags);
     if ( !__test_and_clear_bit(vgein_id, &vgein->bmp) )
         ASSERT_UNREACHABLE();
+    vgein->owners[vgein_id] = NULL;
     spin_unlock_irqrestore(&vgein->lock, flags);
 
 #ifdef VGEIN_DEBUG
@@ -168,3 +181,36 @@ void vgein_release(struct vcpu *v, unsigned int vgein_id, unsigned int cpu)
             __func__, v, vgein_id, cpu, vgein->bmp);
 #endif
 }
+
+void hgei_interrupt(void)
+{
+    unsigned long hgei_mask, flags;
+    struct vgein_ctrl *vgein = &this_cpu(vgein);
+
+    hgei_mask = csr_read(CSR_HGEIP) & csr_read(CSR_HGEIE);
+    csr_clear(CSR_HGEIE, hgei_mask);
+
+    spin_lock_irqsave(&vgein->lock, flags);
+
+    for_each_set_bit ( guest_file_id, hgei_mask )
+    {
+        /*
+         * guest_file_id shouldn't be zero, as it will indicate that no
+         * guest external interrupt source is selected for VS-level external
+         * interrupts.
+         */
+        ASSERT(guest_file_id);
+
+        if ( vgein->owners[guest_file_id] )
+        {
+#ifdef VGEIN_DEBUG
+            gprintk(XENLOG_DEBUG, "%s: kick ->%pv, hgei_mask(%#lx)\n",
+                    __func__, vgein->owners[guest_file_id], hgei_mask);
+#endif
+
+            vcpu_kick(vgein->owners[guest_file_id]);
+        }
+    }
+
+    spin_unlock_irqrestore(&vgein->lock, flags);
+}
diff --git a/xen/arch/riscv/domain.c b/xen/arch/riscv/domain.c
index 2dfe4c2e72ce..29181968224c 100644
--- a/xen/arch/riscv/domain.c
+++ b/xen/arch/riscv/domain.c
@@ -136,6 +136,8 @@ static void vcpu_csr_init(struct vcpu *v)
         v->arch.hstateen0 = (hstateen0 & csr_masks.hstateen0) |
                             csr_masks.ro_one.hstateen0;
     }
+
+    v->arch.hie = MIP_SGEIP;
 }
 
 static void continue_new_vcpu(struct vcpu *prev)
@@ -398,6 +400,7 @@ static void restore_csr_regs(struct vcpu *vcpu)
     csr_write(CSR_HEDELEG, vcpu->arch.hedeleg);
     csr_write(CSR_HIDELEG, vcpu->arch.hideleg);
     csr_write(CSR_HVIP, vcpu->arch.hvip);
+    csr_write(CSR_HIE, vcpu->arch.hie);
     csr_write64(CSR_HENVCFG, vcpu->arch.henvcfg);
     csr_write(CSR_HCOUNTEREN, vcpu->arch.hcounteren);
     csr_write64(CSR_HTIMEDELTA, vcpu->arch.htimedelta);
diff --git a/xen/arch/riscv/imsic.c b/xen/arch/riscv/imsic.c
index d7b137a1f559..07152066116a 100644
--- a/xen/arch/riscv/imsic.c
+++ b/xen/arch/riscv/imsic.c
@@ -510,12 +510,31 @@ void cf_check imsic_ctxt_switch_from(struct vcpu *v)
 
     write_lock_irqsave(&imsic_state->vsfile_lock, flags);
     imsic_state->vsfile_cpu = v->processor;
+    /*
+     * Start to observe the VS-file from HS-mode: while the vCPU isn't
+     * running an interrupt pending in its VS-file is reported through HGEIP
+     * instead of being delivered to VS-mode, which lets Xen wake the vCPU up.
+     */
+    csr_set(CSR_HGEIE, BIT(imsic_state->guest_file_id, UL));
     write_unlock_irqrestore(&imsic_state->vsfile_lock, flags);
 }
 
 void cf_check imsic_ctxt_switch_to(struct vcpu *v)
 {
-    /* Nothing to do */
+    struct vimsic_state *imsic_state = v->arch.vimsic_state;
+    unsigned long flags;
+
+    /* A s/w VS-file is never observed through HGEIP. */
+    if ( !vcpu_guest_file_id(v) )
+        return;
+
+    /*
+     * The vCPU is about to run, so hstatus.VGEIN delivers the VS-file's
+     * interrupts to it directly and there is nothing left for Xen to observe.
+     */
+    read_lock_irqsave(&imsic_state->vsfile_lock, flags);
+    csr_clear(CSR_HGEIE, BIT(imsic_state->guest_file_id, UL));
+    read_unlock_irqrestore(&imsic_state->vsfile_lock, flags);
 }
 
 int imsic_map_guest_file(struct vcpu *v, unsigned int vsfile_id)
@@ -646,6 +665,12 @@ static void cf_check imsic_vsfile_local_read_clear(void *data)
     struct imsic_mrif *mrif = idata->mrif;
     unsigned long new_hstatus, old_hstatus, old_vsiselect;
 
+    /*
+     * The HGEIE bit imsic_ctxt_switch_from() armed belongs to the old owner
+     * only.
+     */
+    csr_clear(CSR_HGEIE, BIT(idata->hgei, UL));
+
     old_vsiselect = csr_read(CSR_VSISELECT);
     old_hstatus = csr_read(CSR_HSTATUS);
     new_hstatus = old_hstatus & ~HSTATUS_VGEIN;
@@ -991,6 +1016,21 @@ int __init vimsic_make_domu_dt_node(struct kernel_info *kinfo,
     return fdt_end_node(fdt);
 }
 
+/*
+ * Start to observe the interrupt file from HS-mode, the same way
+ * imsic_ctxt_switch_from() does it for a vCPU which is switched out.
+ *
+ * The counterpart, clearing the bit of the interrupt file which is left
+ * behind, is done by imsic_vsfile_local_read_clear(), which already runs on
+ * the pCPU owning that file.
+ */
+static void cf_check imsic_local_hgeie_set(void *data)
+{
+    const struct imsic_vsfile_data *idata = data;
+
+    csr_set(CSR_HGEIE, BIT(idata->hgei, UL));
+}
+
 static void cf_check imsic_vsfile_local_update(void *data)
 {
     unsigned int i;
@@ -1144,6 +1184,19 @@ void imsic_migrate_vcpu(struct vcpu *v)
     vsfile_data.mrif = &tmrif;
     imsic_call_on_cpu(new_vsfile_cpu, imsic_vsfile_local_update, &vsfile_data);
 
+    /*
+     * A vCPU which isn't going to run right away (a cpupool move, or a
+     * migration of a vCPU which isn't runnable) is never switched in, so
+     * nobody would arm HGEIE for the new interrupt file and the state just
+     * restored into it would stay invisible to Xen until the vCPU is switched
+     * out the next time, losing the wake up it is meant to cause.
+     *
+     * For a vCPU which is about to run imsic_ctxt_switch_to() clears the bit
+     * anyway, as interrupts are then delivered to the vCPU directly.
+     */
+    if ( !v->is_running )
+        imsic_call_on_cpu(new_vsfile_cpu, imsic_local_hgeie_set, &vsfile_data);
+
     /* Set VCPU HSTATUS.VGEIN to new IMSIC VS-file */
     vcpu_guest_cpu_user_regs(v)->hstatus &= ~HSTATUS_VGEIN;
     vcpu_guest_cpu_user_regs(v)->hstatus |=
diff --git a/xen/arch/riscv/include/asm/aia.h b/xen/arch/riscv/include/asm/aia.h
index 8e4eb2f6b14e..6a05bdd8c236 100644
--- a/xen/arch/riscv/include/asm/aia.h
+++ b/xen/arch/riscv/include/asm/aia.h
@@ -12,4 +12,6 @@ void aia_init(void);
 unsigned int vgein_assign(struct vcpu *v);
 void vgein_release(struct vcpu *v, unsigned int vgein_id, unsigned int cpu);
 
+void hgei_interrupt(void);
+
 #endif /* RISCV_AIA_H */
diff --git a/xen/arch/riscv/include/asm/domain.h b/xen/arch/riscv/include/asm/domain.h
index 23e301782068..6d5eafdf5522 100644
--- a/xen/arch/riscv/include/asm/domain.h
+++ b/xen/arch/riscv/include/asm/domain.h
@@ -72,6 +72,7 @@ struct arch_vcpu {
     register_t hvip;
     uint64_t   hviprio1;
     uint64_t   hviprio2;
+    register_t hie;
 
     register_t vsatp;
     register_t vscause;
diff --git a/xen/arch/riscv/traps.c b/xen/arch/riscv/traps.c
index f5f83fce10ba..b08cf2ff2e31 100644
--- a/xen/arch/riscv/traps.c
+++ b/xen/arch/riscv/traps.c
@@ -12,9 +12,10 @@
 #include <xen/sched.h>
 #include <xen/softirq.h>
 
-#include <asm/extable.h>
+#include <asm/aia.h>
 #include <asm/cpufeature.h>
 #include <asm/emulate.h>
+#include <asm/extable.h>
 #include <asm/intc.h>
 #include <asm/processor.h>
 #include <asm/riscv_encoding.h>
@@ -273,6 +274,10 @@ void do_trap(struct cpu_user_regs *cpu_regs)
                 timer_interrupt();
                 break;
 
+            case IRQ_S_GEXT:
+                hgei_interrupt();
+                break;
+
             default:
                 intr_handled = false;
                 break;
-- 
2.55.0



^ permalink raw reply related	[flat|nested] 244+ messages in thread

* [PATCH v2 37/39] xen/riscv: map IMSIC interrupt file for vCPUs
  2026-08-27 15:20 [PATCH v2 00/39] [RISC-V] virtual interrupt controller (vAPLIC/vIMSIC) support Oleksii Kurochko
                   ` (35 preceding siblings ...)
  2026-08-27 15:21 ` [PATCH v2 36/39] xen/riscv: wake up a descheduled vCPU on a guest external interrupt Oleksii Kurochko
@ 2026-08-27 15:21 ` Oleksii Kurochko
  2026-09-21 11:36   ` Jan Beulich
  2026-08-27 15:21 ` [PATCH v2 38/39] xen/riscv: implement continue_new_vcpu() Oleksii Kurochko
  2026-08-27 15:21 ` [PATCH v2 39/39] xen/riscv: introduce IMSIC h/w interrupt file attaching to vcpu Oleksii Kurochko
  38 siblings, 1 reply; 244+ messages in thread
From: Oleksii Kurochko @ 2026-08-27 15:21 UTC (permalink / raw)
  To: xen-devel
  Cc: Romain Caritey, Baptiste Le Duc, Zheng Zhang, Oleksii Kurochko,
	Alistair Francis, Connor Davis, Andrew Cooper, Anthony PERARD,
	Michal Orzel, Jan Beulich, Julien Grall, Roger Pau Monné,
	Stefano Stabellini

A guest running in VS-mode expects its own IMSIC S-file at offset 0 of its
guest-physical IMSIC block. Physically, the guest-file (G-file) assigned to
this vCPU lives at a hart-relative offset given by guest_file_id (assigned
via the vGEIN allocator). Therefore, imsic_map_guest_file() uses stage-2
translation to redirect the guest's fixed per-vCPU GPA page (offset 0) to
the specific physical guest-file page.

Signed-off-by: Oleksii Kurochko <oleksii.kurochko@gmail.com>
---
Changes in v2:
 - Use GUEST_IMSIC_S_BASE instead of imsic_cfg.base_addr as the base of the
   guest address to map to, and change the type of gaddr to paddr_t as it
   holds a guest physical address.
 - Rename guest_stride to guest_offset: it is an offset of the VS-file inside
   the pCPU's IMSIC block, not a stride.
 - Use PRIpaddr for physical addresses and %u for unsigned values in the
   debug/error messages.
 - Switch the mapping failure message from printk() to dprintk(XENLOG_ERR, ...).
 - Update the comment above imsic_map_guest_file(): vCPUs aren't pinned, they
   run on the pCPU chosen by the scheduler, and mention that on migration a
   VS-file is acquired on the new pCPU and mapped at the same GFN, so the
   stale mapping is replaced rather than explicitly torn down.
---
---
 xen/arch/riscv/imsic.c | 66 +++++++++++++++++++++++++++++++++++++++++-
 1 file changed, 65 insertions(+), 1 deletion(-)

diff --git a/xen/arch/riscv/imsic.c b/xen/arch/riscv/imsic.c
index 07152066116a..374a21ace15f 100644
--- a/xen/arch/riscv/imsic.c
+++ b/xen/arch/riscv/imsic.c
@@ -29,6 +29,7 @@
 #include <asm/aia.h>
 #include <asm/aplic.h>
 #include <asm/imsic.h>
+#include <asm/p2m.h>
 
 #define IMSIC_HART_SIZE(guest_bits) (BIT(guest_bits, U) * IMSIC_MMIO_PAGE_SZ)
 
@@ -537,9 +538,72 @@ void cf_check imsic_ctxt_switch_to(struct vcpu *v)
     read_unlock_irqrestore(&imsic_state->vsfile_lock, flags);
 }
 
+/*
+ * Map the physical IMSIC guest interrupt file (G-file) assigned to vCPU
+ * into the domain's stage-2 guest-physical address space.
+ *
+ * In the machine's physical address space (SPA), each hart's IMSIC
+ * supervisor-level file (S-file) is located at offset 0 of its address block,
+ * followed contiguously by GEILEN guest files at offsets of 1, 2, ..., N pages.
+ *
+ * Because a guest OS running in VS-mode expects its own supervisor-level
+ * interrupt file to be at offset 0 of its guest-physical IMSIC block, the
+ * hypervisor must use stage-2 address translation to map the vCPU's
+ * guest-physical "supervisor" page (GPA offset 0) to the specific
+ * physical guest file page (SPA offset guest_file_id) on the physical hart.
+ *
+ * A vCPU runs on the pCPU the scheduler picked for it (v->processor), and
+ * the guest file it is given (guest_file_id, from the vGEIN allocator)
+ * belongs to that very pCPU's IMSIC. A guest_file_id of 0 indicates that no
+ * hardware guest file is selected (matching the architectural behavior where
+ * vGEIN = 0 in the hstatus CSR selects no guest external interrupt source),
+ * requiring the VS-file to be emulated in software.
+ *
+ * Consequently the mapping installed here is only valid as long as the vCPU
+ * stays on that pCPU. When it migrates, a VS-file is acquired on the new
+ * pCPU and mapped at the very same GFN, so the stale mapping needs no
+ * explicit tear-down: it is simply replaced.
+ *
+ * The base guest-physical address advertised to the guest in the device
+ * tree matches offset 0 of the vCPU's virtual IMSIC block. Stage-2
+ * translation ensures that guest supervisor accesses to this page are
+ * transparently routed to the real hardware VS-file granted to it on
+ * the pCPU it currently runs on.
+ */
 int imsic_map_guest_file(struct vcpu *v, unsigned int vsfile_id)
 {
-    return -EOPNOTSUPP;
+    struct domain *d = v->domain;
+    unsigned int cpu = v->processor;
+    paddr_t gaddr = GUEST_IMSIC_S_BASE + (IMSIC_MMIO_PAGE_SZ * v->vcpu_id);
+    paddr_t paddr, guest_offset;
+    int res;
+
+    /* Nothing to map in the case of sw interrupt file. */
+    if ( !vsfile_id )
+        return 0;
+
+    guest_offset = vsfile_id * IMSIC_MMIO_PAGE_SZ;
+
+    paddr = imsic_cfg.msi[cpu].base_addr + imsic_cfg.msi[cpu].offset +
+            guest_offset;
+
+#ifdef IMSIC_DEBUG
+    printk(XENLOG_DEBUG
+           "%s: %pv: ga(%#"PRIpaddr") -> pa(%#"PRIpaddr"), cpu(%u), "
+           "guest_file_id(%u) base_addr(%#"PRIpaddr") offset(%#lx)\n",
+           __func__, v, gaddr, paddr, cpu, vsfile_id,
+           imsic_cfg.msi[cpu].base_addr, imsic_cfg.msi[cpu].offset);
+#endif
+
+    res = map_regions_p2mt(d, gaddr_to_gfn(gaddr),
+                           PFN_DOWN(IMSIC_MMIO_PAGE_SZ), maddr_to_mfn(paddr),
+                           arch_dt_passthrough_p2m_type());
+    if ( res )
+        dprintk(XENLOG_ERR,
+                "%s: Failed to map %#"PRIpaddr" to the guest at %#"PRIpaddr"\n",
+                __func__, paddr, gaddr);
+
+    return res;
 }
 
 int cf_check vcpu_imsic_init(struct vcpu *v)
-- 
2.55.0



^ permalink raw reply related	[flat|nested] 244+ messages in thread

* [PATCH v2 38/39] xen/riscv: implement continue_new_vcpu()
  2026-08-27 15:20 [PATCH v2 00/39] [RISC-V] virtual interrupt controller (vAPLIC/vIMSIC) support Oleksii Kurochko
                   ` (36 preceding siblings ...)
  2026-08-27 15:21 ` [PATCH v2 37/39] xen/riscv: map IMSIC interrupt file for vCPUs Oleksii Kurochko
@ 2026-08-27 15:21 ` Oleksii Kurochko
  2026-09-21 12:12   ` Jan Beulich
  2026-08-27 15:21 ` [PATCH v2 39/39] xen/riscv: introduce IMSIC h/w interrupt file attaching to vcpu Oleksii Kurochko
  38 siblings, 1 reply; 244+ messages in thread
From: Oleksii Kurochko @ 2026-08-27 15:21 UTC (permalink / raw)
  To: xen-devel
  Cc: Romain Caritey, Baptiste Le Duc, Zheng Zhang, Oleksii Kurochko,
	Alistair Francis, Connor Davis, Andrew Cooper, Anthony PERARD,
	Michal Orzel, Jan Beulich, Julien Grall, Roger Pau Monné,
	Stefano Stabellini

continue_new_vcpu() is the arch hook invoked the first time a freshly
created vCPU is scheduled. Implement both cases it has to cover:
 - for the idle vCPU, switch to its own stack and jump to idle_loop();
 - for a guest vCPU, restore hstatus and enter the guest through the new
   return_to_new_vcpu() path in entry.S, which loads sepc, passes the
   hart id in a0 and the DTB address in a1 as expected by the RISC-V
   boot protocol, sets sstatus.SPP and executes sret.

Interrupts have to stay disabled across the restore. The trap entry
logic implicitly clears hstatus.SPV, so an interrupt taken between the
write of hstatus and sret would make sret return to HS-mode instead of
VS-mode, and restoring SPV afterwards is non-trivial. Instead interrupts
are simply kept off and sstatus.SPIE is set, so that SIE is restored from
SPIE once sret has been executed. Also, it follows what hardware will do
with real CPU which is also started with interrupts disabled.

Introduce get_cpu_info() and reset_stack_and_jump() in asm/current.h,
needed by the above. get_cpu_info() is a macro rather than a static
inline because asm/current.h is pulled in by <xen/percpu.h> before
this_cpu() is defined and before <xen/sched.h> completes struct vcpu.

idle_loop() is added as a stub on purpose; its real implementation will
come separately later.

Signed-off-by: Oleksii Kurochko <oleksii.kurochko@gmail.com>
---
Changes in v2:
 - New patch.
---
---
 xen/arch/riscv/domain.c              | 44 +++++++++++++++++++++++++++-
 xen/arch/riscv/entry.S               | 23 +++++++++++++++
 xen/arch/riscv/include/asm/current.h |  4 +++
 3 files changed, 70 insertions(+), 1 deletion(-)

diff --git a/xen/arch/riscv/domain.c b/xen/arch/riscv/domain.c
index 29181968224c..0782148b7207 100644
--- a/xen/arch/riscv/domain.c
+++ b/xen/arch/riscv/domain.c
@@ -8,10 +8,13 @@
 #include <xen/smp.h>
 #include <xen/vmap.h>
 
+#include <asm/aia.h>
+#include <asm/aplic.h>
 #include <asm/bitops.h>
 #include <asm/cpufeature.h>
 #include <asm/csr.h>
 #include <asm/current.h>
+#include <asm/imsic.h>
 #include <asm/intc.h>
 #include <asm/mmio.h>
 #include <asm/riscv_encoding.h>
@@ -140,9 +143,43 @@ static void vcpu_csr_init(struct vcpu *v)
     v->arch.hie = MIP_SGEIP;
 }
 
+static void schedule_tail(struct vcpu *prev);
+static void noreturn idle_loop(void);
+void noreturn return_to_new_vcpu(void);
+
 static void continue_new_vcpu(struct vcpu *prev)
 {
-    BUG_ON("unimplemented\n");
+    schedule_tail(prev);
+
+    if ( is_idle_vcpu(current) )
+        reset_stack_and_jump(idle_loop);
+    else
+    {
+        /*
+         * During a context switch to a new vCPU, interrupts must be disabled
+         * to guarantee that the vCPU's CSR state can be safely restored into
+         * the hart without being clobbered by an interrupt trap.
+         *
+         * For example, when return_to_new_vcpu() finishes, it executes sret.
+         * At that point, the hart checks hstatus.SPV=1 and sstatus.SPP=1 in
+         * order to return from HS-mode into VS-mode. If an interrupt were to
+         * arrive before sret, the trap entry logic would implicitly clear
+         * hstatus.SPV to 0. Correctly restoring it afterwards is non-trivial,
+         * and if left as 0, sret would incorrectly return to HS-mode instead
+         * of VS-mode.
+         *
+         * To avoid this, interrupts are kept disabled during the restore.
+         * Additionally, setting sstatus.SPIE=1 ensures that after sret is
+         * executed (as sstatus.SIE will be loaded from SPIE), HS-mode will
+         * continue to receive interrupts normally.
+         */
+        local_irq_disable();
+        csr_set(CSR_SSTATUS, SSTATUS_SPIE);
+
+        csr_write(CSR_HSTATUS, vcpu_guest_cpu_user_regs(current)->hstatus);
+
+        reset_stack_and_jump(return_to_new_vcpu);
+    }
 }
 
 int arch_vcpu_create(struct vcpu *v)
@@ -551,3 +588,8 @@ static void __init __maybe_unused build_assertions(void)
      */
     BUILD_BUG_ON(offsetof(struct cpu_info, guest_cpu_user_regs));
 }
+
+static void noreturn idle_loop(void)
+{
+    BUG_ON("unimplemented");
+}
diff --git a/xen/arch/riscv/entry.S b/xen/arch/riscv/entry.S
index 331446a238d3..bf1843dcea4f 100644
--- a/xen/arch/riscv/entry.S
+++ b/xen/arch/riscv/entry.S
@@ -143,3 +143,26 @@ FUNC(__context_switch)
 
         ret
 END(__context_switch)
+
+/* t0 is used as a temporary reg and is clobbered to oblivion */
+FUNC(return_to_new_vcpu)
+        /* Swap tp with sscratch */
+        csrrw   tp, CSR_SSCRATCH, tp
+
+        /* Set vCPU registers */
+        REG_L   t0, CPU_USER_REGS_SEPC(sp)
+        csrw    sepc, t0
+
+        /* Hartid goes to a0 */
+        REG_L   a0, CPU_USER_REGS_A0(sp)
+
+        /* DTB goes to a1 */
+        REG_L   a1, CPU_USER_REGS_A1(sp)
+
+        /* Set guest mode to supervisor */
+        li      t0, SSTATUS_SPP
+        csrs    CSR_SSTATUS, t0
+
+        /* Enter guest */
+        sret
+END(return_to_new_vcpu)
diff --git a/xen/arch/riscv/include/asm/current.h b/xen/arch/riscv/include/asm/current.h
index 78ec52fd8a35..f8babcc3d926 100644
--- a/xen/arch/riscv/include/asm/current.h
+++ b/xen/arch/riscv/include/asm/current.h
@@ -47,6 +47,8 @@ DECLARE_PER_CPU(struct vcpu *, curr_vcpu);
 #define set_current(vcpu)  do { current = (vcpu); } while (0)
 #define get_cpu_current(cpu)  per_cpu(curr_vcpu, cpu)
 
+#define get_cpu_info() (current->arch.cpu_info)
+
 #define guest_cpu_user_regs() ({ BUG_ON("unimplemented"); NULL; })
 #define vcpu_guest_cpu_user_regs(vcpu) \
     (&(vcpu)->arch.cpu_info->guest_cpu_user_regs)
@@ -58,6 +60,8 @@ DECLARE_PER_CPU(struct vcpu *, curr_vcpu);
     unreachable();                                          \
 } while ( false )
 
+#define reset_stack_and_jump(fn) switch_stack_and_jump(get_cpu_info(), fn)
+
 #define get_per_cpu_offset() __per_cpu_offset[smp_processor_id()]
 
 #endif /* __ASSEMBLER__ */
-- 
2.55.0



^ permalink raw reply related	[flat|nested] 244+ messages in thread

* [PATCH v2 39/39] xen/riscv: introduce IMSIC h/w interrupt file attaching to vcpu
  2026-08-27 15:20 [PATCH v2 00/39] [RISC-V] virtual interrupt controller (vAPLIC/vIMSIC) support Oleksii Kurochko
                   ` (37 preceding siblings ...)
  2026-08-27 15:21 ` [PATCH v2 38/39] xen/riscv: implement continue_new_vcpu() Oleksii Kurochko
@ 2026-08-27 15:21 ` Oleksii Kurochko
  2026-09-21 12:32   ` Jan Beulich
  38 siblings, 1 reply; 244+ messages in thread
From: Oleksii Kurochko @ 2026-08-27 15:21 UTC (permalink / raw)
  To: xen-devel
  Cc: Romain Caritey, Baptiste Le Duc, Zheng Zhang, Oleksii Kurochko,
	Alistair Francis, Connor Davis, Andrew Cooper, Anthony PERARD,
	Michal Orzel, Jan Beulich, Julien Grall, Roger Pau Monné,
	Stefano Stabellini

Introduce imsic_vsfile_attach() to initialize the AIA-related state needed
for a vCPU to have a working guest interrupt file.

A guest (VS) interrupt file must be mapped to one of a pCPU's
hardware interrupt files (if they exist), so the pCPU a vCPU will actually
run on needs to be known first. arch_vcpu_create() is therefore not a
suitable place to call vcpu_aia_init(), since the pCPU assigned to a
vCPU can still change before it is first scheduled. To avoid
reassigning the VS interrupt file id and remapping it to a different
pCPU's hardware interrupt file, imsic_vsfile_attach() is called from a
later point in the scheduling path (e.g. continue_new_vcpu()). Since
it will end up being called from a non-__init context, it is not
itself marked __init.

Introduce imsic_update_state() to update a vCPU's guest IMSIC state
(the guest interrupt file id and the pCPU whose hardware interrupt
file it is mapped to) as a single consistent unit. This state can be
read concurrently, e.g. by a future helper that checks whether a
vCPU has a pending IMSIC interrupt, though no such consumer exists
yet at this stage, so it is protected by a lock.

Signed-off-by: Oleksii Kurochko <oleksii.kurochko@gmail.com>
---
Changes in v2:
 - Update vcpu_aia_init() to catch sw interrupt file and update some debug
   messages in it.
 - Add vgein_release() if IMSIC h/w mapping failed.
 - imsic_update_state(): store v->processor rather than cpuid_to_hartid(),
   as ->vsfile_cpu is consumed as a Xen CPU id (aplic_hart_field(),
   cpumask_of()) and its NR_CPUS sentinel lives in that numbering space.
 - Drop parantethis aroud guest_file_id ? ... in imsic_update_state().
 - Rename vcpu_aia_init to imsic_vsfile_attach() and move the code to
   imsic.c.
---
---
 xen/arch/riscv/domain.c            |   2 +
 xen/arch/riscv/imsic.c             | 146 +++++++++++++++++++++++------
 xen/arch/riscv/include/asm/imsic.h |   2 +
 3 files changed, 121 insertions(+), 29 deletions(-)

diff --git a/xen/arch/riscv/domain.c b/xen/arch/riscv/domain.c
index 0782148b7207..15b6bfffa97d 100644
--- a/xen/arch/riscv/domain.c
+++ b/xen/arch/riscv/domain.c
@@ -155,6 +155,8 @@ static void continue_new_vcpu(struct vcpu *prev)
         reset_stack_and_jump(idle_loop);
     else
     {
+        imsic_vsfile_attach(current);
+
         /*
          * During a context switch to a new vCPU, interrupts must be disabled
          * to guarantee that the vCPU's CSR state can be safely restored into
diff --git a/xen/arch/riscv/imsic.c b/xen/arch/riscv/imsic.c
index 374a21ace15f..ad638d748517 100644
--- a/xen/arch/riscv/imsic.c
+++ b/xen/arch/riscv/imsic.c
@@ -212,7 +212,14 @@ unsigned int vcpu_guest_file_id(const struct vcpu *v)
 
 void imsic_update_state(struct vcpu *v, unsigned int guest_file_id)
 {
-    BUG_ON("unimplemented\n");
+    unsigned long flags;
+    struct vimsic_state *vimsic_state = v->arch.vimsic_state;
+    unsigned int cpu = guest_file_id ? v->processor : NR_CPUS;
+
+    write_lock_irqsave(&vimsic_state->vsfile_lock, flags);
+    vimsic_state->guest_file_id = guest_file_id;
+    vimsic_state->vsfile_cpu = cpu;
+    write_unlock_irqrestore(&vimsic_state->vsfile_lock, flags);
 }
 
 void __init imsic_ids_local_delivery(bool enable)
@@ -640,6 +647,16 @@ struct imsic_vsfile_data {
     struct imsic_mrif *mrif;
 };
 
+/*
+ * Number of 64-bit EIx groups needed to cover all the interrupt identities an
+ * IMSIC interrupt file provides, which are 0 (never valid, but it still
+ * occupies a bit) up to and including imsic_cfg.nr_ids.
+ */
+static unsigned int imsic_nr_eix(void)
+{
+    return DIV_ROUND_UP(imsic_cfg.nr_ids + 1, BITS_PER_TYPE(uint64_t));
+}
+
 /*
  * Execute func() on the pCPU which owns the IMSIC interrupt file func() is
  * going to work with.
@@ -1138,20 +1155,90 @@ static void cf_check imsic_vsfile_local_update(void *data)
     csr_write(CSR_VSISELECT, old_vsiselect);
 }
 
+/*
+ * Point the vCPU's HSTATUS.VGEIN at the guest interrupt file it has been
+ * given. It is applied to the hart when the vCPU's context is restored.
+ */
+static void vcpu_set_vgein(struct vcpu *v, unsigned int vsfile_id)
+{
+    unsigned long hstatus = vcpu_guest_cpu_user_regs(v)->hstatus;
+
+    hstatus &= ~HSTATUS_VGEIN;
+    hstatus |= MASK_INSR(vsfile_id, HSTATUS_VGEIN);
+
+    vcpu_guest_cpu_user_regs(v)->hstatus = hstatus;
+}
+
+/*
+ * Take a h/w guest interrupt file of 'cpu' for the vCPU: zero the file out,
+ * map it into the domain's G-stage at the vCPU's virtual IMSIC page and
+ * record the new location in the per-vCPU IMSIC state.
+ *
+ * HSTATUS.VGEIN is deliberately left alone: the vCPU may be pointed at the
+ * file only when the file already holds the vCPU's interrupt state, which in
+ * the case of imsic_migrate_vcpu() happens only after the old file has been
+ * moved to the new one. Thereby it is up to the caller to call
+ * vcpu_set_vgein() at the right moment.
+ *
+ * Returns the id of the taken interrupt file, or 0 if none could be taken, in
+ * which case the domain is crashed.
+ */
+static unsigned int imsic_vsfile_acquire(struct vcpu *v, unsigned int cpu)
+{
+    struct imsic_vsfile_data vsfile_data = { .nr_eix = imsic_nr_eix() };
+    unsigned int vsfile_id;
+    int rc;
+
+    vsfile_id = vgein_assign(v);
+    if ( !vsfile_id )
+    {
+        /*
+         * vgein_assign() returns 0 when no free h/w guest interrupt file is
+         * available. s/w guest interrupt files aren't supported yet, so such
+         * a vCPU can't be run.
+         */
+        domain_crash(v->domain,
+                     "%pv: no free h/w guest interrupt file on CPU%u\n",
+                     v, cpu);
+        return 0;
+    }
+
+    vsfile_data.hgei = vsfile_id;
+
+    /* The file could still hold the state of its previous owner */
+    imsic_call_on_cpu(cpu, imsic_vsfile_local_clear, &vsfile_data);
+
+    rc = imsic_map_guest_file(v, vsfile_id);
+    if ( rc )
+    {
+        vgein_release(v, vsfile_id, cpu);
+
+        /* Can't continue w/o correctly mapped IMSIC interrupt file */
+        domain_crash(v->domain,
+                     "%pv: failed to map h/w guest interrupt file %u: %d\n",
+                     v, vsfile_id, rc);
+        return 0;
+    }
+
+    imsic_update_state(v, vsfile_id);
+
+    return vsfile_id;
+}
+
 void imsic_migrate_vcpu(struct vcpu *v)
 {
-    unsigned int new_vsfile_hgei;
+    unsigned int new_vsfile_id;
     unsigned int new_vsfile_cpu;
-    unsigned int nr_hw_eix = DIV_ROUND_UP(imsic_cfg.nr_ids + 1,
-                                          BITS_PER_TYPE(uint64_t));
-    struct imsic_vsfile_data vsfile_data = {
-        .nr_eix = nr_hw_eix,
-    };
+    unsigned int nr_hw_eix = imsic_nr_eix();
     struct vimsic_state *imsic_state = v->arch.vimsic_state;
     unsigned long flags;
     unsigned int old_vsfile_id;
     unsigned int old_vsfile_cpu;
     struct imsic_mrif tmrif = { };
+    struct imsic_vsfile_data vsfile_data = {
+        .nr_eix = nr_hw_eix,
+        .mrif = &tmrif,
+    };
 
     /*
      * The scheduler can mark a freshly created vCPU's unit as migrated and
@@ -1189,25 +1276,10 @@ void imsic_migrate_vcpu(struct vcpu *v)
      */
     new_vsfile_cpu = v->processor;
 
-    new_vsfile_hgei = vgein_assign(v);
-
-    /* We don't support SW interrupt files at the moment. */
-    BUG_ON(!new_vsfile_hgei);
-
-    vsfile_data.hgei = new_vsfile_hgei;
-
-    /* Zero-out new IMSIC VS-file */
-    imsic_call_on_cpu(new_vsfile_cpu, imsic_vsfile_local_clear, &vsfile_data);
-
-    /* Update G-stage mapping for the new IMSIC VS-file */
-    if ( imsic_map_guest_file(v, new_vsfile_hgei) )
-    {
-        domain_crash(v->domain, "Migration to hw interrupt file failed\n");
-
+    /* Zero-out, map and start to use the new IMSIC VS-file */
+    new_vsfile_id = imsic_vsfile_acquire(v, new_vsfile_cpu);
+    if ( !new_vsfile_id )
         return;
-    }
-
-    imsic_update_state(v, new_vsfile_hgei);
 
     /*
      * TODO: Modify the relevant translation tables at all IOMMUs so that MSIs
@@ -1245,7 +1317,7 @@ void imsic_migrate_vcpu(struct vcpu *v)
     vgein_release(v, old_vsfile_id, old_vsfile_cpu);
 
     /* Restore register state in the new IMSIC VS-file */
-    vsfile_data.mrif = &tmrif;
+    vsfile_data.hgei = new_vsfile_id;
     imsic_call_on_cpu(new_vsfile_cpu, imsic_vsfile_local_update, &vsfile_data);
 
     /*
@@ -1262,7 +1334,23 @@ void imsic_migrate_vcpu(struct vcpu *v)
         imsic_call_on_cpu(new_vsfile_cpu, imsic_local_hgeie_set, &vsfile_data);
 
     /* Set VCPU HSTATUS.VGEIN to new IMSIC VS-file */
-    vcpu_guest_cpu_user_regs(v)->hstatus &= ~HSTATUS_VGEIN;
-    vcpu_guest_cpu_user_regs(v)->hstatus |=
-            MASK_INSR(new_vsfile_hgei, HSTATUS_VGEIN);
+    vcpu_set_vgein(v, new_vsfile_id);
+}
+
+void imsic_vsfile_attach(struct vcpu *v)
+{
+    unsigned int new_vsfile_id;
+
+    if ( !aia_usable() )
+        return;
+
+    new_vsfile_id = imsic_vsfile_acquire(v, v->processor);
+    if ( !new_vsfile_id )
+        return;
+
+    /*
+     * The vCPU has never run yet, so the just zeroed out file is all the
+     * interrupt state it has and HSTATUS.VGEIN can be pointed at it at once.
+     */
+    vcpu_set_vgein(v, new_vsfile_id);
 }
diff --git a/xen/arch/riscv/include/asm/imsic.h b/xen/arch/riscv/include/asm/imsic.h
index 6395b539c52d..f0edf0bff5d9 100644
--- a/xen/arch/riscv/include/asm/imsic.h
+++ b/xen/arch/riscv/include/asm/imsic.h
@@ -125,4 +125,6 @@ int imsic_map_guest_file(struct vcpu *v, unsigned int vsfile_id);
 
 void imsic_migrate_vcpu(struct vcpu *v);
 
+void imsic_vsfile_attach(struct vcpu *v);
+
 #endif /* ASM_RISCV_IMSIC_H */
-- 
2.55.0



^ permalink raw reply related	[flat|nested] 244+ messages in thread

* Re: [PATCH v2 04/39] xen/riscv: introduce csr_read64()
  2026-08-27 15:20 ` [PATCH v2 04/39] xen/riscv: introduce csr_read64() Oleksii Kurochko
@ 2026-08-27 15:36   ` Andrew Cooper
  2026-08-31 12:42     ` Oleksii Kurochko
  0 siblings, 1 reply; 244+ messages in thread
From: Andrew Cooper @ 2026-08-27 15:36 UTC (permalink / raw)
  To: Oleksii Kurochko, xen-devel
  Cc: Andrew Cooper, Romain Caritey, Baptiste Le Duc, Zheng Zhang,
	Alistair Francis, Connor Davis, Anthony PERARD, Michal Orzel,
	Jan Beulich, Julien Grall, Roger Pau Monné,
	Stefano Stabellini

On 27/08/2026 4:20 pm, Oleksii Kurochko wrote:
> diff --git a/xen/arch/riscv/include/asm/csr.h b/xen/arch/riscv/include/asm/csr.h
> index 888d6a2a86d6..a5cdd6f99c8e 100644
> --- a/xen/arch/riscv/include/asm/csr.h
> +++ b/xen/arch/riscv/include/asm/csr.h
> @@ -39,12 +39,36 @@
>      csr_write(csr, v_);             \
>      csr_write(csr ## H, v_ >> 32);  \
>  })
> +
> +/*
> + * The two halves are read by separate instructions, so a CSR which hardware
> + * increments can carry from the low half into the high one in between,
> + * yielding a value the CSR never held. Re-read the high half and retry the
> + * sequence if it changed.
> + */
> +#define csr_read64(csr)                         \
> +({                                              \
> +    uint32_t hi_, lo_;                          \
> +                                                \
> +    do {                                        \
> +        hi_ = csr_read(csr ## H);               \
> +        lo_ = csr_read(csr);                    \
> +    } while ( hi_ != csr_read(csr ## H) );      \
> +                                                \
> +    ((uint64_t)hi_ << 32) | lo_;                \
> +})

This double reads H in the looping case.  You want something more like:

hi = csr_read();
do {
    old = hi;
    lo = csr_read();
} while ( (hi = csr_read()) != old );


Still, this only matters for volatile CSRs, and is unnecessary in the
general case.  I'd suggest naming it csr_volatile_read64().  Most CSRs
can use a simple split access.

~Andrew


^ permalink raw reply	[flat|nested] 244+ messages in thread

* Re: [PATCH v2 04/39] xen/riscv: introduce csr_read64()
  2026-08-27 15:36   ` Andrew Cooper
@ 2026-08-31 12:42     ` Oleksii Kurochko
  2026-09-01  7:07       ` Jan Beulich
  0 siblings, 1 reply; 244+ messages in thread
From: Oleksii Kurochko @ 2026-08-31 12:42 UTC (permalink / raw)
  To: Andrew Cooper, xen-devel
  Cc: Romain Caritey, Baptiste Le Duc, Zheng Zhang, Alistair Francis,
	Connor Davis, Anthony PERARD, Michal Orzel, Jan Beulich,
	Julien Grall, Roger Pau Monné, Stefano Stabellini



On 8/27/26 5:36 PM, Andrew Cooper wrote:
> On 27/08/2026 4:20 pm, Oleksii Kurochko wrote:
>> diff --git a/xen/arch/riscv/include/asm/csr.h b/xen/arch/riscv/include/asm/csr.h
>> index 888d6a2a86d6..a5cdd6f99c8e 100644
>> --- a/xen/arch/riscv/include/asm/csr.h
>> +++ b/xen/arch/riscv/include/asm/csr.h
>> @@ -39,12 +39,36 @@
>>       csr_write(csr, v_);             \
>>       csr_write(csr ## H, v_ >> 32);  \
>>   })
>> +
>> +/*
>> + * The two halves are read by separate instructions, so a CSR which hardware
>> + * increments can carry from the low half into the high one in between,
>> + * yielding a value the CSR never held. Re-read the high half and retry the
>> + * sequence if it changed.
>> + */
>> +#define csr_read64(csr)                         \
>> +({                                              \
>> +    uint32_t hi_, lo_;                          \
>> +                                                \
>> +    do {                                        \
>> +        hi_ = csr_read(csr ## H);               \
>> +        lo_ = csr_read(csr);                    \
>> +    } while ( hi_ != csr_read(csr ## H) );      \
>> +                                                \
>> +    ((uint64_t)hi_ << 32) | lo_;                \
>> +})
> 
> This double reads H in the looping case.  You want something more like:
> 
> hi = csr_read();
> do {
>      old = hi;
>      lo = csr_read();
> } while ( (hi = csr_read()) != old );

Good point. I'll apply that.

> 
> 
> Still, this only matters for volatile CSRs, and is unnecessary in the
> general case.  I'd suggest naming it csr_volatile_read64(). 

Yes, that makes sense. I will rename it to csr_volatile_read64().

> Most CSRs
> can use a simple split access.

I may have misunderstood you here, but wouldn't it still make sense to 
have a macro covering the case where a register is 64-bit on RV32 yet 
accessed through two CSRs? VSIE and VSIEH, for example.

My plan was to use a single csr_read64() (but while loop then really 
isn't needed in this case) call to abstract the access to VSIE, so that 
the code looks the same on RV32 and RV64.

Does that make sense, or would it be better to have separate vsie and 
vsieh fields instead?

~ Oleksii


^ permalink raw reply	[flat|nested] 244+ messages in thread

* Re: [PATCH v2 01/39] xen/riscv: drop pregs from struct cpu_user_regs
  2026-08-27 15:20 ` [PATCH v2 01/39] xen/riscv: drop pregs from struct cpu_user_regs Oleksii Kurochko
@ 2026-08-31 12:48   ` Baptiste Le Duc
  2026-09-01  6:58     ` Jan Beulich
  0 siblings, 1 reply; 244+ messages in thread
From: Baptiste Le Duc @ 2026-08-31 12:48 UTC (permalink / raw)
  To: Oleksii Kurochko
  Cc: xen-devel, Romain Caritey, Baptiste Le Duc, Zheng Zhang,
	Alistair Francis, Connor Davis, Andrew Cooper, Anthony PERARD,
	Michal Orzel, Jan Beulich, Julien Grall, Roger Pau Monné,
	Stefano Stabellini

On Thu, 27 Aug 2026 17:20:45 +0200, Oleksii Kurochko <oleksii.kurochko@gmail.com> wrote:
> As nothing is using pregs in upstream or downstream ports of RISC-V drop
> it from struct cpu_user_regs. Once it will really be needed re-introduce
> it.

Reviewed-by: Baptiste Le Duc <baptiste.le-duc@vates.tech>

-- 
Baptiste Le Duc <baptiste.le-duc@vates.tech>


^ permalink raw reply	[flat|nested] 244+ messages in thread

* Re: [PATCH v2 02/39] xen/riscv: drop bug.h's duplicate instruction length helpers
  2026-08-27 15:20 ` [PATCH v2 02/39] xen/riscv: drop bug.h's duplicate instruction length helpers Oleksii Kurochko
@ 2026-08-31 12:48   ` Baptiste Le Duc
  2026-09-01  7:01   ` Jan Beulich
  1 sibling, 0 replies; 244+ messages in thread
From: Baptiste Le Duc @ 2026-08-31 12:48 UTC (permalink / raw)
  To: Oleksii Kurochko
  Cc: xen-devel, Romain Caritey, Baptiste Le Duc, Zheng Zhang,
	Alistair Francis, Connor Davis, Andrew Cooper, Anthony PERARD,
	Michal Orzel, Jan Beulich, Julien Grall, Roger Pau Monné,
	Stefano Stabellini

On Thu, 27 Aug 2026 17:20:46 +0200, Oleksii Kurochko <oleksii.kurochko@gmail.com> wrote:
> asm/riscv_encoding.h already provides INSN_16BIT_MASK and INSN_LEN(), and
> emulate.c uses them, so the tree carried two spellings of the same thing
> which could drift apart. COMPRESSED_INSN_MASK never had a user.
> 
> No functional change.
> 
> 
> [...]

Reviewed-by: Baptiste Le Duc <baptiste.le-duc@vates.tech>

-- 
Baptiste Le Duc <baptiste.le-duc@vates.tech>


^ permalink raw reply	[flat|nested] 244+ messages in thread

* Re: [PATCH v2 03/39] xen/riscv: set the guest's XLEN explicitly in hstatus.VSXL
  2026-08-27 15:20 ` [PATCH v2 03/39] xen/riscv: set the guest's XLEN explicitly in hstatus.VSXL Oleksii Kurochko
@ 2026-08-31 12:48   ` Baptiste Le Duc
  2026-09-01  7:03     ` Jan Beulich
                       ` (2 more replies)
  2026-09-01 15:20   ` Jan Beulich
  1 sibling, 3 replies; 244+ messages in thread
From: Baptiste Le Duc @ 2026-08-31 12:48 UTC (permalink / raw)
  To: Oleksii Kurochko
  Cc: xen-devel, Romain Caritey, Baptiste Le Duc, Zheng Zhang,
	Alistair Francis, Connor Davis, Andrew Cooper, Anthony PERARD,
	Michal Orzel, Jan Beulich, Julien Grall, Roger Pau Monné,
	Stefano Stabellini

> hstatus.VSXL is WARL, so its reset value is implementation-defined. Xen
> supports 64-bit guests only, so program it explicitly instead of relying
> on whatever the hardware happens to leave there.
> 
> This matters beyond the guest's own view of itself: decoding a trapped
> instruction depends on the effective XLEN of the guest, as the encodings
> which exist for XLEN=64 only must not be recognized for a 32-bit one.
>
It's not clear which instruction "decoding a trapped instruction" refers
to without more context. I assume you mean decode_ldst_insn() in
emulate.c, but the patch introducing that function comes later in the
series, so this isn't obvious on a first read.

Please reorder the series so this patch follows the one introducing
decode_ldst_insn(), or reference it explicitly in the commit message
(e.g. "load/store trap emulation, introduced later in this series in
emulate.c, needs...").

-- 
Baptiste Le Duc <baptiste.le-duc@vates.tech>


^ permalink raw reply	[flat|nested] 244+ messages in thread

* Re: [PATCH v2 05/39] xen/riscv: request a G-stage flush on vmenter when VMIDs are disabled
  2026-08-27 15:20 ` [PATCH v2 05/39] xen/riscv: request a G-stage flush on vmenter when VMIDs are disabled Oleksii Kurochko
@ 2026-08-31 12:48   ` Baptiste Le Duc
  2026-09-01  8:43     ` Oleksii Kurochko
  0 siblings, 1 reply; 244+ messages in thread
From: Baptiste Le Duc @ 2026-08-31 12:48 UTC (permalink / raw)
  To: Oleksii Kurochko
  Cc: xen-devel, Romain Caritey, Baptiste Le Duc, Zheng Zhang,
	Alistair Francis, Connor Davis, Andrew Cooper, Anthony PERARD,
	Michal Orzel, Jan Beulich, Julien Grall, Roger Pau Monné,
	Stefano Stabellini

> vmid_handle_vmenter() reports that no flush is needed when VMIDs are
> unavailable (vmid=off, or hardware with no more than one VMID bit). Every
> domain then runs under VMID 0 with nothing flushed in between, so as soon
> as a hart runs more than one domain, a domain entered there can use the
> G-stage translations left behind by the domain which ran before it.
> 
> Adjust the comment in p2m_handle_vmenter() accordingly: skipping the
> VS-stage flush no longer relies on an old VMID not being reused, which
> doesn't hold when there are no VMIDs to begin with.
> 
> While at it, spell the other early return as a bool literal.
> 
> Fixes: bff3b9ea4696 ("xen/riscv: introduce VMID allocation and manegement")
> Signed-off-by: Oleksii Kurochko <oleksii.kurochko@gmail.com>
>

FWIW this comment and p2m_handle_vmenter() itself get dropped a few
patches later in "implement vCPU context switching", which folds the
VMID claim into p2m_ctxt_switch_to(). The fix survives there via the
need_flush check. Is this patch really needed?

-- 
Baptiste Le Duc <baptiste.le-duc@vates.tech>


^ permalink raw reply	[flat|nested] 244+ messages in thread

* Re: [PATCH v2 06/39] xen/riscv: use UINT64_MAX to disable the VS-timer
  2026-08-27 15:20 ` [PATCH v2 06/39] xen/riscv: use UINT64_MAX to disable the VS-timer Oleksii Kurochko
@ 2026-08-31 12:48   ` Baptiste Le Duc
  2026-09-01  7:12   ` Jan Beulich
  1 sibling, 0 replies; 244+ messages in thread
From: Baptiste Le Duc @ 2026-08-31 12:48 UTC (permalink / raw)
  To: Oleksii Kurochko
  Cc: xen-devel, Romain Caritey, Baptiste Le Duc, Zheng Zhang,
	Alistair Francis, Connor Davis, Andrew Cooper, Anthony PERARD,
	Michal Orzel, Jan Beulich, Julien Grall, Roger Pau Monné,
	Stefano Stabellini

On Thu, 27 Aug 2026 17:20:50 +0200, Oleksii Kurochko <oleksii.kurochko@gmail.com> wrote:
> vstimecmp is a 64-bit CSR independently of XLEN, which is why it is
> written with csr_write64(). On RV32 that macro splits the value into
> the vstimecmp/vstimecmph pair, so passing ULONG_MAX (0xffffffff there)
> writes all ones to the low half and zero to the high half, leaving the
> CSR at 0x00000000ffffffff rather than at its maximum. A VS-timer irq
> would then become pending as soon as (time + htimedelta) reaches 2^32,
> which is exactly what the code is trying to avoid.
> 
> [...]

Reviewed-by: Baptiste Le Duc <baptiste.le-duc@vates.tech>

-- 
Baptiste Le Duc <baptiste.le-duc@vates.tech>


^ permalink raw reply	[flat|nested] 244+ messages in thread

* Re: [PATCH v2 01/39] xen/riscv: drop pregs from struct cpu_user_regs
  2026-08-31 12:48   ` Baptiste Le Duc
@ 2026-09-01  6:58     ` Jan Beulich
  0 siblings, 0 replies; 244+ messages in thread
From: Jan Beulich @ 2026-09-01  6:58 UTC (permalink / raw)
  To: Oleksii Kurochko
  Cc: xen-devel, Romain Caritey, Zheng Zhang, Alistair Francis,
	Connor Davis, Andrew Cooper, Anthony PERARD, Michal Orzel,
	Julien Grall, Roger Pau Monné, Stefano Stabellini,
	Baptiste Le Duc

On 31.08.2026 14:48, Baptiste Le Duc wrote:
> On Thu, 27 Aug 2026 17:20:45 +0200, Oleksii Kurochko <oleksii.kurochko@gmail.com> wrote:
>> As nothing is using pregs in upstream or downstream ports of RISC-V drop
>> it from struct cpu_user_regs. Once it will really be needed re-introduce
>> it.
> 
> Reviewed-by: Baptiste Le Duc <baptiste.le-duc@vates.tech>

Acked-by: Jan Beulich <jbeulich@suse.com>



^ permalink raw reply	[flat|nested] 244+ messages in thread

* Re: [PATCH v2 02/39] xen/riscv: drop bug.h's duplicate instruction length helpers
  2026-08-27 15:20 ` [PATCH v2 02/39] xen/riscv: drop bug.h's duplicate instruction length helpers Oleksii Kurochko
  2026-08-31 12:48   ` Baptiste Le Duc
@ 2026-09-01  7:01   ` Jan Beulich
  2026-09-02 10:48     ` Oleksii Kurochko
  1 sibling, 1 reply; 244+ messages in thread
From: Jan Beulich @ 2026-09-01  7:01 UTC (permalink / raw)
  To: Oleksii Kurochko
  Cc: Romain Caritey, Baptiste Le Duc, Zheng Zhang, Alistair Francis,
	Connor Davis, Andrew Cooper, Anthony PERARD, Michal Orzel,
	Julien Grall, Roger Pau Monné, Stefano Stabellini, xen-devel

On 27.08.2026 17:20, Oleksii Kurochko wrote:
> asm/riscv_encoding.h already provides INSN_16BIT_MASK and INSN_LEN(), and
> emulate.c uses them, so the tree carried two spellings of the same thing
> which could drift apart. COMPRESSED_INSN_MASK never had a user.
> 
> No functional change.
> 
> Signed-off-by: Oleksii Kurochko <oleksii.kurochko@gmail.com>

As I'm happy to see the duplication go away:
Acked-by: Jan Beulich <jbeulich@suse.com>
However, ...

> --- a/xen/arch/riscv/traps.c
> +++ b/xen/arch/riscv/traps.c
> @@ -214,7 +214,7 @@ void do_trap(struct cpu_user_regs *cpu_regs)
>                  die();
>              }
>  
> -            cpu_regs->sepc += GET_INSN_LENGTH(*(uint16_t *)pc);
> +            cpu_regs->sepc += INSN_LEN(*(uint16_t *)pc);

... for one I'd prefer if we took the opportunity and add "const" to the
pointer target here.

And then

#define INSN_16BIT_MASK			0x3
#define INSN_32BIT_MASK			0x1c

are really named backwards, seeing e.g. their use in

#define INSN_IS_16BIT(insn)		\
	(((insn) & INSN_16BIT_MASK) != INSN_16BIT_MASK)
#define INSN_IS_32BIT(insn)		\
	(((insn) & INSN_16BIT_MASK) == INSN_16BIT_MASK && \
	 ((insn) & INSN_32BIT_MASK) != INSN_32BIT_MASK)

Furthermore,

#define INSN_LEN(insn)			(INSN_IS_16BIT(insn) ? 2 : 4)

fails to use INSN_32BIT_MASK / INSN_IS_32BIT() altogether.

Jan


^ permalink raw reply	[flat|nested] 244+ messages in thread

* Re: [PATCH v2 03/39] xen/riscv: set the guest's XLEN explicitly in hstatus.VSXL
  2026-08-31 12:48   ` Baptiste Le Duc
@ 2026-09-01  7:03     ` Jan Beulich
  2026-09-01  8:40     ` Oleksii Kurochko
  2026-09-01 15:16     ` Jan Beulich
  2 siblings, 0 replies; 244+ messages in thread
From: Jan Beulich @ 2026-09-01  7:03 UTC (permalink / raw)
  To: Baptiste Le Duc, Oleksii Kurochko
  Cc: xen-devel, Romain Caritey, Zheng Zhang, Alistair Francis,
	Connor Davis, Andrew Cooper, Anthony PERARD, Michal Orzel,
	Julien Grall, Roger Pau Monné, Stefano Stabellini

On 31.08.2026 14:48, Baptiste Le Duc wrote:
>> hstatus.VSXL is WARL, so its reset value is implementation-defined. Xen
>> supports 64-bit guests only, so program it explicitly instead of relying
>> on whatever the hardware happens to leave there.
>>
>> This matters beyond the guest's own view of itself: decoding a trapped
>> instruction depends on the effective XLEN of the guest, as the encodings
>> which exist for XLEN=64 only must not be recognized for a 32-bit one.
>>
> It's not clear which instruction "decoding a trapped instruction" refers
> to without more context. I assume you mean decode_ldst_insn() in
> emulate.c, but the patch introducing that function comes later in the
> series, so this isn't obvious on a first read.
> 
> Please reorder the series so this patch follows the one introducing
> decode_ldst_insn(), or reference it explicitly in the commit message
> (e.g. "load/store trap emulation, introduced later in this series in
> emulate.c, needs...").

Just to remind you: No "later in this series" or alike in patch descriptions
please. Such wording loses meaning when viewed in git history.

Jan


^ permalink raw reply	[flat|nested] 244+ messages in thread

* Re: [PATCH v2 04/39] xen/riscv: introduce csr_read64()
  2026-08-31 12:42     ` Oleksii Kurochko
@ 2026-09-01  7:07       ` Jan Beulich
  0 siblings, 0 replies; 244+ messages in thread
From: Jan Beulich @ 2026-09-01  7:07 UTC (permalink / raw)
  To: Oleksii Kurochko
  Cc: Romain Caritey, Baptiste Le Duc, Zheng Zhang, Alistair Francis,
	Connor Davis, Anthony PERARD, Michal Orzel, Julien Grall,
	Roger Pau Monné, Stefano Stabellini, Andrew Cooper,
	xen-devel

On 31.08.2026 14:42, Oleksii Kurochko wrote:
> 
> 
> On 8/27/26 5:36 PM, Andrew Cooper wrote:
>> On 27/08/2026 4:20 pm, Oleksii Kurochko wrote:
>>> diff --git a/xen/arch/riscv/include/asm/csr.h b/xen/arch/riscv/include/asm/csr.h
>>> index 888d6a2a86d6..a5cdd6f99c8e 100644
>>> --- a/xen/arch/riscv/include/asm/csr.h
>>> +++ b/xen/arch/riscv/include/asm/csr.h
>>> @@ -39,12 +39,36 @@
>>>       csr_write(csr, v_);             \
>>>       csr_write(csr ## H, v_ >> 32);  \
>>>   })
>>> +
>>> +/*
>>> + * The two halves are read by separate instructions, so a CSR which hardware
>>> + * increments can carry from the low half into the high one in between,
>>> + * yielding a value the CSR never held. Re-read the high half and retry the
>>> + * sequence if it changed.
>>> + */
>>> +#define csr_read64(csr)                         \
>>> +({                                              \
>>> +    uint32_t hi_, lo_;                          \
>>> +                                                \
>>> +    do {                                        \
>>> +        hi_ = csr_read(csr ## H);               \
>>> +        lo_ = csr_read(csr);                    \
>>> +    } while ( hi_ != csr_read(csr ## H) );      \
>>> +                                                \
>>> +    ((uint64_t)hi_ << 32) | lo_;                \
>>> +})
>>
>> This double reads H in the looping case.  You want something more like:
>>
>> hi = csr_read();
>> do {
>>      old = hi;
>>      lo = csr_read();
>> } while ( (hi = csr_read()) != old );
> 
> Good point. I'll apply that.
> 
>>
>>
>> Still, this only matters for volatile CSRs, and is unnecessary in the
>> general case.  I'd suggest naming it csr_volatile_read64(). 
> 
> Yes, that makes sense. I will rename it to csr_volatile_read64().
> 
>> Most CSRs
>> can use a simple split access.
> 
> I may have misunderstood you here, but wouldn't it still make sense to 
> have a macro covering the case where a register is 64-bit on RV32 yet 
> accessed through two CSRs? VSIE and VSIEH, for example.
> 
> My plan was to use a single csr_read64() (but while loop then really 
> isn't needed in this case) call to abstract the access to VSIE, so that 
> the code looks the same on RV32 and RV64.
> 
> Does that make sense, or would it be better to have separate vsie and 
> vsieh fields instead?

For registers which can't change under your feet (or where both halves
are independent of one another) the simpler accessor form may still be
useful. And I really mean "change under your feet", i.e. "which
hardware increments" (as you have it in the comment) is really only a
subset of the cases where csr_volatile_read64() will need using.

Jan


^ permalink raw reply	[flat|nested] 244+ messages in thread

* Re: [PATCH v2 06/39] xen/riscv: use UINT64_MAX to disable the VS-timer
  2026-08-27 15:20 ` [PATCH v2 06/39] xen/riscv: use UINT64_MAX to disable the VS-timer Oleksii Kurochko
  2026-08-31 12:48   ` Baptiste Le Duc
@ 2026-09-01  7:12   ` Jan Beulich
  2026-09-01  8:47     ` Oleksii Kurochko
  1 sibling, 1 reply; 244+ messages in thread
From: Jan Beulich @ 2026-09-01  7:12 UTC (permalink / raw)
  To: Oleksii Kurochko
  Cc: Romain Caritey, Baptiste Le Duc, Zheng Zhang, Alistair Francis,
	Connor Davis, Andrew Cooper, Anthony PERARD, Michal Orzel,
	Julien Grall, Roger Pau Monné, Stefano Stabellini, xen-devel

On 27.08.2026 17:20, Oleksii Kurochko wrote:
> vstimecmp is a 64-bit CSR independently of XLEN, which is why it is
> written with csr_write64(). On RV32 that macro splits the value into
> the vstimecmp/vstimecmph pair, so passing ULONG_MAX (0xffffffff there)
> writes all ones to the low half and zero to the high half, leaving the
> CSR at 0x00000000ffffffff rather than at its maximum. A VS-timer irq
> would then become pending as soon as (time + htimedelta) reaches 2^32,
> which is exactly what the code is trying to avoid.
> 
> Use UINT64_MAX, which matches the width of the CSR. On RV64 it is equal
> to ULONG_MAX, so no functional change there.

Hmm. This again is an example where code (likely) will be silently wrong
for RV128. Presumably the CSR would be XLEN bits wide there as well, and
hence you'd need to write it with 128 bits of ones. Imo ~0 is what wants
using here.

Jan


^ permalink raw reply	[flat|nested] 244+ messages in thread

* Re: [PATCH v2 03/39] xen/riscv: set the guest's XLEN explicitly in hstatus.VSXL
  2026-08-31 12:48   ` Baptiste Le Duc
  2026-09-01  7:03     ` Jan Beulich
@ 2026-09-01  8:40     ` Oleksii Kurochko
  2026-09-01 15:16     ` Jan Beulich
  2 siblings, 0 replies; 244+ messages in thread
From: Oleksii Kurochko @ 2026-09-01  8:40 UTC (permalink / raw)
  To: Baptiste Le Duc
  Cc: xen-devel, Romain Caritey, Zheng Zhang, Alistair Francis,
	Connor Davis, Andrew Cooper, Anthony PERARD, Michal Orzel,
	Jan Beulich, Julien Grall, Roger Pau Monné,
	Stefano Stabellini



On 8/31/26 2:48 PM, Baptiste Le Duc wrote:
>> hstatus.VSXL is WARL, so its reset value is implementation-defined. Xen
>> supports 64-bit guests only, so program it explicitly instead of relying
>> on whatever the hardware happens to leave there.
>>
>> This matters beyond the guest's own view of itself: decoding a trapped
>> instruction depends on the effective XLEN of the guest, as the encodings
>> which exist for XLEN=64 only must not be recognized for a 32-bit one.
>>
> It's not clear which instruction "decoding a trapped instruction" refers
> to without more context. I assume you mean decode_ldst_insn() in
> emulate.c, but the patch introducing that function comes later in the
> series, so this isn't obvious on a first read.
> 
> Please reorder the series so this patch follows the one introducing
> decode_ldst_insn(), or reference it explicitly in the commit message
> (e.g. "load/store trap emulation, introduced later in this series in
> emulate.c, needs...").
> 

I think I can just drop this paragraph from commit message. Even without 
it considering that we are supporting only rv64 guest we should set 
hstatus.VSXL correspondingly and not rely on what hardware will put there.

~ Oleksii



^ permalink raw reply	[flat|nested] 244+ messages in thread

* Re: [PATCH v2 05/39] xen/riscv: request a G-stage flush on vmenter when VMIDs are disabled
  2026-08-31 12:48   ` Baptiste Le Duc
@ 2026-09-01  8:43     ` Oleksii Kurochko
  0 siblings, 0 replies; 244+ messages in thread
From: Oleksii Kurochko @ 2026-09-01  8:43 UTC (permalink / raw)
  To: Baptiste Le Duc
  Cc: xen-devel, Romain Caritey, Zheng Zhang, Alistair Francis,
	Connor Davis, Andrew Cooper, Anthony PERARD, Michal Orzel,
	Jan Beulich, Julien Grall, Roger Pau Monné,
	Stefano Stabellini



On 8/31/26 2:48 PM, Baptiste Le Duc wrote:
>> vmid_handle_vmenter() reports that no flush is needed when VMIDs are
>> unavailable (vmid=off, or hardware with no more than one VMID bit). Every
>> domain then runs under VMID 0 with nothing flushed in between, so as soon
>> as a hart runs more than one domain, a domain entered there can use the
>> G-stage translations left behind by the domain which ran before it.
>>
>> Adjust the comment in p2m_handle_vmenter() accordingly: skipping the
>> VS-stage flush no longer relies on an old VMID not being reused, which
>> doesn't hold when there are no VMIDs to begin with.
>>
>> While at it, spell the other early return as a bool literal.
>>
>> Fixes: bff3b9ea4696 ("xen/riscv: introduce VMID allocation and manegement")
>> Signed-off-by: Oleksii Kurochko <oleksii.kurochko@gmail.com>
>>
> 
> FWIW this comment and p2m_handle_vmenter() itself get dropped a few
> patches later in "implement vCPU context switching", which folds the
> VMID claim into p2m_ctxt_switch_to(). The fix survives there via the
> need_flush check. Is this patch really needed?
> 

I think yes as it is fixing current implementation of what we have in 
staging.

We could back to this topic after the discussion of "implement vCPU 
context switching" in the case someone will suggest better way to handle 
p2m context switch (probably without dropping of p2m_handle_vmenter().

~ Oleksii





^ permalink raw reply	[flat|nested] 244+ messages in thread

* Re: [PATCH v2 06/39] xen/riscv: use UINT64_MAX to disable the VS-timer
  2026-09-01  7:12   ` Jan Beulich
@ 2026-09-01  8:47     ` Oleksii Kurochko
  0 siblings, 0 replies; 244+ messages in thread
From: Oleksii Kurochko @ 2026-09-01  8:47 UTC (permalink / raw)
  To: Jan Beulich
  Cc: Romain Caritey, Baptiste Le Duc, Zheng Zhang, Alistair Francis,
	Connor Davis, Andrew Cooper, Anthony PERARD, Michal Orzel,
	Julien Grall, Roger Pau Monné, Stefano Stabellini, xen-devel



On 9/1/26 9:12 AM, Jan Beulich wrote:
> On 27.08.2026 17:20, Oleksii Kurochko wrote:
>> vstimecmp is a 64-bit CSR independently of XLEN, which is why it is
>> written with csr_write64(). On RV32 that macro splits the value into
>> the vstimecmp/vstimecmph pair, so passing ULONG_MAX (0xffffffff there)
>> writes all ones to the low half and zero to the high half, leaving the
>> CSR at 0x00000000ffffffff rather than at its maximum. A VS-timer irq
>> would then become pending as soon as (time + htimedelta) reaches 2^32,
>> which is exactly what the code is trying to avoid.
>>
>> Use UINT64_MAX, which matches the width of the CSR. On RV64 it is equal
>> to ULONG_MAX, so no functional change there.
> 
> Hmm. This again is an example where code (likely) will be silently wrong
> for RV128. Presumably the CSR would be XLEN bits wide there as well, and
> hence you'd need to write it with 128 bits of ones. Imo ~0 is what wants
> using here.
> 
Agree, it makes would be better to have ~0 here. I will apply that.

Thanks.

~ Oleksii


^ permalink raw reply	[flat|nested] 244+ messages in thread

* Re: [PATCH v2 03/39] xen/riscv: set the guest's XLEN explicitly in hstatus.VSXL
  2026-08-31 12:48   ` Baptiste Le Duc
  2026-09-01  7:03     ` Jan Beulich
  2026-09-01  8:40     ` Oleksii Kurochko
@ 2026-09-01 15:16     ` Jan Beulich
  2 siblings, 0 replies; 244+ messages in thread
From: Jan Beulich @ 2026-09-01 15:16 UTC (permalink / raw)
  To: Baptiste Le Duc, Oleksii Kurochko
  Cc: xen-devel, Romain Caritey, Zheng Zhang, Alistair Francis,
	Connor Davis, Andrew Cooper, Anthony PERARD, Michal Orzel,
	Julien Grall, Roger Pau Monné, Stefano Stabellini

On 31.08.2026 14:48, Baptiste Le Duc wrote:
>> hstatus.VSXL is WARL, so its reset value is implementation-defined. Xen
>> supports 64-bit guests only, so program it explicitly instead of relying
>> on whatever the hardware happens to leave there.
>>
>> This matters beyond the guest's own view of itself: decoding a trapped
>> instruction depends on the effective XLEN of the guest, as the encodings
>> which exist for XLEN=64 only must not be recognized for a 32-bit one.
>>
> It's not clear which instruction "decoding a trapped instruction" refers
> to without more context.

Use "decoding of trapped instructions" instead? (I think I'd prefer the
paragraph to remain here.)

Jan

> I assume you mean decode_ldst_insn() in
> emulate.c, but the patch introducing that function comes later in the
> series, so this isn't obvious on a first read.
> 
> Please reorder the series so this patch follows the one introducing
> decode_ldst_insn(), or reference it explicitly in the commit message
> (e.g. "load/store trap emulation, introduced later in this series in
> emulate.c, needs...").
> 



^ permalink raw reply	[flat|nested] 244+ messages in thread

* Re: [PATCH v2 03/39] xen/riscv: set the guest's XLEN explicitly in hstatus.VSXL
  2026-08-27 15:20 ` [PATCH v2 03/39] xen/riscv: set the guest's XLEN explicitly in hstatus.VSXL Oleksii Kurochko
  2026-08-31 12:48   ` Baptiste Le Duc
@ 2026-09-01 15:20   ` Jan Beulich
  2026-09-02 11:42     ` Oleksii Kurochko
  1 sibling, 1 reply; 244+ messages in thread
From: Jan Beulich @ 2026-09-01 15:20 UTC (permalink / raw)
  To: Oleksii Kurochko
  Cc: Romain Caritey, Baptiste Le Duc, Zheng Zhang, Alistair Francis,
	Connor Davis, Andrew Cooper, Anthony PERARD, Michal Orzel,
	Julien Grall, Roger Pau Monné, Stefano Stabellini, xen-devel

On 27.08.2026 17:20, Oleksii Kurochko wrote:
> --- a/xen/arch/riscv/domain.c
> +++ b/xen/arch/riscv/domain.c
> @@ -88,7 +88,13 @@ static void vcpu_csr_init(struct vcpu *v)
>  {
>      v->arch.hedeleg = HEDELEG_DEFAULT & csr_masks.hedeleg;
>  
> -    vcpu_guest_cpu_user_regs(v)->hstatus = HSTATUS_SPV | HSTATUS_SPVP;
> +    /*
> +     * Xen supports 64-bit guests only, so set the guest's XLEN explicitly
> +     * rather than leaving it to the WARL behaviour of hstatus.VSXL, which the
> +     * decoding of a trapped instruction depends on.
> +     */
> +    vcpu_guest_cpu_user_regs(v)->hstatus =
> +        HSTATUS_SPV | HSTATUS_SPVP | MASK_INSR(HSTATUS_VSXL_64, HSTATUS_VSXL);

The comment is correct right now, but the situation better would change
at some point. Can't you arrange for things to be correct here also for
a future where 32- and 128-bit guests would also be supported?

> --- a/xen/arch/riscv/include/asm/riscv_encoding.h
> +++ b/xen/arch/riscv/include/asm/riscv_encoding.h
> @@ -68,6 +68,8 @@
>  #if __riscv_xlen == 64
>  #define HSTATUS_VSXL			_UL(0x300000000)
>  #define HSTATUS_VSXL_SHIFT		32
> +#define HSTATUS_VSXL_64			_UL(2)
> +#define HSTATUS_VSXL_32			_UL(1)
>  #endif

While adding the two #define-s, would you mind considering to remove the
unused (and supposed to remain so) HSTATUS_VSXL_SHIFT?

Jan


^ permalink raw reply	[flat|nested] 244+ messages in thread

* Re: [PATCH v2 07/39] xen/riscv: add missing APLIC register offsets, masks to asm/aplic.h
  2026-08-27 15:20 ` [PATCH v2 07/39] xen/riscv: add missing APLIC register offsets, masks to asm/aplic.h Oleksii Kurochko
@ 2026-09-01 15:36   ` Baptiste Le Duc
  2026-09-01 15:53     ` Jan Beulich
  0 siblings, 1 reply; 244+ messages in thread
From: Baptiste Le Duc @ 2026-09-01 15:36 UTC (permalink / raw)
  To: Oleksii Kurochko
  Cc: xen-devel, Romain Caritey, Baptiste Le Duc, Zheng Zhang,
	Alistair Francis, Connor Davis, Andrew Cooper, Anthony PERARD,
	Michal Orzel, Jan Beulich, Julien Grall, Roger Pau Monné,
	Stefano Stabellini

On Thu, 27 Aug 2026 17:20:51 +0200, Oleksii Kurochko <oleksii.kurochko@gmail.com> wrote:
> These definitions are required for correct decoding of APLIC MMIO
> accesses and target configuration, and will be used by both the
> physical and virtual APLIC implementations.
> 
> While adding them, rearrange the header in the style of x86's
> asm/msr-index.h: a register's offset is immediately followed by the
> definitions of that register's fields, with the blocks sorted by
> offset.  This makes the relation between a register and its fields
> obvious from the layout alone, so no comment is needed to express it.
> 
> [...]

Reviewed-by: Baptiste Le Duc <baptiste.le-duc@vates.tech>

-- 
Baptiste Le Duc <baptiste.le-duc@vates.tech>


^ permalink raw reply	[flat|nested] 244+ messages in thread

* Re: [PATCH v2 08/39] xen/riscv: introduce device-agnostic MMIO emulation dispatch
  2026-08-27 15:20 ` [PATCH v2 08/39] xen/riscv: introduce device-agnostic MMIO emulation dispatch Oleksii Kurochko
@ 2026-09-01 15:36   ` Baptiste Le Duc
  2026-09-03 10:28     ` Oleksii Kurochko
  2026-09-09 13:24   ` Jan Beulich
  1 sibling, 1 reply; 244+ messages in thread
From: Baptiste Le Duc @ 2026-09-01 15:36 UTC (permalink / raw)
  To: Oleksii Kurochko
  Cc: xen-devel, Romain Caritey, Baptiste Le Duc, Zheng Zhang,
	Alistair Francis, Connor Davis, Andrew Cooper, Anthony PERARD,
	Michal Orzel, Jan Beulich, Julien Grall, Roger Pau Monné,
	Stefano Stabellini

> RISC-V guests can expose several virtual interrupt controllers at
> distinct GPA ranges: vPLIC (hasn't been introduced yet) for legacy machines,
> vAPLIC and vIMSIC for AIA-compliant ones (are being introduced in the follow
> up patches). 
As Jan said here [1], we shouldn't use "as later in this series" in
commit message...

[1]: https://lore.kernel.org/xen-devel/cover.1787838835.git.oleksii.kurochko@gmail.com/T/#m56fbac1ceb0642d5d868e9dcb2b5ed93ecbe5058
> Routing MMIO faults via a per-device is_access() check in the
> trap handler would couple it to every device it must serve, requiring a
> new conditional branch in the fault path each time a new emulated device is
> added.
> 
> Introduce a per-domain MMIO handler registration table, modeled
> after the equivalent ARM framework, so that virtual devices
> self-register their GPA ranges and read/write callbacks at domain
> creation time. The MMIO fault path delegates to a single
> try_handle_mmio() entry point and remains agnostic of which device
> owns a particular address.
> 
> A subsequent patch wires this into the MMIO fault path in traps.c.
...same here
> 
> Signed-off-by: Oleksii Kurochko <oleksii.kurochko@gmail.com>
>
> diff --git a/xen/arch/riscv/Makefile b/xen/arch/riscv/Makefile
> index 3b948c11dd..ce6410a299 100644
> --- a/xen/arch/riscv/Makefile
> +++ b/xen/arch/riscv/Makefile
> @@ -14,6 +14,7 @@ obj-y += intc.o
>  obj-y += irq.o
>  obj-y += kernel.init.o
>  obj-y += mm.o
> +obj-y += mmio.o
>  obj-y += p2m.o
>  obj-y += paging.o
>  obj-y += pt.o
> diff --git a/xen/arch/riscv/domain.c b/xen/arch/riscv/domain.c
> index 57c37cb2df..ec327a5e8a 100644
> --- a/xen/arch/riscv/domain.c
> +++ b/xen/arch/riscv/domain.c
> @@ -12,6 +12,7 @@
>  #include <asm/cpufeature.h>
>  #include <asm/csr.h>
>  #include <asm/intc.h>
> +#include <asm/mmio.h>
>  #include <asm/riscv_encoding.h>
>  #include <asm/vtimer.h>
>  
> @@ -316,6 +317,8 @@ int arch_domain_create(struct domain *d,
>      if ( (rc = p2m_init(d, config)) != 0)
>          goto fail;
>  
> +    domain_io_init(d);
> +
>      if ( (rc = domain_vintc_init(d)) )
>          goto fail;
>  
> diff --git a/xen/arch/riscv/include/asm/domain.h b/xen/arch/riscv/include/asm/domain.h
> index e035b33ddf..15e8fa1968 100644
> --- a/xen/arch/riscv/include/asm/domain.h
> +++ b/xen/arch/riscv/include/asm/domain.h
> @@ -9,6 +9,7 @@
>  
>  #include <asm/cpufeature.h>
>  #include <asm/guest-layout.h>
> +#include <asm/mmio.h>
>  #include <asm/p2m.h>
>  #include <asm/vtimer.h>
>  
> @@ -101,6 +102,8 @@ struct arch_domain {
>      const unsigned long *isa;
>  
>      struct vintc *vintc;
> +
> +    struct vmmio vmmio;
>  };
>  
>  #include <xen/sched.h>
> diff --git a/xen/arch/riscv/include/asm/mmio.h b/xen/arch/riscv/include/asm/mmio.h
> new file mode 100644
> index 0000000000..582969e535
> --- /dev/null
> +++ b/xen/arch/riscv/include/asm/mmio.h
> @@ -0,0 +1,63 @@
> +/* SPDX-License-Identifier: GPL-2.0-or-later */
> +#ifndef RISCV_MMIO_H
> +#define RISCV_MMIO_H
> +
> +#include <xen/lib.h>
> +#include <xen/rwlock.h>
> +
> +struct domain;
> +struct vcpu;
> +
> +#define MAX_IO_HANDLER  16
> +
> +typedef struct {
> +    paddr_t gpa;
> +    unsigned int len;  /* access width in bytes (1, 2, 4, 8) */
> +    bool is_write;
> +    /* store: value to write; load: value read (set by handler) */
> +    register_t data;
> +} mmio_info_t;
> +
> +enum io_state
> +{
> +    IO_ABORT,       /* The IO was handled and led to an abort. */
> +    IO_HANDLED,     /* The IO was successfully handled. */
> +    IO_UNHANDLED,   /* No handler found for the IO. */
> +};
> +
> +typedef enum io_state (mmio_read_t)(struct vcpu *v, mmio_info_t *info);
> +typedef enum io_state (mmio_write_t)(struct vcpu *v, const mmio_info_t *info);
> +
> +struct mmio_handler_ops {
> +    mmio_read_t *read;
> +    mmio_write_t *write;
> +};
> +
> +struct mmio_handler {
> +    paddr_t addr;
> +    paddr_t size;
> +    const struct mmio_handler_ops *ops;
> +};
> +
> +struct vmmio {
> +    unsigned int num_entries;
> +    rwlock_t lock;
> +    struct mmio_handler handlers[MAX_IO_HANDLER];
> +};
> +
> +int do_mmio(mmio_info_t *info, paddr_t fault_addr, unsigned int len);
> +int register_mmio_handler(struct domain *d,
> +                          const struct mmio_handler_ops *ops,
> +                          paddr_t addr, paddr_t size);
> +void domain_io_init(struct domain *d);
> +
> +#endif /* RISCV_MMIO_H */
> +
> +/*
> + * Local variables:
> + * mode: C
> + * c-file-style: "BSD"
> + * c-basic-offset: 4
> + * indent-tabs-mode: nil
> + * End:
> + */
> diff --git a/xen/arch/riscv/mmio.c b/xen/arch/riscv/mmio.c
> new file mode 100644
> index 0000000000..d241ab5ea1
> --- /dev/null
> +++ b/xen/arch/riscv/mmio.c
> @@ -0,0 +1,176 @@
> +/* SPDX-License-Identifier: GPL-2.0-or-later */
> +
> +#include <xen/bsearch.h>
> +#include <xen/lib.h>
> +#include <xen/rwlock.h>
> +#include <xen/sched.h>
> +#include <xen/string.h>
> +
> +#include <asm/current.h>
> +#include <asm/mmio.h>
> +
> +/*
> + * bsearch() comparator: @key holds the address to look up in its addr field,
> + * @elem is an entry of vmmio->handlers. Relies on the regions not
> + * overlapping, which register_mmio_handler() enforces.
> + */
> +static int cmp_mmio_handler(const void *key, const void *elem)
> +{
> +    const struct mmio_handler *handler0 = key;
> +    const struct mmio_handler *handler1 = elem;
> +
> +    if ( handler0->addr < handler1->addr )
> +        return -1;
> +
> +    if ( handler0->addr >= (handler1->addr + handler1->size) )
> +        return 1;
> +
> +    return 0;
> +}
> +
> +/*
> + * Return a copy of the matching handler rather than a pointer into
> + * vmmio->handlers: a concurrent register_mmio_handler() shifts entries
> + * up to keep the array sorted, so an escaped pointer could refer to a
> + * different (or torn) entry once the lock is dropped. The copy stays
> + * valid as the ops structures are never freed.
> + */
> +static bool find_mmio_handler(struct domain *d, paddr_t gpa,
> +                              struct mmio_handler *out)
> +{
> +    struct vmmio *vmmio = &d->arch.vmmio;
> +    struct mmio_handler key = { .addr = gpa };
> +    const struct mmio_handler *handler;
> +
> +    read_lock(&vmmio->lock);
> +    handler = bsearch(&key, vmmio->handlers, vmmio->num_entries,
> +                      sizeof(*handler), cmp_mmio_handler);
> +    if ( handler )
> +        *out = *handler;
> +    read_unlock(&vmmio->lock);
> +
> +    return handler != NULL;
> +}
> +
> +static enum io_state try_handle_mmio(mmio_info_t *info)
> +{
> +    struct vcpu *v = current;
> +    struct mmio_handler handler = {};
> +
> +    if ( !find_mmio_handler(v->domain, info->gpa, &handler) )
> +        return IO_UNHANDLED;
> +
> +    if ( info->is_write )
> +        return handler.ops->write(v, info);
> +    else
> +        return handler.ops->read(v, info);
> +}
> +
> +/*
> + * Check alignment and dispatch a decoded MMIO access to a registered
> + * handler. On success (0), info->data holds the read value for loads.
> + *
> + * There is no "retry" outcome to handle: find_mmio_handler() returns a
> + * copy of the matching handler taken under vmmio->lock and the ops
> + * structures are never freed, so the lookup result cannot go stale
> + * between finding the handler and invoking it.
> + */
> +int do_mmio(mmio_info_t *info, paddr_t fault_addr, unsigned int len)
> +{
> +    /* Fault address should be aligned to length of MMIO */
> +    if ( fault_addr & (len - 1) )
> +        return -EIO;
> +
> +    info->gpa = fault_addr;
> +    info->len = len;
> +
> +    switch ( try_handle_mmio(info) )
> +    {
> +    case IO_HANDLED:
> +        return 0;
> +
> +    case IO_ABORT:
> +        return -EIO;
> +
> +    default:
> +        return -EOPNOTSUPP;
> +    }
> +}
> +
> +int register_mmio_handler(struct domain *d,
> +                          const struct mmio_handler_ops *ops,
> +                          paddr_t addr, paddr_t size)
> +{
> +    struct vmmio *vmmio = &d->arch.vmmio;
> +    struct mmio_handler *handlers = vmmio->handlers;
> +    paddr_t end = addr + size;
> +    unsigned int i;
> +    int rc = 0;
> +    bool overlap;
> +
> +    if ( !ops || !ops->read || !ops->write || !size || end < addr )
> +        return -EINVAL;
Just a question: is the aim of end < addr check to handle possible overflow of end?

-- 
Baptiste Le Duc <baptiste.le-duc@vates.tech>


^ permalink raw reply	[flat|nested] 244+ messages in thread

* Re: [PATCH v2 07/39] xen/riscv: add missing APLIC register offsets, masks to asm/aplic.h
  2026-09-01 15:36   ` Baptiste Le Duc
@ 2026-09-01 15:53     ` Jan Beulich
  2026-09-02 13:22       ` Jan Beulich
  0 siblings, 1 reply; 244+ messages in thread
From: Jan Beulich @ 2026-09-01 15:53 UTC (permalink / raw)
  To: Oleksii Kurochko
  Cc: xen-devel, Romain Caritey, Zheng Zhang, Alistair Francis,
	Connor Davis, Andrew Cooper, Anthony PERARD, Michal Orzel,
	Julien Grall, Roger Pau Monné, Stefano Stabellini,
	Baptiste Le Duc

On 01.09.2026 17:36, Baptiste Le Duc wrote:
> On Thu, 27 Aug 2026 17:20:51 +0200, Oleksii Kurochko <oleksii.kurochko@gmail.com> wrote:
>> These definitions are required for correct decoding of APLIC MMIO
>> accesses and target configuration, and will be used by both the
>> physical and virtual APLIC implementations.
>>
>> While adding them, rearrange the header in the style of x86's
>> asm/msr-index.h: a register's offset is immediately followed by the
>> definitions of that register's fields, with the blocks sorted by
>> offset.  This makes the relation between a register and its fields
>> obvious from the layout alone, so no comment is needed to express it.
>>
>> [...]
> 
> Reviewed-by: Baptiste Le Duc <baptiste.le-duc@vates.tech>

Acked-by: Jan Beulich <jbeulich@suse.com>

If there's no dependency on earlier patches (nor the prereq series), this
could go in right away. Yet nothing is being said anywhere, unless I
overlooked anything.

Jan


^ permalink raw reply	[flat|nested] 244+ messages in thread

* Re: [PATCH v2 02/39] xen/riscv: drop bug.h's duplicate instruction length helpers
  2026-09-01  7:01   ` Jan Beulich
@ 2026-09-02 10:48     ` Oleksii Kurochko
  2026-09-02 13:02       ` Jan Beulich
  0 siblings, 1 reply; 244+ messages in thread
From: Oleksii Kurochko @ 2026-09-02 10:48 UTC (permalink / raw)
  To: Jan Beulich
  Cc: Romain Caritey, Baptiste Le Duc, Zheng Zhang, Alistair Francis,
	Connor Davis, Andrew Cooper, Anthony PERARD, Michal Orzel,
	Julien Grall, Roger Pau Monné, Stefano Stabellini, xen-devel



On 9/1/26 9:01 AM, Jan Beulich wrote:
> On 27.08.2026 17:20, Oleksii Kurochko wrote:
>> asm/riscv_encoding.h already provides INSN_16BIT_MASK and INSN_LEN(), and
>> emulate.c uses them, so the tree carried two spellings of the same thing
>> which could drift apart. COMPRESSED_INSN_MASK never had a user.
>>
>> No functional change.
>>
>> Signed-off-by: Oleksii Kurochko <oleksii.kurochko@gmail.com>
> 
> As I'm happy to see the duplication go away:
> Acked-by: Jan Beulich <jbeulich@suse.com>

Thanks.

> However, ...
> 
>> --- a/xen/arch/riscv/traps.c
>> +++ b/xen/arch/riscv/traps.c
>> @@ -214,7 +214,7 @@ void do_trap(struct cpu_user_regs *cpu_regs)
>>                   die();
>>               }
>>   
>> -            cpu_regs->sepc += GET_INSN_LENGTH(*(uint16_t *)pc);
>> +            cpu_regs->sepc += INSN_LEN(*(uint16_t *)pc);
> 
> ... for one I'd prefer if we took the opportunity and add "const" to the
> pointer target here.

Thanks for adding 'const' during commit.

> 
> And then
> 
> #define INSN_16BIT_MASK			0x3
> #define INSN_32BIT_MASK			0x1c
> 
> are really named backwards, seeing e.g. their use in

It was derived from OpenSBI project and I haven't paid enough attention 
for these defines.

Now looking at them again I fully agree that names here not really 
correct. It should be according to the spec:
    #define INSN_32BIT_MASK          0x3
    #define INSN_48BIT_MASK          0x1c

Then ...

> 
> #define INSN_IS_16BIT(insn)		\
> 	(((insn) & INSN_16BIT_MASK) != INSN_16BIT_MASK)
> #define INSN_IS_32BIT(insn)		\
> 	(((insn) & INSN_16BIT_MASK) == INSN_16BIT_MASK && \
> 	 ((insn) & INSN_32BIT_MASK) != INSN_32BIT_MASK)

...

#define INSN_IS_16BIT(insn)      \
     (((insn) & INSN_32BIT_MASK) != INSN_32BIT_MASK)

#define INSN_IS_32BIT(insn)      \
     ((((insn) & INSN_32BIT_MASK) == INSN_32BIT_MASK) && \
      (((insn) & INSN_48BIT_MASK) != INSN_48BIT_MASK))

> 
> Furthermore,
> 
> #define INSN_LEN(insn)			(INSN_IS_16BIT(insn) ? 2 : 4)
> 
> fails to use INSN_32BIT_MASK / INSN_IS_32BIT() altogether.

Then INSN_LEN will be redefined as:

/*
  * Length in bytes of the instruction whose first parcel is @insn.  Callers
  * must have established that the encoding is 16- or 32-bit wide; the ISA
  * defines no instruction wider than that, and the wider encodings are
  * reserved.
  */
#define INSN_LEN(insn) \
     (INSN_IS_16BIT(insn) ? 2 : (INSN_IS_32BIT(insn) ? 4 : 0))

I am thinking if it makes sense to have here the generic way to 
calculate INSN_LEN. Something like:

/*
  * Length in bytes of the instruction whose first 16-bit parcel is 
@insn, or
  * zero for the encoding reserved for instructions of 192 bits or more, 
whose
  * length the ISA leaves unspecified.  See "Expanded Instruction-Length
  * Encoding" in the unprivileged spec; the first parcel holds all of the
  * information needed.
  */
static inline unsigned int INSN_LEN(uint16_t insn)
{
     unsigned int nnn;

     if ( INSN_IS_16BIT(insn) )
         return 2;

     if ( INSN_IS_32BIT(insn) )
         return 4;

     if ( INSN_IS_48BIT(insn) )
         return 6;

     if ( INSN_IS_64BIT(insn) )
         return 8;

     /* xnnnxxxxx1111111 with nnn != 111 is (80 + 16 * nnn) bits wide. */
     nnn = MASK_EXTR(insn, INSN_NNN_MASK);

     return (nnn == 7) ? 0 : 10 + 2 * nnn;
}

I think that for now it is enough to go without common implmenntation of 
INSN_LEN and just return 0 as suggested above.

If you are okay with suggested changes I will send a separate patch for 
them.

~ Oleksii


^ permalink raw reply	[flat|nested] 244+ messages in thread

* Re: [PATCH v2 03/39] xen/riscv: set the guest's XLEN explicitly in hstatus.VSXL
  2026-09-01 15:20   ` Jan Beulich
@ 2026-09-02 11:42     ` Oleksii Kurochko
  2026-09-02 13:07       ` Jan Beulich
  0 siblings, 1 reply; 244+ messages in thread
From: Oleksii Kurochko @ 2026-09-02 11:42 UTC (permalink / raw)
  To: Jan Beulich
  Cc: Romain Caritey, Baptiste Le Duc, Zheng Zhang, Alistair Francis,
	Connor Davis, Andrew Cooper, Anthony PERARD, Michal Orzel,
	Julien Grall, Roger Pau Monné, Stefano Stabellini, xen-devel



On 9/1/26 5:20 PM, Jan Beulich wrote:
> On 27.08.2026 17:20, Oleksii Kurochko wrote:
>> --- a/xen/arch/riscv/domain.c
>> +++ b/xen/arch/riscv/domain.c
>> @@ -88,7 +88,13 @@ static void vcpu_csr_init(struct vcpu *v)
>>   {
>>       v->arch.hedeleg = HEDELEG_DEFAULT & csr_masks.hedeleg;
>>   
>> -    vcpu_guest_cpu_user_regs(v)->hstatus = HSTATUS_SPV | HSTATUS_SPVP;
>> +    /*
>> +     * Xen supports 64-bit guests only, so set the guest's XLEN explicitly
>> +     * rather than leaving it to the WARL behaviour of hstatus.VSXL, which the
>> +     * decoding of a trapped instruction depends on.
>> +     */
>> +    vcpu_guest_cpu_user_regs(v)->hstatus =
>> +        HSTATUS_SPV | HSTATUS_SPVP | MASK_INSR(HSTATUS_VSXL_64, HSTATUS_VSXL);
> 
> The comment is correct right now, but the situation better would change
> at some point. Can't you arrange for things to be correct here also for
> a future where 32- and 128-bit guests would also be supported?

I am not sure about 128-bit guests as H extension is dependent on RV32 
or RV64 but probably it will be changed:
```
The hypervisor extension depends on an "I" base integer ISA with 32 x 
registers (RV32I or RV64I), not RV32E or RV64E, which have only 16 x 
registers.
```

I will introduce the following (also it will be needed also to check if 
we could VSXL set at all as implmentation can make that field read-only 
and do VSXLLEN=HSXLEN):

/*
  * Return the hstatus.VSXL value encoding the guest's XLEN. The switch()
  * deliberately has no default case, so that adding a new domain_type (a
  * 128-bit one, in particular) fails to build until this mapping is 
updated.
  */
static unsigned int domain_vsxl(const struct domain *d)
{
     switch ( d->type )
     {
     case DOMAIN_32BIT:
         return HSTATUS_VSXL_32;

     case DOMAIN_64BIT:
         return HSTATUS_VSXL_64;
     }

     ASSERT_UNREACHABLE();

     return HSTATUS_VSXL_64;
}

It will also affect then common code as vcpu_csr_init() could be then 
called before domain type is set:

+++ b/xen/common/device-tree/dom0less-build.c
@@ -812,17 +812,18 @@ static int __init construct_domU(struct 
kernel_info *kinfo,
      else if ( rc == 0 && !strcmp(dom0less_enhanced, "no-xenstore") )
          kinfo->dom0less_feature = DOM0LESS_ENHANCED_NO_XS;

-    if ( vcpu_create(d, 0) == NULL )
-        return -ENOMEM;
-
      d->max_pages = ((paddr_t)mem * SZ_1K) >> PAGE_SHIFT;

      rc = kernel_probe(kinfo, node);
      if ( rc < 0 )
          return rc;

+    /* The domain type needs to be known before the first vCPU is 
created. */
      set_domain_type(d, kinfo);

+    if ( vcpu_create(d, 0) == NULL )
+        return -ENOMEM;


> 
>> --- a/xen/arch/riscv/include/asm/riscv_encoding.h
>> +++ b/xen/arch/riscv/include/asm/riscv_encoding.h
>> @@ -68,6 +68,8 @@
>>   #if __riscv_xlen == 64
>>   #define HSTATUS_VSXL			_UL(0x300000000)
>>   #define HSTATUS_VSXL_SHIFT		32
>> +#define HSTATUS_VSXL_64			_UL(2)
>> +#define HSTATUS_VSXL_32			_UL(1)
>>   #endif
> 
> While adding the two #define-s, would you mind considering to remove the
> unused (and supposed to remain so) HSTATUS_VSXL_SHIFT?

Sure, I will drop that.

Thanks.

~ Oleksii


^ permalink raw reply	[flat|nested] 244+ messages in thread

* Re: [PATCH v2 09/39] xen/riscv: implement virtual APLIC MMIO emulation
  2026-08-27 15:20 ` [PATCH v2 09/39] xen/riscv: implement virtual APLIC MMIO emulation Oleksii Kurochko
@ 2026-09-02 11:51   ` Baptiste Le Duc
  2026-09-04 11:58     ` Oleksii Kurochko
  2026-09-02 12:31   ` Baptiste Le Duc
  2026-09-09 14:26   ` Jan Beulich
  2 siblings, 1 reply; 244+ messages in thread
From: Baptiste Le Duc @ 2026-09-02 11:51 UTC (permalink / raw)
  To: Oleksii Kurochko
  Cc: xen-devel, Romain Caritey, Baptiste Le Duc, Zheng Zhang,
	Alistair Francis, Connor Davis, Andrew Cooper, Anthony PERARD,
	Michal Orzel, Jan Beulich, Julien Grall, Roger Pau Monné,
	Stefano Stabellini

> Guests running under Xen program interrupt routing by writing to APLIC
> MMIO registers. Xen must intercept these accesses to enforce interrupt
> isolation between domains and to translate guest routing intent into the
> underlying physical MSI topology.
> 
> Writes are gated by the domain's authorised interrupt bitmap so that a
> guest cannot affect interrupts it does not own. TARGET register writes
> additionally require translation of the hart and IMSIC guest-file
> indices from virtual to physical, as the APLIC uses these fields
> directly to compute the MSI delivery address.
> 
> Delegation (APLIC_SOURCECFG_D) is not yet supported.
> 
> Co-developed-by: Romain Caritey <Romain.Caritey@microchip.com>
> Signed-off-by: Oleksii Kurochko <oleksii.kurochko@gmail.com>
>
> diff --git a/xen/arch/riscv/aplic-priv.h b/xen/arch/riscv/aplic-priv.h
> index 35100d3a64..b3a1f79c5b 100644
> --- a/xen/arch/riscv/aplic-priv.h
> +++ b/xen/arch/riscv/aplic-priv.h
> @@ -47,4 +47,7 @@ struct aplic_priv {
>   */
>  extern unsigned int guest_aplic_num_sources;
>  
> +uint32_t aplic_msi_target_gen(const struct vcpu *target_vcpu,
> +                              uint32_t base_val);
> +
>  #endif /* ASM_RISCV_APLIC_PRIV_H */
> diff --git a/xen/arch/riscv/aplic.c b/xen/arch/riscv/aplic.c
> index 3681f0669e..66ba4986a9 100644
> --- a/xen/arch/riscv/aplic.c
> +++ b/xen/arch/riscv/aplic.c
> @@ -16,6 +16,7 @@
>  #include <xen/irq.h>
>  #include <xen/mm.h>
>  #include <xen/sections.h>
> +#include <xen/sched.h>
>  #include <xen/spinlock.h>
>  #include <xen/types.h>
>  #include <xen/vmap.h>
> @@ -28,8 +29,6 @@
>  #include <asm/io.h>
>  #include <asm/riscv_encoding.h>
>  
> -#define APLIC_DEFAULT_PRIORITY  1
> -
>  static struct aplic_priv aplic = {
>      .lock = SPIN_LOCK_UNLOCKED,
>  };
> @@ -38,6 +37,127 @@ static struct intc_info __ro_after_init aplic_info = {
>      .hw_variant = INTC_APLIC,
>  };
>  
> +/*
> + * The arrangement of IMSIC interrupt files in MMIO space follows a topology
> + * defined by the RISC-V AIA specification. An IMSIC group is a set of
> + * interrupt files (e.g., in a cluster or socket) co-located in memory.
> + *
> + * The physical address of an outgoing MSI is calculated by bitwise ORing a
> + * Base Physical Page Number (Base PPN) with the Group Index (g), the Hart
> + * Index (h) and, for a supervisor-level interrupt domain, the Guest Index:
> + *
> + *   ( Base PPN | (g << (HHXS + 12)) | (h << LHXS) | guest ) << 12
Nit: it should be Guest Index (according to the spec) instead of `guest`
wording:
    ( Base PPN | (g << (HHXS + 12)) | (h << LHXS) | Guest Index ) << 12
> + *
> + * where Base PPN, HHXS, LHXS, HHXW and LHXW come from the {m,s}msiaddrcfg[h]
> + * registers of the interrupt domain that sends the MSI:
> + *
> + * XLEN-1       HHXS+24          LHXS+12          12          0
> + * |            |                |                |           |
> + * ------------------------------------------------------------
> + * |xxxx|   g   |xxxxxxxx|   h   |xxxx|Guest Index|     0     |
> + * ------------------------------------------------------------
> + *
> + * - g: group number.
> + * - h: hart number relative to the group.
> + * - xxxx: remaining Base PPN bits; each gap may be zero-width.
> + * - Guest Index: selects one of the 4 KiB pages right above the hart's own
> + *   supervisor-level file, i.e. it starts at bit 12; LHXS must therefore be
> + *   at least as large as the number of guest index bits.
> + * - Bits 11:0: always zero because IMSIC files are 4 KiB page-aligned.
> + *
> + * For wired interrupts in MSI delivery mode (domaincfg.DM = 1) the APLIC
> + * builds that address itself from the "Hart Index" field (bits 31:18) of the
> + * corresponding target[i] register. That field holds a hart index *number*,
> + * in which both indices are packed adjacently:
> + *
> + * 13          lhxw+hhxw   lhxw       0
> + * |           |           |          |
> + * ------------------------------------
> + * |     0     |Group Index|Hart Index|
> + * ------------------------------------
> + *
> + * - lhxw (Low Hart Index Width): the number of bits used for the hart number
> + *   within a group.
> + * - hhxw (High Hart Index Width): the number of bits used for the group
> + *   number; the remaining bits of the field must be zero.
>
I think it's not very clear that the schema represents the "Hart Index" field
i.e. target[i] bits 31:18. Moreover, the schema like that is wrong as it is
not Group Index or Hart Index but `g` and `h`.

I'd suggest something like this:

    * For wired interrupts in MSI delivery mode (domaincfg.DM = 1), the APLIC
    * computes the MSI target address itself from the "Hart Index" field
    * (bits 31:18) of the corresponding target[i] register. This 14-bit field
    * holds both g and h:
    *
    * 13          lhxw+hhxw   lhxw       0
    * |           |           |          |
    * ------------------------------------
    * |     0     |     g     |    h     |
    * ------------------------------------
    *
    * - lhxw (Low Hart Index Width): the number of bits used for the hart number
    *   within a group.
    * - hhxw (High Hart Index Width): the number of bits used for the group
    *   number; the remaining bits of the field must be zero.


> + *
> + * The Guest Index isn't a part of it: for a supervisor-level interrupt domain
> + * it has its own field (bits 17:12) in target[i].
> + *
> + * Because there are "xxxx" gaps (Base PPN bits) between the indices in the
> + * physical address (depending on HHXS and LHXS), software must extract the
> + * group and hart components separately and pack them into the APLIC-defined
> + * Hart Index format to ensure correct MSI targeting.
> + */
> +static unsigned long aplic_hart_field(unsigned int cpu)
I should have renamed this to aplic_hart_index() as it's formerly what
the function returns.
> +{
> +    const struct imsic_config *imsic = imsic_get_config();
> +    const struct imsic_msi *msi = &imsic->msi[cpu];
Nit: this could be const ...
> +    /* Low Hart Index Shift */
> +    unsigned int lhxs = imsic->guest_index_bits;
It seems incoherent with the diagram above as there is some xxxx
between Guest Index bits and lhxs + 12. Therefore, it is not that obvious
that lhxs is equal to guest_index_bits.
> +    /* Low Hart Index Width */
> +    unsigned int lhxw = imsic->hart_index_bits;
> +    /* High Hart Index Width */
> +    unsigned int hhxw = imsic->group_index_bits;
> +    /* High Hart Index Shift */
> +    unsigned int hhxs =
> +        imsic->group_index_shift - APLIC_xMSICFGADDR_PPN_SHIFT * 2;
... 
> +    /*
> +     * msi->base_addr is the base of the MMIO regset this CPU's interrupt
So if I understood correctly, msi->base_addr corresponds to the group
terminology? Is it always the case?
> +     * files live in, and one regset can cover several harts; msi->offset
> +     * selects this CPU's block inside it. The hart index bits are part of
> +     * that offset, so both indexes have to be derived from the full address.
> +     */
> +    paddr_t target_addr = msi->base_addr + msi->offset;
> +    unsigned long tppn = target_addr >> APLIC_xMSICFGADDR_PPN_SHIFT;
> +    unsigned long g =
> +        (tppn >> APLIC_xMSICFGADDR_PPN_HHX_SHIFT(hhxs)) &
> +        APLIC_xMSICFGADDR_PPN_HHX_MASK(hhxw);
> +    unsigned long h =
> +        (tppn >> APLIC_xMSICFGADDR_PPN_LHX_SHIFT(lhxs)) &
> +        APLIC_xMSICFGADDR_PPN_LHX_MASK(lhxw);
> +
> +    return (g << lhxw) | h;
> +}
> +
> +uint32_t aplic_msi_target_gen(const struct vcpu *target_vcpu,
> +                              uint32_t base_val)
> +{
> +    unsigned int guest_id = vcpu_guest_file_id(target_vcpu);
> +    unsigned long hart_field = aplic_hart_field(target_vcpu->processor);
Nit: could be const
Should be hart_index too, according to previous comment.
> +
> +    base_val &= APLIC_TARGET_EIID;
> +    base_val |= MASK_INSR(guest_id, APLIC_TARGET_GUEST_IDX);
> +    base_val |= MASK_INSR(hart_field, APLIC_TARGET_HART_IDX);
> +
> +    return base_val;
> +}
> +
> +uint32_t aplic_hw_read_reg(unsigned int offset)
> +{
> +    unsigned long flags;
> +    uint32_t val;
> +
> +    ASSERT((offset < aplic.size) && IS_ALIGNED(offset, sizeof(uint32_t)));
> +
> +    spin_lock_irqsave(&aplic.lock, flags);
> +    val = readl((volatile void __iomem *)aplic.regs + offset);
> +    spin_unlock_irqrestore(&aplic.lock, flags);
> +
> +    return val;
> +}
> +
> +void aplic_hw_write_reg(unsigned int offset, uint32_t value)
> +{
> +    unsigned long flags;
> +
> +    ASSERT((offset < aplic.size) && IS_ALIGNED(offset, sizeof(uint32_t)));
> +
> +    spin_lock_irqsave(&aplic.lock, flags);
> +    writel(value, (volatile void __iomem *)aplic.regs + offset);
> +    spin_unlock_irqrestore(&aplic.lock, flags);
> +}
> +
>  static void __init aplic_init_hw_interrupts(void)
>  {
>      unsigned int i;
> @@ -53,9 +173,9 @@ static void __init aplic_init_hw_interrupts(void)
>          /*
>           * Low bits of target register contains Interrupt Priority bits which
>           * can't be zero according to AIA spec.
> -         * Thereby they are initialized to APLIC_DEFAULT_PRIORITY.
> +         * Thereby they are initialized to APLIC_TARGET_IPRIO_DEFAULT.
>           */
> -        writel(APLIC_DEFAULT_PRIORITY, &aplic.regs->target[i]);
> +        writel(APLIC_TARGET_IPRIO_DEFAULT, &aplic.regs->target[i]);
>      }
>  
>      writel(APLIC_DOMAINCFG_IE | APLIC_DOMAINCFG_DM, &aplic.regs->domaincfg);
> diff --git a/xen/arch/riscv/include/asm/aplic.h b/xen/arch/riscv/include/asm/aplic.h
> index a2af55d54f..babba38607 100644
> --- a/xen/arch/riscv/include/asm/aplic.h
> +++ b/xen/arch/riscv/include/asm/aplic.h
> @@ -39,6 +39,13 @@
>  #define  APLIC_DOMAINCFG_IE             BIT(8, U)
>  #define  APLIC_DOMAINCFG_DM             BIT(2, U)
>  #define  APLIC_DOMAINCFG_BE             BIT(0, U)
> +/*
> + * The bits a write may change. Everything else, including the read-only zero
> + * bit 7 and the reserved bits, has to read back as zero, and BE is WARL and
> + * hardwired to 0 as Xen is little-endian only.
> + */
> +#define  APLIC_DOMAINCFG_WMASK          (APLIC_DOMAINCFG_IE | \
> +                                         APLIC_DOMAINCFG_DM)
>  
>  #define APLIC_SOURCECFG_BASE            0x0004
>  #define APLIC_SOURCECFG_LAST            0x0ffc
> @@ -89,6 +96,9 @@
>  #define  APLIC_TARGET_GUEST_IDX         GENMASK(17, 12)
>  /* Bit 11 is reserved and reads as zero */
>  #define  APLIC_TARGET_EIID              GENMASK(10, 0)
> +/* If target is in DM mode */
I think this comment is not clear; I expect, by reading it, to have
domaincfg.DM = 1 which is MSI mode, but I think you were talking about
direct delivery mode, right? If so, I would change this comment to

/* If target is in direct delivery mode (domaincfg.DM = 0) */
> +#define  APLIC_TARGET_IPRIO             GENMASK(7, 0)
> +#define   APLIC_TARGET_IPRIO_DEFAULT    1U
>  
>  #define APLIC_IDC_SIZE                  32
>  
> @@ -98,6 +108,27 @@
>  #define APLIC_SIZE(nr_cpus) \
>      (APLIC_MIN_SIZE + APLIC_SIZE_ALIGN(APLIC_IDC_SIZE * (nr_cpus)))
>  
> +/*
> + * Using setip is fine here, as all SET* and CLR* register groups consist of 32
> + * registers and therefore have identical sizes.
> + *
> + * Lowest 2 bits are always zero for SET* and CLR* registers.
> + */
> +#define APLIC_SETCLR_OFFSET_MASK \
> +    (sizeof_field(struct aplic_regs, setip) - sizeof(uint32_t))
> +
> +#define APLIC_xMSICFGADDR_PPN_SHIFT IMSIC_MMIO_PAGE_SHIFT
> +
> +#define APLIC_xMSICFGADDR_PPN_HHX_MASK(hhxw) \
> +    (BIT(hhxw, UL) - 1)
> +#define APLIC_xMSICFGADDR_PPN_HHX_SHIFT(hhxs) \
> +    ((hhxs) + APLIC_xMSICFGADDR_PPN_SHIFT)
> +
> +#define APLIC_xMSICFGADDR_PPN_LHX_MASK(lhxw) \
> +    (BIT(lhxw, UL) - 1)
> +#define APLIC_xMSICFGADDR_PPN_LHX_SHIFT(lhxs) \
> +    (lhxs)
> +
>  struct aplic_regs {
>      uint32_t domaincfg;         /* 0x0000 */
>      uint32_t sourcecfg[1023];   /* 0x0004 */
> @@ -141,4 +172,7 @@ struct aplic_regs {
>      uint32_t target[1023];      /* 0x3004 */
>  };
>  
> +uint32_t aplic_hw_read_reg(unsigned int offset);
> +void aplic_hw_write_reg(unsigned int offset, uint32_t value);
> +
>  #endif /* ASM_RISCV_APLIC_H */
> diff --git a/xen/arch/riscv/include/asm/imsic.h b/xen/arch/riscv/include/asm/imsic.h
> index 2425430ed1..93f9e44c7d 100644
> --- a/xen/arch/riscv/include/asm/imsic.h
> +++ b/xen/arch/riscv/include/asm/imsic.h
> @@ -40,6 +40,19 @@ struct imsic_config {
>      /* Base address */
>      paddr_t base_addr;
>  
> +    /*
> +     * MSI Target Address Scheme
> +     *
> +     * XLEN-1       HHXS+24          LHXS+12          12          0
> +     * |            |                |                |           |
> +     * ------------------------------------------------------------
> +     * |xxxx|   g   |xxxxxxxx|   h   |xxxx|Guest Index|     0     |
> +     * ------------------------------------------------------------
> +     * - g: group number.
> +     * - h: hart number relative to the group.
> +     * - xxxx: remaining Base PPN bits; each gap may be zero-width.
> +     */
> +
Is this really needed as you already explain this above in aplic.c?
Please choose one place between the two if not.
>      /* Bits representing Guest index, HART index, and Group index */
>      unsigned int guest_index_bits;
>      unsigned int hart_index_bits;
> diff --git a/xen/arch/riscv/include/asm/vaplic.h b/xen/arch/riscv/include/asm/vaplic.h
> index 96080bfbc2..046c604915 100644
> --- a/xen/arch/riscv/include/asm/vaplic.h
> +++ b/xen/arch/riscv/include/asm/vaplic.h
> @@ -21,11 +21,16 @@ struct domain;
>  
>  struct vaplic_regs {
>      uint32_t domaincfg;
> +
> +    uint32_t *target;
>  };
>  
>  struct vaplic {
>      struct vintc vintc;
>      struct vaplic_regs regs;
> +
> +    paddr_t regs_start;
> +    unsigned int regs_size;
>  };
>  
>  int domain_vaplic_init(struct domain *d);
> diff --git a/xen/arch/riscv/vaplic.c b/xen/arch/riscv/vaplic.c
> index 14f6e3164a..8726f7203d 100644
> --- a/xen/arch/riscv/vaplic.c
> +++ b/xen/arch/riscv/vaplic.c
> @@ -17,6 +17,7 @@
>  #include <asm/aia.h>
>  #include <asm/imsic.h>
>  #include <asm/intc.h>
> +#include <asm/mmio.h>
>  #include <asm/vaplic.h>
>  
>  #include "aplic-priv.h"
> @@ -27,6 +28,279 @@ unsigned int __ro_after_init guest_aplic_num_sources;
>  
>  #define FDT_VAPLIC_INT_CELLS 2
>  
> +#define AUTH_IRQ_BIT(d, irqn) \
> +    (((irqn) < (d)->arch.vintc->nr_virqs) && \
> +     test_bit(irqn, (d)->arch.vintc->used_irqs))
> +
> +/*
> + * Convert a byte offset (within a SETIP/CLRIP/SETIE/CLRIE register group) to
> + * a 32-bit word index into the used_irqs bitmap. Each word covers 32
> + * interrupt sources. For SOURCECFG and TARGET groups the same division also
> + * yields the interrupt number directly, because those arrays store one 32-bit
> + * register per source.
> + */
> +#define regoffset_to_word_idx(reg_val) ((reg_val) / sizeof(uint32_t))
> +
> +static uint32_t vaplic_target_read(const struct domain *d, unsigned int irqn)
> +{
> +    const struct vaplic *vaplic = to_vaplic(d);
> +
> +    /* target[0] doesn't exist so irqn == 0 should be impossible */
> +    if ( !irqn || irqn >= vaplic->vintc.nr_virqs )
> +        return 0;
> +
> +    return read_atomic(&vaplic->regs.target[irqn]);
> +}
> +
> +static inline uint32_t generate_auth_mask(const struct domain *currd,
> +                                          unsigned int word_idx)
> +{
> +    unsigned int first_bit = word_idx * sizeof(uint32_t) * BITS_PER_BYTE;
> +
> +    if ( word_idx >= DIV_ROUND_UP(currd->arch.vintc->nr_virqs,
> +                                  sizeof(uint32_t) * BITS_PER_BYTE) )
> +    {
> +        gdprintk(XENLOG_DEBUG, "incorrect word_idx(%u) is passed\n", word_idx);
> +
> +        return 0;
> +    }
> +
> +    return currd->arch.vintc->used_irqs[first_bit / BITS_PER_LONG] >>
> +           (first_bit % BITS_PER_LONG);
> +}
> +
> +static bool vaplic_emulate_load(const struct vcpu *curr, paddr_t addr,
> +                                uint32_t *out)
> +{
> +    const struct domain *currd = curr->domain;
> +    const struct vaplic *vaplic = to_vaplic(currd);
> +    const unsigned int offset = addr & APLIC_CTRL_REGION_OFFSET_MASK;
> +    uint32_t auth_mask;
> +    unsigned int i;
> +
> +    ASSERT(curr == current);
> +
> +    switch ( offset )
> +    {
> +    case APLIC_DOMAINCFG:
> +        *out = vaplic->regs.domaincfg;
> +
> +        return true;
> +
> +    case APLIC_SETIPNUM:
> +    case APLIC_SETIPNUM_LE:
> +    case APLIC_CLRIPNUM:
> +    case APLIC_SETIENUM:
> +    case APLIC_CLRIENUM:
> +    case APLIC_CLRIE_BASE ... APLIC_CLRIE_LAST:
> +        /*
> +         * Based on the RISC-V AIA spec a read of these registers
> +         * always returns zero
> +         */
> +        *out = 0;
> +
> +        return true;
> +
> +    case APLIC_SETIP_BASE ... APLIC_SETIP_LAST:
> +    case APLIC_CLRIP_BASE ... APLIC_CLRIP_LAST:
> +    case APLIC_SETIE_BASE ... APLIC_SETIE_LAST:
> +        i = regoffset_to_word_idx(offset & APLIC_SETCLR_OFFSET_MASK);
> +        auth_mask = generate_auth_mask(currd, i);
> +
> +        break;
> +
> +    case APLIC_TARGET_BASE ... APLIC_TARGET_LAST:
> +        /*
> +         * As target registers start from 1:
> +         *  0x3000 genmsi
> +         *  0x3004 target[1]
> +         *  0x3008 target[2]
> +         *   ...
> +         *  0x3FFC target[1023]
> +         * It is necessary to calculate an interrupt number by subtracting
> +         * APLIC_GENMSI instead of APLIC_TARGET_BASE.
> +         */
> +        i = regoffset_to_word_idx(offset - APLIC_GENMSI);
> +
> +        *out = AUTH_IRQ_BIT(currd, i) ? vaplic_target_read(currd, i) : 0;
> +
> +        return true;
> +
> +    default:
> +        gdprintk(XENLOG_WARNING, "Unhandled APLIC read at offset %#x\n",
> +                 offset);
> +
> +        return false;
> +    }
> +
> +    *out = aplic_hw_read_reg(offset) & auth_mask;
> +
> +    return true;
> +}
> +
> +static bool vaplic_emulate_store(const struct vcpu *curr, paddr_t addr,
> +                                 uint32_t value)
> +{
> +    const struct domain *currd = curr->domain;
> +    unsigned int offset = addr & APLIC_CTRL_REGION_OFFSET_MASK;
> +
> +    ASSERT(curr == current);
> +
> +    switch ( offset )
> +    {
> +    case APLIC_SETIP_BASE ... APLIC_SETIP_LAST:
> +    case APLIC_CLRIP_BASE ... APLIC_CLRIP_LAST:
> +    case APLIC_SETIE_BASE ... APLIC_SETIE_LAST:
> +    case APLIC_CLRIE_BASE ... APLIC_CLRIE_LAST:
> +    {
> +        unsigned int word_idx =
> +            regoffset_to_word_idx(offset & APLIC_SETCLR_OFFSET_MASK);
> +
> +        value &= generate_auth_mask(currd, word_idx);
> +
> +        break;
> +    }
> +
> +    case APLIC_SOURCECFG_BASE ... APLIC_SOURCECFG_LAST:
> +        if ( value & APLIC_SOURCECFG_D )
> +        {
> +            dprintk(XENLOG_ERR, "APLIC_SOURCECFG_D isn't supported\n");
> +
> +            goto fail;
> +        }
> +
> +        /*
> +         * As sourcecfg register starts from 1:
> +         *   0x0000 domaincfg
> +         *   0x0004 sourcecfg[1]
> +         *   0x0008 sourcecfg[2]
> +         *    ...
> +         *   0x0FFC sourcecfg[1023]
> +         * It is necessary to calculate an interrupt number by subtracting
> +         * APLIC_DOMAINCFG instead of APLIC_SOURCECFG_BASE.
> +         */
> +        if ( !AUTH_IRQ_BIT(currd,
> +                           regoffset_to_word_idx(offset - APLIC_DOMAINCFG)) )
> +            /* Interrupt not enabled, ignore it */
> +            return true;
> +
> +        if ( value > APLIC_SOURCECFG_SM_LEVEL_LOW )
> +        {
> +            gdprintk(XENLOG_ERR,
> +                     "value(%#x) is incorrect for sourcecfg register\n",
> +                     value);
> +
> +            return true;
> +        }
> +
> +        break;
> +
> +    case APLIC_TARGET_BASE ... APLIC_TARGET_LAST:
> +    {
> +        struct vaplic *vaplic = to_vaplic(currd);
> +        struct vcpu *target_vcpu;
> +        unsigned int guest_hart_idx = MASK_EXTR(value, APLIC_TARGET_HART_IDX);
> +        /*
> +         * Look at vaplic_emulate_load() for explanation why APLIC_GENMSI is
> +         * subtracted.
> +         */
> +        unsigned int srcn = regoffset_to_word_idx(offset - APLIC_GENMSI);
> +
> +        if ( !AUTH_IRQ_BIT(currd, srcn) )
> +            /* Interrupt not enabled, ignore it */
> +            return true;
> +
> +        target_vcpu = domain_vcpu(currd, guest_hart_idx);
> +
> +        if ( !target_vcpu )
> +        {
> +            dprintk(XENLOG_ERR, "Invalid vCPU id in target register\n");
> +
> +            /* Ignore such writings */
> +            return true;
> +        }
> +
> +        if ( vaplic->regs.domaincfg & APLIC_DOMAINCFG_DM )
> +        {
> +            /*
> +             * A non-zero guest index asks for delivery to an interrupt file of
> +             * nested guest. The vIMSIC node has no riscv,guest-index-bits
> +             * property, so a guest is told its harts have no guest interrupt
> +             * files and the field is read-only zero for them. The write isn't
> +             * rejected (that would throw away a valid hart index and EIID);
> +             * instead the field is dropped, which is also what
> +             * aplic_msi_target_gen() does with it when programming the h/w.
> +             */
> +            if ( MASK_EXTR(value, APLIC_TARGET_GUEST_IDX) )
> +            {
> +                printk_once(XENLOG_WARNING
> +                            "%pd: vAPLIC target guest index != 0 is unsupported\n",
> +                            currd);
> +
> +                /* Ignore such writes ... */
> +                return true;
> +            }
>
Comment above this says "The write isn't rejected ... instead the field
is dropped, which is also what aplic_msi_target_gen() does with it." But
the code doesn't follow it as it returns true immediately here before
the write occurred and without zeroing the guest index field.

-- 
Baptiste Le Duc <baptiste.le-duc@vates.tech>


^ permalink raw reply	[flat|nested] 244+ messages in thread

* Re: [PATCH v2 09/39] xen/riscv: implement virtual APLIC MMIO emulation
  2026-08-27 15:20 ` [PATCH v2 09/39] xen/riscv: implement virtual APLIC MMIO emulation Oleksii Kurochko
  2026-09-02 11:51   ` Baptiste Le Duc
@ 2026-09-02 12:31   ` Baptiste Le Duc
  2026-09-04 14:02     ` Oleksii Kurochko
  2026-09-09 14:26   ` Jan Beulich
  2 siblings, 1 reply; 244+ messages in thread
From: Baptiste Le Duc @ 2026-09-02 12:31 UTC (permalink / raw)
  To: Oleksii Kurochko
  Cc: xen-devel, Romain Caritey, Baptiste Le Duc, Zheng Zhang,
	Alistair Francis, Connor Davis, Andrew Cooper, Anthony PERARD,
	Michal Orzel, Jan Beulich, Julien Grall, Roger Pau Monné,
	Stefano Stabellini

> Guests running under Xen program interrupt routing by writing to APLIC
> MMIO registers. Xen must intercept these accesses to enforce interrupt
> isolation between domains and to translate guest routing intent into the
> underlying physical MSI topology.
> 
> Writes are gated by the domain's authorised interrupt bitmap so that a
> guest cannot affect interrupts it does not own. TARGET register writes
> additionally require translation of the hart and IMSIC guest-file
> indices from virtual to physical, as the APLIC uses these fields
> directly to compute the MSI delivery address.
> 
> Delegation (APLIC_SOURCECFG_D) is not yet supported.
> 
> Co-developed-by: Romain Caritey <Romain.Caritey@microchip.com>
> Signed-off-by: Oleksii Kurochko <oleksii.kurochko@gmail.com>
>
> diff --git a/xen/arch/riscv/aplic-priv.h b/xen/arch/riscv/aplic-priv.h
> index 35100d3a64..b3a1f79c5b 100644
> --- a/xen/arch/riscv/aplic-priv.h
> +++ b/xen/arch/riscv/aplic-priv.h
> @@ -47,4 +47,7 @@ struct aplic_priv {
>   */
>  extern unsigned int guest_aplic_num_sources;
>  
> +uint32_t aplic_msi_target_gen(const struct vcpu *target_vcpu,
> +                              uint32_t base_val);
> +
>  #endif /* ASM_RISCV_APLIC_PRIV_H */
> diff --git a/xen/arch/riscv/aplic.c b/xen/arch/riscv/aplic.c
> index 3681f0669e..66ba4986a9 100644
> --- a/xen/arch/riscv/aplic.c
> +++ b/xen/arch/riscv/aplic.c
> @@ -16,6 +16,7 @@
>  #include <xen/irq.h>
>  #include <xen/mm.h>
>  #include <xen/sections.h>
> +#include <xen/sched.h>
>  #include <xen/spinlock.h>
>  #include <xen/types.h>
>  #include <xen/vmap.h>
> @@ -28,8 +29,6 @@
>  #include <asm/io.h>
>  #include <asm/riscv_encoding.h>
>  
> -#define APLIC_DEFAULT_PRIORITY  1
> -
>  static struct aplic_priv aplic = {
>      .lock = SPIN_LOCK_UNLOCKED,
>  };
> @@ -38,6 +37,127 @@ static struct intc_info __ro_after_init aplic_info = {
>      .hw_variant = INTC_APLIC,
>  };
>  
> +/*
> + * The arrangement of IMSIC interrupt files in MMIO space follows a topology
> + * defined by the RISC-V AIA specification. An IMSIC group is a set of
> + * interrupt files (e.g., in a cluster or socket) co-located in memory.
> + *
> + * The physical address of an outgoing MSI is calculated by bitwise ORing a
> + * Base Physical Page Number (Base PPN) with the Group Index (g), the Hart
> + * Index (h) and, for a supervisor-level interrupt domain, the Guest Index:
> + *
> + *   ( Base PPN | (g << (HHXS + 12)) | (h << LHXS) | guest ) << 12
> + *
> + * where Base PPN, HHXS, LHXS, HHXW and LHXW come from the {m,s}msiaddrcfg[h]
> + * registers of the interrupt domain that sends the MSI:
> + *
> + * XLEN-1       HHXS+24          LHXS+12          12          0
> + * |            |                |                |           |
> + * ------------------------------------------------------------
> + * |xxxx|   g   |xxxxxxxx|   h   |xxxx|Guest Index|     0     |
> + * ------------------------------------------------------------
> + *
> + * - g: group number.
> + * - h: hart number relative to the group.
> + * - xxxx: remaining Base PPN bits; each gap may be zero-width.
> + * - Guest Index: selects one of the 4 KiB pages right above the hart's own
> + *   supervisor-level file, i.e. it starts at bit 12; LHXS must therefore be
> + *   at least as large as the number of guest index bits.
> + * - Bits 11:0: always zero because IMSIC files are 4 KiB page-aligned.
> + *
> + * For wired interrupts in MSI delivery mode (domaincfg.DM = 1) the APLIC
> + * builds that address itself from the "Hart Index" field (bits 31:18) of the
> + * corresponding target[i] register. That field holds a hart index *number*,
> + * in which both indices are packed adjacently:
> + *
> + * 13          lhxw+hhxw   lhxw       0
> + * |           |           |          |
> + * ------------------------------------
> + * |     0     |Group Index|Hart Index|
> + * ------------------------------------
> + *
> + * - lhxw (Low Hart Index Width): the number of bits used for the hart number
> + *   within a group.
> + * - hhxw (High Hart Index Width): the number of bits used for the group
> + *   number; the remaining bits of the field must be zero.
> + *
> + * The Guest Index isn't a part of it: for a supervisor-level interrupt domain
> + * it has its own field (bits 17:12) in target[i].
> + *
> + * Because there are "xxxx" gaps (Base PPN bits) between the indices in the
> + * physical address (depending on HHXS and LHXS), software must extract the
> + * group and hart components separately and pack them into the APLIC-defined
> + * Hart Index format to ensure correct MSI targeting.
> + */
> +static unsigned long aplic_hart_field(unsigned int cpu)
> +{
> +    const struct imsic_config *imsic = imsic_get_config();
> +    const struct imsic_msi *msi = &imsic->msi[cpu];
> +    /* Low Hart Index Shift */
> +    unsigned int lhxs = imsic->guest_index_bits;
> +    /* Low Hart Index Width */
> +    unsigned int lhxw = imsic->hart_index_bits;
> +    /* High Hart Index Width */
> +    unsigned int hhxw = imsic->group_index_bits;
> +    /* High Hart Index Shift */
> +    unsigned int hhxs =
> +        imsic->group_index_shift - APLIC_xMSICFGADDR_PPN_SHIFT * 2;
> +    /*
> +     * msi->base_addr is the base of the MMIO regset this CPU's interrupt
> +     * files live in, and one regset can cover several harts; msi->offset
> +     * selects this CPU's block inside it. The hart index bits are part of
> +     * that offset, so both indexes have to be derived from the full address.
> +     */
> +    paddr_t target_addr = msi->base_addr + msi->offset;
> +    unsigned long tppn = target_addr >> APLIC_xMSICFGADDR_PPN_SHIFT;
> +    unsigned long g =
> +        (tppn >> APLIC_xMSICFGADDR_PPN_HHX_SHIFT(hhxs)) &
> +        APLIC_xMSICFGADDR_PPN_HHX_MASK(hhxw);
> +    unsigned long h =
> +        (tppn >> APLIC_xMSICFGADDR_PPN_LHX_SHIFT(lhxs)) &
> +        APLIC_xMSICFGADDR_PPN_LHX_MASK(lhxw);
> +
> +    return (g << lhxw) | h;
> +}
> +
> +uint32_t aplic_msi_target_gen(const struct vcpu *target_vcpu,
> +                              uint32_t base_val)
> +{
> +    unsigned int guest_id = vcpu_guest_file_id(target_vcpu);
> +    unsigned long hart_field = aplic_hart_field(target_vcpu->processor);
> +
> +    base_val &= APLIC_TARGET_EIID;
> +    base_val |= MASK_INSR(guest_id, APLIC_TARGET_GUEST_IDX);
> +    base_val |= MASK_INSR(hart_field, APLIC_TARGET_HART_IDX);
> +
> +    return base_val;
> +}
> +
> +uint32_t aplic_hw_read_reg(unsigned int offset)
> +{
> +    unsigned long flags;
> +    uint32_t val;
> +
> +    ASSERT((offset < aplic.size) && IS_ALIGNED(offset, sizeof(uint32_t)));
> +
> +    spin_lock_irqsave(&aplic.lock, flags);
> +    val = readl((volatile void __iomem *)aplic.regs + offset);
> +    spin_unlock_irqrestore(&aplic.lock, flags);
> +
> +    return val;
> +}
> +
> +void aplic_hw_write_reg(unsigned int offset, uint32_t value)
> +{
> +    unsigned long flags;
> +
> +    ASSERT((offset < aplic.size) && IS_ALIGNED(offset, sizeof(uint32_t)));
> +
> +    spin_lock_irqsave(&aplic.lock, flags);
> +    writel(value, (volatile void __iomem *)aplic.regs + offset);
> +    spin_unlock_irqrestore(&aplic.lock, flags);
> +}
> +
>  static void __init aplic_init_hw_interrupts(void)
>  {
>      unsigned int i;
> @@ -53,9 +173,9 @@ static void __init aplic_init_hw_interrupts(void)
>          /*
>           * Low bits of target register contains Interrupt Priority bits which
>           * can't be zero according to AIA spec.
> -         * Thereby they are initialized to APLIC_DEFAULT_PRIORITY.
> +         * Thereby they are initialized to APLIC_TARGET_IPRIO_DEFAULT.
>           */
> -        writel(APLIC_DEFAULT_PRIORITY, &aplic.regs->target[i]);
> +        writel(APLIC_TARGET_IPRIO_DEFAULT, &aplic.regs->target[i]);
>      }
>  
>      writel(APLIC_DOMAINCFG_IE | APLIC_DOMAINCFG_DM, &aplic.regs->domaincfg);
> diff --git a/xen/arch/riscv/include/asm/aplic.h b/xen/arch/riscv/include/asm/aplic.h
> index a2af55d54f..babba38607 100644
> --- a/xen/arch/riscv/include/asm/aplic.h
> +++ b/xen/arch/riscv/include/asm/aplic.h
> @@ -39,6 +39,13 @@
>  #define  APLIC_DOMAINCFG_IE             BIT(8, U)
>  #define  APLIC_DOMAINCFG_DM             BIT(2, U)
>  #define  APLIC_DOMAINCFG_BE             BIT(0, U)
> +/*
> + * The bits a write may change. Everything else, including the read-only zero
> + * bit 7 and the reserved bits, has to read back as zero, and BE is WARL and
> + * hardwired to 0 as Xen is little-endian only.
> + */
> +#define  APLIC_DOMAINCFG_WMASK          (APLIC_DOMAINCFG_IE | \
> +                                         APLIC_DOMAINCFG_DM)
>  
>  #define APLIC_SOURCECFG_BASE            0x0004
>  #define APLIC_SOURCECFG_LAST            0x0ffc
> @@ -89,6 +96,9 @@
>  #define  APLIC_TARGET_GUEST_IDX         GENMASK(17, 12)
>  /* Bit 11 is reserved and reads as zero */
>  #define  APLIC_TARGET_EIID              GENMASK(10, 0)
> +/* If target is in DM mode */
> +#define  APLIC_TARGET_IPRIO             GENMASK(7, 0)
> +#define   APLIC_TARGET_IPRIO_DEFAULT    1U
>  
>  #define APLIC_IDC_SIZE                  32
>  
> @@ -98,6 +108,27 @@
>  #define APLIC_SIZE(nr_cpus) \
>      (APLIC_MIN_SIZE + APLIC_SIZE_ALIGN(APLIC_IDC_SIZE * (nr_cpus)))
>  
> +/*
> + * Using setip is fine here, as all SET* and CLR* register groups consist of 32
> + * registers and therefore have identical sizes.
> + *
> + * Lowest 2 bits are always zero for SET* and CLR* registers.
> + */
> +#define APLIC_SETCLR_OFFSET_MASK \
> +    (sizeof_field(struct aplic_regs, setip) - sizeof(uint32_t))
> +
> +#define APLIC_xMSICFGADDR_PPN_SHIFT IMSIC_MMIO_PAGE_SHIFT
> +
> +#define APLIC_xMSICFGADDR_PPN_HHX_MASK(hhxw) \
> +    (BIT(hhxw, UL) - 1)
> +#define APLIC_xMSICFGADDR_PPN_HHX_SHIFT(hhxs) \
> +    ((hhxs) + APLIC_xMSICFGADDR_PPN_SHIFT)
> +
> +#define APLIC_xMSICFGADDR_PPN_LHX_MASK(lhxw) \
> +    (BIT(lhxw, UL) - 1)
> +#define APLIC_xMSICFGADDR_PPN_LHX_SHIFT(lhxs) \
> +    (lhxs)
> +
>  struct aplic_regs {
>      uint32_t domaincfg;         /* 0x0000 */
>      uint32_t sourcecfg[1023];   /* 0x0004 */
> @@ -141,4 +172,7 @@ struct aplic_regs {
>      uint32_t target[1023];      /* 0x3004 */
>  };
>  
> +uint32_t aplic_hw_read_reg(unsigned int offset);
> +void aplic_hw_write_reg(unsigned int offset, uint32_t value);
> +
>  #endif /* ASM_RISCV_APLIC_H */
> diff --git a/xen/arch/riscv/include/asm/imsic.h b/xen/arch/riscv/include/asm/imsic.h
> index 2425430ed1..93f9e44c7d 100644
> --- a/xen/arch/riscv/include/asm/imsic.h
> +++ b/xen/arch/riscv/include/asm/imsic.h
> @@ -40,6 +40,19 @@ struct imsic_config {
>      /* Base address */
>      paddr_t base_addr;
>  
> +    /*
> +     * MSI Target Address Scheme
> +     *
> +     * XLEN-1       HHXS+24          LHXS+12          12          0
> +     * |            |                |                |           |
> +     * ------------------------------------------------------------
> +     * |xxxx|   g   |xxxxxxxx|   h   |xxxx|Guest Index|     0     |
> +     * ------------------------------------------------------------
> +     * - g: group number.
> +     * - h: hart number relative to the group.
> +     * - xxxx: remaining Base PPN bits; each gap may be zero-width.
> +     */
> +
>      /* Bits representing Guest index, HART index, and Group index */
>      unsigned int guest_index_bits;
>      unsigned int hart_index_bits;
> diff --git a/xen/arch/riscv/include/asm/vaplic.h b/xen/arch/riscv/include/asm/vaplic.h
> index 96080bfbc2..046c604915 100644
> --- a/xen/arch/riscv/include/asm/vaplic.h
> +++ b/xen/arch/riscv/include/asm/vaplic.h
> @@ -21,11 +21,16 @@ struct domain;
>  
>  struct vaplic_regs {
>      uint32_t domaincfg;
> +
> +    uint32_t *target;
>  };
>  
>  struct vaplic {
>      struct vintc vintc;
>      struct vaplic_regs regs;
> +
> +    paddr_t regs_start;
> +    unsigned int regs_size;
>  };
>  
>  int domain_vaplic_init(struct domain *d);
> diff --git a/xen/arch/riscv/vaplic.c b/xen/arch/riscv/vaplic.c
> index 14f6e3164a..8726f7203d 100644
> --- a/xen/arch/riscv/vaplic.c
> +++ b/xen/arch/riscv/vaplic.c
> @@ -17,6 +17,7 @@
>  #include <asm/aia.h>
>  #include <asm/imsic.h>
>  #include <asm/intc.h>
> +#include <asm/mmio.h>
>  #include <asm/vaplic.h>
>  
>  #include "aplic-priv.h"
> @@ -27,6 +28,279 @@ unsigned int __ro_after_init guest_aplic_num_sources;
>  
>  #define FDT_VAPLIC_INT_CELLS 2
>  
> +#define AUTH_IRQ_BIT(d, irqn) \
> +    (((irqn) < (d)->arch.vintc->nr_virqs) && \
> +     test_bit(irqn, (d)->arch.vintc->used_irqs))
> +
> +/*
> + * Convert a byte offset (within a SETIP/CLRIP/SETIE/CLRIE register group) to
> + * a 32-bit word index into the used_irqs bitmap. Each word covers 32
> + * interrupt sources. For SOURCECFG and TARGET groups the same division also
> + * yields the interrupt number directly, because those arrays store one 32-bit
> + * register per source.
> + */
> +#define regoffset_to_word_idx(reg_val) ((reg_val) / sizeof(uint32_t))
> +
> +static uint32_t vaplic_target_read(const struct domain *d, unsigned int irqn)
> +{
> +    const struct vaplic *vaplic = to_vaplic(d);
> +
> +    /* target[0] doesn't exist so irqn == 0 should be impossible */
> +    if ( !irqn || irqn >= vaplic->vintc.nr_virqs )
> +        return 0;
> +
> +    return read_atomic(&vaplic->regs.target[irqn]);
> +}
> +
> +static inline uint32_t generate_auth_mask(const struct domain *currd,
> +                                          unsigned int word_idx)
> +{
> +    unsigned int first_bit = word_idx * sizeof(uint32_t) * BITS_PER_BYTE;
> +
> +    if ( word_idx >= DIV_ROUND_UP(currd->arch.vintc->nr_virqs,
> +                                  sizeof(uint32_t) * BITS_PER_BYTE) )
> +    {
> +        gdprintk(XENLOG_DEBUG, "incorrect word_idx(%u) is passed\n", word_idx);
> +
> +        return 0;
> +    }
> +
> +    return currd->arch.vintc->used_irqs[first_bit / BITS_PER_LONG] >>
> +           (first_bit % BITS_PER_LONG);
> +}
> +
> +static bool vaplic_emulate_load(const struct vcpu *curr, paddr_t addr,
> +                                uint32_t *out)
> +{
> +    const struct domain *currd = curr->domain;
> +    const struct vaplic *vaplic = to_vaplic(currd);
> +    const unsigned int offset = addr & APLIC_CTRL_REGION_OFFSET_MASK;
> +    uint32_t auth_mask;
> +    unsigned int i;
> +
> +    ASSERT(curr == current);
> +
> +    switch ( offset )
> +    {
> +    case APLIC_DOMAINCFG:
> +        *out = vaplic->regs.domaincfg;
> +
> +        return true;
> +
> +    case APLIC_SETIPNUM:
> +    case APLIC_SETIPNUM_LE:
> +    case APLIC_CLRIPNUM:
> +    case APLIC_SETIENUM:
> +    case APLIC_CLRIENUM:
> +    case APLIC_CLRIE_BASE ... APLIC_CLRIE_LAST:
> +        /*
> +         * Based on the RISC-V AIA spec a read of these registers
> +         * always returns zero
> +         */
> +        *out = 0;
> +
> +        return true;
> +
> +    case APLIC_SETIP_BASE ... APLIC_SETIP_LAST:
> +    case APLIC_CLRIP_BASE ... APLIC_CLRIP_LAST:
> +    case APLIC_SETIE_BASE ... APLIC_SETIE_LAST:
> +        i = regoffset_to_word_idx(offset & APLIC_SETCLR_OFFSET_MASK);
> +        auth_mask = generate_auth_mask(currd, i);
> +
> +        break;
> +
> +    case APLIC_TARGET_BASE ... APLIC_TARGET_LAST:
> +        /*
> +         * As target registers start from 1:
> +         *  0x3000 genmsi
> +         *  0x3004 target[1]
> +         *  0x3008 target[2]
> +         *   ...
> +         *  0x3FFC target[1023]
> +         * It is necessary to calculate an interrupt number by subtracting
> +         * APLIC_GENMSI instead of APLIC_TARGET_BASE.
> +         */
> +        i = regoffset_to_word_idx(offset - APLIC_GENMSI);
> +
> +        *out = AUTH_IRQ_BIT(currd, i) ? vaplic_target_read(currd, i) : 0;
> +
> +        return true;
> +
> +    default:
> +        gdprintk(XENLOG_WARNING, "Unhandled APLIC read at offset %#x\n",
> +                 offset);
> +
> +        return false;
> +    }
> +
> +    *out = aplic_hw_read_reg(offset) & auth_mask;
> +
> +    return true;
> +}
> +
> +static bool vaplic_emulate_store(const struct vcpu *curr, paddr_t addr,
> +                                 uint32_t value)
> +{
> +    const struct domain *currd = curr->domain;
> +    unsigned int offset = addr & APLIC_CTRL_REGION_OFFSET_MASK;
> +
> +    ASSERT(curr == current);
> +
> +    switch ( offset )
> +    {
> +    case APLIC_SETIP_BASE ... APLIC_SETIP_LAST:
> +    case APLIC_CLRIP_BASE ... APLIC_CLRIP_LAST:
> +    case APLIC_SETIE_BASE ... APLIC_SETIE_LAST:
> +    case APLIC_CLRIE_BASE ... APLIC_CLRIE_LAST:
> +    {
> +        unsigned int word_idx =
> +            regoffset_to_word_idx(offset & APLIC_SETCLR_OFFSET_MASK);
> +
> +        value &= generate_auth_mask(currd, word_idx);
> +
> +        break;
> +    }
> +
> +    case APLIC_SOURCECFG_BASE ... APLIC_SOURCECFG_LAST:
> +        if ( value & APLIC_SOURCECFG_D )
> +        {
> +            dprintk(XENLOG_ERR, "APLIC_SOURCECFG_D isn't supported\n");
> +
> +            goto fail;
> +        }
> +
> +        /*
> +         * As sourcecfg register starts from 1:
> +         *   0x0000 domaincfg
> +         *   0x0004 sourcecfg[1]
> +         *   0x0008 sourcecfg[2]
> +         *    ...
> +         *   0x0FFC sourcecfg[1023]
> +         * It is necessary to calculate an interrupt number by subtracting
> +         * APLIC_DOMAINCFG instead of APLIC_SOURCECFG_BASE.
> +         */
> +        if ( !AUTH_IRQ_BIT(currd,
> +                           regoffset_to_word_idx(offset - APLIC_DOMAINCFG)) )
> +            /* Interrupt not enabled, ignore it */
> +            return true;
> +
> +        if ( value > APLIC_SOURCECFG_SM_LEVEL_LOW )
This compares the whole value against 7, not the extracted SM field
(bits [2:0]). A perfectly legal SM=0 write with any bit set in the
reserved [9:3] range (e.g. value=8) gets rejected here even though the
actual field is fine Should be MASK_EXTR(value, APLIC_SOURCECFG_SM) >
APLIC_SOURCECFG_SM_LEVEL_LOW.

SM is WARL. If SM invalid but other fields valid, shouldn't reject whole
write. Instead override value.SM with current valid SM in this branch,
so other valid fields still get written.
> +        {
> +            gdprintk(XENLOG_ERR,
> +                     "value(%#x) is incorrect for sourcecfg register\n",
> +                     value);
> +
> +            return true;
> +        }
> +
> +        break;
> +
> +    case APLIC_TARGET_BASE ... APLIC_TARGET_LAST:
> +    {
> +        struct vaplic *vaplic = to_vaplic(currd);
> +        struct vcpu *target_vcpu;
> +        unsigned int guest_hart_idx = MASK_EXTR(value, APLIC_TARGET_HART_IDX);
> +        /*
> +         * Look at vaplic_emulate_load() for explanation why APLIC_GENMSI is
> +         * subtracted.
> +         */
> +        unsigned int srcn = regoffset_to_word_idx(offset - APLIC_GENMSI);
> +
> +        if ( !AUTH_IRQ_BIT(currd, srcn) )
> +            /* Interrupt not enabled, ignore it */
> +            return true;
> +
> +        target_vcpu = domain_vcpu(currd, guest_hart_idx);
> +
> +        if ( !target_vcpu )
> +        {
> +            dprintk(XENLOG_ERR, "Invalid vCPU id in target register\n");
> +
> +            /* Ignore such writings */
> +            return true;
> +        }
> +
> +        if ( vaplic->regs.domaincfg & APLIC_DOMAINCFG_DM )
> +        {
> +            /*
> +             * A non-zero guest index asks for delivery to an interrupt file of
> +             * nested guest. The vIMSIC node has no riscv,guest-index-bits
> +             * property, so a guest is told its harts have no guest interrupt
> +             * files and the field is read-only zero for them. The write isn't
> +             * rejected (that would throw away a valid hart index and EIID);
> +             * instead the field is dropped, which is also what
> +             * aplic_msi_target_gen() does with it when programming the h/w.
> +             */
> +            if ( MASK_EXTR(value, APLIC_TARGET_GUEST_IDX) )
> +            {
> +                printk_once(XENLOG_WARNING
> +                            "%pd: vAPLIC target guest index != 0 is unsupported\n",
> +                            currd);
> +
> +                /* Ignore such writes ... */
> +                return true;
> +            }
Comment above this says "The write isn't rejected ... instead the field
is dropped, which is also what aplic_msi_target_gen() does with it." But
the code doesn't follow it as it returns true immediately here before
the write occurred and without zeroing the guest index field.
> +
> +            write_atomic(&vaplic->regs.target[srcn], value);
> +
> +            value = aplic_msi_target_gen(target_vcpu, value);
> +        }
> +        else
> +        {
> +            /*
> +             * IPRIO is WARL and zero isn't a legal value for it, so normalize
> +             * it once: the guest then reads back exactly what it gets.
> +             */
> +            unsigned int iprio = MASK_EXTR(value, APLIC_TARGET_IPRIO) ?:
> +                                 APLIC_TARGET_IPRIO_DEFAULT;
> +            unsigned long h = cpuid_to_hartid(guest_hart_idx);
> +
> +            value = MASK_INSR(guest_hart_idx, APLIC_TARGET_HART_IDX) |
> +                    MASK_INSR(iprio, APLIC_TARGET_IPRIO);
> +
> +            write_atomic(&vaplic->regs.target[srcn], value);
> +
> +            value = MASK_INSR(h, APLIC_TARGET_HART_IDX) |
> +                    MASK_INSR(iprio, APLIC_TARGET_IPRIO);
> +        }
> +
> +        break;
> +    }
> +
> +    case APLIC_SETIPNUM:
> +    case APLIC_SETIPNUM_LE:
> +    case APLIC_CLRIPNUM:
> +    case APLIC_SETIENUM:
> +    case APLIC_CLRIENUM:
> +        if ( !value || !AUTH_IRQ_BIT(currd, value) )
> +            return true;
> +
> +        break;
> +
> +    case APLIC_DOMAINCFG:
> +    {
> +        struct vaplic *vaplic = to_vaplic(currd);
> +
> +        vaplic->regs.domaincfg = APLIC_DOMAINCFG_RO |
> +                                 (value & APLIC_DOMAINCFG_WMASK);
> +
APLIC_DOMAINCFG_WMASK includes APLIC_DOMAINCFG_DM, so
the guest can clear DM through this write. But aplic.c:
aplic_init_hw_interrupts() sets the real hardware APLIC's domaincfg to IE|DM
exactly once and never touches it again. Is this expected?

Moreover, I saw that d8fbe0bbc7's commit message claims: "a guest's
domaincfg.DM reads back as a fixed one, so is there situation where we would
allow direct delivery mode? If not, the else branch should be dropped.

-- 
Baptiste Le Duc <baptiste.le-duc@vates.tech>


^ permalink raw reply	[flat|nested] 244+ messages in thread

* Re: [PATCH v2 02/39] xen/riscv: drop bug.h's duplicate instruction length helpers
  2026-09-02 10:48     ` Oleksii Kurochko
@ 2026-09-02 13:02       ` Jan Beulich
  2026-09-02 13:45         ` Oleksii Kurochko
  0 siblings, 1 reply; 244+ messages in thread
From: Jan Beulich @ 2026-09-02 13:02 UTC (permalink / raw)
  To: Oleksii Kurochko
  Cc: Romain Caritey, Baptiste Le Duc, Zheng Zhang, Alistair Francis,
	Connor Davis, Andrew Cooper, Anthony PERARD, Michal Orzel,
	Julien Grall, Roger Pau Monné, Stefano Stabellini, xen-devel

On 02.09.2026 12:48, Oleksii Kurochko wrote:
> On 9/1/26 9:01 AM, Jan Beulich wrote:
>> And then
>>
>> #define INSN_16BIT_MASK			0x3
>> #define INSN_32BIT_MASK			0x1c
>>
>> are really named backwards, seeing e.g. their use in
> 
> It was derived from OpenSBI project and I haven't paid enough attention 
> for these defines.
> 
> Now looking at them again I fully agree that names here not really 
> correct. It should be according to the spec:
>     #define INSN_32BIT_MASK          0x3
>     #define INSN_48BIT_MASK          0x1c

Especially the latter would then still be misnamed, as the mask merely
identifies insns >= 48 bits. Even for the former I'd still question the
name to some degree ("mask" doesn't mean all of the bits need to be set).

In how many places are you going to need these constants? If, by suitably
using helpers, it's just one - maybe better to get away without any named
constants in this case (i.e. when suitable names are apparently difficult
to come up with)?

> I think that for now it is enough to go without common implmenntation of 
> INSN_LEN and just return 0 as suggested above.

Suitably commented upon that would certainly be okay with me.

Jan


^ permalink raw reply	[flat|nested] 244+ messages in thread

* Re: [PATCH v2 03/39] xen/riscv: set the guest's XLEN explicitly in hstatus.VSXL
  2026-09-02 11:42     ` Oleksii Kurochko
@ 2026-09-02 13:07       ` Jan Beulich
  2026-09-02 13:29         ` Oleksii Kurochko
  0 siblings, 1 reply; 244+ messages in thread
From: Jan Beulich @ 2026-09-02 13:07 UTC (permalink / raw)
  To: Oleksii Kurochko
  Cc: Romain Caritey, Baptiste Le Duc, Zheng Zhang, Alistair Francis,
	Connor Davis, Andrew Cooper, Anthony PERARD, Michal Orzel,
	Julien Grall, Roger Pau Monné, Stefano Stabellini, xen-devel

On 02.09.2026 13:42, Oleksii Kurochko wrote:
> On 9/1/26 5:20 PM, Jan Beulich wrote:
>> On 27.08.2026 17:20, Oleksii Kurochko wrote:
>>> --- a/xen/arch/riscv/domain.c
>>> +++ b/xen/arch/riscv/domain.c
>>> @@ -88,7 +88,13 @@ static void vcpu_csr_init(struct vcpu *v)
>>>   {
>>>       v->arch.hedeleg = HEDELEG_DEFAULT & csr_masks.hedeleg;
>>>   
>>> -    vcpu_guest_cpu_user_regs(v)->hstatus = HSTATUS_SPV | HSTATUS_SPVP;
>>> +    /*
>>> +     * Xen supports 64-bit guests only, so set the guest's XLEN explicitly
>>> +     * rather than leaving it to the WARL behaviour of hstatus.VSXL, which the
>>> +     * decoding of a trapped instruction depends on.
>>> +     */
>>> +    vcpu_guest_cpu_user_regs(v)->hstatus =
>>> +        HSTATUS_SPV | HSTATUS_SPVP | MASK_INSR(HSTATUS_VSXL_64, HSTATUS_VSXL);
>>
>> The comment is correct right now, but the situation better would change
>> at some point. Can't you arrange for things to be correct here also for
>> a future where 32- and 128-bit guests would also be supported?
> 
> I am not sure about 128-bit guests as H extension is dependent on RV32 
> or RV64 but probably it will be changed:
> ```
> The hypervisor extension depends on an "I" base integer ISA with 32 x 
> registers (RV32I or RV64I), not RV32E or RV64E, which have only 16 x 
> registers.
> ```

Lots of updates like this likely will be needed for RV128 to actually become
a thing.

> I will introduce the following (also it will be needed also to check if 
> we could VSXL set at all as implmentation can make that field read-only 
> and do VSXLLEN=HSXLEN):
> 
> /*
>   * Return the hstatus.VSXL value encoding the guest's XLEN. The switch()
>   * deliberately has no default case, so that adding a new domain_type (a
>   * 128-bit one, in particular) fails to build until this mapping is 
> updated.
>   */
> static unsigned int domain_vsxl(const struct domain *d)
> {
>      switch ( d->type )
>      {
>      case DOMAIN_32BIT:
>          return HSTATUS_VSXL_32;
> 
>      case DOMAIN_64BIT:
>          return HSTATUS_VSXL_64;
>      }
> 
>      ASSERT_UNREACHABLE();
> 
>      return HSTATUS_VSXL_64;

Why not simply return 0 here? You genuinely don't know the size.

> }
> 
> It will also affect then common code as vcpu_csr_init() could be then 
> called before domain type is set:
> 
> +++ b/xen/common/device-tree/dom0less-build.c
> @@ -812,17 +812,18 @@ static int __init construct_domU(struct 
> kernel_info *kinfo,
>       else if ( rc == 0 && !strcmp(dom0less_enhanced, "no-xenstore") )
>           kinfo->dom0less_feature = DOM0LESS_ENHANCED_NO_XS;
> 
> -    if ( vcpu_create(d, 0) == NULL )
> -        return -ENOMEM;
> -
>       d->max_pages = ((paddr_t)mem * SZ_1K) >> PAGE_SHIFT;
> 
>       rc = kernel_probe(kinfo, node);
>       if ( rc < 0 )
>           return rc;
> 
> +    /* The domain type needs to be known before the first vCPU is 
> created. */
>       set_domain_type(d, kinfo);
> 
> +    if ( vcpu_create(d, 0) == NULL )
> +        return -ENOMEM;

I don't understand the need for this, likely because I don't see why
domain_vsxl() would need calling from underneath vcpu_create().

Jan


^ permalink raw reply	[flat|nested] 244+ messages in thread

* Re: [PATCH v2 07/39] xen/riscv: add missing APLIC register offsets, masks to asm/aplic.h
  2026-09-01 15:53     ` Jan Beulich
@ 2026-09-02 13:22       ` Jan Beulich
  2026-09-02 13:52         ` Oleksii Kurochko
  0 siblings, 1 reply; 244+ messages in thread
From: Jan Beulich @ 2026-09-02 13:22 UTC (permalink / raw)
  To: Oleksii Kurochko
  Cc: xen-devel, Romain Caritey, Zheng Zhang, Alistair Francis,
	Connor Davis, Andrew Cooper, Anthony PERARD, Michal Orzel,
	Julien Grall, Roger Pau Monné, Stefano Stabellini,
	Baptiste Le Duc

On 01.09.2026 17:53, Jan Beulich wrote:
> On 01.09.2026 17:36, Baptiste Le Duc wrote:
>> On Thu, 27 Aug 2026 17:20:51 +0200, Oleksii Kurochko <oleksii.kurochko@gmail.com> wrote:
>>> These definitions are required for correct decoding of APLIC MMIO
>>> accesses and target configuration, and will be used by both the
>>> physical and virtual APLIC implementations.
>>>
>>> While adding them, rearrange the header in the style of x86's
>>> asm/msr-index.h: a register's offset is immediately followed by the
>>> definitions of that register's fields, with the blocks sorted by
>>> offset.  This makes the relation between a register and its fields
>>> obvious from the layout alone, so no comment is needed to express it.
>>>
>>> [...]
>>
>> Reviewed-by: Baptiste Le Duc <baptiste.le-duc@vates.tech>
> 
> Acked-by: Jan Beulich <jbeulich@suse.com>
> 
> If there's no dependency on earlier patches (nor the prereq series), this
> could go in right away. Yet nothing is being said anywhere, unless I
> overlooked anything.

Doesn't apply cleanly to current staging, so left out for the time being.

Jan


^ permalink raw reply	[flat|nested] 244+ messages in thread

* Re: [PATCH v2 03/39] xen/riscv: set the guest's XLEN explicitly in hstatus.VSXL
  2026-09-02 13:07       ` Jan Beulich
@ 2026-09-02 13:29         ` Oleksii Kurochko
  2026-09-02 14:31           ` Jan Beulich
  0 siblings, 1 reply; 244+ messages in thread
From: Oleksii Kurochko @ 2026-09-02 13:29 UTC (permalink / raw)
  To: Jan Beulich
  Cc: Romain Caritey, Baptiste Le Duc, Zheng Zhang, Alistair Francis,
	Connor Davis, Andrew Cooper, Anthony PERARD, Michal Orzel,
	Julien Grall, Roger Pau Monné, Stefano Stabellini, xen-devel



On 9/2/26 3:07 PM, Jan Beulich wrote:
> On 02.09.2026 13:42, Oleksii Kurochko wrote:
>> On 9/1/26 5:20 PM, Jan Beulich wrote:
>>> On 27.08.2026 17:20, Oleksii Kurochko wrote:
>>>> --- a/xen/arch/riscv/domain.c
>>>> +++ b/xen/arch/riscv/domain.c
>>>> @@ -88,7 +88,13 @@ static void vcpu_csr_init(struct vcpu *v)
>>>>    {
>>>>        v->arch.hedeleg = HEDELEG_DEFAULT & csr_masks.hedeleg;
>>>>    
>>>> -    vcpu_guest_cpu_user_regs(v)->hstatus = HSTATUS_SPV | HSTATUS_SPVP;
>>>> +    /*
>>>> +     * Xen supports 64-bit guests only, so set the guest's XLEN explicitly
>>>> +     * rather than leaving it to the WARL behaviour of hstatus.VSXL, which the
>>>> +     * decoding of a trapped instruction depends on.
>>>> +     */
>>>> +    vcpu_guest_cpu_user_regs(v)->hstatus =
>>>> +        HSTATUS_SPV | HSTATUS_SPVP | MASK_INSR(HSTATUS_VSXL_64, HSTATUS_VSXL);
>>>
>>> The comment is correct right now, but the situation better would change
>>> at some point. Can't you arrange for things to be correct here also for
>>> a future where 32- and 128-bit guests would also be supported?
>>
>> I am not sure about 128-bit guests as H extension is dependent on RV32
>> or RV64 but probably it will be changed:
>> ```
>> The hypervisor extension depends on an "I" base integer ISA with 32 x
>> registers (RV32I or RV64I), not RV32E or RV64E, which have only 16 x
>> registers.
>> ```
> 
> Lots of updates like this likely will be needed for RV128 to actually become
> a thing.
> 
>> I will introduce the following (also it will be needed also to check if
>> we could VSXL set at all as implmentation can make that field read-only
>> and do VSXLLEN=HSXLEN):
>>
>> /*
>>    * Return the hstatus.VSXL value encoding the guest's XLEN. The switch()
>>    * deliberately has no default case, so that adding a new domain_type (a
>>    * 128-bit one, in particular) fails to build until this mapping is
>> updated.
>>    */
>> static unsigned int domain_vsxl(const struct domain *d)
>> {
>>       switch ( d->type )
>>       {
>>       case DOMAIN_32BIT:
>>           return HSTATUS_VSXL_32;
>>
>>       case DOMAIN_64BIT:
>>           return HSTATUS_VSXL_64;
>>       }
>>
>>       ASSERT_UNREACHABLE();
>>
>>       return HSTATUS_VSXL_64;
> 
> Why not simply return 0 here? You genuinely don't know the size.

Agree, just 0 will be better.

> 
>> }
>>
>> It will also affect then common code as vcpu_csr_init() could be then
>> called before domain type is set:
>>
>> +++ b/xen/common/device-tree/dom0less-build.c
>> @@ -812,17 +812,18 @@ static int __init construct_domU(struct
>> kernel_info *kinfo,
>>        else if ( rc == 0 && !strcmp(dom0less_enhanced, "no-xenstore") )
>>            kinfo->dom0less_feature = DOM0LESS_ENHANCED_NO_XS;
>>
>> -    if ( vcpu_create(d, 0) == NULL )
>> -        return -ENOMEM;
>> -
>>        d->max_pages = ((paddr_t)mem * SZ_1K) >> PAGE_SHIFT;
>>
>>        rc = kernel_probe(kinfo, node);
>>        if ( rc < 0 )
>>            return rc;
>>
>> +    /* The domain type needs to be known before the first vCPU is
>> created. */
>>        set_domain_type(d, kinfo);
>>
>> +    if ( vcpu_create(d, 0) == NULL )
>> +        return -ENOMEM;
> 
> I don't understand the need for this, likely because I don't see why
> domain_vsxl() would need calling from underneath vcpu_create().

The call trace will be the following:
   vcpu_create() -> arch_vcpu_create() -> vcpu_csr_init() -> 
domain_vsxldomain_vsxl()

     unsigned int vsxl = domain_vsxl(v->domain);

     ...

     vcpu_guest_cpu_user_regs(v)->hstatus =
         HSTATUS_SPV | HSTATUS_SPVP | MASK_INSR(vsxl, HSTATUS_VSXL);

Without moving vcpu_create(d, 0) after set_domain_type(), domain_vsxl() 
will return something wrong.

I also thought about updating of VSXL for each vCPU in 
construct_domain() where d->type is already known and then no changes in 
common code are needed. But I think it is a little bit better just have 
a change in common code.

~ Oleksii


^ permalink raw reply	[flat|nested] 244+ messages in thread

* Re: [PATCH v2 02/39] xen/riscv: drop bug.h's duplicate instruction length helpers
  2026-09-02 13:02       ` Jan Beulich
@ 2026-09-02 13:45         ` Oleksii Kurochko
  2026-09-02 14:27           ` Jan Beulich
  0 siblings, 1 reply; 244+ messages in thread
From: Oleksii Kurochko @ 2026-09-02 13:45 UTC (permalink / raw)
  To: Jan Beulich
  Cc: Romain Caritey, Baptiste Le Duc, Zheng Zhang, Alistair Francis,
	Connor Davis, Andrew Cooper, Anthony PERARD, Michal Orzel,
	Julien Grall, Roger Pau Monné, Stefano Stabellini, xen-devel



On 9/2/26 3:02 PM, Jan Beulich wrote:
> On 02.09.2026 12:48, Oleksii Kurochko wrote:
>> On 9/1/26 9:01 AM, Jan Beulich wrote:
>>> And then
>>>
>>> #define INSN_16BIT_MASK			0x3
>>> #define INSN_32BIT_MASK			0x1c
>>>
>>> are really named backwards, seeing e.g. their use in
>>
>> It was derived from OpenSBI project and I haven't paid enough attention
>> for these defines.
>>
>> Now looking at them again I fully agree that names here not really
>> correct. It should be according to the spec:
>>      #define INSN_32BIT_MASK          0x3
>>      #define INSN_48BIT_MASK          0x1c
> 
> Especially the latter would then still be misnamed, as the mask merely
> identifies insns >= 48 bits. Even for the former I'd still question the
> name to some degree ("mask" doesn't mean all of the bits need to be set).
> 
> In how many places are you going to need these constants? If, by suitably
> using helpers, it's just one - maybe better to get away without any named
> constants in this case (i.e. when suitable names are apparently difficult
> to come up with)?

INSN_16BIT_MASK - will be used only once (except INSN_IS_32BIT() and 
INSN_IS_16BIT()) in insn_fetch_faulted() introduced later in this patch 
series:
         di->insn = htinst | INSN_16BIT_MASK;

INSN_32BIT_MASK - is used only inside INSN_IS_32BIT().

And INSN_IS_32BIT() and INSN_IS_16BIT() are using in several places.

I am okay to drop these masks and then just have a comment:

/*
  * Instruction length encoding helpers (see RISC-V Unprivileged ISA,
  * Section "Base Instruction-Length Encoding").
  *
  * - Instructions with bits [1:0] != 11 are 16-bit (compressed).
  * - Instructions with bits [1:0] == 11 and bits [4:2] != 111 are 32-bit.
  */
#define INSN_IS_16BIT(insn)      (((insn) & 0x3) != 0x3)
#define INSN_IS_32BIT(insn)      (((insn) & 0x3) == 0x3 && ((insn) & 
0x1c) != 0x1c)

As an option we could come up with the following names:

/* Bitmasks for instruction length encoding fields */
#define INSN_LEN_1_0_MASK        0x3   /* Bits [1:0] for 16-bit vs 
 >=32-bit */
#define INSN_LEN_4_2_MASK        0x1c  /* Bits [4:2] for 32-bit vs 
 >=48-bit */

#define INSN_IS_16BIT(insn)      (((insn) & INSN_LEN_1_0_MASK) != 
INSN_LEN_1_0_MASK)
#define INSN_IS_32BIT(insn)      \
     (((insn) & INSN_LEN_1_0_MASK) == INSN_LEN_1_0_MASK && \
      ((insn) & INSN_LEN_4_2_MASK) != INSN_LEN_4_2_MASK)

But it seems the first option still looks better.

Would you also prefer Option 1?

> 
>> I think that for now it is enough to go without common implmenntation of
>> INSN_LEN and just return 0 as suggested above.
> 
> Suitably commented upon that would certainly be okay with me.

The the following comment looks good enough for me:

/*
  * Length in bytes of the instruction whose first parcel is @insn: 2 or 
4, or
  * 0 when the encoding is 48 bits or wider.  No ratified extension 
defines an
  * instruction of such a length, so rather than open-coding a decoder for
  * encodings which cannot legitimately occur, leave it to the caller to 
treat
  * the 0 as an illegal instruction.
  */
#define INSN_LEN(insn) \
     (INSN_IS_16BIT(insn) ? 2 : (INSN_IS_32BIT(insn) ? 4 : 0))

~ Oleksii


^ permalink raw reply	[flat|nested] 244+ messages in thread

* Re: [PATCH v2 07/39] xen/riscv: add missing APLIC register offsets, masks to asm/aplic.h
  2026-09-02 13:22       ` Jan Beulich
@ 2026-09-02 13:52         ` Oleksii Kurochko
  0 siblings, 0 replies; 244+ messages in thread
From: Oleksii Kurochko @ 2026-09-02 13:52 UTC (permalink / raw)
  To: Jan Beulich
  Cc: xen-devel, Romain Caritey, Zheng Zhang, Alistair Francis,
	Connor Davis, Andrew Cooper, Anthony PERARD, Michal Orzel,
	Julien Grall, Roger Pau Monné, Stefano Stabellini,
	Baptiste Le Duc



On 9/2/26 3:22 PM, Jan Beulich wrote:
> On 01.09.2026 17:53, Jan Beulich wrote:
>> On 01.09.2026 17:36, Baptiste Le Duc wrote:
>>> On Thu, 27 Aug 2026 17:20:51 +0200, Oleksii Kurochko <oleksii.kurochko@gmail.com> wrote:
>>>> These definitions are required for correct decoding of APLIC MMIO
>>>> accesses and target configuration, and will be used by both the
>>>> physical and virtual APLIC implementations.
>>>>
>>>> While adding them, rearrange the header in the style of x86's
>>>> asm/msr-index.h: a register's offset is immediately followed by the
>>>> definitions of that register's fields, with the blocks sorted by
>>>> offset.  This makes the relation between a register and its fields
>>>> obvious from the layout alone, so no comment is needed to express it.
>>>>
>>>> [...]
>>>
>>> Reviewed-by: Baptiste Le Duc <baptiste.le-duc@vates.tech>
>>
>> Acked-by: Jan Beulich <jbeulich@suse.com>

Thanks.

>>
>> If there's no dependency on earlier patches (nor the prereq series), this
>> could go in right away. Yet nothing is being said anywhere, unless I
>> overlooked anything.
> 
> Doesn't apply cleanly to current staging, so left out for the time being.

Yes, it isn't applied because of:

[PATCH v8 12/20] xen/riscv: introduce minimal virtual APLIC (vAPLIC) 
infrastructure

~ Oleksii


^ permalink raw reply	[flat|nested] 244+ messages in thread

* Re: [PATCH v2 02/39] xen/riscv: drop bug.h's duplicate instruction length helpers
  2026-09-02 13:45         ` Oleksii Kurochko
@ 2026-09-02 14:27           ` Jan Beulich
  0 siblings, 0 replies; 244+ messages in thread
From: Jan Beulich @ 2026-09-02 14:27 UTC (permalink / raw)
  To: Oleksii Kurochko
  Cc: Romain Caritey, Baptiste Le Duc, Zheng Zhang, Alistair Francis,
	Connor Davis, Andrew Cooper, Anthony PERARD, Michal Orzel,
	Julien Grall, Roger Pau Monné, Stefano Stabellini, xen-devel

On 02.09.2026 15:45, Oleksii Kurochko wrote:
> On 9/2/26 3:02 PM, Jan Beulich wrote:
>> On 02.09.2026 12:48, Oleksii Kurochko wrote:
>>> On 9/1/26 9:01 AM, Jan Beulich wrote:
>>>> And then
>>>>
>>>> #define INSN_16BIT_MASK			0x3
>>>> #define INSN_32BIT_MASK			0x1c
>>>>
>>>> are really named backwards, seeing e.g. their use in
>>>
>>> It was derived from OpenSBI project and I haven't paid enough attention
>>> for these defines.
>>>
>>> Now looking at them again I fully agree that names here not really
>>> correct. It should be according to the spec:
>>>      #define INSN_32BIT_MASK          0x3
>>>      #define INSN_48BIT_MASK          0x1c
>>
>> Especially the latter would then still be misnamed, as the mask merely
>> identifies insns >= 48 bits. Even for the former I'd still question the
>> name to some degree ("mask" doesn't mean all of the bits need to be set).
>>
>> In how many places are you going to need these constants? If, by suitably
>> using helpers, it's just one - maybe better to get away without any named
>> constants in this case (i.e. when suitable names are apparently difficult
>> to come up with)?
> 
> INSN_16BIT_MASK - will be used only once (except INSN_IS_32BIT() and 
> INSN_IS_16BIT()) in insn_fetch_faulted() introduced later in this patch 
> series:
>          di->insn = htinst | INSN_16BIT_MASK;
> 
> INSN_32BIT_MASK - is used only inside INSN_IS_32BIT().
> 
> And INSN_IS_32BIT() and INSN_IS_16BIT() are using in several places.
> 
> I am okay to drop these masks and then just have a comment:
> 
> /*
>   * Instruction length encoding helpers (see RISC-V Unprivileged ISA,
>   * Section "Base Instruction-Length Encoding").
>   *
>   * - Instructions with bits [1:0] != 11 are 16-bit (compressed).
>   * - Instructions with bits [1:0] == 11 and bits [4:2] != 111 are 32-bit.
>   */
> #define INSN_IS_16BIT(insn)      (((insn) & 0x3) != 0x3)
> #define INSN_IS_32BIT(insn)      (((insn) & 0x3) == 0x3 && ((insn) & 
> 0x1c) != 0x1c)

Largely okay; I'd still prefer

#define INSN_IS_16BIT(insn)      (((insn) & 3) != 3)
#define INSN_IS_32BIT(insn)      (!INSN_IS_16BIT(insn) && ((insn) & 0x1c) != 0x1c)

> As an option we could come up with the following names:
> 
> /* Bitmasks for instruction length encoding fields */
> #define INSN_LEN_1_0_MASK        0x3   /* Bits [1:0] for 16-bit vs 
>  >=32-bit */
> #define INSN_LEN_4_2_MASK        0x1c  /* Bits [4:2] for 32-bit vs 
>  >=48-bit */
> 
> #define INSN_IS_16BIT(insn)      (((insn) & INSN_LEN_1_0_MASK) != 
> INSN_LEN_1_0_MASK)
> #define INSN_IS_32BIT(insn)      \
>      (((insn) & INSN_LEN_1_0_MASK) == INSN_LEN_1_0_MASK && \
>       ((insn) & INSN_LEN_4_2_MASK) != INSN_LEN_4_2_MASK)
> 
> But it seems the first option still looks better.
> 
> Would you also prefer Option 1?

I do, yes.

>>> I think that for now it is enough to go without common implmenntation of
>>> INSN_LEN and just return 0 as suggested above.
>>
>> Suitably commented upon that would certainly be okay with me.
> 
> The the following comment looks good enough for me:
> 
> /*
>   * Length in bytes of the instruction whose first parcel is @insn: 2 or 
> 4, or
>   * 0 when the encoding is 48 bits or wider.  No ratified extension 
> defines an
>   * instruction of such a length, so rather than open-coding a decoder for
>   * encodings which cannot legitimately occur, leave it to the caller to 
> treat
>   * the 0 as an illegal instruction.
>   */
> #define INSN_LEN(insn) \
>      (INSN_IS_16BIT(insn) ? 2 : (INSN_IS_32BIT(insn) ? 4 : 0))

Reads okay, thanks.

Jan


^ permalink raw reply	[flat|nested] 244+ messages in thread

* Re: [PATCH v2 03/39] xen/riscv: set the guest's XLEN explicitly in hstatus.VSXL
  2026-09-02 13:29         ` Oleksii Kurochko
@ 2026-09-02 14:31           ` Jan Beulich
  2026-09-02 15:17             ` Oleksii Kurochko
  0 siblings, 1 reply; 244+ messages in thread
From: Jan Beulich @ 2026-09-02 14:31 UTC (permalink / raw)
  To: Oleksii Kurochko
  Cc: Romain Caritey, Baptiste Le Duc, Zheng Zhang, Alistair Francis,
	Connor Davis, Andrew Cooper, Anthony PERARD, Michal Orzel,
	Julien Grall, Roger Pau Monné, Stefano Stabellini, xen-devel

On 02.09.2026 15:29, Oleksii Kurochko wrote:
> On 9/2/26 3:07 PM, Jan Beulich wrote:
>> On 02.09.2026 13:42, Oleksii Kurochko wrote:
>>> It will also affect then common code as vcpu_csr_init() could be then
>>> called before domain type is set:
>>>
>>> +++ b/xen/common/device-tree/dom0less-build.c
>>> @@ -812,17 +812,18 @@ static int __init construct_domU(struct
>>> kernel_info *kinfo,
>>>        else if ( rc == 0 && !strcmp(dom0less_enhanced, "no-xenstore") )
>>>            kinfo->dom0less_feature = DOM0LESS_ENHANCED_NO_XS;
>>>
>>> -    if ( vcpu_create(d, 0) == NULL )
>>> -        return -ENOMEM;
>>> -
>>>        d->max_pages = ((paddr_t)mem * SZ_1K) >> PAGE_SHIFT;
>>>
>>>        rc = kernel_probe(kinfo, node);
>>>        if ( rc < 0 )
>>>            return rc;
>>>
>>> +    /* The domain type needs to be known before the first vCPU is
>>> created. */
>>>        set_domain_type(d, kinfo);
>>>
>>> +    if ( vcpu_create(d, 0) == NULL )
>>> +        return -ENOMEM;
>>
>> I don't understand the need for this, likely because I don't see why
>> domain_vsxl() would need calling from underneath vcpu_create().
> 
> The call trace will be the following:
>    vcpu_create() -> arch_vcpu_create() -> vcpu_csr_init() -> 
> domain_vsxldomain_vsxl()
> 
>      unsigned int vsxl = domain_vsxl(v->domain);
> 
>      ...
> 
>      vcpu_guest_cpu_user_regs(v)->hstatus =
>          HSTATUS_SPV | HSTATUS_SPVP | MASK_INSR(vsxl, HSTATUS_VSXL);
> 
> Without moving vcpu_create(d, 0) after set_domain_type(), domain_vsxl() 
> will return something wrong.

Question is - do you need to set ->hstatus this early?

> I also thought about updating of VSXL for each vCPU in 
> construct_domain() where d->type is already known and then no changes in 
> common code are needed. But I think it is a little bit better just have 
> a change in common code.

If the maintainers accept that change, it certainly may end up being easier.

Jan


^ permalink raw reply	[flat|nested] 244+ messages in thread

* Re: [PATCH v2 12/39] xen/riscv: implement vCPU context switching
  2026-08-27 15:20 ` [PATCH v2 12/39] xen/riscv: implement vCPU context switching Oleksii Kurochko
@ 2026-09-02 14:42   ` Oleksii Kurochko
  2026-09-04  8:26   ` Baptiste Le Duc
                     ` (2 subsequent siblings)
  3 siblings, 0 replies; 244+ messages in thread
From: Oleksii Kurochko @ 2026-09-02 14:42 UTC (permalink / raw)
  To: xen-devel
  Cc: Romain Caritey, Baptiste Le Duc, Zheng Zhang, Alistair Francis,
	Connor Davis, Andrew Cooper, Anthony PERARD, Michal Orzel,
	Jan Beulich, Julien Grall, Roger Pau Monné,
	Stefano Stabellini



On 8/27/26 5:20 PM, Oleksii Kurochko wrote:
>   }
>   
> +static void save_csr_regs(struct vcpu *vcpu)
> +{
> +    /*
> +     * There is no need to save these CSRs as only hypervisor writes them in
> +     * restore_csr_regs() and guest can't access them so they shouldn't be
> +     * stored here. Keep them commented here just for symmetry with the
> +     * restore CSRs register part.
> +     *
> +     * vcpu->arch.hedeleg = csr_read(CSR_HEDELEG);
> +     * vcpu->arch.hideleg = csr_read(CSR_HIDELEG);
> +     * vcpu->arch.henvcfg = csr_read64(CSR_HENVCFG);
> +     * vcpu->arch.hcounteren = csr_read(CSR_HCOUNTEREN);
> +     * vcpu->arch.htimedelta = csr_read64(CSR_HTIMEDELTA);
> +     *
> +     * if ( riscv_isa_extension_available(NULL, RISCV_ISA_EXT_smstateen) )
> +     *     vcpu->arch.hstateen0 = csr_read(CSR_HSTATEEN0);
> +     */
> +
> +    vcpu->arch.hvip = csr_read(CSR_HVIP);
> +
> +    vcpu->arch.vsstatus = csr_read(CSR_VSSTATUS);
> +    vcpu->arch.vsie = csr_read(CSR_VSIE);
It should be heere csr_read64() (accidentally this changed moved to the 
next patch).

> +    vcpu->arch.vstvec = csr_read(CSR_VSTVEC);
> +    vcpu->arch.vsscratch = csr_read(CSR_VSSCRATCH);
> +    vcpu->arch.vscause = csr_read(CSR_VSCAUSE);
> +    vcpu->arch.vstval = csr_read(CSR_VSTVAL);
> +    vcpu->arch.vsepc = csr_read(CSR_VSEPC);
> +}
> +
> +static void restore_csr_regs(struct vcpu *vcpu)
> +{
> +    csr_write(CSR_HEDELEG, vcpu->arch.hedeleg);
> +    csr_write(CSR_HIDELEG, vcpu->arch.hideleg);
> +    csr_write(CSR_HVIP, vcpu->arch.hvip);
> +    csr_write64(CSR_HENVCFG, vcpu->arch.henvcfg);
> +    csr_write(CSR_HCOUNTEREN, vcpu->arch.hcounteren);
> +    csr_write64(CSR_HTIMEDELTA, vcpu->arch.htimedelta);
> +
> +    if ( riscv_isa_extension_available(NULL, RISCV_ISA_EXT_smstateen) )
> +        csr_write(CSR_HSTATEEN0, vcpu->arch.hstateen0);
> +
> +    csr_write(CSR_VSSTATUS, vcpu->arch.vsstatus);
> +    csr_write(CSR_VSIE, vcpu->arch.vsie);

It should be heere csr_write64() (accidentally this changed moved to the 
next patch).

> +    csr_write(CSR_VSTVEC, vcpu->arch.vstvec);
> +    csr_write(CSR_VSSCRATCH, vcpu->arch.vsscratch);
> +    csr_write(CSR_VSCAUSE, vcpu->arch.vscause);
> +    csr_write(CSR_VSTVAL, vcpu->arch.vstval);
> +    csr_write(CSR_VSEPC, vcpu->arch.vsepc);
> +}
> +

[...]

> diff --git a/xen/arch/riscv/include/asm/domain.h b/xen/arch/riscv/include/asm/domain.h
> index 15e8fa19685e..90ed584bb844 100644
> --- a/xen/arch/riscv/include/asm/domain.h
> +++ b/xen/arch/riscv/include/asm/domain.h
> @@ -29,6 +29,12 @@ struct arch_vcpu_io {
>   struct arch_vcpu {
>       struct vcpu_vmid vmid;
>   
> +    /*
> +     * The last CPU this vCPU ran on. Initialised to NR_CPUS
> +     * (never ran).
> +     */
> +    unsigned int last_cpu;
> +
>       /*
>        * Callee saved registers for Xen's state used to switch from
>        * prev's stack to the next's stack during context switch.
> @@ -60,11 +66,19 @@ struct arch_vcpu {
>       register_t hcounteren;
>       register_t hedeleg;
>       register_t hideleg;
> -    register_t henvcfg;
> +    uint64_t   henvcfg;
>       register_t hstateen0;
> +    uint64_t   htimedelta;
>       register_t hvip;
>   
>       register_t vsatp;
> +    register_t vscause;
> +    register_t vsepc;
> +    register_t vsie;
It should be uint64_t.

> +    register_t vsscratch;
> +    register_t vsstatus;
> +    register_t vstval;
> +    register_t vstvec;
>   
Sorry for inconvenience.

~ Oleksii


^ permalink raw reply	[flat|nested] 244+ messages in thread

* Re: [PATCH v2 03/39] xen/riscv: set the guest's XLEN explicitly in hstatus.VSXL
  2026-09-02 14:31           ` Jan Beulich
@ 2026-09-02 15:17             ` Oleksii Kurochko
  2026-09-02 15:56               ` Oleksii Kurochko
  0 siblings, 1 reply; 244+ messages in thread
From: Oleksii Kurochko @ 2026-09-02 15:17 UTC (permalink / raw)
  To: Jan Beulich
  Cc: Romain Caritey, Baptiste Le Duc, Zheng Zhang, Alistair Francis,
	Connor Davis, Andrew Cooper, Anthony PERARD, Michal Orzel,
	Julien Grall, Roger Pau Monné, Stefano Stabellini, xen-devel



On 9/2/26 4:31 PM, Jan Beulich wrote:
> On 02.09.2026 15:29, Oleksii Kurochko wrote:
>> On 9/2/26 3:07 PM, Jan Beulich wrote:
>>> On 02.09.2026 13:42, Oleksii Kurochko wrote:
>>>> It will also affect then common code as vcpu_csr_init() could be then
>>>> called before domain type is set:
>>>>
>>>> +++ b/xen/common/device-tree/dom0less-build.c
>>>> @@ -812,17 +812,18 @@ static int __init construct_domU(struct
>>>> kernel_info *kinfo,
>>>>         else if ( rc == 0 && !strcmp(dom0less_enhanced, "no-xenstore") )
>>>>             kinfo->dom0less_feature = DOM0LESS_ENHANCED_NO_XS;
>>>>
>>>> -    if ( vcpu_create(d, 0) == NULL )
>>>> -        return -ENOMEM;
>>>> -
>>>>         d->max_pages = ((paddr_t)mem * SZ_1K) >> PAGE_SHIFT;
>>>>
>>>>         rc = kernel_probe(kinfo, node);
>>>>         if ( rc < 0 )
>>>>             return rc;
>>>>
>>>> +    /* The domain type needs to be known before the first vCPU is
>>>> created. */
>>>>         set_domain_type(d, kinfo);
>>>>
>>>> +    if ( vcpu_create(d, 0) == NULL )
>>>> +        return -ENOMEM;
>>>
>>> I don't understand the need for this, likely because I don't see why
>>> domain_vsxl() would need calling from underneath vcpu_create().
>>
>> The call trace will be the following:
>>     vcpu_create() -> arch_vcpu_create() -> vcpu_csr_init() ->
>> domain_vsxldomain_vsxl()
>>
>>       unsigned int vsxl = domain_vsxl(v->domain);
>>
>>       ...
>>
>>       vcpu_guest_cpu_user_regs(v)->hstatus =
>>           HSTATUS_SPV | HSTATUS_SPVP | MASK_INSR(vsxl, HSTATUS_VSXL);
>>
>> Without moving vcpu_create(d, 0) after set_domain_type(), domain_vsxl()
>> will return something wrong.
> 
> Question is - do you need to set ->hstatus this early?

Good point. I think that there is no really such need. It could be set 
(at least, VSXL) just before jumping to new vCPU where we know domain 
type for sure.

~ Oleksii


^ permalink raw reply	[flat|nested] 244+ messages in thread

* Re: [PATCH v2 03/39] xen/riscv: set the guest's XLEN explicitly in hstatus.VSXL
  2026-09-02 15:17             ` Oleksii Kurochko
@ 2026-09-02 15:56               ` Oleksii Kurochko
  2026-09-02 17:45                 ` Oleksii Kurochko
  0 siblings, 1 reply; 244+ messages in thread
From: Oleksii Kurochko @ 2026-09-02 15:56 UTC (permalink / raw)
  To: Jan Beulich
  Cc: Romain Caritey, Baptiste Le Duc, Zheng Zhang, Alistair Francis,
	Connor Davis, Andrew Cooper, Anthony PERARD, Michal Orzel,
	Julien Grall, Roger Pau Monné, Stefano Stabellini, xen-devel



On 9/2/26 5:17 PM, Oleksii Kurochko wrote:
> 
> 
> On 9/2/26 4:31 PM, Jan Beulich wrote:
>> On 02.09.2026 15:29, Oleksii Kurochko wrote:
>>> On 9/2/26 3:07 PM, Jan Beulich wrote:
>>>> On 02.09.2026 13:42, Oleksii Kurochko wrote:
>>>>> It will also affect then common code as vcpu_csr_init() could be then
>>>>> called before domain type is set:
>>>>>
>>>>> +++ b/xen/common/device-tree/dom0less-build.c
>>>>> @@ -812,17 +812,18 @@ static int __init construct_domU(struct
>>>>> kernel_info *kinfo,
>>>>>         else if ( rc == 0 && !strcmp(dom0less_enhanced, "no- 
>>>>> xenstore") )
>>>>>             kinfo->dom0less_feature = DOM0LESS_ENHANCED_NO_XS;
>>>>>
>>>>> -    if ( vcpu_create(d, 0) == NULL )
>>>>> -        return -ENOMEM;
>>>>> -
>>>>>         d->max_pages = ((paddr_t)mem * SZ_1K) >> PAGE_SHIFT;
>>>>>
>>>>>         rc = kernel_probe(kinfo, node);
>>>>>         if ( rc < 0 )
>>>>>             return rc;
>>>>>
>>>>> +    /* The domain type needs to be known before the first vCPU is
>>>>> created. */
>>>>>         set_domain_type(d, kinfo);
>>>>>
>>>>> +    if ( vcpu_create(d, 0) == NULL )
>>>>> +        return -ENOMEM;
>>>>
>>>> I don't understand the need for this, likely because I don't see why
>>>> domain_vsxl() would need calling from underneath vcpu_create().
>>>
>>> The call trace will be the following:
>>>     vcpu_create() -> arch_vcpu_create() -> vcpu_csr_init() ->
>>> domain_vsxldomain_vsxl()
>>>
>>>       unsigned int vsxl = domain_vsxl(v->domain);
>>>
>>>       ...
>>>
>>>       vcpu_guest_cpu_user_regs(v)->hstatus =
>>>           HSTATUS_SPV | HSTATUS_SPVP | MASK_INSR(vsxl, HSTATUS_VSXL);
>>>
>>> Without moving vcpu_create(d, 0) after set_domain_type(), domain_vsxl()
>>> will return something wrong.
>>
>> Question is - do you need to set ->hstatus this early?
> 
> Good point. I think that there is no really such need. It could be set 
> (at least, VSXL) just before jumping to new vCPU where we know domain 
> type for sure.

I planned to set VSXL bits here: 
https://lore.kernel.org/xen-devel/cover.1787838835.git.oleksii.kurochko@gmail.com/T/#m4144ea90815b48f2d281a2267cd28f50bd4cd54c

But it seems that I can't do that there as considering that platform can 
do VSXLEN == HSXLEN == 64 but someone will try do run
vCPU in 32bit mode we can't just crash domain in continue_new_vcpu().

Then still to set it in arch_vcpu_create() will be better (or in the 
construct_domain() if we want to avoid to change dom0less common code). 
At this stage it is easier to reject to create such vCPU which violates 
platform implementation.

~ Oleksii


^ permalink raw reply	[flat|nested] 244+ messages in thread

* Re: [PATCH v2 03/39] xen/riscv: set the guest's XLEN explicitly in hstatus.VSXL
  2026-09-02 15:56               ` Oleksii Kurochko
@ 2026-09-02 17:45                 ` Oleksii Kurochko
  0 siblings, 0 replies; 244+ messages in thread
From: Oleksii Kurochko @ 2026-09-02 17:45 UTC (permalink / raw)
  To: Jan Beulich
  Cc: Romain Caritey, Baptiste Le Duc, Zheng Zhang, Alistair Francis,
	Connor Davis, Andrew Cooper, Anthony PERARD, Michal Orzel,
	Julien Grall, Roger Pau Monné, Stefano Stabellini, xen-devel



On 9/2/26 5:56 PM, Oleksii Kurochko wrote:
> 
> 
> On 9/2/26 5:17 PM, Oleksii Kurochko wrote:
>>
>>
>> On 9/2/26 4:31 PM, Jan Beulich wrote:
>>> On 02.09.2026 15:29, Oleksii Kurochko wrote:
>>>> On 9/2/26 3:07 PM, Jan Beulich wrote:
>>>>> On 02.09.2026 13:42, Oleksii Kurochko wrote:
>>>>>> It will also affect then common code as vcpu_csr_init() could be then
>>>>>> called before domain type is set:
>>>>>>
>>>>>> +++ b/xen/common/device-tree/dom0less-build.c
>>>>>> @@ -812,17 +812,18 @@ static int __init construct_domU(struct
>>>>>> kernel_info *kinfo,
>>>>>>         else if ( rc == 0 && !strcmp(dom0less_enhanced, "no- 
>>>>>> xenstore") )
>>>>>>             kinfo->dom0less_feature = DOM0LESS_ENHANCED_NO_XS;
>>>>>>
>>>>>> -    if ( vcpu_create(d, 0) == NULL )
>>>>>> -        return -ENOMEM;
>>>>>> -
>>>>>>         d->max_pages = ((paddr_t)mem * SZ_1K) >> PAGE_SHIFT;
>>>>>>
>>>>>>         rc = kernel_probe(kinfo, node);
>>>>>>         if ( rc < 0 )
>>>>>>             return rc;
>>>>>>
>>>>>> +    /* The domain type needs to be known before the first vCPU is
>>>>>> created. */
>>>>>>         set_domain_type(d, kinfo);
>>>>>>
>>>>>> +    if ( vcpu_create(d, 0) == NULL )
>>>>>> +        return -ENOMEM;
>>>>>
>>>>> I don't understand the need for this, likely because I don't see why
>>>>> domain_vsxl() would need calling from underneath vcpu_create().
>>>>
>>>> The call trace will be the following:
>>>>     vcpu_create() -> arch_vcpu_create() -> vcpu_csr_init() ->
>>>> domain_vsxldomain_vsxl()
>>>>
>>>>       unsigned int vsxl = domain_vsxl(v->domain);
>>>>
>>>>       ...
>>>>
>>>>       vcpu_guest_cpu_user_regs(v)->hstatus =
>>>>           HSTATUS_SPV | HSTATUS_SPVP | MASK_INSR(vsxl, HSTATUS_VSXL);
>>>>
>>>> Without moving vcpu_create(d, 0) after set_domain_type(), domain_vsxl()
>>>> will return something wrong.
>>>
>>> Question is - do you need to set ->hstatus this early?
>>
>> Good point. I think that there is no really such need. It could be set 
>> (at least, VSXL) just before jumping to new vCPU where we know domain 
>> type for sure.
> 
> I planned to set VSXL bits here: https://lore.kernel.org/xen-devel/ 
> cover.1787838835.git.oleksii.kurochko@gmail.com/T/ 
> #m4144ea90815b48f2d281a2267cd28f50bd4cd54c
> 
> But it seems that I can't do that there as considering that platform can 
> do VSXLEN == HSXLEN == 64 but someone will try do run
> vCPU in 32bit mode we can't just crash domain in continue_new_vcpu().

I think that I can have a check that platform supports VSXLEN guest 
requested based on d->type in construct_domain() where d->type will be 
for sure properly set and reject construction of a domain VSXLEN of 
which isn't supported by a platform. And it looks a proper place for 
such check in general. Then in continue_new_vcpu() just set 
hstatus.VSXLEN without any issue as at that moment we will for sure now 
that requested VSXLEN is correct.

Doing in such way will continue to follow your suggestion (not ->hstatus 
so early in vcpu_csr_init()) and also ...

> 
> Then still to set it in arch_vcpu_create() will be better (or in the 
> construct_domain() if we want to avoid to change dom0less common code). 
> At this stage it is easier to reject to create such vCPU which violates 
> platform implementation.

...  help to void changing of common code.

~ Oleksii


^ permalink raw reply	[flat|nested] 244+ messages in thread

* Re: [PATCH v2 08/39] xen/riscv: introduce device-agnostic MMIO emulation dispatch
  2026-09-01 15:36   ` Baptiste Le Duc
@ 2026-09-03 10:28     ` Oleksii Kurochko
  0 siblings, 0 replies; 244+ messages in thread
From: Oleksii Kurochko @ 2026-09-03 10:28 UTC (permalink / raw)
  To: Baptiste Le Duc
  Cc: xen-devel, Romain Caritey, Zheng Zhang, Alistair Francis,
	Connor Davis, Andrew Cooper, Anthony PERARD, Michal Orzel,
	Jan Beulich, Julien Grall, Roger Pau Monné,
	Stefano Stabellini



On 9/1/26 5:36 PM, Baptiste Le Duc wrote:
>> RISC-V guests can expose several virtual interrupt controllers at
>> distinct GPA ranges: vPLIC (hasn't been introduced yet) for legacy machines,
>> vAPLIC and vIMSIC for AIA-compliant ones (are being introduced in the follow
>> up patches).
> As Jan said here [1], we shouldn't use "as later in this series" in
> commit message...

I will reword this paragraph to:
```
A RISC-V guest can be given several emulated devices at distinct GPA
ranges; the virtual interrupt controllers alone account for vPLIC on
legacy machines and vAPLIC together with vIMSIC on AIA-compliant ones.
Routing MMIO faults via a per-device is_access() check in the trap
handler would couple that handler to every device it must serve,
requiring a new conditional branch in the fault path for each emulated
device added.
```

> 
> [1]: https://lore.kernel.org/xen-devel/cover.1787838835.git.oleksii.kurochko@gmail.com/T/#m56fbac1ceb0642d5d868e9dcb2b5ed93ecbe5058
>> Routing MMIO faults via a per-device is_access() check in the
>> trap handler would couple it to every device it must serve, requiring a
>> new conditional branch in the fault path each time a new emulated device is
>> added.
>>
>> Introduce a per-domain MMIO handler registration table, modeled
>> after the equivalent ARM framework, so that virtual devices
>> self-register their GPA ranges and read/write callbacks at domain
>> creation time. The MMIO fault path delegates to a single
>> try_handle_mmio() entry point and remains agnostic of which device
>> owns a particular address.
>>
>> A subsequent patch wires this into the MMIO fault path in traps.c.
> ...same here

I'll reword this to:
```
Nothing registers a handler and try_handle_mmio() has no callers yet,
so this patch is a no-op; the trap handler is left untouched.
```

+

I will add after Signed-off-by:
```
---
Wiring this into the MMIO fault path in traps.c is done in this patch
series later.
```


[...]

>> +
>> +int register_mmio_handler(struct domain *d,
>> +                          const struct mmio_handler_ops *ops,
>> +                          paddr_t addr, paddr_t size)
>> +{
>> +    struct vmmio *vmmio = &d->arch.vmmio;
>> +    struct mmio_handler *handlers = vmmio->handlers;
>> +    paddr_t end = addr + size;
>> +    unsigned int i;
>> +    int rc = 0;
>> +    bool overlap;
>> +
>> +    if ( !ops || !ops->read || !ops->write || !size || end < addr )
>> +        return -EINVAL;
> Just a question: is the aim of end < addr check to handle possible overflow of end?
> 

Yes, your understanding is correct.

Thanks.

~ Oleksii


^ permalink raw reply	[flat|nested] 244+ messages in thread

* Re: [PATCH v2 11/39] xen/riscv: add helper to check APLIC MSI mode
  2026-08-27 15:20 ` [PATCH v2 11/39] xen/riscv: add helper to check APLIC MSI mode Oleksii Kurochko
@ 2026-09-04  8:26   ` Baptiste Le Duc
  2026-09-09 14:53     ` Jan Beulich
  0 siblings, 1 reply; 244+ messages in thread
From: Baptiste Le Duc @ 2026-09-04  8:26 UTC (permalink / raw)
  To: Oleksii Kurochko
  Cc: xen-devel, Romain Caritey, Baptiste Le Duc, Zheng Zhang,
	Alistair Francis, Connor Davis, Andrew Cooper, Anthony PERARD,
	Michal Orzel, Jan Beulich, Julien Grall, Roger Pau Monné,
	Stefano Stabellini

On Thu, 27 Aug 2026 17:20:55 +0200, Oleksii Kurochko <oleksii.kurochko@gmail.com> wrote:
> Use convient helper instead of open-coding the things.

Reviewed-by: Baptiste Le Duc <baptiste.le-duc@vates.tech>

-- 
Baptiste Le Duc <baptiste.le-duc@vates.tech>


^ permalink raw reply	[flat|nested] 244+ messages in thread

* Re: [PATCH v2 10/39] xen/riscv: build the target hart index via aplic_hart_field()
  2026-08-27 15:20 ` [PATCH v2 10/39] xen/riscv: build the target hart index via aplic_hart_field() Oleksii Kurochko
@ 2026-09-04  8:26   ` Baptiste Le Duc
  2026-09-04 14:28     ` Oleksii Kurochko
  2026-09-09 14:51     ` Jan Beulich
  2026-09-09 14:52   ` Jan Beulich
  1 sibling, 2 replies; 244+ messages in thread
From: Baptiste Le Duc @ 2026-09-04  8:26 UTC (permalink / raw)
  To: Oleksii Kurochko
  Cc: xen-devel, Romain Caritey, Baptiste Le Duc, Zheng Zhang,
	Alistair Francis, Connor Davis, Andrew Cooper, Anthony PERARD,
	Michal Orzel, Jan Beulich, Julien Grall, Roger Pau Monné,
	Stefano Stabellini

On Thu, 27 Aug 2026 17:20:54 +0200, Oleksii Kurochko <oleksii.kurochko@gmail.com> wrote:
> aplic_set_irq_affinity() open-coded the packing of the group and hart
> indices into the target register, and got two things wrong along the
> way:
> 
>  - imsic_config.msi[] is indexed by logical CPU id, but the index was
>    run through cpuid_to_hartid() first. On any platform where the two
>    spaces differ this picks another CPU's interrupt file, or reads past
>    the array;
> 
> [...]

Reviewed-by: Baptiste Le Duc <baptiste.le-duc@vates.tech>

-- 
Baptiste Le Duc <baptiste.le-duc@vates.tech>


^ permalink raw reply	[flat|nested] 244+ messages in thread

* Re: [PATCH v2 12/39] xen/riscv: implement vCPU context switching
  2026-08-27 15:20 ` [PATCH v2 12/39] xen/riscv: implement vCPU context switching Oleksii Kurochko
  2026-09-02 14:42   ` Oleksii Kurochko
@ 2026-09-04  8:26   ` Baptiste Le Duc
  2026-09-04  8:33     ` Jan Beulich
  2026-09-04 14:55     ` Oleksii Kurochko
  2026-09-05  7:25   ` Oleksii Kurochko
  2026-09-10 13:29   ` Jan Beulich
  3 siblings, 2 replies; 244+ messages in thread
From: Baptiste Le Duc @ 2026-09-04  8:26 UTC (permalink / raw)
  To: Oleksii Kurochko
  Cc: xen-devel, Romain Caritey, Baptiste Le Duc, Zheng Zhang,
	Alistair Francis, Connor Davis, Andrew Cooper, Anthony PERARD,
	Michal Orzel, Jan Beulich, Julien Grall, Roger Pau Monné,
	Stefano Stabellini

As I understand it, a generation wrap doesn't retire a single VMID, it
resets next_vmid to 1, which makes every VMID in 1..max_vmid reusable
again in the new generation. We do a full (local) flush at that point to
avoid two different vCPUs ending up with the same VMID valid at once,
across generations.

If this is correct, doing a full flush there also throws away entries
for the current vCPU that a local HFENCE.GVMA(vmid) per retired VMID
could have preserved. A local-flush-per-VMID approach could also
reduce how often we need a full flush at all.

Is there a reason we don't do local flushing instead? I see x86 and KVM
use the same flush-all design on wrap, so I assume there's a reason I'm
missing, I'd like to understand it.

Thanks in advance.

> H/VS CSRs, virtual timer and P2M context, and __context_switch() in assembly,
> which switches Xen's own callee-saved state (and thereby the stack) from
> prev to next. Virtual interrupt controller context switch will be
> introduced later.
> 
> Add offsets of struct arch_vcpu's xen_saved_context to asm-offsets.c for
> use by __context_switch().
> 
> henvcfg and htimedelta are 64-bit on both RV32 and RV64, so store them as
> uint64_t and use csr_{read,write}64() instead of open-coding accesses to
> the high halves.
> 
> A hart which drops out of a domain's dirty_cpumask stops being a target
> of p2m_tlb_flush() while its TLB may still hold G-stage translations of
> that domain, and neither the vCPU which just ran nor any other vCPU of
> that domain which ran there earlier has had its VMID invalidated. Move
> the hart to a new VMID generation at that point: a VMID number is never
> re-used until a full local flush has happened, hence none of those
> translations can be reached again.
> 
> Claim the VMID in p2m_ctxt_switch_to() rather than at the next guest
> entry. VMIDs are a per-hart resource, so the (generation, vmid) pair a
> migrating vCPU brings from another hart is meaningless here and may even
> match this hart's current generation, leaving the vCPU under a VMID owned
> by another domain. ctxt_switch_to() invalidates that pair, but claiming a
> replacement only on guest entry is too late: p2m_ctxt_switch_to() has by
> then already made HGATP live, and speculation can populate G-stage entries
> of the incoming domain under the stale VMID. The local flush for a wrapped
> generation moves along with the claim.
> 
> That leaves p2m_handle_vmenter() with nothing to do, so drop it together
> with its call from check_for_pcpu_work(). A VMID can only be invalidated
> while its vCPU isn't running: vmid_flush_vcpu() is called for the vCPU
> being switched in, and vmid_flush_hart() runs either from schedule_tail(),
> ahead of ctxt_switch_to(), or from the wrap path of vmid_handle_vmenter()
> itself. A P2M change on another hart doesn't invalidate it either, as
> p2m_tlb_flush() drops the stale entries directly with
> sbi_remote_hfence_gvma() instead of retiring the VMIDs which tag them. A
> guest therefore always runs under the VMID claimed on its way in, and
> there is nothing left for a guest entry hook to notice.
> 
> p2m_handle_vmenter() also skipped the HGATP write when the VMID it claimed
> was unchanged. That isn't carried over: HGATP holds the G-stage root as
> well, and skipping the write is only correct where that root is already
> the incoming domain's. On the guest entry path it is, on the context
> switch path it is not.
> 
> While at it, fix the inclusion order of headers in asm-offsets.c: Xen's
> headers go first, then arch specific ones.
This could have a dedicated patch no?
> Signed-off-by: Oleksii Kurochko <oleksii.kurochko@gmail.com>
>
> diff --git a/xen/arch/riscv/domain.c b/xen/arch/riscv/domain.c
> index ec327a5e8a..91a46d630f 100644
> --- a/xen/arch/riscv/domain.c
> +++ b/xen/arch/riscv/domain.c
> @@ -11,9 +11,11 @@
>  #include <asm/bitops.h>
>  #include <asm/cpufeature.h>
>  #include <asm/csr.h>
> +#include <asm/current.h>
>  #include <asm/intc.h>
>  #include <asm/mmio.h>
>  #include <asm/riscv_encoding.h>
> +#include <asm/vmid.h>
>  #include <asm/vtimer.h>
>  
>  struct csr_masks {
> @@ -158,6 +160,8 @@ int arch_vcpu_create(struct vcpu *v)
>      if ( is_idle_vcpu(v) )
>          return 0;
>  
> +    v->arch.last_cpu = NR_CPUS;
> +
>      vcpu_csr_init(v);
>  
>      if ( (rc = vcpu_vtimer_init(v)) )
> @@ -329,6 +333,169 @@ int arch_domain_create(struct domain *d,
>      return rc;
>  }
>  
> +static void save_csr_regs(struct vcpu *vcpu)
> +{
> +    /*
> +     * There is no need to save these CSRs as only hypervisor writes them in
> +     * restore_csr_regs() and guest can't access them so they shouldn't be
> +     * stored here. Keep them commented here just for symmetry with the
> +     * restore CSRs register part.
> +     *
> +     * vcpu->arch.hedeleg = csr_read(CSR_HEDELEG);
> +     * vcpu->arch.hideleg = csr_read(CSR_HIDELEG);
> +     * vcpu->arch.henvcfg = csr_read64(CSR_HENVCFG);
> +     * vcpu->arch.hcounteren = csr_read(CSR_HCOUNTEREN);
> +     * vcpu->arch.htimedelta = csr_read64(CSR_HTIMEDELTA);
> +     *
> +     * if ( riscv_isa_extension_available(NULL, RISCV_ISA_EXT_smstateen) )
> +     *     vcpu->arch.hstateen0 = csr_read(CSR_HSTATEEN0);
> +     */
> +
> +    vcpu->arch.hvip = csr_read(CSR_HVIP);
> +
> +    vcpu->arch.vsstatus = csr_read(CSR_VSSTATUS);
> +    vcpu->arch.vsie = csr_read(CSR_VSIE);


> +    vcpu->arch.vstvec = csr_read(CSR_VSTVEC);
> +    vcpu->arch.vsscratch = csr_read(CSR_VSSCRATCH);
> +    vcpu->arch.vscause = csr_read(CSR_VSCAUSE);
> +    vcpu->arch.vstval = csr_read(CSR_VSTVAL);
> +    vcpu->arch.vsepc = csr_read(CSR_VSEPC);
> +}
> +
> +static void restore_csr_regs(struct vcpu *vcpu)
> +{
> +    csr_write(CSR_HEDELEG, vcpu->arch.hedeleg);
> +    csr_write(CSR_HIDELEG, vcpu->arch.hideleg);
> +    csr_write(CSR_HVIP, vcpu->arch.hvip);
> +    csr_write64(CSR_HENVCFG, vcpu->arch.henvcfg);
> +    csr_write(CSR_HCOUNTEREN, vcpu->arch.hcounteren);
> +    csr_write64(CSR_HTIMEDELTA, vcpu->arch.htimedelta);
> +
> +    if ( riscv_isa_extension_available(NULL, RISCV_ISA_EXT_smstateen) )
> +        csr_write(CSR_HSTATEEN0, vcpu->arch.hstateen0);
> +
> +    csr_write(CSR_VSSTATUS, vcpu->arch.vsstatus);
> +    csr_write(CSR_VSIE, vcpu->arch.vsie);


> +    csr_write(CSR_VSTVEC, vcpu->arch.vstvec);
> +    csr_write(CSR_VSSCRATCH, vcpu->arch.vsscratch);
> +    csr_write(CSR_VSCAUSE, vcpu->arch.vscause);
> +    csr_write(CSR_VSTVAL, vcpu->arch.vstval);
> +    csr_write(CSR_VSEPC, vcpu->arch.vsepc);
> +}
> +
> +static void ctxt_switch_from(struct vcpu *p)
Is it expected to have diverse names for the vcpu arg? Above it's vcpu,
here it's p (I assume it's for `previous` but I think the _from alone is
enough to understand) and below it's n. Shouldn't be better to keep the same name?

-- 
Baptiste Le Duc <baptiste.le-duc@vates.tech>


^ permalink raw reply	[flat|nested] 244+ messages in thread

* Re: [PATCH v2 12/39] xen/riscv: implement vCPU context switching
  2026-09-04  8:26   ` Baptiste Le Duc
@ 2026-09-04  8:33     ` Jan Beulich
  2026-09-04  9:54       ` Baptiste Le Duc
  2026-09-04 14:55     ` Oleksii Kurochko
  1 sibling, 1 reply; 244+ messages in thread
From: Jan Beulich @ 2026-09-04  8:33 UTC (permalink / raw)
  To: Baptiste Le Duc
  Cc: xen-devel, Romain Caritey, Zheng Zhang, Alistair Francis,
	Connor Davis, Andrew Cooper, Anthony PERARD, Michal Orzel,
	Julien Grall, Roger Pau Monné, Stefano Stabellini,
	Oleksii Kurochko

On 04.09.2026 10:26, Baptiste Le Duc wrote:
>> @@ -329,6 +333,169 @@ int arch_domain_create(struct domain *d,
>>      return rc;
>>  }
>>  
>> +static void save_csr_regs(struct vcpu *vcpu)
>> +{
>> +    /*
>> +     * There is no need to save these CSRs as only hypervisor writes them in
>> +     * restore_csr_regs() and guest can't access them so they shouldn't be
>> +     * stored here. Keep them commented here just for symmetry with the
>> +     * restore CSRs register part.
>> +     *
>> +     * vcpu->arch.hedeleg = csr_read(CSR_HEDELEG);
>> +     * vcpu->arch.hideleg = csr_read(CSR_HIDELEG);
>> +     * vcpu->arch.henvcfg = csr_read64(CSR_HENVCFG);
>> +     * vcpu->arch.hcounteren = csr_read(CSR_HCOUNTEREN);
>> +     * vcpu->arch.htimedelta = csr_read64(CSR_HTIMEDELTA);
>> +     *
>> +     * if ( riscv_isa_extension_available(NULL, RISCV_ISA_EXT_smstateen) )
>> +     *     vcpu->arch.hstateen0 = csr_read(CSR_HSTATEEN0);
>> +     */
>> +
>> +    vcpu->arch.hvip = csr_read(CSR_HVIP);
>> +
>> +    vcpu->arch.vsstatus = csr_read(CSR_VSSTATUS);
>> +    vcpu->arch.vsie = csr_read(CSR_VSIE);
> 
> 
>> +    vcpu->arch.vstvec = csr_read(CSR_VSTVEC);
>> +    vcpu->arch.vsscratch = csr_read(CSR_VSSCRATCH);
>> +    vcpu->arch.vscause = csr_read(CSR_VSCAUSE);
>> +    vcpu->arch.vstval = csr_read(CSR_VSTVAL);
>> +    vcpu->arch.vsepc = csr_read(CSR_VSEPC);
>> +}
>> +
>> +static void restore_csr_regs(struct vcpu *vcpu)
>> +{
>> +    csr_write(CSR_HEDELEG, vcpu->arch.hedeleg);
>> +    csr_write(CSR_HIDELEG, vcpu->arch.hideleg);
>> +    csr_write(CSR_HVIP, vcpu->arch.hvip);
>> +    csr_write64(CSR_HENVCFG, vcpu->arch.henvcfg);
>> +    csr_write(CSR_HCOUNTEREN, vcpu->arch.hcounteren);
>> +    csr_write64(CSR_HTIMEDELTA, vcpu->arch.htimedelta);
>> +
>> +    if ( riscv_isa_extension_available(NULL, RISCV_ISA_EXT_smstateen) )
>> +        csr_write(CSR_HSTATEEN0, vcpu->arch.hstateen0);
>> +
>> +    csr_write(CSR_VSSTATUS, vcpu->arch.vsstatus);
>> +    csr_write(CSR_VSIE, vcpu->arch.vsie);
> 
> 
>> +    csr_write(CSR_VSTVEC, vcpu->arch.vstvec);
>> +    csr_write(CSR_VSSCRATCH, vcpu->arch.vsscratch);
>> +    csr_write(CSR_VSCAUSE, vcpu->arch.vscause);
>> +    csr_write(CSR_VSTVAL, vcpu->arch.vstval);
>> +    csr_write(CSR_VSEPC, vcpu->arch.vsepc);
>> +}
>> +
>> +static void ctxt_switch_from(struct vcpu *p)
> Is it expected to have diverse names for the vcpu arg? Above it's vcpu,
> here it's p (I assume it's for `previous` but I think the _from alone is
> enough to understand) and below it's n. Shouldn't be better to keep the same name?

Context switch code is special, and using p and n can be warranted there.
Everywhere else it should be v, nothing else (and I said so to Oleksii
more than once before). Unless, of course, multiple vCPU-s come into play
at the same time.

Jan


^ permalink raw reply	[flat|nested] 244+ messages in thread

* Re: [PATCH v2 13/39] xen/riscv: save and restore AIA state on vCPU context switch
  2026-08-27 15:20 ` [PATCH v2 13/39] xen/riscv: save and restore AIA state on vCPU context switch Oleksii Kurochko
@ 2026-09-04  9:52   ` Baptiste Le Duc
  2026-09-04 16:40     ` Oleksii Kurochko
  0 siblings, 1 reply; 244+ messages in thread
From: Baptiste Le Duc @ 2026-09-04  9:52 UTC (permalink / raw)
  To: Oleksii Kurochko
  Cc: xen-devel, Romain Caritey, Baptiste Le Duc, Zheng Zhang,
	Alistair Francis, Connor Davis, Andrew Cooper, Anthony PERARD,
	Michal Orzel, Jan Beulich, Julien Grall, Roger Pau Monné,
	Stefano Stabellini

> vsiselect and hviprio{1,2} are per-hart CSRs which a guest can change, so
> they have to be part of the vCPU context:
Where in the spec did you see that? Because in AIA spec section 6.3.1,
it is written that "When vsiselect has a value in the range 0x30-0x3F,
an attempt from VS-mode to access sireg (really vsireg) causes a virtual
instruction exception" and this even when hstateen0.CSRIND is set as hstateen0
just control whether a guest/S-mode is allowed to access a CSR (exactly
as you described below).

Therefore, the hypervisor has two options to modify the priority of a
major irq:
- emulate the iprio array in software.
- Use hviprio1/hviprio2 (only 10 irqs configurable).

But the guest shouldn't be able to modify h CSRs at all, in any case, or
I may have misunderstood a part of the spec.

For the moment I don't see any catch of possible instruction exception
in do_trap().

>  - vsiselect is written directly by VS-mode through siselect;
>  - hviprio1 and hviprio2 hold the priorities of the local interrupts which
>    VS-mode reaches through the iprio array of vsiselect/vsireg, so writes
>    the guest performs there land in these CSRs.
hviprio1 and hviprio2 hold priorities for interrupts 1 (SSI), 5 (STI),
13 (counter overflow), and 14-23 (local) so calling all of them "local"
is wrong.
> Without saving them, one vCPU's selector leaks into another vCPU's vsireg
> accesses and one guest's interrupt priorities apply to the next guest which
> runs on the same hart.
> 
> Whether the CSRs may be touched at all is gated by hstateen0 when Smstateen
> is implemented: SVSLCT for vsiselect/vsireg and AIA for the rest of the AIA
I couldn't find any reference to SVSLCT in the spec. I assume you wanted
to refer to CSRIND and SVSLCT is an OpenSBI's own nickname.

-- 
Baptiste Le Duc <baptiste.le-duc@vates.tech>


^ permalink raw reply	[flat|nested] 244+ messages in thread

* Re: [PATCH v2 12/39] xen/riscv: implement vCPU context switching
  2026-09-04  8:33     ` Jan Beulich
@ 2026-09-04  9:54       ` Baptiste Le Duc
  0 siblings, 0 replies; 244+ messages in thread
From: Baptiste Le Duc @ 2026-09-04  9:54 UTC (permalink / raw)
  To: Jan Beulich
  Cc: Baptiste Le Duc, xen-devel, Romain Caritey, Zheng Zhang,
	Alistair Francis, Connor Davis, Andrew Cooper, Anthony PERARD,
	Michal Orzel, Julien Grall, Roger Pau Monné,
	Stefano Stabellini, Oleksii Kurochko

On 2026-09-04 10:33 +0200, Jan Beulich wrote:
> On 04.09.2026 10:26, Baptiste Le Duc wrote:
> >> @@ -329,6 +333,169 @@ int arch_domain_create(struct domain *d,
> >>      return rc;
> >>  }
> >>  
> >> +static void save_csr_regs(struct vcpu *vcpu)
> >> +{
> >> +    /*
> >> +     * There is no need to save these CSRs as only hypervisor writes them in
> >> +     * restore_csr_regs() and guest can't access them so they shouldn't be
> >> +     * stored here. Keep them commented here just for symmetry with the
> >> +     * restore CSRs register part.
> >> +     *
> >> +     * vcpu->arch.hedeleg = csr_read(CSR_HEDELEG);
> >> +     * vcpu->arch.hideleg = csr_read(CSR_HIDELEG);
> >> +     * vcpu->arch.henvcfg = csr_read64(CSR_HENVCFG);
> >> +     * vcpu->arch.hcounteren = csr_read(CSR_HCOUNTEREN);
> >> +     * vcpu->arch.htimedelta = csr_read64(CSR_HTIMEDELTA);
> >> +     *
> >> +     * if ( riscv_isa_extension_available(NULL, RISCV_ISA_EXT_smstateen) )
> >> +     *     vcpu->arch.hstateen0 = csr_read(CSR_HSTATEEN0);
> >> +     */
> >> +
> >> +    vcpu->arch.hvip = csr_read(CSR_HVIP);
> >> +
> >> +    vcpu->arch.vsstatus = csr_read(CSR_VSSTATUS);
> >> +    vcpu->arch.vsie = csr_read(CSR_VSIE);
> > 
> > 
> >> +    vcpu->arch.vstvec = csr_read(CSR_VSTVEC);
> >> +    vcpu->arch.vsscratch = csr_read(CSR_VSSCRATCH);
> >> +    vcpu->arch.vscause = csr_read(CSR_VSCAUSE);
> >> +    vcpu->arch.vstval = csr_read(CSR_VSTVAL);
> >> +    vcpu->arch.vsepc = csr_read(CSR_VSEPC);
> >> +}
> >> +
> >> +static void restore_csr_regs(struct vcpu *vcpu)
> >> +{
> >> +    csr_write(CSR_HEDELEG, vcpu->arch.hedeleg);
> >> +    csr_write(CSR_HIDELEG, vcpu->arch.hideleg);
> >> +    csr_write(CSR_HVIP, vcpu->arch.hvip);
> >> +    csr_write64(CSR_HENVCFG, vcpu->arch.henvcfg);
> >> +    csr_write(CSR_HCOUNTEREN, vcpu->arch.hcounteren);
> >> +    csr_write64(CSR_HTIMEDELTA, vcpu->arch.htimedelta);
> >> +
> >> +    if ( riscv_isa_extension_available(NULL, RISCV_ISA_EXT_smstateen) )
> >> +        csr_write(CSR_HSTATEEN0, vcpu->arch.hstateen0);
> >> +
> >> +    csr_write(CSR_VSSTATUS, vcpu->arch.vsstatus);
> >> +    csr_write(CSR_VSIE, vcpu->arch.vsie);
> > 
> > 
> >> +    csr_write(CSR_VSTVEC, vcpu->arch.vstvec);
> >> +    csr_write(CSR_VSSCRATCH, vcpu->arch.vsscratch);
> >> +    csr_write(CSR_VSCAUSE, vcpu->arch.vscause);
> >> +    csr_write(CSR_VSTVAL, vcpu->arch.vstval);
> >> +    csr_write(CSR_VSEPC, vcpu->arch.vsepc);
> >> +}
> >> +
> >> +static void ctxt_switch_from(struct vcpu *p)
> > Is it expected to have diverse names for the vcpu arg? Above it's vcpu,
> > here it's p (I assume it's for `previous` but I think the _from alone is
> > enough to understand) and below it's n. Shouldn't be better to keep the same name?
> 
> Context switch code is special, and using p and n can be warranted there.
> Everywhere else it should be v, nothing else (and I said so to Oleksii
> more than once before). Unless, of course, multiple vCPU-s come into play
> at the same time.
> 
Ok noted, thanks for clarifying that.

> Jan
> 
> 
> 




^ permalink raw reply	[flat|nested] 244+ messages in thread

* Re: [PATCH v2 14/39] xen/riscv: introduce vintc_ctxt_switch_{from,to}()
  2026-08-27 15:20 ` [PATCH v2 14/39] xen/riscv: introduce vintc_ctxt_switch_{from,to}() Oleksii Kurochko
@ 2026-09-04 11:25   ` Baptiste Le Duc
  2026-09-04 16:54     ` Oleksii Kurochko
  2026-09-10 14:54   ` Jan Beulich
  1 sibling, 1 reply; 244+ messages in thread
From: Baptiste Le Duc @ 2026-09-04 11:25 UTC (permalink / raw)
  To: Oleksii Kurochko
  Cc: xen-devel, Romain Caritey, Baptiste Le Duc, Zheng Zhang,
	Alistair Francis, Connor Davis, Andrew Cooper, Anthony PERARD,
	Michal Orzel, Jan Beulich, Julien Grall, Roger Pau Monné,
	Stefano Stabellini

> Virtual interrupt controller state must be preserved across vCPU context
> switches.
> 
> Introduce vintc_ctxt_switch_{from,to}() wrappers around new
> ctxt_switch_{from,to}() hooks in struct vintc_ops, and call them from the
> context switch path, so that this state can be saved/restored without
> knowing which vINTC variant a domain uses.
> 
> No vINTC variant implements the hooks yet: the vAPLIC implementation is
> added separately.
So this patch couldn't be applied alone as, at the time of this commit, you only set in vaplic.c:
    static const struct vintc_ops vintc_ops = {
        .vcpu_init = vcpu_imsic_init,
        .vcpu_deinit = vcpu_imsic_deinit,
    };

So ops->ctxt_switch_from(v) or ops->ctxt_switch_to(v) will try to
deference NULL pointer causing segfault. I don't know if it's matter but
worth to mention somewhere.

-- 
Baptiste Le Duc <baptiste.le-duc@vates.tech>


^ permalink raw reply	[flat|nested] 244+ messages in thread

* Re: [PATCH v2 15/39] xen/riscv: add IMSIC vCPU context switch handlers
  2026-08-27 15:20 ` [PATCH v2 15/39] xen/riscv: add IMSIC vCPU context switch handlers Oleksii Kurochko
@ 2026-09-04 11:33   ` Baptiste Le Duc
  2026-09-04 16:56     ` Oleksii Kurochko
  2026-09-10 14:57   ` Jan Beulich
  1 sibling, 1 reply; 244+ messages in thread
From: Baptiste Le Duc @ 2026-09-04 11:33 UTC (permalink / raw)
  To: Oleksii Kurochko
  Cc: xen-devel, Romain Caritey, Baptiste Le Duc, Zheng Zhang,
	Alistair Francis, Connor Davis, Andrew Cooper, Anthony PERARD,
	Michal Orzel, Jan Beulich, Julien Grall, Roger Pau Monné,
	Stefano Stabellini

> IMSIC state currently needs to track only which physical CPU owns a vCPU's
> IMSIC guest interrupt file, as the CPU id is part of the physical address
> the file is mapped at.
> 
> Add imsic_ctxt_switch_from() to record that CPU when a vCPU is switched
> out. A vCPU running on the s/w VS-file has no h/w file bound to a CPU, so
> there is nothing to record for it. The recorded value stays unused until
> vCPU migration support, which needs it to find the file to move away from,
> is added later.
> 
> imsic_ctxt_switch_to() has nothing to do: by the time a vCPU is switched
> in, VGEIN is already assigned to it and its guest interrupt file is already
> mapped. Work is only required once a vCPU can move to a different CPU,
> which means recalculating VGEIN and remapping the file; that is handled
> separately by the vCPU migration patches.
> 
> Install both as the ctxt_switch_{from,to} hooks of struct vintc_ops. MSI
> delivery is the only mode Xen supports ( aplic_init() panics on an APLIC
> without an "msi-parent" property, and a guest's domaincfg.DM reads back as
> a fixed one) so the vAPLIC state to save and restore is always the IMSIC
> one and no vAPLIC-level forwarder is needed. Being indirect call targets,
> both handlers get cf_check.
> 
> Co-developed-by: Romain Caritey <Romain.Caritey@microchip.com>
> Signed-off-by: Oleksii Kurochko <oleksii.kurochko@gmail.com>
>
> diff --git a/xen/arch/riscv/imsic.c b/xen/arch/riscv/imsic.c
> index ad0a220eda..3787f270d8 100644
> --- a/xen/arch/riscv/imsic.c
> +++ b/xen/arch/riscv/imsic.c
> @@ -20,6 +20,7 @@
>  #include <xen/init.h>
>  #include <xen/libfdt/libfdt.h>
>  #include <xen/macros.h>
> +#include <xen/rwlock.h>
>  #include <xen/sched.h>
>  #include <xen/smp.h>
>  #include <xen/spinlock.h>
> @@ -342,6 +343,28 @@ static int __init imsic_parse_node(const struct dt_device_node *node,
>      return 0;
>  }
>  
> +void cf_check imsic_ctxt_switch_from(struct vcpu *v)
> +{
> +    struct vimsic_state *imsic_state = v->arch.vimsic_state;
> +    unsigned long flags;
> +
> +    /*
> +     * A vCPU using the s/w IMSIC VS-file (guest_file_id == 0) has no h/w
> +     * VS-file bound to a physical CPU, so there is no location to record.
> +     */
> +    if ( !vcpu_guest_file_id(v) )
> +        return;
> +
> +    write_lock_irqsave(&imsic_state->vsfile_lock, flags);
> +    imsic_state->vsfile_cpu = v->processor;
> +    write_unlock_irqrestore(&imsic_state->vsfile_lock, flags);
> +}
> +
> +void cf_check imsic_ctxt_switch_to(struct vcpu *v)
> +{
> +    /* Nothing to do */
> +}
> +
>  int cf_check vcpu_imsic_init(struct vcpu *v)
>  {
>      struct vimsic_state *imsic_state;
> diff --git a/xen/arch/riscv/include/asm/imsic.h b/xen/arch/riscv/include/asm/imsic.h
> index 93f9e44c7d..73129c3c9e 100644
> --- a/xen/arch/riscv/include/asm/imsic.h
> +++ b/xen/arch/riscv/include/asm/imsic.h
> @@ -109,4 +109,7 @@ unsigned int vcpu_guest_file_id(const struct vcpu *v);
>  
>  int vimsic_make_domu_dt_node(struct kernel_info *kinfo, unsigned int *phandle);
>  
> +void imsic_ctxt_switch_from(struct vcpu *v);
> +void imsic_ctxt_switch_to(struct vcpu *v);
> +
>  #endif /* ASM_RISCV_IMSIC_H */
> diff --git a/xen/arch/riscv/vaplic.c b/xen/arch/riscv/vaplic.c
> index 8726f7203d..6c60fe2baf 100644
> --- a/xen/arch/riscv/vaplic.c
> +++ b/xen/arch/riscv/vaplic.c
> @@ -422,6 +422,13 @@ static const struct mmio_handler_ops vaplic_mmio_ops = {
>  static const struct vintc_ops vintc_ops = {
>      .vcpu_init = vcpu_imsic_init,
>      .vcpu_deinit = vcpu_imsic_deinit,
> +    /*
> +     * MSI delivery is the only supported mode: aplic_init() panics on an
> +     * APLIC without an "msi-parent", so the vAPLIC state to save and restore
> +     * is always the IMSIC one.
> +     */
> +    .ctxt_switch_from = imsic_ctxt_switch_from,
> +    .ctxt_switch_to = imsic_ctxt_switch_to,

Maybe this patch could be merged with previous one: patch 50fe9554e1c0 ("xen/riscv: introduce vintc_ctxt_switch_{from,to}()")

-- 
Baptiste Le Duc <baptiste.le-duc@vates.tech>


^ permalink raw reply	[flat|nested] 244+ messages in thread

* Re: [PATCH v2 09/39] xen/riscv: implement virtual APLIC MMIO emulation
  2026-09-02 11:51   ` Baptiste Le Duc
@ 2026-09-04 11:58     ` Oleksii Kurochko
  2026-09-04 12:03       ` Jan Beulich
  0 siblings, 1 reply; 244+ messages in thread
From: Oleksii Kurochko @ 2026-09-04 11:58 UTC (permalink / raw)
  To: Baptiste Le Duc
  Cc: xen-devel, Romain Caritey, Zheng Zhang, Alistair Francis,
	Connor Davis, Andrew Cooper, Anthony PERARD, Michal Orzel,
	Jan Beulich, Julien Grall, Roger Pau Monné,
	Stefano Stabellini



On 9/2/26 1:51 PM, Baptiste Le Duc wrote:
>>   
>> +/*
>> + * The arrangement of IMSIC interrupt files in MMIO space follows a topology
>> + * defined by the RISC-V AIA specification. An IMSIC group is a set of
>> + * interrupt files (e.g., in a cluster or socket) co-located in memory.
>> + *
>> + * The physical address of an outgoing MSI is calculated by bitwise ORing a
>> + * Base Physical Page Number (Base PPN) with the Group Index (g), the Hart
>> + * Index (h) and, for a supervisor-level interrupt domain, the Guest Index:
>> + *
>> + *   ( Base PPN | (g << (HHXS + 12)) | (h << LHXS) | guest ) << 12
> Nit: it should be Guest Index (according to the spec) instead of `guest`
> wording:
>      ( Base PPN | (g << (HHXS + 12)) | (h << LHXS) | Guest Index ) << 12

Applied.

>> + *
>> + * where Base PPN, HHXS, LHXS, HHXW and LHXW come from the {m,s}msiaddrcfg[h]
>> + * registers of the interrupt domain that sends the MSI:
>> + *
>> + * XLEN-1       HHXS+24          LHXS+12          12          0
>> + * |            |                |                |           |
>> + * ------------------------------------------------------------
>> + * |xxxx|   g   |xxxxxxxx|   h   |xxxx|Guest Index|     0     |
>> + * ------------------------------------------------------------
>> + *
>> + * - g: group number.
>> + * - h: hart number relative to the group.
>> + * - xxxx: remaining Base PPN bits; each gap may be zero-width.
>> + * - Guest Index: selects one of the 4 KiB pages right above the hart's own
>> + *   supervisor-level file, i.e. it starts at bit 12; LHXS must therefore be
>> + *   at least as large as the number of guest index bits.
>> + * - Bits 11:0: always zero because IMSIC files are 4 KiB page-aligned.
>> + *
>> + * For wired interrupts in MSI delivery mode (domaincfg.DM = 1) the APLIC
>> + * builds that address itself from the "Hart Index" field (bits 31:18) of the
>> + * corresponding target[i] register. That field holds a hart index *number*,
>> + * in which both indices are packed adjacently:
>> + *
>> + * 13          lhxw+hhxw   lhxw       0
>> + * |           |           |          |
>> + * ------------------------------------
>> + * |     0     |Group Index|Hart Index|
>> + * ------------------------------------
>> + *
>> + * - lhxw (Low Hart Index Width): the number of bits used for the hart number
>> + *   within a group.
>> + * - hhxw (High Hart Index Width): the number of bits used for the group
>> + *   number; the remaining bits of the field must be zero.
>>
> I think it's not very clear that the schema represents the "Hart Index" field
> i.e. target[i] bits 31:18. Moreover, the schema like that is wrong as it is
> not Group Index or Hart Index but `g` and `h`.
> 
> I'd suggest something like this:
> 
>      * For wired interrupts in MSI delivery mode (domaincfg.DM = 1), the APLIC
>      * computes the MSI target address itself from the "Hart Index" field
>      * (bits 31:18) of the corresponding target[i] register. This 14-bit field
>      * holds both g and h:
>      *
>      * 13          lhxw+hhxw   lhxw       0
>      * |           |           |          |
>      * ------------------------------------
>      * |     0     |     g     |    h     |
>      * ------------------------------------
>      *
>      * - lhxw (Low Hart Index Width): the number of bits used for the hart number
>      *   within a group.
>      * - hhxw (High Hart Index Width): the number of bits used for the group
>      *   number; the remaining bits of the field must be zero.

Applied.

> 
>> + *
>> + * The Guest Index isn't a part of it: for a supervisor-level interrupt domain
>> + * it has its own field (bits 17:12) in target[i].
>> + *
>> + * Because there are "xxxx" gaps (Base PPN bits) between the indices in the
>> + * physical address (depending on HHXS and LHXS), software must extract the
>> + * group and hart components separately and pack them into the APLIC-defined
>> + * Hart Index format to ensure correct MSI targeting.
>> + */
>> +static unsigned long aplic_hart_field(unsigned int cpu)
> I should have renamed this to aplic_hart_index() as it's formerly what
> the function returns.

Applied.

>> +{
>> +    const struct imsic_config *imsic = imsic_get_config();
>> +    const struct imsic_msi *msi = &imsic->msi[cpu];
> Nit: this could be const ...

Sorry, I am not understand what do you expect from me to do with 'const' 
here. At the moment we don't chnage anything in this function connected 
to msi variable, just a reading.

>> +    /* Low Hart Index Shift */
>> +    unsigned int lhxs = imsic->guest_index_bits;
> It seems incoherent with the diagram above as there is some xxxx
> between Guest Index bits and lhxs + 12. Therefore, it is not that obvious
> that lhxs is equal to guest_index_bits.
>> +    /* Low Hart Index Width */
>> +    unsigned int lhxw = imsic->hart_index_bits;
>> +    /* High Hart Index Width */
>> +    unsigned int hhxw = imsic->group_index_bits;
>> +    /* High Hart Index Shift */
>> +    unsigned int hhxs =
>> +        imsic->group_index_shift - APLIC_xMSICFGADDR_PPN_SHIFT * 2;
> ...
>> +    /*
>> +     * msi->base_addr is the base of the MMIO regset this CPU's interrupt
> So if I understood correctly, msi->base_addr corresponds to the group
> terminology? Is it always the case?

I think - yes. This value is taken from DTS which and is used to 
describe IMSIC group.

>> +     * files live in, and one regset can cover several harts; msi->offset
>> +     * selects this CPU's block inside it. The hart index bits are part of
>> +     * that offset, so both indexes have to be derived from the full address.
>> +     */
>> +    paddr_t target_addr = msi->base_addr + msi->offset;
>> +    unsigned long tppn = target_addr >> APLIC_xMSICFGADDR_PPN_SHIFT;
>> +    unsigned long g =
>> +        (tppn >> APLIC_xMSICFGADDR_PPN_HHX_SHIFT(hhxs)) &
>> +        APLIC_xMSICFGADDR_PPN_HHX_MASK(hhxw);
>> +    unsigned long h =
>> +        (tppn >> APLIC_xMSICFGADDR_PPN_LHX_SHIFT(lhxs)) &
>> +        APLIC_xMSICFGADDR_PPN_LHX_MASK(lhxw);
>> +
>> +    return (g << lhxw) | h;
>> +}
>> +
>> +uint32_t aplic_msi_target_gen(const struct vcpu *target_vcpu,
>> +                              uint32_t base_val)
>> +{
>> +    unsigned int guest_id = vcpu_guest_file_id(target_vcpu);
>> +    unsigned long hart_field = aplic_hart_field(target_vcpu->processor);
> Nit: could be const

Techically I agree. Then for guest_id it should const too, right?

But it seems like Xen in such cases don't use const. Have you found a 
rule that we have to use const in such cases?

I don't mind to put const here but then it would be nice if someone will 
tell me some kind of rule...

> Should be hart_index too, according to previous comment.

Applied.
>> +
>> +    base_val &= APLIC_TARGET_EIID;
>> +    base_val |= MASK_INSR(guest_id, APLIC_TARGET_GUEST_IDX);
>> +    base_val |= MASK_INSR(hart_field, APLIC_TARGET_HART_IDX);
>> +
>> +    return base_val;
>> +}
>> +
>> +uint32_t aplic_hw_read_reg(unsigned int offset)
>> +{
>> +    unsigned long flags;
>> +    uint32_t val;
>> +
>> +    ASSERT((offset < aplic.size) && IS_ALIGNED(offset, sizeof(uint32_t)));
>> +
>> +    spin_lock_irqsave(&aplic.lock, flags);
>> +    val = readl((volatile void __iomem *)aplic.regs + offset);
>> +    spin_unlock_irqrestore(&aplic.lock, flags);
>> +
>> +    return val;
>> +}
>> +
>> +void aplic_hw_write_reg(unsigned int offset, uint32_t value)
>> +{
>> +    unsigned long flags;
>> +
>> +    ASSERT((offset < aplic.size) && IS_ALIGNED(offset, sizeof(uint32_t)));
>> +
>> +    spin_lock_irqsave(&aplic.lock, flags);
>> +    writel(value, (volatile void __iomem *)aplic.regs + offset);
>> +    spin_unlock_irqrestore(&aplic.lock, flags);
>> +}
>> +
>>   static void __init aplic_init_hw_interrupts(void)
>>   {
>>       unsigned int i;
>> @@ -53,9 +173,9 @@ static void __init aplic_init_hw_interrupts(void)
>>           /*
>>            * Low bits of target register contains Interrupt Priority bits which
>>            * can't be zero according to AIA spec.
>> -         * Thereby they are initialized to APLIC_DEFAULT_PRIORITY.
>> +         * Thereby they are initialized to APLIC_TARGET_IPRIO_DEFAULT.
>>            */
>> -        writel(APLIC_DEFAULT_PRIORITY, &aplic.regs->target[i]);
>> +        writel(APLIC_TARGET_IPRIO_DEFAULT, &aplic.regs->target[i]);
>>       }
>>   
>>       writel(APLIC_DOMAINCFG_IE | APLIC_DOMAINCFG_DM, &aplic.regs->domaincfg);
>> diff --git a/xen/arch/riscv/include/asm/aplic.h b/xen/arch/riscv/include/asm/aplic.h
>> index a2af55d54f..babba38607 100644
>> --- a/xen/arch/riscv/include/asm/aplic.h
>> +++ b/xen/arch/riscv/include/asm/aplic.h
>> @@ -39,6 +39,13 @@
>>   #define  APLIC_DOMAINCFG_IE             BIT(8, U)
>>   #define  APLIC_DOMAINCFG_DM             BIT(2, U)
>>   #define  APLIC_DOMAINCFG_BE             BIT(0, U)
>> +/*
>> + * The bits a write may change. Everything else, including the read-only zero
>> + * bit 7 and the reserved bits, has to read back as zero, and BE is WARL and
>> + * hardwired to 0 as Xen is little-endian only.
>> + */
>> +#define  APLIC_DOMAINCFG_WMASK          (APLIC_DOMAINCFG_IE | \
>> +                                         APLIC_DOMAINCFG_DM)
>>   
>>   #define APLIC_SOURCECFG_BASE            0x0004
>>   #define APLIC_SOURCECFG_LAST            0x0ffc
>> @@ -89,6 +96,9 @@
>>   #define  APLIC_TARGET_GUEST_IDX         GENMASK(17, 12)
>>   /* Bit 11 is reserved and reads as zero */
>>   #define  APLIC_TARGET_EIID              GENMASK(10, 0)
>> +/* If target is in DM mode */
> I think this comment is not clear; I expect, by reading it, to have
> domaincfg.DM = 1 which is MSI mode, but I think you were talking about
> direct delivery mode, right? If so, I would change this comment to
> 
> /* If target is in direct delivery mode (domaincfg.DM = 0) */

Yes, my comment is incorrect, it should be yours. Applied.

>> +#define  APLIC_TARGET_IPRIO             GENMASK(7, 0)
>> +#define   APLIC_TARGET_IPRIO_DEFAULT    1U
>>   
>>   #define APLIC_IDC_SIZE                  32
>>   
>> @@ -98,6 +108,27 @@
>>   #define APLIC_SIZE(nr_cpus) \
>>       (APLIC_MIN_SIZE + APLIC_SIZE_ALIGN(APLIC_IDC_SIZE * (nr_cpus)))
>>   
>> +/*
>> + * Using setip is fine here, as all SET* and CLR* register groups consist of 32
>> + * registers and therefore have identical sizes.
>> + *
>> + * Lowest 2 bits are always zero for SET* and CLR* registers.
>> + */
>> +#define APLIC_SETCLR_OFFSET_MASK \
>> +    (sizeof_field(struct aplic_regs, setip) - sizeof(uint32_t))
>> +
>> +#define APLIC_xMSICFGADDR_PPN_SHIFT IMSIC_MMIO_PAGE_SHIFT
>> +
>> +#define APLIC_xMSICFGADDR_PPN_HHX_MASK(hhxw) \
>> +    (BIT(hhxw, UL) - 1)
>> +#define APLIC_xMSICFGADDR_PPN_HHX_SHIFT(hhxs) \
>> +    ((hhxs) + APLIC_xMSICFGADDR_PPN_SHIFT)
>> +
>> +#define APLIC_xMSICFGADDR_PPN_LHX_MASK(lhxw) \
>> +    (BIT(lhxw, UL) - 1)
>> +#define APLIC_xMSICFGADDR_PPN_LHX_SHIFT(lhxs) \
>> +    (lhxs)
>> +
>>   struct aplic_regs {
>>       uint32_t domaincfg;         /* 0x0000 */
>>       uint32_t sourcecfg[1023];   /* 0x0004 */
>> @@ -141,4 +172,7 @@ struct aplic_regs {
>>       uint32_t target[1023];      /* 0x3004 */
>>   };
>>   
>> +uint32_t aplic_hw_read_reg(unsigned int offset);
>> +void aplic_hw_write_reg(unsigned int offset, uint32_t value);
>> +
>>   #endif /* ASM_RISCV_APLIC_H */
>> diff --git a/xen/arch/riscv/include/asm/imsic.h b/xen/arch/riscv/include/asm/imsic.h
>> index 2425430ed1..93f9e44c7d 100644
>> --- a/xen/arch/riscv/include/asm/imsic.h
>> +++ b/xen/arch/riscv/include/asm/imsic.h
>> @@ -40,6 +40,19 @@ struct imsic_config {
>>       /* Base address */
>>       paddr_t base_addr;
>>   
>> +    /*
>> +     * MSI Target Address Scheme
>> +     *
>> +     * XLEN-1       HHXS+24          LHXS+12          12          0
>> +     * |            |                |                |           |
>> +     * ------------------------------------------------------------
>> +     * |xxxx|   g   |xxxxxxxx|   h   |xxxx|Guest Index|     0     |
>> +     * ------------------------------------------------------------
>> +     * - g: group number.
>> +     * - h: hart number relative to the group.
>> +     * - xxxx: remaining Base PPN bits; each gap may be zero-width.
>> +     */
>> +
> Is this really needed as you already explain this above in aplic.c?
> Please choose one place between the two if not.

No, I don't think so. For me, it is also enough to have in one place. I 
will drop it here and keep only in aplic.c.

[...]
>> +
>> +static bool vaplic_emulate_store(const struct vcpu *curr, paddr_t addr,
>> +                                 uint32_t value)
>> +{
>> +    const struct domain *currd = curr->domain;
>> +    unsigned int offset = addr & APLIC_CTRL_REGION_OFFSET_MASK;
>> +
>> +    ASSERT(curr == current);
>> +
>> +    switch ( offset )
>> +    {
>> +    case APLIC_SETIP_BASE ... APLIC_SETIP_LAST:
>> +    case APLIC_CLRIP_BASE ... APLIC_CLRIP_LAST:
>> +    case APLIC_SETIE_BASE ... APLIC_SETIE_LAST:
>> +    case APLIC_CLRIE_BASE ... APLIC_CLRIE_LAST:
>> +    {
>> +        unsigned int word_idx =
>> +            regoffset_to_word_idx(offset & APLIC_SETCLR_OFFSET_MASK);
>> +
>> +        value &= generate_auth_mask(currd, word_idx);
>> +
>> +        break;
>> +    }
>> +
>> +    case APLIC_SOURCECFG_BASE ... APLIC_SOURCECFG_LAST:
>> +        if ( value & APLIC_SOURCECFG_D )
>> +        {
>> +            dprintk(XENLOG_ERR, "APLIC_SOURCECFG_D isn't supported\n");
>> +
>> +            goto fail;
>> +        }
>> +
>> +        /*
>> +         * As sourcecfg register starts from 1:
>> +         *   0x0000 domaincfg
>> +         *   0x0004 sourcecfg[1]
>> +         *   0x0008 sourcecfg[2]
>> +         *    ...
>> +         *   0x0FFC sourcecfg[1023]
>> +         * It is necessary to calculate an interrupt number by subtracting
>> +         * APLIC_DOMAINCFG instead of APLIC_SOURCECFG_BASE.
>> +         */
>> +        if ( !AUTH_IRQ_BIT(currd,
>> +                           regoffset_to_word_idx(offset - APLIC_DOMAINCFG)) )
>> +            /* Interrupt not enabled, ignore it */
>> +            return true;
>> +
>> +        if ( value > APLIC_SOURCECFG_SM_LEVEL_LOW )
>> +        {
>> +            gdprintk(XENLOG_ERR,
>> +                     "value(%#x) is incorrect for sourcecfg register\n",
>> +                     value);
>> +
>> +            return true;
>> +        }
>> +
>> +        break;
>> +
>> +    case APLIC_TARGET_BASE ... APLIC_TARGET_LAST:
>> +    {
>> +        struct vaplic *vaplic = to_vaplic(currd);
>> +        struct vcpu *target_vcpu;
>> +        unsigned int guest_hart_idx = MASK_EXTR(value, APLIC_TARGET_HART_IDX);
>> +        /*
>> +         * Look at vaplic_emulate_load() for explanation why APLIC_GENMSI is
>> +         * subtracted.
>> +         */
>> +        unsigned int srcn = regoffset_to_word_idx(offset - APLIC_GENMSI);
>> +
>> +        if ( !AUTH_IRQ_BIT(currd, srcn) )
>> +            /* Interrupt not enabled, ignore it */
>> +            return true;
>> +
>> +        target_vcpu = domain_vcpu(currd, guest_hart_idx);
>> +
>> +        if ( !target_vcpu )
>> +        {
>> +            dprintk(XENLOG_ERR, "Invalid vCPU id in target register\n");
>> +
>> +            /* Ignore such writings */
>> +            return true;
>> +        }
>> +
>> +        if ( vaplic->regs.domaincfg & APLIC_DOMAINCFG_DM )
>> +        {
>> +            /*
>> +             * A non-zero guest index asks for delivery to an interrupt file of
>> +             * nested guest. The vIMSIC node has no riscv,guest-index-bits
>> +             * property, so a guest is told its harts have no guest interrupt
>> +             * files and the field is read-only zero for them. The write isn't
>> +             * rejected (that would throw away a valid hart index and EIID);
>> +             * instead the field is dropped, which is also what
>> +             * aplic_msi_target_gen() does with it when programming the h/w.
>> +             */
>> +            if ( MASK_EXTR(value, APLIC_TARGET_GUEST_IDX) )
>> +            {
>> +                printk_once(XENLOG_WARNING
>> +                            "%pd: vAPLIC target guest index != 0 is unsupported\n",
>> +                            currd);
>> +
>> +                /* Ignore such writes ... */
>> +                return true;
>> +            }
>>
> Comment above this says "The write isn't rejected ... instead the field
> is dropped, which is also what aplic_msi_target_gen() does with it." But
> the code doesn't follow it as it returns true immediately here before
> the write occurred and without zeroing the guest index field.
> 

You're right that the comment doesn't match the code, but the fix is in 
the comment rather than in the code. vaplic->regs.target[] comes from 
xvzalloc_array(), so the guest index field starts as zero, and every 
write carrying a non-zero guest index is rejected here, the field can 
therefore never become non-zero and there is nothing to mask out. 
Rejecting the whole write is deliberate: a non-zero guest index is 
illegal for a guest whose vIMSIC advertises no guest interrupt files.

The second half of the comment was wrong too: aplic_msi_target_gen() 
doesn't drop the field, it overwrites it with vcpu_guest_file_id() of 
the target vCPU, which is non-zero when that vCPU owns a h/w VS-file. 
I'll reword the comment in v3 accordingly:

/*
* A non-zero guest index asks for delivery to an interrupt file
* of a nested guest. The vIMSIC node has no riscv,guest-index-bits
* property, so a guest is told its harts have no guest interrupt
* files and the field reads as zero for them. Such a write is
* illegal and is therefore ignored as a whole: the stored copy
* keeps the zero it was allocated with, so the field never needs
* to be masked out here.
* What ends up in the h/w register is Xen's own value anyway:
* aplic_msi_target_gen() overwrites the field with
* vcpu_guest_file_id() of the target vCPU.
*/

Are you okay with that?

Thanks!

~ Oleksii



^ permalink raw reply	[flat|nested] 244+ messages in thread

* Re: [PATCH v2 09/39] xen/riscv: implement virtual APLIC MMIO emulation
  2026-09-04 11:58     ` Oleksii Kurochko
@ 2026-09-04 12:03       ` Jan Beulich
  0 siblings, 0 replies; 244+ messages in thread
From: Jan Beulich @ 2026-09-04 12:03 UTC (permalink / raw)
  To: Oleksii Kurochko, Baptiste Le Duc
  Cc: xen-devel, Romain Caritey, Zheng Zhang, Alistair Francis,
	Connor Davis, Andrew Cooper, Anthony PERARD, Michal Orzel,
	Julien Grall, Roger Pau Monné, Stefano Stabellini

On 04.09.2026 13:58, Oleksii Kurochko wrote:
> On 9/2/26 1:51 PM, Baptiste Le Duc wrote:
>>> +uint32_t aplic_msi_target_gen(const struct vcpu *target_vcpu,
>>> +                              uint32_t base_val)
>>> +{
>>> +    unsigned int guest_id = vcpu_guest_file_id(target_vcpu);
>>> +    unsigned long hart_field = aplic_hart_field(target_vcpu->processor);
>> Nit: could be const
> 
> Techically I agree. Then for guest_id it should const too, right?
> 
> But it seems like Xen in such cases don't use const. Have you found a 
> rule that we have to use const in such cases?
> 
> I don't mind to put const here but then it would be nice if someone will 
> tell me some kind of rule...

We want const on pointer targets whenever possible. We don't normally want
const on plain variables, unless it needs emphasizing that a variable is to
change value (which should be very rare).

Jan


^ permalink raw reply	[flat|nested] 244+ messages in thread

* Re: [PATCH v2 09/39] xen/riscv: implement virtual APLIC MMIO emulation
  2026-09-02 12:31   ` Baptiste Le Duc
@ 2026-09-04 14:02     ` Oleksii Kurochko
  0 siblings, 0 replies; 244+ messages in thread
From: Oleksii Kurochko @ 2026-09-04 14:02 UTC (permalink / raw)
  To: Baptiste Le Duc
  Cc: xen-devel, Romain Caritey, Zheng Zhang, Alistair Francis,
	Connor Davis, Andrew Cooper, Anthony PERARD, Michal Orzel,
	Jan Beulich, Julien Grall, Roger Pau Monné,
	Stefano Stabellini



On 9/2/26 2:31 PM, Baptiste Le Duc wrote:
>> +
>> +static bool vaplic_emulate_store(const struct vcpu *curr, paddr_t addr,
>> +                                 uint32_t value)
>> +{
>> +    const struct domain *currd = curr->domain;
>> +    unsigned int offset = addr & APLIC_CTRL_REGION_OFFSET_MASK;
>> +
>> +    ASSERT(curr == current);
>> +
>> +    switch ( offset )
>> +    {
>> +    case APLIC_SETIP_BASE ... APLIC_SETIP_LAST:
>> +    case APLIC_CLRIP_BASE ... APLIC_CLRIP_LAST:
>> +    case APLIC_SETIE_BASE ... APLIC_SETIE_LAST:
>> +    case APLIC_CLRIE_BASE ... APLIC_CLRIE_LAST:
>> +    {
>> +        unsigned int word_idx =
>> +            regoffset_to_word_idx(offset & APLIC_SETCLR_OFFSET_MASK);
>> +
>> +        value &= generate_auth_mask(currd, word_idx);
>> +
>> +        break;
>> +    }
>> +
>> +    case APLIC_SOURCECFG_BASE ... APLIC_SOURCECFG_LAST:
>> +        if ( value & APLIC_SOURCECFG_D )
>> +        {
>> +            dprintk(XENLOG_ERR, "APLIC_SOURCECFG_D isn't supported\n");
>> +
>> +            goto fail;
>> +        }
>> +
>> +        /*
>> +         * As sourcecfg register starts from 1:
>> +         *   0x0000 domaincfg
>> +         *   0x0004 sourcecfg[1]
>> +         *   0x0008 sourcecfg[2]
>> +         *    ...
>> +         *   0x0FFC sourcecfg[1023]
>> +         * It is necessary to calculate an interrupt number by subtracting
>> +         * APLIC_DOMAINCFG instead of APLIC_SOURCECFG_BASE.
>> +         */
>> +        if ( !AUTH_IRQ_BIT(currd,
>> +                           regoffset_to_word_idx(offset - APLIC_DOMAINCFG)) )
>> +            /* Interrupt not enabled, ignore it */
>> +            return true;
>> +
>> +        if ( value > APLIC_SOURCECFG_SM_LEVEL_LOW )
> This compares the whole value against 7, not the extracted SM field
> (bits [2:0]). A perfectly legal SM=0 write with any bit set in the
> reserved [9:3] range (e.g. value=8) gets rejected here even though the
> actual field is fine Should be MASK_EXTR(value, APLIC_SOURCECFG_SM) >
> APLIC_SOURCECFG_SM_LEVEL_LOW.

Agree, if() written in this way is incorrect in the way what is going 
before this if().

I think that we don't need it at all and what we want instead is 
ignoring write to others bits then D (bit10) and SM(bits 2:0) as they 
are reserved and read as zeros.

         /*
          * Only D (bit 10) and SM (bits 2:0) are implemented, the rest 
of the
          * bits are reserved and read as zero, so ignore what a guest 
writes
          * to them.
          */
         value &= (APLIC_SOURCECFG_D | APLIC_SOURCECFG_SM);

Probably it make sense to introduce and use it above:

/*
  * All other bits of sourcecfg[] are reserved and read as zero, so drop 
them on a write.
  */
#define  APLIC_SOURCECFG_WMASK          (APLIC_SOURCECFG_D | 
APLIC_SOURCECFG_SM)

And ...

> 
> SM is WARL. If SM invalid but other fields valid, shouldn't reject whole
> write. Instead override value.SM with current valid SM in this branch,
> so other valid fields still get written.

... considering that SM is WARL it means that technically any value 
could be written to this field but read of this register should be 
return always something valid. So considering that in the case of 
SOURCECFG we don't have a shadow copy for vAPLIC and use just real h/w 
we could ignore fully the value it is trying to write to SM field as 
even it is something illegal h/w will choose something legal instead.

If one day we will need a copy of SOURCECFG for vAPLIC we will need to 
do something like this to emulate behavior of real SOURCECFG:

         /*
          * SM is a WARL field. If the guest wrote reserved values (2 or 3),
          * optionally coerce them to a supported default (e.g., 
Inactive/0).
          */
         if ( value == 2 || value == 3 )
             value = APLIC_SOURCECFG_SM_INACTIVE;

For now we can do nothing. I can put TODO:

         /*
          * SM is WARL, so the reserved values 0x2 and 0x3 need no handling
          * here: vAPLIC keeps no shadow copy of sourcecfg[], the value is
          * written straight to the h/w register and is read back from 
it, so
          * it is the h/w which substitutes a legal value for an illegal 
one.
          *
          * TODO: when vAPLIC starts to shadow sourcecfg[], the WARL 
behaviour
          * will have to be emulated here instead, e.g.:
          *   if ( value == 0x2 || value == 0x3 )
          *       value = APLIC_SOURCECFG_SM_INACTIVE;
          */

Note that here it is okay not to use MASK_EXTR as we have always D=0 so 
sourcecfg[i] value is basically SM field (as other bits are reserved and 
are read only)

So my final suggestion is:

      case APLIC_SOURCECFG_BASE ... APLIC_SOURCECFG_LAST:
+        /*
+         * Only D (bit 10) and SM (bits 2:0) are implemented, the rest 
of the
+         * bits are reserved and read as zero, so ignore what a guest 
writes
+         * to them.
+         */
+        value &= APLIC_SOURCECFG_WMASK;
+
          if ( value & APLIC_SOURCECFG_D )
          {
              dprintk(XENLOG_ERR, "APLIC_SOURCECFG_D isn't supported\n");
@@ -169,6 +176,18 @@ static bool vaplic_emulate_store(const struct vcpu 
*curr, paddr_t addr,
              goto fail;
          }

+        /*
+         * SM is WARL, so the reserved values 0x2 and 0x3 need no handling
+         * here: vAPLIC keeps no shadow copy of sourcecfg[], the value is
+         * written straight to the h/w register and is read back from 
it, so
+         * it is the h/w which substitutes a legal value for an illegal 
one.
+         *
+         * TODO: when vAPLIC starts to shadow sourcecfg[], the WARL 
behaviour
+         * will have to be emulated here instead, e.g.:
+         *   if ( value == 0x2 || value == 0x3 )
+         *       value = APLIC_SOURCECFG_SM_INACTIVE;
+         */
+
          /*
           * As sourcecfg register starts from 1:
           *   0x0000 domaincfg
@@ -184,15 +203,6 @@ static bool vaplic_emulate_store(const struct vcpu 
*curr, paddr_t addr,
              /* Interrupt not enabled, ignore it */
              return true;

-        if ( value > APLIC_SOURCECFG_SM_LEVEL_LOW )
-        {
-            gdprintk(XENLOG_ERR,
-                     "value(%#x) is incorrect for sourcecfg register\n",
-                     value);
-
-            return true;
-        }
-
          break;

Does it make sense? Or I still missing something.




>> +        {
>> +            gdprintk(XENLOG_ERR,
>> +                     "value(%#x) is incorrect for sourcecfg register\n",
>> +                     value);
>> +
>> +            return true;
>> +        }
>> +
>> +        break;
>> +
>> +    case APLIC_TARGET_BASE ... APLIC_TARGET_LAST:
>> +    {
>> +        struct vaplic *vaplic = to_vaplic(currd);
>> +        struct vcpu *target_vcpu;
>> +        unsigned int guest_hart_idx = MASK_EXTR(value, APLIC_TARGET_HART_IDX);
>> +        /*
>> +         * Look at vaplic_emulate_load() for explanation why APLIC_GENMSI is
>> +         * subtracted.
>> +         */
>> +        unsigned int srcn = regoffset_to_word_idx(offset - APLIC_GENMSI);
>> +
>> +        if ( !AUTH_IRQ_BIT(currd, srcn) )
>> +            /* Interrupt not enabled, ignore it */
>> +            return true;
>> +
>> +        target_vcpu = domain_vcpu(currd, guest_hart_idx);
>> +
>> +        if ( !target_vcpu )
>> +        {
>> +            dprintk(XENLOG_ERR, "Invalid vCPU id in target register\n");
>> +
>> +            /* Ignore such writings */
>> +            return true;
>> +        }
>> +
>> +        if ( vaplic->regs.domaincfg & APLIC_DOMAINCFG_DM )
>> +        {
>> +            /*
>> +             * A non-zero guest index asks for delivery to an interrupt file of
>> +             * nested guest. The vIMSIC node has no riscv,guest-index-bits
>> +             * property, so a guest is told its harts have no guest interrupt
>> +             * files and the field is read-only zero for them. The write isn't
>> +             * rejected (that would throw away a valid hart index and EIID);
>> +             * instead the field is dropped, which is also what
>> +             * aplic_msi_target_gen() does with it when programming the h/w.
>> +             */
>> +            if ( MASK_EXTR(value, APLIC_TARGET_GUEST_IDX) )
>> +            {
>> +                printk_once(XENLOG_WARNING
>> +                            "%pd: vAPLIC target guest index != 0 is unsupported\n",
>> +                            currd);
>> +
>> +                /* Ignore such writes ... */
>> +                return true;
>> +            }
> Comment above this says "The write isn't rejected ... instead the field
> is dropped, which is also what aplic_msi_target_gen() does with it." But
> the code doesn't follow it as it returns true immediately here before
> the write occurred and without zeroing the guest index field.

It looks like the same question in the other thread [1] at the end.

If you don't mind lets continue discussion there. I responded there.

[1] 
https://lore.kernel.org/xen-devel/cover.1787838835.git.oleksii.kurochko@gmail.com/T/#m0de75013bd31481a2f6abd6f36ccccb8ede87a20

>> +
>> +            write_atomic(&vaplic->regs.target[srcn], value);
>> +
>> +            value = aplic_msi_target_gen(target_vcpu, value);
>> +        }
>> +        else
>> +        {
>> +            /*
>> +             * IPRIO is WARL and zero isn't a legal value for it, so normalize
>> +             * it once: the guest then reads back exactly what it gets.
>> +             */
>> +            unsigned int iprio = MASK_EXTR(value, APLIC_TARGET_IPRIO) ?:
>> +                                 APLIC_TARGET_IPRIO_DEFAULT;
>> +            unsigned long h = cpuid_to_hartid(guest_hart_idx);
>> +
>> +            value = MASK_INSR(guest_hart_idx, APLIC_TARGET_HART_IDX) |
>> +                    MASK_INSR(iprio, APLIC_TARGET_IPRIO);
>> +
>> +            write_atomic(&vaplic->regs.target[srcn], value);
>> +
>> +            value = MASK_INSR(h, APLIC_TARGET_HART_IDX) |
>> +                    MASK_INSR(iprio, APLIC_TARGET_IPRIO);
>> +        }
>> +
>> +        break;
>> +    }
>> +
>> +    case APLIC_SETIPNUM:
>> +    case APLIC_SETIPNUM_LE:
>> +    case APLIC_CLRIPNUM:
>> +    case APLIC_SETIENUM:
>> +    case APLIC_CLRIENUM:
>> +        if ( !value || !AUTH_IRQ_BIT(currd, value) )
>> +            return true;
>> +
>> +        break;
>> +
>> +    case APLIC_DOMAINCFG:
>> +    {
>> +        struct vaplic *vaplic = to_vaplic(currd);
>> +
>> +        vaplic->regs.domaincfg = APLIC_DOMAINCFG_RO |
>> +                                 (value & APLIC_DOMAINCFG_WMASK);
>> +
> APLIC_DOMAINCFG_WMASK includes APLIC_DOMAINCFG_DM, so
> the guest can clear DM through this write. But aplic.c:
> aplic_init_hw_interrupts() sets the real hardware APLIC's domaincfg to IE|DM
> exactly once and never touches it again. Is this expected?

Yes, it is expected as we are supporting now only APLIC+IMSIC in Xen and 
it is the reason why we here started to provided a shadow copy of 
domaincfg register for vAPLIC instead of using real APLIC domaincfg 
register.

> 
> Moreover, I saw that d8fbe0bbc7's commit message claims: "a guest's
> domaincfg.DM reads back as a fixed one, so is there situation where we would
> allow direct delivery mode? If not, the else branch should be dropped.
> 

At the moment, we started with a support only when we are working in MSI 
mode but commonly it is possible that IMSIC will be absent and we don't 
have any other choice as started to support delivery mode.

Thanks for review!

~ Oleksii




^ permalink raw reply	[flat|nested] 244+ messages in thread

* Re: [PATCH v2 10/39] xen/riscv: build the target hart index via aplic_hart_field()
  2026-09-04  8:26   ` Baptiste Le Duc
@ 2026-09-04 14:28     ` Oleksii Kurochko
  2026-09-09 14:51     ` Jan Beulich
  1 sibling, 0 replies; 244+ messages in thread
From: Oleksii Kurochko @ 2026-09-04 14:28 UTC (permalink / raw)
  To: Baptiste Le Duc
  Cc: xen-devel, Romain Caritey, Zheng Zhang, Alistair Francis,
	Connor Davis, Andrew Cooper, Anthony PERARD, Michal Orzel,
	Jan Beulich, Julien Grall, Roger Pau Monné,
	Stefano Stabellini



On 9/4/26 10:26 AM, Baptiste Le Duc wrote:
> On Thu, 27 Aug 2026 17:20:54 +0200, Oleksii Kurochko <oleksii.kurochko@gmail.com> wrote:
>> aplic_set_irq_affinity() open-coded the packing of the group and hart
>> indices into the target register, and got two things wrong along the
>> way:
>>
>>   - imsic_config.msi[] is indexed by logical CPU id, but the index was
>>     run through cpuid_to_hartid() first. On any platform where the two
>>     spaces differ this picks another CPU's interrupt file, or reads past
>>     the array;
>>
>> [...]
> 
> Reviewed-by: Baptiste Le Duc <baptiste.le-duc@vates.tech>
> 

Thanks.

Considering your suggestion regarding renaming of the function 
aplic_hart_field() in the prev. patches. I will also do here:

@ -344 +344 @@ static void cf_check aplic_set_irq_affinity(struct 
irq_desc *desc, const cpumask
-    value = MASK_INSR(aplic_hart_field(cpu), APLIC_TARGET_HART_IDX) |
+    value = MASK_INSR(aplic_hart_index(cpu), APLIC_TARGET_HART_IDX) |

+

Update the functions name in the commit message.

~ Oleksii



^ permalink raw reply	[flat|nested] 244+ messages in thread

* Re: [PATCH v2 12/39] xen/riscv: implement vCPU context switching
  2026-09-04  8:26   ` Baptiste Le Duc
  2026-09-04  8:33     ` Jan Beulich
@ 2026-09-04 14:55     ` Oleksii Kurochko
  2026-09-07  8:17       ` Jan Beulich
  1 sibling, 1 reply; 244+ messages in thread
From: Oleksii Kurochko @ 2026-09-04 14:55 UTC (permalink / raw)
  To: Baptiste Le Duc
  Cc: xen-devel, Romain Caritey, Zheng Zhang, Alistair Francis,
	Connor Davis, Andrew Cooper, Anthony PERARD, Michal Orzel,
	Jan Beulich, Julien Grall, Roger Pau Monné,
	Stefano Stabellini



On 9/4/26 10:26 AM, Baptiste Le Duc wrote:
> As I understand it, a generation wrap doesn't retire a single VMID, it
> resets next_vmid to 1, which makes every VMID in 1..max_vmid reusable
> again in the new generation. We do a full (local) flush at that point to
> avoid two different vCPUs ending up with the same VMID valid at once,
> across generations.
> 
> If this is correct, doing a full flush there also throws away entries
> for the current vCPU that a local HFENCE.GVMA(vmid) per retired VMID

We are doing flushed for the pCPU on which a vCPU is ran.

> could have preserved. A local-flush-per-VMID approach could also
> reduce how often we need a full flush at all.
> 
> Is there a reason we don't do local flushing instead? I see x86 and KVM
> use the same flush-all design on wrap, so I assume there's a reason I'm
> missing, I'd like to understand it.

What do you mean here by "local flushing instead"? We are doing local flush:

     if ( unlikely(need_flush) )
         local_hfence_gvma_all();

Do you mean why we don't do hfence_gvma only for specific VMID?

A vCPU's VMID is valid only while vmid->generation == data->generation 
(vmid.c:141). Bumping the generation invalidates every vCPU's VMID on 
this hart simultaneously, so every G-stage entry in the TLB (whatever 
number it is tagged with) belongs to a (vcpu, vmid) binding that can 
never be consulted again. Each of those vCPUs will be handed a fresh 
number on its next vmenter before it can run.

That includes the current vCPU, which is the case you're worried about. 
At the wrap it is being assigned VMID 1, not its previous number, so its 
old entries are unreachable regardless of whether we flush them. 
hfence.gvma per retired VMID would preserve them physically but not 
usefully [A concrete example. vCPU A is running on the hart with 
VMID=100 in generation G; the TLB holds G-stage entries tagged VMID=100. 
A wrap occurs: the generation becomes G+1, next_vmid is reset to 1, and 
A is assigned VMID=1 (vmid.c:154). From that moment on, the hardware 
looks up translations for A under the tag VMID=1. The entries tagged 100 
will no longer match anything: A isn't 100 any more, and no one else 
will be handed 100 until the next wrap.
So A loses its warm entries not because we did an hfence.gvma, but 
because it was renumbered. The flush has nothing to do with it. It 
merely discards what has already become unreachable.]; they'd just 
occupy TLB capacity until natural eviction. Preserving them would 
require a different allocator that keeps a vCPU's number stable across a 
rollover (Linux/KVM-arm64 style, with an active/reserved set pinning 
live ASIDs), not a different flush granularity.

So x86's hvm_asid_handle_vmenter() and KVM's equivalent aren't doing 
this out of inertia — with a round-robin generation allocator, the full 
flush is free of useful collateral damage and strictly cheaper than the 
alternative. Preserving entries across a rollover is a real 
optimisation, but it's an allocator change, and IMO worth doing only if 
profiling shows the wrap flush matters.

> 
>> H/VS CSRs, virtual timer and P2M context, and __context_switch() in assembly,
>> which switches Xen's own callee-saved state (and thereby the stack) from
>> prev to next. Virtual interrupt controller context switch will be
>> introduced later.
>>
>> Add offsets of struct arch_vcpu's xen_saved_context to asm-offsets.c for
>> use by __context_switch().
>>
>> henvcfg and htimedelta are 64-bit on both RV32 and RV64, so store them as
>> uint64_t and use csr_{read,write}64() instead of open-coding accesses to
>> the high halves.
>>
>> A hart which drops out of a domain's dirty_cpumask stops being a target
>> of p2m_tlb_flush() while its TLB may still hold G-stage translations of
>> that domain, and neither the vCPU which just ran nor any other vCPU of
>> that domain which ran there earlier has had its VMID invalidated. Move
>> the hart to a new VMID generation at that point: a VMID number is never
>> re-used until a full local flush has happened, hence none of those
>> translations can be reached again.
>>
>> Claim the VMID in p2m_ctxt_switch_to() rather than at the next guest
>> entry. VMIDs are a per-hart resource, so the (generation, vmid) pair a
>> migrating vCPU brings from another hart is meaningless here and may even
>> match this hart's current generation, leaving the vCPU under a VMID owned
>> by another domain. ctxt_switch_to() invalidates that pair, but claiming a
>> replacement only on guest entry is too late: p2m_ctxt_switch_to() has by
>> then already made HGATP live, and speculation can populate G-stage entries
>> of the incoming domain under the stale VMID. The local flush for a wrapped
>> generation moves along with the claim.
>>
>> That leaves p2m_handle_vmenter() with nothing to do, so drop it together
>> with its call from check_for_pcpu_work(). A VMID can only be invalidated
>> while its vCPU isn't running: vmid_flush_vcpu() is called for the vCPU
>> being switched in, and vmid_flush_hart() runs either from schedule_tail(),
>> ahead of ctxt_switch_to(), or from the wrap path of vmid_handle_vmenter()
>> itself. A P2M change on another hart doesn't invalidate it either, as
>> p2m_tlb_flush() drops the stale entries directly with
>> sbi_remote_hfence_gvma() instead of retiring the VMIDs which tag them. A
>> guest therefore always runs under the VMID claimed on its way in, and
>> there is nothing left for a guest entry hook to notice.
>>
>> p2m_handle_vmenter() also skipped the HGATP write when the VMID it claimed
>> was unchanged. That isn't carried over: HGATP holds the G-stage root as
>> well, and skipping the write is only correct where that root is already
>> the incoming domain's. On the guest entry path it is, on the context
>> switch path it is not.
>>
>> While at it, fix the inclusion order of headers in asm-offsets.c: Xen's
>> headers go first, then arch specific ones.
> This could have a dedicated patch no?

It could but considering that it is pretty small fix I think it could be 
part of this patch. If you are insisting on moving that to separate 
patch I will happy to do that.

>> Signed-off-by: Oleksii Kurochko <oleksii.kurochko@gmail.com>
>>
>> diff --git a/xen/arch/riscv/domain.c b/xen/arch/riscv/domain.c
>> index ec327a5e8a..91a46d630f 100644
>> --- a/xen/arch/riscv/domain.c
>> +++ b/xen/arch/riscv/domain.c
>> @@ -11,9 +11,11 @@
>>   #include <asm/bitops.h>
>>   #include <asm/cpufeature.h>
>>   #include <asm/csr.h>
>> +#include <asm/current.h>
>>   #include <asm/intc.h>
>>   #include <asm/mmio.h>
>>   #include <asm/riscv_encoding.h>
>> +#include <asm/vmid.h>
>>   #include <asm/vtimer.h>
>>   
>>   struct csr_masks {
>> @@ -158,6 +160,8 @@ int arch_vcpu_create(struct vcpu *v)
>>       if ( is_idle_vcpu(v) )
>>           return 0;
>>   
>> +    v->arch.last_cpu = NR_CPUS;
>> +
>>       vcpu_csr_init(v);
>>   
>>       if ( (rc = vcpu_vtimer_init(v)) )
>> @@ -329,6 +333,169 @@ int arch_domain_create(struct domain *d,
>>       return rc;
>>   }
>>   
>> +static void save_csr_regs(struct vcpu *vcpu)
>> +{
>> +    /*
>> +     * There is no need to save these CSRs as only hypervisor writes them in
>> +     * restore_csr_regs() and guest can't access them so they shouldn't be
>> +     * stored here. Keep them commented here just for symmetry with the
>> +     * restore CSRs register part.
>> +     *
>> +     * vcpu->arch.hedeleg = csr_read(CSR_HEDELEG);
>> +     * vcpu->arch.hideleg = csr_read(CSR_HIDELEG);
>> +     * vcpu->arch.henvcfg = csr_read64(CSR_HENVCFG);
>> +     * vcpu->arch.hcounteren = csr_read(CSR_HCOUNTEREN);
>> +     * vcpu->arch.htimedelta = csr_read64(CSR_HTIMEDELTA);
>> +     *
>> +     * if ( riscv_isa_extension_available(NULL, RISCV_ISA_EXT_smstateen) )
>> +     *     vcpu->arch.hstateen0 = csr_read(CSR_HSTATEEN0);
>> +     */
>> +
>> +    vcpu->arch.hvip = csr_read(CSR_HVIP);
>> +
>> +    vcpu->arch.vsstatus = csr_read(CSR_VSSTATUS);
>> +    vcpu->arch.vsie = csr_read(CSR_VSIE);
> 
> 
>> +    vcpu->arch.vstvec = csr_read(CSR_VSTVEC);
>> +    vcpu->arch.vsscratch = csr_read(CSR_VSSCRATCH);
>> +    vcpu->arch.vscause = csr_read(CSR_VSCAUSE);
>> +    vcpu->arch.vstval = csr_read(CSR_VSTVAL);
>> +    vcpu->arch.vsepc = csr_read(CSR_VSEPC);
>> +}
>> +
>> +static void restore_csr_regs(struct vcpu *vcpu)
>> +{
>> +    csr_write(CSR_HEDELEG, vcpu->arch.hedeleg);
>> +    csr_write(CSR_HIDELEG, vcpu->arch.hideleg);
>> +    csr_write(CSR_HVIP, vcpu->arch.hvip);
>> +    csr_write64(CSR_HENVCFG, vcpu->arch.henvcfg);
>> +    csr_write(CSR_HCOUNTEREN, vcpu->arch.hcounteren);
>> +    csr_write64(CSR_HTIMEDELTA, vcpu->arch.htimedelta);
>> +
>> +    if ( riscv_isa_extension_available(NULL, RISCV_ISA_EXT_smstateen) )
>> +        csr_write(CSR_HSTATEEN0, vcpu->arch.hstateen0);
>> +
>> +    csr_write(CSR_VSSTATUS, vcpu->arch.vsstatus);
>> +    csr_write(CSR_VSIE, vcpu->arch.vsie);
> 
> 
>> +    csr_write(CSR_VSTVEC, vcpu->arch.vstvec);
>> +    csr_write(CSR_VSSCRATCH, vcpu->arch.vsscratch);
>> +    csr_write(CSR_VSCAUSE, vcpu->arch.vscause);
>> +    csr_write(CSR_VSTVAL, vcpu->arch.vstval);
>> +    csr_write(CSR_VSEPC, vcpu->arch.vsepc);
>> +}
>> +
>> +static void ctxt_switch_from(struct vcpu *p)
> Is it expected to have diverse names for the vcpu arg? Above it's vcpu,
> here it's p (I assume it's for `previous` but I think the _from alone is
> enough to understand) and below it's n. Shouldn't be better to keep the same name?

Above should be used n. It is what usually is used in Xen in such cases. 
vcpu isn't the best one name in general as it should be v. But for these 
functions I will use n and p correspondignly.

Thanks for noticing that.

~ Oleksii


^ permalink raw reply	[flat|nested] 244+ messages in thread

* Re: [PATCH v2 13/39] xen/riscv: save and restore AIA state on vCPU context switch
  2026-09-04  9:52   ` Baptiste Le Duc
@ 2026-09-04 16:40     ` Oleksii Kurochko
  0 siblings, 0 replies; 244+ messages in thread
From: Oleksii Kurochko @ 2026-09-04 16:40 UTC (permalink / raw)
  To: Baptiste Le Duc
  Cc: xen-devel, Romain Caritey, Zheng Zhang, Alistair Francis,
	Connor Davis, Andrew Cooper, Anthony PERARD, Michal Orzel,
	Jan Beulich, Julien Grall, Roger Pau Monné,
	Stefano Stabellini



On 9/4/26 11:52 AM, Baptiste Le Duc wrote:
>> vsiselect and hviprio{1,2} are per-hart CSRs which a guest can change, so
>> they have to be part of the vCPU context:
> Where in the spec did you see that? Because in AIA spec section 6.3.1,
> it is written that "When vsiselect has a value in the range 0x30-0x3F,
> an attempt from VS-mode to access sireg (really vsireg) causes a virtual
> instruction exception" and this even when hstateen0.CSRIND is set as hstateen0
> just control whether a guest/S-mode is allowed to access a CSR (exactly
> as you described below).

Your understanding is correct, it was me who confused the things. Sorry 
for that.

> 
> Therefore, the hypervisor has two options to modify the priority of a
> major irq:
> - emulate the iprio array in software.
> - Use hviprio1/hviprio2 (only 10 irqs configurable).
> 
> But the guest shouldn't be able to modify h CSRs at all, in any case, or
> I may have misunderstood a part of the spec.
> 
> For the moment I don't see any catch of possible instruction exception
> in do_trap().

There is no such because we don't emulate range 0x30-0x3F. We don't have 
such use cases now.

I think that I have to recheck what should be saved/restored now.

There is no need to save/restore CSR_HVIPRIO* during context switch as 
we don't have support of handling of 0x30-0x3f. I will introduce that 
later when we really will need that.

VSISELECT should be save/restored then only in this patch as we have 
hstateen0.SMSTATEEN0_SVSLCT set so guest could change VSISELECT directly 
so we need to store/restore.

Am I missing something?

With having only VSISELECT saved/restored in this patch I think the 
commit message should be:

xen/riscv: save and restore vsiselect on vCPU context switch

vsiselect is a per-hart CSR which a guest changes on its own: when V=1,
VS-mode accesses to siselect are really accesses to vsiselect.
Architecturally a vCPU has to find there the value it last wrote, but as
long as the CSR isn't part of the vCPU context it finds whatever selector
the vCPU which ran on the hart before it left behind. A guest which writes
siselect, is descheduled and then reads sireg without rewriting siselect
therefore reaches a register it never selected, and it can also observe
another guest's selector value.

When Smstateen is implemented, access to vsiselect and vsireg is gated by
hstateen0.CSRIND (bit 60, SMSTATEEN0_SVSLCT in Xen's headers), and
v->arch.hstateen0 holds the bits vcpu_csr_init() ended up with. A clear
bit there covers the two cases in which the CSR has to be skipped:

- Xen didn't hand the guest access to it, so the guest can't have changed
      the CSR and there is no state to preserve;
- M-mode denied the state altogether. Smstateen makes a bit which is zero
      in mstateen0 read-only zero in hstateen0, and a zero bit in mstateen0
      traps accesses from every privilege mode less privileged than M-mode,
      HS-mode included, so Xen couldn't even read the CSR to save it.

Without Smstateen no bit controls access to the CSR, so it is saved and
restored whenever Ssaia is available.

Signed-off-by: Oleksii Kurochko <oleksii.kurochko@gmail.com>
---
Changes in v3:
- Update the commit message.
- Save and restore only VSISELECT.
---
Changes in v2:
- New  patch.
---

diff --git a/xen/arch/riscv/domain.c b/xen/arch/riscv/domain.c
index 0ad851ee0f5f..1085ef152b8b 100644
--- a/xen/arch/riscv/domain.c
+++ b/xen/arch/riscv/domain.c
@@ -327,6 +327,28 @@ int arch_domain_create(struct domain *d,
      return rc;
  }

+/*
+ * vsiselect is a per-hart CSR, but a guest changes it on its own: when 
V=1,
+ * VS-mode accesses to siselect are really accesses to vsiselect. Hence 
it is
+ * part of the vCPU context.
+ *
+ * When Smstateen is implemented, hstateen0.CSRIND (SMSTATEEN0_SVSLCT) 
gates
+ * that access, and a bit staying clear in v->arch.hstateen0 (see
+ * vcpu_csr_init()) means either that the guest was never given access 
to the
+ * CSR, and so can't have changed it, or that M-mode denied the state
+ * altogether, in which case the CSR can't be accessed from HS-mode either.
+ */
+static bool vcpu_can_access_vsiselect(const struct vcpu *v)
+{
+    if ( !riscv_isa_extension_available(NULL, RISCV_ISA_EXT_ssaia) )
+        return false;
+
+    if ( !riscv_isa_extension_available(NULL, RISCV_ISA_EXT_smstateen) )
+        return true;
+
+    return v->arch.hstateen0 & SMSTATEEN0_SVSLCT;
+}
+
  static void save_csr_regs(struct vcpu *p)
  {
      /*
@@ -354,6 +376,9 @@ static void save_csr_regs(struct vcpu *p)
      p->arch.vscause = csr_read(CSR_VSCAUSE);
      p->arch.vstval = csr_read(CSR_VSTVAL);
      p->arch.vsepc = csr_read(CSR_VSEPC);
+
+    if ( vcpu_can_access_vsiselect(p) )
+        p->arch.vsiselect = csr_read(CSR_VSISELECT);
  }

  static void restore_csr_regs(struct vcpu *n)
@@ -375,6 +400,9 @@ static void restore_csr_regs(struct vcpu *n)
      csr_write(CSR_VSCAUSE, n->arch.vscause);
      csr_write(CSR_VSTVAL, n->arch.vstval);
      csr_write(CSR_VSEPC, n->arch.vsepc);
+
+    if ( vcpu_can_access_vsiselect(n) )
+        csr_write(CSR_VSISELECT, n->arch.vsiselect);
  }

  static void ctxt_switch_from(struct vcpu *p)
diff --git a/xen/arch/riscv/include/asm/domain.h 
b/xen/arch/riscv/include/asm/domain.h
index 58d1e8076876..b0824d7f9add 100644
--- a/xen/arch/riscv/include/asm/domain.h
+++ b/xen/arch/riscv/include/asm/domain.h
@@ -75,6 +75,7 @@ struct arch_vcpu {
      register_t vscause;
      register_t vsepc;
      uint64_t   vsie;
+    register_t vsiselect;
      register_t vsscratch;
      register_t vsstatus;
      register_t vstval;


Does it make sense to you?



> 
>>   - vsiselect is written directly by VS-mode through siselect;
>>   - hviprio1 and hviprio2 hold the priorities of the local interrupts which
>>     VS-mode reaches through the iprio array of vsiselect/vsireg, so writes
>>     the guest performs there land in these CSRs.
> hviprio1 and hviprio2 hold priorities for interrupts 1 (SSI), 5 (STI),
> 13 (counter overflow), and 14-23 (local) so calling all of them "local"
> is wrong.

Agree, it is incorrect to call them "local"

>> Without saving them, one vCPU's selector leaks into another vCPU's vsireg
>> accesses and one guest's interrupt priorities apply to the next guest which
>> runs on the same hart.
>>
>> Whether the CSRs may be touched at all is gated by hstateen0 when Smstateen
>> is implemented: SVSLCT for vsiselect/vsireg and AIA for the rest of the AIA
> I couldn't find any reference to SVSLCT in the spec. I assume you wanted
> to refer to CSRIND and SVSLCT is an OpenSBI's own nickname.
> 

Indeed, SVSLCT is the OpenSBI nickname/macro definition for this feature 
and CSRIND would be better to use in commit message.

Thanks.

~ Oleksii


^ permalink raw reply related	[flat|nested] 244+ messages in thread

* Re: [PATCH v2 14/39] xen/riscv: introduce vintc_ctxt_switch_{from,to}()
  2026-09-04 11:25   ` Baptiste Le Duc
@ 2026-09-04 16:54     ` Oleksii Kurochko
  0 siblings, 0 replies; 244+ messages in thread
From: Oleksii Kurochko @ 2026-09-04 16:54 UTC (permalink / raw)
  To: Baptiste Le Duc
  Cc: xen-devel, Romain Caritey, Zheng Zhang, Alistair Francis,
	Connor Davis, Andrew Cooper, Anthony PERARD, Michal Orzel,
	Jan Beulich, Julien Grall, Roger Pau Monné,
	Stefano Stabellini



On 9/4/26 1:25 PM, Baptiste Le Duc wrote:
>> Virtual interrupt controller state must be preserved across vCPU context
>> switches.
>>
>> Introduce vintc_ctxt_switch_{from,to}() wrappers around new
>> ctxt_switch_{from,to}() hooks in struct vintc_ops, and call them from the
>> context switch path, so that this state can be saved/restored without
>> knowing which vINTC variant a domain uses.
>>
>> No vINTC variant implements the hooks yet: the vAPLIC implementation is
>> added separately.
> So this patch couldn't be applied alone as, at the time of this commit, you only set in vaplic.c:
>      static const struct vintc_ops vintc_ops = {
>          .vcpu_init = vcpu_imsic_init,
>          .vcpu_deinit = vcpu_imsic_deinit,
>      };
> 
> So ops->ctxt_switch_from(v) or ops->ctxt_switch_to(v) will try to
> deference NULL pointer causing segfault. I don't know if it's matter but
> worth to mention somewhere.
> 

Just abstraction is introduced here and context switch isn't happen for 
now so we are safe.

~ Oleksii



^ permalink raw reply	[flat|nested] 244+ messages in thread

* Re: [PATCH v2 15/39] xen/riscv: add IMSIC vCPU context switch handlers
  2026-09-04 11:33   ` Baptiste Le Duc
@ 2026-09-04 16:56     ` Oleksii Kurochko
  0 siblings, 0 replies; 244+ messages in thread
From: Oleksii Kurochko @ 2026-09-04 16:56 UTC (permalink / raw)
  To: Baptiste Le Duc
  Cc: xen-devel, Romain Caritey, Zheng Zhang, Alistair Francis,
	Connor Davis, Andrew Cooper, Anthony PERARD, Michal Orzel,
	Jan Beulich, Julien Grall, Roger Pau Monné,
	Stefano Stabellini



On 9/4/26 1:33 PM, Baptiste Le Duc wrote:
>> IMSIC state currently needs to track only which physical CPU owns a vCPU's
>> IMSIC guest interrupt file, as the CPU id is part of the physical address
>> the file is mapped at.
>>
>> Add imsic_ctxt_switch_from() to record that CPU when a vCPU is switched
>> out. A vCPU running on the s/w VS-file has no h/w file bound to a CPU, so
>> there is nothing to record for it. The recorded value stays unused until
>> vCPU migration support, which needs it to find the file to move away from,
>> is added later.
>>
>> imsic_ctxt_switch_to() has nothing to do: by the time a vCPU is switched
>> in, VGEIN is already assigned to it and its guest interrupt file is already
>> mapped. Work is only required once a vCPU can move to a different CPU,
>> which means recalculating VGEIN and remapping the file; that is handled
>> separately by the vCPU migration patches.
>>
>> Install both as the ctxt_switch_{from,to} hooks of struct vintc_ops. MSI
>> delivery is the only mode Xen supports ( aplic_init() panics on an APLIC
>> without an "msi-parent" property, and a guest's domaincfg.DM reads back as
>> a fixed one) so the vAPLIC state to save and restore is always the IMSIC
>> one and no vAPLIC-level forwarder is needed. Being indirect call targets,
>> both handlers get cf_check.
>>
>> Co-developed-by: Romain Caritey <Romain.Caritey@microchip.com>
>> Signed-off-by: Oleksii Kurochko <oleksii.kurochko@gmail.com>
>>
>> diff --git a/xen/arch/riscv/imsic.c b/xen/arch/riscv/imsic.c
>> index ad0a220eda..3787f270d8 100644
>> --- a/xen/arch/riscv/imsic.c
>> +++ b/xen/arch/riscv/imsic.c
>> @@ -20,6 +20,7 @@
>>   #include <xen/init.h>
>>   #include <xen/libfdt/libfdt.h>
>>   #include <xen/macros.h>
>> +#include <xen/rwlock.h>
>>   #include <xen/sched.h>
>>   #include <xen/smp.h>
>>   #include <xen/spinlock.h>
>> @@ -342,6 +343,28 @@ static int __init imsic_parse_node(const struct dt_device_node *node,
>>       return 0;
>>   }
>>   
>> +void cf_check imsic_ctxt_switch_from(struct vcpu *v)
>> +{
>> +    struct vimsic_state *imsic_state = v->arch.vimsic_state;
>> +    unsigned long flags;
>> +
>> +    /*
>> +     * A vCPU using the s/w IMSIC VS-file (guest_file_id == 0) has no h/w
>> +     * VS-file bound to a physical CPU, so there is no location to record.
>> +     */
>> +    if ( !vcpu_guest_file_id(v) )
>> +        return;
>> +
>> +    write_lock_irqsave(&imsic_state->vsfile_lock, flags);
>> +    imsic_state->vsfile_cpu = v->processor;
>> +    write_unlock_irqrestore(&imsic_state->vsfile_lock, flags);
>> +}
>> +
>> +void cf_check imsic_ctxt_switch_to(struct vcpu *v)
>> +{
>> +    /* Nothing to do */
>> +}
>> +
>>   int cf_check vcpu_imsic_init(struct vcpu *v)
>>   {
>>       struct vimsic_state *imsic_state;
>> diff --git a/xen/arch/riscv/include/asm/imsic.h b/xen/arch/riscv/include/asm/imsic.h
>> index 93f9e44c7d..73129c3c9e 100644
>> --- a/xen/arch/riscv/include/asm/imsic.h
>> +++ b/xen/arch/riscv/include/asm/imsic.h
>> @@ -109,4 +109,7 @@ unsigned int vcpu_guest_file_id(const struct vcpu *v);
>>   
>>   int vimsic_make_domu_dt_node(struct kernel_info *kinfo, unsigned int *phandle);
>>   
>> +void imsic_ctxt_switch_from(struct vcpu *v);
>> +void imsic_ctxt_switch_to(struct vcpu *v);
>> +
>>   #endif /* ASM_RISCV_IMSIC_H */
>> diff --git a/xen/arch/riscv/vaplic.c b/xen/arch/riscv/vaplic.c
>> index 8726f7203d..6c60fe2baf 100644
>> --- a/xen/arch/riscv/vaplic.c
>> +++ b/xen/arch/riscv/vaplic.c
>> @@ -422,6 +422,13 @@ static const struct mmio_handler_ops vaplic_mmio_ops = {
>>   static const struct vintc_ops vintc_ops = {
>>       .vcpu_init = vcpu_imsic_init,
>>       .vcpu_deinit = vcpu_imsic_deinit,
>> +    /*
>> +     * MSI delivery is the only supported mode: aplic_init() panics on an
>> +     * APLIC without an "msi-parent", so the vAPLIC state to save and restore
>> +     * is always the IMSIC one.
>> +     */
>> +    .ctxt_switch_from = imsic_ctxt_switch_from,
>> +    .ctxt_switch_to = imsic_ctxt_switch_to,
> 
> Maybe this patch could be merged with previous one: patch 50fe9554e1c0 ("xen/riscv: introduce vintc_ctxt_switch_{from,to}()")

At some point I agree but I think I prefer a little bit to separate 
them. It is safe to have them separately as context switch isn't 
happening between these patches so any problem will occur. What I 
intented to do is that to separately introduce abstraction and 
separately introduce users of this abstraction.

~ Oleksii


^ permalink raw reply	[flat|nested] 244+ messages in thread

* Re: [PATCH v2 12/39] xen/riscv: implement vCPU context switching
  2026-08-27 15:20 ` [PATCH v2 12/39] xen/riscv: implement vCPU context switching Oleksii Kurochko
  2026-09-02 14:42   ` Oleksii Kurochko
  2026-09-04  8:26   ` Baptiste Le Duc
@ 2026-09-05  7:25   ` Oleksii Kurochko
  2026-09-10 13:29   ` Jan Beulich
  3 siblings, 0 replies; 244+ messages in thread
From: Oleksii Kurochko @ 2026-09-05  7:25 UTC (permalink / raw)
  To: xen-devel
  Cc: Romain Caritey, Baptiste Le Duc, Zheng Zhang, Alistair Francis,
	Connor Davis, Andrew Cooper, Anthony PERARD, Michal Orzel,
	Jan Beulich, Julien Grall, Roger Pau Monné,
	Stefano Stabellini


I've updated the part of handling of VMID for p2m during context switch 
as some things were still missed. This one implementation looks more 
correct to me.

diff --git a/xen/arch/riscv/domain.c b/xen/arch/riscv/domain.c
index 0ad851ee0f5f..c05d6f8abaaa 100644
--- a/xen/arch/riscv/domain.c
+++ b/xen/arch/riscv/domain.c
@@ -404,16 +404,6 @@ static void ctxt_switch_to(struct vcpu *n)
      if ( is_idle_vcpu(n) )
          return;

-    /*
-     * If this vCPU last ran on a different pCPU, invalidate its VMID so
-     * vmid_handle_vmenter() assigns a fresh one from the current 
pCPU's pool.
-     * Without this, two pCPUs could independently assign the same
-     * (generation, vmid) pair, generation counters start at the same value
-     * on all pCPUs and increment independently, causing TLB contamination.
-     */
-    if ( n->arch.last_cpu != smp_processor_id() )
-        vmid_flush_vcpu(n);
-
      vtimer_ctxt_switch_to(n);

      restore_csr_regs(n);
@@ -421,6 +411,15 @@ static void ctxt_switch_to(struct vcpu *n)
      p2m_ctxt_switch_to(n);
  }

+/*
+ * Domain whose p2m this hart's HGATP points at. ctxt_switch_to() bails out
+ * early for the idle vCPU, so HGATP survives a pass through idle and keeps
+ * pointing at the domain which ran here last. That domain, rather than the
+ * one the scheduler switched away from, is what owns this hart's G-stage
+ * translations.
+ */
+static DEFINE_PER_CPU(struct domain *, hgatp_owner);
+
  static void schedule_tail(struct vcpu *prev)
  {
      unsigned int cpu = smp_processor_id();
@@ -429,40 +428,88 @@ static void schedule_tail(struct vcpu *prev)

      ctxt_switch_from(prev);

+    write_atomic(&prev->dirty_cpu, VCPU_CPU_CLEAN);
+
      /*
-     * Mark this CPU in next domain's dirty cpumasks before calling
-     * ctxt_switch_to(). This avoids a race on things like p2m flushing,
-     * which is synchronised on that function.
+     * Switching to the idle vCPU leaves HGATP alone, so this hart 
keeps both
+     * the G-stage translations of its owner and its place in that domain's
+     * dirty_cpumask: p2m_tlb_flush() goes on reaching it, and a domain 
which
+     * idles between two runs on the same hart keeps its VMIDs.
       */
-    if ( prev->domain != current->domain )
+    if ( !is_idle_vcpu(current) )
+    {
+        struct domain *owner = this_cpu(hgatp_owner);
+
+        if ( owner != current->domain )
+        {
+            /*
+             * Once this hart drops out of the owner's dirty_cpumask it 
stops
+             * being a target of p2m_tlb_flush(), while its TLB may 
still hold
+             * G-stage translations of that domain: none of the vCPUs 
of that
+             * domain which ran here has had its VMID invalidated. Move the
+             * hart to a new VMID generation so that none of them can be
+             * reached again.
+             */
+            if ( owner )
+            {
+                vmid_flush_hart();
+
+                cpumask_clear_cpu(cpu, owner->dirty_cpumask);
+            }
+
+            /*
+             * Mark this hart in the incoming domain's dirty_cpumask before
+             * ctxt_switch_to() points HGATP at its p2m. This avoids a 
race on
+             * things like p2m flushing, which is synchronised on that
+             * function.
+             */
+            cpumask_set_cpu(cpu, current->domain->dirty_cpumask);
+
+            /*
+             * Pairs with the barrier in p2m_tlb_flush(). 
cpumask_set_cpu() is
+             * an unordered AMO on RISC-V, so without this a concurrent 
flusher
+             * could read the mask without this hart in it while this 
hart is
+             * already walking the p2m it is about to be pointed at.
+             */
+            smp_mb();
+
+            this_cpu(hgatp_owner) = current->domain;
+        }
+    }
+
+    if ( !is_idle_vcpu(current) )
      {
-        cpumask_set_cpu(cpu, current->domain->dirty_cpumask);
+        bool need_flush;
+
+        /*
+         * A VMID is meaningful only on the hart whose pool issued it:
+         * generations are per-hart counters which all start at 1 and 
advance
+         * independently, so the pair a vCPU brings from another hart 
may match
+         * this hart's generation by coincidence, leaving the vCPU 
under a VMID
+         * which is live here for someone else.
+         */
+        if ( current->arch.last_cpu != cpu )
+            vmid_flush_vcpu(current);

          /*
-         * Once this hart drops out of prev's dirty_cpumask it stops 
being a
-         * target of p2m_tlb_flush(), while its TLB may still hold G-stage
-         * translations of prev's domain: neither the vCPU which just 
ran nor
-         * any other vCPU of that domain which ran here earlier has had its
-         * VMID invalidated. Move the hart to a new VMID generation so that
-         * none of them can be reached again.
-         *
-         * Switching away from the idle vCPU needs no bump: the idle domain
-         * has no p2m of its own, and whatever G-stage entries this 
hart may
-         * still hold (or speculatively create while HGATP keeps 
pointing at
-         * the last guest's p2m) are tagged with a VMID which was 
already made
-         * stale when that guest was switched out. Skipping the bump 
here also
-         * avoids burning a generation on every pass through idle.
+         * Claim the VMID here rather than leaving it to the next guest 
entry:
+         * ctxt_switch_to() makes HGATP live below, and a stale VMID there
+         * pairs this domain's G-stage root with a tag which may 
already have
+         * been re-issued to a vCPU of another domain.
           */
-        if ( !is_idle_vcpu(prev) )
-            vmid_flush_hart();
+        need_flush = vmid_handle_vmenter(&current->arch.vmid);

-        cpumask_clear_cpu(cpu, prev->domain->dirty_cpumask);
+        /*
+         * A VMID isn't re-used until the generation it was issued in 
wraps, so
+         * a G-stage flush is needed only when vmid_handle_vmenter() 
says so.
+         */
+        if ( unlikely(need_flush) )
+            local_hfence_gvma_all();
      }
-    write_atomic(&current->dirty_cpu, cpu);

      ctxt_switch_to(current);

-    write_atomic(&prev->dirty_cpu, VCPU_CPU_CLEAN);
+    write_atomic(&current->dirty_cpu, cpu);

      current->arch.last_cpu = cpu;

diff --git a/xen/arch/riscv/p2m.c b/xen/arch/riscv/p2m.c
index 1f7a6907525d..98c2d6de6933 100644
--- a/xen/arch/riscv/p2m.c
+++ b/xen/arch/riscv/p2m.c
@@ -243,6 +243,15 @@ static void p2m_tlb_flush(struct p2m_domain *p2m)

      p2m->need_flush = false;

+    /*
+     * Order the p2m updates above against the read of dirty_cpumask below,
+     * pairing with the barrier in schedule_tail(). Either that hart is 
seen
+     * here and gets an HFENCE.GVMA, or it adds itself to the mask 
afterwards,
+     * in which case it starts walking this p2m only once the updates are
+     * visible to it.
+     */
+    smp_mb();
+
      sbi_remote_hfence_gvma(d->dirty_cpumask, 0, 0);
  }

@@ -1523,22 +1532,12 @@ void p2m_ctxt_switch_from(struct vcpu *p)
  void p2m_ctxt_switch_to(struct vcpu *n)
  {
      struct p2m_domain *p2m = p2m_get_hostp2m(n->domain);
-    bool need_flush;

      if ( is_idle_vcpu(n) )
          return;

-    need_flush = vmid_handle_vmenter(&n->arch.vmid);
-
      csr_write(CSR_HGATP, construct_hgatp(p2m, n->arch.vmid.vmid));

-    /*
-     * A VMID isn't re-used until the generation it was issued in wraps, so
-     * a G-stage flush is needed only when vmid_handle_vmenter() says so.
-     */
-    if ( unlikely(need_flush) )
-        local_hfence_gvma_all();
-
      csr_write(CSR_VSATP, n->arch.vsatp);

      /*

Any concerns about this implementation?

Thanks in advance.

~ Oleksii


^ permalink raw reply related	[flat|nested] 244+ messages in thread

* Re: [PATCH v2 12/39] xen/riscv: implement vCPU context switching
  2026-09-04 14:55     ` Oleksii Kurochko
@ 2026-09-07  8:17       ` Jan Beulich
  2026-09-08  9:06         ` Oleksii Kurochko
  0 siblings, 1 reply; 244+ messages in thread
From: Jan Beulich @ 2026-09-07  8:17 UTC (permalink / raw)
  To: Oleksii Kurochko
  Cc: xen-devel, Romain Caritey, Zheng Zhang, Alistair Francis,
	Connor Davis, Andrew Cooper, Anthony PERARD, Michal Orzel,
	Julien Grall, Roger Pau Monné, Stefano Stabellini,
	Baptiste Le Duc

On 04.09.2026 16:55, Oleksii Kurochko wrote:
> 
> 
> On 9/4/26 10:26 AM, Baptiste Le Duc wrote:
>> As I understand it, a generation wrap doesn't retire a single VMID, it
>> resets next_vmid to 1, which makes every VMID in 1..max_vmid reusable
>> again in the new generation. We do a full (local) flush at that point to
>> avoid two different vCPUs ending up with the same VMID valid at once,
>> across generations.
>>
>> If this is correct, doing a full flush there also throws away entries
>> for the current vCPU that a local HFENCE.GVMA(vmid) per retired VMID
> 
> We are doing flushed for the pCPU on which a vCPU is ran.
> 
>> could have preserved. A local-flush-per-VMID approach could also
>> reduce how often we need a full flush at all.
>>
>> Is there a reason we don't do local flushing instead? I see x86 and KVM
>> use the same flush-all design on wrap, so I assume there's a reason I'm
>> missing, I'd like to understand it.
> 
> What do you mean here by "local flushing instead"? We are doing local flush:
> 
>      if ( unlikely(need_flush) )
>          local_hfence_gvma_all();
> 
> Do you mean why we don't do hfence_gvma only for specific VMID?
> 
> A vCPU's VMID is valid only while vmid->generation == data->generation 
> (vmid.c:141). Bumping the generation invalidates every vCPU's VMID on 
> this hart simultaneously, so every G-stage entry in the TLB (whatever 
> number it is tagged with) belongs to a (vcpu, vmid) binding that can 
> never be consulted again. Each of those vCPUs will be handed a fresh 
> number on its next vmenter before it can run.
> 
> That includes the current vCPU, which is the case you're worried about. 
> At the wrap it is being assigned VMID 1, not its previous number, so its 
> old entries are unreachable regardless of whether we flush them. 
> hfence.gvma per retired VMID would preserve them physically but not 
> usefully [A concrete example. vCPU A is running on the hart with 
> VMID=100 in generation G; the TLB holds G-stage entries tagged VMID=100. 
> A wrap occurs: the generation becomes G+1, next_vmid is reset to 1, and 
> A is assigned VMID=1 (vmid.c:154). From that moment on, the hardware 
> looks up translations for A under the tag VMID=1. The entries tagged 100 
> will no longer match anything: A isn't 100 any more, and no one else 
> will be handed 100 until the next wrap.
> So A loses its warm entries not because we did an hfence.gvma, but 
> because it was renumbered. The flush has nothing to do with it. It 
> merely discards what has already become unreachable.]; they'd just 
> occupy TLB capacity until natural eviction. Preserving them would 
> require a different allocator that keeps a vCPU's number stable across a 
> rollover (Linux/KVM-arm64 style, with an active/reserved set pinning 
> live ASIDs), not a different flush granularity.
> 
> So x86's hvm_asid_handle_vmenter() and KVM's equivalent aren't doing 
> this out of inertia — with a round-robin generation allocator, the full 
> flush is free of useful collateral damage and strictly cheaper than the 
> alternative. Preserving entries across a rollover is a real 
> optimisation, but it's an allocator change, and IMO worth doing only if 
> profiling shows the wrap flush matters.
> 
>>
>>> H/VS CSRs, virtual timer and P2M context, and __context_switch() in assembly,
>>> which switches Xen's own callee-saved state (and thereby the stack) from
>>> prev to next. Virtual interrupt controller context switch will be
>>> introduced later.
>>>
>>> Add offsets of struct arch_vcpu's xen_saved_context to asm-offsets.c for
>>> use by __context_switch().
>>>
>>> henvcfg and htimedelta are 64-bit on both RV32 and RV64, so store them as
>>> uint64_t and use csr_{read,write}64() instead of open-coding accesses to
>>> the high halves.
>>>
>>> A hart which drops out of a domain's dirty_cpumask stops being a target
>>> of p2m_tlb_flush() while its TLB may still hold G-stage translations of
>>> that domain, and neither the vCPU which just ran nor any other vCPU of
>>> that domain which ran there earlier has had its VMID invalidated. Move
>>> the hart to a new VMID generation at that point: a VMID number is never
>>> re-used until a full local flush has happened, hence none of those
>>> translations can be reached again.
>>>
>>> Claim the VMID in p2m_ctxt_switch_to() rather than at the next guest
>>> entry. VMIDs are a per-hart resource, so the (generation, vmid) pair a
>>> migrating vCPU brings from another hart is meaningless here and may even
>>> match this hart's current generation, leaving the vCPU under a VMID owned
>>> by another domain. ctxt_switch_to() invalidates that pair, but claiming a
>>> replacement only on guest entry is too late: p2m_ctxt_switch_to() has by
>>> then already made HGATP live, and speculation can populate G-stage entries
>>> of the incoming domain under the stale VMID. The local flush for a wrapped
>>> generation moves along with the claim.
>>>
>>> That leaves p2m_handle_vmenter() with nothing to do, so drop it together
>>> with its call from check_for_pcpu_work(). A VMID can only be invalidated
>>> while its vCPU isn't running: vmid_flush_vcpu() is called for the vCPU
>>> being switched in, and vmid_flush_hart() runs either from schedule_tail(),
>>> ahead of ctxt_switch_to(), or from the wrap path of vmid_handle_vmenter()
>>> itself. A P2M change on another hart doesn't invalidate it either, as
>>> p2m_tlb_flush() drops the stale entries directly with
>>> sbi_remote_hfence_gvma() instead of retiring the VMIDs which tag them. A
>>> guest therefore always runs under the VMID claimed on its way in, and
>>> there is nothing left for a guest entry hook to notice.
>>>
>>> p2m_handle_vmenter() also skipped the HGATP write when the VMID it claimed
>>> was unchanged. That isn't carried over: HGATP holds the G-stage root as
>>> well, and skipping the write is only correct where that root is already
>>> the incoming domain's. On the guest entry path it is, on the context
>>> switch path it is not.
>>>
>>> While at it, fix the inclusion order of headers in asm-offsets.c: Xen's
>>> headers go first, then arch specific ones.
>> This could have a dedicated patch no?
> 
> It could but considering that it is pretty small fix I think it could be 
> part of this patch. If you are insisting on moving that to separate 
> patch I will happy to do that.
> 
>>> Signed-off-by: Oleksii Kurochko <oleksii.kurochko@gmail.com>
>>>
>>> diff --git a/xen/arch/riscv/domain.c b/xen/arch/riscv/domain.c
>>> index ec327a5e8a..91a46d630f 100644
>>> --- a/xen/arch/riscv/domain.c
>>> +++ b/xen/arch/riscv/domain.c
>>> @@ -11,9 +11,11 @@
>>>   #include <asm/bitops.h>
>>>   #include <asm/cpufeature.h>
>>>   #include <asm/csr.h>
>>> +#include <asm/current.h>
>>>   #include <asm/intc.h>
>>>   #include <asm/mmio.h>
>>>   #include <asm/riscv_encoding.h>
>>> +#include <asm/vmid.h>
>>>   #include <asm/vtimer.h>
>>>   
>>>   struct csr_masks {
>>> @@ -158,6 +160,8 @@ int arch_vcpu_create(struct vcpu *v)
>>>       if ( is_idle_vcpu(v) )
>>>           return 0;
>>>   
>>> +    v->arch.last_cpu = NR_CPUS;
>>> +
>>>       vcpu_csr_init(v);
>>>   
>>>       if ( (rc = vcpu_vtimer_init(v)) )
>>> @@ -329,6 +333,169 @@ int arch_domain_create(struct domain *d,
>>>       return rc;
>>>   }
>>>   
>>> +static void save_csr_regs(struct vcpu *vcpu)
>>> +{
>>> +    /*
>>> +     * There is no need to save these CSRs as only hypervisor writes them in
>>> +     * restore_csr_regs() and guest can't access them so they shouldn't be
>>> +     * stored here. Keep them commented here just for symmetry with the
>>> +     * restore CSRs register part.
>>> +     *
>>> +     * vcpu->arch.hedeleg = csr_read(CSR_HEDELEG);
>>> +     * vcpu->arch.hideleg = csr_read(CSR_HIDELEG);
>>> +     * vcpu->arch.henvcfg = csr_read64(CSR_HENVCFG);
>>> +     * vcpu->arch.hcounteren = csr_read(CSR_HCOUNTEREN);
>>> +     * vcpu->arch.htimedelta = csr_read64(CSR_HTIMEDELTA);
>>> +     *
>>> +     * if ( riscv_isa_extension_available(NULL, RISCV_ISA_EXT_smstateen) )
>>> +     *     vcpu->arch.hstateen0 = csr_read(CSR_HSTATEEN0);
>>> +     */
>>> +
>>> +    vcpu->arch.hvip = csr_read(CSR_HVIP);
>>> +
>>> +    vcpu->arch.vsstatus = csr_read(CSR_VSSTATUS);
>>> +    vcpu->arch.vsie = csr_read(CSR_VSIE);
>>
>>
>>> +    vcpu->arch.vstvec = csr_read(CSR_VSTVEC);
>>> +    vcpu->arch.vsscratch = csr_read(CSR_VSSCRATCH);
>>> +    vcpu->arch.vscause = csr_read(CSR_VSCAUSE);
>>> +    vcpu->arch.vstval = csr_read(CSR_VSTVAL);
>>> +    vcpu->arch.vsepc = csr_read(CSR_VSEPC);
>>> +}
>>> +
>>> +static void restore_csr_regs(struct vcpu *vcpu)
>>> +{
>>> +    csr_write(CSR_HEDELEG, vcpu->arch.hedeleg);
>>> +    csr_write(CSR_HIDELEG, vcpu->arch.hideleg);
>>> +    csr_write(CSR_HVIP, vcpu->arch.hvip);
>>> +    csr_write64(CSR_HENVCFG, vcpu->arch.henvcfg);
>>> +    csr_write(CSR_HCOUNTEREN, vcpu->arch.hcounteren);
>>> +    csr_write64(CSR_HTIMEDELTA, vcpu->arch.htimedelta);
>>> +
>>> +    if ( riscv_isa_extension_available(NULL, RISCV_ISA_EXT_smstateen) )
>>> +        csr_write(CSR_HSTATEEN0, vcpu->arch.hstateen0);
>>> +
>>> +    csr_write(CSR_VSSTATUS, vcpu->arch.vsstatus);
>>> +    csr_write(CSR_VSIE, vcpu->arch.vsie);
>>
>>
>>> +    csr_write(CSR_VSTVEC, vcpu->arch.vstvec);
>>> +    csr_write(CSR_VSSCRATCH, vcpu->arch.vsscratch);
>>> +    csr_write(CSR_VSCAUSE, vcpu->arch.vscause);
>>> +    csr_write(CSR_VSTVAL, vcpu->arch.vstval);
>>> +    csr_write(CSR_VSEPC, vcpu->arch.vsepc);
>>> +}
>>> +
>>> +static void ctxt_switch_from(struct vcpu *p)
>> Is it expected to have diverse names for the vcpu arg? Above it's vcpu,
>> here it's p (I assume it's for `previous` but I think the _from alone is
>> enough to understand) and below it's n. Shouldn't be better to keep the same name?
> 
> Above should be used n.

Why would that be? n in such contexts stands for "next", while here
it can only be "previous".

Jan


^ permalink raw reply	[flat|nested] 244+ messages in thread

* Re: [PATCH v2 16/39] xen/riscv: extend exception tables with type and data fields
  2026-08-27 15:21 ` [PATCH v2 16/39] xen/riscv: extend exception tables with type and data fields Oleksii Kurochko
@ 2026-09-07 15:57   ` Baptiste Le Duc
  2026-09-08  6:06     ` Jan Beulich
  2026-09-08  9:19     ` Oleksii Kurochko
  2026-09-08 13:44   ` Jan Beulich
  1 sibling, 2 replies; 244+ messages in thread
From: Baptiste Le Duc @ 2026-09-07 15:57 UTC (permalink / raw)
  To: Oleksii Kurochko
  Cc: xen-devel, Romain Caritey, Baptiste Le Duc, Zheng Zhang,
	Alistair Francis, Connor Davis, Andrew Cooper, Anthony PERARD,
	Michal Orzel, Jan Beulich, Julien Grall, Roger Pau Monné,
	Stefano Stabellini

> Extend the RISC-V exception table format to include a type and
> auxiliary data field.
> 
> The existing format only supports simple fixups. Some use cases require
> additional context from the fault (e.g. capturing trap information),
> which cannot be expressed with the current EX_TYPE_FIXUP entries.
> 
> Introduce a generic ASM_EXTABLE_RAW() helper to describe entries with a
> handler type and associated data. Reimplement ASM_EXTABLE() in terms of
> it using EX_TYPE_FIXUP for compatibility.
> 
> Add EX_TYPE_TRAP_INFO to allow handlers to retrieve trap state
> (sepc/scause/stval) and pass it to the fixup path. The data field is
> used to encode which GPR contains a pointer to a struct trap_info.
> 
> Provide ASM_EXTABLE_TRAP_INFO() as a convenience wrapper for this case.
> 
> Also add gpr-num.h, providing symbolic GPR numbers for use in assembly
> and inline asm. This is derived from Linux 6.16 with minor adjustments such
> as using .irp instead of open-coding the same using a set of .equ.
> 
> Update the exception handling code to dispatch based on the entry type.
> 
> Signed-off-by: Oleksii Kurochko <oleksii.kurochko@gmail.com>
>
> diff --git a/xen/arch/riscv/extable.c b/xen/arch/riscv/extable.c
> index 5b89c4278c..6470198d01 100644
> --- a/xen/arch/riscv/extable.c
> +++ b/xen/arch/riscv/extable.c
> @@ -6,8 +6,10 @@
>  #include <xen/sort.h>
>  #include <xen/virtual_region.h>
>  
> +#include <asm/csr.h>
>  #include <asm/extable.h>
>  #include <asm/processor.h>
> +#include <asm/traps.h>
>  
>  #define EX_FIELD(ptr, field) ((unsigned long)&(ptr)->field + (ptr)->field)
>  
> @@ -32,6 +34,12 @@ static void __init cf_check swap_ex(void *a, void *b)
>  
>      x->fixup = y->fixup + delta;
>      y->fixup = tmp.fixup - delta;
> +
> +    x->type = y->type;
> +    y->type = tmp.type;
> +
> +    x->data = y->data;
> +    y->data = tmp.data;
>  }
>  
>  static int cf_check cmp_ex(const void *a, const void *b)
> @@ -59,7 +67,49 @@ static void ex_handler_fixup(const struct exception_table_entry *ex,
>      regs->sepc = ex_fixup(ex);
>  }
>  
> -bool fixup_exception(struct cpu_user_regs *regs)
> +#define CHECK_GPR_INDEX(num, name)                      \
> +    BUILD_BUG_ON(offsetof(struct cpu_user_regs, name)   \
> +                 != (num) * sizeof(unsigned long));
> +
> +static unsigned long regs_get_gpr(const struct cpu_user_regs *regs,
> +                                  unsigned int num)
> +{
> +    /*
> +     * The GPR number -> struct index mapping below relies on x0..x31 being
> +     * laid out at the start of struct cpu_user_regs in architectural order,
> +     * matching the register numbers GPR_LIST() hands to the assembler.
> +     */
> +    GPR_LIST(CHECK_GPR_INDEX)
> +
> +    ASSERT(num < 32);
> +
> +    return ((const unsigned long *)regs)[num];
> +}
> +
> +#undef CHECK_GPR_INDEX
> +
> +static void ex_handler_trap_info(const struct exception_table_entry *ex,
> +                                 struct cpu_user_regs *regs,
> +                                 unsigned long cause)
> +{
> +    struct trap_info *trap_info =
> +        (struct trap_info *)regs_get_gpr(regs, ex->data);
> +
> +    BUG_ON(!trap_info);
> +
> +    /*
> +     * Only stval still needs a CSR read: sepc and scause were already
> +     * captured by the trap entry path and do_trap() respectively. Latch
> +     * trap_info->sepc before regs->sepc is pointed at the fixup code.
> +     */
> +    trap_info->sepc = regs->sepc;
> +    trap_info->scause = cause;
> +    trap_info->stval = csr_read(CSR_STVAL);
> +
> +    regs->sepc = ex_fixup(ex);
> +}
> +
> +bool fixup_exception(struct cpu_user_regs *regs, unsigned long cause)
>  {
>      unsigned long pc = regs->sepc;
>      const struct virtual_region *region = find_text_region(pc);
> @@ -77,7 +127,23 @@ bool fixup_exception(struct cpu_user_regs *regs)
>      if ( !ex )
>          return false;
>  
> -    ex_handler_fixup(ex, regs);
> +    switch ( ex->type )
> +    {
> +    case EX_TYPE_FIXUP:
> +        ex_handler_fixup(ex, regs);
> +        break;
> +
> +    case EX_TYPE_TRAP_INFO:
> +        ex_handler_trap_info(ex, regs, cause);
> +        break;
> +
> +    default:
> +        printk(XENLOG_ERR
> +               "Unsupported exception table entry type %u for pc %#lx\n",
> +               ex->type, pc);
> +
> +        return false;
> +    }
>  
>      return true;
>  }
> diff --git a/xen/arch/riscv/include/asm/extable.h b/xen/arch/riscv/include/asm/extable.h
> index c0128a9181..7378f86e7e 100644
> --- a/xen/arch/riscv/include/asm/extable.h
> +++ b/xen/arch/riscv/include/asm/extable.h
> @@ -3,17 +3,24 @@
>  #ifndef ASM__RISCV__ASM_EXTABLE_H
>  #define ASM__RISCV__ASM_EXTABLE_H
>  
> +#include <asm/gpr-num.h>
> +
> +#define EX_TYPE_FIXUP       0
> +#define EX_TYPE_TRAP_INFO   1
> +
>  #ifdef __ASSEMBLER__
>  
> -#define ASM_EXTABLE(insn, fixup) \
> -    .pushsection .ex_table, "a"; \
> -    .balign     4;               \
> -    .word       (insn) - .;      \
> -    .word       (fixup) - .;     \
> +#define ASM_EXTABLE_RAW(insn, fixup, type, data)    \
> +    .pushsection .ex_table, "a";                    \
> +    .balign     4;                                  \
> +    .word       (insn) - .;                         \
> +    .word       (fixup) - .;                        \
> +    .half       (type);                             \
> +    .half       (data);                             \
>      .popsection
>  
> -.macro asm_extable, insn, fixup
> -    ASM_EXTABLE(\insn, \fixup)
> +.macro _asm_extable, insn, fixup
> +    ASM_EXTABLE_RAW(\insn, \fixup, EX_TYPE_FIXUP, 0)
>  .endm
>  
>  #else /* __ASSEMBLER__ */
> @@ -23,20 +30,36 @@
>  
>  struct cpu_user_regs;
>  
> -#define ASM_EXTABLE(insn, fixup)      \
> -    ".pushsection .ex_table, \"a\"\n" \
> -    ".balign    4\n"                  \
> -    ".word      (" #insn " - .)\n"    \
> -    ".word      (" #fixup " - .)\n"   \
> +#define ASM_EXTABLE_RAW(insn, fixup, type, data)    \
> +    ".pushsection .ex_table, \"a\"\n"               \
> +    ".balign    4\n"                                \
> +    ".word      (" insn ") - .\n"                   \
> +    ".word      (" fixup ") - .\n"                  \
> +    ".half      (" type ")\n"                       \
> +    ".half      (" data ")\n"                       \
>      ".popsection\n"
>  
> +#define ASM_EXTABLE(insn, fixup)    \
> +    ASM_EXTABLE_RAW(#insn, #fixup, __stringify(EX_TYPE_FIXUP), "0")
> +
> +#define EX_TRAP_INFO_REG(gpr)   \
> +    "(.L_gpr_num_" #gpr ")"
> +
> +#define ASM_EXTABLE_TRAP_INFO(insn, fixup, data)                    \
> +    DEFINE_ASM_GPR_NUMS                                             \
> +    ASM_EXTABLE_RAW(#insn, #fixup, __stringify(EX_TYPE_TRAP_INFO),  \
> +                    EX_TRAP_INFO_REG(data))
> +
>  /*
> - * The exception table consists of pairs of relative offsets: the first
> - * is the relative offset to an instruction that is allowed to fault,
> - * and the second is the relative offset at which the program should
> - * continue. No general-purpose registers are modified by the exception
> - * handling mechanism itself, so it is up to the fixup code to handle
> - * any necessary state cleanup.
> + * Each exception table entry consists of two relative offsets and a
> + * handler description: `insn` is the relative offset to an instruction
> + * that is allowed to fault, `fixup` is the relative offset at which the
> + * program should continue, `type` selects how the exception is handled
> + * (EX_TYPE_*), and `data` holds auxiliary information for the handler
> + * (e.g. for EX_TYPE_TRAP_INFO, the number of the GPR that contains a
> + * pointer to a struct trap_info). No general-purpose registers are
> + * modified by the exception handling mechanism itself, so it is up to
> + * the fixup code to handle any necessary state cleanup.
>   *
>   * The exception table and fixup code live out of line with the main
>   * instruction path. This means when everything is well, we don't even
> @@ -45,14 +68,15 @@ struct cpu_user_regs;
>   */
>  struct exception_table_entry {
>      int32_t insn, fixup;
> +    uint16_t type, data;
>  };
>  
>  extern struct exception_table_entry __start___ex_table[];
>  extern struct exception_table_entry __stop___ex_table[];
>  
>  void sort_exception_tables(void);
> -bool fixup_exception(struct cpu_user_regs *regs);
> +bool fixup_exception(struct cpu_user_regs *regs, unsigned long cause);
>  
> -#endif /* __ASSEMBLY__ */
> +#endif /* __ASSEMBLER__ */
>  
>  #endif /* ASM__RISCV__ASM_EXTABLE_H */
> diff --git a/xen/arch/riscv/include/asm/gpr-num.h b/xen/arch/riscv/include/asm/gpr-num.h
> new file mode 100644
> index 0000000000..3b97a72e6c
> --- /dev/null
> +++ b/xen/arch/riscv/include/asm/gpr-num.h
> @@ -0,0 +1,37 @@
> +/* SPDX-License-Identifier: GPL-2.0-only */
> +#ifndef RISCV_GPR_NUM_H
> +#define RISCV_GPR_NUM_H
Nit: commit message says this is derived from Linux 6.16. Other
imported RISC-V headers here carry an in-file note (bitops.h: "Based on
linux/arch/.../bitops.h") but this file doesn't.
> +/*
> + * GPRs by ABI name, together with their register number (x0 .. x31).
> + *
> + * This is the single source of truth for the mapping: it generates the
> + * .L_gpr_num_<name> assembler symbols used to turn a register name emitted
> + * by the compiler into a register number, and struct cpu_user_regs is
> + * checked against it at build time (see regs_get_gpr()). Neither list can
> + * therefore be changed without the other.
> + */
> +#define GPR_LIST(x)                                 \
> +    x(0,  zero) x(1,  ra)  x(2,  sp)  x(3,  gp)     \
> +    x(4,  tp)   x(5,  t0)  x(6,  t1)  x(7,  t2)     \
> +    x(8,  s0)   x(9,  s1)  x(10, a0)  x(11, a1)     \
> +    x(12, a2)   x(13, a3)  x(14, a4)  x(15, a5)     \
> +    x(16, a6)   x(17, a7)  x(18, s2)  x(19, s3)     \
> +    x(20, s4)   x(21, s5)  x(22, s6)  x(23, s7)     \
> +    x(24, s8)   x(25, s9)  x(26, s10) x(27, s11)    \
> +    x(28, t3)   x(29, t4)  x(30, t5)  x(31, t6)
> +
> +#ifdef __ASSEMBLER__
> +
> +#define GPR_NUM_EQU(num, name)  .equ .L_gpr_num_##name, num;
> +GPR_LIST(GPR_NUM_EQU)
> +#undef GPR_NUM_EQU
> +
> +#else /* __ASSEMBLER__ */
> +
> +#define GPR_NUM_EQU(num, name)  ".equ .L_gpr_num_" #name ", " #num "\n"
> +#define DEFINE_ASM_GPR_NUMS     GPR_LIST(GPR_NUM_EQU)
> +
> +#endif /* __ASSEMBLER__ */
> +
> +#endif /* RISCV_GPR_NUM_H */
> diff --git a/xen/arch/riscv/include/asm/processor.h b/xen/arch/riscv/include/asm/processor.h
> index b1745c1071..e7b0f2321a 100644
> --- a/xen/arch/riscv/include/asm/processor.h
> +++ b/xen/arch/riscv/include/asm/processor.h
> @@ -12,7 +12,19 @@
>  
>  #ifndef __ASSEMBLER__
>  
> -/* On stack VCPU state */
> +/*
> + * On stack VCPU state.
> + *
> + * x0..x31 must remain at the start of this structure, in architectural
> + * register-number order: code which resolves a register number to its saved
> + * value indexes this structure directly (instruction emulation via
> + * REG_PTR() from asm/riscv_encoding.h, exception table fixups via
> + * regs_get_gpr()). ->zero therefore has to stay at offset 0 and must always
> + * read as 0, since it supplies the value of x0 when x0 is used as a source
> + * operand. The layout is checked against GPR_LIST() at build time; see
> + * regs_get_gpr() in extable.c. Do not reorder these fields or insert
> + * anything between them.
> + */
>  {
>      unsigned long zero;
Comment claims ->zero "must always read as 0" as it's hard-wired to zero
by the HW, but nothing enforces that, it's still a plain writable
unsigned long. Maybe a write-side counterpart that special-cases num==0
as a no-op, or with a minimum ASSERT(num != 0) / BUG_ON(num == 0) to
anticipate any future forbidden writes.

-- 
Baptiste Le Duc <baptiste.le-duc@vates.tech>


^ permalink raw reply	[flat|nested] 244+ messages in thread

* Re: [PATCH v2 17/39] xen/riscv: decouple INSN_PSEUDO_VS_* from the hypervisor's XLEN
  2026-08-27 15:21 ` [PATCH v2 17/39] xen/riscv: decouple INSN_PSEUDO_VS_* from the hypervisor's XLEN Oleksii Kurochko
@ 2026-09-07 15:57   ` Baptiste Le Duc
  2026-09-08  9:34     ` Oleksii Kurochko
  0 siblings, 1 reply; 244+ messages in thread
From: Baptiste Le Duc @ 2026-09-07 15:57 UTC (permalink / raw)
  To: Oleksii Kurochko
  Cc: xen-devel, Romain Caritey, Baptiste Le Duc, Zheng Zhang,
	Alistair Francis, Connor Davis, Andrew Cooper, Anthony PERARD,
	Michal Orzel, Jan Beulich, Julien Grall, Roger Pau Monné,
	Stefano Stabellini

> htinst reports a pseudoinstruction when a guest page fault is taken on an
> implicit memory access done for VS-stage address translation. Four such
> values are defined, differing in the access type (read or write) and in the
> access width: 4 bytes (0x2000/0x2020) or 8 bytes (0x3000/0x3020).
> 
> That width is the width of a VS-stage PTE, i.e. it follows the guest's
> paging mode (4 bytes for Sv32, 8 bytes for Sv39 and wider) and has nothing
> to do with the XLEN Xen itself is built for. Selecting just one pair with
> where a guest running with VSXL=32 and Sv32 in vsatp produces the 4-byte
Sentence is broken. Guess you mean "Selecting just one pair based on Xen's XLEN misses the case where
…".

> forms. Such an htinst would not be recognized as a pseudoinstruction and the
> fault would be mistaken for an ordinary MMIO trap: Xen would fetch and
> decode whatever instruction sepc happens to point at (unrelated to the
> access which faulted) and emulate it against a guest physical address
> derived from htval, which for an implicit access holds the address of a
> VS-stage PTE rather than of any access the guest performed.
> 
> Define all four values unconditionally instead, named after the access width
> they encode rather than after the build's XLEN. On RV32 the 8-byte forms
> simply never occur, so recognizing them costs nothing.
> 
> Dropping the ladder loses no build-time coverage: a build for an XLEN other
> than 32 or 64 already fails on the equivalent ladders in asm/asm.h and
> asm/config.h, so no replacement #error is needed here. Adding one keyed on
> CONFIG_RISCV_* would in any case re-introduce exactly the conflation this
> patch removes.
> 
> This diverges from the imported version of riscv_encoding.h.
> 
> No functional change: the values have no user yet.
> 
> Signed-off-by: Oleksii Kurochko <oleksii.kurochko@gmail.com>
>
> diff --git a/xen/arch/riscv/include/asm/riscv_encoding.h b/xen/arch/riscv/include/asm/riscv_encoding.h
> index c63e5e3046..2d2e7e11b3 100644
> --- a/xen/arch/riscv/include/asm/riscv_encoding.h
> +++ b/xen/arch/riscv/include/asm/riscv_encoding.h
> @@ -839,25 +839,17 @@
>  #define INSN_MASK_FENCE_TSO		0xffffffff
>  #define INSN_MATCH_FENCE_TSO		0x8330000f
>  
> -#if __riscv_xlen == 64
> -
>  /* 64-bit read for VS-stage address translation (RV64) */
> -#define INSN_PSEUDO_VS_LOAD		0x00003000
> +#define INSN_PSEUDO_VS_LOAD64		0x00003000
>  
>  /* 64-bit write for VS-stage address translation (RV64) */
> -#define INSN_PSEUDO_VS_STORE	0x00003020
> -
> -#elif __riscv_xlen == 32
> +#define INSN_PSEUDO_VS_STORE64		0x00003020
>  
>  /* 32-bit read for VS-stage address translation (RV32) */
> -#define INSN_PSEUDO_VS_LOAD		0x00002000
> +#define INSN_PSEUDO_VS_LOAD32		0x00002000
>  
>  /* 32-bit write for VS-stage address translation (RV32) */
> -#define INSN_PSEUDO_VS_STORE	0x00002020
> -
Whole point of patch is these no longer depend on build XLEN, yet
comments still say "(RV64)"/"(RV32)" which could be confusing. Maybe it
should be better to indicate, as the spec does, that RV32 values are
used when VSXLEN=32 (only sv32 paging mode) and RV64 values when
VSXLEN=64 (sv39+ paging modes).

-- 
Baptiste Le Duc <baptiste.le-duc@vates.tech>


^ permalink raw reply	[flat|nested] 244+ messages in thread

* Re: [PATCH v2 20/39] xen/riscv: detect Shtvala
  2026-08-27 15:21 ` [PATCH v2 20/39] xen/riscv: detect Shtvala Oleksii Kurochko
@ 2026-09-07 15:57   ` Baptiste Le Duc
  2026-09-08 10:15     ` Oleksii Kurochko
  0 siblings, 1 reply; 244+ messages in thread
From: Baptiste Le Duc @ 2026-09-07 15:57 UTC (permalink / raw)
  To: Oleksii Kurochko
  Cc: xen-devel, Romain Caritey, Baptiste Le Duc, Zheng Zhang,
	Alistair Francis, Connor Davis, Andrew Cooper, Anthony PERARD,
	Michal Orzel, Jan Beulich, Julien Grall, Roger Pau Monné,
	Stefano Stabellini

> Shtvala says that htval is written with the faulting guest physical address
> on a guest-page fault. The H extension itself allows an implementation to
> write htval with either that address or with zero, so where the extension is
> absent a zero htval cannot be told apart from a genuine fault on guest
> physical address 0-3.
>
> It is not offered to guests. Shtvala describes the HS-mode trap interface,
> which a VS-mode guest never sees, and the H extension it belongs to is
> already withheld from guests. Its guest-facing counterpart is a separate
> extension, Shvstvala.
> 
> Signed-off-by: Oleksii Kurochko <oleksii.kurochko@gmail.com>
>
I thought this commit message is not very clear. Here is a more direct
suggestion:

```
The H extension allows htval, on a guest-page fault, to be written with
either the faulting guest physical address or zero. Shtvala extension
removes the ambiguity of this zero-write by guaranteeing that htval is
written with the faulting guest physical address in every circumstance
permitted by the ISA.

Not offered to guests: Shtvala describes htval, an HS-mode-only trap
register a VS-mode guest never touches and the H extension it belongs to
is already hidden from guests. The guest-visible equivalent is a
separate extension, Shvstvala, covering vstval instead.
```

Btw, I saw Linux has Documentation/devicetree/bindings/riscv/extensions.yaml
which describes all of the extensions supported, don't you think it would
be useful to have the same in docs/misc/devicetree/...?

-- 
Baptiste Le Duc <baptiste.le-duc@vates.tech>


^ permalink raw reply	[flat|nested] 244+ messages in thread

* Re: [PATCH v2 19/39] xen/riscv: implement trap redirection to a guest
  2026-08-27 15:21 ` [PATCH v2 19/39] xen/riscv: implement trap redirection to a guest Oleksii Kurochko
@ 2026-09-07 15:57   ` Baptiste Le Duc
  2026-09-08 10:01     ` Oleksii Kurochko
  2026-09-08 14:16   ` Jan Beulich
  2026-09-08 14:16   ` Jan Beulich
  2 siblings, 1 reply; 244+ messages in thread
From: Baptiste Le Duc @ 2026-09-07 15:57 UTC (permalink / raw)
  To: Oleksii Kurochko
  Cc: xen-devel, Romain Caritey, Baptiste Le Duc, Zheng Zhang,
	Alistair Francis, Connor Davis, Andrew Cooper, Anthony PERARD,
	Michal Orzel, Jan Beulich, Julien Grall, Roger Pau Monné,
	Stefano Stabellini

> Some traps taken by Xen on behalf of a guest can't or shouldn't be handled
> by the hypervisor and have to be reflected to the guest's own S-mode trap
> handler instead: the access faults which handle_guest_page_fault() injects
> for a fault that can never become an emulated access, and, later on, a
> fault taken by the hlv/hlvx sequences of riscv_read_guest() while
> accessing guest memory on a vCPU's behalf.
>
Access faults from handle_guest_page_fault() aren't "taken by Xen on
behalf of a guest" as those are guest-page faults taken directly from the
guest's own execution. Xen just decides they can't be emulated and
reflects them back as access faults. Only the hlv/hlvx case is Xen
trapping on the guest's behalf (Xen itself executes the faulting access).
Conflating the two under one description makes the paragraph confusing.

Suggest splitting into two:

    Two kinds of traps can't or shouldn't be handled by the hypervisor and
    have to be reflected to the guest's own S-mode trap handler instead:

    - Traps Xen takes on the guest's behalf: the hlv/hlvx sequences
    riscv_read_guest() uses to access guest memory.
    - Access faults handle_guest_page_fault() injects for a guest-page
    fault that can never become an emulated access.

> 
> Implement trap_redirect(), until now a BUG_ON() placeholder, for that
> purpose. It makes the trap appear to the guest as if it had been taken
> directly in VS-mode: the trap information is transferred to the guest's
> virtual supervisor CSRs and the vCPU is resumed at its exception vector in
> supervisor mode, following the trap entry rules of the RISC-V privileged
> specification.
> 
> Add the STVEC_* definitions needed to tell the BASE and MODE fields of
> vstvec apart.
> 
> The implementation is based on kvm_riscv_vcpu_trap_redirect() from Linux,
> with a few deviations:
>  - The function reads and writes physical VS-mode CSRs, so it is only
>    meaningful for the currently running vCPU. Instead of taking a
>    struct vcpu argument, it always operates on current.
>  - The MODE field of vstvec is masked off explicitly when computing the
>    exception target PC (exceptions always vector to BASE), rather than
>    relying on the hardwired zero bit of sepc to drop it on VM entry.
>  - Assertions document the preconditions: the trap must have been taken
>    from virtualized mode (hstatus.SPV set), and only synchronous
>    exceptions may be redirected - interrupts must be injected via hvip
>    instead, so that the hardware performs VS-mode trap entry itself,
>    respecting vsstatus.SIE and vectored vstvec dispatch.
> 
> Signed-off-by: Oleksii Kurochko <oleksii.kurochko@gmail.com>
>
> diff --git a/xen/arch/riscv/include/asm/riscv_encoding.h b/xen/arch/riscv/include/asm/riscv_encoding.h
> index 2d2e7e11b3..b2071f4758 100644
> --- a/xen/arch/riscv/include/asm/riscv_encoding.h
> +++ b/xen/arch/riscv/include/asm/riscv_encoding.h
> @@ -109,6 +109,12 @@
>  #define SIP_SSIP			MIP_SSIP
>  #define SIP_STIP			MIP_STIP
>  
> +/* stvec/vstvec: MODE is bits [1:0], BASE is bits [XLEN-1:2] */
> +#define STVEC_MODE_MASK			_UL(0x3)
> +#define STVEC_MODE_DIRECT		_UL(0x0)
> +#define STVEC_MODE_VECTORED		_UL(0x1)
> +#define STVEC_BASE_MASK			(~STVEC_MODE_MASK)
Nit: STVEC_MODE_DIRECT and STVEC_MODE_VECTORED aren't used anywhere in
this patch (only STVEC_BASE_MASK is). Either use them where you decide
exceptions always target BASE regardless of MODE, or drop them until a
patch that needs them.

-- 
Baptiste Le Duc <baptiste.le-duc@vates.tech>


^ permalink raw reply	[flat|nested] 244+ messages in thread

* Re: [PATCH v2 18/39] xen/riscv: add guest page fault handling stub
  2026-08-27 15:21 ` [PATCH v2 18/39] xen/riscv: add guest page fault handling stub Oleksii Kurochko
@ 2026-09-07 15:57   ` Baptiste Le Duc
  2026-09-08  9:49     ` Oleksii Kurochko
  2026-09-08 14:10   ` Jan Beulich
  1 sibling, 1 reply; 244+ messages in thread
From: Baptiste Le Duc @ 2026-09-07 15:57 UTC (permalink / raw)
  To: Oleksii Kurochko
  Cc: xen-devel, Romain Caritey, Baptiste Le Duc, Zheng Zhang,
	Alistair Francis, Connor Davis, Andrew Cooper, Anthony PERARD,
	Michal Orzel, Jan Beulich, Julien Grall, Roger Pau Monné,
	Stefano Stabellini

> Add a handler for guest page faults and hook it into the trap path,
> providing the trap-side entry point which will later feed the MMIO
> dispatch.
> 
> This will be used, for example, to trap accesses to APLIC registers so
> that a guest can initialize and drive an emulated interrupt controller.
> 
> Two of the situations handled here are already decided, as neither can
> ever be turned into an emulated access:
> 
>  - A fault reported with a pseudoinstruction in htinst was taken on an
>    implicit access made for VS-stage address translation, so htval holds
>    the address of a VS-stage PTE rather than of anything the guest asked
>    for, and the guest physical address behind the original access is not
>    known. This is orthogonal to the cause and can accompany any of the
>    three, which is why it is checked first. scause keeps reporting the
>    type of the original access, and on bare hardware a PTE which cannot
>    be read raises an access fault of exactly that type, so reflect one
>    back to the guest.
> 
>  - A fetch fault means the guest tried to execute from a guest physical
>    address which is unmapped or which G-stage does not allow to be
>    executed. On bare hardware a fetch from physical memory which does
>    not exist, or which may not be executed, raises an instruction access
>    fault, so reflect one back too.
> 
> Explicit loads and stores are where MMIO emulation will hook in.
> 
> Neither of the two paths above consults the p2m first, and neither will
> the MMIO one: RISC-V has no populate-on-demand, no paging and no
> mem_access, so every guest mapping is established eagerly and a G-stage
> fault never denotes a mapping Xen could install to let the faulting
> access complete.
> 
> Both of the helpers this leans on, resolve_faulting_gpa() and
> trap_redirect(), are BUG_ON() placeholders for now, so each of the three
> causes currently takes the host down rather than the domain. That is no
> worse than before this patch, where the same causes fell through to
> do_unexpected_trap() and die(). Implementing the helpers is left to
> later patches.
> 
> Signed-off-by: Oleksii Kurochko <oleksii.kurochko@gmail.com>
>

I think the commit message it not very clear as it conflates two
different countable things (the 3 fault causes reported via scause vs.
the pseudoinstruction condition, which is orthogonal and can accompany
any of them), which makes "two situations decided" or "any of the three"
hard to follow on first read. 

Here is a proposition with a split to distinguish fetch-fault and
pseudoinstruction cases into explicit bullets with their outcome stated
("Decided here"), and added spec-mentioned conditions about the
pseudoinstruction's existence conditions:
```
    Add a handler for guest page faults and hook it into the trap path,
    providing the trap-side entry point which will later feed the MMIO
    dispatch.

    This will be used, for example, to trap accesses to APLIC registers so
    that a guest can initialize and drive an emulated interrupt controller.

    A G-stage (stage-2) fault has one of three causes, reported via scause:

        - Fetch fault: guest tried to execute from a guest-physical address
        that is unmapped or that G-stage marks non-executable. Never
        emulatable (nothing to emulate a fetch into). On real hardware
        this raises an instruction access fault, so Xen reflects the same
        fault back to the guest. Decided here.

        - Load fault / Store fault: left undecided by this patch, this is
        where MMIO emulation will hook in later.

    Any of these three faults can instead be reported via a pseudoinstruction
    in htinst, when both:

        (a) the fault occurred on an implicit access Xen made to walk a
            VS-stage page table, and
        (b) htval holds a nonzero value: the guest-physical address of that
            VS-stage PTE, not of the guest's original access.

    However, none of these paths consult the p2m first, and the future MMIO path
    won't either: RISC-V has no populate-on-demand, no paging, and no
    mem_access, so every guest mapping is established eagerly. A G-stage
    fault therefore never indicates a mapping Xen could lazily resolve to
    let the access complete.

    Both helpers this handler relies on, resolve_faulting_gpa() and
    trap_redirect(), are BUG_ON() placeholders for now, so all three causes
    currently take the host down instead of just the guest. This is no worse
    than before this patch, where these traps fell through to
    do_unexpected_trap() and die().
```

-- 
Baptiste Le Duc <baptiste.le-duc@vates.tech>


^ permalink raw reply	[flat|nested] 244+ messages in thread

* Re: [PATCH v2 16/39] xen/riscv: extend exception tables with type and data fields
  2026-09-07 15:57   ` Baptiste Le Duc
@ 2026-09-08  6:06     ` Jan Beulich
  2026-09-08  8:18       ` Baptiste Le Duc
  2026-09-08  9:19     ` Oleksii Kurochko
  1 sibling, 1 reply; 244+ messages in thread
From: Jan Beulich @ 2026-09-08  6:06 UTC (permalink / raw)
  To: Baptiste Le Duc, Oleksii Kurochko
  Cc: xen-devel, Romain Caritey, Zheng Zhang, Alistair Francis,
	Connor Davis, Andrew Cooper, Anthony PERARD, Michal Orzel,
	Julien Grall, Roger Pau Monné, Stefano Stabellini

On 07.09.2026 17:57, Baptiste Le Duc wrote:
>> --- a/xen/arch/riscv/include/asm/processor.h
>> +++ b/xen/arch/riscv/include/asm/processor.h
>> @@ -12,7 +12,19 @@
>>  
>>  #ifndef __ASSEMBLER__
>>  
>> -/* On stack VCPU state */
>> +/*
>> + * On stack VCPU state.
>> + *
>> + * x0..x31 must remain at the start of this structure, in architectural
>> + * register-number order: code which resolves a register number to its saved
>> + * value indexes this structure directly (instruction emulation via
>> + * REG_PTR() from asm/riscv_encoding.h, exception table fixups via
>> + * regs_get_gpr()). ->zero therefore has to stay at offset 0 and must always
>> + * read as 0, since it supplies the value of x0 when x0 is used as a source
>> + * operand. The layout is checked against GPR_LIST() at build time; see
>> + * regs_get_gpr() in extable.c. Do not reorder these fields or insert
>> + * anything between them.
>> + */
>>  {
>>      unsigned long zero;
> Comment claims ->zero "must always read as 0" as it's hard-wired to zero
> by the HW, but nothing enforces that, it's still a plain writable
> unsigned long. Maybe a write-side counterpart that special-cases num==0
> as a no-op, or with a minimum ASSERT(num != 0) / BUG_ON(num == 0) to
> anticipate any future forbidden writes.

With polarity inverted, though.

Jan


^ permalink raw reply	[flat|nested] 244+ messages in thread

* Re: [PATCH v2 16/39] xen/riscv: extend exception tables with type and data fields
  2026-09-08  6:06     ` Jan Beulich
@ 2026-09-08  8:18       ` Baptiste Le Duc
  0 siblings, 0 replies; 244+ messages in thread
From: Baptiste Le Duc @ 2026-09-08  8:18 UTC (permalink / raw)
  To: Jan Beulich
  Cc: Baptiste Le Duc, Oleksii Kurochko, xen-devel, Romain Caritey,
	Zheng Zhang, Alistair Francis, Connor Davis, Andrew Cooper,
	Anthony PERARD, Michal Orzel, Julien Grall, Roger Pau Monné,
	Stefano Stabellini

On 2026-09-08 08:06:29+02:00, Jan Beulich wrote:
> On 07.09.2026 17:57, Baptiste Le Duc wrote:
> 
> >> --- a/xen/arch/riscv/include/asm/processor.h
> > Comment claims ->zero "must always read as 0" as it's hard-wired to zero
> > by the HW, but nothing enforces that, it's still a plain writable
> > unsigned long. Maybe a write-side counterpart that special-cases num==0
> > as a no-op, or with a minimum ASSERT(num != 0) / BUG_ON(num == 0) to
> > anticipate any future forbidden writes.
> 
> With polarity inverted, though.
Yes sorry, it should be ASSERT(num == 0) / BUG_ON(num != 0)
> 
> Jan




^ permalink raw reply	[flat|nested] 244+ messages in thread

* Re: [PATCH v2 12/39] xen/riscv: implement vCPU context switching
  2026-09-07  8:17       ` Jan Beulich
@ 2026-09-08  9:06         ` Oleksii Kurochko
  0 siblings, 0 replies; 244+ messages in thread
From: Oleksii Kurochko @ 2026-09-08  9:06 UTC (permalink / raw)
  To: Jan Beulich
  Cc: xen-devel, Romain Caritey, Zheng Zhang, Alistair Francis,
	Connor Davis, Andrew Cooper, Anthony PERARD, Michal Orzel,
	Julien Grall, Roger Pau Monné, Stefano Stabellini,
	Baptiste Le Duc



On 9/7/26 10:17 AM, Jan Beulich wrote:
> On 04.09.2026 16:55, Oleksii Kurochko wrote:
>>
>>
>> On 9/4/26 10:26 AM, Baptiste Le Duc wrote:
>>> As I understand it, a generation wrap doesn't retire a single VMID, it
>>> resets next_vmid to 1, which makes every VMID in 1..max_vmid reusable
>>> again in the new generation. We do a full (local) flush at that point to
>>> avoid two different vCPUs ending up with the same VMID valid at once,
>>> across generations.
>>>
>>> If this is correct, doing a full flush there also throws away entries
>>> for the current vCPU that a local HFENCE.GVMA(vmid) per retired VMID
>>
>> We are doing flushed for the pCPU on which a vCPU is ran.
>>
>>> could have preserved. A local-flush-per-VMID approach could also
>>> reduce how often we need a full flush at all.
>>>
>>> Is there a reason we don't do local flushing instead? I see x86 and KVM
>>> use the same flush-all design on wrap, so I assume there's a reason I'm
>>> missing, I'd like to understand it.
>>
>> What do you mean here by "local flushing instead"? We are doing local flush:
>>
>>       if ( unlikely(need_flush) )
>>           local_hfence_gvma_all();
>>
>> Do you mean why we don't do hfence_gvma only for specific VMID?
>>
>> A vCPU's VMID is valid only while vmid->generation == data->generation
>> (vmid.c:141). Bumping the generation invalidates every vCPU's VMID on
>> this hart simultaneously, so every G-stage entry in the TLB (whatever
>> number it is tagged with) belongs to a (vcpu, vmid) binding that can
>> never be consulted again. Each of those vCPUs will be handed a fresh
>> number on its next vmenter before it can run.
>>
>> That includes the current vCPU, which is the case you're worried about.
>> At the wrap it is being assigned VMID 1, not its previous number, so its
>> old entries are unreachable regardless of whether we flush them.
>> hfence.gvma per retired VMID would preserve them physically but not
>> usefully [A concrete example. vCPU A is running on the hart with
>> VMID=100 in generation G; the TLB holds G-stage entries tagged VMID=100.
>> A wrap occurs: the generation becomes G+1, next_vmid is reset to 1, and
>> A is assigned VMID=1 (vmid.c:154). From that moment on, the hardware
>> looks up translations for A under the tag VMID=1. The entries tagged 100
>> will no longer match anything: A isn't 100 any more, and no one else
>> will be handed 100 until the next wrap.
>> So A loses its warm entries not because we did an hfence.gvma, but
>> because it was renumbered. The flush has nothing to do with it. It
>> merely discards what has already become unreachable.]; they'd just
>> occupy TLB capacity until natural eviction. Preserving them would
>> require a different allocator that keeps a vCPU's number stable across a
>> rollover (Linux/KVM-arm64 style, with an active/reserved set pinning
>> live ASIDs), not a different flush granularity.
>>
>> So x86's hvm_asid_handle_vmenter() and KVM's equivalent aren't doing
>> this out of inertia — with a round-robin generation allocator, the full
>> flush is free of useful collateral damage and strictly cheaper than the
>> alternative. Preserving entries across a rollover is a real
>> optimisation, but it's an allocator change, and IMO worth doing only if
>> profiling shows the wrap flush matters.
>>
>>>
>>>> H/VS CSRs, virtual timer and P2M context, and __context_switch() in assembly,
>>>> which switches Xen's own callee-saved state (and thereby the stack) from
>>>> prev to next. Virtual interrupt controller context switch will be
>>>> introduced later.
>>>>
>>>> Add offsets of struct arch_vcpu's xen_saved_context to asm-offsets.c for
>>>> use by __context_switch().
>>>>
>>>> henvcfg and htimedelta are 64-bit on both RV32 and RV64, so store them as
>>>> uint64_t and use csr_{read,write}64() instead of open-coding accesses to
>>>> the high halves.
>>>>
>>>> A hart which drops out of a domain's dirty_cpumask stops being a target
>>>> of p2m_tlb_flush() while its TLB may still hold G-stage translations of
>>>> that domain, and neither the vCPU which just ran nor any other vCPU of
>>>> that domain which ran there earlier has had its VMID invalidated. Move
>>>> the hart to a new VMID generation at that point: a VMID number is never
>>>> re-used until a full local flush has happened, hence none of those
>>>> translations can be reached again.
>>>>
>>>> Claim the VMID in p2m_ctxt_switch_to() rather than at the next guest
>>>> entry. VMIDs are a per-hart resource, so the (generation, vmid) pair a
>>>> migrating vCPU brings from another hart is meaningless here and may even
>>>> match this hart's current generation, leaving the vCPU under a VMID owned
>>>> by another domain. ctxt_switch_to() invalidates that pair, but claiming a
>>>> replacement only on guest entry is too late: p2m_ctxt_switch_to() has by
>>>> then already made HGATP live, and speculation can populate G-stage entries
>>>> of the incoming domain under the stale VMID. The local flush for a wrapped
>>>> generation moves along with the claim.
>>>>
>>>> That leaves p2m_handle_vmenter() with nothing to do, so drop it together
>>>> with its call from check_for_pcpu_work(). A VMID can only be invalidated
>>>> while its vCPU isn't running: vmid_flush_vcpu() is called for the vCPU
>>>> being switched in, and vmid_flush_hart() runs either from schedule_tail(),
>>>> ahead of ctxt_switch_to(), or from the wrap path of vmid_handle_vmenter()
>>>> itself. A P2M change on another hart doesn't invalidate it either, as
>>>> p2m_tlb_flush() drops the stale entries directly with
>>>> sbi_remote_hfence_gvma() instead of retiring the VMIDs which tag them. A
>>>> guest therefore always runs under the VMID claimed on its way in, and
>>>> there is nothing left for a guest entry hook to notice.
>>>>
>>>> p2m_handle_vmenter() also skipped the HGATP write when the VMID it claimed
>>>> was unchanged. That isn't carried over: HGATP holds the G-stage root as
>>>> well, and skipping the write is only correct where that root is already
>>>> the incoming domain's. On the guest entry path it is, on the context
>>>> switch path it is not.
>>>>
>>>> While at it, fix the inclusion order of headers in asm-offsets.c: Xen's
>>>> headers go first, then arch specific ones.
>>> This could have a dedicated patch no?
>>
>> It could but considering that it is pretty small fix I think it could be
>> part of this patch. If you are insisting on moving that to separate
>> patch I will happy to do that.
>>
>>>> Signed-off-by: Oleksii Kurochko <oleksii.kurochko@gmail.com>
>>>>
>>>> diff --git a/xen/arch/riscv/domain.c b/xen/arch/riscv/domain.c
>>>> index ec327a5e8a..91a46d630f 100644
>>>> --- a/xen/arch/riscv/domain.c
>>>> +++ b/xen/arch/riscv/domain.c
>>>> @@ -11,9 +11,11 @@
>>>>    #include <asm/bitops.h>
>>>>    #include <asm/cpufeature.h>
>>>>    #include <asm/csr.h>
>>>> +#include <asm/current.h>
>>>>    #include <asm/intc.h>
>>>>    #include <asm/mmio.h>
>>>>    #include <asm/riscv_encoding.h>
>>>> +#include <asm/vmid.h>
>>>>    #include <asm/vtimer.h>
>>>>    
>>>>    struct csr_masks {
>>>> @@ -158,6 +160,8 @@ int arch_vcpu_create(struct vcpu *v)
>>>>        if ( is_idle_vcpu(v) )
>>>>            return 0;
>>>>    
>>>> +    v->arch.last_cpu = NR_CPUS;
>>>> +
>>>>        vcpu_csr_init(v);
>>>>    
>>>>        if ( (rc = vcpu_vtimer_init(v)) )
>>>> @@ -329,6 +333,169 @@ int arch_domain_create(struct domain *d,
>>>>        return rc;
>>>>    }
>>>>    
>>>> +static void save_csr_regs(struct vcpu *vcpu)
>>>> +{
>>>> +    /*
>>>> +     * There is no need to save these CSRs as only hypervisor writes them in
>>>> +     * restore_csr_regs() and guest can't access them so they shouldn't be
>>>> +     * stored here. Keep them commented here just for symmetry with the
>>>> +     * restore CSRs register part.
>>>> +     *
>>>> +     * vcpu->arch.hedeleg = csr_read(CSR_HEDELEG);
>>>> +     * vcpu->arch.hideleg = csr_read(CSR_HIDELEG);
>>>> +     * vcpu->arch.henvcfg = csr_read64(CSR_HENVCFG);
>>>> +     * vcpu->arch.hcounteren = csr_read(CSR_HCOUNTEREN);
>>>> +     * vcpu->arch.htimedelta = csr_read64(CSR_HTIMEDELTA);
>>>> +     *
>>>> +     * if ( riscv_isa_extension_available(NULL, RISCV_ISA_EXT_smstateen) )
>>>> +     *     vcpu->arch.hstateen0 = csr_read(CSR_HSTATEEN0);
>>>> +     */
>>>> +
>>>> +    vcpu->arch.hvip = csr_read(CSR_HVIP);
>>>> +
>>>> +    vcpu->arch.vsstatus = csr_read(CSR_VSSTATUS);
>>>> +    vcpu->arch.vsie = csr_read(CSR_VSIE);
>>>
>>>
>>>> +    vcpu->arch.vstvec = csr_read(CSR_VSTVEC);
>>>> +    vcpu->arch.vsscratch = csr_read(CSR_VSSCRATCH);
>>>> +    vcpu->arch.vscause = csr_read(CSR_VSCAUSE);
>>>> +    vcpu->arch.vstval = csr_read(CSR_VSTVAL);
>>>> +    vcpu->arch.vsepc = csr_read(CSR_VSEPC);
>>>> +}
>>>> +
>>>> +static void restore_csr_regs(struct vcpu *vcpu)
>>>> +{
>>>> +    csr_write(CSR_HEDELEG, vcpu->arch.hedeleg);
>>>> +    csr_write(CSR_HIDELEG, vcpu->arch.hideleg);
>>>> +    csr_write(CSR_HVIP, vcpu->arch.hvip);
>>>> +    csr_write64(CSR_HENVCFG, vcpu->arch.henvcfg);
>>>> +    csr_write(CSR_HCOUNTEREN, vcpu->arch.hcounteren);
>>>> +    csr_write64(CSR_HTIMEDELTA, vcpu->arch.htimedelta);
>>>> +
>>>> +    if ( riscv_isa_extension_available(NULL, RISCV_ISA_EXT_smstateen) )
>>>> +        csr_write(CSR_HSTATEEN0, vcpu->arch.hstateen0);
>>>> +
>>>> +    csr_write(CSR_VSSTATUS, vcpu->arch.vsstatus);
>>>> +    csr_write(CSR_VSIE, vcpu->arch.vsie);
>>>
>>>
>>>> +    csr_write(CSR_VSTVEC, vcpu->arch.vstvec);
>>>> +    csr_write(CSR_VSSCRATCH, vcpu->arch.vsscratch);
>>>> +    csr_write(CSR_VSCAUSE, vcpu->arch.vscause);
>>>> +    csr_write(CSR_VSTVAL, vcpu->arch.vstval);
>>>> +    csr_write(CSR_VSEPC, vcpu->arch.vsepc);
>>>> +}
>>>> +
>>>> +static void ctxt_switch_from(struct vcpu *p)
>>> Is it expected to have diverse names for the vcpu arg? Above it's vcpu,
>>> here it's p (I assume it's for `previous` but I think the _from alone is
>>> enough to understand) and below it's n. Shouldn't be better to keep the same name?
>>
>> Above should be used n.
> 
> Why would that be? n in such contexts stands for "next", while here
> it can only be "previous".

Sorry for confusion.

I meant for restore_csr_regs() and correpondingly p for save_csr_regs().

I will also renaim the function to cxt_switch_to/from style.

~ Oleksii


^ permalink raw reply	[flat|nested] 244+ messages in thread

* Re: [PATCH v2 16/39] xen/riscv: extend exception tables with type and data fields
  2026-09-07 15:57   ` Baptiste Le Duc
  2026-09-08  6:06     ` Jan Beulich
@ 2026-09-08  9:19     ` Oleksii Kurochko
  2026-09-08 16:26       ` Baptiste Le Duc
  1 sibling, 1 reply; 244+ messages in thread
From: Oleksii Kurochko @ 2026-09-08  9:19 UTC (permalink / raw)
  To: Baptiste Le Duc
  Cc: xen-devel, Romain Caritey, Zheng Zhang, Alistair Francis,
	Connor Davis, Andrew Cooper, Anthony PERARD, Michal Orzel,
	Jan Beulich, Julien Grall, Roger Pau Monné,
	Stefano Stabellini



On 9/7/26 5:57 PM, Baptiste Le Duc wrote:
>> Extend the RISC-V exception table format to include a type and
>> auxiliary data field.
>>
>> The existing format only supports simple fixups. Some use cases require
>> additional context from the fault (e.g. capturing trap information),
>> which cannot be expressed with the current EX_TYPE_FIXUP entries.
>>
>> Introduce a generic ASM_EXTABLE_RAW() helper to describe entries with a
>> handler type and associated data. Reimplement ASM_EXTABLE() in terms of
>> it using EX_TYPE_FIXUP for compatibility.
>>
>> Add EX_TYPE_TRAP_INFO to allow handlers to retrieve trap state
>> (sepc/scause/stval) and pass it to the fixup path. The data field is
>> used to encode which GPR contains a pointer to a struct trap_info.
>>
>> Provide ASM_EXTABLE_TRAP_INFO() as a convenience wrapper for this case.
>>
>> Also add gpr-num.h, providing symbolic GPR numbers for use in assembly
>> and inline asm. This is derived from Linux 6.16 with minor adjustments such
>> as using .irp instead of open-coding the same using a set of .equ.
>>
>> Update the exception handling code to dispatch based on the entry type.
>>
>> Signed-off-by: Oleksii Kurochko <oleksii.kurochko@gmail.com>
>>
>> diff --git a/xen/arch/riscv/extable.c b/xen/arch/riscv/extable.c
>> index 5b89c4278c..6470198d01 100644
>> --- a/xen/arch/riscv/extable.c
>> +++ b/xen/arch/riscv/extable.c
>> @@ -6,8 +6,10 @@
>>   #include <xen/sort.h>
>>   #include <xen/virtual_region.h>
>>   
>> +#include <asm/csr.h>
>>   #include <asm/extable.h>
>>   #include <asm/processor.h>
>> +#include <asm/traps.h>
>>   
>>   #define EX_FIELD(ptr, field) ((unsigned long)&(ptr)->field + (ptr)->field)
>>   
>> @@ -32,6 +34,12 @@ static void __init cf_check swap_ex(void *a, void *b)
>>   
>>       x->fixup = y->fixup + delta;
>>       y->fixup = tmp.fixup - delta;
>> +
>> +    x->type = y->type;
>> +    y->type = tmp.type;
>> +
>> +    x->data = y->data;
>> +    y->data = tmp.data;
>>   }
>>   
>>   static int cf_check cmp_ex(const void *a, const void *b)
>> @@ -59,7 +67,49 @@ static void ex_handler_fixup(const struct exception_table_entry *ex,
>>       regs->sepc = ex_fixup(ex);
>>   }
>>   
>> -bool fixup_exception(struct cpu_user_regs *regs)
>> +#define CHECK_GPR_INDEX(num, name)                      \
>> +    BUILD_BUG_ON(offsetof(struct cpu_user_regs, name)   \
>> +                 != (num) * sizeof(unsigned long));
>> +
>> +static unsigned long regs_get_gpr(const struct cpu_user_regs *regs,
>> +                                  unsigned int num)
>> +{
>> +    /*
>> +     * The GPR number -> struct index mapping below relies on x0..x31 being
>> +     * laid out at the start of struct cpu_user_regs in architectural order,
>> +     * matching the register numbers GPR_LIST() hands to the assembler.
>> +     */
>> +    GPR_LIST(CHECK_GPR_INDEX)
>> +
>> +    ASSERT(num < 32);
>> +
>> +    return ((const unsigned long *)regs)[num];
>> +}
>> +
>> +#undef CHECK_GPR_INDEX
>> +
>> +static void ex_handler_trap_info(const struct exception_table_entry *ex,
>> +                                 struct cpu_user_regs *regs,
>> +                                 unsigned long cause)
>> +{
>> +    struct trap_info *trap_info =
>> +        (struct trap_info *)regs_get_gpr(regs, ex->data);
>> +
>> +    BUG_ON(!trap_info);
>> +
>> +    /*
>> +     * Only stval still needs a CSR read: sepc and scause were already
>> +     * captured by the trap entry path and do_trap() respectively. Latch
>> +     * trap_info->sepc before regs->sepc is pointed at the fixup code.
>> +     */
>> +    trap_info->sepc = regs->sepc;
>> +    trap_info->scause = cause;
>> +    trap_info->stval = csr_read(CSR_STVAL);
>> +
>> +    regs->sepc = ex_fixup(ex);
>> +}
>> +
>> +bool fixup_exception(struct cpu_user_regs *regs, unsigned long cause)
>>   {
>>       unsigned long pc = regs->sepc;
>>       const struct virtual_region *region = find_text_region(pc);
>> @@ -77,7 +127,23 @@ bool fixup_exception(struct cpu_user_regs *regs)
>>       if ( !ex )
>>           return false;
>>   
>> -    ex_handler_fixup(ex, regs);
>> +    switch ( ex->type )
>> +    {
>> +    case EX_TYPE_FIXUP:
>> +        ex_handler_fixup(ex, regs);
>> +        break;
>> +
>> +    case EX_TYPE_TRAP_INFO:
>> +        ex_handler_trap_info(ex, regs, cause);
>> +        break;
>> +
>> +    default:
>> +        printk(XENLOG_ERR
>> +               "Unsupported exception table entry type %u for pc %#lx\n",
>> +               ex->type, pc);
>> +
>> +        return false;
>> +    }
>>   
>>       return true;
>>   }
>> diff --git a/xen/arch/riscv/include/asm/extable.h b/xen/arch/riscv/include/asm/extable.h
>> index c0128a9181..7378f86e7e 100644
>> --- a/xen/arch/riscv/include/asm/extable.h
>> +++ b/xen/arch/riscv/include/asm/extable.h
>> @@ -3,17 +3,24 @@
>>   #ifndef ASM__RISCV__ASM_EXTABLE_H
>>   #define ASM__RISCV__ASM_EXTABLE_H
>>   
>> +#include <asm/gpr-num.h>
>> +
>> +#define EX_TYPE_FIXUP       0
>> +#define EX_TYPE_TRAP_INFO   1
>> +
>>   #ifdef __ASSEMBLER__
>>   
>> -#define ASM_EXTABLE(insn, fixup) \
>> -    .pushsection .ex_table, "a"; \
>> -    .balign     4;               \
>> -    .word       (insn) - .;      \
>> -    .word       (fixup) - .;     \
>> +#define ASM_EXTABLE_RAW(insn, fixup, type, data)    \
>> +    .pushsection .ex_table, "a";                    \
>> +    .balign     4;                                  \
>> +    .word       (insn) - .;                         \
>> +    .word       (fixup) - .;                        \
>> +    .half       (type);                             \
>> +    .half       (data);                             \
>>       .popsection
>>   
>> -.macro asm_extable, insn, fixup
>> -    ASM_EXTABLE(\insn, \fixup)
>> +.macro _asm_extable, insn, fixup
>> +    ASM_EXTABLE_RAW(\insn, \fixup, EX_TYPE_FIXUP, 0)
>>   .endm
>>   
>>   #else /* __ASSEMBLER__ */
>> @@ -23,20 +30,36 @@
>>   
>>   struct cpu_user_regs;
>>   
>> -#define ASM_EXTABLE(insn, fixup)      \
>> -    ".pushsection .ex_table, \"a\"\n" \
>> -    ".balign    4\n"                  \
>> -    ".word      (" #insn " - .)\n"    \
>> -    ".word      (" #fixup " - .)\n"   \
>> +#define ASM_EXTABLE_RAW(insn, fixup, type, data)    \
>> +    ".pushsection .ex_table, \"a\"\n"               \
>> +    ".balign    4\n"                                \
>> +    ".word      (" insn ") - .\n"                   \
>> +    ".word      (" fixup ") - .\n"                  \
>> +    ".half      (" type ")\n"                       \
>> +    ".half      (" data ")\n"                       \
>>       ".popsection\n"
>>   
>> +#define ASM_EXTABLE(insn, fixup)    \
>> +    ASM_EXTABLE_RAW(#insn, #fixup, __stringify(EX_TYPE_FIXUP), "0")
>> +
>> +#define EX_TRAP_INFO_REG(gpr)   \
>> +    "(.L_gpr_num_" #gpr ")"
>> +
>> +#define ASM_EXTABLE_TRAP_INFO(insn, fixup, data)                    \
>> +    DEFINE_ASM_GPR_NUMS                                             \
>> +    ASM_EXTABLE_RAW(#insn, #fixup, __stringify(EX_TYPE_TRAP_INFO),  \
>> +                    EX_TRAP_INFO_REG(data))
>> +
>>   /*
>> - * The exception table consists of pairs of relative offsets: the first
>> - * is the relative offset to an instruction that is allowed to fault,
>> - * and the second is the relative offset at which the program should
>> - * continue. No general-purpose registers are modified by the exception
>> - * handling mechanism itself, so it is up to the fixup code to handle
>> - * any necessary state cleanup.
>> + * Each exception table entry consists of two relative offsets and a
>> + * handler description: `insn` is the relative offset to an instruction
>> + * that is allowed to fault, `fixup` is the relative offset at which the
>> + * program should continue, `type` selects how the exception is handled
>> + * (EX_TYPE_*), and `data` holds auxiliary information for the handler
>> + * (e.g. for EX_TYPE_TRAP_INFO, the number of the GPR that contains a
>> + * pointer to a struct trap_info). No general-purpose registers are
>> + * modified by the exception handling mechanism itself, so it is up to
>> + * the fixup code to handle any necessary state cleanup.
>>    *
>>    * The exception table and fixup code live out of line with the main
>>    * instruction path. This means when everything is well, we don't even
>> @@ -45,14 +68,15 @@ struct cpu_user_regs;
>>    */
>>   struct exception_table_entry {
>>       int32_t insn, fixup;
>> +    uint16_t type, data;
>>   };
>>   
>>   extern struct exception_table_entry __start___ex_table[];
>>   extern struct exception_table_entry __stop___ex_table[];
>>   
>>   void sort_exception_tables(void);
>> -bool fixup_exception(struct cpu_user_regs *regs);
>> +bool fixup_exception(struct cpu_user_regs *regs, unsigned long cause);
>>   
>> -#endif /* __ASSEMBLY__ */
>> +#endif /* __ASSEMBLER__ */
>>   
>>   #endif /* ASM__RISCV__ASM_EXTABLE_H */
>> diff --git a/xen/arch/riscv/include/asm/gpr-num.h b/xen/arch/riscv/include/asm/gpr-num.h
>> new file mode 100644
>> index 0000000000..3b97a72e6c
>> --- /dev/null
>> +++ b/xen/arch/riscv/include/asm/gpr-num.h
>> @@ -0,0 +1,37 @@
>> +/* SPDX-License-Identifier: GPL-2.0-only */
>> +#ifndef RISCV_GPR_NUM_H
>> +#define RISCV_GPR_NUM_H
> Nit: commit message says this is derived from Linux 6.16. Other
> imported RISC-V headers here carry an in-file note (bitops.h: "Based on
> linux/arch/.../bitops.h") but this file doesn't.
>> +/*
>> + * GPRs by ABI name, together with their register number (x0 .. x31).
>> + *
>> + * This is the single source of truth for the mapping: it generates the
>> + * .L_gpr_num_<name> assembler symbols used to turn a register name emitted
>> + * by the compiler into a register number, and struct cpu_user_regs is
>> + * checked against it at build time (see regs_get_gpr()). Neither list can
>> + * therefore be changed without the other.
>> + */
>> +#define GPR_LIST(x)                                 \
>> +    x(0,  zero) x(1,  ra)  x(2,  sp)  x(3,  gp)     \
>> +    x(4,  tp)   x(5,  t0)  x(6,  t1)  x(7,  t2)     \
>> +    x(8,  s0)   x(9,  s1)  x(10, a0)  x(11, a1)     \
>> +    x(12, a2)   x(13, a3)  x(14, a4)  x(15, a5)     \
>> +    x(16, a6)   x(17, a7)  x(18, s2)  x(19, s3)     \
>> +    x(20, s4)   x(21, s5)  x(22, s6)  x(23, s7)     \
>> +    x(24, s8)   x(25, s9)  x(26, s10) x(27, s11)    \
>> +    x(28, t3)   x(29, t4)  x(30, t5)  x(31, t6)
>> +
>> +#ifdef __ASSEMBLER__
>> +
>> +#define GPR_NUM_EQU(num, name)  .equ .L_gpr_num_##name, num;
>> +GPR_LIST(GPR_NUM_EQU)
>> +#undef GPR_NUM_EQU
>> +
>> +#else /* __ASSEMBLER__ */
>> +
>> +#define GPR_NUM_EQU(num, name)  ".equ .L_gpr_num_" #name ", " #num "\n"
>> +#define DEFINE_ASM_GPR_NUMS     GPR_LIST(GPR_NUM_EQU)
>> +
>> +#endif /* __ASSEMBLER__ */
>> +
>> +#endif /* RISCV_GPR_NUM_H */
>> diff --git a/xen/arch/riscv/include/asm/processor.h b/xen/arch/riscv/include/asm/processor.h
>> index b1745c1071..e7b0f2321a 100644
>> --- a/xen/arch/riscv/include/asm/processor.h
>> +++ b/xen/arch/riscv/include/asm/processor.h
>> @@ -12,7 +12,19 @@
>>   
>>   #ifndef __ASSEMBLER__
>>   
>> -/* On stack VCPU state */
>> +/*
>> + * On stack VCPU state.
>> + *
>> + * x0..x31 must remain at the start of this structure, in architectural
>> + * register-number order: code which resolves a register number to its saved
>> + * value indexes this structure directly (instruction emulation via
>> + * REG_PTR() from asm/riscv_encoding.h, exception table fixups via
>> + * regs_get_gpr()). ->zero therefore has to stay at offset 0 and must always
>> + * read as 0, since it supplies the value of x0 when x0 is used as a source
>> + * operand. The layout is checked against GPR_LIST() at build time; see
>> + * regs_get_gpr() in extable.c. Do not reorder these fields or insert
>> + * anything between them.
>> + */
>>   {
>>       unsigned long zero;
> Comment claims ->zero "must always read as 0" as it's hard-wired to zero
> by the HW, but nothing enforces that, it's still a plain writable
> unsigned long. Maybe a write-side counterpart that special-cases num==0
> as a no-op, or with a minimum ASSERT(num != 0) / BUG_ON(num == 0) to
> anticipate any future forbidden writes.
> 

Could you please clarify where do you want me to put this check in this 
patch? In regs_get_gpr()? There is no write-side in this patch. Am i 
missing something?


Thanks in advance.

~ Oleksii



^ permalink raw reply	[flat|nested] 244+ messages in thread

* Re: [PATCH v2 17/39] xen/riscv: decouple INSN_PSEUDO_VS_* from the hypervisor's XLEN
  2026-09-07 15:57   ` Baptiste Le Duc
@ 2026-09-08  9:34     ` Oleksii Kurochko
  2026-09-08 16:04       ` Baptiste Le Duc
  0 siblings, 1 reply; 244+ messages in thread
From: Oleksii Kurochko @ 2026-09-08  9:34 UTC (permalink / raw)
  To: Baptiste Le Duc
  Cc: xen-devel, Romain Caritey, Zheng Zhang, Alistair Francis,
	Connor Davis, Andrew Cooper, Anthony PERARD, Michal Orzel,
	Jan Beulich, Julien Grall, Roger Pau Monné,
	Stefano Stabellini



On 9/7/26 5:57 PM, Baptiste Le Duc wrote:
>> htinst reports a pseudoinstruction when a guest page fault is taken on an
>> implicit memory access done for VS-stage address translation. Four such
>> values are defined, differing in the access type (read or write) and in the
>> access width: 4 bytes (0x2000/0x2020) or 8 bytes (0x3000/0x3020).
>>
>> That width is the width of a VS-stage PTE, i.e. it follows the guest's
>> paging mode (4 bytes for Sv32, 8 bytes for Sv39 and wider) and has nothing
>> to do with the XLEN Xen itself is built for. Selecting just one pair with
>> where a guest running with VSXL=32 and Sv32 in vsatp produces the 4-byte
> Sentence is broken. Guess you mean "Selecting just one pair based on Xen's XLEN misses the case where
> …".

Likely it is becuase of my low level English but it seems that original 
version wast okay and what you added is just a part from prev. sentence 
but I will add your suggestion for better clearness. Thanks for noticing 
that!

> 
>> forms. Such an htinst would not be recognized as a pseudoinstruction and the
>> fault would be mistaken for an ordinary MMIO trap: Xen would fetch and
>> decode whatever instruction sepc happens to point at (unrelated to the
>> access which faulted) and emulate it against a guest physical address
>> derived from htval, which for an implicit access holds the address of a
>> VS-stage PTE rather than of any access the guest performed.
>>
>> Define all four values unconditionally instead, named after the access width
>> they encode rather than after the build's XLEN. On RV32 the 8-byte forms
>> simply never occur, so recognizing them costs nothing.
>>
>> Dropping the ladder loses no build-time coverage: a build for an XLEN other
>> than 32 or 64 already fails on the equivalent ladders in asm/asm.h and
>> asm/config.h, so no replacement #error is needed here. Adding one keyed on
>> CONFIG_RISCV_* would in any case re-introduce exactly the conflation this
>> patch removes.
>>
>> This diverges from the imported version of riscv_encoding.h.
>>
>> No functional change: the values have no user yet.
>>
>> Signed-off-by: Oleksii Kurochko <oleksii.kurochko@gmail.com>
>>
>> diff --git a/xen/arch/riscv/include/asm/riscv_encoding.h b/xen/arch/riscv/include/asm/riscv_encoding.h
>> index c63e5e3046..2d2e7e11b3 100644
>> --- a/xen/arch/riscv/include/asm/riscv_encoding.h
>> +++ b/xen/arch/riscv/include/asm/riscv_encoding.h
>> @@ -839,25 +839,17 @@
>>   #define INSN_MASK_FENCE_TSO		0xffffffff
>>   #define INSN_MATCH_FENCE_TSO		0x8330000f
>>   
>> -#if __riscv_xlen == 64
>> -
>>   /* 64-bit read for VS-stage address translation (RV64) */
>> -#define INSN_PSEUDO_VS_LOAD		0x00003000
>> +#define INSN_PSEUDO_VS_LOAD64		0x00003000
>>   
>>   /* 64-bit write for VS-stage address translation (RV64) */
>> -#define INSN_PSEUDO_VS_STORE	0x00003020
>> -
>> -#elif __riscv_xlen == 32
>> +#define INSN_PSEUDO_VS_STORE64		0x00003020
>>   
>>   /* 32-bit read for VS-stage address translation (RV32) */
>> -#define INSN_PSEUDO_VS_LOAD		0x00002000
>> +#define INSN_PSEUDO_VS_LOAD32		0x00002000
>>   
>>   /* 32-bit write for VS-stage address translation (RV32) */
>> -#define INSN_PSEUDO_VS_STORE	0x00002020
>> -
> Whole point of patch is these no longer depend on build XLEN, yet
> comments still say "(RV64)"/"(RV32)" which could be confusing. Maybe it
> should be better to indicate, as the spec does, that RV32 values are
> used when VSXLEN=32 (only sv32 paging mode) and RV64 values when
> VSXLEN=64 (sv39+ paging modes).
> 

Could you please clarify to me what in the spec it is?

This comments are just copy from the spec:

Table 39. Special pseudoinstruction values for guest-page faults. The 
RV32 values are used when VSXLEN=32, and the RV64 values when VSXLEN=64.
Value           Meaning
0x00002000      32-bit read for VS-stage address translation (RV32)
0x00002020      32-bit write for VS-stage address translation (RV32)

Value           Meaning
0x00003000      64-bit read for VS-stage address translation (RV64)
0x00003020      64-bit write for VS-stage address translation (RV64)

So the comments are just copy of "Meaning" column from the spec.

And I think that Meaning column is fine here as we could have a case of 
when hypervisor has XLEN=64 but guests could be on it RV32 and RV64 and 
if a guest is RV32 (what means VSXLEN=32) then the comment above 
defintion mean that we hav 32-bit read/write VS-stage address 
translation for (RV32) guest and the similar is for RV64.

Do I miss something? Is a comments make more sense now or I have to 
still update them in some way?

Thanks!

~ Oleksii




^ permalink raw reply	[flat|nested] 244+ messages in thread

* Re: [PATCH v2 18/39] xen/riscv: add guest page fault handling stub
  2026-09-07 15:57   ` Baptiste Le Duc
@ 2026-09-08  9:49     ` Oleksii Kurochko
  0 siblings, 0 replies; 244+ messages in thread
From: Oleksii Kurochko @ 2026-09-08  9:49 UTC (permalink / raw)
  To: Baptiste Le Duc
  Cc: xen-devel, Romain Caritey, Zheng Zhang, Alistair Francis,
	Connor Davis, Andrew Cooper, Anthony PERARD, Michal Orzel,
	Jan Beulich, Julien Grall, Roger Pau Monné,
	Stefano Stabellini



On 9/7/26 5:57 PM, Baptiste Le Duc wrote:
>> Add a handler for guest page faults and hook it into the trap path,
>> providing the trap-side entry point which will later feed the MMIO
>> dispatch.
>>
>> This will be used, for example, to trap accesses to APLIC registers so
>> that a guest can initialize and drive an emulated interrupt controller.
>>
>> Two of the situations handled here are already decided, as neither can
>> ever be turned into an emulated access:
>>
>>   - A fault reported with a pseudoinstruction in htinst was taken on an
>>     implicit access made for VS-stage address translation, so htval holds
>>     the address of a VS-stage PTE rather than of anything the guest asked
>>     for, and the guest physical address behind the original access is not
>>     known. This is orthogonal to the cause and can accompany any of the
>>     three, which is why it is checked first. scause keeps reporting the
>>     type of the original access, and on bare hardware a PTE which cannot
>>     be read raises an access fault of exactly that type, so reflect one
>>     back to the guest.
>>
>>   - A fetch fault means the guest tried to execute from a guest physical
>>     address which is unmapped or which G-stage does not allow to be
>>     executed. On bare hardware a fetch from physical memory which does
>>     not exist, or which may not be executed, raises an instruction access
>>     fault, so reflect one back too.
>>
>> Explicit loads and stores are where MMIO emulation will hook in.
>>
>> Neither of the two paths above consults the p2m first, and neither will
>> the MMIO one: RISC-V has no populate-on-demand, no paging and no
>> mem_access, so every guest mapping is established eagerly and a G-stage
>> fault never denotes a mapping Xen could install to let the faulting
>> access complete.
>>
>> Both of the helpers this leans on, resolve_faulting_gpa() and
>> trap_redirect(), are BUG_ON() placeholders for now, so each of the three
>> causes currently takes the host down rather than the domain. That is no
>> worse than before this patch, where the same causes fell through to
>> do_unexpected_trap() and die(). Implementing the helpers is left to
>> later patches.
>>
>> Signed-off-by: Oleksii Kurochko <oleksii.kurochko@gmail.com>
>>
> 
> I think the commit message it not very clear as it conflates two
> different countable things (the 3 fault causes reported via scause vs.
> the pseudoinstruction condition, which is orthogonal and can accompany
> any of them), which makes "two situations decided" or "any of the three"
> hard to follow on first read.
> 
> Here is a proposition with a split to distinguish fetch-fault and
> pseudoinstruction cases into explicit bullets with their outcome stated
> ("Decided here"), and added spec-mentioned conditions about the
> pseudoinstruction's existence conditions:
> ```
>      Add a handler for guest page faults and hook it into the trap path,
>      providing the trap-side entry point which will later feed the MMIO
>      dispatch.
> 
>      This will be used, for example, to trap accesses to APLIC registers so
>      that a guest can initialize and drive an emulated interrupt controller.
> 
>      A G-stage (stage-2) fault has one of three causes, reported via scause:
> 
>          - Fetch fault: guest tried to execute from a guest-physical address
>          that is unmapped or that G-stage marks non-executable. Never
>          emulatable (nothing to emulate a fetch into). On real hardware
>          this raises an instruction access fault, so Xen reflects the same
>          fault back to the guest. Decided here.
> 
>          - Load fault / Store fault: left undecided by this patch, this is
>          where MMIO emulation will hook in later.
> 
>      Any of these three faults can instead be reported via a pseudoinstruction
>      in htinst, when both:
> 
>          (a) the fault occurred on an implicit access Xen made to walk a
>              VS-stage page table, and
>          (b) htval holds a nonzero value: the guest-physical address of that
>              VS-stage PTE, not of the guest's original access.
> 
>      However, none of these paths consult the p2m first, and the future MMIO path
>      won't either: RISC-V has no populate-on-demand, no paging, and no
>      mem_access, so every guest mapping is established eagerly. A G-stage
>      fault therefore never indicates a mapping Xen could lazily resolve to
>      let the access complete.
> 
>      Both helpers this handler relies on, resolve_faulting_gpa() and
>      trap_redirect(), are BUG_ON() placeholders for now, so all three causes
>      currently take the host down instead of just the guest. This is no worse
>      than before this patch, where these traps fell through to
>      do_unexpected_trap() and die().
> ```

I will apply your suggestion.

Thanks.

~ Oleksii


^ permalink raw reply	[flat|nested] 244+ messages in thread

* Re: [PATCH v2 19/39] xen/riscv: implement trap redirection to a guest
  2026-09-07 15:57   ` Baptiste Le Duc
@ 2026-09-08 10:01     ` Oleksii Kurochko
  2026-09-08 14:58       ` Oleksii Kurochko
  0 siblings, 1 reply; 244+ messages in thread
From: Oleksii Kurochko @ 2026-09-08 10:01 UTC (permalink / raw)
  To: Baptiste Le Duc
  Cc: xen-devel, Romain Caritey, Zheng Zhang, Alistair Francis,
	Connor Davis, Andrew Cooper, Anthony PERARD, Michal Orzel,
	Jan Beulich, Julien Grall, Roger Pau Monné,
	Stefano Stabellini



On 9/7/26 5:57 PM, Baptiste Le Duc wrote:
>> Some traps taken by Xen on behalf of a guest can't or shouldn't be handled
>> by the hypervisor and have to be reflected to the guest's own S-mode trap
>> handler instead: the access faults which handle_guest_page_fault() injects
>> for a fault that can never become an emulated access, and, later on, a
>> fault taken by the hlv/hlvx sequences of riscv_read_guest() while
>> accessing guest memory on a vCPU's behalf.
>>
> Access faults from handle_guest_page_fault() aren't "taken by Xen on
> behalf of a guest" as those are guest-page faults taken directly from the
> guest's own execution. Xen just decides they can't be emulated and
> reflects them back as access faults. Only the hlv/hlvx case is Xen
> trapping on the guest's behalf (Xen itself executes the faulting access).> Conflating the two under one description makes the paragraph confusing.
> 
> Suggest splitting into two:
> 
>      Two kinds of traps can't or shouldn't be handled by the hypervisor and
>      have to be reflected to the guest's own S-mode trap handler instead:
> 
>      - Traps Xen takes on the guest's behalf: the hlv/hlvx sequences
>      riscv_read_guest() uses to access guest memory.
>      - Access faults handle_guest_page_fault() injects for a guest-page
>      fault that can never become an emulated access.

Thanks, I'll update original paragraph with what you suggested.>
>>
>> Implement trap_redirect(), until now a BUG_ON() placeholder, for that
>> purpose. It makes the trap appear to the guest as if it had been taken
>> directly in VS-mode: the trap information is transferred to the guest's
>> virtual supervisor CSRs and the vCPU is resumed at its exception vector in
>> supervisor mode, following the trap entry rules of the RISC-V privileged
>> specification.
>>
>> Add the STVEC_* definitions needed to tell the BASE and MODE fields of
>> vstvec apart.
>>
>> The implementation is based on kvm_riscv_vcpu_trap_redirect() from Linux,
>> with a few deviations:
>>   - The function reads and writes physical VS-mode CSRs, so it is only
>>     meaningful for the currently running vCPU. Instead of taking a
>>     struct vcpu argument, it always operates on current.
>>   - The MODE field of vstvec is masked off explicitly when computing the
>>     exception target PC (exceptions always vector to BASE), rather than
>>     relying on the hardwired zero bit of sepc to drop it on VM entry.
>>   - Assertions document the preconditions: the trap must have been taken
>>     from virtualized mode (hstatus.SPV set), and only synchronous
>>     exceptions may be redirected - interrupts must be injected via hvip
>>     instead, so that the hardware performs VS-mode trap entry itself,
>>     respecting vsstatus.SIE and vectored vstvec dispatch.
>>
>> Signed-off-by: Oleksii Kurochko <oleksii.kurochko@gmail.com>
>>
>> diff --git a/xen/arch/riscv/include/asm/riscv_encoding.h b/xen/arch/riscv/include/asm/riscv_encoding.h
>> index 2d2e7e11b3..b2071f4758 100644
>> --- a/xen/arch/riscv/include/asm/riscv_encoding.h
>> +++ b/xen/arch/riscv/include/asm/riscv_encoding.h
>> @@ -109,6 +109,12 @@
>>   #define SIP_SSIP			MIP_SSIP
>>   #define SIP_STIP			MIP_STIP
>>   
>> +/* stvec/vstvec: MODE is bits [1:0], BASE is bits [XLEN-1:2] */
>> +#define STVEC_MODE_MASK			_UL(0x3)
>> +#define STVEC_MODE_DIRECT		_UL(0x0)
>> +#define STVEC_MODE_VECTORED		_UL(0x1)
>> +#define STVEC_BASE_MASK			(~STVEC_MODE_MASK)
> Nit: STVEC_MODE_DIRECT and STVEC_MODE_VECTORED aren't used anywhere in
> this patch (only STVEC_BASE_MASK is). Either use them where you decide
> exceptions always target BASE regardless of MODE, or drop them until a
> patch that needs them.

IMO it is fine to introduce *_DIRECT/VECORED here as they are used 
implicitly through STVEC_BASE_MASK and thereby it will be better to 
introduce them here now instead of open-code them and then just update 
STVEC_BASE_MASK again when *_DIRECT/VECORED will be re-introduced.

Thanks for review.

~ Oleksii


^ permalink raw reply	[flat|nested] 244+ messages in thread

* Re: [PATCH v2 20/39] xen/riscv: detect Shtvala
  2026-09-07 15:57   ` Baptiste Le Duc
@ 2026-09-08 10:15     ` Oleksii Kurochko
  2026-09-08 15:49       ` Baptiste Le Duc
  0 siblings, 1 reply; 244+ messages in thread
From: Oleksii Kurochko @ 2026-09-08 10:15 UTC (permalink / raw)
  To: Baptiste Le Duc
  Cc: xen-devel, Romain Caritey, Zheng Zhang, Alistair Francis,
	Connor Davis, Andrew Cooper, Anthony PERARD, Michal Orzel,
	Jan Beulich, Julien Grall, Roger Pau Monné,
	Stefano Stabellini



On 9/7/26 5:57 PM, Baptiste Le Duc wrote:
>> Shtvala says that htval is written with the faulting guest physical address
>> on a guest-page fault. The H extension itself allows an implementation to
>> write htval with either that address or with zero, so where the extension is
>> absent a zero htval cannot be told apart from a genuine fault on guest
>> physical address 0-3.
>>
>> It is not offered to guests. Shtvala describes the HS-mode trap interface,
>> which a VS-mode guest never sees, and the H extension it belongs to is
>> already withheld from guests. Its guest-facing counterpart is a separate
>> extension, Shvstvala.
>>
>> Signed-off-by: Oleksii Kurochko <oleksii.kurochko@gmail.com>
>>
> I thought this commit message is not very clear. Here is a more direct
> suggestion:
> 
> ```
> The H extension allows htval, on a guest-page fault, to be written with
> either the faulting guest physical address or zero. Shtvala extension
> removes the ambiguity of this zero-write by guaranteeing that htval is
> written with the faulting guest physical address in every circumstance
> permitted by the ISA.
> 
> Not offered to guests: Shtvala describes htval, an HS-mode-only trap
> register a VS-mode guest never touches and the H extension it belongs to
> is already hidden from guests. The guest-visible equivalent is a
> separate extension, Shvstvala, covering vstval instead.
> ```

Sounds good to me. I will apply your suggestion.

> 
> Btw, I saw Linux has Documentation/devicetree/bindings/riscv/extensions.yaml
> which describes all of the extensions supported, don't you think it would
> be useful to have the same in docs/misc/devicetree/...?

I think it could be useful.

I think also about booting.txt already exitsted in the codebase. Would 
you be okay with that? I think I will add the section to booting.txt and 
pointing to riscv_isa_ext[] in cpufeature.c and so we won't miss an 
update of doc if we will add or remove support of an extension. Does it 
sound good to you?

~ Oleksii




^ permalink raw reply	[flat|nested] 244+ messages in thread

* Re: [PATCH v2 16/39] xen/riscv: extend exception tables with type and data fields
  2026-08-27 15:21 ` [PATCH v2 16/39] xen/riscv: extend exception tables with type and data fields Oleksii Kurochko
  2026-09-07 15:57   ` Baptiste Le Duc
@ 2026-09-08 13:44   ` Jan Beulich
  2026-09-09 11:20     ` Oleksii Kurochko
  1 sibling, 1 reply; 244+ messages in thread
From: Jan Beulich @ 2026-09-08 13:44 UTC (permalink / raw)
  To: Oleksii Kurochko
  Cc: Romain Caritey, Baptiste Le Duc, Zheng Zhang, Alistair Francis,
	Connor Davis, Andrew Cooper, Anthony PERARD, Michal Orzel,
	Julien Grall, Roger Pau Monné, Stefano Stabellini, xen-devel

On 27.08.2026 17:21, Oleksii Kurochko wrote:
> @@ -59,7 +67,49 @@ static void ex_handler_fixup(const struct exception_table_entry *ex,
>      regs->sepc = ex_fixup(ex);
>  }
>  
> -bool fixup_exception(struct cpu_user_regs *regs)
> +#define CHECK_GPR_INDEX(num, name)                      \
> +    BUILD_BUG_ON(offsetof(struct cpu_user_regs, name)   \
> +                 != (num) * sizeof(unsigned long));

Nit: Placement of the !=. Also there should be no semicolon here; it wants
to ...

> +static unsigned long regs_get_gpr(const struct cpu_user_regs *regs,
> +                                  unsigned int num)
> +{
> +    /*
> +     * The GPR number -> struct index mapping below relies on x0..x31 being
> +     * laid out at the start of struct cpu_user_regs in architectural order,
> +     * matching the register numbers GPR_LIST() hands to the assembler.
> +     */
> +    GPR_LIST(CHECK_GPR_INDEX)

... appear here instead, for this to actually look like a statement.

> +    ASSERT(num < 32);
> +
> +    return ((const unsigned long *)regs)[num];

What about release builds? You'd happily overrun the array there. Maybe
(ab)use array_index_nospec() here?

> +}
> +
> +#undef CHECK_GPR_INDEX

If the sole use of the macro is in a single function, it wants #define-ing
(and #undef-ing) there, not outside of it.

> +static void ex_handler_trap_info(const struct exception_table_entry *ex,
> +                                 struct cpu_user_regs *regs,
> +                                 unsigned long cause)
> +{
> +    struct trap_info *trap_info =
> +        (struct trap_info *)regs_get_gpr(regs, ex->data);
> +
> +    BUG_ON(!trap_info);

This feels extremely weak. As you're fetching from a GPR, the majority of
possible values stored in GPRs is going to be invalid, not just NULL. And
while the accesses below would trap on NULL anyway, whether other bogus
values would trap is pretty hard to predict. If trap_info is expected to
always live on the stack, why not check for that (perhaps also check that
the low few bits are clear)?

> @@ -23,20 +30,36 @@
>  
>  struct cpu_user_regs;
>  
> -#define ASM_EXTABLE(insn, fixup)      \
> -    ".pushsection .ex_table, \"a\"\n" \
> -    ".balign    4\n"                  \
> -    ".word      (" #insn " - .)\n"    \
> -    ".word      (" #fixup " - .)\n"   \
> +#define ASM_EXTABLE_RAW(insn, fixup, type, data)    \
> +    ".pushsection .ex_table, \"a\"\n"               \
> +    ".balign    4\n"                                \
> +    ".word      (" insn ") - .\n"                   \
> +    ".word      (" fixup ") - .\n"                  \
> +    ".half      (" type ")\n"                       \
> +    ".half      (" data ")\n"                       \
>      ".popsection\n"
>  
> +#define ASM_EXTABLE(insn, fixup)    \
> +    ASM_EXTABLE_RAW(#insn, #fixup, __stringify(EX_TYPE_FIXUP), "0")
> +
> +#define EX_TRAP_INFO_REG(gpr)   \
> +    "(.L_gpr_num_" #gpr ")"

This doesn't need to be wrapped across lines, does it?

> +#define ASM_EXTABLE_TRAP_INFO(insn, fixup, data)                    \
> +    DEFINE_ASM_GPR_NUMS                                             \
> +    ASM_EXTABLE_RAW(#insn, #fixup, __stringify(EX_TYPE_TRAP_INFO),  \
> +                    EX_TRAP_INFO_REG(data))

There's no visible statement separator between DEFINE_ASM_GPR_NUMS and
ASM_EXTABLE_RAW(), which only works because DEFINE_ASM_GPR_NUMS appends
a separator also at the very end of its expansion. I think that better
would be changed, such that at use sites such as this one a separator
becomes mandatory.

>  /*
> - * The exception table consists of pairs of relative offsets: the first
> - * is the relative offset to an instruction that is allowed to fault,
> - * and the second is the relative offset at which the program should
> - * continue. No general-purpose registers are modified by the exception
> - * handling mechanism itself, so it is up to the fixup code to handle
> - * any necessary state cleanup.
> + * Each exception table entry consists of two relative offsets and a
> + * handler description: `insn` is the relative offset to an instruction
> + * that is allowed to fault, `fixup` is the relative offset at which the
> + * program should continue,

"... in case of a fault, ..."

> --- /dev/null
> +++ b/xen/arch/riscv/include/asm/gpr-num.h
> @@ -0,0 +1,37 @@
> +/* SPDX-License-Identifier: GPL-2.0-only */
> +#ifndef RISCV_GPR_NUM_H
> +#define RISCV_GPR_NUM_H
> +
> +/*
> + * GPRs by ABI name, together with their register number (x0 .. x31).
> + *
> + * This is the single source of truth for the mapping:

True until here, but ...

> it generates the
> + * .L_gpr_num_<name> assembler symbols

... no, it doesn't. It's ...

> used to turn a register name emitted
> + * by the compiler into a register number, and struct cpu_user_regs is
> + * checked against it at build time (see regs_get_gpr()). Neither list can
> + * therefore be changed without the other.
> + */
> +#define GPR_LIST(x)                                 \
> +    x(0,  zero) x(1,  ra)  x(2,  sp)  x(3,  gp)     \
> +    x(4,  tp)   x(5,  t0)  x(6,  t1)  x(7,  t2)     \
> +    x(8,  s0)   x(9,  s1)  x(10, a0)  x(11, a1)     \
> +    x(12, a2)   x(13, a3)  x(14, a4)  x(15, a5)     \
> +    x(16, a6)   x(17, a7)  x(18, s2)  x(19, s3)     \
> +    x(20, s4)   x(21, s5)  x(22, s6)  x(23, s7)     \
> +    x(24, s8)   x(25, s9)  x(26, s10) x(27, s11)    \
> +    x(28, t3)   x(29, t4)  x(30, t5)  x(31, t6)
> +
> +#ifdef __ASSEMBLER__
> +
> +#define GPR_NUM_EQU(num, name)  .equ .L_gpr_num_##name, num;
> +GPR_LIST(GPR_NUM_EQU)
> +#undef GPR_NUM_EQU

... this construct which does. This is relevant to separate, since
GPR_LIST() is also used elsewhere.

> --- a/xen/arch/riscv/include/asm/processor.h
> +++ b/xen/arch/riscv/include/asm/processor.h
> @@ -12,7 +12,19 @@
>  
>  #ifndef __ASSEMBLER__
>  
> -/* On stack VCPU state */
> +/*
> + * On stack VCPU state.
> + *
> + * x0..x31 must remain at the start of this structure, in architectural
> + * register-number order:

I understand that the order need retaining. But why would it being at the
start of the struct be (overly) relevant? You could use the "zero" field
as the anchor for calculations.

Jan


^ permalink raw reply	[flat|nested] 244+ messages in thread

* Re: [PATCH v2 18/39] xen/riscv: add guest page fault handling stub
  2026-08-27 15:21 ` [PATCH v2 18/39] xen/riscv: add guest page fault handling stub Oleksii Kurochko
  2026-09-07 15:57   ` Baptiste Le Duc
@ 2026-09-08 14:10   ` Jan Beulich
  2026-09-09 15:09     ` Oleksii Kurochko
  1 sibling, 1 reply; 244+ messages in thread
From: Jan Beulich @ 2026-09-08 14:10 UTC (permalink / raw)
  To: Oleksii Kurochko
  Cc: Romain Caritey, Baptiste Le Duc, Zheng Zhang, Alistair Francis,
	Connor Davis, Andrew Cooper, Anthony PERARD, Michal Orzel,
	Julien Grall, Roger Pau Monné, Stefano Stabellini, xen-devel

On 27.08.2026 17:21, Oleksii Kurochko wrote:
> --- /dev/null
> +++ b/xen/arch/riscv/emulate.c
> @@ -0,0 +1,179 @@
> +/* SPDX-License-Identifier: GPL-2.0-or-later */
> +
> +/*
> + * RISC-V instruction emulation for trapped guest accesses
> + */
> +
> +#include <xen/bug.h>
> +#include <xen/errno.h>
> +#include <xen/sched.h>
> +#include <xen/types.h>
> +
> +#include <asm/csr.h>
> +#include <asm/current.h>
> +#include <asm/emulate.h>
> +#include <asm/riscv_encoding.h>
> +#include <asm/traps.h>
> +
> +/*
> + * The hardware-reported details of a guest page fault, gathered once by
> + * handle_guest_page_fault() and passed down to the emulation of the faulted
> + * access.
> + */
> +struct guest_fault {
> +    /* The guest register state as saved on entry to do_trap(). */
> +    struct cpu_user_regs *regs;

If the comment was true, this could be pointer-to-const.

> +    /* scause: a fetch, a load or a store/AMO guest page fault. */
> +    unsigned long cause;
> +    /*
> +     * htinst: the trapped instruction in its transformed form, or one of the
> +     * special values (zero, or a pseudoinstruction).
> +     */
> +    unsigned long htinst;
> +    /* htval: as written by hardware; see resolve_faulting_gpa(). */
> +    unsigned long htval;
> +    /* stval: the guest virtual address of the faulting access. */
> +    unsigned long stval;
> +    /* The faulting guest physical address, filled by resolve_faulting_gpa(). */
> +    paddr_t gpa;
> +};
> +
> +/*
> + * Is @htinst one of the pseudoinstructions reported for a guest page fault
> + * taken on an implicit memory access done for VS-stage address translation?
> + *
> + * All four values are recognized regardless of the hypervisor's XLEN: the
> + * width they encode is that of a VS-stage PTE, i.e. it follows the guest's
> + * paging mode (4 bytes for Sv32, 8 otherwise). On RV32 the 64-bit forms
> + * simply never occur.
> + */
> +static bool htinst_is_pseudo(unsigned long htinst)
> +{
> +    switch ( htinst )
> +    {
> +    case INSN_PSEUDO_VS_LOAD32:
> +    case INSN_PSEUDO_VS_STORE32:
> +    case INSN_PSEUDO_VS_LOAD64:
> +    case INSN_PSEUDO_VS_STORE64:
> +        return true;
> +
> +    default:
> +        return false;
> +    }
> +}

This feels fragile. New pseudo-insns can appear at any time. If the value as
a whole is non-zero, aiui the low two bits being zero indicate a pseudo-insn.
In which case enumerating pseudo-insns we are currently aware of isn't
necessary.

> +static void inject_access_fault(const struct guest_fault *gf)
> +{
> +    struct trap_info utrap = {};
> +
> +    switch ( gf->cause )
> +    {
> +    case CAUSE_FETCH_GUEST_PAGE_FAULT:
> +        utrap.scause = CAUSE_FETCH_ACCESS;
> +        break;
> +
> +    case CAUSE_LOAD_GUEST_PAGE_FAULT:
> +        utrap.scause = CAUSE_LOAD_ACCESS;
> +        break;
> +
> +    case CAUSE_STORE_GUEST_PAGE_FAULT:
> +        utrap.scause = CAUSE_STORE_ACCESS;
> +        break;
> +
> +    default:
> +        domain_crash(current->domain, "Impossible cause (%#lx) in %s?\n",
> +                     gf->cause, __func__);
> +        return;
> +    }
> +
> +    utrap.sepc = gf->regs->sepc;
> +    utrap.stval = gf->stval;

Would there be anything wrong with putting these in utrap's initializer?

> +    trap_redirect(&utrap);
> +}
> +
> +void handle_guest_page_fault(struct cpu_user_regs *regs, unsigned long cause)
> +{
> +    struct guest_fault gf = {
> +        .regs = regs,
> +        .cause = cause,
> +        .htinst = csr_read(CSR_HTINST),
> +        .htval = csr_read(CSR_HTVAL),
> +        .stval = csr_read(CSR_STVAL),
> +        .gpa = INVALID_PADDR,
> +    };

At some point RISC-V code will (very likely) also be scanned for Misra violations.
The csr_read()s here violate rule 13.1 ("Initializer lists shall not contain
persistent side effects"), and I think it would be better if such was avoided from
the start.

> +    int rc;
> +
> +    /*
> +     * A guest-page fault may arise due to an implicit memory access during
> +     * first-stage (VS-stage) address translation, in which case a guest
> +     * physical address written to htval is that of the implicit memory
> +     * access that faulted - for example, the address of a VS-level page
> +     * table entry that could not be read. (The guest physical address
> +     * corresponding to the original virtual address is unknown when
> +     * VS-stage translation fails to complete)
> +     *
> +     * In such cases htinst reports one of the pseudoinstructions recognized
> +     * by htinst_is_pseudo(), and the fault requires separate handling (since
> +     * G-stage translation failed on an unpopulated/unmapped guest physical
> +     * address during a hardware page-table walk). To match bare hardware
> +     * behavior, we must inject an access fault of the ORIGINAL access type
> +     * (Instruction, Load, or Store/AMO) that initiated the address
> +     * translation.
> +     */
> +    if ( htinst_is_pseudo(gf.htinst) )
> +    {
> +        inject_access_fault(&gf);
> +
> +        return;
> +    }

I.e. you imply that guests won't put their page tables in MMIO? That's
fragile imo; I have seen OSes to use video frame buffers for all kinds
of (transient) purposes, for example.

> +    resolve_faulting_gpa(&gf);

Since the function is only a stub right now - how is one to tell whether
this indeed can never fail?

> +    switch ( cause )
> +    {
> +    case CAUSE_LOAD_GUEST_PAGE_FAULT:
> +        rc = emulate_load(&gf);
> +        break;
> +
> +    case CAUSE_STORE_GUEST_PAGE_FAULT:
> +        rc = emulate_store(&gf);
> +        break;
> +
> +    case CAUSE_FETCH_GUEST_PAGE_FAULT:
> +        /*
> +         * Guest is trying to reach unmapped/unpopulated or G-stage PTE doesn't
> +         * allow execution (X=0). Generate fetch fault in this case.
> +         */

Is there perhaps a comma missing before "or", to help parsing the sentence?

> +        inject_access_fault(&gf);
> +        rc = 0;
> +        break;

Simply "return" instead of the latter two statements?

> +    default:
> +        rc = -EOPNOTSUPP;
> +        ASSERT_UNREACHABLE();

To fit a common pattern, these two lines want to be the other way around.

> +        break;
> +    }
> +
> +    if ( rc )
> +        domain_crash(current->domain,
> +                     "%s: unable to handle guest page fault (cause=%#lx) at "
> +                     "gpa %#"PRIpaddr"\n",

Please avoid wrapping of format strings across lines.

Jan


^ permalink raw reply	[flat|nested] 244+ messages in thread

* Re: [PATCH v2 19/39] xen/riscv: implement trap redirection to a guest
  2026-08-27 15:21 ` [PATCH v2 19/39] xen/riscv: implement trap redirection to a guest Oleksii Kurochko
  2026-09-07 15:57   ` Baptiste Le Duc
@ 2026-09-08 14:16   ` Jan Beulich
  2026-09-08 15:25     ` Oleksii Kurochko
  2026-09-08 14:16   ` Jan Beulich
  2 siblings, 1 reply; 244+ messages in thread
From: Jan Beulich @ 2026-09-08 14:16 UTC (permalink / raw)
  To: Oleksii Kurochko
  Cc: Romain Caritey, Baptiste Le Duc, Zheng Zhang, Alistair Francis,
	Connor Davis, Andrew Cooper, Anthony PERARD, Michal Orzel,
	Julien Grall, Roger Pau Monné, Stefano Stabellini, xen-devel

On 27.08.2026 17:21, Oleksii Kurochko wrote:
> --- a/xen/arch/riscv/include/asm/riscv_encoding.h
> +++ b/xen/arch/riscv/include/asm/riscv_encoding.h
> @@ -109,6 +109,12 @@
>  #define SIP_SSIP			MIP_SSIP
>  #define SIP_STIP			MIP_STIP
>  
> +/* stvec/vstvec: MODE is bits [1:0], BASE is bits [XLEN-1:2] */
> +#define STVEC_MODE_MASK			_UL(0x3)
> +#define STVEC_MODE_DIRECT		_UL(0x0)
> +#define STVEC_MODE_VECTORED		_UL(0x1)

As on earlier occasions: Do the 0x here actually add any value? There are
none ...

> +#define STVEC_BASE_MASK			(~STVEC_MODE_MASK)
> +
>  #define PRV_U				_UL(0)
>  #define PRV_S				_UL(1)
>  #define PRV_M				_UL(3)

... here, for example.

> --- a/xen/arch/riscv/traps.c
> +++ b/xen/arch/riscv/traps.c
> @@ -294,5 +294,53 @@ enum mc_disposition arch_do_multicall_call(struct mc_state *state)
>  /* Redirect trap to Guest. */
>  void trap_redirect(const struct trap_info *trap)
>  {
> -    BUG_ON("unimplemented");
> +    struct cpu_user_regs *regs = vcpu_guest_cpu_user_regs(current);
> +    unsigned long vsstatus = csr_read(CSR_VSSTATUS);
> +
> +    /*
> +     * Redirecting a trap makes sense only if the trap was taken from
> +     * virtualized mode, i.e. sret is going to return to VS-mode.
> +     */
> +    ASSERT(regs->hstatus & HSTATUS_SPV);
> +
> +    /*
> +     * Only synchronous exceptions can be redirected. Interrupts must be
> +     * injected via hvip instead, so that the hardware itself performs
> +     * VS-mode trap entry, respecting vsstatus.SIE and the vectored
> +     * dispatch (BASE + 4 * cause) if vstvec is configured so.
> +     */
> +    ASSERT(!(trap->scause & CAUSE_IRQ_FLAG));
> +
> +    /* Change Guest SSTATUS.SPP bit */
> +    vsstatus &= ~SSTATUS_SPP;
> +    if ( regs->sstatus & SSTATUS_SPP )
> +        vsstatus |= SSTATUS_SPP;
> +
> +    /* Change Guest SSTATUS.SPIE bit */
> +    vsstatus &= ~SSTATUS_SPIE;
> +    if ( vsstatus & SSTATUS_SIE )
> +        vsstatus |= SSTATUS_SPIE;
> +
> +    /* Clear Guest SSTATUS.SIE bit */
> +    vsstatus &= ~SSTATUS_SIE;
> +
> +    /* Update Guest SSTATUS */
> +    csr_write(CSR_VSSTATUS, vsstatus);
> +
> +    /* Update Guest SCAUSE, STVAL, and SEPC */
> +    csr_write(CSR_VSCAUSE, trap->scause);
> +    csr_write(CSR_VSTVAL, trap->stval);
> +    csr_write(CSR_VSEPC, trap->sepc);
> +
> +    /*
> +     * Set Guest PC to Guest exception vector.
> +     *
> +     * vstvec's MODE field is not part of the address. Exceptions always
> +     * target BASE regardless of MODE, so mask it off explicitly instead of
> +     * relying on the hardwired zero bit of sepc to drop it.
> +     */
> +    regs->sepc = csr_read(CSR_VSTVEC) & STVEC_BASE_MASK;

Nit: Given how much the comment talks about MODE, imo using ~STVEC_MODE_MASK
here directly (and dropping STVEC_BASE_MASK) might be better.

Jan


^ permalink raw reply	[flat|nested] 244+ messages in thread

* Re: [PATCH v2 19/39] xen/riscv: implement trap redirection to a guest
  2026-08-27 15:21 ` [PATCH v2 19/39] xen/riscv: implement trap redirection to a guest Oleksii Kurochko
  2026-09-07 15:57   ` Baptiste Le Duc
  2026-09-08 14:16   ` Jan Beulich
@ 2026-09-08 14:16   ` Jan Beulich
  2 siblings, 0 replies; 244+ messages in thread
From: Jan Beulich @ 2026-09-08 14:16 UTC (permalink / raw)
  To: Oleksii Kurochko
  Cc: Romain Caritey, Baptiste Le Duc, Zheng Zhang, Alistair Francis,
	Connor Davis, Andrew Cooper, Anthony PERARD, Michal Orzel,
	Julien Grall, Roger Pau Monné, Stefano Stabellini, xen-devel

On 27.08.2026 17:21, Oleksii Kurochko wrote:
> --- a/xen/arch/riscv/include/asm/riscv_encoding.h
> +++ b/xen/arch/riscv/include/asm/riscv_encoding.h
> @@ -109,6 +109,12 @@
>  #define SIP_SSIP			MIP_SSIP
>  #define SIP_STIP			MIP_STIP
>  
> +/* stvec/vstvec: MODE is bits [1:0], BASE is bits [XLEN-1:2] */
> +#define STVEC_MODE_MASK			_UL(0x3)
> +#define STVEC_MODE_DIRECT		_UL(0x0)
> +#define STVEC_MODE_VECTORED		_UL(0x1)

As on earlier occasions: Do the 0x here actually add any value? There are
none ...

> +#define STVEC_BASE_MASK			(~STVEC_MODE_MASK)
> +
>  #define PRV_U				_UL(0)
>  #define PRV_S				_UL(1)
>  #define PRV_M				_UL(3)

... here, for example.

> --- a/xen/arch/riscv/traps.c
> +++ b/xen/arch/riscv/traps.c
> @@ -294,5 +294,53 @@ enum mc_disposition arch_do_multicall_call(struct mc_state *state)
>  /* Redirect trap to Guest. */
>  void trap_redirect(const struct trap_info *trap)
>  {
> -    BUG_ON("unimplemented");
> +    struct cpu_user_regs *regs = vcpu_guest_cpu_user_regs(current);
> +    unsigned long vsstatus = csr_read(CSR_VSSTATUS);
> +
> +    /*
> +     * Redirecting a trap makes sense only if the trap was taken from
> +     * virtualized mode, i.e. sret is going to return to VS-mode.
> +     */
> +    ASSERT(regs->hstatus & HSTATUS_SPV);
> +
> +    /*
> +     * Only synchronous exceptions can be redirected. Interrupts must be
> +     * injected via hvip instead, so that the hardware itself performs
> +     * VS-mode trap entry, respecting vsstatus.SIE and the vectored
> +     * dispatch (BASE + 4 * cause) if vstvec is configured so.
> +     */
> +    ASSERT(!(trap->scause & CAUSE_IRQ_FLAG));
> +
> +    /* Change Guest SSTATUS.SPP bit */
> +    vsstatus &= ~SSTATUS_SPP;
> +    if ( regs->sstatus & SSTATUS_SPP )
> +        vsstatus |= SSTATUS_SPP;
> +
> +    /* Change Guest SSTATUS.SPIE bit */
> +    vsstatus &= ~SSTATUS_SPIE;
> +    if ( vsstatus & SSTATUS_SIE )
> +        vsstatus |= SSTATUS_SPIE;
> +
> +    /* Clear Guest SSTATUS.SIE bit */
> +    vsstatus &= ~SSTATUS_SIE;
> +
> +    /* Update Guest SSTATUS */
> +    csr_write(CSR_VSSTATUS, vsstatus);
> +
> +    /* Update Guest SCAUSE, STVAL, and SEPC */
> +    csr_write(CSR_VSCAUSE, trap->scause);
> +    csr_write(CSR_VSTVAL, trap->stval);
> +    csr_write(CSR_VSEPC, trap->sepc);
> +
> +    /*
> +     * Set Guest PC to Guest exception vector.
> +     *
> +     * vstvec's MODE field is not part of the address. Exceptions always
> +     * target BASE regardless of MODE, so mask it off explicitly instead of
> +     * relying on the hardwired zero bit of sepc to drop it.
> +     */
> +    regs->sepc = csr_read(CSR_VSTVEC) & STVEC_BASE_MASK;

Nit: Given how much the comment talks about MODE, imo using ~STVEC_MODE_MASK
here directly (and dropping STVEC_BASE_MASK) might be better.

Jan


^ permalink raw reply	[flat|nested] 244+ messages in thread

* Re: [PATCH v2 19/39] xen/riscv: implement trap redirection to a guest
  2026-09-08 10:01     ` Oleksii Kurochko
@ 2026-09-08 14:58       ` Oleksii Kurochko
  2026-09-08 15:05         ` Jan Beulich
  0 siblings, 1 reply; 244+ messages in thread
From: Oleksii Kurochko @ 2026-09-08 14:58 UTC (permalink / raw)
  To: Baptiste Le Duc
  Cc: xen-devel, Romain Caritey, Zheng Zhang, Alistair Francis,
	Connor Davis, Andrew Cooper, Anthony PERARD, Michal Orzel,
	Jan Beulich, Julien Grall, Roger Pau Monné,
	Stefano Stabellini



On 9/8/26 12:01 PM, Oleksii Kurochko wrote:
> 
> 
> On 9/7/26 5:57 PM, Baptiste Le Duc wrote:
>>> Some traps taken by Xen on behalf of a guest can't or shouldn't be 
>>> handled
>>> by the hypervisor and have to be reflected to the guest's own S-mode 
>>> trap
>>> handler instead: the access faults which handle_guest_page_fault() 
>>> injects
>>> for a fault that can never become an emulated access, and, later on, a
>>> fault taken by the hlv/hlvx sequences of riscv_read_guest() while
>>> accessing guest memory on a vCPU's behalf.
>>>
>> Access faults from handle_guest_page_fault() aren't "taken by Xen on
>> behalf of a guest" as those are guest-page faults taken directly from the
>> guest's own execution. Xen just decides they can't be emulated and
>> reflects them back as access faults. Only the hlv/hlvx case is Xen
>> trapping on the guest's behalf (Xen itself executes the faulting 
>> access).> Conflating the two under one description makes the paragraph 
>> confusing.
>>
>> Suggest splitting into two:
>>
>>      Two kinds of traps can't or shouldn't be handled by the 
>> hypervisor and
>>      have to be reflected to the guest's own S-mode trap handler instead:
>>
>>      - Traps Xen takes on the guest's behalf: the hlv/hlvx sequences
>>      riscv_read_guest() uses to access guest memory.
>>      - Access faults handle_guest_page_fault() injects for a guest-page
>>      fault that can never become an emulated access.
> 
> Thanks, I'll update original paragraph with what you suggested.>
>>>
>>> Implement trap_redirect(), until now a BUG_ON() placeholder, for that
>>> purpose. It makes the trap appear to the guest as if it had been taken
>>> directly in VS-mode: the trap information is transferred to the guest's
>>> virtual supervisor CSRs and the vCPU is resumed at its exception 
>>> vector in
>>> supervisor mode, following the trap entry rules of the RISC-V privileged
>>> specification.
>>>
>>> Add the STVEC_* definitions needed to tell the BASE and MODE fields of
>>> vstvec apart.
>>>
>>> The implementation is based on kvm_riscv_vcpu_trap_redirect() from 
>>> Linux,
>>> with a few deviations:
>>>   - The function reads and writes physical VS-mode CSRs, so it is only
>>>     meaningful for the currently running vCPU. Instead of taking a
>>>     struct vcpu argument, it always operates on current.
>>>   - The MODE field of vstvec is masked off explicitly when computing the
>>>     exception target PC (exceptions always vector to BASE), rather than
>>>     relying on the hardwired zero bit of sepc to drop it on VM entry.
>>>   - Assertions document the preconditions: the trap must have been taken
>>>     from virtualized mode (hstatus.SPV set), and only synchronous
>>>     exceptions may be redirected - interrupts must be injected via hvip
>>>     instead, so that the hardware performs VS-mode trap entry itself,
>>>     respecting vsstatus.SIE and vectored vstvec dispatch.
>>>
>>> Signed-off-by: Oleksii Kurochko <oleksii.kurochko@gmail.com>
>>>
>>> diff --git a/xen/arch/riscv/include/asm/riscv_encoding.h b/xen/arch/ 
>>> riscv/include/asm/riscv_encoding.h
>>> index 2d2e7e11b3..b2071f4758 100644
>>> --- a/xen/arch/riscv/include/asm/riscv_encoding.h
>>> +++ b/xen/arch/riscv/include/asm/riscv_encoding.h
>>> @@ -109,6 +109,12 @@
>>>   #define SIP_SSIP            MIP_SSIP
>>>   #define SIP_STIP            MIP_STIP
>>> +/* stvec/vstvec: MODE is bits [1:0], BASE is bits [XLEN-1:2] */
>>> +#define STVEC_MODE_MASK            _UL(0x3)
>>> +#define STVEC_MODE_DIRECT        _UL(0x0)
>>> +#define STVEC_MODE_VECTORED        _UL(0x1)
>>> +#define STVEC_BASE_MASK            (~STVEC_MODE_MASK)
>> Nit: STVEC_MODE_DIRECT and STVEC_MODE_VECTORED aren't used anywhere in
>> this patch (only STVEC_BASE_MASK is). Either use them where you decide
>> exceptions always target BASE regardless of MODE, or drop them until a
>> patch that needs them.
> 
> IMO it is fine to introduce *_DIRECT/VECORED here as they are used 
> implicitly through STVEC_BASE_MASK and thereby it will be better to 
> introduce them here now instead of open-code them and then just update 
> STVEC_BASE_MASK again when *_DIRECT/VECORED will be re-introduced.
> 

Oh, sorry, you are right. STVEC_MODE_DIRECT and STVEC_MODE_VECTORED are 
really not used here (in this implementation). I planned to do:

#define STVEC_MODE_MASK (STVEC_MODE_DIRECT | STVEC_MODE_VECTORED)

But I missed to do in that way.

I will update the defintion of STVEC_MODE_MASK in suggested above way.

~ Oleksii


^ permalink raw reply	[flat|nested] 244+ messages in thread

* Re: [PATCH v2 19/39] xen/riscv: implement trap redirection to a guest
  2026-09-08 14:58       ` Oleksii Kurochko
@ 2026-09-08 15:05         ` Jan Beulich
  2026-09-08 15:47           ` Baptiste Le Duc
  0 siblings, 1 reply; 244+ messages in thread
From: Jan Beulich @ 2026-09-08 15:05 UTC (permalink / raw)
  To: Oleksii Kurochko
  Cc: xen-devel, Romain Caritey, Zheng Zhang, Alistair Francis,
	Connor Davis, Andrew Cooper, Anthony PERARD, Michal Orzel,
	Julien Grall, Roger Pau Monné, Stefano Stabellini,
	Baptiste Le Duc

On 08.09.2026 16:58, Oleksii Kurochko wrote:
> On 9/8/26 12:01 PM, Oleksii Kurochko wrote:
>> On 9/7/26 5:57 PM, Baptiste Le Duc wrote:
>>>> --- a/xen/arch/riscv/include/asm/riscv_encoding.h
>>>> +++ b/xen/arch/riscv/include/asm/riscv_encoding.h
>>>> @@ -109,6 +109,12 @@
>>>>   #define SIP_SSIP            MIP_SSIP
>>>>   #define SIP_STIP            MIP_STIP
>>>> +/* stvec/vstvec: MODE is bits [1:0], BASE is bits [XLEN-1:2] */
>>>> +#define STVEC_MODE_MASK            _UL(0x3)
>>>> +#define STVEC_MODE_DIRECT        _UL(0x0)
>>>> +#define STVEC_MODE_VECTORED        _UL(0x1)
>>>> +#define STVEC_BASE_MASK            (~STVEC_MODE_MASK)
>>> Nit: STVEC_MODE_DIRECT and STVEC_MODE_VECTORED aren't used anywhere in
>>> this patch (only STVEC_BASE_MASK is). Either use them where you decide
>>> exceptions always target BASE regardless of MODE, or drop them until a
>>> patch that needs them.
>>
>> IMO it is fine to introduce *_DIRECT/VECORED here as they are used 
>> implicitly through STVEC_BASE_MASK and thereby it will be better to 
>> introduce them here now instead of open-code them and then just update 
>> STVEC_BASE_MASK again when *_DIRECT/VECORED will be re-introduced.
>>
> 
> Oh, sorry, you are right. STVEC_MODE_DIRECT and STVEC_MODE_VECTORED are 
> really not used here (in this implementation). I planned to do:
> 
> #define STVEC_MODE_MASK (STVEC_MODE_DIRECT | STVEC_MODE_VECTORED)
> 
> But I missed to do in that way.
> 
> I will update the defintion of STVEC_MODE_MASK in suggested above way.

But that's yield a mask value of 1, when you want it to be 3. That ORing
together looks bogus to me anyway.

Jan


^ permalink raw reply	[flat|nested] 244+ messages in thread

* Re: [PATCH v2 19/39] xen/riscv: implement trap redirection to a guest
  2026-09-08 14:16   ` Jan Beulich
@ 2026-09-08 15:25     ` Oleksii Kurochko
  0 siblings, 0 replies; 244+ messages in thread
From: Oleksii Kurochko @ 2026-09-08 15:25 UTC (permalink / raw)
  To: Jan Beulich
  Cc: Romain Caritey, Baptiste Le Duc, Zheng Zhang, Alistair Francis,
	Connor Davis, Andrew Cooper, Anthony PERARD, Michal Orzel,
	Julien Grall, Roger Pau Monné, Stefano Stabellini, xen-devel



On 9/8/26 4:16 PM, Jan Beulich wrote:
> On 27.08.2026 17:21, Oleksii Kurochko wrote:
>> --- a/xen/arch/riscv/include/asm/riscv_encoding.h
>> +++ b/xen/arch/riscv/include/asm/riscv_encoding.h
>> @@ -109,6 +109,12 @@
>>   #define SIP_SSIP			MIP_SSIP
>>   #define SIP_STIP			MIP_STIP
>>   
>> +/* stvec/vstvec: MODE is bits [1:0], BASE is bits [XLEN-1:2] */
>> +#define STVEC_MODE_MASK			_UL(0x3)
>> +#define STVEC_MODE_DIRECT		_UL(0x0)
>> +#define STVEC_MODE_VECTORED		_UL(0x1)
> 
> As on earlier occasions: Do the 0x here actually add any value? There are
> none ...
> 
>> +#define STVEC_BASE_MASK			(~STVEC_MODE_MASK)
>> +
>>   #define PRV_U				_UL(0)
>>   #define PRV_S				_UL(1)
>>   #define PRV_M				_UL(3)
> 
> ... here, for example.

Agree, not too much sense.

> 
>> --- a/xen/arch/riscv/traps.c
>> +++ b/xen/arch/riscv/traps.c
>> @@ -294,5 +294,53 @@ enum mc_disposition arch_do_multicall_call(struct mc_state *state)
>>   /* Redirect trap to Guest. */
>>   void trap_redirect(const struct trap_info *trap)
>>   {
>> -    BUG_ON("unimplemented");
>> +    struct cpu_user_regs *regs = vcpu_guest_cpu_user_regs(current);
>> +    unsigned long vsstatus = csr_read(CSR_VSSTATUS);
>> +
>> +    /*
>> +     * Redirecting a trap makes sense only if the trap was taken from
>> +     * virtualized mode, i.e. sret is going to return to VS-mode.
>> +     */
>> +    ASSERT(regs->hstatus & HSTATUS_SPV);
>> +
>> +    /*
>> +     * Only synchronous exceptions can be redirected. Interrupts must be
>> +     * injected via hvip instead, so that the hardware itself performs
>> +     * VS-mode trap entry, respecting vsstatus.SIE and the vectored
>> +     * dispatch (BASE + 4 * cause) if vstvec is configured so.
>> +     */
>> +    ASSERT(!(trap->scause & CAUSE_IRQ_FLAG));
>> +
>> +    /* Change Guest SSTATUS.SPP bit */
>> +    vsstatus &= ~SSTATUS_SPP;
>> +    if ( regs->sstatus & SSTATUS_SPP )
>> +        vsstatus |= SSTATUS_SPP;
>> +
>> +    /* Change Guest SSTATUS.SPIE bit */
>> +    vsstatus &= ~SSTATUS_SPIE;
>> +    if ( vsstatus & SSTATUS_SIE )
>> +        vsstatus |= SSTATUS_SPIE;
>> +
>> +    /* Clear Guest SSTATUS.SIE bit */
>> +    vsstatus &= ~SSTATUS_SIE;
>> +
>> +    /* Update Guest SSTATUS */
>> +    csr_write(CSR_VSSTATUS, vsstatus);
>> +
>> +    /* Update Guest SCAUSE, STVAL, and SEPC */
>> +    csr_write(CSR_VSCAUSE, trap->scause);
>> +    csr_write(CSR_VSTVAL, trap->stval);
>> +    csr_write(CSR_VSEPC, trap->sepc);
>> +
>> +    /*
>> +     * Set Guest PC to Guest exception vector.
>> +     *
>> +     * vstvec's MODE field is not part of the address. Exceptions always
>> +     * target BASE regardless of MODE, so mask it off explicitly instead of
>> +     * relying on the hardwired zero bit of sepc to drop it.
>> +     */
>> +    regs->sepc = csr_read(CSR_VSTVEC) & STVEC_BASE_MASK;
> 
> Nit: Given how much the comment talks about MODE, imo using ~STVEC_MODE_MASK
> here directly (and dropping STVEC_BASE_MASK) might be better.
> 
Agree, it could be dropped. I will update that in v3.

Thanks.

~ Oleksii


^ permalink raw reply	[flat|nested] 244+ messages in thread

* Re: [PATCH v2 19/39] xen/riscv: implement trap redirection to a guest
  2026-09-08 15:05         ` Jan Beulich
@ 2026-09-08 15:47           ` Baptiste Le Duc
  2026-09-08 15:58             ` Jan Beulich
  0 siblings, 1 reply; 244+ messages in thread
From: Baptiste Le Duc @ 2026-09-08 15:47 UTC (permalink / raw)
  To: Jan Beulich
  Cc: Oleksii Kurochko, xen-devel, Romain Caritey, Zheng Zhang,
	Alistair Francis, Connor Davis, Andrew Cooper, Anthony PERARD,
	Michal Orzel, Julien Grall, Roger Pau Monné,
	Stefano Stabellini, Baptiste Le Duc

On 2026-09-08 17:05 +0200, Jan Beulich wrote:
> On 08.09.2026 16:58, Oleksii Kurochko wrote:
> > On 9/8/26 12:01 PM, Oleksii Kurochko wrote:
> >> On 9/7/26 5:57 PM, Baptiste Le Duc wrote:
> >>>> --- a/xen/arch/riscv/include/asm/riscv_encoding.h
> >>>> +++ b/xen/arch/riscv/include/asm/riscv_encoding.h
> >>>> @@ -109,6 +109,12 @@
> >>>>   #define SIP_SSIP            MIP_SSIP
> >>>>   #define SIP_STIP            MIP_STIP
> >>>> +/* stvec/vstvec: MODE is bits [1:0], BASE is bits [XLEN-1:2] */
> >>>> +#define STVEC_MODE_MASK            _UL(0x3)
> >>>> +#define STVEC_MODE_DIRECT        _UL(0x0)
> >>>> +#define STVEC_MODE_VECTORED        _UL(0x1)
> >>>> +#define STVEC_BASE_MASK            (~STVEC_MODE_MASK)
> >>> Nit: STVEC_MODE_DIRECT and STVEC_MODE_VECTORED aren't used anywhere in
> >>> this patch (only STVEC_BASE_MASK is). Either use them where you decide
> >>> exceptions always target BASE regardless of MODE, or drop them until a
> >>> patch that needs them.
> >>
> >> IMO it is fine to introduce *_DIRECT/VECORED here as they are used 
> >> implicitly through STVEC_BASE_MASK and thereby it will be better to 
> >> introduce them here now instead of open-code them and then just update 
> >> STVEC_BASE_MASK again when *_DIRECT/VECORED will be re-introduced.
> >>
> > 
> > Oh, sorry, you are right. STVEC_MODE_DIRECT and STVEC_MODE_VECTORED are 
> > really not used here (in this implementation). I planned to do:
> > 
> > #define STVEC_MODE_MASK (STVEC_MODE_DIRECT | STVEC_MODE_VECTORED)
> > 
> > But I missed to do in that way.
> > 
> > I will update the defintion of STVEC_MODE_MASK in suggested above way.
> 
> But that's yield a mask value of 1, when you want it to be 3. That ORing
> together looks bogus to me anyway.
>
Yes I agree. It might be enough, for the moment, to only keep #define
STVEC_MODE_MASK _UL(3) and drop the others defines (even STVEC_BASE_MASK as Jan
mentioned the comment in the caller is already precise enough).
> 
> Jan
> 
> 




^ permalink raw reply	[flat|nested] 244+ messages in thread

* Re: [PATCH v2 20/39] xen/riscv: detect Shtvala
  2026-09-08 10:15     ` Oleksii Kurochko
@ 2026-09-08 15:49       ` Baptiste Le Duc
  0 siblings, 0 replies; 244+ messages in thread
From: Baptiste Le Duc @ 2026-09-08 15:49 UTC (permalink / raw)
  To: Oleksii Kurochko
  Cc: Baptiste Le Duc, xen-devel, Romain Caritey, Zheng Zhang,
	Alistair Francis, Connor Davis, Andrew Cooper, Anthony PERARD,
	Michal Orzel, Jan Beulich, Julien Grall, Roger Pau Monné,
	Stefano Stabellini

On 2026-09-08 12:15 +0200, Oleksii Kurochko wrote:
> 
> 
> On 9/7/26 5:57 PM, Baptiste Le Duc wrote:
> >> Shtvala says that htval is written with the faulting guest physical address
> >> on a guest-page fault. The H extension itself allows an implementation to
> >> write htval with either that address or with zero, so where the extension is
> >> absent a zero htval cannot be told apart from a genuine fault on guest
> >> physical address 0-3.
> >>
> >> It is not offered to guests. Shtvala describes the HS-mode trap interface,
> >> which a VS-mode guest never sees, and the H extension it belongs to is
> >> already withheld from guests. Its guest-facing counterpart is a separate
> >> extension, Shvstvala.
> >>
> >> Signed-off-by: Oleksii Kurochko <oleksii.kurochko@gmail.com>
> >>
> > I thought this commit message is not very clear. Here is a more direct
> > suggestion:
> > 
> > ```
> > The H extension allows htval, on a guest-page fault, to be written with
> > either the faulting guest physical address or zero. Shtvala extension
> > removes the ambiguity of this zero-write by guaranteeing that htval is
> > written with the faulting guest physical address in every circumstance
> > permitted by the ISA.
> > 
> > Not offered to guests: Shtvala describes htval, an HS-mode-only trap
> > register a VS-mode guest never touches and the H extension it belongs to
> > is already hidden from guests. The guest-visible equivalent is a
> > separate extension, Shvstvala, covering vstval instead.
> > ```
> 
> Sounds good to me. I will apply your suggestion.
> 
> > 
> > Btw, I saw Linux has Documentation/devicetree/bindings/riscv/extensions.yaml
> > which describes all of the extensions supported, don't you think it would
> > be useful to have the same in docs/misc/devicetree/...?
> 
> I think it could be useful.
> 
> I think also about booting.txt already exitsted in the codebase. Would 
> you be okay with that? I think I will add the section to booting.txt and 
> pointing to riscv_isa_ext[] in cpufeature.c and so we won't miss an 
> update of doc if we will add or remove support of an extension. Does it 
> sound good to you?
> 
Yes it sounds okay!
Thanks
> ~ Oleksii
> 
> 
> 
> 




^ permalink raw reply	[flat|nested] 244+ messages in thread

* Re: [PATCH v2 19/39] xen/riscv: implement trap redirection to a guest
  2026-09-08 15:47           ` Baptiste Le Duc
@ 2026-09-08 15:58             ` Jan Beulich
  0 siblings, 0 replies; 244+ messages in thread
From: Jan Beulich @ 2026-09-08 15:58 UTC (permalink / raw)
  To: Baptiste Le Duc
  Cc: Oleksii Kurochko, xen-devel, Romain Caritey, Zheng Zhang,
	Alistair Francis, Connor Davis, Andrew Cooper, Anthony PERARD,
	Michal Orzel, Julien Grall, Roger Pau Monné,
	Stefano Stabellini

On 08.09.2026 17:47, Baptiste Le Duc wrote:
> On 2026-09-08 17:05 +0200, Jan Beulich wrote:
>> On 08.09.2026 16:58, Oleksii Kurochko wrote:
>>> On 9/8/26 12:01 PM, Oleksii Kurochko wrote:
>>>> On 9/7/26 5:57 PM, Baptiste Le Duc wrote:
>>>>>> --- a/xen/arch/riscv/include/asm/riscv_encoding.h
>>>>>> +++ b/xen/arch/riscv/include/asm/riscv_encoding.h
>>>>>> @@ -109,6 +109,12 @@
>>>>>>   #define SIP_SSIP            MIP_SSIP
>>>>>>   #define SIP_STIP            MIP_STIP
>>>>>> +/* stvec/vstvec: MODE is bits [1:0], BASE is bits [XLEN-1:2] */
>>>>>> +#define STVEC_MODE_MASK            _UL(0x3)
>>>>>> +#define STVEC_MODE_DIRECT        _UL(0x0)
>>>>>> +#define STVEC_MODE_VECTORED        _UL(0x1)
>>>>>> +#define STVEC_BASE_MASK            (~STVEC_MODE_MASK)
>>>>> Nit: STVEC_MODE_DIRECT and STVEC_MODE_VECTORED aren't used anywhere in
>>>>> this patch (only STVEC_BASE_MASK is). Either use them where you decide
>>>>> exceptions always target BASE regardless of MODE, or drop them until a
>>>>> patch that needs them.
>>>>
>>>> IMO it is fine to introduce *_DIRECT/VECORED here as they are used 
>>>> implicitly through STVEC_BASE_MASK and thereby it will be better to 
>>>> introduce them here now instead of open-code them and then just update 
>>>> STVEC_BASE_MASK again when *_DIRECT/VECORED will be re-introduced.
>>>>
>>>
>>> Oh, sorry, you are right. STVEC_MODE_DIRECT and STVEC_MODE_VECTORED are 
>>> really not used here (in this implementation). I planned to do:
>>>
>>> #define STVEC_MODE_MASK (STVEC_MODE_DIRECT | STVEC_MODE_VECTORED)
>>>
>>> But I missed to do in that way.
>>>
>>> I will update the defintion of STVEC_MODE_MASK in suggested above way.
>>
>> But that's yield a mask value of 1, when you want it to be 3. That ORing
>> together looks bogus to me anyway.
>>
> Yes I agree. It might be enough, for the moment, to only keep #define
> STVEC_MODE_MASK _UL(3) and drop the others defines (even STVEC_BASE_MASK as Jan
> mentioned the comment in the caller is already precise enough).

To me having STVEC_MODE_MASK without at least one of
STVEC_MODE_{DIRECT,VECTORED} would feel odd / incomplete.

Jan


^ permalink raw reply	[flat|nested] 244+ messages in thread

* Re: [PATCH v2 17/39] xen/riscv: decouple INSN_PSEUDO_VS_* from the hypervisor's XLEN
  2026-09-08  9:34     ` Oleksii Kurochko
@ 2026-09-08 16:04       ` Baptiste Le Duc
  2026-09-09 12:57         ` Oleksii Kurochko
  0 siblings, 1 reply; 244+ messages in thread
From: Baptiste Le Duc @ 2026-09-08 16:04 UTC (permalink / raw)
  To: Oleksii Kurochko
  Cc: Baptiste Le Duc, xen-devel, Romain Caritey, Zheng Zhang,
	Alistair Francis, Connor Davis, Andrew Cooper, Anthony PERARD,
	Michal Orzel, Jan Beulich, Julien Grall, Roger Pau Monné,
	Stefano Stabellini

On 2026-09-08 11:34 +0200, Oleksii Kurochko wrote:
> 
> 
> On 9/7/26 5:57 PM, Baptiste Le Duc wrote:
> >> htinst reports a pseudoinstruction when a guest page fault is taken on an
> >> implicit memory access done for VS-stage address translation. Four such
> >> values are defined, differing in the access type (read or write) and in the
> >> access width: 4 bytes (0x2000/0x2020) or 8 bytes (0x3000/0x3020).
> >>
> >> That width is the width of a VS-stage PTE, i.e. it follows the guest's
> >> paging mode (4 bytes for Sv32, 8 bytes for Sv39 and wider) and has nothing
> >> to do with the XLEN Xen itself is built for. Selecting just one pair with
> >> where a guest running with VSXL=32 and Sv32 in vsatp produces the 4-byte
> > Sentence is broken. Guess you mean "Selecting just one pair based on Xen's XLEN misses the case where
> > …".
> 
> Likely it is becuase of my low level English but it seems that original 
> version wast okay and what you added is just a part from prev. sentence 
> but I will add your suggestion for better clearness. Thanks for noticing 
> that!
> 
> > 
> >> forms. Such an htinst would not be recognized as a pseudoinstruction and the
> >> fault would be mistaken for an ordinary MMIO trap: Xen would fetch and
> >> decode whatever instruction sepc happens to point at (unrelated to the
> >> access which faulted) and emulate it against a guest physical address
> >> derived from htval, which for an implicit access holds the address of a
> >> VS-stage PTE rather than of any access the guest performed.
> >>
> >> Define all four values unconditionally instead, named after the access width
> >> they encode rather than after the build's XLEN. On RV32 the 8-byte forms
> >> simply never occur, so recognizing them costs nothing.
> >>
> >> Dropping the ladder loses no build-time coverage: a build for an XLEN other
> >> than 32 or 64 already fails on the equivalent ladders in asm/asm.h and
> >> asm/config.h, so no replacement #error is needed here. Adding one keyed on
> >> CONFIG_RISCV_* would in any case re-introduce exactly the conflation this
> >> patch removes.
> >>
> >> This diverges from the imported version of riscv_encoding.h.
> >>
> >> No functional change: the values have no user yet.
> >>
> >> Signed-off-by: Oleksii Kurochko <oleksii.kurochko@gmail.com>
> >>
> >> diff --git a/xen/arch/riscv/include/asm/riscv_encoding.h b/xen/arch/riscv/include/asm/riscv_encoding.h
> >> index c63e5e3046..2d2e7e11b3 100644
> >> --- a/xen/arch/riscv/include/asm/riscv_encoding.h
> >> +++ b/xen/arch/riscv/include/asm/riscv_encoding.h
> >> @@ -839,25 +839,17 @@
> >>   #define INSN_MASK_FENCE_TSO		0xffffffff
> >>   #define INSN_MATCH_FENCE_TSO		0x8330000f
> >>   
> >> -#if __riscv_xlen == 64
> >> -
> >>   /* 64-bit read for VS-stage address translation (RV64) */
> >> -#define INSN_PSEUDO_VS_LOAD		0x00003000
> >> +#define INSN_PSEUDO_VS_LOAD64		0x00003000
> >>   
> >>   /* 64-bit write for VS-stage address translation (RV64) */
> >> -#define INSN_PSEUDO_VS_STORE	0x00003020
> >> -
> >> -#elif __riscv_xlen == 32
> >> +#define INSN_PSEUDO_VS_STORE64		0x00003020
> >>   
> >>   /* 32-bit read for VS-stage address translation (RV32) */
> >> -#define INSN_PSEUDO_VS_LOAD		0x00002000
> >> +#define INSN_PSEUDO_VS_LOAD32		0x00002000
> >>   
> >>   /* 32-bit write for VS-stage address translation (RV32) */
> >> -#define INSN_PSEUDO_VS_STORE	0x00002020
> >> -
> > Whole point of patch is these no longer depend on build XLEN, yet
> > comments still say "(RV64)"/"(RV32)" which could be confusing. Maybe it
> > should be better to indicate, as the spec does, that RV32 values are
> > used when VSXLEN=32 (only sv32 paging mode) and RV64 values when
> > VSXLEN=64 (sv39+ paging modes).
> > 
> 
> Could you please clarify to me what in the spec it is?

It is the same table as you indicated below (but mine is Table 56, so it
seems we don't have the same spec version, I'm using the version 20260120).

When I found unclear is that "RV32" or "RV64" alone is ambiguous,
since host and guest can differ (e.g. RV64 host running an RV32 guest).
Saying it depends on VSXLEN instead makes clear which one is meant.

> 
> This comments are just copy from the spec:
> 
> Table 39. Special pseudoinstruction values for guest-page faults. The 
> RV32 values are used when VSXLEN=32, and the RV64 values when VSXLEN=64.
> Value           Meaning
> 0x00002000      32-bit read for VS-stage address translation (RV32)
> 0x00002020      32-bit write for VS-stage address translation (RV32)
> 
> Value           Meaning
> 0x00003000      64-bit read for VS-stage address translation (RV64)
> 0x00003020      64-bit write for VS-stage address translation (RV64)
> 
> So the comments are just copy of "Meaning" column from the spec.
> 
> And I think that Meaning column is fine here as we could have a case of 
> when hypervisor has XLEN=64 but guests could be on it RV32 and RV64 and 
> if a guest is RV32 (what means VSXLEN=32) then the comment above 
> defintion mean that we hav 32-bit read/write VS-stage address 
> translation for (RV32) guest and the similar is for RV64.
> 
> Do I miss something? Is a comments make more sense now or I have to 
> still update them in some way?
>
I think it would be more clear to modify the comment by that:
/* 32-bit read for VS-stage address translation (VSXLEN=32) */

But now with your explanation, your comment is more clear as it specify
VS-stage address, so the guest. Feel free to adopt my version or not.
> 
> Thanks!
> 
> ~ Oleksii
> 
> 
> 
> 




^ permalink raw reply	[flat|nested] 244+ messages in thread

* Re: [PATCH v2 16/39] xen/riscv: extend exception tables with type and data fields
  2026-09-08  9:19     ` Oleksii Kurochko
@ 2026-09-08 16:26       ` Baptiste Le Duc
  0 siblings, 0 replies; 244+ messages in thread
From: Baptiste Le Duc @ 2026-09-08 16:26 UTC (permalink / raw)
  To: Oleksii Kurochko
  Cc: Baptiste Le Duc, xen-devel, Romain Caritey, Zheng Zhang,
	Alistair Francis, Connor Davis, Andrew Cooper, Anthony PERARD,
	Michal Orzel, Jan Beulich, Julien Grall, Roger Pau Monné,
	Stefano Stabellini

On 2026-09-08 11:19 +0200, Oleksii Kurochko wrote:
> 
> 
> On 9/7/26 5:57 PM, Baptiste Le Duc wrote:
> >> Extend the RISC-V exception table format to include a type and
> >> auxiliary data field.
> >>
> >> The existing format only supports simple fixups. Some use cases require
> >> additional context from the fault (e.g. capturing trap information),
> >> which cannot be expressed with the current EX_TYPE_FIXUP entries.
> >>
> >> Introduce a generic ASM_EXTABLE_RAW() helper to describe entries with a
> >> handler type and associated data. Reimplement ASM_EXTABLE() in terms of
> >> it using EX_TYPE_FIXUP for compatibility.
> >>
> >> Add EX_TYPE_TRAP_INFO to allow handlers to retrieve trap state
> >> (sepc/scause/stval) and pass it to the fixup path. The data field is
> >> used to encode which GPR contains a pointer to a struct trap_info.
> >>
> >> Provide ASM_EXTABLE_TRAP_INFO() as a convenience wrapper for this case.
> >>
> >> Also add gpr-num.h, providing symbolic GPR numbers for use in assembly
> >> and inline asm. This is derived from Linux 6.16 with minor adjustments such
> >> as using .irp instead of open-coding the same using a set of .equ.
> >>
> >> Update the exception handling code to dispatch based on the entry type.
> >>
> >> Signed-off-by: Oleksii Kurochko <oleksii.kurochko@gmail.com>
> >>
> >> diff --git a/xen/arch/riscv/extable.c b/xen/arch/riscv/extable.c
> >> index 5b89c4278c..6470198d01 100644
> >> --- a/xen/arch/riscv/extable.c
> >> +++ b/xen/arch/riscv/extable.c
> >> @@ -6,8 +6,10 @@
> >>   #include <xen/sort.h>
> >>   #include <xen/virtual_region.h>
> >>   
> >> +#include <asm/csr.h>
> >>   #include <asm/extable.h>
> >>   #include <asm/processor.h>
> >> +#include <asm/traps.h>
> >>   
> >>   #define EX_FIELD(ptr, field) ((unsigned long)&(ptr)->field + (ptr)->field)
> >>   
> >> @@ -32,6 +34,12 @@ static void __init cf_check swap_ex(void *a, void *b)
> >>   
> >>       x->fixup = y->fixup + delta;
> >>       y->fixup = tmp.fixup - delta;
> >> +
> >> +    x->type = y->type;
> >> +    y->type = tmp.type;
> >> +
> >> +    x->data = y->data;
> >> +    y->data = tmp.data;
> >>   }
> >>   
> >>   static int cf_check cmp_ex(const void *a, const void *b)
> >> @@ -59,7 +67,49 @@ static void ex_handler_fixup(const struct exception_table_entry *ex,
> >>       regs->sepc = ex_fixup(ex);
> >>   }
> >>   
> >> -bool fixup_exception(struct cpu_user_regs *regs)
> >> +#define CHECK_GPR_INDEX(num, name)                      \
> >> +    BUILD_BUG_ON(offsetof(struct cpu_user_regs, name)   \
> >> +                 != (num) * sizeof(unsigned long));
> >> +
> >> +static unsigned long regs_get_gpr(const struct cpu_user_regs *regs,
> >> +                                  unsigned int num)
> >> +{
> >> +    /*
> >> +     * The GPR number -> struct index mapping below relies on x0..x31 being
> >> +     * laid out at the start of struct cpu_user_regs in architectural order,
> >> +     * matching the register numbers GPR_LIST() hands to the assembler.
> >> +     */
> >> +    GPR_LIST(CHECK_GPR_INDEX)
> >> +
> >> +    ASSERT(num < 32);
> >> +
> >> +    return ((const unsigned long *)regs)[num];
> >> +}
> >> +
> >> +#undef CHECK_GPR_INDEX
> >> +
> >> +static void ex_handler_trap_info(const struct exception_table_entry *ex,
> >> +                                 struct cpu_user_regs *regs,
> >> +                                 unsigned long cause)
> >> +{
> >> +    struct trap_info *trap_info =
> >> +        (struct trap_info *)regs_get_gpr(regs, ex->data);
> >> +
> >> +    BUG_ON(!trap_info);
> >> +
> >> +    /*
> >> +     * Only stval still needs a CSR read: sepc and scause were already
> >> +     * captured by the trap entry path and do_trap() respectively. Latch
> >> +     * trap_info->sepc before regs->sepc is pointed at the fixup code.
> >> +     */
> >> +    trap_info->sepc = regs->sepc;
> >> +    trap_info->scause = cause;
> >> +    trap_info->stval = csr_read(CSR_STVAL);
> >> +
> >> +    regs->sepc = ex_fixup(ex);
> >> +}
> >> +
> >> +bool fixup_exception(struct cpu_user_regs *regs, unsigned long cause)
> >>   {
> >>       unsigned long pc = regs->sepc;
> >>       const struct virtual_region *region = find_text_region(pc);
> >> @@ -77,7 +127,23 @@ bool fixup_exception(struct cpu_user_regs *regs)
> >>       if ( !ex )
> >>           return false;
> >>   
> >> -    ex_handler_fixup(ex, regs);
> >> +    switch ( ex->type )
> >> +    {
> >> +    case EX_TYPE_FIXUP:
> >> +        ex_handler_fixup(ex, regs);
> >> +        break;
> >> +
> >> +    case EX_TYPE_TRAP_INFO:
> >> +        ex_handler_trap_info(ex, regs, cause);
> >> +        break;
> >> +
> >> +    default:
> >> +        printk(XENLOG_ERR
> >> +               "Unsupported exception table entry type %u for pc %#lx\n",
> >> +               ex->type, pc);
> >> +
> >> +        return false;
> >> +    }
> >>   
> >>       return true;
> >>   }
> >> diff --git a/xen/arch/riscv/include/asm/extable.h b/xen/arch/riscv/include/asm/extable.h
> >> index c0128a9181..7378f86e7e 100644
> >> --- a/xen/arch/riscv/include/asm/extable.h
> >> +++ b/xen/arch/riscv/include/asm/extable.h
> >> @@ -3,17 +3,24 @@
> >>   #ifndef ASM__RISCV__ASM_EXTABLE_H
> >>   #define ASM__RISCV__ASM_EXTABLE_H
> >>   
> >> +#include <asm/gpr-num.h>
> >> +
> >> +#define EX_TYPE_FIXUP       0
> >> +#define EX_TYPE_TRAP_INFO   1
> >> +
> >>   #ifdef __ASSEMBLER__
> >>   
> >> -#define ASM_EXTABLE(insn, fixup) \
> >> -    .pushsection .ex_table, "a"; \
> >> -    .balign     4;               \
> >> -    .word       (insn) - .;      \
> >> -    .word       (fixup) - .;     \
> >> +#define ASM_EXTABLE_RAW(insn, fixup, type, data)    \
> >> +    .pushsection .ex_table, "a";                    \
> >> +    .balign     4;                                  \
> >> +    .word       (insn) - .;                         \
> >> +    .word       (fixup) - .;                        \
> >> +    .half       (type);                             \
> >> +    .half       (data);                             \
> >>       .popsection
> >>   
> >> -.macro asm_extable, insn, fixup
> >> -    ASM_EXTABLE(\insn, \fixup)
> >> +.macro _asm_extable, insn, fixup
> >> +    ASM_EXTABLE_RAW(\insn, \fixup, EX_TYPE_FIXUP, 0)
> >>   .endm
> >>   
> >>   #else /* __ASSEMBLER__ */
> >> @@ -23,20 +30,36 @@
> >>   
> >>   struct cpu_user_regs;
> >>   
> >> -#define ASM_EXTABLE(insn, fixup)      \
> >> -    ".pushsection .ex_table, \"a\"\n" \
> >> -    ".balign    4\n"                  \
> >> -    ".word      (" #insn " - .)\n"    \
> >> -    ".word      (" #fixup " - .)\n"   \
> >> +#define ASM_EXTABLE_RAW(insn, fixup, type, data)    \
> >> +    ".pushsection .ex_table, \"a\"\n"               \
> >> +    ".balign    4\n"                                \
> >> +    ".word      (" insn ") - .\n"                   \
> >> +    ".word      (" fixup ") - .\n"                  \
> >> +    ".half      (" type ")\n"                       \
> >> +    ".half      (" data ")\n"                       \
> >>       ".popsection\n"
> >>   
> >> +#define ASM_EXTABLE(insn, fixup)    \
> >> +    ASM_EXTABLE_RAW(#insn, #fixup, __stringify(EX_TYPE_FIXUP), "0")
> >> +
> >> +#define EX_TRAP_INFO_REG(gpr)   \
> >> +    "(.L_gpr_num_" #gpr ")"
> >> +
> >> +#define ASM_EXTABLE_TRAP_INFO(insn, fixup, data)                    \
> >> +    DEFINE_ASM_GPR_NUMS                                             \
> >> +    ASM_EXTABLE_RAW(#insn, #fixup, __stringify(EX_TYPE_TRAP_INFO),  \
> >> +                    EX_TRAP_INFO_REG(data))
> >> +
> >>   /*
> >> - * The exception table consists of pairs of relative offsets: the first
> >> - * is the relative offset to an instruction that is allowed to fault,
> >> - * and the second is the relative offset at which the program should
> >> - * continue. No general-purpose registers are modified by the exception
> >> - * handling mechanism itself, so it is up to the fixup code to handle
> >> - * any necessary state cleanup.
> >> + * Each exception table entry consists of two relative offsets and a
> >> + * handler description: `insn` is the relative offset to an instruction
> >> + * that is allowed to fault, `fixup` is the relative offset at which the
> >> + * program should continue, `type` selects how the exception is handled
> >> + * (EX_TYPE_*), and `data` holds auxiliary information for the handler
> >> + * (e.g. for EX_TYPE_TRAP_INFO, the number of the GPR that contains a
> >> + * pointer to a struct trap_info). No general-purpose registers are
> >> + * modified by the exception handling mechanism itself, so it is up to
> >> + * the fixup code to handle any necessary state cleanup.
> >>    *
> >>    * The exception table and fixup code live out of line with the main
> >>    * instruction path. This means when everything is well, we don't even
> >> @@ -45,14 +68,15 @@ struct cpu_user_regs;
> >>    */
> >>   struct exception_table_entry {
> >>       int32_t insn, fixup;
> >> +    uint16_t type, data;
> >>   };
> >>   
> >>   extern struct exception_table_entry __start___ex_table[];
> >>   extern struct exception_table_entry __stop___ex_table[];
> >>   
> >>   void sort_exception_tables(void);
> >> -bool fixup_exception(struct cpu_user_regs *regs);
> >> +bool fixup_exception(struct cpu_user_regs *regs, unsigned long cause);
> >>   
> >> -#endif /* __ASSEMBLY__ */
> >> +#endif /* __ASSEMBLER__ */
> >>   
> >>   #endif /* ASM__RISCV__ASM_EXTABLE_H */
> >> diff --git a/xen/arch/riscv/include/asm/gpr-num.h b/xen/arch/riscv/include/asm/gpr-num.h
> >> new file mode 100644
> >> index 0000000000..3b97a72e6c
> >> --- /dev/null
> >> +++ b/xen/arch/riscv/include/asm/gpr-num.h
> >> @@ -0,0 +1,37 @@
> >> +/* SPDX-License-Identifier: GPL-2.0-only */
> >> +#ifndef RISCV_GPR_NUM_H
> >> +#define RISCV_GPR_NUM_H
> > Nit: commit message says this is derived from Linux 6.16. Other
> > imported RISC-V headers here carry an in-file note (bitops.h: "Based on
> > linux/arch/.../bitops.h") but this file doesn't.
> >> +/*
> >> + * GPRs by ABI name, together with their register number (x0 .. x31).
> >> + *
> >> + * This is the single source of truth for the mapping: it generates the
> >> + * .L_gpr_num_<name> assembler symbols used to turn a register name emitted
> >> + * by the compiler into a register number, and struct cpu_user_regs is
> >> + * checked against it at build time (see regs_get_gpr()). Neither list can
> >> + * therefore be changed without the other.
> >> + */
> >> +#define GPR_LIST(x)                                 \
> >> +    x(0,  zero) x(1,  ra)  x(2,  sp)  x(3,  gp)     \
> >> +    x(4,  tp)   x(5,  t0)  x(6,  t1)  x(7,  t2)     \
> >> +    x(8,  s0)   x(9,  s1)  x(10, a0)  x(11, a1)     \
> >> +    x(12, a2)   x(13, a3)  x(14, a4)  x(15, a5)     \
> >> +    x(16, a6)   x(17, a7)  x(18, s2)  x(19, s3)     \
> >> +    x(20, s4)   x(21, s5)  x(22, s6)  x(23, s7)     \
> >> +    x(24, s8)   x(25, s9)  x(26, s10) x(27, s11)    \
> >> +    x(28, t3)   x(29, t4)  x(30, t5)  x(31, t6)
> >> +
> >> +#ifdef __ASSEMBLER__
> >> +
> >> +#define GPR_NUM_EQU(num, name)  .equ .L_gpr_num_##name, num;
> >> +GPR_LIST(GPR_NUM_EQU)
> >> +#undef GPR_NUM_EQU
> >> +
> >> +#else /* __ASSEMBLER__ */
> >> +
> >> +#define GPR_NUM_EQU(num, name)  ".equ .L_gpr_num_" #name ", " #num "\n"
> >> +#define DEFINE_ASM_GPR_NUMS     GPR_LIST(GPR_NUM_EQU)
> >> +
> >> +#endif /* __ASSEMBLER__ */
> >> +
> >> +#endif /* RISCV_GPR_NUM_H */
> >> diff --git a/xen/arch/riscv/include/asm/processor.h b/xen/arch/riscv/include/asm/processor.h
> >> index b1745c1071..e7b0f2321a 100644
> >> --- a/xen/arch/riscv/include/asm/processor.h
> >> +++ b/xen/arch/riscv/include/asm/processor.h
> >> @@ -12,7 +12,19 @@
> >>   
> >>   #ifndef __ASSEMBLER__
> >>   
> >> -/* On stack VCPU state */
> >> +/*
> >> + * On stack VCPU state.
> >> + *
> >> + * x0..x31 must remain at the start of this structure, in architectural
> >> + * register-number order: code which resolves a register number to its saved
> >> + * value indexes this structure directly (instruction emulation via
> >> + * REG_PTR() from asm/riscv_encoding.h, exception table fixups via
> >> + * regs_get_gpr()). ->zero therefore has to stay at offset 0 and must always
> >> + * read as 0, since it supplies the value of x0 when x0 is used as a source
> >> + * operand. The layout is checked against GPR_LIST() at build time; see
> >> + * regs_get_gpr() in extable.c. Do not reorder these fields or insert
> >> + * anything between them.
> >> + */
> >>   {
> >>       unsigned long zero;
> > Comment claims ->zero "must always read as 0" as it's hard-wired to zero
> > by the HW, but nothing enforces that, it's still a plain writable
> > unsigned long. Maybe a write-side counterpart that special-cases num==0
> > as a no-op, or with a minimum ASSERT(num != 0) / BUG_ON(num == 0) to
> > anticipate any future forbidden writes.
> > 
> 
> Could you please clarify where do you want me to put this check in this 
> patch? In regs_get_gpr()? There is no write-side in this patch. Am i 
> missing something?

Honestly, I don't know where it would be most appropriate. But I'd like
to highlight that nothing strictly forbids Xen code from writing a value
!= 0 into regs->zero.

I saw that in emulate_load(), which is introduced later, you used a
branch to avoid an incorrect load into regs->zero:
    /*
    * A load into x0 discards its result: writing regs->zero would break
    * the invariant that it reads as zero when x0 is a source operand
    * elsewhere.
    */

One solution could be to make cpu_user_regs private and add get/set
methods, with a check in the setter that forbids this type of write, but
that would imply some big changes, so it may not be the appropriate fix.

> 
> 
> Thanks in advance.
> 
> ~ Oleksii
> 
> 
> 




^ permalink raw reply	[flat|nested] 244+ messages in thread

* Re: [PATCH v2 16/39] xen/riscv: extend exception tables with type and data fields
  2026-09-08 13:44   ` Jan Beulich
@ 2026-09-09 11:20     ` Oleksii Kurochko
  2026-09-09 12:22       ` Jan Beulich
  0 siblings, 1 reply; 244+ messages in thread
From: Oleksii Kurochko @ 2026-09-09 11:20 UTC (permalink / raw)
  To: Jan Beulich
  Cc: Romain Caritey, Baptiste Le Duc, Zheng Zhang, Alistair Francis,
	Connor Davis, Andrew Cooper, Anthony PERARD, Michal Orzel,
	Julien Grall, Roger Pau Monné, Stefano Stabellini, xen-devel



On 9/8/26 3:44 PM, Jan Beulich wrote:
> On 27.08.2026 17:21, Oleksii Kurochko wrote:
>> @@ -59,7 +67,49 @@ static void ex_handler_fixup(const struct exception_table_entry *ex,
>>       regs->sepc = ex_fixup(ex);
>>   }
>>   
>> -bool fixup_exception(struct cpu_user_regs *regs)
>> +#define CHECK_GPR_INDEX(num, name)                      \
>> +    BUILD_BUG_ON(offsetof(struct cpu_user_regs, name)   \
>> +                 != (num) * sizeof(unsigned long));
> 
> Nit: Placement of the !=. Also there should be no semicolon here; it wants
> to ...
> 
>> +static unsigned long regs_get_gpr(const struct cpu_user_regs *regs,
>> +                                  unsigned int num)
>> +{
>> +    /*
>> +     * The GPR number -> struct index mapping below relies on x0..x31 being
>> +     * laid out at the start of struct cpu_user_regs in architectural order,
>> +     * matching the register numbers GPR_LIST() hands to the assembler.
>> +     */
>> +    GPR_LIST(CHECK_GPR_INDEX)
> 
> ... appear here instead, for this to actually look like a statement.

I will apply that.

> 
>> +    ASSERT(num < 32);
>> +
>> +    return ((const unsigned long *)regs)[num];
> 
> What about release builds? You'd happily overrun the array there. Maybe
> (ab)use array_index_nospec() here?

num is coming not from guest, not from calculation in runtume, it is 
generated by assembler at the build time. So it should be always correct.

So just having the following looks okay to me:

static unsigned long regs_get_gpr(const struct cpu_user_regs *regs,
                                   unsigned int num)
{
#define CHECK_GPR_INDEX(num, name) \
     BUILD_BUG_ON(offsetof(struct cpu_user_regs, name) != \
                  (num) * sizeof(unsigned long))

#define GPR_CASE(nr, name) case nr: return regs->name;

     /*
      * The GPR number -> struct index mapping below relies on x0..x31 being
      * laid out at the start of struct cpu_user_regs in architectural 
order,
      * matching the register numbers GPR_LIST() hands to the assembler.
      */
     GPR_LIST(CHECK_GPR_INDEX);

#undef CHECK_GPR_INDEX

     switch ( num )
     {
     GPR_LIST(GPR_CASE)
     }

#undef GPR_CASE

     ASSERT_UNREACHABLE();

     return 0;
}

Any thoughts on that regard?
  >> +}
>> +
>> +#undef CHECK_GPR_INDEX
> 
> If the sole use of the macro is in a single function, it wants #define-ing
> (and #undef-ing) there, not outside of it.
> 

I will move inside.

>> +static void ex_handler_trap_info(const struct exception_table_entry *ex,
>> +                                 struct cpu_user_regs *regs,
>> +                                 unsigned long cause)
>> +{
>> +    struct trap_info *trap_info =
>> +        (struct trap_info *)regs_get_gpr(regs, ex->data);
>> +
>> +    BUG_ON(!trap_info);
> 
> This feels extremely weak. As you're fetching from a GPR, the majority of
> possible values stored in GPRs is going to be invalid, not just NULL. And
> while the accesses below would trap on NULL anyway, whether other bogus
> values would trap is pretty hard to predict. If trap_info is expected to
> always live on the stack, why not check for that (perhaps also check that
> the low few bits are clear)?
> 

Considering the nature if how trap_info is filled I think we could just 
drop BUG_ON(), it is guaranteed by compilation that trap_info will be 
correct.

I think it could be also hard to force trap_info be always allocated on 
the stack.

>> @@ -23,20 +30,36 @@
>>   
>>   struct cpu_user_regs;
>>   
>> -#define ASM_EXTABLE(insn, fixup)      \
>> -    ".pushsection .ex_table, \"a\"\n" \
>> -    ".balign    4\n"                  \
>> -    ".word      (" #insn " - .)\n"    \
>> -    ".word      (" #fixup " - .)\n"   \
>> +#define ASM_EXTABLE_RAW(insn, fixup, type, data)    \
>> +    ".pushsection .ex_table, \"a\"\n"               \
>> +    ".balign    4\n"                                \
>> +    ".word      (" insn ") - .\n"                   \
>> +    ".word      (" fixup ") - .\n"                  \
>> +    ".half      (" type ")\n"                       \
>> +    ".half      (" data ")\n"                       \
>>       ".popsection\n"
>>   
>> +#define ASM_EXTABLE(insn, fixup)    \
>> +    ASM_EXTABLE_RAW(#insn, #fixup, __stringify(EX_TYPE_FIXUP), "0")
>> +
>> +#define EX_TRAP_INFO_REG(gpr)   \
>> +    "(.L_gpr_num_" #gpr ")"
> 
> This doesn't need to be wrapped across lines, does it?

Oh, really, it could be one line.

> 
>> +#define ASM_EXTABLE_TRAP_INFO(insn, fixup, data)                    \
>> +    DEFINE_ASM_GPR_NUMS                                             \
>> +    ASM_EXTABLE_RAW(#insn, #fixup, __stringify(EX_TYPE_TRAP_INFO),  \
>> +                    EX_TRAP_INFO_REG(data))
> 
> There's no visible statement separator between DEFINE_ASM_GPR_NUMS and
> ASM_EXTABLE_RAW(), which only works because DEFINE_ASM_GPR_NUMS appends
> a separator also at the very end of its expansion. I think that better
> would be changed, such that at use sites such as this one a separator
> becomes mandatory.

I will do the following then:

  #define ASM_EXTABLE_TRAP_INFO(insn, fixup, data)                    \
-    DEFINE_ASM_GPR_NUMS                                             \
+    DEFINE_ASM_GPR_NUMS "\n"                                        \
      ASM_EXTABLE_RAW(#insn, #fixup, __stringify(EX_TYPE_TRAP_INFO),  \
                      EX_TRAP_INFO_REG(data))

diff --git a/xen/arch/riscv/include/asm/gpr-num.h 
b/xen/arch/riscv/include/asm/gpr-num.h
index 3b97a72e6c30..d497bd501e87 100644
--- a/xen/arch/riscv/include/asm/gpr-num.h
+++ b/xen/arch/riscv/include/asm/gpr-num.h
@@ -23,13 +23,19 @@

  #ifdef __ASSEMBLER__

-#define GPR_NUM_EQU(num, name)  .equ .L_gpr_num_##name, num;
+/*
+ * The separator is emitted ahead of each entry rather than after it, 
so that
+ * the expansion doesn't end with one: whatever follows a use of the 
list has
+ * to supply its own separator, instead of silently relying on a 
trailing one.
+ */
+#define GPR_NUM_EQU(num, name)  ; .equ .L_gpr_num_##name, num
  GPR_LIST(GPR_NUM_EQU)
  #undef GPR_NUM_EQU

  #else /* __ASSEMBLER__ */

-#define GPR_NUM_EQU(num, name)  ".equ .L_gpr_num_" #name ", " #num "\n"
+/* See the comment ahead of the __ASSEMBLER__ flavour above. */
+#define GPR_NUM_EQU(num, name)  "\n.equ .L_gpr_num_" #name ", " #num
  #define DEFINE_ASM_GPR_NUMS     GPR_LIST(GPR_NUM_EQU)

> 
>>   /*
>> - * The exception table consists of pairs of relative offsets: the first
>> - * is the relative offset to an instruction that is allowed to fault,
>> - * and the second is the relative offset at which the program should
>> - * continue. No general-purpose registers are modified by the exception
>> - * handling mechanism itself, so it is up to the fixup code to handle
>> - * any necessary state cleanup.
>> + * Each exception table entry consists of two relative offsets and a
>> + * handler description: `insn` is the relative offset to an instruction
>> + * that is allowed to fault, `fixup` is the relative offset at which the
>> + * program should continue,
> 
> "... in case of a fault, ..."

Applied.

> 
>> --- /dev/null
>> +++ b/xen/arch/riscv/include/asm/gpr-num.h
>> @@ -0,0 +1,37 @@
>> +/* SPDX-License-Identifier: GPL-2.0-only */
>> +#ifndef RISCV_GPR_NUM_H
>> +#define RISCV_GPR_NUM_H
>> +
>> +/*
>> + * GPRs by ABI name, together with their register number (x0 .. x31).
>> + *
>> + * This is the single source of truth for the mapping:
> 
> True until here, but ...
> 
>> it generates the
>> + * .L_gpr_num_<name> assembler symbols
> 
> ... no, it doesn't. It's ...

I will update the comment to:
/*
  * GPRs by ABI name, together with their register number (x0 .. x31).
  *
  * This is the single source of truth for the mapping; users expand it with
  * their own per-register macro. Among them, struct cpu_user_regs is 
checked
  * against this list at build time (see regs_get_gpr()), so neither 
list can
  * be changed without the other.
  */
#define GPR_LIST(x)                                 \
     ...

and then ...

> 
>> used to turn a register name emitted
>> + * by the compiler into a register number, and struct cpu_user_regs is
>> + * checked against it at build time (see regs_get_gpr()). Neither list can
>> + * therefore be changed without the other.
>> + */
>> +#define GPR_LIST(x)                                 \
>> +    x(0,  zero) x(1,  ra)  x(2,  sp)  x(3,  gp)     \
>> +    x(4,  tp)   x(5,  t0)  x(6,  t1)  x(7,  t2)     \
>> +    x(8,  s0)   x(9,  s1)  x(10, a0)  x(11, a1)     \
>> +    x(12, a2)   x(13, a3)  x(14, a4)  x(15, a5)     \
>> +    x(16, a6)   x(17, a7)  x(18, s2)  x(19, s3)     \
>> +    x(20, s4)   x(21, s5)  x(22, s6)  x(23, s7)     \
>> +    x(24, s8)   x(25, s9)  x(26, s10) x(27, s11)    \
>> +    x(28, t3)   x(29, t4)  x(30, t5)  x(31, t6)
>> +
>> +#ifdef __ASSEMBLER__
>> +
>> +#define GPR_NUM_EQU(num, name)  .equ .L_gpr_num_##name, num;
>> +GPR_LIST(GPR_NUM_EQU)
>> +#undef GPR_NUM_EQU
> 
> ... this construct which does. This is relevant to separate, since
> GPR_LIST() is also used elsewhere.
> 

... here:

/*
  * Generate the .L_gpr_num_<name> assembler symbols, used to turn a 
register
  * name emitted by the compiler into a register number.
  *
  * The separator is emitted ahead of each entry rather than after it, 
so that


>> --- a/xen/arch/riscv/include/asm/processor.h
>> +++ b/xen/arch/riscv/include/asm/processor.h
>> @@ -12,7 +12,19 @@
>>   
>>   #ifndef __ASSEMBLER__
>>   
>> -/* On stack VCPU state */
>> +/*
>> + * On stack VCPU state.
>> + *
>> + * x0..x31 must remain at the start of this structure, in architectural
>> + * register-number order:
> 
> I understand that the order need retaining. But why would it being at the
> start of the struct be (overly) relevant? You could use the "zero" field
> as the anchor for calculations.

You're right.

The placement requirement comes only from REG_PTR() computing a byte 
offset from the struct base. Anchoring it on ->zero instead removes the 
need for the block to sit at the start:

#define REG_PTR(insn, pos, regs) \
     (&(regs)->zero + (REG_OFFSET(insn, pos) / REGBYTES))

I'll do that and reword the comment to state the actual invariants: the 
x0..x31 fields have to stay contiguous and in ascending register-number 
order, and ->zero has to read as 0.

Thanks.

~ Oleksii


^ permalink raw reply related	[flat|nested] 244+ messages in thread

* Re: [PATCH v2 21/39] xen/riscv: resolve the faulting guest physical address
  2026-08-27 15:21 ` [PATCH v2 21/39] xen/riscv: resolve the faulting guest physical address Oleksii Kurochko
@ 2026-09-09 12:04   ` Baptiste Le Duc
  2026-09-11 12:56     ` Oleksii Kurochko
  2026-09-10 15:06   ` Jan Beulich
  1 sibling, 1 reply; 244+ messages in thread
From: Baptiste Le Duc @ 2026-09-09 12:04 UTC (permalink / raw)
  To: Oleksii Kurochko
  Cc: xen-devel, Romain Caritey, Baptiste Le Duc, Zheng Zhang,
	Alistair Francis, Connor Davis, Andrew Cooper, Anthony PERARD,
	Michal Orzel, Jan Beulich, Julien Grall, Roger Pau Monné,
	Stefano Stabellini

> Take the guest physical address from htval and stval: on a guest-page fault
> htval holds it shifted right by 2, so that an address wider than XLEN fits,
> and stval holds the faulting guest virtual address, whose two least
> significant bits are those of the guest physical address. The shift is done
> on paddr_t rather than on the raw register: a guest physical address is 34
> bits wide on RV32 with Sv32x4, so shifting an XLEN-wide value would drop its
> top two bits.
>
> Those two low bits come from stval only for a fault on an explicit access.
> Where one is taken on an implicit access made for VS-stage translation htval
> holds the address of the VS-stage PTE which could not be read, while stval
> still holds the guest virtual address which started the walk, and the low
> bits of the address written to htval are zero instead. htinst tells the two
> apart, which is what the spec points at it for.
> 
I'd just precise (the spec is also not clear on this point though), what
is "current XLEN" here, clearly indicate that htval holds the GPA >> 2 and
stval holds VGA and also put the two different cases we need to
distinguish clearly:
```
Recover the guest physical address from htval and stval. On a guest-page
fault to hypervisor, htval holds the guest physical address shifted
right by 2, so that an address wider than HSXLEN fits, and stval holds
the faulting guest virtual address. The shift is done on paddr_t rather
than on the raw register: a guest physical address is 34 bits wide on
RV32 with Sv32x4, so shifting an XLEN-wide value would drop its top two
bits.

However, there are two cases to distinguish when recovering the
faulting GPA:
    - Explicit memory access: we use the two least significant bits of
    stval, which are the same as those of the guest physical
    address.
    - Implicit memory access for VS-stage translation: the two least
    significant bits of htval are zero.

These two cases can be distinguished using the value provided in
register htinst.
```
> 
> stval needs no check against an ISA extension: a guest-page fault writes it
> with the faulting guest virtual address regardless. Sstvala would not be the
> right thing to test for either (it covers stval across every trap type
> which writes it, a wider guarantee than what is needed here).
> 
> htval does need one. The H extension lets an implementation write it with
> either the faulting address or zero, so without Shtvala a zero htval cannot
> be told apart from a genuine fault on guest physical address 0-3, and the
> address has to be recovered by decoding the access and walking the VS-stage
> page tables in software instead. That is left as a TODO, and until it is
> written such hardware panics rather than acting on an address which may not
> be the one which faulted.
> 
> Signed-off-by: Oleksii Kurochko <oleksii.kurochko@gmail.com>
>
> diff --git a/xen/arch/riscv/emulate.c b/xen/arch/riscv/emulate.c
> index f9da075104..ff530ef2df 100644
> --- a/xen/arch/riscv/emulate.c
> +++ b/xen/arch/riscv/emulate.c
> @@ -9,6 +9,7 @@
>  #include <xen/sched.h>
>  #include <xen/types.h>
>  
> +#include <asm/cpufeature.h>
>  #include <asm/csr.h>
>  #include <asm/current.h>
>  #include <asm/emulate.h>
> @@ -62,10 +63,28 @@ static bool htinst_is_pseudo(unsigned long htinst)
>      }
>  }
>  
> -/* Reconstruct the guest physical address of the access which faulted. */
> +/* Resolves the guest physical address the access faulted on into @gf->gpa. */
Why did you change the comment apart for adding @gf->gpa? I think the
"reconstruct" is more clear but there might be another reason why you
changed it.

-- 
Baptiste Le Duc <baptiste.le-duc@vates.tech>


^ permalink raw reply	[flat|nested] 244+ messages in thread

* Re: [PATCH v2 22/39] xen/riscv: add guest memory read helper
  2026-08-27 15:21 ` [PATCH v2 22/39] xen/riscv: add guest memory read helper Oleksii Kurochko
@ 2026-09-09 12:04   ` Baptiste Le Duc
  2026-09-10 15:19     ` Jan Beulich
  2026-09-11 13:06     ` Oleksii Kurochko
  2026-09-10 15:28   ` Jan Beulich
  1 sibling, 2 replies; 244+ messages in thread
From: Baptiste Le Duc @ 2026-09-09 12:04 UTC (permalink / raw)
  To: Oleksii Kurochko
  Cc: xen-devel, Romain Caritey, Baptiste Le Duc, Zheng Zhang,
	Alistair Francis, Connor Davis, Andrew Cooper, Anthony PERARD,
	Michal Orzel, Jan Beulich, Julien Grall, Roger Pau Monné,
	Stefano Stabellini

> Introduce riscv_read_guest() to allow Xen to safely read guest memory
> using HLV/HLVX instructions while reliably capturing trap context.

> This is required for instruction fetch emulation and MMIO decoding, where
> Xen must inspect guest memory that may not be directly accessible and may
> fault.
> 
> The implementation is based on kvm_riscv_vcpu_unpriv_read() from Linux,
> with one deviation: the hlv/hlvx instructions translate the guest address
> through the live vsatp/hgatp CSRs, i.e. through the address space of the
> currently running vCPU, so the function can only be called safely for
> current. Instead of taking a struct vcpu argument, it always operates on
> current directly.
> 
> Signed-off-by: Oleksii Kurochko <oleksii.kurochko@gmail.com>
>
> diff --git a/xen/arch/riscv/guestcopy.c b/xen/arch/riscv/guestcopy.c
> index 8a89212e0b..b2327822ac 100644
> --- a/xen/arch/riscv/guestcopy.c
> +++ b/xen/arch/riscv/guestcopy.c
> @@ -6,6 +6,7 @@
>  #include <xen/string.h>
>  
>  #include <asm/guest_access.h>
> +#include <asm/traps.h>
>  
>  #define COPY_from_guest     0U
>  #define COPY_to_guest       BIT(0, U)
> @@ -114,3 +115,89 @@ unsigned long copy_to_guest_phys(struct domain *d, paddr_t gpa, void *buf,
>      return copy_guest(buf, gpa, len, GPA_INFO(d),
>                        COPY_to_guest | COPY_gpa);
>  }
> +
> +/*
> + * Read machine word from guest memory
> + *
> + * @guest_addr: Guest address to read
> + * @read_insn: Flag representing whether we are reading instruction
> + * @trap: Output pointer to trap details if something went wrong during read
> + *
> + * The hlv/hlvx instructions translate guest_addr through the live
> + * vsatp/hgatp CSRs, so the read is only meaningful for the address
> + * space of the currently running vCPU.
> + *
> + * At most two halfwords are fetched when @read_insn is true, i.e. encodings
> + * wider than 32 bits are not supported. Such an encoding cannot be completed
> + * by calling this function again at @guest_addr + 4: the length check is
> + * applied to the first halfword read, which would then be a continuation of
> + * the instruction rather than its opcode. It is up to the caller to reject
> + * anything that is neither a 16- nor a 32-bit encoding.
> + */
> +unsigned long riscv_read_guest(unsigned long guest_addr, bool read_insn,
> +                               struct trap_info *trap)
Nit: every other function in this file / declared in this header
(raw_copy_from_guest, copy_to_guest_phys, ...) has no riscv_ prefix. Why
does this one get it?
> +{
> +    /*
> +     * Poison the result: if the very first access faults, the fixup skips
> +     * over the loads without writing it. Callers must check trap->scause.
> +     */
> +    unsigned long val = ~0UL, tmp;
>
The actual "did a trap happen" contract callers rely on is
trap->scause == 0. If the very first access faults, fixup_exception() will
write scause accordingly to a non-zero value, but nothing in this function
clears trap->scause on the success path.
> +
> +    /*
> +     * hlv/hlvx use hstatus.SPVP for the privilege of the access, and the
> +     * live vsatp/hgatp for the translation. Xen never installs a value of
> +     * its own in hstatus (it is only saved on trap entry and restored
> +     * before sret) and it doesn't reschedule before returning to the
> +     * guest, so all three still belong to the vCPU which trapped.
> +     *
> +     * Check the saved copy rather than the live CSR: a nested trap taken
> +     * from HS-mode clears hstatus.SPV in the CSR (but leaves SPVP alone).
> +     */
> +    ASSERT(vcpu_guest_cpu_user_regs(current)->hstatus & HSTATUS_SPV);
Just to understand, what is the aim of this check? Is it to be sure this
function has been called during a guest fault and not a nested HS fault?

-- 
Baptiste Le Duc <baptiste.le-duc@vates.tech>


^ permalink raw reply	[flat|nested] 244+ messages in thread

* Re: [PATCH v2 16/39] xen/riscv: extend exception tables with type and data fields
  2026-09-09 11:20     ` Oleksii Kurochko
@ 2026-09-09 12:22       ` Jan Beulich
  2026-09-09 12:42         ` Oleksii Kurochko
  0 siblings, 1 reply; 244+ messages in thread
From: Jan Beulich @ 2026-09-09 12:22 UTC (permalink / raw)
  To: Oleksii Kurochko
  Cc: Romain Caritey, Baptiste Le Duc, Zheng Zhang, Alistair Francis,
	Connor Davis, Andrew Cooper, Anthony PERARD, Michal Orzel,
	Julien Grall, Roger Pau Monné, Stefano Stabellini, xen-devel

On 09.09.2026 13:20, Oleksii Kurochko wrote:
> On 9/8/26 3:44 PM, Jan Beulich wrote:
>> On 27.08.2026 17:21, Oleksii Kurochko wrote:
>>> @@ -59,7 +67,49 @@ static void ex_handler_fixup(const struct exception_table_entry *ex,
>>>       regs->sepc = ex_fixup(ex);
>>>   }
>>>   
>>> -bool fixup_exception(struct cpu_user_regs *regs)
>>> +#define CHECK_GPR_INDEX(num, name)                      \
>>> +    BUILD_BUG_ON(offsetof(struct cpu_user_regs, name)   \
>>> +                 != (num) * sizeof(unsigned long));
>>
>> Nit: Placement of the !=. Also there should be no semicolon here; it wants
>> to ...
>>
>>> +static unsigned long regs_get_gpr(const struct cpu_user_regs *regs,
>>> +                                  unsigned int num)
>>> +{
>>> +    /*
>>> +     * The GPR number -> struct index mapping below relies on x0..x31 being
>>> +     * laid out at the start of struct cpu_user_regs in architectural order,
>>> +     * matching the register numbers GPR_LIST() hands to the assembler.
>>> +     */
>>> +    GPR_LIST(CHECK_GPR_INDEX)
>>
>> ... appear here instead, for this to actually look like a statement.
> 
> I will apply that.
> 
>>
>>> +    ASSERT(num < 32);
>>> +
>>> +    return ((const unsigned long *)regs)[num];
>>
>> What about release builds? You'd happily overrun the array there. Maybe
>> (ab)use array_index_nospec() here?
> 
> num is coming not from guest, not from calculation in runtume, it is 
> generated by assembler at the build time. So it should be always correct.
> 
> So just having the following looks okay to me:
> 
> static unsigned long regs_get_gpr(const struct cpu_user_regs *regs,
>                                    unsigned int num)
> {
> #define CHECK_GPR_INDEX(num, name) \
>      BUILD_BUG_ON(offsetof(struct cpu_user_regs, name) != \
>                   (num) * sizeof(unsigned long))
> 
> #define GPR_CASE(nr, name) case nr: return regs->name;
> 
>      /*
>       * The GPR number -> struct index mapping below relies on x0..x31 being
>       * laid out at the start of struct cpu_user_regs in architectural 
> order,
>       * matching the register numbers GPR_LIST() hands to the assembler.
>       */
>      GPR_LIST(CHECK_GPR_INDEX);
> 
> #undef CHECK_GPR_INDEX
> 
>      switch ( num )
>      {
>      GPR_LIST(GPR_CASE)
>      }
> 
> #undef GPR_CASE
> 
>      ASSERT_UNREACHABLE();
> 
>      return 0;
> }
> 
> Any thoughts on that regard?

Depends very much on how efficiently the compiler translates this (as opposed
to the other variant).

Jan


^ permalink raw reply	[flat|nested] 244+ messages in thread

* Re: [PATCH v2 16/39] xen/riscv: extend exception tables with type and data fields
  2026-09-09 12:22       ` Jan Beulich
@ 2026-09-09 12:42         ` Oleksii Kurochko
  0 siblings, 0 replies; 244+ messages in thread
From: Oleksii Kurochko @ 2026-09-09 12:42 UTC (permalink / raw)
  To: Jan Beulich
  Cc: Romain Caritey, Baptiste Le Duc, Zheng Zhang, Alistair Francis,
	Connor Davis, Andrew Cooper, Anthony PERARD, Michal Orzel,
	Julien Grall, Roger Pau Monné, Stefano Stabellini, xen-devel



On 9/9/26 2:22 PM, Jan Beulich wrote:
> On 09.09.2026 13:20, Oleksii Kurochko wrote:
>> On 9/8/26 3:44 PM, Jan Beulich wrote:
>>> On 27.08.2026 17:21, Oleksii Kurochko wrote:
>>>> @@ -59,7 +67,49 @@ static void ex_handler_fixup(const struct exception_table_entry *ex,
>>>>        regs->sepc = ex_fixup(ex);
>>>>    }
>>>>    
>>>> -bool fixup_exception(struct cpu_user_regs *regs)
>>>> +#define CHECK_GPR_INDEX(num, name)                      \
>>>> +    BUILD_BUG_ON(offsetof(struct cpu_user_regs, name)   \
>>>> +                 != (num) * sizeof(unsigned long));
>>>
>>> Nit: Placement of the !=. Also there should be no semicolon here; it wants
>>> to ...
>>>
>>>> +static unsigned long regs_get_gpr(const struct cpu_user_regs *regs,
>>>> +                                  unsigned int num)
>>>> +{
>>>> +    /*
>>>> +     * The GPR number -> struct index mapping below relies on x0..x31 being
>>>> +     * laid out at the start of struct cpu_user_regs in architectural order,
>>>> +     * matching the register numbers GPR_LIST() hands to the assembler.
>>>> +     */
>>>> +    GPR_LIST(CHECK_GPR_INDEX)
>>>
>>> ... appear here instead, for this to actually look like a statement.
>>
>> I will apply that.
>>
>>>
>>>> +    ASSERT(num < 32);
>>>> +
>>>> +    return ((const unsigned long *)regs)[num];
>>>
>>> What about release builds? You'd happily overrun the array there. Maybe
>>> (ab)use array_index_nospec() here?
>>
>> num is coming not from guest, not from calculation in runtume, it is
>> generated by assembler at the build time. So it should be always correct.
>>
>> So just having the following looks okay to me:
>>
>> static unsigned long regs_get_gpr(const struct cpu_user_regs *regs,
>>                                     unsigned int num)
>> {
>> #define CHECK_GPR_INDEX(num, name) \
>>       BUILD_BUG_ON(offsetof(struct cpu_user_regs, name) != \
>>                    (num) * sizeof(unsigned long))
>>
>> #define GPR_CASE(nr, name) case nr: return regs->name;
>>
>>       /*
>>        * The GPR number -> struct index mapping below relies on x0..x31 being
>>        * laid out at the start of struct cpu_user_regs in architectural
>> order,
>>        * matching the register numbers GPR_LIST() hands to the assembler.
>>        */
>>       GPR_LIST(CHECK_GPR_INDEX);
>>
>> #undef CHECK_GPR_INDEX
>>
>>       switch ( num )
>>       {
>>       GPR_LIST(GPR_CASE)
>>       }
>>
>> #undef GPR_CASE
>>
>>       ASSERT_UNREACHABLE();
>>
>>       return 0;
>> }
>>
>> Any thoughts on that regard?
> 
> Depends very much on how efficiently the compiler translates this (as opposed
> to the other variant).

It is worthier. I will follow then your original suggestion and use 
array_index_nospec().

~ Oleksii


^ permalink raw reply	[flat|nested] 244+ messages in thread

* Re: [PATCH v2 17/39] xen/riscv: decouple INSN_PSEUDO_VS_* from the hypervisor's XLEN
  2026-09-08 16:04       ` Baptiste Le Duc
@ 2026-09-09 12:57         ` Oleksii Kurochko
  0 siblings, 0 replies; 244+ messages in thread
From: Oleksii Kurochko @ 2026-09-09 12:57 UTC (permalink / raw)
  To: Baptiste Le Duc
  Cc: xen-devel, Romain Caritey, Zheng Zhang, Alistair Francis,
	Connor Davis, Andrew Cooper, Anthony PERARD, Michal Orzel,
	Jan Beulich, Julien Grall, Roger Pau Monné,
	Stefano Stabellini



On 9/8/26 6:04 PM, Baptiste Le Duc wrote:
> On 2026-09-08 11:34 +0200, Oleksii Kurochko wrote:
>>
>>
>> On 9/7/26 5:57 PM, Baptiste Le Duc wrote:
>>>> htinst reports a pseudoinstruction when a guest page fault is taken on an
>>>> implicit memory access done for VS-stage address translation. Four such
>>>> values are defined, differing in the access type (read or write) and in the
>>>> access width: 4 bytes (0x2000/0x2020) or 8 bytes (0x3000/0x3020).
>>>>
>>>> That width is the width of a VS-stage PTE, i.e. it follows the guest's
>>>> paging mode (4 bytes for Sv32, 8 bytes for Sv39 and wider) and has nothing
>>>> to do with the XLEN Xen itself is built for. Selecting just one pair with
>>>> where a guest running with VSXL=32 and Sv32 in vsatp produces the 4-byte
>>> Sentence is broken. Guess you mean "Selecting just one pair based on Xen's XLEN misses the case where
>>> …".
>>
>> Likely it is becuase of my low level English but it seems that original
>> version wast okay and what you added is just a part from prev. sentence
>> but I will add your suggestion for better clearness. Thanks for noticing
>> that!
>>
>>>
>>>> forms. Such an htinst would not be recognized as a pseudoinstruction and the
>>>> fault would be mistaken for an ordinary MMIO trap: Xen would fetch and
>>>> decode whatever instruction sepc happens to point at (unrelated to the
>>>> access which faulted) and emulate it against a guest physical address
>>>> derived from htval, which for an implicit access holds the address of a
>>>> VS-stage PTE rather than of any access the guest performed.
>>>>
>>>> Define all four values unconditionally instead, named after the access width
>>>> they encode rather than after the build's XLEN. On RV32 the 8-byte forms
>>>> simply never occur, so recognizing them costs nothing.
>>>>
>>>> Dropping the ladder loses no build-time coverage: a build for an XLEN other
>>>> than 32 or 64 already fails on the equivalent ladders in asm/asm.h and
>>>> asm/config.h, so no replacement #error is needed here. Adding one keyed on
>>>> CONFIG_RISCV_* would in any case re-introduce exactly the conflation this
>>>> patch removes.
>>>>
>>>> This diverges from the imported version of riscv_encoding.h.
>>>>
>>>> No functional change: the values have no user yet.
>>>>
>>>> Signed-off-by: Oleksii Kurochko <oleksii.kurochko@gmail.com>
>>>>
>>>> diff --git a/xen/arch/riscv/include/asm/riscv_encoding.h b/xen/arch/riscv/include/asm/riscv_encoding.h
>>>> index c63e5e3046..2d2e7e11b3 100644
>>>> --- a/xen/arch/riscv/include/asm/riscv_encoding.h
>>>> +++ b/xen/arch/riscv/include/asm/riscv_encoding.h
>>>> @@ -839,25 +839,17 @@
>>>>    #define INSN_MASK_FENCE_TSO		0xffffffff
>>>>    #define INSN_MATCH_FENCE_TSO		0x8330000f
>>>>    
>>>> -#if __riscv_xlen == 64
>>>> -
>>>>    /* 64-bit read for VS-stage address translation (RV64) */
>>>> -#define INSN_PSEUDO_VS_LOAD		0x00003000
>>>> +#define INSN_PSEUDO_VS_LOAD64		0x00003000
>>>>    
>>>>    /* 64-bit write for VS-stage address translation (RV64) */
>>>> -#define INSN_PSEUDO_VS_STORE	0x00003020
>>>> -
>>>> -#elif __riscv_xlen == 32
>>>> +#define INSN_PSEUDO_VS_STORE64		0x00003020
>>>>    
>>>>    /* 32-bit read for VS-stage address translation (RV32) */
>>>> -#define INSN_PSEUDO_VS_LOAD		0x00002000
>>>> +#define INSN_PSEUDO_VS_LOAD32		0x00002000
>>>>    
>>>>    /* 32-bit write for VS-stage address translation (RV32) */
>>>> -#define INSN_PSEUDO_VS_STORE	0x00002020
>>>> -
>>> Whole point of patch is these no longer depend on build XLEN, yet
>>> comments still say "(RV64)"/"(RV32)" which could be confusing. Maybe it
>>> should be better to indicate, as the spec does, that RV32 values are
>>> used when VSXLEN=32 (only sv32 paging mode) and RV64 values when
>>> VSXLEN=64 (sv39+ paging modes).
>>>
>>
>> Could you please clarify to me what in the spec it is?
> 
> It is the same table as you indicated below (but mine is Table 56, so it
> seems we don't have the same spec version, I'm using the version 20260120).
> 
> When I found unclear is that "RV32" or "RV64" alone is ambiguous,
> since host and guest can differ (e.g. RV64 host running an RV32 guest).
> Saying it depends on VSXLEN instead makes clear which one is meant.
> 
>>
>> This comments are just copy from the spec:
>>
>> Table 39. Special pseudoinstruction values for guest-page faults. The
>> RV32 values are used when VSXLEN=32, and the RV64 values when VSXLEN=64.
>> Value           Meaning
>> 0x00002000      32-bit read for VS-stage address translation (RV32)
>> 0x00002020      32-bit write for VS-stage address translation (RV32)
>>
>> Value           Meaning
>> 0x00003000      64-bit read for VS-stage address translation (RV64)
>> 0x00003020      64-bit write for VS-stage address translation (RV64)
>>
>> So the comments are just copy of "Meaning" column from the spec.
>>
>> And I think that Meaning column is fine here as we could have a case of
>> when hypervisor has XLEN=64 but guests could be on it RV32 and RV64 and
>> if a guest is RV32 (what means VSXLEN=32) then the comment above
>> defintion mean that we hav 32-bit read/write VS-stage address
>> translation for (RV32) guest and the similar is for RV64.
>>
>> Do I miss something? Is a comments make more sense now or I have to
>> still update them in some way?
>>
> I think it would be more clear to modify the comment by that:
> /* 32-bit read for VS-stage address translation (VSXLEN=32) */
> 
> But now with your explanation, your comment is more clear as it specify
> VS-stage address, so the guest. Feel free to adopt my version or not.
Agree your suggestion here will be more clear. I will apply it.

Thanks.

~ Oleksii


^ permalink raw reply	[flat|nested] 244+ messages in thread

* Re: [PATCH v2 08/39] xen/riscv: introduce device-agnostic MMIO emulation dispatch
  2026-08-27 15:20 ` [PATCH v2 08/39] xen/riscv: introduce device-agnostic MMIO emulation dispatch Oleksii Kurochko
  2026-09-01 15:36   ` Baptiste Le Duc
@ 2026-09-09 13:24   ` Jan Beulich
  2026-09-09 14:04     ` Oleksii Kurochko
  1 sibling, 1 reply; 244+ messages in thread
From: Jan Beulich @ 2026-09-09 13:24 UTC (permalink / raw)
  To: Oleksii Kurochko
  Cc: Romain Caritey, Baptiste Le Duc, Zheng Zhang, Alistair Francis,
	Connor Davis, Andrew Cooper, Anthony PERARD, Michal Orzel,
	Julien Grall, Roger Pau Monné, Stefano Stabellini, xen-devel

On 27.08.2026 17:20, Oleksii Kurochko wrote:
> --- /dev/null
> +++ b/xen/arch/riscv/include/asm/mmio.h
> @@ -0,0 +1,63 @@
> +/* SPDX-License-Identifier: GPL-2.0-or-later */
> +#ifndef RISCV_MMIO_H
> +#define RISCV_MMIO_H
> +
> +#include <xen/lib.h>
> +#include <xen/rwlock.h>
> +
> +struct domain;
> +struct vcpu;
> +
> +#define MAX_IO_HANDLER  16
> +
> +typedef struct {
> +    paddr_t gpa;
> +    unsigned int len;  /* access width in bytes (1, 2, 4, 8) */
> +    bool is_write;
> +    /* store: value to write; load: value read (set by handler) */

Nit: Comment style (twice). I'd also like to mention that enumerating
access widths is prone to go stale once the V or Q extensions are
supported.

> +    register_t data;
> +} mmio_info_t;
> +
> +enum io_state
> +{
> +    IO_ABORT,       /* The IO was handled and led to an abort. */
> +    IO_HANDLED,     /* The IO was successfully handled. */
> +    IO_UNHANDLED,   /* No handler found for the IO. */
> +};
> +
> +typedef enum io_state (mmio_read_t)(struct vcpu *v, mmio_info_t *info);
> +typedef enum io_state (mmio_write_t)(struct vcpu *v, const mmio_info_t *info);

I don't quite understand the (need for) parentheses around the typedef
names.

> +/*
> + * Check alignment and dispatch a decoded MMIO access to a registered
> + * handler. On success (0), info->data holds the read value for loads.
> + *
> + * There is no "retry" outcome to handle: find_mmio_handler() returns a
> + * copy of the matching handler taken under vmmio->lock and the ops
> + * structures are never freed, so the lookup result cannot go stale
> + * between finding the handler and invoking it.
> + */
> +int do_mmio(mmio_info_t *info, paddr_t fault_addr, unsigned int len)
> +{
> +    /* Fault address should be aligned to length of MMIO */
> +    if ( fault_addr & (len - 1) )
> +        return -EIO;

Better first check (or at least assert) that len is a power of 2?

> +int register_mmio_handler(struct domain *d,
> +                          const struct mmio_handler_ops *ops,
> +                          paddr_t addr, paddr_t size)
> +{
> +    struct vmmio *vmmio = &d->arch.vmmio;
> +    struct mmio_handler *handlers = vmmio->handlers;
> +    paddr_t end = addr + size;
> +    unsigned int i;
> +    int rc = 0;
> +    bool overlap;
> +
> +    if ( !ops || !ops->read || !ops->write || !size || end < addr )
> +        return -EINVAL;

"!size || end < addr" can be had shorter as "end <= addr".

Whether it's worth checking ops to be non-NULL I question, bit I wouldn't
insist on dropping the check.

> +    write_lock(&vmmio->lock);
> +
> +    if ( vmmio->num_entries >= ARRAY_SIZE(vmmio->handlers) )
> +    {
> +        rc = -ENOSPC;
> +        goto out;
> +    }
> +
> +    /*
> +     * The array is kept sorted by base address, so rather than appending and
> +     * re-sorting, find the slot the new region belongs to and shift the tail
> +     * up by one.
> +     */
> +    for ( i = vmmio->num_entries;
> +          i > 0 && handlers[i - 1].addr > addr;
> +          i-- )
> +        /* Nothing */;

    for ( i = vmmio->num_entries; i-- > 0 && handlers[i].addr > addr; )
        /* Nothing */;

?

> +    /*
> +     * Regions are required not to overlap; check both neighbours. Their
> +     * addr + size cannot overflow, as such regions are rejected above when
> +     * they get registered.
> +     */
> +    overlap = (i > 0 && handlers[i - 1].addr + handlers[i - 1].size > addr) ||
> +              (i < vmmio->num_entries && end > handlers[i].addr);
> +
> +    if ( overlap )
> +    {
> +        rc = -EEXIST;

I fear -EEXIST can be misleading; it generally means _this_ range is
already covered, not some sub-range thereof.

> +void domain_io_init(struct domain *d)
> +{
> +    rwlock_init(&d->arch.vmmio.lock);
> +    d->arch.vmmio.num_entries = 0;

The latter shouldn't be necessary, as struct domain-s start out zero
filled.

Jan


^ permalink raw reply	[flat|nested] 244+ messages in thread

* Re: [PATCH v2 08/39] xen/riscv: introduce device-agnostic MMIO emulation dispatch
  2026-09-09 13:24   ` Jan Beulich
@ 2026-09-09 14:04     ` Oleksii Kurochko
  2026-09-09 14:32       ` Jan Beulich
  0 siblings, 1 reply; 244+ messages in thread
From: Oleksii Kurochko @ 2026-09-09 14:04 UTC (permalink / raw)
  To: Jan Beulich
  Cc: Romain Caritey, Baptiste Le Duc, Zheng Zhang, Alistair Francis,
	Connor Davis, Andrew Cooper, Anthony PERARD, Michal Orzel,
	Julien Grall, Roger Pau Monné, Stefano Stabellini, xen-devel



On 9/9/26 3:24 PM, Jan Beulich wrote:
> On 27.08.2026 17:20, Oleksii Kurochko wrote:

> 
>> +/*
>> + * Check alignment and dispatch a decoded MMIO access to a registered
>> + * handler. On success (0), info->data holds the read value for loads.
>> + *
>> + * There is no "retry" outcome to handle: find_mmio_handler() returns a
>> + * copy of the matching handler taken under vmmio->lock and the ops
>> + * structures are never freed, so the lookup result cannot go stale
>> + * between finding the handler and invoking it.
>> + */
>> +int do_mmio(mmio_info_t *info, paddr_t fault_addr, unsigned int len)
>> +{
>> +    /* Fault address should be aligned to length of MMIO */
>> +    if ( fault_addr & (len - 1) )
>> +        return -EIO;
> 
> Better first check (or at least assert) that len is a power of 2?

It make sense. I will do then:

     if ( len & (len - 1) || fault_addr & (len - 1) )

> 
>> +int register_mmio_handler(struct domain *d,
>> +                          const struct mmio_handler_ops *ops,
>> +                          paddr_t addr, paddr_t size)
>> +{
>> +    struct vmmio *vmmio = &d->arch.vmmio;
>> +    struct mmio_handler *handlers = vmmio->handlers;
>> +    paddr_t end = addr + size;
>> +    unsigned int i;
>> +    int rc = 0;
>> +    bool overlap;
>> +
>> +    if ( !ops || !ops->read || !ops->write || !size || end < addr )
>> +        return -EINVAL;
> 
> "!size || end < addr" can be had shorter as "end <= addr".

I will apply this.

> 
> Whether it's worth checking ops to be non-NULL I question, bit I wouldn't
> insist on dropping the check.
> 

Probably it isn't really needed but just extra check that someone miss 
to provide implementation of ->read, ->write still could be useful. Also 
it is executed only at boot time so not big perfomance impact.


>> +    write_lock(&vmmio->lock);
>> +
>> +    if ( vmmio->num_entries >= ARRAY_SIZE(vmmio->handlers) )
>> +    {
>> +        rc = -ENOSPC;
>> +        goto out;
>> +    }
>> +
>> +    /*
>> +     * The array is kept sorted by base address, so rather than appending and
>> +     * re-sorting, find the slot the new region belongs to and shift the tail
>> +     * up by one.
>> +     */
>> +    for ( i = vmmio->num_entries;
>> +          i > 0 && handlers[i - 1].addr > addr;
>> +          i-- )
>> +        /* Nothing */;
> 
>      for ( i = vmmio->num_entries; i-- > 0 && handlers[i].addr > addr; )
>          /* Nothing */;
> 
> ?

It seems like it will break the code after it.

This breaks cases:
1. On a normal exit (handlers[i].addr <= addr), i is the index of the 
entry that was found, whereas the insertion slot ought to be i + 1. The 
code below, however, uses i as the insertion slot and handlers[i - 1] as 
the left neighbour - an off-by-one.

2.If every entry has a bigger addr than the new one, the loop exits when 
i == 0: 0 > 0 is false, yet i-- has already taken effect, so i == 
UINT_MAX. Then i > 0 is true, leading to a read of handlers[UINT_MAX - 
1] and to memmove() with a size of (num_entries - UINT_MAX).

Case 1 pretty easy to fix, just use proper indexing but case 2 will 
require extra check at least. Thereby I think we could keep here 
original for loop.
> 
>> +    /*
>> +     * Regions are required not to overlap; check both neighbours. Their
>> +     * addr + size cannot overflow, as such regions are rejected above when
>> +     * they get registered.
>> +     */
>> +    overlap = (i > 0 && handlers[i - 1].addr + handlers[i - 1].size > addr) ||
>> +              (i < vmmio->num_entries && end > handlers[i].addr);
>> +
>> +    if ( overlap )
>> +    {
>> +        rc = -EEXIST;
> 
> I fear -EEXIST can be misleading; it generally means _this_ range is
> already covered, not some sub-range thereof.

Then probably EADDRINUSE() would be better.

Thanks.

~ Oleksii


^ permalink raw reply	[flat|nested] 244+ messages in thread

* Re: [PATCH v2 09/39] xen/riscv: implement virtual APLIC MMIO emulation
  2026-08-27 15:20 ` [PATCH v2 09/39] xen/riscv: implement virtual APLIC MMIO emulation Oleksii Kurochko
  2026-09-02 11:51   ` Baptiste Le Duc
  2026-09-02 12:31   ` Baptiste Le Duc
@ 2026-09-09 14:26   ` Jan Beulich
  2026-09-10 10:37     ` Oleksii Kurochko
  2 siblings, 1 reply; 244+ messages in thread
From: Jan Beulich @ 2026-09-09 14:26 UTC (permalink / raw)
  To: Oleksii Kurochko
  Cc: Romain Caritey, Baptiste Le Duc, Zheng Zhang, Alistair Francis,
	Connor Davis, Andrew Cooper, Anthony PERARD, Michal Orzel,
	Julien Grall, Roger Pau Monné, Stefano Stabellini, xen-devel

On 27.08.2026 17:20, Oleksii Kurochko wrote:
> +uint32_t aplic_msi_target_gen(const struct vcpu *target_vcpu,
> +                              uint32_t base_val)
> +{
> +    unsigned int guest_id = vcpu_guest_file_id(target_vcpu);
> +    unsigned long hart_field = aplic_hart_field(target_vcpu->processor);

What guarantees target_vcpu's ->processor field to be meaningful at this
point? (Also again naming of the parameter: Generally it wants to be "v"
or "curr"; only very special cases may use other names.)

> +uint32_t aplic_hw_read_reg(unsigned int offset)
> +{
> +    unsigned long flags;
> +    uint32_t val;
> +
> +    ASSERT((offset < aplic.size) && IS_ALIGNED(offset, sizeof(uint32_t)));
> +
> +    spin_lock_irqsave(&aplic.lock, flags);
> +    val = readl((volatile void __iomem *)aplic.regs + offset);

Please can this have const alongside volatile?

> @@ -98,6 +108,27 @@
>  #define APLIC_SIZE(nr_cpus) \
>      (APLIC_MIN_SIZE + APLIC_SIZE_ALIGN(APLIC_IDC_SIZE * (nr_cpus)))
>  
> +/*
> + * Using setip is fine here, as all SET* and CLR* register groups consist of 32
> + * registers and therefore have identical sizes.
> + *
> + * Lowest 2 bits are always zero for SET* and CLR* registers.
> + */
> +#define APLIC_SETCLR_OFFSET_MASK \
> +    (sizeof_field(struct aplic_regs, setip) - sizeof(uint32_t))
> +
> +#define APLIC_xMSICFGADDR_PPN_SHIFT IMSIC_MMIO_PAGE_SHIFT
> +
> +#define APLIC_xMSICFGADDR_PPN_HHX_MASK(hhxw) \
> +    (BIT(hhxw, UL) - 1)
> +#define APLIC_xMSICFGADDR_PPN_HHX_SHIFT(hhxs) \
> +    ((hhxs) + APLIC_xMSICFGADDR_PPN_SHIFT)
> +
> +#define APLIC_xMSICFGADDR_PPN_LHX_MASK(lhxw) \
> +    (BIT(lhxw, UL) - 1)
> +#define APLIC_xMSICFGADDR_PPN_LHX_SHIFT(lhxs) \
> +    (lhxs)

May I ask to avoid unnecessary line splitting here as well?

> --- a/xen/arch/riscv/include/asm/vaplic.h
> +++ b/xen/arch/riscv/include/asm/vaplic.h
> @@ -21,11 +21,16 @@ struct domain;
>  
>  struct vaplic_regs {
>      uint32_t domaincfg;
> +
> +    uint32_t *target;
>  };

A pointer in this structure is odd, as this (supposedly) is a set of
guest register values. The field name also doesn't clarify its purpose.
All in all: Likely a comment is needed here.

> --- a/xen/arch/riscv/vaplic.c
> +++ b/xen/arch/riscv/vaplic.c
> @@ -17,6 +17,7 @@
>  #include <asm/aia.h>
>  #include <asm/imsic.h>
>  #include <asm/intc.h>
> +#include <asm/mmio.h>
>  #include <asm/vaplic.h>
>  
>  #include "aplic-priv.h"
> @@ -27,6 +28,279 @@ unsigned int __ro_after_init guest_aplic_num_sources;
>  
>  #define FDT_VAPLIC_INT_CELLS 2
>  
> +#define AUTH_IRQ_BIT(d, irqn) \
> +    (((irqn) < (d)->arch.vintc->nr_virqs) && \
> +     test_bit(irqn, (d)->arch.vintc->used_irqs))
> +
> +/*
> + * Convert a byte offset (within a SETIP/CLRIP/SETIE/CLRIE register group) to
> + * a 32-bit word index into the used_irqs bitmap. Each word covers 32
> + * interrupt sources. For SOURCECFG and TARGET groups the same division also
> + * yields the interrupt number directly, because those arrays store one 32-bit
> + * register per source.
> + */
> +#define regoffset_to_word_idx(reg_val) ((reg_val) / sizeof(uint32_t))
> +
> +static uint32_t vaplic_target_read(const struct domain *d, unsigned int irqn)
> +{
> +    const struct vaplic *vaplic = to_vaplic(d);
> +
> +    /* target[0] doesn't exist so irqn == 0 should be impossible */
> +    if ( !irqn || irqn >= vaplic->vintc.nr_virqs )
> +        return 0;
> +
> +    return read_atomic(&vaplic->regs.target[irqn]);
> +}
> +
> +static inline uint32_t generate_auth_mask(const struct domain *currd,
> +                                          unsigned int word_idx)
> +{
> +    unsigned int first_bit = word_idx * sizeof(uint32_t) * BITS_PER_BYTE;
> +
> +    if ( word_idx >= DIV_ROUND_UP(currd->arch.vintc->nr_virqs,
> +                                  sizeof(uint32_t) * BITS_PER_BYTE) )
> +    {
> +        gdprintk(XENLOG_DEBUG, "incorrect word_idx(%u) is passed\n", word_idx);

Along the lines of earlier remarks: What value does "is passed" add?

> +static bool vaplic_emulate_store(const struct vcpu *curr, paddr_t addr,
> +                                 uint32_t value)
> +{
> +    const struct domain *currd = curr->domain;
> +    unsigned int offset = addr & APLIC_CTRL_REGION_OFFSET_MASK;
> +
> +    ASSERT(curr == current);
> +
> +    switch ( offset )
> +    {
> +    case APLIC_SETIP_BASE ... APLIC_SETIP_LAST:
> +    case APLIC_CLRIP_BASE ... APLIC_CLRIP_LAST:
> +    case APLIC_SETIE_BASE ... APLIC_SETIE_LAST:
> +    case APLIC_CLRIE_BASE ... APLIC_CLRIE_LAST:
> +    {
> +        unsigned int word_idx =
> +            regoffset_to_word_idx(offset & APLIC_SETCLR_OFFSET_MASK);
> +
> +        value &= generate_auth_mask(currd, word_idx);
> +
> +        break;
> +    }
> +
> +    case APLIC_SOURCECFG_BASE ... APLIC_SOURCECFG_LAST:
> +        if ( value & APLIC_SOURCECFG_D )
> +        {
> +            dprintk(XENLOG_ERR, "APLIC_SOURCECFG_D isn't supported\n");

gdprintk()?

> +            goto fail;
> +        }
> +
> +        /*
> +         * As sourcecfg register starts from 1:
> +         *   0x0000 domaincfg
> +         *   0x0004 sourcecfg[1]
> +         *   0x0008 sourcecfg[2]
> +         *    ...
> +         *   0x0FFC sourcecfg[1023]
> +         * It is necessary to calculate an interrupt number by subtracting
> +         * APLIC_DOMAINCFG instead of APLIC_SOURCECFG_BASE.
> +         */
> +        if ( !AUTH_IRQ_BIT(currd,
> +                           regoffset_to_word_idx(offset - APLIC_DOMAINCFG)) )
> +            /* Interrupt not enabled, ignore it */
> +            return true;
> +
> +        if ( value > APLIC_SOURCECFG_SM_LEVEL_LOW )
> +        {
> +            gdprintk(XENLOG_ERR,
> +                     "value(%#x) is incorrect for sourcecfg register\n",
> +                     value);
> +
> +            return true;
> +        }
> +
> +        break;
> +
> +    case APLIC_TARGET_BASE ... APLIC_TARGET_LAST:
> +    {
> +        struct vaplic *vaplic = to_vaplic(currd);
> +        struct vcpu *target_vcpu;

This, btw, is a case where I think not using "v" is warranted.

> +        unsigned int guest_hart_idx = MASK_EXTR(value, APLIC_TARGET_HART_IDX);
> +        /*
> +         * Look at vaplic_emulate_load() for explanation why APLIC_GENMSI is
> +         * subtracted.
> +         */
> +        unsigned int srcn = regoffset_to_word_idx(offset - APLIC_GENMSI);
> +
> +        if ( !AUTH_IRQ_BIT(currd, srcn) )
> +            /* Interrupt not enabled, ignore it */
> +            return true;
> +
> +        target_vcpu = domain_vcpu(currd, guest_hart_idx);
> +
> +        if ( !target_vcpu )
> +        {
> +            dprintk(XENLOG_ERR, "Invalid vCPU id in target register\n");
> +
> +            /* Ignore such writings */
> +            return true;
> +        }
> +
> +        if ( vaplic->regs.domaincfg & APLIC_DOMAINCFG_DM )
> +        {
> +            /*
> +             * A non-zero guest index asks for delivery to an interrupt file of
> +             * nested guest. The vIMSIC node has no riscv,guest-index-bits
> +             * property, so a guest is told its harts have no guest interrupt
> +             * files and the field is read-only zero for them. The write isn't
> +             * rejected (that would throw away a valid hart index and EIID);
> +             * instead the field is dropped, which is also what
> +             * aplic_msi_target_gen() does with it when programming the h/w.
> +             */
> +            if ( MASK_EXTR(value, APLIC_TARGET_GUEST_IDX) )
> +            {
> +                printk_once(XENLOG_WARNING
> +                            "%pd: vAPLIC target guest index != 0 is unsupported\n",
> +                            currd);
> +
> +                /* Ignore such writes ... */
> +                return true;
> +            }
> +
> +            write_atomic(&vaplic->regs.target[srcn], value);
> +
> +            value = aplic_msi_target_gen(target_vcpu, value);
> +        }
> +        else
> +        {
> +            /*
> +             * IPRIO is WARL and zero isn't a legal value for it, so normalize
> +             * it once: the guest then reads back exactly what it gets.
> +             */

What is "reads back exactly what it gets" supposed to express? The use of
"once" there also isn't quite clear to me.

> +            unsigned int iprio = MASK_EXTR(value, APLIC_TARGET_IPRIO) ?:
> +                                 APLIC_TARGET_IPRIO_DEFAULT;
> +            unsigned long h = cpuid_to_hartid(guest_hart_idx);
> +
> +            value = MASK_INSR(guest_hart_idx, APLIC_TARGET_HART_IDX) |
> +                    MASK_INSR(iprio, APLIC_TARGET_IPRIO);
> +
> +            write_atomic(&vaplic->regs.target[srcn], value);
> +
> +            value = MASK_INSR(h, APLIC_TARGET_HART_IDX) |
> +                    MASK_INSR(iprio, APLIC_TARGET_IPRIO);
> +        }
> +
> +        break;
> +    }
> +
> +    case APLIC_SETIPNUM:
> +    case APLIC_SETIPNUM_LE:
> +    case APLIC_CLRIPNUM:
> +    case APLIC_SETIENUM:
> +    case APLIC_CLRIENUM:
> +        if ( !value || !AUTH_IRQ_BIT(currd, value) )
> +            return true;
> +
> +        break;
> +
> +    case APLIC_DOMAINCFG:
> +    {
> +        struct vaplic *vaplic = to_vaplic(currd);
> +
> +        vaplic->regs.domaincfg = APLIC_DOMAINCFG_RO |
> +                                 (value & APLIC_DOMAINCFG_WMASK);
> +
> +        return true;
> +    }

May I suggest that you arrange case blocks (primarily) by offset? This would
then mean for DOMAINCFG handling to move to the top, helping at least a little
with SOURCECFG_{BASE,LAST} handling (slightly oddly) using APLIC_DOMAINCFG.

> @@ -122,7 +447,29 @@ int domain_vaplic_init(struct domain *d)
>       */
>      d->arch.vintc->nr_virqs = guest_aplic_num_sources + 1;
>  
> -    return 0;
> +    /* Slot 0 is unused: APLIC source numbering starts at 1 (see used_irqs). */
> +    vaplic->regs.target = xvzalloc_array(uint32_t, d->arch.vintc->nr_virqs);
> +    if ( !vaplic->regs.target )
> +    {
> +        d->arch.vintc = NULL;
> +        xvfree(vaplic);
> +
> +        return -ENOMEM;
> +    }
> +
> +    vaplic->regs_start = GUEST_APLIC_S_BASE;
> +    vaplic->regs_size = APLIC_SIZE(d->max_vcpus);
> +
> +    rc = register_mmio_handler(d, &vaplic_mmio_ops,
> +                               vaplic->regs_start, vaplic->regs_size);
> +    if ( rc )
> +    {
> +        d->arch.vintc = NULL;
> +        xvfree(vaplic->regs.target);
> +        xvfree(vaplic);
> +    }

Could you perhaps arrange for it to be possible to simply call
domain_vaplic_deinit() here (and maybe also on at least some of the earlier
error paths)? The code above loks very similar to ...

> @@ -134,5 +481,6 @@ void domain_vaplic_deinit(struct domain *d)
>  
>      vaplic = to_vaplic(d);
>      d->arch.vintc = NULL;
> +    xvfree(vaplic->regs.target);
>      xvfree(vaplic);
>  }

... what's here.

Jan


^ permalink raw reply	[flat|nested] 244+ messages in thread

* Re: [PATCH v2 08/39] xen/riscv: introduce device-agnostic MMIO emulation dispatch
  2026-09-09 14:04     ` Oleksii Kurochko
@ 2026-09-09 14:32       ` Jan Beulich
  0 siblings, 0 replies; 244+ messages in thread
From: Jan Beulich @ 2026-09-09 14:32 UTC (permalink / raw)
  To: Oleksii Kurochko
  Cc: Romain Caritey, Baptiste Le Duc, Zheng Zhang, Alistair Francis,
	Connor Davis, Andrew Cooper, Anthony PERARD, Michal Orzel,
	Julien Grall, Roger Pau Monné, Stefano Stabellini, xen-devel

On 09.09.2026 16:04, Oleksii Kurochko wrote:
> On 9/9/26 3:24 PM, Jan Beulich wrote:
>> On 27.08.2026 17:20, Oleksii Kurochko wrote:
>>> +/*
>>> + * Check alignment and dispatch a decoded MMIO access to a registered
>>> + * handler. On success (0), info->data holds the read value for loads.
>>> + *
>>> + * There is no "retry" outcome to handle: find_mmio_handler() returns a
>>> + * copy of the matching handler taken under vmmio->lock and the ops
>>> + * structures are never freed, so the lookup result cannot go stale
>>> + * between finding the handler and invoking it.
>>> + */
>>> +int do_mmio(mmio_info_t *info, paddr_t fault_addr, unsigned int len)
>>> +{
>>> +    /* Fault address should be aligned to length of MMIO */
>>> +    if ( fault_addr & (len - 1) )
>>> +        return -EIO;
>>
>> Better first check (or at least assert) that len is a power of 2?
> 
> It make sense. I will do then:
> 
>      if ( len & (len - 1) || fault_addr & (len - 1) )

With more parentheses added, I suppose.

>>> +int register_mmio_handler(struct domain *d,
>>> +                          const struct mmio_handler_ops *ops,
>>> +                          paddr_t addr, paddr_t size)
>>> +{
>>> +    struct vmmio *vmmio = &d->arch.vmmio;
>>> +    struct mmio_handler *handlers = vmmio->handlers;
>>> +    paddr_t end = addr + size;
>>> +    unsigned int i;
>>> +    int rc = 0;
>>> +    bool overlap;
>>> +
>>> +    if ( !ops || !ops->read || !ops->write || !size || end < addr )
>>> +        return -EINVAL;
>>
>> "!size || end < addr" can be had shorter as "end <= addr".
> 
> I will apply this.
> 
>>
>> Whether it's worth checking ops to be non-NULL I question, bit I wouldn't
>> insist on dropping the check.
> 
> Probably it isn't really needed but just extra check that someone miss 
> to provide implementation of ->read, ->write still could be useful. Also 
> it is executed only at boot time so not big perfomance impact.

I questioned merely the checking of ops itself, not that of the ->read
and ->write hooks.

>>> +    write_lock(&vmmio->lock);
>>> +
>>> +    if ( vmmio->num_entries >= ARRAY_SIZE(vmmio->handlers) )
>>> +    {
>>> +        rc = -ENOSPC;
>>> +        goto out;
>>> +    }
>>> +
>>> +    /*
>>> +     * The array is kept sorted by base address, so rather than appending and
>>> +     * re-sorting, find the slot the new region belongs to and shift the tail
>>> +     * up by one.
>>> +     */
>>> +    for ( i = vmmio->num_entries;
>>> +          i > 0 && handlers[i - 1].addr > addr;
>>> +          i-- )
>>> +        /* Nothing */;
>>
>>      for ( i = vmmio->num_entries; i-- > 0 && handlers[i].addr > addr; )
>>          /* Nothing */;
>>
>> ?
> 
> It seems like it will break the code after it.
> 
> This breaks cases:
> 1. On a normal exit (handlers[i].addr <= addr), i is the index of the 
> entry that was found, whereas the insertion slot ought to be i + 1. The 
> code below, however, uses i as the insertion slot and handlers[i - 1] as 
> the left neighbour - an off-by-one.
> 
> 2.If every entry has a bigger addr than the new one, the loop exits when 
> i == 0: 0 > 0 is false, yet i-- has already taken effect, so i == 
> UINT_MAX. Then i > 0 is true, leading to a read of handlers[UINT_MAX - 
> 1] and to memmove() with a size of (num_entries - UINT_MAX).
> 
> Case 1 pretty easy to fix, just use proper indexing but case 2 will 
> require extra check at least. Thereby I think we could keep here 
> original for loop.

Oh, I'm sorry for the bad suggestion then.

Jan


^ permalink raw reply	[flat|nested] 244+ messages in thread

* Re: [PATCH v2 10/39] xen/riscv: build the target hart index via aplic_hart_field()
  2026-09-04  8:26   ` Baptiste Le Duc
  2026-09-04 14:28     ` Oleksii Kurochko
@ 2026-09-09 14:51     ` Jan Beulich
  1 sibling, 0 replies; 244+ messages in thread
From: Jan Beulich @ 2026-09-09 14:51 UTC (permalink / raw)
  To: Baptiste Le Duc, Oleksii Kurochko
  Cc: xen-devel, Romain Caritey, Zheng Zhang, Alistair Francis,
	Connor Davis, Andrew Cooper, Anthony PERARD, Michal Orzel,
	Julien Grall, Roger Pau Monné, Stefano Stabellini

On 04.09.2026 10:26, Baptiste Le Duc wrote:
> On Thu, 27 Aug 2026 17:20:54 +0200, Oleksii Kurochko <oleksii.kurochko@gmail.com> wrote:
>> aplic_set_irq_affinity() open-coded the packing of the group and hart
>> indices into the target register, and got two things wrong along the
>> way:
>>
>>  - imsic_config.msi[] is indexed by logical CPU id, but the index was
>>    run through cpuid_to_hartid() first. On any platform where the two
>>    spaces differ this picks another CPU's interrupt file, or reads past
>>    the array;
>>
>> [...]
> 
> Reviewed-by: Baptiste Le Duc <baptiste.le-duc@vates.tech>

I'm puzzled by this being unconditional, considering the naming comment
you gave on patch 09. With the (aiui now agreed upon) renaming:
Acked-by: Jan Beulich <jbeulich@suse.com>

Jan


^ permalink raw reply	[flat|nested] 244+ messages in thread

* Re: [PATCH v2 10/39] xen/riscv: build the target hart index via aplic_hart_field()
  2026-08-27 15:20 ` [PATCH v2 10/39] xen/riscv: build the target hart index via aplic_hart_field() Oleksii Kurochko
  2026-09-04  8:26   ` Baptiste Le Duc
@ 2026-09-09 14:52   ` Jan Beulich
  2026-09-10 10:59     ` Oleksii Kurochko
  1 sibling, 1 reply; 244+ messages in thread
From: Jan Beulich @ 2026-09-09 14:52 UTC (permalink / raw)
  To: Oleksii Kurochko
  Cc: Romain Caritey, Baptiste Le Duc, Zheng Zhang, Alistair Francis,
	Connor Davis, Andrew Cooper, Anthony PERARD, Michal Orzel,
	Julien Grall, Roger Pau Monné, Stefano Stabellini, xen-devel

On 27.08.2026 17:20, Oleksii Kurochko wrote:
> @@ -340,27 +338,11 @@ static void cf_check aplic_set_irq_affinity(struct irq_desc *desc, const cpumask
>  
>      ASSERT(spin_is_locked(&desc->lock));
>  
> -    cpu = cpuid_to_hartid(aplic_get_cpu_from_mask(mask));
> -    hhxw = imsic->group_index_bits;
> -    lhxw = imsic->hart_index_bits;
> -    /*
> -     * Although this variable is used only once in the calculation of
> -     * group_index, and it might seem that hhxs could be defined as:
> -     *   hhxs = imsic->group_index_shift - IMSIC_MMIO_PAGE_SHIFT;
> -     * and then the addition of IMSIC_MMIO_PAGE_SHIFT could be omitted
> -     * when calculating the group index.
> -     * It was done intentionally this way to follow the formula from
> -     * the AIA specification for calculating the MSI address.
> -     */
> -    hhxs = imsic->group_index_shift - IMSIC_MMIO_PAGE_SHIFT * 2;
> -    base_ppn = imsic->msi[cpu].base_addr >> IMSIC_MMIO_PAGE_SHIFT;
> -
> -    /* Update hart and EEID in the target register */
> -    group_index = (base_ppn >> (hhxs + IMSIC_MMIO_PAGE_SHIFT)) &
> -                  (BIT(hhxw, UL) - 1);
> -    value = desc->irq;

Hmm, only after sending the ack I noticed that there's no masking here, ...

> -    value |= cpu << APLIC_TARGET_HART_IDX_SHIFT;
> -    value |= group_index << (lhxw + APLIC_TARGET_HART_IDX_SHIFT);
> +    cpu = aplic_get_cpu_from_mask(mask);
> +
> +    /* Update hart index and EIID in the target register */
> +    value = MASK_INSR(aplic_hart_field(cpu), APLIC_TARGET_HART_IDX) |
> +            (desc->irq & APLIC_TARGET_EIID);

... but there is masking here. Chopping off bits doesn't look as if it can
lead to anything good. What's the deal here?

Jan


^ permalink raw reply	[flat|nested] 244+ messages in thread

* Re: [PATCH v2 11/39] xen/riscv: add helper to check APLIC MSI mode
  2026-09-04  8:26   ` Baptiste Le Duc
@ 2026-09-09 14:53     ` Jan Beulich
  0 siblings, 0 replies; 244+ messages in thread
From: Jan Beulich @ 2026-09-09 14:53 UTC (permalink / raw)
  To: Oleksii Kurochko
  Cc: xen-devel, Romain Caritey, Zheng Zhang, Alistair Francis,
	Connor Davis, Andrew Cooper, Anthony PERARD, Michal Orzel,
	Julien Grall, Roger Pau Monné, Stefano Stabellini,
	Baptiste Le Duc

On 04.09.2026 10:26, Baptiste Le Duc wrote:
> On Thu, 27 Aug 2026 17:20:55 +0200, Oleksii Kurochko <oleksii.kurochko@gmail.com> wrote:
>> Use convient helper instead of open-coding the things.
> 
> Reviewed-by: Baptiste Le Duc <baptiste.le-duc@vates.tech>

Acked-by: Jan Beulich <jbeulich@suse.com>



^ permalink raw reply	[flat|nested] 244+ messages in thread

* Re: [PATCH v2 18/39] xen/riscv: add guest page fault handling stub
  2026-09-08 14:10   ` Jan Beulich
@ 2026-09-09 15:09     ` Oleksii Kurochko
  2026-09-10  6:38       ` Jan Beulich
  0 siblings, 1 reply; 244+ messages in thread
From: Oleksii Kurochko @ 2026-09-09 15:09 UTC (permalink / raw)
  To: Jan Beulich
  Cc: Romain Caritey, Baptiste Le Duc, Zheng Zhang, Alistair Francis,
	Connor Davis, Andrew Cooper, Anthony PERARD, Michal Orzel,
	Julien Grall, Roger Pau Monné, Stefano Stabellini, xen-devel



On 9/8/26 4:10 PM, Jan Beulich wrote:
> On 27.08.2026 17:21, Oleksii Kurochko wrote:
>> --- /dev/null
>> +++ b/xen/arch/riscv/emulate.c
>> @@ -0,0 +1,179 @@
>> +/* SPDX-License-Identifier: GPL-2.0-or-later */
>> +
>> +/*
>> + * RISC-V instruction emulation for trapped guest accesses
>> + */
>> +
>> +#include <xen/bug.h>
>> +#include <xen/errno.h>
>> +#include <xen/sched.h>
>> +#include <xen/types.h>
>> +
>> +#include <asm/csr.h>
>> +#include <asm/current.h>
>> +#include <asm/emulate.h>
>> +#include <asm/riscv_encoding.h>
>> +#include <asm/traps.h>
>> +
>> +/*
>> + * The hardware-reported details of a guest page fault, gathered once by
>> + * handle_guest_page_fault() and passed down to the emulation of the faulted
>> + * access.
>> + */
>> +struct guest_fault {
>> +    /* The guest register state as saved on entry to do_trap(). */
>> +    struct cpu_user_regs *regs;
> 
> If the comment was true, this could be pointer-to-const.

I think it can't be pointer-to-const as emulate_load/store functions 
wants to change PC register after MMIO access emulation is finished to 
not trap again.

Regarding the comment itself I agree that it isn't fully true right now 
because there is no trap from guest and so no guest registers 
save/restore but it will for sure be and we can't not to save/restore 
guest registers when do_trap() happens.

I will re-word it to:

/* The guest register state */

> 
>> +    /* scause: a fetch, a load or a store/AMO guest page fault. */
>> +    unsigned long cause;
>> +    /*
>> +     * htinst: the trapped instruction in its transformed form, or one of the
>> +     * special values (zero, or a pseudoinstruction).
>> +     */
>> +    unsigned long htinst;
>> +    /* htval: as written by hardware; see resolve_faulting_gpa(). */
>> +    unsigned long htval;
>> +    /* stval: the guest virtual address of the faulting access. */
>> +    unsigned long stval;
>> +    /* The faulting guest physical address, filled by resolve_faulting_gpa(). */
>> +    paddr_t gpa;
>> +};
>> +
>> +/*
>> + * Is @htinst one of the pseudoinstructions reported for a guest page fault
>> + * taken on an implicit memory access done for VS-stage address translation?
>> + *
>> + * All four values are recognized regardless of the hypervisor's XLEN: the
>> + * width they encode is that of a VS-stage PTE, i.e. it follows the guest's
>> + * paging mode (4 bytes for Sv32, 8 otherwise). On RV32 the 64-bit forms
>> + * simply never occur.
>> + */
>> +static bool htinst_is_pseudo(unsigned long htinst)
>> +{
>> +    switch ( htinst )
>> +    {
>> +    case INSN_PSEUDO_VS_LOAD32:
>> +    case INSN_PSEUDO_VS_STORE32:
>> +    case INSN_PSEUDO_VS_LOAD64:
>> +    case INSN_PSEUDO_VS_STORE64:
>> +        return true;
>> +
>> +    default:
>> +        return false;
>> +    }
>> +}
> 
> This feels fragile. New pseudo-insns can appear at any time. If the value as
> a whole is non-zero, aiui the low two bits being zero indicate a pseudo-insn.
> In which case enumerating pseudo-insns we are currently aware of isn't
> necessary.

I will write it simpler	 then:

/*
  * Is @htinst one of the special pseudoinstruction values, reported for 
a guest
  * page fault taken on an implicit memory access done for VS-stage address
  * translation?
  *
  * It is enough to check only bits[1:0] as according to the spec:
  *
  * The value is one of the special pseudoinstructions defined later, all of
  * which have bits 1:0 equal to 00.
  */
static bool htinst_is_pseudo(unsigned long htinst)
{
     return htinst && ((htinst & 3) == 0);
}

> 
>> +static void inject_access_fault(const struct guest_fault *gf)
>> +{
>> +    struct trap_info utrap = {};
>> +
>> +    switch ( gf->cause )
>> +    {
>> +    case CAUSE_FETCH_GUEST_PAGE_FAULT:
>> +        utrap.scause = CAUSE_FETCH_ACCESS;
>> +        break;
>> +
>> +    case CAUSE_LOAD_GUEST_PAGE_FAULT:
>> +        utrap.scause = CAUSE_LOAD_ACCESS;
>> +        break;
>> +
>> +    case CAUSE_STORE_GUEST_PAGE_FAULT:
>> +        utrap.scause = CAUSE_STORE_ACCESS;
>> +        break;
>> +
>> +    default:
>> +        domain_crash(current->domain, "Impossible cause (%#lx) in %s?\n",
>> +                     gf->cause, __func__);
>> +        return;
>> +    }
>> +
>> +    utrap.sepc = gf->regs->sepc;
>> +    utrap.stval = gf->stval;
> 
> Would there be anything wrong with putting these in utrap's initializer?

It could be initializers. I will use utrap's initializer.

> 
>> +    trap_redirect(&utrap);
>> +}
>> +
>> +void handle_guest_page_fault(struct cpu_user_regs *regs, unsigned long cause)
>> +{
>> +    struct guest_fault gf = {
>> +        .regs = regs,
>> +        .cause = cause,
>> +        .htinst = csr_read(CSR_HTINST),
>> +        .htval = csr_read(CSR_HTVAL),
>> +        .stval = csr_read(CSR_STVAL),
>> +        .gpa = INVALID_PADDR,
>> +    };
> 
> At some point RISC-V code will (very likely) also be scanned for Misra violations.
> The csr_read()s here violate rule 13.1 ("Initializer lists shall not contain
> persistent side effects"), and I think it would be better if such was avoided from
> the start.

I will do the following then:

struct guest_fault gf = {
     .regs = regs,
     .cause = cause,
     .gpa = INVALID_PADDR,
};
int rc;

gf.htinst = csr_read(CSR_HTINST);
gf.htval = csr_read(CSR_HTVAL);
gf.stval = csr_read(CSR_STVAL);

> 
>> +    int rc;
>> +
>> +    /*
>> +     * A guest-page fault may arise due to an implicit memory access during
>> +     * first-stage (VS-stage) address translation, in which case a guest
>> +     * physical address written to htval is that of the implicit memory
>> +     * access that faulted - for example, the address of a VS-level page
>> +     * table entry that could not be read. (The guest physical address
>> +     * corresponding to the original virtual address is unknown when
>> +     * VS-stage translation fails to complete)
>> +     *
>> +     * In such cases htinst reports one of the pseudoinstructions recognized
>> +     * by htinst_is_pseudo(), and the fault requires separate handling (since
>> +     * G-stage translation failed on an unpopulated/unmapped guest physical
>> +     * address during a hardware page-table walk). To match bare hardware
>> +     * behavior, we must inject an access fault of the ORIGINAL access type
>> +     * (Instruction, Load, or Store/AMO) that initiated the address
>> +     * translation.
>> +     */
>> +    if ( htinst_is_pseudo(gf.htinst) )
>> +    {
>> +        inject_access_fault(&gf);
>> +
>> +        return;
>> +    }
> 
> I.e. you imply that guests won't put their page tables in MMIO? That's
> fragile imo; I have seen OSes to use video frame buffers for all kinds
> of (transient) purposes, for example.

I think it is okay for now and if it will a real use case then an update 
of this code will be needed.

> 
>> +    resolve_faulting_gpa(&gf);
> 
> Since the function is only a stub right now - how is one to tell whether
> this indeed can never fail?

It can't be tell. But what is wrong if it could fail? (Actually with 
current implementation introduced in later patches you can find it can 
fail if a necessary extension or software page walk isn't introduced).

If we can't resolve faulting GPA address then we can't continue to work 
and so at least domain should be crashed.

> 
>> +    switch ( cause )
>> +    {
>> +    case CAUSE_LOAD_GUEST_PAGE_FAULT:
>> +        rc = emulate_load(&gf);
>> +        break;
>> +
>> +    case CAUSE_STORE_GUEST_PAGE_FAULT:
>> +        rc = emulate_store(&gf);
>> +        break;
>> +
>> +    case CAUSE_FETCH_GUEST_PAGE_FAULT:
>> +        /*
>> +         * Guest is trying to reach unmapped/unpopulated or G-stage PTE doesn't
>> +         * allow execution (X=0). Generate fetch fault in this case.
>> +         */
> 
> Is there perhaps a comma missing before "or", to help parsing the sentence?

I will add one.

> 
>> +        inject_access_fault(&gf);
>> +        rc = 0;
>> +        break;
> 
> Simply "return" instead of the latter two statements?

It makes sense. I will do just return.

Thanks.

~ Oleksii


^ permalink raw reply	[flat|nested] 244+ messages in thread

* Re: [PATCH v2 18/39] xen/riscv: add guest page fault handling stub
  2026-09-09 15:09     ` Oleksii Kurochko
@ 2026-09-10  6:38       ` Jan Beulich
  2026-09-11 11:47         ` Oleksii Kurochko
  0 siblings, 1 reply; 244+ messages in thread
From: Jan Beulich @ 2026-09-10  6:38 UTC (permalink / raw)
  To: Oleksii Kurochko
  Cc: Romain Caritey, Baptiste Le Duc, Zheng Zhang, Alistair Francis,
	Connor Davis, Andrew Cooper, Anthony PERARD, Michal Orzel,
	Julien Grall, Roger Pau Monné, Stefano Stabellini, xen-devel

On 09.09.2026 17:09, Oleksii Kurochko wrote:
> On 9/8/26 4:10 PM, Jan Beulich wrote:
>> On 27.08.2026 17:21, Oleksii Kurochko wrote:
>>> --- /dev/null
>>> +++ b/xen/arch/riscv/emulate.c
>>> @@ -0,0 +1,179 @@
>>> +/* SPDX-License-Identifier: GPL-2.0-or-later */
>>> +
>>> +/*
>>> + * RISC-V instruction emulation for trapped guest accesses
>>> + */
>>> +
>>> +#include <xen/bug.h>
>>> +#include <xen/errno.h>
>>> +#include <xen/sched.h>
>>> +#include <xen/types.h>
>>> +
>>> +#include <asm/csr.h>
>>> +#include <asm/current.h>
>>> +#include <asm/emulate.h>
>>> +#include <asm/riscv_encoding.h>
>>> +#include <asm/traps.h>
>>> +
>>> +/*
>>> + * The hardware-reported details of a guest page fault, gathered once by
>>> + * handle_guest_page_fault() and passed down to the emulation of the faulted
>>> + * access.
>>> + */
>>> +struct guest_fault {
>>> +    /* The guest register state as saved on entry to do_trap(). */
>>> +    struct cpu_user_regs *regs;
>>
>> If the comment was true, this could be pointer-to-const.
> 
> I think it can't be pointer-to-const as emulate_load/store functions 
> wants to change PC register after MMIO access emulation is finished to 
> not trap again.

Of course, hence how I started the sentence.

>>> +    /* scause: a fetch, a load or a store/AMO guest page fault. */
>>> +    unsigned long cause;
>>> +    /*
>>> +     * htinst: the trapped instruction in its transformed form, or one of the
>>> +     * special values (zero, or a pseudoinstruction).
>>> +     */
>>> +    unsigned long htinst;
>>> +    /* htval: as written by hardware; see resolve_faulting_gpa(). */
>>> +    unsigned long htval;
>>> +    /* stval: the guest virtual address of the faulting access. */
>>> +    unsigned long stval;
>>> +    /* The faulting guest physical address, filled by resolve_faulting_gpa(). */
>>> +    paddr_t gpa;
>>> +};
>>> +
>>> +/*
>>> + * Is @htinst one of the pseudoinstructions reported for a guest page fault
>>> + * taken on an implicit memory access done for VS-stage address translation?
>>> + *
>>> + * All four values are recognized regardless of the hypervisor's XLEN: the
>>> + * width they encode is that of a VS-stage PTE, i.e. it follows the guest's
>>> + * paging mode (4 bytes for Sv32, 8 otherwise). On RV32 the 64-bit forms
>>> + * simply never occur.
>>> + */
>>> +static bool htinst_is_pseudo(unsigned long htinst)
>>> +{
>>> +    switch ( htinst )
>>> +    {
>>> +    case INSN_PSEUDO_VS_LOAD32:
>>> +    case INSN_PSEUDO_VS_STORE32:
>>> +    case INSN_PSEUDO_VS_LOAD64:
>>> +    case INSN_PSEUDO_VS_STORE64:
>>> +        return true;
>>> +
>>> +    default:
>>> +        return false;
>>> +    }
>>> +}
>>
>> This feels fragile. New pseudo-insns can appear at any time. If the value as
>> a whole is non-zero, aiui the low two bits being zero indicate a pseudo-insn.
>> In which case enumerating pseudo-insns we are currently aware of isn't
>> necessary.
> 
> I will write it simpler	 then:
> 
> /*
>   * Is @htinst one of the special pseudoinstruction values, reported for 
> a guest
>   * page fault taken on an implicit memory access done for VS-stage address
>   * translation?
>   *
>   * It is enough to check only bits[1:0] as according to the spec:
>   *
>   * The value is one of the special pseudoinstructions defined later, all of
>   * which have bits 1:0 equal to 00.
>   */
> static bool htinst_is_pseudo(unsigned long htinst)
> {
>      return htinst && ((htinst & 3) == 0);
> }

And preferably

     return htinst && !(htinst & 3);

to be self-consistent.

>>> +    /*
>>> +     * A guest-page fault may arise due to an implicit memory access during
>>> +     * first-stage (VS-stage) address translation, in which case a guest
>>> +     * physical address written to htval is that of the implicit memory
>>> +     * access that faulted - for example, the address of a VS-level page
>>> +     * table entry that could not be read. (The guest physical address
>>> +     * corresponding to the original virtual address is unknown when
>>> +     * VS-stage translation fails to complete)
>>> +     *
>>> +     * In such cases htinst reports one of the pseudoinstructions recognized
>>> +     * by htinst_is_pseudo(), and the fault requires separate handling (since
>>> +     * G-stage translation failed on an unpopulated/unmapped guest physical
>>> +     * address during a hardware page-table walk). To match bare hardware
>>> +     * behavior, we must inject an access fault of the ORIGINAL access type
>>> +     * (Instruction, Load, or Store/AMO) that initiated the address
>>> +     * translation.
>>> +     */
>>> +    if ( htinst_is_pseudo(gf.htinst) )
>>> +    {
>>> +        inject_access_fault(&gf);
>>> +
>>> +        return;
>>> +    }
>>
>> I.e. you imply that guests won't put their page tables in MMIO? That's
>> fragile imo; I have seen OSes to use video frame buffers for all kinds
>> of (transient) purposes, for example.
> 
> I think it is okay for now and if it will a real use case then an update 
> of this code will be needed.

May I then ask that you leave a remark (maybe even fixme) to this effect?

>>> +    resolve_faulting_gpa(&gf);
>>
>> Since the function is only a stub right now - how is one to tell whether
>> this indeed can never fail?
> 
> It can't be tell. But what is wrong if it could fail? (Actually with 
> current implementation introduced in later patches you can find it can 
> fail if a necessary extension or software page walk isn't introduced).

Well, quite obviously if it can fail, its return value would need checking
here.

Jan


^ permalink raw reply	[flat|nested] 244+ messages in thread

* Re: [PATCH v2 09/39] xen/riscv: implement virtual APLIC MMIO emulation
  2026-09-09 14:26   ` Jan Beulich
@ 2026-09-10 10:37     ` Oleksii Kurochko
  2026-09-10 11:14       ` Jan Beulich
  0 siblings, 1 reply; 244+ messages in thread
From: Oleksii Kurochko @ 2026-09-10 10:37 UTC (permalink / raw)
  To: Jan Beulich
  Cc: Romain Caritey, Baptiste Le Duc, Zheng Zhang, Alistair Francis,
	Connor Davis, Andrew Cooper, Anthony PERARD, Michal Orzel,
	Julien Grall, Roger Pau Monné, Stefano Stabellini, xen-devel



On 9/9/26 4:26 PM, Jan Beulich wrote:
> On 27.08.2026 17:20, Oleksii Kurochko wrote:
>> +uint32_t aplic_msi_target_gen(const struct vcpu *target_vcpu,
>> +                              uint32_t base_val)
>> +{
>> +    unsigned int guest_id = vcpu_guest_file_id(target_vcpu);
>> +    unsigned long hart_field = aplic_hart_field(target_vcpu->processor);
> 
> What guarantees target_vcpu's ->processor field to be meaningful at this
> point? 

Good question. Considering the places where it is called, I would expect 
target_vcpu->processor to have something meaningful, since it is called 
from a place that can only be reached when the guest is running.

Even without that, I don't think there is a big issue here, as 
->processor is initialized to 0 at allocation time. This means that the 
target register will be configured in such a way that CPU 0 will handle 
such IRQs.

>(Also again naming of the parameter: Generally it wants to be "v"
> or "curr"; only very special cases may use other names.)

I will use just `v` then here.

> 
>> +uint32_t aplic_hw_read_reg(unsigned int offset)
>> +{
>> +    unsigned long flags;
>> +    uint32_t val;
>> +
>> +    ASSERT((offset < aplic.size) && IS_ALIGNED(offset, sizeof(uint32_t)));
>> +
>> +    spin_lock_irqsave(&aplic.lock, flags);
>> +    val = readl((volatile void __iomem *)aplic.regs + offset);
> 
> Please can this have const alongside volatile?

Sure, I will add.

> 
>> @@ -98,6 +108,27 @@
>>   #define APLIC_SIZE(nr_cpus) \
>>       (APLIC_MIN_SIZE + APLIC_SIZE_ALIGN(APLIC_IDC_SIZE * (nr_cpus)))
>>   
>> +/*
>> + * Using setip is fine here, as all SET* and CLR* register groups consist of 32
>> + * registers and therefore have identical sizes.
>> + *
>> + * Lowest 2 bits are always zero for SET* and CLR* registers.
>> + */
>> +#define APLIC_SETCLR_OFFSET_MASK \
>> +    (sizeof_field(struct aplic_regs, setip) - sizeof(uint32_t))
>> +
>> +#define APLIC_xMSICFGADDR_PPN_SHIFT IMSIC_MMIO_PAGE_SHIFT
>> +
>> +#define APLIC_xMSICFGADDR_PPN_HHX_MASK(hhxw) \
>> +    (BIT(hhxw, UL) - 1)
>> +#define APLIC_xMSICFGADDR_PPN_HHX_SHIFT(hhxs) \
>> +    ((hhxs) + APLIC_xMSICFGADDR_PPN_SHIFT)
>> +
>> +#define APLIC_xMSICFGADDR_PPN_LHX_MASK(lhxw) \
>> +    (BIT(lhxw, UL) - 1)
>> +#define APLIC_xMSICFGADDR_PPN_LHX_SHIFT(lhxs) \
>> +    (lhxs)
> 
> May I ask to avoid unnecessary line splitting here as well?

I will fix that for APLIC_xMSICFGADDR_PPN_HHX_MASK, 
APLIC_xMSICFGADDR_PPN_LHX_MASK and APLIC_xMSICFGADDR_PPN_LHX_SHIFT.

> 
>> --- a/xen/arch/riscv/include/asm/vaplic.h
>> +++ b/xen/arch/riscv/include/asm/vaplic.h
>> @@ -21,11 +21,16 @@ struct domain;
>>   
>>   struct vaplic_regs {
>>       uint32_t domaincfg;
>> +
>> +    uint32_t *target;
>>   };
> 
> A pointer in this structure is odd, as this (supposedly) is a set of
> guest register values. The field name also doesn't clarify its purpose.
> All in all: Likely a comment is needed here.

It is a pointer because I tried to save some memory and allocate 
register target based on how many irqs a guest really needed.

As an option I can allocate that statically and use the value from AIA
spec: target[1]-target[1023]. Would it better?

If a dynamic allocation is still fine then I will add the following comment:

     /*
      * Guest's view of the APLIC target registers, indexed by IRQ number.
      *
      * The array holds d->arch.vintc->nr_virqs elements; target[0] is 
unused
      * as APLIC interrupt sources start from 1.
      */
     uint32_t *target;


> 
>> --- a/xen/arch/riscv/vaplic.c
>> +++ b/xen/arch/riscv/vaplic.c
>> @@ -17,6 +17,7 @@
>>   #include <asm/aia.h>
>>   #include <asm/imsic.h>
>>   #include <asm/intc.h>
>> +#include <asm/mmio.h>
>>   #include <asm/vaplic.h>
>>   
>>   #include "aplic-priv.h"
>> @@ -27,6 +28,279 @@ unsigned int __ro_after_init guest_aplic_num_sources;
>>   
>>   #define FDT_VAPLIC_INT_CELLS 2
>>   
>> +#define AUTH_IRQ_BIT(d, irqn) \
>> +    (((irqn) < (d)->arch.vintc->nr_virqs) && \
>> +     test_bit(irqn, (d)->arch.vintc->used_irqs))
>> +
>> +/*
>> + * Convert a byte offset (within a SETIP/CLRIP/SETIE/CLRIE register group) to
>> + * a 32-bit word index into the used_irqs bitmap. Each word covers 32
>> + * interrupt sources. For SOURCECFG and TARGET groups the same division also
>> + * yields the interrupt number directly, because those arrays store one 32-bit
>> + * register per source.
>> + */
>> +#define regoffset_to_word_idx(reg_val) ((reg_val) / sizeof(uint32_t))
>> +
>> +static uint32_t vaplic_target_read(const struct domain *d, unsigned int irqn)
>> +{
>> +    const struct vaplic *vaplic = to_vaplic(d);
>> +
>> +    /* target[0] doesn't exist so irqn == 0 should be impossible */
>> +    if ( !irqn || irqn >= vaplic->vintc.nr_virqs )
>> +        return 0;
>> +
>> +    return read_atomic(&vaplic->regs.target[irqn]);
>> +}
>> +
>> +static inline uint32_t generate_auth_mask(const struct domain *currd,
>> +                                          unsigned int word_idx)
>> +{
>> +    unsigned int first_bit = word_idx * sizeof(uint32_t) * BITS_PER_BYTE;
>> +
>> +    if ( word_idx >= DIV_ROUND_UP(currd->arch.vintc->nr_virqs,
>> +                                  sizeof(uint32_t) * BITS_PER_BYTE) )
>> +    {
>> +        gdprintk(XENLOG_DEBUG, "incorrect word_idx(%u) is passed\n", word_idx);
> 
> Along the lines of earlier remarks: What value does "is passed" add?

Probably not too much sense. I will drop it.

> 
>> +static bool vaplic_emulate_store(const struct vcpu *curr, paddr_t addr,
>> +                                 uint32_t value)
>> +{
>> +    const struct domain *currd = curr->domain;
>> +    unsigned int offset = addr & APLIC_CTRL_REGION_OFFSET_MASK;
>> +
>> +    ASSERT(curr == current);
>> +
>> +    switch ( offset )
>> +    {
>> +    case APLIC_SETIP_BASE ... APLIC_SETIP_LAST:
>> +    case APLIC_CLRIP_BASE ... APLIC_CLRIP_LAST:
>> +    case APLIC_SETIE_BASE ... APLIC_SETIE_LAST:
>> +    case APLIC_CLRIE_BASE ... APLIC_CLRIE_LAST:
>> +    {
>> +        unsigned int word_idx =
>> +            regoffset_to_word_idx(offset & APLIC_SETCLR_OFFSET_MASK);
>> +
>> +        value &= generate_auth_mask(currd, word_idx);
>> +
>> +        break;
>> +    }
>> +
>> +    case APLIC_SOURCECFG_BASE ... APLIC_SOURCECFG_LAST:
>> +        if ( value & APLIC_SOURCECFG_D )
>> +        {
>> +            dprintk(XENLOG_ERR, "APLIC_SOURCECFG_D isn't supported\n");
> 
> gdprintk()?

Agree, it would be better to be gdprintk() here.

> 
>> +            goto fail;
>> +        }
>> +
>> +        /*
>> +         * As sourcecfg register starts from 1:
>> +         *   0x0000 domaincfg
>> +         *   0x0004 sourcecfg[1]
>> +         *   0x0008 sourcecfg[2]
>> +         *    ...
>> +         *   0x0FFC sourcecfg[1023]
>> +         * It is necessary to calculate an interrupt number by subtracting
>> +         * APLIC_DOMAINCFG instead of APLIC_SOURCECFG_BASE.
>> +         */
>> +        if ( !AUTH_IRQ_BIT(currd,
>> +                           regoffset_to_word_idx(offset - APLIC_DOMAINCFG)) )
>> +            /* Interrupt not enabled, ignore it */
>> +            return true;
>> +
>> +        if ( value > APLIC_SOURCECFG_SM_LEVEL_LOW )
>> +        {
>> +            gdprintk(XENLOG_ERR,
>> +                     "value(%#x) is incorrect for sourcecfg register\n",
>> +                     value);
>> +
>> +            return true;
>> +        }
>> +
>> +        break;
>> +
>> +    case APLIC_TARGET_BASE ... APLIC_TARGET_LAST:
>> +    {
>> +        struct vaplic *vaplic = to_vaplic(currd);
>> +        struct vcpu *target_vcpu;
> 
> This, btw, is a case where I think not using "v" is warranted.
> 
>> +        unsigned int guest_hart_idx = MASK_EXTR(value, APLIC_TARGET_HART_IDX);
>> +        /*
>> +         * Look at vaplic_emulate_load() for explanation why APLIC_GENMSI is
>> +         * subtracted.
>> +         */
>> +        unsigned int srcn = regoffset_to_word_idx(offset - APLIC_GENMSI);
>> +
>> +        if ( !AUTH_IRQ_BIT(currd, srcn) )
>> +            /* Interrupt not enabled, ignore it */
>> +            return true;
>> +
>> +        target_vcpu = domain_vcpu(currd, guest_hart_idx);
>> +
>> +        if ( !target_vcpu )
>> +        {
>> +            dprintk(XENLOG_ERR, "Invalid vCPU id in target register\n");
>> +
>> +            /* Ignore such writings */
>> +            return true;
>> +        }
>> +
>> +        if ( vaplic->regs.domaincfg & APLIC_DOMAINCFG_DM )
>> +        {
>> +            /*
>> +             * A non-zero guest index asks for delivery to an interrupt file of
>> +             * nested guest. The vIMSIC node has no riscv,guest-index-bits
>> +             * property, so a guest is told its harts have no guest interrupt
>> +             * files and the field is read-only zero for them. The write isn't
>> +             * rejected (that would throw away a valid hart index and EIID);
>> +             * instead the field is dropped, which is also what
>> +             * aplic_msi_target_gen() does with it when programming the h/w.
>> +             */
>> +            if ( MASK_EXTR(value, APLIC_TARGET_GUEST_IDX) )
>> +            {
>> +                printk_once(XENLOG_WARNING
>> +                            "%pd: vAPLIC target guest index != 0 is unsupported\n",
>> +                            currd);
>> +
>> +                /* Ignore such writes ... */
>> +                return true;
>> +            }
>> +
>> +            write_atomic(&vaplic->regs.target[srcn], value);
>> +
>> +            value = aplic_msi_target_gen(target_vcpu, value);
>> +        }
>> +        else
>> +        {
>> +            /*
>> +             * IPRIO is WARL and zero isn't a legal value for it, so normalize
>> +             * it once: the guest then reads back exactly what it gets.
>> +             */
> 
> What is "reads back exactly what it gets" supposed to express? The use of
> "once" there also isn't quite clear to me.

"once" meant the substitution is done in a single place (before the 
value is stored) so nothing has to be adjusted again on the read path; 
"reads back exactly what it gets" meant the shadow copy holds the same 
legal priority that is programmed into the hardware, not the zero the 
guest wrote.

I will re-word the comment to:

/*
  * IPRIO is WARL and zero isn't a legal value for it, so a write
  * of zero is replaced by APLIC_TARGET_IPRIO_DEFAULT. This is done
  * before the value is stored, so the guest-visible copy and what
  * is programmed into the h/w hold the same legal priority and no
  * fix-up is needed when the guest reads the register back.
  */

> 
>> +            unsigned int iprio = MASK_EXTR(value, APLIC_TARGET_IPRIO) ?:
>> +                                 APLIC_TARGET_IPRIO_DEFAULT;
>> +            unsigned long h = cpuid_to_hartid(guest_hart_idx);
>> +
>> +            value = MASK_INSR(guest_hart_idx, APLIC_TARGET_HART_IDX) |
>> +                    MASK_INSR(iprio, APLIC_TARGET_IPRIO);
>> +
>> +            write_atomic(&vaplic->regs.target[srcn], value);
>> +
>> +            value = MASK_INSR(h, APLIC_TARGET_HART_IDX) |
>> +                    MASK_INSR(iprio, APLIC_TARGET_IPRIO);
>> +        }
>> +
>> +        break;
>> +    }
>> +
>> +    case APLIC_SETIPNUM:
>> +    case APLIC_SETIPNUM_LE:
>> +    case APLIC_CLRIPNUM:
>> +    case APLIC_SETIENUM:
>> +    case APLIC_CLRIENUM:
>> +        if ( !value || !AUTH_IRQ_BIT(currd, value) )
>> +            return true;
>> +
>> +        break;
>> +
>> +    case APLIC_DOMAINCFG:
>> +    {
>> +        struct vaplic *vaplic = to_vaplic(currd);
>> +
>> +        vaplic->regs.domaincfg = APLIC_DOMAINCFG_RO |
>> +                                 (value & APLIC_DOMAINCFG_WMASK);
>> +
>> +        return true;
>> +    }
> 
> May I suggest that you arrange case blocks (primarily) by offset? This would
> then mean for DOMAINCFG handling to move to the top, helping at least a little
> with SOURCECFG_{BASE,LAST} handling (slightly oddly) using APLIC_DOMAINCFG.

I will do that.

> 
>> @@ -122,7 +447,29 @@ int domain_vaplic_init(struct domain *d)
>>        */
>>       d->arch.vintc->nr_virqs = guest_aplic_num_sources + 1;
>>   
>> -    return 0;
>> +    /* Slot 0 is unused: APLIC source numbering starts at 1 (see used_irqs). */
>> +    vaplic->regs.target = xvzalloc_array(uint32_t, d->arch.vintc->nr_virqs);
>> +    if ( !vaplic->regs.target )
>> +    {
>> +        d->arch.vintc = NULL;
>> +        xvfree(vaplic);
>> +
>> +        return -ENOMEM;
>> +    }
>> +
>> +    vaplic->regs_start = GUEST_APLIC_S_BASE;
>> +    vaplic->regs_size = APLIC_SIZE(d->max_vcpus);
>> +
>> +    rc = register_mmio_handler(d, &vaplic_mmio_ops,
>> +                               vaplic->regs_start, vaplic->regs_size);
>> +    if ( rc )
>> +    {
>> +        d->arch.vintc = NULL;
>> +        xvfree(vaplic->regs.target);
>> +        xvfree(vaplic);
>> +    }
> 
> Could you perhaps arrange for it to be possible to simply call
> domain_vaplic_deinit() here (and maybe also on at least some of the earlier
> error paths)? The code above loks very similar to ...
> 
>> @@ -134,5 +481,6 @@ void domain_vaplic_deinit(struct domain *d)
>>   
>>       vaplic = to_vaplic(d);
>>       d->arch.vintc = NULL;
>> +    xvfree(vaplic->regs.target);
>>       xvfree(vaplic);
>>   }
> 
> ... what's here.

Agree, domain_vaplic_deinit() could be used instead of open-coding when 
recieved rc is handled from  register_mmio_handler() and error path above.

Thanks!

~ Oleksii



^ permalink raw reply	[flat|nested] 244+ messages in thread

* Re: [PATCH v2 10/39] xen/riscv: build the target hart index via aplic_hart_field()
  2026-09-09 14:52   ` Jan Beulich
@ 2026-09-10 10:59     ` Oleksii Kurochko
  2026-09-10 11:23       ` Jan Beulich
  2026-09-10 11:23       ` Jan Beulich
  0 siblings, 2 replies; 244+ messages in thread
From: Oleksii Kurochko @ 2026-09-10 10:59 UTC (permalink / raw)
  To: Jan Beulich
  Cc: Romain Caritey, Baptiste Le Duc, Zheng Zhang, Alistair Francis,
	Connor Davis, Andrew Cooper, Anthony PERARD, Michal Orzel,
	Julien Grall, Roger Pau Monné, Stefano Stabellini, xen-devel



On 9/9/26 4:52 PM, Jan Beulich wrote:
> On 27.08.2026 17:20, Oleksii Kurochko wrote:
>> @@ -340,27 +338,11 @@ static void cf_check aplic_set_irq_affinity(struct irq_desc *desc, const cpumask
>>   
>>       ASSERT(spin_is_locked(&desc->lock));
>>   
>> -    cpu = cpuid_to_hartid(aplic_get_cpu_from_mask(mask));
>> -    hhxw = imsic->group_index_bits;
>> -    lhxw = imsic->hart_index_bits;
>> -    /*
>> -     * Although this variable is used only once in the calculation of
>> -     * group_index, and it might seem that hhxs could be defined as:
>> -     *   hhxs = imsic->group_index_shift - IMSIC_MMIO_PAGE_SHIFT;
>> -     * and then the addition of IMSIC_MMIO_PAGE_SHIFT could be omitted
>> -     * when calculating the group index.
>> -     * It was done intentionally this way to follow the formula from
>> -     * the AIA specification for calculating the MSI address.
>> -     */
>> -    hhxs = imsic->group_index_shift - IMSIC_MMIO_PAGE_SHIFT * 2;
>> -    base_ppn = imsic->msi[cpu].base_addr >> IMSIC_MMIO_PAGE_SHIFT;
>> -
>> -    /* Update hart and EEID in the target register */
>> -    group_index = (base_ppn >> (hhxs + IMSIC_MMIO_PAGE_SHIFT)) &
>> -                  (BIT(hhxw, UL) - 1);
>> -    value = desc->irq;
> 
> Hmm, only after sending the ack I noticed that there's no masking here, ...
> 
>> -    value |= cpu << APLIC_TARGET_HART_IDX_SHIFT;
>> -    value |= group_index << (lhxw + APLIC_TARGET_HART_IDX_SHIFT);
>> +    cpu = aplic_get_cpu_from_mask(mask);
>> +
>> +    /* Update hart index and EIID in the target register */
>> +    value = MASK_INSR(aplic_hart_field(cpu), APLIC_TARGET_HART_IDX) |
>> +            (desc->irq & APLIC_TARGET_EIID);
> 
> ... but there is masking here. Chopping off bits doesn't look as if it can
> lead to anything good. What's the deal here?

The mask is a no-op: desc->irq < NR_IRQS (1024) always fits the 11-bit 
EIID field, so I'll drop it and add a BUILD_BUG_ON() instead.

Would it be better to:

+    /* desc->irq < NR_IRQS, so it always fits the EIID field */
+    BUILD_BUG_ON(NR_IRQS - 1 > APLIC_TARGET_EIID);
+
+    value = MASK_INSR(aplic_hart_index(cpu), APLIC_TARGET_HART_IDX) | 
desc->irq;

?

Thanks.

~ Oleksii



^ permalink raw reply	[flat|nested] 244+ messages in thread

* Re: [PATCH v2 09/39] xen/riscv: implement virtual APLIC MMIO emulation
  2026-09-10 10:37     ` Oleksii Kurochko
@ 2026-09-10 11:14       ` Jan Beulich
  2026-09-10 14:24         ` Oleksii Kurochko
  0 siblings, 1 reply; 244+ messages in thread
From: Jan Beulich @ 2026-09-10 11:14 UTC (permalink / raw)
  To: Oleksii Kurochko
  Cc: Romain Caritey, Baptiste Le Duc, Zheng Zhang, Alistair Francis,
	Connor Davis, Andrew Cooper, Anthony PERARD, Michal Orzel,
	Julien Grall, Roger Pau Monné, Stefano Stabellini, xen-devel

On 10.09.2026 12:37, Oleksii Kurochko wrote:
> On 9/9/26 4:26 PM, Jan Beulich wrote:
>> On 27.08.2026 17:20, Oleksii Kurochko wrote:
>>> +uint32_t aplic_msi_target_gen(const struct vcpu *target_vcpu,
>>> +                              uint32_t base_val)
>>> +{
>>> +    unsigned int guest_id = vcpu_guest_file_id(target_vcpu);
>>> +    unsigned long hart_field = aplic_hart_field(target_vcpu->processor);
>>
>> What guarantees target_vcpu's ->processor field to be meaningful at this
>> point? 
> 
> Good question. Considering the places where it is called, I would expect 
> target_vcpu->processor to have something meaningful, since it is called 
> from a place that can only be reached when the guest is running.
> 
> Even without that, I don't think there is a big issue here, as 
> ->processor is initialized to 0 at allocation time. This means that the 
> target register will be configured in such a way that CPU 0 will handle 
> such IRQs.

I may not have been explicit enough then: Whether the guest (as a whole)
is running is of no interest. If the specific vCPU is running, all is fine.
If the specific vCPU is in the process of being moved to a different CPU,
and if you read ->processor just before the new value is put there, is
all going to be fine as well? I doubt that.

>>> --- a/xen/arch/riscv/include/asm/vaplic.h
>>> +++ b/xen/arch/riscv/include/asm/vaplic.h
>>> @@ -21,11 +21,16 @@ struct domain;
>>>   
>>>   struct vaplic_regs {
>>>       uint32_t domaincfg;
>>> +
>>> +    uint32_t *target;
>>>   };
>>
>> A pointer in this structure is odd, as this (supposedly) is a set of
>> guest register values. The field name also doesn't clarify its purpose.
>> All in all: Likely a comment is needed here.
> 
> It is a pointer because I tried to save some memory and allocate 
> register target based on how many irqs a guest really needed.
> 
> As an option I can allocate that statically and use the value from AIA
> spec: target[1]-target[1023]. Would it better?

No, I think dynamic allocation is justified here. (Question is whether it
should be struct vaplic_regs as a whole, but that depends on whether
another runtime-sized array may need adding to it later on.)

Jan


^ permalink raw reply	[flat|nested] 244+ messages in thread

* Re: [PATCH v2 10/39] xen/riscv: build the target hart index via aplic_hart_field()
  2026-09-10 10:59     ` Oleksii Kurochko
  2026-09-10 11:23       ` Jan Beulich
@ 2026-09-10 11:23       ` Jan Beulich
  1 sibling, 0 replies; 244+ messages in thread
From: Jan Beulich @ 2026-09-10 11:23 UTC (permalink / raw)
  To: Oleksii Kurochko
  Cc: Romain Caritey, Baptiste Le Duc, Zheng Zhang, Alistair Francis,
	Connor Davis, Andrew Cooper, Anthony PERARD, Michal Orzel,
	Julien Grall, Roger Pau Monné, Stefano Stabellini, xen-devel

On 10.09.2026 12:59, Oleksii Kurochko wrote:
> On 9/9/26 4:52 PM, Jan Beulich wrote:
>> On 27.08.2026 17:20, Oleksii Kurochko wrote:
>>> @@ -340,27 +338,11 @@ static void cf_check aplic_set_irq_affinity(struct irq_desc *desc, const cpumask
>>>   
>>>       ASSERT(spin_is_locked(&desc->lock));
>>>   
>>> -    cpu = cpuid_to_hartid(aplic_get_cpu_from_mask(mask));
>>> -    hhxw = imsic->group_index_bits;
>>> -    lhxw = imsic->hart_index_bits;
>>> -    /*
>>> -     * Although this variable is used only once in the calculation of
>>> -     * group_index, and it might seem that hhxs could be defined as:
>>> -     *   hhxs = imsic->group_index_shift - IMSIC_MMIO_PAGE_SHIFT;
>>> -     * and then the addition of IMSIC_MMIO_PAGE_SHIFT could be omitted
>>> -     * when calculating the group index.
>>> -     * It was done intentionally this way to follow the formula from
>>> -     * the AIA specification for calculating the MSI address.
>>> -     */
>>> -    hhxs = imsic->group_index_shift - IMSIC_MMIO_PAGE_SHIFT * 2;
>>> -    base_ppn = imsic->msi[cpu].base_addr >> IMSIC_MMIO_PAGE_SHIFT;
>>> -
>>> -    /* Update hart and EEID in the target register */
>>> -    group_index = (base_ppn >> (hhxs + IMSIC_MMIO_PAGE_SHIFT)) &
>>> -                  (BIT(hhxw, UL) - 1);
>>> -    value = desc->irq;
>>
>> Hmm, only after sending the ack I noticed that there's no masking here, ...
>>
>>> -    value |= cpu << APLIC_TARGET_HART_IDX_SHIFT;
>>> -    value |= group_index << (lhxw + APLIC_TARGET_HART_IDX_SHIFT);
>>> +    cpu = aplic_get_cpu_from_mask(mask);
>>> +
>>> +    /* Update hart index and EIID in the target register */
>>> +    value = MASK_INSR(aplic_hart_field(cpu), APLIC_TARGET_HART_IDX) |
>>> +            (desc->irq & APLIC_TARGET_EIID);
>>
>> ... but there is masking here. Chopping off bits doesn't look as if it can
>> lead to anything good. What's the deal here?
> 
> The mask is a no-op: desc->irq < NR_IRQS (1024) always fits the 11-bit 
> EIID field, so I'll drop it and add a BUILD_BUG_ON() instead.
> 
> Would it be better to:
> 
> +    /* desc->irq < NR_IRQS, so it always fits the EIID field */
> +    BUILD_BUG_ON(NR_IRQS - 1 > APLIC_TARGET_EIID);

If the question is whether to prefer BUILD_BUG_ON() over BUG_ON(), then:
Yes please. However, APLIC_TARGET_EIID is a mask (despite its name not
indicating that), which only happens to start at bit 0. This kind of
assumption would better be avoided.

This raises another question though: No matter how big a RISC-V system
is, it can only ever have 1k IRQs? How does that work with a single
MSI-X device having up to 2k MSIs?

Jan

Jan


^ permalink raw reply	[flat|nested] 244+ messages in thread

* Re: [PATCH v2 10/39] xen/riscv: build the target hart index via aplic_hart_field()
  2026-09-10 10:59     ` Oleksii Kurochko
@ 2026-09-10 11:23       ` Jan Beulich
  2026-09-10 12:44         ` Oleksii Kurochko
  2026-09-10 11:23       ` Jan Beulich
  1 sibling, 1 reply; 244+ messages in thread
From: Jan Beulich @ 2026-09-10 11:23 UTC (permalink / raw)
  To: Oleksii Kurochko
  Cc: Romain Caritey, Baptiste Le Duc, Zheng Zhang, Alistair Francis,
	Connor Davis, Andrew Cooper, Anthony PERARD, Michal Orzel,
	Julien Grall, Roger Pau Monné, Stefano Stabellini, xen-devel

On 10.09.2026 12:59, Oleksii Kurochko wrote:
> On 9/9/26 4:52 PM, Jan Beulich wrote:
>> On 27.08.2026 17:20, Oleksii Kurochko wrote:
>>> @@ -340,27 +338,11 @@ static void cf_check aplic_set_irq_affinity(struct irq_desc *desc, const cpumask
>>>   
>>>       ASSERT(spin_is_locked(&desc->lock));
>>>   
>>> -    cpu = cpuid_to_hartid(aplic_get_cpu_from_mask(mask));
>>> -    hhxw = imsic->group_index_bits;
>>> -    lhxw = imsic->hart_index_bits;
>>> -    /*
>>> -     * Although this variable is used only once in the calculation of
>>> -     * group_index, and it might seem that hhxs could be defined as:
>>> -     *   hhxs = imsic->group_index_shift - IMSIC_MMIO_PAGE_SHIFT;
>>> -     * and then the addition of IMSIC_MMIO_PAGE_SHIFT could be omitted
>>> -     * when calculating the group index.
>>> -     * It was done intentionally this way to follow the formula from
>>> -     * the AIA specification for calculating the MSI address.
>>> -     */
>>> -    hhxs = imsic->group_index_shift - IMSIC_MMIO_PAGE_SHIFT * 2;
>>> -    base_ppn = imsic->msi[cpu].base_addr >> IMSIC_MMIO_PAGE_SHIFT;
>>> -
>>> -    /* Update hart and EEID in the target register */
>>> -    group_index = (base_ppn >> (hhxs + IMSIC_MMIO_PAGE_SHIFT)) &
>>> -                  (BIT(hhxw, UL) - 1);
>>> -    value = desc->irq;
>>
>> Hmm, only after sending the ack I noticed that there's no masking here, ...
>>
>>> -    value |= cpu << APLIC_TARGET_HART_IDX_SHIFT;
>>> -    value |= group_index << (lhxw + APLIC_TARGET_HART_IDX_SHIFT);
>>> +    cpu = aplic_get_cpu_from_mask(mask);
>>> +
>>> +    /* Update hart index and EIID in the target register */
>>> +    value = MASK_INSR(aplic_hart_field(cpu), APLIC_TARGET_HART_IDX) |
>>> +            (desc->irq & APLIC_TARGET_EIID);
>>
>> ... but there is masking here. Chopping off bits doesn't look as if it can
>> lead to anything good. What's the deal here?
> 
> The mask is a no-op: desc->irq < NR_IRQS (1024) always fits the 11-bit 
> EIID field, so I'll drop it and add a BUILD_BUG_ON() instead.
> 
> Would it be better to:
> 
> +    /* desc->irq < NR_IRQS, so it always fits the EIID field */
> +    BUILD_BUG_ON(NR_IRQS - 1 > APLIC_TARGET_EIID);

If the question is whether to prefer BUILD_BUG_ON() over BUG_ON(), then:
Yes please. However, APLIC_TARGET_EIID is a mask (despite its name not
indicating that), which only happens to start at bit 0. This kind of
assumption would better be avoided.

This raises another question though: No matter how big a RISC-V system
is, it can only ever have 1k IRQs? How does that work with a single
MSI-X device having up to 2k MSIs?

Jan

Jan


^ permalink raw reply	[flat|nested] 244+ messages in thread

* Re: [PATCH v2 10/39] xen/riscv: build the target hart index via aplic_hart_field()
  2026-09-10 11:23       ` Jan Beulich
@ 2026-09-10 12:44         ` Oleksii Kurochko
  2026-09-10 12:57           ` Jan Beulich
  0 siblings, 1 reply; 244+ messages in thread
From: Oleksii Kurochko @ 2026-09-10 12:44 UTC (permalink / raw)
  To: Jan Beulich
  Cc: Romain Caritey, Baptiste Le Duc, Zheng Zhang, Alistair Francis,
	Connor Davis, Andrew Cooper, Anthony PERARD, Michal Orzel,
	Julien Grall, Roger Pau Monné, Stefano Stabellini, xen-devel



On 9/10/26 1:23 PM, Jan Beulich wrote:
> On 10.09.2026 12:59, Oleksii Kurochko wrote:
>> On 9/9/26 4:52 PM, Jan Beulich wrote:
>>> On 27.08.2026 17:20, Oleksii Kurochko wrote:
>>>> @@ -340,27 +338,11 @@ static void cf_check aplic_set_irq_affinity(struct irq_desc *desc, const cpumask
>>>>    
>>>>        ASSERT(spin_is_locked(&desc->lock));
>>>>    
>>>> -    cpu = cpuid_to_hartid(aplic_get_cpu_from_mask(mask));
>>>> -    hhxw = imsic->group_index_bits;
>>>> -    lhxw = imsic->hart_index_bits;
>>>> -    /*
>>>> -     * Although this variable is used only once in the calculation of
>>>> -     * group_index, and it might seem that hhxs could be defined as:
>>>> -     *   hhxs = imsic->group_index_shift - IMSIC_MMIO_PAGE_SHIFT;
>>>> -     * and then the addition of IMSIC_MMIO_PAGE_SHIFT could be omitted
>>>> -     * when calculating the group index.
>>>> -     * It was done intentionally this way to follow the formula from
>>>> -     * the AIA specification for calculating the MSI address.
>>>> -     */
>>>> -    hhxs = imsic->group_index_shift - IMSIC_MMIO_PAGE_SHIFT * 2;
>>>> -    base_ppn = imsic->msi[cpu].base_addr >> IMSIC_MMIO_PAGE_SHIFT;
>>>> -
>>>> -    /* Update hart and EEID in the target register */
>>>> -    group_index = (base_ppn >> (hhxs + IMSIC_MMIO_PAGE_SHIFT)) &
>>>> -                  (BIT(hhxw, UL) - 1);
>>>> -    value = desc->irq;
>>>
>>> Hmm, only after sending the ack I noticed that there's no masking here, ...
>>>
>>>> -    value |= cpu << APLIC_TARGET_HART_IDX_SHIFT;
>>>> -    value |= group_index << (lhxw + APLIC_TARGET_HART_IDX_SHIFT);
>>>> +    cpu = aplic_get_cpu_from_mask(mask);
>>>> +
>>>> +    /* Update hart index and EIID in the target register */
>>>> +    value = MASK_INSR(aplic_hart_field(cpu), APLIC_TARGET_HART_IDX) |
>>>> +            (desc->irq & APLIC_TARGET_EIID);
>>>
>>> ... but there is masking here. Chopping off bits doesn't look as if it can
>>> lead to anything good. What's the deal here?
>>
>> The mask is a no-op: desc->irq < NR_IRQS (1024) always fits the 11-bit
>> EIID field, so I'll drop it and add a BUILD_BUG_ON() instead.
>>
>> Would it be better to:
>>
>> +    /* desc->irq < NR_IRQS, so it always fits the EIID field */
>> +    BUILD_BUG_ON(NR_IRQS - 1 > APLIC_TARGET_EIID);
> 
> If the question is whether to prefer BUILD_BUG_ON() over BUG_ON(), then:
> Yes please. However, APLIC_TARGET_EIID is a mask (despite its name not
> indicating that), which only happens to start at bit 0. This kind of
> assumption would better be avoided.

Then BUILD_BUG_on should be updated to:

  /* desc->irq < NR_IRQS, so it always fits the EIID field */
     BUILD_BUG_ON(NR_IRQS - 1 > MASK_EXTR(~0U, APLIC_TARGET_EIID));


> 
> This raises another question though: No matter how big a RISC-V system
> is, it can only ever have 1k IRQs? How does that work with a single
> MSI-X device having up to 2k MSIs?

Device MSIs never pass through the APLIC. They are written straight into 
an IMSIC interrupt file. The ID space belongs to each file, so each hart 
has up to 2047 IDs (IMSIC_MAX_ID).

1k it is limitation for wired interrupts (which could be delivered in 
MSI mode where APLIC + IMSIC is needed) which are going through APLIC.

~ Oleksii


^ permalink raw reply	[flat|nested] 244+ messages in thread

* Re: [PATCH v2 10/39] xen/riscv: build the target hart index via aplic_hart_field()
  2026-09-10 12:44         ` Oleksii Kurochko
@ 2026-09-10 12:57           ` Jan Beulich
  2026-09-11  9:47             ` Oleksii Kurochko
  0 siblings, 1 reply; 244+ messages in thread
From: Jan Beulich @ 2026-09-10 12:57 UTC (permalink / raw)
  To: Oleksii Kurochko
  Cc: Romain Caritey, Baptiste Le Duc, Zheng Zhang, Alistair Francis,
	Connor Davis, Andrew Cooper, Anthony PERARD, Michal Orzel,
	Julien Grall, Roger Pau Monné, Stefano Stabellini, xen-devel

On 10.09.2026 14:44, Oleksii Kurochko wrote:
> 
> 
> On 9/10/26 1:23 PM, Jan Beulich wrote:
>> On 10.09.2026 12:59, Oleksii Kurochko wrote:
>>> On 9/9/26 4:52 PM, Jan Beulich wrote:
>>>> On 27.08.2026 17:20, Oleksii Kurochko wrote:
>>>>> @@ -340,27 +338,11 @@ static void cf_check aplic_set_irq_affinity(struct irq_desc *desc, const cpumask
>>>>>    
>>>>>        ASSERT(spin_is_locked(&desc->lock));
>>>>>    
>>>>> -    cpu = cpuid_to_hartid(aplic_get_cpu_from_mask(mask));
>>>>> -    hhxw = imsic->group_index_bits;
>>>>> -    lhxw = imsic->hart_index_bits;
>>>>> -    /*
>>>>> -     * Although this variable is used only once in the calculation of
>>>>> -     * group_index, and it might seem that hhxs could be defined as:
>>>>> -     *   hhxs = imsic->group_index_shift - IMSIC_MMIO_PAGE_SHIFT;
>>>>> -     * and then the addition of IMSIC_MMIO_PAGE_SHIFT could be omitted
>>>>> -     * when calculating the group index.
>>>>> -     * It was done intentionally this way to follow the formula from
>>>>> -     * the AIA specification for calculating the MSI address.
>>>>> -     */
>>>>> -    hhxs = imsic->group_index_shift - IMSIC_MMIO_PAGE_SHIFT * 2;
>>>>> -    base_ppn = imsic->msi[cpu].base_addr >> IMSIC_MMIO_PAGE_SHIFT;
>>>>> -
>>>>> -    /* Update hart and EEID in the target register */
>>>>> -    group_index = (base_ppn >> (hhxs + IMSIC_MMIO_PAGE_SHIFT)) &
>>>>> -                  (BIT(hhxw, UL) - 1);
>>>>> -    value = desc->irq;
>>>>
>>>> Hmm, only after sending the ack I noticed that there's no masking here, ...
>>>>
>>>>> -    value |= cpu << APLIC_TARGET_HART_IDX_SHIFT;
>>>>> -    value |= group_index << (lhxw + APLIC_TARGET_HART_IDX_SHIFT);
>>>>> +    cpu = aplic_get_cpu_from_mask(mask);
>>>>> +
>>>>> +    /* Update hart index and EIID in the target register */
>>>>> +    value = MASK_INSR(aplic_hart_field(cpu), APLIC_TARGET_HART_IDX) |
>>>>> +            (desc->irq & APLIC_TARGET_EIID);
>>>>
>>>> ... but there is masking here. Chopping off bits doesn't look as if it can
>>>> lead to anything good. What's the deal here?
>>>
>>> The mask is a no-op: desc->irq < NR_IRQS (1024) always fits the 11-bit
>>> EIID field, so I'll drop it and add a BUILD_BUG_ON() instead.
>>>
>>> Would it be better to:
>>>
>>> +    /* desc->irq < NR_IRQS, so it always fits the EIID field */
>>> +    BUILD_BUG_ON(NR_IRQS - 1 > APLIC_TARGET_EIID);
>>
>> If the question is whether to prefer BUILD_BUG_ON() over BUG_ON(), then:
>> Yes please. However, APLIC_TARGET_EIID is a mask (despite its name not
>> indicating that), which only happens to start at bit 0. This kind of
>> assumption would better be avoided.
> 
> Then BUILD_BUG_on should be updated to:
> 
>   /* desc->irq < NR_IRQS, so it always fits the EIID field */
>      BUILD_BUG_ON(NR_IRQS - 1 > MASK_EXTR(~0U, APLIC_TARGET_EIID));
> 
> 
>>
>> This raises another question though: No matter how big a RISC-V system
>> is, it can only ever have 1k IRQs? How does that work with a single
>> MSI-X device having up to 2k MSIs?
> 
> Device MSIs never pass through the APLIC. They are written straight into 
> an IMSIC interrupt file. The ID space belongs to each file, so each hart 
> has up to 2047 IDs (IMSIC_MAX_ID).
> 
> 1k it is limitation for wired interrupts (which could be delivered in 
> MSI mode where APLIC + IMSIC is needed) which are going through APLIC.

But desc->irq and NR_IRQS have to represent both. Which then puts the
BUILD_BUG_ON() above under question.

Jan


^ permalink raw reply	[flat|nested] 244+ messages in thread

* Re: [PATCH v2 12/39] xen/riscv: implement vCPU context switching
  2026-08-27 15:20 ` [PATCH v2 12/39] xen/riscv: implement vCPU context switching Oleksii Kurochko
                     ` (2 preceding siblings ...)
  2026-09-05  7:25   ` Oleksii Kurochko
@ 2026-09-10 13:29   ` Jan Beulich
  2026-09-11 10:43     ` Oleksii Kurochko
  3 siblings, 1 reply; 244+ messages in thread
From: Jan Beulich @ 2026-09-10 13:29 UTC (permalink / raw)
  To: Oleksii Kurochko
  Cc: Romain Caritey, Baptiste Le Duc, Zheng Zhang, Alistair Francis,
	Connor Davis, Andrew Cooper, Anthony PERARD, Michal Orzel,
	Julien Grall, Roger Pau Monné, Stefano Stabellini, xen-devel

On 27.08.2026 17:20, Oleksii Kurochko wrote:
> +static void ctxt_switch_from(struct vcpu *p)
> +{
> +    /*
> +     * When the idle VCPU is running, Xen will always stay in hypervisor
> +     * mode.
> +     * Therefore we don't need to save the context of an idle VCPU.
> +     */
> +    if ( is_idle_vcpu(p) )
> +        return;
> +
> +    p2m_ctxt_switch_from(p);
> +
> +    vtimer_ctxt_switch_from(p);
> +
> +    save_csr_regs(p);
> +}
> +
> +static void ctxt_switch_to(struct vcpu *n)
> +{
> +    /*
> +     * When the idle VCPU is running, Xen will always stay in hypervisor
> +     * mode.
> +     * Therefore we don't need to restore the context of an idle VCPU.
> +     */
> +    if ( is_idle_vcpu(n) )
> +        return;
> +
> +    /*
> +     * If this vCPU last ran on a different pCPU, invalidate its VMID so
> +     * vmid_handle_vmenter() assigns a fresh one from the current pCPU's pool.
> +     * Without this, two pCPUs could independently assign the same
> +     * (generation, vmid) pair, generation counters start at the same value
> +     * on all pCPUs and increment independently, causing TLB contamination.
> +     */
> +    if ( n->arch.last_cpu != smp_processor_id() )
> +        vmid_flush_vcpu(n);

I wonder why you need this, when we don't have anything similar in x86/HVM
(and at the first glance Arm doesn't have anything similar either).

> +    vtimer_ctxt_switch_to(n);
> +
> +    restore_csr_regs(n);
> +
> +    p2m_ctxt_switch_to(n);
> +}

In the absenmce of a comment towards the need for this specific order I'd
expect these three calls to be ordered the opposite of their counterparts
in ctxt_switch_from().

> +static void schedule_tail(struct vcpu *prev)
> +{
> +    unsigned int cpu = smp_processor_id();
> +
> +    ASSERT(prev != current);
> +
> +    ctxt_switch_from(prev);
> +
> +    /*
> +     * Mark this CPU in next domain's dirty cpumasks before calling
> +     * ctxt_switch_to(). This avoids a race on things like p2m flushing,
> +     * which is synchronised on that function.
> +     */
> +    if ( prev->domain != current->domain )
> +    {
> +        cpumask_set_cpu(cpu, current->domain->dirty_cpumask);
> +
> +        /*
> +         * Once this hart drops out of prev's dirty_cpumask it stops being a
> +         * target of p2m_tlb_flush(), while its TLB may still hold G-stage
> +         * translations of prev's domain: neither the vCPU which just ran nor
> +         * any other vCPU of that domain which ran here earlier has had its
> +         * VMID invalidated. Move the hart to a new VMID generation so that
> +         * none of them can be reached again.
> +         *
> +         * Switching away from the idle vCPU needs no bump: the idle domain
> +         * has no p2m of its own, and whatever G-stage entries this hart may
> +         * still hold (or speculatively create while HGATP keeps pointing at
> +         * the last guest's p2m) are tagged with a VMID which was already made
> +         * stale when that guest was switched out. Skipping the bump here also
> +         * avoids burning a generation on every pass through idle.
> +         */
> +        if ( !is_idle_vcpu(prev) )
> +            vmid_flush_hart();
> +
> +        cpumask_clear_cpu(cpu, prev->domain->dirty_cpumask);
> +    }
> +    write_atomic(&current->dirty_cpu, cpu);
> +
> +    ctxt_switch_to(current);
> +
> +    write_atomic(&prev->dirty_cpu, VCPU_CPU_CLEAN);
> +
> +    current->arch.last_cpu = cpu;
> +
> +    /*
> +     * sched_context_switched() internally uses a spinlock,
> +     * which requires interrupts to be enabled.
> +     */
> +    local_irq_enable();
> +
> +    sched_context_switched(prev, current);
> +}
> +
> +void context_switch(struct vcpu *prev, struct vcpu *next)
> +{
> +    ASSERT(local_irq_is_enabled());
> +    ASSERT(prev != next);
> +    ASSERT(!vcpu_cpu_dirty(next));
> +
> +    local_irq_disable();
> +
> +    set_current(next);
> +
> +    prev = __context_switch(prev, next);
> +
> +    schedule_tail(prev);
> +}

__context_switch() switches stacks, which can easily collide with code the
compiler has emitted. For example, the call to schedule_tail() may not be
a tail call, and context_switch()'s return address may have been spilled
to the stack (or into one of the s<N> registers). There's a reason Arm and
x86 have reset_stack_and_jump().

> --- a/xen/arch/riscv/entry.S
> +++ b/xen/arch/riscv/entry.S
> @@ -99,3 +99,47 @@ restore_registers:
>  
>          sret
>  END(handle_trap)
> +
> +/*
> + * struct vcpu *__context_switch(struct vcpu *prev, struct vcpu *next)
> + *
> + * This is called on prev's stack, and returns on next's.

With ra being switched it may also return to other than the caller. If
that's really intended, I think it also needs calling out here.

> + * a0 - prev
> + * a1 - next
> + *
> + * Returns prev in a0
> + */
> +FUNC(__context_switch)
> +        REG_S   s0, VCPU_XEN_SAVED_CONTEXT_S0(a0)
> +        REG_S   s1, VCPU_XEN_SAVED_CONTEXT_S1(a0)
> +        REG_S   s2, VCPU_XEN_SAVED_CONTEXT_S2(a0)
> +        REG_S   s3, VCPU_XEN_SAVED_CONTEXT_S3(a0)
> +        REG_S   s4, VCPU_XEN_SAVED_CONTEXT_S4(a0)
> +        REG_S   s5, VCPU_XEN_SAVED_CONTEXT_S5(a0)
> +        REG_S   s6, VCPU_XEN_SAVED_CONTEXT_S6(a0)
> +        REG_S   s7, VCPU_XEN_SAVED_CONTEXT_S7(a0)
> +        REG_S   s8, VCPU_XEN_SAVED_CONTEXT_S8(a0)
> +        REG_S   s9, VCPU_XEN_SAVED_CONTEXT_S9(a0)
> +        REG_S   s10, VCPU_XEN_SAVED_CONTEXT_S10(a0)
> +        REG_S   s11, VCPU_XEN_SAVED_CONTEXT_S11(a0)
> +        REG_S   sp, VCPU_XEN_SAVED_CONTEXT_SP(a0)
> +        REG_S   ra, VCPU_XEN_SAVED_CONTEXT_RA(a0)
> +
> +        REG_L   s0, VCPU_XEN_SAVED_CONTEXT_S0(a1)
> +        REG_L   s1, VCPU_XEN_SAVED_CONTEXT_S1(a1)
> +        REG_L   s2, VCPU_XEN_SAVED_CONTEXT_S2(a1)
> +        REG_L   s3, VCPU_XEN_SAVED_CONTEXT_S3(a1)
> +        REG_L   s4, VCPU_XEN_SAVED_CONTEXT_S4(a1)
> +        REG_L   s5, VCPU_XEN_SAVED_CONTEXT_S5(a1)
> +        REG_L   s6, VCPU_XEN_SAVED_CONTEXT_S6(a1)
> +        REG_L   s7, VCPU_XEN_SAVED_CONTEXT_S7(a1)
> +        REG_L   s8, VCPU_XEN_SAVED_CONTEXT_S8(a1)
> +        REG_L   s9, VCPU_XEN_SAVED_CONTEXT_S9(a1)
> +        REG_L   s10, VCPU_XEN_SAVED_CONTEXT_S10(a1)
> +        REG_L   s11, VCPU_XEN_SAVED_CONTEXT_S11(a1)
> +        REG_L   sp, VCPU_XEN_SAVED_CONTEXT_SP(a1)
> +        REG_L   ra, VCPU_XEN_SAVED_CONTEXT_RA(a1)
> +
> +        ret
> +END(__context_switch)

What about gp and tp?

> --- a/xen/arch/riscv/include/asm/system.h
> +++ b/xen/arch/riscv/include/asm/system.h
> @@ -76,6 +76,10 @@ static inline bool local_irq_is_enabled(void)
>  
>  #define arch_fetch_and_add(x, v) __sync_fetch_and_add(x, v)
>  
> +struct vcpu;

I don't think this is needed, as ...

> +struct vcpu *__context_switch(struct vcpu *prev, struct vcpu *next);

... parsing of the return type will make the struct known (before
parameters are parsed).

Also - can't next be pointer-to-const?

Jan


^ permalink raw reply	[flat|nested] 244+ messages in thread

* Re: [PATCH v2 09/39] xen/riscv: implement virtual APLIC MMIO emulation
  2026-09-10 11:14       ` Jan Beulich
@ 2026-09-10 14:24         ` Oleksii Kurochko
  2026-09-12  8:50           ` SeungJu Cheon
  0 siblings, 1 reply; 244+ messages in thread
From: Oleksii Kurochko @ 2026-09-10 14:24 UTC (permalink / raw)
  To: Jan Beulich
  Cc: Romain Caritey, Baptiste Le Duc, Zheng Zhang, Alistair Francis,
	Connor Davis, Andrew Cooper, Anthony PERARD, Michal Orzel,
	Julien Grall, Roger Pau Monné, Stefano Stabellini, xen-devel



On 9/10/26 1:14 PM, Jan Beulich wrote:
> On 10.09.2026 12:37, Oleksii Kurochko wrote:
>> On 9/9/26 4:26 PM, Jan Beulich wrote:
>>> On 27.08.2026 17:20, Oleksii Kurochko wrote:
>>>> +uint32_t aplic_msi_target_gen(const struct vcpu *target_vcpu,
>>>> +                              uint32_t base_val)
>>>> +{
>>>> +    unsigned int guest_id = vcpu_guest_file_id(target_vcpu);
>>>> +    unsigned long hart_field = aplic_hart_field(target_vcpu->processor);
>>>
>>> What guarantees target_vcpu's ->processor field to be meaningful at this
>>> point?
>>
>> Good question. Considering the places where it is called, I would expect
>> target_vcpu->processor to have something meaningful, since it is called
>> from a place that can only be reached when the guest is running.
>>
>> Even without that, I don't think there is a big issue here, as
>> ->processor is initialized to 0 at allocation time. This means that the
>> target register will be configured in such a way that CPU 0 will handle
>> such IRQs.
> 
> I may not have been explicit enough then: Whether the guest (as a whole)
> is running is of no interest. If the specific vCPU is running, all is fine.
> If the specific vCPU is in the process of being moved to a different CPU,
> and if you read ->processor just before the new value is put there, is
> all going to be fine as well? I doubt that.
> 

You're right, and it's worse than a stale CPU number: v->processor is 
updated by the scheduler (sched_unit_migrate_finish()) before 
sched_move_irqs() -> imsic_migrate_vcpu() moves the interrupt file, so a 
concurrent vAPLIC TARGET write can combine the new CPU with the old 
guest file index (an MSI into someone else's file, which 
aplic_reconfigure_target(), which is introduced later in this patch 
series, won't catch), or compute a correct old target but write it after 
aplic_reconfigure_target() (the function which is called during 
migration to re-target irqs to new pCPU) has already scanned.

In v3 I'll (a) stop using ->processor and take the (guest_file_id, 
vsfile_cpu) pair, which imsic_update_state() updates atomically under 
vsfile_lock, and (b) do the snapshot plus the h/w TARGET write under 
aplic.lock, which aplic_reconfigure_target() also holds. As 
imsic_update_state() completes before aplic_reconfigure_target() (also 
that could be checked in this patch series and is introduced a little 
bit later. Probably I have to re-order some patches again) takes the 
lock, the emulated write either happens before the scan (and gets fixed 
up, or skipped as already correct) or after it (and sees the new location).

Any better option I have now?

~ Oleksii


^ permalink raw reply	[flat|nested] 244+ messages in thread

* Re: [PATCH v2 14/39] xen/riscv: introduce vintc_ctxt_switch_{from,to}()
  2026-08-27 15:20 ` [PATCH v2 14/39] xen/riscv: introduce vintc_ctxt_switch_{from,to}() Oleksii Kurochko
  2026-09-04 11:25   ` Baptiste Le Duc
@ 2026-09-10 14:54   ` Jan Beulich
  1 sibling, 0 replies; 244+ messages in thread
From: Jan Beulich @ 2026-09-10 14:54 UTC (permalink / raw)
  To: Oleksii Kurochko
  Cc: Romain Caritey, Baptiste Le Duc, Zheng Zhang, Alistair Francis,
	Connor Davis, Andrew Cooper, Anthony PERARD, Michal Orzel,
	Julien Grall, Roger Pau Monné, Stefano Stabellini, xen-devel

On 27.08.2026 17:20, Oleksii Kurochko wrote:
> Virtual interrupt controller state must be preserved across vCPU context
> switches.
> 
> Introduce vintc_ctxt_switch_{from,to}() wrappers around new
> ctxt_switch_{from,to}() hooks in struct vintc_ops, and call them from the
> context switch path, so that this state can be saved/restored without
> knowing which vINTC variant a domain uses.
> 
> No vINTC variant implements the hooks yet: the vAPLIC implementation is
> added separately.
> 
> Signed-off-by: Oleksii Kurochko <oleksii.kurochko@gmail.com>

Acked-by: Jan Beulich <jbeulich@suse.com>

Also in case you decide to ...

> ---
> Changes in v2:
>  - Update the commit message.
>  - s/vintc_state_{save,restore}/vintc_ctxt_switch_{to,from}.
>  - s/{re}store_state/ctxt_switch_{to,from} for vintc_ops.
>  - s/vcpu/v for vintc_ctxt_switch_{to,from}() arguments.

... rename them again to n / p.

Jan


^ permalink raw reply	[flat|nested] 244+ messages in thread

* Re: [PATCH v2 15/39] xen/riscv: add IMSIC vCPU context switch handlers
  2026-08-27 15:20 ` [PATCH v2 15/39] xen/riscv: add IMSIC vCPU context switch handlers Oleksii Kurochko
  2026-09-04 11:33   ` Baptiste Le Duc
@ 2026-09-10 14:57   ` Jan Beulich
  2026-09-11 11:19     ` Oleksii Kurochko
  1 sibling, 1 reply; 244+ messages in thread
From: Jan Beulich @ 2026-09-10 14:57 UTC (permalink / raw)
  To: Oleksii Kurochko
  Cc: Romain Caritey, Baptiste Le Duc, Zheng Zhang, Alistair Francis,
	Connor Davis, Andrew Cooper, Anthony PERARD, Michal Orzel,
	Julien Grall, Roger Pau Monné, Stefano Stabellini, xen-devel

On 27.08.2026 17:20, Oleksii Kurochko wrote:
> --- a/xen/arch/riscv/vaplic.c
> +++ b/xen/arch/riscv/vaplic.c
> @@ -422,6 +422,13 @@ static const struct mmio_handler_ops vaplic_mmio_ops = {
>  static const struct vintc_ops vintc_ops = {
>      .vcpu_init = vcpu_imsic_init,
>      .vcpu_deinit = vcpu_imsic_deinit,
> +    /*
> +     * MSI delivery is the only supported mode: aplic_init() panics on an
> +     * APLIC without an "msi-parent", so the vAPLIC state to save and restore
> +     * is always the IMSIC one.
> +     */
> +    .ctxt_switch_from = imsic_ctxt_switch_from,
> +    .ctxt_switch_to = imsic_ctxt_switch_to,
>  };

As previously expressed, I'm not happy with comments like this. aplic_init()
isn't related to vAPLIC behavior. We're doing virtualization, so at least
conceptually host and guest behavior want properly separating. Then it may
still be that for the time being only a certain subset of possibilities is
supported.

Jan


^ permalink raw reply	[flat|nested] 244+ messages in thread

* Re: [PATCH v2 21/39] xen/riscv: resolve the faulting guest physical address
  2026-08-27 15:21 ` [PATCH v2 21/39] xen/riscv: resolve the faulting guest physical address Oleksii Kurochko
  2026-09-09 12:04   ` Baptiste Le Duc
@ 2026-09-10 15:06   ` Jan Beulich
  1 sibling, 0 replies; 244+ messages in thread
From: Jan Beulich @ 2026-09-10 15:06 UTC (permalink / raw)
  To: Oleksii Kurochko
  Cc: Romain Caritey, Baptiste Le Duc, Zheng Zhang, Alistair Francis,
	Connor Davis, Andrew Cooper, Anthony PERARD, Michal Orzel,
	Julien Grall, Roger Pau Monné, Stefano Stabellini, xen-devel

On 27.08.2026 17:21, Oleksii Kurochko wrote:
> Take the guest physical address from htval and stval: on a guest-page fault
> htval holds it shifted right by 2, so that an address wider than XLEN fits,
> and stval holds the faulting guest virtual address, whose two least
> significant bits are those of the guest physical address. The shift is done
> on paddr_t rather than on the raw register: a guest physical address is 34
> bits wide on RV32 with Sv32x4, so shifting an XLEN-wide value would drop its
> top two bits.
> 
> Those two low bits come from stval only for a fault on an explicit access.
> Where one is taken on an implicit access made for VS-stage translation htval
> holds the address of the VS-stage PTE which could not be read, while stval
> still holds the guest virtual address which started the walk, and the low
> bits of the address written to htval are zero instead. htinst tells the two
> apart, which is what the spec points at it for.
> 
> stval needs no check against an ISA extension: a guest-page fault writes it
> with the faulting guest virtual address regardless. Sstvala would not be the
> right thing to test for either (it covers stval across every trap type
> which writes it, a wider guarantee than what is needed here).
> 
> htval does need one. The H extension lets an implementation write it with
> either the faulting address or zero, so without Shtvala a zero htval cannot
> be told apart from a genuine fault on guest physical address 0-3, and the
> address has to be recovered by decoding the access and walking the VS-stage
> page tables in software instead. That is left as a TODO, and until it is
> written such hardware panics rather than acting on an address which may not
> be the one which faulted.
> 
> Signed-off-by: Oleksii Kurochko <oleksii.kurochko@gmail.com>

Acked-by: Jan Beulich <jbeulich@suse.com>



^ permalink raw reply	[flat|nested] 244+ messages in thread

* Re: [PATCH v2 22/39] xen/riscv: add guest memory read helper
  2026-09-09 12:04   ` Baptiste Le Duc
@ 2026-09-10 15:19     ` Jan Beulich
  2026-09-11 13:06     ` Oleksii Kurochko
  1 sibling, 0 replies; 244+ messages in thread
From: Jan Beulich @ 2026-09-10 15:19 UTC (permalink / raw)
  To: Baptiste Le Duc, Oleksii Kurochko
  Cc: xen-devel, Romain Caritey, Zheng Zhang, Alistair Francis,
	Connor Davis, Andrew Cooper, Anthony PERARD, Michal Orzel,
	Julien Grall, Roger Pau Monné, Stefano Stabellini

On 09.09.2026 14:04, Baptiste Le Duc wrote:
>> Introduce riscv_read_guest() to allow Xen to safely read guest memory
>> using HLV/HLVX instructions while reliably capturing trap context.
> 
>> This is required for instruction fetch emulation and MMIO decoding, where
>> Xen must inspect guest memory that may not be directly accessible and may
>> fault.
>>
>> The implementation is based on kvm_riscv_vcpu_unpriv_read() from Linux,
>> with one deviation: the hlv/hlvx instructions translate the guest address
>> through the live vsatp/hgatp CSRs, i.e. through the address space of the
>> currently running vCPU, so the function can only be called safely for
>> current. Instead of taking a struct vcpu argument, it always operates on
>> current directly.
>>
>> Signed-off-by: Oleksii Kurochko <oleksii.kurochko@gmail.com>
>>
>> diff --git a/xen/arch/riscv/guestcopy.c b/xen/arch/riscv/guestcopy.c
>> index 8a89212e0b..b2327822ac 100644
>> --- a/xen/arch/riscv/guestcopy.c
>> +++ b/xen/arch/riscv/guestcopy.c
>> @@ -6,6 +6,7 @@
>>  #include <xen/string.h>
>>  
>>  #include <asm/guest_access.h>
>> +#include <asm/traps.h>
>>  
>>  #define COPY_from_guest     0U
>>  #define COPY_to_guest       BIT(0, U)
>> @@ -114,3 +115,89 @@ unsigned long copy_to_guest_phys(struct domain *d, paddr_t gpa, void *buf,
>>      return copy_guest(buf, gpa, len, GPA_INFO(d),
>>                        COPY_to_guest | COPY_gpa);
>>  }
>> +
>> +/*
>> + * Read machine word from guest memory
>> + *
>> + * @guest_addr: Guest address to read
>> + * @read_insn: Flag representing whether we are reading instruction
>> + * @trap: Output pointer to trap details if something went wrong during read
>> + *
>> + * The hlv/hlvx instructions translate guest_addr through the live
>> + * vsatp/hgatp CSRs, so the read is only meaningful for the address
>> + * space of the currently running vCPU.
>> + *
>> + * At most two halfwords are fetched when @read_insn is true, i.e. encodings
>> + * wider than 32 bits are not supported. Such an encoding cannot be completed
>> + * by calling this function again at @guest_addr + 4: the length check is
>> + * applied to the first halfword read, which would then be a continuation of
>> + * the instruction rather than its opcode. It is up to the caller to reject
>> + * anything that is neither a 16- nor a 32-bit encoding.
>> + */
>> +unsigned long riscv_read_guest(unsigned long guest_addr, bool read_insn,
>> +                               struct trap_info *trap)
> Nit: every other function in this file / declared in this header
> (raw_copy_from_guest, copy_to_guest_phys, ...) has no riscv_ prefix. Why
> does this one get it?
>> +{
>> +    /*
>> +     * Poison the result: if the very first access faults, the fixup skips
>> +     * over the loads without writing it. Callers must check trap->scause.
>> +     */
>> +    unsigned long val = ~0UL, tmp;
>>
> The actual "did a trap happen" contract callers rely on is
> trap->scause == 0. If the very first access faults, fixup_exception() will
> write scause accordingly to a non-zero value, but nothing in this function
> clears trap->scause on the success path.

So perhaps the assumption is that callers pass in a zero-filled struct?
Would want (need) spelling out, though.

Jan


^ permalink raw reply	[flat|nested] 244+ messages in thread

* Re: [PATCH v2 22/39] xen/riscv: add guest memory read helper
  2026-08-27 15:21 ` [PATCH v2 22/39] xen/riscv: add guest memory read helper Oleksii Kurochko
  2026-09-09 12:04   ` Baptiste Le Duc
@ 2026-09-10 15:28   ` Jan Beulich
  2026-09-11 13:57     ` Oleksii Kurochko
  1 sibling, 1 reply; 244+ messages in thread
From: Jan Beulich @ 2026-09-10 15:28 UTC (permalink / raw)
  To: Oleksii Kurochko
  Cc: Romain Caritey, Baptiste Le Duc, Zheng Zhang, Alistair Francis,
	Connor Davis, Andrew Cooper, Anthony PERARD, Michal Orzel,
	Julien Grall, Roger Pau Monné, Stefano Stabellini, xen-devel

On 27.08.2026 17:21, Oleksii Kurochko wrote:
> --- a/xen/arch/riscv/guestcopy.c
> +++ b/xen/arch/riscv/guestcopy.c
> @@ -6,6 +6,7 @@
>  #include <xen/string.h>
>  
>  #include <asm/guest_access.h>
> +#include <asm/traps.h>
>  
>  #define COPY_from_guest     0U
>  #define COPY_to_guest       BIT(0, U)
> @@ -114,3 +115,89 @@ unsigned long copy_to_guest_phys(struct domain *d, paddr_t gpa, void *buf,
>      return copy_guest(buf, gpa, len, GPA_INFO(d),
>                        COPY_to_guest | COPY_gpa);
>  }
> +
> +/*
> + * Read machine word from guest memory
> + *
> + * @guest_addr: Guest address to read
> + * @read_insn: Flag representing whether we are reading instruction
> + * @trap: Output pointer to trap details if something went wrong during read
> + *
> + * The hlv/hlvx instructions translate guest_addr through the live
> + * vsatp/hgatp CSRs, so the read is only meaningful for the address
> + * space of the currently running vCPU.
> + *
> + * At most two halfwords are fetched when @read_insn is true, i.e. encodings
> + * wider than 32 bits are not supported. Such an encoding cannot be completed
> + * by calling this function again at @guest_addr + 4: the length check is
> + * applied to the first halfword read, which would then be a continuation of
> + * the instruction rather than its opcode. It is up to the caller to reject
> + * anything that is neither a 16- nor a 32-bit encoding.
> + */
> +unsigned long riscv_read_guest(unsigned long guest_addr, bool read_insn,
> +                               struct trap_info *trap)
> +{
> +    /*
> +     * Poison the result: if the very first access faults, the fixup skips
> +     * over the loads without writing it. Callers must check trap->scause.
> +     */
> +    unsigned long val = ~0UL, tmp;
> +
> +    /*
> +     * hlv/hlvx use hstatus.SPVP for the privilege of the access, and the
> +     * live vsatp/hgatp for the translation. Xen never installs a value of
> +     * its own in hstatus (it is only saved on trap entry and restored
> +     * before sret) and it doesn't reschedule before returning to the
> +     * guest, so all three still belong to the vCPU which trapped.
> +     *
> +     * Check the saved copy rather than the live CSR: a nested trap taken
> +     * from HS-mode clears hstatus.SPV in the CSR (but leaves SPVP alone).
> +     */
> +    ASSERT(vcpu_guest_cpu_user_regs(current)->hstatus & HSTATUS_SPV);

The first paragraph talks of just hstatus.SPVP. The second paragraph then
starting "Check ..." means that still refers to hstatus.SPVP, when - aiui -
hstatus.SPV is meant.

Furthermore, instead of special casing nested faults here, but otherwise
saying "Xen doesn't modify", wouldn't it be better to word things such
that they remain correct if Xen ends up having a need to touch some other
part of hstatus (including, potentially, SPV)? IOW - I think it is natural
that the original guest value is checked. Question being of how much value
that checking is: vcpu_guest_cpu_user_regs(current)->hstatus can't possibly
have SPV clear, can it? Only nested exception frames could.

Jan


^ permalink raw reply	[flat|nested] 244+ messages in thread

* Re: [PATCH v2 23/39] xen/riscv: look up the exception table for any trap taken in Xen context
  2026-08-27 15:21 ` [PATCH v2 23/39] xen/riscv: look up the exception table for any trap taken in Xen context Oleksii Kurochko
@ 2026-09-10 15:31   ` Jan Beulich
  2026-09-18  8:44   ` Baptiste Le Duc
  1 sibling, 0 replies; 244+ messages in thread
From: Jan Beulich @ 2026-09-10 15:31 UTC (permalink / raw)
  To: Oleksii Kurochko
  Cc: Romain Caritey, Baptiste Le Duc, Zheng Zhang, Alistair Francis,
	Connor Davis, Andrew Cooper, Anthony PERARD, Michal Orzel,
	Julien Grall, Roger Pau Monné, Stefano Stabellini, xen-devel

On 27.08.2026 17:21, Oleksii Kurochko wrote:
> do_trap() consulted the exception table only for CAUSE_ILLEGAL_INSTRUCTION,
> which covers csr_read_safe() but not the hlv/hlvx sequences reading guest
> memory: those fault with load/store (guest) page fault causes and would
> reach do_unexpected_trap() instead of their fixup.
> 
> Move the lookup ahead of the cause switch, and gate it on the trap having
> been taken in Xen context and not being an interrupt:
> 
> - sepc of a trap taken from the guest is a guest VA/PA, which the
>   guest can point at an address listed in the exception table; Xen would
>   then act on that entry and, for EX_TYPE_TRAP_INFO, write through a
>   pointer fully under guest control. Entries are matched by exact address,
>   so this needs no more than a numerical collision.
> 
> - an interrupt taken at an address listed in the table would otherwise be
>   "fixed up" as if the access itself had faulted, silently skipping it and
>   handing the caller the interrupt's scause as a fault cause.
> 
> Returning early skips check_for_pcpu_work(), which is correct: that only
> runs for traps taken from the guest.
> 
> With that in place a G-stage fault reaching the switch can no longer have
> been caused by an hlv/hlvx covered by an entry, so anything left must have
> come from the guest; assert as much.
> 
> Cache the "trap came from the guest" test in a local, it is now used four
> times.
> 
> Signed-off-by: Oleksii Kurochko <oleksii.kurochko@gmail.com>

Acked-by: Jan Beulich <jbeulich@suse.com>



^ permalink raw reply	[flat|nested] 244+ messages in thread

* Re: [PATCH v2 10/39] xen/riscv: build the target hart index via aplic_hart_field()
  2026-09-10 12:57           ` Jan Beulich
@ 2026-09-11  9:47             ` Oleksii Kurochko
  0 siblings, 0 replies; 244+ messages in thread
From: Oleksii Kurochko @ 2026-09-11  9:47 UTC (permalink / raw)
  To: Jan Beulich
  Cc: Romain Caritey, Baptiste Le Duc, Zheng Zhang, Alistair Francis,
	Connor Davis, Andrew Cooper, Anthony PERARD, Michal Orzel,
	Julien Grall, Roger Pau Monné, Stefano Stabellini, xen-devel



On 9/10/26 2:57 PM, Jan Beulich wrote:
> On 10.09.2026 14:44, Oleksii Kurochko wrote:
>>
>> On 9/10/26 1:23 PM, Jan Beulich wrote:
>>> On 10.09.2026 12:59, Oleksii Kurochko wrote:
>>>> On 9/9/26 4:52 PM, Jan Beulich wrote:
>>>>> On 27.08.2026 17:20, Oleksii Kurochko wrote:
>>>>>> @@ -340,27 +338,11 @@ static void cf_check aplic_set_irq_affinity(struct irq_desc *desc, const cpumask
>>>>>>     
>>>>>>         ASSERT(spin_is_locked(&desc->lock));
>>>>>>     
>>>>>> -    cpu = cpuid_to_hartid(aplic_get_cpu_from_mask(mask));
>>>>>> -    hhxw = imsic->group_index_bits;
>>>>>> -    lhxw = imsic->hart_index_bits;
>>>>>> -    /*
>>>>>> -     * Although this variable is used only once in the calculation of
>>>>>> -     * group_index, and it might seem that hhxs could be defined as:
>>>>>> -     *   hhxs = imsic->group_index_shift - IMSIC_MMIO_PAGE_SHIFT;
>>>>>> -     * and then the addition of IMSIC_MMIO_PAGE_SHIFT could be omitted
>>>>>> -     * when calculating the group index.
>>>>>> -     * It was done intentionally this way to follow the formula from
>>>>>> -     * the AIA specification for calculating the MSI address.
>>>>>> -     */
>>>>>> -    hhxs = imsic->group_index_shift - IMSIC_MMIO_PAGE_SHIFT * 2;
>>>>>> -    base_ppn = imsic->msi[cpu].base_addr >> IMSIC_MMIO_PAGE_SHIFT;
>>>>>> -
>>>>>> -    /* Update hart and EEID in the target register */
>>>>>> -    group_index = (base_ppn >> (hhxs + IMSIC_MMIO_PAGE_SHIFT)) &
>>>>>> -                  (BIT(hhxw, UL) - 1);
>>>>>> -    value = desc->irq;
>>>>> Hmm, only after sending the ack I noticed that there's no masking here, ...
>>>>>
>>>>>> -    value |= cpu << APLIC_TARGET_HART_IDX_SHIFT;
>>>>>> -    value |= group_index << (lhxw + APLIC_TARGET_HART_IDX_SHIFT);
>>>>>> +    cpu = aplic_get_cpu_from_mask(mask);
>>>>>> +
>>>>>> +    /* Update hart index and EIID in the target register */
>>>>>> +    value = MASK_INSR(aplic_hart_field(cpu), APLIC_TARGET_HART_IDX) |
>>>>>> +            (desc->irq & APLIC_TARGET_EIID);
>>>>> ... but there is masking here. Chopping off bits doesn't look as if it can
>>>>> lead to anything good. What's the deal here?
>>>> The mask is a no-op: desc->irq < NR_IRQS (1024) always fits the 11-bit
>>>> EIID field, so I'll drop it and add a BUILD_BUG_ON() instead.
>>>>
>>>> Would it be better to:
>>>>
>>>> +    /* desc->irq < NR_IRQS, so it always fits the EIID field */
>>>> +    BUILD_BUG_ON(NR_IRQS - 1 > APLIC_TARGET_EIID);
>>> If the question is whether to prefer BUILD_BUG_ON() over BUG_ON(), then:
>>> Yes please. However, APLIC_TARGET_EIID is a mask (despite its name not
>>> indicating that), which only happens to start at bit 0. This kind of
>>> assumption would better be avoided.
>> Then BUILD_BUG_on should be updated to:
>>
>>    /* desc->irq < NR_IRQS, so it always fits the EIID field */
>>       BUILD_BUG_ON(NR_IRQS - 1 > MASK_EXTR(~0U, APLIC_TARGET_EIID));
>>
>>
>>> This raises another question though: No matter how big a RISC-V system
>>> is, it can only ever have 1k IRQs? How does that work with a single
>>> MSI-X device having up to 2k MSIs?
>> Device MSIs never pass through the APLIC. They are written straight into
>> an IMSIC interrupt file. The ID space belongs to each file, so each hart
>> has up to 2047 IDs (IMSIC_MAX_ID).
>>
>> 1k it is limitation for wired interrupts (which could be delivered in
>> MSI mode where APLIC + IMSIC is needed) which are going through APLIC.
> But desc->irq and NR_IRQS have to represent both. Which then puts the
> BUILD_BUG_ON() above under question.

Agreed. Right now desc->irq, the APLIC source number and the IMSIC EIID
are all the same number: aplic_set_irq_affinity() writes desc->irq into
EIID, and aplic_handle_interrupt() hands the IMSIC identity from stopei
straight to do_IRQ(). That only works because there's no MSI support on
RISC-V yet, so NR_IRQS covers wired interrupts only.

Once device MSIs are supported, that identity mapping can't stay anyway.
IMSIC IDs are per interrupt file and shared between wired and MSI
interrupts, so we'll need to allocate EIIDs and map desc->irq to
(hart, EIID). target[] will then get the allocated EIID rather than
desc->irq, and NR_IRQS will grow beyond 1024.

So for this patch I'll tie the check to the APLIC source range instead 
of NR_IRQS:

BUILD_BUG_ON(ARRAY_SIZE(aplic.regs->target) >
              MASK_EXTR(~0U, APLIC_TARGET_EIID));
ASSERT(desc->irq && desc->irq <= aplic_info.num_irqs);

The ASSERT also covers the target[desc->irq - 1] indexing.

~ Oleksii



^ permalink raw reply	[flat|nested] 244+ messages in thread

* Re: [PATCH v2 12/39] xen/riscv: implement vCPU context switching
  2026-09-10 13:29   ` Jan Beulich
@ 2026-09-11 10:43     ` Oleksii Kurochko
  0 siblings, 0 replies; 244+ messages in thread
From: Oleksii Kurochko @ 2026-09-11 10:43 UTC (permalink / raw)
  To: Jan Beulich
  Cc: Romain Caritey, Baptiste Le Duc, Zheng Zhang, Alistair Francis,
	Connor Davis, Andrew Cooper, Anthony PERARD, Michal Orzel,
	Julien Grall, Roger Pau Monné, Stefano Stabellini, xen-devel



On 9/10/26 3:29 PM, Jan Beulich wrote:
> On 27.08.2026 17:20, Oleksii Kurochko wrote:
>> +static void ctxt_switch_from(struct vcpu *p)
>> +{
>> +    /*
>> +     * When the idle VCPU is running, Xen will always stay in hypervisor
>> +     * mode.
>> +     * Therefore we don't need to save the context of an idle VCPU.
>> +     */
>> +    if ( is_idle_vcpu(p) )
>> +        return;
>> +
>> +    p2m_ctxt_switch_from(p);
>> +
>> +    vtimer_ctxt_switch_from(p);
>> +
>> +    save_csr_regs(p);
>> +}
>> +
>> +static void ctxt_switch_to(struct vcpu *n)
>> +{
>> +    /*
>> +     * When the idle VCPU is running, Xen will always stay in hypervisor
>> +     * mode.
>> +     * Therefore we don't need to restore the context of an idle VCPU.
>> +     */
>> +    if ( is_idle_vcpu(n) )
>> +        return;
>> +
>> +    /*
>> +     * If this vCPU last ran on a different pCPU, invalidate its VMID so
>> +     * vmid_handle_vmenter() assigns a fresh one from the current pCPU's pool.
>> +     * Without this, two pCPUs could independently assign the same
>> +     * (generation, vmid) pair, generation counters start at the same value
>> +     * on all pCPUs and increment independently, causing TLB contamination.
>> +     */
>> +    if ( n->arch.last_cpu != smp_processor_id() )
>> +        vmid_flush_vcpu(n);
> 
> I wonder why you need this, when we don't have anything similar in x86/HVM
> (and at the first glance Arm doesn't have anything similar either).

x86 does have the equivalent: vmx_do_resume() calls 
hvm_asid_flush_vcpu() in the active_cpu != smp_processor_id() branch, 
and svm_do_resume() does the same when launch_core != smp_processor_id() 
("Migrating to another ASID domain. Request a new ASID."). The RISC-V 
VMID allocator follows the x86 ASID scheme: VMIDs are a per-pCPU 
resource with a per-pCPU generation, so a (generation, vmid) pair 
obtained on one pCPU means nothing on another. Arm doesn't need this 
because it allocates a single VMID per domain from a global bitmap.

Also, note that I've update a little bit how VMIDs are flushed here [1]
but this check still present IIURC.

[1] 
https://lore.kernel.org/xen-devel/cover.1787838835.git.oleksii.kurochko@gmail.com/T/#m2c06e58c03a09022af112388be3585bf3ae6e4dc

> 
>> +    vtimer_ctxt_switch_to(n);
>> +
>> +    restore_csr_regs(n);
>> +
>> +    p2m_ctxt_switch_to(n);
>> +}
> 
> In the absenmce of a comment towards the need for this specific order I'd
> expect these three calls to be ordered the opposite of their counterparts
> in ctxt_switch_from().

I will put restore_csr_regs(n) (and rename it to 
csr_regs_ctxt_switch_to(n)) before vtimer_ctxt_switch_to(). There is no 
any specific requirement to be ordered in the way it is now.

> 
>> +static void schedule_tail(struct vcpu *prev)
>> +{
>> +    unsigned int cpu = smp_processor_id();
>> +
>> +    ASSERT(prev != current);
>> +
>> +    ctxt_switch_from(prev);
>> +
>> +    /*
>> +     * Mark this CPU in next domain's dirty cpumasks before calling
>> +     * ctxt_switch_to(). This avoids a race on things like p2m flushing,
>> +     * which is synchronised on that function.
>> +     */
>> +    if ( prev->domain != current->domain )
>> +    {
>> +        cpumask_set_cpu(cpu, current->domain->dirty_cpumask);
>> +
>> +        /*
>> +         * Once this hart drops out of prev's dirty_cpumask it stops being a
>> +         * target of p2m_tlb_flush(), while its TLB may still hold G-stage
>> +         * translations of prev's domain: neither the vCPU which just ran nor
>> +         * any other vCPU of that domain which ran here earlier has had its
>> +         * VMID invalidated. Move the hart to a new VMID generation so that
>> +         * none of them can be reached again.
>> +         *
>> +         * Switching away from the idle vCPU needs no bump: the idle domain
>> +         * has no p2m of its own, and whatever G-stage entries this hart may
>> +         * still hold (or speculatively create while HGATP keeps pointing at
>> +         * the last guest's p2m) are tagged with a VMID which was already made
>> +         * stale when that guest was switched out. Skipping the bump here also
>> +         * avoids burning a generation on every pass through idle.
>> +         */
>> +        if ( !is_idle_vcpu(prev) )
>> +            vmid_flush_hart();
>> +
>> +        cpumask_clear_cpu(cpu, prev->domain->dirty_cpumask);
>> +    }
>> +    write_atomic(&current->dirty_cpu, cpu);
>> +
>> +    ctxt_switch_to(current);
>> +
>> +    write_atomic(&prev->dirty_cpu, VCPU_CPU_CLEAN);
>> +
>> +    current->arch.last_cpu = cpu;
>> +
>> +    /*
>> +     * sched_context_switched() internally uses a spinlock,
>> +     * which requires interrupts to be enabled.
>> +     */
>> +    local_irq_enable();
>> +
>> +    sched_context_switched(prev, current);
>> +}
>> +
>> +void context_switch(struct vcpu *prev, struct vcpu *next)
>> +{
>> +    ASSERT(local_irq_is_enabled());
>> +    ASSERT(prev != next);
>> +    ASSERT(!vcpu_cpu_dirty(next));
>> +
>> +    local_irq_disable();
>> +
>> +    set_current(next);
>> +
>> +    prev = __context_switch(prev, next);
>> +
>> +    schedule_tail(prev);
>> +}
> 
> __context_switch() switches stacks, which can easily collide with code the
> compiler has emitted. For example, the call to schedule_tail() may not be
> a tail call, and context_switch()'s return address may have been spilled
> to the stack (or into one of the s<N> registers). There's a reason Arm and
> x86 have reset_stack_and_jump().

RISC-V will have reset_stack_and_jump() that too but just introduced 
later and will be used for different use case (in continue_new_vcpu() 
introduced later in this patch series). But as the Arm RISC-V doesn't 
use reset_stack_and_jump() in context_switch().

This follows the Arm model: every vCPU has its own Xen stack, and from 
the incoming vCPU's point of view __context_switch() is ABI-conforming. 
It restores exactly the sp/ra/s0-s11 that vCPU had when it itself called 
__context_switch() from context_switch(). So after the return we are in 
next's own context_switch() frame, and anything the compiler spilled 
there (ra included) belongs to next. The only exception is a vCPU which 
has never run: its ra points at continue_new_vcpu() on an empty stack, 
and that's where reset_stack_and_jump() is needed, as on Arm. I'll make 
continue_new_vcpu() noreturn accordingly. x86 differs because its stacks 
are per-pCPU (IIUC), hence its context_switch() can't return.

Here is some diagram for better understanding:
vCPU A (stack A)                vCPU B (stack B, switched out earlier)
schedule()                      schedule()
  `- context_switch(A, B)         `- context_switch(B, X)
      [A's frame: ra, s-regs]         [B's frame: ra, s-regs]
      `- __context_switch() --------> "returns" here
         (save A, load B)             schedule_tail(prev = A)
                                      ld ra <- B's frame (B's own ra)
                                      ret -> B's sched_context_switch()
                                          -> ... -> back into B


> 
>> --- a/xen/arch/riscv/entry.S
>> +++ b/xen/arch/riscv/entry.S
>> @@ -99,3 +99,47 @@ restore_registers:
>>   
>>           sret
>>   END(handle_trap)
>> +
>> +/*
>> + * struct vcpu *__context_switch(struct vcpu *prev, struct vcpu *next)
>> + *
>> + * This is called on prev's stack, and returns on next's.
> 
> With ra being switched it may also return to other than the caller. If
> that's really intended, I think it also needs calling out here.

Yes, it's intended. Normally it returns into next's own context_switch()
(where next itself last called __context_switch()), and for a vCPU
which has never run it returns to continue_new_vcpu(). I'll update the
comment to:

  * This is called on prev's stack, and returns on next's. As ra is
  * switched too, it doesn't return to its caller: it returns to where
  * next last called it from, i.e. into next's own context_switch(), or,
  * for a vCPU which has never run, to continue_new_vcpu() with an empty
  * stack.


> 
>> + * a0 - prev
>> + * a1 - next
>> + *
>> + * Returns prev in a0
>> + */
>> +FUNC(__context_switch)
>> +        REG_S   s0, VCPU_XEN_SAVED_CONTEXT_S0(a0)
>> +        REG_S   s1, VCPU_XEN_SAVED_CONTEXT_S1(a0)
>> +        REG_S   s2, VCPU_XEN_SAVED_CONTEXT_S2(a0)
>> +        REG_S   s3, VCPU_XEN_SAVED_CONTEXT_S3(a0)
>> +        REG_S   s4, VCPU_XEN_SAVED_CONTEXT_S4(a0)
>> +        REG_S   s5, VCPU_XEN_SAVED_CONTEXT_S5(a0)
>> +        REG_S   s6, VCPU_XEN_SAVED_CONTEXT_S6(a0)
>> +        REG_S   s7, VCPU_XEN_SAVED_CONTEXT_S7(a0)
>> +        REG_S   s8, VCPU_XEN_SAVED_CONTEXT_S8(a0)
>> +        REG_S   s9, VCPU_XEN_SAVED_CONTEXT_S9(a0)
>> +        REG_S   s10, VCPU_XEN_SAVED_CONTEXT_S10(a0)
>> +        REG_S   s11, VCPU_XEN_SAVED_CONTEXT_S11(a0)
>> +        REG_S   sp, VCPU_XEN_SAVED_CONTEXT_SP(a0)
>> +        REG_S   ra, VCPU_XEN_SAVED_CONTEXT_RA(a0)
>> +
>> +        REG_L   s0, VCPU_XEN_SAVED_CONTEXT_S0(a1)
>> +        REG_L   s1, VCPU_XEN_SAVED_CONTEXT_S1(a1)
>> +        REG_L   s2, VCPU_XEN_SAVED_CONTEXT_S2(a1)
>> +        REG_L   s3, VCPU_XEN_SAVED_CONTEXT_S3(a1)
>> +        REG_L   s4, VCPU_XEN_SAVED_CONTEXT_S4(a1)
>> +        REG_L   s5, VCPU_XEN_SAVED_CONTEXT_S5(a1)
>> +        REG_L   s6, VCPU_XEN_SAVED_CONTEXT_S6(a1)
>> +        REG_L   s7, VCPU_XEN_SAVED_CONTEXT_S7(a1)
>> +        REG_L   s8, VCPU_XEN_SAVED_CONTEXT_S8(a1)
>> +        REG_L   s9, VCPU_XEN_SAVED_CONTEXT_S9(a1)
>> +        REG_L   s10, VCPU_XEN_SAVED_CONTEXT_S10(a1)
>> +        REG_L   s11, VCPU_XEN_SAVED_CONTEXT_S11(a1)
>> +        REG_L   sp, VCPU_XEN_SAVED_CONTEXT_SP(a1)
>> +        REG_L   ra, VCPU_XEN_SAVED_CONTEXT_RA(a1)
>> +
>> +        ret
>> +END(__context_switch)
> 
> What about gp and tp?

tp points to this hart's pcpu_info (set up once per hart by
setup_tp()), i.e. it's per-pCPU rather than per-vCPU state.
__context_switch() starts and ends on the same hart, so tp has to be
left alone.

gp isn't used by Xen at all: there's no __global_pointer$ in the
linker script, so no gp-relative relaxation happens, and the compiler
never allocates gp.

Neither of them is callee-saved per the psABI, so there's nothing to
preserve across the call. The guest's gp/tp are part of the guest
state and are going to be saved/restored via cpu_user_regs by the
trap entry/exit path.


> 
>> --- a/xen/arch/riscv/include/asm/system.h
>> +++ b/xen/arch/riscv/include/asm/system.h
>> @@ -76,6 +76,10 @@ static inline bool local_irq_is_enabled(void)
>>   
>>   #define arch_fetch_and_add(x, v) __sync_fetch_and_add(x, v)
>>   
>> +struct vcpu;
> 
> I don't think this is needed, as ...
> 
>> +struct vcpu *__context_switch(struct vcpu *prev, struct vcpu *next);
> 
> ... parsing of the return type will make the struct known (before
> parameters are parsed).

Make sense to me. I will drop forward declaration.

> 
> Also - can't next be pointer-to-const?
It could be. I will add  const.

Thanks!

~ Oleksii




^ permalink raw reply	[flat|nested] 244+ messages in thread

* Re: [PATCH v2 15/39] xen/riscv: add IMSIC vCPU context switch handlers
  2026-09-10 14:57   ` Jan Beulich
@ 2026-09-11 11:19     ` Oleksii Kurochko
  0 siblings, 0 replies; 244+ messages in thread
From: Oleksii Kurochko @ 2026-09-11 11:19 UTC (permalink / raw)
  To: Jan Beulich
  Cc: Romain Caritey, Baptiste Le Duc, Zheng Zhang, Alistair Francis,
	Connor Davis, Andrew Cooper, Anthony PERARD, Michal Orzel,
	Julien Grall, Roger Pau Monné, Stefano Stabellini, xen-devel



On 9/10/26 4:57 PM, Jan Beulich wrote:
> On 27.08.2026 17:20, Oleksii Kurochko wrote:
>> --- a/xen/arch/riscv/vaplic.c
>> +++ b/xen/arch/riscv/vaplic.c
>> @@ -422,6 +422,13 @@ static const struct mmio_handler_ops vaplic_mmio_ops = {
>>   static const struct vintc_ops vintc_ops = {
>>       .vcpu_init = vcpu_imsic_init,
>>       .vcpu_deinit = vcpu_imsic_deinit,
>> +    /*
>> +     * MSI delivery is the only supported mode: aplic_init() panics on an
>> +     * APLIC without an "msi-parent", so the vAPLIC state to save and restore
>> +     * is always the IMSIC one.
>> +     */
>> +    .ctxt_switch_from = imsic_ctxt_switch_from,
>> +    .ctxt_switch_to = imsic_ctxt_switch_to,
>>   };
> 
> As previously expressed, I'm not happy with comments like this. aplic_init()
> isn't related to vAPLIC behavior. We're doing virtualization, so at least
> conceptually host and guest behavior want properly separating. Then it may
> still be that for the time being only a certain subset of possibilities is
> supported.

I will drop the part "aplic_init() panics on an APLIC without an 
"msi-parent". It is really not very relevant here.

Thanks.

~ Oleksii


^ permalink raw reply	[flat|nested] 244+ messages in thread

* Re: [PATCH v2 18/39] xen/riscv: add guest page fault handling stub
  2026-09-10  6:38       ` Jan Beulich
@ 2026-09-11 11:47         ` Oleksii Kurochko
  0 siblings, 0 replies; 244+ messages in thread
From: Oleksii Kurochko @ 2026-09-11 11:47 UTC (permalink / raw)
  To: Jan Beulich
  Cc: Romain Caritey, Baptiste Le Duc, Zheng Zhang, Alistair Francis,
	Connor Davis, Andrew Cooper, Anthony PERARD, Michal Orzel,
	Julien Grall, Roger Pau Monné, Stefano Stabellini, xen-devel



On 9/10/26 8:38 AM, Jan Beulich wrote:
> On 09.09.2026 17:09, Oleksii Kurochko wrote:
>> On 9/8/26 4:10 PM, Jan Beulich wrote:
>>> On 27.08.2026 17:21, Oleksii Kurochko wrote:
>>>> --- /dev/null
>>>> +++ b/xen/arch/riscv/emulate.c
>>>> @@ -0,0 +1,179 @@
>>>> +/* SPDX-License-Identifier: GPL-2.0-or-later */
>>>> +
>>>> +/*
>>>> + * RISC-V instruction emulation for trapped guest accesses
>>>> + */
>>>> +
>>>> +#include <xen/bug.h>
>>>> +#include <xen/errno.h>
>>>> +#include <xen/sched.h>
>>>> +#include <xen/types.h>
>>>> +
>>>> +#include <asm/csr.h>
>>>> +#include <asm/current.h>
>>>> +#include <asm/emulate.h>
>>>> +#include <asm/riscv_encoding.h>
>>>> +#include <asm/traps.h>
>>>> +
>>>> +/*
>>>> + * The hardware-reported details of a guest page fault, gathered once by
>>>> + * handle_guest_page_fault() and passed down to the emulation of the faulted
>>>> + * access.
>>>> + */
>>>> +struct guest_fault {
>>>> +    /* The guest register state as saved on entry to do_trap(). */
>>>> +    struct cpu_user_regs *regs;
>>>
>>> If the comment was true, this could be pointer-to-const.
>>
>> I think it can't be pointer-to-const as emulate_load/store functions
>> wants to change PC register after MMIO access emulation is finished to
>> not trap again.
> 
> Of course, hence how I started the sentence.
> 
>>>> +    /* scause: a fetch, a load or a store/AMO guest page fault. */
>>>> +    unsigned long cause;
>>>> +    /*
>>>> +     * htinst: the trapped instruction in its transformed form, or one of the
>>>> +     * special values (zero, or a pseudoinstruction).
>>>> +     */
>>>> +    unsigned long htinst;
>>>> +    /* htval: as written by hardware; see resolve_faulting_gpa(). */
>>>> +    unsigned long htval;
>>>> +    /* stval: the guest virtual address of the faulting access. */
>>>> +    unsigned long stval;
>>>> +    /* The faulting guest physical address, filled by resolve_faulting_gpa(). */
>>>> +    paddr_t gpa;
>>>> +};
>>>> +
>>>> +/*
>>>> + * Is @htinst one of the pseudoinstructions reported for a guest page fault
>>>> + * taken on an implicit memory access done for VS-stage address translation?
>>>> + *
>>>> + * All four values are recognized regardless of the hypervisor's XLEN: the
>>>> + * width they encode is that of a VS-stage PTE, i.e. it follows the guest's
>>>> + * paging mode (4 bytes for Sv32, 8 otherwise). On RV32 the 64-bit forms
>>>> + * simply never occur.
>>>> + */
>>>> +static bool htinst_is_pseudo(unsigned long htinst)
>>>> +{
>>>> +    switch ( htinst )
>>>> +    {
>>>> +    case INSN_PSEUDO_VS_LOAD32:
>>>> +    case INSN_PSEUDO_VS_STORE32:
>>>> +    case INSN_PSEUDO_VS_LOAD64:
>>>> +    case INSN_PSEUDO_VS_STORE64:
>>>> +        return true;
>>>> +
>>>> +    default:
>>>> +        return false;
>>>> +    }
>>>> +}
>>>
>>> This feels fragile. New pseudo-insns can appear at any time. If the value as
>>> a whole is non-zero, aiui the low two bits being zero indicate a pseudo-insn.
>>> In which case enumerating pseudo-insns we are currently aware of isn't
>>> necessary.
>>
>> I will write it simpler	 then:
>>
>> /*
>>    * Is @htinst one of the special pseudoinstruction values, reported for
>> a guest
>>    * page fault taken on an implicit memory access done for VS-stage address
>>    * translation?
>>    *
>>    * It is enough to check only bits[1:0] as according to the spec:
>>    *
>>    * The value is one of the special pseudoinstructions defined later, all of
>>    * which have bits 1:0 equal to 00.
>>    */
>> static bool htinst_is_pseudo(unsigned long htinst)
>> {
>>       return htinst && ((htinst & 3) == 0);
>> }
> 
> And preferably
> 
>       return htinst && !(htinst & 3);
> 
> to be self-consistent.
> 

Good point. I will apply your suggestion.

>>>> +    /*
>>>> +     * A guest-page fault may arise due to an implicit memory access during
>>>> +     * first-stage (VS-stage) address translation, in which case a guest
>>>> +     * physical address written to htval is that of the implicit memory
>>>> +     * access that faulted - for example, the address of a VS-level page
>>>> +     * table entry that could not be read. (The guest physical address
>>>> +     * corresponding to the original virtual address is unknown when
>>>> +     * VS-stage translation fails to complete)
>>>> +     *
>>>> +     * In such cases htinst reports one of the pseudoinstructions recognized
>>>> +     * by htinst_is_pseudo(), and the fault requires separate handling (since
>>>> +     * G-stage translation failed on an unpopulated/unmapped guest physical
>>>> +     * address during a hardware page-table walk). To match bare hardware
>>>> +     * behavior, we must inject an access fault of the ORIGINAL access type
>>>> +     * (Instruction, Load, or Store/AMO) that initiated the address
>>>> +     * translation.
>>>> +     */
>>>> +    if ( htinst_is_pseudo(gf.htinst) )
>>>> +    {
>>>> +        inject_access_fault(&gf);
>>>> +
>>>> +        return;
>>>> +    }
>>>
>>> I.e. you imply that guests won't put their page tables in MMIO? That's
>>> fragile imo; I have seen OSes to use video frame buffers for all kinds
>>> of (transient) purposes, for example.
>>
>> I think it is okay for now and if it will a real use case then an update
>> of this code will be needed.
> 
> May I then ask that you leave a remark (maybe even fixme) to this effect?

Sure, I will then add the following to the comment above if ():

      * FIXME: This assumes that guest page tables never reside in an 
emulated
      * MMIO region, i.e. that an implicit access faulting at G-stage always
      * targets an unpopulated GPA. Guests may (even transiently) place page
      * tables in MMIO-backed memory, e.g. a video frame buffer. Supporting
      * that would require walking the VS-stage page tables in software,
      * accessing the PTEs (including A/D updates) through the MMIO 
handlers,
      * and then emulating the original access, instead of injecting a 
fault.

> 
>>>> +    resolve_faulting_gpa(&gf);
>>>
>>> Since the function is only a stub right now - how is one to tell whether
>>> this indeed can never fail?
>>
>> It can't be tell. But what is wrong if it could fail? (Actually with
>> current implementation introduced in later patches you can find it can
>> fail if a necessary extension or software page walk isn't introduced).
> 
> Well, quite obviously if it can fail, its return value would need checking
> here.
> 

That what I thought about after I sent my e-mail as a possible option.

I will update the prototype to:

+static int resolve_faulting_gpa(struct guest_fault *gf)
  {
-    BUG_ON("unimplemented");
+    return -EOPNOTSUPP;
  }

And handle an error code in the following way:

@@ -136,7 +144,8 @@ void handle_guest_page_fault(struct cpu_user_regs 
*regs, unsigned long cause
)
          return;
      }

-    resolve_faulting_gpa(&gf);
+    if ( rc = resolve_faulting_gpa(&gf) )
+        goto out;

      switch ( cause )
      {
@@ -163,6 +172,7 @@ void handle_guest_page_fault(struct cpu_user_regs 
*regs, unsigned long cause
)
          break;
      }

+ out:
      if ( rc )
          domain_crash(current->domain,

and then in the next patch "[PATCH v2 21/39] xen/riscv: resolve the 
faulting guest physical address" I will do "return -EOPNOTSUPP" instead 
of panic():

  -        panic("Shtvala isn't supported by h/w; s/w VS-stage walk 
required\n");
++    {
++        printk_once(XENLOG_WARNING
++                    "Shtvala isn't supported by h/w; s/w VS-stage walk 
required\n");
++        return -EOPNOTSUPP;
++    }

~ Oleksii


^ permalink raw reply	[flat|nested] 244+ messages in thread

* Re: [PATCH v2 21/39] xen/riscv: resolve the faulting guest physical address
  2026-09-09 12:04   ` Baptiste Le Duc
@ 2026-09-11 12:56     ` Oleksii Kurochko
  0 siblings, 0 replies; 244+ messages in thread
From: Oleksii Kurochko @ 2026-09-11 12:56 UTC (permalink / raw)
  To: Baptiste Le Duc
  Cc: xen-devel, Romain Caritey, Zheng Zhang, Alistair Francis,
	Connor Davis, Andrew Cooper, Anthony PERARD, Michal Orzel,
	Jan Beulich, Julien Grall, Roger Pau Monné,
	Stefano Stabellini



On 9/9/26 2:04 PM, Baptiste Le Duc wrote:
>> Take the guest physical address from htval and stval: on a guest-page fault
>> htval holds it shifted right by 2, so that an address wider than XLEN fits,
>> and stval holds the faulting guest virtual address, whose two least
>> significant bits are those of the guest physical address. The shift is done
>> on paddr_t rather than on the raw register: a guest physical address is 34
>> bits wide on RV32 with Sv32x4, so shifting an XLEN-wide value would drop its
>> top two bits.
>>
>> Those two low bits come from stval only for a fault on an explicit access.
>> Where one is taken on an implicit access made for VS-stage translation htval
>> holds the address of the VS-stage PTE which could not be read, while stval
>> still holds the guest virtual address which started the walk, and the low
>> bits of the address written to htval are zero instead. htinst tells the two
>> apart, which is what the spec points at it for.
>>
> I'd just precise (the spec is also not clear on this point though), what
> is "current XLEN" here, clearly indicate that htval holds the GPA >> 2 and

My understanding was that "current XLEN" == hypervisor XLEN as htval is 
hypervisor register and has HSXLEN size so it was okay for me to have 
just XLEN in the original commit message. But I am okay to use your 
suggestion ...

> stval holds VGA and also put the two different cases we need to
> distinguish clearly:
> ```
> Recover the guest physical address from htval and stval. On a guest-page
> fault to hypervisor, htval holds the guest physical address shifted
> right by 2, so that an address wider than HSXLEN fits, and stval holds

... here.

> the faulting guest virtual address. The shift is done on paddr_t rather
> than on the raw register: a guest physical address is 34 bits wide on
> RV32 with Sv32x4, so shifting an XLEN-wide value would drop its top two
> bits.
> 
> However, there are two cases to distinguish when recovering the
> faulting GPA:
>      - Explicit memory access: we use the two least significant bits of
>      stval, which are the same as those of the guest physical
>      address.
>      - Implicit memory access for VS-stage translation: the two least
>      significant bits of htval are zero.

The original phrasing "the two least significant bits of htval are zero" 
is inaccurate for the following reasons:
- htval holds a shifted address (GPA >> 2): The htval CSR stores the 
faulting Guest Physical Address (GPA) shifted right by 2 bits. As a 
result, the two least significant bits of htval (htval[1:0]) actually 
correspond to bits 2 and 3 of the original GPA (GPA[3:2]).

- htval bits are not guaranteed to be zero: Implicit memory accesses 
during VS-stage address translation fetch PTEs that are 4-byte aligned 
(Sv32) or 8-byte aligned (Sv39/Sv48/Sv57). Since PTEs can reside at 
offsets like 0x04, 0x08, or 0x0C, GPA (and therefore htval[1:0]) can be 
non-zero.

- What is actually guaranteed to be zero: Because all VS-stage PTE 
accesses are at least 4-byte aligned, it is the lowest two bits of the 
unshifted Guest Physical Address (GPA[1:0]) that are guaranteed to be 
00, not the low bits of htval.

So here I think we want to clarify then:
```
Implicit memory access for VS-stage translation: the two least 
significant bits of the guest physical address (GPA[1:0]) are zero
```

> 
> These two cases can be distinguished using the value provided in
> register htinst.
> ```
>>
>> stval needs no check against an ISA extension: a guest-page fault writes it
>> with the faulting guest virtual address regardless. Sstvala would not be the
>> right thing to test for either (it covers stval across every trap type
>> which writes it, a wider guarantee than what is needed here).
>>
>> htval does need one. The H extension lets an implementation write it with
>> either the faulting address or zero, so without Shtvala a zero htval cannot
>> be told apart from a genuine fault on guest physical address 0-3, and the
>> address has to be recovered by decoding the access and walking the VS-stage
>> page tables in software instead. That is left as a TODO, and until it is
>> written such hardware panics rather than acting on an address which may not
>> be the one which faulted.
>>
>> Signed-off-by: Oleksii Kurochko <oleksii.kurochko@gmail.com>
>>
>> diff --git a/xen/arch/riscv/emulate.c b/xen/arch/riscv/emulate.c
>> index f9da075104..ff530ef2df 100644
>> --- a/xen/arch/riscv/emulate.c
>> +++ b/xen/arch/riscv/emulate.c
>> @@ -9,6 +9,7 @@
>>   #include <xen/sched.h>
>>   #include <xen/types.h>
>>   
>> +#include <asm/cpufeature.h>
>>   #include <asm/csr.h>
>>   #include <asm/current.h>
>>   #include <asm/emulate.h>
>> @@ -62,10 +63,28 @@ static bool htinst_is_pseudo(unsigned long htinst)
>>       }
>>   }
>>   
>> -/* Reconstruct the guest physical address of the access which faulted. */
>> +/* Resolves the guest physical address the access faulted on into @gf->gpa. */
> Why did you change the comment apart for adding @gf->gpa? I think the
> "reconstruct" is more clear but there might be another reason why you
> changed it.
> 

I thought that it will just a little bit clearer (and anyway it should a 
part of another patch...) so I will revert the change here and go with 
"reconstruct".

Thanks.

~ Oleksii



^ permalink raw reply	[flat|nested] 244+ messages in thread

* Re: [PATCH v2 22/39] xen/riscv: add guest memory read helper
  2026-09-09 12:04   ` Baptiste Le Duc
  2026-09-10 15:19     ` Jan Beulich
@ 2026-09-11 13:06     ` Oleksii Kurochko
  2026-09-11 13:41       ` Oleksii Kurochko
  1 sibling, 1 reply; 244+ messages in thread
From: Oleksii Kurochko @ 2026-09-11 13:06 UTC (permalink / raw)
  To: Baptiste Le Duc
  Cc: xen-devel, Romain Caritey, Zheng Zhang, Alistair Francis,
	Connor Davis, Andrew Cooper, Anthony PERARD, Michal Orzel,
	Jan Beulich, Julien Grall, Roger Pau Monné,
	Stefano Stabellini



On 9/9/26 2:04 PM, Baptiste Le Duc wrote:
>> Introduce riscv_read_guest() to allow Xen to safely read guest memory
>> using HLV/HLVX instructions while reliably capturing trap context.
> 
>> This is required for instruction fetch emulation and MMIO decoding, where
>> Xen must inspect guest memory that may not be directly accessible and may
>> fault.
>>
>> The implementation is based on kvm_riscv_vcpu_unpriv_read() from Linux,
>> with one deviation: the hlv/hlvx instructions translate the guest address
>> through the live vsatp/hgatp CSRs, i.e. through the address space of the
>> currently running vCPU, so the function can only be called safely for
>> current. Instead of taking a struct vcpu argument, it always operates on
>> current directly.
>>
>> Signed-off-by: Oleksii Kurochko <oleksii.kurochko@gmail.com>
>>
>> diff --git a/xen/arch/riscv/guestcopy.c b/xen/arch/riscv/guestcopy.c
>> index 8a89212e0b..b2327822ac 100644
>> --- a/xen/arch/riscv/guestcopy.c
>> +++ b/xen/arch/riscv/guestcopy.c
>> @@ -6,6 +6,7 @@
>>   #include <xen/string.h>
>>   
>>   #include <asm/guest_access.h>
>> +#include <asm/traps.h>
>>   
>>   #define COPY_from_guest     0U
>>   #define COPY_to_guest       BIT(0, U)
>> @@ -114,3 +115,89 @@ unsigned long copy_to_guest_phys(struct domain *d, paddr_t gpa, void *buf,
>>       return copy_guest(buf, gpa, len, GPA_INFO(d),
>>                         COPY_to_guest | COPY_gpa);
>>   }
>> +
>> +/*
>> + * Read machine word from guest memory
>> + *
>> + * @guest_addr: Guest address to read
>> + * @read_insn: Flag representing whether we are reading instruction
>> + * @trap: Output pointer to trap details if something went wrong during read
>> + *
>> + * The hlv/hlvx instructions translate guest_addr through the live
>> + * vsatp/hgatp CSRs, so the read is only meaningful for the address
>> + * space of the currently running vCPU.
>> + *
>> + * At most two halfwords are fetched when @read_insn is true, i.e. encodings
>> + * wider than 32 bits are not supported. Such an encoding cannot be completed
>> + * by calling this function again at @guest_addr + 4: the length check is
>> + * applied to the first halfword read, which would then be a continuation of
>> + * the instruction rather than its opcode. It is up to the caller to reject
>> + * anything that is neither a 16- nor a 32-bit encoding.
>> + */
>> +unsigned long riscv_read_guest(unsigned long guest_addr, bool read_insn,
>> +                               struct trap_info *trap)
> Nit: every other function in this file / declared in this header
> (raw_copy_from_guest, copy_to_guest_phys, ...) has no riscv_ prefix. Why
> does this one get it?

Agree not to much sense. I will drop it.

>> +{
>> +    /*
>> +     * Poison the result: if the very first access faults, the fixup skips
>> +     * over the loads without writing it. Callers must check trap->scause.
>> +     */
>> +    unsigned long val = ~0UL, tmp;
>>
> The actual "did a trap happen" contract callers rely on is
> trap->scause == 0. If the very first access faults, fixup_exception() will
> write scause accordingly to a non-zero value, but nothing in this function
> clears trap->scause on the success path.

It is because caller is expected to zero-fill trap as it is happening 
now. I will mention that explicitly. I will do the following:

- * @trap: Output pointer to trap details if something went wrong during 
read
+ * @trap: Output pointer to trap details if something went wrong during 
read.
+ *        It must be zero-initialised by the caller: it is written only 
when
+ *        an access faults, so trap->scause == 0 on return is what 
tells the
+ *        caller that the read succeeded.


>> +
>> +    /*
>> +     * hlv/hlvx use hstatus.SPVP for the privilege of the access, and the
>> +     * live vsatp/hgatp for the translation. Xen never installs a value of
>> +     * its own in hstatus (it is only saved on trap entry and restored
>> +     * before sret) and it doesn't reschedule before returning to the
>> +     * guest, so all three still belong to the vCPU which trapped.
>> +     *
>> +     * Check the saved copy rather than the live CSR: a nested trap taken
>> +     * from HS-mode clears hstatus.SPV in the CSR (but leaves SPVP alone).
>> +     */
>> +    ASSERT(vcpu_guest_cpu_user_regs(current)->hstatus & HSTATUS_SPV);
> Just to understand, what is the aim of this check? Is it to be sure this
> function has been called during a guest fault and not a nested HS fault?
> 

Yes.

~ Oleksii




^ permalink raw reply	[flat|nested] 244+ messages in thread

* Re: [PATCH v2 22/39] xen/riscv: add guest memory read helper
  2026-09-11 13:06     ` Oleksii Kurochko
@ 2026-09-11 13:41       ` Oleksii Kurochko
  2026-09-11 13:47         ` Jan Beulich
  0 siblings, 1 reply; 244+ messages in thread
From: Oleksii Kurochko @ 2026-09-11 13:41 UTC (permalink / raw)
  To: Baptiste Le Duc
  Cc: xen-devel, Romain Caritey, Zheng Zhang, Alistair Francis,
	Connor Davis, Andrew Cooper, Anthony PERARD, Michal Orzel,
	Jan Beulich, Julien Grall, Roger Pau Monné,
	Stefano Stabellini



On 9/11/26 3:06 PM, Oleksii Kurochko wrote:
> On 9/9/26 2:04 PM, Baptiste Le Duc wrote:
>>> Introduce riscv_read_guest() to allow Xen to safely read guest memory
>>> using HLV/HLVX instructions while reliably capturing trap context.
>>
>>> This is required for instruction fetch emulation and MMIO decoding, 
>>> where
>>> Xen must inspect guest memory that may not be directly accessible and 
>>> may
>>> fault.
>>>
>>> The implementation is based on kvm_riscv_vcpu_unpriv_read() from Linux,
>>> with one deviation: the hlv/hlvx instructions translate the guest 
>>> address
>>> through the live vsatp/hgatp CSRs, i.e. through the address space of the
>>> currently running vCPU, so the function can only be called safely for
>>> current. Instead of taking a struct vcpu argument, it always operates on
>>> current directly.
>>>
>>> Signed-off-by: Oleksii Kurochko <oleksii.kurochko@gmail.com>
>>>
>>> diff --git a/xen/arch/riscv/guestcopy.c b/xen/arch/riscv/guestcopy.c
>>> index 8a89212e0b..b2327822ac 100644
>>> --- a/xen/arch/riscv/guestcopy.c
>>> +++ b/xen/arch/riscv/guestcopy.c
>>> @@ -6,6 +6,7 @@
>>>   #include <xen/string.h>
>>>   #include <asm/guest_access.h>
>>> +#include <asm/traps.h>
>>>   #define COPY_from_guest     0U
>>>   #define COPY_to_guest       BIT(0, U)
>>> @@ -114,3 +115,89 @@ unsigned long copy_to_guest_phys(struct domain 
>>> *d, paddr_t gpa, void *buf,
>>>       return copy_guest(buf, gpa, len, GPA_INFO(d),
>>>                         COPY_to_guest | COPY_gpa);
>>>   }
>>> +
>>> +/*
>>> + * Read machine word from guest memory
>>> + *
>>> + * @guest_addr: Guest address to read
>>> + * @read_insn: Flag representing whether we are reading instruction
>>> + * @trap: Output pointer to trap details if something went wrong 
>>> during read
>>> + *
>>> + * The hlv/hlvx instructions translate guest_addr through the live
>>> + * vsatp/hgatp CSRs, so the read is only meaningful for the address
>>> + * space of the currently running vCPU.
>>> + *
>>> + * At most two halfwords are fetched when @read_insn is true, i.e. 
>>> encodings
>>> + * wider than 32 bits are not supported. Such an encoding cannot be 
>>> completed
>>> + * by calling this function again at @guest_addr + 4: the length 
>>> check is
>>> + * applied to the first halfword read, which would then be a 
>>> continuation of
>>> + * the instruction rather than its opcode. It is up to the caller to 
>>> reject
>>> + * anything that is neither a 16- nor a 32-bit encoding.
>>> + */
>>> +unsigned long riscv_read_guest(unsigned long guest_addr, bool 
>>> read_insn,
>>> +                               struct trap_info *trap)
>> Nit: every other function in this file / declared in this header
>> (raw_copy_from_guest, copy_to_guest_phys, ...) has no riscv_ prefix. Why
>> does this one get it?
> 
> Agree not to much sense. I will drop it.

Probably we still want to have riscv_ prefix to show that this 
read_guest() is specific to RISC-V but others (raw_copy_from_guest, 
copy_to_guest_phys, ...) could be used in Xen common code too.

So I think we could keep riscv_ prefix.

~ Oleksii


^ permalink raw reply	[flat|nested] 244+ messages in thread

* Re: [PATCH v2 22/39] xen/riscv: add guest memory read helper
  2026-09-11 13:41       ` Oleksii Kurochko
@ 2026-09-11 13:47         ` Jan Beulich
  2026-09-11 13:50           ` Oleksii Kurochko
  0 siblings, 1 reply; 244+ messages in thread
From: Jan Beulich @ 2026-09-11 13:47 UTC (permalink / raw)
  To: Oleksii Kurochko
  Cc: xen-devel, Romain Caritey, Zheng Zhang, Alistair Francis,
	Connor Davis, Andrew Cooper, Anthony PERARD, Michal Orzel,
	Julien Grall, Roger Pau Monné, Stefano Stabellini,
	Baptiste Le Duc

On 11.09.2026 15:41, Oleksii Kurochko wrote:
> On 9/11/26 3:06 PM, Oleksii Kurochko wrote:
>> On 9/9/26 2:04 PM, Baptiste Le Duc wrote:
>>>> Introduce riscv_read_guest() to allow Xen to safely read guest memory
>>>> using HLV/HLVX instructions while reliably capturing trap context.
>>>
>>>> This is required for instruction fetch emulation and MMIO decoding, 
>>>> where
>>>> Xen must inspect guest memory that may not be directly accessible and 
>>>> may
>>>> fault.
>>>>
>>>> The implementation is based on kvm_riscv_vcpu_unpriv_read() from Linux,
>>>> with one deviation: the hlv/hlvx instructions translate the guest 
>>>> address
>>>> through the live vsatp/hgatp CSRs, i.e. through the address space of the
>>>> currently running vCPU, so the function can only be called safely for
>>>> current. Instead of taking a struct vcpu argument, it always operates on
>>>> current directly.
>>>>
>>>> Signed-off-by: Oleksii Kurochko <oleksii.kurochko@gmail.com>
>>>>
>>>> diff --git a/xen/arch/riscv/guestcopy.c b/xen/arch/riscv/guestcopy.c
>>>> index 8a89212e0b..b2327822ac 100644
>>>> --- a/xen/arch/riscv/guestcopy.c
>>>> +++ b/xen/arch/riscv/guestcopy.c
>>>> @@ -6,6 +6,7 @@
>>>>   #include <xen/string.h>
>>>>   #include <asm/guest_access.h>
>>>> +#include <asm/traps.h>
>>>>   #define COPY_from_guest     0U
>>>>   #define COPY_to_guest       BIT(0, U)
>>>> @@ -114,3 +115,89 @@ unsigned long copy_to_guest_phys(struct domain 
>>>> *d, paddr_t gpa, void *buf,
>>>>       return copy_guest(buf, gpa, len, GPA_INFO(d),
>>>>                         COPY_to_guest | COPY_gpa);
>>>>   }
>>>> +
>>>> +/*
>>>> + * Read machine word from guest memory
>>>> + *
>>>> + * @guest_addr: Guest address to read
>>>> + * @read_insn: Flag representing whether we are reading instruction
>>>> + * @trap: Output pointer to trap details if something went wrong 
>>>> during read
>>>> + *
>>>> + * The hlv/hlvx instructions translate guest_addr through the live
>>>> + * vsatp/hgatp CSRs, so the read is only meaningful for the address
>>>> + * space of the currently running vCPU.
>>>> + *
>>>> + * At most two halfwords are fetched when @read_insn is true, i.e. 
>>>> encodings
>>>> + * wider than 32 bits are not supported. Such an encoding cannot be 
>>>> completed
>>>> + * by calling this function again at @guest_addr + 4: the length 
>>>> check is
>>>> + * applied to the first halfword read, which would then be a 
>>>> continuation of
>>>> + * the instruction rather than its opcode. It is up to the caller to 
>>>> reject
>>>> + * anything that is neither a 16- nor a 32-bit encoding.
>>>> + */
>>>> +unsigned long riscv_read_guest(unsigned long guest_addr, bool 
>>>> read_insn,
>>>> +                               struct trap_info *trap)
>>> Nit: every other function in this file / declared in this header
>>> (raw_copy_from_guest, copy_to_guest_phys, ...) has no riscv_ prefix. Why
>>> does this one get it?
>>
>> Agree not to much sense. I will drop it.
> 
> Probably we still want to have riscv_ prefix to show that this 
> read_guest() is specific to RISC-V but others (raw_copy_from_guest, 
> copy_to_guest_phys, ...) could be used in Xen common code too.
> 
> So I think we could keep riscv_ prefix.

Please may I suggest to avoid unnecessary prefixes?

Jan


^ permalink raw reply	[flat|nested] 244+ messages in thread

* Re: [PATCH v2 22/39] xen/riscv: add guest memory read helper
  2026-09-11 13:47         ` Jan Beulich
@ 2026-09-11 13:50           ` Oleksii Kurochko
  0 siblings, 0 replies; 244+ messages in thread
From: Oleksii Kurochko @ 2026-09-11 13:50 UTC (permalink / raw)
  To: Jan Beulich
  Cc: xen-devel, Romain Caritey, Zheng Zhang, Alistair Francis,
	Connor Davis, Andrew Cooper, Anthony PERARD, Michal Orzel,
	Julien Grall, Roger Pau Monné, Stefano Stabellini,
	Baptiste Le Duc



On 9/11/26 3:47 PM, Jan Beulich wrote:
> On 11.09.2026 15:41, Oleksii Kurochko wrote:
>> On 9/11/26 3:06 PM, Oleksii Kurochko wrote:
>>> On 9/9/26 2:04 PM, Baptiste Le Duc wrote:
>>>>> Introduce riscv_read_guest() to allow Xen to safely read guest memory
>>>>> using HLV/HLVX instructions while reliably capturing trap context.
>>>>
>>>>> This is required for instruction fetch emulation and MMIO decoding,
>>>>> where
>>>>> Xen must inspect guest memory that may not be directly accessible and
>>>>> may
>>>>> fault.
>>>>>
>>>>> The implementation is based on kvm_riscv_vcpu_unpriv_read() from Linux,
>>>>> with one deviation: the hlv/hlvx instructions translate the guest
>>>>> address
>>>>> through the live vsatp/hgatp CSRs, i.e. through the address space of the
>>>>> currently running vCPU, so the function can only be called safely for
>>>>> current. Instead of taking a struct vcpu argument, it always operates on
>>>>> current directly.
>>>>>
>>>>> Signed-off-by: Oleksii Kurochko <oleksii.kurochko@gmail.com>
>>>>>
>>>>> diff --git a/xen/arch/riscv/guestcopy.c b/xen/arch/riscv/guestcopy.c
>>>>> index 8a89212e0b..b2327822ac 100644
>>>>> --- a/xen/arch/riscv/guestcopy.c
>>>>> +++ b/xen/arch/riscv/guestcopy.c
>>>>> @@ -6,6 +6,7 @@
>>>>>    #include <xen/string.h>
>>>>>    #include <asm/guest_access.h>
>>>>> +#include <asm/traps.h>
>>>>>    #define COPY_from_guest     0U
>>>>>    #define COPY_to_guest       BIT(0, U)
>>>>> @@ -114,3 +115,89 @@ unsigned long copy_to_guest_phys(struct domain
>>>>> *d, paddr_t gpa, void *buf,
>>>>>        return copy_guest(buf, gpa, len, GPA_INFO(d),
>>>>>                          COPY_to_guest | COPY_gpa);
>>>>>    }
>>>>> +
>>>>> +/*
>>>>> + * Read machine word from guest memory
>>>>> + *
>>>>> + * @guest_addr: Guest address to read
>>>>> + * @read_insn: Flag representing whether we are reading instruction
>>>>> + * @trap: Output pointer to trap details if something went wrong
>>>>> during read
>>>>> + *
>>>>> + * The hlv/hlvx instructions translate guest_addr through the live
>>>>> + * vsatp/hgatp CSRs, so the read is only meaningful for the address
>>>>> + * space of the currently running vCPU.
>>>>> + *
>>>>> + * At most two halfwords are fetched when @read_insn is true, i.e.
>>>>> encodings
>>>>> + * wider than 32 bits are not supported. Such an encoding cannot be
>>>>> completed
>>>>> + * by calling this function again at @guest_addr + 4: the length
>>>>> check is
>>>>> + * applied to the first halfword read, which would then be a
>>>>> continuation of
>>>>> + * the instruction rather than its opcode. It is up to the caller to
>>>>> reject
>>>>> + * anything that is neither a 16- nor a 32-bit encoding.
>>>>> + */
>>>>> +unsigned long riscv_read_guest(unsigned long guest_addr, bool
>>>>> read_insn,
>>>>> +                               struct trap_info *trap)
>>>> Nit: every other function in this file / declared in this header
>>>> (raw_copy_from_guest, copy_to_guest_phys, ...) has no riscv_ prefix. Why
>>>> does this one get it?
>>>
>>> Agree not to much sense. I will drop it.
>>
>> Probably we still want to have riscv_ prefix to show that this
>> read_guest() is specific to RISC-V but others (raw_copy_from_guest,
>> copy_to_guest_phys, ...) could be used in Xen common code too.
>>
>> So I think we could keep riscv_ prefix.
> 
> Please may I suggest to avoid unnecessary prefixes?

Sure then I will drop it as originally suggested.

~ Oleksii


^ permalink raw reply	[flat|nested] 244+ messages in thread

* Re: [PATCH v2 22/39] xen/riscv: add guest memory read helper
  2026-09-10 15:28   ` Jan Beulich
@ 2026-09-11 13:57     ` Oleksii Kurochko
  2026-09-11 14:00       ` Jan Beulich
  0 siblings, 1 reply; 244+ messages in thread
From: Oleksii Kurochko @ 2026-09-11 13:57 UTC (permalink / raw)
  To: Jan Beulich
  Cc: Romain Caritey, Baptiste Le Duc, Zheng Zhang, Alistair Francis,
	Connor Davis, Andrew Cooper, Anthony PERARD, Michal Orzel,
	Julien Grall, Roger Pau Monné, Stefano Stabellini, xen-devel



On 9/10/26 5:28 PM, Jan Beulich wrote:
> On 27.08.2026 17:21, Oleksii Kurochko wrote:
>> --- a/xen/arch/riscv/guestcopy.c
>> +++ b/xen/arch/riscv/guestcopy.c
>> @@ -6,6 +6,7 @@
>>   #include <xen/string.h>
>>   
>>   #include <asm/guest_access.h>
>> +#include <asm/traps.h>
>>   
>>   #define COPY_from_guest     0U
>>   #define COPY_to_guest       BIT(0, U)
>> @@ -114,3 +115,89 @@ unsigned long copy_to_guest_phys(struct domain *d, paddr_t gpa, void *buf,
>>       return copy_guest(buf, gpa, len, GPA_INFO(d),
>>                         COPY_to_guest | COPY_gpa);
>>   }
>> +
>> +/*
>> + * Read machine word from guest memory
>> + *
>> + * @guest_addr: Guest address to read
>> + * @read_insn: Flag representing whether we are reading instruction
>> + * @trap: Output pointer to trap details if something went wrong during read
>> + *
>> + * The hlv/hlvx instructions translate guest_addr through the live
>> + * vsatp/hgatp CSRs, so the read is only meaningful for the address
>> + * space of the currently running vCPU.
>> + *
>> + * At most two halfwords are fetched when @read_insn is true, i.e. encodings
>> + * wider than 32 bits are not supported. Such an encoding cannot be completed
>> + * by calling this function again at @guest_addr + 4: the length check is
>> + * applied to the first halfword read, which would then be a continuation of
>> + * the instruction rather than its opcode. It is up to the caller to reject
>> + * anything that is neither a 16- nor a 32-bit encoding.
>> + */
>> +unsigned long riscv_read_guest(unsigned long guest_addr, bool read_insn,
>> +                               struct trap_info *trap)
>> +{
>> +    /*
>> +     * Poison the result: if the very first access faults, the fixup skips
>> +     * over the loads without writing it. Callers must check trap->scause.
>> +     */
>> +    unsigned long val = ~0UL, tmp;
>> +
>> +    /*
>> +     * hlv/hlvx use hstatus.SPVP for the privilege of the access, and the
>> +     * live vsatp/hgatp for the translation. Xen never installs a value of
>> +     * its own in hstatus (it is only saved on trap entry and restored
>> +     * before sret) and it doesn't reschedule before returning to the
>> +     * guest, so all three still belong to the vCPU which trapped.
>> +     *
>> +     * Check the saved copy rather than the live CSR: a nested trap taken
>> +     * from HS-mode clears hstatus.SPV in the CSR (but leaves SPVP alone).
>> +     */
>> +    ASSERT(vcpu_guest_cpu_user_regs(current)->hstatus & HSTATUS_SPV);
> 
> The first paragraph talks of just hstatus.SPVP. The second paragraph then
> starting "Check ..." means that still refers to hstatus.SPVP, when - aiui -
> hstatus.SPV is meant.
> 
> Furthermore, instead of special casing nested faults here, but otherwise
> saying "Xen doesn't modify", wouldn't it be better to word things such
> that they remain correct if Xen ends up having a need to touch some other
> part of hstatus (including, potentially, SPV)? IOW - I think it is natural
> that the original guest value is checked.

You are right:
1. The transition between the two paragraphs was confusing regarding 
SPVP vs SPV. I will update the comment to clearly distinguish that 
hlv/hlvx rely on SPVP, whereas the ASSERT verifies SPV.

2. Re-phrasing this around the invariant that vcpu_guest_cpu_user_regs 
represents the guest state at trap entry makes much more sense and is 
future-proof against any potential changes to live hstatus manipulation 
in Xen.

3. Regarding the value of the ASSERT: yes, for any valid guest trap 
frame SPV must be set. The ASSERT serves as a sanity check to ensure 
riscv_read_guest() is never accidentally invoked outside a valid guest 
vCPU trap context.I will update the comment as follows in v3:

/*
      * hlv/hlvx instructions use the live hstatus.SPVP for access 
privilege,
      * and live vsatp/hgatp for translation. Since Xen does not reschedule
      * before returning to the guest, these CSRs still belong to current.
      *
      * Check SPV in the saved guest registers rather than the live CSR:
      * the saved copy reflects the guest virtualization mode (V=1) at 
the time
      * of trap entry, which remains invariant even if nested traps or 
HS-mode
      * execution modify the live CSR.
      */
     ASSERT(vcpu_guest_cpu_user_regs(current)->hstatus & HSTATUS_SPV);

> Question being of how much value
> that checking is: vcpu_guest_cpu_user_regs(current)->hstatus can't possibly
> have SPV clear, can it? Only nested exception frames could.

Given that vcpu_guest_cpu_user_regs(current)->hstatus will always have 
SPV set for any valid guest trap frame, the ASSERT is purely a defensive 
sanity check to ensure riscv_read_guest() is never called outside a 
guest trap context. Would you prefer to keep this defensive ASSERT (with 
the updated comment), or drop it as redundant?

Thanks.

~ Oleksii



^ permalink raw reply	[flat|nested] 244+ messages in thread

* Re: [PATCH v2 22/39] xen/riscv: add guest memory read helper
  2026-09-11 13:57     ` Oleksii Kurochko
@ 2026-09-11 14:00       ` Jan Beulich
  2026-09-11 14:29         ` Oleksii Kurochko
  0 siblings, 1 reply; 244+ messages in thread
From: Jan Beulich @ 2026-09-11 14:00 UTC (permalink / raw)
  To: Oleksii Kurochko
  Cc: Romain Caritey, Baptiste Le Duc, Zheng Zhang, Alistair Francis,
	Connor Davis, Andrew Cooper, Anthony PERARD, Michal Orzel,
	Julien Grall, Roger Pau Monné, Stefano Stabellini, xen-devel

On 11.09.2026 15:57, Oleksii Kurochko wrote:
> 
> 
> On 9/10/26 5:28 PM, Jan Beulich wrote:
>> On 27.08.2026 17:21, Oleksii Kurochko wrote:
>>> @@ -114,3 +115,89 @@ unsigned long copy_to_guest_phys(struct domain *d, paddr_t gpa, void *buf,
>>>       return copy_guest(buf, gpa, len, GPA_INFO(d),
>>>                         COPY_to_guest | COPY_gpa);
>>>   }
>>> +
>>> +/*
>>> + * Read machine word from guest memory
>>> + *
>>> + * @guest_addr: Guest address to read
>>> + * @read_insn: Flag representing whether we are reading instruction
>>> + * @trap: Output pointer to trap details if something went wrong during read
>>> + *
>>> + * The hlv/hlvx instructions translate guest_addr through the live
>>> + * vsatp/hgatp CSRs, so the read is only meaningful for the address
>>> + * space of the currently running vCPU.
>>> + *
>>> + * At most two halfwords are fetched when @read_insn is true, i.e. encodings
>>> + * wider than 32 bits are not supported. Such an encoding cannot be completed
>>> + * by calling this function again at @guest_addr + 4: the length check is
>>> + * applied to the first halfword read, which would then be a continuation of
>>> + * the instruction rather than its opcode. It is up to the caller to reject
>>> + * anything that is neither a 16- nor a 32-bit encoding.
>>> + */
>>> +unsigned long riscv_read_guest(unsigned long guest_addr, bool read_insn,
>>> +                               struct trap_info *trap)
>>> +{
>>> +    /*
>>> +     * Poison the result: if the very first access faults, the fixup skips
>>> +     * over the loads without writing it. Callers must check trap->scause.
>>> +     */
>>> +    unsigned long val = ~0UL, tmp;
>>> +
>>> +    /*
>>> +     * hlv/hlvx use hstatus.SPVP for the privilege of the access, and the
>>> +     * live vsatp/hgatp for the translation. Xen never installs a value of
>>> +     * its own in hstatus (it is only saved on trap entry and restored
>>> +     * before sret) and it doesn't reschedule before returning to the
>>> +     * guest, so all three still belong to the vCPU which trapped.
>>> +     *
>>> +     * Check the saved copy rather than the live CSR: a nested trap taken
>>> +     * from HS-mode clears hstatus.SPV in the CSR (but leaves SPVP alone).
>>> +     */
>>> +    ASSERT(vcpu_guest_cpu_user_regs(current)->hstatus & HSTATUS_SPV);
>>[...]
>> Question being of how much value
>> that checking is: vcpu_guest_cpu_user_regs(current)->hstatus can't possibly
>> have SPV clear, can it? Only nested exception frames could.
> 
> Given that vcpu_guest_cpu_user_regs(current)->hstatus will always have 
> SPV set for any valid guest trap frame, the ASSERT is purely a defensive 
> sanity check to ensure riscv_read_guest() is never called outside a 
> guest trap context.

It is not, afaict: vcpu_guest_cpu_user_regs(current) will give you the guest
frame no matter what context you're in. For what you want, you'd need to
pass struct cpu_user_regs * into here.

Jan

> Would you prefer to keep this defensive ASSERT (with 
> the updated comment), or drop it as redundant?
> 
> Thanks.
> 
> ~ Oleksii
> 



^ permalink raw reply	[flat|nested] 244+ messages in thread

* Re: [PATCH v2 22/39] xen/riscv: add guest memory read helper
  2026-09-11 14:00       ` Jan Beulich
@ 2026-09-11 14:29         ` Oleksii Kurochko
  0 siblings, 0 replies; 244+ messages in thread
From: Oleksii Kurochko @ 2026-09-11 14:29 UTC (permalink / raw)
  To: Jan Beulich
  Cc: Romain Caritey, Baptiste Le Duc, Zheng Zhang, Alistair Francis,
	Connor Davis, Andrew Cooper, Anthony PERARD, Michal Orzel,
	Julien Grall, Roger Pau Monné, Stefano Stabellini, xen-devel



On 9/11/26 4:00 PM, Jan Beulich wrote:
> On 11.09.2026 15:57, Oleksii Kurochko wrote:
>>
>>
>> On 9/10/26 5:28 PM, Jan Beulich wrote:
>>> On 27.08.2026 17:21, Oleksii Kurochko wrote:
>>>> @@ -114,3 +115,89 @@ unsigned long copy_to_guest_phys(struct domain *d, paddr_t gpa, void *buf,
>>>>        return copy_guest(buf, gpa, len, GPA_INFO(d),
>>>>                          COPY_to_guest | COPY_gpa);
>>>>    }
>>>> +
>>>> +/*
>>>> + * Read machine word from guest memory
>>>> + *
>>>> + * @guest_addr: Guest address to read
>>>> + * @read_insn: Flag representing whether we are reading instruction
>>>> + * @trap: Output pointer to trap details if something went wrong during read
>>>> + *
>>>> + * The hlv/hlvx instructions translate guest_addr through the live
>>>> + * vsatp/hgatp CSRs, so the read is only meaningful for the address
>>>> + * space of the currently running vCPU.
>>>> + *
>>>> + * At most two halfwords are fetched when @read_insn is true, i.e. encodings
>>>> + * wider than 32 bits are not supported. Such an encoding cannot be completed
>>>> + * by calling this function again at @guest_addr + 4: the length check is
>>>> + * applied to the first halfword read, which would then be a continuation of
>>>> + * the instruction rather than its opcode. It is up to the caller to reject
>>>> + * anything that is neither a 16- nor a 32-bit encoding.
>>>> + */
>>>> +unsigned long riscv_read_guest(unsigned long guest_addr, bool read_insn,
>>>> +                               struct trap_info *trap)
>>>> +{
>>>> +    /*
>>>> +     * Poison the result: if the very first access faults, the fixup skips
>>>> +     * over the loads without writing it. Callers must check trap->scause.
>>>> +     */
>>>> +    unsigned long val = ~0UL, tmp;
>>>> +
>>>> +    /*
>>>> +     * hlv/hlvx use hstatus.SPVP for the privilege of the access, and the
>>>> +     * live vsatp/hgatp for the translation. Xen never installs a value of
>>>> +     * its own in hstatus (it is only saved on trap entry and restored
>>>> +     * before sret) and it doesn't reschedule before returning to the
>>>> +     * guest, so all three still belong to the vCPU which trapped.
>>>> +     *
>>>> +     * Check the saved copy rather than the live CSR: a nested trap taken
>>>> +     * from HS-mode clears hstatus.SPV in the CSR (but leaves SPVP alone).
>>>> +     */
>>>> +    ASSERT(vcpu_guest_cpu_user_regs(current)->hstatus & HSTATUS_SPV);
>>> [...]
>>> Question being of how much value
>>> that checking is: vcpu_guest_cpu_user_regs(current)->hstatus can't possibly
>>> have SPV clear, can it? Only nested exception frames could.
>>
>> Given that vcpu_guest_cpu_user_regs(current)->hstatus will always have
>> SPV set for any valid guest trap frame, the ASSERT is purely a defensive
>> sanity check to ensure riscv_read_guest() is never called outside a
>> guest trap context.
> 
> It is not, afaict: vcpu_guest_cpu_user_regs(current) will give you the guest
> frame no matter what context you're in. For what you want, you'd need to
> pass struct cpu_user_regs * into here.

Agree, struct cpu_user_regs * will be required.

But I am looking at how read_guest() is used and it shouldn't be used 
when nested HS trap happen never and it seems like it is too much to 
pass struct cpu_user_regs * just to check that.

I think it is enough just to have:

     case CAUSE_FETCH_GUEST_PAGE_FAULT:
     case CAUSE_LOAD_GUEST_PAGE_FAULT:
     case CAUSE_STORE_GUEST_PAGE_FAULT:
         /*
          * A guest page fault taken in Xen context comes from an hlv/hlvx
          * access made on a vCPU's behalf and is dealt with by the
          * fixup_exception() above, so only a guest can get here.
          */
         BUG_ON(!from_guest);

where we already checked that when riscv_read_guest() is used then it 
isn't nested HS trap.

So it looks like we could just drop the ASSERT() and probably update the 
comment above function and mention that riscv_read_guest() shouldn't be 
called in nested HS trap.

~ Oleksii


^ permalink raw reply	[flat|nested] 244+ messages in thread

* Re: [PATCH v2 09/39] xen/riscv: implement virtual APLIC MMIO emulation
  2026-09-10 14:24         ` Oleksii Kurochko
@ 2026-09-12  8:50           ` SeungJu Cheon
  2026-09-22  8:50             ` Oleksii Kurochko
  0 siblings, 1 reply; 244+ messages in thread
From: SeungJu Cheon @ 2026-09-12  8:50 UTC (permalink / raw)
  To: Oleksii Kurochko
  Cc: Jan Beulich, Romain Caritey, Baptiste Le Duc, Zheng Zhang,
	Alistair Francis, Connor Davis, Andrew Cooper, Anthony PERARD,
	Michal Orzel, Julien Grall, Roger Pau Monné,
	Stefano Stabellini, xen-devel

On Thu, Sep 10, 2026 at 04:24:06PM +0200, Oleksii Kurochko wrote:
[...]
> In v3 I'll (a) stop using ->processor and take the (guest_file_id,
> vsfile_cpu) pair, which imsic_update_state() updates atomically under
> vsfile_lock, and (b) do the snapshot plus the h/w TARGET write under
> aplic.lock, which aplic_reconfigure_target() also holds. As
> imsic_update_state() completes before aplic_reconfigure_target() (also that
> could be checked in this patch series and is introduced a little bit later.
> Probably I have to re-order some patches again) takes the lock, the emulated
> write either happens before the scan (and gets fixed up, or skipped as
> already correct) or after it (and sees the new location).
> 
> Any better option I have now?

Unless I am missing something, the snapshot also needs to handle the
case where the target vCPU has not been attached yet:
vcpu_guest_file_id() returns zero until the vCPU has gone through
imsic_vsfile_attach(), i.e. until it is scheduled for the first time,
and vsfile_cpu is NR_CPUS until then.

With the current code, a write targeting such a vCPU makes
aplic_msi_target_gen() program Guest Index 0 into the physical APLIC
target register. According to AIA section 4.5.16, Guest Index 0 selects
the hart's supervisor-level interrupt file rather than a VS-level guest
interrupt file. Could this cause the MSI to be delivered to Xen's own
interrupt file with the EIID supplied by the guest?

I also could not find where such a target would be updated once the
VS-file is attached. imsic_migrate_vcpu() reprograms the relevant
targets during migration, but the initial imsic_vsfile_attach() path
does not appear to replay targets which were written before the
attachment.

Whether a write targeting an unattached vCPU should be supported seems
like a separate question. Independently of that choice, would it make
sense to avoid programming the physical TARGET register while
guest_file_id is zero? The virtual target could either be rejected, or
retained in the shadow target[] and programmed once the VS-file is
attached.

Thanks,
SeungJu


^ permalink raw reply	[flat|nested] 244+ messages in thread

* Re: [PATCH v2 24/39] xen/riscv: add helpers for decoding a trapped load or store
  2026-08-27 15:21 ` [PATCH v2 24/39] xen/riscv: add helpers for decoding a trapped load or store Oleksii Kurochko
@ 2026-09-14 11:03   ` Jan Beulich
  2026-09-14 15:57     ` Oleksii Kurochko
  2026-09-18  8:44   ` Baptiste Le Duc
  1 sibling, 1 reply; 244+ messages in thread
From: Jan Beulich @ 2026-09-14 11:03 UTC (permalink / raw)
  To: Oleksii Kurochko
  Cc: Romain Caritey, Baptiste Le Duc, Zheng Zhang, Alistair Francis,
	Connor Davis, Andrew Cooper, Anthony PERARD, Michal Orzel,
	Julien Grall, Roger Pau Monné, Stefano Stabellini, xen-devel

On 27.08.2026 17:21, Oleksii Kurochko wrote:
> emulate_load() and emulate_store() will both need to obtain the
> instruction which caused a guest MMIO trap, decode it, and locate the
> register operand it names. Add what the two share, ahead of either of
> them being implemented: struct decoded_insn, insn_fetch_faulted(),
> decode_ldst_insn(), guest_xlen(), guest_gpr() and advance_pc().
> 
> The mask/match chain is adapted from Linux's KVM RISC-V implementation.
> 
> Nothing calls any of this yet, so tag the functions __maybe_unused to
> keep the build going; the tags go away once emulate_load() and
> emulate_store() gain their bodies later.

That'll be a lot of churn to drop those __maybe_unused again. As this
is merely transient, did you consider putting

   (void)is_load_guest_page_fault;

etc in e.g. emulate_load()?

> @@ -13,9 +14,29 @@
>  #include <asm/csr.h>
>  #include <asm/current.h>
>  #include <asm/emulate.h>
> +#include <asm/guest_access.h>
> +#include <asm/processor.h>
>  #include <asm/riscv_encoding.h>
>  #include <asm/traps.h>
>  
> +/*
> + * Determine the trapped load or store instruction which caused a guest MMIO
> + * trap.
> + */
> +struct decoded_insn {
> +    /* The instruction itself, and its length in bytes. */
> +    unsigned long insn;
> +    unsigned int insn_len;
> +    /* Width of the memory access, in bytes. */
> +    unsigned int len;
> +    /* Number of the register operand: rd for a load, rs2 for a store. */
> +    unsigned int reg;
> +    /* The access is a store rather than a load. */
> +    bool is_write;
> +    /* The load zero-extends its result rather than sign-extending it. */
> +    bool is_unsigned;
> +};

I wonder how efficient this is. With use of bitfield the size of this struct
can likely be more than halved. With suitable choice of widths this may not
even cause significantly worse generated code.

One thing in any event: Why would the insn field need to be wider than 32
bits?

> @@ -39,6 +60,71 @@ struct guest_fault {
>      paddr_t gpa;
>  };
>  
> +static bool is_load_guest_page_fault(unsigned long scause)
> +{
> +    return scause == CAUSE_LOAD_GUEST_PAGE_FAULT;
> +}

With no "store" counterpart this may end up being a little fragile (at the
use site(s)).

> +/*
> + * The effective XLEN of the guest at the point of the trap: hstatus.VSXL for a
> + * trap taken from VS-mode, vsstatus.UXL for one taken from VU-mode.
> + *
> + * VSXL is consulted whichever mode the trap came from, as it also gives the
> + * width of vsstatus itself: where VSXL says 32, that register has no UXL field
> + * to consult and VU-mode is 32-bit as well, there being nothing to configure.
> + *
> + * It is needed to decode a trapped instruction: the encodings which exist only
> + * for XLEN=64 must not be recognized for a 32-bit guest. Besides those simply
> + * being reserved there, the compressed ones are ambiguous: C.LD and C.FLW
> + * share the encoding 0x6000 (mask 0xe003), and likewise C.SD/C.FSW,
> + * C.LDSP/C.FLWSP and C.SDSP/C.FSWSP.
> + *
> + * IS_ENABLED() can't be used here as HSTATUS_VSXL is defined for
> + * __riscv_xlen == 64 only, the field not existing on RV32 in the first place.
> + */
> +static __maybe_unused unsigned int guest_xlen(const struct cpu_user_regs *regs)
> +{
> +#ifdef CONFIG_RISCV_32
> +    return 32;
> +#else
> +    unsigned long xl = MASK_EXTR(regs->hstatus, HSTATUS_VSXL);
> +
> +    if ( (xl == XLEN_FIELD_64) && !(regs->sstatus & SSTATUS_SPP) )

How about xl > XLEN_FIELD_32 here, to be RV128-compatible?

> @@ -87,6 +173,250 @@ static void resolve_faulting_gpa(struct guest_fault *gf)
>                (htinst_is_pseudo(gf->htinst) ? 0 : (gf->stval & 3));
>  }
>  
> +/*
> + * Where the value of a decoded instruction's register operand is held.
> + *
> + * Relies on x0..x31 being laid out at the start of struct cpu_user_regs in
> + * architectural register-number order; see the comment there.
> + */
> +static __maybe_unused unsigned long *guest_gpr(struct cpu_user_regs *regs,
> +                                               unsigned int reg)
> +{
> +    ASSERT(reg < 32);
> +
> +    return REG_PTR(reg, 0, regs);
> +}

For future callers of this: For 32-bit environments hardware guarantees
upper halves of registers to be zero?

> +/*
> + * Obtain the instruction which caused a guest MMIO trap, filling in
> + * @di->insn and @di->insn_len. It either comes transformed in htinst, or has
> + * to be fetched from guest memory.
> + *
> + * Returns true if the fetch faulted in turn; the resulting trap has then
> + * already been redirected to the guest and there is nothing further for the
> + * caller to do. Where it returns false, @di has been filled in and emulation
> + * is to continue.
> + */
> +static bool __maybe_unused insn_fetch_faulted(const struct guest_fault *gf,
> +                                              struct decoded_insn *di)
> +{
> +    unsigned long htinst = gf->htinst;
> +
> +    /*
> +     * A pseudoinstruction says nothing about the instruction the guest was
> +     * executing, and comes with a guest physical address which isn't the one
> +     * that instruction accessed. handle_guest_page_fault() deals with such a
> +     * fault on its own, so no emulation can ever start for one.
> +     */
> +    ASSERT(!htinst_is_pseudo(htinst));
> +
> +    if ( htinst & BIT(0, UL) )
> +    {
> +        /*
> +         * Bit[0] == 1 implies trapped instruction value is
> +         * transformed instruction or custom instruction.
> +         *
> +         * The transformation always yields the 32-bit format, with bits[1:0]
> +         * holding a marker instead of the original opcode bits: bit[0] set to
> +         * flag the transformation, bit[1] clear if the trapped instruction
> +         * was a compressed one. Restoring the opcode bits makes the value the
> +         * valid 32-bit encoding decode_ldst_insn() matches against. Its
> +         * INSN_MASK_C_* cases exist for the branch below, where a compressed
> +         * instruction is read from guest memory as is: a trapped one arrives
> +         * here already expanded to its 32-bit equivalent, and the opcode bits
> +         * just restored keep it from matching those cases anyway.
> +         *
> +         * The length then cannot come from the value anymore, only from
> +         * bit[1]. And only a 16- or a 32-bit instruction is ever reported
> +         * this way: the standard load and store instructions the hardware
> +         * transforms are all of one of these two lengths, anything else comes
> +         * as the zero special value handled below.
> +         */
> +        di->insn = htinst | INSN_16BIT_MASK;
> +        di->insn_len = (htinst & BIT(1, UL)) ? 4 : 2;

Hmm, so ->insn_len doesn't describe ->insn, as suggested by the comment in
the struct. That wants clarifying there.

> +    }
> +    else
> +    {
> +        const struct cpu_user_regs *regs = gf->regs;
> +        struct trap_info utrap = {};
> +
> +        /*
> +         * Bit[0] == 0 implies trapped instruction value is
> +         * zero or special value. With the pseudoinstructions ruled out
> +         * above, only zero is left: the instruction has to be read from
> +         * guest memory.
> +         */
> +
> +        di->insn = riscv_read_guest(regs->sepc, true, &utrap);
> +        if ( utrap.scause )
> +        {
> +            /*
> +             * If during getting of trapped instruction a fault happen in
> +             * G-stage translation then CAUSE_LOAD_GUEST_PAGE_FAULT is
> +             * generated. Such faults during this operation is considered as
> +             * bus error.
> +             */
> +            if ( is_load_guest_page_fault(utrap.scause) )
> +                utrap.scause = CAUSE_FETCH_ACCESS;
> +
> +            utrap.sepc = regs->sepc;

Couldn't this be part of the initializer of utrap? Or does read_guest()
alter the field?

> +            trap_redirect(&utrap);
> +
> +            return true;
> +        }
> +
> +        /*
> +         * riscv_read_guest() fetches at most two halfwords, so a wider
> +         * encoding has been read in part only and cannot be decoded here.
> +         *
> +         * Report an illegal instruction, which is what the guest would have
> +         * got for such an encoding anyway: the ISA defines no instruction
> +         * wider than 32 bits.
> +         */

Such wording is at risk of going stale. Better say that no guest-exposed
extensions have wider than 32-bit insns.

> +        if ( !INSN_IS_16BIT(di->insn) && !INSN_IS_32BIT(di->insn) )
> +        {
> +            utrap.sepc = regs->sepc;

With the earlier remark this may then also not be needed here.

> +            utrap.scause = CAUSE_ILLEGAL_INSTRUCTION;
> +            /*
> +             * stval is left zero: the spec allows that for an illegal
> +             * instruction, and only part of the instruction is in hand.
> +             */

Not just this - stval may also not be wide enough to hold the full insn.

> +            trap_redirect(&utrap);
> +
> +            return true;
> +        }
> +
> +        di->insn_len = INSN_LEN(di->insn);

If you moved this up a little, you could avoid the separate use of
INSN_{32,64}BIT_MASK above, by going from the value calculated here.

> +    }
> +
> +    return false;
> +}
> +
> +/*
> + * Decode the load or store instruction fetched into @di, filling in the
> + * remaining fields of it (@di->insn and @di->insn_len are filled by
> + * insn_fetch_faulted()).
> + *
> + * @xlen is the effective XLEN of the guest, needed as
> + * the encodings which exist for XLEN=64 only must not be recognized for a
> + * 32-bit guest.
> + *
> + * Returns false if the instruction is not a load or store which can be
> + * emulated here.
> + */
> +static __maybe_unused bool decode_ldst_insn(struct decoded_insn *di,
> +                                            unsigned int xlen)
> +{
> +    unsigned long insn = di->insn;
> +    /* Register fields of the uncompressed forms ... */
> +    unsigned int rd = RV_RD(insn);
> +    unsigned int rs2 = RV_RS2(insn);
> +    /*
> +     * ... and of the compressed ones, where the 3-bit field selects one of
> +     * x8..x15, while the stack-pointer-relative forms have a full-width one.
> +     */
> +    unsigned int rs2s = RVC_RS2S(insn);
> +    unsigned int rs2c = RVC_RS2(insn);
> +
> +    di->is_write = false;
> +    di->is_unsigned = false;

Elsewhere we established that the whole struct has to start out zeroed.
Why not leverage that also here?

> +    di->reg = rd;
> +
> +    if ( (insn & INSN_MASK_LB) == INSN_MATCH_LB )
> +        di->len = 1;
> +    else if ( (insn & INSN_MASK_LBU) == INSN_MATCH_LBU )
> +    {
> +        di->len = 1;
> +        di->is_unsigned = true;
> +    }
> +    else if ( (insn & INSN_MASK_LH) == INSN_MATCH_LH )
> +        di->len = 2;
> +    else if ( (insn & INSN_MASK_LHU) == INSN_MATCH_LHU )
> +    {
> +        di->len = 2;
> +        di->is_unsigned = true;
> +    }
> +    else if ( (insn & INSN_MASK_LW) == INSN_MATCH_LW )
> +        di->len = 4;
> +    else if ( xlen == 64 && (insn & INSN_MASK_LWU) == INSN_MATCH_LWU )
> +    {
> +        di->len = 4;
> +        di->is_unsigned = true;
> +    }
> +    else if ( (insn & INSN_MASK_C_LW) == INSN_MATCH_C_LW )
> +    {
> +        di->len = 4;
> +        di->reg = rs2s;
> +    }

These insns encode the access width uniformly, i.e. doing things the
way done above is rather inefficient.

> +    /* c.lwsp and c.ldsp are reserved with rd being x0. */
> +    else if ( (insn & INSN_MASK_C_LWSP) == INSN_MATCH_C_LWSP && rd )
> +        di->len = 4;

Careful with insns not part of the base ISA: Between the trap and you
getting to fetch and decode, the in-memory insn may have changed. You
posibly set yourself up for vulnerabilities if you permit C encodings
for guests not having C exposed to them.

Jan


^ permalink raw reply	[flat|nested] 244+ messages in thread

* Re: [PATCH v2 25/39] xen/riscv: add guest load emulation for trapped MMIO accesses
  2026-08-27 15:21 ` [PATCH v2 25/39] xen/riscv: add guest load emulation for trapped MMIO accesses Oleksii Kurochko
@ 2026-09-14 11:48   ` Jan Beulich
  2026-09-16  4:16     ` Oleksii Kurochko
  2026-09-18  9:16   ` Baptiste Le Duc
  1 sibling, 1 reply; 244+ messages in thread
From: Jan Beulich @ 2026-09-14 11:48 UTC (permalink / raw)
  To: Oleksii Kurochko
  Cc: Romain Caritey, Baptiste Le Duc, Zheng Zhang, Alistair Francis,
	Connor Davis, Andrew Cooper, Anthony PERARD, Michal Orzel,
	Julien Grall, Roger Pau Monné, Stefano Stabellini, xen-devel

On 27.08.2026 17:21, Oleksii Kurochko wrote:
> @@ -419,7 +416,41 @@ static __maybe_unused bool decode_ldst_insn(struct decoded_insn *di,
>  
>  static int emulate_load(const struct guest_fault *gf)
>  {
> -    return -EOPNOTSUPP;
> +    struct cpu_user_regs *regs = gf->regs;
> +    mmio_info_t info = { .is_write = false };
> +    struct decoded_insn di;
> +    unsigned int shift = 0;
> +    int rc;
> +
> +    /* A fault taken re-reading the instruction is redirected to the guest. */
> +    if ( insn_fetch_faulted(gf, &di) )
> +        return 0;
> +
> +    if ( !decode_ldst_insn(&di, guest_xlen(regs)) || di.is_write )
> +        return -EOPNOTSUPP;
> +
> +    if ( !di.is_unsigned )
> +        shift = BITS_PER_BYTE * (sizeof(unsigned long) - di.len);

This is one of the cases where sizeof(<type>) is not only unclear to
read, but actively risky: Which variable(s) of that type does this
refer to? What if those variable(s)' type(s) change? Aha, ...

> +#ifdef EMULATE_LOAD_DEBUG
> +    gdprintk(XENLOG_DEBUG, "pc=%#lx, addr=%#"PRIpaddr", len=%u, shift=%u\n",
> +             regs->sepc, gf->gpa, di.len, shift);
> +#endif
> +
> +    rc = do_mmio(&info, gf->gpa, di.len);
> +    if ( rc )
> +        return rc;
> +
> +    /*
> +     * A load into x0 discards its result: writing regs->zero would break the
> +     * invariant that it reads as zero when x0 is a source operand elsewhere.
> +     */
> +    if ( di.reg )
> +        *guest_gpr(regs, di.reg) = (long)(info.data << shift) >> shift;

... you apparently mean sizeof(info.data) there.

The comment is (nit) also too long for my taste. Everything from the
colon onwards is imo redundant.

Jan


^ permalink raw reply	[flat|nested] 244+ messages in thread

* Re: [PATCH v2 26/39] xen/riscv: add guest store emulation for trapped MMIO accesses
  2026-08-27 15:21 ` [PATCH v2 26/39] xen/riscv: add guest store " Oleksii Kurochko
@ 2026-09-14 12:01   ` Jan Beulich
  2026-09-16  4:53     ` Oleksii Kurochko
  2026-09-18  9:16   ` Baptiste Le Duc
  1 sibling, 1 reply; 244+ messages in thread
From: Jan Beulich @ 2026-09-14 12:01 UTC (permalink / raw)
  To: Oleksii Kurochko
  Cc: Romain Caritey, Baptiste Le Duc, Zheng Zhang, Alistair Francis,
	Connor Davis, Andrew Cooper, Anthony PERARD, Michal Orzel,
	Julien Grall, Roger Pau Monné, Stefano Stabellini, xen-devel

On 27.08.2026 17:21, Oleksii Kurochko wrote:
> --- a/xen/arch/riscv/emulate.c
> +++ b/xen/arch/riscv/emulate.c
> @@ -453,9 +453,28 @@ static int emulate_load(const struct guest_fault *gf)
>      return 0;
>  }
>  
> -static int emulate_store(struct guest_fault *gf)
> +static int emulate_store(const struct guest_fault *gf)
>  {
> -    return -EOPNOTSUPP;
> +    struct cpu_user_regs *regs = gf->regs;
> +    mmio_info_t info = { .is_write = true };
> +    struct decoded_insn di;
> +    int rc;
> +
> +    if ( insn_fetch_faulted(gf, &di) )
> +        return 0;
> +
> +    if ( !decode_ldst_insn(&di, guest_xlen(regs)) || !di.is_write )
> +        return -EOPNOTSUPP;
> +
> +    info.data = *guest_gpr(regs, di.reg);

This came to mind only here, but applies to the earlier patch as well:
There's no checking of di.len, not even by an assertion. The above is
fragile as to extensions like Zilsd. Zilsd itself may still be okay as
the overrun of the register field will hit the correct one, but the
general concern remains (plus of course that moving across fields is
UB).

Jan


^ permalink raw reply	[flat|nested] 244+ messages in thread

* Re: [PATCH v2 27/39] xen/riscv: introduce arch_move_irqs()
  2026-08-27 15:21 ` [PATCH v2 27/39] xen/riscv: introduce arch_move_irqs() Oleksii Kurochko
@ 2026-09-14 12:07   ` Jan Beulich
  2026-09-16  5:32     ` Oleksii Kurochko
  2026-09-22 17:00   ` Baptiste Le Duc
  1 sibling, 1 reply; 244+ messages in thread
From: Jan Beulich @ 2026-09-14 12:07 UTC (permalink / raw)
  To: Oleksii Kurochko
  Cc: Romain Caritey, Baptiste Le Duc, Zheng Zhang, Alistair Francis,
	Connor Davis, Andrew Cooper, Anthony PERARD, Michal Orzel,
	Julien Grall, Roger Pau Monné, Stefano Stabellini, xen-devel

On 27.08.2026 17:21, Oleksii Kurochko wrote:
> When migrating a vCPU between pCPUs the hypervisor must also migrate
> the associated virtual interrupt state. arch_move_irqs() is the
> per-arch hook called by generic code to trigger that.
> 
> Replace the static inline BUG_ON placeholder in asm/irq.h with a real
> implementation in intc.c dispatching through a new move_irqs vintc_ops
> callback. Wire it up in vAPLIC, which delegates to imsic_migrate_vcpu()
> which itself still a stub to be implemented in follow-up patches.
> 
> Note that technically ASSERT() in arch_move_irqs() could be skipped as
> it will be anyway NULL pointer dereference (and a trap will occur) if
> something isn't properly initialized but sometimes it is harder to
> find place where NULL pointer derefence happened as it isn't
> guaraunted that all necessary registers will be filled with something
> useful.
> As at the moment I don't find any case when ->move_irqs() could be
> skipped, the check that ->move_irq isn't NULL is added to ASSERT()
> instead of adding "if ( ...->move_irq) vitnc->ops->move_irqs(v)".

All of these two paragraphs look stale / inapllicable; ...

> --- a/xen/arch/riscv/intc.c
> +++ b/xen/arch/riscv/intc.c
> @@ -192,3 +192,11 @@ void vintc_ctxt_switch_to(struct vcpu *v)
>  
>      ops->ctxt_switch_to(v);
>  }
> +
> +/* Move vCPU's IRQs from one pCPU to another */
> +void arch_move_irqs(struct vcpu *v)
> +{
> +    const struct vintc_ops *ops = v->domain->arch.vintc->ops;
> +
> +    ops->move_irqs(v);
> +}

There's no ASSERT() here (and I'd prefer if none was added). With the
description pruned:
Acked-by: Jan Beulich <jbeulich@suse.com>

Jan


^ permalink raw reply	[flat|nested] 244+ messages in thread

* Re: [PATCH v2 28/39] xen/riscv: handle the case when no vCPU migration is needed
  2026-08-27 15:21 ` [PATCH v2 28/39] xen/riscv: handle the case when no vCPU migration is needed Oleksii Kurochko
@ 2026-09-14 12:12   ` Jan Beulich
  2026-09-16  5:55     ` Oleksii Kurochko
  2026-09-18  9:21     ` Baptiste Le Duc
  0 siblings, 2 replies; 244+ messages in thread
From: Jan Beulich @ 2026-09-14 12:12 UTC (permalink / raw)
  To: Oleksii Kurochko
  Cc: Romain Caritey, Baptiste Le Duc, Zheng Zhang, Alistair Francis,
	Connor Davis, Andrew Cooper, Anthony PERARD, Michal Orzel,
	Julien Grall, Roger Pau Monné, Stefano Stabellini, xen-devel

On 27.08.2026 17:21, Oleksii Kurochko wrote:
> The IMSIC vsfile mapping is performed in continue_new_vcpu(), since the
> target pCPU must be known at that point. It is therefore possible for
> imsic_migrate_vcpu() to be called before continue_new_vcpu() has
> executed, in which case v->arch.last_pcpu is NR_CPUS and there is nothing
> to migrate.
> 
> Add a BUG_ON("unimplemented") placeholder in imsic_migrate_vcpu() to guard
> against silent incorrect behaviour or unexpected panics in guest VMs until
> the function is fully implemented.

This doesn't adequately describe the change made: The BUG_ON() was already
there.

> --- a/xen/arch/riscv/imsic.c
> +++ b/xen/arch/riscv/imsic.c
> @@ -689,5 +689,15 @@ int __init vimsic_make_domu_dt_node(struct kernel_info *kinfo,
>  
>  void imsic_migrate_vcpu(struct vcpu *v)
>  {
> +    /*
> +     * The scheduler can mark a freshly created vCPU's unit as migrated and
> +     * invoke this before the vCPU has ever run (see the migrated branch in
> +     * schedule()). No need to do migration for such vCPUs as they aren't fully
> +     * initialized (for example, context_switch() will be called after
> +     * imsic_migrate_vcpu()).
> +     */
> +    if ( v->arch.last_cpu == NR_CPUS )

May I suggest to use >= ? I'm still somewhat unconvinced of NR_CPUS being a
good sentinel. If we/you decided to switch to ~0, >= here would continue to
be correct.

Jan

> +        return;
> +
>      BUG_ON("unimplemented");
>  }



^ permalink raw reply	[flat|nested] 244+ messages in thread

* Re: [PATCH v2 29/39] xen/riscv: introduce aplic_reconfigure_target()
  2026-08-27 15:21 ` [PATCH v2 29/39] xen/riscv: introduce aplic_reconfigure_target() Oleksii Kurochko
@ 2026-09-14 12:25   ` Jan Beulich
  2026-09-17  4:55     ` Oleksii Kurochko
  0 siblings, 1 reply; 244+ messages in thread
From: Jan Beulich @ 2026-09-14 12:25 UTC (permalink / raw)
  To: Oleksii Kurochko
  Cc: Romain Caritey, Baptiste Le Duc, Zheng Zhang, Alistair Francis,
	Connor Davis, Andrew Cooper, Anthony PERARD, Michal Orzel,
	Julien Grall, Roger Pau Monné, Stefano Stabellini, xen-devel

On 27.08.2026 17:21, Oleksii Kurochko wrote:
> When a vCPU is migrated to a different pCPU, its IMSIC guest interrupt
> file changes. Any APLIC interrupt previously configured to deliver an
> MSI to the old interrupt file must be retargeted to the new one.
> 
> Implement aplic_reconfigure_target() to scan all interrupts allocated
> to the domain and update their APLIC TARGET registers accordingly.
> 
> Signed-off-by: Oleksii Kurochko <oleksii.kurochko@gmail.com>

First of all I'd like to understand how this "reconfigure" works without
losing interrupts and at the same time without other possible races. An
interrupt can be raised at any time, after all.

> --- a/xen/arch/riscv/aplic.c
> +++ b/xen/arch/riscv/aplic.c
> @@ -138,6 +138,48 @@ uint32_t aplic_msi_target_gen(const struct vcpu *target_vcpu,
>      return base_val;
>  }
>  
> +void aplic_reconfigure_target(const struct vcpu *v,
> +                              unsigned int old_guest_file_id,
> +                              unsigned int old_cpu)
> +{
> +    const struct vintc *vintc = v->domain->arch.vintc;
> +    const unsigned long *auth_irq_bmp = vintc->used_irqs;
> +    unsigned long old_hart_field = aplic_hart_field(old_cpu);

Once again a question you may already recognize: What extra value does
"field" in the variable name add?

> +    unsigned long flags;
> +    unsigned int irqn;
> +
> +    /* Support only MSI mode at the moment */
> +    BUG_ON(!aplic_msi_mode());
> +
> +    spin_lock_irqsave(&aplic.lock, flags);

Taking a global lock for a per-vCPU operation isn't going to scale
very well. Even more so when then ...

> +    bitmap_for_each ( irqn, auth_irq_bmp, vintc->nr_virqs )

... you run a loop with perhaps many (hundreds? thousands?)
iterations.

> +    {
> +        volatile uint32_t __iomem *ptarget;
> +        uint32_t target_val;
> +        unsigned int guest_index, hart_index;
> +
> +        if ( !irqn )
> +            continue;
> +
> +        ptarget = &aplic.regs->target[irqn - 1];
> +        target_val = readl(ptarget);
> +
> +        guest_index = MASK_EXTR(target_val, APLIC_TARGET_GUEST_IDX);
> +        hart_index = MASK_EXTR(target_val, APLIC_TARGET_HART_IDX);
> +
> +        if ( (guest_index != old_guest_file_id) ||
> +             (hart_index != old_hart_field) )
> +            continue;

Along the lines of the naming comment above: This would be more
logical to follow if it was

        if ( (guest_id != old_guest_id) ||
             (hart != old_hart) )
            continue;

i.e. names on each side of the != suitably matching up.

Jan


^ permalink raw reply	[flat|nested] 244+ messages in thread

* Re: [PATCH v2 30/39] xen/riscv: prepare new IMSIC VS-file
  2026-08-27 15:21 ` [PATCH v2 30/39] xen/riscv: prepare new IMSIC VS-file Oleksii Kurochko
@ 2026-09-14 13:13   ` Jan Beulich
  2026-09-17 14:50     ` Oleksii Kurochko
  2026-09-21 16:15   ` Baptiste Le Duc
  1 sibling, 1 reply; 244+ messages in thread
From: Jan Beulich @ 2026-09-14 13:13 UTC (permalink / raw)
  To: Oleksii Kurochko
  Cc: Romain Caritey, Baptiste Le Duc, Zheng Zhang, Alistair Francis,
	Connor Davis, Andrew Cooper, Anthony PERARD, Michal Orzel,
	Julien Grall, Roger Pau Monné, Stefano Stabellini, xen-devel

On 27.08.2026 17:21, Oleksii Kurochko wrote:
> @@ -77,6 +78,64 @@ do {                            \
>      csr_clear(CSR_SIREG, v);    \
>  } while (0)
>  
> +#define imsic_vs_csr_write(c, v)    \
> +do {                                \
> +    csr_write(CSR_VSISELECT, (c));  \
> +    csr_write(CSR_VSIREG, (v));     \

As patch context also tells: Excess parentheses.

> +} while ( 0 )
> +
> +/*
> + * Generic switchcase expansion pyramid.
> + * F is the per-operation leaf macro, ireg is the base register index.
> + * Optional extra args (e.g. an operation and/or a value) are forwarded to F
> + * via __VA_ARGS__.

Just that there's no F below.

> + * imsic_switchcase_break(ireg, op, v) - emit "case ireg: op(ireg,v); break;"
> + * imsic_switchcase_ret(ireg, op, ...) - emit "case ireg: return op(ireg[,v]);"
> + *   The variadic tail is optional so the same leaf works for both read (no v)
> + *   and swap (with v).
> + */
> +#define imsic_switchcase_break(ireg, op, v) \
> +    case ireg:                              \
> +        op(ireg, v);                        \
> +        break;
> +
> +#define imsic_switchcase_ret(ireg, op, ...) \
> +    case ireg:                              \
> +        return op(ireg, ##__VA_ARGS__);
> +
> +#define imsic_switchcase_2(F, ireg, ...)    \
> +    F(ireg + 0, ##__VA_ARGS__)              \
> +    F(ireg + 1, ##__VA_ARGS__)

Ah, there is an F here.

This (recurring below) shows another problem: The two F invocations
look syntacticlly incorrect, due to the missing semicolon. Semicolon
use wants redoing everywhere here.

Further (and again throughout) I think we'd be better off using either
standard C constructs (e.g. __VA_ARGS__) or the gcc extension
permitting use of ## after a comma. A mix of both always looks odd
(to me at least).

Finally, unlike further up, here (and below) ireg wants parenthesizing.

> @@ -389,6 +448,76 @@ int cf_check vcpu_imsic_init(struct vcpu *v)
>      return 0;
>  }
>  
> +/*
> + * Arguments of the imsic_vsfile_local_*() helpers, which are executed by the
> + * pCPU owning the interrupt file, thereby through imsic_call_on_cpu().
> + */
> +struct imsic_vsfile_data {
> +    unsigned int hgei;
> +    unsigned int nr_eix;
> +    struct imsic_mrif *mrif;

I can't spot any use of this field (and hence I also can't judge
whether const wants adding).

> +};
> +
> +/*
> + * Execute func() on the pCPU which owns the IMSIC interrupt file func() is
> + * going to work with.
> + *
> + * An IMSIC VS-file is reachable only through hstatus.VGEIN of the hart the
> + * file belongs to, and a guest interrupt file index is meaningless on any
> + * other hart, so such work always has to be done by that very hart.
> + *
> + * The local case runs with IRQs disabled to provide func() with the same
> + * environment it is given when it is called from the function call IPI
> + * handler.
> + */
> +static void imsic_call_on_cpu(unsigned int cpu, void (*func)(void *),
> +                              void *data)

If this is supposed to be passed struct imsic_vsfile_data *, why not say
so here? Be as type-safe as possible. Of course the callback function
has to use void *.

> +static void cf_check imsic_vsfile_local_clear(void *data)
> +{
> +    unsigned int i;
> +    const struct imsic_vsfile_data *idata = data;
> +    unsigned long new_hstatus, old_hstatus, old_vsiselect;
> +
> +    /* We can only zero-out if we have a IMSIC VS-file */
> +    if ( !idata->hgei )
> +        return;

Wouldn't it make sense to avoid the call here altogether then?

> +    old_vsiselect = csr_read(CSR_VSISELECT);

Likely obvious to you, but I can't spot why vsiselect would need saving
here. If you want me to ack such code, please add at least brief comments.

> +    old_hstatus = csr_read(CSR_HSTATUS);
> +    new_hstatus = old_hstatus & ~HSTATUS_VGEIN;
> +    new_hstatus |= MASK_INSR(idata->hgei, HSTATUS_VGEIN);
> +    csr_write(CSR_HSTATUS, new_hstatus);
> +
> +    imsic_vs_csr_write(IMSIC_EIDELIVERY, 0);
> +    imsic_vs_csr_write(IMSIC_EITHRESHOLD, 0);
> +
> +    for ( i = 0; i < idata->nr_eix; i++ )
> +    {
> +        imsic_eix_write(IMSIC_EIP0 + i * 2, 0);
> +        imsic_eix_write(IMSIC_EIE0 + i * 2, 0);
> +#ifdef CONFIG_RISCV_32
> +        imsic_eix_write(IMSIC_EIP0 + i * 2 + 1, 0);
> +        imsic_eix_write(IMSIC_EIE0 + i * 2 + 1, 0);
> +#endif

In asm/imsic.h I see

#define IMSIC_EIPx_BITS         32

Why is the number of CSR writes different here for RV32 vs RV64? And
if so, why would you not use the 64-bit write function, allowing the
#ifdef to be omitted?

> @@ -689,6 +818,14 @@ int __init vimsic_make_domu_dt_node(struct kernel_info *kinfo,
>  
>  void imsic_migrate_vcpu(struct vcpu *v)
>  {
> +    unsigned int new_vsfile_hgei;
> +    unsigned int new_vsfile_cpu;
> +    unsigned int nr_hw_eix = DIV_ROUND_UP(imsic_cfg.nr_ids + 1,
> +                                          BITS_PER_TYPE(uint64_t));

As written, this could also be 64. uint64_t is a fixed-width type after
all. The question here is: Which variable's type do you really mean
here?

> +    struct imsic_vsfile_data vsfile_data = {
> +        .nr_eix = nr_hw_eix,

The local variable looks to be used only here. Is there really a need
for such a local variable?

> @@ -699,5 +836,27 @@ void imsic_migrate_vcpu(struct vcpu *v)
>      if ( v->arch.last_cpu == NR_CPUS )
>          return;
>  
> +    /*
> +     * At this point, all interrupt producers are still using the old IMSIC
> +     * VS-file.
> +     */
> +
> +    /*
> +     * Latch the pCPU the new interrupt file is taken from: vgein_assign()
> +     * allocates it from v->processor's pool of guest interrupt files, and
> +     * only that hart can access the file afterwards.
> +     */
> +    new_vsfile_cpu = v->processor;

Same here: Is this variable really needed? And what exactly is the comment
telling me?

> +    new_vsfile_hgei = vgein_assign(v);
> +
> +    /* We don't support SW interrupt files at the moment. */
> +    BUG_ON(!new_vsfile_hgei);
> +
> +    vsfile_data.hgei = new_vsfile_hgei;

And again - any real need for the separate local variable?

Jan


^ permalink raw reply	[flat|nested] 244+ messages in thread

* Re: [PATCH v2 31/39] xen/riscv: implement APLIC-hart sync barrier for vCPU migration
  2026-08-27 15:21 ` [PATCH v2 31/39] xen/riscv: implement APLIC-hart sync barrier for vCPU migration Oleksii Kurochko
@ 2026-09-14 13:27   ` Jan Beulich
  2026-09-18 11:53     ` Oleksii Kurochko
  2026-09-22 17:00   ` Baptiste Le Duc
  1 sibling, 1 reply; 244+ messages in thread
From: Jan Beulich @ 2026-09-14 13:27 UTC (permalink / raw)
  To: Oleksii Kurochko
  Cc: Romain Caritey, Baptiste Le Duc, Zheng Zhang, Alistair Francis,
	Connor Davis, Andrew Cooper, Anthony PERARD, Michal Orzel,
	Julien Grall, Roger Pau Monné, Stefano Stabellini, xen-devel

On 27.08.2026 17:21, Oleksii Kurochko wrote:
> During migration of a virtual hart to a different guest interrupt file,
> straggler MSIs from the APLIC could arrive at the old interrupt file
> after the switch.
> 
> genmsi is used despite not supporting guest interrupt files because the
> AIA spec guarantees that all MSIs previously sent from the APLIC to the
> same hart are visible at the hart's IMSIC before the extempore MSI from
> genmsi becomes visible.

Hmm. As indicated, I'm learning RISC-V as I'm reviewing patches. This
paragraph, if left as is, would make sure I simply can't ack the patch.
I just don't understand what is being talked about. I can guess parts,
but for example I don't know what "genmsi" is.

> @@ -205,6 +207,30 @@ void aplic_hw_write_reg(unsigned int offset, uint32_t value)
>      spin_unlock_irqrestore(&aplic.lock, flags);
>  }
>  
> +/*
> + * As needed, synchronize with all IOMMUs and APLICs to ensure that no
> + * straggler MSIs will arrive at the old interrupt file after this step.
> + */
> +void aplic_genmsi_barrier(void)
> +{
> +    const struct imsic_config *imsic = imsic_get_config();
> +    unsigned int cpu = smp_processor_id();
> +    unsigned long flags;
> +    uint32_t val;
> +
> +    val = MASK_INSR(aplic_hart_field(cpu), APLIC_TARGET_HART_IDX) |
> +          (imsic->sync_id & APLIC_TARGET_EIID);

Along the lines of a question on an earlier patch: What if this ANDing
actually chops off bits?

Jan


^ permalink raw reply	[flat|nested] 244+ messages in thread

* Re: [PATCH v2 32/39] xen/riscv: remap interrupts to new IMSIC VS-file
  2026-08-27 15:21 ` [PATCH v2 32/39] xen/riscv: remap interrupts to new IMSIC VS-file Oleksii Kurochko
@ 2026-09-14 15:02   ` Jan Beulich
  2026-09-21  8:03     ` Oleksii Kurochko
  2026-09-23 13:34   ` Baptiste Le Duc
  2026-09-23 15:25   ` Baptiste Le Duc
  2 siblings, 1 reply; 244+ messages in thread
From: Jan Beulich @ 2026-09-14 15:02 UTC (permalink / raw)
  To: Oleksii Kurochko
  Cc: Romain Caritey, Baptiste Le Duc, Zheng Zhang, Alistair Francis,
	Connor Davis, Andrew Cooper, Anthony PERARD, Michal Orzel,
	Julien Grall, Roger Pau Monné, Stefano Stabellini, xen-devel

On 27.08.2026 17:21, Oleksii Kurochko wrote:
> @@ -60,6 +61,12 @@ static unsigned int __ro_after_init guest_num_msis;
>  #define IMSIC_DISABLE_EITHRESHOLD   1
>  #define IMSIC_ENABLE_EITHRESHOLD    0
>  
> +#define imsic_csr_read(c)           \
> +({                                  \
> +    csr_write(CSR_SISELECT, (c));   \

Nit: Excess parentheses again.

> @@ -242,6 +254,15 @@ void imsic_irq_disable(unsigned int irq)
>      spin_unlock(&imsic_cfg.lock);
>  }
>  
> +static bool imsic_local_is_pending(unsigned int id)
> +{
> +    unsigned long isel =
> +        (id / BITS_PER_LONG) * (BITS_PER_LONG / IMSIC_EIPx_BITS) + IMSIC_EIP0;
> +    unsigned long bit = BIT(id % BITS_PER_LONG, UL);

Both can be unsigned int, can't they?

> +    return !!(imsic_csr_read(isel) & bit);
> +}

No need for !! here.

What about endianness, btw? Does the IMSIC always match the CPU (and
its setting)?

Overall, what does "local" in the function name signify? (For a static
function, the "imsic" prefix may also be unnecessary.)

> @@ -497,6 +523,27 @@ static void imsic_call_on_cpu(unsigned int cpu, void (*func)(void *),
>          on_selected_cpus(cpumask_of(cpu), func, data, 1);
>  }
>  
> +/*
> + * Ensure that all the MSIs the APLIC has already generated for the hart this
> + * runs on have really reached the hart's IMSIC.
> + *
> + * The barrier is the one described by the AIA specification in
> + * "Synchronizing interactions between a hart and the APLIC": ask the APLIC to
> + * send an MSI to the hart itself and wait until it shows up as pending in the
> + * hart's own interrupt file. As it says nothing about MSIs on their way to
> + * any other hart, it has to be executed by the pCPU owning the interrupt file
> + * the MSIs were being sent to.
> + */
> +static void cf_check imsic_aplic_sync(void *data)

If the parameter isn't used, maybe best to name it "unused"?

> @@ -848,8 +899,22 @@ void imsic_migrate_vcpu(struct vcpu *v)
>      if ( v->arch.last_cpu == NR_CPUS )
>          return;
>  
> +    read_lock_irqsave(&imsic_state->vsfile_lock, flags);
> +    old_vsfile_id = imsic_state->guest_file_id;
> +    old_vsfile_cpu = imsic_state->vsfile_cpu;
> +    read_unlock_irqrestore(&imsic_state->vsfile_lock, flags);
> +
> +    /*
> +     * We don't support SW interrupt files at the moment. Bail out before
> +     * anything is touched, as the old file has no owning pCPU in that case
> +     * and there is nothing to retarget the producers away from.
> +     */
> +    if ( old_vsfile_cpu == NR_CPUS )
> +        panic("IMSIC SW-file isn't supported\n");
> +
>      /*
>       * At this point, all interrupt producers are still using the old IMSIC
> +     * VS-file so we first move all interrupt producers to the new IMSIC
>       * VS-file.
>       */

Isn't the new part of the comment premature? Moving doesn't start until ...

> @@ -870,5 +935,43 @@ void imsic_migrate_vcpu(struct vcpu *v)
>      /* Zero-out new IMSIC VS-file */
>      imsic_call_on_cpu(new_vsfile_cpu, imsic_vsfile_local_clear, &vsfile_data);
>  
> +    /* Update G-stage mapping for the new IMSIC VS-file */
> +    if ( imsic_map_guest_file(v, new_vsfile_hgei) )
> +    {
> +        domain_crash(v->domain, "Migration to hw interrupt file failed\n");
> +
> +        return;
> +    }
> +
> +    imsic_update_state(v, new_vsfile_hgei);
> +
> +    /*
> +     * TODO: Modify the relevant translation tables at all IOMMUs so that MSIs
> +     *       for this virtual interrupt file are now sent to the new physical
> +     *       interrupt file.
> +     */
> +    if ( iommu_enabled )
> +        printk_once("IMSIC: IOMMU MSI retargeting is not implemented\n");
> +
> +    /*
> +     * If any interrupts at an APLIC are forwarded by MSIs to the old interrupt
> +     * file, reconfigure the APLIC to send them to the new interrupt file.
> +     */
> +    aplic_reconfigure_target(v, old_vsfile_id, old_vsfile_cpu);

... here, as it looks.

Jan


^ permalink raw reply	[flat|nested] 244+ messages in thread

* Re: [PATCH v2 33/39] xen/riscv: dump old interrupt file to memory
  2026-08-27 15:21 ` [PATCH v2 33/39] xen/riscv: dump old interrupt file to memory Oleksii Kurochko
@ 2026-09-14 15:15   ` Jan Beulich
  2026-09-21  9:51     ` Oleksii Kurochko
  2026-09-23 15:15   ` Baptiste Le Duc
  1 sibling, 1 reply; 244+ messages in thread
From: Jan Beulich @ 2026-09-14 15:15 UTC (permalink / raw)
  To: Oleksii Kurochko
  Cc: Romain Caritey, Baptiste Le Duc, Zheng Zhang, Alistair Francis,
	Connor Davis, Andrew Cooper, Anthony PERARD, Michal Orzel,
	Julien Grall, Roger Pau Monné, Stefano Stabellini, xen-devel

On 27.08.2026 17:21, Oleksii Kurochko wrote:
> At the old interrupt file, dump to memory all the eip and eie arrays).
> After this step is done, the old interrupt file is no longer in use so
> old intrrupt file VGEIN could be released.
> 
> Restoring of old interrupt file state will be done in follow-up
> patch.
> 
> There are cases where it is needed to specify on which cpu it is
> necessary to VGEIN should be released so update vgein_release() to
> deal with that.

Beside this being difficult to parse, it looks like it is inapplicable? As
said ...

> Keep BUG_ON("unimplemented") placeholder in imsic_migrate_vcpu() to guard
> against silent incorrect behaviour or unexpected panics in guest VMs until
> the function is fully implemented.
> 
> vgein_release() is stub for now and will be introduced later.

... also here?

> --- a/xen/arch/riscv/imsic.c
> +++ b/xen/arch/riscv/imsic.c
> @@ -56,6 +56,24 @@ static unsigned int __ro_after_init guest_num_msis;
>   */
>  #define GUEST_IMSIC_MAX_MSIS 255U
>  
> +/*
> + * The interrupt identities an IMSIC interrupt file provides are 0 (which is
> + * never valid, but still occupies a bit) up to IMSIC_MAX_ID inclusive, so
> + * IMSIC_MAX_ID + 1 bits have to be covered.
> + */
> +#define IMSIC_MAX_EIX DIV_ROUND_UP(IMSIC_MAX_ID + 1, BITS_PER_TYPE(uint64_t))

As before - plain 64 please, or it needs to become clear where the uint64_t
is actually coming from.

> +struct imsic_mrif_eix {
> +    unsigned long eip[BITS_PER_TYPE(uint64_t) / BITS_PER_LONG];
> +    unsigned long eie[BITS_PER_TYPE(uint64_t) / BITS_PER_LONG];

Same here. Yet better may be to use DECLARE_BITMAP()?

> @@ -85,6 +103,15 @@ do {                            \
>      csr_clear(CSR_SIREG, v);    \
>  } while (0)
>  
> +#define imsic_vs_csr_swap(c, v)     \
> +({                                  \
> +    unsigned long r_;               \
> +                                    \
> +    csr_write(CSR_VSISELECT, (c));  \
> +    r_ = csr_swap(CSR_VSIREG, (v)); \
> +    r_;                             \
> +})

Excess parentheses again. Plus - what use is r_ here?

> @@ -130,6 +157,21 @@ do {                                \
>      imsic_switchcase_32(F, ireg + 0, ##__VA_ARGS__) \
>      imsic_switchcase_32(F, ireg + 32, ##__VA_ARGS__)
>  
> +static unsigned long imsic_eix_swap(unsigned int ireg, unsigned long val)
> +{
> +    switch ( ireg )
> +    {
> +    imsic_switchcase_64(imsic_switchcase_ret, IMSIC_EIP0,
> +                        imsic_vs_csr_swap, val)
> +    imsic_switchcase_64(imsic_switchcase_ret, IMSIC_EIE0,
> +                        imsic_vs_csr_swap, val)
> +    default:
> +        ASSERT_UNREACHABLE();
> +    }

There still wants to "break" in the default case.

> @@ -973,5 +1071,11 @@ void imsic_migrate_vcpu(struct vcpu *v)
>       * to the new IMSIC VS-file.
>       */
>  
> +    /* Read and clear register state from old IMSIC VS-file */
> +    imsic_vsfile_read_clear(old_vsfile_id, old_vsfile_cpu, nr_hw_eix, &tmrif);

Why is &tmrif being passed into the function, when it's not otherwise used
here? The function could itself have a suitable local var.

Jan


^ permalink raw reply	[flat|nested] 244+ messages in thread

* Re: [PATCH v2 34/39] xen/riscv: restore register state in the new IMSIC VS-file
  2026-08-27 15:21 ` [PATCH v2 34/39] xen/riscv: restore register state in the new IMSIC VS-file Oleksii Kurochko
@ 2026-09-14 15:21   ` Jan Beulich
  2026-09-21 10:58     ` Oleksii Kurochko
  2026-09-23 15:42   ` Baptiste Le Duc
  1 sibling, 1 reply; 244+ messages in thread
From: Jan Beulich @ 2026-09-14 15:21 UTC (permalink / raw)
  To: Oleksii Kurochko
  Cc: Romain Caritey, Baptiste Le Duc, Zheng Zhang, Alistair Francis,
	Connor Davis, Andrew Cooper, Anthony PERARD, Michal Orzel,
	Julien Grall, Roger Pau Monné, Stefano Stabellini, xen-devel

On 27.08.2026 17:21, Oleksii Kurochko wrote:
> @@ -630,13 +649,13 @@ static void cf_check imsic_vsfile_local_read_clear(void *data)
>      old_vsiselect = csr_read(CSR_VSISELECT);
>      old_hstatus = csr_read(CSR_HSTATUS);
>      new_hstatus = old_hstatus & ~HSTATUS_VGEIN;
> -    new_hstatus |= ((unsigned long)idata->hgei) << HSTATUS_VGEIN_SHIFT;
> +    new_hstatus |= MASK_INSR(idata->hgei, HSTATUS_VGEIN);

Please put into final shape upon introduction.

>      csr_write(CSR_HSTATUS, new_hstatus);
>  
>      /*
> -     * There is no need to use atomic functions version to store
> -     * values in MRIF because imsic_vsfile_read_clear() is always called
> -     * with pointer to temporary MRIF on stack.
> +     * No atomic accessors are needed to store the values into the MRIF here,
> +     * as imsic_vsfile_read_clear() is always called with a pointer to a
> +     * temporary MRIF on the stack.
>       */

Same for this comment perhaps.

> @@ -1077,5 +1140,12 @@ void imsic_migrate_vcpu(struct vcpu *v)
>      /* Free-up old IMSIC VS-file */
>      vgein_release(v, old_vsfile_id, old_vsfile_cpu);
>  
> -    BUG_ON("unimplemented");
> +    /* Restore register state in the new IMSIC VS-file */
> +    vsfile_data.mrif = &tmrif;

Ah, here &tmrif is used a 2nd time.

> +    imsic_call_on_cpu(new_vsfile_cpu, imsic_vsfile_local_update, &vsfile_data);
> +
> +    /* Set VCPU HSTATUS.VGEIN to new IMSIC VS-file */
> +    vcpu_guest_cpu_user_regs(v)->hstatus &= ~HSTATUS_VGEIN;
> +    vcpu_guest_cpu_user_regs(v)->hstatus |=
> +            MASK_INSR(new_vsfile_hgei, HSTATUS_VGEIN);

Nit: Indentation.

Other comments on earlier patches apply here (and possibly elsewhere) as
well. Just ftaod.

Jan


^ permalink raw reply	[flat|nested] 244+ messages in thread

* Re: [PATCH v2 24/39] xen/riscv: add helpers for decoding a trapped load or store
  2026-09-14 11:03   ` Jan Beulich
@ 2026-09-14 15:57     ` Oleksii Kurochko
  2026-09-15  5:18       ` Jan Beulich
  0 siblings, 1 reply; 244+ messages in thread
From: Oleksii Kurochko @ 2026-09-14 15:57 UTC (permalink / raw)
  To: Jan Beulich
  Cc: Romain Caritey, Baptiste Le Duc, Zheng Zhang, Alistair Francis,
	Connor Davis, Andrew Cooper, Anthony PERARD, Michal Orzel,
	Julien Grall, Roger Pau Monné, Stefano Stabellini, xen-devel



On 9/14/26 1:03 PM, Jan Beulich wrote:
> On 27.08.2026 17:21, Oleksii Kurochko wrote:
>> emulate_load() and emulate_store() will both need to obtain the
>> instruction which caused a guest MMIO trap, decode it, and locate the
>> register operand it names. Add what the two share, ahead of either of
>> them being implemented: struct decoded_insn, insn_fetch_faulted(),
>> decode_ldst_insn(), guest_xlen(), guest_gpr() and advance_pc().
>>
>> The mask/match chain is adapted from Linux's KVM RISC-V implementation.
>>
>> Nothing calls any of this yet, so tag the functions __maybe_unused to
>> keep the build going; the tags go away once emulate_load() and
>> emulate_store() gain their bodies later.
> 
> That'll be a lot of churn to drop those __maybe_unused again. As this
> is merely transient, did you consider putting
> 
>     (void)is_load_guest_page_fault;
> 
> etc in e.g. emulate_load()?

I think it could be really an option. I will rework in that way.

> 
>> @@ -13,9 +14,29 @@
>>   #include <asm/csr.h>
>>   #include <asm/current.h>
>>   #include <asm/emulate.h>
>> +#include <asm/guest_access.h>
>> +#include <asm/processor.h>
>>   #include <asm/riscv_encoding.h>
>>   #include <asm/traps.h>
>>   
>> +/*
>> + * Determine the trapped load or store instruction which caused a guest MMIO
>> + * trap.
>> + */
>> +struct decoded_insn {
>> +    /* The instruction itself, and its length in bytes. */
>> +    unsigned long insn;
>> +    unsigned int insn_len;
>> +    /* Width of the memory access, in bytes. */
>> +    unsigned int len;
>> +    /* Number of the register operand: rd for a load, rs2 for a store. */
>> +    unsigned int reg;
>> +    /* The access is a store rather than a load. */
>> +    bool is_write;
>> +    /* The load zero-extends its result rather than sign-extending it. */
>> +    bool is_unsigned;
>> +};
> 
> I wonder how efficient this is. With use of bitfield the size of this struct
> can likely be more than halved. With suitable choice of widths this may not
> even cause significantly worse generated code.
> 

We could compress the structure into 8 bytes:

struct decoded_insn {
     /*
      * The instruction itself: no ratified extension defines one wider than
      * 32 bits, and insn_fetch_faulted() rejects anything longer.
      */
     uint32_t insn;
     /* Length of the instruction in bytes: 2 or 4. */
     unsigned int insn_len:3;
     /* Width of the memory access, in bytes: 1, 2, 4 or 8. */
     unsigned int len:4;
     /* Number of the register operand: rd for a load, rs2 for a store. */
     unsigned int reg:5;
     /* The access is a store rather than a load. */
     bool is_write:1;
     /* The load zero-extends its result rather than sign-extending it. */
     bool is_unsigned:1;
};

> One thing in any event: Why would the insn field need to be wider than 32
> bits?

Initial idea was that htinst register is HSXLEN so here it will be nice 
to emphasize this.

But considering that read_guest() can read maximum 32-bit instructions 
and no ratified extension defines one wider than 32 bits, and 
insn_fetch_faulted() rejects anything longer then we could really use 
here uint32_t for insn + the comment will be useful:

struct decoded_insn {
     /*
      * The trapped instruction: as read from guest memory, or the 32-bit
      * equivalent the hardware transformed it into (see 
insn_fetch_faulted()).
      * None of the extensions exposed to guests has instructions wider than
      * 32 bits, and insn_fetch_faulted() rejects anything longer.
      */
     uint32_t insn;


> 
>> @@ -39,6 +60,71 @@ struct guest_fault {
>>       paddr_t gpa;
>>   };
>>   
>> +static bool is_load_guest_page_fault(unsigned long scause)
>> +{
>> +    return scause == CAUSE_LOAD_GUEST_PAGE_FAULT;
>> +}
> 
> With no "store" counterpart this may end up being a little fragile (at the
> use site(s)).

I will drop then function and just open-code where it is used.

> 
>> +/*
>> + * The effective XLEN of the guest at the point of the trap: hstatus.VSXL for a
>> + * trap taken from VS-mode, vsstatus.UXL for one taken from VU-mode.
>> + *
>> + * VSXL is consulted whichever mode the trap came from, as it also gives the
>> + * width of vsstatus itself: where VSXL says 32, that register has no UXL field
>> + * to consult and VU-mode is 32-bit as well, there being nothing to configure.
>> + *
>> + * It is needed to decode a trapped instruction: the encodings which exist only
>> + * for XLEN=64 must not be recognized for a 32-bit guest. Besides those simply
>> + * being reserved there, the compressed ones are ambiguous: C.LD and C.FLW
>> + * share the encoding 0x6000 (mask 0xe003), and likewise C.SD/C.FSW,
>> + * C.LDSP/C.FLWSP and C.SDSP/C.FSWSP.
>> + *
>> + * IS_ENABLED() can't be used here as HSTATUS_VSXL is defined for
>> + * __riscv_xlen == 64 only, the field not existing on RV32 in the first place.
>> + */
>> +static __maybe_unused unsigned int guest_xlen(const struct cpu_user_regs *regs)
>> +{
>> +#ifdef CONFIG_RISCV_32
>> +    return 32;
>> +#else
>> +    unsigned long xl = MASK_EXTR(regs->hstatus, HSTATUS_VSXL);
>> +
>> +    if ( (xl == XLEN_FIELD_64) && !(regs->sstatus & SSTATUS_SPP) )
> 
> How about xl > XLEN_FIELD_32 here, to be RV128-compatible?

good point. I'll apply.

> 
>> @@ -87,6 +173,250 @@ static void resolve_faulting_gpa(struct guest_fault *gf)
>>                 (htinst_is_pseudo(gf->htinst) ? 0 : (gf->stval & 3));
>>   }
>>   
>> +/*
>> + * Where the value of a decoded instruction's register operand is held.
>> + *
>> + * Relies on x0..x31 being laid out at the start of struct cpu_user_regs in
>> + * architectural register-number order; see the comment there.
>> + */
>> +static __maybe_unused unsigned long *guest_gpr(struct cpu_user_regs *regs,
>> +                                               unsigned int reg)
>> +{
>> +    ASSERT(reg < 32);
>> +
>> +    return REG_PTR(reg, 0, regs);
>> +}
> 
> For future callers of this: For 32-bit environments hardware guarantees
> upper halves of registers to be zero?

I think that no as according to the spec:

Whenever XLEN in any mode is set to a value less than the widest 
supported XLEN, all operations must ignore source operand register bits 
above the configured XLEN, and must sign-extend results to fill the 
entire widest supported XLEN in the destination register. Similarly, pc 
bits above XLEN are ignored, and when the pc is written, it is 
sign-extended to fill the widest supported XLEN.

I think we want to add the following to the comment above guest_gpr():

* The register is held at its full width, whatever the guest's XLEN is (see
  * guest_xlen()). Where XLEN is narrower, the bits above it are not 
guaranteed
  * to be zero, nor even a sign extension: hardware only ignores them in 
source
  * operands, so they may have been left there by more privileged code 
running
  * at a wider XLEN. Hence callers must not rely on those bits when 
reading a
  * value, and must sign-extend what they write from bit XLEN-1, as hardware
  * does for the result of an operation.


> 
>> +/*
>> + * Obtain the instruction which caused a guest MMIO trap, filling in
>> + * @di->insn and @di->insn_len. It either comes transformed in htinst, or has
>> + * to be fetched from guest memory.
>> + *
>> + * Returns true if the fetch faulted in turn; the resulting trap has then
>> + * already been redirected to the guest and there is nothing further for the
>> + * caller to do. Where it returns false, @di has been filled in and emulation
>> + * is to continue.
>> + */
>> +static bool __maybe_unused insn_fetch_faulted(const struct guest_fault *gf,
>> +                                              struct decoded_insn *di)
>> +{
>> +    unsigned long htinst = gf->htinst;
>> +
>> +    /*
>> +     * A pseudoinstruction says nothing about the instruction the guest was
>> +     * executing, and comes with a guest physical address which isn't the one
>> +     * that instruction accessed. handle_guest_page_fault() deals with such a
>> +     * fault on its own, so no emulation can ever start for one.
>> +     */
>> +    ASSERT(!htinst_is_pseudo(htinst));
>> +
>> +    if ( htinst & BIT(0, UL) )
>> +    {
>> +        /*
>> +         * Bit[0] == 1 implies trapped instruction value is
>> +         * transformed instruction or custom instruction.
>> +         *
>> +         * The transformation always yields the 32-bit format, with bits[1:0]
>> +         * holding a marker instead of the original opcode bits: bit[0] set to
>> +         * flag the transformation, bit[1] clear if the trapped instruction
>> +         * was a compressed one. Restoring the opcode bits makes the value the
>> +         * valid 32-bit encoding decode_ldst_insn() matches against. Its
>> +         * INSN_MASK_C_* cases exist for the branch below, where a compressed
>> +         * instruction is read from guest memory as is: a trapped one arrives
>> +         * here already expanded to its 32-bit equivalent, and the opcode bits
>> +         * just restored keep it from matching those cases anyway.
>> +         *
>> +         * The length then cannot come from the value anymore, only from
>> +         * bit[1]. And only a 16- or a 32-bit instruction is ever reported
>> +         * this way: the standard load and store instructions the hardware
>> +         * transforms are all of one of these two lengths, anything else comes
>> +         * as the zero special value handled below.
>> +         */
>> +        di->insn = htinst | INSN_16BIT_MASK;
>> +        di->insn_len = (htinst & BIT(1, UL)) ? 4 : 2;
> 
> Hmm, so ->insn_len doesn't describe ->insn, as suggested by the comment in
> the struct. That wants clarifying there.

Right, insn_len is the length of the trapped instruction in guest 
memory, which is what advance_pc() needs, whereas insn may hold its 
32-bit transformed equivalent. I'll clarify both comments in the struct.:

struct decoded_insn {
     /*
      * The trapped instruction: as read from guest memory, or the 32-bit
      * equivalent the hardware transformed it into (see 
insn_fetch_faulted()).
      * None of the extensions exposed to guests has instructions wider than
      * 32 bits, and insn_fetch_faulted() rejects anything longer.
      */
     uint32_t insn;
     /*
      * Length in bytes of the trapped instruction in guest memory: 2 or 4.
      * This need not be the length of the encoding in insn, as a compressed
      * instruction may have been transformed into its 32-bit equivalent.
      */
     unsigned int insn_len:3;


> 
>> +    }
>> +    else
>> +    {
>> +        const struct cpu_user_regs *regs = gf->regs;
>> +        struct trap_info utrap = {};
>> +
>> +        /*
>> +         * Bit[0] == 0 implies trapped instruction value is
>> +         * zero or special value. With the pseudoinstructions ruled out
>> +         * above, only zero is left: the instruction has to be read from
>> +         * guest memory.
>> +         */
>> +
>> +        di->insn = riscv_read_guest(regs->sepc, true, &utrap);
>> +        if ( utrap.scause )
>> +        {
>> +            /*
>> +             * If during getting of trapped instruction a fault happen in
>> +             * G-stage translation then CAUSE_LOAD_GUEST_PAGE_FAULT is
>> +             * generated. Such faults during this operation is considered as
>> +             * bus error.
>> +             */
>> +            if ( is_load_guest_page_fault(utrap.scause) )
>> +                utrap.scause = CAUSE_FETCH_ACCESS;
>> +
>> +            utrap.sepc = regs->sepc;
> 
> Couldn't this be part of the initializer of utrap? Or does read_guest()
> alter the field?

It can't: on a fault read_guest() overwrites it. The exception table 
fixup records the sepc of the nested trap, i.e. Xen's own PC at the 
faulting hlvx, while the trap is to be reported to the guest at its 
instruction. I'll add a comment saying so:

             /*
              * Not set in the initializer: on a fault read_guest() 
leaves in
              * utrap.sepc the address of its own faulting access, 
whereas the
              * trap is to be reported at the guest instruction.
              */
             utrap.sepc = regs->sepc;


> 
>> +            trap_redirect(&utrap);
>> +
>> +            return true;
>> +        }
>> +
>> +        /*
>> +         * riscv_read_guest() fetches at most two halfwords, so a wider
>> +         * encoding has been read in part only and cannot be decoded here.
>> +         *
>> +         * Report an illegal instruction, which is what the guest would have
>> +         * got for such an encoding anyway: the ISA defines no instruction
>> +         * wider than 32 bits.
>> +         */
> 
> Such wording is at risk of going stale. Better say that no guest-exposed
> extensions have wider than 32-bit insns.

I will re-word in the following way:

         /*
          * read_guest() fetches at most two halfwords, so a wider 
encoding has
          * been read in part only and cannot be decoded here.
          *
          * Report an illegal instruction: none of the extensions exposed to
          * guests has instructions wider than 32 bits, so such an 
encoding is
          * not a valid instruction for the guest in the first place.
          */


> 
>> +        if ( !INSN_IS_16BIT(di->insn) && !INSN_IS_32BIT(di->insn) )
>> +        {
>> +            utrap.sepc = regs->sepc;
> 
> With the earlier remark this may then also not be needed here.
> 
>> +            utrap.scause = CAUSE_ILLEGAL_INSTRUCTION;
>> +            /*
>> +             * stval is left zero: the spec allows that for an illegal
>> +             * instruction, and only part of the instruction is in hand.
>> +             */
> 
> Not just this - stval may also not be wide enough to hold the full insn.

Right, I'll mention that as well:

             /*
              * stval is left zero, which the spec allows for an illegal
              * instruction: only part of the instruction is in hand, 
and stval,
              * being only XLEN bits wide, may not be able to hold all of it
              * anyway.
              */


> 
>> +            trap_redirect(&utrap);
>> +
>> +            return true;
>> +        }
>> +
>> +        di->insn_len = INSN_LEN(di->insn);
> 
> If you moved this up a little, you could avoid the separate use of
> INSN_{32,64}BIT_MASK above, by going from the value calculated here.
> 

I will do in this way as INSN_LEN() will (after a conversation in 
another thread) return zero if insn is something not 16 or 32:

         di->insn_len = INSN_LEN(di->insn);
...
         if ( !di->insn_len )

>> +    }
>> +
>> +    return false;
>> +}
>> +
>> +/*
>> + * Decode the load or store instruction fetched into @di, filling in the
>> + * remaining fields of it (@di->insn and @di->insn_len are filled by
>> + * insn_fetch_faulted()).
>> + *
>> + * @xlen is the effective XLEN of the guest, needed as
>> + * the encodings which exist for XLEN=64 only must not be recognized for a
>> + * 32-bit guest.
>> + *
>> + * Returns false if the instruction is not a load or store which can be
>> + * emulated here.
>> + */
>> +static __maybe_unused bool decode_ldst_insn(struct decoded_insn *di,
>> +                                            unsigned int xlen)
>> +{
>> +    unsigned long insn = di->insn;
>> +    /* Register fields of the uncompressed forms ... */
>> +    unsigned int rd = RV_RD(insn);
>> +    unsigned int rs2 = RV_RS2(insn);
>> +    /*
>> +     * ... and of the compressed ones, where the 3-bit field selects one of
>> +     * x8..x15, while the stack-pointer-relative forms have a full-width one.
>> +     */
>> +    unsigned int rs2s = RVC_RS2S(insn);
>> +    unsigned int rs2c = RVC_RS2(insn);
>> +
>> +    di->is_write = false;
>> +    di->is_unsigned = false;
> 
> Elsewhere we established that the whole struct has to start out zeroed.
> Why not leverage that also here?

The callers don't actually zero it at present: di is declared without an 
initializer in both emulate_load() and emulate_store(), which is why 
these two fields get reset here. But I agree that's the better way: I'll 
have both callers initialize di with {} and drop the resets, stating in 
the comment that @di is expected to start out zeroed:

"
... insn_fetch_faulted()). Fields which don't apply to the instruction 
are left
  * alone, so @di is expected to start out zeroed.
"


> 
>> +    di->reg = rd;
>> +
>> +    if ( (insn & INSN_MASK_LB) == INSN_MATCH_LB )
>> +        di->len = 1;
>> +    else if ( (insn & INSN_MASK_LBU) == INSN_MATCH_LBU )
>> +    {
>> +        di->len = 1;
>> +        di->is_unsigned = true;
>> +    }
>> +    else if ( (insn & INSN_MASK_LH) == INSN_MATCH_LH )
>> +        di->len = 2;
>> +    else if ( (insn & INSN_MASK_LHU) == INSN_MATCH_LHU )
>> +    {
>> +        di->len = 2;
>> +        di->is_unsigned = true;
>> +    }
>> +    else if ( (insn & INSN_MASK_LW) == INSN_MATCH_LW )
>> +        di->len = 4;
>> +    else if ( xlen == 64 && (insn & INSN_MASK_LWU) == INSN_MATCH_LWU )
>> +    {
>> +        di->len = 4;
>> +        di->is_unsigned = true;
>> +    }
>> +    else if ( (insn & INSN_MASK_C_LW) == INSN_MATCH_C_LW )
>> +    {
>> +        di->len = 4;
>> +        di->reg = rs2s;
>> +    }
> 
> These insns encode the access width uniformly, i.e. doing things the
> way done above is rather inefficient.

I think that I don't know how to do that better at the moment.

It could be less of if/else if to do in this way:

static bool decode_ldst_insn(struct decoded_insn *di, unsigned int xlen)
{
     uint32_t insn = di->insn;
     unsigned int funct3, width_log2;

     if ( INSN_IS_16BIT(insn) )
     {
         /*
          * C.LW, C.LD, C.SW and C.SD (bits[1:0] == 00), and their 
sp-relative
          * C.*SP forms (bits[1:0] == 10), have bits[15:13] of the form x1y:
          * x is set for a store, and y selects a width of 4 or 8 bytes.
          */
         funct3 = RV_X(insn, 13, 3);

         if ( (insn & 1) || !(funct3 & 2) )
             return false;

         di->is_write = funct3 & 4;
         width_log2 = 2 + (funct3 & 1);

         if ( !(insn & 2) )
             di->reg = RVC_RS2S(insn);
         else if ( di->is_write )
             di->reg = RVC_RS2(insn);
         else
         {
             di->reg = RV_RD(insn);
             /* C.LWSP and C.LDSP are reserved with rd being x0. */
             if ( !di->reg )
                 return false;
         }
     }
     else
     {
         /*
          * funct3[1:0] is log2 of the width in bytes, and funct3[2] selects
          * zero-extension for a load, while being reserved for a store.
          */
         funct3 = RV_X(insn, 12, 3);
         width_log2 = funct3 & 3;

         switch ( insn & INSN_OPCODE_MASK )
         {
         case INSN_OPCODE_LOAD:
             di->is_unsigned = funct3 & 4;
             di->reg = RV_RD(insn);
             break;

         case INSN_OPCODE_STORE:
             if ( funct3 & 4 )
                 return false;
             di->is_write = true;
             di->reg = RV_RS2(insn);
             break;

         default:
             return false;
         }
     }

     di->len = 1U << width_log2;

     /*
      * No access is wider than XLEN, and one as wide as XLEN exists only in
      * its sign-extending form: this rules out the encodings which 
exist for
      * XLEN=64 only on a 32-bit guest, including C.FLW for C.LD (and 
alike).
      */
     if ( (di->len * BITS_PER_BYTE > xlen) ||
          (di->is_unsigned && di->len * BITS_PER_BYTE == xlen) )
         return false;

     return true;


     return true;
}

But I am not sure this is what you meant.

> 
>> +    /* c.lwsp and c.ldsp are reserved with rd being x0. */
>> +    else if ( (insn & INSN_MASK_C_LWSP) == INSN_MATCH_C_LWSP && rd )
>> +        di->len = 4;
> 
> Careful with insns not part of the base ISA: Between the trap and you
> getting to fetch and decode, the in-memory insn may have changed. You
> posibly set yourself up for vulnerabilities if you permit C encodings
> for guests not having C exposed to them.

I think then it will be better to reject it duing instruction fetch in 
insn_fetch_faulted():

         di->insn_len = INSN_LEN(di->insn);

         /*
          * read_guest() fetches at most two halfwords, so a wider 
encoding has
          * been read in part only and cannot be decoded here.
          *
          * Report an illegal instruction: none of the extensions exposed to
          * guests has instructions wider than 32 bits, so such an 
encoding is
          * not a valid instruction for the guest in the first place. 
The same
          * goes for a compressed encoding where C isn't exposed to the 
guest:
          * the instruction in memory may have been changed since the 
trap, so
          * what is read back must not be taken to be what trapped.
          */
         if ( !di->insn_len ||
              (di->insn_len == 2 &&
               !riscv_isa_extension_available(current->domain->arch.isa,
                                              RISCV_ISA_EXT_c)) )
         {
             ...

Would it be better?

Thanks.

~ Oleksii



^ permalink raw reply	[flat|nested] 244+ messages in thread

* Re: [PATCH v2 24/39] xen/riscv: add helpers for decoding a trapped load or store
  2026-09-14 15:57     ` Oleksii Kurochko
@ 2026-09-15  5:18       ` Jan Beulich
  0 siblings, 0 replies; 244+ messages in thread
From: Jan Beulich @ 2026-09-15  5:18 UTC (permalink / raw)
  To: Oleksii Kurochko
  Cc: Romain Caritey, Baptiste Le Duc, Zheng Zhang, Alistair Francis,
	Connor Davis, Andrew Cooper, Anthony PERARD, Michal Orzel,
	Julien Grall, Roger Pau Monné, Stefano Stabellini, xen-devel

On 14.09.2026 17:57, Oleksii Kurochko wrote:
> On 9/14/26 1:03 PM, Jan Beulich wrote:
>> On 27.08.2026 17:21, Oleksii Kurochko wrote:
>>> @@ -13,9 +14,29 @@
>>>   #include <asm/csr.h>
>>>   #include <asm/current.h>
>>>   #include <asm/emulate.h>
>>> +#include <asm/guest_access.h>
>>> +#include <asm/processor.h>
>>>   #include <asm/riscv_encoding.h>
>>>   #include <asm/traps.h>
>>>   
>>> +/*
>>> + * Determine the trapped load or store instruction which caused a guest MMIO
>>> + * trap.
>>> + */
>>> +struct decoded_insn {
>>> +    /* The instruction itself, and its length in bytes. */
>>> +    unsigned long insn;
>>> +    unsigned int insn_len;
>>> +    /* Width of the memory access, in bytes. */
>>> +    unsigned int len;
>>> +    /* Number of the register operand: rd for a load, rs2 for a store. */
>>> +    unsigned int reg;
>>> +    /* The access is a store rather than a load. */
>>> +    bool is_write;
>>> +    /* The load zero-extends its result rather than sign-extending it. */
>>> +    bool is_unsigned;
>>> +};
>>
>> I wonder how efficient this is. With use of bitfield the size of this struct
>> can likely be more than halved. With suitable choice of widths this may not
>> even cause significantly worse generated code.
>>
> 
> We could compress the structure into 8 bytes:
> 
> struct decoded_insn {
>      /*
>       * The instruction itself: no ratified extension defines one wider than
>       * 32 bits, and insn_fetch_faulted() rejects anything longer.
>       */
>      uint32_t insn;
>      /* Length of the instruction in bytes: 2 or 4. */
>      unsigned int insn_len:3;
>      /* Width of the memory access, in bytes: 1, 2, 4 or 8. */
>      unsigned int len:4;
>      /* Number of the register operand: rd for a load, rs2 for a store. */
>      unsigned int reg:5;
>      /* The access is a store rather than a load. */
>      bool is_write:1;
>      /* The load zero-extends its result rather than sign-extending it. */
>      bool is_unsigned:1;
> };

Likely this is going a little too far: The non-bool fields may want to
be 8 bits wide, for better code gen.

>>> +    if ( (insn & INSN_MASK_LB) == INSN_MATCH_LB )
>>> +        di->len = 1;
>>> +    else if ( (insn & INSN_MASK_LBU) == INSN_MATCH_LBU )
>>> +    {
>>> +        di->len = 1;
>>> +        di->is_unsigned = true;
>>> +    }
>>> +    else if ( (insn & INSN_MASK_LH) == INSN_MATCH_LH )
>>> +        di->len = 2;
>>> +    else if ( (insn & INSN_MASK_LHU) == INSN_MATCH_LHU )
>>> +    {
>>> +        di->len = 2;
>>> +        di->is_unsigned = true;
>>> +    }
>>> +    else if ( (insn & INSN_MASK_LW) == INSN_MATCH_LW )
>>> +        di->len = 4;
>>> +    else if ( xlen == 64 && (insn & INSN_MASK_LWU) == INSN_MATCH_LWU )
>>> +    {
>>> +        di->len = 4;
>>> +        di->is_unsigned = true;
>>> +    }
>>> +    else if ( (insn & INSN_MASK_C_LW) == INSN_MATCH_C_LW )
>>> +    {
>>> +        di->len = 4;
>>> +        di->reg = rs2s;
>>> +    }
>>
>> These insns encode the access width uniformly, i.e. doing things the
>> way done above is rather inefficient.
> 
> I think that I don't know how to do that better at the moment.
> 
> It could be less of if/else if to do in this way:
> 
> static bool decode_ldst_insn(struct decoded_insn *di, unsigned int xlen)
> {
>      uint32_t insn = di->insn;
>      unsigned int funct3, width_log2;
> 
>      if ( INSN_IS_16BIT(insn) )
>      {
>          /*
>           * C.LW, C.LD, C.SW and C.SD (bits[1:0] == 00), and their 
> sp-relative
>           * C.*SP forms (bits[1:0] == 10), have bits[15:13] of the form x1y:
>           * x is set for a store, and y selects a width of 4 or 8 bytes.
>           */
>          funct3 = RV_X(insn, 13, 3);
> 
>          if ( (insn & 1) || !(funct3 & 2) )
>              return false;
> 
>          di->is_write = funct3 & 4;
>          width_log2 = 2 + (funct3 & 1);
> 
>          if ( !(insn & 2) )
>              di->reg = RVC_RS2S(insn);
>          else if ( di->is_write )
>              di->reg = RVC_RS2(insn);
>          else
>          {
>              di->reg = RV_RD(insn);
>              /* C.LWSP and C.LDSP are reserved with rd being x0. */
>              if ( !di->reg )
>                  return false;
>          }
>      }
>      else
>      {
>          /*
>           * funct3[1:0] is log2 of the width in bytes, and funct3[2] selects
>           * zero-extension for a load, while being reserved for a store.
>           */
>          funct3 = RV_X(insn, 12, 3);
>          width_log2 = funct3 & 3;
> 
>          switch ( insn & INSN_OPCODE_MASK )
>          {
>          case INSN_OPCODE_LOAD:
>              di->is_unsigned = funct3 & 4;
>              di->reg = RV_RD(insn);
>              break;
> 
>          case INSN_OPCODE_STORE:
>              if ( funct3 & 4 )
>                  return false;
>              di->is_write = true;
>              di->reg = RV_RS2(insn);
>              break;
> 
>          default:
>              return false;
>          }
>      }
> 
>      di->len = 1U << width_log2;
> 
>      /*
>       * No access is wider than XLEN, and one as wide as XLEN exists only in
>       * its sign-extending form: this rules out the encodings which 
> exist for
>       * XLEN=64 only on a 32-bit guest, including C.FLW for C.LD (and 
> alike).
>       */
>      if ( (di->len * BITS_PER_BYTE > xlen) ||
>           (di->is_unsigned && di->len * BITS_PER_BYTE == xlen) )
>          return false;
> 
>      return true;
> 
> 
>      return true;
> }
> 
> But I am not sure this is what you meant.

Yes, this goes along the lines of what I was thinking of.

>>> +    /* c.lwsp and c.ldsp are reserved with rd being x0. */
>>> +    else if ( (insn & INSN_MASK_C_LWSP) == INSN_MATCH_C_LWSP && rd )
>>> +        di->len = 4;
>>
>> Careful with insns not part of the base ISA: Between the trap and you
>> getting to fetch and decode, the in-memory insn may have changed. You
>> posibly set yourself up for vulnerabilities if you permit C encodings
>> for guests not having C exposed to them.
> 
> I think then it will be better to reject it duing instruction fetch in 
> insn_fetch_faulted():
> 
>          di->insn_len = INSN_LEN(di->insn);
> 
>          /*
>           * read_guest() fetches at most two halfwords, so a wider 
> encoding has
>           * been read in part only and cannot be decoded here.
>           *
>           * Report an illegal instruction: none of the extensions exposed to
>           * guests has instructions wider than 32 bits, so such an 
> encoding is
>           * not a valid instruction for the guest in the first place. 
> The same
>           * goes for a compressed encoding where C isn't exposed to the 
> guest:
>           * the instruction in memory may have been changed since the 
> trap, so
>           * what is read back must not be taken to be what trapped.
>           */
>          if ( !di->insn_len ||
>               (di->insn_len == 2 &&
>                !riscv_isa_extension_available(current->domain->arch.isa,
>                                               RISCV_ISA_EXT_c)) )
>          {
>              ...
> 
> Would it be better?

It's an option. Where exactly the check is best placed I can't easily say.

Jan


^ permalink raw reply	[flat|nested] 244+ messages in thread

* Re: [PATCH v2 25/39] xen/riscv: add guest load emulation for trapped MMIO accesses
  2026-09-14 11:48   ` Jan Beulich
@ 2026-09-16  4:16     ` Oleksii Kurochko
  0 siblings, 0 replies; 244+ messages in thread
From: Oleksii Kurochko @ 2026-09-16  4:16 UTC (permalink / raw)
  To: Jan Beulich
  Cc: Romain Caritey, Baptiste Le Duc, Zheng Zhang, Alistair Francis,
	Connor Davis, Andrew Cooper, Anthony PERARD, Michal Orzel,
	Julien Grall, Roger Pau Monné, Stefano Stabellini, xen-devel



On 9/14/26 1:48 PM, Jan Beulich wrote:
> On 27.08.2026 17:21, Oleksii Kurochko wrote:
>> @@ -419,7 +416,41 @@ static __maybe_unused bool decode_ldst_insn(struct decoded_insn *di,
>>   
>>   static int emulate_load(const struct guest_fault *gf)
>>   {
>> -    return -EOPNOTSUPP;
>> +    struct cpu_user_regs *regs = gf->regs;
>> +    mmio_info_t info = { .is_write = false };
>> +    struct decoded_insn di;
>> +    unsigned int shift = 0;
>> +    int rc;
>> +
>> +    /* A fault taken re-reading the instruction is redirected to the guest. */
>> +    if ( insn_fetch_faulted(gf, &di) )
>> +        return 0;
>> +
>> +    if ( !decode_ldst_insn(&di, guest_xlen(regs)) || di.is_write )
>> +        return -EOPNOTSUPP;
>> +
>> +    if ( !di.is_unsigned )
>> +        shift = BITS_PER_BYTE * (sizeof(unsigned long) - di.len);
> 
> This is one of the cases where sizeof(<type>) is not only unclear to
> read, but actively risky: Which variable(s) of that type does this
> refer to? What if those variable(s)' type(s) change? Aha, ...
> 
>> +#ifdef EMULATE_LOAD_DEBUG
>> +    gdprintk(XENLOG_DEBUG, "pc=%#lx, addr=%#"PRIpaddr", len=%u, shift=%u\n",
>> +             regs->sepc, gf->gpa, di.len, shift);
>> +#endif
>> +
>> +    rc = do_mmio(&info, gf->gpa, di.len);
>> +    if ( rc )
>> +        return rc;
>> +
>> +    /*
>> +     * A load into x0 discards its result: writing regs->zero would break the
>> +     * invariant that it reads as zero when x0 is a source operand elsewhere.
>> +     */
>> +    if ( di.reg )
>> +        *guest_gpr(regs, di.reg) = (long)(info.data << shift) >> shift;
> 
> ... you apparently mean sizeof(info.data) there.

Good point. I will try to follow such approach in future and use the 
variable name instead of a type.

> 
> The comment is (nit) also too long for my taste. Everything from the
> colon onwards is imo redundant.
Probably you are right. It is a little bit obvious just from the 
defintion of x0 that it should be always zero. I will drop that part of 
the comment after the colon.

THanks.

~ Oleksii


^ permalink raw reply	[flat|nested] 244+ messages in thread

* Re: [PATCH v2 26/39] xen/riscv: add guest store emulation for trapped MMIO accesses
  2026-09-14 12:01   ` Jan Beulich
@ 2026-09-16  4:53     ` Oleksii Kurochko
  2026-09-16  5:15       ` Jan Beulich
  0 siblings, 1 reply; 244+ messages in thread
From: Oleksii Kurochko @ 2026-09-16  4:53 UTC (permalink / raw)
  To: Jan Beulich
  Cc: Romain Caritey, Baptiste Le Duc, Zheng Zhang, Alistair Francis,
	Connor Davis, Andrew Cooper, Anthony PERARD, Michal Orzel,
	Julien Grall, Roger Pau Monné, Stefano Stabellini, xen-devel



On 9/14/26 2:01 PM, Jan Beulich wrote:
> On 27.08.2026 17:21, Oleksii Kurochko wrote:
>> --- a/xen/arch/riscv/emulate.c
>> +++ b/xen/arch/riscv/emulate.c
>> @@ -453,9 +453,28 @@ static int emulate_load(const struct guest_fault *gf)
>>       return 0;
>>   }
>>   
>> -static int emulate_store(struct guest_fault *gf)
>> +static int emulate_store(const struct guest_fault *gf)
>>   {
>> -    return -EOPNOTSUPP;
>> +    struct cpu_user_regs *regs = gf->regs;
>> +    mmio_info_t info = { .is_write = true };
>> +    struct decoded_insn di;
>> +    int rc;
>> +
>> +    if ( insn_fetch_faulted(gf, &di) )
>> +        return 0;
>> +
>> +    if ( !decode_ldst_insn(&di, guest_xlen(regs)) || !di.is_write )
>> +        return -EOPNOTSUPP;
>> +
>> +    info.data = *guest_gpr(regs, di.reg);
> 
> This came to mind only here, but applies to the earlier patch as well:
> There's no checking of di.len, not even by an assertion. The above is
> fragile as to extensions like Zilsd. Zilsd itself may still be okay as
> the overrun of the register field will hit the correct one, but the
> general concern remains (plus of course that moving across fields is
> UB).

Nothing can produce di.len > sizeof(register_t) today, as the 8-byte 
cases are all gated on xlen == 64, so I'd add the assertion at the point 
where the lengths are assigned, covering both emulate_load() (where the 
shift calculation would underflow) and emulate_store() at once:
   ASSERT(di->len <= sizeof(register_t));
right before decode_ldst_insn()'s final "return true".

Probably it makes sense to have just "if (di->len >= sizeof(register_t) 
)" with the comment and then return false in deocde_lst_insn():

+    /*
+     * An access wider than a register could not be carried through: the
+     * register operand guest_gpr() hands out is register_t-wide, as is the
+     * value an emulated access moves. None of the encodings above yields
+     * such an access, the 8-byte ones all being gated on XLEN=64, but a
+     * future extension might (Zilsd, say, whose 8-byte accesses exist 
for a
+     * 32-bit guest).
+     */
+    if ( di->len > sizeof(register_t) )
+        return false;

(i think that the same could be also true for D and Zdinx but I will 
mention only Zilsd as an example)

Also, I think it make sense to add the comment to guest_gpr() than 
di->len is checked in decode_ldst_insn(). Alternative will be to update 
the proto of guest_gpr() and pass `di` and then have extra ASSERT() in 
guest_gpr() for the case if someone will try to use guest_gpr() without 
using insn_fetch_faulted() & decode_ldst_insn() before guest_gpr().
I will apply this alternative way, it looks to me better for now and 
then will add the following ASSERT:

     /*
      * decode_ldst_insn() is what fills @di in, and it rejects an access
      * wider than a register.
      */
     ASSERT(di->len <= sizeof(register_t));

Thanks.

~ Oleksii


^ permalink raw reply	[flat|nested] 244+ messages in thread

* Re: [PATCH v2 26/39] xen/riscv: add guest store emulation for trapped MMIO accesses
  2026-09-16  4:53     ` Oleksii Kurochko
@ 2026-09-16  5:15       ` Jan Beulich
  2026-09-16  5:23         ` Oleksii Kurochko
  0 siblings, 1 reply; 244+ messages in thread
From: Jan Beulich @ 2026-09-16  5:15 UTC (permalink / raw)
  To: Oleksii Kurochko
  Cc: Romain Caritey, Baptiste Le Duc, Zheng Zhang, Alistair Francis,
	Connor Davis, Andrew Cooper, Anthony PERARD, Michal Orzel,
	Julien Grall, Roger Pau Monné, Stefano Stabellini, xen-devel

On 16.09.2026 06:53, Oleksii Kurochko wrote:
> On 9/14/26 2:01 PM, Jan Beulich wrote:
>> On 27.08.2026 17:21, Oleksii Kurochko wrote:
>>> --- a/xen/arch/riscv/emulate.c
>>> +++ b/xen/arch/riscv/emulate.c
>>> @@ -453,9 +453,28 @@ static int emulate_load(const struct guest_fault *gf)
>>>       return 0;
>>>   }
>>>   
>>> -static int emulate_store(struct guest_fault *gf)
>>> +static int emulate_store(const struct guest_fault *gf)
>>>   {
>>> -    return -EOPNOTSUPP;
>>> +    struct cpu_user_regs *regs = gf->regs;
>>> +    mmio_info_t info = { .is_write = true };
>>> +    struct decoded_insn di;
>>> +    int rc;
>>> +
>>> +    if ( insn_fetch_faulted(gf, &di) )
>>> +        return 0;
>>> +
>>> +    if ( !decode_ldst_insn(&di, guest_xlen(regs)) || !di.is_write )
>>> +        return -EOPNOTSUPP;
>>> +
>>> +    info.data = *guest_gpr(regs, di.reg);
>>
>> This came to mind only here, but applies to the earlier patch as well:
>> There's no checking of di.len, not even by an assertion. The above is
>> fragile as to extensions like Zilsd. Zilsd itself may still be okay as
>> the overrun of the register field will hit the correct one, but the
>> general concern remains (plus of course that moving across fields is
>> UB).
> 
> Nothing can produce di.len > sizeof(register_t) today, as the 8-byte 
> cases are all gated on xlen == 64, so I'd add the assertion at the point 
> where the lengths are assigned, covering both emulate_load() (where the 
> shift calculation would underflow) and emulate_store() at once:
>    ASSERT(di->len <= sizeof(register_t));
> right before decode_ldst_insn()'s final "return true".
> 
> Probably it makes sense to have just "if (di->len >= sizeof(register_t) 
> )" with the comment and then return false in deocde_lst_insn():
> 
> +    /*
> +     * An access wider than a register could not be carried through: the
> +     * register operand guest_gpr() hands out is register_t-wide, as is the
> +     * value an emulated access moves. None of the encodings above yields
> +     * such an access, the 8-byte ones all being gated on XLEN=64, but a
> +     * future extension might (Zilsd, say, whose 8-byte accesses exist 
> for a
> +     * 32-bit guest).
> +     */
> +    if ( di->len > sizeof(register_t) )
> +        return false;
> 
> (i think that the same could be also true for D and Zdinx but I will 
> mention only Zilsd as an example)

Since you don't support floating point extensions so far, that's probably
best. I don't quite understand the mentioning of Zdinx, though: That
extension (by itself) doesn't add any memory access insns.

> Also, I think it make sense to add the comment to guest_gpr() than 
> di->len is checked in decode_ldst_insn(). Alternative will be to update 
> the proto of guest_gpr() and pass `di` and then have extra ASSERT() in 
> guest_gpr() for the case if someone will try to use guest_gpr() without 
> using insn_fetch_faulted() & decode_ldst_insn() before guest_gpr().
> I will apply this alternative way, it looks to me better for now and 
> then will add the following ASSERT:
> 
>      /*
>       * decode_ldst_insn() is what fills @di in, and it rejects an access
>       * wider than a register.
>       */
>      ASSERT(di->len <= sizeof(register_t));

Here and above using register_t won't help with Zilsd. The type is tied
to Xen's xlen, but you mean to check against the guest's here.

Jan


^ permalink raw reply	[flat|nested] 244+ messages in thread

* Re: [PATCH v2 26/39] xen/riscv: add guest store emulation for trapped MMIO accesses
  2026-09-16  5:15       ` Jan Beulich
@ 2026-09-16  5:23         ` Oleksii Kurochko
  0 siblings, 0 replies; 244+ messages in thread
From: Oleksii Kurochko @ 2026-09-16  5:23 UTC (permalink / raw)
  To: Jan Beulich
  Cc: Romain Caritey, Baptiste Le Duc, Zheng Zhang, Alistair Francis,
	Connor Davis, Andrew Cooper, Anthony PERARD, Michal Orzel,
	Julien Grall, Roger Pau Monné, Stefano Stabellini, xen-devel



On 9/16/26 7:15 AM, Jan Beulich wrote:
> On 16.09.2026 06:53, Oleksii Kurochko wrote:
>> On 9/14/26 2:01 PM, Jan Beulich wrote:
>>> On 27.08.2026 17:21, Oleksii Kurochko wrote:
>>>> --- a/xen/arch/riscv/emulate.c
>>>> +++ b/xen/arch/riscv/emulate.c
>>>> @@ -453,9 +453,28 @@ static int emulate_load(const struct guest_fault *gf)
>>>>        return 0;
>>>>    }
>>>>    
>>>> -static int emulate_store(struct guest_fault *gf)
>>>> +static int emulate_store(const struct guest_fault *gf)
>>>>    {
>>>> -    return -EOPNOTSUPP;
>>>> +    struct cpu_user_regs *regs = gf->regs;
>>>> +    mmio_info_t info = { .is_write = true };
>>>> +    struct decoded_insn di;
>>>> +    int rc;
>>>> +
>>>> +    if ( insn_fetch_faulted(gf, &di) )
>>>> +        return 0;
>>>> +
>>>> +    if ( !decode_ldst_insn(&di, guest_xlen(regs)) || !di.is_write )
>>>> +        return -EOPNOTSUPP;
>>>> +
>>>> +    info.data = *guest_gpr(regs, di.reg);
>>>
>>> This came to mind only here, but applies to the earlier patch as well:
>>> There's no checking of di.len, not even by an assertion. The above is
>>> fragile as to extensions like Zilsd. Zilsd itself may still be okay as
>>> the overrun of the register field will hit the correct one, but the
>>> general concern remains (plus of course that moving across fields is
>>> UB).
>>
>> Nothing can produce di.len > sizeof(register_t) today, as the 8-byte
>> cases are all gated on xlen == 64, so I'd add the assertion at the point
>> where the lengths are assigned, covering both emulate_load() (where the
>> shift calculation would underflow) and emulate_store() at once:
>>     ASSERT(di->len <= sizeof(register_t));
>> right before decode_ldst_insn()'s final "return true".
>>
>> Probably it makes sense to have just "if (di->len >= sizeof(register_t)
>> )" with the comment and then return false in deocde_lst_insn():
>>
>> +    /*
>> +     * An access wider than a register could not be carried through: the
>> +     * register operand guest_gpr() hands out is register_t-wide, as is the
>> +     * value an emulated access moves. None of the encodings above yields
>> +     * such an access, the 8-byte ones all being gated on XLEN=64, but a
>> +     * future extension might (Zilsd, say, whose 8-byte accesses exist
>> for a
>> +     * 32-bit guest).
>> +     */
>> +    if ( di->len > sizeof(register_t) )
>> +        return false;
>>
>> (i think that the same could be also true for D and Zdinx but I will
>> mention only Zilsd as an example)
> 
> Since you don't support floating point extensions so far, that's probably
> best. I don't quite understand the mentioning of Zdinx, though: That
> extension (by itself) doesn't add any memory access insns.
> 
>> Also, I think it make sense to add the comment to guest_gpr() than
>> di->len is checked in decode_ldst_insn(). Alternative will be to update
>> the proto of guest_gpr() and pass `di` and then have extra ASSERT() in
>> guest_gpr() for the case if someone will try to use guest_gpr() without
>> using insn_fetch_faulted() & decode_ldst_insn() before guest_gpr().
>> I will apply this alternative way, it looks to me better for now and
>> then will add the following ASSERT:
>>
>>       /*
>>        * decode_ldst_insn() is what fills @di in, and it rejects an access
>>        * wider than a register.
>>        */
>>       ASSERT(di->len <= sizeof(register_t));
> 
> Here and above using register_t won't help with Zilsd. The type is tied
> to Xen's xlen, but you mean to check against the guest's here.

Oh, you are right, it should be guest's xlen.


Thanks.

~ Oleksii


^ permalink raw reply	[flat|nested] 244+ messages in thread

* Re: [PATCH v2 27/39] xen/riscv: introduce arch_move_irqs()
  2026-09-14 12:07   ` Jan Beulich
@ 2026-09-16  5:32     ` Oleksii Kurochko
  0 siblings, 0 replies; 244+ messages in thread
From: Oleksii Kurochko @ 2026-09-16  5:32 UTC (permalink / raw)
  To: Jan Beulich
  Cc: Romain Caritey, Baptiste Le Duc, Zheng Zhang, Alistair Francis,
	Connor Davis, Andrew Cooper, Anthony PERARD, Michal Orzel,
	Julien Grall, Roger Pau Monné, Stefano Stabellini, xen-devel



On 9/14/26 2:07 PM, Jan Beulich wrote:
> On 27.08.2026 17:21, Oleksii Kurochko wrote:
>> When migrating a vCPU between pCPUs the hypervisor must also migrate
>> the associated virtual interrupt state. arch_move_irqs() is the
>> per-arch hook called by generic code to trigger that.
>>
>> Replace the static inline BUG_ON placeholder in asm/irq.h with a real
>> implementation in intc.c dispatching through a new move_irqs vintc_ops
>> callback. Wire it up in vAPLIC, which delegates to imsic_migrate_vcpu()
>> which itself still a stub to be implemented in follow-up patches.
>>
>> Note that technically ASSERT() in arch_move_irqs() could be skipped as
>> it will be anyway NULL pointer dereference (and a trap will occur) if
>> something isn't properly initialized but sometimes it is harder to
>> find place where NULL pointer derefence happened as it isn't
>> guaraunted that all necessary registers will be filled with something
>> useful.
>> As at the moment I don't find any case when ->move_irqs() could be
>> skipped, the check that ->move_irq isn't NULL is added to ASSERT()
>> instead of adding "if ( ...->move_irq) vitnc->ops->move_irqs(v)".
> 
> All of these two paragraphs look stale / inapllicable; ...
> 
>> --- a/xen/arch/riscv/intc.c
>> +++ b/xen/arch/riscv/intc.c
>> @@ -192,3 +192,11 @@ void vintc_ctxt_switch_to(struct vcpu *v)
>>   
>>       ops->ctxt_switch_to(v);
>>   }
>> +
>> +/* Move vCPU's IRQs from one pCPU to another */
>> +void arch_move_irqs(struct vcpu *v)
>> +{
>> +    const struct vintc_ops *ops = v->domain->arch.vintc->ops;
>> +
>> +    ops->move_irqs(v);
>> +}
> 
> There's no ASSERT() here (and I'd prefer if none was added). With the
> description pruned:

I will drop last two paragraphs. They are really stale.

> Acked-by: Jan Beulich <jbeulich@suse.com>

Thanks.

~ Oleksii
> 
> Jan



^ permalink raw reply	[flat|nested] 244+ messages in thread

* Re: [PATCH v2 28/39] xen/riscv: handle the case when no vCPU migration is needed
  2026-09-14 12:12   ` Jan Beulich
@ 2026-09-16  5:55     ` Oleksii Kurochko
  2026-09-16 13:02       ` Jan Beulich
  2026-09-18  9:21     ` Baptiste Le Duc
  1 sibling, 1 reply; 244+ messages in thread
From: Oleksii Kurochko @ 2026-09-16  5:55 UTC (permalink / raw)
  To: Jan Beulich
  Cc: Romain Caritey, Baptiste Le Duc, Zheng Zhang, Alistair Francis,
	Connor Davis, Andrew Cooper, Anthony PERARD, Michal Orzel,
	Julien Grall, Roger Pau Monné, Stefano Stabellini, xen-devel



On 9/14/26 2:12 PM, Jan Beulich wrote:
> On 27.08.2026 17:21, Oleksii Kurochko wrote:
>> The IMSIC vsfile mapping is performed in continue_new_vcpu(), since the
>> target pCPU must be known at that point. It is therefore possible for
>> imsic_migrate_vcpu() to be called before continue_new_vcpu() has
>> executed, in which case v->arch.last_pcpu is NR_CPUS and there is nothing
>> to migrate.
>>
>> Add a BUG_ON("unimplemented") placeholder in imsic_migrate_vcpu() to guard
>> against silent incorrect behaviour or unexpected panics in guest VMs until
>> the function is fully implemented.
> 
> This doesn't adequately describe the change made: The BUG_ON() was already
> there.

Agree, it should be just:

Keep the BUG_ON() at the end of the function until imsic_migrate_vcpu() 
is fully implemented, to avoid ending up with a vCPU which isn't fully 
migrated to the new IMSIC interrupt file.

> 
>> --- a/xen/arch/riscv/imsic.c
>> +++ b/xen/arch/riscv/imsic.c
>> @@ -689,5 +689,15 @@ int __init vimsic_make_domu_dt_node(struct kernel_info *kinfo,
>>   
>>   void imsic_migrate_vcpu(struct vcpu *v)
>>   {
>> +    /*
>> +     * The scheduler can mark a freshly created vCPU's unit as migrated and
>> +     * invoke this before the vCPU has ever run (see the migrated branch in
>> +     * schedule()). No need to do migration for such vCPUs as they aren't fully
>> +     * initialized (for example, context_switch() will be called after
>> +     * imsic_migrate_vcpu()).
>> +     */
>> +    if ( v->arch.last_cpu == NR_CPUS )
> 
> May I suggest to use >= ? I'm still somewhat unconvinced of NR_CPUS being a
> good sentinel. If we/you decided to switch to ~0, >= here would continue to
> be correct.

I agree with >= but I am not quite sure that I fully understand what is 
wrong with NR_CPUS. We have for example the following:

static inline unsigned int smp_processor_id(void)
{
     unsigned int id = tp->processor_id;

     BUG_ON(id >= NR_CPUS);

     return id;
}

So it is guaranteed that NR_CPUS what be used as cpu id and so it still 
could be considered as a good sentinel.

Thanks.

~ Oleksii



^ permalink raw reply	[flat|nested] 244+ messages in thread

* Re: [PATCH v2 28/39] xen/riscv: handle the case when no vCPU migration is needed
  2026-09-16  5:55     ` Oleksii Kurochko
@ 2026-09-16 13:02       ` Jan Beulich
  2026-09-17  5:12         ` Oleksii Kurochko
  0 siblings, 1 reply; 244+ messages in thread
From: Jan Beulich @ 2026-09-16 13:02 UTC (permalink / raw)
  To: Oleksii Kurochko
  Cc: Romain Caritey, Baptiste Le Duc, Zheng Zhang, Alistair Francis,
	Connor Davis, Andrew Cooper, Anthony PERARD, Michal Orzel,
	Julien Grall, Roger Pau Monné, Stefano Stabellini, xen-devel

On 16.09.2026 07:55, Oleksii Kurochko wrote:
> On 9/14/26 2:12 PM, Jan Beulich wrote:
>> On 27.08.2026 17:21, Oleksii Kurochko wrote:
>>> --- a/xen/arch/riscv/imsic.c
>>> +++ b/xen/arch/riscv/imsic.c
>>> @@ -689,5 +689,15 @@ int __init vimsic_make_domu_dt_node(struct kernel_info *kinfo,
>>>   
>>>   void imsic_migrate_vcpu(struct vcpu *v)
>>>   {
>>> +    /*
>>> +     * The scheduler can mark a freshly created vCPU's unit as migrated and
>>> +     * invoke this before the vCPU has ever run (see the migrated branch in
>>> +     * schedule()). No need to do migration for such vCPUs as they aren't fully
>>> +     * initialized (for example, context_switch() will be called after
>>> +     * imsic_migrate_vcpu()).
>>> +     */
>>> +    if ( v->arch.last_cpu == NR_CPUS )
>>
>> May I suggest to use >= ? I'm still somewhat unconvinced of NR_CPUS being a
>> good sentinel. If we/you decided to switch to ~0, >= here would continue to
>> be correct.
> 
> I agree with >= but I am not quite sure that I fully understand what is 
> wrong with NR_CPUS. We have for example the following:
> 
> static inline unsigned int smp_processor_id(void)
> {
>      unsigned int id = tp->processor_id;
> 
>      BUG_ON(id >= NR_CPUS);
> 
>      return id;
> }
> 
> So it is guaranteed that NR_CPUS what be used as cpu id and so it still 
> could be considered as a good sentinel.

Arbitrary numbers can be problematic when used as a sentinel. If you look
at disassembly, you may not recognize that number as a sentinel. Further
there's also a code-gen concern: ~0, aiui, will always generate the same
code (to e.g. load into a register). NR_CPUS, depending on .config, may
not. The value may not be loadable by a single insn.

Jan


^ permalink raw reply	[flat|nested] 244+ messages in thread

* Re: [PATCH v2 29/39] xen/riscv: introduce aplic_reconfigure_target()
  2026-09-14 12:25   ` Jan Beulich
@ 2026-09-17  4:55     ` Oleksii Kurochko
  0 siblings, 0 replies; 244+ messages in thread
From: Oleksii Kurochko @ 2026-09-17  4:55 UTC (permalink / raw)
  To: Jan Beulich
  Cc: Romain Caritey, Baptiste Le Duc, Zheng Zhang, Alistair Francis,
	Connor Davis, Andrew Cooper, Anthony PERARD, Michal Orzel,
	Julien Grall, Roger Pau Monné, Stefano Stabellini, xen-devel



On 9/14/26 2:25 PM, Jan Beulich wrote:
> On 27.08.2026 17:21, Oleksii Kurochko wrote:
>> When a vCPU is migrated to a different pCPU, its IMSIC guest interrupt
>> file changes. Any APLIC interrupt previously configured to deliver an
>> MSI to the old interrupt file must be retargeted to the new one.
>>
>> Implement aplic_reconfigure_target() to scan all interrupts allocated
>> to the domain and update their APLIC TARGET registers accordingly.
>>
>> Signed-off-by: Oleksii Kurochko <oleksii.kurochko@gmail.com>
> 
> First of all I'd like to understand how this "reconfigure" works without
> losing interrupts and at the same time without other possible races. An
> interrupt can be raised at any time, after all.

The RISC-V AIA specification relies on this separation for the 6-step 
vCPU migration sequence:

Step 1. Setting eidelivery = 0 at the old interrupt file stops new traps 
on the host CPU.

Step 3: Reconfiguring APLIC/IOMMU and flushing the interconnect forces 
all in-flight "straggler" MSIs to reach the old interrupt file.

Step 4: Because eidelivery = 0 did not block incoming MSIs from setting 
bits in eip, those straggler MSIs safely landed in the old eip array.

Step 5: The hypervisor reads/dumps the old eip array and bitwise ORs it 
into the new interrupt file, guaranteeing that no in-flight MSIs are 
lost during the transition.

(Note that I re-word some steps and skipped some for simplicity. Here 
you can find full text: 
https://github.com/riscv/riscv-aia/blob/main/src/VSLevel.adoc?plain=1#L134)

Does it make sense now?

> 
>> --- a/xen/arch/riscv/aplic.c
>> +++ b/xen/arch/riscv/aplic.c
>> @@ -138,6 +138,48 @@ uint32_t aplic_msi_target_gen(const struct vcpu *target_vcpu,
>>       return base_val;
>>   }
>>   
>> +void aplic_reconfigure_target(const struct vcpu *v,
>> +                              unsigned int old_guest_file_id,
>> +                              unsigned int old_cpu)
>> +{
>> +    const struct vintc *vintc = v->domain->arch.vintc;
>> +    const unsigned long *auth_irq_bmp = vintc->used_irqs;
>> +    unsigned long old_hart_field = aplic_hart_field(old_cpu);
> 
> Once again a question you may already recognize: What extra value does
> "field" in the variable name add?

Because aplic_hart_field() construct target's part of hart field which 
isn't contains only pure hart value (apparently, thanks to the way how 
spec is written and things are done).

Note that based on other reviews from this patch series it is renamed to:
   unsigned long old_hart_index = aplic_hart_index(old_cpu);
(and here _index for the same reason it is basically how AIA spec calls 
this part of the target register)


> 
>> +    unsigned long flags;
>> +    unsigned int irqn;
>> +
>> +    /* Support only MSI mode at the moment */
>> +    BUG_ON(!aplic_msi_mode());
>> +
>> +    spin_lock_irqsave(&aplic.lock, flags);
> 
> Taking a global lock for a per-vCPU operation isn't going to scale
> very well. Even more so when then ...
> 
>> +    bitmap_for_each ( irqn, auth_irq_bmp, vintc->nr_virqs )
> 
> ... you run a loop with perhaps many (hundreds? thousands?)
> iterations.

I agree.

Then per vcpu's target register lock (or per-irq lock) + APLIC's global 
lock mention here only for a short period when APLIC register would be 
needed.

I've done such change for support of IMSIC software interrupt file but 
it seems like it is started to need earlier.

> 
>> +    {
>> +        volatile uint32_t __iomem *ptarget;
>> +        uint32_t target_val;
>> +        unsigned int guest_index, hart_index;
>> +
>> +        if ( !irqn )
>> +            continue;
>> +
>> +        ptarget = &aplic.regs->target[irqn - 1];
>> +        target_val = readl(ptarget);
>> +
>> +        guest_index = MASK_EXTR(target_val, APLIC_TARGET_GUEST_IDX);
>> +        hart_index = MASK_EXTR(target_val, APLIC_TARGET_HART_IDX);
>> +
>> +        if ( (guest_index != old_guest_file_id) ||
>> +             (hart_index != old_hart_field) )
>> +            continue;
> 
> Along the lines of the naming comment above: This would be more
> logical to follow if it was
> 
>          if ( (guest_id != old_guest_id) ||
>               (hart != old_hart) )
>              continue;
> 
> i.e. names on each side of the != suitably matching up.

I agree with guest_id suggestion but hart_index should be left as 
according to the spec what is stored in hart index field of target 
register isn't pure hart cpu id but it is a combination of hart cpu id + 
group index (check the comment above aplic_hart_field() for better context).

Thanks.

~ Oleksii


^ permalink raw reply	[flat|nested] 244+ messages in thread

* Re: [PATCH v2 28/39] xen/riscv: handle the case when no vCPU migration is needed
  2026-09-16 13:02       ` Jan Beulich
@ 2026-09-17  5:12         ` Oleksii Kurochko
  2026-09-17  5:20           ` Jan Beulich
  0 siblings, 1 reply; 244+ messages in thread
From: Oleksii Kurochko @ 2026-09-17  5:12 UTC (permalink / raw)
  To: Jan Beulich
  Cc: Romain Caritey, Baptiste Le Duc, Zheng Zhang, Alistair Francis,
	Connor Davis, Andrew Cooper, Anthony PERARD, Michal Orzel,
	Julien Grall, Roger Pau Monné, Stefano Stabellini, xen-devel



On 9/16/26 3:02 PM, Jan Beulich wrote:
> On 16.09.2026 07:55, Oleksii Kurochko wrote:
>> On 9/14/26 2:12 PM, Jan Beulich wrote:
>>> On 27.08.2026 17:21, Oleksii Kurochko wrote:
>>>> --- a/xen/arch/riscv/imsic.c
>>>> +++ b/xen/arch/riscv/imsic.c
>>>> @@ -689,5 +689,15 @@ int __init vimsic_make_domu_dt_node(struct kernel_info *kinfo,
>>>>    
>>>>    void imsic_migrate_vcpu(struct vcpu *v)
>>>>    {
>>>> +    /*
>>>> +     * The scheduler can mark a freshly created vCPU's unit as migrated and
>>>> +     * invoke this before the vCPU has ever run (see the migrated branch in
>>>> +     * schedule()). No need to do migration for such vCPUs as they aren't fully
>>>> +     * initialized (for example, context_switch() will be called after
>>>> +     * imsic_migrate_vcpu()).
>>>> +     */
>>>> +    if ( v->arch.last_cpu == NR_CPUS )
>>>
>>> May I suggest to use >= ? I'm still somewhat unconvinced of NR_CPUS being a
>>> good sentinel. If we/you decided to switch to ~0, >= here would continue to
>>> be correct.
>>
>> I agree with >= but I am not quite sure that I fully understand what is
>> wrong with NR_CPUS. We have for example the following:
>>
>> static inline unsigned int smp_processor_id(void)
>> {
>>       unsigned int id = tp->processor_id;
>>
>>       BUG_ON(id >= NR_CPUS);
>>
>>       return id;
>> }
>>
>> So it is guaranteed that NR_CPUS what be used as cpu id and so it still
>> could be considered as a good sentinel.
> 
> Arbitrary numbers can be problematic when used as a sentinel. If you look
> at disassembly, you may not recognize that number as a sentinel. Further
> there's also a code-gen concern: ~0, aiui, will always generate the same
> code (to e.g. load into a register). NR_CPUS, depending on .config, may
> not. The value may not be loadable by a single insn.

Witch such explanation it started to be more sense in it.

I will introduce then

/* Value of arch_vcpu.last_cpu for a vCPU which hasn't run yet. */
#define VCPU_NEVER_RAN (~0U)

and use it to work with v->arch.last_cpu.

Just to be sure that I understand correctly your suggestion with ~0U is 
only for the case of ->last_cpu and check if vcpu was ran or not.

For

struct pcpu_info pcpu_info[NR_CPUS] = { [0 ... NR_CPUS - 1] = {
     .processor_id = NR_CPUS,
}};

and

struct vimsic_state {
...
     /*
      * s/w IMSIC VS-file -> vsfile_cpu == NR_CPUS
      * h/w IMSIC VS-file -> vsfile_cpu < NR_CPUS
      */
     unsigned int vsfile_cpu;
};

I can continue to use NR_CPUS, right?

~ Oleksii


^ permalink raw reply	[flat|nested] 244+ messages in thread

* Re: [PATCH v2 28/39] xen/riscv: handle the case when no vCPU migration is needed
  2026-09-17  5:12         ` Oleksii Kurochko
@ 2026-09-17  5:20           ` Jan Beulich
  2026-09-17  8:40             ` Oleksii Kurochko
  0 siblings, 1 reply; 244+ messages in thread
From: Jan Beulich @ 2026-09-17  5:20 UTC (permalink / raw)
  To: Oleksii Kurochko
  Cc: Romain Caritey, Baptiste Le Duc, Zheng Zhang, Alistair Francis,
	Connor Davis, Andrew Cooper, Anthony PERARD, Michal Orzel,
	Julien Grall, Roger Pau Monné, Stefano Stabellini, xen-devel

On 17.09.2026 07:12, Oleksii Kurochko wrote:
> 
> 
> On 9/16/26 3:02 PM, Jan Beulich wrote:
>> On 16.09.2026 07:55, Oleksii Kurochko wrote:
>>> On 9/14/26 2:12 PM, Jan Beulich wrote:
>>>> On 27.08.2026 17:21, Oleksii Kurochko wrote:
>>>>> --- a/xen/arch/riscv/imsic.c
>>>>> +++ b/xen/arch/riscv/imsic.c
>>>>> @@ -689,5 +689,15 @@ int __init vimsic_make_domu_dt_node(struct kernel_info *kinfo,
>>>>>    
>>>>>    void imsic_migrate_vcpu(struct vcpu *v)
>>>>>    {
>>>>> +    /*
>>>>> +     * The scheduler can mark a freshly created vCPU's unit as migrated and
>>>>> +     * invoke this before the vCPU has ever run (see the migrated branch in
>>>>> +     * schedule()). No need to do migration for such vCPUs as they aren't fully
>>>>> +     * initialized (for example, context_switch() will be called after
>>>>> +     * imsic_migrate_vcpu()).
>>>>> +     */
>>>>> +    if ( v->arch.last_cpu == NR_CPUS )
>>>>
>>>> May I suggest to use >= ? I'm still somewhat unconvinced of NR_CPUS being a
>>>> good sentinel. If we/you decided to switch to ~0, >= here would continue to
>>>> be correct.
>>>
>>> I agree with >= but I am not quite sure that I fully understand what is
>>> wrong with NR_CPUS. We have for example the following:
>>>
>>> static inline unsigned int smp_processor_id(void)
>>> {
>>>       unsigned int id = tp->processor_id;
>>>
>>>       BUG_ON(id >= NR_CPUS);
>>>
>>>       return id;
>>> }
>>>
>>> So it is guaranteed that NR_CPUS what be used as cpu id and so it still
>>> could be considered as a good sentinel.
>>
>> Arbitrary numbers can be problematic when used as a sentinel. If you look
>> at disassembly, you may not recognize that number as a sentinel. Further
>> there's also a code-gen concern: ~0, aiui, will always generate the same
>> code (to e.g. load into a register). NR_CPUS, depending on .config, may
>> not. The value may not be loadable by a single insn.
> 
> Witch such explanation it started to be more sense in it.
> 
> I will introduce then
> 
> /* Value of arch_vcpu.last_cpu for a vCPU which hasn't run yet. */
> #define VCPU_NEVER_RAN (~0U)
> 
> and use it to work with v->arch.last_cpu.
> 
> Just to be sure that I understand correctly your suggestion with ~0U is 
> only for the case of ->last_cpu and check if vcpu was ran or not.
> 
> For
> 
> struct pcpu_info pcpu_info[NR_CPUS] = { [0 ... NR_CPUS - 1] = {
>      .processor_id = NR_CPUS,
> }};
> 
> and
> 
> struct vimsic_state {
> ...
>      /*
>       * s/w IMSIC VS-file -> vsfile_cpu == NR_CPUS
>       * h/w IMSIC VS-file -> vsfile_cpu < NR_CPUS
>       */
>      unsigned int vsfile_cpu;
> };
> 
> I can continue to use NR_CPUS, right?

You _can_ everywhere. It may merely be beneficial to use ~0 instead, at
least in some cases. The "how to load value into a register" aspect of
course doesn't affect static initializers. The "easy to recognize" one,
otoh, may apply there as well. You get to judge...

Jan


^ permalink raw reply	[flat|nested] 244+ messages in thread

* Re: [PATCH v2 28/39] xen/riscv: handle the case when no vCPU migration is needed
  2026-09-17  5:20           ` Jan Beulich
@ 2026-09-17  8:40             ` Oleksii Kurochko
  2026-09-17 10:41               ` Jan Beulich
  0 siblings, 1 reply; 244+ messages in thread
From: Oleksii Kurochko @ 2026-09-17  8:40 UTC (permalink / raw)
  To: Jan Beulich
  Cc: Romain Caritey, Baptiste Le Duc, Zheng Zhang, Alistair Francis,
	Connor Davis, Andrew Cooper, Anthony PERARD, Michal Orzel,
	Julien Grall, Roger Pau Monné, Stefano Stabellini, xen-devel



On 9/17/26 7:20 AM, Jan Beulich wrote:
> On 17.09.2026 07:12, Oleksii Kurochko wrote:
>>
>>
>> On 9/16/26 3:02 PM, Jan Beulich wrote:
>>> On 16.09.2026 07:55, Oleksii Kurochko wrote:
>>>> On 9/14/26 2:12 PM, Jan Beulich wrote:
>>>>> On 27.08.2026 17:21, Oleksii Kurochko wrote:
>>>>>> --- a/xen/arch/riscv/imsic.c
>>>>>> +++ b/xen/arch/riscv/imsic.c
>>>>>> @@ -689,5 +689,15 @@ int __init vimsic_make_domu_dt_node(struct kernel_info *kinfo,
>>>>>>     
>>>>>>     void imsic_migrate_vcpu(struct vcpu *v)
>>>>>>     {
>>>>>> +    /*
>>>>>> +     * The scheduler can mark a freshly created vCPU's unit as migrated and
>>>>>> +     * invoke this before the vCPU has ever run (see the migrated branch in
>>>>>> +     * schedule()). No need to do migration for such vCPUs as they aren't fully
>>>>>> +     * initialized (for example, context_switch() will be called after
>>>>>> +     * imsic_migrate_vcpu()).
>>>>>> +     */
>>>>>> +    if ( v->arch.last_cpu == NR_CPUS )
>>>>>
>>>>> May I suggest to use >= ? I'm still somewhat unconvinced of NR_CPUS being a
>>>>> good sentinel. If we/you decided to switch to ~0, >= here would continue to
>>>>> be correct.
>>>>
>>>> I agree with >= but I am not quite sure that I fully understand what is
>>>> wrong with NR_CPUS. We have for example the following:
>>>>
>>>> static inline unsigned int smp_processor_id(void)
>>>> {
>>>>        unsigned int id = tp->processor_id;
>>>>
>>>>        BUG_ON(id >= NR_CPUS);
>>>>
>>>>        return id;
>>>> }
>>>>
>>>> So it is guaranteed that NR_CPUS what be used as cpu id and so it still
>>>> could be considered as a good sentinel.
>>>
>>> Arbitrary numbers can be problematic when used as a sentinel. If you look
>>> at disassembly, you may not recognize that number as a sentinel. Further
>>> there's also a code-gen concern: ~0, aiui, will always generate the same
>>> code (to e.g. load into a register). NR_CPUS, depending on .config, may
>>> not. The value may not be loadable by a single insn.
>>
>> Witch such explanation it started to be more sense in it.
>>
>> I will introduce then
>>
>> /* Value of arch_vcpu.last_cpu for a vCPU which hasn't run yet. */
>> #define VCPU_NEVER_RAN (~0U)
>>
>> and use it to work with v->arch.last_cpu.
>>
>> Just to be sure that I understand correctly your suggestion with ~0U is
>> only for the case of ->last_cpu and check if vcpu was ran or not.
>>
>> For
>>
>> struct pcpu_info pcpu_info[NR_CPUS] = { [0 ... NR_CPUS - 1] = {
>>       .processor_id = NR_CPUS,
>> }};
>>
>> and
>>
>> struct vimsic_state {
>> ...
>>       /*
>>        * s/w IMSIC VS-file -> vsfile_cpu == NR_CPUS
>>        * h/w IMSIC VS-file -> vsfile_cpu < NR_CPUS
>>        */
>>       unsigned int vsfile_cpu;
>> };
>>
>> I can continue to use NR_CPUS, right?
> 
> You _can_ everywhere. It may merely be beneficial to use ~0 instead, at
> least in some cases. The "how to load value into a register" aspect of
> course doesn't affect static initializers. The "easy to recognize" one,
> otoh, may apply there as well. You get to judge...
> 
I checked how NR_CPUS is used and it is okay to change to ~0U for 
vsfile_cpu mentioned above as it could really affect how to load value 
into a register but I am not sure about .processor_id as it is just 
static initializer (maybe just for consistency).

Also as hartid_to_cpuid() returning NR_CPUS to follow the common 
convention of returning an out-of-range CPU number when nothing was 
found (like cpumask_first() / cpumask_next() do), so callers check the 
result with >= rather than against a specific sentinel.

If to use ~0U for vsfile_cpu then suggested name VCPU_NEVER_RAN isn't 
good. Then probably CPU_NONE will be better.

If we will start to use ~0 instead of NR_CPUS then IIUC it won't be any 
sense to use ">=" suggested above and "==" could be continue to be used, 
right?


~ Oleksii


^ permalink raw reply	[flat|nested] 244+ messages in thread

* Re: [PATCH v2 28/39] xen/riscv: handle the case when no vCPU migration is needed
  2026-09-17  8:40             ` Oleksii Kurochko
@ 2026-09-17 10:41               ` Jan Beulich
  0 siblings, 0 replies; 244+ messages in thread
From: Jan Beulich @ 2026-09-17 10:41 UTC (permalink / raw)
  To: Oleksii Kurochko
  Cc: Romain Caritey, Baptiste Le Duc, Zheng Zhang, Alistair Francis,
	Connor Davis, Andrew Cooper, Anthony PERARD, Michal Orzel,
	Julien Grall, Roger Pau Monné, Stefano Stabellini, xen-devel

On 17.09.2026 10:40, Oleksii Kurochko wrote:
> On 9/17/26 7:20 AM, Jan Beulich wrote:
>> On 17.09.2026 07:12, Oleksii Kurochko wrote:
>>> On 9/16/26 3:02 PM, Jan Beulich wrote:
>>>> On 16.09.2026 07:55, Oleksii Kurochko wrote:
>>>>> On 9/14/26 2:12 PM, Jan Beulich wrote:
>>>>>> On 27.08.2026 17:21, Oleksii Kurochko wrote:
>>>>>>> --- a/xen/arch/riscv/imsic.c
>>>>>>> +++ b/xen/arch/riscv/imsic.c
>>>>>>> @@ -689,5 +689,15 @@ int __init vimsic_make_domu_dt_node(struct kernel_info *kinfo,
>>>>>>>     
>>>>>>>     void imsic_migrate_vcpu(struct vcpu *v)
>>>>>>>     {
>>>>>>> +    /*
>>>>>>> +     * The scheduler can mark a freshly created vCPU's unit as migrated and
>>>>>>> +     * invoke this before the vCPU has ever run (see the migrated branch in
>>>>>>> +     * schedule()). No need to do migration for such vCPUs as they aren't fully
>>>>>>> +     * initialized (for example, context_switch() will be called after
>>>>>>> +     * imsic_migrate_vcpu()).
>>>>>>> +     */
>>>>>>> +    if ( v->arch.last_cpu == NR_CPUS )
>>>>>>
>>>>>> May I suggest to use >= ? I'm still somewhat unconvinced of NR_CPUS being a
>>>>>> good sentinel. If we/you decided to switch to ~0, >= here would continue to
>>>>>> be correct.
>>>>>
>>>>> I agree with >= but I am not quite sure that I fully understand what is
>>>>> wrong with NR_CPUS. We have for example the following:
>>>>>
>>>>> static inline unsigned int smp_processor_id(void)
>>>>> {
>>>>>        unsigned int id = tp->processor_id;
>>>>>
>>>>>        BUG_ON(id >= NR_CPUS);
>>>>>
>>>>>        return id;
>>>>> }
>>>>>
>>>>> So it is guaranteed that NR_CPUS what be used as cpu id and so it still
>>>>> could be considered as a good sentinel.
>>>>
>>>> Arbitrary numbers can be problematic when used as a sentinel. If you look
>>>> at disassembly, you may not recognize that number as a sentinel. Further
>>>> there's also a code-gen concern: ~0, aiui, will always generate the same
>>>> code (to e.g. load into a register). NR_CPUS, depending on .config, may
>>>> not. The value may not be loadable by a single insn.
>>>
>>> Witch such explanation it started to be more sense in it.
>>>
>>> I will introduce then
>>>
>>> /* Value of arch_vcpu.last_cpu for a vCPU which hasn't run yet. */
>>> #define VCPU_NEVER_RAN (~0U)
>>>
>>> and use it to work with v->arch.last_cpu.
>>>
>>> Just to be sure that I understand correctly your suggestion with ~0U is
>>> only for the case of ->last_cpu and check if vcpu was ran or not.
>>>
>>> For
>>>
>>> struct pcpu_info pcpu_info[NR_CPUS] = { [0 ... NR_CPUS - 1] = {
>>>       .processor_id = NR_CPUS,
>>> }};
>>>
>>> and
>>>
>>> struct vimsic_state {
>>> ...
>>>       /*
>>>        * s/w IMSIC VS-file -> vsfile_cpu == NR_CPUS
>>>        * h/w IMSIC VS-file -> vsfile_cpu < NR_CPUS
>>>        */
>>>       unsigned int vsfile_cpu;
>>> };
>>>
>>> I can continue to use NR_CPUS, right?
>>
>> You _can_ everywhere. It may merely be beneficial to use ~0 instead, at
>> least in some cases. The "how to load value into a register" aspect of
>> course doesn't affect static initializers. The "easy to recognize" one,
>> otoh, may apply there as well. You get to judge...
>>
> I checked how NR_CPUS is used and it is okay to change to ~0U for 
> vsfile_cpu mentioned above as it could really affect how to load value 
> into a register but I am not sure about .processor_id as it is just 
> static initializer (maybe just for consistency).
> 
> Also as hartid_to_cpuid() returning NR_CPUS to follow the common 
> convention of returning an out-of-range CPU number when nothing was 
> found (like cpumask_first() / cpumask_next() do), so callers check the 
> result with >= rather than against a specific sentinel.
> 
> If to use ~0U for vsfile_cpu then suggested name VCPU_NEVER_RAN isn't 
> good. Then probably CPU_NONE will be better.
> 
> If we will start to use ~0 instead of NR_CPUS then IIUC it won't be any 
> sense to use ">=" suggested above and "==" could be continue to be used, 
> right?

I fear you may not like the answer: Depends. IOW I can't give a concrete
reply unless seeing the concrete use(s).

Jan


^ permalink raw reply	[flat|nested] 244+ messages in thread

* Re: [PATCH v2 30/39] xen/riscv: prepare new IMSIC VS-file
  2026-09-14 13:13   ` Jan Beulich
@ 2026-09-17 14:50     ` Oleksii Kurochko
  2026-09-18  6:02       ` Jan Beulich
  0 siblings, 1 reply; 244+ messages in thread
From: Oleksii Kurochko @ 2026-09-17 14:50 UTC (permalink / raw)
  To: Jan Beulich
  Cc: Romain Caritey, Baptiste Le Duc, Zheng Zhang, Alistair Francis,
	Connor Davis, Andrew Cooper, Anthony PERARD, Michal Orzel,
	Julien Grall, Roger Pau Monné, Stefano Stabellini, xen-devel



On 9/14/26 3:13 PM, Jan Beulich wrote:
> On 27.08.2026 17:21, Oleksii Kurochko wrote:
>> @@ -77,6 +78,64 @@ do {                            \
>>       csr_clear(CSR_SIREG, v);    \
>>   } while (0)
>>   
>> +#define imsic_vs_csr_write(c, v)    \
>> +do {                                \
>> +    csr_write(CSR_VSISELECT, (c));  \
>> +    csr_write(CSR_VSIREG, (v));     \
> 
> As patch context also tells: Excess parentheses.

I will drop them then.

> 
>> +} while ( 0 )
>> +
>> +/*
>> + * Generic switchcase expansion pyramid.
>> + * F is the per-operation leaf macro, ireg is the base register index.
>> + * Optional extra args (e.g. an operation and/or a value) are forwarded to F
>> + * via __VA_ARGS__.
> 
> Just that there's no F below.

Oh, right. I will move this comment a little bit down.

> 
>> + * imsic_switchcase_break(ireg, op, v) - emit "case ireg: op(ireg,v); break;"
>> + * imsic_switchcase_ret(ireg, op, ...) - emit "case ireg: return op(ireg[,v]);"
>> + *   The variadic tail is optional so the same leaf works for both read (no v)
>> + *   and swap (with v).
>> + */
>> +#define imsic_switchcase_break(ireg, op, v) \
>> +    case ireg:                              \
>> +        op(ireg, v);                        \
>> +        break;
>> +
>> +#define imsic_switchcase_ret(ireg, op, ...) \
>> +    case ireg:                              \
>> +        return op(ireg, ##__VA_ARGS__);
>> +
>> +#define imsic_switchcase_2(F, ireg, ...)    \
>> +    F(ireg + 0, ##__VA_ARGS__)              \
>> +    F(ireg + 1, ##__VA_ARGS__)
> 
> Ah, there is an F here.
> 
> This (recurring below) shows another problem: The two F invocations
> look syntacticlly incorrect, due to the missing semicolon. Semicolon
> use wants redoing everywhere here.

I will drop then ';' from imsic_switchcase_{break,ret}.

> 
> Further (and again throughout) I think we'd be better off using either
> standard C constructs (e.g. __VA_ARGS__) or the gcc extension
> permitting use of ## after a comma. A mix of both always looks odd
> (to me at least).

I will follow C standard here.

> 
> Finally, unlike further up, here (and below) ireg wants parenthesizing.

I will add some.


> 
>> @@ -389,6 +448,76 @@ int cf_check vcpu_imsic_init(struct vcpu *v)
>>       return 0;
>>   }
>>   
>> +/*
>> + * Arguments of the imsic_vsfile_local_*() helpers, which are executed by the
>> + * pCPU owning the interrupt file, thereby through imsic_call_on_cpu().
>> + */
>> +struct imsic_vsfile_data {
>> +    unsigned int hgei;
>> +    unsigned int nr_eix;
>> +    struct imsic_mrif *mrif;
> 
> I can't spot any use of this field (and hence I also can't judge
> whether const wants adding).

It should be introduced later in the another patch.

> 
>> +};
>> +
>> +/*
>> + * Execute func() on the pCPU which owns the IMSIC interrupt file func() is
>> + * going to work with.
>> + *
>> + * An IMSIC VS-file is reachable only through hstatus.VGEIN of the hart the
>> + * file belongs to, and a guest interrupt file index is meaningless on any
>> + * other hart, so such work always has to be done by that very hart.
>> + *
>> + * The local case runs with IRQs disabled to provide func() with the same
>> + * environment it is given when it is called from the function call IPI
>> + * handler.
>> + */
>> +static void imsic_call_on_cpu(unsigned int cpu, void (*func)(void *),
>> +                              void *data)
> 
> If this is supposed to be passed struct imsic_vsfile_data *, why not say
> so here? Be as type-safe as possible. Of course the callback function
> has to use void *.

At the moment, I don't see where I am using void * so I will use struct 
imsic_vsfile_data * instead.

> 
>> +static void cf_check imsic_vsfile_local_clear(void *data)
>> +{
>> +    unsigned int i;
>> +    const struct imsic_vsfile_data *idata = data;
>> +    unsigned long new_hstatus, old_hstatus, old_vsiselect;
>> +
>> +    /* We can only zero-out if we have a IMSIC VS-file */
>> +    if ( !idata->hgei )
>> +        return;
> 
> Wouldn't it make sense to avoid the call here altogether then?

I think it is better to have this if () here instead of the caller side 
as this function one day could be just directly (w/ imsic_call_on_cpu) 
and even the way how it is called now and in the case of 
imsic_call_on_cpu() is executed on local cpu then it will be basically 
just direct call of imsic_vsfile_local_clear(). So in the case I am not 
missing something I prefer to have a check here.

> 
>> +    old_vsiselect = csr_read(CSR_VSISELECT);
> 
> Likely obvious to you, but I can't spot why vsiselect would need saving
> here. If you want me to ack such code, please add at least brief comments.

I think then it will be better to put the comment once above struct 
imsic_vsfile_data and then just point here to that comment as basically 
it will be needed for all imsic_vsfile_local_* helpers.

So basically I am suggesting:

--- a/xen/arch/riscv/imsic.c
+++ b/xen/arch/riscv/imsic.c
@@ ... @@
  /*
   * Arguments of the imsic_vsfile_local_*() helpers, which are executed 
by the
   * pCPU owning the interrupt file, thereby through imsic_call_on_cpu().
+ *
+ * The helpers interrupt whatever vCPU context is loaded on that pCPU, 
which
+ * generally isn't the vCPU the interrupt file belongs to. To reach the 
file
+ * they retarget hstatus.VGEIN and select the file's registers through
+ * vsiselect. Both CSRs are live state of the interrupted vCPU 
(vsiselect is
+ * saved to struct arch_vcpu only on context switch, but the 
interrupted vCPU
+ * may resume guest execution without one), hence the helpers have to 
restore
+ * them before returning.
   */
  struct imsic_vsfile_data {
      unsigned int hgei;
      unsigned int nr_eix;
      struct imsic_mrif *mrif;
  };
@@ ... @@ static void cf_check imsic_vsfile_local_clear(void *data)
      /* We can only zero-out if we have a IMSIC VS-file */
      if ( !idata->hgei )
          return;

+    /* See the comment ahead of struct imsic_vsfile_data. */
      old_vsiselect = csr_read(CSR_VSISELECT);
      old_hstatus = csr_read(CSR_HSTATUS);
@@ ... @@ static void cf_check imsic_vsfile_local_read_clear(void *data)
      csr_clear(CSR_HGEIE, BIT(idata->hgei, UL));

+    /* See the comment ahead of struct imsic_vsfile_data. */
      old_vsiselect = csr_read(CSR_VSISELECT);
      old_hstatus = csr_read(CSR_HSTATUS);
@@ ... @@ static void cf_check imsic_vsfile_local_update(void *data)
       * stack.
       */

+    /* See the comment ahead of struct imsic_vsfile_data. */
      old_vsiselect = csr_read(CSR_VSISELECT);
      old_hstatus = csr_read(CSR_HSTATUS);


Does it look clear now?

> 
>> +    old_hstatus = csr_read(CSR_HSTATUS);
>> +    new_hstatus = old_hstatus & ~HSTATUS_VGEIN;
>> +    new_hstatus |= MASK_INSR(idata->hgei, HSTATUS_VGEIN);
>> +    csr_write(CSR_HSTATUS, new_hstatus);
>> +
>> +    imsic_vs_csr_write(IMSIC_EIDELIVERY, 0);
>> +    imsic_vs_csr_write(IMSIC_EITHRESHOLD, 0);
>> +
>> +    for ( i = 0; i < idata->nr_eix; i++ )
>> +    {
>> +        imsic_eix_write(IMSIC_EIP0 + i * 2, 0);
>> +        imsic_eix_write(IMSIC_EIE0 + i * 2, 0);
>> +#ifdef CONFIG_RISCV_32
>> +        imsic_eix_write(IMSIC_EIP0 + i * 2 + 1, 0);
>> +        imsic_eix_write(IMSIC_EIE0 + i * 2 + 1, 0);
>> +#endif
> 
> In asm/imsic.h I see
> 
> #define IMSIC_EIPx_BITS         32
> 
> Why is the number of CSR writes different here for RV32 vs RV64? 

IMSIC_EIPx_BITS is the unit the AIA spec numbers the eip<k>/eie<k> 
registers by. On RV32 all of eip0..eip63 exist and are 32 bits wide. On 
RV64 only the even-numbered ones exist, each being 64 bits wide and 
covering what eip<k> and eip<k+1> cover on RV32; accessing an 
odd-numbered one is an illegal instruction. Hence one 64-bit group of 
interrupt identities takes one register on RV64, but two on RV32.

I will add some small comments:

     for ( i = 0; i < idata->nr_eix; i++ )
     {
         /* On RV64 a 64-bit EIx group is the even-numbered register 
alone. */
         imsic_eix_write(IMSIC_EIP0 + i * 2, 0);
         imsic_eix_write(IMSIC_EIE0 + i * 2, 0);
#ifdef CONFIG_RISCV_32
         /*
          * On RV32 it is split into the even-numbered (low half) and the
          * following odd-numbered (high half) register.
          */
         imsic_eix_write(IMSIC_EIP0 + i * 2 + 1, 0);
         imsic_eix_write(IMSIC_EIE0 + i * 2 + 1, 0);
#endif
     }

> And
> if so, why would you not use the 64-bit write function, allowing the
> #ifdef to be omitted?

I can introduce something like:

/*
  * On RV64 a 64-bit EIx group is the even-numbered register alone, whereas
  * on RV32 it is split into the even-numbered (low half) and the following
  * odd-numbered (high half) register.
  */
static void imsic_eix_write64(unsigned int ireg, uint64_t val)
{
     imsic_eix_write(ireg, val);
     if ( IS_ENABLED(CONFIG_RISCV_32) )
         imsic_eix_write(ireg + 1, val >> 32);
}

And then:

     for ( i = 0; i < idata->nr_eix; i++ )
     {
         imsic_eix_write64(IMSIC_EIP0 + i * 2, 0);
         imsic_eix_write64(IMSIC_EIE0 + i * 2, 0);
     }

Would it be better?

Or instead of imsic_eix_write64() I could just have a combination of 
imisc_vsfile_local_clear():

     imsic_eix_write(ireg, 0);
     if ( IS_ENABLED(CONFIG_RISCV_32) )
         imsic_eix_write(ireg + 1, 0);

> 
>> @@ -689,6 +818,14 @@ int __init vimsic_make_domu_dt_node(struct kernel_info *kinfo,
>>   
>>   void imsic_migrate_vcpu(struct vcpu *v)
>>   {
>> +    unsigned int new_vsfile_hgei;
>> +    unsigned int new_vsfile_cpu;
>> +    unsigned int nr_hw_eix = DIV_ROUND_UP(imsic_cfg.nr_ids + 1,
>> +                                          BITS_PER_TYPE(uint64_t));
> 
> As written, this could also be 64. uint64_t is a fixed-width type after
> all. The question here is: Which variable's type do you really mean
> here?

The minimum number of hardware EIx groups is 1 (for the minimum 63 
supported interrupt identities, i.e. DIV_ROUND_UP(63 + 1, 64) = 1), and 
the maximum is 32 (for 2047 identities), on both RV32 and RV64.You are 
right regarding BITS_PER_TYPE(uint64_t). Since an architectural EIx 
group always covers 64 interrupt identities regardless of XLEN, using 
BITS_PER_TYPE(uint64_t) is unnecessarily indirect when we mean a 
constant 64-bit group size.I will simplify this in v3 to use 64.

> 
>> +    struct imsic_vsfile_data vsfile_data = {
>> +        .nr_eix = nr_hw_eix,
> 
> The local variable looks to be used only here. Is there really a need
> for such a local variable?

No, I will drop nr_hw_eix.

> 
>> @@ -699,5 +836,27 @@ void imsic_migrate_vcpu(struct vcpu *v)
>>       if ( v->arch.last_cpu == NR_CPUS )
>>           return;
>>   
>> +    /*
>> +     * At this point, all interrupt producers are still using the old IMSIC
>> +     * VS-file.
>> +     */
>> +
>> +    /*
>> +     * Latch the pCPU the new interrupt file is taken from: vgein_assign()
>> +     * allocates it from v->processor's pool of guest interrupt files, and
>> +     * only that hart can access the file afterwards.
>> +     */
>> +    new_vsfile_cpu = v->processor;
> 
> Same here: Is this variable really needed? 

Technically no, it could be used v->processor everywhere but just for 
readability (to how spec is wording migration process) I think I will 
prefer to have new_vsfile_cpu here. But if it doesn't make sense I can 
agree to drop it.

> And what exactly is the comment
> telling me?

I am re-reading it now and it looks just useless.

I think that initially I thought that for some reason v->processor could 
change during the end of migration and so by that I wanted to fix new 
vsfile cpu so all the interrupts will go there before migration functon 
for that vcpu will be called again and reschedule all the interrupt to 
new v->processor.

I think it isn't real case so the comment could be dropped.

> 
>> +    new_vsfile_hgei = vgein_assign(v);
>> +
>> +    /* We don't support SW interrupt files at the moment. */
>> +    BUG_ON(!new_vsfile_hgei);
>> +
>> +    vsfile_data.hgei = new_vsfile_hgei;
> 
> And again - any real need for the separate local variable?

Here I agree, we could have only vsfile_data.hgei.

Thanks.

~ Oleksii




^ permalink raw reply	[flat|nested] 244+ messages in thread

* Re: [PATCH v2 30/39] xen/riscv: prepare new IMSIC VS-file
  2026-09-17 14:50     ` Oleksii Kurochko
@ 2026-09-18  6:02       ` Jan Beulich
  0 siblings, 0 replies; 244+ messages in thread
From: Jan Beulich @ 2026-09-18  6:02 UTC (permalink / raw)
  To: Oleksii Kurochko
  Cc: Romain Caritey, Baptiste Le Duc, Zheng Zhang, Alistair Francis,
	Connor Davis, Andrew Cooper, Anthony PERARD, Michal Orzel,
	Julien Grall, Roger Pau Monné, Stefano Stabellini, xen-devel

On 17.09.2026 16:50, Oleksii Kurochko wrote:
> On 9/14/26 3:13 PM, Jan Beulich wrote:
>> On 27.08.2026 17:21, Oleksii Kurochko wrote:
>>> +#define imsic_switchcase_break(ireg, op, v) \
>>> +    case ireg:                              \
>>> +        op(ireg, v);                        \
>>> +        break;
>>> +
>>> +#define imsic_switchcase_ret(ireg, op, ...) \
>>> +    case ireg:                              \
>>> +        return op(ireg, ##__VA_ARGS__);
>>> +
>>> +#define imsic_switchcase_2(F, ireg, ...)    \
>>> +    F(ireg + 0, ##__VA_ARGS__)              \
>>> +    F(ireg + 1, ##__VA_ARGS__)
>>
>> Ah, there is an F here.
>>
>> This (recurring below) shows another problem: The two F invocations
>> look syntacticlly incorrect, due to the missing semicolon. Semicolon
>> use wants redoing everywhere here.
> 
> I will drop then ';' from imsic_switchcase_{break,ret}.

Provided that works, i.e. you have no cae where __VA_ARGS__ expands to
nothing.

>>> +static void cf_check imsic_vsfile_local_clear(void *data)
>>> +{
>>> +    unsigned int i;
>>> +    const struct imsic_vsfile_data *idata = data;
>>> +    unsigned long new_hstatus, old_hstatus, old_vsiselect;
>>> +
>>> +    /* We can only zero-out if we have a IMSIC VS-file */
>>> +    if ( !idata->hgei )
>>> +        return;
>>
>> Wouldn't it make sense to avoid the call here altogether then?
> 
> I think it is better to have this if () here instead of the caller side 
> as this function one day could be just directly (w/ imsic_call_on_cpu) 
> and even the way how it is called now and in the case of 
> imsic_call_on_cpu() is executed on local cpu then it will be basically 
> just direct call of imsic_vsfile_local_clear(). So in the case I am not 
> missing something I prefer to have a check here.
> 
>>
>>> +    old_vsiselect = csr_read(CSR_VSISELECT);
>>
>> Likely obvious to you, but I can't spot why vsiselect would need saving
>> here. If you want me to ack such code, please add at least brief comments.
> 
> I think then it will be better to put the comment once above struct 
> imsic_vsfile_data and then just point here to that comment as basically 
> it will be needed for all imsic_vsfile_local_* helpers.
> 
> So basically I am suggesting:
> 
> --- a/xen/arch/riscv/imsic.c
> +++ b/xen/arch/riscv/imsic.c
> @@ ... @@
>   /*
>    * Arguments of the imsic_vsfile_local_*() helpers, which are executed 
> by the
>    * pCPU owning the interrupt file, thereby through imsic_call_on_cpu().
> + *
> + * The helpers interrupt whatever vCPU context is loaded on that pCPU, 
> which
> + * generally isn't the vCPU the interrupt file belongs to. To reach the 
> file
> + * they retarget hstatus.VGEIN and select the file's registers through
> + * vsiselect. Both CSRs are live state of the interrupted vCPU 
> (vsiselect is
> + * saved to struct arch_vcpu only on context switch, but the 
> interrupted vCPU
> + * may resume guest execution without one), hence the helpers have to 
> restore
> + * them before returning.
>    */
>   struct imsic_vsfile_data {
>       unsigned int hgei;
>       unsigned int nr_eix;
>       struct imsic_mrif *mrif;
>   };
> @@ ... @@ static void cf_check imsic_vsfile_local_clear(void *data)
>       /* We can only zero-out if we have a IMSIC VS-file */
>       if ( !idata->hgei )
>           return;
> 
> +    /* See the comment ahead of struct imsic_vsfile_data. */
>       old_vsiselect = csr_read(CSR_VSISELECT);
>       old_hstatus = csr_read(CSR_HSTATUS);
> @@ ... @@ static void cf_check imsic_vsfile_local_read_clear(void *data)
>       csr_clear(CSR_HGEIE, BIT(idata->hgei, UL));
> 
> +    /* See the comment ahead of struct imsic_vsfile_data. */
>       old_vsiselect = csr_read(CSR_VSISELECT);
>       old_hstatus = csr_read(CSR_HSTATUS);
> @@ ... @@ static void cf_check imsic_vsfile_local_update(void *data)
>        * stack.
>        */
> 
> +    /* See the comment ahead of struct imsic_vsfile_data. */
>       old_vsiselect = csr_read(CSR_VSISELECT);
>       old_hstatus = csr_read(CSR_HSTATUS);
> 
> 
> Does it look clear now?

Not really, I'm afraid. A much shorter comment mentioning that
imsic_..._write() alter vsiselect (if I got things right) would imo
be both more direct, more clear, and could easily live at all three
sites individually.

>>> +    old_hstatus = csr_read(CSR_HSTATUS);
>>> +    new_hstatus = old_hstatus & ~HSTATUS_VGEIN;
>>> +    new_hstatus |= MASK_INSR(idata->hgei, HSTATUS_VGEIN);
>>> +    csr_write(CSR_HSTATUS, new_hstatus);
>>> +
>>> +    imsic_vs_csr_write(IMSIC_EIDELIVERY, 0);
>>> +    imsic_vs_csr_write(IMSIC_EITHRESHOLD, 0);
>>> +
>>> +    for ( i = 0; i < idata->nr_eix; i++ )
>>> +    {
>>> +        imsic_eix_write(IMSIC_EIP0 + i * 2, 0);
>>> +        imsic_eix_write(IMSIC_EIE0 + i * 2, 0);
>>> +#ifdef CONFIG_RISCV_32
>>> +        imsic_eix_write(IMSIC_EIP0 + i * 2 + 1, 0);
>>> +        imsic_eix_write(IMSIC_EIE0 + i * 2 + 1, 0);
>>> +#endif
>>
>> In asm/imsic.h I see
>>
>> #define IMSIC_EIPx_BITS         32
>>
>> Why is the number of CSR writes different here for RV32 vs RV64? 
> 
> IMSIC_EIPx_BITS is the unit the AIA spec numbers the eip<k>/eie<k> 
> registers by. On RV32 all of eip0..eip63 exist and are 32 bits wide. On 
> RV64 only the even-numbered ones exist, each being 64 bits wide and 
> covering what eip<k> and eip<k+1> cover on RV32; accessing an 
> odd-numbered one is an illegal instruction. Hence one 64-bit group of 
> interrupt identities takes one register on RV64, but two on RV32.
> 
> I will add some small comments:
> 
>      for ( i = 0; i < idata->nr_eix; i++ )
>      {
>          /* On RV64 a 64-bit EIx group is the even-numbered register 
> alone. */
>          imsic_eix_write(IMSIC_EIP0 + i * 2, 0);
>          imsic_eix_write(IMSIC_EIE0 + i * 2, 0);
> #ifdef CONFIG_RISCV_32
>          /*
>           * On RV32 it is split into the even-numbered (low half) and the
>           * following odd-numbered (high half) register.
>           */
>          imsic_eix_write(IMSIC_EIP0 + i * 2 + 1, 0);
>          imsic_eix_write(IMSIC_EIE0 + i * 2 + 1, 0);
> #endif
>      }
> 
>> And
>> if so, why would you not use the 64-bit write function, allowing the
>> #ifdef to be omitted?
> 
> I can introduce something like:
> 
> /*
>   * On RV64 a 64-bit EIx group is the even-numbered register alone, whereas
>   * on RV32 it is split into the even-numbered (low half) and the following
>   * odd-numbered (high half) register.
>   */
> static void imsic_eix_write64(unsigned int ireg, uint64_t val)
> {
>      imsic_eix_write(ireg, val);
>      if ( IS_ENABLED(CONFIG_RISCV_32) )
>          imsic_eix_write(ireg + 1, val >> 32);
> }
> 
> And then:
> 
>      for ( i = 0; i < idata->nr_eix; i++ )
>      {
>          imsic_eix_write64(IMSIC_EIP0 + i * 2, 0);
>          imsic_eix_write64(IMSIC_EIE0 + i * 2, 0);
>      }
> 
> Would it be better?

Imo yes. That said, when making the original comment, I was (apparently
wrongly, as per the stuff further up) assuming these are direct CSR
writes.

Jan


^ permalink raw reply	[flat|nested] 244+ messages in thread

* Re: [PATCH v2 23/39] xen/riscv: look up the exception table for any trap taken in Xen context
  2026-08-27 15:21 ` [PATCH v2 23/39] xen/riscv: look up the exception table for any trap taken in Xen context Oleksii Kurochko
  2026-09-10 15:31   ` Jan Beulich
@ 2026-09-18  8:44   ` Baptiste Le Duc
  2026-09-22  9:31     ` Oleksii Kurochko
  1 sibling, 1 reply; 244+ messages in thread
From: Baptiste Le Duc @ 2026-09-18  8:44 UTC (permalink / raw)
  To: Oleksii Kurochko
  Cc: xen-devel, Romain Caritey, Baptiste Le Duc, Zheng Zhang,
	Alistair Francis, Connor Davis, Andrew Cooper, Anthony PERARD,
	Michal Orzel, Jan Beulich, Julien Grall, Roger Pau Monné,
	Stefano Stabellini

> do_trap() consulted the exception table only for CAUSE_ILLEGAL_INSTRUCTION,
> which covers csr_read_safe() but not the hlv/hlvx sequences reading guest
> memory: those fault with load/store (guest) page fault causes and would
> reach do_unexpected_trap() instead of their fixup.
> 
> Move the lookup ahead of the cause switch, and gate it on the trap having
> been taken in Xen context and not being an interrupt:
> 
> - sepc of a trap taken from the guest is a guest VA/PA, which the
>   guest can point at an address listed in the exception table; Xen would
>   then act on that entry and, for EX_TYPE_TRAP_INFO, write through a
>   pointer fully under guest control. Entries are matched by exact address,
>   so this needs no more than a numerical collision.
> 
> - an interrupt taken at an address listed in the table would otherwise be
>   "fixed up" as if the access itself had faulted, silently skipping it and
>   handing the caller the interrupt's scause as a fault cause.
> 
> Returning early skips check_for_pcpu_work(), which is correct: that only
> runs for traps taken from the guest.
> 
> With that in place a G-stage fault reaching the switch can no longer have
> been caused by an hlv/hlvx covered by an entry, so anything left must have
> come from the guest; assert as much.
What means `assert as much` here? As you referencing any assert in the code,
because in this patch, I couldn't find any.
> 
> Cache the "trap came from the guest" test in a local, it is now used four
> times.
Nit: By reading this sentence, I would expect to have the introduction
of `from_guest` local here instead of in patch cc2d3b97e8
xen/riscv: add guest page fault handling stub

-- 
Baptiste Le Duc <baptiste.le-duc@vates.tech>


^ permalink raw reply	[flat|nested] 244+ messages in thread

* Re: [PATCH v2 24/39] xen/riscv: add helpers for decoding a trapped load or store
  2026-08-27 15:21 ` [PATCH v2 24/39] xen/riscv: add helpers for decoding a trapped load or store Oleksii Kurochko
  2026-09-14 11:03   ` Jan Beulich
@ 2026-09-18  8:44   ` Baptiste Le Duc
  2026-09-22 11:03     ` Oleksii Kurochko
  1 sibling, 1 reply; 244+ messages in thread
From: Baptiste Le Duc @ 2026-09-18  8:44 UTC (permalink / raw)
  To: Oleksii Kurochko
  Cc: xen-devel, Romain Caritey, Baptiste Le Duc, Zheng Zhang,
	Alistair Francis, Connor Davis, Andrew Cooper, Anthony PERARD,
	Michal Orzel, Jan Beulich, Julien Grall, Roger Pau Monné,
	Stefano Stabellini

> emulate_load() and emulate_store() will both need to obtain the
> instruction which caused a guest MMIO trap, decode it, and locate the
> register operand it names. Add what the two share, ahead of either of
> them being implemented: struct decoded_insn, insn_fetch_faulted(),
> decode_ldst_insn(), guest_xlen(), guest_gpr() and advance_pc().
> 
> The mask/match chain is adapted from Linux's KVM RISC-V implementation.
Nit: maybe you could add the Origin: trailer as mentioned in the
sending-patches.adoc.
> 
> Nothing calls any of this yet, so tag the functions __maybe_unused to
> keep the build going; the tags go away once emulate_load() and
> emulate_store() gain their bodies later.


> 
> Signed-off-by: Oleksii Kurochko <oleksii.kurochko@gmail.com>
>
> diff --git a/xen/arch/riscv/emulate.c b/xen/arch/riscv/emulate.c
> index ff530ef2df..81a50643a5 100644
> --- a/xen/arch/riscv/emulate.c
> +++ b/xen/arch/riscv/emulate.c
> @@ -5,6 +5,7 @@
>   */
>  
>  #include <xen/bug.h>
> +#include <xen/compiler.h>
>  #include <xen/errno.h>
>  #include <xen/sched.h>
>  #include <xen/types.h>
> @@ -13,9 +14,29 @@
>  #include <asm/csr.h>
>  #include <asm/current.h>
>  #include <asm/emulate.h>
> +#include <asm/guest_access.h>
> +#include <asm/processor.h>
>  #include <asm/riscv_encoding.h>
>  #include <asm/traps.h>
>  
> +/*
> + * Determine the trapped load or store instruction which caused a guest MMIO
> + * trap.
> + */
> +struct decoded_insn {
> +    /* The instruction itself, and its length in bytes. */
> +    unsigned long insn;
> +    unsigned int insn_len;
> +    /* Width of the memory access, in bytes. */
> +    unsigned int len;
> +    /* Number of the register operand: rd for a load, rs2 for a store. */
> +    unsigned int reg;
> +    /* The access is a store rather than a load. */
> +    bool is_write;
> +    /* The load zero-extends its result rather than sign-extending it. */
> +    bool is_unsigned;
> +};


> +
>  /*
>   * The hardware-reported details of a guest page fault, gathered once by
>   * handle_guest_page_fault() and passed down to the emulation of the faulted
> @@ -39,6 +60,71 @@ struct guest_fault {
>      paddr_t gpa;
>  };
>  
> +static bool is_load_guest_page_fault(unsigned long scause)
> +{
> +    return scause == CAUSE_LOAD_GUEST_PAGE_FAULT;
> +}


> +
> +static __maybe_unused void advance_pc(struct cpu_user_regs *regs,
> +                                      unsigned int step)
> +{
> +    regs->sepc += step;
> +}
> +
> +/*
> + * The effective XLEN of the guest at the point of the trap: hstatus.VSXL for a
> + * trap taken from VS-mode, vsstatus.UXL for one taken from VU-mode.
> + *
> + * VSXL is consulted whichever mode the trap came from, as it also gives the
> + * width of vsstatus itself: where VSXL says 32, that register has no UXL field
> + * to consult and VU-mode is 32-bit as well, there being nothing to configure.
> + *
> + * It is needed to decode a trapped instruction: the encodings which exist only
> + * for XLEN=64 must not be recognized for a 32-bit guest. Besides those simply
> + * being reserved there, the compressed ones are ambiguous: C.LD and C.FLW
> + * share the encoding 0x6000 (mask 0xe003), and likewise C.SD/C.FSW,
> + * C.LDSP/C.FLWSP and C.SDSP/C.FSWSP.
> + *
> + * IS_ENABLED() can't be used here as HSTATUS_VSXL is defined for
> + * __riscv_xlen == 64 only, the field not existing on RV32 in the first place.
> + */
> +static __maybe_unused unsigned int guest_xlen(const struct cpu_user_regs *regs)
> +{
> +#ifdef CONFIG_RISCV_32
> +    return 32;
> +#else
> +    unsigned long xl = MASK_EXTR(regs->hstatus, HSTATUS_VSXL);
> +
> +    if ( (xl == XLEN_FIELD_64) && !(regs->sstatus & SSTATUS_SPP) )


> +        xl = MASK_EXTR(csr_read(CSR_VSSTATUS), SSTATUS64_UXL);
> +
> +    switch ( xl )
> +    {
> +    case XLEN_FIELD_32:
> +        return 32;
> +
> +    case XLEN_FIELD_64:
> +        return 64;
> +
> +    default:
> +        /*
> +         * The field holds nothing else in practice: XLEN_FIELD_128 would mean
> +         * RV128, which no implementation provides, and the only value left is
> +         * reserved. ASSERT_UNREACHABLE() being debug-only, a width still has
> +         * to be answered in release builds.
> +         *
> +         * Answer 32, that being the safe way to be wrong: the decoder then
> +         * fails to recognize the RV64-only encodings and emulation gives up.
> +         * Answering 64 for what may well be a 32-bit guest would instead have
> +         * it take C.FLW for C.LD and C.FSW for C.SD (see above), i.e. quietly
> +         * emulate an access of the wrong width against the wrong register.
> +         */
> +        ASSERT_UNREACHABLE();
> +        return 32;
> +    }
> +#endif
> +}
> +
>  /*
>   * Is @htinst one of the pseudoinstructions reported for a guest page fault
>   * taken on an implicit memory access done for VS-stage address translation?
> @@ -87,6 +173,250 @@ static void resolve_faulting_gpa(struct guest_fault *gf)
>                (htinst_is_pseudo(gf->htinst) ? 0 : (gf->stval & 3));
>  }
>  
> +/*
> + * Where the value of a decoded instruction's register operand is held.
> + *
> + * Relies on x0..x31 being laid out at the start of struct cpu_user_regs in
> + * architectural register-number order; see the comment there.
> + */
> +static __maybe_unused unsigned long *guest_gpr(struct cpu_user_regs *regs,
> +                                               unsigned int reg)
> +{
> +    ASSERT(reg < 32);
> +
> +    return REG_PTR(reg, 0, regs);
> +}


> +
> +/*
> + * Obtain the instruction which caused a guest MMIO trap, filling in
> + * @di->insn and @di->insn_len. It either comes transformed in htinst, or has
> + * to be fetched from guest memory.
> + *
> + * Returns true if the fetch faulted in turn; the resulting trap has then
> + * already been redirected to the guest and there is nothing further for the
> + * caller to do. Where it returns false, @di has been filled in and emulation
> + * is to continue.
> + */
> +static bool __maybe_unused insn_fetch_faulted(const struct guest_fault *gf,
> +                                              struct decoded_insn *di)
> +{
> +    unsigned long htinst = gf->htinst;
> +
> +    /*
> +     * A pseudoinstruction says nothing about the instruction the guest was
> +     * executing, and comes with a guest physical address which isn't the one
> +     * that instruction accessed. handle_guest_page_fault() deals with such a
> +     * fault on its own, so no emulation can ever start for one.
> +     */
> +    ASSERT(!htinst_is_pseudo(htinst));
> +
> +    if ( htinst & BIT(0, UL) )
> +    {
> +        /*
> +         * Bit[0] == 1 implies trapped instruction value is
> +         * transformed instruction or custom instruction.
> +         *
> +         * The transformation always yields the 32-bit format, with bits[1:0]
> +         * holding a marker instead of the original opcode bits: bit[0] set to
> +         * flag the transformation, bit[1] clear if the trapped instruction
> +         * was a compressed one. Restoring the opcode bits makes the value the
> +         * valid 32-bit encoding decode_ldst_insn() matches against. Its
> +         * INSN_MASK_C_* cases exist for the branch below, where a compressed
> +         * instruction is read from guest memory as is: a trapped one arrives
> +         * here already expanded to its 32-bit equivalent, and the opcode bits
> +         * just restored keep it from matching those cases anyway.
> +         *
> +         * The length then cannot come from the value anymore, only from
> +         * bit[1]. And only a 16- or a 32-bit instruction is ever reported
> +         * this way: the standard load and store instructions the hardware
> +         * transforms are all of one of these two lengths, anything else comes
> +         * as the zero special value handled below.
> +         */
> +        di->insn = htinst | INSN_16BIT_MASK;
> +        di->insn_len = (htinst & BIT(1, UL)) ? 4 : 2;


> +    }
> +    else
> +    {
> +        const struct cpu_user_regs *regs = gf->regs;
> +        struct trap_info utrap = {};
> +
> +        /*
> +         * Bit[0] == 0 implies trapped instruction value is
> +         * zero or special value. With the pseudoinstructions ruled out
> +         * above, only zero is left: the instruction has to be read from
> +         * guest memory.
> +         */
> +
> +        di->insn = riscv_read_guest(regs->sepc, true, &utrap);
> +        if ( utrap.scause )
> +        {
> +            /*
> +             * If during getting of trapped instruction a fault happen in
> +             * G-stage translation then CAUSE_LOAD_GUEST_PAGE_FAULT is
> +             * generated. Such faults during this operation is considered as
> +             * bus error.
> +             */
> +            if ( is_load_guest_page_fault(utrap.scause) )
> +                utrap.scause = CAUSE_FETCH_ACCESS;
> +
> +            utrap.sepc = regs->sepc;


> +
> +            trap_redirect(&utrap);
> +
> +            return true;
> +        }
> +
> +        /*
> +         * riscv_read_guest() fetches at most two halfwords, so a wider
> +         * encoding has been read in part only and cannot be decoded here.
> +         *
> +         * Report an illegal instruction, which is what the guest would have
> +         * got for such an encoding anyway: the ISA defines no instruction
> +         * wider than 32 bits.
> +         */


> +        if ( !INSN_IS_16BIT(di->insn) && !INSN_IS_32BIT(di->insn) )
> +        {
> +            utrap.sepc = regs->sepc;


> +            utrap.scause = CAUSE_ILLEGAL_INSTRUCTION;
> +            /*
> +             * stval is left zero: the spec allows that for an illegal
> +             * instruction, and only part of the instruction is in hand.
> +             */


> +
> +            trap_redirect(&utrap);
> +
> +            return true;
> +        }
> +
> +        di->insn_len = INSN_LEN(di->insn);


> +    }
> +
> +    return false;
> +}
> +
> +/*
> + * Decode the load or store instruction fetched into @di, filling in the
> + * remaining fields of it (@di->insn and @di->insn_len are filled by
> + * insn_fetch_faulted()).
> + *
> + * @xlen is the effective XLEN of the guest, needed as
> + * the encodings which exist for XLEN=64 only must not be recognized for a
> + * 32-bit guest.
> + *
> + * Returns false if the instruction is not a load or store which can be
> + * emulated here.
> + */
> +static __maybe_unused bool decode_ldst_insn(struct decoded_insn *di,
> +                                            unsigned int xlen)
> +{
> +    unsigned long insn = di->insn;
> +    /* Register fields of the uncompressed forms ... */
> +    unsigned int rd = RV_RD(insn);
> +    unsigned int rs2 = RV_RS2(insn);
> +    /*
> +     * ... and of the compressed ones, where the 3-bit field selects one of
> +     * x8..x15, while the stack-pointer-relative forms have a full-width one.
> +     */
> +    unsigned int rs2s = RVC_RS2S(insn);
> +    unsigned int rs2c = RVC_RS2(insn);
> +
This naming are confusing because above you described di->reg to be rd
for load and rs2 for store but here ...
> +    di->is_write = false;
> +    di->is_unsigned = false;


> +    di->reg = rd;
> +
> +    if ( (insn & INSN_MASK_LB) == INSN_MATCH_LB )
> +        di->len = 1;
> +    else if ( (insn & INSN_MASK_LBU) == INSN_MATCH_LBU )
> +    {
> +        di->len = 1;
> +        di->is_unsigned = true;
> +    }
> +    else if ( (insn & INSN_MASK_LH) == INSN_MATCH_LH )
> +        di->len = 2;
> +    else if ( (insn & INSN_MASK_LHU) == INSN_MATCH_LHU )
> +    {
> +        di->len = 2;
> +        di->is_unsigned = true;
> +    }
> +    else if ( (insn & INSN_MASK_LW) == INSN_MATCH_LW )
> +        di->len = 4;
> +    else if ( xlen == 64 && (insn & INSN_MASK_LWU) == INSN_MATCH_LWU )
> +    {
> +        di->len = 4;
> +        di->is_unsigned = true;
> +    }


> +    else if ( (insn & INSN_MASK_C_LW) == INSN_MATCH_C_LW )
> +    {
> +        di->len = 4;


> +        di->reg = rs2s;
... you assigned rs2s for a load. According to the spec, it should be rd'.

I would suggest something generic to load and store. Maybe rxs with a comment to explain it concerns rs2' for store and rd' for load.

    /*
     * ... and of the compressed ones, where the 3-bit field selects one of
     * x8..x15, while the stack-pointer-relative forms have a full-width one.
     * rxs is named after that field's spec mnemonic, rd'/rs2': rd' for
     * compressed loads, rs2' for compressed stores.
     */
    unsigned int rxs = RVC_RS2S(insn);

> +    }
> +    /* c.lwsp and c.ldsp are reserved with rd being x0. */
> +    else if ( (insn & INSN_MASK_C_LWSP) == INSN_MATCH_C_LWSP && rd )
> +        di->len = 4;
> +    else if ( xlen == 64 && (insn & INSN_MASK_LD) == INSN_MATCH_LD )
> +        di->len = 8;
> +    else if ( xlen == 64 && (insn & INSN_MASK_C_LD) == INSN_MATCH_C_LD )
> +    {
> +        di->len = 8;
> +        di->reg = rs2s;
> +    }
> +    else if ( xlen == 64 && (insn & INSN_MASK_C_LDSP) == INSN_MATCH_C_LDSP &&
> +              rd )
> +        di->len = 8;
> +    else if ( (insn & INSN_MASK_SB) == INSN_MATCH_SB )
> +    {
> +        di->len = 1;
> +        di->is_write = true;
> +        di->reg = rs2;
> +    }
> +    else if ( (insn & INSN_MASK_SH) == INSN_MATCH_SH )
> +    {
> +        di->len = 2;
> +        di->is_write = true;
> +        di->reg = rs2;
> +    }
> +    else if ( (insn & INSN_MASK_SW) == INSN_MATCH_SW )
> +    {
> +        di->len = 4;
> +        di->is_write = true;
> +        di->reg = rs2;
> +    }
> +    else if ( (insn & INSN_MASK_C_SW) == INSN_MATCH_C_SW )
> +    {
> +        di->len = 4;
> +        di->is_write = true;
> +        di->reg = rs2s;
> +    }
> +    else if ( (insn & INSN_MASK_C_SWSP) == INSN_MATCH_C_SWSP )
> +    {
> +        di->len = 4;
> +        di->is_write = true;
> +        di->reg = rs2c;
> +    }
> +    else if ( xlen == 64 && (insn & INSN_MASK_SD) == INSN_MATCH_SD )
> +    {
> +        di->len = 8;
> +        di->is_write = true;
> +        di->reg = rs2;
> +    }
> +    else if ( xlen == 64 && (insn & INSN_MASK_C_SD) == INSN_MATCH_C_SD )
> +    {
> +        di->len = 8;
> +        di->is_write = true;
> +        di->reg = rs2s;
> +    }
> +    else if ( xlen == 64 && (insn & INSN_MASK_C_SDSP) == INSN_MATCH_C_SDSP )
> +    {
> +        di->len = 8;
> +        di->is_write = true;
> +        di->reg = rs2c;
> +    }
> +    else
> +        return false;
> +
> +    return true;
> +}
> +
>  static int emulate_load(const struct guest_fault *gf)
>  {
>      return -EOPNOTSUPP;
> diff --git a/xen/arch/riscv/include/asm/guest_access.h b/xen/arch/riscv/include/asm/guest_access.h
> index 8d679319de..39c28dd2ec 100644
> --- a/xen/arch/riscv/include/asm/guest_access.h
> +++ b/xen/arch/riscv/include/asm/guest_access.h
> @@ -5,6 +5,7 @@
>  #include <xen/types.h>
>  
>  struct domain;
> +struct trap_info;
>  
>  unsigned long raw_copy_to_guest(void *to, const void *from, unsigned len);
>  unsigned long raw_copy_from_guest(void *to, const void *from, unsigned len);
> @@ -25,6 +26,9 @@ unsigned long raw_clear_guest(void *to, unsigned int len);
>  unsigned long copy_to_guest_phys(struct domain *d, paddr_t gpa, void *buf,
>                                   unsigned long len);
>  
> +unsigned long riscv_read_guest(unsigned long guest_addr, bool read_insn,
> +                               struct trap_info *trap);
> +
>  #endif /* ASM__RISCV__GUEST_ACCESS_H */
>  /*
>   * Local variables:
> diff --git a/xen/arch/riscv/include/asm/riscv_encoding.h b/xen/arch/riscv/include/asm/riscv_encoding.h
> index b2071f4758..656a5fcccb 100644
> --- a/xen/arch/riscv/include/asm/riscv_encoding.h
> +++ b/xen/arch/riscv/include/asm/riscv_encoding.h
> @@ -65,6 +65,14 @@
>  #define SSTATUS64_UXL			MSTATUS_UXL
>  #define SSTATUS64_SD			MSTATUS64_SD
>  
> +/*
> + * Width encoded by the MXL, SXL, UXL and VSXL fields, all of which share one
> + * encoding. 0 is reserved.
> + */
> +#define XLEN_FIELD_32			_UL(1)
> +#define XLEN_FIELD_64			_UL(2)
> +#define XLEN_FIELD_128			_UL(3)
> +
>  #if __riscv_xlen == 64
>  #define HSTATUS_VSXL			_UL(0x300000000)
>  #define HSTATUS_VSXL_SHIFT		32
> @@ -896,6 +904,8 @@
>  					 (RV_X(x, 7, 2) << 6))
>  #define RVC_SDSP_IMM(x)			((RV_X(x, 10, 3) << 3) | \
>  					 (RV_X(x, 7, 3) << 6))
> +#define RV_RD(insn)			RV_X(insn, SH_RD, 5)
> +#define RV_RS2(insn)			RV_X(insn, SH_RS2, 5)
Nit: format

-- 
Baptiste Le Duc <baptiste.le-duc@vates.tech>


^ permalink raw reply	[flat|nested] 244+ messages in thread

* Re: [PATCH v2 25/39] xen/riscv: add guest load emulation for trapped MMIO accesses
  2026-08-27 15:21 ` [PATCH v2 25/39] xen/riscv: add guest load emulation for trapped MMIO accesses Oleksii Kurochko
  2026-09-14 11:48   ` Jan Beulich
@ 2026-09-18  9:16   ` Baptiste Le Duc
  2026-09-22 11:22     ` Oleksii Kurochko
  1 sibling, 1 reply; 244+ messages in thread
From: Baptiste Le Duc @ 2026-09-18  9:16 UTC (permalink / raw)
  To: Oleksii Kurochko
  Cc: xen-devel, Romain Caritey, Baptiste Le Duc, Zheng Zhang,
	Alistair Francis, Connor Davis, Andrew Cooper, Anthony PERARD,
	Michal Orzel, Jan Beulich, Julien Grall, Roger Pau Monné,
	Stefano Stabellini

> Implement emulate_load() on top of the decoding interface introduced by
> the previous patch: fetch the trapped instruction, decode it, dispatch
> the access to a registered MMIO handler via do_mmio(), write the result
> back into the destination register and step over the instruction.
> 
> Xen dispatches MMIO synchronously to an in-hypervisor handler, so unlike
> KVM RISC-V there is no userspace exit/return step and no equivalent of
> the kvm_io_bus_read() / KVM_EXIT_MMIO / kvm_riscv_vcpu_mmio_return()
> split; the result is consumed in place.
> 
> Sign extension is done here rather than in the handlers: a signed load
> is normalized by a shift pair, so a handler need only report the value
> it read.
> 
> At the moment vINTC is the only backend registered with the MMIO
> dispatch, so in practice this only covers vINTC traps. An access which
> no handler claims currently crashes the domain; injecting an access
> fault into the guest instead is left for later.
> 
> Signed-off-by: Oleksii Kurochko <oleksii.kurochko@gmail.com>
>
> diff --git a/xen/arch/riscv/emulate.c b/xen/arch/riscv/emulate.c
> index 81a50643a5..e52f285180 100644
> --- a/xen/arch/riscv/emulate.c
> +++ b/xen/arch/riscv/emulate.c
> @@ -5,7 +5,6 @@
>   */
>  
>  #include <xen/bug.h>
> -#include <xen/compiler.h>
>  #include <xen/errno.h>
>  #include <xen/sched.h>
>  #include <xen/types.h>
> @@ -15,6 +14,7 @@
>  #include <asm/current.h>
>  #include <asm/emulate.h>
>  #include <asm/guest_access.h>
> +#include <asm/mmio.h>
>  #include <asm/processor.h>
>  #include <asm/riscv_encoding.h>
>  #include <asm/traps.h>
> @@ -65,8 +65,7 @@ static bool is_load_guest_page_fault(unsigned long scause)
>      return scause == CAUSE_LOAD_GUEST_PAGE_FAULT;
>  }
>  
> -static __maybe_unused void advance_pc(struct cpu_user_regs *regs,
> -                                      unsigned int step)
> +static void advance_pc(struct cpu_user_regs *regs, unsigned int step)
>  {
>      regs->sepc += step;
>  }
> @@ -88,7 +87,7 @@ static __maybe_unused void advance_pc(struct cpu_user_regs *regs,
>   * IS_ENABLED() can't be used here as HSTATUS_VSXL is defined for
>   * __riscv_xlen == 64 only, the field not existing on RV32 in the first place.
>   */
> -static __maybe_unused unsigned int guest_xlen(const struct cpu_user_regs *regs)
> +static unsigned int guest_xlen(const struct cpu_user_regs *regs)
>  {
>  #ifdef CONFIG_RISCV_32
>      return 32;
> @@ -179,8 +178,7 @@ static void resolve_faulting_gpa(struct guest_fault *gf)
>   * Relies on x0..x31 being laid out at the start of struct cpu_user_regs in
>   * architectural register-number order; see the comment there.
>   */
> -static __maybe_unused unsigned long *guest_gpr(struct cpu_user_regs *regs,
> -                                               unsigned int reg)
Could we unified this function with regs_get_gpr()?

-- 
Baptiste Le Duc <baptiste.le-duc@vates.tech>


^ permalink raw reply	[flat|nested] 244+ messages in thread

* Re: [PATCH v2 26/39] xen/riscv: add guest store emulation for trapped MMIO accesses
  2026-08-27 15:21 ` [PATCH v2 26/39] xen/riscv: add guest store " Oleksii Kurochko
  2026-09-14 12:01   ` Jan Beulich
@ 2026-09-18  9:16   ` Baptiste Le Duc
  2026-09-22 11:38     ` Oleksii Kurochko
  1 sibling, 1 reply; 244+ messages in thread
From: Baptiste Le Duc @ 2026-09-18  9:16 UTC (permalink / raw)
  To: Oleksii Kurochko
  Cc: xen-devel, Romain Caritey, Baptiste Le Duc, Zheng Zhang,
	Alistair Francis, Connor Davis, Andrew Cooper, Anthony PERARD,
	Michal Orzel, Jan Beulich, Julien Grall, Roger Pau Monné,
	Stefano Stabellini

> Extend the guest page fault handler with store emulation to support MMIO
> write accesses.
> 
> The instruction decode mirrors emulate_load() and, like it, is adapted
> from Linux's KVM RISC-V implementation. As with the load path, the
> completion is synchronous through try_handle_mmio() rather than KVM's
> userspace exit/return split, since Xen's MMIO handlers run in the
> hypervisor. Faults taken while re-reading the trapped instruction are
> handled by decode_ldst_insn(), shared with the load path.
> 
> When a guest store instruction faults, the trapped instruction is decoded
> using HTINST or, if unavailable, fetched via unprivileged access. At the
Nit: commit message restates the HTINST-or-unprivileged-fetch decode
mechanism, which is already described in the prep patch introducing
decode_ldst_insn()/insn_fetch_faulted(), and isn't repeated in
emulate_load()'s commit message. Suggest trimming for symmetry with the
load commit, e.g.:

  When a guest store instruction faults, the trapped instruction is
  decoded via decode_ldst_insn(), shared with the load path. At the
  moment only virtual interrupt controller (vINTC) traps are expected to
  occur, since it is currently the only backend registered with the MMIO
  handler dispatch, so in practice the store is emulated via the vINTC
  backend.

-- 
Baptiste Le Duc <baptiste.le-duc@vates.tech>


^ permalink raw reply	[flat|nested] 244+ messages in thread

* Re: [PATCH v2 28/39] xen/riscv: handle the case when no vCPU migration is needed
  2026-09-14 12:12   ` Jan Beulich
  2026-09-16  5:55     ` Oleksii Kurochko
@ 2026-09-18  9:21     ` Baptiste Le Duc
  1 sibling, 0 replies; 244+ messages in thread
From: Baptiste Le Duc @ 2026-09-18  9:21 UTC (permalink / raw)
  To: Jan Beulich
  Cc: Oleksii Kurochko, Romain Caritey, Baptiste Le Duc, Zheng Zhang,
	Alistair Francis, Connor Davis, Andrew Cooper, Anthony PERARD,
	Michal Orzel, Julien Grall, Roger Pau Monné,
	Stefano Stabellini, xen-devel

On 2026-09-14 14:12:07+02:00, Jan Beulich wrote:
> On 27.08.2026 17:21, Oleksii Kurochko wrote:
> 
> > The IMSIC vsfile mapping is performed in continue_new_vcpu(), since the
> > target pCPU must be known at that point. It is therefore possible for
> > imsic_migrate_vcpu() to be called before continue_new_vcpu() has
> > executed, in which case v->arch.last_pcpu is NR_CPUS and there is nothing
> > to migrate.
> > 
> > Add a BUG_ON("unimplemented") placeholder in imsic_migrate_vcpu() to guard
> > against silent incorrect behaviour or unexpected panics in guest VMs until
> > the function is fully implemented.
> 
> This doesn't adequately describe the change made: The BUG_ON() was already
> there.
> 
> > --- a/xen/arch/riscv/imsic.c
> > +++ b/xen/arch/riscv/imsic.c
> > @@ -689,5 +689,15 @@ int __init vimsic_make_domu_dt_node(struct kernel_info *kinfo,
> >  
> >  void imsic_migrate_vcpu(struct vcpu *v)
> >  {
> > +    /*
> > +     * The scheduler can mark a freshly created vCPU's unit as migrated and
> > +     * invoke this before the vCPU has ever run (see the migrated branch in
> > +     * schedule()). No need to do migration for such vCPUs as they aren't fully
> > +     * initialized (for example, context_switch() will be called after
> > +     * imsic_migrate_vcpu()).
> > +     */
> > +    if ( v->arch.last_cpu == NR_CPUS )
> 
> May I suggest to use >= ? I'm still somewhat unconvinced of NR_CPUS being a
> good sentinel. If we/you decided to switch to ~0, >= here would continue to
> be correct.
I also agree that ~0 would be better.
> 
> Jan




^ permalink raw reply	[flat|nested] 244+ messages in thread

* Re: [PATCH v2 31/39] xen/riscv: implement APLIC-hart sync barrier for vCPU migration
  2026-09-14 13:27   ` Jan Beulich
@ 2026-09-18 11:53     ` Oleksii Kurochko
  2026-09-22 17:03       ` Baptiste Le Duc
  0 siblings, 1 reply; 244+ messages in thread
From: Oleksii Kurochko @ 2026-09-18 11:53 UTC (permalink / raw)
  To: Jan Beulich
  Cc: Romain Caritey, Baptiste Le Duc, Zheng Zhang, Alistair Francis,
	Connor Davis, Andrew Cooper, Anthony PERARD, Michal Orzel,
	Julien Grall, Roger Pau Monné, Stefano Stabellini, xen-devel



On 9/14/26 3:27 PM, Jan Beulich wrote:
> On 27.08.2026 17:21, Oleksii Kurochko wrote:
>> During migration of a virtual hart to a different guest interrupt file,
>> straggler MSIs from the APLIC could arrive at the old interrupt file
>> after the switch.
>>
>> genmsi is used despite not supporting guest interrupt files because the
>> AIA spec guarantees that all MSIs previously sent from the APLIC to the
>> same hart are visible at the hart's IMSIC before the extempore MSI from
>> genmsi becomes visible.
> 
> Hmm. As indicated, I'm learning RISC-V as I'm reviewing patches. This
> paragraph, if left as is, would make sure I simply can't ack the patch.
> I just don't understand what is being talked about. I can guess parts,
> but for example I don't know what "genmsi" is.

I will reword then commit message in the following way:

```
When a vCPU is moved to a different guest interrupt file, MSIs that the
APLIC has already sent towards the old file may still be in flight. They
must land before the old file's state is saved and the switch is done,
otherwise they would be lost.

To wait for them, use the APLIC's genmsi register. Writing it makes the
APLIC itself send an MSI (an "extempore" MSI) with a given interrupt
identity to a given hart. genmsi can only target the hart's supervisor-
level interrupt file, not a guest one, but the AIA spec guarantees that
all MSIs previously sent by the same APLIC to the same hart become
visible at the hart's IMSIC before the extempore MSI does. So once the
extempore MSI has been delivered, no older MSI from this APLIC to the
hart can still be in flight, whichever interrupt file it targets.

The last interrupt identity (nr_ids) is reserved for this purpose.
```

> 
>> @@ -205,6 +207,30 @@ void aplic_hw_write_reg(unsigned int offset, uint32_t value)
>>       spin_unlock_irqrestore(&aplic.lock, flags);
>>   }
>>   
>> +/*
>> + * As needed, synchronize with all IOMMUs and APLICs to ensure that no
>> + * straggler MSIs will arrive at the old interrupt file after this step.
>> + */
>> +void aplic_genmsi_barrier(void)
>> +{
>> +    const struct imsic_config *imsic = imsic_get_config();
>> +    unsigned int cpu = smp_processor_id();
>> +    unsigned long flags;
>> +    uint32_t val;
>> +
>> +    val = MASK_INSR(aplic_hart_field(cpu), APLIC_TARGET_HART_IDX) |
>> +          (imsic->sync_id & APLIC_TARGET_EIID);
> 
> Along the lines of a question on an earlier patch: What if this ANDing
> actually chops off bits?

I will do then the same as I did in aplic_set_irq_affinity() (i 
mentioned that in the one of the replies connected to this function in 
this patch series):

     /*
      * sync_id is nr_ids, which imsic_parse_node() limits to IMSIC_MAX_ID,
      * so it always fits into the EIID field.
      */
     BUILD_BUG_ON(IMSIC_MAX_ID > MASK_EXTR(~0U, APLIC_TARGET_EIID));
     ASSERT(imsic->sync_id <= IMSIC_MAX_ID);

     val = MASK_INSR(aplic_hart_index(cpu), APLIC_TARGET_HART_IDX) |
           imsic->sync_id;

Thanks.

~ Oleksii



^ permalink raw reply	[flat|nested] 244+ messages in thread

* Re: [PATCH v2 35/39] xen/riscv: add basic VGEIN management for AIA guests
  2026-08-27 15:21 ` [PATCH v2 35/39] xen/riscv: add basic VGEIN management for AIA guests Oleksii Kurochko
@ 2026-09-18 12:38   ` Jan Beulich
  2026-09-23 16:06   ` Baptiste Le Duc
  1 sibling, 0 replies; 244+ messages in thread
From: Jan Beulich @ 2026-09-18 12:38 UTC (permalink / raw)
  To: Oleksii Kurochko
  Cc: Romain Caritey, Baptiste Le Duc, Zheng Zhang, Alistair Francis,
	Connor Davis, Andrew Cooper, Anthony PERARD, Michal Orzel,
	Julien Grall, Roger Pau Monné, Stefano Stabellini, xen-devel

On 27.08.2026 17:21, Oleksii Kurochko wrote:
> It was decided to add support for IMSIC from the start instead of having APLIC
> operate in direct delivery mode, as it requires a trap-and-emulation approach,
> which is not optimal from a performance standpoint.
> 
> AIA provides a hardware-accelerated mechanism for delivering external
> interrupts to domains via "guest interrupt files" located in IMSIC.
> A single physical hart can implement multiple such files (up to GEILEN),
> allowing several virtual harts to receive interrupts directly from hardware.
> 
> Introduce per-CPU tracking of guest interrupt file identifiers (VGEIN)
> for systems implementing AIA specification. Each CPU maintains
> a bitmap describing which guest interrupt files are currently in use.
> 
> Implement helpers to initialize the bitmap based on the number of available
> guest interrupt files (GEILEN), assign a VGEIN to a vCPU, and release it
> when no longer needed.
> 
> Signed-off-by: Oleksii Kurochko <oleksii.kurochko@gmail.com>

Acked-by: Jan Beulich <jbeulich@suse.com>



^ permalink raw reply	[flat|nested] 244+ messages in thread

* Re: [PATCH v2 36/39] xen/riscv: wake up a descheduled vCPU on a guest external interrupt
  2026-08-27 15:21 ` [PATCH v2 36/39] xen/riscv: wake up a descheduled vCPU on a guest external interrupt Oleksii Kurochko
@ 2026-09-18 12:52   ` Jan Beulich
  2026-09-21 14:01     ` Oleksii Kurochko
  0 siblings, 1 reply; 244+ messages in thread
From: Jan Beulich @ 2026-09-18 12:52 UTC (permalink / raw)
  To: Oleksii Kurochko
  Cc: Romain Caritey, Baptiste Le Duc, Zheng Zhang, Alistair Francis,
	Connor Davis, Andrew Cooper, Anthony PERARD, Michal Orzel,
	Julien Grall, Roger Pau Monné, Stefano Stabellini, xen-devel

On 27.08.2026 17:21, Oleksii Kurochko wrote:
> @@ -62,23 +69,25 @@ static int cf_check cpu_callback(struct notifier_block *nfb,
>                                   unsigned long action, void *hcpu)
>  {
>      unsigned int cpu = (unsigned long)hcpu;
> -    int rc = 0;
>  
>      switch ( action )
>      {
>      case CPU_STARTING:
> -        rc = vgein_init();
> +    {
> +        int rc = vgein_init();
> +
>          if ( rc )
>              printk(XENLOG_ERR "AIA: failed to init vgein for CPU%u: %d\n",
>                     cpu, rc);
>          break;
> +    }
>  
>      case CPU_DYING:
>          vgein_deinit();
>          break;
>      }
>  
> -    return notifier_from_errno(rc);
> +    return NOTIFY_DONE;
>  }

What is this hunk doing in this patch? Was this meant to be merged into the
prior one? But then - why?

> @@ -168,3 +181,36 @@ void vgein_release(struct vcpu *v, unsigned int vgein_id, unsigned int cpu)
>              __func__, v, vgein_id, cpu, vgein->bmp);
>  #endif
>  }
> +
> +void hgei_interrupt(void)
> +{
> +    unsigned long hgei_mask, flags;
> +    struct vgein_ctrl *vgein = &this_cpu(vgein);
> +
> +    hgei_mask = csr_read(CSR_HGEIP) & csr_read(CSR_HGEIE);

Misra, aiui, isn't going to like this. You may want to split it up.

> +    csr_clear(CSR_HGEIE, hgei_mask);
> +
> +    spin_lock_irqsave(&vgein->lock, flags);
> +
> +    for_each_set_bit ( guest_file_id, hgei_mask )
> +    {
> +        /*
> +         * guest_file_id shouldn't be zero, as it will indicate that no
> +         * guest external interrupt source is selected for VS-level external
> +         * interrupts.
> +         */
> +        ASSERT(guest_file_id);

While it only affects debug builds, this check still needlessly is
done on every loop iteration, when doing it once ahead of the loop
would suffice.

> +        if ( vgein->owners[guest_file_id] )
> +        {
> +#ifdef VGEIN_DEBUG
> +            gprintk(XENLOG_DEBUG, "%s: kick ->%pv, hgei_mask(%#lx)\n",
> +                    __func__, vgein->owners[guest_file_id], hgei_mask);
> +#endif

This can ocur very frequently (when VGEIN_DEBUG is defined). A
trace record may be a better alternative.

> --- a/xen/arch/riscv/domain.c
> +++ b/xen/arch/riscv/domain.c
> @@ -136,6 +136,8 @@ static void vcpu_csr_init(struct vcpu *v)
>          v->arch.hstateen0 = (hstateen0 & csr_masks.hstateen0) |
>                              csr_masks.ro_one.hstateen0;
>      }
> +
> +    v->arch.hie = MIP_SGEIP;

Neither part of the rhs identifier has anything to do with the CSR
having its default value set here. That's perhaps again a piece of
RISC-V I'm missing, but I can't make sense of this.

> --- a/xen/arch/riscv/imsic.c
> +++ b/xen/arch/riscv/imsic.c
> @@ -510,12 +510,31 @@ void cf_check imsic_ctxt_switch_from(struct vcpu *v)
>  
>      write_lock_irqsave(&imsic_state->vsfile_lock, flags);
>      imsic_state->vsfile_cpu = v->processor;
> +    /*
> +     * Start to observe the VS-file from HS-mode: while the vCPU isn't
> +     * running an interrupt pending in its VS-file is reported through HGEIP
> +     * instead of being delivered to VS-mode, which lets Xen wake the vCPU up.
> +     */
> +    csr_set(CSR_HGEIE, BIT(imsic_state->guest_file_id, UL));
>      write_unlock_irqrestore(&imsic_state->vsfile_lock, flags);
>  }

It is suspicious for the HGEIE write to be the last step. How's this free
of a window where an interrupt is lost. (Sorry, likely another blind spot
of mine wrt RISC-V.)

>  void cf_check imsic_ctxt_switch_to(struct vcpu *v)
>  {
> -    /* Nothing to do */
> +    struct vimsic_state *imsic_state = v->arch.vimsic_state;
> +    unsigned long flags;
> +
> +    /* A s/w VS-file is never observed through HGEIP. */
> +    if ( !vcpu_guest_file_id(v) )
> +        return;
> +
> +    /*
> +     * The vCPU is about to run, so hstatus.VGEIN delivers the VS-file's
> +     * interrupts to it directly and there is nothing left for Xen to observe.
> +     */
> +    read_lock_irqsave(&imsic_state->vsfile_lock, flags);
> +    csr_clear(CSR_HGEIE, BIT(imsic_state->guest_file_id, UL));
> +    read_unlock_irqrestore(&imsic_state->vsfile_lock, flags);
>  }

How can this be a read-lock when you write a CSR? Or else - why is locking
here necessary in the firt place?

Jan


^ permalink raw reply	[flat|nested] 244+ messages in thread

* Re: [PATCH v2 32/39] xen/riscv: remap interrupts to new IMSIC VS-file
  2026-09-14 15:02   ` Jan Beulich
@ 2026-09-21  8:03     ` Oleksii Kurochko
  2026-09-21  8:28       ` Jan Beulich
  0 siblings, 1 reply; 244+ messages in thread
From: Oleksii Kurochko @ 2026-09-21  8:03 UTC (permalink / raw)
  To: Jan Beulich
  Cc: Romain Caritey, Baptiste Le Duc, Zheng Zhang, Alistair Francis,
	Connor Davis, Andrew Cooper, Anthony PERARD, Michal Orzel,
	Julien Grall, Roger Pau Monné, Stefano Stabellini, xen-devel



On 9/14/26 5:02 PM, Jan Beulich wrote:
> On 27.08.2026 17:21, Oleksii Kurochko wrote:
>> @@ -60,6 +61,12 @@ static unsigned int __ro_after_init guest_num_msis;
>>   #define IMSIC_DISABLE_EITHRESHOLD   1
>>   #define IMSIC_ENABLE_EITHRESHOLD    0
>>   
>> +#define imsic_csr_read(c)           \
>> +({                                  \
>> +    csr_write(CSR_SISELECT, (c));   \
> 
> Nit: Excess parentheses again.

I will drop them.

> 
>> @@ -242,6 +254,15 @@ void imsic_irq_disable(unsigned int irq)
>>       spin_unlock(&imsic_cfg.lock);
>>   }
>>   
>> +static bool imsic_local_is_pending(unsigned int id)
>> +{
>> +    unsigned long isel =
>> +        (id / BITS_PER_LONG) * (BITS_PER_LONG / IMSIC_EIPx_BITS) + IMSIC_EIP0;
>> +    unsigned long bit = BIT(id % BITS_PER_LONG, UL);
> 
> Both can be unsigned int, can't they?

Yes, agreed. Both isel and bit fit within unsigned int. I will update 
them in the next version.

> 
>> +    return !!(imsic_csr_read(isel) & bit);
>> +}
> 
> No need for !! here.
> 
> What about endianness, btw? Does the IMSIC always match the CPU (and
> its setting)?

No, the IMSIC does not dynamically adapt its register interfaces based 
on the CPU's runtime endianness configuration (e.g. mstatus.SBE/MBE):

- CSR Accesses (imsic_csr_read): Indirect CSR accesses (siselect/sireg 
or miselect/mireg) operate using standard RISC-V CSR instructions at 
current XLEN width. Values are read and written directly into 
architectural GPRs without byte-swapping.

- MMIO Ports: For incoming device MSIs, IMSIC uses fixed memory-mapped 
offsets: offset 0x000 (seteipnum_le) always expects Little-Endian, while 
offset 0x004 (seteipnum_be) expects Big-Endian.

- Memory-Resident Interrupt Files (MRIF): For virtualized environments, 
MRIF structures in memory are strictly defined in Little-Endian byte 
order regardless of whether the CPU harts operate in Big-Endian or 
Little-Endian mode.

> 
> Overall, what does "local" in the function name signify? (For a static
> function, the "imsic" prefix may also be unnecessary.)

It signifies that local (on which code is executed now) hart's IMSIC 
CSRs (isel and ireg in the case of imsic_csr_read()) are touched.

> 
>> @@ -497,6 +523,27 @@ static void imsic_call_on_cpu(unsigned int cpu, void (*func)(void *),
>>           on_selected_cpus(cpumask_of(cpu), func, data, 1);
>>   }
>>   
>> +/*
>> + * Ensure that all the MSIs the APLIC has already generated for the hart this
>> + * runs on have really reached the hart's IMSIC.
>> + *
>> + * The barrier is the one described by the AIA specification in
>> + * "Synchronizing interactions between a hart and the APLIC": ask the APLIC to
>> + * send an MSI to the hart itself and wait until it shows up as pending in the
>> + * hart's own interrupt file. As it says nothing about MSIs on their way to
>> + * any other hart, it has to be executed by the pCPU owning the interrupt file
>> + * the MSIs were being sent to.
>> + */
>> +static void cf_check imsic_aplic_sync(void *data)
> 
> If the parameter isn't used, maybe best to name it "unused"?

Makes sense to me. I will do that in the next version + I will update 
the type to 'struct imsic_vsfile_data *' as it was suggested for 
imsic_call_on_cpu() in "Re: [PATCH v2 30/39] xen/riscv: prepare new 
IMSIC VS-file".

> 
>> @@ -848,8 +899,22 @@ void imsic_migrate_vcpu(struct vcpu *v)
>>       if ( v->arch.last_cpu == NR_CPUS )
>>           return;
>>   
>> +    read_lock_irqsave(&imsic_state->vsfile_lock, flags);
>> +    old_vsfile_id = imsic_state->guest_file_id;
>> +    old_vsfile_cpu = imsic_state->vsfile_cpu;
>> +    read_unlock_irqrestore(&imsic_state->vsfile_lock, flags);
>> +
>> +    /*
>> +     * We don't support SW interrupt files at the moment. Bail out before
>> +     * anything is touched, as the old file has no owning pCPU in that case
>> +     * and there is nothing to retarget the producers away from.
>> +     */
>> +    if ( old_vsfile_cpu == NR_CPUS )
>> +        panic("IMSIC SW-file isn't supported\n");
>> +
>>       /*
>>        * At this point, all interrupt producers are still using the old IMSIC
>> +     * VS-file so we first move all interrupt producers to the new IMSIC
>>        * VS-file.
>>        */
> 
> Isn't the new part of the comment premature? Moving doesn't start until ...
> 
>> @@ -870,5 +935,43 @@ void imsic_migrate_vcpu(struct vcpu *v)
>>       /* Zero-out new IMSIC VS-file */
>>       imsic_call_on_cpu(new_vsfile_cpu, imsic_vsfile_local_clear, &vsfile_data);
>>   
>> +    /* Update G-stage mapping for the new IMSIC VS-file */
>> +    if ( imsic_map_guest_file(v, new_vsfile_hgei) )
>> +    {
>> +        domain_crash(v->domain, "Migration to hw interrupt file failed\n");
>> +
>> +        return;
>> +    }
>> +
>> +    imsic_update_state(v, new_vsfile_hgei);
>> +
>> +    /*
>> +     * TODO: Modify the relevant translation tables at all IOMMUs so that MSIs
>> +     *       for this virtual interrupt file are now sent to the new physical
>> +     *       interrupt file.
>> +     */
>> +    if ( iommu_enabled )
>> +        printk_once("IMSIC: IOMMU MSI retargeting is not implemented\n");
>> +
>> +    /*
>> +     * If any interrupts at an APLIC are forwarded by MSIs to the old interrupt
>> +     * file, reconfigure the APLIC to send them to the new interrupt file.
>> +     */
>> +    aplic_reconfigure_target(v, old_vsfile_id, old_vsfile_cpu);
> 
> ... here, as it looks.

At some point I agree but the steps before are preparation of moving and 
is a part of moving process.

Then probably it make sense to reword the comment to:

    /*
      * At this point, all interrupt producers are still using the old IMSIC
      * VS-file.  Allocate and clear the new one before redirecting anything
      * to it.
      */

Would it be better?

~ Oleksii



^ permalink raw reply	[flat|nested] 244+ messages in thread

* Re: [PATCH v2 32/39] xen/riscv: remap interrupts to new IMSIC VS-file
  2026-09-21  8:03     ` Oleksii Kurochko
@ 2026-09-21  8:28       ` Jan Beulich
  2026-09-21  8:50         ` Oleksii Kurochko
  0 siblings, 1 reply; 244+ messages in thread
From: Jan Beulich @ 2026-09-21  8:28 UTC (permalink / raw)
  To: Oleksii Kurochko
  Cc: Romain Caritey, Baptiste Le Duc, Zheng Zhang, Alistair Francis,
	Connor Davis, Andrew Cooper, Anthony PERARD, Michal Orzel,
	Julien Grall, Roger Pau Monné, Stefano Stabellini, xen-devel

On 21.09.2026 10:03, Oleksii Kurochko wrote:
> On 9/14/26 5:02 PM, Jan Beulich wrote:
>> On 27.08.2026 17:21, Oleksii Kurochko wrote:
>>> @@ -242,6 +254,15 @@ void imsic_irq_disable(unsigned int irq)
>>>       spin_unlock(&imsic_cfg.lock);
>>>   }
>>>   
>>> +static bool imsic_local_is_pending(unsigned int id)
>>> +{
>>> +    unsigned long isel =
>>> +        (id / BITS_PER_LONG) * (BITS_PER_LONG / IMSIC_EIPx_BITS) + IMSIC_EIP0;
>>> +    unsigned long bit = BIT(id % BITS_PER_LONG, UL);
>>
>> Both can be unsigned int, can't they?
> 
> Yes, agreed. Both isel and bit fit within unsigned int. I will update 
> them in the next version.
> 
>>
>>> +    return !!(imsic_csr_read(isel) & bit);
>>> +}
>>
>> No need for !! here.
>>
>> What about endianness, btw? Does the IMSIC always match the CPU (and
>> its setting)?
> 
> No, the IMSIC does not dynamically adapt its register interfaces based 
> on the CPU's runtime endianness configuration (e.g. mstatus.SBE/MBE):
> 
> - CSR Accesses (imsic_csr_read): Indirect CSR accesses (siselect/sireg 
> or miselect/mireg) operate using standard RISC-V CSR instructions at 
> current XLEN width. Values are read and written directly into 
> architectural GPRs without byte-swapping.

I.e. you need you add endianness conversion.

>> Overall, what does "local" in the function name signify? (For a static
>> function, the "imsic" prefix may also be unnecessary.)
> 
> It signifies that local (on which code is executed now) hart's IMSIC 
> CSRs (isel and ireg in the case of imsic_csr_read()) are touched.

That's the expected thing for CSR access, though.

>>> @@ -848,8 +899,22 @@ void imsic_migrate_vcpu(struct vcpu *v)
>>>       if ( v->arch.last_cpu == NR_CPUS )
>>>           return;
>>>   
>>> +    read_lock_irqsave(&imsic_state->vsfile_lock, flags);
>>> +    old_vsfile_id = imsic_state->guest_file_id;
>>> +    old_vsfile_cpu = imsic_state->vsfile_cpu;
>>> +    read_unlock_irqrestore(&imsic_state->vsfile_lock, flags);
>>> +
>>> +    /*
>>> +     * We don't support SW interrupt files at the moment. Bail out before
>>> +     * anything is touched, as the old file has no owning pCPU in that case
>>> +     * and there is nothing to retarget the producers away from.
>>> +     */
>>> +    if ( old_vsfile_cpu == NR_CPUS )
>>> +        panic("IMSIC SW-file isn't supported\n");
>>> +
>>>       /*
>>>        * At this point, all interrupt producers are still using the old IMSIC
>>> +     * VS-file so we first move all interrupt producers to the new IMSIC
>>>        * VS-file.
>>>        */
>>
>> Isn't the new part of the comment premature? Moving doesn't start until ...
>>
>>> @@ -870,5 +935,43 @@ void imsic_migrate_vcpu(struct vcpu *v)
>>>       /* Zero-out new IMSIC VS-file */
>>>       imsic_call_on_cpu(new_vsfile_cpu, imsic_vsfile_local_clear, &vsfile_data);
>>>   
>>> +    /* Update G-stage mapping for the new IMSIC VS-file */
>>> +    if ( imsic_map_guest_file(v, new_vsfile_hgei) )
>>> +    {
>>> +        domain_crash(v->domain, "Migration to hw interrupt file failed\n");
>>> +
>>> +        return;
>>> +    }
>>> +
>>> +    imsic_update_state(v, new_vsfile_hgei);
>>> +
>>> +    /*
>>> +     * TODO: Modify the relevant translation tables at all IOMMUs so that MSIs
>>> +     *       for this virtual interrupt file are now sent to the new physical
>>> +     *       interrupt file.
>>> +     */
>>> +    if ( iommu_enabled )
>>> +        printk_once("IMSIC: IOMMU MSI retargeting is not implemented\n");
>>> +
>>> +    /*
>>> +     * If any interrupts at an APLIC are forwarded by MSIs to the old interrupt
>>> +     * file, reconfigure the APLIC to send them to the new interrupt file.
>>> +     */
>>> +    aplic_reconfigure_target(v, old_vsfile_id, old_vsfile_cpu);
>>
>> ... here, as it looks.
> 
> At some point I agree but the steps before are preparation of moving and 
> is a part of moving process.
> 
> Then probably it make sense to reword the comment to:
> 
>     /*
>       * At this point, all interrupt producers are still using the old IMSIC
>       * VS-file.  Allocate and clear the new one before redirecting anything
>       * to it.
>       */
> 
> Would it be better?

Imo yes.

Jan


^ permalink raw reply	[flat|nested] 244+ messages in thread

* Re: [PATCH v2 32/39] xen/riscv: remap interrupts to new IMSIC VS-file
  2026-09-21  8:28       ` Jan Beulich
@ 2026-09-21  8:50         ` Oleksii Kurochko
  0 siblings, 0 replies; 244+ messages in thread
From: Oleksii Kurochko @ 2026-09-21  8:50 UTC (permalink / raw)
  To: Jan Beulich
  Cc: Romain Caritey, Baptiste Le Duc, Zheng Zhang, Alistair Francis,
	Connor Davis, Andrew Cooper, Anthony PERARD, Michal Orzel,
	Julien Grall, Roger Pau Monné, Stefano Stabellini, xen-devel



On 9/21/26 10:28 AM, Jan Beulich wrote:
> On 21.09.2026 10:03, Oleksii Kurochko wrote:
>> On 9/14/26 5:02 PM, Jan Beulich wrote:
>>> On 27.08.2026 17:21, Oleksii Kurochko wrote:
>>>> @@ -242,6 +254,15 @@ void imsic_irq_disable(unsigned int irq)
>>>>        spin_unlock(&imsic_cfg.lock);
>>>>    }
>>>>    
>>>> +static bool imsic_local_is_pending(unsigned int id)
>>>> +{
>>>> +    unsigned long isel =
>>>> +        (id / BITS_PER_LONG) * (BITS_PER_LONG / IMSIC_EIPx_BITS) + IMSIC_EIP0;
>>>> +    unsigned long bit = BIT(id % BITS_PER_LONG, UL);
>>> Both can be unsigned int, can't they?
>> Yes, agreed. Both isel and bit fit within unsigned int. I will update
>> them in the next version.

Sorry for not noticing that in first reply but local variable `bit` 
should be unsigned long as if id % 64 >= 32 we will have overflow of 
'unsigned int'.

>>
>>>> +    return !!(imsic_csr_read(isel) & bit);
>>>> +}
>>> No need for !! here.
>>>
>>> What about endianness, btw? Does the IMSIC always match the CPU (and
>>> its setting)?
>> No, the IMSIC does not dynamically adapt its register interfaces based
>> on the CPU's runtime endianness configuration (e.g. mstatus.SBE/MBE):
>>
>> - CSR Accesses (imsic_csr_read): Indirect CSR accesses (siselect/sireg
>> or miselect/mireg) operate using standard RISC-V CSR instructions at
>> current XLEN width. Values are read and written directly into
>> architectural GPRs without byte-swapping.
> I.e. you need you add endianness conversion.

I think I don't really get why. My understanding is that endianness is 
about an access to memory. We don't have here load/store instruction or 
an explicit access to memory. We have here only CSR instruction which 
loads value from IMSIC register to GPR w/ an access to any memory.

Probably I wasn't clear here "he IMSIC does not dynamically adapt its 
register interfaces based on the CPU's runtime endianness configuration 
(e.g. mstatus.SBE/MBE):" and it would be better to reply as:

```
endianness (mstatus.SBE/MBE) only governs memory accesses, whereas
eipK is accessed via CSR instructions, which transfer an XLEN-wide
value between the CSR and a GPR with no notion of byte order. The AIA
spec defines eipK in terms of bit significance (bit i of eipK is
identity K*32+i), so BIT(id % BITS_PER_LONG) is correct regardless of
the CPU's data endianness. Endianness only matters for the memory-mapped
seteipnum register, which is why the spec provides both seteipnum_le
and seteipnum_be.
```

> 
>>> Overall, what does "local" in the function name signify? (For a static
>>> function, the "imsic" prefix may also be unnecessary.)
>> It signifies that local (on which code is executed now) hart's IMSIC
>> CSRs (isel and ireg in the case of imsic_csr_read()) are touched.
> That's the expected thing for CSR access, though.

Fair enough. I'll drop both the "imsic_" prefix and "local" and rename
it to irq_is_pending().

~ Oleksii


^ permalink raw reply	[flat|nested] 244+ messages in thread

* Re: [PATCH v2 33/39] xen/riscv: dump old interrupt file to memory
  2026-09-14 15:15   ` Jan Beulich
@ 2026-09-21  9:51     ` Oleksii Kurochko
  0 siblings, 0 replies; 244+ messages in thread
From: Oleksii Kurochko @ 2026-09-21  9:51 UTC (permalink / raw)
  To: Jan Beulich
  Cc: Romain Caritey, Baptiste Le Duc, Zheng Zhang, Alistair Francis,
	Connor Davis, Andrew Cooper, Anthony PERARD, Michal Orzel,
	Julien Grall, Roger Pau Monné, Stefano Stabellini, xen-devel



On 9/14/26 5:15 PM, Jan Beulich wrote:
> On 27.08.2026 17:21, Oleksii Kurochko wrote:
>> At the old interrupt file, dump to memory all the eip and eie arrays).
>> After this step is done, the old interrupt file is no longer in use so
>> old intrrupt file VGEIN could be released.
>>
>> Restoring of old interrupt file state will be done in follow-up
>> patch.
>>
>> There are cases where it is needed to specify on which cpu it is
>> necessary to VGEIN should be released so update vgein_release() to
>> deal with that.
> 
> Beside this being difficult to parse, it looks like it is inapplicable? As
> said ...
> 
>> Keep BUG_ON("unimplemented") placeholder in imsic_migrate_vcpu() to guard
>> against silent incorrect behaviour or unexpected panics in guest VMs until
>> the function is fully implemented.
>>
>> vgein_release() is stub for now and will be introduced later.
> 
> ... also here?

Agree that paragraph should be dropped, it ins't applicable anymore.

> 
>> --- a/xen/arch/riscv/imsic.c
>> +++ b/xen/arch/riscv/imsic.c
>> @@ -56,6 +56,24 @@ static unsigned int __ro_after_init guest_num_msis;
>>    */
>>   #define GUEST_IMSIC_MAX_MSIS 255U
>>   
>> +/*
>> + * The interrupt identities an IMSIC interrupt file provides are 0 (which is
>> + * never valid, but still occupies a bit) up to IMSIC_MAX_ID inclusive, so
>> + * IMSIC_MAX_ID + 1 bits have to be covered.
>> + */
>> +#define IMSIC_MAX_EIX DIV_ROUND_UP(IMSIC_MAX_ID + 1, BITS_PER_TYPE(uint64_t))
> 
> As before - plain 64 please, or it needs to become clear where the uint64_t
> is actually coming from.

I will use plain 64.

> 
>> +struct imsic_mrif_eix {
>> +    unsigned long eip[BITS_PER_TYPE(uint64_t) / BITS_PER_LONG];
>> +    unsigned long eie[BITS_PER_TYPE(uint64_t) / BITS_PER_LONG];
> 
> Same here. Yet better may be to use DECLARE_BITMAP()?

It will be better. I'll use:

     DECLARE_BITMAP(eip, 64);
     DECLARE_BITMAP(eie, 64);

> 
>> @@ -85,6 +103,15 @@ do {                            \
>>       csr_clear(CSR_SIREG, v);    \
>>   } while (0)
>>   
>> +#define imsic_vs_csr_swap(c, v)     \
>> +({                                  \
>> +    unsigned long r_;               \
>> +                                    \
>> +    csr_write(CSR_VSISELECT, (c));  \
>> +    r_ = csr_swap(CSR_VSIREG, (v)); \
>> +    r_;                             \
>> +})
> 
> Excess parentheses again. Plus - what use is r_ here?

r_ stands for `return value` but it should be dropped as it could be 
done so imsic_vs_csr_swap() just returns a value directly:

#define imsic_vs_csr_swap(c, v)     \
({                                  \
     csr_write(CSR_VSISELECT, c);    \
     csr_swap(CSR_VSIREG, v);        \
})


> 
>> @@ -130,6 +157,21 @@ do {                                \
>>       imsic_switchcase_32(F, ireg + 0, ##__VA_ARGS__) \
>>       imsic_switchcase_32(F, ireg + 32, ##__VA_ARGS__)
>>   
>> +static unsigned long imsic_eix_swap(unsigned int ireg, unsigned long val)
>> +{
>> +    switch ( ireg )
>> +    {
>> +    imsic_switchcase_64(imsic_switchcase_ret, IMSIC_EIP0,
>> +                        imsic_vs_csr_swap, val)
>> +    imsic_switchcase_64(imsic_switchcase_ret, IMSIC_EIE0,
>> +                        imsic_vs_csr_swap, val)
>> +    default:
>> +        ASSERT_UNREACHABLE();
>> +    }
> 
> There still wants to "break" in the default case.
> 

I'll add.

>> @@ -973,5 +1071,11 @@ void imsic_migrate_vcpu(struct vcpu *v)
>>        * to the new IMSIC VS-file.
>>        */
>>   
>> +    /* Read and clear register state from old IMSIC VS-file */
>> +    imsic_vsfile_read_clear(old_vsfile_id, old_vsfile_cpu, nr_hw_eix, &tmrif);
> 
> Why is &tmrif being passed into the function, when it's not otherwise used
> here? The function could itself have a suitable local var.
> 
tmrif isn't a scratch buffer local to the read: it carries the register 
state of the old interrupt file over to the new one. In this patch it's 
only filled in, but the next patch ("restore register state in the new 
IMSIC VS-file") passes the same buffer, via vsfile_data.mrif, to 
imsic_vsfile_local_update() on the new pCPU. By then the old file has 
been cleared and released, so the state has to live in the caller. I'll 
add a sentence to this patch's description saying the dumped state is 
consumed by the following patch:

The state is dumped into a buffer provided by the caller rather than one
local to imsic_vsfile_read_clear(), as it has to outlive the old 
interrupt file: once that file is released, the state is still needed to 
be restored into the new interrupt file.

Just to emphasize that in code I will use vsfile_data.mrif instead of 
&tmrif:

   imsic_vsfile_read_clear(old_vsfile_id, old_vsfile_cpu, 
vsfile_data.nr_eix, vsfile_data.mrif);

Thanks.

~ Oleksii




^ permalink raw reply	[flat|nested] 244+ messages in thread

* Re: [PATCH v2 34/39] xen/riscv: restore register state in the new IMSIC VS-file
  2026-09-14 15:21   ` Jan Beulich
@ 2026-09-21 10:58     ` Oleksii Kurochko
  0 siblings, 0 replies; 244+ messages in thread
From: Oleksii Kurochko @ 2026-09-21 10:58 UTC (permalink / raw)
  To: Jan Beulich
  Cc: Romain Caritey, Baptiste Le Duc, Zheng Zhang, Alistair Francis,
	Connor Davis, Andrew Cooper, Anthony PERARD, Michal Orzel,
	Julien Grall, Roger Pau Monné, Stefano Stabellini, xen-devel



On 9/14/26 5:21 PM, Jan Beulich wrote:
> On 27.08.2026 17:21, Oleksii Kurochko wrote:
>> @@ -630,13 +649,13 @@ static void cf_check imsic_vsfile_local_read_clear(void *data)
>>       old_vsiselect = csr_read(CSR_VSISELECT);
>>       old_hstatus = csr_read(CSR_HSTATUS);
>>       new_hstatus = old_hstatus & ~HSTATUS_VGEIN;
>> -    new_hstatus |= ((unsigned long)idata->hgei) << HSTATUS_VGEIN_SHIFT;
>> +    new_hstatus |= MASK_INSR(idata->hgei, HSTATUS_VGEIN);
> 
> Please put into final shape upon introduction.

Oh, right, this ...

> 
>>       csr_write(CSR_HSTATUS, new_hstatus);
>>   
>>       /*
>> -     * There is no need to use atomic functions version to store
>> -     * values in MRIF because imsic_vsfile_read_clear() is always called
>> -     * with pointer to temporary MRIF on stack.
>> +     * No atomic accessors are needed to store the values into the MRIF here,
>> +     * as imsic_vsfile_read_clear() is always called with a pointer to a
>> +     * temporary MRIF on the stack.
>>        */
> 
> Same for this comment perhaps.

... and this should be part of prev. patch.

> 
>> @@ -1077,5 +1140,12 @@ void imsic_migrate_vcpu(struct vcpu *v)
>>       /* Free-up old IMSIC VS-file */
>>       vgein_release(v, old_vsfile_id, old_vsfile_cpu);
>>   
>> -    BUG_ON("unimplemented");
>> +    /* Restore register state in the new IMSIC VS-file */
>> +    vsfile_data.mrif = &tmrif;
> 
> Ah, here &tmrif is used a 2nd time.

I think it could be dropped and just properly init vsfile_data.mrif 
during declaration:

     struct imsic_vsfile_data vsfile_data = {
         .nr_eix = imsic_nr_eix(),
         .mrif = &(struct imsic_mrif){ },
     };

and then use vsfile.mrif instead of &tmrif.

> 
>> +    imsic_call_on_cpu(new_vsfile_cpu, imsic_vsfile_local_update, &vsfile_data);
>> +
>> +    /* Set VCPU HSTATUS.VGEIN to new IMSIC VS-file */
>> +    vcpu_guest_cpu_user_regs(v)->hstatus &= ~HSTATUS_VGEIN;
>> +    vcpu_guest_cpu_user_regs(v)->hstatus |=
>> +            MASK_INSR(new_vsfile_hgei, HSTATUS_VGEIN);
> 
> Nit: Indentation.
> 
> Other comments on earlier patches apply here (and possibly elsewhere) as
> well. Just ftaod.

I will fix them.

Thanks.

~ Oleksii


^ permalink raw reply	[flat|nested] 244+ messages in thread

* Re: [PATCH v2 37/39] xen/riscv: map IMSIC interrupt file for vCPUs
  2026-08-27 15:21 ` [PATCH v2 37/39] xen/riscv: map IMSIC interrupt file for vCPUs Oleksii Kurochko
@ 2026-09-21 11:36   ` Jan Beulich
  2026-09-21 14:35     ` Oleksii Kurochko
  0 siblings, 1 reply; 244+ messages in thread
From: Jan Beulich @ 2026-09-21 11:36 UTC (permalink / raw)
  To: Oleksii Kurochko
  Cc: Romain Caritey, Baptiste Le Duc, Zheng Zhang, Alistair Francis,
	Connor Davis, Andrew Cooper, Anthony PERARD, Michal Orzel,
	Julien Grall, Roger Pau Monné, Stefano Stabellini, xen-devel

On 27.08.2026 17:21, Oleksii Kurochko wrote:
> A guest running in VS-mode expects its own IMSIC S-file at offset 0 of its
> guest-physical IMSIC block. Physically, the guest-file (G-file) assigned to
> this vCPU lives at a hart-relative offset given by guest_file_id (assigned
> via the vGEIN allocator). Therefore, imsic_map_guest_file() uses stage-2
> translation to redirect the guest's fixed per-vCPU GPA page (offset 0) to
> the specific physical guest-file page.
> 
> Signed-off-by: Oleksii Kurochko <oleksii.kurochko@gmail.com>

Acked-by: Jan Beulich <jbeulich@suse.com>
perhaps with ...

> @@ -537,9 +538,72 @@ void cf_check imsic_ctxt_switch_to(struct vcpu *v)
>      read_unlock_irqrestore(&imsic_state->vsfile_lock, flags);
>  }
>  
> +/*
> + * Map the physical IMSIC guest interrupt file (G-file) assigned to vCPU
> + * into the domain's stage-2 guest-physical address space.
> + *
> + * In the machine's physical address space (SPA), each hart's IMSIC
> + * supervisor-level file (S-file) is located at offset 0 of its address block,
> + * followed contiguously by GEILEN guest files at offsets of 1, 2, ..., N pages.
> + *
> + * Because a guest OS running in VS-mode expects its own supervisor-level
> + * interrupt file to be at offset 0 of its guest-physical IMSIC block, the
> + * hypervisor must use stage-2 address translation to map the vCPU's
> + * guest-physical "supervisor" page (GPA offset 0) to the specific
> + * physical guest file page (SPA offset guest_file_id) on the physical hart.
> + *
> + * A vCPU runs on the pCPU the scheduler picked for it (v->processor), and
> + * the guest file it is given (guest_file_id, from the vGEIN allocator)
> + * belongs to that very pCPU's IMSIC. A guest_file_id of 0 indicates that no
> + * hardware guest file is selected (matching the architectural behavior where
> + * vGEIN = 0 in the hstatus CSR selects no guest external interrupt source),
> + * requiring the VS-file to be emulated in software.
> + *
> + * Consequently the mapping installed here is only valid as long as the vCPU
> + * stays on that pCPU. When it migrates, a VS-file is acquired on the new
> + * pCPU and mapped at the very same GFN, so the stale mapping needs no
> + * explicit tear-down: it is simply replaced.
> + *
> + * The base guest-physical address advertised to the guest in the device
> + * tree matches offset 0 of the vCPU's virtual IMSIC block. Stage-2
> + * translation ensures that guest supervisor accesses to this page are
> + * transparently routed to the real hardware VS-file granted to it on
> + * the pCPU it currently runs on.
> + */
>  int imsic_map_guest_file(struct vcpu *v, unsigned int vsfile_id)
>  {
> -    return -EOPNOTSUPP;
> +    struct domain *d = v->domain;
> +    unsigned int cpu = v->processor;
> +    paddr_t gaddr = GUEST_IMSIC_S_BASE + (IMSIC_MMIO_PAGE_SZ * v->vcpu_id);
> +    paddr_t paddr, guest_offset;
> +    int res;
> +
> +    /* Nothing to map in the case of sw interrupt file. */
> +    if ( !vsfile_id )
> +        return 0;
> +
> +    guest_offset = vsfile_id * IMSIC_MMIO_PAGE_SZ;
> +
> +    paddr = imsic_cfg.msi[cpu].base_addr + imsic_cfg.msi[cpu].offset +
> +            guest_offset;
> +
> +#ifdef IMSIC_DEBUG
> +    printk(XENLOG_DEBUG
> +           "%s: %pv: ga(%#"PRIpaddr") -> pa(%#"PRIpaddr"), cpu(%u), "
> +           "guest_file_id(%u) base_addr(%#"PRIpaddr") offset(%#lx)\n",
> +           __func__, v, gaddr, paddr, cpu, vsfile_id,
> +           imsic_cfg.msi[cpu].base_addr, imsic_cfg.msi[cpu].offset);

... this also converted to dprintk(), or at least using XENLOG_G_DEBUG.

Jan


^ permalink raw reply	[flat|nested] 244+ messages in thread

* Re: [PATCH v2 38/39] xen/riscv: implement continue_new_vcpu()
  2026-08-27 15:21 ` [PATCH v2 38/39] xen/riscv: implement continue_new_vcpu() Oleksii Kurochko
@ 2026-09-21 12:12   ` Jan Beulich
  2026-09-22  8:23     ` Oleksii Kurochko
  0 siblings, 1 reply; 244+ messages in thread
From: Jan Beulich @ 2026-09-21 12:12 UTC (permalink / raw)
  To: Oleksii Kurochko
  Cc: Romain Caritey, Baptiste Le Duc, Zheng Zhang, Alistair Francis,
	Connor Davis, Andrew Cooper, Anthony PERARD, Michal Orzel,
	Julien Grall, Roger Pau Monné, Stefano Stabellini, xen-devel

On 27.08.2026 17:21, Oleksii Kurochko wrote:
> continue_new_vcpu() is the arch hook invoked the first time a freshly
> created vCPU is scheduled. Implement both cases it has to cover:
>  - for the idle vCPU, switch to its own stack and jump to idle_loop();
>  - for a guest vCPU, restore hstatus and enter the guest through the new
>    return_to_new_vcpu() path in entry.S, which loads sepc, passes the
>    hart id in a0 and the DTB address in a1 as expected by the RISC-V
>    boot protocol, sets sstatus.SPP and executes sret.

Is this a requirement for all CPUs, or just for the boot one? (I can't
quite see why secondary processors would need passing a DTB address.)

> Interrupts have to stay disabled across the restore. The trap entry
> logic implicitly clears hstatus.SPV, so an interrupt taken between the
> write of hstatus and sret would make sret return to HS-mode instead of
> VS-mode, and restoring SPV afterwards is non-trivial. Instead interrupts
> are simply kept off and sstatus.SPIE is set, so that SIE is restored from
> SPIE once sret has been executed.

As written this reads as if the guest would be responsible for doing this.
Isn't it rather SRET itself which does this?

> Also, it follows what hardware will do
> with real CPU which is also started with interrupts disabled.

Further up, aiui, you talk about the host's interrupt state. How vCPU-s
are started, however, is virtual interrupt state. Mixing both isn't
very helpful.

> Introduce get_cpu_info() and reset_stack_and_jump() in asm/current.h,
> needed by the above. get_cpu_info() is a macro rather than a static
> inline because asm/current.h is pulled in by <xen/percpu.h> before
> this_cpu() is defined and before <xen/sched.h> completes struct vcpu.

This is odd, given the similarity to Arm. They get away without using
"current", and hence without using this_cpu().

> --- a/xen/arch/riscv/domain.c
> +++ b/xen/arch/riscv/domain.c
> @@ -8,10 +8,13 @@
>  #include <xen/smp.h>
>  #include <xen/vmap.h>
>  
> +#include <asm/aia.h>
> +#include <asm/aplic.h>
>  #include <asm/bitops.h>
>  #include <asm/cpufeature.h>
>  #include <asm/csr.h>
>  #include <asm/current.h>
> +#include <asm/imsic.h>
>  #include <asm/intc.h>
>  #include <asm/mmio.h>
>  #include <asm/riscv_encoding.h>

What makes these additions necessary here?

> @@ -140,9 +143,43 @@ static void vcpu_csr_init(struct vcpu *v)
>      v->arch.hie = MIP_SGEIP;
>  }
>  
> +static void schedule_tail(struct vcpu *prev);
> +static void noreturn idle_loop(void);
> +void noreturn return_to_new_vcpu(void);

For this last one: asmlinkage?

>  static void continue_new_vcpu(struct vcpu *prev)
>  {
> -    BUG_ON("unimplemented\n");
> +    schedule_tail(prev);
> +
> +    if ( is_idle_vcpu(current) )
> +        reset_stack_and_jump(idle_loop);
> +    else

This is the kind of "else" which I consider particularly confusing: It
suggests that the if() body can actually be exited at the bottom, when
(by the name "reset_stack_and_jump") it hopefully cannot.

> +    {
> +        /*
> +         * During a context switch to a new vCPU, interrupts must be disabled
> +         * to guarantee that the vCPU's CSR state can be safely restored into
> +         * the hart without being clobbered by an interrupt trap.
> +         *
> +         * For example, when return_to_new_vcpu() finishes, it executes sret.
> +         * At that point, the hart checks hstatus.SPV=1 and sstatus.SPP=1 in
> +         * order to return from HS-mode into VS-mode. If an interrupt were to
> +         * arrive before sret, the trap entry logic would implicitly clear
> +         * hstatus.SPV to 0. Correctly restoring it afterwards is non-trivial,
> +         * and if left as 0, sret would incorrectly return to HS-mode instead
> +         * of VS-mode.
> +         *
> +         * To avoid this, interrupts are kept disabled during the restore.
> +         * Additionally, setting sstatus.SPIE=1 ensures that after sret is
> +         * executed (as sstatus.SIE will be loaded from SPIE), HS-mode will
> +         * continue to receive interrupts normally.
> +         */
> +        local_irq_disable();
> +        csr_set(CSR_SSTATUS, SSTATUS_SPIE);
> +
> +        csr_write(CSR_HSTATUS, vcpu_guest_cpu_user_regs(current)->hstatus);
> +
> +        reset_stack_and_jump(return_to_new_vcpu);

What are the criteria by which you split CSR accesses between doing some here
and some in return_to_new_vcpu()? In particular you set sstatus.SPIE here but
sstatus.SPP there, when both could - I think - be done with a single CSR
access.

> --- a/xen/arch/riscv/entry.S
> +++ b/xen/arch/riscv/entry.S
> @@ -143,3 +143,26 @@ FUNC(__context_switch)
>  
>          ret
>  END(__context_switch)
> +
> +/* t0 is used as a temporary reg and is clobbered to oblivion */
> +FUNC(return_to_new_vcpu)
> +        /* Swap tp with sscratch */
> +        csrrw   tp, CSR_SSCRATCH, tp

What is this about? I'm not aware of any counterpart code, yet all on its
own this I can't see it being overly useful.

> +        /* Set vCPU registers */
> +        REG_L   t0, CPU_USER_REGS_SEPC(sp)
> +        csrw    sepc, t0
> +
> +        /* Hartid goes to a0 */
> +        REG_L   a0, CPU_USER_REGS_A0(sp)
> +
> +        /* DTB goes to a1 */
> +        REG_L   a1, CPU_USER_REGS_A1(sp)

The fields loaded are merely .a0 and .a1 of the register struct. There's
nothing here making sure (all on its own) that what is loaded is what is
said by the comments. If e.g. the first comment was /* .a0 holds the
hart id */ or some such to remind readers what is being loaded without
giving the impression that the correct value is _established_ here, that
may be better.

> +        /* Set guest mode to supervisor */
> +        li      t0, SSTATUS_SPP
> +        csrs    CSR_SSTATUS, t0
> +
> +        /* Enter guest */
> +        sret
> +END(return_to_new_vcpu)

Aiui SRET does not switch stacks. Shouldn't you therefore clear sp here?
And perhaps also other GPRs, not the least ra? Exposing hypervisor
register values to guests is, well, a bit of a problem.

Jan


^ permalink raw reply	[flat|nested] 244+ messages in thread

* Re: [PATCH v2 39/39] xen/riscv: introduce IMSIC h/w interrupt file attaching to vcpu
  2026-08-27 15:21 ` [PATCH v2 39/39] xen/riscv: introduce IMSIC h/w interrupt file attaching to vcpu Oleksii Kurochko
@ 2026-09-21 12:32   ` Jan Beulich
  2026-09-22  8:31     ` Oleksii Kurochko
  0 siblings, 1 reply; 244+ messages in thread
From: Jan Beulich @ 2026-09-21 12:32 UTC (permalink / raw)
  To: Oleksii Kurochko
  Cc: Romain Caritey, Baptiste Le Duc, Zheng Zhang, Alistair Francis,
	Connor Davis, Andrew Cooper, Anthony PERARD, Michal Orzel,
	Julien Grall, Roger Pau Monné, Stefano Stabellini, xen-devel

On 27.08.2026 17:21, Oleksii Kurochko wrote:
> Introduce imsic_vsfile_attach() to initialize the AIA-related state needed
> for a vCPU to have a working guest interrupt file.
> 
> A guest (VS) interrupt file must be mapped to one of a pCPU's
> hardware interrupt files (if they exist), so the pCPU a vCPU will actually
> run on needs to be known first. arch_vcpu_create() is therefore not a
> suitable place to call vcpu_aia_init(), since the pCPU assigned to a
> vCPU can still change before it is first scheduled. To avoid
> reassigning the VS interrupt file id and remapping it to a different
> pCPU's hardware interrupt file, imsic_vsfile_attach() is called from a
> later point in the scheduling path (e.g. continue_new_vcpu()).

Hmm, why does first-time handling need to be this different from the
handling of a vCPU moving across pCPU-s? The sole difference should be
"no state to load" vs "load state that was saved on the old pCPU".

Jan


^ permalink raw reply	[flat|nested] 244+ messages in thread

* Re: [PATCH v2 36/39] xen/riscv: wake up a descheduled vCPU on a guest external interrupt
  2026-09-18 12:52   ` Jan Beulich
@ 2026-09-21 14:01     ` Oleksii Kurochko
  2026-09-21 15:08       ` Jan Beulich
  0 siblings, 1 reply; 244+ messages in thread
From: Oleksii Kurochko @ 2026-09-21 14:01 UTC (permalink / raw)
  To: Jan Beulich
  Cc: Romain Caritey, Baptiste Le Duc, Zheng Zhang, Alistair Francis,
	Connor Davis, Andrew Cooper, Anthony PERARD, Michal Orzel,
	Julien Grall, Roger Pau Monné, Stefano Stabellini, xen-devel



On 9/18/26 2:52 PM, Jan Beulich wrote:
> On 27.08.2026 17:21, Oleksii Kurochko wrote:
>> @@ -62,23 +69,25 @@ static int cf_check cpu_callback(struct notifier_block *nfb,
>>                                    unsigned long action, void *hcpu)
>>   {
>>       unsigned int cpu = (unsigned long)hcpu;
>> -    int rc = 0;
>>   
>>       switch ( action )
>>       {
>>       case CPU_STARTING:
>> -        rc = vgein_init();
>> +    {
>> +        int rc = vgein_init();
>> +
>>           if ( rc )
>>               printk(XENLOG_ERR "AIA: failed to init vgein for CPU%u: %d\n",
>>                      cpu, rc);
>>           break;
>> +    }
>>   
>>       case CPU_DYING:
>>           vgein_deinit();
>>           break;
>>       }
>>   
>> -    return notifier_from_errno(rc);
>> +    return NOTIFY_DONE;
>>   }
> 
> What is this hunk doing in this patch? Was this meant to be merged into the
> prior one?

Yes, it was meant to be a part of prev. patch.

> But then - why?

I had a case with what rc returns but looking at my downstream branches 
I don't face this case anymore so this hunk should be just dropped.

> 
>> @@ -168,3 +181,36 @@ void vgein_release(struct vcpu *v, unsigned int vgein_id, unsigned int cpu)
>>               __func__, v, vgein_id, cpu, vgein->bmp);
>>   #endif
>>   }
>> +
>> +void hgei_interrupt(void)
>> +{
>> +    unsigned long hgei_mask, flags;
>> +    struct vgein_ctrl *vgein = &this_cpu(vgein);
>> +
>> +    hgei_mask = csr_read(CSR_HGEIP) & csr_read(CSR_HGEIE);
> 
> Misra, aiui, isn't going to like this. You may want to split it up.

I will write in the following way then:
     hgei_mask = csr_read(CSR_HGEIP);
     hgei_mask &= csr_read(CSR_HGEIE);

> 
>> +    csr_clear(CSR_HGEIE, hgei_mask);
>> +
>> +    spin_lock_irqsave(&vgein->lock, flags);
>> +
>> +    for_each_set_bit ( guest_file_id, hgei_mask )
>> +    {
>> +        /*
>> +         * guest_file_id shouldn't be zero, as it will indicate that no
>> +         * guest external interrupt source is selected for VS-level external
>> +         * interrupts.
>> +         */
>> +        ASSERT(guest_file_id);
> 
> While it only affects debug builds, this check still needlessly is
> done on every loop iteration, when doing it once ahead of the loop
> would suffice.

Good point. I will do in the following way then before the loop:

     /*
      * Bit 0 of HGEIP/HGEIE is read-only zero: guest interrupt file ID 0
      * means that no guest external interrupt source is selected.
      */
     ASSERT(!(hgei_mask & 1));


> 
>> +        if ( vgein->owners[guest_file_id] )
>> +        {
>> +#ifdef VGEIN_DEBUG
>> +            gprintk(XENLOG_DEBUG, "%s: kick ->%pv, hgei_mask(%#lx)\n",
>> +                    __func__, vgein->owners[guest_file_id], hgei_mask);
>> +#endif
> 
> This can ocur very frequently (when VGEIN_DEBUG is defined). A
> trace record may be a better alternative.

Agree, it would be nice but tracing isn't ready for RISC-V.

Considering that we haven't had any issue with hgei interrupt for a long 
time I will just drop gprintk() for now and use `trace record` when 
functionality will be ready.

> 
>> --- a/xen/arch/riscv/domain.c
>> +++ b/xen/arch/riscv/domain.c
>> @@ -136,6 +136,8 @@ static void vcpu_csr_init(struct vcpu *v)
>>           v->arch.hstateen0 = (hstateen0 & csr_masks.hstateen0) |
>>                               csr_masks.ro_one.hstateen0;
>>       }
>> +
>> +    v->arch.hie = MIP_SGEIP;
> 
> Neither part of the rhs identifier has anything to do with the CSR
> having its default value set here. That's perhaps again a piece of
> RISC-V I'm missing, but I can't make sense of this.

All interrupt pending/enable CSRs share one bit layout (bit N 
corresponds to interrupt cause N), which is why MIP_SGEIP happened to be 
numerically right. I'll use BIT(IRQ_S_GEXT, UL) instead and add a 
comment: hie.SGEIE is what allows the SGEI raised through hgeie to be 
taken by Xen at all, while hie's VS-level bits alias the guest's vsie 
and are left cleared. I will apply the following change:

-    v->arch.hie = MIP_SGEIP;
+    /*
+     * Enable SGEIs, so that a guest interrupt file marked in HGEIE while
+     * the vCPU is descheduled can raise an interrupt to Xen.
+     *
+     * The VS-level bits of hie alias the guest's vsie, which is saved and
+     * restored separately, so they are left clear here.
+     */
+    v->arch.hie = BIT(IRQ_S_GEXT, UL);


> 
>> --- a/xen/arch/riscv/imsic.c
>> +++ b/xen/arch/riscv/imsic.c
>> @@ -510,12 +510,31 @@ void cf_check imsic_ctxt_switch_from(struct vcpu *v)
>>   
>>       write_lock_irqsave(&imsic_state->vsfile_lock, flags);
>>       imsic_state->vsfile_cpu = v->processor;
>> +    /*
>> +     * Start to observe the VS-file from HS-mode: while the vCPU isn't
>> +     * running an interrupt pending in its VS-file is reported through HGEIP
>> +     * instead of being delivered to VS-mode, which lets Xen wake the vCPU up.
>> +     */
>> +    csr_set(CSR_HGEIE, BIT(imsic_state->guest_file_id, UL));
>>       write_unlock_irqrestore(&imsic_state->vsfile_lock, flags);
>>   }
> 
> It is suspicious for the HGEIE write to be the last step. How's this free
> of a window where an interrupt is lost. (Sorry, likely another blind spot
> of mine wrt RISC-V.)

There's no window: a guest interrupt file's hgeip bit is 
level-sensitive, i.e. it reflects whether the file currently has a 
pending-and-enabled interrupt (the MSI itself stays latched in the 
file's eip[] until the guest claims it), and hip.SGEIP is simply (hgeip 
& hgeie) != 0. So an MSI arriving after the vCPU stopped running but 
before hgeie is set raises an SGEI as soon as the bit is set, taken once 
interrupts are re-enabled. I'll extend the comment to say so:

     /*
      * Start to observe the VS-file from HS-mode: while the vCPU isn't
      * running an interrupt pending in its VS-file is reported through 
HGEIP
      * instead of being delivered to VS-mode, which lets Xen wake the 
vCPU up.
      *
      * HGEIP is level-sensitive, reflecting the VS-file's current state, so
      * an interrupt that became pending before this point raises an SGEI as
      * soon as the bit is set in HGEIE; nothing is lost in between.
      */

Does it make sense?

> 
>>   void cf_check imsic_ctxt_switch_to(struct vcpu *v)
>>   {
>> -    /* Nothing to do */
>> +    struct vimsic_state *imsic_state = v->arch.vimsic_state;
>> +    unsigned long flags;
>> +
>> +    /* A s/w VS-file is never observed through HGEIP. */
>> +    if ( !vcpu_guest_file_id(v) )
>> +        return;
>> +
>> +    /*
>> +     * The vCPU is about to run, so hstatus.VGEIN delivers the VS-file's
>> +     * interrupts to it directly and there is nothing left for Xen to observe.
>> +     */
>> +    read_lock_irqsave(&imsic_state->vsfile_lock, flags);
>> +    csr_clear(CSR_HGEIE, BIT(imsic_state->guest_file_id, UL));
>> +    read_unlock_irqrestore(&imsic_state->vsfile_lock, flags);
>>   }
> 
> How can this be a read-lock when you write a CSR?

But here is a protection of ->guest_file_id not of write to a CSR and we 
want to not have a change of ->guest_file_id during an update of CSR_HGEIE.

> Or else - why is locking
> here necessary in the firt place?

Strictly speaking no locking is needed with the current implementation: 
HGEIE is local to this pCPU, and guest_file_id can't change 
concurrently. It's updated either by this very pCPU ahead of switching 
the vCPU in (imsic_migrate_vcpu() called from schedule(), 
imsic_vsfile_attach() right after), or while the vCPU can't be 
scheduled: sched_unit_migrate_finish() defers to unit_context_saved()
while the unit is running, and sched_move_domain() pauses the domain.

(and the similar are true for imsic_ctxt_switch_from())

But IMO we should keep around ->vsfile_lock to not miss the case where 
some case will update ->guest_file_id in parallel with 
imsic_ctxt_switch_to().

Does it make to continue to have read_lock_irqsave(...->vsfile_lock, 
...) here just for potential future cases?

Thanks.

~ Oleksii



^ permalink raw reply	[flat|nested] 244+ messages in thread

* Re: [PATCH v2 37/39] xen/riscv: map IMSIC interrupt file for vCPUs
  2026-09-21 11:36   ` Jan Beulich
@ 2026-09-21 14:35     ` Oleksii Kurochko
  0 siblings, 0 replies; 244+ messages in thread
From: Oleksii Kurochko @ 2026-09-21 14:35 UTC (permalink / raw)
  To: Jan Beulich
  Cc: Romain Caritey, Baptiste Le Duc, Zheng Zhang, Alistair Francis,
	Connor Davis, Andrew Cooper, Anthony PERARD, Michal Orzel,
	Julien Grall, Roger Pau Monné, Stefano Stabellini, xen-devel



On 9/21/26 1:36 PM, Jan Beulich wrote:
> On 27.08.2026 17:21, Oleksii Kurochko wrote:
>> A guest running in VS-mode expects its own IMSIC S-file at offset 0 of its
>> guest-physical IMSIC block. Physically, the guest-file (G-file) assigned to
>> this vCPU lives at a hart-relative offset given by guest_file_id (assigned
>> via the vGEIN allocator). Therefore, imsic_map_guest_file() uses stage-2
>> translation to redirect the guest's fixed per-vCPU GPA page (offset 0) to
>> the specific physical guest-file page.
>>
>> Signed-off-by: Oleksii Kurochko <oleksii.kurochko@gmail.com>
> 
> Acked-by: Jan Beulich <jbeulich@suse.com>

Thanks.

> perhaps with ...
> 
>> @@ -537,9 +538,72 @@ void cf_check imsic_ctxt_switch_to(struct vcpu *v)
>>       read_unlock_irqrestore(&imsic_state->vsfile_lock, flags);
>>   }
>>   
>> +/*
>> + * Map the physical IMSIC guest interrupt file (G-file) assigned to vCPU
>> + * into the domain's stage-2 guest-physical address space.
>> + *
>> + * In the machine's physical address space (SPA), each hart's IMSIC
>> + * supervisor-level file (S-file) is located at offset 0 of its address block,
>> + * followed contiguously by GEILEN guest files at offsets of 1, 2, ..., N pages.
>> + *
>> + * Because a guest OS running in VS-mode expects its own supervisor-level
>> + * interrupt file to be at offset 0 of its guest-physical IMSIC block, the
>> + * hypervisor must use stage-2 address translation to map the vCPU's
>> + * guest-physical "supervisor" page (GPA offset 0) to the specific
>> + * physical guest file page (SPA offset guest_file_id) on the physical hart.
>> + *
>> + * A vCPU runs on the pCPU the scheduler picked for it (v->processor), and
>> + * the guest file it is given (guest_file_id, from the vGEIN allocator)
>> + * belongs to that very pCPU's IMSIC. A guest_file_id of 0 indicates that no
>> + * hardware guest file is selected (matching the architectural behavior where
>> + * vGEIN = 0 in the hstatus CSR selects no guest external interrupt source),
>> + * requiring the VS-file to be emulated in software.
>> + *
>> + * Consequently the mapping installed here is only valid as long as the vCPU
>> + * stays on that pCPU. When it migrates, a VS-file is acquired on the new
>> + * pCPU and mapped at the very same GFN, so the stale mapping needs no
>> + * explicit tear-down: it is simply replaced.
>> + *
>> + * The base guest-physical address advertised to the guest in the device
>> + * tree matches offset 0 of the vCPU's virtual IMSIC block. Stage-2
>> + * translation ensures that guest supervisor accesses to this page are
>> + * transparently routed to the real hardware VS-file granted to it on
>> + * the pCPU it currently runs on.
>> + */
>>   int imsic_map_guest_file(struct vcpu *v, unsigned int vsfile_id)
>>   {
>> -    return -EOPNOTSUPP;
>> +    struct domain *d = v->domain;
>> +    unsigned int cpu = v->processor;
>> +    paddr_t gaddr = GUEST_IMSIC_S_BASE + (IMSIC_MMIO_PAGE_SZ * v->vcpu_id);
>> +    paddr_t paddr, guest_offset;
>> +    int res;
>> +
>> +    /* Nothing to map in the case of sw interrupt file. */
>> +    if ( !vsfile_id )
>> +        return 0;
>> +
>> +    guest_offset = vsfile_id * IMSIC_MMIO_PAGE_SZ;
>> +
>> +    paddr = imsic_cfg.msi[cpu].base_addr + imsic_cfg.msi[cpu].offset +
>> +            guest_offset;
>> +
>> +#ifdef IMSIC_DEBUG
>> +    printk(XENLOG_DEBUG
>> +           "%s: %pv: ga(%#"PRIpaddr") -> pa(%#"PRIpaddr"), cpu(%u), "
>> +           "guest_file_id(%u) base_addr(%#"PRIpaddr") offset(%#lx)\n",
>> +           __func__, v, gaddr, paddr, cpu, vsfile_id,
>> +           imsic_cfg.msi[cpu].base_addr, imsic_cfg.msi[cpu].offset);
> 
> ... this also converted to dprintk(), or at least using XENLOG_G_DEBUG.

I will convert to dprintk().

~ Oleksii


^ permalink raw reply	[flat|nested] 244+ messages in thread

* Re: [PATCH v2 36/39] xen/riscv: wake up a descheduled vCPU on a guest external interrupt
  2026-09-21 14:01     ` Oleksii Kurochko
@ 2026-09-21 15:08       ` Jan Beulich
  2026-09-22 13:37         ` Oleksii Kurochko
  0 siblings, 1 reply; 244+ messages in thread
From: Jan Beulich @ 2026-09-21 15:08 UTC (permalink / raw)
  To: Oleksii Kurochko
  Cc: Romain Caritey, Baptiste Le Duc, Zheng Zhang, Alistair Francis,
	Connor Davis, Andrew Cooper, Anthony PERARD, Michal Orzel,
	Julien Grall, Roger Pau Monné, Stefano Stabellini, xen-devel

On 21.09.2026 16:01, Oleksii Kurochko wrote:
> On 9/18/26 2:52 PM, Jan Beulich wrote:
>> On 27.08.2026 17:21, Oleksii Kurochko wrote:
>>> --- a/xen/arch/riscv/domain.c
>>> +++ b/xen/arch/riscv/domain.c
>>> @@ -136,6 +136,8 @@ static void vcpu_csr_init(struct vcpu *v)
>>>           v->arch.hstateen0 = (hstateen0 & csr_masks.hstateen0) |
>>>                               csr_masks.ro_one.hstateen0;
>>>       }
>>> +
>>> +    v->arch.hie = MIP_SGEIP;
>>
>> Neither part of the rhs identifier has anything to do with the CSR
>> having its default value set here. That's perhaps again a piece of
>> RISC-V I'm missing, but I can't make sense of this.
> 
> All interrupt pending/enable CSRs share one bit layout (bit N 
> corresponds to interrupt cause N), which is why MIP_SGEIP happened to be 
> numerically right. I'll use BIT(IRQ_S_GEXT, UL) instead and add a 
> comment: hie.SGEIE is what allows the SGEI raised through hgeie to be 
> taken by Xen at all, while hie's VS-level bits alias the guest's vsie 
> and are left cleared. I will apply the following change:
> 
> -    v->arch.hie = MIP_SGEIP;
> +    /*
> +     * Enable SGEIs, so that a guest interrupt file marked in HGEIE while
> +     * the vCPU is descheduled can raise an interrupt to Xen.
> +     *
> +     * The VS-level bits of hie alias the guest's vsie, which is saved and
> +     * restored separately, so they are left clear here.
> +     */
> +    v->arch.hie = BIT(IRQ_S_GEXT, UL);

It's somewhat better this way, yes. And as said - in not looking great to
me (doc-wise) is likely an issue of mine, not of yours.

>>> --- a/xen/arch/riscv/imsic.c
>>> +++ b/xen/arch/riscv/imsic.c
>>> @@ -510,12 +510,31 @@ void cf_check imsic_ctxt_switch_from(struct vcpu *v)
>>>   
>>>       write_lock_irqsave(&imsic_state->vsfile_lock, flags);
>>>       imsic_state->vsfile_cpu = v->processor;
>>> +    /*
>>> +     * Start to observe the VS-file from HS-mode: while the vCPU isn't
>>> +     * running an interrupt pending in its VS-file is reported through HGEIP
>>> +     * instead of being delivered to VS-mode, which lets Xen wake the vCPU up.
>>> +     */
>>> +    csr_set(CSR_HGEIE, BIT(imsic_state->guest_file_id, UL));
>>>       write_unlock_irqrestore(&imsic_state->vsfile_lock, flags);
>>>   }
>>
>> It is suspicious for the HGEIE write to be the last step. How's this free
>> of a window where an interrupt is lost. (Sorry, likely another blind spot
>> of mine wrt RISC-V.)
> 
> There's no window: a guest interrupt file's hgeip bit is 
> level-sensitive, i.e. it reflects whether the file currently has a 
> pending-and-enabled interrupt (the MSI itself stays latched in the 
> file's eip[] until the guest claims it), and hip.SGEIP is simply (hgeip 
> & hgeie) != 0. So an MSI arriving after the vCPU stopped running but 
> before hgeie is set raises an SGEI as soon as the bit is set, taken once 
> interrupts are re-enabled. I'll extend the comment to say so:
> 
>      /*
>       * Start to observe the VS-file from HS-mode: while the vCPU isn't
>       * running an interrupt pending in its VS-file is reported through 
> HGEIP
>       * instead of being delivered to VS-mode, which lets Xen wake the 
> vCPU up.
>       *
>       * HGEIP is level-sensitive, reflecting the VS-file's current state, so
>       * an interrupt that became pending before this point raises an SGEI as
>       * soon as the bit is set in HGEIE; nothing is lost in between.
>       */
> 
> Does it make sense?

I think I get what you're trying to explain, but the term "level-sensitive"
here doesn't really help. I'm unconvinced you actually mean that, as it
requires pins / physical signals, which don't exist with MSI. It feels like
you may mean "sticky" instead.

>>>   void cf_check imsic_ctxt_switch_to(struct vcpu *v)
>>>   {
>>> -    /* Nothing to do */
>>> +    struct vimsic_state *imsic_state = v->arch.vimsic_state;
>>> +    unsigned long flags;
>>> +
>>> +    /* A s/w VS-file is never observed through HGEIP. */
>>> +    if ( !vcpu_guest_file_id(v) )
>>> +        return;
>>> +
>>> +    /*
>>> +     * The vCPU is about to run, so hstatus.VGEIN delivers the VS-file's
>>> +     * interrupts to it directly and there is nothing left for Xen to observe.
>>> +     */
>>> +    read_lock_irqsave(&imsic_state->vsfile_lock, flags);
>>> +    csr_clear(CSR_HGEIE, BIT(imsic_state->guest_file_id, UL));
>>> +    read_unlock_irqrestore(&imsic_state->vsfile_lock, flags);
>>>   }
>>
>> How can this be a read-lock when you write a CSR?
> 
> But here is a protection of ->guest_file_id not of write to a CSR and we 
> want to not have a change of ->guest_file_id during an update of CSR_HGEIE.
> 
>> Or else - why is locking
>> here necessary in the firt place?
> 
> Strictly speaking no locking is needed with the current implementation: 
> HGEIE is local to this pCPU, and guest_file_id can't change 
> concurrently. It's updated either by this very pCPU ahead of switching 
> the vCPU in (imsic_migrate_vcpu() called from schedule(), 
> imsic_vsfile_attach() right after), or while the vCPU can't be 
> scheduled: sched_unit_migrate_finish() defers to unit_context_saved()
> while the unit is running, and sched_move_domain() pauses the domain.
> 
> (and the similar are true for imsic_ctxt_switch_from())
> 
> But IMO we should keep around ->vsfile_lock to not miss the case where 
> some case will update ->guest_file_id in parallel with 
> imsic_ctxt_switch_to().
> 
> Does it make to continue to have read_lock_irqsave(...->vsfile_lock, 
> ...) here just for potential future cases?

You get to judge. If the lock typically is uncontended, keeping things
as-is may indeed be fine. Introducing a bottleneck "just for potential
future cases" otoh wouldn't look overly nice to me.

Jan


^ permalink raw reply	[flat|nested] 244+ messages in thread

* Re: [PATCH v2 30/39] xen/riscv: prepare new IMSIC VS-file
  2026-08-27 15:21 ` [PATCH v2 30/39] xen/riscv: prepare new IMSIC VS-file Oleksii Kurochko
  2026-09-14 13:13   ` Jan Beulich
@ 2026-09-21 16:15   ` Baptiste Le Duc
  2026-09-22  6:32     ` Jan Beulich
  2026-09-22 13:01     ` Oleksii Kurochko
  1 sibling, 2 replies; 244+ messages in thread
From: Baptiste Le Duc @ 2026-09-21 16:15 UTC (permalink / raw)
  To: Oleksii Kurochko, xen-devel
  Cc: Romain Caritey, Zheng Zhang, Alistair Francis, Connor Davis,
	Andrew Cooper, Anthony PERARD, Michal Orzel, Jan Beulich,
	Julien Grall, Roger Pau Monné, Stefano Stabellini



On 8/27/26 5:24 PM, Oleksii Kurochko wrote:
> Implement first steps of migration a vCPU to a different guest interrupt file
> procedure:
> - At the old interrupt file, save to memory the values of registers
>    eidelivery and eithreshold, and set eidelivery = 0.
> - At the new interrupt file, set eidelivery = 0, and zero all
>    implemented interrupt-pending bits (the eip array).
> 
> The following steps will be introduced in follow-up patches.
> 
> Add a BUG_ON("unimplemented") placeholder in imsic_migrate_vcpu() to guard
> against silent incorrect behaviour or unexpected panics in guest VMs until
> the function is fully implemented.
> 
> vgein_assign() will be introduced later in a separate patch, for not it is
> only stub.
> 
> Signed-off-by: Oleksii Kurochko <oleksii.kurochko@gmail.com>
> ---
> Changes in v2:
>   - New patch.
> ---
> ---
>   xen/arch/riscv/aia.c             |   9 ++
>   xen/arch/riscv/imsic.c           | 159 +++++++++++++++++++++++++++++++
>   xen/arch/riscv/include/asm/aia.h |   4 +
>   xen/include/xen/config.h         |   1 +
>   4 files changed, 173 insertions(+)
> 
> diff --git a/xen/arch/riscv/aia.c b/xen/arch/riscv/aia.c
> index e31c9c2d24b6..75c82bcfa1b3 100644
> --- a/xen/arch/riscv/aia.c
> +++ b/xen/arch/riscv/aia.c
> @@ -1,8 +1,10 @@
>   /* SPDX-License-Identifier: GPL-2.0-only */
>   
> +#include <xen/bug.h>
>   #include <xen/errno.h>
>   #include <xen/init.h>
>   #include <xen/sections.h>
> +#include <xen/sched.h>
>   #include <xen/types.h>
>   
>   #include <asm/cpufeature.h>
> @@ -21,3 +23,10 @@ void __init aia_init(void)
>   
>       _aia_usable = true;
>   }
> +
> +unsigned int vgein_assign(struct vcpu *v)
> +{
> +    BUG_ON("unimplemented\n");
> +
> +    return 0;
> +}
> diff --git a/xen/arch/riscv/imsic.c b/xen/arch/riscv/imsic.c
> index ad7fbe708bfd..516f0105352a 100644
> --- a/xen/arch/riscv/imsic.c
> +++ b/xen/arch/riscv/imsic.c
> @@ -26,6 +26,7 @@
>   #include <xen/spinlock.h>
>   #include <xen/xvmalloc.h>
>   
> +#include <asm/aia.h>
>   #include <asm/imsic.h>
>   
>   #define IMSIC_HART_SIZE(guest_bits) (BIT(guest_bits, U) * IMSIC_MMIO_PAGE_SZ)
> @@ -77,6 +78,64 @@ do {                            \
>       csr_clear(CSR_SIREG, v);    \
>   } while (0)
>   
> +#define imsic_vs_csr_write(c, v)    \
> +do {                                \
> +    csr_write(CSR_VSISELECT, (c));  \
> +    csr_write(CSR_VSIREG, (v));     \
> +} while ( 0 )
> +
> +/*
> + * Generic switchcase expansion pyramid.
> + * F is the per-operation leaf macro, ireg is the base register index.
> + * Optional extra args (e.g. an operation and/or a value) are forwarded to F
> + * via __VA_ARGS__.
> + *
> + * imsic_switchcase_break(ireg, op, v) - emit "case ireg: op(ireg,v); break;"
> + * imsic_switchcase_ret(ireg, op, ...) - emit "case ireg: return op(ireg[,v]);"
> + *   The variadic tail is optional so the same leaf works for both read (no v)
> + *   and swap (with v).
> + */
> +#define imsic_switchcase_break(ireg, op, v) \
> +    case ireg:                              \
> +        op(ireg, v);                        \
> +        break;
> +
> +#define imsic_switchcase_ret(ireg, op, ...) \
> +    case ireg:                              \
> +        return op(ireg, ##__VA_ARGS__);
> +
> +#define imsic_switchcase_2(F, ireg, ...)    \
> +    F(ireg + 0, ##__VA_ARGS__)              \
> +    F(ireg + 1, ##__VA_ARGS__)
> +#define imsic_switchcase_4(F, ireg, ...)    \
> +    imsic_switchcase_2(F, ireg + 0, ##__VA_ARGS__)  \
> +    imsic_switchcase_2(F, ireg + 2, ##__VA_ARGS__)
> +#define imsic_switchcase_8(F, ireg, ...)    \
> +    imsic_switchcase_4(F, ireg + 0, ##__VA_ARGS__)  \
> +    imsic_switchcase_4(F, ireg + 4, ##__VA_ARGS__)
> +#define imsic_switchcase_16(F, ireg, ...)   \
> +    imsic_switchcase_8(F, ireg + 0, ##__VA_ARGS__)  \
> +    imsic_switchcase_8(F, ireg + 8, ##__VA_ARGS__)
> +#define imsic_switchcase_32(F, ireg, ...)   \
> +    imsic_switchcase_16(F, ireg + 0, ##__VA_ARGS__) \
> +    imsic_switchcase_16(F, ireg + 16, ##__VA_ARGS__)
> +#define imsic_switchcase_64(F, ireg, ...)   \
> +    imsic_switchcase_32(F, ireg + 0, ##__VA_ARGS__) \
> +    imsic_switchcase_32(F, ireg + 32, ##__VA_ARGS__)
> +
> +static void imsic_eix_write(unsigned int ireg, unsigned long val)
> +{
> +    switch ( ireg )
> +    {
> +    imsic_switchcase_64(imsic_switchcase_break, IMSIC_EIP0,
> +                        imsic_vs_csr_write, val)
> +    imsic_switchcase_64(imsic_switchcase_break, IMSIC_EIE0,
> +                        imsic_vs_csr_write, val)
> +    default:
> +        ASSERT_UNREACHABLE();
> +    }
> +}
> +
>   unsigned int vcpu_guest_file_id(const struct vcpu *v)
>   {
>       return ACCESS_ONCE(v->arch.vimsic_state->guest_file_id);
> @@ -389,6 +448,76 @@ int cf_check vcpu_imsic_init(struct vcpu *v)
>       return 0;
>   }
>   
> +/*
> + * Arguments of the imsic_vsfile_local_*() helpers, which are executed by the
> + * pCPU owning the interrupt file, thereby through imsic_call_on_cpu().
> + */
> +struct imsic_vsfile_data {
> +    unsigned int hgei;
> +    unsigned int nr_eix;
> +    struct imsic_mrif *mrif;
> +};
> +
> +/*
> + * Execute func() on the pCPU which owns the IMSIC interrupt file func() is
> + * going to work with.
> + *
> + * An IMSIC VS-file is reachable only through hstatus.VGEIN of the hart the
> + * file belongs to, and a guest interrupt file index is meaningless on any
> + * other hart, so such work always has to be done by that very hart.
> + *
> + * The local case runs with IRQs disabled to provide func() with the same
> + * environment it is given when it is called from the function call IPI
> + * handler.
> + */
> +static void imsic_call_on_cpu(unsigned int cpu, void (*func)(void *),
> +                              void *data)
> +{
> +    if ( cpu == smp_processor_id() )
> +    {
> +        unsigned long flags;
> +
> +        local_irq_save(flags);
> +        func(data);
> +        local_irq_restore(flags);
> +    }
> +    else
> +        on_selected_cpus(cpumask_of(cpu), func, data, 1);
> +}
> +
> +static void cf_check imsic_vsfile_local_clear(void *data)
I think the remark from Jan to direclty pass the type instead of void
could be applied here.
> +{
> +    unsigned int i;
> +    const struct imsic_vsfile_data *idata = data;
> +    unsigned long new_hstatus, old_hstatus, old_vsiselect;
> +
> +    /* We can only zero-out if we have a IMSIC VS-file */
> +    if ( !idata->hgei )
> +        return;
> +
> +    old_vsiselect = csr_read(CSR_VSISELECT);
> +    old_hstatus = csr_read(CSR_HSTATUS);
> +    new_hstatus = old_hstatus & ~HSTATUS_VGEIN;
> +    new_hstatus |= MASK_INSR(idata->hgei, HSTATUS_VGEIN);
> +    csr_write(CSR_HSTATUS, new_hstatus);
> +
> +    imsic_vs_csr_write(IMSIC_EIDELIVERY, 0);
> +    imsic_vs_csr_write(IMSIC_EITHRESHOLD, 0);
> +
> +    for ( i = 0; i < idata->nr_eix; i++ )
> +    {
> +        imsic_eix_write(IMSIC_EIP0 + i * 2, 0);
> +        imsic_eix_write(IMSIC_EIE0 + i * 2, 0);
> +#ifdef CONFIG_RISCV_32
> +        imsic_eix_write(IMSIC_EIP0 + i * 2 + 1, 0);
> +        imsic_eix_write(IMSIC_EIE0 + i * 2 + 1, 0);
> +#endif
> +    }
> +
> +    csr_write(CSR_HSTATUS, old_hstatus);
> +    csr_write(CSR_VSISELECT, old_vsiselect);
> +}
> +
>   void cf_check vcpu_imsic_deinit(struct vcpu *v)
>   {
>       XVFREE(v->arch.vimsic_state);
> @@ -689,6 +818,14 @@ int __init vimsic_make_domu_dt_node(struct kernel_info *kinfo,
>   
>   void imsic_migrate_vcpu(struct vcpu *v)
>   {
> +    unsigned int new_vsfile_hgei;
> +    unsigned int new_vsfile_cpu;
> +    unsigned int nr_hw_eix = DIV_ROUND_UP(imsic_cfg.nr_ids + 1,
> +                                          BITS_PER_TYPE(uint64_t));
This value appears to remain constant after initialization, since it 
depends directly on the hw,
so it is not necessary to calculate it each time.
> +    struct imsic_vsfile_data vsfile_data = {
> +        .nr_eix = nr_hw_eix,
> +    };
> +
>       /*
>        * The scheduler can mark a freshly created vCPU's unit as migrated and
>        * invoke this before the vCPU has ever run (see the migrated branch in
> @@ -699,5 +836,27 @@ void imsic_migrate_vcpu(struct vcpu *v)
>       if ( v->arch.last_cpu == NR_CPUS )
>           return;
>   
> +    /*
> +     * At this point, all interrupt producers are still using the old IMSIC
> +     * VS-file.
> +     */
> +
> +    /*
> +     * Latch the pCPU the new interrupt file is taken from: vgein_assign()
> +     * allocates it from v->processor's pool of guest interrupt files, and
> +     * only that hart can access the file afterwards.
> +     */
> +    new_vsfile_cpu = v->processor;
> +
> +    new_vsfile_hgei = vgein_assign(v);
> +
> +    /* We don't support SW interrupt files at the moment. */
> +    BUG_ON(!new_vsfile_hgei);
> +
> +    vsfile_data.hgei = new_vsfile_hgei;
> +
> +    /* Zero-out new IMSIC VS-file */
> +    imsic_call_on_cpu(new_vsfile_cpu, imsic_vsfile_local_clear, &vsfile_data);
> +
>       BUG_ON("unimplemented");
>   }
> diff --git a/xen/arch/riscv/include/asm/aia.h b/xen/arch/riscv/include/asm/aia.h
> index aaa4bf91fc75..53a1efb042f8 100644
> --- a/xen/arch/riscv/include/asm/aia.h
> +++ b/xen/arch/riscv/include/asm/aia.h
> @@ -3,8 +3,12 @@
>   #ifndef RISCV_AIA_H
>   #define RISCV_AIA_H
>   
> +struct vcpu;
> +
>   bool aia_usable(void);
>   
>   void aia_init(void);
>   
> +unsigned int vgein_assign(struct vcpu *v);
> +
>   #endif /* RISCV_AIA_H */
> diff --git a/xen/include/xen/config.h b/xen/include/xen/config.h
> index dddc8e1920fe..0e29976e8203 100644
> --- a/xen/include/xen/config.h
> +++ b/xen/include/xen/config.h
> @@ -100,6 +100,7 @@
>   #define BITS_PER_INT    (BITS_PER_BYTE * __SIZEOF_INT__)
>   #define BITS_PER_LONG   (BITS_PER_BYTE * BYTES_PER_LONG)
>   #define BITS_PER_LLONG  (BITS_PER_BYTE * __SIZEOF_LONG_LONG__)
> +#define BITS_PER_TYPE(type) (sizeof(type) * BITS_PER_BYTE)
>   
>   /* It is assumed that sizeof(void *) == __alignof(void *) */
>   #define POINTER_ALIGN   __SIZEOF_POINTER__


^ permalink raw reply	[flat|nested] 244+ messages in thread

* Re: [PATCH v2 30/39] xen/riscv: prepare new IMSIC VS-file
  2026-09-21 16:15   ` Baptiste Le Duc
@ 2026-09-22  6:32     ` Jan Beulich
  2026-09-22 13:01     ` Oleksii Kurochko
  1 sibling, 0 replies; 244+ messages in thread
From: Jan Beulich @ 2026-09-22  6:32 UTC (permalink / raw)
  To: Baptiste Le Duc
  Cc: Romain Caritey, Zheng Zhang, Alistair Francis, Connor Davis,
	Andrew Cooper, Anthony PERARD, Michal Orzel, Julien Grall,
	Roger Pau Monné, Stefano Stabellini, Oleksii Kurochko,
	xen-devel

On 21.09.2026 18:15, Baptiste Le Duc wrote:
> On 8/27/26 5:24 PM, Oleksii Kurochko wrote:
>> @@ -389,6 +448,76 @@ int cf_check vcpu_imsic_init(struct vcpu *v)
>>       return 0;
>>   }
>>   
>> +/*
>> + * Arguments of the imsic_vsfile_local_*() helpers, which are executed by the
>> + * pCPU owning the interrupt file, thereby through imsic_call_on_cpu().
>> + */
>> +struct imsic_vsfile_data {
>> +    unsigned int hgei;
>> +    unsigned int nr_eix;
>> +    struct imsic_mrif *mrif;
>> +};
>> +
>> +/*
>> + * Execute func() on the pCPU which owns the IMSIC interrupt file func() is
>> + * going to work with.
>> + *
>> + * An IMSIC VS-file is reachable only through hstatus.VGEIN of the hart the
>> + * file belongs to, and a guest interrupt file index is meaningless on any
>> + * other hart, so such work always has to be done by that very hart.
>> + *
>> + * The local case runs with IRQs disabled to provide func() with the same
>> + * environment it is given when it is called from the function call IPI
>> + * handler.
>> + */
>> +static void imsic_call_on_cpu(unsigned int cpu, void (*func)(void *),
>> +                              void *data)
>> +{
>> +    if ( cpu == smp_processor_id() )
>> +    {
>> +        unsigned long flags;
>> +
>> +        local_irq_save(flags);
>> +        func(data);
>> +        local_irq_restore(flags);
>> +    }
>> +    else
>> +        on_selected_cpus(cpumask_of(cpu), func, data, 1);
>> +}
>> +
>> +static void cf_check imsic_vsfile_local_clear(void *data)
> I think the remark from Jan to direclty pass the type instead of void
> could be applied here.

No, the function pointer is passed to ...

>> @@ -699,5 +836,27 @@ void imsic_migrate_vcpu(struct vcpu *v)
>>       if ( v->arch.last_cpu == NR_CPUS )
>>           return;
>>   
>> +    /*
>> +     * At this point, all interrupt producers are still using the old IMSIC
>> +     * VS-file.
>> +     */
>> +
>> +    /*
>> +     * Latch the pCPU the new interrupt file is taken from: vgein_assign()
>> +     * allocates it from v->processor's pool of guest interrupt files, and
>> +     * only that hart can access the file afterwards.
>> +     */
>> +    new_vsfile_cpu = v->processor;
>> +
>> +    new_vsfile_hgei = vgein_assign(v);
>> +
>> +    /* We don't support SW interrupt files at the moment. */
>> +    BUG_ON(!new_vsfile_hgei);
>> +
>> +    vsfile_data.hgei = new_vsfile_hgei;
>> +
>> +    /* Zero-out new IMSIC VS-file */
>> +    imsic_call_on_cpu(new_vsfile_cpu, imsic_vsfile_local_clear, &vsfile_data);

... imsic_call_on_cpu() here, which in turn passes it to on_selected_cpus().

Jan


^ permalink raw reply	[flat|nested] 244+ messages in thread

* Re: [PATCH v2 38/39] xen/riscv: implement continue_new_vcpu()
  2026-09-21 12:12   ` Jan Beulich
@ 2026-09-22  8:23     ` Oleksii Kurochko
  2026-09-22 10:20       ` Jan Beulich
  0 siblings, 1 reply; 244+ messages in thread
From: Oleksii Kurochko @ 2026-09-22  8:23 UTC (permalink / raw)
  To: Jan Beulich
  Cc: Romain Caritey, Baptiste Le Duc, Zheng Zhang, Alistair Francis,
	Connor Davis, Andrew Cooper, Anthony PERARD, Michal Orzel,
	Julien Grall, Roger Pau Monné, Stefano Stabellini, xen-devel



On 9/21/26 2:12 PM, Jan Beulich wrote:
> On 27.08.2026 17:21, Oleksii Kurochko wrote:
>> continue_new_vcpu() is the arch hook invoked the first time a freshly
>> created vCPU is scheduled. Implement both cases it has to cover:
>>   - for the idle vCPU, switch to its own stack and jump to idle_loop();
>>   - for a guest vCPU, restore hstatus and enter the guest through the new
>>     return_to_new_vcpu() path in entry.S, which loads sepc, passes the
>>     hart id in a0 and the DTB address in a1 as expected by the RISC-V
>>     boot protocol, sets sstatus.SPP and executes sret.
> 
> Is this a requirement for all CPUs, or just for the boot one? (I can't
> quite see why secondary processors would need passing a DTB address.)

It is requirement for boot one. For secondary processors it is HSM boot 
data which is passed to sbi_hsm_hart_start() and then intercpeted by Xen.

At the moment of writing of this commit message we have only boot CPU 
and so only DTB could be passed.

I can update the commit message and the comment in return_to_new_vcpu() 
to tell that it could be DTB address for boot cpu and/or for secondary 
CPUs HSM boot data or it will be better to add info about HSM boot data 
during and an introduction of secondary CPUs support?

> 
>> Interrupts have to stay disabled across the restore. The trap entry
>> logic implicitly clears hstatus.SPV, so an interrupt taken between the
>> write of hstatus and sret would make sret return to HS-mode instead of
>> VS-mode, and restoring SPV afterwards is non-trivial. Instead interrupts
>> are simply kept off and sstatus.SPIE is set, so that SIE is restored from
>> SPIE once sret has been executed.
> 
> As written this reads as if the guest would be responsible for doing this.
> Isn't it rather SRET itself which does this?

IIUC to which part you refer then yes, it is SRET itself which does 
this. So some re-wording should be done ...

> 
>> Also, it follows what hardware will do
>> with real CPU which is also started with interrupts disabled.
> 
> Further up, aiui, you talk about the host's interrupt state. How vCPU-s
> are started, however, is virtual interrupt state. Mixing both isn't
> very helpful.

...:

Interrupts have to stay disabled across the restore. The trap entry
logic implicitly clears hstatus.SPV, so an interrupt taken between the
write of hstatus and sret would make sret return to HS-mode instead of
VS-mode, and restoring SPV afterwards is non-trivial. Hence interrupts
are kept disabled, and sstatus.SPIE is set so that sret itself 
re-enables them (SIE := SPIE) as part of entering the guest.

Then it will be also need to update the comment inside 
continue_new_vcpu() to:

-         * To avoid this, interrupts are kept disabled during the restore.
-         * Additionally, setting sstatus.SPIE=1 ensures that after sret is
-         * executed (as sstatus.SIE will be loaded from SPIE), HS-mode will
-         * continue to receive interrupts normally.
+         * To avoid this, interrupts are kept disabled during the restore,
+         * and sstatus.SPIE is set so that sret itself re-enables them
+         * (SIE := SPIE) as part of entering the guest.
           */

Would it be the wording okay for you now?


> 
>> Introduce get_cpu_info() and reset_stack_and_jump() in asm/current.h,
>> needed by the above. get_cpu_info() is a macro rather than a static
>> inline because asm/current.h is pulled in by <xen/percpu.h> before
>> this_cpu() is defined and before <xen/sched.h> completes struct vcpu.
> 
> This is odd, given the similarity to Arm. They get away without using
> "current", and hence without using this_cpu().

Arm calculates struct cpu_info * based on sp register + STACK_SIZE:

static inline struct cpu_info *get_cpu_info(void)
{
#ifdef __clang__
     unsigned long sp;

     asm ("mov %0, sp" : "=r" (sp));
#else
     register unsigned long sp asm ("sp");
#endif

     return (struct cpu_info *)((sp & ~(STACK_SIZE - 1)) +
                                STACK_SIZE - sizeof(struct cpu_info));
}

what is equal to current->arch.cpu_info as I can see based on how both 
arch-es are initializing v->arch.cpu_info what is used in RISC-V.

> 
>> --- a/xen/arch/riscv/domain.c
>> +++ b/xen/arch/riscv/domain.c
>> @@ -8,10 +8,13 @@
>>   #include <xen/smp.h>
>>   #include <xen/vmap.h>
>>   
>> +#include <asm/aia.h>
>> +#include <asm/aplic.h>
>>   #include <asm/bitops.h>
>>   #include <asm/cpufeature.h>
>>   #include <asm/csr.h>
>>   #include <asm/current.h>
>> +#include <asm/imsic.h>
>>   #include <asm/intc.h>
>>   #include <asm/mmio.h>
>>   #include <asm/riscv_encoding.h>
> 
> What makes these additions necessary here?

None, they are leftovers from an earlier version of this patch. I'll 
drop them; <asm/imsic.h> will be added by the patch which starts using
imsic_vsfile_attach().

> 
>> @@ -140,9 +143,43 @@ static void vcpu_csr_init(struct vcpu *v)
>>       v->arch.hie = MIP_SGEIP;
>>   }
>>   
>> +static void schedule_tail(struct vcpu *prev);
>> +static void noreturn idle_loop(void);
>> +void noreturn return_to_new_vcpu(void);
> 
> For this last one: asmlinkage?

Will add.

> 
>>   static void continue_new_vcpu(struct vcpu *prev)
>>   {
>> -    BUG_ON("unimplemented\n");
>> +    schedule_tail(prev);
>> +
>> +    if ( is_idle_vcpu(current) )
>> +        reset_stack_and_jump(idle_loop);
>> +    else
> 
> This is the kind of "else" which I consider particularly confusing: It
> suggests that the if() body can actually be exited at the bottom, when
> (by the name "reset_stack_and_jump") it hopefully cannot.

Agreed, the "else" isn't needed here. I kept it only because it looked 
more symmetric to me, but I'll drop it and move its body out to the same 
level as the if().

> 
>> +    {
>> +        /*
>> +         * During a context switch to a new vCPU, interrupts must be disabled
>> +         * to guarantee that the vCPU's CSR state can be safely restored into
>> +         * the hart without being clobbered by an interrupt trap.
>> +         *
>> +         * For example, when return_to_new_vcpu() finishes, it executes sret.
>> +         * At that point, the hart checks hstatus.SPV=1 and sstatus.SPP=1 in
>> +         * order to return from HS-mode into VS-mode. If an interrupt were to
>> +         * arrive before sret, the trap entry logic would implicitly clear
>> +         * hstatus.SPV to 0. Correctly restoring it afterwards is non-trivial,
>> +         * and if left as 0, sret would incorrectly return to HS-mode instead
>> +         * of VS-mode.
>> +         *
>> +         * To avoid this, interrupts are kept disabled during the restore.
>> +         * Additionally, setting sstatus.SPIE=1 ensures that after sret is
>> +         * executed (as sstatus.SIE will be loaded from SPIE), HS-mode will
>> +         * continue to receive interrupts normally.
>> +         */
>> +        local_irq_disable();
>> +        csr_set(CSR_SSTATUS, SSTATUS_SPIE);
>> +
>> +        csr_write(CSR_HSTATUS, vcpu_guest_cpu_user_regs(current)->hstatus);
>> +
>> +        reset_stack_and_jump(return_to_new_vcpu);
> 
> What are the criteria by which you split CSR accesses between doing some here
> and some in return_to_new_vcpu()? In particular you set sstatus.SPIE here but
> sstatus.SPP there, when both could - I think - be done with a single CSR
> access.

There was no real criterion (and SPIE is set here only for better 
explanation of the comment). I'll move all the CSR setup sret depends on
(hstatus, sepc, sstatus.SPP and sstatus.SPIE) into return_to_new_vcpu(),
like the regular return-to-guest path in entry.S already does, and set 
SPP and SPIE with a single CSR access there. Only local_irq_disable() 
will stay in continue_new_vcpu().

I will do the following:

+     * return_to_new_vcpu() sets up hstatus.SPV, sstatus.SPP and sepc so
+     * that sret enters the guest in VS-mode. A trap taken in HS-mode
+     * overwrites all of them (trap entry clears hstatus.SPV in 
particular),
+     * so interrupts have to stay disabled until sret. They are 
re-enabled by
+     * sret itself, as return_to_new_vcpu() also sets sstatus.SPIE.
       */
      local_irq_disable();
-    csr_set(CSR_SSTATUS, SSTATUS_SPIE);
-
-    csr_write(CSR_HSTATUS, vcpu_guest_cpu_user_regs(current)->hstatus);

      reset_stack_and_jump(return_to_new_vcpu);

and then:

-/* t0 is used as a temporary reg and is clobbered to oblivion */
+/*
+ * Enter a vCPU for the first time. Must be called with interrupts 
disabled,
+ * see continue_new_vcpu().
+ *
+ * t0 is used as a temporary reg and is clobbered to oblivion.
+ */
  FUNC(return_to_new_vcpu)
          /* Swap tp with sscratch */
          csrrw   tp, CSR_SSCRATCH, tp

          /* Set vCPU registers */
+        REG_L   t0, CPU_USER_REGS_HSTATUS(sp)
+        csrw    CSR_HSTATUS, t0
+
          REG_L   t0, CPU_USER_REGS_SEPC(sp)
-        csrw    sepc, t0
+        csrw    CSR_SEPC, t0

          /* Hartid goes to a0 */
          REG_L   a0, CPU_USER_REGS_A0(sp)

          /* DTB goes to a1 */
          REG_L   a1, CPU_USER_REGS_A1(sp)

-        /* Set guest mode to supervisor */
-        li      t0, SSTATUS_SPP
+        /* Return to (V)S-mode, with interrupts re-enabled by sret */
+        li      t0, SSTATUS_SPP | SSTATUS_SPIE
          csrs    CSR_SSTATUS, t0

          /* Enter guest */
          sret
  END(return_to_new_vcpu)


> 
>> --- a/xen/arch/riscv/entry.S
>> +++ b/xen/arch/riscv/entry.S
>> @@ -143,3 +143,26 @@ FUNC(__context_switch)
>>   
>>           ret
>>   END(__context_switch)
>> +
>> +/* t0 is used as a temporary reg and is clobbered to oblivion */
>> +FUNC(return_to_new_vcpu)
>> +        /* Swap tp with sscratch */
>> +        csrrw   tp, CSR_SSCRATCH, tp
> 
> What is this about? I'm not aware of any counterpart code, yet all on its
> own this I can't see it being overly useful.

It will be needed later when a guest will be able to launch to 
distinguish in handle_trap() [1] if a trap is from guest or not. I can 
drop it for now and re-introduce it with the code of handle_trap() or as 
an option I could update the comment above to:

         /*
          * SSCRATCH holds this hart's struct pcpu_info while a guest 
runs and
          * is zero while Xen runs, so that a trap handler can tell the two
          * apart: tp is Xen's pointer to pcpu_info in Xen context, but 
belongs
          * to the guest once sret has been executed. Establish that by 
swapping
          * the two here; the trap path swaps them back.
          */
         csrrw   tp, CSR_SSCRATCH, tp

(but then the last part will point to the part which isn't yet 
introduced so probably it will be better to drop this line for now)

[1] 
https://gitlab.com/xen-project/people/olkur/xen/-/blob/riscv-next-upstreaming/xen/arch/riscv/entry.S?ref_type=heads&blame=1#L15


> 
>> +        /* Set vCPU registers */
>> +        REG_L   t0, CPU_USER_REGS_SEPC(sp)
>> +        csrw    sepc, t0
>> +
>> +        /* Hartid goes to a0 */
>> +        REG_L   a0, CPU_USER_REGS_A0(sp)
>> +
>> +        /* DTB goes to a1 */
>> +        REG_L   a1, CPU_USER_REGS_A1(sp)
> 
> The fields loaded are merely .a0 and .a1 of the register struct. There's
> nothing here making sure (all on its own) that what is loaded is what is
> said by the comments. If e.g. the first comment was /* .a0 holds the
> hart id */ or some such to remind readers what is being loaded without
> giving the impression that the correct value is _established_ here, that
> may be better.

I will re-word the comments in suggested way:

-        /* Hartid goes to a0 */
+        /* .a0 holds the hart id */
          REG_L   a0, CPU_USER_REGS_A0(sp)

-        /* DTB goes to a1 */
+        /* .a1 holds the address of the DTB */
          REG_L   a1, CPU_USER_REGS_A1(sp)


> 
>> +        /* Set guest mode to supervisor */
>> +        li      t0, SSTATUS_SPP
>> +        csrs    CSR_SSTATUS, t0
>> +
>> +        /* Enter guest */
>> +        sret
>> +END(return_to_new_vcpu)
> 
> Aiui SRET does not switch stacks. Shouldn't you therefore clear sp here?
> And perhaps also other GPRs, not the least ra? Exposing hypervisor
> register values to guests is, well, a bit of a problem.

Good point, sret leaves all GPRs as they are, so the guest would indeed 
see Xen's sp, ra and friends. Only a0 and a1 are architecturally 
meaningful for a booting hart, so I'll clear every other GPR right 
before sret.

I will add the following before sret:

         /*
          * sret doesn't switch stacks and leaves the GPRs alone, so every
          * register which isn't meaningful to the vCPU being started 
has to be
          * cleared here: otherwise the guest would see Xen's values, sp 
(this
          * vCPU's Xen stack) and ra among them.
          */
         .irp reg, ra, sp, gp, tp, t0, t1, t2, s0, s1, a2, a3, a4, a5, 
a6, a7, \
                   s2, s3, s4, s5, s6, s7, s8, s9, s10, s11, t3, t4, t5, t6
         mv      \reg, zero
         .endr

Thanks.

~ Oleksii


^ permalink raw reply	[flat|nested] 244+ messages in thread

* Re: [PATCH v2 39/39] xen/riscv: introduce IMSIC h/w interrupt file attaching to vcpu
  2026-09-21 12:32   ` Jan Beulich
@ 2026-09-22  8:31     ` Oleksii Kurochko
  2026-09-22 10:23       ` Jan Beulich
  0 siblings, 1 reply; 244+ messages in thread
From: Oleksii Kurochko @ 2026-09-22  8:31 UTC (permalink / raw)
  To: Jan Beulich
  Cc: Romain Caritey, Baptiste Le Duc, Zheng Zhang, Alistair Francis,
	Connor Davis, Andrew Cooper, Anthony PERARD, Michal Orzel,
	Julien Grall, Roger Pau Monné, Stefano Stabellini, xen-devel



On 9/21/26 2:32 PM, Jan Beulich wrote:
> On 27.08.2026 17:21, Oleksii Kurochko wrote:
>> Introduce imsic_vsfile_attach() to initialize the AIA-related state needed
>> for a vCPU to have a working guest interrupt file.
>>
>> A guest (VS) interrupt file must be mapped to one of a pCPU's
>> hardware interrupt files (if they exist), so the pCPU a vCPU will actually
>> run on needs to be known first. arch_vcpu_create() is therefore not a
>> suitable place to call vcpu_aia_init(), since the pCPU assigned to a
>> vCPU can still change before it is first scheduled. To avoid
>> reassigning the VS interrupt file id and remapping it to a different
>> pCPU's hardware interrupt file, imsic_vsfile_attach() is called from a
>> later point in the scheduling path (e.g. continue_new_vcpu()).
> 
> Hmm, why does first-time handling need to be this different from the
> handling of a vCPU moving across pCPU-s? The sole difference should be
> "no state to load" vs "load state that was saved on the old pCPU".
> 

Generally I think it could be the same but not all the steps done in 
migration functions aren't needed for new vCPU (despite of the fact they 
aren't harmful). For new vCPU it seems to me it is enough only to attach 
h/w interrupt file to vCPU.

~ Oleksii



^ permalink raw reply	[flat|nested] 244+ messages in thread

* Re: [PATCH v2 09/39] xen/riscv: implement virtual APLIC MMIO emulation
  2026-09-12  8:50           ` SeungJu Cheon
@ 2026-09-22  8:50             ` Oleksii Kurochko
  0 siblings, 0 replies; 244+ messages in thread
From: Oleksii Kurochko @ 2026-09-22  8:50 UTC (permalink / raw)
  To: SeungJu Cheon
  Cc: Jan Beulich, Romain Caritey, Baptiste Le Duc, Zheng Zhang,
	Alistair Francis, Connor Davis, Andrew Cooper, Anthony PERARD,
	Michal Orzel, Julien Grall, Roger Pau Monné,
	Stefano Stabellini, xen-devel



On 9/12/26 10:50 AM, SeungJu Cheon wrote:
> On Thu, Sep 10, 2026 at 04:24:06PM +0200, Oleksii Kurochko wrote:
> [...]
>> In v3 I'll (a) stop using ->processor and take the (guest_file_id,
>> vsfile_cpu) pair, which imsic_update_state() updates atomically under
>> vsfile_lock, and (b) do the snapshot plus the h/w TARGET write under
>> aplic.lock, which aplic_reconfigure_target() also holds. As
>> imsic_update_state() completes before aplic_reconfigure_target() (also that
>> could be checked in this patch series and is introduced a little bit later.
>> Probably I have to re-order some patches again) takes the lock, the emulated
>> write either happens before the scan (and gets fixed up, or skipped as
>> already correct) or after it (and sees the new location).
>>
>> Any better option I have now?
> 
> Unless I am missing something, the snapshot also needs to handle the
> case where the target vCPU has not been attached yet:
> vcpu_guest_file_id() returns zero until the vCPU has gone through
> imsic_vsfile_attach(), i.e. until it is scheduled for the first time,
> and vsfile_cpu is NR_CPUS until then.
> 
> With the current code, a write targeting such a vCPU makes
> aplic_msi_target_gen() program Guest Index 0 into the physical APLIC
> target register. According to AIA section 4.5.16, Guest Index 0 selects
> the hart's supervisor-level interrupt file rather than a VS-level guest
> interrupt file. Could this cause the MSI to be delivered to Xen's own
> interrupt file with the EIID supplied by the guest?
> 
> I also could not find where such a target would be updated once the
> VS-file is attached. imsic_migrate_vcpu() reprograms the relevant
> targets during migration, but the initial imsic_vsfile_attach() path
> does not appear to replay targets which were written before the
> attachment.
> 
> Whether a write targeting an unattached vCPU should be supported seems
> like a separate question. Independently of that choice, would it make
> sense to avoid programming the physical TARGET register while
> guest_file_id is zero? The virtual target could either be rejected, or
> retained in the shadow target[] and programmed once the VS-file is
> attached.
If the target vCPU hasn't attached yet it means that 
continue_to_new_vcpu() for it wasn't called so it will be rejected by 
target register emulated code:

...
             target_vcpu = domain_vcpu(currd, guest_hart_idx);

             if ( !target_vcpu )
             {
                 gdprintk(XENLOG_ERR, "Invalid vCPU id in target 
register\n");

                 /* Ignore such writings */
                 return true;
             }
...

(probably we should back to this question in v3 of this patch as several 
things were updated and some of your concerns mentioned here looks to me 
not really possible)

Thanks.

~ Oleksii


^ permalink raw reply	[flat|nested] 244+ messages in thread

* Re: [PATCH v2 23/39] xen/riscv: look up the exception table for any trap taken in Xen context
  2026-09-18  8:44   ` Baptiste Le Duc
@ 2026-09-22  9:31     ` Oleksii Kurochko
  0 siblings, 0 replies; 244+ messages in thread
From: Oleksii Kurochko @ 2026-09-22  9:31 UTC (permalink / raw)
  To: Baptiste Le Duc
  Cc: xen-devel, Romain Caritey, Zheng Zhang, Alistair Francis,
	Connor Davis, Andrew Cooper, Anthony PERARD, Michal Orzel,
	Jan Beulich, Julien Grall, Roger Pau Monné,
	Stefano Stabellini



On 9/18/26 10:44 AM, Baptiste Le Duc wrote:
>> do_trap() consulted the exception table only for CAUSE_ILLEGAL_INSTRUCTION,
>> which covers csr_read_safe() but not the hlv/hlvx sequences reading guest
>> memory: those fault with load/store (guest) page fault causes and would
>> reach do_unexpected_trap() instead of their fixup.
>>
>> Move the lookup ahead of the cause switch, and gate it on the trap having
>> been taken in Xen context and not being an interrupt:
>>
>> - sepc of a trap taken from the guest is a guest VA/PA, which the
>>    guest can point at an address listed in the exception table; Xen would
>>    then act on that entry and, for EX_TYPE_TRAP_INFO, write through a
>>    pointer fully under guest control. Entries are matched by exact address,
>>    so this needs no more than a numerical collision.
>>
>> - an interrupt taken at an address listed in the table would otherwise be
>>    "fixed up" as if the access itself had faulted, silently skipping it and
>>    handing the caller the interrupt's scause as a fault cause.
>>
>> Returning early skips check_for_pcpu_work(), which is correct: that only
>> runs for traps taken from the guest.
>>
>> With that in place a G-stage fault reaching the switch can no longer have
>> been caused by an hlv/hlvx covered by an entry, so anything left must have
>> come from the guest; assert as much.
> What means `assert as much` here? As you referencing any assert in the code,
> because in this patch, I couldn't find any.

It refers to the BUG_ON(!from_guest) in the guest page fault case of 
do_trap(). That BUG_ON() (and the comment above it) ended up in 
"xen/riscv: add guest page fault handling stub" when the series was 
reordered, so this sentence is stale.

I will drop 'assert as much' from commit message.

>>
>> Cache the "trap came from the guest" test in a local, it is now used four
>> times.
> Nit: By reading this sentence, I would expect to have the introduction
> of `from_guest` local here instead of in patch cc2d3b97e8
> xen/riscv: add guest page fault handling stub
> 

`from_guest` is introduced in "xen/riscv: add guest page fault handling 
stub" so it is a stale part of the commit message.

Thanks!

~ Oleksii


^ permalink raw reply	[flat|nested] 244+ messages in thread

* Re: [PATCH v2 38/39] xen/riscv: implement continue_new_vcpu()
  2026-09-22  8:23     ` Oleksii Kurochko
@ 2026-09-22 10:20       ` Jan Beulich
  2026-09-22 13:58         ` Oleksii Kurochko
  0 siblings, 1 reply; 244+ messages in thread
From: Jan Beulich @ 2026-09-22 10:20 UTC (permalink / raw)
  To: Oleksii Kurochko
  Cc: Romain Caritey, Baptiste Le Duc, Zheng Zhang, Alistair Francis,
	Connor Davis, Andrew Cooper, Anthony PERARD, Michal Orzel,
	Julien Grall, Roger Pau Monné, Stefano Stabellini, xen-devel

On 22.09.2026 10:23, Oleksii Kurochko wrote:
> On 9/21/26 2:12 PM, Jan Beulich wrote:
>> On 27.08.2026 17:21, Oleksii Kurochko wrote:
>>> continue_new_vcpu() is the arch hook invoked the first time a freshly
>>> created vCPU is scheduled. Implement both cases it has to cover:
>>>   - for the idle vCPU, switch to its own stack and jump to idle_loop();
>>>   - for a guest vCPU, restore hstatus and enter the guest through the new
>>>     return_to_new_vcpu() path in entry.S, which loads sepc, passes the
>>>     hart id in a0 and the DTB address in a1 as expected by the RISC-V
>>>     boot protocol, sets sstatus.SPP and executes sret.
>>
>> Is this a requirement for all CPUs, or just for the boot one? (I can't
>> quite see why secondary processors would need passing a DTB address.)
> 
> It is requirement for boot one. For secondary processors it is HSM boot 
> data which is passed to sbi_hsm_hart_start() and then intercpeted by Xen.
> 
> At the moment of writing of this commit message we have only boot CPU 
> and so only DTB could be passed.

That you're talking about Xen. Despite Xen being UP only right now, guests
still can have more than one vCPU, can't they?

> I can update the commit message and the comment in return_to_new_vcpu() 
> to tell that it could be DTB address for boot cpu and/or for secondary 
> CPUs HSM boot data or it will be better to add info about HSM boot data 
> during and an introduction of secondary CPUs support?

As per above you want to deal with multi-vCPU guests right now.

>>> Interrupts have to stay disabled across the restore. The trap entry
>>> logic implicitly clears hstatus.SPV, so an interrupt taken between the
>>> write of hstatus and sret would make sret return to HS-mode instead of
>>> VS-mode, and restoring SPV afterwards is non-trivial. Instead interrupts
>>> are simply kept off and sstatus.SPIE is set, so that SIE is restored from
>>> SPIE once sret has been executed.
>>
>> As written this reads as if the guest would be responsible for doing this.
>> Isn't it rather SRET itself which does this?
> 
> IIUC to which part you refer then yes, it is SRET itself which does 
> this. So some re-wording should be done ...
> 
>>
>>> Also, it follows what hardware will do
>>> with real CPU which is also started with interrupts disabled.
>>
>> Further up, aiui, you talk about the host's interrupt state. How vCPU-s
>> are started, however, is virtual interrupt state. Mixing both isn't
>> very helpful.
> 
> ...:
> 
> Interrupts have to stay disabled across the restore. The trap entry
> logic implicitly clears hstatus.SPV, so an interrupt taken between the
> write of hstatus and sret would make sret return to HS-mode instead of
> VS-mode, and restoring SPV afterwards is non-trivial. Hence interrupts
> are kept disabled, and sstatus.SPIE is set so that sret itself 
> re-enables them (SIE := SPIE) as part of entering the guest.
> 
> Then it will be also need to update the comment inside 
> continue_new_vcpu() to:
> 
> -         * To avoid this, interrupts are kept disabled during the restore.
> -         * Additionally, setting sstatus.SPIE=1 ensures that after sret is
> -         * executed (as sstatus.SIE will be loaded from SPIE), HS-mode will
> -         * continue to receive interrupts normally.
> +         * To avoid this, interrupts are kept disabled during the restore,
> +         * and sstatus.SPIE is set so that sret itself re-enables them
> +         * (SIE := SPIE) as part of entering the guest.
>            */
> 
> Would it be the wording okay for you now?

I think so, yes.

>>> --- a/xen/arch/riscv/entry.S
>>> +++ b/xen/arch/riscv/entry.S
>>> @@ -143,3 +143,26 @@ FUNC(__context_switch)
>>>   
>>>           ret
>>>   END(__context_switch)
>>> +
>>> +/* t0 is used as a temporary reg and is clobbered to oblivion */
>>> +FUNC(return_to_new_vcpu)
>>> +        /* Swap tp with sscratch */
>>> +        csrrw   tp, CSR_SSCRATCH, tp
>>
>> What is this about? I'm not aware of any counterpart code, yet all on its
>> own this I can't see it being overly useful.
> 
> It will be needed later when a guest will be able to launch to 
> distinguish in handle_trap() [1] if a trap is from guest or not. I can 
> drop it for now and re-introduce it with the code of handle_trap() or as 
> an option I could update the comment above to:
> 
>          /*
>           * SSCRATCH holds this hart's struct pcpu_info while a guest 
> runs and
>           * is zero while Xen runs, so that a trap handler can tell the two
>           * apart: tp is Xen's pointer to pcpu_info in Xen context, but 
> belongs
>           * to the guest once sret has been executed. Establish that by 
> swapping
>           * the two here; the trap path swaps them back.
>           */
>          csrrw   tp, CSR_SSCRATCH, tp
> 
> (but then the last part will point to the part which isn't yet 
> introduced so probably it will be better to drop this line for now)

Yes, introducing it together with the other related pieces is going to be
more consistent and easier to follow.

>>> +        /* Set guest mode to supervisor */
>>> +        li      t0, SSTATUS_SPP
>>> +        csrs    CSR_SSTATUS, t0
>>> +
>>> +        /* Enter guest */
>>> +        sret
>>> +END(return_to_new_vcpu)
>>
>> Aiui SRET does not switch stacks. Shouldn't you therefore clear sp here?
>> And perhaps also other GPRs, not the least ra? Exposing hypervisor
>> register values to guests is, well, a bit of a problem.
> 
> Good point, sret leaves all GPRs as they are, so the guest would indeed 
> see Xen's sp, ra and friends. Only a0 and a1 are architecturally 
> meaningful for a booting hart, so I'll clear every other GPR right 
> before sret.
> 
> I will add the following before sret:
> 
>          /*
>           * sret doesn't switch stacks and leaves the GPRs alone, so every
>           * register which isn't meaningful to the vCPU being started 
> has to be
>           * cleared here: otherwise the guest would see Xen's values, sp 
> (this
>           * vCPU's Xen stack) and ra among them.
>           */
>          .irp reg, ra, sp, gp, tp, t0, t1, t2, s0, s1, a2, a3, a4, a5, 
> a6, a7, \
>                    s2, s3, s4, s5, s6, s7, s8, s9, s10, s11, t3, t4, t5, t6
>          mv      \reg, zero
>          .endr

At which point discussing the clobbering of t0 in the comment ahead of
the function also isn't needed anymore.

Jan


^ permalink raw reply	[flat|nested] 244+ messages in thread

* Re: [PATCH v2 39/39] xen/riscv: introduce IMSIC h/w interrupt file attaching to vcpu
  2026-09-22  8:31     ` Oleksii Kurochko
@ 2026-09-22 10:23       ` Jan Beulich
  0 siblings, 0 replies; 244+ messages in thread
From: Jan Beulich @ 2026-09-22 10:23 UTC (permalink / raw)
  To: Oleksii Kurochko
  Cc: Romain Caritey, Baptiste Le Duc, Zheng Zhang, Alistair Francis,
	Connor Davis, Andrew Cooper, Anthony PERARD, Michal Orzel,
	Julien Grall, Roger Pau Monné, Stefano Stabellini, xen-devel

On 22.09.2026 10:31, Oleksii Kurochko wrote:
> On 9/21/26 2:32 PM, Jan Beulich wrote:
>> On 27.08.2026 17:21, Oleksii Kurochko wrote:
>>> Introduce imsic_vsfile_attach() to initialize the AIA-related state needed
>>> for a vCPU to have a working guest interrupt file.
>>>
>>> A guest (VS) interrupt file must be mapped to one of a pCPU's
>>> hardware interrupt files (if they exist), so the pCPU a vCPU will actually
>>> run on needs to be known first. arch_vcpu_create() is therefore not a
>>> suitable place to call vcpu_aia_init(), since the pCPU assigned to a
>>> vCPU can still change before it is first scheduled. To avoid
>>> reassigning the VS interrupt file id and remapping it to a different
>>> pCPU's hardware interrupt file, imsic_vsfile_attach() is called from a
>>> later point in the scheduling path (e.g. continue_new_vcpu()).
>>
>> Hmm, why does first-time handling need to be this different from the
>> handling of a vCPU moving across pCPU-s? The sole difference should be
>> "no state to load" vs "load state that was saved on the old pCPU".
> 
> Generally I think it could be the same but not all the steps done in 
> migration functions aren't needed for new vCPU (despite of the fact they 
> aren't harmful). For new vCPU it seems to me it is enough only to attach 
> h/w interrupt file to vCPU.

Sure, yet still imo you want to aim at re-using as much code as possible,
especially when otherwise you introduce multiple very similar but not
exactly identical variants of logic.

Jan


^ permalink raw reply	[flat|nested] 244+ messages in thread

* Re: [PATCH v2 24/39] xen/riscv: add helpers for decoding a trapped load or store
  2026-09-18  8:44   ` Baptiste Le Duc
@ 2026-09-22 11:03     ` Oleksii Kurochko
  0 siblings, 0 replies; 244+ messages in thread
From: Oleksii Kurochko @ 2026-09-22 11:03 UTC (permalink / raw)
  To: Baptiste Le Duc
  Cc: xen-devel, Romain Caritey, Zheng Zhang, Alistair Francis,
	Connor Davis, Andrew Cooper, Anthony PERARD, Michal Orzel,
	Jan Beulich, Julien Grall, Roger Pau Monné,
	Stefano Stabellini



On 9/18/26 10:44 AM, Baptiste Le Duc wrote:
>> emulate_load() and emulate_store() will both need to obtain the
>> instruction which caused a guest MMIO trap, decode it, and locate the
>> register operand it names. Add what the two share, ahead of either of
>> them being implemented: struct decoded_insn, insn_fetch_faulted(),
>> decode_ldst_insn(), guest_xlen(), guest_gpr() and advance_pc().
>>
>> The mask/match chain is adapted from Linux's KVM RISC-V implementation.
> Nit: maybe you could add the Origin: trailer as mentioned in the
> sending-patches.adoc.

I think that I will drop that sentense as the code in v3 is changed 
pretty significantly so it doesn't too much sense to mention that it was 
derived from Linux's KVM RISC-V.

Basically even if I will put Origin: now here then it will be still 
pretty hard to undestand what was used or not as I mentioned above there 
are a lot of changes done in comparison with original.

I will take it in mind for my future patches.

[...]
>> +
>> +/*
>> + * Decode the load or store instruction fetched into @di, filling in the
>> + * remaining fields of it (@di->insn and @di->insn_len are filled by
>> + * insn_fetch_faulted()).
>> + *
>> + * @xlen is the effective XLEN of the guest, needed as
>> + * the encodings which exist for XLEN=64 only must not be recognized for a
>> + * 32-bit guest.
>> + *
>> + * Returns false if the instruction is not a load or store which can be
>> + * emulated here.
>> + */
>> +static __maybe_unused bool decode_ldst_insn(struct decoded_insn *di,
>> +                                            unsigned int xlen)
>> +{
>> +    unsigned long insn = di->insn;
>> +    /* Register fields of the uncompressed forms ... */
>> +    unsigned int rd = RV_RD(insn);
>> +    unsigned int rs2 = RV_RS2(insn);
>> +    /*
>> +     * ... and of the compressed ones, where the 3-bit field selects one of
>> +     * x8..x15, while the stack-pointer-relative forms have a full-width one.
>> +     */
>> +    unsigned int rs2s = RVC_RS2S(insn);
>> +    unsigned int rs2c = RVC_RS2(insn);
>> +
> This naming are confusing because above you described di->reg to be rd
> for load and rs2 for store but here ...
>> +    di->is_write = false;
>> +    di->is_unsigned = false;
> 
> 
>> +    di->reg = rd;
>> +
>> +    if ( (insn & INSN_MASK_LB) == INSN_MATCH_LB )
>> +        di->len = 1;
>> +    else if ( (insn & INSN_MASK_LBU) == INSN_MATCH_LBU )
>> +    {
>> +        di->len = 1;
>> +        di->is_unsigned = true;
>> +    }
>> +    else if ( (insn & INSN_MASK_LH) == INSN_MATCH_LH )
>> +        di->len = 2;
>> +    else if ( (insn & INSN_MASK_LHU) == INSN_MATCH_LHU )
>> +    {
>> +        di->len = 2;
>> +        di->is_unsigned = true;
>> +    }
>> +    else if ( (insn & INSN_MASK_LW) == INSN_MATCH_LW )
>> +        di->len = 4;
>> +    else if ( xlen == 64 && (insn & INSN_MASK_LWU) == INSN_MATCH_LWU )
>> +    {
>> +        di->len = 4;
>> +        di->is_unsigned = true;
>> +    }
> 
> 
>> +    else if ( (insn & INSN_MASK_C_LW) == INSN_MATCH_C_LW )
>> +    {
>> +        di->len = 4;
> 
> 
>> +        di->reg = rs2s;
> ... you assigned rs2s for a load. According to the spec, it should be rd'.
> 
> I would suggest something generic to load and store. Maybe rxs with a comment to explain it concerns rs2' for store and rd' for load.
> 
>      /*
>       * ... and of the compressed ones, where the 3-bit field selects one of
>       * x8..x15, while the stack-pointer-relative forms have a full-width one.
>       * rxs is named after that field's spec mnemonic, rd'/rs2': rd' for
>       * compressed loads, rs2' for compressed stores.
>       */
>      unsigned int rxs = RVC_RS2S(insn);

Good point. It isn't partly applied to what I suggested in one of the 
reply to Jan B. but I will try to re-use part of your suggestion there.

It will look like:

static bool decode_ldst_insn(struct decoded_insn *di, unsigned int xlen)
{
     uint32_t insn = di->insn;
     unsigned int funct3, width_log2;

     if ( INSN_IS_16BIT(insn) )
     {
         /*
          * C.LW, C.LD, C.SW and C.SD (bits[1:0] == 00), and their 
sp-relative
          * C.*SP forms (bits[1:0] == 10), have bits[15:13] of the form x1y:
          * x is set for a store, and y selects a width of 4 or 8 bytes.
          */
         funct3 = RV_X(insn, 13, 3);

         if ( (insn & 1) || !(funct3 & 2) )
             return false;

         di->is_write = funct3 & 4;
         width_log2 = 2 + (funct3 & 1);

         /*
          * The register operand is rd' of a load or rs2' of a store for the
          * register-relative forms, both being bits[4:2], and rs2 of a 
store
          * or rd of a load for the sp-relative ones.
          */
         if ( !(insn & 2) )
             /* Quadrant 0: bits[4:2] encode rd' (load) or rs2' (store) */
             di->reg = RVC_RS2S(insn);
         else if ( di->is_write )
             /* Quadrant 2 (CSS): bits[6:2] encode rs2 for C.SWSP / 
C.SDSP */
             di->reg = RVC_RS2(insn);
         else
         {
             /* Quadrant 2 (CI): bits[11:7] encode rd for C.LWSP / C.LDSP */
             di->reg = RV_RD(insn);

             /* C.LWSP and C.LDSP are reserved with rd being x0. */
             if ( !di->reg )
                 return false;
         }
     }
     else
     {
         /*
          * funct3[1:0] is log2 of the width in bytes, and funct3[2] selects
          * zero-extension for a load, while being reserved for a store.
          */
         funct3 = RV_X(insn, 12, 3);
         width_log2 = funct3 & 3;

         switch ( insn & INSN_OPCODE_MASK )
         {
         case INSN_OPCODE_LOAD:
             di->is_unsigned = funct3 & 4;
             di->reg = RV_RD(insn);
             break;

         case INSN_OPCODE_STORE:
             if ( funct3 & 4 )
                 return false;
             di->is_write = true;
             di->reg = RV_RS2(insn);
             break;

         default:
             return false;
         }
     }

     di->len = 1U << width_log2;

     /*
      * No access is wider than XLEN, and one as wide as XLEN exists only in
      * its sign-extending form: this rules out the encodings which 
exist for
      * XLEN=64 only on a 32-bit guest, including C.FLW for C.LD (and 
alike).
      */
     if ( (di->len * BITS_PER_BYTE > xlen) ||
          (di->is_unsigned && di->len * BITS_PER_BYTE == xlen) )
         return false;

     return true;
}

> 
>> +    }
>> +    /* c.lwsp and c.ldsp are reserved with rd being x0. */
>> +    else if ( (insn & INSN_MASK_C_LWSP) == INSN_MATCH_C_LWSP && rd )
>> +        di->len = 4;
>> +    else if ( xlen == 64 && (insn & INSN_MASK_LD) == INSN_MATCH_LD )
>> +        di->len = 8;
>> +    else if ( xlen == 64 && (insn & INSN_MASK_C_LD) == INSN_MATCH_C_LD )
>> +    {
>> +        di->len = 8;
>> +        di->reg = rs2s;
>> +    }
>> +    else if ( xlen == 64 && (insn & INSN_MASK_C_LDSP) == INSN_MATCH_C_LDSP &&
>> +              rd )
>> +        di->len = 8;
>> +    else if ( (insn & INSN_MASK_SB) == INSN_MATCH_SB )
>> +    {
>> +        di->len = 1;
>> +        di->is_write = true;
>> +        di->reg = rs2;
>> +    }
>> +    else if ( (insn & INSN_MASK_SH) == INSN_MATCH_SH )
>> +    {
>> +        di->len = 2;
>> +        di->is_write = true;
>> +        di->reg = rs2;
>> +    }
>> +    else if ( (insn & INSN_MASK_SW) == INSN_MATCH_SW )
>> +    {
>> +        di->len = 4;
>> +        di->is_write = true;
>> +        di->reg = rs2;
>> +    }
>> +    else if ( (insn & INSN_MASK_C_SW) == INSN_MATCH_C_SW )
>> +    {
>> +        di->len = 4;
>> +        di->is_write = true;
>> +        di->reg = rs2s;
>> +    }
>> +    else if ( (insn & INSN_MASK_C_SWSP) == INSN_MATCH_C_SWSP )
>> +    {
>> +        di->len = 4;
>> +        di->is_write = true;
>> +        di->reg = rs2c;
>> +    }
>> +    else if ( xlen == 64 && (insn & INSN_MASK_SD) == INSN_MATCH_SD )
>> +    {
>> +        di->len = 8;
>> +        di->is_write = true;
>> +        di->reg = rs2;
>> +    }
>> +    else if ( xlen == 64 && (insn & INSN_MASK_C_SD) == INSN_MATCH_C_SD )
>> +    {
>> +        di->len = 8;
>> +        di->is_write = true;
>> +        di->reg = rs2s;
>> +    }
>> +    else if ( xlen == 64 && (insn & INSN_MASK_C_SDSP) == INSN_MATCH_C_SDSP )
>> +    {
>> +        di->len = 8;
>> +        di->is_write = true;
>> +        di->reg = rs2c;
>> +    }
>> +    else
>> +        return false;
>> +
>> +    return true;
>> +}
>> +

[...]

>> +/*
>> + * Width encoded by the MXL, SXL, UXL and VSXL fields, all of which share one
>> + * encoding. 0 is reserved.
>> + */
>> +#define XLEN_FIELD_32			_UL(1)
>> +#define XLEN_FIELD_64			_UL(2)
>> +#define XLEN_FIELD_128			_UL(3)
>> +
>>   #if __riscv_xlen == 64
>>   #define HSTATUS_VSXL			_UL(0x300000000)
>>   #define HSTATUS_VSXL_SHIFT		32
>> @@ -896,6 +904,8 @@
>>   					 (RV_X(x, 7, 2) << 6))
>>   #define RVC_SDSP_IMM(x)			((RV_X(x, 10, 3) << 3) | \
>>   					 (RV_X(x, 7, 3) << 6))
>> +#define RV_RD(insn)			RV_X(insn, SH_RD, 5)
>> +#define RV_RS2(insn)			RV_X(insn, SH_RS2, 5)
> Nit: format
> 

I think it looks like that because tabs are used there instead of spaces 
and tabs are there because it was orignally taken from the project which 
uses tabs.

Thanks!

~ Oleksii


^ permalink raw reply	[flat|nested] 244+ messages in thread

* Re: [PATCH v2 25/39] xen/riscv: add guest load emulation for trapped MMIO accesses
  2026-09-18  9:16   ` Baptiste Le Duc
@ 2026-09-22 11:22     ` Oleksii Kurochko
  0 siblings, 0 replies; 244+ messages in thread
From: Oleksii Kurochko @ 2026-09-22 11:22 UTC (permalink / raw)
  To: Baptiste Le Duc
  Cc: xen-devel, Romain Caritey, Zheng Zhang, Alistair Francis,
	Connor Davis, Andrew Cooper, Anthony PERARD, Michal Orzel,
	Jan Beulich, Julien Grall, Roger Pau Monné,
	Stefano Stabellini

>> @@ -179,8 +178,7 @@ static void resolve_faulting_gpa(struct guest_fault *gf)
>>    * Relies on x0..x31 being laid out at the start of struct cpu_user_regs in
>>    * architectural register-number order; see the comment there.
>>    */
>> -static __maybe_unused unsigned long *guest_gpr(struct cpu_user_regs *regs,
>> -                                               unsigned int reg)
> Could we unified this function with regs_get_gpr()?
> 

I think it could. I will try to do in the unified way.

Thanks!

~ Oleksii


^ permalink raw reply	[flat|nested] 244+ messages in thread

* Re: [PATCH v2 26/39] xen/riscv: add guest store emulation for trapped MMIO accesses
  2026-09-18  9:16   ` Baptiste Le Duc
@ 2026-09-22 11:38     ` Oleksii Kurochko
  0 siblings, 0 replies; 244+ messages in thread
From: Oleksii Kurochko @ 2026-09-22 11:38 UTC (permalink / raw)
  To: Baptiste Le Duc
  Cc: xen-devel, Romain Caritey, Zheng Zhang, Alistair Francis,
	Connor Davis, Andrew Cooper, Anthony PERARD, Michal Orzel,
	Jan Beulich, Julien Grall, Roger Pau Monné,
	Stefano Stabellini



On 9/18/26 11:16 AM, Baptiste Le Duc wrote:
>> Extend the guest page fault handler with store emulation to support MMIO
>> write accesses.
>>
>> The instruction decode mirrors emulate_load() and, like it, is adapted
>> from Linux's KVM RISC-V implementation. As with the load path, the
>> completion is synchronous through try_handle_mmio() rather than KVM's
>> userspace exit/return split, since Xen's MMIO handlers run in the
>> hypervisor. Faults taken while re-reading the trapped instruction are
>> handled by decode_ldst_insn(), shared with the load path.
>>
>> When a guest store instruction faults, the trapped instruction is decoded
>> using HTINST or, if unavailable, fetched via unprivileged access. At the
> Nit: commit message restates the HTINST-or-unprivileged-fetch decode
> mechanism, which is already described in the prep patch introducing
> decode_ldst_insn()/insn_fetch_faulted(), and isn't repeated in
> emulate_load()'s commit message. Suggest trimming for symmetry with the
> load commit, e.g.:
> 
>    When a guest store instruction faults, the trapped instruction is
>    decoded via decode_ldst_insn(), shared with the load path. At the
>    moment only virtual interrupt controller (vINTC) traps are expected to
>    occur, since it is currently the only backend registered with the MMIO
>    handler dispatch, so in practice the store is emulated via the vINTC
>    backend.
> 

I will update the commit message.

Thanks.

~ Oleksii


^ permalink raw reply	[flat|nested] 244+ messages in thread

* Re: [PATCH v2 30/39] xen/riscv: prepare new IMSIC VS-file
  2026-09-21 16:15   ` Baptiste Le Duc
  2026-09-22  6:32     ` Jan Beulich
@ 2026-09-22 13:01     ` Oleksii Kurochko
  2026-09-22 15:36       ` Baptiste Le Duc
  1 sibling, 1 reply; 244+ messages in thread
From: Oleksii Kurochko @ 2026-09-22 13:01 UTC (permalink / raw)
  To: Baptiste Le Duc, xen-devel
  Cc: Romain Caritey, Zheng Zhang, Alistair Francis, Connor Davis,
	Andrew Cooper, Anthony PERARD, Michal Orzel, Jan Beulich,
	Julien Grall, Roger Pau Monné, Stefano Stabellini



On 9/21/26 6:15 PM, Baptiste Le Duc wrote:
> 
> 
> On 8/27/26 5:24 PM, Oleksii Kurochko wrote:
>> Implement first steps of migration a vCPU to a different guest 
>> interrupt file
>> procedure:
>> - At the old interrupt file, save to memory the values of registers
>>    eidelivery and eithreshold, and set eidelivery = 0.
>> - At the new interrupt file, set eidelivery = 0, and zero all
>>    implemented interrupt-pending bits (the eip array).
>>
>> The following steps will be introduced in follow-up patches.
>>
>> Add a BUG_ON("unimplemented") placeholder in imsic_migrate_vcpu() to 
>> guard
>> against silent incorrect behaviour or unexpected panics in guest VMs 
>> until
>> the function is fully implemented.
>>
>> vgein_assign() will be introduced later in a separate patch, for not 
>> it is
>> only stub.
>>
>> Signed-off-by: Oleksii Kurochko <oleksii.kurochko@gmail.com>
>> ---
>> Changes in v2:
>>   - New patch.
>> ---
>> ---
>>   xen/arch/riscv/aia.c             |   9 ++
>>   xen/arch/riscv/imsic.c           | 159 +++++++++++++++++++++++++++++++
>>   xen/arch/riscv/include/asm/aia.h |   4 +
>>   xen/include/xen/config.h         |   1 +
>>   4 files changed, 173 insertions(+)
>>
>> diff --git a/xen/arch/riscv/aia.c b/xen/arch/riscv/aia.c
>> index e31c9c2d24b6..75c82bcfa1b3 100644
>> --- a/xen/arch/riscv/aia.c
>> +++ b/xen/arch/riscv/aia.c
>> @@ -1,8 +1,10 @@
>>   /* SPDX-License-Identifier: GPL-2.0-only */
>> +#include <xen/bug.h>
>>   #include <xen/errno.h>
>>   #include <xen/init.h>
>>   #include <xen/sections.h>
>> +#include <xen/sched.h>
>>   #include <xen/types.h>
>>   #include <asm/cpufeature.h>
>> @@ -21,3 +23,10 @@ void __init aia_init(void)
>>       _aia_usable = true;
>>   }
>> +
>> +unsigned int vgein_assign(struct vcpu *v)
>> +{
>> +    BUG_ON("unimplemented\n");
>> +
>> +    return 0;
>> +}
>> diff --git a/xen/arch/riscv/imsic.c b/xen/arch/riscv/imsic.c
>> index ad7fbe708bfd..516f0105352a 100644
>> --- a/xen/arch/riscv/imsic.c
>> +++ b/xen/arch/riscv/imsic.c
>> @@ -26,6 +26,7 @@
>>   #include <xen/spinlock.h>
>>   #include <xen/xvmalloc.h>
>> +#include <asm/aia.h>
>>   #include <asm/imsic.h>
>>   #define IMSIC_HART_SIZE(guest_bits) (BIT(guest_bits, U) * 
>> IMSIC_MMIO_PAGE_SZ)
>> @@ -77,6 +78,64 @@ do {                            \
>>       csr_clear(CSR_SIREG, v);    \
>>   } while (0)
>> +#define imsic_vs_csr_write(c, v)    \
>> +do {                                \
>> +    csr_write(CSR_VSISELECT, (c));  \
>> +    csr_write(CSR_VSIREG, (v));     \
>> +} while ( 0 )
>> +
>> +/*
>> + * Generic switchcase expansion pyramid.
>> + * F is the per-operation leaf macro, ireg is the base register index.
>> + * Optional extra args (e.g. an operation and/or a value) are 
>> forwarded to F
>> + * via __VA_ARGS__.
>> + *
>> + * imsic_switchcase_break(ireg, op, v) - emit "case ireg: op(ireg,v); 
>> break;"
>> + * imsic_switchcase_ret(ireg, op, ...) - emit "case ireg: return 
>> op(ireg[,v]);"
>> + *   The variadic tail is optional so the same leaf works for both 
>> read (no v)
>> + *   and swap (with v).
>> + */
>> +#define imsic_switchcase_break(ireg, op, v) \
>> +    case ireg:                              \
>> +        op(ireg, v);                        \
>> +        break;
>> +
>> +#define imsic_switchcase_ret(ireg, op, ...) \
>> +    case ireg:                              \
>> +        return op(ireg, ##__VA_ARGS__);
>> +
>> +#define imsic_switchcase_2(F, ireg, ...)    \
>> +    F(ireg + 0, ##__VA_ARGS__)              \
>> +    F(ireg + 1, ##__VA_ARGS__)
>> +#define imsic_switchcase_4(F, ireg, ...)    \
>> +    imsic_switchcase_2(F, ireg + 0, ##__VA_ARGS__)  \
>> +    imsic_switchcase_2(F, ireg + 2, ##__VA_ARGS__)
>> +#define imsic_switchcase_8(F, ireg, ...)    \
>> +    imsic_switchcase_4(F, ireg + 0, ##__VA_ARGS__)  \
>> +    imsic_switchcase_4(F, ireg + 4, ##__VA_ARGS__)
>> +#define imsic_switchcase_16(F, ireg, ...)   \
>> +    imsic_switchcase_8(F, ireg + 0, ##__VA_ARGS__)  \
>> +    imsic_switchcase_8(F, ireg + 8, ##__VA_ARGS__)
>> +#define imsic_switchcase_32(F, ireg, ...)   \
>> +    imsic_switchcase_16(F, ireg + 0, ##__VA_ARGS__) \
>> +    imsic_switchcase_16(F, ireg + 16, ##__VA_ARGS__)
>> +#define imsic_switchcase_64(F, ireg, ...)   \
>> +    imsic_switchcase_32(F, ireg + 0, ##__VA_ARGS__) \
>> +    imsic_switchcase_32(F, ireg + 32, ##__VA_ARGS__)
>> +
>> +static void imsic_eix_write(unsigned int ireg, unsigned long val)
>> +{
>> +    switch ( ireg )
>> +    {
>> +    imsic_switchcase_64(imsic_switchcase_break, IMSIC_EIP0,
>> +                        imsic_vs_csr_write, val)
>> +    imsic_switchcase_64(imsic_switchcase_break, IMSIC_EIE0,
>> +                        imsic_vs_csr_write, val)
>> +    default:
>> +        ASSERT_UNREACHABLE();
>> +    }
>> +}
>> +
>>   unsigned int vcpu_guest_file_id(const struct vcpu *v)
>>   {
>>       return ACCESS_ONCE(v->arch.vimsic_state->guest_file_id);
>> @@ -389,6 +448,76 @@ int cf_check vcpu_imsic_init(struct vcpu *v)
>>       return 0;
>>   }
>> +/*
>> + * Arguments of the imsic_vsfile_local_*() helpers, which are 
>> executed by the
>> + * pCPU owning the interrupt file, thereby through imsic_call_on_cpu().
>> + */
>> +struct imsic_vsfile_data {
>> +    unsigned int hgei;
>> +    unsigned int nr_eix;
>> +    struct imsic_mrif *mrif;
>> +};
>> +
>> +/*
>> + * Execute func() on the pCPU which owns the IMSIC interrupt file 
>> func() is
>> + * going to work with.
>> + *
>> + * An IMSIC VS-file is reachable only through hstatus.VGEIN of the 
>> hart the
>> + * file belongs to, and a guest interrupt file index is meaningless 
>> on any
>> + * other hart, so such work always has to be done by that very hart.
>> + *
>> + * The local case runs with IRQs disabled to provide func() with the 
>> same
>> + * environment it is given when it is called from the function call IPI
>> + * handler.
>> + */
>> +static void imsic_call_on_cpu(unsigned int cpu, void (*func)(void *),
>> +                              void *data)
>> +{
>> +    if ( cpu == smp_processor_id() )
>> +    {
>> +        unsigned long flags;
>> +
>> +        local_irq_save(flags);
>> +        func(data);
>> +        local_irq_restore(flags);
>> +    }
>> +    else
>> +        on_selected_cpus(cpumask_of(cpu), func, data, 1);
>> +}
>> +
>> +static void cf_check imsic_vsfile_local_clear(void *data)
> I think the remark from Jan to direclty pass the type instead of void
> could be applied here.

I think it can't because of how function pointer is passed to 
on_selected_cpus() through imsic_call_on_cpu().

>> +{
>> +    unsigned int i;
>> +    const struct imsic_vsfile_data *idata = data;
>> +    unsigned long new_hstatus, old_hstatus, old_vsiselect;
>> +
>> +    /* We can only zero-out if we have a IMSIC VS-file */
>> +    if ( !idata->hgei )
>> +        return;
>> +
>> +    old_vsiselect = csr_read(CSR_VSISELECT);
>> +    old_hstatus = csr_read(CSR_HSTATUS);
>> +    new_hstatus = old_hstatus & ~HSTATUS_VGEIN;
>> +    new_hstatus |= MASK_INSR(idata->hgei, HSTATUS_VGEIN);
>> +    csr_write(CSR_HSTATUS, new_hstatus);
>> +
>> +    imsic_vs_csr_write(IMSIC_EIDELIVERY, 0);
>> +    imsic_vs_csr_write(IMSIC_EITHRESHOLD, 0);
>> +
>> +    for ( i = 0; i < idata->nr_eix; i++ )
>> +    {
>> +        imsic_eix_write(IMSIC_EIP0 + i * 2, 0);
>> +        imsic_eix_write(IMSIC_EIE0 + i * 2, 0);
>> +#ifdef CONFIG_RISCV_32
>> +        imsic_eix_write(IMSIC_EIP0 + i * 2 + 1, 0);
>> +        imsic_eix_write(IMSIC_EIE0 + i * 2 + 1, 0);
>> +#endif
>> +    }
>> +
>> +    csr_write(CSR_HSTATUS, old_hstatus);
>> +    csr_write(CSR_VSISELECT, old_vsiselect);
>> +}
>> +
>>   void cf_check vcpu_imsic_deinit(struct vcpu *v)
>>   {
>>       XVFREE(v->arch.vimsic_state);
>> @@ -689,6 +818,14 @@ int __init vimsic_make_domu_dt_node(struct 
>> kernel_info *kinfo,
>>   void imsic_migrate_vcpu(struct vcpu *v)
>>   {
>> +    unsigned int new_vsfile_hgei;
>> +    unsigned int new_vsfile_cpu;
>> +    unsigned int nr_hw_eix = DIV_ROUND_UP(imsic_cfg.nr_ids + 1,
>> +                                          BITS_PER_TYPE(uint64_t));
> This value appears to remain constant after initialization, since it 
> depends directly on the hw,
> so it is not necessary to calculate it each time.

I will add then:

--- a/xen/arch/riscv/include/asm/imsic.h
+++ b/xen/arch/riscv/include/asm/imsic.h
@@ struct imsic_config {
      /* Number off interrupt identities */
      unsigned int nr_ids;

+    /*
+     * Number of 64-bit EIx groups needed to cover all the interrupt
+     * identities, which are 0 (never valid, but it still occupies a 
bit) up
+     * to and including nr_ids.
+     */
+    unsigned int nr_eix;
+

and init it once in imsic_parse_node().

Thanks.

~ Oleksii


^ permalink raw reply	[flat|nested] 244+ messages in thread

* Re: [PATCH v2 36/39] xen/riscv: wake up a descheduled vCPU on a guest external interrupt
  2026-09-21 15:08       ` Jan Beulich
@ 2026-09-22 13:37         ` Oleksii Kurochko
  0 siblings, 0 replies; 244+ messages in thread
From: Oleksii Kurochko @ 2026-09-22 13:37 UTC (permalink / raw)
  To: Jan Beulich
  Cc: Romain Caritey, Baptiste Le Duc, Zheng Zhang, Alistair Francis,
	Connor Davis, Andrew Cooper, Anthony PERARD, Michal Orzel,
	Julien Grall, Roger Pau Monné, Stefano Stabellini, xen-devel



On 9/21/26 5:08 PM, Jan Beulich wrote:
> On 21.09.2026 16:01, Oleksii Kurochko wrote:
>> On 9/18/26 2:52 PM, Jan Beulich wrote:
>>> On 27.08.2026 17:21, Oleksii Kurochko wrote:
>>>> --- a/xen/arch/riscv/imsic.c
>>>> +++ b/xen/arch/riscv/imsic.c
>>>> @@ -510,12 +510,31 @@ void cf_check imsic_ctxt_switch_from(struct vcpu *v)
>>>>    
>>>>        write_lock_irqsave(&imsic_state->vsfile_lock, flags);
>>>>        imsic_state->vsfile_cpu = v->processor;
>>>> +    /*
>>>> +     * Start to observe the VS-file from HS-mode: while the vCPU isn't
>>>> +     * running an interrupt pending in its VS-file is reported through HGEIP
>>>> +     * instead of being delivered to VS-mode, which lets Xen wake the vCPU up.
>>>> +     */
>>>> +    csr_set(CSR_HGEIE, BIT(imsic_state->guest_file_id, UL));
>>>>        write_unlock_irqrestore(&imsic_state->vsfile_lock, flags);
>>>>    }
>>>
>>> It is suspicious for the HGEIE write to be the last step. How's this free
>>> of a window where an interrupt is lost. (Sorry, likely another blind spot
>>> of mine wrt RISC-V.)
>>
>> There's no window: a guest interrupt file's hgeip bit is
>> level-sensitive, i.e. it reflects whether the file currently has a
>> pending-and-enabled interrupt (the MSI itself stays latched in the
>> file's eip[] until the guest claims it), and hip.SGEIP is simply (hgeip
>> & hgeie) != 0. So an MSI arriving after the vCPU stopped running but
>> before hgeie is set raises an SGEI as soon as the bit is set, taken once
>> interrupts are re-enabled. I'll extend the comment to say so:
>>
>>       /*
>>        * Start to observe the VS-file from HS-mode: while the vCPU isn't
>>        * running an interrupt pending in its VS-file is reported through
>> HGEIP
>>        * instead of being delivered to VS-mode, which lets Xen wake the
>> vCPU up.
>>        *
>>        * HGEIP is level-sensitive, reflecting the VS-file's current state, so
>>        * an interrupt that became pending before this point raises an SGEI as
>>        * soon as the bit is set in HGEIE; nothing is lost in between.
>>        */
>>
>> Does it make sense?
> 
> I think I get what you're trying to explain, but the term "level-sensitive"
> here doesn't really help. I'm unconvinced you actually mean that, as it
> requires pins / physical signals, which don't exist with MSI. It feels like
> you may mean "sticky" instead.

Fair point regarding the terminology: 'level-sensitive' usually implies 
physical signal lines, whereas here MSIs are memory writes latched in 
the file's eip[] array. I meant that HGEIP dynamically reflects whether 
the VS-file currently has any pending-and-enabled interrupt latched.

I'll update the comment to clarify this without using 'level-sensitive':

/*
  * Start to observe the VS-file from HS-mode: while the vCPU isn't
  * running an interrupt pending in its VS-file is reported through HGEIP
  * instead of being delivered to VS-mode, which lets Xen wake the vCPU up.
  *
  * An MSI is recorded in the VS-file's eip[] array until the guest claims
  * it, and HGEIP isn't latched but reflects whether the file currently
  * has a pending-and-enabled interrupt. Hence an interrupt which arrived
  * before this point raises an SGEI as soon as the bit is set in HGEIE;
  * nothing is lost in between.
  */

> 
>>>>    void cf_check imsic_ctxt_switch_to(struct vcpu *v)
>>>>    {
>>>> -    /* Nothing to do */
>>>> +    struct vimsic_state *imsic_state = v->arch.vimsic_state;
>>>> +    unsigned long flags;
>>>> +
>>>> +    /* A s/w VS-file is never observed through HGEIP. */
>>>> +    if ( !vcpu_guest_file_id(v) )
>>>> +        return;
>>>> +
>>>> +    /*
>>>> +     * The vCPU is about to run, so hstatus.VGEIN delivers the VS-file's
>>>> +     * interrupts to it directly and there is nothing left for Xen to observe.
>>>> +     */
>>>> +    read_lock_irqsave(&imsic_state->vsfile_lock, flags);
>>>> +    csr_clear(CSR_HGEIE, BIT(imsic_state->guest_file_id, UL));
>>>> +    read_unlock_irqrestore(&imsic_state->vsfile_lock, flags);
>>>>    }
>>>
>>> How can this be a read-lock when you write a CSR?
>>
>> But here is a protection of ->guest_file_id not of write to a CSR and we
>> want to not have a change of ->guest_file_id during an update of CSR_HGEIE.
>>
>>> Or else - why is locking
>>> here necessary in the firt place?
>>
>> Strictly speaking no locking is needed with the current implementation:
>> HGEIE is local to this pCPU, and guest_file_id can't change
>> concurrently. It's updated either by this very pCPU ahead of switching
>> the vCPU in (imsic_migrate_vcpu() called from schedule(),
>> imsic_vsfile_attach() right after), or while the vCPU can't be
>> scheduled: sched_unit_migrate_finish() defers to unit_context_saved()
>> while the unit is running, and sched_move_domain() pauses the domain.
>>
>> (and the similar are true for imsic_ctxt_switch_from())
>>
>> But IMO we should keep around ->vsfile_lock to not miss the case where
>> some case will update ->guest_file_id in parallel with
>> imsic_ctxt_switch_to().
>>
>> Does it make to continue to have read_lock_irqsave(...->vsfile_lock,
>> ...) here just for potential future cases?
> 
> You get to judge. If the lock typically is uncontended, keeping things
> as-is may indeed be fine. Introducing a bottleneck "just for potential
> future cases" otoh wouldn't look overly nice to me.
> 
For the moment, I prefer to have a lock here as it is expected to be 
cheap. If one day it won't be true anymore it will be easier to spot all 
the places and remove it where it will be necessary.

~ Oleksii



^ permalink raw reply	[flat|nested] 244+ messages in thread

* Re: [PATCH v2 38/39] xen/riscv: implement continue_new_vcpu()
  2026-09-22 10:20       ` Jan Beulich
@ 2026-09-22 13:58         ` Oleksii Kurochko
  0 siblings, 0 replies; 244+ messages in thread
From: Oleksii Kurochko @ 2026-09-22 13:58 UTC (permalink / raw)
  To: Jan Beulich
  Cc: Romain Caritey, Baptiste Le Duc, Zheng Zhang, Alistair Francis,
	Connor Davis, Andrew Cooper, Anthony PERARD, Michal Orzel,
	Julien Grall, Roger Pau Monné, Stefano Stabellini, xen-devel



On 9/22/26 12:20 PM, Jan Beulich wrote:
> On 22.09.2026 10:23, Oleksii Kurochko wrote:
>> On 9/21/26 2:12 PM, Jan Beulich wrote:
>>> On 27.08.2026 17:21, Oleksii Kurochko wrote:
>>>> continue_new_vcpu() is the arch hook invoked the first time a freshly
>>>> created vCPU is scheduled. Implement both cases it has to cover:
>>>>    - for the idle vCPU, switch to its own stack and jump to idle_loop();
>>>>    - for a guest vCPU, restore hstatus and enter the guest through the new
>>>>      return_to_new_vcpu() path in entry.S, which loads sepc, passes the
>>>>      hart id in a0 and the DTB address in a1 as expected by the RISC-V
>>>>      boot protocol, sets sstatus.SPP and executes sret.
>>>
>>> Is this a requirement for all CPUs, or just for the boot one? (I can't
>>> quite see why secondary processors would need passing a DTB address.)
>>
>> It is requirement for boot one. For secondary processors it is HSM boot
>> data which is passed to sbi_hsm_hart_start() and then intercpeted by Xen.
>>
>> At the moment of writing of this commit message we have only boot CPU
>> and so only DTB could be passed.
> 
> That you're talking about Xen. Despite Xen being UP only right now, guests
> still can have more than one vCPU, can't they?

They can.

> 
>> I can update the commit message and the comment in return_to_new_vcpu()
>> to tell that it could be DTB address for boot cpu and/or for secondary
>> CPUs HSM boot data or it will be better to add info about HSM boot data
>> during and an introduction of secondary CPUs support?
> 
> As per above you want to deal with multi-vCPU guests right now.

Then I will update the commit message and the comment in 
return_to_new_vcpu() to say that a1 holds the DTB address for the boot 
vCPU, and for secondary vCPUs the opaque value the guest passed to SBI 
HSM hart_start().

For commit message:

  - for a guest vCPU, restore hstatus and enter the guest through the new
     return_to_new_vcpu() path in entry.S, which loads sepc, passes the
     hart id in a0 and, in a1, either the DTB address (boot vCPU) or the
     opaque argument of SBI HSM hart_start() (secondary vCPUs), as 
required by the RISC-V boot protocol and the SBI specification, sets 
sstatus.SPP and executes sret.

In the code:

         /*
          * .a1 holds the DTB address for the boot vCPU, or the opaque value
          * passed to SBI HSM hart_start() for secondary vCPUs
          */

>>>> +        /* Set guest mode to supervisor */
>>>> +        li      t0, SSTATUS_SPP
>>>> +        csrs    CSR_SSTATUS, t0
>>>> +
>>>> +        /* Enter guest */
>>>> +        sret
>>>> +END(return_to_new_vcpu)
>>>
>>> Aiui SRET does not switch stacks. Shouldn't you therefore clear sp here?
>>> And perhaps also other GPRs, not the least ra? Exposing hypervisor
>>> register values to guests is, well, a bit of a problem.
>>
>> Good point, sret leaves all GPRs as they are, so the guest would indeed
>> see Xen's sp, ra and friends. Only a0 and a1 are architecturally
>> meaningful for a booting hart, so I'll clear every other GPR right
>> before sret.
>>
>> I will add the following before sret:
>>
>>           /*
>>            * sret doesn't switch stacks and leaves the GPRs alone, so every
>>            * register which isn't meaningful to the vCPU being started
>> has to be
>>            * cleared here: otherwise the guest would see Xen's values, sp
>> (this
>>            * vCPU's Xen stack) and ra among them.
>>            */
>>           .irp reg, ra, sp, gp, tp, t0, t1, t2, s0, s1, a2, a3, a4, a5,
>> a6, a7, \
>>                     s2, s3, s4, s5, s6, s7, s8, s9, s10, s11, t3, t4, t5, t6
>>           mv      \reg, zero
>>           .endr
> 
> At which point discussing the clobbering of t0 in the comment ahead of
> the function also isn't needed anymore.

Indeed, I'll drop it.

~ Oleksii



^ permalink raw reply	[flat|nested] 244+ messages in thread

* Re: [PATCH v2 30/39] xen/riscv: prepare new IMSIC VS-file
  2026-09-22 13:01     ` Oleksii Kurochko
@ 2026-09-22 15:36       ` Baptiste Le Duc
  0 siblings, 0 replies; 244+ messages in thread
From: Baptiste Le Duc @ 2026-09-22 15:36 UTC (permalink / raw)
  To: Oleksii Kurochko
  Cc: Baptiste Le Duc, xen-devel, Romain Caritey, Zheng Zhang,
	Alistair Francis, Connor Davis, Andrew Cooper, Anthony PERARD,
	Michal Orzel, Jan Beulich, Julien Grall, Roger Pau Monné,
	Stefano Stabellini

On 2026-09-22 15:01:02+02:00, Oleksii Kurochko wrote:
> On 9/21/26 6:15 PM, Baptiste Le Duc wrote:
> > 
> > 
> > On 8/27/26 5:24 PM, Oleksii Kurochko wrote:
> >> Implement first steps of migration a vCPU to a different guest 
> >> interrupt file
> >> procedure:
> >> - At the old interrupt file, save to memory the values of registers
> >>    eidelivery and eithreshold, and set eidelivery = 0.
> >> - At the new interrupt file, set eidelivery = 0, and zero all
> >>    implemented interrupt-pending bits (the eip array).
> >>
> >> The following steps will be introduced in follow-up patches.
> >>
> >> Add a BUG_ON("unimplemented") placeholder in imsic_migrate_vcpu() to 
> >> guard
> >> against silent incorrect behaviour or unexpected panics in guest VMs 
> >> until
> >> the function is fully implemented.
> >>
> >> vgein_assign() will be introduced later in a separate patch, for not 
> >> it is
> >> only stub.
> >>
> >> Signed-off-by: Oleksii Kurochko <oleksii.kurochko@gmail.com>
> >> ---
> >> Changes in v2:
> >>   - New patch.
> >> ---
> >> ---
> >>   xen/arch/riscv/aia.c             |   9 ++
> >>   xen/arch/riscv/imsic.c           | 159 +++++++++++++++++++++++++++++++
> >>   xen/arch/riscv/include/asm/aia.h |   4 +
> >>   xen/include/xen/config.h         |   1 +
> >>   4 files changed, 173 insertions(+)
> >>
> >> diff --git a/xen/arch/riscv/aia.c b/xen/arch/riscv/aia.c
> >> index e31c9c2d24b6..75c82bcfa1b3 100644
> >> --- a/xen/arch/riscv/aia.c
> >> +++ b/xen/arch/riscv/aia.c
> >> @@ -1,8 +1,10 @@
> >>   /* SPDX-License-Identifier: GPL-2.0-only */
> >> +#include <xen/bug.h>
> >>   #include <xen/errno.h>
> >>   #include <xen/init.h>
> >>   #include <xen/sections.h>
> >> +#include <xen/sched.h>
> >>   #include <xen/types.h>
> >>   #include <asm/cpufeature.h>
> >> @@ -21,3 +23,10 @@ void __init aia_init(void)
> >>       _aia_usable = true;
> >>   }
> >> +
> >> +unsigned int vgein_assign(struct vcpu *v)
> >> +{
> >> +    BUG_ON("unimplemented\n");
> >> +
> >> +    return 0;
> >> +}
> >> diff --git a/xen/arch/riscv/imsic.c b/xen/arch/riscv/imsic.c
> >> index ad7fbe708bfd..516f0105352a 100644
> >> --- a/xen/arch/riscv/imsic.c
> >> +++ b/xen/arch/riscv/imsic.c
> >> @@ -26,6 +26,7 @@
> >>   #include <xen/spinlock.h>
> >>   #include <xen/xvmalloc.h>
> >> +#include <asm/aia.h>
> >>   #include <asm/imsic.h>
> >>   #define IMSIC_HART_SIZE(guest_bits) (BIT(guest_bits, U) * 
> >> IMSIC_MMIO_PAGE_SZ)
> >> @@ -77,6 +78,64 @@ do {                            \
> >>       csr_clear(CSR_SIREG, v);    \
> >>   } while (0)
> >> +#define imsic_vs_csr_write(c, v)    \
> >> +do {                                \
> >> +    csr_write(CSR_VSISELECT, (c));  \
> >> +    csr_write(CSR_VSIREG, (v));     \
> >> +} while ( 0 )
> >> +
> >> +/*
> >> + * Generic switchcase expansion pyramid.
> >> + * F is the per-operation leaf macro, ireg is the base register index.
> >> + * Optional extra args (e.g. an operation and/or a value) are 
> >> forwarded to F
> >> + * via __VA_ARGS__.
> >> + *
> >> + * imsic_switchcase_break(ireg, op, v) - emit "case ireg: op(ireg,v); 
> >> break;"
> >> + * imsic_switchcase_ret(ireg, op, ...) - emit "case ireg: return 
> >> op(ireg[,v]);"
> >> + *   The variadic tail is optional so the same leaf works for both 
> >> read (no v)
> >> + *   and swap (with v).
> >> + */
> >> +#define imsic_switchcase_break(ireg, op, v) \
> >> +    case ireg:                              \
> >> +        op(ireg, v);                        \
> >> +        break;
> >> +
> >> +#define imsic_switchcase_ret(ireg, op, ...) \
> >> +    case ireg:                              \
> >> +        return op(ireg, ##__VA_ARGS__);
> >> +
> >> +#define imsic_switchcase_2(F, ireg, ...)    \
> >> +    F(ireg + 0, ##__VA_ARGS__)              \
> >> +    F(ireg + 1, ##__VA_ARGS__)
> >> +#define imsic_switchcase_4(F, ireg, ...)    \
> >> +    imsic_switchcase_2(F, ireg + 0, ##__VA_ARGS__)  \
> >> +    imsic_switchcase_2(F, ireg + 2, ##__VA_ARGS__)
> >> +#define imsic_switchcase_8(F, ireg, ...)    \
> >> +    imsic_switchcase_4(F, ireg + 0, ##__VA_ARGS__)  \
> >> +    imsic_switchcase_4(F, ireg + 4, ##__VA_ARGS__)
> >> +#define imsic_switchcase_16(F, ireg, ...)   \
> >> +    imsic_switchcase_8(F, ireg + 0, ##__VA_ARGS__)  \
> >> +    imsic_switchcase_8(F, ireg + 8, ##__VA_ARGS__)
> >> +#define imsic_switchcase_32(F, ireg, ...)   \
> >> +    imsic_switchcase_16(F, ireg + 0, ##__VA_ARGS__) \
> >> +    imsic_switchcase_16(F, ireg + 16, ##__VA_ARGS__)
> >> +#define imsic_switchcase_64(F, ireg, ...)   \
> >> +    imsic_switchcase_32(F, ireg + 0, ##__VA_ARGS__) \
> >> +    imsic_switchcase_32(F, ireg + 32, ##__VA_ARGS__)
> >> +
> >> +static void imsic_eix_write(unsigned int ireg, unsigned long val)
> >> +{
> >> +    switch ( ireg )
> >> +    {
> >> +    imsic_switchcase_64(imsic_switchcase_break, IMSIC_EIP0,
> >> +                        imsic_vs_csr_write, val)
> >> +    imsic_switchcase_64(imsic_switchcase_break, IMSIC_EIE0,
> >> +                        imsic_vs_csr_write, val)
> >> +    default:
> >> +        ASSERT_UNREACHABLE();
> >> +    }
> >> +}
> >> +
> >>   unsigned int vcpu_guest_file_id(const struct vcpu *v)
> >>   {
> >>       return ACCESS_ONCE(v->arch.vimsic_state->guest_file_id);
> >> @@ -389,6 +448,76 @@ int cf_check vcpu_imsic_init(struct vcpu *v)
> >>       return 0;
> >>   }
> >> +/*
> >> + * Arguments of the imsic_vsfile_local_*() helpers, which are 
> >> executed by the
> >> + * pCPU owning the interrupt file, thereby through imsic_call_on_cpu().
> >> + */
> >> +struct imsic_vsfile_data {
> >> +    unsigned int hgei;
> >> +    unsigned int nr_eix;
> >> +    struct imsic_mrif *mrif;
> >> +};
> >> +
> >> +/*
> >> + * Execute func() on the pCPU which owns the IMSIC interrupt file 
> >> func() is
> >> + * going to work with.
> >> + *
> >> + * An IMSIC VS-file is reachable only through hstatus.VGEIN of the 
> >> hart the
> >> + * file belongs to, and a guest interrupt file index is meaningless 
> >> on any
> >> + * other hart, so such work always has to be done by that very hart.
> >> + *
> >> + * The local case runs with IRQs disabled to provide func() with the 
> >> same
> >> + * environment it is given when it is called from the function call IPI
> >> + * handler.
> >> + */
> >> +static void imsic_call_on_cpu(unsigned int cpu, void (*func)(void *),
> >> +                              void *data)
> >> +{
> >> +    if ( cpu == smp_processor_id() )
> >> +    {
> >> +        unsigned long flags;
> >> +
> >> +        local_irq_save(flags);
> >> +        func(data);
> >> +        local_irq_restore(flags);
> >> +    }
> >> +    else
> >> +        on_selected_cpus(cpumask_of(cpu), func, data, 1);
> >> +}
> >> +
> >> +static void cf_check imsic_vsfile_local_clear(void *data)
> > I think the remark from Jan to direclty pass the type instead of void
> > could be applied here.
> 
> I think it can't because of how function pointer is passed to 
> on_selected_cpus() through imsic_call_on_cpu().
> 
> >> +{
> >> +    unsigned int i;
> >> +    const struct imsic_vsfile_data *idata = data;
> >> +    unsigned long new_hstatus, old_hstatus, old_vsiselect;
> >> +
> >> +    /* We can only zero-out if we have a IMSIC VS-file */
> >> +    if ( !idata->hgei )
> >> +        return;
> >> +
> >> +    old_vsiselect = csr_read(CSR_VSISELECT);
> >> +    old_hstatus = csr_read(CSR_HSTATUS);
> >> +    new_hstatus = old_hstatus & ~HSTATUS_VGEIN;
> >> +    new_hstatus |= MASK_INSR(idata->hgei, HSTATUS_VGEIN);
> >> +    csr_write(CSR_HSTATUS, new_hstatus);
> >> +
> >> +    imsic_vs_csr_write(IMSIC_EIDELIVERY, 0);
> >> +    imsic_vs_csr_write(IMSIC_EITHRESHOLD, 0);
> >> +
> >> +    for ( i = 0; i < idata->nr_eix; i++ )
> >> +    {
> >> +        imsic_eix_write(IMSIC_EIP0 + i * 2, 0);
> >> +        imsic_eix_write(IMSIC_EIE0 + i * 2, 0);
> >> +#ifdef CONFIG_RISCV_32
> >> +        imsic_eix_write(IMSIC_EIP0 + i * 2 + 1, 0);
> >> +        imsic_eix_write(IMSIC_EIE0 + i * 2 + 1, 0);
> >> +#endif
> >> +    }
> >> +
> >> +    csr_write(CSR_HSTATUS, old_hstatus);
> >> +    csr_write(CSR_VSISELECT, old_vsiselect);
> >> +}
> >> +
> >>   void cf_check vcpu_imsic_deinit(struct vcpu *v)
> >>   {
> >>       XVFREE(v->arch.vimsic_state);
> >> @@ -689,6 +818,14 @@ int __init vimsic_make_domu_dt_node(struct 
> >> kernel_info *kinfo,
> >>   void imsic_migrate_vcpu(struct vcpu *v)
> >>   {
> >> +    unsigned int new_vsfile_hgei;
> >> +    unsigned int new_vsfile_cpu;
> >> +    unsigned int nr_hw_eix = DIV_ROUND_UP(imsic_cfg.nr_ids + 1,
> >> +                                          BITS_PER_TYPE(uint64_t));
> > This value appears to remain constant after initialization, since it 
> > depends directly on the hw,
> > so it is not necessary to calculate it each time.
> 
> I will add then:
> 
> --- a/xen/arch/riscv/include/asm/imsic.h
> +++ b/xen/arch/riscv/include/asm/imsic.h
> @@ struct imsic_config {
>       /* Number off interrupt identities */
>       unsigned int nr_ids;
> 
> +    /*
> +     * Number of 64-bit EIx groups needed to cover all the interrupt
> +     * identities, which are 0 (never valid, but it still occupies a 
> bit) up
> +     * to and including nr_ids.
> +     */
> +    unsigned int nr_eix;
> +
Thanks! Maybe use `registers` instead of `groups` wording.
> and init it once in imsic_parse_node().
> 
> Thanks.
> 
> ~ Oleksii




^ permalink raw reply	[flat|nested] 244+ messages in thread

* Re: [PATCH v2 31/39] xen/riscv: implement APLIC-hart sync barrier for vCPU migration
  2026-08-27 15:21 ` [PATCH v2 31/39] xen/riscv: implement APLIC-hart sync barrier for vCPU migration Oleksii Kurochko
  2026-09-14 13:27   ` Jan Beulich
@ 2026-09-22 17:00   ` Baptiste Le Duc
  2026-09-22 18:48     ` Oleksii Kurochko
  1 sibling, 1 reply; 244+ messages in thread
From: Baptiste Le Duc @ 2026-09-22 17:00 UTC (permalink / raw)
  To: Oleksii Kurochko
  Cc: xen-devel, Romain Caritey, Baptiste Le Duc, Zheng Zhang,
	Alistair Francis, Connor Davis, Andrew Cooper, Anthony PERARD,
	Michal Orzel, Jan Beulich, Julien Grall, Roger Pau Monné,
	Stefano Stabellini

> During migration of a virtual hart to a different guest interrupt file,
> straggler MSIs from the APLIC could arrive at the old interrupt file
> after the switch.
> 
> genmsi is used despite not supporting guest interrupt files because the
> AIA spec guarantees that all MSIs previously sent from the APLIC to the
> same hart are visible at the hart's IMSIC before the extempore MSI from
> genmsi becomes visible.


> 
> Signed-off-by: Oleksii Kurochko <oleksii.kurochko@gmail.com>
>
> diff --git a/xen/arch/riscv/aplic.c b/xen/arch/riscv/aplic.c
> index 0af13f28e4..cb11d6aeaa 100644
> --- a/xen/arch/riscv/aplic.c
> +++ b/xen/arch/riscv/aplic.c
> @@ -27,7 +27,9 @@
>  #include <asm/imsic.h>
>  #include <asm/intc.h>
>  #include <asm/io.h>
> +#include <asm/processor.h>
>  #include <asm/riscv_encoding.h>
> +#include <asm/smp.h>
>  
>  static struct aplic_priv aplic = {
>      .lock = SPIN_LOCK_UNLOCKED,
> @@ -205,6 +207,30 @@ void aplic_hw_write_reg(unsigned int offset, uint32_t value)
>      spin_unlock_irqrestore(&aplic.lock, flags);
>  }
>  
> +/*
> + * As needed, synchronize with all IOMMUs and APLICs to ensure that no
> + * straggler MSIs will arrive at the old interrupt file after this step.
> + */
> +void aplic_genmsi_barrier(void)
> +{
> +    const struct imsic_config *imsic = imsic_get_config();
> +    unsigned int cpu = smp_processor_id();
> +    unsigned long flags;
> +    uint32_t val;
> +
> +    val = MASK_INSR(aplic_hart_field(cpu), APLIC_TARGET_HART_IDX) |
> +          (imsic->sync_id & APLIC_TARGET_EIID);


> +
> +    spin_lock_irqsave(&aplic.lock, flags);
> +
> +    writel(val, &aplic.regs->genmsi);
> +
> +    while ( readl(&aplic.regs->genmsi) & APLIC_GENMSI_BUSY )
> +        cpu_relax();
> +
> +    spin_unlock_irqrestore(&aplic.lock, flags);
> +}
> +
According to AIA spec §4.9.3 (Synchronizing interactions between a hart and the APLIC), the sequence needs 6 steps; this implements only steps 2-5:

- Step 1: clear the pending bit for sync_id at the hart's IMSIC before writing genmsi.
- Step 6: after releasing the lock, poll the pending bit for sync_id at the hart's IMSIC until it's set.

Step 4 (Busy clear) only means the APLIC has accepted/sent the MSI, not that it has arrived at the hart (the spec notes an unspecified travel delay).
Without step 6, aplic_genmsi_barrier() returns before the MSI (and thus prior MSIs) actually reach the hart, so it doesn't achieve the barrier it's meant to
provide.

-- 
Baptiste Le Duc <baptiste.le-duc@vates.tech>


^ permalink raw reply	[flat|nested] 244+ messages in thread

* Re: [PATCH v2 27/39] xen/riscv: introduce arch_move_irqs()
  2026-08-27 15:21 ` [PATCH v2 27/39] xen/riscv: introduce arch_move_irqs() Oleksii Kurochko
  2026-09-14 12:07   ` Jan Beulich
@ 2026-09-22 17:00   ` Baptiste Le Duc
  1 sibling, 0 replies; 244+ messages in thread
From: Baptiste Le Duc @ 2026-09-22 17:00 UTC (permalink / raw)
  To: Oleksii Kurochko
  Cc: xen-devel, Romain Caritey, Baptiste Le Duc, Zheng Zhang,
	Alistair Francis, Connor Davis, Andrew Cooper, Anthony PERARD,
	Michal Orzel, Jan Beulich, Julien Grall, Roger Pau Monné,
	Stefano Stabellini

On Thu, 27 Aug 2026 17:21:11 +0200, Oleksii Kurochko <oleksii.kurochko@gmail.com> wrote:
> When migrating a vCPU between pCPUs the hypervisor must also migrate
> the associated virtual interrupt state. arch_move_irqs() is the
> per-arch hook called by generic code to trigger that.
> 
> Replace the static inline BUG_ON placeholder in asm/irq.h with a real
> implementation in intc.c dispatching through a new move_irqs vintc_ops
> callback. Wire it up in vAPLIC, which delegates to imsic_migrate_vcpu()
> which itself still a stub to be implemented in follow-up patches.
> 
> [...]

Reviewed-by: Baptiste Le Duc <baptiste.le-duc@vates.tech>

-- 
Baptiste Le Duc <baptiste.le-duc@vates.tech>


^ permalink raw reply	[flat|nested] 244+ messages in thread

* Re: [PATCH v2 31/39] xen/riscv: implement APLIC-hart sync barrier for vCPU migration
  2026-09-18 11:53     ` Oleksii Kurochko
@ 2026-09-22 17:03       ` Baptiste Le Duc
  0 siblings, 0 replies; 244+ messages in thread
From: Baptiste Le Duc @ 2026-09-22 17:03 UTC (permalink / raw)
  To: Oleksii Kurochko
  Cc: Jan Beulich, Romain Caritey, Baptiste Le Duc, Zheng Zhang,
	Alistair Francis, Connor Davis, Andrew Cooper, Anthony PERARD,
	Michal Orzel, Julien Grall, Roger Pau Monné,
	Stefano Stabellini, xen-devel

On 2026-09-18 13:53:12+02:00, Oleksii Kurochko wrote:
> On 9/14/26 3:27 PM, Jan Beulich wrote:
> 
> > On 27.08.2026 17:21, Oleksii Kurochko wrote:
> > 
> > Hmm. As indicated, I'm learning RISC-V as I'm reviewing patches. This
> > paragraph, if left as is, would make sure I simply can't ack the patch.
> > I just don't understand what is being talked about. I can guess parts,
> > but for example I don't know what "genmsi" is.
> 
> I will reword then commit message in the following way:
> 
> ```
> When a vCPU is moved to a different guest interrupt file, MSIs that the
> APLIC has already sent towards the old file may still be in flight. They
> must land before the old file's state is saved and the switch is done,
> otherwise they would be lost.
> 
> To wait for them, use the APLIC's genmsi register. Writing it makes the
> APLIC itself send an MSI (an "extempore" MSI) with a given interrupt
> identity to a given hart. genmsi can only target the hart's supervisor-
Nit: supervisor = hypervisor in our pov
> level interrupt file, not a guest one, but the AIA spec guarantees that
> all MSIs previously sent by the same APLIC to the same hart become
> visible at the hart's IMSIC before the extempore MSI does. So once the
> extempore MSI has been delivered, no older MSI from this APLIC to the
> hart can still be in flight, whichever interrupt file it targets.
> 
> The last interrupt identity (nr_ids) is reserved for this purpose.
> ```
> 
> > Along the lines of a question on an earlier patch: What if this ANDing
> > actually chops off bits?
> 
> I will do then the same as I did in aplic_set_irq_affinity() (i 
> mentioned that in the one of the replies connected to this function in 
> this patch series):
> 
>      /*
>       * sync_id is nr_ids, which imsic_parse_node() limits to IMSIC_MAX_ID,
>       * so it always fits into the EIID field.
>       */
>      BUILD_BUG_ON(IMSIC_MAX_ID > MASK_EXTR(~0U, APLIC_TARGET_EIID));
>      ASSERT(imsic->sync_id <= IMSIC_MAX_ID);
> 
>      val = MASK_INSR(aplic_hart_index(cpu), APLIC_TARGET_HART_IDX) |
>            imsic->sync_id;
> 
> Thanks.
> 
> ~ Oleksii




^ permalink raw reply	[flat|nested] 244+ messages in thread

* Re: [PATCH v2 31/39] xen/riscv: implement APLIC-hart sync barrier for vCPU migration
  2026-09-22 17:00   ` Baptiste Le Duc
@ 2026-09-22 18:48     ` Oleksii Kurochko
  2026-09-23 10:57       ` Oleksii Kurochko
  0 siblings, 1 reply; 244+ messages in thread
From: Oleksii Kurochko @ 2026-09-22 18:48 UTC (permalink / raw)
  To: Baptiste Le Duc
  Cc: xen-devel, Romain Caritey, Zheng Zhang, Alistair Francis,
	Connor Davis, Andrew Cooper, Anthony PERARD, Michal Orzel,
	Jan Beulich, Julien Grall, Roger Pau Monné,
	Stefano Stabellini



On 9/22/26 7:00 PM, Baptiste Le Duc wrote:
>> During migration of a virtual hart to a different guest interrupt file,
>> straggler MSIs from the APLIC could arrive at the old interrupt file
>> after the switch.
>>
>> genmsi is used despite not supporting guest interrupt files because the
>> AIA spec guarantees that all MSIs previously sent from the APLIC to the
>> same hart are visible at the hart's IMSIC before the extempore MSI from
>> genmsi becomes visible.
> 
> 
>>
>> Signed-off-by: Oleksii Kurochko <oleksii.kurochko@gmail.com>
>>
>> diff --git a/xen/arch/riscv/aplic.c b/xen/arch/riscv/aplic.c
>> index 0af13f28e4..cb11d6aeaa 100644
>> --- a/xen/arch/riscv/aplic.c
>> +++ b/xen/arch/riscv/aplic.c
>> @@ -27,7 +27,9 @@
>>   #include <asm/imsic.h>
>>   #include <asm/intc.h>
>>   #include <asm/io.h>
>> +#include <asm/processor.h>
>>   #include <asm/riscv_encoding.h>
>> +#include <asm/smp.h>
>>   
>>   static struct aplic_priv aplic = {
>>       .lock = SPIN_LOCK_UNLOCKED,
>> @@ -205,6 +207,30 @@ void aplic_hw_write_reg(unsigned int offset, uint32_t value)
>>       spin_unlock_irqrestore(&aplic.lock, flags);
>>   }
>>   
>> +/*
>> + * As needed, synchronize with all IOMMUs and APLICs to ensure that no
>> + * straggler MSIs will arrive at the old interrupt file after this step.
>> + */
>> +void aplic_genmsi_barrier(void)
>> +{
>> +    const struct imsic_config *imsic = imsic_get_config();
>> +    unsigned int cpu = smp_processor_id();
>> +    unsigned long flags;
>> +    uint32_t val;
>> +
>> +    val = MASK_INSR(aplic_hart_field(cpu), APLIC_TARGET_HART_IDX) |
>> +          (imsic->sync_id & APLIC_TARGET_EIID);
> 
> 
>> +
>> +    spin_lock_irqsave(&aplic.lock, flags);
>> +
>> +    writel(val, &aplic.regs->genmsi);
>> +
>> +    while ( readl(&aplic.regs->genmsi) & APLIC_GENMSI_BUSY )
>> +        cpu_relax();
>> +
>> +    spin_unlock_irqrestore(&aplic.lock, flags);
>> +}
>> +
> According to AIA spec §4.9.3 (Synchronizing interactions between a hart and the APLIC), the sequence needs 6 steps; this implements only steps 2-5:
> 
> - Step 1: clear the pending bit for sync_id at the hart's IMSIC before writing genmsi.
> - Step 6: after releasing the lock, poll the pending bit for sync_id at the hart's IMSIC until it's set.
> 
> Step 4 (Busy clear) only means the APLIC has accepted/sent the MSI, not that it has arrived at the hart (the spec notes an unspecified travel delay).
> Without step 6, aplic_genmsi_barrier() returns before the MSI (and thus prior MSIs) actually reach the hart, so it doesn't achieve the barrier it's meant to
> provide.
> 

It is really missed but it exists in riscv-next-upstream branch 
(https://gitlab.com/xen-project/people/olkur/xen/-/blob/riscv-next-upstreaming/xen/arch/riscv/imsic.c#L723).

I will re-check why it is missed here.

Thanks for noticing that.

~ Oleksii



^ permalink raw reply	[flat|nested] 244+ messages in thread

* Re: [PATCH v2 31/39] xen/riscv: implement APLIC-hart sync barrier for vCPU migration
  2026-09-22 18:48     ` Oleksii Kurochko
@ 2026-09-23 10:57       ` Oleksii Kurochko
  2026-09-23 12:15         ` Baptiste Le Duc
  0 siblings, 1 reply; 244+ messages in thread
From: Oleksii Kurochko @ 2026-09-23 10:57 UTC (permalink / raw)
  To: Baptiste Le Duc
  Cc: xen-devel, Romain Caritey, Zheng Zhang, Alistair Francis,
	Connor Davis, Andrew Cooper, Anthony PERARD, Michal Orzel,
	Jan Beulich, Julien Grall, Roger Pau Monné,
	Stefano Stabellini



On 9/22/26 8:48 PM, Oleksii Kurochko wrote:
> 
> 
> On 9/22/26 7:00 PM, Baptiste Le Duc wrote:
>>> During migration of a virtual hart to a different guest interrupt file,
>>> straggler MSIs from the APLIC could arrive at the old interrupt file
>>> after the switch.
>>>
>>> genmsi is used despite not supporting guest interrupt files because the
>>> AIA spec guarantees that all MSIs previously sent from the APLIC to the
>>> same hart are visible at the hart's IMSIC before the extempore MSI from
>>> genmsi becomes visible.
>>
>>
>>>
>>> Signed-off-by: Oleksii Kurochko <oleksii.kurochko@gmail.com>
>>>
>>> diff --git a/xen/arch/riscv/aplic.c b/xen/arch/riscv/aplic.c
>>> index 0af13f28e4..cb11d6aeaa 100644
>>> --- a/xen/arch/riscv/aplic.c
>>> +++ b/xen/arch/riscv/aplic.c
>>> @@ -27,7 +27,9 @@
>>>   #include <asm/imsic.h>
>>>   #include <asm/intc.h>
>>>   #include <asm/io.h>
>>> +#include <asm/processor.h>
>>>   #include <asm/riscv_encoding.h>
>>> +#include <asm/smp.h>
>>>   static struct aplic_priv aplic = {
>>>       .lock = SPIN_LOCK_UNLOCKED,
>>> @@ -205,6 +207,30 @@ void aplic_hw_write_reg(unsigned int offset, 
>>> uint32_t value)
>>>       spin_unlock_irqrestore(&aplic.lock, flags);
>>>   }
>>> +/*
>>> + * As needed, synchronize with all IOMMUs and APLICs to ensure that no
>>> + * straggler MSIs will arrive at the old interrupt file after this 
>>> step.
>>> + */
>>> +void aplic_genmsi_barrier(void)
>>> +{
>>> +    const struct imsic_config *imsic = imsic_get_config();
>>> +    unsigned int cpu = smp_processor_id();
>>> +    unsigned long flags;
>>> +    uint32_t val;
>>> +
>>> +    val = MASK_INSR(aplic_hart_field(cpu), APLIC_TARGET_HART_IDX) |
>>> +          (imsic->sync_id & APLIC_TARGET_EIID);
>>
>>
>>> +
>>> +    spin_lock_irqsave(&aplic.lock, flags);
>>> +
>>> +    writel(val, &aplic.regs->genmsi);
>>> +
>>> +    while ( readl(&aplic.regs->genmsi) & APLIC_GENMSI_BUSY )
>>> +        cpu_relax();
>>> +
>>> +    spin_unlock_irqrestore(&aplic.lock, flags);
>>> +}
>>> +
>> According to AIA spec §4.9.3 (Synchronizing interactions between a 
>> hart and the APLIC), the sequence needs 6 steps; this implements only 
>> steps 2-5:
>>
>> - Step 1: clear the pending bit for sync_id at the hart's IMSIC before 
>> writing genmsi.
>> - Step 6: after releasing the lock, poll the pending bit for sync_id 
>> at the hart's IMSIC until it's set.
>>
>> Step 4 (Busy clear) only means the APLIC has accepted/sent the MSI, 
>> not that it has arrived at the hart (the spec notes an unspecified 
>> travel delay).
>> Without step 6, aplic_genmsi_barrier() returns before the MSI (and 
>> thus prior MSIs) actually reach the hart, so it doesn't achieve the 
>> barrier it's meant to
>> provide.
>>
> 
> It is really missed but it exists in riscv-next-upstream branch 
> (https://gitlab.com/xen-project/people/olkur/xen/-/blob/riscv-next- 
> upstreaming/xen/arch/riscv/imsic.c#L723).
> 
> I will re-check why it is missed here.

Step 1 and step 6 are not missing, they are just not part of
aplic_genmsi_barrier() itself. They are done by its caller, which is 
added in "xen/riscv: remap interrupts to new IMSIC VS-file":

     static void cf_check imsic_aplic_sync(void *unused)
     {
         imsic_local_eix_update(imsic_cfg.sync_id, 1, true, false);

         aplic_genmsi_barrier();

         while ( !imsic_local_is_pending(imsic_cfg.sync_id) )
             cpu_relax();
     }

I did consider doing all six steps inside aplic_genmsi_barrier(), but 
that would make aplic.c reach into IMSIC internals (the local EIx CSR 
accessors, which are private to imsic.c), and I would rather not add 
that dependency. Keeping the APLIC half (write genmsi, wait for Busy to 
clear) in aplic.c and the IMSIC half (clear the pending bit of sync_id 
before, poll it until set afterwards) in imsic.c keeps the layering clean.

You are right, though, that nothing in this patch says so, and that the
helper on its own is not the full barrier its name suggests. I will 
spell the split out in the commit message and in the comment above the 
function.

Probably it will be better to introduce function imsic_aplic_sync() as a 
part of this patch.

~ Oleksii


^ permalink raw reply	[flat|nested] 244+ messages in thread

* Re: [PATCH v2 31/39] xen/riscv: implement APLIC-hart sync barrier for vCPU migration
  2026-09-23 10:57       ` Oleksii Kurochko
@ 2026-09-23 12:15         ` Baptiste Le Duc
  0 siblings, 0 replies; 244+ messages in thread
From: Baptiste Le Duc @ 2026-09-23 12:15 UTC (permalink / raw)
  To: Oleksii Kurochko
  Cc: Baptiste Le Duc, xen-devel, Romain Caritey, Zheng Zhang,
	Alistair Francis, Connor Davis, Andrew Cooper, Anthony PERARD,
	Michal Orzel, Jan Beulich, Julien Grall, Roger Pau Monné,
	Stefano Stabellini

On 2026-09-23 12:57 +0200, Oleksii Kurochko wrote:
> 
> 
> On 9/22/26 8:48 PM, Oleksii Kurochko wrote:
> > 
> > 
> > On 9/22/26 7:00 PM, Baptiste Le Duc wrote:
> >>> During migration of a virtual hart to a different guest interrupt file,
> >>> straggler MSIs from the APLIC could arrive at the old interrupt file
> >>> after the switch.
> >>>
> >>> genmsi is used despite not supporting guest interrupt files because the
> >>> AIA spec guarantees that all MSIs previously sent from the APLIC to the
> >>> same hart are visible at the hart's IMSIC before the extempore MSI from
> >>> genmsi becomes visible.
> >>
> >>
> >>>
> >>> Signed-off-by: Oleksii Kurochko <oleksii.kurochko@gmail.com>
> >>>
> >>> diff --git a/xen/arch/riscv/aplic.c b/xen/arch/riscv/aplic.c
> >>> index 0af13f28e4..cb11d6aeaa 100644
> >>> --- a/xen/arch/riscv/aplic.c
> >>> +++ b/xen/arch/riscv/aplic.c
> >>> @@ -27,7 +27,9 @@
> >>>   #include <asm/imsic.h>
> >>>   #include <asm/intc.h>
> >>>   #include <asm/io.h>
> >>> +#include <asm/processor.h>
> >>>   #include <asm/riscv_encoding.h>
> >>> +#include <asm/smp.h>
> >>>   static struct aplic_priv aplic = {
> >>>       .lock = SPIN_LOCK_UNLOCKED,
> >>> @@ -205,6 +207,30 @@ void aplic_hw_write_reg(unsigned int offset, 
> >>> uint32_t value)
> >>>       spin_unlock_irqrestore(&aplic.lock, flags);
> >>>   }
> >>> +/*
> >>> + * As needed, synchronize with all IOMMUs and APLICs to ensure that no
> >>> + * straggler MSIs will arrive at the old interrupt file after this 
> >>> step.
> >>> + */
> >>> +void aplic_genmsi_barrier(void)
> >>> +{
> >>> +    const struct imsic_config *imsic = imsic_get_config();
> >>> +    unsigned int cpu = smp_processor_id();
> >>> +    unsigned long flags;
> >>> +    uint32_t val;
> >>> +
> >>> +    val = MASK_INSR(aplic_hart_field(cpu), APLIC_TARGET_HART_IDX) |
> >>> +          (imsic->sync_id & APLIC_TARGET_EIID);
> >>
> >>
> >>> +
> >>> +    spin_lock_irqsave(&aplic.lock, flags);
> >>> +
> >>> +    writel(val, &aplic.regs->genmsi);
> >>> +
> >>> +    while ( readl(&aplic.regs->genmsi) & APLIC_GENMSI_BUSY )
> >>> +        cpu_relax();
> >>> +
> >>> +    spin_unlock_irqrestore(&aplic.lock, flags);
> >>> +}
> >>> +
> >> According to AIA spec §4.9.3 (Synchronizing interactions between a 
> >> hart and the APLIC), the sequence needs 6 steps; this implements only 
> >> steps 2-5:
> >>
> >> - Step 1: clear the pending bit for sync_id at the hart's IMSIC before 
> >> writing genmsi.
> >> - Step 6: after releasing the lock, poll the pending bit for sync_id 
> >> at the hart's IMSIC until it's set.
> >>
> >> Step 4 (Busy clear) only means the APLIC has accepted/sent the MSI, 
> >> not that it has arrived at the hart (the spec notes an unspecified 
> >> travel delay).
> >> Without step 6, aplic_genmsi_barrier() returns before the MSI (and 
> >> thus prior MSIs) actually reach the hart, so it doesn't achieve the 
> >> barrier it's meant to
> >> provide.
> >>
> > 
> > It is really missed but it exists in riscv-next-upstream branch 
> > (https://gitlab.com/xen-project/people/olkur/xen/-/blob/riscv-next- 
> > upstreaming/xen/arch/riscv/imsic.c#L723).
> > 
> > I will re-check why it is missed here.
> 
> Step 1 and step 6 are not missing, they are just not part of
> aplic_genmsi_barrier() itself. They are done by its caller, which is 
> added in "xen/riscv: remap interrupts to new IMSIC VS-file":
> 
>      static void cf_check imsic_aplic_sync(void *unused)
>      {
>          imsic_local_eix_update(imsic_cfg.sync_id, 1, true, false);
> 
>          aplic_genmsi_barrier();
> 
>          while ( !imsic_local_is_pending(imsic_cfg.sync_id) )
>              cpu_relax();
>      }
> 
> I did consider doing all six steps inside aplic_genmsi_barrier(), but 
> that would make aplic.c reach into IMSIC internals (the local EIx CSR 
> accessors, which are private to imsic.c), and I would rather not add 
> that dependency. Keeping the APLIC half (write genmsi, wait for Busy to 
> clear) in aplic.c and the IMSIC half (clear the pending bit of sync_id 
> before, poll it until set afterwards) in imsic.c keeps the layering clean.
> 
> You are right, though, that nothing in this patch says so, and that the
> helper on its own is not the full barrier its name suggests. I will 
> spell the split out in the commit message and in the comment above the 
> function.
> 
> Probably it will be better to introduce function imsic_aplic_sync() as a 
> part of this patch.
Yes it would make sense as function imsic_aplic_sync() is introduced in
next patch so reviewer couldn't know the existence of it when reading
this patch. Thanks for that!
> 
> ~ Oleksii
> 
> 
> 




^ permalink raw reply	[flat|nested] 244+ messages in thread

* Re: [PATCH v2 32/39] xen/riscv: remap interrupts to new IMSIC VS-file
  2026-08-27 15:21 ` [PATCH v2 32/39] xen/riscv: remap interrupts to new IMSIC VS-file Oleksii Kurochko
  2026-09-14 15:02   ` Jan Beulich
@ 2026-09-23 13:34   ` Baptiste Le Duc
  2026-09-23 15:45     ` Oleksii Kurochko
  2026-09-23 15:25   ` Baptiste Le Duc
  2 siblings, 1 reply; 244+ messages in thread
From: Baptiste Le Duc @ 2026-09-23 13:34 UTC (permalink / raw)
  To: Oleksii Kurochko
  Cc: xen-devel, Romain Caritey, Baptiste Le Duc, Zheng Zhang,
	Alistair Francis, Connor Davis, Andrew Cooper, Anthony PERARD,
	Michal Orzel, Jan Beulich, Julien Grall, Roger Pau Monné,
	Stefano Stabellini

> After new IMSIC VS-file is zeroed-out it is necessary to do G-stage remaping
> fo new IMSIC VS-file. Also, if any interrupts at an APLIC are forwarded by
> MSIs to the old interrupt file, reconfigure the APLIC to send them to the
> new interrupt file.
> 
> Generally it is needed also to modify the relevant translation tables at
> all IOMMUs so that MSIs for this virtual interrupt file are now sent to
> the new physical interrupt file but it is skipped for now there is no IOMMU
> support for RISC-V.
> 
> Synchronize with APLIC to ensure that no straggler MSIs will arrive at
> the old interrupt file by using of aplic_genmsi_barrier().
> 


> Technically there is no need for read_lock_irqsave() and
> read_unlock_irqrestore() around reading of ->guest_file_id, as a write
> cannot happen in parallel: any update to ->guest_file_id for a vCPU
> will happen either in imsic_migrate_vcpu() itself or before the vCPU


> first gains control (in continue_new_vcpu()), so there is no concurrent
> access to it in imsic_migrate_vcpu(). The lock is added here for
> potential future cases.
> 
> Keep BUG_ON("unimplemented") placeholder in imsic_migrate_vcpu() to guard
> against silent incorrect behaviour or unexpected panics in guest VMs until
> the function is fully implemented.
> 
> imsic_map_guest_file() and imsic_update_state() are stubs for now and will
> be introduced later in a separate patch.
"will be introduced later" would be stale in the future.
> 
> Signed-off-by: Oleksii Kurochko <oleksii.kurochko@gmail.com>


>
> diff --git a/xen/arch/riscv/imsic.c b/xen/arch/riscv/imsic.c
> index 5de4594961..5e9f6995e4 100644
> --- a/xen/arch/riscv/imsic.c
> +++ b/xen/arch/riscv/imsic.c
> @@ -27,6 +27,7 @@
>  #include <xen/xvmalloc.h>
>  
>  #include <asm/aia.h>
> +#include <asm/aplic.h>
>  #include <asm/imsic.h>
>  
>  #define IMSIC_HART_SIZE(guest_bits) (BIT(guest_bits, U) * IMSIC_MMIO_PAGE_SZ)
> @@ -60,6 +61,12 @@ static unsigned int __ro_after_init guest_num_msis;
>  #define IMSIC_DISABLE_EITHRESHOLD   1
>  #define IMSIC_ENABLE_EITHRESHOLD    0
>  
> +#define imsic_csr_read(c)           \
> +({                                  \
> +    csr_write(CSR_SISELECT, (c));   \
> +    csr_read(CSR_SIREG);            \
> +})
> +
>  #define imsic_csr_write(c, v)   \
>  do {                            \
>      csr_write(CSR_SISELECT, c); \
> @@ -141,6 +148,11 @@ unsigned int vcpu_guest_file_id(const struct vcpu *v)
>      return ACCESS_ONCE(v->arch.vimsic_state->guest_file_id);
>  }
>  
> +void imsic_update_state(struct vcpu *v, unsigned int guest_file_id)
> +{
> +    BUG_ON("unimplemented\n");
> +}
> +
>  void __init imsic_ids_local_delivery(bool enable)
>  {
>      if ( enable )
> @@ -242,6 +254,15 @@ void imsic_irq_disable(unsigned int irq)
>      spin_unlock(&imsic_cfg.lock);
>  }
>  
> +static bool imsic_local_is_pending(unsigned int id)
> +{
> +    unsigned long isel =
> +        (id / BITS_PER_LONG) * (BITS_PER_LONG / IMSIC_EIPx_BITS) + IMSIC_EIP0;
> +    unsigned long bit = BIT(id % BITS_PER_LONG, UL);
> +
> +    return !!(imsic_csr_read(isel) & bit);
> +}
> +
>  /* Callers aren't intended to changed imsic_cfg so return const. */
>  const struct imsic_config *imsic_get_config(void)
>  {
> @@ -436,6 +457,11 @@ void cf_check imsic_ctxt_switch_to(struct vcpu *v)
>      /* Nothing to do */
>  }
>  
> +int imsic_map_guest_file(struct vcpu *v, unsigned int vsfile_id)
> +{
> +    return -EOPNOTSUPP;
> +}
> +
>  int cf_check vcpu_imsic_init(struct vcpu *v)
>  {
>      struct vimsic_state *imsic_state;
> @@ -497,6 +523,27 @@ static void imsic_call_on_cpu(unsigned int cpu, void (*func)(void *),
>          on_selected_cpus(cpumask_of(cpu), func, data, 1);
>  }
>  
> +/*
> + * Ensure that all the MSIs the APLIC has already generated for the hart this
> + * runs on have really reached the hart's IMSIC.
> + *
> + * The barrier is the one described by the AIA specification in
> + * "Synchronizing interactions between a hart and the APLIC": ask the APLIC to
> + * send an MSI to the hart itself and wait until it shows up as pending in the
> + * hart's own interrupt file. As it says nothing about MSIs on their way to
> + * any other hart, it has to be executed by the pCPU owning the interrupt file
> + * the MSIs were being sent to.
> + */
> +static void cf_check imsic_aplic_sync(void *data)
It seems data arg is not used here
> +{
> +    imsic_local_eix_update(imsic_cfg.sync_id, 1, true, false);
> +
> +    aplic_genmsi_barrier();
> +
> +    while ( !imsic_local_is_pending(imsic_cfg.sync_id) )
> +        cpu_relax();
> +}
> +
>  static void cf_check imsic_vsfile_local_clear(void *data)
>  {
>      unsigned int i;
> @@ -837,6 +884,10 @@ void imsic_migrate_vcpu(struct vcpu *v)
>      struct imsic_vsfile_data vsfile_data = {
>          .nr_eix = nr_hw_eix,
>      };
> +    struct vimsic_state *imsic_state = v->arch.vimsic_state;
> +    unsigned long flags;
> +    unsigned int old_vsfile_id;
> +    unsigned int old_vsfile_cpu;
>  
>      /*
>       * The scheduler can mark a freshly created vCPU's unit as migrated and
> @@ -848,8 +899,22 @@ void imsic_migrate_vcpu(struct vcpu *v)
>      if ( v->arch.last_cpu == NR_CPUS )
>          return;
>  
> +    read_lock_irqsave(&imsic_state->vsfile_lock, flags);
> +    old_vsfile_id = imsic_state->guest_file_id;
> +    old_vsfile_cpu = imsic_state->vsfile_cpu;
> +    read_unlock_irqrestore(&imsic_state->vsfile_lock, flags);
> +
> +    /*
> +     * We don't support SW interrupt files at the moment. Bail out before
> +     * anything is touched, as the old file has no owning pCPU in that case
> +     * and there is nothing to retarget the producers away from.
> +     */
> +    if ( old_vsfile_cpu == NR_CPUS )
> +        panic("IMSIC SW-file isn't supported\n");
> +
>      /*
>       * At this point, all interrupt producers are still using the old IMSIC
> +     * VS-file so we first move all interrupt producers to the new IMSIC
>       * VS-file.
>       */
>  
> @@ -870,5 +935,43 @@ void imsic_migrate_vcpu(struct vcpu *v)
>      /* Zero-out new IMSIC VS-file */
>      imsic_call_on_cpu(new_vsfile_cpu, imsic_vsfile_local_clear, &vsfile_data);
> +    /* Update G-stage mapping for the new IMSIC VS-file */
> +    if ( imsic_map_guest_file(v, new_vsfile_hgei) )
> +    {
> +        domain_crash(v->domain, "Migration to hw interrupt file failed\n");
> +
> +        return;
> +    }
> +
> +    imsic_update_state(v, new_vsfile_hgei);
> +
This gets rewritten later in the series by "xen/riscv: introduce IMSIC h/w
interrupt file attaching to vcpu", which moves the clear/map/update sequence
into imsic_vsfile_acquire() (and adds vgein_release() on the error path).

Could imsic_vsfile_acquire() be introduced here (or in a prep patch) instead,
so that the later patch only adds imsic_vsfile_attach()? That would avoid the
churn.

-- 
Baptiste Le Duc <baptiste.le-duc@vates.tech>


^ permalink raw reply	[flat|nested] 244+ messages in thread

* Re: [PATCH v2 33/39] xen/riscv: dump old interrupt file to memory
  2026-08-27 15:21 ` [PATCH v2 33/39] xen/riscv: dump old interrupt file to memory Oleksii Kurochko
  2026-09-14 15:15   ` Jan Beulich
@ 2026-09-23 15:15   ` Baptiste Le Duc
  2026-09-23 16:02     ` Oleksii Kurochko
  1 sibling, 1 reply; 244+ messages in thread
From: Baptiste Le Duc @ 2026-09-23 15:15 UTC (permalink / raw)
  To: Oleksii Kurochko
  Cc: xen-devel, Romain Caritey, Baptiste Le Duc, Zheng Zhang,
	Alistair Francis, Connor Davis, Andrew Cooper, Anthony PERARD,
	Michal Orzel, Jan Beulich, Julien Grall, Roger Pau Monné,
	Stefano Stabellini

> At the old interrupt file, dump to memory all the eip and eie arrays).
Typo `)`
> After this step is done, the old interrupt file is no longer in use so
> old intrrupt file VGEIN could be released.
> 
> Restoring of old interrupt file state will be done in follow-up
> patch.
> 


> There are cases where it is needed to specify on which cpu it is
> necessary to VGEIN should be released so update vgein_release() to

The sentence miss a verb, here is a proposal:
```
There are cases where the cpu on which the VGEIN is released needs to
be specified, so update vgein_release() to deal with that.
```
Moreover, could you explain me the cases you are talking about? It's not
clear by reading the commit message in the first place.


> deal with that.




> 


> Keep BUG_ON("unimplemented") placeholder in imsic_migrate_vcpu() to guard


> against silent incorrect behaviour or unexpected panics in guest VMs until
> the function is fully implemented.
> 


> vgein_release() is stub for now and will be introduced later.


> 
> Signed-off-by: Oleksii Kurochko <oleksii.kurochko@gmail.com>
>
> diff --git a/xen/arch/riscv/aia.c b/xen/arch/riscv/aia.c
> index 75c82bcfa1..be3901ec0c 100644
> --- a/xen/arch/riscv/aia.c
> +++ b/xen/arch/riscv/aia.c
> @@ -30,3 +30,8 @@ unsigned int vgein_assign(struct vcpu *v)
>  
>      return 0;
>  }
> +
> +void vgein_release(struct vcpu *v, unsigned int vgein_id, unsigned int cpu)
> +{
> +    BUG_ON("unimplemented\n");
> +}
> diff --git a/xen/arch/riscv/imsic.c b/xen/arch/riscv/imsic.c
> index 5e9f6995e4..3cba58e0c1 100644
> --- a/xen/arch/riscv/imsic.c
> +++ b/xen/arch/riscv/imsic.c
> @@ -56,6 +56,24 @@ static unsigned int __ro_after_init guest_num_msis;
>   */
>  #define GUEST_IMSIC_MAX_MSIS 255U
>  
> +/*
> + * The interrupt identities an IMSIC interrupt file provides are 0 (which is
> + * never valid, but still occupies a bit) up to IMSIC_MAX_ID inclusive, so
> + * IMSIC_MAX_ID + 1 bits have to be covered.
> + */
> +#define IMSIC_MAX_EIX DIV_ROUND_UP(IMSIC_MAX_ID + 1, BITS_PER_TYPE(uint64_t))


> +
> +struct imsic_mrif_eix {
> +    unsigned long eip[BITS_PER_TYPE(uint64_t) / BITS_PER_LONG];
> +    unsigned long eie[BITS_PER_TYPE(uint64_t) / BITS_PER_LONG];


> +};
> +
> +struct imsic_mrif {
> +    struct imsic_mrif_eix eix[IMSIC_MAX_EIX];
> +    unsigned long eithreshold;
> +    unsigned long eidelivery;
> +};
> +
Maybe I didn't get something but I couldn't find anything in the commit
message explaining why do we use mrif here.

Moreover, mrif, as described in aia spec (8.3 Memory-resident interrupt
files), seems to be only usable with IOMMU that Xen doesn't support.

If you want to have something in memory that could store some interrupt
file info, we should take another name to not be confusing.

-- 
Baptiste Le Duc <baptiste.le-duc@vates.tech>


^ permalink raw reply	[flat|nested] 244+ messages in thread

* Re: [PATCH v2 32/39] xen/riscv: remap interrupts to new IMSIC VS-file
  2026-08-27 15:21 ` [PATCH v2 32/39] xen/riscv: remap interrupts to new IMSIC VS-file Oleksii Kurochko
  2026-09-14 15:02   ` Jan Beulich
  2026-09-23 13:34   ` Baptiste Le Duc
@ 2026-09-23 15:25   ` Baptiste Le Duc
  2026-09-23 15:48     ` Oleksii Kurochko
  2 siblings, 1 reply; 244+ messages in thread
From: Baptiste Le Duc @ 2026-09-23 15:25 UTC (permalink / raw)
  To: Oleksii Kurochko, xen-devel
  Cc: Romain Caritey, Zheng Zhang, Alistair Francis, Connor Davis,
	Andrew Cooper, Anthony PERARD, Michal Orzel, Jan Beulich,
	Julien Grall, Roger Pau Monné, Stefano Stabellini

 > xen/riscv: remap interrupts to new IMSIC VS-file

Nit: The next patch uses "restore" in its title, we might want to use
"save" terminology here for symmetry? e.g.:
     "xen/riscv: save old interrupt file state to memory"


On 8/27/26 5:22 PM, Oleksii Kurochko wrote:
> After new IMSIC VS-file is zeroed-out it is necessary to do G-stage remaping
> fo new IMSIC VS-file. Also, if any interrupts at an APLIC are forwarded by
> MSIs to the old interrupt file, reconfigure the APLIC to send them to the
> new interrupt file.
> 
> Generally it is needed also to modify the relevant translation tables at
> all IOMMUs so that MSIs for this virtual interrupt file are now sent to
> the new physical interrupt file but it is skipped for now there is no IOMMU
> support for RISC-V.
> 
> Synchronize with APLIC to ensure that no straggler MSIs will arrive at
> the old interrupt file by using of aplic_genmsi_barrier().
> 
> Technically there is no need for read_lock_irqsave() and
> read_unlock_irqrestore() around reading of ->guest_file_id, as a write
> cannot happen in parallel: any update to ->guest_file_id for a vCPU
> will happen either in imsic_migrate_vcpu() itself or before the vCPU
> first gains control (in continue_new_vcpu()), so there is no concurrent
> access to it in imsic_migrate_vcpu(). The lock is added here for
> potential future cases.
> 
> Keep BUG_ON("unimplemented") placeholder in imsic_migrate_vcpu() to guard
> against silent incorrect behaviour or unexpected panics in guest VMs until
> the function is fully implemented.
> 
> imsic_map_guest_file() and imsic_update_state() are stubs for now and will
> be introduced later in a separate patch.
> 
> Signed-off-by: Oleksii Kurochko <oleksii.kurochko@gmail.com>
> ---
> Changes in v2:
>   - New patch.
> ---
> ---
>   xen/arch/riscv/imsic.c             | 103 +++++++++++++++++++++++++++++
>   xen/arch/riscv/include/asm/imsic.h |   3 +
>   2 files changed, 106 insertions(+)
> 
> diff --git a/xen/arch/riscv/imsic.c b/xen/arch/riscv/imsic.c
> index 5de45949610d..5e9f6995e443 100644
> --- a/xen/arch/riscv/imsic.c
> +++ b/xen/arch/riscv/imsic.c
> @@ -27,6 +27,7 @@
>   #include <xen/xvmalloc.h>
>   
>   #include <asm/aia.h>
> +#include <asm/aplic.h>
>   #include <asm/imsic.h>
>   
>   #define IMSIC_HART_SIZE(guest_bits) (BIT(guest_bits, U) * IMSIC_MMIO_PAGE_SZ)
> @@ -60,6 +61,12 @@ static unsigned int __ro_after_init guest_num_msis;
>   #define IMSIC_DISABLE_EITHRESHOLD   1
>   #define IMSIC_ENABLE_EITHRESHOLD    0
>   
> +#define imsic_csr_read(c)           \
> +({                                  \
> +    csr_write(CSR_SISELECT, (c));   \
> +    csr_read(CSR_SIREG);            \
> +})
> +
>   #define imsic_csr_write(c, v)   \
>   do {                            \
>       csr_write(CSR_SISELECT, c); \
> @@ -141,6 +148,11 @@ unsigned int vcpu_guest_file_id(const struct vcpu *v)
>       return ACCESS_ONCE(v->arch.vimsic_state->guest_file_id);
>   }
>   
> +void imsic_update_state(struct vcpu *v, unsigned int guest_file_id)
> +{
> +    BUG_ON("unimplemented\n");
> +}
> +
>   void __init imsic_ids_local_delivery(bool enable)
>   {
>       if ( enable )
> @@ -242,6 +254,15 @@ void imsic_irq_disable(unsigned int irq)
>       spin_unlock(&imsic_cfg.lock);
>   }
>   
> +static bool imsic_local_is_pending(unsigned int id)
> +{
> +    unsigned long isel =
> +        (id / BITS_PER_LONG) * (BITS_PER_LONG / IMSIC_EIPx_BITS) + IMSIC_EIP0;
> +    unsigned long bit = BIT(id % BITS_PER_LONG, UL);
> +
> +    return !!(imsic_csr_read(isel) & bit);
> +}
> +
>   /* Callers aren't intended to changed imsic_cfg so return const. */
>   const struct imsic_config *imsic_get_config(void)
>   {
> @@ -436,6 +457,11 @@ void cf_check imsic_ctxt_switch_to(struct vcpu *v)
>       /* Nothing to do */
>   }
>   
> +int imsic_map_guest_file(struct vcpu *v, unsigned int vsfile_id)
> +{
> +    return -EOPNOTSUPP;
> +}
> +
>   int cf_check vcpu_imsic_init(struct vcpu *v)
>   {
>       struct vimsic_state *imsic_state;
> @@ -497,6 +523,27 @@ static void imsic_call_on_cpu(unsigned int cpu, void (*func)(void *),
>           on_selected_cpus(cpumask_of(cpu), func, data, 1);
>   }
>   
> +/*
> + * Ensure that all the MSIs the APLIC has already generated for the hart this
> + * runs on have really reached the hart's IMSIC.
> + *
> + * The barrier is the one described by the AIA specification in
> + * "Synchronizing interactions between a hart and the APLIC": ask the APLIC to
> + * send an MSI to the hart itself and wait until it shows up as pending in the
> + * hart's own interrupt file. As it says nothing about MSIs on their way to
> + * any other hart, it has to be executed by the pCPU owning the interrupt file
> + * the MSIs were being sent to.
> + */
> +static void cf_check imsic_aplic_sync(void *data)
> +{
> +    imsic_local_eix_update(imsic_cfg.sync_id, 1, true, false);
> +
> +    aplic_genmsi_barrier();
> +
> +    while ( !imsic_local_is_pending(imsic_cfg.sync_id) )
> +        cpu_relax();
> +}
> +
>   static void cf_check imsic_vsfile_local_clear(void *data)
>   {
>       unsigned int i;
> @@ -837,6 +884,10 @@ void imsic_migrate_vcpu(struct vcpu *v)
>       struct imsic_vsfile_data vsfile_data = {
>           .nr_eix = nr_hw_eix,
>       };
> +    struct vimsic_state *imsic_state = v->arch.vimsic_state;
> +    unsigned long flags;
> +    unsigned int old_vsfile_id;
> +    unsigned int old_vsfile_cpu;
>   
>       /*
>        * The scheduler can mark a freshly created vCPU's unit as migrated and
> @@ -848,8 +899,22 @@ void imsic_migrate_vcpu(struct vcpu *v)
>       if ( v->arch.last_cpu == NR_CPUS )
>           return;
>   
> +    read_lock_irqsave(&imsic_state->vsfile_lock, flags);
> +    old_vsfile_id = imsic_state->guest_file_id;
> +    old_vsfile_cpu = imsic_state->vsfile_cpu;
> +    read_unlock_irqrestore(&imsic_state->vsfile_lock, flags);
> +
> +    /*
> +     * We don't support SW interrupt files at the moment. Bail out before
> +     * anything is touched, as the old file has no owning pCPU in that case
> +     * and there is nothing to retarget the producers away from.
> +     */
> +    if ( old_vsfile_cpu == NR_CPUS )
> +        panic("IMSIC SW-file isn't supported\n");
> +
>       /*
>        * At this point, all interrupt producers are still using the old IMSIC
> +     * VS-file so we first move all interrupt producers to the new IMSIC
>        * VS-file.
>        */
>   
> @@ -870,5 +935,43 @@ void imsic_migrate_vcpu(struct vcpu *v)
>       /* Zero-out new IMSIC VS-file */
>       imsic_call_on_cpu(new_vsfile_cpu, imsic_vsfile_local_clear, &vsfile_data);
>   
> +    /* Update G-stage mapping for the new IMSIC VS-file */
> +    if ( imsic_map_guest_file(v, new_vsfile_hgei) )
> +    {
> +        domain_crash(v->domain, "Migration to hw interrupt file failed\n");
> +
> +        return;
> +    }
> +
> +    imsic_update_state(v, new_vsfile_hgei);
> +
> +    /*
> +     * TODO: Modify the relevant translation tables at all IOMMUs so that MSIs
> +     *       for this virtual interrupt file are now sent to the new physical
> +     *       interrupt file.
> +     */
> +    if ( iommu_enabled )
> +        printk_once("IMSIC: IOMMU MSI retargeting is not implemented\n");
> +
> +    /*
> +     * If any interrupts at an APLIC are forwarded by MSIs to the old interrupt
> +     * file, reconfigure the APLIC to send them to the new interrupt file.
> +     */
> +    aplic_reconfigure_target(v, old_vsfile_id, old_vsfile_cpu);
> +
> +    /*
> +     * Synchronizing interactions between a hart and the APLIC.
> +     *
> +     * The MSIs to be flushed are the ones still on their way to the old
> +     * interrupt file, so the barrier has to be done by the pCPU which owns
> +     * that file.
> +     */
> +    imsic_call_on_cpu(old_vsfile_cpu, imsic_aplic_sync, NULL);
> +
> +    /*
> +     * At this point, all interrupt producers have been moved
> +     * to the new IMSIC VS-file.
> +     */
> +
>       BUG_ON("unimplemented");
>   }
> diff --git a/xen/arch/riscv/include/asm/imsic.h b/xen/arch/riscv/include/asm/imsic.h
> index 6ea2e4b8ca12..6395b539c52d 100644
> --- a/xen/arch/riscv/include/asm/imsic.h
> +++ b/xen/arch/riscv/include/asm/imsic.h
> @@ -114,12 +114,15 @@ void imsic_ids_local_delivery(bool enable);
>   int vcpu_imsic_init(struct vcpu *v);
>   void vcpu_imsic_deinit(struct vcpu *v);
>   unsigned int vcpu_guest_file_id(const struct vcpu *v);
> +void imsic_update_state(struct vcpu *v, unsigned int guest_file_id);
>   
>   int vimsic_make_domu_dt_node(struct kernel_info *kinfo, unsigned int *phandle);
>   
>   void imsic_ctxt_switch_from(struct vcpu *v);
>   void imsic_ctxt_switch_to(struct vcpu *v);
>   
> +int imsic_map_guest_file(struct vcpu *v, unsigned int vsfile_id);
> +
>   void imsic_migrate_vcpu(struct vcpu *v);
>   
>   #endif /* ASM_RISCV_IMSIC_H */



^ permalink raw reply	[flat|nested] 244+ messages in thread

* Re: [PATCH v2 34/39] xen/riscv: restore register state in the new IMSIC VS-file
  2026-08-27 15:21 ` [PATCH v2 34/39] xen/riscv: restore register state in the new IMSIC VS-file Oleksii Kurochko
  2026-09-14 15:21   ` Jan Beulich
@ 2026-09-23 15:42   ` Baptiste Le Duc
  2026-09-23 16:08     ` Oleksii Kurochko
  1 sibling, 1 reply; 244+ messages in thread
From: Baptiste Le Duc @ 2026-09-23 15:42 UTC (permalink / raw)
  To: Oleksii Kurochko
  Cc: xen-devel, Romain Caritey, Baptiste Le Duc, Zheng Zhang,
	Alistair Francis, Connor Davis, Andrew Cooper, Anthony PERARD,
	Michal Orzel, Jan Beulich, Julien Grall, Roger Pau Monné,
	Stefano Stabellini

> At this point, all interrupt producers have been moved to the new
> IMSIC VS-file so we move register state from the old IMSIC VS/SW-file
> to the new IMSIC VS-file.
> 
> As new IMSIC VS-file is ready to be used update vCPU's hstatus with
> new VGEIN.
> 
> As the whole migration procedure is finished add some extra explanatory
> comments.
> 
> Signed-off-by: Oleksii Kurochko <oleksii.kurochko@gmail.com>
>
> diff --git a/xen/arch/riscv/imsic.c b/xen/arch/riscv/imsic.c
> index 3cba58e0c1..d7b137a1f5 100644
> --- a/xen/arch/riscv/imsic.c
> +++ b/xen/arch/riscv/imsic.c
> @@ -112,6 +112,12 @@ do {                            \
>      r_;                             \
>  })
>  
> +#define imsic_vs_csr_set(c, v)      \
> +do {                                \
> +    csr_write(CSR_VSISELECT, (c));  \
> +    csr_set(CSR_VSIREG, (v));       \
> +} while ( 0 )
> +
>  #define imsic_vs_csr_write(c, v)    \
>  do {                                \
>      csr_write(CSR_VSISELECT, (c));  \
> @@ -185,6 +191,19 @@ static void imsic_eix_write(unsigned int ireg, unsigned long val)
>      }
>  }
>  
> +static void imsic_eix_set(unsigned int ireg, unsigned long val)
> +{
> +    switch ( ireg )
> +    {
> +    imsic_switchcase_64(imsic_switchcase_break, IMSIC_EIP0,
> +                        imsic_vs_csr_set, val)
> +    imsic_switchcase_64(imsic_switchcase_break, IMSIC_EIE0,
> +                        imsic_vs_csr_set, val)
> +    default:
> +        ASSERT_UNREACHABLE();
> +    }
> +}
> +
>  unsigned int vcpu_guest_file_id(const struct vcpu *v)
>  {
>      return ACCESS_ONCE(v->arch.vimsic_state->guest_file_id);
> @@ -630,13 +649,13 @@ static void cf_check imsic_vsfile_local_read_clear(void *data)
>      old_vsiselect = csr_read(CSR_VSISELECT);
>      old_hstatus = csr_read(CSR_HSTATUS);
>      new_hstatus = old_hstatus & ~HSTATUS_VGEIN;
> -    new_hstatus |= ((unsigned long)idata->hgei) << HSTATUS_VGEIN_SHIFT;
> +    new_hstatus |= MASK_INSR(idata->hgei, HSTATUS_VGEIN);


>      csr_write(CSR_HSTATUS, new_hstatus);
>  
>      /*
> -     * There is no need to use atomic functions version to store
> -     * values in MRIF because imsic_vsfile_read_clear() is always called
> -     * with pointer to temporary MRIF on stack.
> +     * No atomic accessors are needed to store the values into the MRIF here,
> +     * as imsic_vsfile_read_clear() is always called with a pointer to a
> +     * temporary MRIF on the stack.
>       */


>  
>      mrif->eidelivery = imsic_vs_csr_swap(IMSIC_EIDELIVERY, 0);
> @@ -972,6 +991,49 @@ int __init vimsic_make_domu_dt_node(struct kernel_info *kinfo,
>      return fdt_end_node(fdt);
>  }
>  
> +static void cf_check imsic_vsfile_local_update(void *data)
> +{
> +    unsigned int i;
> +    struct imsic_mrif_eix *eix;
> +    const struct imsic_vsfile_data *idata = data;
> +    struct imsic_mrif *mrif = idata->mrif;
> +    unsigned long new_hstatus, old_hstatus, old_vsiselect;
> +
> +    /* We can only update if we have a HW IMSIC context */
> +    if ( !idata->hgei )
> +        return;
> +
> +    /*
> +     * No atomic accessors are needed to read the values out of the MRIF here,
> +     * as this is always called with a pointer to a temporary MRIF on the
> +     * stack.
> +     */
idata->mrif may point anywhere, so what the comment really states is a
requirement on callers. It also only makes full sense next to KVM, where
a shared SW-file MRIF is accessed atomically; Xen has neither of those
(yet). Either explicitely explain that callers must declare mrif on
their stack or move the comment directly in call sites.

-- 
Baptiste Le Duc <baptiste.le-duc@vates.tech>


^ permalink raw reply	[flat|nested] 244+ messages in thread

* Re: [PATCH v2 32/39] xen/riscv: remap interrupts to new IMSIC VS-file
  2026-09-23 13:34   ` Baptiste Le Duc
@ 2026-09-23 15:45     ` Oleksii Kurochko
  2026-09-23 16:10       ` Baptiste Le Duc
  0 siblings, 1 reply; 244+ messages in thread
From: Oleksii Kurochko @ 2026-09-23 15:45 UTC (permalink / raw)
  To: Baptiste Le Duc
  Cc: xen-devel, Romain Caritey, Zheng Zhang, Alistair Francis,
	Connor Davis, Andrew Cooper, Anthony PERARD, Michal Orzel,
	Jan Beulich, Julien Grall, Roger Pau Monné,
	Stefano Stabellini



On 9/23/26 3:34 PM, Baptiste Le Duc wrote:
>> After new IMSIC VS-file is zeroed-out it is necessary to do G-stage remaping
>> fo new IMSIC VS-file. Also, if any interrupts at an APLIC are forwarded by
>> MSIs to the old interrupt file, reconfigure the APLIC to send them to the
>> new interrupt file.
>>
>> Generally it is needed also to modify the relevant translation tables at
>> all IOMMUs so that MSIs for this virtual interrupt file are now sent to
>> the new physical interrupt file but it is skipped for now there is no IOMMU
>> support for RISC-V.
>>
>> Synchronize with APLIC to ensure that no straggler MSIs will arrive at
>> the old interrupt file by using of aplic_genmsi_barrier().
>>
> 
> 
>> Technically there is no need for read_lock_irqsave() and
>> read_unlock_irqrestore() around reading of ->guest_file_id, as a write
>> cannot happen in parallel: any update to ->guest_file_id for a vCPU
>> will happen either in imsic_migrate_vcpu() itself or before the vCPU
> 
> 
>> first gains control (in continue_new_vcpu()), so there is no concurrent
>> access to it in imsic_migrate_vcpu(). The lock is added here for
>> potential future cases.
>>
>> Keep BUG_ON("unimplemented") placeholder in imsic_migrate_vcpu() to guard
>> against silent incorrect behaviour or unexpected panics in guest VMs until
>> the function is fully implemented.
>>
>> imsic_map_guest_file() and imsic_update_state() are stubs for now and will
>> be introduced later in a separate patch.
> "will be introduced later" would be stale in the future.

I will drop that part from the commit message.

>>
>> Signed-off-by: Oleksii Kurochko <oleksii.kurochko@gmail.com>
> 
> 
>>
>> diff --git a/xen/arch/riscv/imsic.c b/xen/arch/riscv/imsic.c
>> index 5de4594961..5e9f6995e4 100644
>> --- a/xen/arch/riscv/imsic.c
>> +++ b/xen/arch/riscv/imsic.c
>> @@ -27,6 +27,7 @@
>>   #include <xen/xvmalloc.h>
>>   
>>   #include <asm/aia.h>
>> +#include <asm/aplic.h>
>>   #include <asm/imsic.h>
>>   
>>   #define IMSIC_HART_SIZE(guest_bits) (BIT(guest_bits, U) * IMSIC_MMIO_PAGE_SZ)
>> @@ -60,6 +61,12 @@ static unsigned int __ro_after_init guest_num_msis;
>>   #define IMSIC_DISABLE_EITHRESHOLD   1
>>   #define IMSIC_ENABLE_EITHRESHOLD    0
>>   
>> +#define imsic_csr_read(c)           \
>> +({                                  \
>> +    csr_write(CSR_SISELECT, (c));   \
>> +    csr_read(CSR_SIREG);            \
>> +})
>> +
>>   #define imsic_csr_write(c, v)   \
>>   do {                            \
>>       csr_write(CSR_SISELECT, c); \
>> @@ -141,6 +148,11 @@ unsigned int vcpu_guest_file_id(const struct vcpu *v)
>>       return ACCESS_ONCE(v->arch.vimsic_state->guest_file_id);
>>   }
>>   
>> +void imsic_update_state(struct vcpu *v, unsigned int guest_file_id)
>> +{
>> +    BUG_ON("unimplemented\n");
>> +}
>> +
>>   void __init imsic_ids_local_delivery(bool enable)
>>   {
>>       if ( enable )
>> @@ -242,6 +254,15 @@ void imsic_irq_disable(unsigned int irq)
>>       spin_unlock(&imsic_cfg.lock);
>>   }
>>   
>> +static bool imsic_local_is_pending(unsigned int id)
>> +{
>> +    unsigned long isel =
>> +        (id / BITS_PER_LONG) * (BITS_PER_LONG / IMSIC_EIPx_BITS) + IMSIC_EIP0;
>> +    unsigned long bit = BIT(id % BITS_PER_LONG, UL);
>> +
>> +    return !!(imsic_csr_read(isel) & bit);
>> +}
>> +
>>   /* Callers aren't intended to changed imsic_cfg so return const. */
>>   const struct imsic_config *imsic_get_config(void)
>>   {
>> @@ -436,6 +457,11 @@ void cf_check imsic_ctxt_switch_to(struct vcpu *v)
>>       /* Nothing to do */
>>   }
>>   
>> +int imsic_map_guest_file(struct vcpu *v, unsigned int vsfile_id)
>> +{
>> +    return -EOPNOTSUPP;
>> +}
>> +
>>   int cf_check vcpu_imsic_init(struct vcpu *v)
>>   {
>>       struct vimsic_state *imsic_state;
>> @@ -497,6 +523,27 @@ static void imsic_call_on_cpu(unsigned int cpu, void (*func)(void *),
>>           on_selected_cpus(cpumask_of(cpu), func, data, 1);
>>   }
>>   
>> +/*
>> + * Ensure that all the MSIs the APLIC has already generated for the hart this
>> + * runs on have really reached the hart's IMSIC.
>> + *
>> + * The barrier is the one described by the AIA specification in
>> + * "Synchronizing interactions between a hart and the APLIC": ask the APLIC to
>> + * send an MSI to the hart itself and wait until it shows up as pending in the
>> + * hart's own interrupt file. As it says nothing about MSIs on their way to
>> + * any other hart, it has to be executed by the pCPU owning the interrupt file
>> + * the MSIs were being sent to.
>> + */
>> +static void cf_check imsic_aplic_sync(void *data)
> It seems data arg is not used here

It will be renamed to `unused` in the v3 but we still need to have it 
becuase how this function is called through imsic_call_on_cpu().

>> +{
>> +    imsic_local_eix_update(imsic_cfg.sync_id, 1, true, false);
>> +
>> +    aplic_genmsi_barrier();
>> +
>> +    while ( !imsic_local_is_pending(imsic_cfg.sync_id) )
>> +        cpu_relax();
>> +}
>> +
>>   static void cf_check imsic_vsfile_local_clear(void *data)
>>   {
>>       unsigned int i;
>> @@ -837,6 +884,10 @@ void imsic_migrate_vcpu(struct vcpu *v)
>>       struct imsic_vsfile_data vsfile_data = {
>>           .nr_eix = nr_hw_eix,
>>       };
>> +    struct vimsic_state *imsic_state = v->arch.vimsic_state;
>> +    unsigned long flags;
>> +    unsigned int old_vsfile_id;
>> +    unsigned int old_vsfile_cpu;
>>   
>>       /*
>>        * The scheduler can mark a freshly created vCPU's unit as migrated and
>> @@ -848,8 +899,22 @@ void imsic_migrate_vcpu(struct vcpu *v)
>>       if ( v->arch.last_cpu == NR_CPUS )
>>           return;
>>   
>> +    read_lock_irqsave(&imsic_state->vsfile_lock, flags);
>> +    old_vsfile_id = imsic_state->guest_file_id;
>> +    old_vsfile_cpu = imsic_state->vsfile_cpu;
>> +    read_unlock_irqrestore(&imsic_state->vsfile_lock, flags);
>> +
>> +    /*
>> +     * We don't support SW interrupt files at the moment. Bail out before
>> +     * anything is touched, as the old file has no owning pCPU in that case
>> +     * and there is nothing to retarget the producers away from.
>> +     */
>> +    if ( old_vsfile_cpu == NR_CPUS )
>> +        panic("IMSIC SW-file isn't supported\n");
>> +
>>       /*
>>        * At this point, all interrupt producers are still using the old IMSIC
>> +     * VS-file so we first move all interrupt producers to the new IMSIC
>>        * VS-file.
>>        */
>>   
>> @@ -870,5 +935,43 @@ void imsic_migrate_vcpu(struct vcpu *v)
>>       /* Zero-out new IMSIC VS-file */
>>       imsic_call_on_cpu(new_vsfile_cpu, imsic_vsfile_local_clear, &vsfile_data);
>> +    /* Update G-stage mapping for the new IMSIC VS-file */
>> +    if ( imsic_map_guest_file(v, new_vsfile_hgei) )
>> +    {
>> +        domain_crash(v->domain, "Migration to hw interrupt file failed\n");
>> +
>> +        return;
>> +    }
>> +
>> +    imsic_update_state(v, new_vsfile_hgei);
>> +
> This gets rewritten later in the series by "xen/riscv: introduce IMSIC h/w
> interrupt file attaching to vcpu", which moves the clear/map/update sequence
> into imsic_vsfile_acquire() (and adds vgein_release() on the error path).
> 
> Could imsic_vsfile_acquire() be introduced here (or in a prep patch) instead,
> so that the later patch only adds imsic_vsfile_attach()? That would avoid the
> churn.
> 

It could, I just thought that it will be easier to justify necessity of 
it by introduction in "xen/riscv: introduce IMSIC h/w interrupt file 
attaching to vcpu". But I am okay to move it to this patch.

Thanks.

~ Oleksii



^ permalink raw reply	[flat|nested] 244+ messages in thread

* Re: [PATCH v2 32/39] xen/riscv: remap interrupts to new IMSIC VS-file
  2026-09-23 15:25   ` Baptiste Le Duc
@ 2026-09-23 15:48     ` Oleksii Kurochko
  2026-09-23 16:11       ` Baptiste Le Duc
  0 siblings, 1 reply; 244+ messages in thread
From: Oleksii Kurochko @ 2026-09-23 15:48 UTC (permalink / raw)
  To: Baptiste Le Duc, xen-devel
  Cc: Romain Caritey, Zheng Zhang, Alistair Francis, Connor Davis,
	Andrew Cooper, Anthony PERARD, Michal Orzel, Jan Beulich,
	Julien Grall, Roger Pau Monné, Stefano Stabellini



On 9/23/26 5:25 PM, Baptiste Le Duc wrote:
>  > xen/riscv: remap interrupts to new IMSIC VS-file
> 
> Nit: The next patch uses "restore" in its title, we might want to use
> "save" terminology here for symmetry? e.g.:
>      "xen/riscv: save old interrupt file state to memory"

Not only saving old interrupt file state happens here but also moving of 
all interrupts to new IMSIC VS-file. That why remap sounds okay here to 
me. If it is Nit: and no one else are against that I prefer to still
have the commit subject as it is written now.

~ Oleksii


^ permalink raw reply	[flat|nested] 244+ messages in thread

* Re: [PATCH v2 33/39] xen/riscv: dump old interrupt file to memory
  2026-09-23 15:15   ` Baptiste Le Duc
@ 2026-09-23 16:02     ` Oleksii Kurochko
  2026-09-23 16:16       ` Baptiste Le Duc
  0 siblings, 1 reply; 244+ messages in thread
From: Oleksii Kurochko @ 2026-09-23 16:02 UTC (permalink / raw)
  To: Baptiste Le Duc
  Cc: xen-devel, Romain Caritey, Zheng Zhang, Alistair Francis,
	Connor Davis, Andrew Cooper, Anthony PERARD, Michal Orzel,
	Jan Beulich, Julien Grall, Roger Pau Monné,
	Stefano Stabellini



On 9/23/26 5:15 PM, Baptiste Le Duc wrote:
>> At the old interrupt file, dump to memory all the eip and eie arrays).
> Typo `)`

Will drop `)`.

>> After this step is done, the old interrupt file is no longer in use so
>> old intrrupt file VGEIN could be released.
>>
>> Restoring of old interrupt file state will be done in follow-up
>> patch.
>>
> 
> 
>> There are cases where it is needed to specify on which cpu it is
>> necessary to VGEIN should be released so update vgein_release() to
> 
> The sentence miss a verb, here is a proposal:
> ```
> There are cases where the cpu on which the VGEIN is released needs to
> be specified, so update vgein_release() to deal with that.
> ```

I think it could be dropped at all as vgein_release() stub is just 
introduced here and not updated.

> Moreover, could you explain me the cases you are talking about? It's not
> clear by reading the commit message in the first place.

For example, during migration of vCPU, vCPU->processor points to new CPU 
where it will be run but we still have to free VGEIN on the prev. 
->processor.

> 
> 
>> deal with that.
> 
> 
> 
> 
>>
> 
> 
>> Keep BUG_ON("unimplemented") placeholder in imsic_migrate_vcpu() to guard
> 
> 
>> against silent incorrect behaviour or unexpected panics in guest VMs until
>> the function is fully implemented.
>>
> 
> 
>> vgein_release() is stub for now and will be introduced later.
> 
> 
>>
>> Signed-off-by: Oleksii Kurochko <oleksii.kurochko@gmail.com>
>>
>> diff --git a/xen/arch/riscv/aia.c b/xen/arch/riscv/aia.c
>> index 75c82bcfa1..be3901ec0c 100644
>> --- a/xen/arch/riscv/aia.c
>> +++ b/xen/arch/riscv/aia.c
>> @@ -30,3 +30,8 @@ unsigned int vgein_assign(struct vcpu *v)
>>   
>>       return 0;
>>   }
>> +
>> +void vgein_release(struct vcpu *v, unsigned int vgein_id, unsigned int cpu)
>> +{
>> +    BUG_ON("unimplemented\n");
>> +}
>> diff --git a/xen/arch/riscv/imsic.c b/xen/arch/riscv/imsic.c
>> index 5e9f6995e4..3cba58e0c1 100644
>> --- a/xen/arch/riscv/imsic.c
>> +++ b/xen/arch/riscv/imsic.c
>> @@ -56,6 +56,24 @@ static unsigned int __ro_after_init guest_num_msis;
>>    */
>>   #define GUEST_IMSIC_MAX_MSIS 255U
>>   
>> +/*
>> + * The interrupt identities an IMSIC interrupt file provides are 0 (which is
>> + * never valid, but still occupies a bit) up to IMSIC_MAX_ID inclusive, so
>> + * IMSIC_MAX_ID + 1 bits have to be covered.
>> + */
>> +#define IMSIC_MAX_EIX DIV_ROUND_UP(IMSIC_MAX_ID + 1, BITS_PER_TYPE(uint64_t))
> 
> 
>> +
>> +struct imsic_mrif_eix {
>> +    unsigned long eip[BITS_PER_TYPE(uint64_t) / BITS_PER_LONG];
>> +    unsigned long eie[BITS_PER_TYPE(uint64_t) / BITS_PER_LONG];
> 
> 
>> +};
>> +
>> +struct imsic_mrif {
>> +    struct imsic_mrif_eix eix[IMSIC_MAX_EIX];
>> +    unsigned long eithreshold;
>> +    unsigned long eidelivery;
>> +};
>> +
> Maybe I didn't get something but I couldn't find anything in the commit
> message explaining why do we use mrif here.

It is just convenient way to temporary store h/w interrupt file. Also it 
could be used not only for ...

> 
> Moreover, mrif, as described in aia spec (8.3 Memory-resident interrupt
> files), seems to be only usable with IOMMU that Xen doesn't support.

... IOMMU but also to support more guest interrupts file implemented by 
IMSIC (basically what I am calling as software interrupt file). Without 
memory-resident interrupt files, the number of virtual RISC-V harts that 
can directly receive MSIs from devices is limited by the total number of 
guest interrupt files implemented by all IMSICs in the system, because 
all MSIs to RISC-V harts must go through IMSICs. For a single RISC-V 
hart, the number of guest interrupt files is the GEILEN parameter 
defined by the Privileged Architecture, which can be at most 31 for RV32 
and 63 for RV64.

> 
> If you want to have something in memory that could store some interrupt
> file info, we should take another name to not be confusing.

It seems like it is okay to use memory residential interrupt file (mrif) 
here based on KVM's code who are using mrif for the same purpose I 
described above.

In short, MRIF is a joint virtualization technology shared between the 
IOMMU and the hypervisor. The IOMMU uses the MRIF as a memory target to 
land incoming hardware MSIs, while the hypervisor manages these MRIFs in 
RAM as software data structures to support an effectively unlimited 
number of vCPUs that don't currently hold a physical IMSIC guest file slot.

~ Oleksii



^ permalink raw reply	[flat|nested] 244+ messages in thread

* Re: [PATCH v2 35/39] xen/riscv: add basic VGEIN management for AIA guests
  2026-08-27 15:21 ` [PATCH v2 35/39] xen/riscv: add basic VGEIN management for AIA guests Oleksii Kurochko
  2026-09-18 12:38   ` Jan Beulich
@ 2026-09-23 16:06   ` Baptiste Le Duc
  1 sibling, 0 replies; 244+ messages in thread
From: Baptiste Le Duc @ 2026-09-23 16:06 UTC (permalink / raw)
  To: Oleksii Kurochko
  Cc: xen-devel, Romain Caritey, Baptiste Le Duc, Zheng Zhang,
	Alistair Francis, Connor Davis, Andrew Cooper, Anthony PERARD,
	Michal Orzel, Jan Beulich, Julien Grall, Roger Pau Monné,
	Stefano Stabellini

On Thu, 27 Aug 2026 17:21:19 +0200, Oleksii Kurochko <oleksii.kurochko@gmail.com> wrote:
> It was decided to add support for IMSIC from the start instead of having APLIC
> operate in direct delivery mode, as it requires a trap-and-emulation approach,
> which is not optimal from a performance standpoint.
> 
> AIA provides a hardware-accelerated mechanism for delivering external
> interrupts to domains via "guest interrupt files" located in IMSIC.
> A single physical hart can implement multiple such files (up to GEILEN),
> allowing several virtual harts to receive interrupts directly from hardware.
> 
> [...]

Reviewed-by: Baptiste Le Duc <baptiste.le-duc@vates.tech>

-- 
Baptiste Le Duc <baptiste.le-duc@vates.tech>


^ permalink raw reply	[flat|nested] 244+ messages in thread

* Re: [PATCH v2 34/39] xen/riscv: restore register state in the new IMSIC VS-file
  2026-09-23 15:42   ` Baptiste Le Duc
@ 2026-09-23 16:08     ` Oleksii Kurochko
  2026-09-23 16:21       ` Baptiste Le Duc
  0 siblings, 1 reply; 244+ messages in thread
From: Oleksii Kurochko @ 2026-09-23 16:08 UTC (permalink / raw)
  To: Baptiste Le Duc
  Cc: xen-devel, Romain Caritey, Zheng Zhang, Alistair Francis,
	Connor Davis, Andrew Cooper, Anthony PERARD, Michal Orzel,
	Jan Beulich, Julien Grall, Roger Pau Monné,
	Stefano Stabellini



On 9/23/26 5:42 PM, Baptiste Le Duc wrote:
>> At this point, all interrupt producers have been moved to the new
>> IMSIC VS-file so we move register state from the old IMSIC VS/SW-file
>> to the new IMSIC VS-file.
>>
>> As new IMSIC VS-file is ready to be used update vCPU's hstatus with
>> new VGEIN.
>>
>> As the whole migration procedure is finished add some extra explanatory
>> comments.
>>
>> Signed-off-by: Oleksii Kurochko <oleksii.kurochko@gmail.com>
>>
>> diff --git a/xen/arch/riscv/imsic.c b/xen/arch/riscv/imsic.c
>> index 3cba58e0c1..d7b137a1f5 100644
>> --- a/xen/arch/riscv/imsic.c
>> +++ b/xen/arch/riscv/imsic.c
>> @@ -112,6 +112,12 @@ do {                            \
>>       r_;                             \
>>   })
>>   
>> +#define imsic_vs_csr_set(c, v)      \
>> +do {                                \
>> +    csr_write(CSR_VSISELECT, (c));  \
>> +    csr_set(CSR_VSIREG, (v));       \
>> +} while ( 0 )
>> +
>>   #define imsic_vs_csr_write(c, v)    \
>>   do {                                \
>>       csr_write(CSR_VSISELECT, (c));  \
>> @@ -185,6 +191,19 @@ static void imsic_eix_write(unsigned int ireg, unsigned long val)
>>       }
>>   }
>>   
>> +static void imsic_eix_set(unsigned int ireg, unsigned long val)
>> +{
>> +    switch ( ireg )
>> +    {
>> +    imsic_switchcase_64(imsic_switchcase_break, IMSIC_EIP0,
>> +                        imsic_vs_csr_set, val)
>> +    imsic_switchcase_64(imsic_switchcase_break, IMSIC_EIE0,
>> +                        imsic_vs_csr_set, val)
>> +    default:
>> +        ASSERT_UNREACHABLE();
>> +    }
>> +}
>> +
>>   unsigned int vcpu_guest_file_id(const struct vcpu *v)
>>   {
>>       return ACCESS_ONCE(v->arch.vimsic_state->guest_file_id);
>> @@ -630,13 +649,13 @@ static void cf_check imsic_vsfile_local_read_clear(void *data)
>>       old_vsiselect = csr_read(CSR_VSISELECT);
>>       old_hstatus = csr_read(CSR_HSTATUS);
>>       new_hstatus = old_hstatus & ~HSTATUS_VGEIN;
>> -    new_hstatus |= ((unsigned long)idata->hgei) << HSTATUS_VGEIN_SHIFT;
>> +    new_hstatus |= MASK_INSR(idata->hgei, HSTATUS_VGEIN);
> 
> 
>>       csr_write(CSR_HSTATUS, new_hstatus);
>>   
>>       /*
>> -     * There is no need to use atomic functions version to store
>> -     * values in MRIF because imsic_vsfile_read_clear() is always called
>> -     * with pointer to temporary MRIF on stack.
>> +     * No atomic accessors are needed to store the values into the MRIF here,
>> +     * as imsic_vsfile_read_clear() is always called with a pointer to a
>> +     * temporary MRIF on the stack.
>>        */
> 
> 
>>   
>>       mrif->eidelivery = imsic_vs_csr_swap(IMSIC_EIDELIVERY, 0);
>> @@ -972,6 +991,49 @@ int __init vimsic_make_domu_dt_node(struct kernel_info *kinfo,
>>       return fdt_end_node(fdt);
>>   }
>>   
>> +static void cf_check imsic_vsfile_local_update(void *data)
>> +{
>> +    unsigned int i;
>> +    struct imsic_mrif_eix *eix;
>> +    const struct imsic_vsfile_data *idata = data;
>> +    struct imsic_mrif *mrif = idata->mrif;
>> +    unsigned long new_hstatus, old_hstatus, old_vsiselect;
>> +
>> +    /* We can only update if we have a HW IMSIC context */
>> +    if ( !idata->hgei )
>> +        return;
>> +
>> +    /*
>> +     * No atomic accessors are needed to read the values out of the MRIF here,
>> +     * as this is always called with a pointer to a temporary MRIF on the
>> +     * stack.
>> +     */
> idata->mrif may point anywhere, so what the comment really states is a
> requirement on callers. It also only makes full sense next to KVM, where
> a shared SW-file MRIF is accessed atomically; Xen has neither of those
> (yet). Either explicitely explain that callers must declare mrif on
> their stack or move the comment directly in call sites.

It is mentioned in the comment "is always called with a pointer to a 
temporary MRIF on the stack.".

With SW-file MRIF I expect that atomic operations should be used so this 
functions shouldn't just use for them and I assume KVM has something 
different function to work with SW-file MRIF. Anyway just mentioning KVM 
code isn't always useful as it forces me to go and investigate what is 
going on there what I am not fully convinced that it is okay...

~ Oleksii




^ permalink raw reply	[flat|nested] 244+ messages in thread

* Re: [PATCH v2 32/39] xen/riscv: remap interrupts to new IMSIC VS-file
  2026-09-23 15:45     ` Oleksii Kurochko
@ 2026-09-23 16:10       ` Baptiste Le Duc
  0 siblings, 0 replies; 244+ messages in thread
From: Baptiste Le Duc @ 2026-09-23 16:10 UTC (permalink / raw)
  To: Oleksii Kurochko
  Cc: Baptiste Le Duc, xen-devel, Romain Caritey, Zheng Zhang,
	Alistair Francis, Connor Davis, Andrew Cooper, Anthony PERARD,
	Michal Orzel, Jan Beulich, Julien Grall, Roger Pau Monné,
	Stefano Stabellini

On 2026-09-23 17:45 +0200, Oleksii Kurochko wrote:
> 
> 
> On 9/23/26 3:34 PM, Baptiste Le Duc wrote:
> >> After new IMSIC VS-file is zeroed-out it is necessary to do G-stage remaping
> >> fo new IMSIC VS-file. Also, if any interrupts at an APLIC are forwarded by
> >> MSIs to the old interrupt file, reconfigure the APLIC to send them to the
> >> new interrupt file.
> >>
> >> Generally it is needed also to modify the relevant translation tables at
> >> all IOMMUs so that MSIs for this virtual interrupt file are now sent to
> >> the new physical interrupt file but it is skipped for now there is no IOMMU
> >> support for RISC-V.
> >>
> >> Synchronize with APLIC to ensure that no straggler MSIs will arrive at
> >> the old interrupt file by using of aplic_genmsi_barrier().
> >>
> > 
> > 
> >> Technically there is no need for read_lock_irqsave() and
> >> read_unlock_irqrestore() around reading of ->guest_file_id, as a write
> >> cannot happen in parallel: any update to ->guest_file_id for a vCPU
> >> will happen either in imsic_migrate_vcpu() itself or before the vCPU
> > 
> > 
> >> first gains control (in continue_new_vcpu()), so there is no concurrent
> >> access to it in imsic_migrate_vcpu(). The lock is added here for
> >> potential future cases.
> >>
> >> Keep BUG_ON("unimplemented") placeholder in imsic_migrate_vcpu() to guard
> >> against silent incorrect behaviour or unexpected panics in guest VMs until
> >> the function is fully implemented.
> >>
> >> imsic_map_guest_file() and imsic_update_state() are stubs for now and will
> >> be introduced later in a separate patch.
> > "will be introduced later" would be stale in the future.
> 
> I will drop that part from the commit message.
> 
> >>
> >> Signed-off-by: Oleksii Kurochko <oleksii.kurochko@gmail.com>
> > 
> > 
> >>
> >> diff --git a/xen/arch/riscv/imsic.c b/xen/arch/riscv/imsic.c
> >> index 5de4594961..5e9f6995e4 100644
> >> --- a/xen/arch/riscv/imsic.c
> >> +++ b/xen/arch/riscv/imsic.c
> >> @@ -27,6 +27,7 @@
> >>   #include <xen/xvmalloc.h>
> >>   
> >>   #include <asm/aia.h>
> >> +#include <asm/aplic.h>
> >>   #include <asm/imsic.h>
> >>   
> >>   #define IMSIC_HART_SIZE(guest_bits) (BIT(guest_bits, U) * IMSIC_MMIO_PAGE_SZ)
> >> @@ -60,6 +61,12 @@ static unsigned int __ro_after_init guest_num_msis;
> >>   #define IMSIC_DISABLE_EITHRESHOLD   1
> >>   #define IMSIC_ENABLE_EITHRESHOLD    0
> >>   
> >> +#define imsic_csr_read(c)           \
> >> +({                                  \
> >> +    csr_write(CSR_SISELECT, (c));   \
> >> +    csr_read(CSR_SIREG);            \
> >> +})
> >> +
> >>   #define imsic_csr_write(c, v)   \
> >>   do {                            \
> >>       csr_write(CSR_SISELECT, c); \
> >> @@ -141,6 +148,11 @@ unsigned int vcpu_guest_file_id(const struct vcpu *v)
> >>       return ACCESS_ONCE(v->arch.vimsic_state->guest_file_id);
> >>   }
> >>   
> >> +void imsic_update_state(struct vcpu *v, unsigned int guest_file_id)
> >> +{
> >> +    BUG_ON("unimplemented\n");
> >> +}
> >> +
> >>   void __init imsic_ids_local_delivery(bool enable)
> >>   {
> >>       if ( enable )
> >> @@ -242,6 +254,15 @@ void imsic_irq_disable(unsigned int irq)
> >>       spin_unlock(&imsic_cfg.lock);
> >>   }
> >>   
> >> +static bool imsic_local_is_pending(unsigned int id)
> >> +{
> >> +    unsigned long isel =
> >> +        (id / BITS_PER_LONG) * (BITS_PER_LONG / IMSIC_EIPx_BITS) + IMSIC_EIP0;
> >> +    unsigned long bit = BIT(id % BITS_PER_LONG, UL);
> >> +
> >> +    return !!(imsic_csr_read(isel) & bit);
> >> +}
> >> +
> >>   /* Callers aren't intended to changed imsic_cfg so return const. */
> >>   const struct imsic_config *imsic_get_config(void)
> >>   {
> >> @@ -436,6 +457,11 @@ void cf_check imsic_ctxt_switch_to(struct vcpu *v)
> >>       /* Nothing to do */
> >>   }
> >>   
> >> +int imsic_map_guest_file(struct vcpu *v, unsigned int vsfile_id)
> >> +{
> >> +    return -EOPNOTSUPP;
> >> +}
> >> +
> >>   int cf_check vcpu_imsic_init(struct vcpu *v)
> >>   {
> >>       struct vimsic_state *imsic_state;
> >> @@ -497,6 +523,27 @@ static void imsic_call_on_cpu(unsigned int cpu, void (*func)(void *),
> >>           on_selected_cpus(cpumask_of(cpu), func, data, 1);
> >>   }
> >>   
> >> +/*
> >> + * Ensure that all the MSIs the APLIC has already generated for the hart this
> >> + * runs on have really reached the hart's IMSIC.
> >> + *
> >> + * The barrier is the one described by the AIA specification in
> >> + * "Synchronizing interactions between a hart and the APLIC": ask the APLIC to
> >> + * send an MSI to the hart itself and wait until it shows up as pending in the
> >> + * hart's own interrupt file. As it says nothing about MSIs on their way to
> >> + * any other hart, it has to be executed by the pCPU owning the interrupt file
> >> + * the MSIs were being sent to.
> >> + */
> >> +static void cf_check imsic_aplic_sync(void *data)
> > It seems data arg is not used here
> 
> It will be renamed to `unused` in the v3 but we still need to have it 
> becuase how this function is called through imsic_call_on_cpu().
> 
> >> +{
> >> +    imsic_local_eix_update(imsic_cfg.sync_id, 1, true, false);
> >> +
> >> +    aplic_genmsi_barrier();
> >> +
> >> +    while ( !imsic_local_is_pending(imsic_cfg.sync_id) )
> >> +        cpu_relax();
> >> +}
> >> +
> >>   static void cf_check imsic_vsfile_local_clear(void *data)
> >>   {
> >>       unsigned int i;
> >> @@ -837,6 +884,10 @@ void imsic_migrate_vcpu(struct vcpu *v)
> >>       struct imsic_vsfile_data vsfile_data = {
> >>           .nr_eix = nr_hw_eix,
> >>       };
> >> +    struct vimsic_state *imsic_state = v->arch.vimsic_state;
> >> +    unsigned long flags;
> >> +    unsigned int old_vsfile_id;
> >> +    unsigned int old_vsfile_cpu;
> >>   
> >>       /*
> >>        * The scheduler can mark a freshly created vCPU's unit as migrated and
> >> @@ -848,8 +899,22 @@ void imsic_migrate_vcpu(struct vcpu *v)
> >>       if ( v->arch.last_cpu == NR_CPUS )
> >>           return;
> >>   
> >> +    read_lock_irqsave(&imsic_state->vsfile_lock, flags);
> >> +    old_vsfile_id = imsic_state->guest_file_id;
> >> +    old_vsfile_cpu = imsic_state->vsfile_cpu;
> >> +    read_unlock_irqrestore(&imsic_state->vsfile_lock, flags);
> >> +
> >> +    /*
> >> +     * We don't support SW interrupt files at the moment. Bail out before
> >> +     * anything is touched, as the old file has no owning pCPU in that case
> >> +     * and there is nothing to retarget the producers away from.
> >> +     */
> >> +    if ( old_vsfile_cpu == NR_CPUS )
> >> +        panic("IMSIC SW-file isn't supported\n");
> >> +
> >>       /*
> >>        * At this point, all interrupt producers are still using the old IMSIC
> >> +     * VS-file so we first move all interrupt producers to the new IMSIC
> >>        * VS-file.
> >>        */
> >>   
> >> @@ -870,5 +935,43 @@ void imsic_migrate_vcpu(struct vcpu *v)
> >>       /* Zero-out new IMSIC VS-file */
> >>       imsic_call_on_cpu(new_vsfile_cpu, imsic_vsfile_local_clear, &vsfile_data);
> >> +    /* Update G-stage mapping for the new IMSIC VS-file */
> >> +    if ( imsic_map_guest_file(v, new_vsfile_hgei) )
> >> +    {
> >> +        domain_crash(v->domain, "Migration to hw interrupt file failed\n");
> >> +
> >> +        return;
> >> +    }
> >> +
> >> +    imsic_update_state(v, new_vsfile_hgei);
> >> +
> > This gets rewritten later in the series by "xen/riscv: introduce IMSIC h/w
> > interrupt file attaching to vcpu", which moves the clear/map/update sequence
> > into imsic_vsfile_acquire() (and adds vgein_release() on the error path).
> > 
> > Could imsic_vsfile_acquire() be introduced here (or in a prep patch) instead,
> > so that the later patch only adds imsic_vsfile_attach()? That would avoid the
> > churn.
> > 
> 
> It could, I just thought that it will be easier to justify necessity of 
> it by introduction in "xen/riscv: introduce IMSIC h/w interrupt file 
> attaching to vcpu". But I am okay to move it to this patch.
Hum, finally I don't know if it's the good solution. First time I'm
saw this, so I was suprised, but if it's common practice, let's keep
it here for clarity.
> Thanks.
> 
> ~ Oleksii
> 
> 
> 




^ permalink raw reply	[flat|nested] 244+ messages in thread

* Re: [PATCH v2 32/39] xen/riscv: remap interrupts to new IMSIC VS-file
  2026-09-23 15:48     ` Oleksii Kurochko
@ 2026-09-23 16:11       ` Baptiste Le Duc
  0 siblings, 0 replies; 244+ messages in thread
From: Baptiste Le Duc @ 2026-09-23 16:11 UTC (permalink / raw)
  To: Oleksii Kurochko
  Cc: Baptiste Le Duc, xen-devel, Romain Caritey, Zheng Zhang,
	Alistair Francis, Connor Davis, Andrew Cooper, Anthony PERARD,
	Michal Orzel, Jan Beulich, Julien Grall, Roger Pau Monné,
	Stefano Stabellini

On 2026-09-23 17:48 +0200, Oleksii Kurochko wrote:
> 
> 
> On 9/23/26 5:25 PM, Baptiste Le Duc wrote:
> >  > xen/riscv: remap interrupts to new IMSIC VS-file
> > 
> > Nit: The next patch uses "restore" in its title, we might want to use
> > "save" terminology here for symmetry? e.g.:
> >      "xen/riscv: save old interrupt file state to memory"
> 
> Not only saving old interrupt file state happens here but also moving of 
> all interrupts to new IMSIC VS-file. That why remap sounds okay here to 
> me. If it is Nit: and no one else are against that I prefer to still
> have the commit subject as it is written now.
Ok lets keep the original.
> 
> ~ Oleksii
> 
> 




^ permalink raw reply	[flat|nested] 244+ messages in thread

* Re: [PATCH v2 33/39] xen/riscv: dump old interrupt file to memory
  2026-09-23 16:02     ` Oleksii Kurochko
@ 2026-09-23 16:16       ` Baptiste Le Duc
  2026-09-23 18:30         ` Oleksii Kurochko
  0 siblings, 1 reply; 244+ messages in thread
From: Baptiste Le Duc @ 2026-09-23 16:16 UTC (permalink / raw)
  To: Oleksii Kurochko
  Cc: Baptiste Le Duc, xen-devel, Romain Caritey, Zheng Zhang,
	Alistair Francis, Connor Davis, Andrew Cooper, Anthony PERARD,
	Michal Orzel, Jan Beulich, Julien Grall, Roger Pau Monné,
	Stefano Stabellini

On 2026-09-23 18:02 +0200, Oleksii Kurochko wrote:
> 
> 
> On 9/23/26 5:15 PM, Baptiste Le Duc wrote:
> >> At the old interrupt file, dump to memory all the eip and eie arrays).
> > Typo `)`
> 
> Will drop `)`.
> 
> >> After this step is done, the old interrupt file is no longer in use so
> >> old intrrupt file VGEIN could be released.
> >>
> >> Restoring of old interrupt file state will be done in follow-up
> >> patch.
> >>
> > 
> > 
> >> There are cases where it is needed to specify on which cpu it is
> >> necessary to VGEIN should be released so update vgein_release() to
> > 
> > The sentence miss a verb, here is a proposal:
> > ```
> > There are cases where the cpu on which the VGEIN is released needs to
> > be specified, so update vgein_release() to deal with that.
> > ```
> 
> I think it could be dropped at all as vgein_release() stub is just 
> introduced here and not updated.
> 
> > Moreover, could you explain me the cases you are talking about? It's not
> > clear by reading the commit message in the first place.
> 
> For example, during migration of vCPU, vCPU->processor points to new CPU 
> where it will be run but we still have to free VGEIN on the prev. 
> ->processor.
> 
> > 
> > 
> >> deal with that.
> > 
> > 
> > 
> > 
> >>
> > 
> > 
> >> Keep BUG_ON("unimplemented") placeholder in imsic_migrate_vcpu() to guard
> > 
> > 
> >> against silent incorrect behaviour or unexpected panics in guest VMs until
> >> the function is fully implemented.
> >>
> > 
> > 
> >> vgein_release() is stub for now and will be introduced later.
> > 
> > 
> >>
> >> Signed-off-by: Oleksii Kurochko <oleksii.kurochko@gmail.com>
> >>
> >> diff --git a/xen/arch/riscv/aia.c b/xen/arch/riscv/aia.c
> >> index 75c82bcfa1..be3901ec0c 100644
> >> --- a/xen/arch/riscv/aia.c
> >> +++ b/xen/arch/riscv/aia.c
> >> @@ -30,3 +30,8 @@ unsigned int vgein_assign(struct vcpu *v)
> >>   
> >>       return 0;
> >>   }
> >> +
> >> +void vgein_release(struct vcpu *v, unsigned int vgein_id, unsigned int cpu)
> >> +{
> >> +    BUG_ON("unimplemented\n");
> >> +}
> >> diff --git a/xen/arch/riscv/imsic.c b/xen/arch/riscv/imsic.c
> >> index 5e9f6995e4..3cba58e0c1 100644
> >> --- a/xen/arch/riscv/imsic.c
> >> +++ b/xen/arch/riscv/imsic.c
> >> @@ -56,6 +56,24 @@ static unsigned int __ro_after_init guest_num_msis;
> >>    */
> >>   #define GUEST_IMSIC_MAX_MSIS 255U
> >>   
> >> +/*
> >> + * The interrupt identities an IMSIC interrupt file provides are 0 (which is
> >> + * never valid, but still occupies a bit) up to IMSIC_MAX_ID inclusive, so
> >> + * IMSIC_MAX_ID + 1 bits have to be covered.
> >> + */
> >> +#define IMSIC_MAX_EIX DIV_ROUND_UP(IMSIC_MAX_ID + 1, BITS_PER_TYPE(uint64_t))
> > 
> > 
> >> +
> >> +struct imsic_mrif_eix {
> >> +    unsigned long eip[BITS_PER_TYPE(uint64_t) / BITS_PER_LONG];
> >> +    unsigned long eie[BITS_PER_TYPE(uint64_t) / BITS_PER_LONG];
> > 
> > 
> >> +};
> >> +
> >> +struct imsic_mrif {
> >> +    struct imsic_mrif_eix eix[IMSIC_MAX_EIX];
> >> +    unsigned long eithreshold;
> >> +    unsigned long eidelivery;
> >> +};
> >> +
> > Maybe I didn't get something but I couldn't find anything in the commit
> > message explaining why do we use mrif here.
> 
> It is just convenient way to temporary store h/w interrupt file. Also it 
> could be used not only for ...
> 
> > 
> > Moreover, mrif, as described in aia spec (8.3 Memory-resident interrupt
> > files), seems to be only usable with IOMMU that Xen doesn't support.
> 
> ... IOMMU but also to support more guest interrupts file implemented by 
> IMSIC (basically what I am calling as software interrupt file). Without 
> memory-resident interrupt files, the number of virtual RISC-V harts that 
> can directly receive MSIs from devices is limited by the total number of 
> guest interrupt files implemented by all IMSICs in the system, because 
> all MSIs to RISC-V harts must go through IMSICs. For a single RISC-V 
> hart, the number of guest interrupt files is the GEILEN parameter 
> defined by the Privileged Architecture, which can be at most 31 for RV32 
> and 63 for RV64.
> 
> > 
> > If you want to have something in memory that could store some interrupt
> > file info, we should take another name to not be confusing.
> 
> It seems like it is okay to use memory residential interrupt file (mrif) 
> here based on KVM's code who are using mrif for the same purpose I 
> described above.
> 
> In short, MRIF is a joint virtualization technology shared between the 
> IOMMU and the hypervisor. The IOMMU uses the MRIF as a memory target to 
> land incoming hardware MSIs, while the hypervisor manages these MRIFs in 
> RAM as software data structures to support an effectively unlimited 
> number of vCPUs that don't currently hold a physical IMSIC guest file slot.
> 
Yes, I read the spec to understand but here, mrif doesn't catch the MSIs
right? so it's not exactly the behaviour mentioned or you have in mind
to add full support when IOMMU will be supported?

> ~ Oleksii
> 
> 
> 




^ permalink raw reply	[flat|nested] 244+ messages in thread

* Re: [PATCH v2 34/39] xen/riscv: restore register state in the new IMSIC VS-file
  2026-09-23 16:08     ` Oleksii Kurochko
@ 2026-09-23 16:21       ` Baptiste Le Duc
  0 siblings, 0 replies; 244+ messages in thread
From: Baptiste Le Duc @ 2026-09-23 16:21 UTC (permalink / raw)
  To: Oleksii Kurochko
  Cc: Baptiste Le Duc, xen-devel, Romain Caritey, Zheng Zhang,
	Alistair Francis, Connor Davis, Andrew Cooper, Anthony PERARD,
	Michal Orzel, Jan Beulich, Julien Grall, Roger Pau Monné,
	Stefano Stabellini

On 2026-09-23 18:08 +0200, Oleksii Kurochko wrote:
> 
> 
> On 9/23/26 5:42 PM, Baptiste Le Duc wrote:
> >> At this point, all interrupt producers have been moved to the new
> >> IMSIC VS-file so we move register state from the old IMSIC VS/SW-file
> >> to the new IMSIC VS-file.
> >>
> >> As new IMSIC VS-file is ready to be used update vCPU's hstatus with
> >> new VGEIN.
> >>
> >> As the whole migration procedure is finished add some extra explanatory
> >> comments.
> >>
> >> Signed-off-by: Oleksii Kurochko <oleksii.kurochko@gmail.com>
> >>
> >> diff --git a/xen/arch/riscv/imsic.c b/xen/arch/riscv/imsic.c
> >> index 3cba58e0c1..d7b137a1f5 100644
> >> --- a/xen/arch/riscv/imsic.c
> >> +++ b/xen/arch/riscv/imsic.c
> >> @@ -112,6 +112,12 @@ do {                            \
> >>       r_;                             \
> >>   })
> >>   
> >> +#define imsic_vs_csr_set(c, v)      \
> >> +do {                                \
> >> +    csr_write(CSR_VSISELECT, (c));  \
> >> +    csr_set(CSR_VSIREG, (v));       \
> >> +} while ( 0 )
> >> +
> >>   #define imsic_vs_csr_write(c, v)    \
> >>   do {                                \
> >>       csr_write(CSR_VSISELECT, (c));  \
> >> @@ -185,6 +191,19 @@ static void imsic_eix_write(unsigned int ireg, unsigned long val)
> >>       }
> >>   }
> >>   
> >> +static void imsic_eix_set(unsigned int ireg, unsigned long val)
> >> +{
> >> +    switch ( ireg )
> >> +    {
> >> +    imsic_switchcase_64(imsic_switchcase_break, IMSIC_EIP0,
> >> +                        imsic_vs_csr_set, val)
> >> +    imsic_switchcase_64(imsic_switchcase_break, IMSIC_EIE0,
> >> +                        imsic_vs_csr_set, val)
> >> +    default:
> >> +        ASSERT_UNREACHABLE();
> >> +    }
> >> +}
> >> +
> >>   unsigned int vcpu_guest_file_id(const struct vcpu *v)
> >>   {
> >>       return ACCESS_ONCE(v->arch.vimsic_state->guest_file_id);
> >> @@ -630,13 +649,13 @@ static void cf_check imsic_vsfile_local_read_clear(void *data)
> >>       old_vsiselect = csr_read(CSR_VSISELECT);
> >>       old_hstatus = csr_read(CSR_HSTATUS);
> >>       new_hstatus = old_hstatus & ~HSTATUS_VGEIN;
> >> -    new_hstatus |= ((unsigned long)idata->hgei) << HSTATUS_VGEIN_SHIFT;
> >> +    new_hstatus |= MASK_INSR(idata->hgei, HSTATUS_VGEIN);
> > 
> > 
> >>       csr_write(CSR_HSTATUS, new_hstatus);
> >>   
> >>       /*
> >> -     * There is no need to use atomic functions version to store
> >> -     * values in MRIF because imsic_vsfile_read_clear() is always called
> >> -     * with pointer to temporary MRIF on stack.
> >> +     * No atomic accessors are needed to store the values into the MRIF here,
> >> +     * as imsic_vsfile_read_clear() is always called with a pointer to a
> >> +     * temporary MRIF on the stack.
> >>        */
> > 
> > 
> >>   
> >>       mrif->eidelivery = imsic_vs_csr_swap(IMSIC_EIDELIVERY, 0);
> >> @@ -972,6 +991,49 @@ int __init vimsic_make_domu_dt_node(struct kernel_info *kinfo,
> >>       return fdt_end_node(fdt);
> >>   }
> >>   
> >> +static void cf_check imsic_vsfile_local_update(void *data)
> >> +{
> >> +    unsigned int i;
> >> +    struct imsic_mrif_eix *eix;
> >> +    const struct imsic_vsfile_data *idata = data;
> >> +    struct imsic_mrif *mrif = idata->mrif;
> >> +    unsigned long new_hstatus, old_hstatus, old_vsiselect;
> >> +
> >> +    /* We can only update if we have a HW IMSIC context */
> >> +    if ( !idata->hgei )
> >> +        return;
> >> +
> >> +    /*
> >> +     * No atomic accessors are needed to read the values out of the MRIF here,
> >> +     * as this is always called with a pointer to a temporary MRIF on the
> >> +     * stack.
> >> +     */
> > idata->mrif may point anywhere, so what the comment really states is a
> > requirement on callers. It also only makes full sense next to KVM, where
> > a shared SW-file MRIF is accessed atomically; Xen has neither of those
> > (yet). Either explicitely explain that callers must declare mrif on
> > their stack or move the comment directly in call sites.
> 
> It is mentioned in the comment "is always called with a pointer to a 
> temporary MRIF on the stack.".
Oh okay, I think I misunderstood the comment at the first place then. I
thought you wanted to say the pointer itself is on the stack, not MRIF.
Now it's more clear.

> 
> With SW-file MRIF I expect that atomic operations should be used so this 
> functions shouldn't just use for them and I assume KVM has something 
> different function to work with SW-file MRIF. Anyway just mentioning KVM 
> code isn't always useful as it forces me to go and investigate what is 
> going on there what I am not fully convinced that it is okay...
Yes sorry, no needs for more investigations here.
> 
> ~ Oleksii
> 
> 
> 
> 




^ permalink raw reply	[flat|nested] 244+ messages in thread

* Re: [PATCH v2 33/39] xen/riscv: dump old interrupt file to memory
  2026-09-23 16:16       ` Baptiste Le Duc
@ 2026-09-23 18:30         ` Oleksii Kurochko
  0 siblings, 0 replies; 244+ messages in thread
From: Oleksii Kurochko @ 2026-09-23 18:30 UTC (permalink / raw)
  To: Baptiste Le Duc
  Cc: xen-devel, Romain Caritey, Zheng Zhang, Alistair Francis,
	Connor Davis, Andrew Cooper, Anthony PERARD, Michal Orzel,
	Jan Beulich, Julien Grall, Roger Pau Monné,
	Stefano Stabellini



On 9/23/26 6:16 PM, Baptiste Le Duc wrote:
> On 2026-09-23 18:02 +0200, Oleksii Kurochko wrote:
>>
>>
>> On 9/23/26 5:15 PM, Baptiste Le Duc wrote:
>>>> At the old interrupt file, dump to memory all the eip and eie arrays).
>>> Typo `)`
>>
>> Will drop `)`.
>>
>>>> After this step is done, the old interrupt file is no longer in use so
>>>> old intrrupt file VGEIN could be released.
>>>>
>>>> Restoring of old interrupt file state will be done in follow-up
>>>> patch.
>>>>
>>>
>>>
>>>> There are cases where it is needed to specify on which cpu it is
>>>> necessary to VGEIN should be released so update vgein_release() to
>>>
>>> The sentence miss a verb, here is a proposal:
>>> ```
>>> There are cases where the cpu on which the VGEIN is released needs to
>>> be specified, so update vgein_release() to deal with that.
>>> ```
>>
>> I think it could be dropped at all as vgein_release() stub is just
>> introduced here and not updated.
>>
>>> Moreover, could you explain me the cases you are talking about? It's not
>>> clear by reading the commit message in the first place.
>>
>> For example, during migration of vCPU, vCPU->processor points to new CPU
>> where it will be run but we still have to free VGEIN on the prev.
>> ->processor.
>>
>>>
>>>
>>>> deal with that.
>>>
>>>
>>>
>>>
>>>>
>>>
>>>
>>>> Keep BUG_ON("unimplemented") placeholder in imsic_migrate_vcpu() to guard
>>>
>>>
>>>> against silent incorrect behaviour or unexpected panics in guest VMs until
>>>> the function is fully implemented.
>>>>
>>>
>>>
>>>> vgein_release() is stub for now and will be introduced later.
>>>
>>>
>>>>
>>>> Signed-off-by: Oleksii Kurochko <oleksii.kurochko@gmail.com>
>>>>
>>>> diff --git a/xen/arch/riscv/aia.c b/xen/arch/riscv/aia.c
>>>> index 75c82bcfa1..be3901ec0c 100644
>>>> --- a/xen/arch/riscv/aia.c
>>>> +++ b/xen/arch/riscv/aia.c
>>>> @@ -30,3 +30,8 @@ unsigned int vgein_assign(struct vcpu *v)
>>>>    
>>>>        return 0;
>>>>    }
>>>> +
>>>> +void vgein_release(struct vcpu *v, unsigned int vgein_id, unsigned int cpu)
>>>> +{
>>>> +    BUG_ON("unimplemented\n");
>>>> +}
>>>> diff --git a/xen/arch/riscv/imsic.c b/xen/arch/riscv/imsic.c
>>>> index 5e9f6995e4..3cba58e0c1 100644
>>>> --- a/xen/arch/riscv/imsic.c
>>>> +++ b/xen/arch/riscv/imsic.c
>>>> @@ -56,6 +56,24 @@ static unsigned int __ro_after_init guest_num_msis;
>>>>     */
>>>>    #define GUEST_IMSIC_MAX_MSIS 255U
>>>>    
>>>> +/*
>>>> + * The interrupt identities an IMSIC interrupt file provides are 0 (which is
>>>> + * never valid, but still occupies a bit) up to IMSIC_MAX_ID inclusive, so
>>>> + * IMSIC_MAX_ID + 1 bits have to be covered.
>>>> + */
>>>> +#define IMSIC_MAX_EIX DIV_ROUND_UP(IMSIC_MAX_ID + 1, BITS_PER_TYPE(uint64_t))
>>>
>>>
>>>> +
>>>> +struct imsic_mrif_eix {
>>>> +    unsigned long eip[BITS_PER_TYPE(uint64_t) / BITS_PER_LONG];
>>>> +    unsigned long eie[BITS_PER_TYPE(uint64_t) / BITS_PER_LONG];
>>>
>>>
>>>> +};
>>>> +
>>>> +struct imsic_mrif {
>>>> +    struct imsic_mrif_eix eix[IMSIC_MAX_EIX];
>>>> +    unsigned long eithreshold;
>>>> +    unsigned long eidelivery;
>>>> +};
>>>> +
>>> Maybe I didn't get something but I couldn't find anything in the commit
>>> message explaining why do we use mrif here.
>>
>> It is just convenient way to temporary store h/w interrupt file. Also it
>> could be used not only for ...
>>
>>>
>>> Moreover, mrif, as described in aia spec (8.3 Memory-resident interrupt
>>> files), seems to be only usable with IOMMU that Xen doesn't support.
>>
>> ... IOMMU but also to support more guest interrupts file implemented by
>> IMSIC (basically what I am calling as software interrupt file). Without
>> memory-resident interrupt files, the number of virtual RISC-V harts that
>> can directly receive MSIs from devices is limited by the total number of
>> guest interrupt files implemented by all IMSICs in the system, because
>> all MSIs to RISC-V harts must go through IMSICs. For a single RISC-V
>> hart, the number of guest interrupt files is the GEILEN parameter
>> defined by the Privileged Architecture, which can be at most 31 for RV32
>> and 63 for RV64.
>>
>>>
>>> If you want to have something in memory that could store some interrupt
>>> file info, we should take another name to not be confusing.
>>
>> It seems like it is okay to use memory residential interrupt file (mrif)
>> here based on KVM's code who are using mrif for the same purpose I
>> described above.
>>
>> In short, MRIF is a joint virtualization technology shared between the
>> IOMMU and the hypervisor. The IOMMU uses the MRIF as a memory target to
>> land incoming hardware MSIs, while the hypervisor manages these MRIFs in
>> RAM as software data structures to support an effectively unlimited
>> number of vCPUs that don't currently hold a physical IMSIC guest file slot.
>>
> Yes, I read the spec to understand but here, mrif doesn't catch the MSIs
> right?

Yes, because right now only h/w if(s) are supported. For now it is just 
a storage where we are saving temporary h/w interrupt file (which has 
the same structure as MRIF).
When s/w if(s) will be supported we will need to store somewhere an 
interrupt file in RAM and considering that the structure of h/w if == 
s/w if and basically == mrif thereby mrif is a good candidate. Even 
more, ...

> so it's not exactly the behaviour mentioned or you have in mind
> to add full support when IOMMU will be supported?
... IOMMU points to the the same structure so again h/w  if == s/w if == 
mrif from organization point of you.

And answering your question we have in mind to support IOMMU for IMSIC 
purposes and then we still will need mrif structure.

~ Oleksii




^ permalink raw reply	[flat|nested] 244+ messages in thread

end of thread, other threads:[~2026-09-23 18:31 UTC | newest]

Thread overview: 244+ messages (download: mbox.gz follow: Atom feed
-- links below jump to the message on this page --
2026-08-27 15:20 [PATCH v2 00/39] [RISC-V] virtual interrupt controller (vAPLIC/vIMSIC) support Oleksii Kurochko
2026-08-27 15:20 ` [PATCH v2 01/39] xen/riscv: drop pregs from struct cpu_user_regs Oleksii Kurochko
2026-08-31 12:48   ` Baptiste Le Duc
2026-09-01  6:58     ` Jan Beulich
2026-08-27 15:20 ` [PATCH v2 02/39] xen/riscv: drop bug.h's duplicate instruction length helpers Oleksii Kurochko
2026-08-31 12:48   ` Baptiste Le Duc
2026-09-01  7:01   ` Jan Beulich
2026-09-02 10:48     ` Oleksii Kurochko
2026-09-02 13:02       ` Jan Beulich
2026-09-02 13:45         ` Oleksii Kurochko
2026-09-02 14:27           ` Jan Beulich
2026-08-27 15:20 ` [PATCH v2 03/39] xen/riscv: set the guest's XLEN explicitly in hstatus.VSXL Oleksii Kurochko
2026-08-31 12:48   ` Baptiste Le Duc
2026-09-01  7:03     ` Jan Beulich
2026-09-01  8:40     ` Oleksii Kurochko
2026-09-01 15:16     ` Jan Beulich
2026-09-01 15:20   ` Jan Beulich
2026-09-02 11:42     ` Oleksii Kurochko
2026-09-02 13:07       ` Jan Beulich
2026-09-02 13:29         ` Oleksii Kurochko
2026-09-02 14:31           ` Jan Beulich
2026-09-02 15:17             ` Oleksii Kurochko
2026-09-02 15:56               ` Oleksii Kurochko
2026-09-02 17:45                 ` Oleksii Kurochko
2026-08-27 15:20 ` [PATCH v2 04/39] xen/riscv: introduce csr_read64() Oleksii Kurochko
2026-08-27 15:36   ` Andrew Cooper
2026-08-31 12:42     ` Oleksii Kurochko
2026-09-01  7:07       ` Jan Beulich
2026-08-27 15:20 ` [PATCH v2 05/39] xen/riscv: request a G-stage flush on vmenter when VMIDs are disabled Oleksii Kurochko
2026-08-31 12:48   ` Baptiste Le Duc
2026-09-01  8:43     ` Oleksii Kurochko
2026-08-27 15:20 ` [PATCH v2 06/39] xen/riscv: use UINT64_MAX to disable the VS-timer Oleksii Kurochko
2026-08-31 12:48   ` Baptiste Le Duc
2026-09-01  7:12   ` Jan Beulich
2026-09-01  8:47     ` Oleksii Kurochko
2026-08-27 15:20 ` [PATCH v2 07/39] xen/riscv: add missing APLIC register offsets, masks to asm/aplic.h Oleksii Kurochko
2026-09-01 15:36   ` Baptiste Le Duc
2026-09-01 15:53     ` Jan Beulich
2026-09-02 13:22       ` Jan Beulich
2026-09-02 13:52         ` Oleksii Kurochko
2026-08-27 15:20 ` [PATCH v2 08/39] xen/riscv: introduce device-agnostic MMIO emulation dispatch Oleksii Kurochko
2026-09-01 15:36   ` Baptiste Le Duc
2026-09-03 10:28     ` Oleksii Kurochko
2026-09-09 13:24   ` Jan Beulich
2026-09-09 14:04     ` Oleksii Kurochko
2026-09-09 14:32       ` Jan Beulich
2026-08-27 15:20 ` [PATCH v2 09/39] xen/riscv: implement virtual APLIC MMIO emulation Oleksii Kurochko
2026-09-02 11:51   ` Baptiste Le Duc
2026-09-04 11:58     ` Oleksii Kurochko
2026-09-04 12:03       ` Jan Beulich
2026-09-02 12:31   ` Baptiste Le Duc
2026-09-04 14:02     ` Oleksii Kurochko
2026-09-09 14:26   ` Jan Beulich
2026-09-10 10:37     ` Oleksii Kurochko
2026-09-10 11:14       ` Jan Beulich
2026-09-10 14:24         ` Oleksii Kurochko
2026-09-12  8:50           ` SeungJu Cheon
2026-09-22  8:50             ` Oleksii Kurochko
2026-08-27 15:20 ` [PATCH v2 10/39] xen/riscv: build the target hart index via aplic_hart_field() Oleksii Kurochko
2026-09-04  8:26   ` Baptiste Le Duc
2026-09-04 14:28     ` Oleksii Kurochko
2026-09-09 14:51     ` Jan Beulich
2026-09-09 14:52   ` Jan Beulich
2026-09-10 10:59     ` Oleksii Kurochko
2026-09-10 11:23       ` Jan Beulich
2026-09-10 12:44         ` Oleksii Kurochko
2026-09-10 12:57           ` Jan Beulich
2026-09-11  9:47             ` Oleksii Kurochko
2026-09-10 11:23       ` Jan Beulich
2026-08-27 15:20 ` [PATCH v2 11/39] xen/riscv: add helper to check APLIC MSI mode Oleksii Kurochko
2026-09-04  8:26   ` Baptiste Le Duc
2026-09-09 14:53     ` Jan Beulich
2026-08-27 15:20 ` [PATCH v2 12/39] xen/riscv: implement vCPU context switching Oleksii Kurochko
2026-09-02 14:42   ` Oleksii Kurochko
2026-09-04  8:26   ` Baptiste Le Duc
2026-09-04  8:33     ` Jan Beulich
2026-09-04  9:54       ` Baptiste Le Duc
2026-09-04 14:55     ` Oleksii Kurochko
2026-09-07  8:17       ` Jan Beulich
2026-09-08  9:06         ` Oleksii Kurochko
2026-09-05  7:25   ` Oleksii Kurochko
2026-09-10 13:29   ` Jan Beulich
2026-09-11 10:43     ` Oleksii Kurochko
2026-08-27 15:20 ` [PATCH v2 13/39] xen/riscv: save and restore AIA state on vCPU context switch Oleksii Kurochko
2026-09-04  9:52   ` Baptiste Le Duc
2026-09-04 16:40     ` Oleksii Kurochko
2026-08-27 15:20 ` [PATCH v2 14/39] xen/riscv: introduce vintc_ctxt_switch_{from,to}() Oleksii Kurochko
2026-09-04 11:25   ` Baptiste Le Duc
2026-09-04 16:54     ` Oleksii Kurochko
2026-09-10 14:54   ` Jan Beulich
2026-08-27 15:20 ` [PATCH v2 15/39] xen/riscv: add IMSIC vCPU context switch handlers Oleksii Kurochko
2026-09-04 11:33   ` Baptiste Le Duc
2026-09-04 16:56     ` Oleksii Kurochko
2026-09-10 14:57   ` Jan Beulich
2026-09-11 11:19     ` Oleksii Kurochko
2026-08-27 15:21 ` [PATCH v2 16/39] xen/riscv: extend exception tables with type and data fields Oleksii Kurochko
2026-09-07 15:57   ` Baptiste Le Duc
2026-09-08  6:06     ` Jan Beulich
2026-09-08  8:18       ` Baptiste Le Duc
2026-09-08  9:19     ` Oleksii Kurochko
2026-09-08 16:26       ` Baptiste Le Duc
2026-09-08 13:44   ` Jan Beulich
2026-09-09 11:20     ` Oleksii Kurochko
2026-09-09 12:22       ` Jan Beulich
2026-09-09 12:42         ` Oleksii Kurochko
2026-08-27 15:21 ` [PATCH v2 17/39] xen/riscv: decouple INSN_PSEUDO_VS_* from the hypervisor's XLEN Oleksii Kurochko
2026-09-07 15:57   ` Baptiste Le Duc
2026-09-08  9:34     ` Oleksii Kurochko
2026-09-08 16:04       ` Baptiste Le Duc
2026-09-09 12:57         ` Oleksii Kurochko
2026-08-27 15:21 ` [PATCH v2 18/39] xen/riscv: add guest page fault handling stub Oleksii Kurochko
2026-09-07 15:57   ` Baptiste Le Duc
2026-09-08  9:49     ` Oleksii Kurochko
2026-09-08 14:10   ` Jan Beulich
2026-09-09 15:09     ` Oleksii Kurochko
2026-09-10  6:38       ` Jan Beulich
2026-09-11 11:47         ` Oleksii Kurochko
2026-08-27 15:21 ` [PATCH v2 19/39] xen/riscv: implement trap redirection to a guest Oleksii Kurochko
2026-09-07 15:57   ` Baptiste Le Duc
2026-09-08 10:01     ` Oleksii Kurochko
2026-09-08 14:58       ` Oleksii Kurochko
2026-09-08 15:05         ` Jan Beulich
2026-09-08 15:47           ` Baptiste Le Duc
2026-09-08 15:58             ` Jan Beulich
2026-09-08 14:16   ` Jan Beulich
2026-09-08 15:25     ` Oleksii Kurochko
2026-09-08 14:16   ` Jan Beulich
2026-08-27 15:21 ` [PATCH v2 20/39] xen/riscv: detect Shtvala Oleksii Kurochko
2026-09-07 15:57   ` Baptiste Le Duc
2026-09-08 10:15     ` Oleksii Kurochko
2026-09-08 15:49       ` Baptiste Le Duc
2026-08-27 15:21 ` [PATCH v2 21/39] xen/riscv: resolve the faulting guest physical address Oleksii Kurochko
2026-09-09 12:04   ` Baptiste Le Duc
2026-09-11 12:56     ` Oleksii Kurochko
2026-09-10 15:06   ` Jan Beulich
2026-08-27 15:21 ` [PATCH v2 22/39] xen/riscv: add guest memory read helper Oleksii Kurochko
2026-09-09 12:04   ` Baptiste Le Duc
2026-09-10 15:19     ` Jan Beulich
2026-09-11 13:06     ` Oleksii Kurochko
2026-09-11 13:41       ` Oleksii Kurochko
2026-09-11 13:47         ` Jan Beulich
2026-09-11 13:50           ` Oleksii Kurochko
2026-09-10 15:28   ` Jan Beulich
2026-09-11 13:57     ` Oleksii Kurochko
2026-09-11 14:00       ` Jan Beulich
2026-09-11 14:29         ` Oleksii Kurochko
2026-08-27 15:21 ` [PATCH v2 23/39] xen/riscv: look up the exception table for any trap taken in Xen context Oleksii Kurochko
2026-09-10 15:31   ` Jan Beulich
2026-09-18  8:44   ` Baptiste Le Duc
2026-09-22  9:31     ` Oleksii Kurochko
2026-08-27 15:21 ` [PATCH v2 24/39] xen/riscv: add helpers for decoding a trapped load or store Oleksii Kurochko
2026-09-14 11:03   ` Jan Beulich
2026-09-14 15:57     ` Oleksii Kurochko
2026-09-15  5:18       ` Jan Beulich
2026-09-18  8:44   ` Baptiste Le Duc
2026-09-22 11:03     ` Oleksii Kurochko
2026-08-27 15:21 ` [PATCH v2 25/39] xen/riscv: add guest load emulation for trapped MMIO accesses Oleksii Kurochko
2026-09-14 11:48   ` Jan Beulich
2026-09-16  4:16     ` Oleksii Kurochko
2026-09-18  9:16   ` Baptiste Le Duc
2026-09-22 11:22     ` Oleksii Kurochko
2026-08-27 15:21 ` [PATCH v2 26/39] xen/riscv: add guest store " Oleksii Kurochko
2026-09-14 12:01   ` Jan Beulich
2026-09-16  4:53     ` Oleksii Kurochko
2026-09-16  5:15       ` Jan Beulich
2026-09-16  5:23         ` Oleksii Kurochko
2026-09-18  9:16   ` Baptiste Le Duc
2026-09-22 11:38     ` Oleksii Kurochko
2026-08-27 15:21 ` [PATCH v2 27/39] xen/riscv: introduce arch_move_irqs() Oleksii Kurochko
2026-09-14 12:07   ` Jan Beulich
2026-09-16  5:32     ` Oleksii Kurochko
2026-09-22 17:00   ` Baptiste Le Duc
2026-08-27 15:21 ` [PATCH v2 28/39] xen/riscv: handle the case when no vCPU migration is needed Oleksii Kurochko
2026-09-14 12:12   ` Jan Beulich
2026-09-16  5:55     ` Oleksii Kurochko
2026-09-16 13:02       ` Jan Beulich
2026-09-17  5:12         ` Oleksii Kurochko
2026-09-17  5:20           ` Jan Beulich
2026-09-17  8:40             ` Oleksii Kurochko
2026-09-17 10:41               ` Jan Beulich
2026-09-18  9:21     ` Baptiste Le Duc
2026-08-27 15:21 ` [PATCH v2 29/39] xen/riscv: introduce aplic_reconfigure_target() Oleksii Kurochko
2026-09-14 12:25   ` Jan Beulich
2026-09-17  4:55     ` Oleksii Kurochko
2026-08-27 15:21 ` [PATCH v2 30/39] xen/riscv: prepare new IMSIC VS-file Oleksii Kurochko
2026-09-14 13:13   ` Jan Beulich
2026-09-17 14:50     ` Oleksii Kurochko
2026-09-18  6:02       ` Jan Beulich
2026-09-21 16:15   ` Baptiste Le Duc
2026-09-22  6:32     ` Jan Beulich
2026-09-22 13:01     ` Oleksii Kurochko
2026-09-22 15:36       ` Baptiste Le Duc
2026-08-27 15:21 ` [PATCH v2 31/39] xen/riscv: implement APLIC-hart sync barrier for vCPU migration Oleksii Kurochko
2026-09-14 13:27   ` Jan Beulich
2026-09-18 11:53     ` Oleksii Kurochko
2026-09-22 17:03       ` Baptiste Le Duc
2026-09-22 17:00   ` Baptiste Le Duc
2026-09-22 18:48     ` Oleksii Kurochko
2026-09-23 10:57       ` Oleksii Kurochko
2026-09-23 12:15         ` Baptiste Le Duc
2026-08-27 15:21 ` [PATCH v2 32/39] xen/riscv: remap interrupts to new IMSIC VS-file Oleksii Kurochko
2026-09-14 15:02   ` Jan Beulich
2026-09-21  8:03     ` Oleksii Kurochko
2026-09-21  8:28       ` Jan Beulich
2026-09-21  8:50         ` Oleksii Kurochko
2026-09-23 13:34   ` Baptiste Le Duc
2026-09-23 15:45     ` Oleksii Kurochko
2026-09-23 16:10       ` Baptiste Le Duc
2026-09-23 15:25   ` Baptiste Le Duc
2026-09-23 15:48     ` Oleksii Kurochko
2026-09-23 16:11       ` Baptiste Le Duc
2026-08-27 15:21 ` [PATCH v2 33/39] xen/riscv: dump old interrupt file to memory Oleksii Kurochko
2026-09-14 15:15   ` Jan Beulich
2026-09-21  9:51     ` Oleksii Kurochko
2026-09-23 15:15   ` Baptiste Le Duc
2026-09-23 16:02     ` Oleksii Kurochko
2026-09-23 16:16       ` Baptiste Le Duc
2026-09-23 18:30         ` Oleksii Kurochko
2026-08-27 15:21 ` [PATCH v2 34/39] xen/riscv: restore register state in the new IMSIC VS-file Oleksii Kurochko
2026-09-14 15:21   ` Jan Beulich
2026-09-21 10:58     ` Oleksii Kurochko
2026-09-23 15:42   ` Baptiste Le Duc
2026-09-23 16:08     ` Oleksii Kurochko
2026-09-23 16:21       ` Baptiste Le Duc
2026-08-27 15:21 ` [PATCH v2 35/39] xen/riscv: add basic VGEIN management for AIA guests Oleksii Kurochko
2026-09-18 12:38   ` Jan Beulich
2026-09-23 16:06   ` Baptiste Le Duc
2026-08-27 15:21 ` [PATCH v2 36/39] xen/riscv: wake up a descheduled vCPU on a guest external interrupt Oleksii Kurochko
2026-09-18 12:52   ` Jan Beulich
2026-09-21 14:01     ` Oleksii Kurochko
2026-09-21 15:08       ` Jan Beulich
2026-09-22 13:37         ` Oleksii Kurochko
2026-08-27 15:21 ` [PATCH v2 37/39] xen/riscv: map IMSIC interrupt file for vCPUs Oleksii Kurochko
2026-09-21 11:36   ` Jan Beulich
2026-09-21 14:35     ` Oleksii Kurochko
2026-08-27 15:21 ` [PATCH v2 38/39] xen/riscv: implement continue_new_vcpu() Oleksii Kurochko
2026-09-21 12:12   ` Jan Beulich
2026-09-22  8:23     ` Oleksii Kurochko
2026-09-22 10:20       ` Jan Beulich
2026-09-22 13:58         ` Oleksii Kurochko
2026-08-27 15:21 ` [PATCH v2 39/39] xen/riscv: introduce IMSIC h/w interrupt file attaching to vcpu Oleksii Kurochko
2026-09-21 12:32   ` Jan Beulich
2026-09-22  8:31     ` Oleksii Kurochko
2026-09-22 10:23       ` Jan Beulich

This is an external index of several public inboxes,
see mirroring instructions on how to clone and mirror
all data and code used by this external index.