From: Matt Turner <mattst88@gmail.com>
To: qemu-devel@nongnu.org
Cc: richard.henderson@linaro.org, pbonzini@redhat.com,
philmd@mailo.com, zhao1.liu@intel.com, laurent@vivier.eu,
deller@gmx.de, pierrick.bouvier@oss.qualcomm.com,
Matt Turner <mattst88@gmail.com>
Subject: [PATCH 5/8] RFC: accel/tcg: allow cross-page goto_tb chaining in user-only builds
Date: Tue, 18 Aug 2026 13:42:44 -0400 [thread overview]
Message-ID: <20260818174247.649526-6-mattst88@gmail.com> (raw)
In-Reply-To: <20260818174247.649526-1-mattst88@gmail.com>
translator_use_goto_tb() refuses to chain unless the destination is on the
same page as the start of the TB. For guests whose text is much larger than
a page this is expensive: an emulated alpha gcc compiling a 255k line
translation unit takes the indirect dispatch path for 8.4 billion of its
34.2 billion TB exits, and a large share of those are ordinary direct
branches that simply crossed an 8 KiB page boundary.
The restriction was made unconditional by d3a2a1d803 ("accel/tcg:
Introduce translator_use_goto_tb"), whose rationale was:
Various targets avoid the page crossing test for CONFIG_USER_ONLY,
but that is wrong: mmap and mprotect can change page permissions.
That is true, but in user-only builds the invalidation path already covers
it. There are no page tables: every mmap, mprotect and munmap reaches
page_set_flags(), which calls tb_invalidate_phys_range() whenever the flags
actually change, and tb_phys_invalidate() calls tb_jmp_unlink() to reset
incoming jumps. A chained cross-page jump is therefore broken whenever the
destination page's permissions change. This is not true in system mode,
where TBs are keyed by physical address and a page table change invalidates
nothing, so the restriction is kept there.
Add tests/tcg/alpha/test-xpage-chain.c to cover the hazard directly. It
places a direct branch near the end of one page targeting the next page,
runs it 200000 times so the chain is established, then checks that
mprotect(PROT_NONE) makes the next call fault, and that remapping the page
with different code runs the new code rather than a stale translation.
The test detects the hazard it is meant to detect: with the
tb_invalidate_phys_range() call in page_set_flags() commented out, it fails
both phases, executing page B after PROT_NONE and returning the stale
result.
Measured with qemu-alpha running an emulated alpha gcc 16.2.0 compiling the
SQLite 3.45.1 amalgamation on an x86-64 host, LTO build, on top of the
preceding patches:
before: 890,713,633,237 instructions
after: 869,178,598,378 instructions -2.42%
before: 84.44s wall clock
after: 80.49s wall clock -4.68%
Note that this is worth more in time than in instructions, the reverse of
the preceding patch: a chained jump replaces a cache probe whose loads can
miss, so the instructions it removes are more expensive than average.
Measured before the inline jump cache probe, when a missed chain cost a
helper call rather than an inline probe, the same change was worth -7.9%.
RFC because this reverses a deliberate decision and the reasoning above
wants review from someone who knows the invalidation paths better than I
do.
Signed-off-by: Matt Turner <mattst88@gmail.com>
---
accel/tcg/translator.c | 12 +++-
tests/tcg/alpha/Makefile.target | 2 +-
tests/tcg/alpha/test-xpage-chain.c | 111 +++++++++++++++++++++++++++++
3 files changed, 123 insertions(+), 2 deletions(-)
create mode 100644 tests/tcg/alpha/test-xpage-chain.c
diff --git ./accel/tcg/translator.c ./accel/tcg/translator.c
index 85bb21e911..3eab9f570d 100644
--- ./accel/tcg/translator.c
+++ ./accel/tcg/translator.c
@@ -108,6 +108,17 @@ static void gen_tb_end(const TranslationBlock *tb, uint32_t cflags,
bool translator_is_same_page(const DisasContextBase *db, vaddr addr)
{
+ /*
+ * In user-only mode there are no page tables. Every mmap, mprotect and
+ * munmap goes through page_set_flags(), which calls
+ * tb_invalidate_phys_range() whenever the flags actually change, and
+ * tb_phys_invalidate() unlinks incoming jumps. A cross-page link is
+ * therefore broken whenever the destination page's permissions change,
+ * so the same-page restriction is not needed.
+ */
+ if (IS_ENABLED(CONFIG_USER_ONLY)) {
+ return true;
+ }
return ((addr ^ db->pc_first) & TARGET_PAGE_MASK) == 0;
}
@@ -118,7 +129,6 @@ bool translator_use_goto_tb(DisasContextBase *db, vaddr dest)
return false;
}
- /* Check for the dest on the same page as the start of the TB. */
return translator_is_same_page(db, dest);
}
diff --git ./tests/tcg/alpha/Makefile.target ./tests/tcg/alpha/Makefile.target
index 36d8ed1eae..eee986bab6 100644
--- ./tests/tcg/alpha/Makefile.target
+++ ./tests/tcg/alpha/Makefile.target
@@ -5,7 +5,7 @@
ALPHA_SRC=$(SRC_PATH)/tests/tcg/alpha
VPATH+=$(ALPHA_SRC)
-ALPHA_TESTS=hello-alpha test-cond test-cmov test-ovf test-cvttq
+ALPHA_TESTS=hello-alpha test-cond test-cmov test-ovf test-cvttq test-xpage-chain
TESTS+=$(ALPHA_TESTS)
test-cmov: EXTRA_CFLAGS=-DTEST_CMOV
diff --git ./tests/tcg/alpha/test-xpage-chain.c ./tests/tcg/alpha/test-xpage-chain.c
new file mode 100644
index 0000000000..7916d544af
--- /dev/null
+++ ./tests/tcg/alpha/test-xpage-chain.c
@@ -0,0 +1,111 @@
+/*
+ * Cross-page TB chaining hazard test.
+ *
+ * Phase 1: a direct branch (br) near the end of page A targets page B.
+ * Run it enough times that QEMU chains TB_A -> TB_B.
+ * Phase 2: mprotect page B away. Re-running must fault.
+ * Phase 3: remap page B with different code. Re-running must execute the
+ * NEW code, not a stale chained translation of the old code.
+ *
+ * SPDX-License-Identifier: GPL-2.0-or-later
+ */
+#include <stdio.h>
+#include <stdlib.h>
+#include <string.h>
+#include <setjmp.h>
+#include <signal.h>
+#include <sys/mman.h>
+#include <unistd.h>
+
+#define PS 8192
+
+static sigjmp_buf jb;
+/*
+ * Written by the SIGSEGV handler and read by main(), so it must not be
+ * cached in a register across the faulting call.
+ */
+static volatile sig_atomic_t caught;
+
+static void segv(int sig)
+{
+ caught = 1;
+ siglongjmp(jb, 1);
+}
+
+/* lda $0, imm($31) -> v0 = imm */
+static unsigned int lda_v0(int imm)
+{
+ return 0x201F0000u | (unsigned short)imm;
+}
+
+int main(void)
+{
+ struct sigaction sa;
+ int rc = 0;
+ unsigned char *m = mmap(NULL, 2 * PS, PROT_READ | PROT_WRITE | PROT_EXEC,
+ MAP_PRIVATE | MAP_ANONYMOUS, -1, 0);
+ if (m == MAP_FAILED) {
+ perror("mmap");
+ return 2;
+ }
+
+ unsigned char *pa = m, *pb = m + PS;
+ unsigned int *entry = (unsigned int *)(pa + PS - 64);
+ unsigned int *tgt = (unsigned int *)(pb + 16);
+
+ entry[0] = lda_v0(1);
+ long disp = ((long)tgt - ((long)&entry[1] + 4)) / 4;
+ entry[1] = 0xC3E00000u | (unsigned int)(disp & 0x1FFFFF); /* br $31,tgt */
+ tgt[0] = 0x6BFA8001u; /* ret */
+ __builtin___clear_cache((char *)m, (char *)m + 2 * PS);
+
+ long (*fn)(void) = (long (*)(void))entry;
+
+ for (int i = 0; i < 200000; i++) {
+ if (fn() != 1) {
+ printf("FAIL: phase 1 wrong result\n");
+ return 1;
+ }
+ }
+ printf("phase 1 ok (chained)\n");
+
+ memset(&sa, 0, sizeof(sa));
+ sa.sa_handler = segv;
+ sigemptyset(&sa.sa_mask);
+ if (sigaction(SIGSEGV, &sa, NULL) != 0) {
+ perror("sigaction");
+ return 2;
+ }
+ if (mprotect(pb, PS, PROT_NONE) != 0) {
+ perror("mprotect");
+ return 2;
+ }
+ if (sigsetjmp(jb, 1) == 0) {
+ fn();
+ printf("FAIL: phase 2 executed page B after mprotect(PROT_NONE)\n");
+ rc = 1;
+ } else if (!caught) {
+ printf("FAIL: phase 2 longjmp without entering the handler\n");
+ rc = 1;
+ } else {
+ printf("phase 2 ok (faulted)\n");
+ }
+
+ /* Phase 3: remap with different code, expect the new code to run. */
+ if (mprotect(pb, PS, PROT_READ | PROT_WRITE | PROT_EXEC) != 0) {
+ perror("mprotect back");
+ return 2;
+ }
+ tgt[0] = lda_v0(2);
+ tgt[1] = 0x6BFA8001u;
+ __builtin___clear_cache((char *)pb, (char *)pb + PS);
+
+ long r = fn();
+ if (r != 2) {
+ printf("FAIL: phase 3 returned %ld, expected 2 (stale chain)\n", r);
+ rc = 1;
+ } else {
+ printf("phase 3 ok (new code ran)\n");
+ }
+ return rc;
+}
--
2.54.0
next prev parent reply other threads:[~2026-08-18 17:43 UTC|newest]
Thread overview: 10+ messages / expand[flat|nested] mbox.gz Atom feed top
2026-08-18 17:42 [RFC PATCH 0/8] accel/tcg: cut per-block dispatch overhead Matt Turner
2026-08-18 17:42 ` [PATCH 1/8] accel/tcg: cache the result of curr_cflags() Matt Turner
2026-08-18 17:42 ` [PATCH 2/8] accel/tcg: enlarge the TB jump cache to 64K entries Matt Turner
2026-08-18 17:42 ` [PATCH 3/8] accel/tcg: skip the can_do_io stores in user-only builds Matt Turner
2026-08-18 17:42 ` [PATCH 4/8] RFC: tcg: probe the TB jump cache inline instead of calling a helper Matt Turner
2026-08-18 21:51 ` Pierrick Bouvier
2026-08-18 17:42 ` Matt Turner [this message]
2026-08-18 17:42 ` [PATCH 6/8] RFC: accel/tcg: only poll for interrupts in blocks that can close a cycle Matt Turner
2026-08-18 17:42 ` [PATCH 7/8] RFC: accel/tcg: poison the jump cache instead of polling for indirect exits Matt Turner
2026-08-18 17:42 ` [PATCH 8/8] RFC: tcg: fold a guest displacement into the host addressing mode Matt Turner
Reply instructions:
You may reply publicly to this message via plain-text email
using any one of the following methods:
* Save the following mbox file, import it into your mail client,
and reply-to-all from there: mbox
Avoid top-posting and favor interleaved quoting:
https://en.wikipedia.org/wiki/Posting_style#Interleaved_style
* Reply using the --to, --cc, and --in-reply-to
switches of git-send-email(1):
git send-email \
--in-reply-to=20260818174247.649526-6-mattst88@gmail.com \
--to=mattst88@gmail.com \
--cc=deller@gmx.de \
--cc=laurent@vivier.eu \
--cc=pbonzini@redhat.com \
--cc=philmd@mailo.com \
--cc=pierrick.bouvier@oss.qualcomm.com \
--cc=qemu-devel@nongnu.org \
--cc=richard.henderson@linaro.org \
--cc=zhao1.liu@intel.com \
/path/to/YOUR_REPLY
https://kernel.org/pub/software/scm/git/docs/git-send-email.html
* If your mail client supports setting the In-Reply-To header
via mailto: links, try the mailto: link
Be sure your reply has a Subject: header at the top and a blank line
before the message body.
This is an external index of several public inboxes,
see mirroring instructions on how to clone and mirror
all data and code used by this external index.