diff --git a/executor/common_kvm_amd64.h b/executor/common_kvm_amd64.h index 170d658da195..b631c1dd2dc4 100644 --- a/executor/common_kvm_amd64.h +++ b/executor/common_kvm_amd64.h @@ -1126,7 +1126,7 @@ static void install_syzos_code(void* host_mem, size_t mem_size) { size_t size = (char*)&__stop_guest - (char*)&__start_guest; if (size > mem_size) - fail("SyzOS size exceeds guest memory"); + fail("SYZOS size exceeds guest memory"); memcpy(host_mem, &__start_guest, size); } diff --git a/executor/common_kvm_amd64_syzos.h b/executor/common_kvm_amd64_syzos.h index 5584a62b99c4..d184dd4c2a71 100644 --- a/executor/common_kvm_amd64_syzos.h +++ b/executor/common_kvm_amd64_syzos.h @@ -6,11 +6,12 @@ // This file provides guest code running inside the AMD64 KVM. -#include "common_kvm_syzos.h" -#include "kvm.h" #include #include +#include "common_kvm_syzos.h" +#include "kvm.h" + // There are no particular rules to assign numbers here, but changing them will // result in losing some existing reproducers. Therefore, we try to leave spaces // between unrelated IDs. @@ -43,16 +44,6 @@ typedef enum { SYZOS_API_STOP, // Must be the last one } syzos_api_id; -struct api_call_header { - uint64 call; - uint64 size; -}; - -struct api_call_uexit { - struct api_call_header header; - uint64 exit_code; -}; - struct api_call_code { struct api_call_header header; uint8 insns[]; @@ -70,26 +61,6 @@ struct api_call_cpuid { uint32 ecx; }; -struct api_call_1 { - struct api_call_header header; - uint64 arg; -}; - -struct api_call_2 { - struct api_call_header header; - uint64 args[2]; -}; - -struct api_call_3 { - struct api_call_header header; - uint64 args[3]; -}; - -struct api_call_5 { - struct api_call_header header; - uint64 args[5]; -}; - // This struct must match the push/pop order in nested_vm_exit_handler_intel_asm(). struct l2_guest_regs { uint64 rax, rbx, rcx, rdx, rsi, rdi, rbp; @@ -165,8 +136,8 @@ __attribute__((naked)) GUEST_CODE static void uexit_irq_handler() // TODO(glider): executor/style_test.go insists that single-line compound statements should not // be used e.g. in the following case: // if (call == SYZOS_API_UEXIT) { -// struct api_call_uexit* ucmd = (struct api_call_uexit*)cmd; -// guest_uexit(ucmd->exit_code); +// struct api_call_1* ccmd = (struct api_call_1*)cmd; +// guest_uexit(ccmd->arg); // } else if (call == SYZOS_API_WR_CRN) { // guest_handle_wr_crn((struct api_call_2*)cmd); // Style check fails here // } @@ -188,8 +159,8 @@ guest_main(uint64 size, uint64 cpu) volatile uint64 call = cmd->call; if (call == SYZOS_API_UEXIT) { // Issue a user exit. - struct api_call_uexit* ucmd = (struct api_call_uexit*)cmd; - guest_uexit(ucmd->exit_code); + struct api_call_1* ccmd = (struct api_call_1*)cmd; + guest_uexit(ccmd->arg); } else if (call == SYZOS_API_CODE) { // Execute an instruction blob. struct api_call_code* ccmd = (struct api_call_code*)cmd; diff --git a/executor/common_kvm_arm64.h b/executor/common_kvm_arm64.h index 58678a9df798..6442cb49110c 100644 --- a/executor/common_kvm_arm64.h +++ b/executor/common_kvm_arm64.h @@ -70,8 +70,8 @@ static void vm_set_user_memory_region(int vmfd, uint32 slot, uint32 flags, uint6 #define ADRP_OPCODE 0x90000000 #define ADRP_OPCODE_MASK 0x9f000000 -// Code loading SyzOS into guest memory does not handle data relocations (see -// https://github.com/google/syzkaller/issues/5565), so SyzOS will crash soon after encountering an +// Code loading SYZOS into guest memory does not handle data relocations (see +// https://github.com/google/syzkaller/issues/5565), so SYZOS will crash soon after encountering an // ADRP instruction. Detect these instructions to catch regressions early. // The most common reason for using data relocaions is accessing global variables and constants. // Sometimes the compiler may choose to emit a read-only constant to zero-initialize a structure @@ -81,7 +81,7 @@ static void validate_guest_code(void* mem, size_t size) uint32* insns = (uint32*)mem; for (size_t i = 0; i < size / 4; i++) { if ((insns[i] & ADRP_OPCODE_MASK) == ADRP_OPCODE) - fail("ADRP instruction detected in SyzOS, exiting"); + fail("ADRP instruction detected in SYZOS, exiting"); } } @@ -89,7 +89,7 @@ static void install_syzos_code(void* host_mem, size_t mem_size) { size_t size = (char*)&__stop_guest - (char*)&__start_guest; if (size > mem_size) - fail("SyzOS size exceeds guest memory"); + fail("SYZOS size exceeds guest memory"); memcpy(host_mem, &__start_guest, size); validate_guest_code(host_mem, size); } diff --git a/executor/common_kvm_arm64_syzos.h b/executor/common_kvm_arm64_syzos.h index 2ef7819d9470..f03e0da7d11b 100644 --- a/executor/common_kvm_arm64_syzos.h +++ b/executor/common_kvm_arm64_syzos.h @@ -6,11 +6,12 @@ // This file provides guest code running inside the ARM64 KVM. -#include "common_kvm_syzos.h" -#include "kvm.h" #include #include +#include "common_kvm_syzos.h" +#include "kvm.h" + // Compilers will eagerly try to transform the switch statement in guest_main() // into a jump table, unless the cases are sparse enough. // We use prime numbers multiplied by 10 to prevent this behavior. @@ -31,31 +32,6 @@ typedef enum { SYZOS_API_STOP, // Must be the last one } syzos_api_id; -struct api_call_header { - uint64 call; - uint64 size; -}; - -struct api_call_uexit { - struct api_call_header header; - uint64 exit_code; -}; - -struct api_call_1 { - struct api_call_header header; - uint64 arg; -}; - -struct api_call_2 { - struct api_call_header header; - uint64 args[2]; -}; - -struct api_call_3 { - struct api_call_header header; - uint64 args[3]; -}; - struct api_call_code { struct api_call_header header; uint32 insns[]; @@ -127,8 +103,8 @@ guest_main(uint64 size, uint64 cpu) return; switch (cmd->call) { case SYZOS_API_UEXIT: { - struct api_call_uexit* ucmd = (struct api_call_uexit*)cmd; - guest_uexit(ucmd->exit_code); + struct api_call_1* ccmd = (struct api_call_1*)cmd; + guest_uexit(ccmd->arg); break; } case SYZOS_API_CODE: { @@ -1219,7 +1195,7 @@ GUEST_CODE static void its_send_sync_cmd(uint64 cmdq_base, uint32 vcpu_id) } // This function is carefully written in a way that prevents jump table emission. -// SyzOS cannot reference global constants, and compilers are very eager to generate a jump table +// SYZOS cannot reference global constants, and compilers are very eager to generate a jump table // for a switch over GITS commands. // To work around that, we replace the switch statement with a series of if statements. // In addition, cmd->type is stored in a volatile variable, so that it is read on each if statement, diff --git a/executor/common_kvm_riscv64.h b/executor/common_kvm_riscv64.h index 500240638c8b..b9ceef5d1a09 100644 --- a/executor/common_kvm_riscv64.h +++ b/executor/common_kvm_riscv64.h @@ -12,13 +12,14 @@ #include #include -#if SYZ_EXECUTOR || __NR_syz_kvm_setup_cpu -struct kvm_text { - uintptr_t type; - const void* text; - uintptr_t size; -}; +#include "common_kvm.h" +#include "kvm.h" +#if SYZ_EXECUTOR || __NR_syz_kvm_setup_syzos_vm || __NR_syz_kvm_add_vcpu +#include "common_kvm_riscv64_syzos.h" +#endif + +#if SYZ_EXECUTOR || __NR_syz_kvm_setup_cpu || __NR_syz_kvm_add_vcpu // Construct RISC-V register id for KVM. #define RISCV_CORE_REG(idx) (KVM_REG_RISCV | KVM_REG_SIZE_U64 | KVM_REG_RISCV_CORE | (idx)) #define RISCV_CSR_REG(idx) (KVM_REG_RISCV | KVM_REG_SIZE_U64 | KVM_REG_RISCV_CSR | (idx)) @@ -83,9 +84,6 @@ enum riscv_core_index { // Indicate the Supervisor Interrupt Enable state. #define SSTATUS_SIE (1UL << 1) -// Define the starting physical address for the guest code. -#define CODE_START 0x80000000ULL - // Set a single register value for the specified CPU file descriptor. static inline int kvm_set_reg(int cpufd, unsigned long id, unsigned long value) { @@ -96,6 +94,61 @@ static inline int kvm_set_reg(int cpufd, unsigned long id, unsigned long value) return ioctl(cpufd, KVM_SET_ONE_REG, ®); } +struct kvm_text { + uintptr_t type; + const void* text; + uintptr_t size; +}; +#endif + +#if SYZ_EXECUTOR || __NR_syz_kvm_setup_cpu || __NR_syz_kvm_setup_syzos_vm || __NR_syz_kvm_add_vcpu +// The exception vector table setup and SBI invocation here follow the +// implementation in Linux kselftest KVM RISC-V tests. +// See https://elixir.bootlin.com/linux/v6.19-rc5/source/tools/testing/selftests/kvm/lib/riscv/processor.c#L337 . + +#define KVM_RISCV_SELFTESTS_SBI_EXT 0x08FFFFFF +#define KVM_RISCV_SELFTESTS_SBI_UNEXP 1 + +struct sbiret { + long error; + long value; +}; + +struct sbiret sbi_ecall(unsigned long arg0, unsigned long arg1, + unsigned long arg2, unsigned long arg3, + unsigned long arg4, unsigned long arg5, + int fid, int ext) +{ + struct sbiret ret; + + register unsigned long a0 asm("a0") = arg0; + register unsigned long a1 asm("a1") = arg1; + register unsigned long a2 asm("a2") = arg2; + register unsigned long a3 asm("a3") = arg3; + register unsigned long a4 asm("a4") = arg4; + register unsigned long a5 asm("a5") = arg5; + register unsigned long a6 asm("a6") = fid; + register unsigned long a7 asm("a7") = ext; + asm volatile("ecall" + : "+r"(a0), "+r"(a1) + : "r"(a2), "r"(a3), "r"(a4), "r"(a5), "r"(a6), "r"(a7) + : "memory"); + ret.error = a0; + ret.value = a1; + + return ret; +} + +__attribute__((used)) __attribute((__aligned__(16))) static void guest_unexp_trap(void) +{ + sbi_ecall(0, 0, 0, 0, 0, 0, + KVM_RISCV_SELFTESTS_SBI_UNEXP, + KVM_RISCV_SELFTESTS_SBI_EXT); +} +#endif + +#if SYZ_EXECUTOR || __NR_syz_kvm_setup_cpu + // syz_kvm_setup_cpu$riscv64(fd fd_kvmvm, cpufd fd_kvmcpu, usermem vma[24], text ptr[in, array[kvm_text_riscv64, 1]], ntext len[text], flags const[0], opts ptr[in, array[kvm_setup_opt_riscv64, 1]], nopt len[opts]) static volatile long syz_kvm_setup_cpu(volatile long a0, volatile long a1, volatile long a2, volatile long a3, volatile long a4, volatile long a5, volatile long a6, volatile long a7) { @@ -113,7 +166,7 @@ static volatile long syz_kvm_setup_cpu(volatile long a0, volatile long a1, volat struct kvm_userspace_memory_region mem = { .slot = (unsigned int)i, .flags = 0, - .guest_phys_addr = CODE_START + i * page_size, + .guest_phys_addr = RISCV64_ADDR_USER_CODE + i * page_size, .memory_size = page_size, .userspace_addr = (uintptr_t)(host_mem + i * page_size), @@ -131,13 +184,14 @@ static volatile long syz_kvm_setup_cpu(volatile long a0, volatile long a1, volat if (size > guest_mem_size) size = guest_mem_size; memcpy(host_mem, text, size); + memcpy(host_mem + page_size, (void*)guest_unexp_trap, KVM_PAGE_SIZE); // Initialize VCPU registers. // Set PC (program counter) to start of code. - if (kvm_set_reg(cpufd, RISCV_CORE_REG(CORE_PC), CODE_START)) + if (kvm_set_reg(cpufd, RISCV_CORE_REG(CORE_PC), RISCV64_ADDR_USER_CODE)) return -1; // Set SP (stack pointer) at end of memory, reserving space for stack. - unsigned long stack_top = CODE_START + guest_mem_size - page_size; + unsigned long stack_top = RISCV64_ADDR_USER_CODE + guest_mem_size - page_size; if (kvm_set_reg(cpufd, RISCV_CORE_REG(CORE_SP), stack_top)) return -1; // Set privilege mode to S-mode. @@ -148,9 +202,17 @@ static volatile long syz_kvm_setup_cpu(volatile long a0, volatile long a1, volat if (kvm_set_reg(cpufd, RISCV_CSR_REG(CSR_SSTATUS), sstatus)) return -1; // Set STVEC. - unsigned long stvec = CODE_START + page_size; + unsigned long stvec = RISCV64_ADDR_USER_CODE + page_size; if (kvm_set_reg(cpufd, RISCV_CSR_REG(CSR_STVEC), stvec)) return -1; + // Set GP. + unsigned long current_gp = 0; + asm volatile("add %0, gp, zero" + : "=r"(current_gp) + : + : "memory"); + if (kvm_set_reg(cpufd, RISCV_CORE_REG(CORE_GP), current_gp)) + return -1; return 0; } @@ -175,4 +237,252 @@ static long syz_kvm_assert_reg(volatile long a0, volatile long a1, volatile long } #endif -#endif // EXECUTOR_COMMON_KVM_RISCV64_H \ No newline at end of file +#if SYZ_EXECUTOR || __NR_syz_kvm_setup_syzos_vm || __NR_syz_kvm_add_vcpu +struct kvm_syz_vm { + int vmfd; + int next_cpu_id; + void* host_mem; + size_t total_pages; + void* user_text; +}; +#endif + +#if SYZ_EXECUTOR || __NR_syz_kvm_setup_syzos_vm +struct addr_size { + void* addr; + size_t size; +}; + +static struct addr_size alloc_guest_mem(struct addr_size* free, size_t size) +{ + struct addr_size ret = {.addr = NULL, .size = 0}; + + if (free->size < size) + return ret; + ret.addr = free->addr; + ret.size = size; + free->addr = (void*)((char*)free->addr + size); + free->size -= size; + return ret; +} + +// Call KVM_SET_USER_MEMORY_REGION for the given pages. +static void vm_set_user_memory_region(int vmfd, uint32 slot, uint32 flags, uint64 guest_phys_addr, uint64 memory_size, uint64 userspace_addr) +{ + struct kvm_userspace_memory_region memreg; + memreg.slot = slot; + memreg.flags = flags; + memreg.guest_phys_addr = guest_phys_addr; + memreg.memory_size = memory_size; + memreg.userspace_addr = userspace_addr; + ioctl(vmfd, KVM_SET_USER_MEMORY_REGION, &memreg); +} + +#define AUIPC_OPCODE 0x17 +#define AUIPC_OPCODE_MASK 0x7f + +// Code loading SYZOS into guest memory does not handle data relocations (see +// https://github.com/google/syzkaller/issues/5565), so SYZOS will crash soon after encountering an +// AUIPC instruction. Detect these instructions to catch regressions early. +// The most common reason for using data relocaions is accessing global variables and constants. +// Sometimes the compiler may choose to emit a read-only constant to zero-initialize a structure +// or to generate a jump table for a switch statement. +static void validate_guest_code(void* mem, size_t size) +{ + uint32* insns = (uint32*)mem; + for (size_t i = 0; i < size / 4; i++) { + if ((insns[i] & AUIPC_OPCODE_MASK) == AUIPC_OPCODE) + fail("AUIPC instruction detected in SYZOS, exiting"); + } +} + +static void install_syzos_code(void* host_mem, size_t mem_size) +{ + size_t size = (char*)&__stop_guest - (char*)&__start_guest; + if (size > mem_size) + fail("SYZOS size exceeds guest memory"); + memcpy(host_mem, &__start_guest, size); + validate_guest_code(host_mem, size); +} + +// Flags for mem_region. +#define MEM_REGION_FLAG_USER_CODE (1 << 0) +#define MEM_REGION_FLAG_DIRTY_LOG (1 << 1) +#define MEM_REGION_FLAG_READONLY (1 << 2) +#define MEM_REGION_FLAG_EXECUTOR_CODE (1 << 3) +#define MEM_REGION_FLAG_EXCEPTION_VEC (1 << 4) +#define MEM_REGION_FLAG_NO_HOST_MEM (1 << 6) + +struct mem_region { + uint64 gpa; + int pages; + uint32 flags; +}; + +// SYZOS guest virtual memory layout (must be in sync with executor/kvm.h): +static const struct mem_region syzos_mem_regions[] = { + // Exception vector table (1 page at 0x1000). + {RISCV64_ADDR_EXCEPTION_VECTOR, 1, MEM_REGION_FLAG_READONLY | MEM_REGION_FLAG_EXCEPTION_VEC}, + // CLINT at 0x02000000 (MMIO, no memory). + {RISCV64_ADDR_CLINT, 1, MEM_REGION_FLAG_NO_HOST_MEM}, + // PLIC at 0x0c000000 (MMIO, no memory). + {RISCV64_ADDR_PLIC, 1, MEM_REGION_FLAG_NO_HOST_MEM}, + // Unmapped region to trigger page faults (1 page at 0x40000000). + {RISCV64_ADDR_EXIT, 1, MEM_REGION_FLAG_NO_HOST_MEM}, + // Writable region with KVM_MEM_LOG_DIRTY_PAGES (2 pages). + {RISCV64_ADDR_DIRTY_PAGES, 2, MEM_REGION_FLAG_DIRTY_LOG}, + // User code (KVM_MAX_VCPU pages, starting at 0x80000000). + {RISCV64_ADDR_USER_CODE, KVM_MAX_VCPU, MEM_REGION_FLAG_READONLY | MEM_REGION_FLAG_USER_CODE}, + // Executor guest code (4 pages). + {SYZOS_ADDR_EXECUTOR_CODE, 4, MEM_REGION_FLAG_READONLY | MEM_REGION_FLAG_EXECUTOR_CODE}, + // Scratch memory for runtime code (1 page). + {RISCV64_ADDR_SCRATCH_CODE, 1, 0}, + // Per-vCPU stacks (1 page). + {RISCV64_ADDR_STACK_BASE, 1, 0}, +}; + +static void setup_vm(int vmfd, struct kvm_syz_vm* vm) +{ + struct addr_size allocator = {.addr = vm->host_mem, .size = vm->total_pages * KVM_PAGE_SIZE}; + int slot = 0; // Slot numbers do not matter, they just have to be different. + + for (size_t i = 0; i < sizeof(syzos_mem_regions) / sizeof(syzos_mem_regions[0]); i++) { + const struct mem_region* r = &syzos_mem_regions[i]; + if (r->flags & MEM_REGION_FLAG_NO_HOST_MEM) + continue; + struct addr_size next = alloc_guest_mem(&allocator, r->pages * KVM_PAGE_SIZE); + uint32 flags = 0; + if (r->flags & MEM_REGION_FLAG_DIRTY_LOG) + flags |= KVM_MEM_LOG_DIRTY_PAGES; + if (r->flags & MEM_REGION_FLAG_READONLY) + flags |= KVM_MEM_READONLY; + if (r->flags & MEM_REGION_FLAG_USER_CODE) + vm->user_text = next.addr; + if (r->flags & MEM_REGION_FLAG_EXCEPTION_VEC) + memcpy(next.addr, (void*)guest_unexp_trap, KVM_PAGE_SIZE); + if (r->flags & MEM_REGION_FLAG_EXECUTOR_CODE) + install_syzos_code(next.addr, next.size); + vm_set_user_memory_region(vmfd, slot++, flags, r->gpa, next.size, (uintptr_t)next.addr); + } + + // Map the remaining pages at an unused address. + if (allocator.size > 0) { + struct addr_size next = alloc_guest_mem(&allocator, allocator.size); + vm_set_user_memory_region(vmfd, slot++, 0, 0, next.size, (uintptr_t)next.addr); + } +} + +static long syz_kvm_setup_syzos_vm(volatile long a0, volatile long a1) +{ + const int vmfd = a0; + void* host_mem = (void*)a1; + struct kvm_syz_vm* ret = (struct kvm_syz_vm*)host_mem; + ret->host_mem = (void*)((uint64)host_mem + KVM_PAGE_SIZE); + ret->total_pages = KVM_GUEST_PAGES - 1; + setup_vm(vmfd, ret); + ret->vmfd = vmfd; + ret->next_cpu_id = 0; + + return (long)ret; +} +#endif + +#if SYZ_EXECUTOR || __NR_syz_kvm_add_vcpu +// Set up CPU registers. +static void reset_cpu_regs(int cpufd, int cpu_id, size_t text_size) +{ + // PC points to the relative offset of guest_main() within the guest code. + kvm_set_reg(cpufd, RISCV_CORE_REG(CORE_PC), executor_fn_guest_addr(guest_main)); + kvm_set_reg(cpufd, RISCV_CORE_REG(CORE_SP), RISCV64_ADDR_STACK_BASE + KVM_PAGE_SIZE - 128); + kvm_set_reg(cpufd, RISCV_CORE_REG(CORE_TP), cpu_id); + // Pass parameters to guest_main(). + kvm_set_reg(cpufd, RISCV_CORE_REG(CORE_A0), text_size); + kvm_set_reg(cpufd, RISCV_CORE_REG(CORE_A1), cpu_id); + // Set SSTATUS and MODE. + kvm_set_reg(cpufd, RISCV_CORE_REG(CORE_MODE), 1); + kvm_set_reg(cpufd, RISCV_CSR_REG(CSR_SSTATUS), SSTATUS_SPP | SSTATUS_SPIE); + // Set GP. + unsigned long current_gp = 0; + asm volatile("add %0, gp, zero" + : "=r"(current_gp) + : + : "memory"); + kvm_set_reg(cpufd, RISCV_CORE_REG(CORE_GP), current_gp); + // Set STVEC. + kvm_set_reg(cpufd, RISCV_CSR_REG(CSR_STVEC), RISCV64_ADDR_EXCEPTION_VECTOR); +} + +static void install_user_code(int cpufd, void* user_text_slot, int cpu_id, const void* text, size_t text_size) +{ + if ((cpu_id < 0) || (cpu_id >= KVM_MAX_VCPU)) + return; + if (!user_text_slot) + return; + if (text_size > KVM_PAGE_SIZE) + text_size = KVM_PAGE_SIZE; + void* target = (void*)((uint64)user_text_slot + (KVM_PAGE_SIZE * cpu_id)); + memcpy(target, text, text_size); + reset_cpu_regs(cpufd, cpu_id, text_size); +} + +static long syz_kvm_add_vcpu(volatile long a0, volatile long a1, volatile long a2, volatile long a3) +{ + struct kvm_syz_vm* vm = (struct kvm_syz_vm*)a0; + struct kvm_text* utext = (struct kvm_text*)a1; + const void* text = utext->text; + size_t text_size = utext->size; + + if (!vm) { + errno = EINVAL; + return -1; + } + if (vm->next_cpu_id == KVM_MAX_VCPU) { + errno = ENOMEM; + return -1; + } + int cpu_id = vm->next_cpu_id; + int cpufd = ioctl(vm->vmfd, KVM_CREATE_VCPU, cpu_id); + if (cpufd == -1) + return -1; + // Only increment next_cpu_id if CPU creation succeeded. + vm->next_cpu_id++; + install_user_code(cpufd, vm->user_text, cpu_id, text, text_size); + return cpufd; +} +#endif + +#if SYZ_EXECUTOR || __NR_syz_kvm_assert_syzos_uexit +static long syz_kvm_assert_syzos_uexit(volatile long a0, volatile long a1, + volatile long a2) +{ +#if !SYZ_EXECUTOR + int cpufd = (int)a0; +#endif + struct kvm_run* run = (struct kvm_run*)a1; + uint64 expect = a2; + + if (!run || (run->exit_reason != KVM_EXIT_MMIO) || + (run->mmio.phys_addr != RISCV64_ADDR_UEXIT)) { +#if !SYZ_EXECUTOR + fprintf(stderr, "[SYZOS-DEBUG] Assertion Triggered on VCPU %d\n", cpufd); +#endif + errno = EINVAL; + return -1; + } + + uint64_t actual_code = ((uint64_t*)(run->mmio.data))[0]; + if (actual_code != expect) { +#if !SYZ_EXECUTOR + fprintf(stderr, "[SYZOS-DEBUG] Exit Code Mismatch on VCPU %d\n", cpufd); + fprintf(stderr, " Expected: 0x%lx\n", (unsigned long)expect); + fprintf(stderr, " Actual: 0x%lx\n", + (unsigned long)actual_code); +#endif + errno = EDOM; + return -1; + } + return 0; +} +#endif + +#endif // EXECUTOR_COMMON_KVM_RISCV64_H diff --git a/executor/common_kvm_riscv64_syzos.h b/executor/common_kvm_riscv64_syzos.h new file mode 100644 index 000000000000..6cc36d31f452 --- /dev/null +++ b/executor/common_kvm_riscv64_syzos.h @@ -0,0 +1,148 @@ +// Copyright 2026 syzkaller project authors. All rights reserved. +// Use of this source code is governed by Apache 2 LICENSE that can be found in the LICENSE file. + +#ifndef EXECUTOR_COMMON_KVM_RISCV64_SYZOS_H +#define EXECUTOR_COMMON_KVM_RISCV64_SYZOS_H + +// This file provides guest code running inside the RISCV64 KVM. + +#include + +#include "common_kvm_syzos.h" +#include "kvm.h" + +// Remember these constants must match those in sys/linux/dev_kvm_riscv64.txt. +typedef enum { + SYZOS_API_UEXIT = 0, + SYZOS_API_CODE = 10, + SYZOS_API_CSRR = 100, + SYZOS_API_CSRW = 101, + SYZOS_API_STOP, // Must be the last one +} syzos_api_id; + +struct api_call_code { + struct api_call_header header; + uint32 insns[]; +}; + +GUEST_CODE static void guest_uexit(uint64 exit_code); +GUEST_CODE static void guest_execute_code(uint32* insns, uint64 size); +GUEST_CODE static void guest_handle_csrr(uint32 csr); +GUEST_CODE static void guest_handle_csrw(uint32 csr, uint64 val); + +// Main guest function that performs necessary setup and passes the control to the user-provided +// payload. +// The inner loop uses a complex if-statement, because Clang is eager to insert a jump table into +// a switch statement. +// We add single-line comments to justify having the compound statements below. +__attribute__((used)) +GUEST_CODE static void +guest_main(uint64 size, uint64 cpu) +{ + uint64 addr = RISCV64_ADDR_USER_CODE + cpu * 0x1000; + + while (size >= sizeof(struct api_call_header)) { + struct api_call_header* cmd = (struct api_call_header*)addr; + if (cmd->call >= SYZOS_API_STOP) + return; + if (cmd->size > size) + return; + volatile uint64 call = cmd->call; + if (call == SYZOS_API_UEXIT) { + // Issue a user exit. + struct api_call_1* ccmd = (struct api_call_1*)cmd; + guest_uexit(ccmd->arg); + } else if (call == SYZOS_API_CODE) { + // Execute an instruction blob. + struct api_call_code* ccmd = (struct api_call_code*)cmd; + guest_execute_code(ccmd->insns, cmd->size - sizeof(struct api_call_header)); + } else if (call == SYZOS_API_CSRR) { + // Execute a csrr instruction. + struct api_call_1* ccmd = (struct api_call_1*)cmd; + guest_handle_csrr(ccmd->arg); + } else if (call == SYZOS_API_CSRW) { + // Execute a csrw instruction. + struct api_call_2* ccmd = (struct api_call_2*)cmd; + guest_handle_csrw(ccmd->args[0], ccmd->args[1]); + } + addr += cmd->size; + size -= cmd->size; + }; + guest_uexit((uint64)-1); +} + +// Perform a userspace exit that can be handled by the host. +// The host returns from ioctl(KVM_RUN) with kvm_run.exit_reason=KVM_EXIT_MMIO, +// and can handle the call depending on the data passed as exit code. +GUEST_CODE static noinline void guest_uexit(uint64 exit_code) +{ + volatile uint64* ptr = (volatile uint64*)RISCV64_ADDR_UEXIT; + *ptr = exit_code; +} + +GUEST_CODE static noinline void guest_execute_code(uint32* insns, uint64 size) +{ + asm volatile("fence.i" :: + : "memory"); + volatile void (*fn)() = (volatile void (*)())insns; + fn(); +} + +// Host sets CORE_TP to contain the virtual CPU id. +GUEST_CODE static uint32 get_cpu_id() +{ + uint64 val = 0; + asm volatile("mv %0, tp" + : "=r"(val)); + return (uint32)val; +} + +#define MAX_CACHE_LINE_SIZE 256 +#define RISCV_OPCODE_SYSTEM 0x73 +#define FUNCT3_CSRRW 0x1 +#define FUNCT3_CSRRS 0x2 +#define REG_ZERO 0 +#define REG_A0 10 +#define ENCODE_CSR_INSN(csr, rs1, funct3, rd) \ + (((csr) << 20) | ((rs1) << 15) | ((funct3) << 12) | ((rd) << 7) | RISCV_OPCODE_SYSTEM) + +GUEST_CODE static noinline void +guest_handle_csrr(uint32 csr) +{ + uint32 cpu_id = get_cpu_id(); + // Make sure CPUs use different cache lines for scratch code. + uint32* insn = (uint32*)((uint64)RISCV64_ADDR_SCRATCH_CODE + cpu_id * MAX_CACHE_LINE_SIZE); + // insn[0] - csrr a0, csr + // insn[1] - ret + insn[0] = ENCODE_CSR_INSN(csr, REG_ZERO, FUNCT3_CSRRS, REG_A0); + insn[1] = 0x00008067; + asm volatile("fence.i" :: + : "memory"); + asm volatile( + "jalr ra, 0(%0)" + : + : "r"(insn) + : "ra", "memory"); +} + +GUEST_CODE static noinline void +guest_handle_csrw(uint32 csr, uint64 val) +{ + uint32 cpu_id = get_cpu_id(); + // Make sure CPUs use different cache lines for scratch code. + uint32* insn = (uint32*)((uint64)RISCV64_ADDR_SCRATCH_CODE + cpu_id * MAX_CACHE_LINE_SIZE); + // insn[0] - csrw csr, a0 + // insn[1] - ret + insn[0] = ENCODE_CSR_INSN(csr, REG_A0, FUNCT3_CSRRW, REG_ZERO); + insn[1] = 0x00008067; + asm volatile("fence.i" :: + : "memory"); + asm volatile( + "mv a0, %0\n" + "jalr ra, 0(%1)" + : + : "r"(val), "r"(insn) + : "a0", "ra", "memory"); +} + +#endif // EXECUTOR_COMMON_KVM_RISCV64_SYZOS_H \ No newline at end of file diff --git a/executor/common_kvm_syzos.h b/executor/common_kvm_syzos.h index 923284efe9d2..ad32064a6b0e 100644 --- a/executor/common_kvm_syzos.h +++ b/executor/common_kvm_syzos.h @@ -50,4 +50,30 @@ // Start/end of the guest section. extern char *__start_guest, *__stop_guest; +// Common SYZOS API call descriptors. +struct api_call_header { + uint64 call; + uint64 size; +}; + +struct api_call_1 { + struct api_call_header header; + uint64 arg; +}; + +struct api_call_2 { + struct api_call_header header; + uint64 args[2]; +}; + +struct api_call_3 { + struct api_call_header header; + uint64 args[3]; +}; + +struct api_call_5 { + struct api_call_header header; + uint64 args[5]; +}; + #endif // EXECUTOR_COMMON_KVM_SYZOS_H diff --git a/executor/kvm.h b/executor/kvm.h index a390becbb26b..bcb3c066d4f7 100644 --- a/executor/kvm.h +++ b/executor/kvm.h @@ -532,4 +532,26 @@ #endif // ARM64 SYZOS definitions +// RISCV64 SYZOS definitions. +#if GOARCH_riscv64 +// Core Local INTerruptor address. +#define RISCV64_ADDR_CLINT 0x02000000 +// Platform Level Interrupt Controller address. +#define RISCV64_ADDR_PLIC 0x0c000000 +// Write to this page to trigger a page fault and stop KVM_RUN. +#define RISCV64_ADDR_EXIT 0x40000000 +// Two writable pages with KVM_MEM_LOG_DIRTY_PAGES explicitly set. +#define RISCV64_ADDR_DIRTY_PAGES 0x40001000 +#define RISCV64_ADDR_USER_CODE 0x80000000 +// Location of the SYZOS guest code. Name shared with x86 SYZOS. +#define SYZOS_ADDR_EXECUTOR_CODE 0x80008000 +#define RISCV64_ADDR_SCRATCH_CODE 0x80010000 +#define RISCV64_ADDR_STACK_BASE 0x80020000 +#define RISCV64_ADDR_EXCEPTION_VECTOR 0x00001000 + +// Dedicated address within the exit page for the uexit command. +#define RISCV64_ADDR_UEXIT (RISCV64_ADDR_EXIT + 256) + +#endif // RISCV64 SYZOS definitions + #endif // EXECUTOR_KVM_H diff --git a/executor/test_linux.h b/executor/test_linux.h index 6d36d201e949..aab3529b8037 100644 --- a/executor/test_linux.h +++ b/executor/test_linux.h @@ -314,7 +314,7 @@ static int test_syzos() printf("mmap failed (%d)\n", errno); return 1; } - // Right now SyzOS testing just boils down to installing code into memory. + // Right now SYZOS testing just boils down to installing code into memory. install_syzos_code(mem, mem_size); munmap(mem, mem_size); return 0; diff --git a/pkg/vminfo/linux_syscalls.go b/pkg/vminfo/linux_syscalls.go index c74d83d1bac4..8e5871ba3318 100644 --- a/pkg/vminfo/linux_syscalls.go +++ b/pkg/vminfo/linux_syscalls.go @@ -193,7 +193,9 @@ func linuxSyzKvmSupported(ctx *checkContext, call *prog.Syscall) string { if ctx.target.Arch == targets.ARM64 { return "" } - case "syz_kvm_setup_cpu$riscv64", "syz_kvm_assert_reg$riscv64": + case "syz_kvm_setup_cpu$riscv64", "syz_kvm_assert_reg$riscv64", "syz_kvm_setup_syzos_vm$riscv64", + "syz_kvm_add_vcpu$riscv64", "syz_kvm_assert_syzos_kvm_exit$riscv64", + "syz_kvm_assert_syzos_uexit$riscv64": if ctx.target.Arch == targets.RiscV64 { return "" } diff --git a/sys/linux/dev_kvm.txt b/sys/linux/dev_kvm.txt index 555089723053..0fa343ddecbb 100644 --- a/sys/linux/dev_kvm.txt +++ b/sys/linux/dev_kvm.txt @@ -639,7 +639,7 @@ kvm_regs_arm64_extra = 0x603000000013c01b, 0x603000000013c01f, 0x603000000013c02 # For riscv64, https://elixir.bootlin.com/linux/v6.19-rc4/source/Documentation/virt/kvm/api.rst#L2765 kvm_regs_riscv64_config = 0x8030000000100000, 0x8030000000100001, 0x8030000000100002, 0x8030000000100003, 0x8030000000100004, 0x8030000000100005, 0x8030000000100006, 0x8030000000100007 kvm_regs_riscv64_core = 0x8030000000200000, 0x8030000000200001, 0x8030000000200002, 0x8030000000200003, 0x8030000000200004, 0x8030000000200005, 0x8030000000200006, 0x8030000000200007, 0x8030000000200008, 0x8030000000200009, 0x803000000020000a, 0x803000000020000b, 0x803000000020000c, 0x803000000020000d, 0x803000000020000e, 0x803000000020000f, 0x8030000000200010, 0x8030000000200011, 0x8030000000200012, 0x8030000000200013, 0x8030000000200014, 0x8030000000200015, 0x8030000000200016, 0x8030000000200017, 0x8030000000200018, 0x8030000000200019, 0x803000000020001a, 0x803000000020001b, 0x803000000020001c, 0x803000000020001d, 0x803000000020001e, 0x803000000020001f, 0x8030000000200020 -kvm_regs_riscv64_csr = 0x8030000000300000, 0x8030000000300001, 0x8030000000300002, 0x8030000000300003, 0x8030000000300004, 0x8030000000300005, 0x8030000000300006, 0x8030000000300007, 0x8030000000300008 +kvm_regs_riscv64_csr = 0x8030000000300000, 0x8030000000300001, 0x8030000000300002, 0x8030000000300003, 0x8030000000300004, 0x8030000000300005, 0x8030000000300006, 0x8030000000300007, 0x8030000000300008, 0x8030000000300009, 0x803000000030000a kvm_regs_riscv64_timer = 0x8030000004000000, 0x8030000004000001, 0x8030000004000002, 0x8030000004000003 kvm_regs_riscv64_f = 0x8020000005000000, 0x8020000005000001, 0x8020000005000002, 0x8020000005000003, 0x8020000005000004, 0x8020000005000005, 0x8020000005000006, 0x8020000005000007, 0x8020000005000008, 0x8020000005000009, 0x802000000500000a, 0x802000000500000b, 0x802000000500000c, 0x802000000500000d, 0x802000000500000e, 0x802000000500000f, 0x8020000005000010, 0x8020000005000011, 0x8020000005000012, 0x8020000005000013, 0x8020000005000014, 0x8020000005000015, 0x8020000005000016, 0x8020000005000017, 0x8020000005000018, 0x8020000005000019, 0x802000000500001a, 0x802000000500001b, 0x802000000500001c, 0x802000000500001d, 0x802000000500001e, 0x802000000500001f, 0x8020000005000020 kvm_regs_riscv64_d = 0x8030000006000000, 0x8030000006000001, 0x8030000006000002, 0x8030000006000003, 0x8030000006000004, 0x8030000006000005, 0x8030000006000006, 0x8030000006000007, 0x8030000006000008, 0x8030000006000009, 0x803000000600000a, 0x803000000600000b, 0x803000000600000c, 0x803000000600000d, 0x803000000600000e, 0x803000000600000f, 0x8030000006000010, 0x8030000006000011, 0x8030000006000012, 0x8030000006000013, 0x8030000006000014, 0x8030000006000015, 0x8030000006000016, 0x8030000006000017, 0x8030000006000018, 0x8030000006000019, 0x803000000600001a, 0x803000000600001b, 0x803000000600001c, 0x803000000600001d, 0x803000000600001e, 0x803000000600001f, 0x8020000006000020 diff --git a/sys/linux/dev_kvm_riscv64.txt b/sys/linux/dev_kvm_riscv64.txt index 5e7e508a9697..c7ad437832a8 100644 --- a/sys/linux/dev_kvm_riscv64.txt +++ b/sys/linux/dev_kvm_riscv64.txt @@ -32,4 +32,53 @@ kvm_text_riscv64 { size len[text, intptr] } +# kvm_syz_vm is a VM handler used by syzos-related pseudo-syscalls. It is actually an opaque pointer under the hood. +resource kvm_syz_vm$riscv64[int64] + +# Map the given memory into the VM and set up syzos there. +syz_kvm_setup_syzos_vm$riscv64(fd fd_kvmvm, usermem vma[1024]) kvm_syz_vm$riscv64 + +# Create a VCPU inside a kvm_syz_vm VM. +# Prohibit flattening the input arguments, so that it is easier to reason about them. +syz_kvm_add_vcpu$riscv64(vm kvm_syz_vm$riscv64, text ptr[in, kvm_text_syzos_riscv64], opts ptr[in, array[kvm_setup_opt_riscv64, 1]], nopt len[opts]) fd_kvmcpu (no_squash) + +kvm_text_syzos_riscv64 { + typ const[0, intptr] + text ptr[in, array[syzos_api_call$riscv64, 1:32]] + size bytesize[text, int64] +} + +type syzos_api$riscv64[NUM, PAYLOAD] { + call const[NUM, int64] + size bytesize[parent, int64] + payload PAYLOAD +} + +syzos_api_code$riscv64 { + insns text[riscv64] + ret const[0x8067, int32] +} [packed] + +syzos_api_csrr { + arg_reg flags[riscv64_csr, int64] +} + +syzos_api_csrw { + arg_reg flags[riscv64_csr, int64] + arg_value int64 +} + +# Table 5 in https://docs.riscv.org/reference/isa/_attachments/riscv-privileged.pdf . +riscv64_csr = 0x100, 0x104, 0x105, 0x140, 0x141, 0x142, 0x143, 0x144, 0x180, 0x106, 0x10a + +syzos_api_call$riscv64 [ + uexit syzos_api$riscv64[0, intptr] + code syzos_api$riscv64[10, syzos_api_code$riscv64] + csrr syzos_api$riscv64[100, syzos_api_csrr] + csrw syzos_api$riscv64[101, syzos_api_csrw] +] [varlen] + +# Test assertions, will not be used by the fuzzer. syz_kvm_assert_reg$riscv64(fd fd_kvmcpu, reg int64, value int64) (no_generate) +syz_kvm_assert_syzos_uexit$riscv64(cpufd fd_kvmcpu, run kvm_run_ptr, exitcode int64) (no_generate) +syz_kvm_assert_syzos_kvm_exit$riscv64(run kvm_run_ptr, exitcode int64) (no_generate) diff --git a/sys/linux/test/riscv64-syz_kvm_setup_syzos_vm b/sys/linux/test/riscv64-syz_kvm_setup_syzos_vm new file mode 100644 index 000000000000..5942f1b9f26d --- /dev/null +++ b/sys/linux/test/riscv64-syz_kvm_setup_syzos_vm @@ -0,0 +1,35 @@ +# +# requires: arch=riscv64 -threaded +# + +r0 = openat$kvm(0, &AUTO='/dev/kvm\x00', 0x0, 0x0) +r1 = ioctl$KVM_CREATE_VM(r0, AUTO, 0x0) +r2 = syz_kvm_setup_syzos_vm$riscv64(r1, &(0x7f0000c00000/0x400000)=nil) + +# Perform two uexits. The first one is done via a code blob: +# lui a0, 0x40000 +# addi a0, a0, 0x100 # a0 = RISCV64_ADDR_UEXIT +# sd zero, 0(a0) +# ret +# The second uexit is done via a syzos API command that sets uexit exit code to 0xaaaa. +# +r3 = syz_kvm_add_vcpu$riscv64(r2, &AUTO={0x0, &AUTO=[@code={AUTO, AUTO, {"370500401305051023300500", 0x00008067}}, @uexit={AUTO, AUTO, 0xaaaa}], AUTO}, 0x0, 0x0) + +r4 = ioctl$KVM_GET_VCPU_MMAP_SIZE(r0, AUTO) +r5 = mmap$KVM_VCPU(&(0x7f0000009000/0x1000)=nil, r4, 0x3, 0x1, r3, 0x0) + +# Run till the first uexit. +# +ioctl$KVM_RUN(r3, AUTO, 0x0) +syz_kvm_assert_syzos_uexit$riscv64(r3, r5, 0x0) +# Run till the second uexit. +# +ioctl$KVM_RUN(r3, AUTO, 0x0) +syz_kvm_assert_syzos_uexit$riscv64(r3, r5, 0xaaaa) +# Run till the end of guest_main(). 0xffffffffffffffff is UEXIT_END. +# +ioctl$KVM_RUN(r3, AUTO, 0x0) +# Ensure that exit reason is KVM_EXIT_MMIO and uexit code is UEXIT_END. +# +syz_kvm_assert_syzos_kvm_exit$riscv64(r5, 0x6) +syz_kvm_assert_syzos_uexit$riscv64(r3, r5, 0xffffffffffffffff) diff --git a/sys/linux/test/riscv64-syz_kvm_setup_syzos_vm-csrr b/sys/linux/test/riscv64-syz_kvm_setup_syzos_vm-csrr new file mode 100644 index 000000000000..1b7fffda12f4 --- /dev/null +++ b/sys/linux/test/riscv64-syz_kvm_setup_syzos_vm-csrr @@ -0,0 +1,18 @@ +# +# requires: arch=riscv64 -threaded +# + +r0 = openat$kvm(0, &AUTO='/dev/kvm\x00', 0x0, 0x0) +r1 = ioctl$KVM_CREATE_VM(r0, AUTO, 0x0) +r2 = syz_kvm_setup_syzos_vm$riscv64(r1, &(0x7f0000c00000/0x400000)=nil) + +# Number 0x140 is sscratch (encode). +# +r3 = syz_kvm_add_vcpu$riscv64(r2, &AUTO={0x0, &AUTO=[@csrr={AUTO, AUTO, {0x140}}], AUTO}, 0x0, 0x0) +r4 = ioctl$KVM_GET_VCPU_MMAP_SIZE(r0, AUTO) +r5 = mmap$KVM_VCPU(&(0x7f0000009000/0x1000)=nil, r4, 0x3, 0x1, r3, 0x0) + +# Run till end of guest_main(). +# +ioctl$KVM_RUN(r3, AUTO, 0x0) +syz_kvm_assert_syzos_uexit$riscv64(r3, r5, 0xffffffffffffffff) diff --git a/sys/linux/test/riscv64-syz_kvm_setup_syzos_vm-csrw b/sys/linux/test/riscv64-syz_kvm_setup_syzos_vm-csrw new file mode 100644 index 000000000000..5cf3f4fc5f86 --- /dev/null +++ b/sys/linux/test/riscv64-syz_kvm_setup_syzos_vm-csrw @@ -0,0 +1,19 @@ +# +# requires: arch=riscv64 -threaded +# + +r0 = openat$kvm(0, &AUTO='/dev/kvm\x00', 0x0, 0x0) +r1 = ioctl$KVM_CREATE_VM(r0, AUTO, 0x0) +r2 = syz_kvm_setup_syzos_vm$riscv64(r1, &(0x7f0000c00000/0x400000)=nil) + +# Number 0x140 is sscratch (encode). +# +r3 = syz_kvm_add_vcpu$riscv64(r2, &AUTO={0x0, &AUTO=[@csrw={AUTO, AUTO, {0x140, 0xfefefee0}}], AUTO}, 0x0, 0x0) +r4 = ioctl$KVM_GET_VCPU_MMAP_SIZE(r0, AUTO) +r5 = mmap$KVM_VCPU(&(0x7f0000009000/0x1000)=nil, r4, 0x3, 0x1, r3, 0x0) +ioctl$KVM_RUN(r3, AUTO, 0x0) +syz_kvm_assert_syzos_uexit$riscv64(r3, r5, 0xffffffffffffffff) + +# ID 0x8030000003000003 is sscratch (kvm reg id). +# +syz_kvm_assert_reg$riscv64(r3, 0x8030000003000003, 0xfefefee0) diff --git a/tools/check-syzos.sh b/tools/check-syzos.sh index 163753f03dc6..304fac73bd53 100755 --- a/tools/check-syzos.sh +++ b/tools/check-syzos.sh @@ -54,6 +54,12 @@ elif [ "$TARGETARCH" = "arm64" ]; then if command -v aarch64-linux-gnu-objdump > /dev/null; then OBJDUMP_CMD="aarch64-linux-gnu-objdump" fi +elif [ "$TARGETARCH" = "riscv64" ]; then + ARCH="riscv64" + PATTERNS_TO_FIND='auipc' + if command -v riscv64-linux-gnu-objdump > /dev/null; then + OBJDUMP_CMD="riscv64-linux-gnu-objdump" + fi else echo "[INFO] Unsupported architecture '$TARGETARCH', skipping check." exit 0 @@ -151,7 +157,7 @@ if [ -n "$FOUND_INSTRUCTIONS" ]; then echo echo "------------------------------------------------------------------" echo "[FAIL] Found problematic data access instructions in '$SECTION_TO_CHECK'." - echo "The following instructions are likely to cause crashes in SyzOS:" + echo "The following instructions are likely to cause crashes in SYZOS:" echo "$FOUND_INSTRUCTIONS" | sed 's/^/ /' echo "------------------------------------------------------------------" echo