[PATCH] target/i386/tcg: fix EIP truncation for wrapping 16-bit near branches

"Paul Galbraith" <[email protected]>
Newsgroups org.nongnu.qemu-devel
Message-ID <[email protected]>
gen_jmp_rel() decided whether goto_tb chaining was safe by checking if
the branch target's *linear* address stayed on the same guest page as
the translation block, then treated that as proof the EIP addition
could not wrap. For a 16-bit branch (MO_16) that reasoning does not
hold: the wrap boundary is at 0x10000 in EIP, not in the linear
address, so two linear addresses can share a page while the
corresponding EIP values straddle 0x10000. On the CF_PCREL path this
let a wrapping CALL/JMP rel16 execute with EIP left at its raw,
untruncated value whenever the unwrapped target happened to land on
the same page as the branch itself.

Require additionally that cs_base be page aligned before allowing
goto_tb: then the EIP wrap boundary (mask + 1) maps to a page-aligned
linear address, crossing it would cross a page boundary, and the
same-page test genuinely excludes the wrap. A translation-time check
of the computed EIP would not be sound here, because a CF_PCREL TB is
matched by physical address and cs_base -- not virtual PC -- and may
later run at a different linear address, shifting EIP by a multiple
of the page size; the alignment of cs_base, by contrast, is part of
the TB key and stable across reuse. This also subsumes the previous
CODE32-only special case, which forced chaining off unconditionally
for every data16 branch in a 32-bit segment. 64-bit mode is unchanged
(cs_base is 0 there).

Adds a regression test that installs a 16-bit code segment at a
deliberately non-page-aligned base and runs the same wrapping jmp
rel16 from two sites 48 bytes apart -- one whose translation block
starts on a different page than the unwrapped target (must already
pass) and one on the same page (where the defect manifested).

Signed-off-by: Paul Galbraith <[email protected]>
---
 target/i386/tcg/translate.c                |  18 +-
 tests/tcg/i386/Makefile.softmmu-target     |   6 +-
 tests/tcg/i386/system/test-branch-wrap16.c | 221 +++++++++++++++++++++
 3 files changed, 237 insertions(+), 8 deletions(-)
 create mode 100644 tests/tcg/i386/system/test-branch-wrap16.c

diff --git a/target/i386/tcg/translate.c b/target/i386/tcg/translate.c
index 2115c5cd24..c9755d28dd 100644
--- a/target/i386/tcg/translate.c
+++ b/target/i386/tcg/translate.c
@@ -2027,9 +2027,6 @@ static void gen_jmp_rel(DisasContext *s, MemOp ot, int diff, int tb_num)
     if (!CODE64(s)) {
         if (ot == MO_16) {
             mask = 0xffff;
-            if (tb_cflags(s->base.tb) & CF_PCREL && CODE32(s)) {
-                use_goto_tb = false;
-            }
         } else {
             mask = 0xffffffff;
         }
@@ -2039,11 +2036,18 @@ static void gen_jmp_rel(DisasContext *s, MemOp ot, int diff, int tb_num)
     if (tb_cflags(s->base.tb) & CF_PCREL) {
         tcg_gen_addi_tl(cpu_eip, cpu_eip, new_pc - s->pc_save);
         /*
-         * If we can prove the branch does not leave the page and we have
-         * no extra masking to apply (data16 branch in code32, see above),
-         * then we have also proven that the addition does not wrap.
+         * The same-page test proves that the addition does not wrap EIP
+         * only if the wrap boundary (mask + 1) corresponds to a
+         * page-aligned linear address, i.e. only if cs_base is page
+         * aligned: then crossing the boundary would cross a page
+         * boundary, which the test excludes.  This must not be relaxed
+         * to a translation-time check of EIP itself, because a CF_PCREL
+         * TB may later run at a different linear address, shifting EIP
+         * by a multiple of the page size.
          */
-        if (!use_goto_tb || !translator_is_same_page(&s->base, new_pc)) {
+        if (!use_goto_tb
+            || (s->cs_base & ~TARGET_PAGE_MASK) != 0
+            || !translator_is_same_page(&s->base, new_pc)) {
             tcg_gen_andi_tl(cpu_eip, cpu_eip, mask);
             use_goto_tb = false;
         }
diff --git a/tests/tcg/i386/Makefile.softmmu-target b/tests/tcg/i386/Makefile.softmmu-target
index 4096a1cf31..16d3473fb6 100644
--- a/tests/tcg/i386/Makefile.softmmu-target
+++ b/tests/tcg/i386/Makefile.softmmu-target
@@ -18,7 +18,11 @@ LDFLAGS=-Wl,-T$(LINK_SCRIPT) -Wl,-melf_i386
 CFLAGS+=-nostdlib -ggdb -O0 $(MINILIB_INC)
 LDFLAGS+=-static -nostdlib $(CRT_OBJS) $(MINILIB_OBJS) -lgcc
 
-TESTS+=$(MULTIARCH_TESTS)
+I386_SYSTEM_TEST_SRCS=$(wildcard $(I386_SYSTEM_SRC)/*.c)
+I386_SYSTEM_TESTS=$(patsubst $(I386_SYSTEM_SRC)/%.c,%,$(I386_SYSTEM_TEST_SRCS))
+VPATH+=$(I386_SYSTEM_SRC)
+
+TESTS+=$(MULTIARCH_TESTS) $(I386_SYSTEM_TESTS)
 EXTRA_RUNS+=$(MULTIARCH_RUNS)
 
 # building head blobs
diff --git a/tests/tcg/i386/system/test-branch-wrap16.c b/tests/tcg/i386/system/test-branch-wrap16.c
new file mode 100644
index 0000000000..76839895f8
--- /dev/null
+++ b/tests/tcg/i386/system/test-branch-wrap16.c
@@ -0,0 +1,221 @@
+/*
+ * Regression test: a 16-bit near JMP rel16 whose target wraps EIP through
+ * 0xFFFF must land at the truncated target, not at the unwrapped one.
+ *
+ * gen_jmp_rel() in target/i386/tcg/translate.c decides whether it is safe
+ * to chain translation blocks (goto_tb) by checking whether the *linear*
+ * address of the branch target lands on the same guest page as the start
+ * of the current translation block. For a 16-bit branch (MO_16) the wrap
+ * boundary is at 0x10000 in EIP, not in the linear address, so passing
+ * that same-page test does not prove the EIP addition did not wrap: two
+ * linear addresses can share a page while the corresponding EIP values
+ * straddle 0x10000. When that happens on the CF_PCREL path (system-mode
+ * TCG, i.e. here, not linux-user, which is why this is not alongside
+ * test-i386-code16.S) the masking AND is skipped and EIP is left with
+ * the raw, unwrapped, out-of-range value.
+ *
+ * This only shows up for a branch site whose *own* linear address and
+ * whose *unwrapped* target's linear address fall on the same page, which
+ * additionally requires cs_base not to be 4 KiB-aligned (with an aligned
+ * cs_base the 0x10000 wrap in EIP always coincides with a page boundary
+ * in the linear address, and the same-page test can never be fooled).
+ *
+ * The test below runs the identical wrapping JMP rel16 from two sites 48
+ * bytes apart in a purpose-built 16-bit code segment:
+ *
+ *   A (control): TB starts on a different page than the unwrapped target
+ *                -> masked correctly even before the fix -> must pass.
+ *   B (regression check): TB starts on the SAME page as the unwrapped
+ *                target -> this is exactly where the defect manifested.
+ *
+ * Both land on a small hand-encoded stub that records where it arrived
+ * (via a 32-bit-addressed store, using DS which is still the flat data
+ * segment throughout) and far-jumps back into 32-bit code to report it.
+ */
+
+#include <stdint.h>
+#include <minilib.h>
+
+struct gdt_desc {
+    uint16_t limit_lo;
+    uint16_t base_lo;
+    uint8_t  base_mid;
+    uint8_t  access;
+    uint8_t  limit_hi_flags;
+    uint8_t  base_hi;
+};
+
+struct gdtr {
+    uint16_t limit;
+    uint32_t base;
+} __attribute__((packed));
+
+/*
+ * Selectors 0x08/0x10 must describe exactly the same flat code/data
+ * segments boot.S already loaded into CS/DS/ES/SS/FS/GS: those registers
+ * are never reloaded here, so their cached (shadow) descriptor state has
+ * to remain valid against this replacement table.
+ */
+#define SEL_CODE32 0x08
+#define SEL_DATA32 0x10
+#define SEL_CODE16 0x18
+
+static struct gdt_desc test_gdt[4];
+static struct gdtr test_gdtr;
+
+static void set_desc(struct gdt_desc *d, uint32_t base, uint32_t limit,
+                      uint8_t access, uint8_t gran)
+{
+    d->limit_lo = limit & 0xffff;
+    d->base_lo = base & 0xffff;
+    d->base_mid = (base >> 16) & 0xff;
+    d->access = access;
+    d->limit_hi_flags = ((limit >> 16) & 0x0f) | (gran & 0xf0);
+    d->base_hi = (base >> 24) & 0xff;
+}
+
+/*
+ * A page (4 KiB) of headroom, rounded up to at runtime, so that
+ * cs_base = page_base(arena) + 0x10 is guaranteed NOT 4 KiB-aligned
+ * (cs_base & 0xfff == 0x10) while every other bit of cs_base's low 16
+ * bits is otherwise known exactly relative to a 4 KiB page boundary.
+ * That is what makes the branch-site/target page relationship below
+ * fall out of plain arithmetic instead of luck.
+ *
+ * This is computed at runtime rather than via an
+ * __attribute__((aligned(0x10000))) buffer deliberately: boot.S's
+ * multiboot header uses the AOUT-kludge (flags bit 0x10000), which
+ * assumes a single constant "file offset -> load address" mapping
+ * across the whole image. A 64 KiB-aligned .bss object makes the
+ * linker widen that segment's own file alignment past 4 KiB, which
+ * breaks that assumption and corrupts the load. Ordinary alignment on
+ * the buffer plus a runtime round-up avoids perturbing the segment
+ * layout entirely.
+ *
+ * No paging is set up by boot.S, so linear == physical here and a
+ * pointer into arena is directly usable as a linear address.
+ */
+#define ARENA_SIZE (0x11000 + 0x1000)
+static uint8_t arena[ARENA_SIZE] __attribute__((aligned(16)));
+
+/*
+ * Geometry (base = page-aligned-up(arena), cs_base = base + 0x10):
+ *
+ *   branch A  at IP 0xFFC0 -> linear base+0xFFD0, page base+0xF000
+ *   branch B  at IP 0xFFF0 -> linear base+0x10000, page base+0x10000
+ *   both target IP 0x0020  -> linear base+0x10030 (masked, correct)
+ *   unwrapped EIP for both is 0x10020 -> linear base+0x10030 (untruncated)
+ *
+ * A: unwrapped target's page (base+0x10000) differs from A's own TB
+ *    page (base+0xF000) -> gen_jmp_rel() emits the AND -> correct.
+ * B: unwrapped target's page (base+0x10000) is the SAME as B's own TB
+ *    page (base+0x10000) -> the AND used to be skipped -> defect.
+ */
+#define BRANCH_A_IP 0xFFC0u
+#define BRANCH_B_IP 0xFFF0u
+#define TARGET_IP   0x0020u
+#define ARENA_OFF(ip) (0x10u + (ip))
+
+static uint8_t *base;
+
+/* rel16 for "jmp" (3-byte instruction), measured from the next IP */
+#define A_REL ((uint16_t)((0x10000u + TARGET_IP) - (BRANCH_A_IP + 3u)))
+#define B_REL ((uint16_t)((0x10000u + TARGET_IP) - (BRANCH_B_IP + 3u)))
+
+#define RESULT_NONE  0u
+#define RESULT_RIGHT 1u
+#define RESULT_WRONG 2u
+static volatile uint32_t g_result;
+
+static void write_branch(uint32_t off, uint16_t rel)
+{
+    base[off + 0] = 0xE9;              /* jmp rel16 */
+    base[off + 1] = rel & 0xff;
+    base[off + 2] = (rel >> 8) & 0xff;
+}
+
+/*
+ * A landing stub: "mov byte [addr32], marker" (address-size override,
+ * since the default in a 16-bit code segment is 16-bit addressing) then
+ * a far jump back to 32-bit flat code ("data32 ljmp $sel, $off32").
+ */
+static void write_stub(uint32_t off, uint8_t marker, uint32_t ret_addr)
+{
+    uint32_t addr = (uint32_t)&g_result;
+    uint8_t *p = &base[off];
+    int i = 0;
+
+    p[i++] = 0x67;                      /* address-size override */
+    p[i++] = 0xC6;                      /* MOV r/m8, imm8 */
+    p[i++] = 0x05;                      /* modrm: disp32, no base */
+    p[i++] = addr & 0xff;
+    p[i++] = (addr >> 8) & 0xff;
+    p[i++] = (addr >> 16) & 0xff;
+    p[i++] = (addr >> 24) & 0xff;
+    p[i++] = marker;
+
+    p[i++] = 0x66;                      /* operand-size override */
+    p[i++] = 0xEA;                      /* JMP ptr16:32 (direct far jump) */
+    p[i++] = ret_addr & 0xff;
+    p[i++] = (ret_addr >> 8) & 0xff;
+    p[i++] = (ret_addr >> 16) & 0xff;
+    p[i++] = (ret_addr >> 24) & 0xff;
+    p[i++] = SEL_CODE32 & 0xff;
+    p[i++] = (SEL_CODE32 >> 8) & 0xff;
+}
+
+static void write_stubs(uint32_t ret_addr)
+{
+    write_stub(ARENA_OFF(TARGET_IP), RESULT_RIGHT, ret_addr);
+    write_stub(ARENA_OFF(TARGET_IP + 0x10000u), RESULT_WRONG, ret_addr);
+}
+
+int main(void)
+{
+    uint32_t cs_base;
+
+    base = (uint8_t *)(((uint32_t)arena + 0xFFFu) & ~0xFFFu);
+    cs_base = (uint32_t)base + 0x10;
+
+    /* entries 0/1/2 mirror boot.S's null/code32/data32 descriptors */
+    set_desc(&test_gdt[0], 0, 0, 0, 0);
+    set_desc(&test_gdt[1], 0, 0xFFFFF, 0x9b, 0xC0);
+    set_desc(&test_gdt[2], 0, 0xFFFFF, 0x93, 0xC0);
+    /* a genuine 16-bit code segment: G=0, D/B=0, byte-granular 64K limit */
+    set_desc(&test_gdt[3], cs_base, 0xFFFF, 0x9b, 0x00);
+
+    test_gdtr.limit = sizeof(test_gdt) - 1;
+    test_gdtr.base = (uint32_t)&test_gdt;
+
+    write_branch(ARENA_OFF(BRANCH_A_IP), A_REL);
+    write_branch(ARENA_OFF(BRANCH_B_IP), B_REL);
+
+    asm volatile("lgdt %0" : : "m"(test_gdtr) : "memory");
+
+    /* -- A: control. Must already pass before the fix. -- */
+    write_stubs((uint32_t)&&L_return_a);
+    g_result = RESULT_NONE;
+    asm volatile("ljmpl $0x18, $0xFFC0" : : : "memory");
+L_return_a:
+    if (g_result != RESULT_RIGHT) {
+        ml_printf("FAIL: control branch A landed wrong (result=%d)\n",
+                   (int)g_result);
+        return 1;
+    }
+    ml_printf("A (control, different page): landed correctly\n");
+
+    /* -- B: the regression check. -- */
+    write_stubs((uint32_t)&&L_return_b);
+    g_result = RESULT_NONE;
+    asm volatile("ljmpl $0x18, $0xFFF0" : : : "memory");
+L_return_b:
+    if (g_result != RESULT_RIGHT) {
+        ml_printf("FAIL: branch wraparound left EIP untruncated "
+                   "(result=%d)\n", (int)g_result);
+        return 1;
+    }
+    ml_printf("B (same page as unwrapped target): landed correctly\n");
+
+    ml_printf("PASS\n");
+    return 0;
+}
-- 
2.55.0.windows.3
lmpx.com only provides a reader for public news (NNTP) servers. It is not affiliated with the servers or forums shown here and is not responsible for the content of articles, which is written by their respective authors.