mirror of
https://github.com/torvalds/linux.git
synced 2026-05-28 17:13:52 +02:00
FRED-enabled SEV-(ES,SNP) guests fail to boot due to the following issues
in the early boot sequence:
* FRED does not have a #VC exception handler in the dispatch logic
* Early FRED #VC exceptions attempt to use uninitialized per-CPU GHCBs
instead of boot_ghcb
Add X86_TRAP_VC case to fred_hwexc() with a new exc_vmm_communication()
function that provides the unified entry point FRED requires, dispatching
to existing user/kernel handlers based on privilege level. The function is
already declared via DECLARE_IDTENTRY_VC().
Fix early GHCB access by falling back to boot_ghcb in
__sev_{get,put}_ghcb() when per-CPU GHCBs are not yet initialized.
Fixes: 14619d912b ("x86/fred: FRED entry/exit and dispatch code")
Signed-off-by: Nikunj A Dadhania <nikunj@amd.com>
Signed-off-by: Borislav Petkov (AMD) <bp@alien8.de>
Reviewed-by: Tom Lendacky <thomas.lendacky@amd.com>
Cc: <stable@kernel.org> # 6.12+
Link: https://patch.msgid.link/20260318075654.1792916-4-nikunj@amd.com
190 lines
4.6 KiB
C
190 lines
4.6 KiB
C
// SPDX-License-Identifier: GPL-2.0-only
|
|
/*
|
|
* AMD Memory Encryption Support
|
|
*
|
|
* Copyright (C) 2019 SUSE
|
|
*
|
|
* Author: Joerg Roedel <jroedel@suse.de>
|
|
*/
|
|
|
|
#define pr_fmt(fmt) "SEV: " fmt
|
|
|
|
#include <linux/bug.h>
|
|
#include <linux/kernel.h>
|
|
|
|
#include <asm/cpu_entry_area.h>
|
|
#include <asm/msr.h>
|
|
#include <asm/ptrace.h>
|
|
#include <asm/sev.h>
|
|
|
|
#include "internal.h"
|
|
|
|
static __always_inline bool on_vc_stack(struct pt_regs *regs)
|
|
{
|
|
unsigned long sp = regs->sp;
|
|
|
|
/* User-mode RSP is not trusted */
|
|
if (user_mode(regs))
|
|
return false;
|
|
|
|
/* SYSCALL gap still has user-mode RSP */
|
|
if (ip_within_syscall_gap(regs))
|
|
return false;
|
|
|
|
return ((sp >= __this_cpu_ist_bottom_va(VC)) && (sp < __this_cpu_ist_top_va(VC)));
|
|
}
|
|
|
|
/*
|
|
* This function handles the case when an NMI is raised in the #VC
|
|
* exception handler entry code, before the #VC handler has switched off
|
|
* its IST stack. In this case, the IST entry for #VC must be adjusted,
|
|
* so that any nested #VC exception will not overwrite the stack
|
|
* contents of the interrupted #VC handler.
|
|
*
|
|
* The IST entry is adjusted unconditionally so that it can be also be
|
|
* unconditionally adjusted back in __sev_es_ist_exit(). Otherwise a
|
|
* nested sev_es_ist_exit() call may adjust back the IST entry too
|
|
* early.
|
|
*
|
|
* The __sev_es_ist_enter() and __sev_es_ist_exit() functions always run
|
|
* on the NMI IST stack, as they are only called from NMI handling code
|
|
* right now.
|
|
*/
|
|
void noinstr __sev_es_ist_enter(struct pt_regs *regs)
|
|
{
|
|
unsigned long old_ist, new_ist;
|
|
|
|
/* Read old IST entry */
|
|
new_ist = old_ist = __this_cpu_read(cpu_tss_rw.x86_tss.ist[IST_INDEX_VC]);
|
|
|
|
/*
|
|
* If NMI happened while on the #VC IST stack, set the new IST
|
|
* value below regs->sp, so that the interrupted stack frame is
|
|
* not overwritten by subsequent #VC exceptions.
|
|
*/
|
|
if (on_vc_stack(regs))
|
|
new_ist = regs->sp;
|
|
|
|
/*
|
|
* Reserve additional 8 bytes and store old IST value so this
|
|
* adjustment can be unrolled in __sev_es_ist_exit().
|
|
*/
|
|
new_ist -= sizeof(old_ist);
|
|
*(unsigned long *)new_ist = old_ist;
|
|
|
|
/* Set new IST entry */
|
|
this_cpu_write(cpu_tss_rw.x86_tss.ist[IST_INDEX_VC], new_ist);
|
|
}
|
|
|
|
void noinstr __sev_es_ist_exit(void)
|
|
{
|
|
unsigned long ist;
|
|
|
|
/* Read IST entry */
|
|
ist = __this_cpu_read(cpu_tss_rw.x86_tss.ist[IST_INDEX_VC]);
|
|
|
|
if (WARN_ON(ist == __this_cpu_ist_top_va(VC)))
|
|
return;
|
|
|
|
/* Read back old IST entry and write it to the TSS */
|
|
this_cpu_write(cpu_tss_rw.x86_tss.ist[IST_INDEX_VC], *(unsigned long *)ist);
|
|
}
|
|
|
|
void noinstr __sev_es_nmi_complete(void)
|
|
{
|
|
struct ghcb_state state;
|
|
struct ghcb *ghcb;
|
|
|
|
ghcb = __sev_get_ghcb(&state);
|
|
|
|
vc_ghcb_invalidate(ghcb);
|
|
ghcb_set_sw_exit_code(ghcb, SVM_VMGEXIT_NMI_COMPLETE);
|
|
ghcb_set_sw_exit_info_1(ghcb, 0);
|
|
ghcb_set_sw_exit_info_2(ghcb, 0);
|
|
|
|
sev_es_wr_ghcb_msr(__pa_nodebug(ghcb));
|
|
VMGEXIT();
|
|
|
|
__sev_put_ghcb(&state);
|
|
}
|
|
|
|
/*
|
|
* Nothing shall interrupt this code path while holding the per-CPU
|
|
* GHCB. The backup GHCB is only for NMIs interrupting this path.
|
|
*
|
|
* Callers must disable local interrupts around it.
|
|
*/
|
|
noinstr struct ghcb *__sev_get_ghcb(struct ghcb_state *state)
|
|
{
|
|
struct sev_es_runtime_data *data;
|
|
struct ghcb *ghcb;
|
|
|
|
WARN_ON(!irqs_disabled());
|
|
|
|
if (!sev_cfg.ghcbs_initialized)
|
|
return boot_ghcb;
|
|
|
|
data = this_cpu_read(runtime_data);
|
|
ghcb = &data->ghcb_page;
|
|
|
|
if (unlikely(data->ghcb_active)) {
|
|
/* GHCB is already in use - save its contents */
|
|
|
|
if (unlikely(data->backup_ghcb_active)) {
|
|
/*
|
|
* Backup-GHCB is also already in use. There is no way
|
|
* to continue here so just kill the machine. To make
|
|
* panic() work, mark GHCBs inactive so that messages
|
|
* can be printed out.
|
|
*/
|
|
data->ghcb_active = false;
|
|
data->backup_ghcb_active = false;
|
|
|
|
instrumentation_begin();
|
|
panic("Unable to handle #VC exception! GHCB and Backup GHCB are already in use");
|
|
instrumentation_end();
|
|
}
|
|
|
|
/* Mark backup_ghcb active before writing to it */
|
|
data->backup_ghcb_active = true;
|
|
|
|
state->ghcb = &data->backup_ghcb;
|
|
|
|
/* Backup GHCB content */
|
|
*state->ghcb = *ghcb;
|
|
} else {
|
|
state->ghcb = NULL;
|
|
data->ghcb_active = true;
|
|
}
|
|
|
|
return ghcb;
|
|
}
|
|
|
|
noinstr void __sev_put_ghcb(struct ghcb_state *state)
|
|
{
|
|
struct sev_es_runtime_data *data;
|
|
struct ghcb *ghcb;
|
|
|
|
WARN_ON(!irqs_disabled());
|
|
|
|
if (!sev_cfg.ghcbs_initialized)
|
|
return;
|
|
|
|
data = this_cpu_read(runtime_data);
|
|
ghcb = &data->ghcb_page;
|
|
|
|
if (state->ghcb) {
|
|
/* Restore GHCB from Backup */
|
|
*ghcb = *state->ghcb;
|
|
data->backup_ghcb_active = false;
|
|
state->ghcb = NULL;
|
|
} else {
|
|
/*
|
|
* Invalidate the GHCB so a VMGEXIT instruction issued
|
|
* from userspace won't appear to be valid.
|
|
*/
|
|
vc_ghcb_invalidate(ghcb);
|
|
data->ghcb_active = false;
|
|
}
|
|
}
|