* [RFC PATCH 01/12] drivers: hv: Add HYPERV_VSM kconfig option
2026-09-01 16:55 [RFC PATCH 00/12] Introduce LVBS support for Hyper-V guests Thara Gopinath
@ 2026-09-01 16:55 ` Thara Gopinath
2026-09-01 16:55 ` [RFC PATCH 02/12] drivers: hv: hv_common: Allocate Hyper-V output arg page when VSM is enabled Thara Gopinath
` (10 subsequent siblings)
11 siblings, 0 replies; 34+ messages in thread
From: Thara Gopinath @ 2026-09-01 16:55 UTC (permalink / raw)
To: kys, haiyangz, wei.liu, decui, tglx, mingo, bp, dave.hansen, hpa,
ardb, ilias.apalodimas
Cc: James.Bottomley, "longli, javierm, lszubowi, francescopompo2,
tgopinath, x86, linux-hyperv, linux-kernel, linux-efi,
Thara Gopinath
Introduce CONFIG_HYPERV_VSM, the top-level switch that gates
VTL0-side support for Microsoft Hyper-V's Virtual Secure Mode (VSM).
Enabling it lets the VTL0 kernel load a small secure kernel into
VTL1 and stand up the VTL0<->VTL1 interfaces needed to request
VSM services.
Depends on HYPERV and on X86_64 which is the only architecture currently
implemented.
Signed-off-by: Thara Gopinath <tgopinath@linux.microsoft.com>
---
drivers/hv/Kconfig | 9 +++++++++
1 file changed, 9 insertions(+)
diff --git a/drivers/hv/Kconfig b/drivers/hv/Kconfig
index aa11bcefddf2c..a325fc61be604 100644
--- a/drivers/hv/Kconfig
+++ b/drivers/hv/Kconfig
@@ -40,6 +40,15 @@ config HYPERV_VTL_MODE
If unsure, say N
+config HYPERV_VSM
+ bool "Microsoft Hyper-V VSM driver"
+ depends on HYPERV && X86_64
+ help
+ Select this option to enable Hyper-V Virtual Secure Mode.
+ Enabling this option will load a secure kernel in VTL1 and
+ establish an interface between VTL0 and VTL1 to request for
+ VSM services.
+
config HYPERV_TIMER
def_bool HYPERV && X86
--
2.34.1
^ permalink raw reply related [flat|nested] 34+ messages in thread* [RFC PATCH 02/12] drivers: hv: hv_common: Allocate Hyper-V output arg page when VSM is enabled
2026-09-01 16:55 [RFC PATCH 00/12] Introduce LVBS support for Hyper-V guests Thara Gopinath
2026-09-01 16:55 ` [RFC PATCH 01/12] drivers: hv: Add HYPERV_VSM kconfig option Thara Gopinath
@ 2026-09-01 16:55 ` Thara Gopinath
2026-09-01 17:12 ` sashiko-bot
2026-09-01 16:55 ` [RFC PATCH 03/12] drivers: hv: Reserve memory for VSM secure kernel during early boot Thara Gopinath
` (9 subsequent siblings)
11 siblings, 1 reply; 34+ messages in thread
From: Thara Gopinath @ 2026-09-01 16:55 UTC (permalink / raw)
To: kys, haiyangz, wei.liu, decui, tglx, mingo, bp, dave.hansen, hpa,
ardb, ilias.apalodimas
Cc: James.Bottomley, "longli, javierm, lszubowi, francescopompo2,
tgopinath, x86, linux-hyperv, linux-kernel, linux-efi,
Thara Gopinath
hv_output_page_exists() controls whether the per-CPU Hyper-V output
argument page is allocated at boot. This page is required for hypercalls
that return results via an output GPA, like HvCallGetVpRegisters.
The VSM drivers read VSM-specific VP registers (e.g. HvRegisterVsmVpStatus,
HvRegisterVsmCapabilities) via get_vp_registers(), which issues
HvCallGetVpRegisters and therefore depends on the output argument page
being present. Without it, those hypercalls fail and VSM initialization
cannot proceed.
Extend hv_output_page_exists() to return true when CONFIG_HYPERV_VSM is
enabled.
Signed-off-by: Thara Gopinath <tgopinath@linux.microsoft.com>
---
drivers/hv/hv_common.c | 3 ++-
1 file changed, 2 insertions(+), 1 deletion(-)
diff --git a/drivers/hv/hv_common.c b/drivers/hv/hv_common.c
index 31256cb22b39e..44f4a10984618 100644
--- a/drivers/hv/hv_common.c
+++ b/drivers/hv/hv_common.c
@@ -259,7 +259,8 @@ static void hv_kmsg_dump_register(void)
static inline bool hv_output_page_exists(void)
{
- return hv_parent_partition() || IS_ENABLED(CONFIG_HYPERV_VTL_MODE);
+ return hv_parent_partition() || IS_ENABLED(CONFIG_HYPERV_VTL_MODE) ||
+ IS_ENABLED(CONFIG_HYPERV_VSM);
}
void __init hv_get_partition_id(void)
--
2.34.1
^ permalink raw reply related [flat|nested] 34+ messages in thread* Re: [RFC PATCH 02/12] drivers: hv: hv_common: Allocate Hyper-V output arg page when VSM is enabled
2026-09-01 16:55 ` [RFC PATCH 02/12] drivers: hv: hv_common: Allocate Hyper-V output arg page when VSM is enabled Thara Gopinath
@ 2026-09-01 17:12 ` sashiko-bot
2026-09-01 22:56 ` Wei Liu
0 siblings, 1 reply; 34+ messages in thread
From: sashiko-bot @ 2026-09-01 17:12 UTC (permalink / raw)
To: Thara Gopinath; +Cc: linux-hyperv
Thank you for your contribution! Sashiko AI review found 1 potential issue(s) to consider:
Pre-existing issues:
- [High] hv_apicid_to_vp_index() unconditionally dereferences the unallocated hyperv_pcpu_output_arg pointer in standard/SNP guests.
--
commit 0ee3313e113c9ca7969923f2af5a1ed18ddb25d2
Author: Thara Gopinath <tgopinath@linux.microsoft.com>
drivers: hv: hv_common: Allocate Hyper-V output arg page when VSM is enabled
This commit extends hv_output_page_exists() to return true when
CONFIG_HYPERV_VSM is enabled. This ensures the per-CPU output argument
page is allocated at boot for VSM hypercalls.
> diff --git a/drivers/hv/hv_common.c b/drivers/hv/hv_common.c
> index 31256cb22b39e..44f4a10984618 100644
> --- a/drivers/hv/hv_common.c
> +++ b/drivers/hv/hv_common.c
> @@ -259,7 +259,8 @@ static void hv_kmsg_dump_register(void)
>
> static inline bool hv_output_page_exists(void)
> {
> - return hv_parent_partition() || IS_ENABLED(CONFIG_HYPERV_VTL_MODE);
> + return hv_parent_partition() || IS_ENABLED(CONFIG_HYPERV_VTL_MODE) ||
> + IS_ENABLED(CONFIG_HYPERV_VSM);
[Severity: High]
This is a pre-existing issue, but modifying this function highlights a
vulnerability for SEV-SNP guests where VSM and VTL modes are disabled.
When an SEV-SNP guest boots secondary CPUs, hv_smp_prepare_cpus() overrides
the wakeup callback with hv_snp_boot_ap(), which then calls
hv_apicid_to_vp_index() in arch/x86/hyperv/ivm.c.
If CONFIG_HYPERV_VSM and CONFIG_HYPERV_VTL_MODE are both disabled,
hv_output_page_exists() evaluates to false. This means hyperv_pcpu_output_arg
is never allocated during hv_common_init() and remains NULL.
Consequently, arch/x86/hyperv/hv_init.c:hv_apicid_to_vp_index() dereferences
this unallocated pointer:
arch/x86/hyperv/hv_init.c:hv_apicid_to_vp_index() {
...
output = *this_cpu_ptr(hyperv_pcpu_output_arg);
...
status = hv_do_hypercall(HVCALL_GET_VP_ID_FROM_APIC_ID, input, output);
...
}
Since this_cpu_ptr(NULL) resolves to the base of the per-CPU area, it
dereferences a garbage pointer. This is then passed to hv_do_hypercall(),
causing the hypervisor to overwrite random physical memory with the hypercall
result, and eventually oopsing the kernel when output[0] is read.
Should hv_output_page_exists() also be extended to account for SEV-SNP
guests that rely on hv_apicid_to_vp_index() during boot?
> }
>
> void __init hv_get_partition_id(void)
--
Sashiko AI review · https://sashiko.dev/#/patchset/20260901165647.3160413-1-tgopinath@linux.microsoft.com?part=2
^ permalink raw reply [flat|nested] 34+ messages in thread* Re: [RFC PATCH 02/12] drivers: hv: hv_common: Allocate Hyper-V output arg page when VSM is enabled
2026-09-01 17:12 ` sashiko-bot
@ 2026-09-01 22:56 ` Wei Liu
0 siblings, 0 replies; 34+ messages in thread
From: Wei Liu @ 2026-09-01 22:56 UTC (permalink / raw)
To: sashiko-reviews; +Cc: Thara Gopinath, linux-hyperv, Wei Liu
On Tue, Sep 01, 2026 at 05:12:38PM +0000, sashiko-bot@kernel.org wrote:
> Thank you for your contribution! Sashiko AI review found 1 potential issue(s) to consider:
>
> Pre-existing issues:
> - [High] hv_apicid_to_vp_index() unconditionally dereferences the unallocated hyperv_pcpu_output_arg pointer in standard/SNP guests.
> --
>
> commit 0ee3313e113c9ca7969923f2af5a1ed18ddb25d2
> Author: Thara Gopinath <tgopinath@linux.microsoft.com>
>
> drivers: hv: hv_common: Allocate Hyper-V output arg page when VSM is enabled
>
> This commit extends hv_output_page_exists() to return true when
> CONFIG_HYPERV_VSM is enabled. This ensures the per-CPU output argument
> page is allocated at boot for VSM hypercalls.
>
> > diff --git a/drivers/hv/hv_common.c b/drivers/hv/hv_common.c
> > index 31256cb22b39e..44f4a10984618 100644
> > --- a/drivers/hv/hv_common.c
> > +++ b/drivers/hv/hv_common.c
> > @@ -259,7 +259,8 @@ static void hv_kmsg_dump_register(void)
> >
> > static inline bool hv_output_page_exists(void)
> > {
> > - return hv_parent_partition() || IS_ENABLED(CONFIG_HYPERV_VTL_MODE);
> > + return hv_parent_partition() || IS_ENABLED(CONFIG_HYPERV_VTL_MODE) ||
> > + IS_ENABLED(CONFIG_HYPERV_VSM);
>
> [Severity: High]
> This is a pre-existing issue, but modifying this function highlights a
> vulnerability for SEV-SNP guests where VSM and VTL modes are disabled.
>
> When an SEV-SNP guest boots secondary CPUs, hv_smp_prepare_cpus() overrides
> the wakeup callback with hv_snp_boot_ap(), which then calls
> hv_apicid_to_vp_index() in arch/x86/hyperv/ivm.c.
>
> If CONFIG_HYPERV_VSM and CONFIG_HYPERV_VTL_MODE are both disabled,
> hv_output_page_exists() evaluates to false. This means hyperv_pcpu_output_arg
> is never allocated during hv_common_init() and remains NULL.
>
> Consequently, arch/x86/hyperv/hv_init.c:hv_apicid_to_vp_index() dereferences
> this unallocated pointer:
>
> arch/x86/hyperv/hv_init.c:hv_apicid_to_vp_index() {
> ...
> output = *this_cpu_ptr(hyperv_pcpu_output_arg);
> ...
> status = hv_do_hypercall(HVCALL_GET_VP_ID_FROM_APIC_ID, input, output);
> ...
> }
>
> Since this_cpu_ptr(NULL) resolves to the base of the per-CPU area, it
> dereferences a garbage pointer. This is then passed to hv_do_hypercall(),
> causing the hypervisor to overwrite random physical memory with the hypercall
> result, and eventually oopsing the kernel when output[0] is read.
>
This looks like a real bug. It should / will be fixed separately.
Wei
^ permalink raw reply [flat|nested] 34+ messages in thread
* [RFC PATCH 03/12] drivers: hv: Reserve memory for VSM secure kernel during early boot
2026-09-01 16:55 [RFC PATCH 00/12] Introduce LVBS support for Hyper-V guests Thara Gopinath
2026-09-01 16:55 ` [RFC PATCH 01/12] drivers: hv: Add HYPERV_VSM kconfig option Thara Gopinath
2026-09-01 16:55 ` [RFC PATCH 02/12] drivers: hv: hv_common: Allocate Hyper-V output arg page when VSM is enabled Thara Gopinath
@ 2026-09-01 16:55 ` Thara Gopinath
2026-09-01 17:10 ` sashiko-bot
2026-09-02 0:59 ` Wei Liu
2026-09-01 16:55 ` [RFC PATCH 04/12] firmware: efi: libstub: x86-stub: Enable VSM awareness in efi os indications variable Thara Gopinath
` (8 subsequent siblings)
11 siblings, 2 replies; 34+ messages in thread
From: Thara Gopinath @ 2026-09-01 16:55 UTC (permalink / raw)
To: kys, haiyangz, wei.liu, decui, tglx, mingo, bp, dave.hansen, hpa,
ardb, ilias.apalodimas
Cc: James.Bottomley, "longli, javierm, lszubowi, francescopompo2,
tgopinath, x86, linux-hyperv, linux-kernel, linux-efi,
Thara Gopinath, Anna Trikalinou
The VSM secure kernel runs in VTL1 and cannot use the normal Linux
kernel memory allocators — its memory must be reserved before the
general-purpose allocator takes ownership of the physical address space.
Add hv_vsm_securekernel.c to handle this early boot reservation. The
driver parses a new "securekernel=<size>[K|M|G][@<addr>]" kernel command
line argument to allow explicit control over the reserved region's size
and base address. If the argument is absent or specifies a region smaller
than the computed minimum, the driver falls back to an automatic
allocation.
The minimum reservation is calculated as:
16 MB (base) + (num_possible_cpus * 4 MB per-CPU)
Memory is reserved via memblock_phys_alloc_range() with 2 MB alignment
(SECKERNEL_ALIGN) and registered as an IORESOURCE_SYSTEM_RAM entry named
"vsm" in the iomem resource tree so it is visible in /proc/iomem and
protected from reuse.
The reservation is skipped if sk_res.start is already set, preventing
duplicate reservations if the setup hook is called more than once.
Co-developed-by: Anna Trikalinou <atrikalinou@microsoft.com>
Signed-off-by: Anna Trikalinou <atrikalinou@microsoft.com>
Signed-off-by: Thara Gopinath <tgopinath@linux.microsoft.com>
---
drivers/hv/Makefile | 1 +
drivers/hv/hv_vsm_securekernel.c | 188 +++++++++++++++++++++++++++++++
2 files changed, 189 insertions(+)
create mode 100644 drivers/hv/hv_vsm_securekernel.c
diff --git a/drivers/hv/Makefile b/drivers/hv/Makefile
index 888a748cc7cb9..880c570832381 100644
--- a/drivers/hv/Makefile
+++ b/drivers/hv/Makefile
@@ -25,3 +25,4 @@ obj-$(subst m,y,$(CONFIG_MSHV_ROOT)) += hv_proc.o
ifneq ($(CONFIG_MSHV_ROOT)$(CONFIG_MSHV_VTL),)
obj-y += mshv_common.o
endif
+obj-$(subst m,y,$(CONFIG_HYPERV_VSM)) += hv_vsm_securekernel.o
diff --git a/drivers/hv/hv_vsm_securekernel.c b/drivers/hv/hv_vsm_securekernel.c
new file mode 100644
index 0000000000000..f90f6204b4d18
--- /dev/null
+++ b/drivers/hv/hv_vsm_securekernel.c
@@ -0,0 +1,188 @@
+// SPDX-License-Identifier: GPL-2.0
+/*
+ * Copyright (c) 2024, Microsoft Corporation.
+ *
+ * Authors:
+ * Anna Trikalinou <atrikalinou@microsoft.com>
+ * Thara Gopinath <tgopinath@linux.microsoft.com>
+ */
+
+#include <linux/memblock.h>
+
+/* Define Memory Reservation for Secure Kernel */
+#define SECKERNEL_ALIGN SZ_2M
+#define SECKERNEL_ADDR_MAX (max_low_pfn_mapped << PAGE_SHIFT)
+/* Secure kernel map (16MB minimum)
+ * Region Offset Size
+ * VSM PAGES 0 < 2MB
+ * SKERNEL ?(ELF) 16MB+ (based on config)
+ */
+#define SECKERNEL_BASE_SIZE (16 * 1024 * 1024)
+#define SECKERNEL_PERCPU_SIZE (4 * 1024 * 1024)
+
+/* Estimate amount of memory needed for Secure Kernel */
+#define SECKERNEL_MIN_SIZE (SECKERNEL_BASE_SIZE + num_possible_cpus() * SECKERNEL_PERCPU_SIZE)
+
+struct resource sk_res = {
+ .name = "vsm",
+ .start = 0,
+ .end = 0,
+ .flags = IORESOURCE_BUSY | IORESOURCE_SYSTEM_RAM,
+ .desc = IORES_DESC_RESERVED
+};
+
+/*
+ * That function parses "simple" securekernel command lines like
+ *
+ * securekernel=size[@offset]
+ *
+ * It returns 0 on success and -EINVAL on failure.
+ */
+static int __init parse_securekernel_simple(char *cmdline,
+ unsigned long long *securekernel_size,
+ unsigned long long *securekernel_base)
+{
+ char *cur = cmdline;
+
+ *securekernel_size = memparse(cmdline, &cur);
+ if (cmdline == cur) {
+ pr_warn("securekernel: memory value expected\n");
+ return -EINVAL;
+ }
+
+ if (*cur == '@') {
+ *securekernel_base = memparse(cur + 1, &cur);
+ } else if (*cur != ' ' && *cur != '\0') {
+ pr_warn("securekernel: unrecognized char: %c\n", *cur);
+ return -EINVAL;
+ }
+
+ return 0;
+}
+
+static __init char *get_last_securekernel(char *cmdline, const char *name)
+{
+ char *p = cmdline, *sk_cmdline = NULL;
+
+ /* find securekernel and use the last one if there are more */
+ p = strstr(p, name);
+ while (p) {
+ sk_cmdline = p;
+ p = strstr(p + 1, name);
+ }
+
+ if (!sk_cmdline)
+ return NULL;
+
+ return sk_cmdline;
+}
+
+static int __init __parse_securekernel(char *cmdline,
+ unsigned long long *securekernel_size,
+ unsigned long long *securekernel_base,
+ const char *name)
+{
+ char *sk_cmdline;
+
+ if (!securekernel_size || !securekernel_base)
+ return -EINVAL;
+
+ *securekernel_size = 0;
+ *securekernel_base = 0;
+
+ sk_cmdline = get_last_securekernel(cmdline, name);
+
+ if (!sk_cmdline)
+ return -EINVAL;
+
+ sk_cmdline += strlen(name);
+
+ return parse_securekernel_simple(sk_cmdline, securekernel_size, securekernel_base);
+}
+
+/*
+ * That function is the entry point for command line parsing and should be
+ * called from the arch-specific code.
+ */
+static int __init parse_securekernel(char *cmdline,
+ unsigned long long *securekernel_size,
+ unsigned long long *securekernel_base)
+{
+ return __parse_securekernel(cmdline, securekernel_size, securekernel_base,
+ "securekernel=");
+}
+
+static int __init hv_vsm_seckernel_mem_init(char *__unused)
+{
+ unsigned long long securekernel_size = 0, securekernel_base = 0;
+ int ret;
+
+ /* Secure Kernel memory is already reserved. Avoid duplicate reservation */
+ if (sk_res.start)
+ return 0;
+ /*
+ * Reserve Secure Kernel memory.
+ * Check command line first, if secure kernel memory was defined
+ */
+ ret = parse_securekernel(boot_command_line, &securekernel_size,
+ &securekernel_base);
+
+ if (ret != 0 || securekernel_size < SECKERNEL_MIN_SIZE) {
+ if (ret != 0)
+ pr_info("%s: securekernel cmd line not defined. Falling back to default.\n",
+ __func__);
+ else if (securekernel_size < SECKERNEL_MIN_SIZE)
+ pr_info("%s: securekernel cmd line too small. Falling back to default.\n",
+ __func__);
+
+ securekernel_size = SECKERNEL_MIN_SIZE;
+ securekernel_base = 0;
+ }
+
+ /* If securekernel_base was specified from command line,
+ * try to reserve memory starting from that address
+ */
+ if (securekernel_base) {
+ unsigned long long start, end;
+
+ end = securekernel_base + securekernel_size;
+ if (end > SECKERNEL_ADDR_MAX || end < securekernel_base) {
+ pr_warn("%s: Invalid Securekernel base address %llx. Falling back to default.\n",
+ __func__, securekernel_base);
+ securekernel_base = 0;
+ } else {
+ start = memblock_phys_alloc_range(securekernel_size, SECKERNEL_ALIGN,
+ securekernel_base,
+ securekernel_base + securekernel_size);
+ if (start != securekernel_base) {
+ pr_warn("%s: memory reservation @ %llx failed-memory is in use\n",
+ __func__, securekernel_base);
+ pr_warn("%s:Falling back to default mem allocation\n", __func__);
+ securekernel_base = 0;
+ }
+ }
+ }
+ /* Default: Find the base address automatically */
+ if (!securekernel_base) {
+ securekernel_base = memblock_phys_alloc_range(securekernel_size, SECKERNEL_ALIGN,
+ 0, SECKERNEL_ADDR_MAX);
+ if (!securekernel_base) {
+ pr_err("%s: Securekernel reservation failed-VSM will not be enabled.\n",
+ __func__);
+ return -EINVAL;
+ }
+ }
+
+ pr_info("Reserving %ldMB of memory at 0x%llx(%ld MB) for securekernel(System RAM:%ldMB)\n",
+ (unsigned long)(securekernel_size >> 20),
+ securekernel_base,
+ (unsigned long)(securekernel_base >> 20),
+ (unsigned long)(memblock_phys_mem_size() >> 20));
+
+ sk_res.start = securekernel_base;
+ sk_res.end = securekernel_base + securekernel_size - 1;
+ insert_resource(&iomem_resource, &sk_res);
+
+ return 0;
+}
+__setup("securekernel", hv_vsm_seckernel_mem_init);
--
2.34.1
^ permalink raw reply related [flat|nested] 34+ messages in thread* Re: [RFC PATCH 03/12] drivers: hv: Reserve memory for VSM secure kernel during early boot
2026-09-01 16:55 ` [RFC PATCH 03/12] drivers: hv: Reserve memory for VSM secure kernel during early boot Thara Gopinath
@ 2026-09-01 17:10 ` sashiko-bot
2026-09-02 0:59 ` Wei Liu
1 sibling, 0 replies; 34+ messages in thread
From: sashiko-bot @ 2026-09-01 17:10 UTC (permalink / raw)
To: Thara Gopinath; +Cc: linux-hyperv
Thank you for your contribution! Sashiko AI review found 4 potential issue(s) to consider:
- [High] The automatic memory reservation fallback is never executed if the `securekernel` parameter is omitted from the kernel command line.
- [High] Manual string parsing of boot_command_line uses a fragile substring match, misparsing unrelated parameters and ignoring the pre-parsed setup argument.
- [Medium] The `__setup` handler returns 0 on success and -EINVAL on failure, inverting the API contract and leaking the parameter to the init process.
- [Low] parse_securekernel_simple fails to validate trailing characters after parsing the base address.
--
commit c057f006b3a9db9a754349283e3c7dd23550b133
Author: Thara Gopinath <tgopinath@linux.microsoft.com>
Date: Tue Sep 1 16:55:17 2026 +0000
drivers: hv: Reserve memory for VSM secure kernel during early boot
The VSM secure kernel runs in VTL1 and requires its memory to be reserved
early. This commit introduces a driver to handle early boot reservation of
this memory, either parsed from the securekernel= command line parameter or
calculated automatically as a fallback.
> diff --git a/drivers/hv/hv_vsm_securekernel.c b/drivers/hv/hv_vsm_securekernel.c
> new file mode 100644
> index 0000000000000..f90f6204b4d18
> --- /dev/null
> +++ b/drivers/hv/hv_vsm_securekernel.c
[ ... ]
> +static int __init parse_securekernel_simple(char *cmdline,
> + unsigned long long *securekernel_size,
> + unsigned long long *securekernel_base)
> +{
> + char *cur = cmdline;
> +
> + *securekernel_size = memparse(cmdline, &cur);
> + if (cmdline == cur) {
> + pr_warn("securekernel: memory value expected\n");
> + return -EINVAL;
> + }
> +
> + if (*cur == '@') {
> + *securekernel_base = memparse(cur + 1, &cur);
> + } else if (*cur != ' ' && *cur != '\0') {
[Severity: Low]
Does this code skip trailing character validation if @ was found?
Because the validation is inside an else if, if a user specifies a base
address with trailing non-space characters (e.g., securekernel=16M@2G_foo),
memparse() consumes the valid number, but the remaining garbage characters
are silently ignored without raising an error.
> + pr_warn("securekernel: unrecognized char: %c\n", *cur);
> + return -EINVAL;
> + }
> +
> + return 0;
> +}
> +
> +static __init char *get_last_securekernel(char *cmdline, const char *name)
> +{
> + char *p = cmdline, *sk_cmdline = NULL;
> +
> + /* find securekernel and use the last one if there are more */
> + p = strstr(p, name);
> + while (p) {
> + sk_cmdline = p;
> + p = strstr(p + 1, name);
[Severity: High]
Does this unbounded substring search misparse unrelated parameters?
Because it lacks word-boundary or prefix validation, strstr() will match
the substring anywhere in the command line. The driver could misparse
independent kernel parameters (e.g., nosecurekernel=100M).
Furthermore, since it explicitly searches for the last occurrence, an unrelated
parameter at the end of the command line containing the substring securekernel=
could overwrite a valid reservation parameter.
> + }
> +
> + if (!sk_cmdline)
> + return NULL;
> +
> + return sk_cmdline;
> +}
[ ... ]
> +static int __init hv_vsm_seckernel_mem_init(char *__unused)
[Severity: High]
Is it intentional to discard the correctly pre-parsed parameter value provided
by the __setup infrastructure?
The __setup() macro already does exact matching and provides the specific
parameter value to the callback via its argument. By explicitly ignoring this
argument (naming it __unused) and implementing a custom parser over the global
boot_command_line, the driver uses a fragile manual parsing routine instead
of the established API.
> +{
> + unsigned long long securekernel_size = 0, securekernel_base = 0;
> + int ret;
[ ... ]
> + /* Default: Find the base address automatically */
> + if (!securekernel_base) {
> + securekernel_base = memblock_phys_alloc_range(securekernel_size, SECKERNEL_ALIGN,
> + 0, SECKERNEL_ADDR_MAX);
> + if (!securekernel_base) {
> + pr_err("%s: Securekernel reservation failed-VSM will not be enabled.\n",
> + __func__);
> + return -EINVAL;
[Severity: Medium]
Will returning -EINVAL (non-zero) on the error path incorrectly tell the
kernel's parser that the parameter was successfully handled?
The kernel API documentation requires __setup handlers to return 1 (or
non-zero) if handled and 0 if not. Returning a non-zero error code treats
the failed parameter as successfully consumed.
> + }
> + }
> +
> + pr_info("Reserving %ldMB of memory at 0x%llx(%ld MB) for securekernel(System RAM:%ldMB)\n",
> + (unsigned long)(securekernel_size >> 20),
> + securekernel_base,
> + (unsigned long)(securekernel_base >> 20),
> + (unsigned long)(memblock_phys_mem_size() >> 20));
> +
> + sk_res.start = securekernel_base;
> + sk_res.end = securekernel_base + securekernel_size - 1;
> + insert_resource(&iomem_resource, &sk_res);
> +
> + return 0;
[Severity: Medium]
Will returning 0 on the success path inform the kernel's setup infrastructure
that the parameter was not handled?
This causes the kernel to erroneously pass the securekernel=... parameter as
an environment variable to the init process. The handler should return 1 when
the parameter is successfully handled.
> +}
> +__setup("securekernel", hv_vsm_seckernel_mem_init);
[Severity: High]
Can the automatic memory reservation fallback ever execute if the parameter is
omitted from the kernel command line?
Functions registered with __setup() are only invoked by init/main.c if the
target string matches a command-line parameter. If absent, the initialization
function is completely bypassed, meaning the core feature (automatic memory
reservation) fails entirely by default, and the VSM secure kernel will not boot
due to lack of reserved memory.
--
Sashiko AI review · https://sashiko.dev/#/patchset/20260901165647.3160413-1-tgopinath@linux.microsoft.com?part=3
^ permalink raw reply [flat|nested] 34+ messages in thread* Re: [RFC PATCH 03/12] drivers: hv: Reserve memory for VSM secure kernel during early boot
2026-09-01 16:55 ` [RFC PATCH 03/12] drivers: hv: Reserve memory for VSM secure kernel during early boot Thara Gopinath
2026-09-01 17:10 ` sashiko-bot
@ 2026-09-02 0:59 ` Wei Liu
2026-09-02 13:38 ` Thara Gopinath
1 sibling, 1 reply; 34+ messages in thread
From: Wei Liu @ 2026-09-02 0:59 UTC (permalink / raw)
To: Thara Gopinath
Cc: kys, haiyangz, wei.liu, decui, tglx, mingo, bp, dave.hansen, hpa,
ardb, ilias.apalodimas, James.Bottomley, javierm, lszubowi,
francescopompo2, tgopinath, x86, linux-hyperv, linux-kernel,
linux-efi, Anna Trikalinou
On Tue, Sep 01, 2026 at 09:55:17AM -0700, Thara Gopinath wrote:
> The VSM secure kernel runs in VTL1 and cannot use the normal Linux
> kernel memory allocators — its memory must be reserved before the
> general-purpose allocator takes ownership of the physical address space.
>
> Add hv_vsm_securekernel.c to handle this early boot reservation. The
> driver parses a new "securekernel=<size>[K|M|G][@<addr>]" kernel command
We should add a prefix to it, unless you tell me there is a plan to
extend this to other virtualization solutions.
Wei
^ permalink raw reply [flat|nested] 34+ messages in thread
* Re: [RFC PATCH 03/12] drivers: hv: Reserve memory for VSM secure kernel during early boot
2026-09-02 0:59 ` Wei Liu
@ 2026-09-02 13:38 ` Thara Gopinath
0 siblings, 0 replies; 34+ messages in thread
From: Thara Gopinath @ 2026-09-02 13:38 UTC (permalink / raw)
To: Wei Liu
Cc: kys, haiyangz, decui, tglx, mingo, bp, dave.hansen, hpa, ardb,
ilias.apalodimas, James.Bottomley, javierm, lszubowi,
francescopompo2, tgopinath, x86, linux-hyperv, linux-kernel,
linux-efi, Anna Trikalinou
On 9/1/2026 8:59 PM, Wei Liu wrote:
> On Tue, Sep 01, 2026 at 09:55:17AM -0700, Thara Gopinath wrote:
>> The VSM secure kernel runs in VTL1 and cannot use the normal Linux
>> kernel memory allocators — its memory must be reserved before the
>> general-purpose allocator takes ownership of the physical address space.
>>
>> Add hv_vsm_securekernel.c to handle this early boot reservation. The
>> driver parses a new "securekernel=<size>[K|M|G][@<addr>]" kernel command
>
> We should add a prefix to it, unless you tell me there is a plan to
> extend this to other virtualization solutions.
Thanks for the reviews Wei.. Yes I will add a prefix hv_ and we can
remove it if/when this gets extended to other virtualization solutions.
Warm Regards
Thara
>
> Wei
^ permalink raw reply [flat|nested] 34+ messages in thread
* [RFC PATCH 04/12] firmware: efi: libstub: x86-stub: Enable VSM awareness in efi os indications variable
2026-09-01 16:55 [RFC PATCH 00/12] Introduce LVBS support for Hyper-V guests Thara Gopinath
` (2 preceding siblings ...)
2026-09-01 16:55 ` [RFC PATCH 03/12] drivers: hv: Reserve memory for VSM secure kernel during early boot Thara Gopinath
@ 2026-09-01 16:55 ` Thara Gopinath
2026-09-01 17:09 ` sashiko-bot
2026-09-02 1:09 ` Wei Liu
2026-09-01 16:55 ` [RFC PATCH 05/12] include: hyperv: hvgdk_mini.h: Add VTL-specific structures and bits Thara Gopinath
` (7 subsequent siblings)
11 siblings, 2 replies; 34+ messages in thread
From: Thara Gopinath @ 2026-09-01 16:55 UTC (permalink / raw)
To: kys, haiyangz, wei.liu, decui, tglx, mingo, bp, dave.hansen, hpa,
ardb, ilias.apalodimas
Cc: James.Bottomley, "longli, javierm, lszubowi, francescopompo2,
tgopinath, x86, linux-hyperv, linux-kernel, linux-efi,
Thara Gopinath
Set bit 0 of the Hyper-V private OsLoaderIndications EFI variable
during exit_boot() so the bootloader/firmware knows the OS intends
to enable VTL1. Without this, VTL1 cannot be brought up from the
Linux kernel.
The support bit is first checked in OsLoaderIndicationsSupported,
and the variable is only written when the VSM bit is not already
set.
Signed-off-by: Thara Gopinath <tgopinath@linux.microsoft.com>
---
drivers/firmware/efi/libstub/x86-stub.c | 57 +++++++++++++++++++++++++
1 file changed, 57 insertions(+)
diff --git a/drivers/firmware/efi/libstub/x86-stub.c b/drivers/firmware/efi/libstub/x86-stub.c
index cef32e2c82d8f..ab3cd4fe36599 100644
--- a/drivers/firmware/efi/libstub/x86-stub.c
+++ b/drivers/firmware/efi/libstub/x86-stub.c
@@ -21,6 +21,17 @@
#include "efistub.h"
#include "x86-stub.h"
+#ifdef CONFIG_HYPERV_VSM
+#define HYPERV_PRIVATE_EFI_NAMESPACE_GUID \
+ EFI_GUID(0x610b9e98, 0xc6f6, 0x47f8, 0x8b, 0x47, 0x2d, 0x2d, 0xa0, 0xd5, 0x2a, 0x91)
+
+static const efi_char16_t efi_HvPrivOsloaderIndications_name[] = L"OsLoaderIndications";
+static const efi_char16_t efi_HvPrivOsloaderIndicationsSupported_name[] =
+ L"OsLoaderIndicationsSupported";
+#define HV_OSLOADER_INDICATION_VSM BIT(0)
+
+#endif
+
extern char _bss[], _ebss[];
const efi_system_table_t *efi_system_table;
@@ -754,6 +765,47 @@ static efi_status_t exit_boot_func(struct efi_boot_memmap *map,
return EFI_SUCCESS;
}
+#ifdef CONFIG_HYPERV_VSM
+static void efi_set_hv_os_indications(void)
+{
+ efi_guid_t guid = HYPERV_PRIVATE_EFI_NAMESPACE_GUID;
+ efi_status_t status;
+ unsigned long size;
+ u32 attr, val;
+
+ size = sizeof(val);
+ status = get_efi_var(efi_HvPrivOsloaderIndicationsSupported_name,
+ &guid, &attr, &size, &val);
+ if (status != EFI_SUCCESS) {
+ efi_err("Could not read Hyper-V OsloaderIndicationsSupported\n");
+ return;
+ }
+
+ if (!(val & HV_OSLOADER_INDICATION_VSM)) {
+ efi_info("Hyper-V does not support VSM in OsloaderIndicationsSupported\n");
+ return;
+ }
+
+ size = sizeof(val);
+ status = get_efi_var(efi_HvPrivOsloaderIndications_name, &guid, &attr, &size, &val);
+ if (status != EFI_SUCCESS) {
+ efi_err("Could not read Hyper-V OsLoaderIndications\n");
+ return;
+ }
+
+ if (val & HV_OSLOADER_INDICATION_VSM) {
+ efi_info("VSM is already supported in OsLoaderIndications.");
+ return;
+ }
+
+ val |= HV_OSLOADER_INDICATION_VSM;
+ size = sizeof(val);
+ status = set_efi_var(efi_HvPrivOsloaderIndications_name, &guid, attr, size, &val);
+ if (status != EFI_SUCCESS)
+ efi_err("Could not set Hyper-V OsLoaderIndications to indicate VSM support\n");
+}
+#endif
+
static efi_status_t exit_boot(struct boot_params *boot_params, void *handle)
{
struct setup_data *e820ext = NULL;
@@ -768,6 +820,11 @@ static efi_status_t exit_boot(struct boot_params *boot_params, void *handle)
if (status != EFI_SUCCESS)
return status;
+#ifdef CONFIG_HYPERV_VSM
+ /* Indicate to bootloader that we will be enabling VTL1 before exiting boot services */
+ efi_set_hv_os_indications();
+#endif
+
/* Might as well exit boot services now */
status = efi_exit_boot_services(handle, &priv, exit_boot_func);
if (status != EFI_SUCCESS)
--
2.34.1
^ permalink raw reply related [flat|nested] 34+ messages in thread* Re: [RFC PATCH 04/12] firmware: efi: libstub: x86-stub: Enable VSM awareness in efi os indications variable
2026-09-01 16:55 ` [RFC PATCH 04/12] firmware: efi: libstub: x86-stub: Enable VSM awareness in efi os indications variable Thara Gopinath
@ 2026-09-01 17:09 ` sashiko-bot
2026-09-02 1:09 ` Wei Liu
1 sibling, 0 replies; 34+ messages in thread
From: sashiko-bot @ 2026-09-01 17:09 UTC (permalink / raw)
To: Thara Gopinath; +Cc: linux-hyperv
Thank you for your contribution! Sashiko AI review found 4 potential issue(s) to consider:
- [High] Uninitialized stack memory leak to NVRAM and unintended EFI variable size modification.
- [Medium] Unconditional execution of `efi_set_hv_os_indications()` causes an EFI error message on all non-Hyper-V systems.
- [Medium] `efi_set_hv_os_indications()` fails to enable VSM if `OsLoaderIndications` does not already exist.
- [Low] Missing newline character in `efi_info()` output.
--
commit 3d285ae76c7e0c1fdff6a68f110b5b5a021e7b38
Author: Thara Gopinath <tgopinath@linux.microsoft.com>
firmware: efi: libstub: x86-stub: Enable VSM awareness in efi os indications variable
This commit sets bit 0 of the Hyper-V private OsLoaderIndications EFI variable
during exit_boot() to let the bootloader or firmware know that the OS intends
to enable VTL1. The variable is written only when supported and when the bit
is not already set.
> diff --git a/drivers/firmware/efi/libstub/x86-stub.c b/drivers/firmware/efi/libstub/x86-stub.c
> index cef32e2c82d8f..ab3cd4fe36599 100644
> --- a/drivers/firmware/efi/libstub/x86-stub.c
> +++ b/drivers/firmware/efi/libstub/x86-stub.c
[ ... ]
> @@ -754,6 +765,47 @@ static efi_status_t exit_boot_func(struct efi_boot_memmap *map,
> return EFI_SUCCESS;
> }
>
> +#ifdef CONFIG_HYPERV_VSM
> +static void efi_set_hv_os_indications(void)
> +{
> + efi_guid_t guid = HYPERV_PRIVATE_EFI_NAMESPACE_GUID;
> + efi_status_t status;
> + unsigned long size;
> + u32 attr, val;
[Severity: High]
Since val is declared without zero-initialization, is it possible for
get_efi_var() to copy fewer than 4 bytes if the actual size of the variable
in NVRAM is smaller?
If so, the upper bytes of val might remain as uninitialized stack memory.
When set_efi_var() is subsequently called with size = sizeof(val), could
this leak up to 3 bytes of uninitialized kernel stack memory into persistent
NVRAM?
Also, could this force the EFI variable to be exactly 4 bytes, potentially
corrupting it if the firmware expects a different size?
> +
> + size = sizeof(val);
> + status = get_efi_var(efi_HvPrivOsloaderIndicationsSupported_name,
> + &guid, &attr, &size, &val);
> + if (status != EFI_SUCCESS) {
> + efi_err("Could not read Hyper-V OsloaderIndicationsSupported\n");
> + return;
> + }
> +
> + if (!(val & HV_OSLOADER_INDICATION_VSM)) {
> + efi_info("Hyper-V does not support VSM in OsloaderIndicationsSupported\n");
> + return;
> + }
> +
> + size = sizeof(val);
> + status = get_efi_var(efi_HvPrivOsloaderIndications_name, &guid, &attr, &size, &val);
> + if (status != EFI_SUCCESS) {
> + efi_err("Could not read Hyper-V OsLoaderIndications\n");
[Severity: Medium]
If the OsLoaderIndications variable does not exist (for example, on a fresh
OS installation or if not pre-populated by firmware), get_efi_var() will
return EFI_NOT_FOUND.
Does treating this as a fatal error prevent VTL1 from being brought up?
Would it be better to handle EFI_NOT_FOUND by assuming val is 0 and
proceeding to create the variable?
> + return;
> + }
> +
> + if (val & HV_OSLOADER_INDICATION_VSM) {
> + efi_info("VSM is already supported in OsLoaderIndications.");
[Severity: Low]
Does this efi_info() string need a trailing newline character?
Because efi_info() maps to efi_printk() which does not append newlines
automatically, this might cause subsequent EFI console output to be
concatenated onto the same line.
> + return;
> + }
> +
> + val |= HV_OSLOADER_INDICATION_VSM;
> + size = sizeof(val);
> + status = set_efi_var(efi_HvPrivOsloaderIndications_name, &guid, attr, size, &val);
> + if (status != EFI_SUCCESS)
> + efi_err("Could not set Hyper-V OsLoaderIndications to indicate VSM support\n");
> +}
> +#endif
> +
> static efi_status_t exit_boot(struct boot_params *boot_params, void *handle)
> {
> struct setup_data *e820ext = NULL;
> @@ -768,6 +820,11 @@ static efi_status_t exit_boot(struct boot_params *boot_params, void *handle)
> if (status != EFI_SUCCESS)
> return status;
>
> +#ifdef CONFIG_HYPERV_VSM
> + /* Indicate to bootloader that we will be enabling VTL1 before exiting boot services */
> + efi_set_hv_os_indications();
[Severity: Medium]
Since this function is called unconditionally on all x86 EFI boots when
CONFIG_HYPERV_VSM is enabled, will this cause get_efi_var() to return
EFI_NOT_FOUND on systems without Hyper-V?
It seems this could result in spurious error messages in the boot log for
all non-Hyper-V users because efi_err() is called unconditionally inside
efi_set_hv_os_indications().
Should there be a check for Hyper-V presence before making this call?
> +#endif
> +
> /* Might as well exit boot services now */
> status = efi_exit_boot_services(handle, &priv, exit_boot_func);
> if (status != EFI_SUCCESS)
--
Sashiko AI review · https://sashiko.dev/#/patchset/20260901165647.3160413-1-tgopinath@linux.microsoft.com?part=4
^ permalink raw reply [flat|nested] 34+ messages in thread* Re: [RFC PATCH 04/12] firmware: efi: libstub: x86-stub: Enable VSM awareness in efi os indications variable
2026-09-01 16:55 ` [RFC PATCH 04/12] firmware: efi: libstub: x86-stub: Enable VSM awareness in efi os indications variable Thara Gopinath
2026-09-01 17:09 ` sashiko-bot
@ 2026-09-02 1:09 ` Wei Liu
2026-09-02 14:23 ` Thara Gopinath
1 sibling, 1 reply; 34+ messages in thread
From: Wei Liu @ 2026-09-02 1:09 UTC (permalink / raw)
To: Thara Gopinath
Cc: kys, haiyangz, wei.liu, decui, tglx, mingo, bp, dave.hansen, hpa,
ardb, ilias.apalodimas, James.Bottomley, javierm, lszubowi,
francescopompo2, tgopinath, x86, linux-hyperv, linux-kernel,
linux-efi
On Tue, Sep 01, 2026 at 09:55:18AM -0700, Thara Gopinath wrote:
> Set bit 0 of the Hyper-V private OsLoaderIndications EFI variable
> during exit_boot() so the bootloader/firmware knows the OS intends
> to enable VTL1. Without this, VTL1 cannot be brought up from the
> Linux kernel.
>
> The support bit is first checked in OsLoaderIndicationsSupported,
> and the variable is only written when the VSM bit is not already
> set.
>
> Signed-off-by: Thara Gopinath <tgopinath@linux.microsoft.com>
> ---
> drivers/firmware/efi/libstub/x86-stub.c | 57 +++++++++++++++++++++++++
> 1 file changed, 57 insertions(+)
>
[...]
> +#ifdef CONFIG_HYPERV_VSM
> +static void efi_set_hv_os_indications(void)
> +{
> + efi_guid_t guid = HYPERV_PRIVATE_EFI_NAMESPACE_GUID;
> + efi_status_t status;
> + unsigned long size;
> + u32 attr, val;
> +
> + size = sizeof(val);
> + status = get_efi_var(efi_HvPrivOsloaderIndicationsSupported_name,
> + &guid, &attr, &size, &val);
> + if (status != EFI_SUCCESS) {
> + efi_err("Could not read Hyper-V OsloaderIndicationsSupported\n");
> + return;
> + }
> +
> + if (!(val & HV_OSLOADER_INDICATION_VSM)) {
> + efi_info("Hyper-V does not support VSM in OsloaderIndicationsSupported\n");
> + return;
> + }
> +
> + size = sizeof(val);
> + status = get_efi_var(efi_HvPrivOsloaderIndications_name, &guid, &attr, &size, &val);
> + if (status != EFI_SUCCESS) {
> + efi_err("Could not read Hyper-V OsLoaderIndications\n");
> + return;
> + }
> +
> + if (val & HV_OSLOADER_INDICATION_VSM) {
> + efi_info("VSM is already supported in OsLoaderIndications.");
> + return;
> + }
> +
> + val |= HV_OSLOADER_INDICATION_VSM;
> + size = sizeof(val);
> + status = set_efi_var(efi_HvPrivOsloaderIndications_name, &guid, attr, size, &val);
I'm not familiar with the security model, so bear with me.
What happens if the VTL0 kernel doesn't use VTL1 at all? Does that
become a security issue, that malware can use the VTL1 to hide itself?
Asking this because I think you will want to enable this in the generic
kernel(s). Not all users have or want to package a secure kernel.
Wei
> + if (status != EFI_SUCCESS)
> + efi_err("Could not set Hyper-V OsLoaderIndications to indicate VSM support\n");
> +}
> +#endif
> +
> static efi_status_t exit_boot(struct boot_params *boot_params, void *handle)
> {
> struct setup_data *e820ext = NULL;
> @@ -768,6 +820,11 @@ static efi_status_t exit_boot(struct boot_params *boot_params, void *handle)
> if (status != EFI_SUCCESS)
> return status;
>
> +#ifdef CONFIG_HYPERV_VSM
> + /* Indicate to bootloader that we will be enabling VTL1 before exiting boot services */
> + efi_set_hv_os_indications();
> +#endif
> +
> /* Might as well exit boot services now */
> status = efi_exit_boot_services(handle, &priv, exit_boot_func);
> if (status != EFI_SUCCESS)
> --
> 2.34.1
>
^ permalink raw reply [flat|nested] 34+ messages in thread* Re: [RFC PATCH 04/12] firmware: efi: libstub: x86-stub: Enable VSM awareness in efi os indications variable
2026-09-02 1:09 ` Wei Liu
@ 2026-09-02 14:23 ` Thara Gopinath
0 siblings, 0 replies; 34+ messages in thread
From: Thara Gopinath @ 2026-09-02 14:23 UTC (permalink / raw)
To: Wei Liu
Cc: kys, haiyangz, decui, tglx, mingo, bp, dave.hansen, hpa, ardb,
ilias.apalodimas, James.Bottomley, javierm, lszubowi,
francescopompo2, tgopinath, x86, linux-hyperv, linux-kernel,
linux-efi
On 9/1/2026 9:09 PM, Wei Liu wrote:
> On Tue, Sep 01, 2026 at 09:55:18AM -0700, Thara Gopinath wrote:
>> Set bit 0 of the Hyper-V private OsLoaderIndications EFI variable
>> during exit_boot() so the bootloader/firmware knows the OS intends
>> to enable VTL1. Without this, VTL1 cannot be brought up from the
>> Linux kernel.
>>
>> The support bit is first checked in OsLoaderIndicationsSupported,
>> and the variable is only written when the VSM bit is not already
>> set.
>>
>> Signed-off-by: Thara Gopinath <tgopinath@linux.microsoft.com>
>> ---
>> drivers/firmware/efi/libstub/x86-stub.c | 57 +++++++++++++++++++++++++
>> 1 file changed, 57 insertions(+)
>>
> [...]
>> +#ifdef CONFIG_HYPERV_VSM
>> +static void efi_set_hv_os_indications(void)
>> +{
>> + efi_guid_t guid = HYPERV_PRIVATE_EFI_NAMESPACE_GUID;
>> + efi_status_t status;
>> + unsigned long size;
>> + u32 attr, val;
>> +
>> + size = sizeof(val);
>> + status = get_efi_var(efi_HvPrivOsloaderIndicationsSupported_name,
>> + &guid, &attr, &size, &val);
>> + if (status != EFI_SUCCESS) {
>> + efi_err("Could not read Hyper-V OsloaderIndicationsSupported\n");
>> + return;
>> + }
>> +
>> + if (!(val & HV_OSLOADER_INDICATION_VSM)) {
>> + efi_info("Hyper-V does not support VSM in OsloaderIndicationsSupported\n");
>> + return;
>> + }
>> +
>> + size = sizeof(val);
>> + status = get_efi_var(efi_HvPrivOsloaderIndications_name, &guid, &attr, &size, &val);
>> + if (status != EFI_SUCCESS) {
>> + efi_err("Could not read Hyper-V OsLoaderIndications\n");
>> + return;
>> + }
>> +
>> + if (val & HV_OSLOADER_INDICATION_VSM) {
>> + efi_info("VSM is already supported in OsLoaderIndications.");
>> + return;
>> + }
>> +
>> + val |= HV_OSLOADER_INDICATION_VSM;
>> + size = sizeof(val);
>> + status = set_efi_var(efi_HvPrivOsloaderIndications_name, &guid, attr, size, &val);
>
> I'm not familiar with the security model, so bear with me.
>
> What happens if the VTL0 kernel doesn't use VTL1 at all? Does that
> become a security issue, that malware can use the VTL1 to hide itself?
>
> Asking this because I think you will want to enable this in the generic
> kernel(s). Not all users have or want to package a secure kernel.
Yes you are right. If we do this and a secure kernel is not loaded, it is a
security hole. Which is why this is bound by the same config option CONFIG_HYPERV_VSM
that does the secure kernel boot and in that path any error / inability to load
and establish VTL1 is treated as a serious error and we panic. Generic kernels
should not enable this option at all. The CONFIG_HYPERV_VSM should be enabled
only if it is known that VTL1 environment can be established. Otherwise the system
will not boot and will panic.
Warm Regards
Thara
>
> Wei
>
>> + if (status != EFI_SUCCESS)
>> + efi_err("Could not set Hyper-V OsLoaderIndications to indicate VSM support\n");
>> +}
>> +#endif
>> +
>> static efi_status_t exit_boot(struct boot_params *boot_params, void *handle)
>> {
>> struct setup_data *e820ext = NULL;
>> @@ -768,6 +820,11 @@ static efi_status_t exit_boot(struct boot_params *boot_params, void *handle)
>> if (status != EFI_SUCCESS)
>> return status;
>>
>> +#ifdef CONFIG_HYPERV_VSM
>> + /* Indicate to bootloader that we will be enabling VTL1 before exiting boot services */
>> + efi_set_hv_os_indications();
>> +#endif
>> +
>> /* Might as well exit boot services now */
>> status = efi_exit_boot_services(handle, &priv, exit_boot_func);
>> if (status != EFI_SUCCESS)
>> --
>> 2.34.1
>>
^ permalink raw reply [flat|nested] 34+ messages in thread
* [RFC PATCH 05/12] include: hyperv: hvgdk_mini.h: Add VTL-specific structures and bits
2026-09-01 16:55 [RFC PATCH 00/12] Introduce LVBS support for Hyper-V guests Thara Gopinath
` (3 preceding siblings ...)
2026-09-01 16:55 ` [RFC PATCH 04/12] firmware: efi: libstub: x86-stub: Enable VSM awareness in efi os indications variable Thara Gopinath
@ 2026-09-01 16:55 ` Thara Gopinath
2026-09-01 16:55 ` [RFC PATCH 06/12] drivers: hv: Add VSM boot driver and enable VTL1 at the partition level Thara Gopinath
` (6 subsequent siblings)
11 siblings, 0 replies; 34+ messages in thread
From: Thara Gopinath @ 2026-09-01 16:55 UTC (permalink / raw)
To: kys, haiyangz, wei.liu, decui, tglx, mingo, bp, dave.hansen, hpa,
ardb, ilias.apalodimas
Cc: James.Bottomley, "longli, javierm, lszubowi, francescopompo2,
tgopinath, x86, linux-hyperv, linux-kernel, linux-efi,
Thara Gopinath
Add the definitions needed by the upcoming VSM / VTL1 bring-up code:
- HV_ACCESS_VSM and HV_ACCESS_VP_REGS partition privilege bits
- HVCALL_ENABLE_PARTITION_VTL and
HVCALL_MODIFY_VTL_PROTECTION_MASK hypercall IDs
- hv_input_enable_partition_vtl input structure and its flags union
- hv_register_vsm_{partition_status,vp_status} register layout unions
- HV_REGISTER_VSM_PARTITION_STATUS and
HV_REGISTER_VSM_VP_SECURE_CONFIG_VTL0 register names
Definitions taken from the Hyper-V TLFS.
Signed-off-by: Thara Gopinath <tgopinath@linux.microsoft.com>
---
include/hyperv/hvgdk_mini.h | 46 +++++++++++++++++++++++++++++++++++++
1 file changed, 46 insertions(+)
diff --git a/include/hyperv/hvgdk_mini.h b/include/hyperv/hvgdk_mini.h
index 6a4e8b9d570fd..a50af532fc7f9 100644
--- a/include/hyperv/hvgdk_mini.h
+++ b/include/hyperv/hvgdk_mini.h
@@ -303,6 +303,8 @@ union hv_hypervisor_version_info {
#define HV_ACCESS_STATS BIT(8)
#define HV_DEBUGGING BIT(11)
#define HV_CPU_MANAGEMENT BIT(12)
+#define HV_ACCESS_VSM BIT(16)
+#define HV_ACCESS_VP_REGS BIT(17)
#define HV_ENABLE_EXTENDED_HYPERCALLS BIT(20)
#define HV_ISOLATION BIT(22)
@@ -438,6 +440,8 @@ union hv_vp_assist_msr_contents { /* HV_REGISTER_VP_ASSIST_PAGE */
#define HVCALL_GET_LOGICAL_PROCESSOR_RUN_TIME 0x0004
#define HVCALL_NOTIFY_LONG_SPIN_WAIT 0x0008
#define HVCALL_SEND_IPI 0x000b
+#define HVCALL_MODIFY_VTL_PROTECTION_MASK 0x000c
+#define HVCALL_ENABLE_PARTITION_VTL 0x000d
#define HVCALL_ENABLE_VP_VTL 0x000f
#define HVCALL_FLUSH_VIRTUAL_ADDRESS_SPACE_EX 0x0013
#define HVCALL_FLUSH_VIRTUAL_ADDRESS_LIST_EX 0x0014
@@ -878,6 +882,46 @@ struct hv_init_vp_context {
u64 msr_cr_pat;
} __packed;
+union hv_enable_partition_vtl_flags {
+ u8 as_uint8;
+ struct {
+ u8 enable_mbec:1;
+ u8 enable_supervisor_shadow_stack:1;
+ u8 enable_hardware_hvpt:1;
+ u8 reserved:5;
+ };
+} __packed;
+
+struct hv_input_enable_partition_vtl {
+ u64 partition_id;
+ union hv_input_vtl target_vtl;
+ union hv_enable_partition_vtl_flags flags;
+ u16 rsvd_z16;
+ u32 rsvd_z32;
+} __packed;
+
+union hv_register_vsm_partition_status {
+ u64 as_uint64;
+ struct {
+ u64 enabled_vtl_set : 16;
+ u64 max_vtl : 4;
+ u64 mbec_enabled_vtl_set: 16;
+ u64 supervisor_shadow_stack_enabled_vtl_set : 4;
+ u64 reserved : 24;
+ };
+} __packed;
+
+union hv_register_vsm_vp_status {
+ u64 as_uint64;
+ struct {
+ u64 active_vtl : 4;
+ u64 active_mbec_enabled : 1;
+ u64 reserved_z0 : 11;
+ u64 enabled_vtl_set : 16;
+ u64 reserved_z1 : 32;
+ };
+} __packed;
+
struct hv_enable_vp_vtl {
u64 partition_id;
u32 vp_index;
@@ -1061,8 +1105,10 @@ enum hv_register_name {
/* Synthetic VSM registers */
HV_REGISTER_VSM_CODE_PAGE_OFFSETS = 0x000D0002,
+ HV_REGISTER_VSM_PARTITION_STATUS = 0x000D0004,
HV_REGISTER_VSM_CAPABILITIES = 0x000D0006,
HV_REGISTER_VSM_PARTITION_CONFIG = 0x000D0007,
+ HV_REGISTER_VSM_VP_SECURE_CONFIG_VTL0 = 0x000D0010,
#if defined(CONFIG_X86)
/* X64 Debug Registers */
--
2.34.1
^ permalink raw reply related [flat|nested] 34+ messages in thread* [RFC PATCH 06/12] drivers: hv: Add VSM boot driver and enable VTL1 at the partition level
2026-09-01 16:55 [RFC PATCH 00/12] Introduce LVBS support for Hyper-V guests Thara Gopinath
` (4 preceding siblings ...)
2026-09-01 16:55 ` [RFC PATCH 05/12] include: hyperv: hvgdk_mini.h: Add VTL-specific structures and bits Thara Gopinath
@ 2026-09-01 16:55 ` Thara Gopinath
2026-09-01 17:24 ` sashiko-bot
` (2 more replies)
2026-09-01 16:55 ` [RFC PATCH 07/12] drivers: hv: hv_vsm_boot: load secure kernel image from firmware Thara Gopinath
` (5 subsequent siblings)
11 siblings, 3 replies; 34+ messages in thread
From: Thara Gopinath @ 2026-09-01 16:55 UTC (permalink / raw)
To: kys, haiyangz, wei.liu, decui, tglx, mingo, bp, dave.hansen, hpa,
ardb, ilias.apalodimas
Cc: James.Bottomley, "longli, javierm, lszubowi, francescopompo2,
tgopinath, x86, linux-hyperv, linux-kernel, linux-efi,
Thara Gopinath
Linux VBS (LVBS) uses Hyper-V's Virtual Secure Mode to run a small
trusted kernel in VTL1 alongside the regular VTL0 kernel, so that
security-sensitive state (e.g. hypervisor-enforced code integrity,
credential isolation) can live behind a higher-privilege boundary
that VTL0 compromise cannot cross. Bringing that up from Linux
requires the VTL0 kernel to drive the VSM setup itself.
Add drivers/hv/hv_vsm_boot.c as the entry point for that sequence.
This first step handles partition-level VTL1 enable only:
- Probe VSM / VP-register privileges and SynIC availability before
doing anything.
- Pin init to the VTL0 boot CPU so VTL1 comes up on the same CPU
(later patches rely on this).
- Read HV_REGISTER_VSM_PARTITION_STATUS, and if VTL1 is not already
enabled, issue HVCALL_ENABLE_PARTITION_VTL with MBEC and confirm
by re-reading the register.
Signed-off-by: Thara Gopinath <tgopinath@linux.microsoft.com>
---
drivers/hv/Makefile | 4 +-
drivers/hv/hv_vsm_boot.c | 178 +++++++++++++++++++++++++++++++++++++++
2 files changed, 180 insertions(+), 2 deletions(-)
create mode 100644 drivers/hv/hv_vsm_boot.c
diff --git a/drivers/hv/Makefile b/drivers/hv/Makefile
index 880c570832381..563ebc36d2700 100644
--- a/drivers/hv/Makefile
+++ b/drivers/hv/Makefile
@@ -22,7 +22,7 @@ mshv_vtl-y := mshv_vtl_main.o
# Code that must be built-in
obj-$(CONFIG_HYPERV) += hv_common.o
obj-$(subst m,y,$(CONFIG_MSHV_ROOT)) += hv_proc.o
-ifneq ($(CONFIG_MSHV_ROOT)$(CONFIG_MSHV_VTL),)
+ifneq ($(CONFIG_MSHV_ROOT)$(CONFIG_MSHV_VTL)$(CONFIG_HYPERV_VSM),)
obj-y += mshv_common.o
endif
-obj-$(subst m,y,$(CONFIG_HYPERV_VSM)) += hv_vsm_securekernel.o
+obj-$(subst m,y,$(CONFIG_HYPERV_VSM)) += hv_vsm_securekernel.o hv_vsm_boot.o
diff --git a/drivers/hv/hv_vsm_boot.c b/drivers/hv/hv_vsm_boot.c
new file mode 100644
index 0000000000000..99e4dc8695837
--- /dev/null
+++ b/drivers/hv/hv_vsm_boot.c
@@ -0,0 +1,178 @@
+// SPDX-License-Identifier: GPL-2.0
+/*
+ * VSM boot framework that enables VTL1, loads secure kernel
+ * and boots VTL1.
+ *
+ * Copyright (c) 2023-2025, Microsoft Corporation.
+ *
+ * Author: Thara Gopinath <tgopinath@linux.microsoft.com>
+ *
+ */
+
+#define pr_fmt(fmt) "vsm: " fmt
+
+#include <linux/hyperv.h>
+#include <linux/cpumask.h>
+#include <asm/mshyperv.h>
+#include "mshv.h"
+
+#define HV_VTL1_ENABLE_BIT BIT(1)
+
+static int hv_vsm_get_register(u32 reg_name, u64 *result)
+{
+ struct hv_register_assoc reg = {
+ .name = reg_name,
+ };
+ union hv_input_vtl input_vtl = {
+ .as_uint8 = 0,
+ };
+ int ret;
+
+ ret = hv_call_get_vp_registers(HV_VP_INDEX_SELF,
+ HV_PARTITION_ID_SELF,
+ 1, input_vtl, ®);
+ if (ret)
+ return ret;
+
+ *result = reg.value.reg64;
+ return 0;
+}
+
+static int __init hv_vsm_enable_partition_vtl(void)
+{
+ u64 status = 0;
+ unsigned long flags;
+ struct hv_input_enable_partition_vtl *hvin = NULL;
+
+ local_irq_save(flags);
+
+ hvin = *this_cpu_ptr(hyperv_pcpu_input_arg);
+ memset(hvin, 0, sizeof(*hvin));
+
+ hvin->partition_id = HV_PARTITION_ID_SELF;
+ hvin->target_vtl.as_uint8 = 1;
+ hvin->flags.enable_mbec = 1;
+
+ status = hv_do_hypercall(HVCALL_ENABLE_PARTITION_VTL, hvin, NULL);
+ if (hv_result(status))
+ pr_err("Enable Partition VTL failed. status=0x%x\n",
+ hv_result(status));
+
+ local_irq_restore(flags);
+
+ return hv_result(status);
+}
+
+static int __init hv_vsm_get_partition_status(u16 *enabled_vtl_set, u8 *max_vtl,
+ u16 *mbec_enabled_vtl_set)
+{
+ u64 result;
+ int ret;
+ union hv_register_vsm_partition_status vsm_partition_status = { 0 };
+
+ ret = hv_vsm_get_register(HV_REGISTER_VSM_PARTITION_STATUS, &result);
+ if (ret)
+ return ret;
+
+ vsm_partition_status = (union hv_register_vsm_partition_status)result;
+ *enabled_vtl_set = vsm_partition_status.enabled_vtl_set;
+ *max_vtl = vsm_partition_status.max_vtl;
+ *mbec_enabled_vtl_set = vsm_partition_status.mbec_enabled_vtl_set;
+ return 0;
+}
+
+static int __init hv_vsm_bootstrap_vtl(void)
+{
+ u16 partition_enabled_vtl_set = 0, partition_mbec_enabled_vtl_set = 0;
+ u8 partition_max_vtl;
+ int ret;
+
+ /* Check and enable VTL1 at the partition level */
+ ret = hv_vsm_get_partition_status(&partition_enabled_vtl_set, &partition_max_vtl,
+ &partition_mbec_enabled_vtl_set);
+ if (ret)
+ return ret;
+
+ if (partition_max_vtl < HV_VTL_SECURE) {
+ pr_err("VTL1 is not supported by the partition\n");
+ return -EINVAL;
+ }
+
+ if (partition_enabled_vtl_set & HV_VTL1_ENABLE_BIT) {
+ pr_info("Partition VTL1 is already enabled\n");
+ } else {
+ ret = hv_vsm_enable_partition_vtl();
+ if (ret) {
+ pr_err("Enabling Partition VTL1 failed with status 0x%x\n",
+ ret);
+ return -EINVAL;
+ }
+ ret = hv_vsm_get_partition_status(&partition_enabled_vtl_set, &partition_max_vtl,
+ &partition_mbec_enabled_vtl_set);
+ if (ret)
+ return ret;
+ if (!(partition_enabled_vtl_set & HV_VTL1_ENABLE_BIT)) {
+ pr_err("Tried Enabling Partition VTL 1 and still failed\n");
+ return -EINVAL;
+ }
+ if (!partition_mbec_enabled_vtl_set) {
+ pr_err("Tried Enabling Partition MBEC and failed\n");
+ return -EINVAL;
+ }
+ }
+ return 0;
+}
+
+static bool __init vsm_arch_has_vsm_access(void)
+{
+ if (!(ms_hyperv.features & HV_MSR_SYNIC_AVAILABLE))
+ return false;
+ if (!(ms_hyperv.priv_high & HV_ACCESS_VSM))
+ return false;
+ if (!(ms_hyperv.priv_high & HV_ACCESS_VP_REGS))
+ return false;
+ return true;
+}
+
+static int __init hv_vsm_boot_init(void)
+{
+ cpumask_var_t mask;
+ unsigned int boot_cpu;
+ int ret;
+
+ if (!vsm_arch_has_vsm_access())
+ return 0;
+
+ /*
+ * 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.
+ * ToDo: Check if copying and restoring current->cpus_mask is enough
+ * ToDo: Verify the assumption that cpumask_first(cpu_online_mask) is
+ * the boot cpu
+ */
+ if (!alloc_cpumask_var(&mask, GFP_KERNEL))
+ panic("Could not allocate cpumask");
+
+ cpumask_copy(mask, ¤t->cpus_mask);
+ boot_cpu = cpumask_first(cpu_online_mask);
+ set_cpus_allowed_ptr(current, cpumask_of(boot_cpu));
+
+ ret = hv_vsm_bootstrap_vtl();
+ /*
+ * At this point VTL0 has already advertised VSM support to the
+ * bootloader/firmware via the Hyper-V OsLoaderIndications EFI
+ * variable (see the x86-stub change). That signals the platform
+ * that a trusted VTL1 will be brought up. If we fail to actually
+ * set VTL1 up here, the partition is left in a state where an
+ * attacker could race to configure VTL1 themselves and gain a
+ * higher-privilege foothold than VTL0. Panic rather than continue
+ * running with that exposure.
+ */
+ if (ret)
+ panic("VTL1 boot failure caused kernel panic; consult log for more details.\n");
+
+ set_cpus_allowed_ptr(current, mask);
+ free_cpumask_var(mask);
+ return ret;
+}
+device_initcall(hv_vsm_boot_init);
--
2.34.1
^ permalink raw reply related [flat|nested] 34+ messages in thread* Re: [RFC PATCH 06/12] drivers: hv: Add VSM boot driver and enable VTL1 at the partition level
2026-09-01 16:55 ` [RFC PATCH 06/12] drivers: hv: Add VSM boot driver and enable VTL1 at the partition level Thara Gopinath
@ 2026-09-01 17:24 ` sashiko-bot
2026-09-02 1:16 ` Wei Liu
2026-09-02 4:43 ` Wei Liu
2 siblings, 0 replies; 34+ messages in thread
From: sashiko-bot @ 2026-09-01 17:24 UTC (permalink / raw)
To: Thara Gopinath; +Cc: linux-hyperv
Thank you for your contribution! Sashiko AI review found 3 potential issue(s) to consider:
- [High] Incorrect validation of the MBEC enablement bitmask allows silent failure of VTL1 security configuration.
- [High] Missing MBEC validation when VTL1 is found to be already enabled, allowing acceptance of insecure pre-configured VTL1 states.
- [Medium] hv_vsm_boot_init pins initialization to cpumask_first(cpu_online_mask), which is incorrect for kexec/kdump where the boot CPU may not be CPU 0.
--
commit cce0c5216b1bbddd78ce31614c3d1587f8bcd4ea
Author: Thara Gopinath <tgopinath@linux.microsoft.com>
drivers: hv: Add VSM boot driver and enable VTL1 at the partition level
This commit introduces a driver to bring up Linux VBS (LVBS) using Hyper-V's
Virtual Secure Mode. It provides the initial sequence to probe privileges, pin
execution to the VTL0 boot CPU, and enable VTL1 at the partition level with
MBEC.
> diff --git a/drivers/hv/hv_vsm_boot.c b/drivers/hv/hv_vsm_boot.c
> new file mode 100644
> index 0000000000000..99e4dc8695837
> --- /dev/null
> +++ b/drivers/hv/hv_vsm_boot.c
[ ... ]
> +static int __init hv_vsm_bootstrap_vtl(void)
> +{
> + u16 partition_enabled_vtl_set = 0, partition_mbec_enabled_vtl_set = 0;
> + u8 partition_max_vtl;
> + int ret;
> +
> + /* Check and enable VTL1 at the partition level */
> + ret = hv_vsm_get_partition_status(&partition_enabled_vtl_set, &partition_max_vtl,
> + &partition_mbec_enabled_vtl_set);
> + if (ret)
> + return ret;
> +
> + if (partition_max_vtl < HV_VTL_SECURE) {
> + pr_err("VTL1 is not supported by the partition\n");
> + return -EINVAL;
> + }
> +
> + if (partition_enabled_vtl_set & HV_VTL1_ENABLE_BIT) {
> + pr_info("Partition VTL1 is already enabled\n");
> + } else {
[Severity: High]
Does this code skip verifying MBEC when VTL1 is already enabled?
If an attacker or previous boot stage races to enable VTL1 but omits
enabling MBEC, checking partition_enabled_vtl_set here finds VTL1 enabled
and skips further configuration without validating
partition_mbec_enabled_vtl_set. Could this allow acceptance of insecure
pre-configured VTL1 states?
> + ret = hv_vsm_enable_partition_vtl();
> + if (ret) {
> + pr_err("Enabling Partition VTL1 failed with status 0x%x\n",
> + ret);
> + return -EINVAL;
> + }
> + ret = hv_vsm_get_partition_status(&partition_enabled_vtl_set, &partition_max_vtl,
> + &partition_mbec_enabled_vtl_set);
> + if (ret)
> + return ret;
> + if (!(partition_enabled_vtl_set & HV_VTL1_ENABLE_BIT)) {
> + pr_err("Tried Enabling Partition VTL 1 and still failed\n");
> + return -EINVAL;
> + }
> + if (!partition_mbec_enabled_vtl_set) {
[Severity: High]
Since partition_mbec_enabled_vtl_set is a bitmask of all VTLs with MBEC,
does this validation only check if the mask is completely empty?
If VTL0 already has MBEC enabled (bit 0 is 1), the mask is non-zero, causing
this check to falsely pass even if the hypervisor failed to enable MBEC for
VTL1. Should this instead isolate the VTL1 bit, for example:
!(partition_mbec_enabled_vtl_set & HV_VTL1_ENABLE_BIT) ?
> + pr_err("Tried Enabling Partition MBEC and failed\n");
> + return -EINVAL;
> + }
> + }
> + return 0;
> +}
[ ... ]
> +static int __init hv_vsm_boot_init(void)
> +{
> + cpumask_var_t mask;
> + unsigned int boot_cpu;
> + int ret;
> +
> + if (!vsm_arch_has_vsm_access())
> + return 0;
> +
> + /*
> + * 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.
> + * ToDo: Check if copying and restoring current->cpus_mask is enough
> + * ToDo: Verify the assumption that cpumask_first(cpu_online_mask) is
> + * the boot cpu
> + */
> + if (!alloc_cpumask_var(&mask, GFP_KERNEL))
> + panic("Could not allocate cpumask");
> +
> + cpumask_copy(mask, ¤t->cpus_mask);
> + boot_cpu = cpumask_first(cpu_online_mask);
> + set_cpus_allowed_ptr(current, cpumask_of(boot_cpu));
[Severity: Medium]
Will this incorrectly pin the task to the lowest-numbered online CPU rather
than the actual boot CPU during a kexec or kdump?
If the kernel is booted via kexec or kdump on a non-zero CPU, lower-numbered
CPUs might be brought online during SMP initialization. In this scenario,
cpumask_first(cpu_online_mask) returns the lowest numbered online CPU, which
is not necessarily the Bootstrap Processor (BSP). Is get_boot_cpu_id() the
proper API to use here?
> +
> + ret = hv_vsm_bootstrap_vtl();
--
Sashiko AI review · https://sashiko.dev/#/patchset/20260901165647.3160413-1-tgopinath@linux.microsoft.com?part=6
^ permalink raw reply [flat|nested] 34+ messages in thread* Re: [RFC PATCH 06/12] drivers: hv: Add VSM boot driver and enable VTL1 at the partition level
2026-09-01 16:55 ` [RFC PATCH 06/12] drivers: hv: Add VSM boot driver and enable VTL1 at the partition level Thara Gopinath
2026-09-01 17:24 ` sashiko-bot
@ 2026-09-02 1:16 ` Wei Liu
2026-09-02 14:28 ` Thara Gopinath
2026-09-02 4:43 ` Wei Liu
2 siblings, 1 reply; 34+ messages in thread
From: Wei Liu @ 2026-09-02 1:16 UTC (permalink / raw)
To: Thara Gopinath
Cc: kys, haiyangz, wei.liu, decui, tglx, mingo, bp, dave.hansen, hpa,
ardb, ilias.apalodimas, James.Bottomley, javierm, lszubowi,
francescopompo2, tgopinath, x86, linux-hyperv, linux-kernel,
linux-efi
On Tue, Sep 01, 2026 at 09:55:20AM -0700, Thara Gopinath wrote:
> Linux VBS (LVBS) uses Hyper-V's Virtual Secure Mode to run a small
> trusted kernel in VTL1 alongside the regular VTL0 kernel, so that
> security-sensitive state (e.g. hypervisor-enforced code integrity,
> credential isolation) can live behind a higher-privilege boundary
> that VTL0 compromise cannot cross. Bringing that up from Linux
> requires the VTL0 kernel to drive the VSM setup itself.
>
> Add drivers/hv/hv_vsm_boot.c as the entry point for that sequence.
> This first step handles partition-level VTL1 enable only:
>
> - Probe VSM / VP-register privileges and SynIC availability before
> doing anything.
> - Pin init to the VTL0 boot CPU so VTL1 comes up on the same CPU
> (later patches rely on this).
> - Read HV_REGISTER_VSM_PARTITION_STATUS, and if VTL1 is not already
> enabled, issue HVCALL_ENABLE_PARTITION_VTL with MBEC and confirm
> by re-reading the register.
>
> Signed-off-by: Thara Gopinath <tgopinath@linux.microsoft.com>
> ---
[...]
> +static int __init hv_vsm_boot_init(void)
> +{
> + cpumask_var_t mask;
> + unsigned int boot_cpu;
> + int ret;
> +
> + if (!vsm_arch_has_vsm_access())
> + return 0;
> +
> + /*
> + * 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.
> + * ToDo: Check if copying and restoring current->cpus_mask is enough
> + * ToDo: Verify the assumption that cpumask_first(cpu_online_mask) is
> + * the boot cpu
Can we get closures on these todos?
I see a bunch of todos in other patches, too. It would be good to close
them as well.
Wei
> + */
> + if (!alloc_cpumask_var(&mask, GFP_KERNEL))
> + panic("Could not allocate cpumask");
> +
> + cpumask_copy(mask, ¤t->cpus_mask);
> + boot_cpu = cpumask_first(cpu_online_mask);
> + set_cpus_allowed_ptr(current, cpumask_of(boot_cpu));
> +
> + ret = hv_vsm_bootstrap_vtl();
> + /*
> + * At this point VTL0 has already advertised VSM support to the
> + * bootloader/firmware via the Hyper-V OsLoaderIndications EFI
> + * variable (see the x86-stub change). That signals the platform
> + * that a trusted VTL1 will be brought up. If we fail to actually
> + * set VTL1 up here, the partition is left in a state where an
> + * attacker could race to configure VTL1 themselves and gain a
> + * higher-privilege foothold than VTL0. Panic rather than continue
> + * running with that exposure.
> + */
> + if (ret)
> + panic("VTL1 boot failure caused kernel panic; consult log for more details.\n");
> +
> + set_cpus_allowed_ptr(current, mask);
> + free_cpumask_var(mask);
> + return ret;
> +}
> +device_initcall(hv_vsm_boot_init);
> --
> 2.34.1
>
>
^ permalink raw reply [flat|nested] 34+ messages in thread* Re: [RFC PATCH 06/12] drivers: hv: Add VSM boot driver and enable VTL1 at the partition level
2026-09-02 1:16 ` Wei Liu
@ 2026-09-02 14:28 ` Thara Gopinath
0 siblings, 0 replies; 34+ messages in thread
From: Thara Gopinath @ 2026-09-02 14:28 UTC (permalink / raw)
To: Wei Liu
Cc: kys, haiyangz, decui, tglx, mingo, bp, dave.hansen, hpa, ardb,
ilias.apalodimas, James.Bottomley, javierm, lszubowi,
francescopompo2, tgopinath, x86, linux-hyperv, linux-kernel,
linux-efi
On 9/1/2026 9:16 PM, Wei Liu wrote:
> On Tue, Sep 01, 2026 at 09:55:20AM -0700, Thara Gopinath wrote:
>> Linux VBS (LVBS) uses Hyper-V's Virtual Secure Mode to run a small
>> trusted kernel in VTL1 alongside the regular VTL0 kernel, so that
>> security-sensitive state (e.g. hypervisor-enforced code integrity,
>> credential isolation) can live behind a higher-privilege boundary
>> that VTL0 compromise cannot cross. Bringing that up from Linux
>> requires the VTL0 kernel to drive the VSM setup itself.
>>
>> Add drivers/hv/hv_vsm_boot.c as the entry point for that sequence.
>> This first step handles partition-level VTL1 enable only:
>>
>> - Probe VSM / VP-register privileges and SynIC availability before
>> doing anything.
>> - Pin init to the VTL0 boot CPU so VTL1 comes up on the same CPU
>> (later patches rely on this).
>> - Read HV_REGISTER_VSM_PARTITION_STATUS, and if VTL1 is not already
>> enabled, issue HVCALL_ENABLE_PARTITION_VTL with MBEC and confirm
>> by re-reading the register.
>>
>> Signed-off-by: Thara Gopinath <tgopinath@linux.microsoft.com>
>> ---
> [...]
>> +static int __init hv_vsm_boot_init(void)
>> +{
>> + cpumask_var_t mask;
>> + unsigned int boot_cpu;
>> + int ret;
>> +
>> + if (!vsm_arch_has_vsm_access())
>> + return 0;
>> +
>> + /*
>> + * 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.
>> + * ToDo: Check if copying and restoring current->cpus_mask is enough
>> + * ToDo: Verify the assumption that cpumask_first(cpu_online_mask) is
>> + * the boot cpu
>
> Can we get closures on these todos?
>
> I see a bunch of todos in other patches, too. It would be good to close
> them as well.
Sorry about those. Most of them are remnants from the when the dev work was
started and are closed or taken care of. I will ensure that these are fixed
and the comments are edited properly in the next revision.
Warm Regards
Thara
>
> Wei
^ permalink raw reply [flat|nested] 34+ messages in thread
* Re: [RFC PATCH 06/12] drivers: hv: Add VSM boot driver and enable VTL1 at the partition level
2026-09-01 16:55 ` [RFC PATCH 06/12] drivers: hv: Add VSM boot driver and enable VTL1 at the partition level Thara Gopinath
2026-09-01 17:24 ` sashiko-bot
2026-09-02 1:16 ` Wei Liu
@ 2026-09-02 4:43 ` Wei Liu
2026-09-04 13:23 ` Thara Gopinath
2 siblings, 1 reply; 34+ messages in thread
From: Wei Liu @ 2026-09-02 4:43 UTC (permalink / raw)
To: Thara Gopinath
Cc: kys, haiyangz, wei.liu, decui, tglx, mingo, bp, dave.hansen, hpa,
ardb, ilias.apalodimas, James.Bottomley, javierm, lszubowi,
francescopompo2, tgopinath, x86, linux-hyperv, linux-kernel,
linux-efi
On Tue, Sep 01, 2026 at 09:55:20AM -0700, Thara Gopinath wrote:
> Linux VBS (LVBS) uses Hyper-V's Virtual Secure Mode to run a small
> trusted kernel in VTL1 alongside the regular VTL0 kernel, so that
> security-sensitive state (e.g. hypervisor-enforced code integrity,
> credential isolation) can live behind a higher-privilege boundary
> that VTL0 compromise cannot cross. Bringing that up from Linux
> requires the VTL0 kernel to drive the VSM setup itself.
>
> Add drivers/hv/hv_vsm_boot.c as the entry point for that sequence.
> This first step handles partition-level VTL1 enable only:
>
> - Probe VSM / VP-register privileges and SynIC availability before
> doing anything.
> - Pin init to the VTL0 boot CPU so VTL1 comes up on the same CPU
> (later patches rely on this).
> - Read HV_REGISTER_VSM_PARTITION_STATUS, and if VTL1 is not already
> enabled, issue HVCALL_ENABLE_PARTITION_VTL with MBEC and confirm
> by re-reading the register.
>
> Signed-off-by: Thara Gopinath <tgopinath@linux.microsoft.com>
> ---
[...]
> + cpumask_copy(mask, ¤t->cpus_mask);
> + boot_cpu = cpumask_first(cpu_online_mask);
> + set_cpus_allowed_ptr(current, cpumask_of(boot_cpu));
> +
> + ret = hv_vsm_bootstrap_vtl();
> + /*
> + * At this point VTL0 has already advertised VSM support to the
> + * bootloader/firmware via the Hyper-V OsLoaderIndications EFI
> + * variable (see the x86-stub change). That signals the platform
> + * that a trusted VTL1 will be brought up. If we fail to actually
> + * set VTL1 up here, the partition is left in a state where an
> + * attacker could race to configure VTL1 themselves and gain a
> + * higher-privilege foothold than VTL0. Panic rather than continue
> + * running with that exposure.
> + */
Okay, I think this answers my question in the previous patch -- if VTL1
is not used by us, that's a security problem.
It is unclear to me, if this code is enabled, how a generic kernel can
work without a secure kernel. There should be a way to configure the
system such that not enabling VTL1 is okay. Is there any pre-EFI command
line parsing we can do?
Wei
> + if (ret)
> + panic("VTL1 boot failure caused kernel panic; consult log for more details.\n");
> +
> + set_cpus_allowed_ptr(current, mask);
> + free_cpumask_var(mask);
> + return ret;
> +}
> +device_initcall(hv_vsm_boot_init);
> --
> 2.34.1
>
>
^ permalink raw reply [flat|nested] 34+ messages in thread* Re: [RFC PATCH 06/12] drivers: hv: Add VSM boot driver and enable VTL1 at the partition level
2026-09-02 4:43 ` Wei Liu
@ 2026-09-04 13:23 ` Thara Gopinath
0 siblings, 0 replies; 34+ messages in thread
From: Thara Gopinath @ 2026-09-04 13:23 UTC (permalink / raw)
To: Wei Liu
Cc: kys, haiyangz, decui, tglx, mingo, bp, dave.hansen, hpa, ardb,
ilias.apalodimas, James.Bottomley, javierm, lszubowi,
francescopompo2, tgopinath, x86, linux-hyperv, linux-kernel,
linux-efi
On 9/2/2026 12:43 AM, Wei Liu wrote:
> On Tue, Sep 01, 2026 at 09:55:20AM -0700, Thara Gopinath wrote:
>> Linux VBS (LVBS) uses Hyper-V's Virtual Secure Mode to run a small
>> trusted kernel in VTL1 alongside the regular VTL0 kernel, so that
>> security-sensitive state (e.g. hypervisor-enforced code integrity,
>> credential isolation) can live behind a higher-privilege boundary
>> that VTL0 compromise cannot cross. Bringing that up from Linux
>> requires the VTL0 kernel to drive the VSM setup itself.
>>
>> Add drivers/hv/hv_vsm_boot.c as the entry point for that sequence.
>> This first step handles partition-level VTL1 enable only:
>>
>> - Probe VSM / VP-register privileges and SynIC availability before
>> doing anything.
>> - Pin init to the VTL0 boot CPU so VTL1 comes up on the same CPU
>> (later patches rely on this).
>> - Read HV_REGISTER_VSM_PARTITION_STATUS, and if VTL1 is not already
>> enabled, issue HVCALL_ENABLE_PARTITION_VTL with MBEC and confirm
>> by re-reading the register.
>>
>> Signed-off-by: Thara Gopinath <tgopinath@linux.microsoft.com>
>> ---
> [...]
>> + cpumask_copy(mask, ¤t->cpus_mask);
>> + boot_cpu = cpumask_first(cpu_online_mask);
>> + set_cpus_allowed_ptr(current, cpumask_of(boot_cpu));
>> +
>> + ret = hv_vsm_bootstrap_vtl();
>> + /*
>> + * At this point VTL0 has already advertised VSM support to the
>> + * bootloader/firmware via the Hyper-V OsLoaderIndications EFI
>> + * variable (see the x86-stub change). That signals the platform
>> + * that a trusted VTL1 will be brought up. If we fail to actually
>> + * set VTL1 up here, the partition is left in a state where an
>> + * attacker could race to configure VTL1 themselves and gain a
>> + * higher-privilege foothold than VTL0. Panic rather than continue
>> + * running with that exposure.
>> + */
>
> Okay, I think this answers my question in the previous patch -- if VTL1
> is not used by us, that's a security problem.
>
> It is unclear to me, if this code is enabled, how a generic kernel can
> work without a secure kernel. There should be a way to configure the
> system such that not enabling VTL1 is okay. Is there any pre-EFI command
> line parsing we can do?
Yes, we can do a pre-efi-exit-boot-service command line parsing and not set
the OsLoaderIndications EFI variable asking HYPER-V to allow VTL1 setup. We
can rename the securekernel command line option to mshv_securekernel and
repurpose it for this as well. Basically if CONFIG_HYPER_VSM is enabled but
if mshv_securekernel command line is not set VTL1 support will not be enabled
and kernel will boot. I did quickly prototype this and it is doable.
Having said that generic kernel should not enable CONFIG_HYPERV_VSM at all.
I can update the KConfig to state the same as well.
Warm Regards
Thara
>
> Wei
>
>
>> + if (ret)
>> + panic("VTL1 boot failure caused kernel panic; consult log for more details.\n");
>> +
>> + set_cpus_allowed_ptr(current, mask);
>> + free_cpumask_var(mask);
>> + return ret;
>> +}
>> +device_initcall(hv_vsm_boot_init);
>> --
>> 2.34.1
>>
>>
^ permalink raw reply [flat|nested] 34+ messages in thread
* [RFC PATCH 07/12] drivers: hv: hv_vsm_boot: load secure kernel image from firmware
2026-09-01 16:55 [RFC PATCH 00/12] Introduce LVBS support for Hyper-V guests Thara Gopinath
` (5 preceding siblings ...)
2026-09-01 16:55 ` [RFC PATCH 06/12] drivers: hv: Add VSM boot driver and enable VTL1 at the partition level Thara Gopinath
@ 2026-09-01 16:55 ` Thara Gopinath
2026-09-01 17:20 ` sashiko-bot
2026-09-02 4:37 ` Wei Liu
2026-09-01 16:55 ` [RFC PATCH 08/12] arch: x86: hyperv: Build initial vCPU context for VTL1 secure kernel Thara Gopinath
` (4 subsequent siblings)
11 siblings, 2 replies; 34+ messages in thread
From: Thara Gopinath @ 2026-09-01 16:55 UTC (permalink / raw)
To: kys, haiyangz, wei.liu, decui, tglx, mingo, bp, dave.hansen, hpa,
ardb, ilias.apalodimas
Cc: James.Bottomley, "longli, javierm, lszubowi, francescopompo2,
tgopinath, x86, linux-hyperv, linux-kernel, linux-efi,
Thara Gopinath, Stanislav Kinsburskii
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 <skinsburskii@linux.microsoft.com>
Signed-off-by: Thara Gopinath <tgopinath@linux.microsoft.com>
---
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 <tgopinath@linux.microsoft.com>
+ *
+ */
+
+#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 <tgopinath@linux.microsoft.com>
+ * Stanislav Kinsburskii <skinsburskii@linux.microsoft.com>
*
*/
@@ -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 <tgopinath@linux.microsoft.com>
+ */
+
+#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
^ permalink raw reply related [flat|nested] 34+ messages in thread* Re: [RFC PATCH 07/12] drivers: hv: hv_vsm_boot: load secure kernel image from firmware
2026-09-01 16:55 ` [RFC PATCH 07/12] drivers: hv: hv_vsm_boot: load secure kernel image from firmware Thara Gopinath
@ 2026-09-01 17:20 ` sashiko-bot
2026-09-02 4:37 ` Wei Liu
1 sibling, 0 replies; 34+ messages in thread
From: sashiko-bot @ 2026-09-01 17:20 UTC (permalink / raw)
To: Thara Gopinath; +Cc: linux-hyperv
Thank you for your contribution! Sashiko AI review found 7 potential issue(s) to consider:
- [High] The ELF loader lacks bounds checking for program headers and segment data against the loaded firmware size, allowing out-of-bounds memory reads.
- [High] Missing validation that `p_memsz >= p_filesz` leads to integer underflow and massive out-of-bounds `memset`.
- [High] Signed-to-unsigned integer promotion bypasses the spatial boundary check, allowing out-of-bounds memory writes.
- [High] The ELF load alignment is passed directly to `PTR_ALIGN` without verifying it is a power of two, breaking pointer arithmetic.
- [High] The bounds check uses a hardcoded maximum size instead of the actual reserved memory resource size, allowing out-of-bounds writes.
- [High] Integer overflow in the calculation of the ELF binary memory footprint bypasses spatial bounds checks.
- [Medium] Incorrect use of the `PTR_ALIGN` macro miscalculates alignment by unnecessarily pre-adding the alignment value, violating generic alignment semantics.
--
commit a2d9d89a62300f04ed251e2db601b303f521e71c
Author: Thara Gopinath <tgopinath@linux.microsoft.com>
drivers: hv: hv_vsm_boot: load secure kernel image from firmware
This commit adds a VTL0-side loader for LVBS bring-up that stages a secure
kernel image into the reserved memory region. It fetches a 64-bit ELF using
request_firmware(), validates the ELF header, and stages the PT_LOAD
segments into memory.
> diff --git a/drivers/hv/hv_vsm_boot.c b/drivers/hv/hv_vsm_boot.c
> --- a/drivers/hv/hv_vsm_boot.c
> +++ b/drivers/hv/hv_vsm_boot.c
[ ... ]
> @@ -38,6 +55,167 @@ static int hv_vsm_get_register(u32 reg_name, u64 *result)
[ ... ]
> +static size_t __init hv_vsm_elf_binary_size(void *image)
> +{
[ ... ]
> + for (i = 0; i < ehdr->e_phnum; i++, phdr++) {
[Severity: High]
Is it safe to blindly iterate over and read memory from phdr here? If the
program header table offset or count extends beyond the firmware limits,
this could result in out-of-bounds memory reads.
> + if (phdr->p_type != PT_LOAD)
> + continue;
> +
> + max_paddr = max(max_paddr, phdr->p_paddr + phdr->p_memsz);
[Severity: High]
Can this calculation overflow? If a malformed ELF segment defines p_paddr
and p_memsz such that their sum exceeds the 64-bit integer limit, it could
wrap around. This would result in an undersized binary size, potentially
bypassing later spatial bounds checks and allowing out-of-bounds writes.
> + }
> +
> + return max_paddr - min_paddr;
> +}
> +
> +static int __init hv_vsm_load_elf(void *image, Elf64_Addr *sk_entry_pa)
> +{
[ ... ]
> + /* 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);
[Severity: Medium]
Does this code miscalculate the alignment? By explicitly adding
first_load_align before invoking PTR_ALIGN, it forces the pointer to skip
an entire alignment block even if it is already aligned, which wastes memory
within the strictly reserved region.
[Severity: High]
Additionally, PTR_ALIGN relies on the alignment being a power of two. Since
first_load_align is only validated below using modulo (modulo SZ_2M), could
a non-power-of-two alignment corrupt the pointer calculation entirely?
> +
> + size = hv_vsm_elf_binary_size(image);
> + if (vsm_skm_va + VSM_SK_INITIAL_MAP_SIZE - base_addr < size) {
[Severity: High]
Can a malicious base_addr bypass this validation entirely? If base_addr
is located beyond the vsm_skm_va + VSM_SK_INITIAL_MAP_SIZE limit, the
pointer arithmetic yields a negative ptrdiff_t. When compared against the
unsigned size_t size, it undergoes standard C integer promotion to a
massive positive value, completely bypassing the boundary condition.
[Severity: High]
Also, does this check artificially limit the copy size against a hardcoded
constant rather than the actual bounds of the allocated sk_res region?
If a user reserves a secure kernel region smaller than 16MB via the kernel
command line, this could allow writes outside the dynamically allocated
physical region.
> + pr_err("secure kernel does not fit: %zu > %td\n", size,
> + vsm_skm_va + VSM_SK_INITIAL_MAP_SIZE - base_addr);
> + return -EFBIG;
> + }
[ ... ]
> + 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);
[Severity: High]
Are the ELF segment properties validated before this memcpy? The source
memory is derived from phdr->p_offset and phdr->p_filesz, which don't appear
to be checked against the total firmware size. This risks out-of-bounds
memory reads.
> +
> + 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);
[Severity: High]
Can this subtraction underflow? If a malformed ELF provides a PT_LOAD
segment where phdr->p_filesz is larger than phdr->p_memsz, this calculation
would result in a massive out-of-bounds memset that could destroy memory.
> + }
> +
> + *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) ||
[Severity: High]
Does this bounds check sufficiently validate the firmware size? It appears to
omit validation for the program headers' offset and count, which could
allow a malformed firmware file to trigger out-of-bounds reads later in
the loader.
> + memcmp(ehdr->e_ident, ELFMAG, SELFMAG) ||
> + (ehdr->e_type != ET_EXEC && ehdr->e_type != ET_DYN)) {
--
Sashiko AI review · https://sashiko.dev/#/patchset/20260901165647.3160413-1-tgopinath@linux.microsoft.com?part=7
^ permalink raw reply [flat|nested] 34+ messages in thread* Re: [RFC PATCH 07/12] drivers: hv: hv_vsm_boot: load secure kernel image from firmware
2026-09-01 16:55 ` [RFC PATCH 07/12] drivers: hv: hv_vsm_boot: load secure kernel image from firmware Thara Gopinath
2026-09-01 17:20 ` sashiko-bot
@ 2026-09-02 4:37 ` Wei Liu
2026-09-02 16:22 ` Thara Gopinath
1 sibling, 1 reply; 34+ messages in thread
From: Wei Liu @ 2026-09-02 4:37 UTC (permalink / raw)
To: Thara Gopinath
Cc: kys, haiyangz, wei.liu, decui, tglx, mingo, bp, dave.hansen, hpa,
ardb, ilias.apalodimas, James.Bottomley, javierm, lszubowi,
francescopompo2, tgopinath, x86, linux-hyperv, linux-kernel,
linux-efi, Stanislav Kinsburskii
On Tue, Sep 01, 2026 at 09:55:21AM -0700, Thara Gopinath wrote:
> 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.
This conflicts with the memory reservation patch, in which there is an
automatic allocation when no kernel command line is specified.
>
> Signed-off-by: Stanislav Kinsburskii <skinsburskii@linux.microsoft.com>
> Signed-off-by: Thara Gopinath <tgopinath@linux.microsoft.com>
> ---
> 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
[...]
> +
> +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");
> +
This log line is wrong.
Wei
^ permalink raw reply [flat|nested] 34+ messages in thread* Re: [RFC PATCH 07/12] drivers: hv: hv_vsm_boot: load secure kernel image from firmware
2026-09-02 4:37 ` Wei Liu
@ 2026-09-02 16:22 ` Thara Gopinath
2026-09-02 22:58 ` Wei Liu
0 siblings, 1 reply; 34+ messages in thread
From: Thara Gopinath @ 2026-09-02 16:22 UTC (permalink / raw)
To: Wei Liu
Cc: kys, haiyangz, decui, tglx, mingo, bp, dave.hansen, hpa, ardb,
ilias.apalodimas, James.Bottomley, javierm, lszubowi,
francescopompo2, tgopinath, x86, linux-hyperv, linux-kernel,
linux-efi, Stanislav Kinsburskii
On 9/2/2026 12:37 AM, Wei Liu wrote:
> On Tue, Sep 01, 2026 at 09:55:21AM -0700, Thara Gopinath wrote:
>> 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.
>
> This conflicts with the memory reservation patch, in which there is an
> automatic allocation when no kernel command line is specified.
Ah no.. So securekernel= has to be specified in the command line for
__setup() to be invoked . It can be left blank without parameters like
securekernel= or with parameters like securekernel=256M@0x80000000. The
reservation logic will take care of reserving the correct memory if there
are no parameters but if there is no command line specified __setup will
not be called. The other way of solving this and invoking the reservations
unconditionally will be to call it from setup_arch like how reserve_crashkernel
is invoked. I am not sure if we want to do that now ?? What do you think ?
But I will reword this and state that the panic happens if there is no
securekernel memory allocated (either because securekernel= was not
specified or because memory reservation itself failed)
>
>>
>> Signed-off-by: Stanislav Kinsburskii <skinsburskii@linux.microsoft.com>
>> Signed-off-by: Thara Gopinath <tgopinath@linux.microsoft.com>
>> ---
>> 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
> [...]
>> +
>> +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");
>> +
>
> This log line is wrong.
I will fix this
Warm Regards
Thara
>
> Wei
^ permalink raw reply [flat|nested] 34+ messages in thread* Re: [RFC PATCH 07/12] drivers: hv: hv_vsm_boot: load secure kernel image from firmware
2026-09-02 16:22 ` Thara Gopinath
@ 2026-09-02 22:58 ` Wei Liu
0 siblings, 0 replies; 34+ messages in thread
From: Wei Liu @ 2026-09-02 22:58 UTC (permalink / raw)
To: Thara Gopinath
Cc: Wei Liu, kys, haiyangz, decui, tglx, mingo, bp, dave.hansen, hpa,
ardb, ilias.apalodimas, James.Bottomley, javierm, lszubowi,
francescopompo2, tgopinath, x86, linux-hyperv, linux-kernel,
linux-efi, Stanislav Kinsburskii
On Wed, Sep 02, 2026 at 12:22:37PM -0400, Thara Gopinath wrote:
>
>
> On 9/2/2026 12:37 AM, Wei Liu wrote:
> > On Tue, Sep 01, 2026 at 09:55:21AM -0700, Thara Gopinath wrote:
> >> 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.
> >
> > This conflicts with the memory reservation patch, in which there is an
> > automatic allocation when no kernel command line is specified.
>
> Ah no.. So securekernel= has to be specified in the command line for
> __setup() to be invoked . It can be left blank without parameters like
> securekernel= or with parameters like securekernel=256M@0x80000000. The
> reservation logic will take care of reserving the correct memory if there
> are no parameters but if there is no command line specified __setup will
> not be called. The other way of solving this and invoking the reservations
> unconditionally will be to call it from setup_arch like how reserve_crashkernel
> is invoked. I am not sure if we want to do that now ?? What do you think ?
>
I see. No need to do that now. Let's see if others have opinions.
Wei
> But I will reword this and state that the panic happens if there is no
> securekernel memory allocated (either because securekernel= was not
> specified or because memory reservation itself failed)
>
> >
> >>
> >> Signed-off-by: Stanislav Kinsburskii <skinsburskii@linux.microsoft.com>
> >> Signed-off-by: Thara Gopinath <tgopinath@linux.microsoft.com>
> >> ---
> >> 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
> > [...]
> >> +
> >> +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");
> >> +
> >
> > This log line is wrong.
>
> I will fix this
>
> Warm Regards
> Thara
> >
> > Wei
>
^ permalink raw reply [flat|nested] 34+ messages in thread
* [RFC PATCH 08/12] arch: x86: hyperv: Build initial vCPU context for VTL1 secure kernel
2026-09-01 16:55 [RFC PATCH 00/12] Introduce LVBS support for Hyper-V guests Thara Gopinath
` (6 preceding siblings ...)
2026-09-01 16:55 ` [RFC PATCH 07/12] drivers: hv: hv_vsm_boot: load secure kernel image from firmware Thara Gopinath
@ 2026-09-01 16:55 ` Thara Gopinath
2026-09-01 17:25 ` sashiko-bot
2026-09-01 16:55 ` [RFC PATCH 09/12] drivers: hv: hv_vsm_boot: Enable VTL1 on the boot processor Thara Gopinath
` (3 subsequent siblings)
11 siblings, 1 reply; 34+ messages in thread
From: Thara Gopinath @ 2026-09-01 16:55 UTC (permalink / raw)
To: kys, haiyangz, wei.liu, decui, tglx, mingo, bp, dave.hansen, hpa,
ardb, ilias.apalodimas
Cc: James.Bottomley, "longli, javierm, lszubowi, francescopompo2,
tgopinath, x86, linux-hyperv, linux-kernel, linux-efi,
Thara Gopinath
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 <tgopinath@linux.microsoft.com>
---
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 <tgopinath@linux.microsoft.com>
+ */
+
+#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
^ permalink raw reply related [flat|nested] 34+ messages in thread* Re: [RFC PATCH 08/12] arch: x86: hyperv: Build initial vCPU context for VTL1 secure kernel
2026-09-01 16:55 ` [RFC PATCH 08/12] arch: x86: hyperv: Build initial vCPU context for VTL1 secure kernel Thara Gopinath
@ 2026-09-01 17:25 ` sashiko-bot
0 siblings, 0 replies; 34+ messages in thread
From: sashiko-bot @ 2026-09-01 17:25 UTC (permalink / raw)
To: Thara Gopinath; +Cc: linux-hyperv
Thank you for your contribution! Sashiko AI review found 3 potential issue(s) to consider:
- [High] Page table initialization out-of-bounds write to the PDE page if the mapping crosses a 1GB boundary.
- [High] Hardware-consumed GDT, TSS, and Kernel Stack pages are mapped but never zero-initialized, exposing host memory fragments.
- [Low] The TSS GDT descriptor limit is set to `tss_size` instead of `tss_size - 1`.
--
commit 6f8a1a825749fc111adb451004cf1aa1f0e24ee1
Author: Thara Gopinath <tgopinath@linux.microsoft.com>
arch: x86: hyperv: Build initial vCPU context for VTL1 secure kernel
This commit adds the architecture-specific initialization function to construct
the initial page tables, GDT/TSS, and CPU register state for transitioning the
boot processor to the VTL1 secure kernel. It allocates and populates the data
structures required by Hyper-V's hv_init_vp_context.
> 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
[ ... ]
> +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;
[Severity: High]
Does this loop overflow the 4KB PDE page if sk_pa crosses a 1GB boundary?
The allocator only guarantees a 2MB alignment for sk_pa, so pd_index can
be near the end of the page directory (e.g., 511). Since num_pte_tables
can be up to 8 (for a 16MB initial mapping), pd_index + i will exceed the
511 maximum index of the page directory, causing an out-of-bounds write into
the adjacent VSM_PTE_PAGES memory.
[ ... ]
> +static void __init hv_vsm_init_page_tables(struct hv_init_vp_context *vp_ctx, phys_addr_t sk_pa)
> +{
[ ... ]
> + 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);
[Severity: High]
By starting the memset at VSM_PML4E_PAGE and stopping before
VSM_KERNEL_STACK_PAGE, does this leave VSM_GDT_PAGE, VSM_TSS_PAGE, and
the kernel stack page completely uninitialized?
The underlying physical memory is not guaranteed to be zeroed by the allocator,
which would expose host memory fragments directly to the secure kernel.
[ ... ]
> +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);
[Severity: High]
Since the memory backing tss was skipped by the memset in
hv_vsm_init_page_tables(), will the hardware task state segment be populated
with random garbage?
This would leave hardware fields like the interrupt stack tables (ist) and
the IO bitmap base uninitialized, potentially causing a fatal exception when an
interrupt is taken in VTL1.
[ ... ]
> + /*
> + * 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);
[Severity: Low]
Is this descriptor's limit off by one?
x86 segment limits are inclusive, so shouldn't this pass tss_size - 1 rather
than tss_size to avoid allowing access one byte past the end of the TSS?
--
Sashiko AI review · https://sashiko.dev/#/patchset/20260901165647.3160413-1-tgopinath@linux.microsoft.com?part=8
^ permalink raw reply [flat|nested] 34+ messages in thread
* [RFC PATCH 09/12] drivers: hv: hv_vsm_boot: Enable VTL1 on the boot processor
2026-09-01 16:55 [RFC PATCH 00/12] Introduce LVBS support for Hyper-V guests Thara Gopinath
` (7 preceding siblings ...)
2026-09-01 16:55 ` [RFC PATCH 08/12] arch: x86: hyperv: Build initial vCPU context for VTL1 secure kernel Thara Gopinath
@ 2026-09-01 16:55 ` Thara Gopinath
2026-09-01 17:36 ` sashiko-bot
2026-09-01 16:55 ` [RFC PATCH 10/12] arch: x86: hyperv: hv_vtl_vsm: Introduce vtlcall Thara Gopinath
` (2 subsequent siblings)
11 siblings, 1 reply; 34+ messages in thread
From: Thara Gopinath @ 2026-09-01 16:55 UTC (permalink / raw)
To: kys, haiyangz, wei.liu, decui, tglx, mingo, bp, dave.hansen, hpa,
ardb, ilias.apalodimas
Cc: James.Bottomley, "longli, javierm, lszubowi, francescopompo2,
tgopinath, x86, linux-hyperv, linux-kernel, linux-efi,
Thara Gopinath, Stanislav Kinsburskii
With the partition-level VTL1 enabled and the secure kernel image loaded
into the reserved region, the next step is to enable VTL1 on the boot
virtual processor so that the hypervisor can dispatch it into the secure
kernel entry point.
Introduce two helpers in hv_vsm_boot.c:
- hv_vsm_get_vp_status() reads HV_REGISTER_VSM_VP_STATUS.
- hv_vsm_enable_vp_vtl() issues HVCALL_ENABLE_VP_VTL with an initial
vCPU context built via hv_vsm_arch_init_vp().
Extend hv_vsm_bootstrap_vtl() to enable VTL1 on the boot VP if it is
not already enabled, and re-query the status to confirm the transition.
Signed-off-by: Stanislav Kinsburskii <stanislav.kinsburskii@gmail.com>
Signed-off-by: Thara Gopinath <tgopinath@linux.microsoft.com>
---
drivers/hv/hv_vsm_boot.c | 73 ++++++++++++++++++++++++++++++++++++++--
1 file changed, 71 insertions(+), 2 deletions(-)
diff --git a/drivers/hv/hv_vsm_boot.c b/drivers/hv/hv_vsm_boot.c
index dc20f935da5b2..abe82e03e1897 100644
--- a/drivers/hv/hv_vsm_boot.c
+++ b/drivers/hv/hv_vsm_boot.c
@@ -216,6 +216,47 @@ static int __init hv_vsm_load_secure_kernel(Elf64_Addr *sk_entry_pa)
return ret;
}
+static int __init hv_vsm_enable_vp_vtl(Elf64_Addr sk_entry_pa)
+{
+ u64 status = 0;
+ unsigned long flags;
+ struct hv_enable_vp_vtl *hvin;
+
+ local_irq_save(flags);
+
+ hvin = *this_cpu_ptr(hyperv_pcpu_input_arg);
+ memset(hvin, 0, sizeof(*hvin));
+
+ hvin->partition_id = HV_PARTITION_ID_SELF;
+ hvin->vp_index = HV_VP_INDEX_SELF;
+ hvin->target_vtl.target_vtl = HV_VTL_SECURE;
+
+ hv_vsm_arch_init_vp(&hvin->vp_context, sk_entry_pa, sk_res.start);
+
+ status = hv_do_hypercall(HVCALL_ENABLE_VP_VTL, hvin, NULL);
+
+ local_irq_restore(flags);
+
+ return hv_result(status);
+}
+
+static int __init hv_vsm_get_vp_status(u16 *enabled_vtl_set, u8 *active_mbec_enabled)
+{
+ u64 result;
+ int ret;
+ union hv_register_vsm_vp_status vsm_vp_status = { 0 };
+
+ ret = hv_vsm_get_register(HV_REGISTER_VSM_VP_STATUS, &result);
+ if (ret)
+ return ret;
+
+ vsm_vp_status = (union hv_register_vsm_vp_status)result;
+ *enabled_vtl_set = vsm_vp_status.enabled_vtl_set;
+ *active_mbec_enabled = vsm_vp_status.active_mbec_enabled;
+
+ return 0;
+}
+
static int __init hv_vsm_enable_partition_vtl(void)
{
u64 status = 0;
@@ -262,7 +303,8 @@ static int __init hv_vsm_get_partition_status(u16 *enabled_vtl_set, u8 *max_vtl,
static int __init hv_vsm_bootstrap_vtl(void)
{
u16 partition_enabled_vtl_set = 0, partition_mbec_enabled_vtl_set = 0;
- u8 partition_max_vtl;
+ u16 vp_enabled_vtl_set = 0;
+ u8 partition_max_vtl, active_mbec_enabled = 0;
Elf64_Addr sk_entry_pa;
int ret;
@@ -300,7 +342,34 @@ static int __init hv_vsm_bootstrap_vtl(void)
}
}
- return hv_vsm_load_secure_kernel(&sk_entry_pa);
+ ret = hv_vsm_load_secure_kernel(&sk_entry_pa);
+ if (ret)
+ return ret;
+
+ /* Check and enable VTL1 for the primary virtual processor */
+ ret = hv_vsm_get_vp_status(&vp_enabled_vtl_set, &active_mbec_enabled);
+ if (ret)
+ return ret;
+
+ if (vp_enabled_vtl_set & HV_VTL1_ENABLE_BIT) {
+ pr_info("VP VTL1 is already enabled\n");
+ } else {
+ ret = hv_vsm_enable_vp_vtl(sk_entry_pa);
+ if (ret) {
+ pr_err("Enabling VP VTL1 failed with status 0x%x\n", ret);
+ /* TODO: Should we disable VTL1 at partition level in this case? */
+ return -EINVAL;
+ }
+ ret = hv_vsm_get_vp_status(&vp_enabled_vtl_set, &active_mbec_enabled);
+ if (ret)
+ return ret;
+
+ if (!(vp_enabled_vtl_set & HV_VTL1_ENABLE_BIT)) {
+ pr_err("Tried Enabling VP VTL1 and still failed\n");
+ return -EINVAL;
+ }
+ }
+ return 0;
}
static void __init hv_vsm_get_sk_mem(void)
--
2.34.1
^ permalink raw reply related [flat|nested] 34+ messages in thread* Re: [RFC PATCH 09/12] drivers: hv: hv_vsm_boot: Enable VTL1 on the boot processor
2026-09-01 16:55 ` [RFC PATCH 09/12] drivers: hv: hv_vsm_boot: Enable VTL1 on the boot processor Thara Gopinath
@ 2026-09-01 17:36 ` sashiko-bot
0 siblings, 0 replies; 34+ messages in thread
From: sashiko-bot @ 2026-09-01 17:36 UTC (permalink / raw)
To: Thara Gopinath; +Cc: linux-hyperv
Thank you for your contribution! Sashiko AI review found 2 potential issue(s) to consider:
New issues:
- [High] Unconditional loading of the secure kernel overwrites VTL1 memory even if VTL1 is already active, leading to memory corruption or fatal faults.
Pre-existing issues:
- [High] Missing bounds check when parsing ELF program headers from firmware, leading to potential out-of-bounds read.
--
commit 58f06b8de69e2f750e55a8ff7220ab38d88c0f8f
Author: Thara Gopinath <tgopinath@linux.microsoft.com>
drivers: hv: hv_vsm_boot: Enable VTL1 on the boot processor
This commit introduces helpers to get VP status and enable VTL1 on the boot
virtual processor. It extends hv_vsm_bootstrap_vtl() to enable VTL1 on the
boot VP if it is not already enabled, and re-queries the status to confirm
the transition.
> diff --git a/drivers/hv/hv_vsm_boot.c b/drivers/hv/hv_vsm_boot.c
> index dc20f935da5b2..abe82e03e1897 100644
> --- a/drivers/hv/hv_vsm_boot.c
> +++ b/drivers/hv/hv_vsm_boot.c
> @@ -216,6 +216,47 @@ static int __init hv_vsm_load_secure_kernel(Elf64_Addr *sk_entry_pa)
> return ret;
> }
[Severity: High]
This isn't a bug introduced by this patch, but there appears to be a missing
bounds check when parsing ELF program headers from the firmware in
hv_vsm_load_elf() which is called by hv_vsm_load_secure_kernel().
The firmware size (fw->size) is discarded instead of being passed to
hv_vsm_load_elf() for validation. Inside hv_vsm_load_elf(), the program header
table offset (e_phoff) is never bounds-checked against the firmware size before
being iterated over:
drivers/hv/hv_vsm_boot.c:hv_vsm_load_elf() {
...
Elf64_Phdr *phdr = image + ehdr->e_phoff;
...
}
Later, the memcpy() copies uncontrolled lengths of kernel memory based on
untrusted firmware headers:
drivers/hv/hv_vsm_boot.c:hv_vsm_load_elf() {
...
memcpy(load_addr, image + phdr->p_offset, phdr->p_filesz);
...
}
Could a malformed or maliciously crafted firmware file cause an out-of-bounds
read past the end of the firmware buffer, potentially leaking sensitive kernel
data into the secure kernel's memory or causing a kernel panic?
[ ... ]
> @@ -300,7 +342,34 @@ static int __init hv_vsm_bootstrap_vtl(void)
> }
> }
>
> - return hv_vsm_load_secure_kernel(&sk_entry_pa);
> + ret = hv_vsm_load_secure_kernel(&sk_entry_pa);
> + if (ret)
> + return ret;
> +
> + /* Check and enable VTL1 for the primary virtual processor */
> + ret = hv_vsm_get_vp_status(&vp_enabled_vtl_set, &active_mbec_enabled);
> + if (ret)
> + return ret;
> +
> + if (vp_enabled_vtl_set & HV_VTL1_ENABLE_BIT) {
> + pr_info("VP VTL1 is already enabled\n");
[Severity: High]
If VTL1 is already enabled, does calling hv_vsm_load_secure_kernel() earlier
in this function overwrite the memory of the currently running VTL1 secure
kernel?
Since hv_vsm_load_secure_kernel() is called unconditionally before checking if
VP VTL1 is active, could unconditionally loading the secure kernel ELF image
cause a regression by triggering a fatal hypervisor intercept/fault in VTL0,
or silently corrupting the active VTL1 (such as during a kexec reboot)?
--
Sashiko AI review · https://sashiko.dev/#/patchset/20260901165647.3160413-1-tgopinath@linux.microsoft.com?part=9
^ permalink raw reply [flat|nested] 34+ messages in thread
* [RFC PATCH 10/12] arch: x86: hyperv: hv_vtl_vsm: Introduce vtlcall
2026-09-01 16:55 [RFC PATCH 00/12] Introduce LVBS support for Hyper-V guests Thara Gopinath
` (8 preceding siblings ...)
2026-09-01 16:55 ` [RFC PATCH 09/12] drivers: hv: hv_vsm_boot: Enable VTL1 on the boot processor Thara Gopinath
@ 2026-09-01 16:55 ` Thara Gopinath
2026-09-01 16:55 ` [RFC PATCH 11/12] drivers: hv: hv_vsm_boot: Boot primary processor in VTL1 Thara Gopinath
2026-09-01 16:55 ` [RFC PATCH 12/12] drivers: hv: hv_vsm_boot: Boot secondary processors " Thara Gopinath
11 siblings, 0 replies; 34+ messages in thread
From: Thara Gopinath @ 2026-09-01 16:55 UTC (permalink / raw)
To: kys, haiyangz, wei.liu, decui, tglx, mingo, bp, dave.hansen, hpa,
ardb, ilias.apalodimas
Cc: James.Bottomley, "longli, javierm, lszubowi, francescopompo2,
tgopinath, x86, linux-hyperv, linux-kernel, linux-efi,
Thara Gopinath
A vtlcall is the mechanism by which a lower VTL (VTL0) transitions
into a higher VTL (VTL1) on Microsoft Hyper-V. It is issued as a
call to a fixed offset within the hypercall page; the hypervisor
saves the lower-VTL context, restores the upper-VTL context, and
resumes execution at the upper VTL. Control returns to VTL0 when
the upper VTL executes a vtlreturn.
Introduce the x86 VTL0-side vtlcall infrastructure:
- hv_vsm_vtlcall(): runtime entry point. Preserves state VTL1 can
clobber (IRQs, FPU, CR2) and invokes the assembly trampoline.
Returns the signed 64-bit status the secure kernel places in a3.
- hv_vsm_init_vtlcall(u64 vtl_call_offset): one-shot initializer
that installs the vtlcall target into a static_call. The offset
is passed in by the caller so the register read stays outside
arch code, mirroring mshv_vtl_return_call_init().
- struct hv_vtlcall_param and CONFIG_HYPERV_VSM API in
include/hyperv/vsm.h, with no-op stubs when VSM is disabled.
- __hv_vsm_vtlcall assembly trampoline in mshv_vtl_asm.S, plus
asm-offsets for the argument block.
Signed-off-by: Thara Gopinath <tgopinath@linux.microsoft.com>
---
arch/x86/hyperv/Makefile | 2 +-
arch/x86/hyperv/hv_vtl_vsm.c | 29 ++++++++++++
arch/x86/hyperv/mshv-asm-offsets.c | 8 ++++
arch/x86/hyperv/mshv_vtl_asm.S | 75 ++++++++++++++++++++++++++++++
include/hyperv/vsm.h | 22 +++++++++
5 files changed, 135 insertions(+), 1 deletion(-)
diff --git a/arch/x86/hyperv/Makefile b/arch/x86/hyperv/Makefile
index 1fdc20e239243..430f5f2bca40a 100644
--- a/arch/x86/hyperv/Makefile
+++ b/arch/x86/hyperv/Makefile
@@ -2,7 +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-$(CONFIG_HYPERV_VSM) += hv_vtl_vsm.o mshv_vtl_asm.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
index edc55264c4d87..d533a8e113935 100644
--- a/arch/x86/hyperv/hv_vtl_vsm.c
+++ b/arch/x86/hyperv/hv_vtl_vsm.c
@@ -15,6 +15,7 @@
#include <hyperv/vsm.h>
#include <asm/msr-index.h>
#include <asm/processor-flags.h>
+#include <hyperv/hvgdk_mini.h>
#include <asm/mshyperv.h>
/* Define PAGE size and related variables for initial secure kernel pages */
@@ -256,3 +257,31 @@ void __init hv_vsm_arch_init_vp(struct hv_init_vp_context *vp_ctx, Elf64_Addr sk
hv_vsm_init_gdt(vp_ctx, sk_pa);
hv_vsm_init_page_tables(vp_ctx, sk_pa);
}
+
+/* Implemented in mshv_vtl_asm.S */
+void __hv_vsm_vtlcall(struct hv_vtlcall_param *args);
+
+DEFINE_STATIC_CALL_NULL(__hv_vsm_vtlcall_hypercall, void (*)(void));
+
+void __init hv_vsm_init_vtlcall(u64 vtl_call_offset)
+{
+ static_call_update(__hv_vsm_vtlcall_hypercall,
+ (void *)((u8 *)hv_hypercall_pg + vtl_call_offset));
+}
+
+s64 hv_vsm_vtlcall(struct hv_vtlcall_param *args)
+{
+ unsigned long flags;
+ u64 cr2;
+
+ local_irq_save(flags);
+ kernel_fpu_begin_mask(0);
+ cr2 = native_read_cr2();
+ __hv_vsm_vtlcall(args);
+ native_write_cr2(cr2);
+ kernel_fpu_end();
+ local_irq_restore(flags);
+
+ /* The secure kernel returns a signed 64-bit status in a3. */
+ return (s64)args->a3;
+}
diff --git a/arch/x86/hyperv/mshv-asm-offsets.c b/arch/x86/hyperv/mshv-asm-offsets.c
index 882c1db6df16c..0f0470ddaee59 100644
--- a/arch/x86/hyperv/mshv-asm-offsets.c
+++ b/arch/x86/hyperv/mshv-asm-offsets.c
@@ -12,6 +12,7 @@
#define COMPILE_OFFSETS
#include <linux/kbuild.h>
+#include <hyperv/vsm.h>
#include <asm/mshyperv.h>
static void __used common(void)
@@ -34,4 +35,11 @@ static void __used common(void)
OFFSET(MSHV_VTL_CPU_CONTEXT_r15, mshv_vtl_cpu_context, r15);
OFFSET(MSHV_VTL_CPU_CONTEXT_cr2, mshv_vtl_cpu_context, cr2);
}
+
+ if (IS_ENABLED(CONFIG_HYPERV_VSM)) {
+ OFFSET(HV_VTLCALL_PARAM_a0, hv_vtlcall_param, a0);
+ OFFSET(HV_VTLCALL_PARAM_a1, hv_vtlcall_param, a1);
+ OFFSET(HV_VTLCALL_PARAM_a2, hv_vtlcall_param, a2);
+ OFFSET(HV_VTLCALL_PARAM_a3, hv_vtlcall_param, a3);
+ }
}
diff --git a/arch/x86/hyperv/mshv_vtl_asm.S b/arch/x86/hyperv/mshv_vtl_asm.S
index f595eefad9abf..4d7007848ecd4 100644
--- a/arch/x86/hyperv/mshv_vtl_asm.S
+++ b/arch/x86/hyperv/mshv_vtl_asm.S
@@ -15,6 +15,8 @@
#include <asm/frame.h>
#include "mshv-asm-offsets.h"
+#ifdef CONFIG_HYPERV_VTL_MODE
+
.text
.section .noinstr.text, "ax"
/*
@@ -114,3 +116,76 @@ SYM_FUNC_END(__mshv_vtl_return_call)
.size mshv_vtl_return_sym, 8
mshv_vtl_return_sym:
.quad __SCK____mshv_vtl_return_hypercall
+
+#endif /* CONFIG_HYPERV_VTL_MODE */
+
+#ifdef CONFIG_HYPERV_VSM
+ .text
+/*
+ * void __hv_vsm_vtlcall(struct hv_vtlcall_param *args)
+ *
+ * Perform a VTL call to switch to the upper VTL.
+ *
+ * The %rcx register is zeroed before the call and is clobbered by the
+ * hypercall. %rax is not restored by the upper VTL (passed via the assist
+ * page) but is unused and can be ignored.
+ *
+ * The args pointer is preserved on the stack across the VTL call since all
+ * argument registers are repurposed during the VTL call.
+ *
+ * Microsoft Hypervisor preserves %rsp during VTL switches.
+ */
+SYM_FUNC_START(__hv_vsm_vtlcall)
+ /* Save callee-saved registers */
+ pushq %rbp
+ mov %rsp, %rbp
+ pushq %r12
+ pushq %r13
+ pushq %r14
+ pushq %r15
+ pushq %rbx
+ pushq %rdi
+
+ /* Load struct fields into VTL calling convention registers */
+ mov HV_VTLCALL_PARAM_a3(%rdi), %r8
+ mov HV_VTLCALL_PARAM_a2(%rdi), %rdx
+ mov HV_VTLCALL_PARAM_a1(%rdi), %rsi
+ mov HV_VTLCALL_PARAM_a0(%rdi), %rdi
+
+ /* Zero %rcx */
+ xorl %ecx, %ecx
+
+ /* VTL call */
+ call STATIC_CALL_TRAMP_STR(__hv_vsm_vtlcall_hypercall)
+
+ /* Restore args pointer from stack */
+ popq %rax
+
+ /* Store results back to struct */
+ mov %rdi, HV_VTLCALL_PARAM_a0(%rax)
+ mov %rsi, HV_VTLCALL_PARAM_a1(%rax)
+ mov %rdx, HV_VTLCALL_PARAM_a2(%rax)
+ mov %r8, HV_VTLCALL_PARAM_a3(%rax)
+
+ /* Restore callee-saved registers */
+ popq %rbx
+ popq %r15
+ popq %r14
+ popq %r13
+ popq %r12
+
+ popq %rbp
+ RET
+SYM_FUNC_END(__hv_vsm_vtlcall)
+
+/*
+ * Ensure static_call_key symbol __SCK____hv_vsm_vtlcall_hypercall is
+ * accessible. Inspired by __ADDRESSABLE(sym) macro.
+ */
+ .section .discard.addressable,"aw"
+ .align 8
+ .type hv_vsm_vtlcall_sym, @object
+ .size hv_vsm_vtlcall_sym, 8
+hv_vsm_vtlcall_sym:
+ .quad __SCK____hv_vsm_vtlcall_hypercall
+#endif /* CONFIG_HYPERV_VSM */
diff --git a/include/hyperv/vsm.h b/include/hyperv/vsm.h
index 51555c09413d5..c1fa4ef3bccde 100644
--- a/include/hyperv/vsm.h
+++ b/include/hyperv/vsm.h
@@ -11,6 +11,8 @@
#ifndef _HYPERV_VSM_H
#define _HYPERV_VSM_H
+#include <linux/types.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
@@ -18,4 +20,24 @@
*/
#define VSM_SK_INITIAL_MAP_SIZE (16 * 1024 * 1024)
+/*
+ * Argument block passed from VTL0 to VTL1 across a vtlcall. Layout is
+ * shared with the arch-specific assembly trampoline that marshals these
+ * into registers.
+ */
+struct hv_vtlcall_param {
+ u64 a0;
+ u64 a1;
+ u64 a2;
+ u64 a3;
+} __packed;
+
+#ifdef CONFIG_HYPERV_VSM
+s64 hv_vsm_vtlcall(struct hv_vtlcall_param *args);
+void hv_vsm_init_vtlcall(u64 vtl_call_offset);
+#else
+static inline s64 hv_vsm_vtlcall(struct hv_vtlcall_param *args) { return 0; }
+static inline void hv_vsm_init_vtlcall(u64 vtl_call_offset) {}
+#endif
+
#endif /* _HYPERV_VSM_H */
--
2.34.1
^ permalink raw reply related [flat|nested] 34+ messages in thread* [RFC PATCH 11/12] drivers: hv: hv_vsm_boot: Boot primary processor in VTL1
2026-09-01 16:55 [RFC PATCH 00/12] Introduce LVBS support for Hyper-V guests Thara Gopinath
` (9 preceding siblings ...)
2026-09-01 16:55 ` [RFC PATCH 10/12] arch: x86: hyperv: hv_vtl_vsm: Introduce vtlcall Thara Gopinath
@ 2026-09-01 16:55 ` Thara Gopinath
2026-09-01 17:35 ` sashiko-bot
2026-09-01 16:55 ` [RFC PATCH 12/12] drivers: hv: hv_vsm_boot: Boot secondary processors " Thara Gopinath
11 siblings, 1 reply; 34+ messages in thread
From: Thara Gopinath @ 2026-09-01 16:55 UTC (permalink / raw)
To: kys, haiyangz, wei.liu, decui, tglx, mingo, bp, dave.hansen, hpa,
ardb, ilias.apalodimas
Cc: James.Bottomley, "longli, javierm, lszubowi, francescopompo2,
tgopinath, x86, linux-hyperv, linux-kernel, linux-efi,
Thara Gopinath
With partition-level VTL1 enabled, the secure kernel image loaded,
its initial vCPU context built, and VTL1 enabled on the boot CPU,
the final step of the primary-CPU bring-up is to actually transition
into VTL1 and start executing the secure kernel. Hyper-V restores
the CPU state from the vp_context supplied during VTL1 enablement
when the first vtlcall is issued.
Issue that first vtlcall on the primary CPU, passing the number of
possible CPUs and the location of the secure kernel reserved region
as the VTL1 boot handshake.
Signed-off-by: Thara Gopinath <tgopinath@linux.microsoft.com>
---
drivers/hv/hv_vsm_boot.c | 63 +++++++++++++++++++++++++++++++++++++++-
1 file changed, 62 insertions(+), 1 deletion(-)
diff --git a/drivers/hv/hv_vsm_boot.c b/drivers/hv/hv_vsm_boot.c
index abe82e03e1897..c4f15c42df1f6 100644
--- a/drivers/hv/hv_vsm_boot.c
+++ b/drivers/hv/hv_vsm_boot.c
@@ -35,6 +35,15 @@
static void *vsm_skm_va;
+/*
+ * By default, when a processor boots in VTL1, we assume that MBEC (Mode-Based Execution Control)
+ * support is also enabled. MBEC distinguishes between user and kernel memory execution permissions.
+ * After the processor boots in VTL1, we verify whether MBEC is actually enabled. If it is not,
+ * we set a global flag to false. This flag is shared across all processors—if any processor fails
+ * to enable MBEC, the system treats MBEC as disabled.
+ */
+static bool hv_vsm_mbec_enabled = true;
+
static int hv_vsm_get_register(u32 reg_name, u64 *result)
{
struct hv_register_assoc reg = {
@@ -300,6 +309,51 @@ static int __init hv_vsm_get_partition_status(u16 *enabled_vtl_set, u8 *max_vtl,
return 0;
}
+static int __init hv_vsm_init_code_page_offsets(void)
+{
+ union hv_register_vsm_page_offsets offsets;
+ u64 result;
+ int ret;
+
+ ret = hv_vsm_get_register(HV_REGISTER_VSM_CODE_PAGE_OFFSETS, &result);
+ if (ret) {
+ pr_err("Failed to read VSM code page offsets: %d\n", ret);
+ return ret;
+ }
+
+ offsets.as_uint64 = result;
+ hv_vsm_init_vtlcall(offsets.vtl_call_offset);
+ return 0;
+}
+
+static int __init hv_vsm_boot_vtl1(void)
+{
+ struct hv_vtlcall_param args = {0};
+ u16 vp_enabled_vtl_set = 0;
+ u8 active_mbec_enabled = 0;
+ int ret;
+ s64 sk_status;
+
+ args.a0 = num_possible_cpus();
+ args.a1 = sk_res.start;
+ args.a2 = resource_size(&sk_res);
+
+ /* Kick start vtl1 boot on the primary cpu. */
+ sk_status = hv_vsm_vtlcall(&args);
+ if (sk_status)
+ pr_warn("VTL1 boot returned status %lld\n", sk_status);
+
+ ret = hv_vsm_get_vp_status(&vp_enabled_vtl_set, &active_mbec_enabled);
+ if (ret)
+ return ret;
+
+ if (!active_mbec_enabled) {
+ pr_err("Failed to enable MBEC for VP0\n");
+ hv_vsm_mbec_enabled = false;
+ }
+ return 0;
+}
+
static int __init hv_vsm_bootstrap_vtl(void)
{
u16 partition_enabled_vtl_set = 0, partition_mbec_enabled_vtl_set = 0;
@@ -369,7 +423,14 @@ static int __init hv_vsm_bootstrap_vtl(void)
return -EINVAL;
}
}
- return 0;
+
+ /* Point the vtlcall trampoline at the correct hypercall page offset */
+ ret = hv_vsm_init_code_page_offsets();
+ if (ret)
+ return ret;
+
+ /* Boot primary virtual processor in VTL1 */
+ return hv_vsm_boot_vtl1();
}
static void __init hv_vsm_get_sk_mem(void)
--
2.34.1
^ permalink raw reply related [flat|nested] 34+ messages in thread* Re: [RFC PATCH 11/12] drivers: hv: hv_vsm_boot: Boot primary processor in VTL1
2026-09-01 16:55 ` [RFC PATCH 11/12] drivers: hv: hv_vsm_boot: Boot primary processor in VTL1 Thara Gopinath
@ 2026-09-01 17:35 ` sashiko-bot
0 siblings, 0 replies; 34+ messages in thread
From: sashiko-bot @ 2026-09-01 17:35 UTC (permalink / raw)
To: Thara Gopinath; +Cc: linux-hyperv
Thank you for your contribution! Sashiko AI review found 1 potential issue(s) to consider:
- [High] Ignored error status from VTL1 boot handshake allows the system to continue in an insecure state, bypassing a security-critical kernel panic.
--
commit 91cb8ed727ae4b19c3d3be7dbd03c34def48f245
Author: Thara Gopinath <tgopinath@linux.microsoft.com>
drivers: hv: hv_vsm_boot: Boot primary processor in VTL1
This commit introduces the final step of the primary-CPU bring-up to
transition into VTL1 and start executing the secure kernel. It issues
the first vtlcall on the primary CPU to pass initialization arguments.
It also adds a check to see if Mode-Based Execution Control (MBEC) is
enabled after booting into VTL1.
> diff --git a/drivers/hv/hv_vsm_boot.c b/drivers/hv/hv_vsm_boot.c
> index abe82e03e1897..c4f15c42df1f6 100644
> --- a/drivers/hv/hv_vsm_boot.c
> +++ b/drivers/hv/hv_vsm_boot.c
[ ... ]
> +static int __init hv_vsm_boot_vtl1(void)
> +{
> + struct hv_vtlcall_param args = {0};
> + u16 vp_enabled_vtl_set = 0;
> + u8 active_mbec_enabled = 0;
> + int ret;
> + s64 sk_status;
> +
> + args.a0 = num_possible_cpus();
> + args.a1 = sk_res.start;
> + args.a2 = resource_size(&sk_res);
> +
> + /* Kick start vtl1 boot on the primary cpu. */
> + sk_status = hv_vsm_vtlcall(&args);
> + if (sk_status)
> + pr_warn("VTL1 boot returned status %lld\n", sk_status);
> +
> + ret = hv_vsm_get_vp_status(&vp_enabled_vtl_set, &active_mbec_enabled);
> + if (ret)
> + return ret;
> +
> + if (!active_mbec_enabled) {
> + pr_err("Failed to enable MBEC for VP0\n");
> + hv_vsm_mbec_enabled = false;
> + }
> + return 0;
> +}
^^^^
[Severity: High]
Does this code silently ignore a VTL1 boot failure?
If hv_vsm_vtlcall() returns a non-zero error in sk_status, this function
merely logs a warning and proceeds to call hv_vsm_get_vp_status().
If hv_vsm_get_vp_status() succeeds despite the VTL1 boot failure,
hv_vsm_boot_vtl1() will return 0, because vp_enabled_vtl_set is never
validated to confirm VTL1 actually booted.
This could allow the system to continue by bypassing failure handling in the
caller. If VTL1 setup fails, could the partition be left in an exposed state
where an attacker might race to configure VTL1 themselves and gain a
higher-privilege foothold than VTL0?
Should this return an error code when hv_vsm_vtlcall() fails?
--
Sashiko AI review · https://sashiko.dev/#/patchset/20260901165647.3160413-1-tgopinath@linux.microsoft.com?part=11
^ permalink raw reply [flat|nested] 34+ messages in thread
* [RFC PATCH 12/12] drivers: hv: hv_vsm_boot: Boot secondary processors in VTL1
2026-09-01 16:55 [RFC PATCH 00/12] Introduce LVBS support for Hyper-V guests Thara Gopinath
` (10 preceding siblings ...)
2026-09-01 16:55 ` [RFC PATCH 11/12] drivers: hv: hv_vsm_boot: Boot primary processor in VTL1 Thara Gopinath
@ 2026-09-01 16:55 ` Thara Gopinath
2026-09-01 17:44 ` sashiko-bot
11 siblings, 1 reply; 34+ messages in thread
From: Thara Gopinath @ 2026-09-01 16:55 UTC (permalink / raw)
To: kys, haiyangz, wei.liu, decui, tglx, mingo, bp, dave.hansen, hpa,
ardb, ilias.apalodimas
Cc: James.Bottomley, "longli, javierm, lszubowi, francescopompo2,
tgopinath, x86, linux-hyperv, linux-kernel, linux-efi,
Thara Gopinath
The next step after the primary CPU is running in VTL1 is to boot
the remaining CPUs in VTL1, so the secure kernel executes on every
VP rather than only VP0. Hyper-V does not drive this: VTL1 needs to
know which APs to expect, and each AP must issue its own vtlcall
from VTL0 to actually transition.
As on the boot CPU, VTL1 must be enabled at each VP and its initial
VP context (RIP, GDT, page tables, etc.) built before that VP can
enter VTL1. This can only be done from VTL1 for non boot CPus.
Introduce VSM_VTL_CALL_FUNC_ID_BOOT_APS, a vtlcall the boot cpu issues
from VTL0, which asks the secure kernel to enable VTL1 and set up the
initial VP context for each AP in the supplied online-CPU mask.
The mask is handed to VTL1 via a shared page whose PFN is passed in
the vtlcall arguments; VTL1 copies it synchronously so the page can be
freed as soon as the call returns.
Once VTL1 has prepared the APs, bring them into VTL1 one at a time
using a CPU-bound FIFO kthread and a completion. Serialising this
way keeps VTL1 entries ordered and lets a per-AP failure be
surfaced synchronously to the caller. Uniprocessor systems skip AP
bring-up entirely.
Signed-off-by: Thara Gopinath <tgopinath@linux.microsoft.com>
---
drivers/hv/hv_vsm.h | 2 +
drivers/hv/hv_vsm_boot.c | 125 ++++++++++++++++++++++++++++++++++++++-
2 files changed, 126 insertions(+), 1 deletion(-)
diff --git a/drivers/hv/hv_vsm.h b/drivers/hv/hv_vsm.h
index 88f099f88eeb4..679cb9dc9afda 100644
--- a/drivers/hv/hv_vsm.h
+++ b/drivers/hv/hv_vsm.h
@@ -12,6 +12,8 @@
#include <linux/ioport.h>
#include <linux/types.h>
+#define VSM_VTL_CALL_FUNC_ID_BOOT_APS 0x1FFE1
+
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 c4f15c42df1f6..ad161b2f56653 100644
--- a/drivers/hv/hv_vsm_boot.c
+++ b/drivers/hv/hv_vsm_boot.c
@@ -17,6 +17,7 @@
#include <linux/namei.h>
#include <linux/acpi.h>
#include <linux/firmware.h>
+#include <linux/kthread.h>
#include <hyperv/vsm.h>
#include <asm/e820/types.h>
#include <asm/mshyperv.h>
@@ -64,6 +65,20 @@ static int hv_vsm_get_register(u32 reg_name, u64 *result)
return 0;
}
+static __init struct page *hv_vsm_alloc_shared_page(void)
+{
+ struct page *page;
+
+ page = alloc_page(GFP_KERNEL);
+ if (!page) {
+ pr_err("Unable to establish VTL0-VTL1 shared page\n");
+ return ERR_PTR(-ENOMEM);
+ }
+
+ memset(page_address(page), 0, PAGE_SIZE);
+ return page;
+}
+
static Elf64_Addr __init hv_vsm_elf_min_load_paddr(void *image)
{
Elf64_Ehdr *ehdr = image;
@@ -326,6 +341,107 @@ static int __init hv_vsm_init_code_page_offsets(void)
return 0;
}
+struct hv_vsm_ap_boot_ctx {
+ struct completion done;
+ int ret;
+};
+
+static int __init hv_vsm_boot_sec_vp_thread_fn(void *arg)
+{
+ struct hv_vsm_ap_boot_ctx *ctx = arg;
+ struct hv_vtlcall_param args = {0};
+ int cpu = smp_processor_id();
+ u16 vp_enabled_vtl_set = 0;
+ u8 active_mbec_enabled = 0;
+ s64 sk_status;
+ int ret = 0;
+
+ pr_info("cpu%d entering vtl1 boot thread\n", cpu);
+ sk_status = hv_vsm_vtlcall(&args);
+ if (sk_status)
+ pr_warn("VP%d VTL1 boot returned status %lld\n", cpu, sk_status);
+
+ ret = hv_vsm_get_vp_status(&vp_enabled_vtl_set, &active_mbec_enabled);
+ if (ret)
+ goto out;
+
+ if (!active_mbec_enabled) {
+ pr_err("Failed to enable MBEC for VP%d\n", cpu);
+ hv_vsm_mbec_enabled = false;
+ }
+out:
+ ctx->ret = ret;
+ complete(&ctx->done);
+ return 0;
+}
+
+static int __init hv_vsm_boot_one_ap(unsigned int cpu)
+{
+ struct hv_vsm_ap_boot_ctx ctx;
+ struct task_struct *t;
+
+ init_completion(&ctx.done);
+ ctx.ret = 0;
+
+ t = kthread_create(hv_vsm_boot_sec_vp_thread_fn, &ctx,
+ "hv-vtl1-ap%u", cpu);
+ if (IS_ERR(t))
+ return PTR_ERR(t);
+
+ kthread_bind(t, cpu);
+ sched_set_fifo(t);
+ wake_up_process(t);
+
+ wait_for_completion(&ctx.done);
+ return ctx.ret;
+}
+
+static int __init hv_vsm_boot_ap_vtl(void)
+{
+ struct hv_vtlcall_param args = {0};
+ struct page *cpu_online_page;
+ unsigned int cpu, cur_cpu = smp_processor_id();
+ s64 sk_status;
+ int ret;
+
+ cpu_online_page = hv_vsm_alloc_shared_page();
+ if (IS_ERR(cpu_online_page))
+ return PTR_ERR(cpu_online_page);
+
+ cpumask_copy(page_address(cpu_online_page), cpu_online_mask);
+
+ /*
+ * Hand VTL1 the set of APs to expect. VTL1 copies the mask
+ * synchronously inside this vtlcall and does not reference the
+ * page after it returns, so freeing it here is safe.
+ */
+ args.a0 = VSM_VTL_CALL_FUNC_ID_BOOT_APS;
+ args.a1 = page_to_pfn(cpu_online_page);
+ sk_status = hv_vsm_vtlcall(&args);
+ __free_page(cpu_online_page);
+ if (sk_status) {
+ pr_err("VTL1 refused BOOT_APS: status %lld\n", sk_status);
+ return -EIO;
+ }
+
+ /*
+ * Bring the APs into VTL1 one at a time. Each AP kthread issues
+ * a single vtlcall on its bound CPU and signals completion; wait
+ * for it to finish before starting the next so VTL1 entries stay
+ * serialized.
+ */
+ for_each_online_cpu(cpu) {
+ if (cpu == cur_cpu)
+ continue;
+ ret = hv_vsm_boot_one_ap(cpu);
+ if (ret) {
+ pr_err("Failed to boot VP%u into VTL1: %d\n", cpu, ret);
+ return ret;
+ }
+ }
+ return 0;
+}
+
static int __init hv_vsm_boot_vtl1(void)
{
struct hv_vtlcall_param args = {0};
@@ -430,7 +546,14 @@ static int __init hv_vsm_bootstrap_vtl(void)
return ret;
/* Boot primary virtual processor in VTL1 */
- return hv_vsm_boot_vtl1();
+ ret = hv_vsm_boot_vtl1();
+ if (ret)
+ return ret;
+
+ if (num_present_cpus() == 1)
+ return 0;
+
+ return hv_vsm_boot_ap_vtl();
}
static void __init hv_vsm_get_sk_mem(void)
--
2.34.1
^ permalink raw reply related [flat|nested] 34+ messages in thread* Re: [RFC PATCH 12/12] drivers: hv: hv_vsm_boot: Boot secondary processors in VTL1
2026-09-01 16:55 ` [RFC PATCH 12/12] drivers: hv: hv_vsm_boot: Boot secondary processors " Thara Gopinath
@ 2026-09-01 17:44 ` sashiko-bot
0 siblings, 0 replies; 34+ messages in thread
From: sashiko-bot @ 2026-09-01 17:44 UTC (permalink / raw)
To: Thara Gopinath; +Cc: linux-hyperv
Thank you for your contribution! Sashiko AI review found 4 potential issue(s) to consider:
- [High] The boot CPU is incorrectly included in the `cpu_online_mask` passed to VTL1.
- [High] AP VTL1 boot failures are silently ignored, leaving the system in a partially-booted secure state.
- [High] Unsafe usage of `smp_processor_id()` in preemptible context and task migration race.
- [Medium] Unprotected snapshot and iteration of `cpu_online_mask`.
--
commit 99445d1f4eda186e7be403a5b296f1c80a9cdae6
Author: Thara Gopinath <tgopinath@linux.microsoft.com>
drivers: hv: hv_vsm_boot: Boot secondary processors in VTL1
This commit adds support to boot secondary processors into VTL1 after the
primary CPU has transitioned. It passes an online CPU mask to VTL1 via a
shared page and uses a CPU-bound kthread to sequentially issue a vtlcall
for each AP.
> diff --git a/drivers/hv/hv_vsm_boot.c b/drivers/hv/hv_vsm_boot.c
> index c4f15c42df1f6..ad161b2f56653 100644
> --- a/drivers/hv/hv_vsm_boot.c
> +++ b/drivers/hv/hv_vsm_boot.c
[ ... ]
> +static int __init hv_vsm_boot_sec_vp_thread_fn(void *arg)
> +{
> + struct hv_vsm_ap_boot_ctx *ctx = arg;
> + struct hv_vtlcall_param args = {0};
> + int cpu = smp_processor_id();
> + u16 vp_enabled_vtl_set = 0;
> + u8 active_mbec_enabled = 0;
> + s64 sk_status;
> + int ret = 0;
> +
> + pr_info("cpu%d entering vtl1 boot thread\n", cpu);
> + sk_status = hv_vsm_vtlcall(&args);
> + if (sk_status)
> + pr_warn("VP%d VTL1 boot returned status %lld\n", cpu, sk_status);
> +
> + ret = hv_vsm_get_vp_status(&vp_enabled_vtl_set, &active_mbec_enabled);
> + if (ret)
> + goto out;
> +
> + if (!active_mbec_enabled) {
> + pr_err("Failed to enable MBEC for VP%d\n", cpu);
> + hv_vsm_mbec_enabled = false;
> + }
> +out:
> + ctx->ret = ret;
[Severity: High]
If hv_vsm_vtlcall() fails and returns an error in sk_status, will this
error be silently ignored? The warning is printed, but ret remains 0 before
calling hv_vsm_get_vp_status(). If hv_vsm_get_vp_status() succeeds, ret
is unconditionally set to 0. Does this leave the caller believing the AP
booted successfully instead of surfacing the error?
> + complete(&ctx->done);
> + return 0;
> +}
[ ... ]
> +static int __init hv_vsm_boot_ap_vtl(void)
> +{
> + struct hv_vtlcall_param args = {0};
> + struct page *cpu_online_page;
> + unsigned int cpu, cur_cpu = smp_processor_id();
[Severity: High]
Is it safe to use smp_processor_id() here in a preemptible context?
Fetching it here triggers a kernel BUG warning. Also, because the thread can
migrate between CPU evaluation and the sleepable operations like alloc_page()
and wait_for_completion() below, could cur_cpu become stale? This might
cause the AP loop to skip the new CPU and incorrectly attempt to boot the
original primary CPU as an AP.
> + s64 sk_status;
> + int ret;
> +
> + cpu_online_page = hv_vsm_alloc_shared_page();
> + if (IS_ERR(cpu_online_page))
> + return PTR_ERR(cpu_online_page);
> +
> + cpumask_copy(page_address(cpu_online_page), cpu_online_mask);
[Severity: Medium]
Should the snapshot of cpu_online_mask and subsequent iteration
be protected by cpus_read_lock()? Accessing this without hotplug
protection could allow concurrent CPU hotplug events to alter the
mask, potentially passing an inconsistent snapshot to VTL1 or binding
an AP boot kthread to a newly offlined CPU.
[Severity: High]
Does this verbatim copy of cpu_online_mask incorrectly include the
already-booted primary CPU in the mask passed to VTL1? If cur_cpu isn't
cleared from the mask first, could VTL1 mistakenly expect the primary CPU to
issue an AP boot vtlcall and hang or corrupt the primary CPU's secure
context?
> +
> + /*
> + * Hand VTL1 the set of APs to expect. VTL1 copies the mask
> + * synchronously inside this vtlcall and does not reference the
> + * page after it returns, so freeing it here is safe.
> + */
> + args.a0 = VSM_VTL_CALL_FUNC_ID_BOOT_APS;
> + args.a1 = page_to_pfn(cpu_online_page);
> + sk_status = hv_vsm_vtlcall(&args);
--
Sashiko AI review · https://sashiko.dev/#/patchset/20260901165647.3160413-1-tgopinath@linux.microsoft.com?part=12
^ permalink raw reply [flat|nested] 34+ messages in thread