[PATCH v5 4/7] tests/tcg: add test for qemu_plugin_set_pc API

Florian Hofhammer posted 7 patches 1 month, 1 week ago
Maintainers: Laurent Vivier <laurent@vivier.eu>, Pierrick Bouvier <pierrick.bouvier@linaro.org>, Brian Cain <brian.cain@oss.qualcomm.com>, "Alex Bennée" <alex.bennee@linaro.org>, Alexandre Iooss <erdnaxe@crans.org>, Mahmoud Mandour <ma.mandourr@gmail.com>, Peter Maydell <peter.maydell@linaro.org>
There is a newer version of this series
[PATCH v5 4/7] tests/tcg: add test for qemu_plugin_set_pc API
Posted by Florian Hofhammer 1 month, 1 week ago
The test executes a non-existent syscall, which the syscall plugin
intercepts and redirects to a clean exit.
Due to architecture-specific quirks, the architecture-specific Makefiles
require setting specific compiler and linker flags in some cases.

Signed-off-by: Florian Hofhammer <florian.hofhammer@epfl.ch>
---
 MAINTAINERS                                   |   1 +
 tests/tcg/arm/Makefile.target                 |   6 +
 tests/tcg/multiarch/Makefile.target           |  17 ++-
 .../{ => plugin}/check-plugin-output.sh       |   0
 .../{ => plugin}/test-plugin-mem-access.c     |   0
 .../tcg/multiarch/plugin/test-plugin-set-pc.c | 140 ++++++++++++++++++
 tests/tcg/plugins/meson.build                 |   1 +
 tests/tcg/plugins/setpc.c                     | 120 +++++++++++++++
 8 files changed, 282 insertions(+), 3 deletions(-)
 rename tests/tcg/multiarch/{ => plugin}/check-plugin-output.sh (100%)
 rename tests/tcg/multiarch/{ => plugin}/test-plugin-mem-access.c (100%)
 create mode 100644 tests/tcg/multiarch/plugin/test-plugin-set-pc.c
 create mode 100644 tests/tcg/plugins/setpc.c

diff --git a/MAINTAINERS b/MAINTAINERS
index 606b16762c..2746ec9a0b 100644
--- a/MAINTAINERS
+++ b/MAINTAINERS
@@ -4083,6 +4083,7 @@ S: Maintained
 F: docs/devel/tcg-plugins.rst
 F: plugins/
 F: tests/tcg/plugins/
+F: tests/tcg/multiarch/plugin/
 F: tests/functional/aarch64/test_tcg_plugins.py
 F: contrib/plugins/
 F: scripts/qemu-plugin-symbols.py
diff --git a/tests/tcg/arm/Makefile.target b/tests/tcg/arm/Makefile.target
index 6189d7a0e2..613bbf0939 100644
--- a/tests/tcg/arm/Makefile.target
+++ b/tests/tcg/arm/Makefile.target
@@ -78,4 +78,10 @@ sha512-vector: sha512.c
 
 ARM_TESTS += sha512-vector
 
+ifeq ($(CONFIG_PLUGIN),y)
+# Require emitting arm32 instructions, otherwise the vCPU might accidentally
+# try to execute Thumb instructions in arm32 mode after qemu_plugin_set_pc()
+test-plugin-set-pc: CFLAGS+=-marm
+endif
+
 TESTS += $(ARM_TESTS)
diff --git a/tests/tcg/multiarch/Makefile.target b/tests/tcg/multiarch/Makefile.target
index 07d0b27bdd..a347efbadf 100644
--- a/tests/tcg/multiarch/Makefile.target
+++ b/tests/tcg/multiarch/Makefile.target
@@ -14,6 +14,10 @@ ifeq ($(filter %-linux-user, $(TARGET)),$(TARGET))
 VPATH 	       += $(MULTIARCH_SRC)/linux
 MULTIARCH_SRCS += $(notdir $(wildcard $(MULTIARCH_SRC)/linux/*.c))
 endif
+ifeq ($(CONFIG_PLUGIN),y)
+VPATH 	       += $(MULTIARCH_SRC)/plugin
+MULTIARCH_SRCS += $(notdir $(wildcard $(MULTIARCH_SRC)/plugin/*.c))
+endif
 MULTIARCH_TESTS = $(MULTIARCH_SRCS:.c=)
 
 #
@@ -200,13 +204,20 @@ run-plugin-test-plugin-mem-access-with-libmem.so: \
 	PLUGIN_ARGS=$(COMMA)print-accesses=true
 run-plugin-test-plugin-mem-access-with-libmem.so: \
 	CHECK_PLUGIN_OUTPUT_COMMAND= \
-	$(SRC_PATH)/tests/tcg/multiarch/check-plugin-output.sh \
+	$(SRC_PATH)/tests/tcg/multiarch/plugin/check-plugin-output.sh \
 	$(QEMU) $<
 run-plugin-test-plugin-syscall-filter-with-libsyscall.so:
+run-plugin-test-plugin-set-pc-with-libsetpc.so:
 
 EXTRA_RUNS_WITH_PLUGIN += run-plugin-test-plugin-mem-access-with-libmem.so \
-			  run-plugin-test-plugin-syscall-filter-with-libsyscall.so
-else
+			  run-plugin-test-plugin-syscall-filter-with-libsyscall.so \
+			  run-plugin-test-plugin-set-pc-with-libsetpc.so
+
+else # CONFIG_PLUGIN=n
+# Do not build the syscall skipping test if it's not tested with the setpc
+# plugin because it will simply fail the test.
+MULTIARCH_TESTS := $(filter-out test-plugin-set-pc, $(MULTIARCH_TESTS))
+
 # test-plugin-syscall-filter needs syscall plugin to succeed
 test-plugin-syscall-filter: CFLAGS+=-DSKIP
 endif
diff --git a/tests/tcg/multiarch/check-plugin-output.sh b/tests/tcg/multiarch/plugin/check-plugin-output.sh
similarity index 100%
rename from tests/tcg/multiarch/check-plugin-output.sh
rename to tests/tcg/multiarch/plugin/check-plugin-output.sh
diff --git a/tests/tcg/multiarch/test-plugin-mem-access.c b/tests/tcg/multiarch/plugin/test-plugin-mem-access.c
similarity index 100%
rename from tests/tcg/multiarch/test-plugin-mem-access.c
rename to tests/tcg/multiarch/plugin/test-plugin-mem-access.c
diff --git a/tests/tcg/multiarch/plugin/test-plugin-set-pc.c b/tests/tcg/multiarch/plugin/test-plugin-set-pc.c
new file mode 100644
index 0000000000..40d9a9e8f0
--- /dev/null
+++ b/tests/tcg/multiarch/plugin/test-plugin-set-pc.c
@@ -0,0 +1,140 @@
+/*
+ * SPDX-License-Identifier: GPL-2.0-or-later
+ *
+ * Copyright (C) 2026, Florian Hofhammer <florian.hofhammer@epfl.ch>
+ *
+ * This test set exercises the qemu_plugin_set_pc() function in four different
+ * contexts:
+ * 1. in a syscall callback,
+ * 2. in an instruction callback during normal execution,
+ * 3. in an instruction callback during signal handling,
+ * 4. in a memory access callback.
+ * Note: using the volatile guards is necessary to prevent the compiler from
+ * doing dead code elimination even on -O0, which would cause everything after
+ * the asserts and thus also the target labels to be optimized away.
+ */
+#include <assert.h>
+#include <signal.h>
+#include <stdint.h>
+#include <stdlib.h>
+#include <stdio.h>
+#include <unistd.h>
+#include <setjmp.h>
+
+#define NOINLINE __attribute__((noinline))
+#define NORETURN __attribute__((noreturn))
+
+static int signal_handled;
+/*
+ * The volatile variable is used as a guard to prevent the compiler from
+ * optimizing away "unreachable" labels.
+ */
+static volatile uint32_t guard = 1;
+
+/*
+ * This test executes a magic syscall which communicates two addresses to the
+ * plugin via the syscall arguments. Whenever we reach the "bad" instruction
+ * during normal execution, the plugin should redirect control flow to the
+ * "good" instruction instead.
+ */
+NOINLINE void test_insn(void)
+{
+    long ret = syscall(4095, &&bad_insn, &&good_insn, NULL);
+    assert(ret == 0 && "Syscall filter did not return expected value");
+    if (guard) {
+bad_insn:
+        assert(0 && "PC redirection in instruction callback failed");
+    } else {
+good_insn:
+        return;
+    }
+}
+
+/*
+ * This signal handler communicates a "bad" and a "good" address to the plugin
+ * similar to the previous test, and skips to the "good" address when the "bad"
+ * one is reached. This serves to test whether PC redirection via
+ * qemu_plugin_set_pc() also works properly in a signal handler context.
+ */
+NOINLINE void usr1_handler(int signum)
+{
+    long ret = syscall(4095, &&bad_signal, &&good_signal, NULL);
+    assert(ret == 0 && "Syscall filter did not return expected value");
+    if (guard) {
+bad_signal:
+        assert(0 && "PC redirection in instruction callback failed");
+    } else {
+good_signal:
+        signal_handled = 1;
+        return;
+    }
+}
+
+/*
+ * This test sends a signal to the process, which should trigger the above
+ * signal handler. The signal handler should then exercise the PC redirection
+ * functionality in the context of a signal handler, which behaves a bit
+ * differently from normal execution.
+ */
+NOINLINE void test_sighandler(void)
+{
+    struct sigaction sa = {0};
+    sa.sa_handler = usr1_handler;
+    sigaction(SIGUSR1, &sa, NULL);
+    pid_t pid = getpid();
+    kill(pid, SIGUSR1);
+    assert(signal_handled == 1 && "Signal handler was not executed properly");
+}
+
+/*
+ * This test communicates a "good" address and the address of a local variable
+ * to the plugin. Upon accessing the local variable, the plugin should then
+ * redirect control flow to the "good" address via qemu_plugin_set_pc().
+ */
+NOINLINE void test_mem(void)
+{
+    long ret = syscall(4095, NULL, &&good_mem, &guard);
+    assert(ret == 0 && "Syscall filter did not return expected value");
+    if (guard) {
+        assert(0 && "PC redirection in memory access callback failed");
+    } else {
+good_mem:
+        return;
+    }
+}
+
+/*
+ * This test executes a magic syscall which is intercepted and its actual
+ * execution skipped via the qemu_plugin_set_pc() API. In a proper plugin,
+ * syscall skipping would rather be implemented via the syscall filtering
+ * callback, but we want to make sure qemu_plugin_set_pc() works in different
+ * contexts.
+ */
+NOINLINE NORETURN
+void test_syscall(void)
+{
+    syscall(4096, &&good_syscall);
+    if (guard) {
+        assert(0 && "PC redirection in syscall callback failed");
+    } else {
+good_syscall:
+        /*
+         * Note: we execute this test last and exit straight from here because
+         * when the plugin redirects control flow upon syscall, the stack frame
+         * for the syscall function (and potential other functions in the call
+         * chain in libc) is still live and the stack is not unwound properly.
+         * Thus, returning from here is risky and breaks on some architectures,
+         * so we just exit directly from this test.
+         */
+        _exit(EXIT_SUCCESS);
+    }
+}
+
+
+int main(int argc, char *argv[])
+{
+    test_insn();
+    test_sighandler();
+    test_mem();
+    test_syscall();
+}
diff --git a/tests/tcg/plugins/meson.build b/tests/tcg/plugins/meson.build
index c5e49753fd..b3e3a9a6d0 100644
--- a/tests/tcg/plugins/meson.build
+++ b/tests/tcg/plugins/meson.build
@@ -7,6 +7,7 @@ test_plugins = [
 'mem.c',
 'patch.c',
 'reset.c',
+'setpc.c',
 'syscall.c',
 ]
 
diff --git a/tests/tcg/plugins/setpc.c b/tests/tcg/plugins/setpc.c
new file mode 100644
index 0000000000..72ae31a0ef
--- /dev/null
+++ b/tests/tcg/plugins/setpc.c
@@ -0,0 +1,120 @@
+/*
+ * SPDX-License-Identifier: GPL-2.0-or-later
+ *
+ * Copyright (C) 2026, Florian Hofhammer <florian.hofhammer@epfl.ch>
+ */
+#include <assert.h>
+#include <glib.h>
+#include <inttypes.h>
+#include <unistd.h>
+
+#include <qemu-plugin.h>
+
+QEMU_PLUGIN_EXPORT int qemu_plugin_version = QEMU_PLUGIN_VERSION;
+
+static uint64_t source_pc;
+static uint64_t target_pc;
+static uint64_t target_vaddr;
+
+static void vcpu_syscall(qemu_plugin_id_t id, unsigned int vcpu_index,
+                         int64_t num, uint64_t a1, uint64_t a2,
+                         uint64_t a3, uint64_t a4, uint64_t a5,
+                         uint64_t a6, uint64_t a7, uint64_t a8)
+{
+    if (num == 4096) {
+        qemu_plugin_outs("Marker syscall detected, jump to clean return\n");
+        qemu_plugin_set_pc(a1);
+    }
+}
+
+static bool vcpu_syscall_filter(qemu_plugin_id_t id, unsigned int vcpu_index,
+                                int64_t num, uint64_t a1, uint64_t a2,
+                                uint64_t a3, uint64_t a4, uint64_t a5,
+                                uint64_t a6, uint64_t a7, uint64_t a8,
+                                uint64_t *sysret)
+{
+    if (num == 4095) {
+        qemu_plugin_outs("Communication syscall detected, set target_pc / "
+                         "target_vaddr\n");
+        source_pc = a1;
+        target_pc = a2;
+        target_vaddr = a3;
+        if (source_pc >> 63 || target_pc >> 63 || target_vaddr >> 63) {
+            /*
+             * Some architectures (e.g., m68k) use 32-bit addresses with the
+             * top bit set, which causes them to get sign-extended somewhere in
+             * the chain to this callback. We mask the top bits off here to get
+             * the actual addresses.
+             */
+            qemu_plugin_outs("High bit in addresses detected: possible sign "
+                             "extension in syscall, masking off top bits\n");
+            source_pc &= UINT32_MAX;
+            target_pc &= UINT32_MAX;
+            target_vaddr &= UINT32_MAX;
+        }
+        *sysret = 0;
+        return true;
+    }
+    return false;
+}
+
+static void vcpu_insn_exec(unsigned int vcpu_index, void *userdata)
+{
+    uint64_t vaddr = (uint64_t)userdata;
+    if (vaddr == source_pc) {
+        g_assert(target_pc != 0);
+        g_assert(target_vaddr == 0);
+
+        qemu_plugin_outs("Marker instruction detected, jump to clean return\n");
+        qemu_plugin_set_pc(target_pc);
+    }
+}
+
+static void vcpu_mem_access(unsigned int vcpu_index,
+                            qemu_plugin_meminfo_t info,
+                            uint64_t vaddr, void *userdata)
+{
+    if (vaddr != 0 && vaddr == target_vaddr) {
+        g_assert(source_pc == 0);
+        g_assert(target_pc != 0);
+        qemu_plugin_mem_value val = qemu_plugin_mem_get_value(info);
+        /* target_vaddr points to our volatile guard ==> should always be 1 */
+        g_assert(val.type == QEMU_PLUGIN_MEM_VALUE_U32);
+        g_assert(val.data.u32 == 1);
+
+        qemu_plugin_outs("Marker mem access detected, jump to clean return\n");
+        qemu_plugin_set_pc(target_pc);
+    }
+}
+
+static void vcpu_tb_trans(qemu_plugin_id_t id, struct qemu_plugin_tb *tb)
+{
+    size_t insns = qemu_plugin_tb_n_insns(tb);
+    for (size_t i = 0; i < insns; i++) {
+        struct qemu_plugin_insn *insn = qemu_plugin_tb_get_insn(tb, i);
+        uint64_t insn_vaddr = qemu_plugin_insn_vaddr(insn);
+        /*
+         * Note: we cannot only register the callbacks if the instruction is
+         * in one of the functions of interest, because symbol lookup for
+         * filtering does not work for all architectures (e.g., ppc64).
+         */
+        qemu_plugin_register_vcpu_insn_exec_cb(insn, vcpu_insn_exec,
+                                               QEMU_PLUGIN_CB_RW_REGS_PC,
+                                               (void *)insn_vaddr);
+        qemu_plugin_register_vcpu_mem_cb(insn, vcpu_mem_access,
+                                         QEMU_PLUGIN_CB_RW_REGS_PC,
+                                         QEMU_PLUGIN_MEM_R, NULL);
+    }
+}
+
+
+QEMU_PLUGIN_EXPORT int qemu_plugin_install(qemu_plugin_id_t id,
+                                           const qemu_info_t *info,
+                                           int argc, char **argv)
+{
+
+    qemu_plugin_register_vcpu_syscall_cb(id, vcpu_syscall);
+    qemu_plugin_register_vcpu_syscall_filter_cb(id, vcpu_syscall_filter);
+    qemu_plugin_register_vcpu_tb_trans_cb(id, vcpu_tb_trans);
+    return 0;
+}
-- 
2.53.0
Re: [PATCH v5 4/7] tests/tcg: add test for qemu_plugin_set_pc API
Posted by Florian Hofhammer 1 month, 1 week ago
On 02/03/2026 14:59, Florian Hofhammer wrote:
> The test executes a non-existent syscall, which the syscall plugin
> intercepts and redirects to a clean exit.
> Due to architecture-specific quirks, the architecture-specific Makefiles
> require setting specific compiler and linker flags in some cases.

Please disregard this patch and use the other version I just sent, I
forgot to update the commit message for this one. My apologies for that!

> 
> Signed-off-by: Florian Hofhammer <florian.hofhammer@epfl.ch>
> ---
>  MAINTAINERS                                   |   1 +
>  tests/tcg/arm/Makefile.target                 |   6 +
>  tests/tcg/multiarch/Makefile.target           |  17 ++-
>  .../{ => plugin}/check-plugin-output.sh       |   0
>  .../{ => plugin}/test-plugin-mem-access.c     |   0
>  .../tcg/multiarch/plugin/test-plugin-set-pc.c | 140 ++++++++++++++++++
>  tests/tcg/plugins/meson.build                 |   1 +
>  tests/tcg/plugins/setpc.c                     | 120 +++++++++++++++
>  8 files changed, 282 insertions(+), 3 deletions(-)
>  rename tests/tcg/multiarch/{ => plugin}/check-plugin-output.sh (100%)
>  rename tests/tcg/multiarch/{ => plugin}/test-plugin-mem-access.c (100%)
>  create mode 100644 tests/tcg/multiarch/plugin/test-plugin-set-pc.c
>  create mode 100644 tests/tcg/plugins/setpc.c
> 
> diff --git a/MAINTAINERS b/MAINTAINERS
> index 606b16762c..2746ec9a0b 100644
> --- a/MAINTAINERS
> +++ b/MAINTAINERS
> @@ -4083,6 +4083,7 @@ S: Maintained
>  F: docs/devel/tcg-plugins.rst
>  F: plugins/
>  F: tests/tcg/plugins/
> +F: tests/tcg/multiarch/plugin/
>  F: tests/functional/aarch64/test_tcg_plugins.py
>  F: contrib/plugins/
>  F: scripts/qemu-plugin-symbols.py
> diff --git a/tests/tcg/arm/Makefile.target b/tests/tcg/arm/Makefile.target
> index 6189d7a0e2..613bbf0939 100644
> --- a/tests/tcg/arm/Makefile.target
> +++ b/tests/tcg/arm/Makefile.target
> @@ -78,4 +78,10 @@ sha512-vector: sha512.c
>  
>  ARM_TESTS += sha512-vector
>  
> +ifeq ($(CONFIG_PLUGIN),y)
> +# Require emitting arm32 instructions, otherwise the vCPU might accidentally
> +# try to execute Thumb instructions in arm32 mode after qemu_plugin_set_pc()
> +test-plugin-set-pc: CFLAGS+=-marm
> +endif
> +
>  TESTS += $(ARM_TESTS)
> diff --git a/tests/tcg/multiarch/Makefile.target b/tests/tcg/multiarch/Makefile.target
> index 07d0b27bdd..a347efbadf 100644
> --- a/tests/tcg/multiarch/Makefile.target
> +++ b/tests/tcg/multiarch/Makefile.target
> @@ -14,6 +14,10 @@ ifeq ($(filter %-linux-user, $(TARGET)),$(TARGET))
>  VPATH 	       += $(MULTIARCH_SRC)/linux
>  MULTIARCH_SRCS += $(notdir $(wildcard $(MULTIARCH_SRC)/linux/*.c))
>  endif
> +ifeq ($(CONFIG_PLUGIN),y)
> +VPATH 	       += $(MULTIARCH_SRC)/plugin
> +MULTIARCH_SRCS += $(notdir $(wildcard $(MULTIARCH_SRC)/plugin/*.c))
> +endif
>  MULTIARCH_TESTS = $(MULTIARCH_SRCS:.c=)
>  
>  #
> @@ -200,13 +204,20 @@ run-plugin-test-plugin-mem-access-with-libmem.so: \
>  	PLUGIN_ARGS=$(COMMA)print-accesses=true
>  run-plugin-test-plugin-mem-access-with-libmem.so: \
>  	CHECK_PLUGIN_OUTPUT_COMMAND= \
> -	$(SRC_PATH)/tests/tcg/multiarch/check-plugin-output.sh \
> +	$(SRC_PATH)/tests/tcg/multiarch/plugin/check-plugin-output.sh \
>  	$(QEMU) $<
>  run-plugin-test-plugin-syscall-filter-with-libsyscall.so:
> +run-plugin-test-plugin-set-pc-with-libsetpc.so:
>  
>  EXTRA_RUNS_WITH_PLUGIN += run-plugin-test-plugin-mem-access-with-libmem.so \
> -			  run-plugin-test-plugin-syscall-filter-with-libsyscall.so
> -else
> +			  run-plugin-test-plugin-syscall-filter-with-libsyscall.so \
> +			  run-plugin-test-plugin-set-pc-with-libsetpc.so
> +
> +else # CONFIG_PLUGIN=n
> +# Do not build the syscall skipping test if it's not tested with the setpc
> +# plugin because it will simply fail the test.
> +MULTIARCH_TESTS := $(filter-out test-plugin-set-pc, $(MULTIARCH_TESTS))
> +
>  # test-plugin-syscall-filter needs syscall plugin to succeed
>  test-plugin-syscall-filter: CFLAGS+=-DSKIP
>  endif
> diff --git a/tests/tcg/multiarch/check-plugin-output.sh b/tests/tcg/multiarch/plugin/check-plugin-output.sh
> similarity index 100%
> rename from tests/tcg/multiarch/check-plugin-output.sh
> rename to tests/tcg/multiarch/plugin/check-plugin-output.sh
> diff --git a/tests/tcg/multiarch/test-plugin-mem-access.c b/tests/tcg/multiarch/plugin/test-plugin-mem-access.c
> similarity index 100%
> rename from tests/tcg/multiarch/test-plugin-mem-access.c
> rename to tests/tcg/multiarch/plugin/test-plugin-mem-access.c
> diff --git a/tests/tcg/multiarch/plugin/test-plugin-set-pc.c b/tests/tcg/multiarch/plugin/test-plugin-set-pc.c
> new file mode 100644
> index 0000000000..40d9a9e8f0
> --- /dev/null
> +++ b/tests/tcg/multiarch/plugin/test-plugin-set-pc.c
> @@ -0,0 +1,140 @@
> +/*
> + * SPDX-License-Identifier: GPL-2.0-or-later
> + *
> + * Copyright (C) 2026, Florian Hofhammer <florian.hofhammer@epfl.ch>
> + *
> + * This test set exercises the qemu_plugin_set_pc() function in four different
> + * contexts:
> + * 1. in a syscall callback,
> + * 2. in an instruction callback during normal execution,
> + * 3. in an instruction callback during signal handling,
> + * 4. in a memory access callback.
> + * Note: using the volatile guards is necessary to prevent the compiler from
> + * doing dead code elimination even on -O0, which would cause everything after
> + * the asserts and thus also the target labels to be optimized away.
> + */
> +#include <assert.h>
> +#include <signal.h>
> +#include <stdint.h>
> +#include <stdlib.h>
> +#include <stdio.h>
> +#include <unistd.h>
> +#include <setjmp.h>
> +
> +#define NOINLINE __attribute__((noinline))
> +#define NORETURN __attribute__((noreturn))
> +
> +static int signal_handled;
> +/*
> + * The volatile variable is used as a guard to prevent the compiler from
> + * optimizing away "unreachable" labels.
> + */
> +static volatile uint32_t guard = 1;
> +
> +/*
> + * This test executes a magic syscall which communicates two addresses to the
> + * plugin via the syscall arguments. Whenever we reach the "bad" instruction
> + * during normal execution, the plugin should redirect control flow to the
> + * "good" instruction instead.
> + */
> +NOINLINE void test_insn(void)
> +{
> +    long ret = syscall(4095, &&bad_insn, &&good_insn, NULL);
> +    assert(ret == 0 && "Syscall filter did not return expected value");
> +    if (guard) {
> +bad_insn:
> +        assert(0 && "PC redirection in instruction callback failed");
> +    } else {
> +good_insn:
> +        return;
> +    }
> +}
> +
> +/*
> + * This signal handler communicates a "bad" and a "good" address to the plugin
> + * similar to the previous test, and skips to the "good" address when the "bad"
> + * one is reached. This serves to test whether PC redirection via
> + * qemu_plugin_set_pc() also works properly in a signal handler context.
> + */
> +NOINLINE void usr1_handler(int signum)
> +{
> +    long ret = syscall(4095, &&bad_signal, &&good_signal, NULL);
> +    assert(ret == 0 && "Syscall filter did not return expected value");
> +    if (guard) {
> +bad_signal:
> +        assert(0 && "PC redirection in instruction callback failed");
> +    } else {
> +good_signal:
> +        signal_handled = 1;
> +        return;
> +    }
> +}
> +
> +/*
> + * This test sends a signal to the process, which should trigger the above
> + * signal handler. The signal handler should then exercise the PC redirection
> + * functionality in the context of a signal handler, which behaves a bit
> + * differently from normal execution.
> + */
> +NOINLINE void test_sighandler(void)
> +{
> +    struct sigaction sa = {0};
> +    sa.sa_handler = usr1_handler;
> +    sigaction(SIGUSR1, &sa, NULL);
> +    pid_t pid = getpid();
> +    kill(pid, SIGUSR1);
> +    assert(signal_handled == 1 && "Signal handler was not executed properly");
> +}
> +
> +/*
> + * This test communicates a "good" address and the address of a local variable
> + * to the plugin. Upon accessing the local variable, the plugin should then
> + * redirect control flow to the "good" address via qemu_plugin_set_pc().
> + */
> +NOINLINE void test_mem(void)
> +{
> +    long ret = syscall(4095, NULL, &&good_mem, &guard);
> +    assert(ret == 0 && "Syscall filter did not return expected value");
> +    if (guard) {
> +        assert(0 && "PC redirection in memory access callback failed");
> +    } else {
> +good_mem:
> +        return;
> +    }
> +}
> +
> +/*
> + * This test executes a magic syscall which is intercepted and its actual
> + * execution skipped via the qemu_plugin_set_pc() API. In a proper plugin,
> + * syscall skipping would rather be implemented via the syscall filtering
> + * callback, but we want to make sure qemu_plugin_set_pc() works in different
> + * contexts.
> + */
> +NOINLINE NORETURN
> +void test_syscall(void)
> +{
> +    syscall(4096, &&good_syscall);
> +    if (guard) {
> +        assert(0 && "PC redirection in syscall callback failed");
> +    } else {
> +good_syscall:
> +        /*
> +         * Note: we execute this test last and exit straight from here because
> +         * when the plugin redirects control flow upon syscall, the stack frame
> +         * for the syscall function (and potential other functions in the call
> +         * chain in libc) is still live and the stack is not unwound properly.
> +         * Thus, returning from here is risky and breaks on some architectures,
> +         * so we just exit directly from this test.
> +         */
> +        _exit(EXIT_SUCCESS);
> +    }
> +}
> +
> +
> +int main(int argc, char *argv[])
> +{
> +    test_insn();
> +    test_sighandler();
> +    test_mem();
> +    test_syscall();
> +}
> diff --git a/tests/tcg/plugins/meson.build b/tests/tcg/plugins/meson.build
> index c5e49753fd..b3e3a9a6d0 100644
> --- a/tests/tcg/plugins/meson.build
> +++ b/tests/tcg/plugins/meson.build
> @@ -7,6 +7,7 @@ test_plugins = [
>  'mem.c',
>  'patch.c',
>  'reset.c',
> +'setpc.c',
>  'syscall.c',
>  ]
>  
> diff --git a/tests/tcg/plugins/setpc.c b/tests/tcg/plugins/setpc.c
> new file mode 100644
> index 0000000000..72ae31a0ef
> --- /dev/null
> +++ b/tests/tcg/plugins/setpc.c
> @@ -0,0 +1,120 @@
> +/*
> + * SPDX-License-Identifier: GPL-2.0-or-later
> + *
> + * Copyright (C) 2026, Florian Hofhammer <florian.hofhammer@epfl.ch>
> + */
> +#include <assert.h>
> +#include <glib.h>
> +#include <inttypes.h>
> +#include <unistd.h>
> +
> +#include <qemu-plugin.h>
> +
> +QEMU_PLUGIN_EXPORT int qemu_plugin_version = QEMU_PLUGIN_VERSION;
> +
> +static uint64_t source_pc;
> +static uint64_t target_pc;
> +static uint64_t target_vaddr;
> +
> +static void vcpu_syscall(qemu_plugin_id_t id, unsigned int vcpu_index,
> +                         int64_t num, uint64_t a1, uint64_t a2,
> +                         uint64_t a3, uint64_t a4, uint64_t a5,
> +                         uint64_t a6, uint64_t a7, uint64_t a8)
> +{
> +    if (num == 4096) {
> +        qemu_plugin_outs("Marker syscall detected, jump to clean return\n");
> +        qemu_plugin_set_pc(a1);
> +    }
> +}
> +
> +static bool vcpu_syscall_filter(qemu_plugin_id_t id, unsigned int vcpu_index,
> +                                int64_t num, uint64_t a1, uint64_t a2,
> +                                uint64_t a3, uint64_t a4, uint64_t a5,
> +                                uint64_t a6, uint64_t a7, uint64_t a8,
> +                                uint64_t *sysret)
> +{
> +    if (num == 4095) {
> +        qemu_plugin_outs("Communication syscall detected, set target_pc / "
> +                         "target_vaddr\n");
> +        source_pc = a1;
> +        target_pc = a2;
> +        target_vaddr = a3;
> +        if (source_pc >> 63 || target_pc >> 63 || target_vaddr >> 63) {
> +            /*
> +             * Some architectures (e.g., m68k) use 32-bit addresses with the
> +             * top bit set, which causes them to get sign-extended somewhere in
> +             * the chain to this callback. We mask the top bits off here to get
> +             * the actual addresses.
> +             */
> +            qemu_plugin_outs("High bit in addresses detected: possible sign "
> +                             "extension in syscall, masking off top bits\n");
> +            source_pc &= UINT32_MAX;
> +            target_pc &= UINT32_MAX;
> +            target_vaddr &= UINT32_MAX;
> +        }
> +        *sysret = 0;
> +        return true;
> +    }
> +    return false;
> +}
> +
> +static void vcpu_insn_exec(unsigned int vcpu_index, void *userdata)
> +{
> +    uint64_t vaddr = (uint64_t)userdata;
> +    if (vaddr == source_pc) {
> +        g_assert(target_pc != 0);
> +        g_assert(target_vaddr == 0);
> +
> +        qemu_plugin_outs("Marker instruction detected, jump to clean return\n");
> +        qemu_plugin_set_pc(target_pc);
> +    }
> +}
> +
> +static void vcpu_mem_access(unsigned int vcpu_index,
> +                            qemu_plugin_meminfo_t info,
> +                            uint64_t vaddr, void *userdata)
> +{
> +    if (vaddr != 0 && vaddr == target_vaddr) {
> +        g_assert(source_pc == 0);
> +        g_assert(target_pc != 0);
> +        qemu_plugin_mem_value val = qemu_plugin_mem_get_value(info);
> +        /* target_vaddr points to our volatile guard ==> should always be 1 */
> +        g_assert(val.type == QEMU_PLUGIN_MEM_VALUE_U32);
> +        g_assert(val.data.u32 == 1);
> +
> +        qemu_plugin_outs("Marker mem access detected, jump to clean return\n");
> +        qemu_plugin_set_pc(target_pc);
> +    }
> +}
> +
> +static void vcpu_tb_trans(qemu_plugin_id_t id, struct qemu_plugin_tb *tb)
> +{
> +    size_t insns = qemu_plugin_tb_n_insns(tb);
> +    for (size_t i = 0; i < insns; i++) {
> +        struct qemu_plugin_insn *insn = qemu_plugin_tb_get_insn(tb, i);
> +        uint64_t insn_vaddr = qemu_plugin_insn_vaddr(insn);
> +        /*
> +         * Note: we cannot only register the callbacks if the instruction is
> +         * in one of the functions of interest, because symbol lookup for
> +         * filtering does not work for all architectures (e.g., ppc64).
> +         */
> +        qemu_plugin_register_vcpu_insn_exec_cb(insn, vcpu_insn_exec,
> +                                               QEMU_PLUGIN_CB_RW_REGS_PC,
> +                                               (void *)insn_vaddr);
> +        qemu_plugin_register_vcpu_mem_cb(insn, vcpu_mem_access,
> +                                         QEMU_PLUGIN_CB_RW_REGS_PC,
> +                                         QEMU_PLUGIN_MEM_R, NULL);
> +    }
> +}
> +
> +
> +QEMU_PLUGIN_EXPORT int qemu_plugin_install(qemu_plugin_id_t id,
> +                                           const qemu_info_t *info,
> +                                           int argc, char **argv)
> +{
> +
> +    qemu_plugin_register_vcpu_syscall_cb(id, vcpu_syscall);
> +    qemu_plugin_register_vcpu_syscall_filter_cb(id, vcpu_syscall_filter);
> +    qemu_plugin_register_vcpu_tb_trans_cb(id, vcpu_tb_trans);
> +    return 0;
> +}
Re: [PATCH v5 4/7] tests/tcg: add test for qemu_plugin_set_pc API
Posted by Pierrick Bouvier 1 month, 1 week ago
On 3/2/26 6:58 AM, Florian Hofhammer wrote:
> On 02/03/2026 14:59, Florian Hofhammer wrote:
>> The test executes a non-existent syscall, which the syscall plugin
>> intercepts and redirects to a clean exit.
>> Due to architecture-specific quirks, the architecture-specific Makefiles
>> require setting specific compiler and linker flags in some cases.
> 
> Please disregard this patch and use the other version I just sent, I
> forgot to update the commit message for this one. My apologies for that!
>

Hi Florian, no worries.

What is the other version you sent?
Feel free to send a v6 for the series if needed, it's more clear than 
fix on individual commits.

>>
>> Signed-off-by: Florian Hofhammer <florian.hofhammer@epfl.ch>
>> ---
>>   MAINTAINERS                                   |   1 +
>>   tests/tcg/arm/Makefile.target                 |   6 +
>>   tests/tcg/multiarch/Makefile.target           |  17 ++-
>>   .../{ => plugin}/check-plugin-output.sh       |   0
>>   .../{ => plugin}/test-plugin-mem-access.c     |   0
>>   .../tcg/multiarch/plugin/test-plugin-set-pc.c | 140 ++++++++++++++++++
>>   tests/tcg/plugins/meson.build                 |   1 +
>>   tests/tcg/plugins/setpc.c                     | 120 +++++++++++++++
>>   8 files changed, 282 insertions(+), 3 deletions(-)
>>   rename tests/tcg/multiarch/{ => plugin}/check-plugin-output.sh (100%)
>>   rename tests/tcg/multiarch/{ => plugin}/test-plugin-mem-access.c (100%)
>>   create mode 100644 tests/tcg/multiarch/plugin/test-plugin-set-pc.c
>>   create mode 100644 tests/tcg/plugins/setpc.c
>>
Re: [PATCH v5 4/7] tests/tcg: add test for qemu_plugin_set_pc API
Posted by Florian Hofhammer 1 month, 1 week ago
On 02/03/2026 19:02, Pierrick Bouvier wrote:
> On 3/2/26 6:58 AM, Florian Hofhammer wrote:
>> On 02/03/2026 14:59, Florian Hofhammer wrote:
>>> The test executes a non-existent syscall, which the syscall plugin
>>> intercepts and redirects to a clean exit.
>>> Due to architecture-specific quirks, the architecture-specific Makefiles
>>> require setting specific compiler and linker flags in some cases.
>>
>> Please disregard this patch and use the other version I just sent, I
>> forgot to update the commit message for this one. My apologies for that!
>>
> 
> Hi Florian, no worries.
> 
> What is the other version you sent?
> Feel free to send a v6 for the series if needed, it's more clear than fix on individual commits.

Ack, going to send a v6.

> 
>>>
>>> Signed-off-by: Florian Hofhammer <florian.hofhammer@epfl.ch>
>>> ---
>>>   MAINTAINERS                                   |   1 +
>>>   tests/tcg/arm/Makefile.target                 |   6 +
>>>   tests/tcg/multiarch/Makefile.target           |  17 ++-
>>>   .../{ => plugin}/check-plugin-output.sh       |   0
>>>   .../{ => plugin}/test-plugin-mem-access.c     |   0
>>>   .../tcg/multiarch/plugin/test-plugin-set-pc.c | 140 ++++++++++++++++++
>>>   tests/tcg/plugins/meson.build                 |   1 +
>>>   tests/tcg/plugins/setpc.c                     | 120 +++++++++++++++
>>>   8 files changed, 282 insertions(+), 3 deletions(-)
>>>   rename tests/tcg/multiarch/{ => plugin}/check-plugin-output.sh (100%)
>>>   rename tests/tcg/multiarch/{ => plugin}/test-plugin-mem-access.c (100%)
>>>   create mode 100644 tests/tcg/multiarch/plugin/test-plugin-set-pc.c
>>>   create mode 100644 tests/tcg/plugins/setpc.c
>>>

Best regards,
Florian