tests/tcg: add tests for qemu_plugin_set_pc API
The test plugin intercepts execution in different contexts. Without the plugin, any of the implemented test functions would trigger an assert and fail. With the plugin, control flow is redirected to skip the assert and return cleanly via the qemu_plugin_set_pc() API. Signed-off-by: Florian Hofhammer <florian.hofhammer@epfl.ch> Reviewed-by: Pierrick Bouvier <pierrick.bouvier@linaro.org> Link: https://lore.kernel.org/qemu-devel/20260305-setpc-v5-v7-5-4c3adba52403@epfl.ch Signed-off-by: Pierrick Bouvier <pierrick.bouvier@linaro.org>
Florian Hofhammer committed
Mar 5, 2026 at 11:06 UTC
0b03c73c997a57dbdb9892794c2ad3fd8ef72295
9 files changed
+269
-3
MAINTAINERS
+1
@@ -4104,6 +4104,7 @@ S: Maintained
4104
F: docs/devel/tcg-plugins.rst
4105
F: plugins/
4106
F: tests/tcg/plugins/
4107
+F: tests/tcg/multiarch/plugin/
4108
F: tests/functional/aarch64/test_tcg_plugins.py
4109
F: contrib/plugins/
4110
F: scripts/qemu-plugin-symbols.py
tests/tcg/arm/Makefile.target
+6
@@ -78,4 +78,10 @@ sha512-vector: sha512.c
78
79
ARM_TESTS += sha512-vector
80
81
+ifeq ($(CONFIG_PLUGIN),y)
82
+# Require emitting arm32 instructions, otherwise the vCPU might accidentally
83
+# try to execute Thumb instructions in arm32 mode after qemu_plugin_set_pc()
84
+test-plugin-set-pc: CFLAGS+=-marm
85
+endif
86
+
87
TESTS += $(ARM_TESTS)
tests/tcg/hexagon/Makefile.target
+8
@@ -126,3 +126,11 @@ v73_scalar: CFLAGS += -Wno-unused-function
126
127
hvx_histogram: hvx_histogram.c hvx_histogram_row.S
128
$(CC) $(CFLAGS) $(CROSS_CC_GUEST_CFLAGS) $^ -o $@ $(LDFLAGS)
129
+
130
+ifeq ($(CONFIG_PLUGIN),y)
131
+# LLVM is way too aggressive with inlining and dead code elimination even at
132
+# -O0, which interferes with the test. What looks like dead code in this test
133
+# to the compiler isn't actually dead code, so we need to disable all potential
134
+# LLVM optimization passes.
135
+test-plugin-set-pc: CFLAGS += -Xclang -disable-llvm-passes
136
+endif
tests/tcg/multiarch/Makefile.target
+14
-3
@@ -14,6 +14,10 @@ ifeq ($(filter %-linux-user, $(TARGET)),$(TARGET))
14
VPATH += $(MULTIARCH_SRC)/linux
15
MULTIARCH_SRCS += $(notdir $(wildcard $(MULTIARCH_SRC)/linux/*.c))
16
endif
17
+ifeq ($(CONFIG_PLUGIN),y)
18
+VPATH += $(MULTIARCH_SRC)/plugin
19
+MULTIARCH_SRCS += $(notdir $(wildcard $(MULTIARCH_SRC)/plugin/*.c))
20
+endif
21
MULTIARCH_TESTS = $(MULTIARCH_SRCS:.c=)
22
23
#
@@ -200,13 +204,20 @@ run-plugin-test-plugin-mem-access-with-libmem.so: \
204
PLUGIN_ARGS=$(COMMA)print-accesses=true
205
run-plugin-test-plugin-mem-access-with-libmem.so: \
206
CHECK_PLUGIN_OUTPUT_COMMAND= \
203
- $(SRC_PATH)/tests/tcg/multiarch/check-plugin-output.sh \
207
+ $(SRC_PATH)/tests/tcg/multiarch/plugin/check-plugin-output.sh \
208
$(QEMU) $<
209
run-plugin-test-plugin-syscall-filter-with-libsyscall.so:
210
+run-plugin-test-plugin-set-pc-with-libsetpc.so:
211
212
EXTRA_RUNS_WITH_PLUGIN += run-plugin-test-plugin-mem-access-with-libmem.so \
208
- run-plugin-test-plugin-syscall-filter-with-libsyscall.so
209
-else
213
+ run-plugin-test-plugin-syscall-filter-with-libsyscall.so \
214
+ run-plugin-test-plugin-set-pc-with-libsetpc.so
215
+
216
+else # CONFIG_PLUGIN=n
217
+# Do not build the syscall skipping test if it's not tested with the setpc
218
+# plugin because it will simply fail the test.
219
+MULTIARCH_TESTS := $(filter-out test-plugin-set-pc, $(MULTIARCH_TESTS))
220
+
221
# test-plugin-syscall-filter needs syscall plugin to succeed
222
test-plugin-syscall-filter: CFLAGS+=-DSKIP
223
endif
tests/tcg/multiarch/plugin/check-plugin-output.sh
renamed
tests/tcg/multiarch/plugin/test-plugin-mem-access.c
renamed
tests/tcg/multiarch/plugin/test-plugin-set-pc.c
new
+134
@@ -0,0 +1,134 @@
1
+/*
2
+ * SPDX-License-Identifier: GPL-2.0-or-later
3
+ *
4
+ * Copyright (C) 2026, Florian Hofhammer <florian.hofhammer@epfl.ch>
5
+ *
6
+ * This test set exercises the qemu_plugin_set_pc() function in four different
7
+ * contexts:
8
+ * 1. in an instruction callback during normal execution,
9
+ * 2. in an instruction callback during signal handling,
10
+ * 3. in a memory access callback.
11
+ * 4. in a syscall callback,
12
+ */
13
+#include <assert.h>
14
+#include <signal.h>
15
+#include <stdint.h>
16
+#include <stdlib.h>
17
+#include <stdio.h>
18
+#include <unistd.h>
19
+
20
+/* If we issue this magic syscall, ... */
21
+#define MAGIC_SYSCALL 4096
22
+/* ... the plugin either jumps directly to the target address ... */
23
+#define SETPC 0
24
+/* ... or just updates the target address for future use in callbacks. */
25
+#define SETTARGET 1
26
+
27
+static int signal_handled;
28
+
29
+void panic(const char *msg)
30
+{
31
+ fprintf(stderr, "Panic: %s\n", msg);
32
+ abort();
33
+}
34
+
35
+/*
36
+ * This test executes a magic syscall which communicates two addresses to the
37
+ * plugin via the syscall arguments. Whenever we reach the "bad" instruction
38
+ * during normal execution, the plugin should redirect control flow to the
39
+ * "good" instruction instead.
40
+ */
41
+void test_insn(void)
42
+{
43
+ long ret = syscall(MAGIC_SYSCALL, SETTARGET, &&bad_insn, &&good_insn,
44
+ NULL);
45
+ assert(ret == 0 && "Syscall filter did not return expected value");
46
+bad_insn:
47
+ panic("PC redirection in instruction callback failed");
48
+good_insn:
49
+ puts("PC redirection in instruction callback succeeded");
50
+}
51
+
52
+/*
53
+ * This signal handler communicates a "bad" and a "good" address to the plugin
54
+ * similar to the previous test, and skips to the "good" address when the "bad"
55
+ * one is reached. This serves to test whether PC redirection via
56
+ * qemu_plugin_set_pc() also works properly in a signal handler context.
57
+ */
58
+void usr1_handler(int signum)
59
+{
60
+ long ret = syscall(MAGIC_SYSCALL, SETTARGET, &&bad_signal, &&good_signal,
61
+ NULL);
62
+ assert(ret == 0 && "Syscall filter did not return expected value");
63
+bad_signal:
64
+ panic("PC redirection in instruction callback failed");
65
+good_signal:
66
+ signal_handled = 1;
67
+ puts("PC redirection in instruction callback succeeded");
68
+}
69
+
70
+/*
71
+ * This test sends a signal to the process, which should trigger the above
72
+ * signal handler. The signal handler should then exercise the PC redirection
73
+ * functionality in the context of a signal handler, which behaves a bit
74
+ * differently from normal execution.
75
+ */
76
+void test_sighandler(void)
77
+{
78
+ struct sigaction sa = {0};
79
+ sa.sa_handler = usr1_handler;
80
+ sigaction(SIGUSR1, &sa, NULL);
81
+ pid_t pid = getpid();
82
+ kill(pid, SIGUSR1);
83
+ assert(signal_handled == 1 && "Signal handler was not executed properly");
84
+}
85
+
86
+/*
87
+ * This test communicates a "good" address and the address of a local variable
88
+ * to the plugin. Upon accessing the local variable, the plugin should then
89
+ * redirect control flow to the "good" address via qemu_plugin_set_pc().
90
+ */
91
+void test_mem(void)
92
+{
93
+ static uint32_t test = 1;
94
+ long ret = syscall(MAGIC_SYSCALL, SETTARGET, NULL, &&good_mem, &test);
95
+ assert(ret == 0 && "Syscall filter did not return expected value");
96
+ /* Ensure read access to the variable to trigger the plugin callback */
97
+ assert(test == 1);
98
+ panic("PC redirection in memory access callback failed");
99
+good_mem:
100
+ puts("PC redirection in memory access callback succeeded");
101
+}
102
+
103
+/*
104
+ * This test executes a magic syscall which is intercepted and its actual
105
+ * execution skipped via the qemu_plugin_set_pc() API. In a proper plugin,
106
+ * syscall skipping would rather be implemented via the syscall filtering
107
+ * callback, but we want to make sure qemu_plugin_set_pc() works in different
108
+ * contexts.
109
+ */
110
+__attribute__((noreturn))
111
+void test_syscall(void)
112
+{
113
+ syscall(MAGIC_SYSCALL, SETPC, &&good_syscall);
114
+ panic("PC redirection in syscall callback failed");
115
+good_syscall:
116
+ /*
117
+ * Note: we execute this test last and exit straight from here because when
118
+ * the plugin redirects control flow upon syscall, the stack frame for the
119
+ * syscall function (and potential other functions in the call chain in
120
+ * libc) is still live and the stack is not unwound properly. Thus,
121
+ * returning from here is risky and breaks on some architectures, so we
122
+ * just exit directly from this test.
123
+ */
124
+ _exit(EXIT_SUCCESS);
125
+}
126
+
127
+
128
+int main(int argc, char *argv[])
129
+{
130
+ test_insn();
131
+ test_sighandler();
132
+ test_mem();
133
+ test_syscall();
134
+}
tests/tcg/plugins/meson.build
+1
@@ -7,6 +7,7 @@ test_plugins = [
7
'mem.c',
8
'patch.c',
9
'reset.c',
10
+'setpc.c',
11
'syscall.c',
12
]
13
tests/tcg/plugins/setpc.c
new
+105
@@ -0,0 +1,105 @@
1
+/*
2
+ * SPDX-License-Identifier: GPL-2.0-or-later
3
+ *
4
+ * Copyright (C) 2026, Florian Hofhammer <florian.hofhammer@epfl.ch>
5
+ */
6
+#include <assert.h>
7
+#include <glib.h>
8
+#include <inttypes.h>
9
+#include <unistd.h>
10
+
11
+#include <qemu-plugin.h>
12
+
13
+/* If we detect this magic syscall, ... */
14
+#define MAGIC_SYSCALL 4096
15
+/* ... the plugin either jumps directly to the target address ... */
16
+#define SETPC 0
17
+/* ... or just updates the target address for future use in callbacks. */
18
+#define SETTARGET 1
19
+
20
+QEMU_PLUGIN_EXPORT int qemu_plugin_version = QEMU_PLUGIN_VERSION;
21
+
22
+static uint64_t source_pc;
23
+static uint64_t target_pc;
24
+static uint64_t target_vaddr;
25
+
26
+static bool vcpu_syscall_filter(qemu_plugin_id_t id, unsigned int vcpu_index,
27
+ int64_t num, uint64_t a1, uint64_t a2,
28
+ uint64_t a3, uint64_t a4, uint64_t a5,
29
+ uint64_t a6, uint64_t a7, uint64_t a8,
30
+ uint64_t *sysret)
31
+{
32
+ if (num == MAGIC_SYSCALL) {
33
+ if (a1 == SETPC) {
34
+ qemu_plugin_outs("Magic syscall detected, jump to clean exit\n");
35
+ qemu_plugin_set_pc(a2);
36
+ } else if (a1 == SETTARGET) {
37
+ qemu_plugin_outs("Magic syscall detected, set target_pc / "
38
+ "target_vaddr\n");
39
+ source_pc = a2;
40
+ target_pc = a3;
41
+ target_vaddr = a4;
42
+ *sysret = 0;
43
+ return true;
44
+ } else {
45
+ qemu_plugin_outs("Unknown magic syscall argument, ignoring\n");
46
+ }
47
+ }
48
+ return false;
49
+}
50
+
51
+static void vcpu_insn_exec(unsigned int vcpu_index, void *userdata)
52
+{
53
+ uint64_t vaddr = (uint64_t)userdata;
54
+ if (vaddr == source_pc) {
55
+ g_assert(target_pc != 0);
56
+ g_assert(target_vaddr == 0);
57
+
58
+ qemu_plugin_outs("Marker insn detected, jump to clean return\n");
59
+ qemu_plugin_set_pc(target_pc);
60
+ }
61
+}
62
+
63
+static void vcpu_mem_access(unsigned int vcpu_index,
64
+ qemu_plugin_meminfo_t info,
65
+ uint64_t vaddr, void *userdata)
66
+{
67
+ if (vaddr != 0 && vaddr == target_vaddr) {
68
+ g_assert(source_pc == 0);
69
+ g_assert(target_pc != 0);
70
+
71
+ qemu_plugin_outs("Marker mem access detected, jump to clean return\n");
72
+ qemu_plugin_set_pc(target_pc);
73
+ }
74
+}
75
+
76
+static void vcpu_tb_trans(qemu_plugin_id_t id, struct qemu_plugin_tb *tb)
77
+{
78
+ size_t insns = qemu_plugin_tb_n_insns(tb);
79
+ for (size_t i = 0; i < insns; i++) {
80
+ struct qemu_plugin_insn *insn = qemu_plugin_tb_get_insn(tb, i);
81
+ uint64_t insn_vaddr = qemu_plugin_insn_vaddr(insn);
82
+ /*
83
+ * Note: we cannot only register the callbacks if the instruction is
84
+ * in one of the functions of interest, because symbol lookup for
85
+ * filtering does not work for all architectures (e.g., ppc64).
86
+ */
87
+ qemu_plugin_register_vcpu_insn_exec_cb(insn, vcpu_insn_exec,
88
+ QEMU_PLUGIN_CB_RW_REGS_PC,
89
+ (void *)insn_vaddr);
90
+ qemu_plugin_register_vcpu_mem_cb(insn, vcpu_mem_access,
91
+ QEMU_PLUGIN_CB_RW_REGS_PC,
92
+ QEMU_PLUGIN_MEM_R, NULL);
93
+ }
94
+}
95
+
96
+
97
+QEMU_PLUGIN_EXPORT int qemu_plugin_install(qemu_plugin_id_t id,
98
+ const qemu_info_t *info,
99
+ int argc, char **argv)
100
+{
101
+
102
+ qemu_plugin_register_vcpu_syscall_filter_cb(id, vcpu_syscall_filter);
103
+ qemu_plugin_register_vcpu_tb_trans_cb(id, vcpu_tb_trans);
104
+ return 0;
105
+}