[RFC PATCH 29/55] KVM: x86: hyperv: divert LLDT and LTR into register intercepts

From: David Woodhouse

Date: Sat Oct 17 2026 - 08:50:31 EST


From: David Woodhouse <dwmw@xxxxxxxxxxxx>

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 <dwmw@xxxxxxxxxxxx>
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