Page MenuHomeFreeBSD

bhyve: keep the reserved bits of EFLAGS on a task switch
Needs ReviewPublic

Authored by wanpengqian_gmail.com on Fri, Oct 2, 5:56 AM.
Tags
None
Referenced Files
F174483077: D60223.diff
Sat, Oct 3, 2:33 PM
F174429877: D60223.id.diff
Sat, Oct 3, 3:46 AM
F174427910: D60223.id188364.diff
Sat, Oct 3, 3:23 AM
F174422377: D60223.diff
Sat, Oct 3, 2:14 AM
Unknown Object (File)
Fri, Oct 2, 12:34 PM
Unknown Object (File)
Fri, Oct 2, 12:01 PM
Unknown Object (File)
Fri, Oct 2, 6:44 AM
Unknown Object (File)
Fri, Oct 2, 6:44 AM
Subscribers

Details

Reviewers
corvink
Group Reviewers
bhyve
Summary

The task switch emulation loaded RFLAGS with the EFLAGS image of the
new task's TSS as it was. A processor does not load the reserved bits:
bit 1 stays set and bits 3, 5, 15 and 22-31 stay clear.

Windows XP stops the other processors with an NMI when it restarts or
shuts down. Its NMI handler is a task gate and the EFLAGS image in that
task's TSS is 0, so the vCPU got RFLAGS 0x4000 (NT). The next VM entry
failed (invalid guest state, bit 1 of RFLAGS must be set) and bhyve
aborted, at every restart of a Windows XP guest with two vCPUs.

Other hypervisors force bit 1 at the same place: KVM's
load_state_from_tss32() has "ctxt->eflags = tss->eflags | 2" and Xen's
hvm_task_switch() has "regs->rflags = tss.eflags | X86_EFLAGS_MBS".

Signed-off-by: Wanpeng Qian <wanpengqian@gmail.com>
Sponsored by: keelos.dev

Test Plan

A reproducer that runs on a stock bhyve: tsstest.c (below), a kernel module for a FreeBSD/i386 guest. Loading it makes one far call to a task whose TSS has an EFLAGS image of 0, which is what the NMI task of Windows XP has. The task stores its EFLAGS and returns with IRET, and the module prints what the task saw.

main (f958aa7e7), guest: the FreeBSD 14.5-RELEASE i386 VM image, started with bhyveload. In the guest: make SYSDIR=/usr/src/sys && kldload ./tsstest.ko

Before (1 vCPU): bhyve ends at the first task switch

vm exit[0]
	reason		VMX
	rip		0x0000000015e014e0
	inst_length	7
	status		0
	exit_reason	33 (VM-entry failure due to invalid guest state)
	qualification	0x0000000000000000
	inst_type		0
	inst_error		0

After (1 and 2 vCPUs, the module loaded three times):

tsstest: the task ran, EFLAGS image 0 in its TSS, EFLAGS 0x4002 in the task: as on a processor

The same module in the same guest image on KVM (Linux 6.12, QEMU 10.0 with -accel kvm) prints the same line. (QEMU without KVM prints 0x4006: its TCG has PF set as well; bit 1 is set there too.)

How it was found: a Windows XP SP2 guest with two vCPUs, on FreeBSD 14.5 with an IDE controller model that is not in the tree (XP has no AHCI driver). Every restart or shutdown of the guest ended bhyve with the VM entry failure above; RFLAGS was 0x4000. XP stops the other processor with an NMI, its NMI vector is a task gate and the EFLAGS image in the NMI task's TSS is 0. With this change the guest restarts and shuts down.

What others do at the same point of a task switch:

  • KVM, arch/x86/kvm/emulate.c, load_state_from_tss32(): ctxt->eflags = tss->eflags | 2; (load_state_from_tss16() likewise). Its emulator also has EFLG_RESERVED_ZEROS_MASK (0xffc0802a) for the bits that are always clear.
  • Xen, xen/arch/x86/hvm/hvm.c, hvm_task_switch(): regs->rflags = tss.eflags | X86_EFLAGS_MBS;
  • QEMU (TCG), target/i386/tcg/seg_helper.c, switch_tss_ra(): cpu_load_eflags(env, new_eflags, eflags_mask) with a mask of TF, AC, ID, IF, IOPL, VM, RF and NT, so the reserved bits are never taken from the TSS.

Guests whose TSS images have bit 1 set do not show the problem, which is probably why it went unnoticed: FreeBSD/i386's double fault task has tss_eflags = PSL_KERNEL, Linux's has .flags = X86_EFLAGS_FIXED (arch/x86/kernel/doublefault_32.c).

The values are those of the SDM: Vol. 1, 3.4.3 (bits 1, 3, 5, 15 and 22-31 of EFLAGS are reserved) and Vol. 3, 27.3.1.4 (VM entry checks on RFLAGS: bit 1 must be 1, the reserved bits 0).

tsstest.c
/*
 * tsstest: a FreeBSD/i386 kernel module that calls a task whose TSS has an
 * EFLAGS image of 0, as the NMI task of Windows XP has.
 *
 * Loading the module makes one far call to the task.  The task stores its
 * EFLAGS and returns with IRET (NT is set, so that is a task switch back),
 * and the module prints what the task saw.  A processor gives 0x4002: NT
 * from the call and the reserved bit 1, which is always set.
 *
 * Under a hypervisor both task switches are emulated.  One that loads the
 * TSS image into RFLAGS as it is enters the task with 0x4000, which is not
 * a valid guest state.
 *
 *	make SYSDIR=/usr/src/sys && kldload ./tsstest.ko
 */

#include <sys/param.h>
#include <sys/systm.h>
#include <sys/kernel.h>
#include <sys/module.h>
#include <sys/pcpu.h>
#include <sys/proc.h>

#include <machine/cpufunc.h>
#include <machine/md_var.h>
#include <machine/segments.h>
#include <machine/tss.h>

#define	TSSTEST_SEL	GSEL(GNDIS_SEL, SEL_KPL)	/* a GDT slot nobody uses */
CTASSERT(TSSTEST_SEL == 0x90);			/* the lcall below */

void tsstest_task(void);
volatile u_int tsstest_eflags;
volatile u_int tsstest_ran;

static struct i386tss tsstest_tss;
static char tsstest_stack[PAGE_SIZE] __aligned(16);

__asm(
"	.text\n"
"	.globl	tsstest_task\n"
"	.type	tsstest_task, @function\n"
"tsstest_task:\n"
"	pushfl\n"
"	popl	tsstest_eflags\n"
"	movl	$1, tsstest_ran\n"
"	iret\n"
"	jmp	tsstest_task\n");

static void
tsstest_run(void)
{
	struct soft_segment_descriptor ssd;
	struct segment_descriptor *sd, saved_sd;
	struct i386tss *cur;
	register_t intr;
	u_int cr0, cur_cr3, cur_ldt;

	intr = intr_disable();

	/*
	 * A task switch does not save CR3 and LDTR in the outgoing TSS, and
	 * loads them from the incoming one: the way back needs them in the
	 * current TSS.
	 */
	cur = PCPU_GET(common_tssp);
	cur_cr3 = cur->tss_cr3;
	cur_ldt = cur->tss_ldt;
	cur->tss_cr3 = rcr3();
	cur->tss_ldt = rldt();

	bzero(&tsstest_tss, sizeof(tsstest_tss));
	tsstest_tss.tss_cr3 = rcr3();
	tsstest_tss.tss_ldt = rldt();
	tsstest_tss.tss_eip = (int)tsstest_task;
	tsstest_tss.tss_eflags = 0;		/* the point of the test */
	tsstest_tss.tss_esp = (int)tsstest_stack + sizeof(tsstest_stack);
	tsstest_tss.tss_cs = GSEL(GCODE_SEL, SEL_KPL);
	tsstest_tss.tss_ds = tsstest_tss.tss_es = tsstest_tss.tss_ss =
	    tsstest_tss.tss_gs = GSEL(GDATA_SEL, SEL_KPL);
	tsstest_tss.tss_fs = GSEL(GPRIV_SEL, SEL_KPL);
	tsstest_tss.tss_ioopt = sizeof(tsstest_tss) << 16;

	bzero(&ssd, sizeof(ssd));
	ssd.ssd_base = (int)&tsstest_tss;
	ssd.ssd_limit = sizeof(tsstest_tss) - 1;
	ssd.ssd_type = SDT_SYS386TSS;
	ssd.ssd_dpl = SEL_KPL;
	ssd.ssd_p = 1;
	sd = &gdt[PCPU_GET(cpuid) * NGDT + GNDIS_SEL].sd;
	saved_sd = *sd;
	ssdtosd(&ssd, sd);

	cr0 = rcr0();
	tsstest_ran = 0;
	tsstest_eflags = 0;

	__asm __volatile("lcall $0x90, $0");	/* TSSTEST_SEL */

	load_cr0(cr0);			/* a task switch sets CR0.TS */
	*sd = saved_sd;
	cur->tss_cr3 = cur_cr3;
	cur->tss_ldt = cur_ldt;

	intr_restore(intr);

	printf("tsstest: the task %s, EFLAGS image 0 in its TSS, EFLAGS %#x "
	    "in the task: %s\n", tsstest_ran ? "ran" : "did not run",
	    tsstest_eflags, tsstest_eflags == (PSL_NT | PSL_RESERVED_DEFAULT) ?
	    "as on a processor" : "NOT what a processor gives (0x4002)");
}

static int
tsstest_modevent(module_t mod, int type, void *arg)
{

	switch (type) {
	case MOD_LOAD:
		tsstest_run();
		return (0);
	case MOD_UNLOAD:
		return (0);
	default:
		return (EOPNOTSUPP);
	}
}

static moduledata_t tsstest_mod = {
	"tsstest",
	tsstest_modevent,
	NULL
};
DECLARE_MODULE(tsstest, tsstest_mod, SI_SUB_DRIVERS, SI_ORDER_ANY);
Makefile
KMOD=	tsstest
SRCS=	tsstest.c

.include <bsd.kmod.mk>

Diff Detail

Repository
rG FreeBSD src repository
Lint
Lint Skipped
Unit
Tests Skipped
Build Status
Buildable 77599
Build 74482: arc lint + arc unit