diff --git a/lib/x86/desc.h b/lib/x86/desc.h
index be52fd4..17c7960 100644
--- a/lib/x86/desc.h
+++ b/lib/x86/desc.h
@@ -142,6 +142,7 @@ void set_intr_task_gate(int vec, void *fn);
 void setup_tss32(void);
 #else
 extern tss64_t tss;
+extern gdt_entry_t gdt64[];
 #endif
 
 unsigned exception_vector(void);
diff --git a/lib/x86/processor.h b/lib/x86/processor.h
index ee7f180..6cb7937 100644
--- a/lib/x86/processor.h
+++ b/lib/x86/processor.h
@@ -46,6 +46,11 @@ struct descriptor_table_ptr {
     ulong base;
 } __attribute__((packed));
 
+struct far_pointer32 {
+    u32 offset;
+    u16 selector;
+} __attribute__((packed));
+
 static inline void barrier(void)
 {
     asm volatile ("" : : : "memory");
diff --git a/x86/cstart64.S b/x86/cstart64.S
index e947888..8bb7af3 100644
--- a/x86/cstart64.S
+++ b/x86/cstart64.S
@@ -7,6 +7,7 @@ boot_idt = 0
 .globl idt_descr
 .globl tss_descr
 .globl gdt64_desc
+.globl gdt64
 
 ipi_vector = 0x20
 
diff --git a/x86/emulator.c b/x86/emulator.c
index 8d262d8..ca7dc37 100644
--- a/x86/emulator.c
+++ b/x86/emulator.c
@@ -768,6 +768,32 @@ asm(
 	".align 4096\n\t"
 );
 
+asm(
+	".align 4096\n\t"
+	"compat_insn_page:\n\t"
+	"ret\n\t"
+	"pushf\n\t"
+	"push 136+save\n\t"
+	"popf\n\t"
+	INSN_XCHG_ALL
+	"ljmp *(%rsp)\n\t"
+	".code32\n\t"
+	"compat_test_insn:\n\t"
+	"in (%dx), %al\n\t"
+	".skip 31, 0x90\n\t"
+	"compat_test_insn_end:\n\t"
+	"ljmp $" xstr(KERNEL_CS) ", $64f\n\t"
+	".code64\n\t"
+	"64:\n\t"
+	INSN_XCHG_ALL
+	"pushf\n\t"
+	"pop 136+save\n\t"
+	"popf\n\t"
+	"ret\n\t"
+	"compat_insn_page_end:\n\t"
+	".align 4096\n\t"
+);
+
 #define MK_INSN(name, str)				\
     asm (						\
 	 ".pushsection .data.insn  \n\t"		\
@@ -810,6 +836,48 @@ static void trap_emulator(uint64_t *mem, void *alt_insn_page,
 	outregs = save;
 }
 
+/*
+ * Trigger emulation of a compatibility mode instruction. Note that this
+ * function requires inregs.rsp to be a valid stack address with room for
+ * a 32-bit far-pointer to be pushed.
+ */
+static void trap_emulator32(uint64_t *mem, void *alt_insn_page,
+			    struct insn_desc *alt_insn)
+{
+	extern u8 compat_insn_page[], compat_test_insn[];
+	ulong *cr3 = (ulong *)read_cr3();
+	struct far_pointer32 compat_fp = {
+		.offset = (uintptr_t)compat_test_insn,
+		.selector = KERNEL_CS32,
+	};
+	void *insn_ram;
+
+	insn_ram = compat_insn_page;
+	memcpy(alt_insn_page, compat_insn_page, 4096);
+	memcpy(alt_insn_page + (compat_test_insn - compat_insn_page),
+	       (void *)(alt_insn->ptr), alt_insn->len);
+	save = inregs;
+	save.rsp -= ((sizeof(compat_fp) + 15) / 16) * 16;
+	memcpy((void *)save.rsp, &compat_fp, sizeof(compat_fp));
+
+	/*
+	 * Load the code TLB with compat_insn_page, but point the page
+	 * tables at alt_insn_page (and keep the data TLB clear, for
+	 * AMD decode assist).  This will make the CPU trap on the
+	 * compat_insn_page instruction but the hypervisor will see
+	 * alt_insn_page.
+	 */
+	install_page(cr3, virt_to_phys(compat_insn_page), insn_ram);
+	invlpg(insn_ram);
+	/* Load code TLB */
+	asm volatile("call *%0" : : "r" (insn_ram));
+	install_page(cr3, virt_to_phys(alt_insn_page), insn_ram);
+	/* Trap, let hypervisor emulate at alt_insn_page */
+	asm volatile("call *%0" : : "r" (insn_ram + 1));
+
+	outregs = save;
+}
+
 static unsigned long rip_advance;
 
 static void advance_rip_and_note_exception(struct ex_regs *regs)
@@ -855,6 +923,47 @@ static void test_jmp_noncanonical(uint64_t *mem)
 	handle_exception(GP_VECTOR, 0);
 }
 
+u16 exception_cs;
+
+static void record_cs_and_advance_rip(struct ex_regs *regs)
+{
+	exception_cs = regs->cs;
+	regs->rip += rip_advance;
+}
+
+/*
+ * This test ensures that emulation of 32-bit far jmp works properly
+ * when the far jmp raises #GP because the offset in the target operand
+ * is beyond the limit of the code segment in the target operand.
+ */
+static void test_jmpfar_32bitgp(uint64_t *mem, uint8_t *insn_page,
+				uint8_t *alt_insn_page, void *insn_ram)
+{
+	void *stack = alloc_page();
+	gdt_entry_t *spare = gdt64 + (FIRST_SPARE_SEL >> 3);
+	struct far_pointer32 spare_fp = {
+		.offset = 0x1000,
+		.selector = FIRST_SPARE_SEL,
+	};
+	/*
+	 * Set up a 32-bit code segment with a single-page limit.
+	 */
+	*spare = gdt64[KERNEL_CS32 >> 3];
+	spare->limit_low = 0;
+	spare->granularity &= ~0xf;
+
+	handle_exception(GP_VECTOR, record_cs_and_advance_rip);
+	MK_INSN(jmp_far32, ".code32; ljmp *(%edx); .code64");
+	exception_cs = -1;
+	rip_advance = insn_jmp_far32.len;
+	inregs = (struct regs){ .rdx = (uintptr_t)&spare_fp,
+				.rsp = (uintptr_t)stack + PAGE_SIZE,
+			      };
+	trap_emulator32(mem, alt_insn_page, &insn_jmp_far32);
+	report("32-bit far jmp #GP preserves CS", exception_cs == KERNEL_CS32);
+	handle_exception(GP_VECTOR, 0);
+}
+
 static void test_movabs(uint64_t *mem, uint8_t *insn_page,
 		       uint8_t *alt_insn_page, void *insn_ram)
 {
@@ -1162,6 +1271,7 @@ int main()
 	test_string_io_mmio(mem);
 
 	test_jmp_noncanonical(mem);
+	test_jmpfar_32bitgp(mem, insn_page, alt_insn_page, insn_ram);
 	test_illegal_movbe();
 
 	return report_summary();
