[PATCH v6 4/7] tests/tcg: add tests 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 v6 4/7] tests/tcg: add tests for qemu_plugin_set_pc API
Posted by Florian Hofhammer 1 month, 1 week ago
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>
---
 MAINTAINERS                                        |   1 +
 tests/tcg/arm/Makefile.target                      |   6 +
 tests/tcg/multiarch/Makefile.target                |  17 ++-
 .../multiarch/{ => plugin}/check-plugin-output.sh  |   0
 .../{ => plugin}/test-plugin-mem-access.c          |   0
 tests/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(-)

diff --git a/MAINTAINERS b/MAINTAINERS
index 6698e5ff69..63c0af4d86 100644
--- a/MAINTAINERS
+++ b/MAINTAINERS
@@ -4104,6 +4104,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 v6 4/7] tests/tcg: add tests for qemu_plugin_set_pc API
Posted by Pierrick Bouvier 1 month, 1 week ago
On 3/3/26 5:07 AM, Florian Hofhammer wrote:
> 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>
> ---
>   MAINTAINERS                                        |   1 +
>   tests/tcg/arm/Makefile.target                      |   6 +
>   tests/tcg/multiarch/Makefile.target                |  17 ++-
>   .../multiarch/{ => plugin}/check-plugin-output.sh  |   0
>   .../{ => plugin}/test-plugin-mem-access.c          |   0
>   tests/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(-)
> 
> diff --git a/MAINTAINERS b/MAINTAINERS
> index 6698e5ff69..63c0af4d86 100644
> --- a/MAINTAINERS
> +++ b/MAINTAINERS
> @@ -4104,6 +4104,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
> +

Is that still needed?

>   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))

Test being compiled in O0, it should not need any noinline attribute.

> +#define NORETURN __attribute__((noreturn))

It's used in a single place, simply move the the attribute there, no 
need for another define.

> +
> +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;
> +

As mentioned on v4, you can simply use an external function for failing 
the test, which won't be tainted with noreturn attribute.
This way, you don't need any guard at all or assert(0), and resulting 
code is linear and easier to read.
It test completes, it all worked. If it crashes, something was wrong.

void panic(void)
{
      g_assert_not_reached();
}

void test_...() {
...
on_panic:
      panic();
after_panic:
      printf("Hello World\n");
}

> +/*
> + * 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);

Since you use two different syscall numbers, it's worth adding two 
defines, that you will duplicate in test and plugin.
Alternatively, you can use the first parameter of syscall to identify 
which kind of "service" you want from plugin, with defines duplicated 
between test and plugin again.
Feel free to pick the solution you prefer.

Reading 4095 the first time here felt like it was a typo mistake.

> +    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.
> +             */

This looks like an actual bug. Using a backtrace here, can you find 
which function extended it?
Parameter should be treated as unsigned everywhere, else something is 
really going wrong.

> +            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);
> +    }

Thanks for adding this case also!
So you'll definitely need a read for this precise test, to be able to 
keep a load. If it would work with a local variable, that's good, else, 
try with a local static (eventually marked volatile if it helps), and in 
last case, use a global variable.

> +}
> +
> +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).
> +         */

Too sad :)
It's not a problem to instrument all instructions though.

> +        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;
> +}
> 

Thanks for the update.
Test looks good, and minus the style nits reported, it looks quite ready.
It would be nice if we could sort the signed parameter extension also to 
avoid a hack on this.

Regards,
Pierrick
Re: [PATCH v6 4/7] tests/tcg: add tests for qemu_plugin_set_pc API
Posted by Florian Hofhammer 1 month, 1 week ago
On 03/03/2026 20:46, Pierrick Bouvier wrote:
> On 3/3/26 5:07 AM, Florian Hofhammer wrote:
>> 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>
>> ---
>>   MAINTAINERS                                        |   1 +
>>   tests/tcg/arm/Makefile.target                      |   6 +
>>   tests/tcg/multiarch/Makefile.target                |  17 ++-
>>   .../multiarch/{ => plugin}/check-plugin-output.sh  |   0
>>   .../{ => plugin}/test-plugin-mem-access.c          |   0
>>   tests/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(-)
>>
>> diff --git a/MAINTAINERS b/MAINTAINERS
>> index 6698e5ff69..63c0af4d86 100644
>> --- a/MAINTAINERS
>> +++ b/MAINTAINERS
>> @@ -4104,6 +4104,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
>> +
> 
> Is that still needed?

Yes, unfortunately. The compiler emits Thumb2 instructions where it
deems it appropriate but the addresses of functions/labels are still at
least 2-byte aligned and therefore don't have the bottom bit set.
Setting the PC to an even address therefore makes the vCPU think the
target is in arm32 mode, and it tries to execute the Thumb2 instructions
in arm32 mode. I'd either need to ensure that the targets are always in
Thumb2 mode and set the bottom bit myself, or ensure that everything is
in arm32 mode from the start.

>>   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))
> 
> Test being compiled in O0, it should not need any noinline attribute.

Ack, removed.

>> +#define NORETURN __attribute__((noreturn))
> 
> It's used in a single place, simply move the the attribute there, no need for another define.

Ditto.

>> +
>> +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;
>> +
> 
> As mentioned on v4, you can simply use an external function for failing the test, which won't be tainted with noreturn attribute.
> This way, you don't need any guard at all or assert(0), and resulting code is linear and easier to read.
> It test completes, it all worked. If it crashes, something was wrong.
> 
> void panic(void)
> {
>      g_assert_not_reached();
> }
> 
> void test_...() {
> ...
> on_panic:
>      panic();
> after_panic:
>      printf("Hello World\n");
> }

I've reworked it to look similar to this (without g_assert_not_reached()
though, because glib is not available in the test itself). Still
required some shuffling of stuff to make sure the compiler doesn't
optimize things away then :) Especially on hexagon, which is the only
target (by default) built with clang instead of gcc. Clang/LLVM seems to
be way more aggressive even at -O0 to do deadcode elimination and
inlining, so I had to add extra CFLAGS for the hexagon target.

>> + * 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);
> 
> Since you use two different syscall numbers, it's worth adding two defines, that you will duplicate in test and plugin.
> Alternatively, you can use the first parameter of syscall to identify which kind of "service" you want from plugin, with defines duplicated between test and plugin again.
> Feel free to pick the solution you prefer.
> 
> Reading 4095 the first time here felt like it was a typo mistake.

Ack, reworked this to be clearer.

> 
>> +    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.
>> +             */
> 
> This looks like an actual bug. Using a backtrace here, can you find which function extended it?
> Parameter should be treated as unsigned everywhere, else something is really going wrong.

tl;dr: register values are considered signed in the syscall handling
code, which seems incorrect to me. Details below.

This seems to be a bigger issue in the syscall handling code and I'm
surprised this never surfaced before. Generally, the CPUArchState struct
in target/*/cpu.h defines the registers as unsigned types. When a
syscall is encountered, the main cpu loop calls do_syscall() from
linux-user/syscall.c, which takes and returns abi_long values. abi_long
is defined as either int32_t or target_long in include/user/abitypes.h
and therefore a signed type, so the register values get converted from
unsigned to signed types here. do_syscall() passes those abi_long values
on, e.g., to record_syscall_start() and send_through_syscall_filters(),
which still take the register values in as abi_long. Those in turn
however pass the values on to qemu_plugin_vcpu_syscall() and
qemu_plugin_vcpu_syscall_filter(), respectively. Those two functions now
take uint64_t values for the arguments, so for 32-bit architectures the
conversion chain is uint32_t (CPUArchState) -> int32_t (do_syscall) ->
uint64_t (callbacks).

This seems to be some bigger refactoring changing all those abi_longs to
abi_ulongs. Would you like me to prepare a separate patch series for
this?

> 
>> +            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);
>> +    }
> 
> Thanks for adding this case also!
> So you'll definitely need a read for this precise test, to be able to keep a load. If it would work with a local variable, that's good, else, try with a local static (eventually marked volatile if it helps), and in last case, use a global variable.

Works fine with a local variable!

> 
>> +}
>> +
>> +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).
>> +         */
> 
> Too sad :)
> It's not a problem to instrument all instructions though.
> 
>> +        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;
>> +}
>>
> 
> Thanks for the update.
> Test looks good, and minus the style nits reported, it looks quite ready.
> It would be nice if we could sort the signed parameter extension also to avoid a hack on this.
> 
> Regards,
> Pierrick

Best regards,
Florian
Re: [PATCH v6 4/7] tests/tcg: add tests for qemu_plugin_set_pc API
Posted by Pierrick Bouvier 1 month, 1 week ago
On 3/4/26 7:35 AM, Florian Hofhammer wrote:
> On 03/03/2026 20:46, Pierrick Bouvier wrote:
>> On 3/3/26 5:07 AM, Florian Hofhammer wrote:
>>> 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>
>>> ---
>>>    MAINTAINERS                                        |   1 +
>>>    tests/tcg/arm/Makefile.target                      |   6 +
>>>    tests/tcg/multiarch/Makefile.target                |  17 ++-
>>>    .../multiarch/{ => plugin}/check-plugin-output.sh  |   0
>>>    .../{ => plugin}/test-plugin-mem-access.c          |   0
>>>    tests/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(-)
>>>

...

>>
>> As mentioned on v4, you can simply use an external function for failing the test, which won't be tainted with noreturn attribute.
>> This way, you don't need any guard at all or assert(0), and resulting code is linear and easier to read.
>> It test completes, it all worked. If it crashes, something was wrong.
>>
>> void panic(void)
>> {
>>       g_assert_not_reached();
>> }
>>
>> void test_...() {
>> ...
>> on_panic:
>>       panic();
>> after_panic:
>>       printf("Hello World\n");
>> }
> 
> I've reworked it to look similar to this (without g_assert_not_reached()
> though, because glib is not available in the test itself). Still
> required some shuffling of stuff to make sure the compiler doesn't
> optimize things away then :) Especially on hexagon, which is the only
> target (by default) built with clang instead of gcc. Clang/LLVM seems to
> be way more aggressive even at -O0 to do deadcode elimination and
> inlining, so I had to add extra CFLAGS for the hexagon target.
>
You're right, I forgot glib is not available there.
assert(0), exit for abort works instead of g_assert_not_reached().

Sounds good for hexagon target.
We can always update the test later if needed, and at least it stays 
simple. Thanks for doing this extra step.

Regards,
Pierrick
Re: [PATCH v6 4/7] tests/tcg: add tests for qemu_plugin_set_pc API
Posted by Florian Hofhammer 1 month, 1 week ago
On 04/03/2026 16:35, Florian Hofhammer wrote:
> On 03/03/2026 20:46, Pierrick Bouvier wrote:
>> On 3/3/26 5:07 AM, Florian Hofhammer wrote:
>>> 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>
>>> ---
>>>   MAINTAINERS                                        |   1 +
>>>   tests/tcg/arm/Makefile.target                      |   6 +
>>>   tests/tcg/multiarch/Makefile.target                |  17 ++-
>>>   .../multiarch/{ => plugin}/check-plugin-output.sh  |   0
>>>   .../{ => plugin}/test-plugin-mem-access.c          |   0
>>>   tests/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(-)
>>>
>>> diff --git a/MAINTAINERS b/MAINTAINERS
>>> index 6698e5ff69..63c0af4d86 100644
>>> --- a/MAINTAINERS
>>> +++ b/MAINTAINERS
>>> @@ -4104,6 +4104,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
>>> +
>>
>> Is that still needed?
> 
> Yes, unfortunately. The compiler emits Thumb2 instructions where it
> deems it appropriate but the addresses of functions/labels are still at
> least 2-byte aligned and therefore don't have the bottom bit set.
> Setting the PC to an even address therefore makes the vCPU think the
> target is in arm32 mode, and it tries to execute the Thumb2 instructions
> in arm32 mode. I'd either need to ensure that the targets are always in
> Thumb2 mode and set the bottom bit myself, or ensure that everything is
> in arm32 mode from the start.
> 
>>>   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))
>>
>> Test being compiled in O0, it should not need any noinline attribute.
> 
> Ack, removed.
> 
>>> +#define NORETURN __attribute__((noreturn))
>>
>> It's used in a single place, simply move the the attribute there, no need for another define.
> 
> Ditto.
> 
>>> +
>>> +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;
>>> +
>>
>> As mentioned on v4, you can simply use an external function for failing the test, which won't be tainted with noreturn attribute.
>> This way, you don't need any guard at all or assert(0), and resulting code is linear and easier to read.
>> It test completes, it all worked. If it crashes, something was wrong.
>>
>> void panic(void)
>> {
>>      g_assert_not_reached();
>> }
>>
>> void test_...() {
>> ...
>> on_panic:
>>      panic();
>> after_panic:
>>      printf("Hello World\n");
>> }
> 
> I've reworked it to look similar to this (without g_assert_not_reached()
> though, because glib is not available in the test itself). Still
> required some shuffling of stuff to make sure the compiler doesn't
> optimize things away then :) Especially on hexagon, which is the only
> target (by default) built with clang instead of gcc. Clang/LLVM seems to
> be way more aggressive even at -O0 to do deadcode elimination and
> inlining, so I had to add extra CFLAGS for the hexagon target.
> 
>>> + * 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);
>>
>> Since you use two different syscall numbers, it's worth adding two defines, that you will duplicate in test and plugin.
>> Alternatively, you can use the first parameter of syscall to identify which kind of "service" you want from plugin, with defines duplicated between test and plugin again.
>> Feel free to pick the solution you prefer.
>>
>> Reading 4095 the first time here felt like it was a typo mistake.
> 
> Ack, reworked this to be clearer.
> 
>>
>>> +    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.
>>> +             */
>>
>> This looks like an actual bug. Using a backtrace here, can you find which function extended it?
>> Parameter should be treated as unsigned everywhere, else something is really going wrong.
> 
> tl;dr: register values are considered signed in the syscall handling
> code, which seems incorrect to me. Details below.
> 
> This seems to be a bigger issue in the syscall handling code and I'm
> surprised this never surfaced before. Generally, the CPUArchState struct
> in target/*/cpu.h defines the registers as unsigned types. When a
> syscall is encountered, the main cpu loop calls do_syscall() from
> linux-user/syscall.c, which takes and returns abi_long values. abi_long
> is defined as either int32_t or target_long in include/user/abitypes.h
> and therefore a signed type, so the register values get converted from
> unsigned to signed types here. do_syscall() passes those abi_long values
> on, e.g., to record_syscall_start() and send_through_syscall_filters(),
> which still take the register values in as abi_long. Those in turn
> however pass the values on to qemu_plugin_vcpu_syscall() and
> qemu_plugin_vcpu_syscall_filter(), respectively. Those two functions now
> take uint64_t values for the arguments, so for 32-bit architectures the
> conversion chain is uint32_t (CPUArchState) -> int32_t (do_syscall) ->
> uint64_t (callbacks).
> 
> This seems to be some bigger refactoring changing all those abi_longs to
> abi_ulongs. Would you like me to prepare a separate patch series for
> this?

As a follow-up on this, changing this seems to be a bit more complicated
than I initially thought. The arguments could be simply made unsigned,
but the return value (which is also just a register content and should
therefore be unsigned if I understand correctly) can't easily be made
unsigned. The per-target main loop relies on the return value of
do_syscall() to be signed to determine whether one of the QEMU-specific
errors (e.g., QEMU_ERESTARTSYS) was returned.

>>
>>> +            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);
>>> +    }
>>
>> Thanks for adding this case also!
>> So you'll definitely need a read for this precise test, to be able to keep a load. If it would work with a local variable, that's good, else, try with a local static (eventually marked volatile if it helps), and in last case, use a global variable.
> 
> Works fine with a local variable!
> 
>>
>>> +}
>>> +
>>> +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).
>>> +         */
>>
>> Too sad :)
>> It's not a problem to instrument all instructions though.
>>
>>> +        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;
>>> +}
>>>
>>
>> Thanks for the update.
>> Test looks good, and minus the style nits reported, it looks quite ready.
>> It would be nice if we could sort the signed parameter extension also to avoid a hack on this.
>>
>> Regards,
>> Pierrick
> 
> Best regards,
> Florian
Re: [PATCH v6 4/7] tests/tcg: add tests for qemu_plugin_set_pc API
Posted by Florian Hofhammer 1 month, 1 week ago
On 04/03/2026 16:45, Florian Hofhammer wrote:
> On 04/03/2026 16:35, Florian Hofhammer wrote:
>> On 03/03/2026 20:46, Pierrick Bouvier wrote:
>>> On 3/3/26 5:07 AM, Florian Hofhammer wrote:
>>>> 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>
>>>> ---
>>>>   MAINTAINERS                                        |   1 +
>>>>   tests/tcg/arm/Makefile.target                      |   6 +
>>>>   tests/tcg/multiarch/Makefile.target                |  17 ++-
>>>>   .../multiarch/{ => plugin}/check-plugin-output.sh  |   0
>>>>   .../{ => plugin}/test-plugin-mem-access.c          |   0
>>>>   tests/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(-)
>>>>
>>>> diff --git a/MAINTAINERS b/MAINTAINERS
>>>> index 6698e5ff69..63c0af4d86 100644
>>>> --- a/MAINTAINERS
>>>> +++ b/MAINTAINERS
>>>> @@ -4104,6 +4104,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
>>>> +
>>>
>>> Is that still needed?
>>
>> Yes, unfortunately. The compiler emits Thumb2 instructions where it
>> deems it appropriate but the addresses of functions/labels are still at
>> least 2-byte aligned and therefore don't have the bottom bit set.
>> Setting the PC to an even address therefore makes the vCPU think the
>> target is in arm32 mode, and it tries to execute the Thumb2 instructions
>> in arm32 mode. I'd either need to ensure that the targets are always in
>> Thumb2 mode and set the bottom bit myself, or ensure that everything is
>> in arm32 mode from the start.
>>
>>>>   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))
>>>
>>> Test being compiled in O0, it should not need any noinline attribute.
>>
>> Ack, removed.
>>
>>>> +#define NORETURN __attribute__((noreturn))
>>>
>>> It's used in a single place, simply move the the attribute there, no need for another define.
>>
>> Ditto.
>>
>>>> +
>>>> +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;
>>>> +
>>>
>>> As mentioned on v4, you can simply use an external function for failing the test, which won't be tainted with noreturn attribute.
>>> This way, you don't need any guard at all or assert(0), and resulting code is linear and easier to read.
>>> It test completes, it all worked. If it crashes, something was wrong.
>>>
>>> void panic(void)
>>> {
>>>      g_assert_not_reached();
>>> }
>>>
>>> void test_...() {
>>> ...
>>> on_panic:
>>>      panic();
>>> after_panic:
>>>      printf("Hello World\n");
>>> }
>>
>> I've reworked it to look similar to this (without g_assert_not_reached()
>> though, because glib is not available in the test itself). Still
>> required some shuffling of stuff to make sure the compiler doesn't
>> optimize things away then :) Especially on hexagon, which is the only
>> target (by default) built with clang instead of gcc. Clang/LLVM seems to
>> be way more aggressive even at -O0 to do deadcode elimination and
>> inlining, so I had to add extra CFLAGS for the hexagon target.
>>
>>>> + * 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);
>>>
>>> Since you use two different syscall numbers, it's worth adding two defines, that you will duplicate in test and plugin.
>>> Alternatively, you can use the first parameter of syscall to identify which kind of "service" you want from plugin, with defines duplicated between test and plugin again.
>>> Feel free to pick the solution you prefer.
>>>
>>> Reading 4095 the first time here felt like it was a typo mistake.
>>
>> Ack, reworked this to be clearer.
>>
>>>
>>>> +    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.
>>>> +             */
>>>
>>> This looks like an actual bug. Using a backtrace here, can you find which function extended it?
>>> Parameter should be treated as unsigned everywhere, else something is really going wrong.
>>
>> tl;dr: register values are considered signed in the syscall handling
>> code, which seems incorrect to me. Details below.
>>
>> This seems to be a bigger issue in the syscall handling code and I'm
>> surprised this never surfaced before. Generally, the CPUArchState struct
>> in target/*/cpu.h defines the registers as unsigned types. When a
>> syscall is encountered, the main cpu loop calls do_syscall() from
>> linux-user/syscall.c, which takes and returns abi_long values. abi_long
>> is defined as either int32_t or target_long in include/user/abitypes.h
>> and therefore a signed type, so the register values get converted from
>> unsigned to signed types here. do_syscall() passes those abi_long values
>> on, e.g., to record_syscall_start() and send_through_syscall_filters(),
>> which still take the register values in as abi_long. Those in turn
>> however pass the values on to qemu_plugin_vcpu_syscall() and
>> qemu_plugin_vcpu_syscall_filter(), respectively. Those two functions now
>> take uint64_t values for the arguments, so for 32-bit architectures the
>> conversion chain is uint32_t (CPUArchState) -> int32_t (do_syscall) ->
>> uint64_t (callbacks).
>>
>> This seems to be some bigger refactoring changing all those abi_longs to
>> abi_ulongs. Would you like me to prepare a separate patch series for
>> this?
> 
> As a follow-up on this, changing this seems to be a bit more complicated
> than I initially thought. The arguments could be simply made unsigned,
> but the return value (which is also just a register content and should
> therefore be unsigned if I understand correctly) can't easily be made
> unsigned. The per-target main loop relies on the return value of
> do_syscall() to be signed to determine whether one of the QEMU-specific
> errors (e.g., QEMU_ERESTARTSYS) was returned.

Note: some architectures already define the return value as unsigned in
the main cpu loop and implicitly cast the return value of do_syscall().
Given that in theory, a syscall's return value could always coincide
with the values of QEMU's special errors, I'm wondering whether it would
make sense to make do_syscall()'s return value unsigned and signal those
special error codes out-of-band instead of in the syscall return value,
e.g., either via a separate pointer argument to do_syscall() or by
making do_syscall() return a struct containing both the return value and
a potential error code.

But maybe I'm getting ahead of myself here, please let me know what you
think!

> 
>>>
>>>> +            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);
>>>> +    }
>>>
>>> Thanks for adding this case also!
>>> So you'll definitely need a read for this precise test, to be able to keep a load. If it would work with a local variable, that's good, else, try with a local static (eventually marked volatile if it helps), and in last case, use a global variable.
>>
>> Works fine with a local variable!
>>
>>>
>>>> +}
>>>> +
>>>> +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).
>>>> +         */
>>>
>>> Too sad :)
>>> It's not a problem to instrument all instructions though.
>>>
>>>> +        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;
>>>> +}
>>>>
>>>
>>> Thanks for the update.
>>> Test looks good, and minus the style nits reported, it looks quite ready.
>>> It would be nice if we could sort the signed parameter extension also to avoid a hack on this.
>>>
>>> Regards,
>>> Pierrick
>>
>> Best regards,
>> Florian
Re: [PATCH v6 4/7] tests/tcg: add tests for qemu_plugin_set_pc API
Posted by Pierrick Bouvier 1 month, 1 week ago
On 3/4/26 8:18 AM, Florian Hofhammer wrote:
>>>>> +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.
>>>>> +             */
>>>>
>>>> This looks like an actual bug. Using a backtrace here, can you find which function extended it?
>>>> Parameter should be treated as unsigned everywhere, else something is really going wrong.
>>>
>>> tl;dr: register values are considered signed in the syscall handling
>>> code, which seems incorrect to me. Details below.
>>>
>>> This seems to be a bigger issue in the syscall handling code and I'm
>>> surprised this never surfaced before. Generally, the CPUArchState struct
>>> in target/*/cpu.h defines the registers as unsigned types. When a
>>> syscall is encountered, the main cpu loop calls do_syscall() from
>>> linux-user/syscall.c, which takes and returns abi_long values. abi_long
>>> is defined as either int32_t or target_long in include/user/abitypes.h
>>> and therefore a signed type, so the register values get converted from
>>> unsigned to signed types here. do_syscall() passes those abi_long values
>>> on, e.g., to record_syscall_start() and send_through_syscall_filters(),
>>> which still take the register values in as abi_long. Those in turn
>>> however pass the values on to qemu_plugin_vcpu_syscall() and
>>> qemu_plugin_vcpu_syscall_filter(), respectively. Those two functions now
>>> take uint64_t values for the arguments, so for 32-bit architectures the
>>> conversion chain is uint32_t (CPUArchState) -> int32_t (do_syscall) ->
>>> uint64_t (callbacks).
>>>
>>> This seems to be some bigger refactoring changing all those abi_longs to
>>> abi_ulongs. Would you like me to prepare a separate patch series for
>>> this?
>>

Indeed, they are signed in linux-user, so maybe my original assumption 
that it *should* be unsigned is wrong. Let me take a look to see what 
should be the correct semantic here.

>> As a follow-up on this, changing this seems to be a bit more complicated
>> than I initially thought. The arguments could be simply made unsigned,
>> but the return value (which is also just a register content and should
>> therefore be unsigned if I understand correctly) can't easily be made
>> unsigned. The per-target main loop relies on the return value of
>> do_syscall() to be signed to determine whether one of the QEMU-specific
>> errors (e.g., QEMU_ERESTARTSYS) was returned.
> 
> Note: some architectures already define the return value as unsigned in
> the main cpu loop and implicitly cast the return value of do_syscall().
> Given that in theory, a syscall's return value could always coincide
> with the values of QEMU's special errors, I'm wondering whether it would
> make sense to make do_syscall()'s return value unsigned and signal those
> special error codes out-of-band instead of in the syscall return value,
> e.g., either via a separate pointer argument to do_syscall() or by
> making do_syscall() return a struct containing both the return value and
> a potential error code.
>

It could be a solution. I'm just a little bit relunctant to make such a 
change now because we have limited time (soft freeze is coming next 
week), so not the right time to make such deep changes and... it has 
been working like that for long time.

> But maybe I'm getting ahead of myself here, please let me know what you
> think!
>
Thanks for your very good analysis. I'll come back once I have more 
information about what should be the correct fix.

Meanwhile, and because we are close from softfreeze, just keep the 
current hack in the test. This way, we can focus on sending your series 
and the additional comment patch you sent.

Thanks,
Pierrick
Re: [PATCH v6 4/7] tests/tcg: add tests for qemu_plugin_set_pc API
Posted by Pierrick Bouvier 1 month, 1 week ago
On 3/4/26 9:56 AM, Pierrick Bouvier wrote:
> On 3/4/26 8:18 AM, Florian Hofhammer wrote:
>>>>>> +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.
>>>>>> +             */
>>>>>
>>>>> This looks like an actual bug. Using a backtrace here, can you find which function extended it?
>>>>> Parameter should be treated as unsigned everywhere, else something is really going wrong.
>>>>
>>>> tl;dr: register values are considered signed in the syscall handling
>>>> code, which seems incorrect to me. Details below.
>>>>
>>>> This seems to be a bigger issue in the syscall handling code and I'm
>>>> surprised this never surfaced before. Generally, the CPUArchState struct
>>>> in target/*/cpu.h defines the registers as unsigned types. When a
>>>> syscall is encountered, the main cpu loop calls do_syscall() from
>>>> linux-user/syscall.c, which takes and returns abi_long values. abi_long
>>>> is defined as either int32_t or target_long in include/user/abitypes.h
>>>> and therefore a signed type, so the register values get converted from
>>>> unsigned to signed types here. do_syscall() passes those abi_long values
>>>> on, e.g., to record_syscall_start() and send_through_syscall_filters(),
>>>> which still take the register values in as abi_long. Those in turn
>>>> however pass the values on to qemu_plugin_vcpu_syscall() and
>>>> qemu_plugin_vcpu_syscall_filter(), respectively. Those two functions now
>>>> take uint64_t values for the arguments, so for 32-bit architectures the
>>>> conversion chain is uint32_t (CPUArchState) -> int32_t (do_syscall) ->
>>>> uint64_t (callbacks).
>>>>
>>>> This seems to be some bigger refactoring changing all those abi_longs to
>>>> abi_ulongs. Would you like me to prepare a separate patch series for
>>>> this?
>>>
> 
> Indeed, they are signed in linux-user, so maybe my original assumption
> that it *should* be unsigned is wrong. Let me take a look to see what
> should be the correct semantic here.
> 
>>> As a follow-up on this, changing this seems to be a bit more complicated
>>> than I initially thought. The arguments could be simply made unsigned,
>>> but the return value (which is also just a register content and should
>>> therefore be unsigned if I understand correctly) can't easily be made
>>> unsigned. The per-target main loop relies on the return value of
>>> do_syscall() to be signed to determine whether one of the QEMU-specific
>>> errors (e.g., QEMU_ERESTARTSYS) was returned.
>>
>> Note: some architectures already define the return value as unsigned in
>> the main cpu loop and implicitly cast the return value of do_syscall().
>> Given that in theory, a syscall's return value could always coincide
>> with the values of QEMU's special errors, I'm wondering whether it would
>> make sense to make do_syscall()'s return value unsigned and signal those
>> special error codes out-of-band instead of in the syscall return value,
>> e.g., either via a separate pointer argument to do_syscall() or by
>> making do_syscall() return a struct containing both the return value and
>> a potential error code.
>>
> 
> It could be a solution. I'm just a little bit relunctant to make such a
> change now because we have limited time (soft freeze is coming next
> week), so not the right time to make such deep changes and... it has
> been working like that for long time.
> 
>> But maybe I'm getting ahead of myself here, please let me know what you
>> think!
>>
> Thanks for your very good analysis. I'll come back once I have more
> information about what should be the correct fix.
> 
> Meanwhile, and because we are close from softfreeze, just keep the
> current hack in the test. This way, we can focus on sending your series
> and the additional comment patch you sent.
> 
> Thanks,
> Pierrick

The attached patch fixes the problem by clamping syscall arguments for 
32-bit targets. Return type has to be signed anyway, so no change is 
needed on this side.

You can add it as the first patch on your next version, and remove the 
hack checking the highest bit in this test, now that the problem is solved.

Regards,
Pierrick
Re: [PATCH v6 4/7] tests/tcg: add tests for qemu_plugin_set_pc API
Posted by Florian Hofhammer 1 month, 1 week ago
On 04/03/2026 20:38, Pierrick Bouvier wrote:
> On 3/4/26 9:56 AM, Pierrick Bouvier wrote:
>> On 3/4/26 8:18 AM, Florian Hofhammer wrote:
>>>>>>> +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.
>>>>>>> +             */
>>>>>>
>>>>>> This looks like an actual bug. Using a backtrace here, can you find which function extended it?
>>>>>> Parameter should be treated as unsigned everywhere, else something is really going wrong.
>>>>>
>>>>> tl;dr: register values are considered signed in the syscall handling
>>>>> code, which seems incorrect to me. Details below.
>>>>>
>>>>> This seems to be a bigger issue in the syscall handling code and I'm
>>>>> surprised this never surfaced before. Generally, the CPUArchState struct
>>>>> in target/*/cpu.h defines the registers as unsigned types. When a
>>>>> syscall is encountered, the main cpu loop calls do_syscall() from
>>>>> linux-user/syscall.c, which takes and returns abi_long values. abi_long
>>>>> is defined as either int32_t or target_long in include/user/abitypes.h
>>>>> and therefore a signed type, so the register values get converted from
>>>>> unsigned to signed types here. do_syscall() passes those abi_long values
>>>>> on, e.g., to record_syscall_start() and send_through_syscall_filters(),
>>>>> which still take the register values in as abi_long. Those in turn
>>>>> however pass the values on to qemu_plugin_vcpu_syscall() and
>>>>> qemu_plugin_vcpu_syscall_filter(), respectively. Those two functions now
>>>>> take uint64_t values for the arguments, so for 32-bit architectures the
>>>>> conversion chain is uint32_t (CPUArchState) -> int32_t (do_syscall) ->
>>>>> uint64_t (callbacks).
>>>>>
>>>>> This seems to be some bigger refactoring changing all those abi_longs to
>>>>> abi_ulongs. Would you like me to prepare a separate patch series for
>>>>> this?
>>>>
>>
>> Indeed, they are signed in linux-user, so maybe my original assumption
>> that it *should* be unsigned is wrong. Let me take a look to see what
>> should be the correct semantic here.
>>
>>>> As a follow-up on this, changing this seems to be a bit more complicated
>>>> than I initially thought. The arguments could be simply made unsigned,
>>>> but the return value (which is also just a register content and should
>>>> therefore be unsigned if I understand correctly) can't easily be made
>>>> unsigned. The per-target main loop relies on the return value of
>>>> do_syscall() to be signed to determine whether one of the QEMU-specific
>>>> errors (e.g., QEMU_ERESTARTSYS) was returned.
>>>
>>> Note: some architectures already define the return value as unsigned in
>>> the main cpu loop and implicitly cast the return value of do_syscall().
>>> Given that in theory, a syscall's return value could always coincide
>>> with the values of QEMU's special errors, I'm wondering whether it would
>>> make sense to make do_syscall()'s return value unsigned and signal those
>>> special error codes out-of-band instead of in the syscall return value,
>>> e.g., either via a separate pointer argument to do_syscall() or by
>>> making do_syscall() return a struct containing both the return value and
>>> a potential error code.
>>>
>>
>> It could be a solution. I'm just a little bit relunctant to make such a
>> change now because we have limited time (soft freeze is coming next
>> week), so not the right time to make such deep changes and... it has
>> been working like that for long time.
>>
>>> But maybe I'm getting ahead of myself here, please let me know what you
>>> think!
>>>
>> Thanks for your very good analysis. I'll come back once I have more
>> information about what should be the correct fix.
>>
>> Meanwhile, and because we are close from softfreeze, just keep the
>> current hack in the test. This way, we can focus on sending your series
>> and the additional comment patch you sent.
>>
>> Thanks,
>> Pierrick
> 
> The attached patch fixes the problem by clamping syscall arguments for 32-bit targets. Return type has to be signed anyway, so no change is needed on this side.
> 
> You can add it as the first patch on your next version, and remove the hack checking the highest bit in this test, now that the problem is solved.
> 
> Regards,
> Pierrick

Ack, thanks for the patch!

Best regards,
Florian