LVBS bring-up requires loading a secure kernel image into VTL1 before
starting it. Add the VTL0-side loader that stages the image in the
memory region reserved by hv_vsm_securekernel, in preparation for the
VTL1 bring-up.
The image is a 64-bit ELF fetched via request_firmware("vsm_sk"). It
is expected to ship inside the signed UKI/initramfs so it is
authenticated end-to-end via Secure Boot before the loader consumes
it; sourcing it from an unauthenticated location would break the LVBS
trust model.
The loader validates the ELF header, stages the PT_LOAD segments into
the reserved region and records the entry point as a physical address
for use at VTL1 start time.
If VSM support has been advertised to the hypervisor but no secure
kernel region was reserved on the command line, panic: LVBS bring-up
is committed at this point and there is no safe way to continue.
Signed-off-by: Stanislav Kinsburskii <[email protected]>
Signed-off-by: Thara Gopinath <[email protected]>
---
drivers/hv/hv_vsm.h | 17 ++++
drivers/hv/hv_vsm_boot.c | 201 ++++++++++++++++++++++++++++++++++++++-
include/hyperv/vsm.h | 21 ++++
3 files changed, 238 insertions(+), 1 deletion(-)
create mode 100644 drivers/hv/hv_vsm.h
create mode 100644 include/hyperv/vsm.h
diff --git a/drivers/hv/hv_vsm.h b/drivers/hv/hv_vsm.h
new file mode 100644
index 0000000000000..88f099f88eeb4
--- /dev/null
+++ b/drivers/hv/hv_vsm.h
@@ -0,0 +1,17 @@
+/* SPDX-License-Identifier: GPL-2.0 */
+/*
+ * Copyright (c) 2023-2026, Microsoft Corporation.
+ *
+ * Author: Thara Gopinath <[email protected]>
+ *
+ */
+
+#ifndef _HV_VSM_H
+#define _HV_VSM_H
+
+#include <linux/ioport.h>
+#include <linux/types.h>
+
+extern struct resource sk_res;
+
+#endif /* _HV_VSM_H */
diff --git a/drivers/hv/hv_vsm_boot.c b/drivers/hv/hv_vsm_boot.c
index 99e4dc8695837..dc20f935da5b2 100644
--- a/drivers/hv/hv_vsm_boot.c
+++ b/drivers/hv/hv_vsm_boot.c
@@ -6,6 +6,7 @@
* Copyright (c) 2023-2025, Microsoft Corporation.
*
* Author: Thara Gopinath <[email protected]>
+ * Stanislav Kinsburskii <[email protected]>
*
*/
@@ -13,10 +14,26 @@
#include <linux/hyperv.h>
#include <linux/cpumask.h>
+#include <linux/namei.h>
+#include <linux/acpi.h>
+#include <linux/firmware.h>
+#include <hyperv/vsm.h>
+#include <asm/e820/types.h>
#include <asm/mshyperv.h>
#include "mshv.h"
+#include "hv_vsm.h"
#define HV_VTL1_ENABLE_BIT BIT(1)
+/*
+ * Firmware name looked up via request_firmware() under /lib/firmware/.
+ *
+ * The secure kernel image is expected to be delivered inside the signed
+ * UKI/initramfs so that it is authenticated end-to-end via Secure Boot
+ * before request_firmware() returns it.
+ */
+#define SK_FW_NAME "vsm_sk"
+
+static void *vsm_skm_va;
static int hv_vsm_get_register(u32 reg_name, u64 *result)
{
@@ -38,6 +55,167 @@ static int hv_vsm_get_register(u32 reg_name, u64 *result)
return 0;
}
+static Elf64_Addr __init hv_vsm_elf_min_load_paddr(void *image)
+{
+ Elf64_Ehdr *ehdr = image;
+ Elf64_Phdr *phdr = image + ehdr->e_phoff;
+ Elf64_Addr paddr = U64_MAX;
+ int i;
+
+ for (i = 0; i < ehdr->e_phnum; i++, phdr++) {
+ if (phdr->p_type != PT_LOAD)
+ continue;
+
+ if (phdr->p_paddr < paddr)
+ paddr = phdr->p_paddr;
+ }
+
+ return paddr;
+}
+
+static size_t __init hv_vsm_elf_binary_size(void *image)
+{
+ Elf64_Ehdr *ehdr = image;
+ Elf64_Phdr *phdr = image + ehdr->e_phoff;
+ Elf64_Addr min_paddr, max_paddr = 0;
+ int i;
+
+ min_paddr = hv_vsm_elf_min_load_paddr(image);
+ if (min_paddr == U64_MAX)
+ return 0;
+
+ for (i = 0; i < ehdr->e_phnum; i++, phdr++) {
+ if (phdr->p_type != PT_LOAD)
+ continue;
+
+ max_paddr = max(max_paddr, phdr->p_paddr + phdr->p_memsz);
+ }
+
+ return max_paddr - min_paddr;
+}
+
+static int __init hv_vsm_load_elf(void *image, Elf64_Addr *sk_entry_pa)
+{
+ Elf64_Ehdr *ehdr = image;
+ Elf64_Phdr *phdr = image + ehdr->e_phoff;
+ Elf64_Addr min_paddr;
+ Elf64_Xword first_load_align = 0;
+ size_t size;
+ void *base_addr;
+ int i;
+
+ /* Find alignment of the first PT_LOAD segment. */
+ for (i = 0; i < ehdr->e_phnum; i++) {
+ if (phdr[i].p_type == PT_LOAD) {
+ first_load_align = phdr[i].p_align;
+ break;
+ }
+ }
+ if (!first_load_align) {
+ pr_err("Secure kernel does not have loadable segments\n");
+ return -EINVAL;
+ }
+
+ /* Align the base load address up to the first PT_LOAD segment
alignment */
+ base_addr = PTR_ALIGN(vsm_skm_va + first_load_align, first_load_align);
+
+ size = hv_vsm_elf_binary_size(image);
+ if (vsm_skm_va + VSM_SK_INITIAL_MAP_SIZE - base_addr < size) {
+ pr_err("secure kernel does not fit: %zu > %td\n", size,
+ vsm_skm_va + VSM_SK_INITIAL_MAP_SIZE - base_addr);
+ return -EFBIG;
+ }
+
+ pr_debug("secure kernel binary size: %#zx\n", size);
+
+ min_paddr = hv_vsm_elf_min_load_paddr(image);
+ pr_debug("secure kernel minimal paddr: %#llx\n", min_paddr);
+
+ pr_debug("loading secure kernel ELF segments:\n");
+
+ /* Validate PT_LOAD alignment first, before touching any target memory.
*/
+ for (i = 0; i < ehdr->e_phnum; i++) {
+ if (phdr[i].p_type != PT_LOAD)
+ continue;
+ if (phdr[i].p_align % SZ_2M) {
+ pr_err("LOAD segment is not aligned by 2MB\n");
+ return -EINVAL;
+ }
+ }
+
+ for (i = 0; i < ehdr->e_phnum; i++, phdr++) {
+ void *load_addr;
+
+ if (phdr->p_type != PT_LOAD)
+ continue;
+
+ /*
+ * Adjust the load address by min_paddr to compensate the
+ * offset.
+ */
+ load_addr = base_addr + (phdr->p_paddr - min_paddr);
+
+ pr_debug(" p_offset: %#016llx, p_filesz: %#016llx, p_memsz:
%#016llx to pa %#016llx\n",
+ phdr->p_offset, phdr->p_filesz, phdr->p_memsz,
+ virt_to_phys(load_addr));
+ memcpy(load_addr, image + phdr->p_offset, phdr->p_filesz);
+
+ if (phdr->p_memsz == phdr->p_filesz)
+ continue;
+
+ pr_debug(" zeroing %#016llx bytes at pa %#016llx\n",
+ phdr->p_memsz - phdr->p_filesz,
+ virt_to_phys(load_addr + phdr->p_filesz));
+ memset(load_addr + phdr->p_filesz, 0,
+ phdr->p_memsz - phdr->p_filesz);
+ }
+
+ *sk_entry_pa = virt_to_phys(base_addr + (ehdr->e_entry - min_paddr));
+ pr_debug("secure kernel entry pa: %#llx\n", *sk_entry_pa);
+
+ return 0;
+}
+
+static int __init hv_vsm_load_secure_kernel(Elf64_Addr *sk_entry_pa)
+{
+ const struct firmware *fw;
+ Elf64_Ehdr *ehdr;
+ int ret;
+
+ ret = request_firmware(&fw, SK_FW_NAME, NULL);
+ if (ret) {
+ pr_err("Failed to load %s firmware: %d\n", SK_FW_NAME, ret);
+ return ret;
+ }
+
+ ehdr = (Elf64_Ehdr *)fw->data;
+ if (fw->size < sizeof(*ehdr) ||
+ memcmp(ehdr->e_ident, ELFMAG, SELFMAG) ||
+ (ehdr->e_type != ET_EXEC && ehdr->e_type != ET_DYN)) {
+ pr_err("Not a valid ELF file: %s\n", SK_FW_NAME);
+ ret = -ENOEXEC;
+ goto out_release;
+ }
+
+ if (ehdr->e_ident[EI_CLASS] != ELFCLASS64) {
+ pr_err("Not a 64-bit compatible ELF file: %s\n", SK_FW_NAME);
+ ret = -ENOEXEC;
+ goto out_release;
+ }
+
+ if (!elf_check_arch(ehdr)) {
+ pr_err("Not a valid ELF file: %s\n", SK_FW_NAME);
+ ret = -ENOEXEC;
+ goto out_release;
+ }
+
+ ret = hv_vsm_load_elf((void *)fw->data, sk_entry_pa);
+
+out_release:
+ release_firmware(fw);
+ return ret;
+}
+
static int __init hv_vsm_enable_partition_vtl(void)
{
u64 status = 0;
@@ -85,6 +263,7 @@ static int __init hv_vsm_bootstrap_vtl(void)
{
u16 partition_enabled_vtl_set = 0, partition_mbec_enabled_vtl_set = 0;
u8 partition_max_vtl;
+ Elf64_Addr sk_entry_pa;
int ret;
/* Check and enable VTL1 at the partition level */
@@ -120,7 +299,25 @@ static int __init hv_vsm_bootstrap_vtl(void)
return -EINVAL;
}
}
- return 0;
+
+ return hv_vsm_load_secure_kernel(&sk_entry_pa);
+}
+
+static void __init hv_vsm_get_sk_mem(void)
+{
+ /*
+ * The reserved secure kernel region is mandatory once VSM support has
+ * been advertised. Without it we cannot load the secure kernel and
+ * bringing up VTL1 is impossible, so fail hard rather than continuing
+ * in an unusable state.
+ */
+ if (!sk_res.start)
+ panic("No memory reserved in cmdline for secure kernel");
+
+ vsm_skm_va = phys_to_virt(sk_res.start);
+
+ pr_info("secure kernel region: %#llx-%#llx (%lld MB)\n",
+ sk_res.start, sk_res.end, resource_size(&sk_res) >> 20);
}
static bool __init vsm_arch_has_vsm_access(void)
@@ -143,6 +340,8 @@ static int __init hv_vsm_boot_init(void)
if (!vsm_arch_has_vsm_access())
return 0;
+ hv_vsm_get_sk_mem();
+
/*
* Copy the current cpu mask and pin rest of the running code to boot
cpu.
* Important since we want boot cpu of VTL0 to be the boot cpu for VTL1.
diff --git a/include/hyperv/vsm.h b/include/hyperv/vsm.h
new file mode 100644
index 0000000000000..51555c09413d5
--- /dev/null
+++ b/include/hyperv/vsm.h
@@ -0,0 +1,21 @@
+/* SPDX-License-Identifier: GPL-2.0 */
+/*
+ * Common definitions shared by generic and arch code for enabling VTL1
+ * and the Virtual Secure Mode (VSM) framework on Microsoft Hyper-V.
+ *
+ * Copyright (c) 2025-2026, Microsoft Corporation.
+ *
+ * Author: Thara Gopinath <[email protected]>
+ */
+
+#ifndef _HYPERV_VSM_H
+#define _HYPERV_VSM_H
+
+/*
+ * Size of memory that is initially mapped for the secure kernel by the
+ * VTL0-side loader. The secure kernel image itself may be larger than
+ * this and map additional memory on its own.
+ */
+#define VSM_SK_INITIAL_MAP_SIZE (16 * 1024 * 1024)
+
+#endif /* _HYPERV_VSM_H */
--
2.34.1