From: David Woodhouse <[email protected]> When a higher VTL arms the ldtr_write or tr_write bits of HvRegisterCrInterceptControl, hv_divert_dt_write() covers GDTR and IDTR writes, but an LTR in the lower VTL retires natively, and the LDTR divert sat in emulator_set_segment(), which task-switch emulation also reaches via __load_segment_descriptor(). A protected-mode task switch with the intercept armed — reachable by a malicious lower VTL, even though 64-bit Windows takes no task switches — had its LDTR load silently dropped, and the higher VTL received a bogus mid-task-switch intercept carrying the wrong RIP and instruction length. The TLFS intercepts cover the LLDT and LTR instructions only.
Add a divert_seg_write(ctxt, selector, seg) emulator op and call it from em_lldt() and em_ltr() before load_segment_descriptor(). Diverting from the instruction handlers, before any architectural effect, keeps hardware task switches out of the intercept; for LTR it also matters that __load_segment_descriptor() marks the TSS descriptor busy in the GDT first, and a busy descriptor would #GP the LTR when it re-executes after a cooperative VTL return. emulator_set_segment() no longer diverts anything. No vmx change is needed: SECONDARY_EXEC_DESC already exits on LTR (handle_desc covers EXIT_REASON_LDTR_TR) whenever any reg_intercept_control bit is set. Signed-off-by: David Woodhouse <[email protected]> Assisted-by: LLM --- arch/x86/kvm/emulate.c | 21 +++++++++++++++++++++ arch/x86/kvm/kvm_emulate.h | 10 ++++++++++ arch/x86/kvm/x86.c | 33 +++++++++++++++++++++------------ 3 files changed, 52 insertions(+), 12 deletions(-) diff --git a/arch/x86/kvm/emulate.c b/arch/x86/kvm/emulate.c index 8013dccb3110..86d165b8185c 100644 --- a/arch/x86/kvm/emulate.c +++ b/arch/x86/kvm/emulate.c @@ -3387,6 +3387,16 @@ static int em_lldt(struct x86_emulate_ctxt *ctxt) /* Disable writeback. */ ctxt->dst.type = OP_NONE; + + /* + * A higher VTL's LDTR-write intercept suspends the LLDT before any + * architectural effect. Diverting here rather than in + * emulator_set_segment() keeps task switches, which load LDTR + * through set_segment as well, out of the intercept. + */ + if (ctxt->ops->divert_seg_write(ctxt, sel, VCPU_SREG_LDTR)) + return X86EMUL_CONTINUE; + return load_segment_descriptor(ctxt, sel, VCPU_SREG_LDTR); } @@ -3401,6 +3411,17 @@ static int em_ltr(struct x86_emulate_ctxt *ctxt) /* Disable writeback. */ ctxt->dst.type = OP_NONE; + + /* + * A higher VTL's TR-write intercept suspends the LTR before any + * architectural effect. The divert must precede + * load_segment_descriptor(): that path marks the TSS descriptor + * busy in the GDT, and a busy descriptor would #GP the re-executed + * LTR after a cooperative VTL return. + */ + if (ctxt->ops->divert_seg_write(ctxt, sel, VCPU_SREG_TR)) + return X86EMUL_CONTINUE; + return load_segment_descriptor(ctxt, sel, VCPU_SREG_TR); } diff --git a/arch/x86/kvm/kvm_emulate.h b/arch/x86/kvm/kvm_emulate.h index 0abff36d0994..8a3b9a40934b 100644 --- a/arch/x86/kvm/kvm_emulate.h +++ b/arch/x86/kvm/kvm_emulate.h @@ -202,6 +202,16 @@ struct x86_emulate_ops { struct desc_struct *desc, u32 *base3, int seg); void (*set_segment)(struct x86_emulate_ctxt *ctxt, u16 selector, struct desc_struct *desc, u32 base3, int seg); + /* + * Divert an LTR or LLDT into a Hyper-V VSM register-intercept exit + * when the higher VTL armed one. Returns true when the write was + * diverted: the instruction must then have no architectural effect, + * so this is checked before descriptor validation (and, for LTR, + * the busy-bit write). Only the instructions divert; task-switch + * loads of LDTR/TR are not covered by the TLFS intercepts. + */ + bool (*divert_seg_write)(struct x86_emulate_ctxt *ctxt, u16 selector, + int seg); unsigned long (*get_cached_segment_base)(struct x86_emulate_ctxt *ctxt, int seg); void (*get_gdt)(struct x86_emulate_ctxt *ctxt, struct desc_ptr *dt); diff --git a/arch/x86/kvm/x86.c b/arch/x86/kvm/x86.c index 052821799120..c5122e6f42bd 100644 --- a/arch/x86/kvm/x86.c +++ b/arch/x86/kvm/x86.c @@ -8859,9 +8859,11 @@ static bool hv_divert_dt_write(struct x86_emulate_ctxt *ctxt, u32 hv_reg, #define HV_CR_INTERCEPT_GDTR_WRITE BIT_ULL(15) #define HV_CR_INTERCEPT_IDTR_WRITE BIT_ULL(16) #define HV_CR_INTERCEPT_LDTR_WRITE BIT_ULL(17) +#define HV_CR_INTERCEPT_TR_WRITE BIT_ULL(18) #define HV_X64_REGISTER_IDTR_ID 0x00070000 #define HV_X64_REGISTER_GDTR_ID 0x00070001 #define HV_X64_REGISTER_LDTR_ID 0x00060006 +#define HV_X64_REGISTER_TR_ID 0x00060007 static void emulator_set_gdt(struct x86_emulate_ctxt *ctxt, struct desc_ptr *dt) { @@ -8949,22 +8951,28 @@ static void emulator_set_segment(struct x86_emulate_ctxt *ctxt, u16 selector, var.unusable = !var.present; var.padding = 0; - if (seg == VCPU_SREG_LDTR) { - struct desc_ptr sel_as_dt = { - .address = selector, - .size = 0, - }; - - /* The intercept message carries the written selector. */ - if (hv_divert_dt_write(ctxt, HV_X64_REGISTER_LDTR_ID, - &sel_as_dt, HV_CR_INTERCEPT_LDTR_WRITE)) - return; - } - kvm_set_segment(vcpu, &var, seg); return; } +static bool emulator_divert_seg_write(struct x86_emulate_ctxt *ctxt, + u16 selector, int seg) +{ + struct desc_ptr sel_as_dt = { + .address = selector, + .size = 0, + }; + + /* The intercept message carries the written selector. */ + if (seg == VCPU_SREG_LDTR) + return hv_divert_dt_write(ctxt, HV_X64_REGISTER_LDTR_ID, + &sel_as_dt, + HV_CR_INTERCEPT_LDTR_WRITE); + + return hv_divert_dt_write(ctxt, HV_X64_REGISTER_TR_ID, &sel_as_dt, + HV_CR_INTERCEPT_TR_WRITE); +} + static int emulator_get_msr_with_filter(struct x86_emulate_ctxt *ctxt, u32 msr_index, u64 *pdata) { @@ -9167,6 +9175,7 @@ static const struct x86_emulate_ops emulate_ops = { .pio_out_emulated = emulator_pio_out_emulated, .get_segment = emulator_get_segment, .set_segment = emulator_set_segment, + .divert_seg_write = emulator_divert_seg_write, .get_cached_segment_base = emulator_get_cached_segment_base, .get_gdt = emulator_get_gdt, .get_idt = emulator_get_idt, -- 2.50.1

