The design of the PLIC is poor: We basically need to trap & moderate each access.
The strategy is as follows: On IRQ arrival in S-Mode, we directly acknowledge the IRQ, save it in a shadow register, and reinject it to the VS-Mode guest. Now disable IRQs for S-Mode, until the guest has claimed and acknowledged the IRQ. After the guest acknowledged the IRQ, reenable IRQs in S-Mode again. Signed-off-by: Ralf Ramsauer <ralf.ramsa...@oth-regensburg.de> --- hypervisor/arch/riscv/Kbuild | 2 +- hypervisor/arch/riscv/include/asm/plic.h | 3 + hypervisor/arch/riscv/plic.c | 576 +++++++++++++++++++++++ 3 files changed, 580 insertions(+), 1 deletion(-) create mode 100644 hypervisor/arch/riscv/plic.c diff --git a/hypervisor/arch/riscv/Kbuild b/hypervisor/arch/riscv/Kbuild index 7809007c..9fd2456f 100644 --- a/hypervisor/arch/riscv/Kbuild +++ b/hypervisor/arch/riscv/Kbuild @@ -15,4 +15,4 @@ always-y := lib.a lib-y := entry.o exception.o setup.o dbg-write.o control.o ivshmem.o paging.o -lib-y += pci.o traps.o lib.o +lib-y += plic.o pci.o traps.o lib.o diff --git a/hypervisor/arch/riscv/include/asm/plic.h b/hypervisor/arch/riscv/include/asm/plic.h index 04cdfa63..c5414e9e 100644 --- a/hypervisor/arch/riscv/include/asm/plic.h +++ b/hypervisor/arch/riscv/include/asm/plic.h @@ -15,4 +15,7 @@ #define PLIC_MAX_IRQS 1024 +extern int plic_set_pending(void); +bool irqchip_irq_in_cell(struct cell *cell, unsigned int irq); + #endif /* _PLIC_H */ diff --git a/hypervisor/arch/riscv/plic.c b/hypervisor/arch/riscv/plic.c new file mode 100644 index 00000000..84f95c0b --- /dev/null +++ b/hypervisor/arch/riscv/plic.c @@ -0,0 +1,576 @@ +/* + * Jailhouse, a Linux-based partitioning hypervisor + * + * Copyright (c) OTH Regensburg, 2022 + * + * Authors: + * Ralf Ramsauer <ralf.ramsa...@oth-regensburg.de> + * + * This work is licensed under the terms of the GNU GPL, version 2. See + * the COPYING file in the top-level directory. + */ + +#include <jailhouse/paging.h> +#include <jailhouse/cell.h> +#include <jailhouse/string.h> +#include <jailhouse/unit.h> +#include <jailhouse/control.h> +#include <jailhouse/processor.h> +#include <jailhouse/printk.h> +#include <asm/csr64.h> + +#define PLIC_PRIO_BASE 0x0 +#define PLIC_PENDING_BASE 0x1000 +#define PLIC_ENABLE_BASE 0x2000 +#define PLIC_ENABLE_OFF 0x80 +#define PLIC_ENABLE_END 0x1f2000 +#define PLIC_CTX_BASE 0x200000 +#define PLIC_CTX_PRIO_TH 0x0 +#define PLIC_CTX_CLAIM 0x4 +#define PLIC_CTX_SZ 0x1000 +#define PLIC_CTX_END 0x4000000 + +#define REG_SZ 4 +#define REG_RANGE(A, B) (A)...((B) - REG_SZ) + +#define PLIC_BITS_PER_REG (REG_SZ * 8) +#define IRQ_BIT(irq) ((irq) % PLIC_BITS_PER_REG) +#define IRQ_MASK(irq) (1 << IRQ_BIT(irq)) + + +/* Could also be used for arm-common/irqchip.c */ +#define IRQCHIP_PINS \ + (sizeof(((struct jailhouse_irqchip *)0)->pin_bitmap) * 8) + +#define IRQ_BITMAP_PINS \ + (sizeof(((struct cell *)0)->arch.irq_bitmap) * 8) + +static void *plic_base; +static unsigned long pending[MAX_CPUS]; + +static inline unsigned long plic_phys(void) +{ + return system_config->platform_info.riscv.plic.base_address; +} + +static inline s16 hart_to_context(unsigned int hartid) +{ + if (hartid > ARRAY_SIZE( + system_config->platform_info.riscv.plic.hart_to_context)) + return -1; + + return system_config->platform_info.riscv.plic.hart_to_context[hartid]; +} + +static inline u16 plic_max_irq(void) +{ + return system_config->platform_info.riscv.plic.max_irq; +} + +static inline u16 plic_max_priority(void) +{ + return system_config->platform_info.riscv.plic.max_priority; +} + +static inline unsigned int plic_size(void) +{ + return system_config->platform_info.riscv.plic.size; +} + +static inline void ext_enable(void) +{ + csr_set(sie, IE_EIE); +} + +static inline void ext_disable(void) +{ + csr_clear(sie, IE_EIE); +} + +static inline void guest_clear_ext(void) +{ + csr_clear(CSR_HVIP, (1 << IRQ_S_EXT) << VSIP_TO_HVIP_SHIFT); +} + +static inline void guest_inject_ext(void) +{ + csr_set(CSR_HVIP, (1 << IRQ_S_EXT) << VSIP_TO_HVIP_SHIFT); +} + +static inline bool guest_ext_pending(void) +{ + return !!(csr_read(CSR_HVIP) & + ((1 << IRQ_S_EXT) << VSIP_TO_HVIP_SHIFT)); +} + +static inline u32 plic_read(u32 reg) +{ + return mmio_read32(plic_base + reg); +} + +static inline void plic_write(u32 reg, u32 value) +{ + mmio_write32(plic_base + reg, value); +} + +static inline u32 plic_read_context(u32 context, u32 off) +{ + return plic_read(PLIC_CTX_BASE + context * PLIC_CTX_SZ + off); +} + +static inline u32 plic_read_claim(u32 context) +{ + return plic_read_context(context, PLIC_CTX_CLAIM); +} + +static inline u32 plic_en_reg(s16 context, unsigned int irq) +{ + u32 reg; + + reg = PLIC_ENABLE_BASE + (context * PLIC_ENABLE_OFF) + + (irq / PLIC_BITS_PER_REG) * REG_SZ; + + return reg; +} + +static inline u32 plic_read_enabled(s16 context, unsigned int irq) +{ + return plic_read(plic_en_reg(context, irq)); +} + +static inline void plic_write_enabled(s16 context, unsigned int irq, u32 val) +{ + plic_write(plic_en_reg(context, irq), val); +} + +static inline bool plic_irq_is_enabled(s16 context, unsigned int irq) +{ + u32 en = plic_read_enabled(context, irq); + + return !!(en & IRQ_MASK(irq)); +} + +static inline void plic_enable_irq(s16 context, unsigned int irq) +{ + u32 val; + + val = plic_read_enabled(context, irq) | IRQ_MASK(irq); + plic_write_enabled(context, irq, val); +} + +static inline void plic_disable_irq(s16 context, unsigned int irq) +{ + u32 val; + + val = plic_read_enabled(context, irq) & ~IRQ_MASK(irq); + plic_write_enabled(context, irq, val); +} + +static bool irq_bitmap_test(u32 *bitmap, unsigned int irq) +{ + u32 val; + + if (irq >= plic_max_irq()) + return false; + + val = bitmap[irq / PLIC_BITS_PER_REG]; + + return !!(val & IRQ_MASK(irq)); +} + +static inline void irq_bitmap_set(u32 *bitmap, unsigned int irq) +{ + bitmap[irq / PLIC_BITS_PER_REG] |= IRQ_MASK(irq); +} + +static inline void irq_bitmap_clear(u32 *bitmap, unsigned int irq) +{ + bitmap[irq / PLIC_BITS_PER_REG] &= ~IRQ_MASK(irq); +} + +inline bool irqchip_irq_in_cell(struct cell *cell, unsigned int irq) +{ + return irq_bitmap_test(cell->arch.irq_bitmap, irq); +} + +int plic_set_pending(void) +{ + int my_context; + u32 irq; + unsigned int cpuid; + + this_cpu_public()->stats[JAILHOUSE_CPU_STAT_VMEXITS_VIRQ]++; + + cpuid = phys_processor_id(); + + /* Assume that phys_processor_id always returns something < 64 */ + my_context = hart_to_context(cpuid); + if (my_context < 0) + return -ENOSYS; + + irq = plic_read_claim(my_context); + if (irq == 0) /* spurious IRQ, should not happen */ + return -EINVAL; + + if (irq > plic_max_irq()) + return -EINVAL; + + pending[cpuid] = irq; + /* + * We can directly inject the IRQ into the guest if the IRQ is not + * pending, because we know that the IRQ is enabled, otherwise we + * wouldn't have received it + */ + guest_inject_ext(); + + /* + * Don't claim complete! This must be done by the guest. We will handle + * that in plic_handler(). In the meanwhile we simply deactivate S-Mode + * External IRQs, and reenable them when the guest claims it. In this + * way, we only need to store one pending IRQ per hart. + */ + ext_disable(); + + return 0; +} + +static inline void plic_passthru(const struct mmio_access *access) +{ + plic_write(access->address, access->value); +} + +static inline enum mmio_result +plic_handle_context_claim(struct mmio_access *access, unsigned long hart) +{ + if (!access->is_write) { + access->value = pending[hart]; + return MMIO_HANDLED; + } + + /* claim write case */ + if (access->value != pending[hart]) { + printk("FATAL: Guest acknowledged non-pending IRQ %lu\n", + access->value); + return MMIO_ERROR; + } + + plic_write(access->address, access->value); + + /* Check if there's another physical IRQ pending */ + /* TODO: This is where we would need to prioritise vIRQs */ + pending[hart] = plic_read(access->address); + if (pending[hart]) + return MMIO_HANDLED; + + guest_clear_ext(); + ext_enable(); + + return MMIO_HANDLED; +} + +static enum mmio_result plic_handle_context(struct mmio_access *access) +{ + unsigned int cpu; + unsigned long hart; + int ctx; + u64 addr; + + addr = access->address - PLIC_CTX_BASE; + ctx = addr / PLIC_CTX_SZ; + + /* + * It is clear that a hart is allowed to access its own context. + * But we also need to allow accesses to context to neighboured + * harts within the cell. + * + * In (probably) 99% of all cases, the current active CPU will access + * its own context. So do this simple check first, and check other + * contexts of the cell (for loop) later. This results in a bit more + * complex code, but results in better performance. + */ + hart = phys_processor_id(); + if (hart_to_context(hart) == ctx) + goto allowed; + + for_each_cpu_except(cpu, &this_cell()->cpu_set, this_cpu_id()) + if (hart_to_context(public_per_cpu(cpu)->phys_id) == ctx) + goto allowed; + + return trace_error(MMIO_ERROR); + +allowed: + addr -= ctx * PLIC_CTX_SZ; + if (addr == PLIC_CTX_CLAIM) { + return plic_handle_context_claim(access, hart); + } else if (addr == PLIC_CTX_PRIO_TH) { + /* We land here if we permit the access */ + if (access->is_write) + plic_passthru(access); + else + access->value = plic_read(access->address); + } else { + return MMIO_ERROR; + } + + return MMIO_HANDLED; +} + +static enum mmio_result plic_handle_prio(struct mmio_access *access) +{ + unsigned int irq; + unsigned int prio = access->value; + + irq = access->address / REG_SZ; + + if (!irqchip_irq_in_cell(this_cell(), irq)) + return MMIO_ERROR; + + /* + * Maybe we can abandon this check. The cell should know the max + * allowed value, so simply allow any value? + */ + if (prio > plic_max_priority()) + return MMIO_ERROR; + + plic_passthru(access); + return MMIO_HANDLED; +} + +static enum mmio_result plic_handle_enable(struct mmio_access *access) +{ + struct public_per_cpu *pc; + u32 irq_allowed_bitmap; + unsigned int idx, cpu; + short int ctx; + + ctx = (access->address - PLIC_ENABLE_BASE) / PLIC_ENABLE_OFF; + + /* Does the context even belong to one of the cell's CPUs? */ + for_each_cpu(cpu, &this_cell()->cpu_set) { + pc = public_per_cpu(cpu); + if (hart_to_context(pc->phys_id) == ctx) + goto allowed; + } + + /* + * FIXME: Why does Linux read non-allowed ctxs? This seems to be an + * actual bug in Linux. When we remove a CPU from Linux, and we later + * change the affinity of the IRQ, then Linux will try to access + * Contexts which it is not in charge of any longer. While Linux + * disables IRQs, it does not adjust smp_affinities when removing CPUs. + * + * For the moment, and as a workaround, simply report any read as 0, + * and forbid writes != 0. + * + * ... Okay, we really have a Linux bug here. + * (a) Linux doesn't remove the affinity from removed CPUs + * (b) Linux allows to set affinity to non-present CPUs + * + * Actually, we should always return MMIO_ERROR here. + */ + +#if 1 + if (!access->is_write) { + access->value = 0; + } else if (access->value != 0) + return MMIO_ERROR; + return MMIO_HANDLED; +#else + return MMIO_ERROR; +#endif + +allowed: + /* + * Now we have to check if we have a read or write access. In case of + * reads, simply return the real value of the PLIC. + * + * In case of writes, compare against the irq_bitmap, if we're allowed + * to perform the write. + */ + idx = ((access->address - PLIC_ENABLE_BASE) % PLIC_ENABLE_OFF) + * 8 / PLIC_BITS_PER_REG; + + if (!access->is_write) { + access->value = plic_read(access->address); + return MMIO_HANDLED; + } + + /* write case */ + irq_allowed_bitmap = this_cell()->arch.irq_bitmap[idx]; + + if (access->value & ~irq_allowed_bitmap) { + printk("FATAL: Cell enabled non-assigned IRQ\n"); + return MMIO_ERROR; + } + + plic_passthru(access); + + return MMIO_HANDLED; +} + +static enum mmio_result plic_handler(void *arg, struct mmio_access *access) +{ + /* only allow 32bit access */ + if (access->size != REG_SZ) + return MMIO_ERROR; + + switch (access->address) { + case REG_RANGE(PLIC_PRIO_BASE, PLIC_PENDING_BASE): + return plic_handle_prio(access); + break; + + case REG_RANGE(PLIC_ENABLE_BASE, PLIC_ENABLE_END): + return plic_handle_enable(access); + break; + + case REG_RANGE(PLIC_CTX_BASE, PLIC_CTX_END): + if (access->address < plic_size()) + return plic_handle_context(access); + break; + + default: + break; + } + + return MMIO_ERROR; +} + +static int plic_cell_init(struct cell *cell) +{ + const struct jailhouse_irqchip *chip; + unsigned int n, pos; + + mmio_region_register(cell, plic_phys(), plic_size(), plic_handler, + cell); + + for_each_irqchip(chip, cell->config, n) { + /* Only support one single PLIC at the moment */ + if (chip->address != + system_config->platform_info.riscv.plic.base_address) + return trace_error(-EINVAL); + + if (chip->pin_base % 32 != 0 || + chip->pin_base + IRQCHIP_PINS > IRQ_BITMAP_PINS) + return trace_error(-EINVAL); + + for (pos = 0; pos < ARRAY_SIZE(chip->pin_bitmap); pos++) + cell->arch.irq_bitmap[chip->pin_base / 32 + pos] |= + chip->pin_bitmap[pos]; + } + + /* This logic is shared with arm-common */ + if (cell == &root_cell) + return 0; + + for_each_irqchip(chip, cell->config, n) { + // TODO: Check if IRQs are disabled before removing them + for (pos = 0; pos < ARRAY_SIZE(chip->pin_bitmap); pos++) + root_cell.arch.irq_bitmap[chip->pin_base / 32 + pos] &= + ~chip->pin_bitmap[pos]; + } + + return 0; +} + +static int plic_init(void) +{ + unsigned int cpu, irq; + s16 context; + + plic_base = paging_map_device(plic_phys(), plic_size()); + if (!plic_base) + return -ENOMEM; + + plic_cell_init(&root_cell); + + /* + * If we check during early initialisation if all enabled IRQs belong + * to the root cell, then we don't need to check if an IRQ belongs to a + * cell on arrival. + */ + for_each_cpu(cpu, &root_cell.cpu_set) { + context = hart_to_context(cpu); + for (irq = 0; irq < plic_max_irq(); irq++) { + if (plic_irq_is_enabled(context, irq) && + !irqchip_irq_in_cell(&root_cell, irq)) { + printk("Error: IRQ %u active in root cell\n", + irq); + return trace_error(-EINVAL); + } + } + } + + return 0; +} + +static void plic_cell_exit(struct cell *cell) +{ + const struct jailhouse_irqchip *chip; + unsigned int n, pos; + + mmio_region_unregister(cell, plic_phys()); + + /* set all pins of the old cell in the root cell */ + for_each_irqchip(chip, cell->config, n) + for (pos = 0; pos < ARRAY_SIZE(chip->pin_bitmap); pos++) + root_cell.arch.irq_bitmap[chip->pin_base / 32 + pos] |= + chip->pin_bitmap[pos]; + + /* mask out pins again that actually didn't belong to the root cell */ + for_each_irqchip(chip, root_cell.config, n) + for (pos = 0; pos < ARRAY_SIZE(chip->pin_bitmap); pos++) + root_cell.arch.irq_bitmap[chip->pin_base / 32 + pos] &= + chip->pin_bitmap[pos]; +} + +static void plic_shutdown(void) +{ + if (plic_base) + paging_unmap_device(plic_phys(), plic_base, plic_size()); +} + +static unsigned int plic_mmio_count_regions(struct cell *cell) +{ + return 1; +} + +static void plic_config_commit(struct cell *cell) +{ + unsigned int irq, n; + s16 ctx; + + if (!cell) + return; + + if (cell == &root_cell) + return; + + for (irq = 0; irq < plic_max_irq(); irq++) { + /* + * Three possibilities: + * 1. IRQ belongs to root cell and was removed (cell + * creation) + * 2. IRQ belonged to non-root cell and was assigned back to + * non-root cell (cell destruction) + * 3. IRQ belonged to non-root cell and is simply gone + * (belongs to no one) + * + * IRQ-Bitmaps are already updated accordingly. All we have to + * do is to ensure that the IRQ is disabled. That's all. + */ + if (!irqchip_irq_in_cell(cell, irq)) + continue; + + /* Disable the IRQ for each defined PLIC context. */ + for (n = 0; n < MAX_CPUS; n++) { + ctx = hart_to_context(n); + if (ctx == -1) + // or break? Are there non-contiguous harts? + continue; + + plic_disable_irq(ctx, irq); + } + } +} + +DEFINE_UNIT (plic, "RISC-V PLIC"); -- 2.36.1 -- You received this message because you are subscribed to the Google Groups "Jailhouse" group. To unsubscribe from this group and stop receiving emails from it, send an email to jailhouse-dev+unsubscr...@googlegroups.com. To view this discussion on the web visit https://groups.google.com/d/msgid/jailhouse-dev/20220627132905.4338-29-ralf.ramsauer%40oth-regensburg.de.