| /* SPDX-License-Identifier: LGPL-2.0-only */ |
| /* |
| * Test interrupts |
| * |
| * Copyright 2024 Nicholas Piggin, IBM Corp. |
| */ |
| #include <libcflat.h> |
| #include <util.h> |
| #include <migrate.h> |
| #include <alloc.h> |
| #include <asm/setup.h> |
| #include <asm/handlers.h> |
| #include <asm/hcall.h> |
| #include <asm/processor.h> |
| #include <asm/time.h> |
| #include <asm/barrier.h> |
| #include <asm/mmu.h> |
| #include "alloc_phys.h" |
| #include "vmalloc.h" |
| |
| static volatile bool got_interrupt; |
| static volatile struct pt_regs recorded_regs; |
| |
| static void mce_handler(struct pt_regs *regs, void *opaque) |
| { |
| bool *is_fetch = opaque; |
| |
| got_interrupt = true; |
| memcpy((void *)&recorded_regs, regs, sizeof(struct pt_regs)); |
| if (*is_fetch) |
| regs->nip = regs->link; |
| else |
| regs_advance_insn(regs); |
| } |
| |
| static void fault_handler(struct pt_regs *regs, void *opaque) |
| { |
| memcpy((void *)&recorded_regs, regs, sizeof(struct pt_regs)); |
| if (regs->trap == 0x400 || regs->trap == 0x480) |
| regs->nip = regs->link; |
| else |
| regs_advance_insn(regs); |
| } |
| |
| static void test_mce(void) |
| { |
| unsigned long addr = -4ULL; |
| uint8_t tmp; |
| bool is_fetch; |
| bool mmu = mmu_enabled(); |
| |
| report_prefix_push("mce"); |
| |
| handle_exception(0x200, mce_handler, &is_fetch); |
| handle_exception(0x300, fault_handler, NULL); |
| handle_exception(0x380, fault_handler, NULL); |
| handle_exception(0x400, fault_handler, NULL); |
| handle_exception(0x480, fault_handler, NULL); |
| |
| if (mmu) |
| mmu_disable(); |
| |
| if (machine_is_powernv()) { |
| enable_mcheck(); |
| } else { |
| report(mfmsr() & MSR_ME, "pseries machine has MSR[ME]=1"); |
| if (!(mfmsr() & MSR_ME)) { /* try to fix it */ |
| enable_mcheck(); |
| } |
| if (mfmsr() & MSR_ME) { |
| disable_mcheck(); |
| report(mfmsr() & MSR_ME, "pseries is unable to change MSR[ME]"); |
| if (!(mfmsr() & MSR_ME)) { /* try to fix it */ |
| enable_mcheck(); |
| } |
| } |
| } |
| |
| is_fetch = false; |
| asm volatile("lbz %0,0(%1)" : "=r"(tmp) : "r"(addr)); |
| |
| /* KVM does not MCE on access outside partition scope */ |
| report_kfail(host_is_kvm, got_interrupt, "MCE on access to invalid real address"); |
| if (got_interrupt) { |
| report(mfspr(SPR_DAR) == addr, "MCE sets DAR correctly"); |
| if (cpu_has_power_mce) |
| report(recorded_regs.msr & (1ULL << 21), "d-side MCE sets SRR1[42]"); |
| got_interrupt = false; |
| } |
| |
| is_fetch = true; |
| asm volatile("mtctr %0 ; bctrl" :: "r"(addr) : "ctr", "lr"); |
| /* KVM does not MCE on access outside partition scope */ |
| report_kfail(host_is_kvm, got_interrupt, "MCE on fetch from invalid real address"); |
| if (got_interrupt) { |
| report(recorded_regs.nip == addr, "MCE sets SRR0 correctly"); |
| if (cpu_has_power_mce) |
| report(!(recorded_regs.msr & (1ULL << 21)), "i-side MCE clears SRR1[42]"); |
| got_interrupt = false; |
| } |
| |
| if (mmu) |
| mmu_enable(NULL); |
| |
| handle_exception(0x200, NULL, NULL); |
| handle_exception(0x300, NULL, NULL); |
| handle_exception(0x380, NULL, NULL); |
| handle_exception(0x400, NULL, NULL); |
| handle_exception(0x480, NULL, NULL); |
| |
| report_prefix_pop(); |
| } |
| |
| static void dside_handler(struct pt_regs *regs, void *data) |
| { |
| got_interrupt = true; |
| memcpy((void *)&recorded_regs, regs, sizeof(struct pt_regs)); |
| regs_advance_insn(regs); |
| } |
| |
| static void iside_handler(struct pt_regs *regs, void *data) |
| { |
| got_interrupt = true; |
| memcpy((void *)&recorded_regs, regs, sizeof(struct pt_regs)); |
| regs->nip = regs->link; |
| } |
| |
| static void test_dseg_nommu(void) |
| { |
| uint64_t msr, tmp; |
| |
| report_prefix_push("dseg"); |
| |
| /* Some HV start in radix mode and need 0x300 */ |
| handle_exception(0x300, &dside_handler, NULL); |
| handle_exception(0x380, &dside_handler, NULL); |
| |
| asm volatile( |
| " mfmsr %0 \n \ |
| ori %1,%0,%2 \n \ |
| mtmsrd %1 \n \ |
| lbz %1,0(0) \n \ |
| mtmsrd %0 " |
| : "=r"(msr), "=r"(tmp) : "i"(MSR_DR): "memory"); |
| |
| report(got_interrupt, "interrupt on NULL dereference"); |
| got_interrupt = false; |
| |
| handle_exception(0x300, NULL, NULL); |
| handle_exception(0x380, NULL, NULL); |
| |
| report_prefix_pop(); |
| } |
| |
| static void test_mmu(void) |
| { |
| uint64_t tmp, addr; |
| phys_addr_t base, top; |
| |
| if (!mmu_enabled()) { |
| test_dseg_nommu(); |
| return; |
| } |
| |
| phys_alloc_get_unused(&base, &top); |
| |
| report_prefix_push("dsi"); |
| addr = top + PAGE_SIZE; |
| handle_exception(0x300, &dside_handler, NULL); |
| asm volatile("lbz %0,0(%1)" : "=r"(tmp) : "r"(addr)); |
| report(got_interrupt, "dsi on out of range dereference"); |
| report(mfspr(SPR_DAR) == addr, "DAR set correctly"); |
| report(mfspr(SPR_DSISR) & (1ULL << 30), "DSISR set correctly"); |
| got_interrupt = false; |
| handle_exception(0x300, NULL, NULL); |
| report_prefix_pop(); |
| |
| report_prefix_push("dseg"); |
| addr = -4ULL; |
| handle_exception(0x380, &dside_handler, NULL); |
| asm volatile("lbz %0,0(%1)" : "=r"(tmp) : "r"(addr)); |
| report(got_interrupt, "dseg on out of range dereference"); |
| report(mfspr(SPR_DAR) == addr, "DAR set correctly"); |
| got_interrupt = false; |
| handle_exception(0x380, NULL, NULL); |
| report_prefix_pop(); |
| |
| report_prefix_push("isi"); |
| addr = top + PAGE_SIZE; |
| handle_exception(0x400, &iside_handler, NULL); |
| asm volatile("mtctr %0 ; bctrl" :: "r"(addr) : "ctr", "lr"); |
| report(got_interrupt, "isi on out of range fetch"); |
| report(recorded_regs.nip == addr, "SRR0 set correctly"); |
| report(recorded_regs.msr & (1ULL << 30), "SRR1 set correctly"); |
| got_interrupt = false; |
| handle_exception(0x400, NULL, NULL); |
| report_prefix_pop(); |
| |
| report_prefix_push("iseg"); |
| addr = -4ULL; |
| handle_exception(0x480, &iside_handler, NULL); |
| asm volatile("mtctr %0 ; bctrl" :: "r"(addr) : "ctr", "lr"); |
| report(got_interrupt, "isi on out of range fetch"); |
| report(recorded_regs.nip == addr, "SRR0 set correctly"); |
| got_interrupt = false; |
| handle_exception(0x480, NULL, NULL); |
| report_prefix_pop(); |
| } |
| |
| static void dec_handler(struct pt_regs *regs, void *data) |
| { |
| got_interrupt = true; |
| memcpy((void *)&recorded_regs, regs, sizeof(struct pt_regs)); |
| regs->msr &= ~MSR_EE; |
| } |
| |
| static void test_dec(void) |
| { |
| uint64_t msr; |
| uint64_t tb; |
| |
| report_prefix_push("decrementer"); |
| |
| handle_exception(0x900, &dec_handler, NULL); |
| |
| asm volatile( |
| " mtdec %1 \n \ |
| mfmsr %0 \n \ |
| ori %0,%0,%2 \n \ |
| mtmsrd %0,1 " |
| : "=r"(msr) : "r"(10000), "i"(MSR_EE): "memory"); |
| |
| tb = get_tb(); |
| while (!got_interrupt) { |
| if (get_tb() - tb > tb_hz * 5) |
| break; /* timeout 5s */ |
| } |
| |
| report(got_interrupt, "interrupt on decrementer underflow"); |
| got_interrupt = false; |
| |
| handle_exception(0x900, NULL, NULL); |
| |
| if (!machine_is_powernv()) |
| goto done; /* Skip HV tests */ |
| |
| handle_exception(0x980, &dec_handler, NULL); |
| |
| mtspr(SPR_LPCR, mfspr(SPR_LPCR) | LPCR_HDICE); |
| asm volatile( |
| " mtspr 0x136,%1 \n \ |
| mtdec %3 \n \ |
| mfmsr %0 \n \ |
| ori %0,%0,%2 \n \ |
| mtmsrd %0,1 " |
| : "=r"(msr) : "r"(10000), "i"(MSR_EE), "r"(0x7fffffff): "memory"); |
| |
| tb = get_tb(); |
| while (!got_interrupt) { |
| if (get_tb() - tb > tb_hz * 5) |
| break; /* timeout 5s */ |
| } |
| |
| mtspr(SPR_LPCR, mfspr(SPR_LPCR) & ~LPCR_HDICE); |
| |
| report(got_interrupt, "interrupt on hdecrementer underflow"); |
| got_interrupt = false; |
| |
| handle_exception(0x980, NULL, NULL); |
| |
| done: |
| report_prefix_pop(); |
| } |
| |
| |
| static volatile uint64_t recorded_heir; |
| |
| static void heai_handler(struct pt_regs *regs, void *data) |
| { |
| got_interrupt = true; |
| memcpy((void *)&recorded_regs, regs, sizeof(struct pt_regs)); |
| regs_advance_insn(regs); |
| if (cpu_has_heai) |
| recorded_heir = mfspr(SPR_HEIR); |
| } |
| |
| static void program_handler(struct pt_regs *regs, void *data) |
| { |
| got_interrupt = true; |
| memcpy((void *)&recorded_regs, regs, sizeof(struct pt_regs)); |
| regs_advance_insn(regs); |
| } |
| |
| /* |
| * This tests invalid instruction handling. powernv (HV) should take an |
| * HEAI interrupt with the HEIR SPR set to the instruction image. pseries |
| * (guest) should take a program interrupt. CPUs which support prefix |
| * should report prefix instruction in (H)SRR1[34]. |
| */ |
| static void test_illegal(void) |
| { |
| report_prefix_push("illegal instruction"); |
| |
| if (machine_is_powernv()) { |
| handle_exception(0xe40, &heai_handler, NULL); |
| } else { |
| handle_exception(0x700, &program_handler, NULL); |
| } |
| |
| asm volatile(".long 0x12345678" ::: "memory"); |
| report(got_interrupt, "interrupt on invalid instruction"); |
| got_interrupt = false; |
| if (cpu_has_heai) |
| report(recorded_heir == 0x12345678, "HEIR: 0x%08lx", recorded_heir); |
| report(!regs_is_prefix(&recorded_regs), "(H)SRR1 prefix bit clear"); |
| |
| if (cpu_has_prefix) { |
| asm volatile(".balign 8 ; .long 0x04000123; .long 0x00badc0d"); |
| report(got_interrupt, "interrupt on invalid prefix instruction"); |
| got_interrupt = false; |
| if (cpu_has_heai) |
| report(recorded_heir == 0x0400012300badc0d, "HEIR: 0x%08lx", recorded_heir); |
| report(regs_is_prefix(&recorded_regs), "(H)SRR1 prefix bit set"); |
| } |
| |
| handle_exception(0xe40, NULL, NULL); |
| handle_exception(0x700, NULL, NULL); |
| |
| report_prefix_pop(); |
| } |
| |
| static void dec_ignore_handler(struct pt_regs *regs, void *data) |
| { |
| mtspr(SPR_DEC, 0x7fffffff); |
| } |
| |
| static void test_privileged(void) |
| { |
| unsigned long msr; |
| |
| if (!mmu_enabled()) |
| return; |
| |
| report_prefix_push("privileged instruction"); |
| |
| handle_exception(0x700, &program_handler, NULL); |
| handle_exception(0x900, &dec_ignore_handler, NULL); |
| enter_usermode(); |
| asm volatile("mfmsr %0" : "=r"(msr) :: "memory"); |
| exit_usermode(); |
| report(got_interrupt, "interrupt on privileged instruction"); |
| got_interrupt = false; |
| handle_exception(0x900, NULL, NULL); |
| handle_exception(0x700, NULL, NULL); |
| |
| report_prefix_pop(); |
| } |
| |
| static void sc_handler(struct pt_regs *regs, void *data) |
| { |
| got_interrupt = true; |
| memcpy((void *)&recorded_regs, regs, sizeof(struct pt_regs)); |
| } |
| |
| static void test_sc(void) |
| { |
| report_prefix_push("syscall"); |
| |
| handle_exception(0xc00, &sc_handler, NULL); |
| |
| asm volatile("sc 0" ::: "memory"); |
| |
| report(got_interrupt, "interrupt on sc 0 instruction"); |
| got_interrupt = false; |
| if (cpu_has_sc_lev) |
| report(((recorded_regs.msr >> 20) & 0x3) == 0, "SRR1 set LEV=0"); |
| if (machine_is_powernv()) { |
| asm volatile("sc 1" ::: "memory"); |
| |
| report(got_interrupt, "interrupt on sc 1 instruction"); |
| got_interrupt = false; |
| if (cpu_has_sc_lev) |
| report(((recorded_regs.msr >> 20) & 0x3) == 1, "SRR1 set LEV=1"); |
| } |
| |
| handle_exception(0xc00, NULL, NULL); |
| |
| report_prefix_pop(); |
| } |
| |
| |
| static void trace_handler(struct pt_regs *regs, void *data) |
| { |
| got_interrupt = true; |
| memcpy((void *)&recorded_regs, regs, sizeof(struct pt_regs)); |
| regs->msr &= ~(MSR_SE | MSR_BE); |
| } |
| |
| static void program_trace_handler(struct pt_regs *regs, void *data) |
| { |
| regs->msr &= ~(MSR_SE | MSR_BE); |
| regs->nip += 4; |
| } |
| |
| extern char trace_insn[]; |
| extern char trace_insn2[]; |
| extern char trace_insn3[]; |
| extern char trace_rfid[]; |
| |
| static void test_trace(void) |
| { |
| unsigned long msr; |
| |
| report_prefix_push("trace"); |
| |
| handle_exception(0xd00, &trace_handler, NULL); |
| |
| msr = mfmsr() | MSR_SE; |
| asm volatile( |
| " mtmsr %0 \n" |
| ".global trace_insn \n" |
| "trace_insn: \n" |
| " nop \n" |
| : : "r"(msr) : "memory"); |
| |
| report(got_interrupt, "interrupt on single step"); |
| got_interrupt = false; |
| report(recorded_regs.nip == (unsigned long)trace_insn + 4, |
| "single step interrupt at the correct address"); |
| if (cpu_has_siar) |
| report(mfspr(SPR_SIAR) == (unsigned long)trace_insn, |
| "single step recorded SIAR at the correct address"); |
| |
| msr = mfmsr() | MSR_SE; |
| asm volatile( |
| " mtmsr %0 \n" |
| ".global trace_insn2 \n" |
| "trace_insn2: \n" |
| " b 1f \n" |
| " nop \n" |
| "1: \n" |
| : : "r"(msr) : "memory"); |
| |
| report(got_interrupt, "interrupt on single step branch"); |
| got_interrupt = false; |
| report(recorded_regs.nip == (unsigned long)trace_insn2 + 8, |
| "single step interrupt at the correct address"); |
| if (cpu_has_siar) |
| report(mfspr(SPR_SIAR) == (unsigned long)trace_insn2, |
| "single step recorded SIAR at the correct address"); |
| |
| msr = mfmsr() | MSR_BE; |
| asm volatile( |
| " mtmsr %0 \n" |
| ".global trace_insn3 \n" |
| "trace_insn3: \n" |
| " nop \n" |
| " b 1f \n" |
| " nop \n" |
| "1: \n" |
| : : "r"(msr) : "memory"); |
| |
| report(got_interrupt, "interrupt on branch trace"); |
| got_interrupt = false; |
| report(recorded_regs.nip == (unsigned long)trace_insn3 + 12, |
| "branch trace interrupt at the correct address"); |
| if (cpu_has_siar) |
| report(mfspr(SPR_SIAR) == (unsigned long)trace_insn3 + 4, |
| "branch trace recorded SIAR at the correct address"); |
| |
| handle_exception(0x700, &program_trace_handler, NULL); |
| msr = mfmsr() | MSR_SE; |
| asm volatile( |
| " mtmsr %0 \n" |
| " trap \n" |
| : : "r"(msr) : "memory"); |
| |
| report(!got_interrupt, "no interrupt on single step trap"); |
| got_interrupt = false; |
| handle_exception(0x700, NULL, NULL); |
| |
| msr = mfmsr() | MSR_SE; |
| mtspr(SPR_SRR0, (unsigned long)trace_rfid); |
| mtspr(SPR_SRR1, mfmsr()); |
| asm volatile( |
| " mtmsr %0 \n" |
| " rfid \n" |
| ".global trace_rfid \n" |
| "trace_rfid: \n" |
| : : "r"(msr) : "memory"); |
| |
| report(!got_interrupt, "no interrupt on single step rfid"); |
| got_interrupt = false; |
| handle_exception(0xd00, NULL, NULL); |
| |
| report_prefix_pop(); |
| } |
| |
| |
| int main(int argc, char **argv) |
| { |
| report_prefix_push("interrupts"); |
| |
| if (vm_available()) |
| setup_vm(); |
| |
| if (cpu_has_power_mce) |
| test_mce(); |
| test_mmu(); |
| test_illegal(); |
| test_privileged(); |
| test_dec(); |
| test_sc(); |
| test_trace(); |
| |
| report_prefix_pop(); |
| |
| return report_summary(); |
| } |