Enabling VTL1 on the boot processor requires handing Hyper-V a fully populated hv_init_vp_context describing the state VTL1 should start in: initial page tables, GDT/TSS, control registers and entry point. Add the arch-specific builder that assembles this context using the memory region reserved for the secure kernel.
Actual enablement of VTL1 using this context is done in a subsequent patch. Signed-off-by: Thara Gopinath <[email protected]> --- arch/x86/hyperv/Makefile | 1 + arch/x86/hyperv/hv_vtl_vsm.c | 258 ++++++++++++++++++++++++++++++++ arch/x86/include/asm/mshyperv.h | 9 ++ 3 files changed, 268 insertions(+) create mode 100644 arch/x86/hyperv/hv_vtl_vsm.c diff --git a/arch/x86/hyperv/Makefile b/arch/x86/hyperv/Makefile index 56292102af623..1fdc20e239243 100644 --- a/arch/x86/hyperv/Makefile +++ b/arch/x86/hyperv/Makefile @@ -2,6 +2,7 @@ obj-y := hv_init.o mmu.o nested.o irqdomain.o ivm.o obj-$(CONFIG_X86_64) += hv_apic.o obj-$(CONFIG_HYPERV_VTL_MODE) += hv_vtl.o mshv_vtl_asm.o +obj-$(CONFIG_HYPERV_VSM) += hv_vtl_vsm.o $(obj)/mshv_vtl_asm.o: $(obj)/mshv-asm-offsets.h diff --git a/arch/x86/hyperv/hv_vtl_vsm.c b/arch/x86/hyperv/hv_vtl_vsm.c new file mode 100644 index 0000000000000..edc55264c4d87 --- /dev/null +++ b/arch/x86/hyperv/hv_vtl_vsm.c @@ -0,0 +1,258 @@ +// SPDX-License-Identifier: GPL-2.0 +/* + * Architecture-specific bring-up state for the VTL1 secure kernel: build + * the page tables, GDT/TSS and initial vCPU register context that Hyper-V + * loads when transitioning the boot processor to VTL1. + * + * Copyright (c) 2025-2026, Microsoft Corporation. + * + * Author: Thara Gopinath <[email protected]> + */ + +#include <linux/align.h> +#include <linux/bits.h> +#include <linux/init.h> +#include <hyperv/vsm.h> +#include <asm/msr-index.h> +#include <asm/processor-flags.h> +#include <asm/mshyperv.h> + +/* Define PAGE size and related variables for initial secure kernel pages */ +#define VSM_PAGE_SHIFT 12 +#define VSM_PAGE_SIZE BIT(VSM_PAGE_SHIFT) +#define PAGE_AT(addr, idx) ((addr) + (idx) * VSM_PAGE_SIZE) +#define VSM_VA_FROM_PA(pa) (pa) /* Assumes identity mapping in secure kernel */ + +/* Number of entries in a page table (all levels) */ +#define VSM_ENTRIES_PER_PT 512 +#define VSM_PMD_SIZE (VSM_PAGE_SIZE * VSM_ENTRIES_PER_PT) + +/* + * Initial memory that will be mapped for secure kernel. + * Secure Kernel memory can be larger than this. + */ +#define VSM_SK_PTE_PAGES_COUNT (ALIGN(VSM_SK_INITIAL_MAP_SIZE, VSM_PMD_SIZE) / VSM_PMD_SIZE) + +/* VSM pages */ +enum { + VSM_GDT_PAGE, + VSM_TSS_PAGE, + VSM_PML4E_PAGE, + VSM_PDPE_PAGE, + VSM_PDE_PAGE, + VSM_PTE_PAGES, + /* PTE tables consume several pages */ + VSM_KERNEL_STACK_PAGE = VSM_PTE_PAGES + VSM_SK_PTE_PAGES_COUNT, + VSM_PAGES_COUNT +}; + +#define VSM_PT_FLAGS (_PAGE_PRESENT | _PAGE_RW) +#define VSM_PTE_FLAGS (VSM_PT_FLAGS | _PAGE_ACCESSED | _PAGE_DIRTY) + +/* Shifts to compute page table mapping */ +#define VSM_PD_TABLE_SHIFT 21 +#define VSM_PDP_TABLE_SHIFT 30 +#define VSM_PML4_TABLE_SHIFT 39 + +/* Given VA, get index into the page table at a given level */ +#define VSM_GET_PML4_INDEX(addr) (((addr) >> VSM_PML4_TABLE_SHIFT) & 0x1FF) +#define VSM_GET_PDP_INDEX(addr) (((addr) >> VSM_PDP_TABLE_SHIFT) & 0x1FF) +#define VSM_GET_PD_INDEX(addr) (((addr) >> VSM_PD_TABLE_SHIFT) & 0x1FF) + +static void __init hv_vsm_fill_pte_tables(phys_addr_t sk_pa, u64 *pde, + int pd_index, int num_pte_tables) +{ + u16 i, j; + phys_addr_t pte_pa; + u64 *pte; + + /* Fill page tables with entries */ + for (i = 0; i < num_pte_tables; i++) { + pte_pa = PAGE_AT(sk_pa, VSM_PTE_PAGES + i); + pte = phys_to_virt(pte_pa); + *(pde + pd_index + i) = pte_pa | VSM_PTE_FLAGS; + for (j = 0; j < VSM_ENTRIES_PER_PT; j++) { + *(pte + j) = + (sk_pa + ((j + (i * VSM_ENTRIES_PER_PT)) * VSM_PAGE_SIZE)) | + VSM_PTE_FLAGS; + } + } +} + +static void __init hv_vsm_init_page_tables(struct hv_init_vp_context *vp_ctx, phys_addr_t sk_pa) +{ + unsigned int pml4_index; + unsigned int pdp_index; + unsigned int pd_index; + phys_addr_t pml4e_pa; + phys_addr_t pdpe_pa; + phys_addr_t pde_pa; + u64 *pml4e; + u64 *pdpe; + u64 *pde; + int num_pte_tables; + + /* Compute the page-table indices at which the secure kernel mapping starts. */ + pml4_index = VSM_GET_PML4_INDEX(sk_pa); + pdp_index = VSM_GET_PDP_INDEX(sk_pa); + pd_index = VSM_GET_PD_INDEX(sk_pa); + + pml4e_pa = PAGE_AT(sk_pa, VSM_PML4E_PAGE); + pdpe_pa = PAGE_AT(sk_pa, VSM_PDPE_PAGE); + pde_pa = PAGE_AT(sk_pa, VSM_PDE_PAGE); + + pml4e = phys_to_virt(pml4e_pa); + pdpe = phys_to_virt(pdpe_pa); + pde = phys_to_virt(pde_pa); + + /* + * Zero the PML4, PDP, PD and PTE pages before populating them so that + * any entry not explicitly written below has its present bit clear. + */ + memset(pml4e, 0, + (VSM_KERNEL_STACK_PAGE - VSM_PML4E_PAGE) * VSM_PAGE_SIZE); + + *(pml4e + pml4_index) = pdpe_pa | VSM_PT_FLAGS; + *(pdpe + pdp_index) = pde_pa | VSM_PT_FLAGS; + + /* + * Initial page tables map only the first VSM_SK_INITIAL_MAP_SIZE size of memory. + * This memory will be used for the Secure Loader and initial Secure Kernel. + */ + num_pte_tables = (VSM_SK_INITIAL_MAP_SIZE / VSM_PAGE_SIZE) / VSM_ENTRIES_PER_PT; + hv_vsm_fill_pte_tables(sk_pa, pde, pd_index, num_pte_tables); + + vp_ctx->cr3 = pml4e_pa; +} + +static void __init hv_vsm_init_gdt(struct hv_init_vp_context *vp_ctx, phys_addr_t sk_pa) +{ + phys_addr_t gdt_pa, tss_pa, kstack_pa; + void *gdt_va; + u64 tss_sk_va, gdt; + struct x86_hw_tss *tss; + size_t gdt_size = sizeof(gdt), tss_size = sizeof(*tss), gdt_offset = 0; + + /* Get a page for the GDT */ + gdt_pa = PAGE_AT(sk_pa, VSM_GDT_PAGE); + gdt_va = phys_to_virt(gdt_pa); + /* Get a page for the TSS */ + tss_pa = PAGE_AT(sk_pa, VSM_TSS_PAGE); + tss = phys_to_virt(tss_pa); + /* Compute the VA that secure kernel will see for the TSS */ + tss_sk_va = VSM_VA_FROM_PA(tss_pa); + /* Get a page for the secure kernel initial stack */ + kstack_pa = PAGE_AT(sk_pa, VSM_KERNEL_STACK_PAGE); + /* Set the initial stack pointer for the kernel to point to bottom of kernel stack */ + tss->sp0 = VSM_VA_FROM_PA(kstack_pa) + VSM_PAGE_SIZE; + vp_ctx->rsp = tss->sp0; + + /* Make and add the NULL descriptor to the GDT */ + gdt = 0; + memcpy(gdt_va + gdt_offset, &gdt, gdt_size); + gdt_offset += gdt_size; + + /* Make and add a code segment descriptor to the GDT */ + gdt = GDT_ENTRY(DESC_CODE64, 0, 0); + memcpy(gdt_va + gdt_offset, &gdt, gdt_size); + gdt_offset += gdt_size; + + /* Make and add a data segment descriptor to the GDT */ + gdt = GDT_ENTRY(DESC_DATA64, 0, 0); + memcpy(gdt_va + gdt_offset, &gdt, gdt_size); + gdt_offset += gdt_size; + + /* + * Make and add a system segment descriptor for the TSS in the GDT. + * + * In 64-bit mode a system-segment descriptor (TSS/LDT) is 16 bytes + * wide: the lower 8 bytes have the same layout as the legacy 32-bit + * descriptor (produced by GDT_ENTRY), and the upper 8 bytes hold + * base[63:32] in the low 32 bits with the high 32 bits reserved 0. + * GDT_ENTRY masks base to 32 bits, so the upper half must be written + * explicitly. + */ + gdt = GDT_ENTRY(DESC_TSS32, tss_sk_va, tss_size); + memcpy(gdt_va + gdt_offset, &gdt, gdt_size); + gdt_offset += gdt_size; + gdt = tss_sk_va >> 32; + memcpy(gdt_va + gdt_offset, &gdt, gdt_size); + gdt_offset += gdt_size; + + /* Set up the GDT register */ + vp_ctx->gdtr.base = VSM_VA_FROM_PA(gdt_pa); + vp_ctx->gdtr.limit = gdt_offset - 1; + + /* Set the code segment (CS) selector */ + vp_ctx->cs.base = 0; + vp_ctx->cs.limit = 0; + vp_ctx->cs.selector = 1 << 3; + vp_ctx->cs.attributes = _DESC_S | _DESC_PRESENT | _DESC_ACCESSED | + _DESC_CODE_READABLE | _DESC_CODE_EXECUTABLE | + _DESC_LONG_CODE | _DESC_GRANULARITY_4K; + + /* Set the data segment (DS) selector */ + vp_ctx->ds.base = 0; + vp_ctx->ds.limit = 0; + vp_ctx->ds.selector = 2 << 3; + vp_ctx->ds.attributes = _DESC_S | _DESC_PRESENT | _DESC_ACCESSED | + _DESC_DATA_WRITABLE | _DESC_GRANULARITY_4K | _DESC_DB; + + /* Set the ES, FS and GS to be the same as DS, for now */ + vp_ctx->es = vp_ctx->ds; + vp_ctx->fs = vp_ctx->ds; + vp_ctx->gs = vp_ctx->ds; + + /* Set the stack selector to 0 (unused in long mode) */ + vp_ctx->ss.selector = 0; + + /* Set the task register selector */ + vp_ctx->tr.base = tss_sk_va; + vp_ctx->tr.limit = tss_size - 1; + vp_ctx->tr.selector = 3 << 3; + vp_ctx->tr.attributes = _DESC_PRESENT | _DESC_SYSTEM(11); +} + +static void __init hv_vsm_init_cpu(struct hv_init_vp_context *vp_ctx, Elf64_Addr sk_entry_pa) +{ + /* Offset rip by any secure kernel header length */ + vp_ctx->rip = VSM_VA_FROM_PA(sk_entry_pa); + + /* ToDo: Check if can be replaced with CR0_STATE */ + vp_ctx->cr0 = + X86_CR0_PG | /* Paging */ + X86_CR0_WP | /* Write Protect */ + X86_CR0_NE | /* Numeric Error */ + X86_CR0_ET | /* Extension Type */ + X86_CR0_MP | /* Math Present */ + X86_CR0_PE; /* Protection Enable */ + + vp_ctx->cr4 = + X86_CR4_PSE | /* Page Size Extensions */ + X86_CR4_PGE | /* Page Global Enable */ + X86_CR4_PAE; /* Physical Address Extensions */ + + vp_ctx->efer = + EFER_LMA | /* Long Mode Active */ + EFER_LME | /* Long Mode Enable */ + EFER_NX | /* No Execute Enable */ + EFER_SCE; /* System Call Enable */ + + /* + * Intel CPUs fail if the architectural read-as-one bit 1 of RFLAGS is not + * set. See Intel SDM Vol 3C, 26.3.1.4 (RFLAGS). + * + * TODO: Has Hyper-V implemented setting this automatically? + */ + vp_ctx->rflags = X86_EFLAGS_FIXED; + + vp_ctx->msr_cr_pat = PAT_VALUE(WB, WT, UC_MINUS, UC, WB, WT, UC_MINUS, UC); +} + +void __init hv_vsm_arch_init_vp(struct hv_init_vp_context *vp_ctx, Elf64_Addr sk_entry_pa, + phys_addr_t sk_pa) +{ + hv_vsm_init_cpu(vp_ctx, sk_entry_pa); + hv_vsm_init_gdt(vp_ctx, sk_pa); + hv_vsm_init_page_tables(vp_ctx, sk_pa); +} diff --git a/arch/x86/include/asm/mshyperv.h b/arch/x86/include/asm/mshyperv.h index f64393e853ee3..5f0d689641f05 100644 --- a/arch/x86/include/asm/mshyperv.h +++ b/arch/x86/include/asm/mshyperv.h @@ -7,6 +7,7 @@ #include <linux/msi.h> #include <linux/io.h> #include <linux/static_call.h> +#include <linux/elf.h> #include <asm/nospec-branch.h> #include <asm/msr.h> #include <hyperv/hvhdk.h> @@ -248,6 +249,14 @@ void hv_crash_asm_end(void); static inline void hv_root_crash_init(void) {} #endif /* CONFIG_MSHV_ROOT && CONFIG_CRASH_DUMP */ +#ifdef CONFIG_HYPERV_VSM +void __init hv_vsm_arch_init_vp(struct hv_init_vp_context *vp_ctx, Elf64_Addr sk_entry_pa, + phys_addr_t sk_pa); +#else /* CONFIG_HYPERV_VSM */ +static inline void __init hv_vsm_arch_init_vp(struct hv_init_vp_context *vp_ctx, + Elf64_Addr sk_entry_pa, phys_addr_t sk_pa) {} +#endif + #else /* CONFIG_HYPERV */ static inline void hyperv_init(void) {} static inline void hyperv_setup_mmu_ops(void) {} -- 2.34.1

