|
[Date Prev][Date Next][Thread Prev][Thread Next][Date Index][Thread Index] Re: [PATCH v1 16/17] xen/riscv: add guest load emulation for trapped MMIO accesses
On 8/13/26 9:15 AM, Jan Beulich wrote: On 29.07.2026 15:40, Oleksii Kurochko wrote: I wasn't able to find a spec what should be returned in this case but in QEMU source code I founded (unassigned_mem_ops → MEMTX_DECODE_ERROR → io_failed() → riscv_cpu_do_transaction_failed().):
void riscv_cpu_do_transaction_failed(CPUState *cs, hwaddr physaddr,
vaddr addr, unsigned size,
MMUAccessType access_type,
int mmu_idx, MemTxAttrs attrs,
MemTxResult response, uintptr_t
retaddr)
{
RISCVCPU *cpu = RISCV_CPU(cs);
CPURISCVState *env = &cpu->env;
if (access_type == MMU_DATA_STORE) {
cs->exception_index = RISCV_EXCP_STORE_AMO_ACCESS_FAULT;
} else if (access_type == MMU_DATA_LOAD) {
cs->exception_index = RISCV_EXCP_LOAD_ACCESS_FAULT;
} else {
cs->exception_index = RISCV_EXCP_INST_ACCESS_FAULT;
}
So RISCV_EXCP_INST_ACCESS_FAULT (in Xen it is CAUSE_FETCH_ACCESS) will
be fine to return.
So do the following:
if ( is_load_guest_page_fault(utrap.scause) )
utrap.scause = CAUSE_FETCH_ACCESS;
will be fair enough instead of:
BUG_ON(is_load_guest_page_fault(utrap.scause)).
Probably, we want to rename CAUSE_FETCH_ACCESS to be closer to RISC-V spec as for value 1 in spec it is used: 1 Instruction access fault Interesting that all other CAUSE_* defines are aligned with the spec...
No, it won't. At least, I don't see such use cases now. I'll use unsigned int instead.
ld/sd instruction which we are trapping here at the moment here could be 2 bit and 4 bit depends on C extension so we need to pass correct instruction length to advance_pc() after it is emulated. Finally, how would the caller know whether it looks at a transformed insn or (as fetched below) a "normal" one? According to the spec ((part from htinst ... ):On a synchronous exception, if a nonzero value is written, one of the following shall be true about the value: • Bit 0 is 1, and replacing bit 1 with 1 makes the value into a valid encoding of a standard instruction. In this case, the instruction that trapped is the same kind as indicated by the register value, and the register value is the transformation of the trapping instruction, as defined later. For example, if bits 1:0 are binary 11 and the register value is the encoding of a standard LW (load word) instruction, then the trapping instruction is LW, and the register value is the transformation of the trapping LW instruction. • Bit 0 is 1, and replacing bit 1 with 1 makes the value into an instruction encoding that is explicitly designated for a custom instruction (not an unused reserved encoding). This is a custom value. The instruction that trapped is a non-standard instruction. The interpretation of a custom value is not otherwise specified by this standard. • The value is one of the special pseudoinstructions defined later, all of which have bits 1:0 equal to 00. So setting bit 0 to 1 we will guarantee that it is normal "normal" instruction.
It is really problem but I think it should be resolved much earlier in handle_guest_page_fault(). I will add the following:
/*
* A guest page fault taken on an implicit memory access performed for
* VS-stage address translation (reading a PTE, or updating its A/D
bits)
* reports a pseudoinstruction in htinst rather than a transformed
* instruction. Such a fault can't be emulated: htval holds the guest
* physical address of a VS-stage PTE rather than of any access the
guest
* itself performed (and its two least significant bits are zero
instead
* of matching stval), while the instruction at sepc is unrelated
to the
* access which actually faulted.
*
* Report an access fault to the guest at the original virtual address,
* which is what stval already holds and what hardware would raise
for a
* page table walk hitting an inaccessible address.
*/
if ( (htinst == INSN_PSEUDO_VS_LOAD) || (htinst ==
INSN_PSEUDO_VS_STORE) )
{
struct cpu_user_regs *regs = vcpu_guest_cpu_user_regs(current);
struct trap_info utrap = {
.scause = (htinst == INSN_PSEUDO_VS_LOAD) ? CAUSE_LOAD_ACCESS
: CAUSE_STORE_ACCESS,
.sepc = regs->sepc,
.stval = csr_read(CSR_STVAL),
};
riscv_trap_redirect(&utrap);
return;
}
and will update the comment:
>> + /*
>> + * Bit[0] == 0 implies trapped instruction value is
>> + * zero or special value. It can't be pseudoinstruction as
it is guaranteed by check in handle_guest_page_fault().
>> + */
If VS-stage failed then CAUSE_LOAD_PAGE_FAULT will happen so BUG_ON() won't occur and it will be passed to guest to handle it. BUG_ON() here catches CAUSE_LOAD_GUEST_PAGE_FAULT (G-stage translation failure).
Also, as I mentioned above I will change BUG_ON() too:
/*
* 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
*/
if ( is_load_guest_page_fault(utrap.scause) )
utrap.scause = CAUSE_FETCH_ACCESS;
+ * TODO: Revisit once P2M mappings can be removed at runtime. + */ + BUG_ON(is_load_guest_page_fault(utrap.scause)); + + utrap.sepc = regs->sepc; + utrap.stval = utrap.sepc;How do you know the fault was at .sepc? A 32-bit insn crossing a page boundary (implying the C extension is available) may well fault only on its higher half. According to the spec, if stval is written with a nonzero value when an instruction access-fault or page-fault exception occurs on a system with variable-length instructions, then stval will contain the virtual address of the portion of the instruction that caused the fault, while sepc will point to the beginning of the instruction. So here, we are trying to emulate what real hardware will do in this case. In regs->sepc, we have the start of the instruction that we didn't touch. sepc is filled according to the spec in this case. Regarding utrap.stval, we know that utrap.sepc points to the correct part of the faulting address, as we are reading the instruction in 16-bit chunks: HLVX_HU(%[val], %[addr]) ; low 16 bits from sepc andi %[tmp], %[val], 3 addi %[tmp], %[tmp], -3 bne %[tmp], zero, 2f ; if not (insn & 3) == 3 -> 16-bit, end addi %[addr], %[addr], 2 ; <- addr is now sepc+2 HLVX_HU(%[tmp], %[addr]) ; high 16 bits, possibly from another pageSo, if a trap happens while reading the high 16 bits (which may be located on another page), then utrap.sepc, if the read fails, will point to the high part of the instruction, which is what the spec requires. Does that make sense?
I don't have any specific scenario where it is needed now so I don't know what to say. And there is no race between find_mmio_handler() and handle_{read,write}() as find_mmio_handler() returns copy of the structure under read_lock():
/*
* 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;
}
At the moment, use cases are pretty strainghforward, a device from guest
trying to read/write into MMIO and so the result logically could be or
it is successfully handled or some issue happened and it is just
aborted. As a real hardware will do, I think.
Also, nit: Blank lines please between non-fall-through case blocks. ....... Otoh none of these masks cover the pseudoinsns that htinst may supply. As I answered above we should handle that before this function will call so here we won't deal with htinst at all. Of course, if what I wrote above is correct. I will double check before applying that. Further, what about A-extension insns? Some (if not all) of them can plausibly be used on MMIO, I think. I’m not really sure that the A-extension is actively used for MMIO. At least, Linux doesn’t do that for now, which is why we don’t handle A-extension instructions here. I think this is related to the fact that MMIO is usually (if not always?) naturally aligned, and naturally aligned loads and stores are guaranteed by RISC-V to execute atomically. Anyway, since we don’t have a case for this for now, I think we could go with the current emulation. If this turns out not to be true in the future, A-extension support can be added separately. +#ifndef CONFIG_RISCV_32 + else if ( (insn & INSN_MASK_LWU) == INSN_MATCH_LWU ) Both points taken. The #ifndef will become a condition in the if-chain; the INSN_MATCH_/INSN_MASK_ definitions are unconditional in riscv_encoding.h, so that builds either way. On guest bitness you're right, and it's worse than the 32-bit-only encodings being reserved in RV32: the compressed RV64 encodings collide with the RV32 single-precision float ones — C.LD and C.FLW are both 0x6000 under mask 0xe003, likewise C.SD/C.FSW, C.LDSP/C.FLWSP and C.SDSP/C.FSWSP. A 32-bit guest doing a c.flw to an emulated MMIO region would be decoded as c.ld, i.e.an 8-byte access with the result written to an integer register. I'll fold both into a helper returning the guest's effective XLEN (hstatus.VSXL, or vsstatus.UXL when the trap was taken from VU-mode, andunconditionally 32 for a RV32 build) and gate the RV64-only cases on it. As a side note, vcpu hstatus setup currently leaves VSXL alone and thus relies on the WARL behaviour of the field; I think Xen should set it explicitly. /** 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. ** 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 unsigned int guest_xlen(const struct cpu_user_regs *regs)
{
#ifdef CONFIG_RISCV_32
return 32;
#else
unsigned long xl = (regs->sstatus & SSTATUS_SPP)
? MASK_EXTR(regs->hstatus, HSTATUS_VSXL)
: MASK_EXTR(csr_read(CSR_VSSTATUS), SSTATUS64_UXL);
/* 1 encodes XLEN=32, 2 encodes XLEN=64. */
return (xl == HSTATUS_VSXL_32) ? 32 : 64;
#endif
}
and then use it in emulate_store/load():
unsigned int xlen = guest_xlen(regs);
...
else if ( (xlen == 64) && ((insn & INSN_MASK_LWU) == INSN_MATCH_LWU) )
{
len = 4;
is_unsigned = true;
}
...
else if ( (xlen == 64) && ((insn & INSN_MASK_C_LD) ==
INSN_MATCH_C_LD) )
At the moment, I wrote this function with handling of MMIO instruction in mind, which are at the moment ld and sd. Even if to permit F/D/Q then do we really need to trap that instructions? Hypervisor could allow access to FPU to guest and then it will be just a question of context switch to properly save and restore FPU. I wonder how easy it is going to be to spot the places needing adjustment once support is to be added. Same perhaps for Zilsd in RV32 guests.
There is a message in handle_guest_page_fault():
if ( rc )
domain_crash(current->domain,
"%s: unable to handle faulted guest %s addr %#lx\n",
__func__,
(cause == CAUSE_LOAD_GUEST_PAGE_FAULT) ? "load" :
"store",
addr);
Probably, it isn't enough and we could print here instruction (in hex)
before return -EOPNOTSUPP.
Thanks. ~ Oleksii
|
![]() |
Lists.xenproject.org is hosted with RackSpace, monitoring our |