[PULL 06/20] linux-user: add plugin API to filter syscalls

Pierrick Bouvier posted 20 patches 1 week, 5 days ago
Maintainers: "Alex Bennée" <alex.bennee@linaro.org>, Pierrick Bouvier <pierrick.bouvier@linaro.org>, Alexandre Iooss <erdnaxe@crans.org>, Mahmoud Mandour <ma.mandourr@gmail.com>, Richard Henderson <richard.henderson@linaro.org>, Paolo Bonzini <pbonzini@redhat.com>, Riku Voipio <riku.voipio@iki.fi>, Laurent Vivier <laurent@vivier.eu>, "Marc-André Lureau" <marcandre.lureau@redhat.com>, "Daniel P. Berrangé" <berrange@redhat.com>, "Philippe Mathieu-Daudé" <philmd@linaro.org>, Kostiantyn Kostiuk <kkostiuk@redhat.com>, Michael Roth <michael.roth@amd.com>
[PULL 06/20] linux-user: add plugin API to filter syscalls
Posted by Pierrick Bouvier 1 week, 5 days ago
From: Ziyang Zhang <functioner@sjtu.edu.cn>

This commit adds a syscall filter API to the TCG plugin API set.
Plugins can register a filter callback to QEMU to decide whether
to intercept a syscall, process it and bypass the QEMU syscall
handler.

Signed-off-by: Ziyang Zhang <functioner@sjtu.edu.cn>
Co-authored-by: Mingyuan Xia <xiamy@ultrarisc.com>
Reviewed-by: Pierrick Bouvier <pierrick.bouvier@linaro.org>
Signed-off-by: Pierrick Bouvier <pierrick.bouvier@linaro.org>
---
 include/qemu/plugin-event.h  |  1 +
 include/qemu/plugin.h        | 33 +++++++++++++++++++++++---------
 include/qemu/qemu-plugin.h   | 32 +++++++++++++++++++++++++++++++
 include/user/syscall-trace.h | 17 +++++++++++++++++
 linux-user/syscall.c         |  7 +++++--
 plugins/api.c                |  7 +++++++
 plugins/core.c               | 37 ++++++++++++++++++++++++++++++++++++
 7 files changed, 123 insertions(+), 11 deletions(-)

diff --git a/include/qemu/plugin-event.h b/include/qemu/plugin-event.h
index 1100dae2123..7f3c5927f1f 100644
--- a/include/qemu/plugin-event.h
+++ b/include/qemu/plugin-event.h
@@ -23,6 +23,7 @@ enum qemu_plugin_event {
     QEMU_PLUGIN_EV_VCPU_INTERRUPT,
     QEMU_PLUGIN_EV_VCPU_EXCEPTION,
     QEMU_PLUGIN_EV_VCPU_HOSTCALL,
+    QEMU_PLUGIN_EV_VCPU_SYSCALL_FILTER,
     QEMU_PLUGIN_EV_MAX, /* total number of plugin events we support */
 };
 
diff --git a/include/qemu/plugin.h b/include/qemu/plugin.h
index cea0a68858b..91df1e78d2c 100644
--- a/include/qemu/plugin.h
+++ b/include/qemu/plugin.h
@@ -55,15 +55,16 @@ void qemu_plugin_opt_parse(const char *optstr, QemuPluginList *head);
 int qemu_plugin_load_list(QemuPluginList *head, Error **errp);
 
 union qemu_plugin_cb_sig {
-    qemu_plugin_simple_cb_t          simple;
-    qemu_plugin_udata_cb_t           udata;
-    qemu_plugin_vcpu_simple_cb_t     vcpu_simple;
-    qemu_plugin_vcpu_udata_cb_t      vcpu_udata;
-    qemu_plugin_vcpu_discon_cb_t     vcpu_discon;
-    qemu_plugin_vcpu_tb_trans_cb_t   vcpu_tb_trans;
-    qemu_plugin_vcpu_mem_cb_t        vcpu_mem;
-    qemu_plugin_vcpu_syscall_cb_t    vcpu_syscall;
-    qemu_plugin_vcpu_syscall_ret_cb_t vcpu_syscall_ret;
+    qemu_plugin_simple_cb_t              simple;
+    qemu_plugin_udata_cb_t               udata;
+    qemu_plugin_vcpu_simple_cb_t         vcpu_simple;
+    qemu_plugin_vcpu_udata_cb_t          vcpu_udata;
+    qemu_plugin_vcpu_discon_cb_t         vcpu_discon;
+    qemu_plugin_vcpu_tb_trans_cb_t       vcpu_tb_trans;
+    qemu_plugin_vcpu_mem_cb_t            vcpu_mem;
+    qemu_plugin_vcpu_syscall_cb_t        vcpu_syscall;
+    qemu_plugin_vcpu_syscall_ret_cb_t    vcpu_syscall_ret;
+    qemu_plugin_vcpu_syscall_filter_cb_t vcpu_syscall_filter;
     void *generic;
 };
 
@@ -169,6 +170,11 @@ qemu_plugin_vcpu_syscall(CPUState *cpu, 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);
 void qemu_plugin_vcpu_syscall_ret(CPUState *cpu, int64_t num, int64_t ret);
+bool
+qemu_plugin_vcpu_syscall_filter(CPUState *cpu, 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 *ret);
 
 void qemu_plugin_vcpu_mem_cb(CPUState *cpu, uint64_t vaddr,
                              uint64_t value_low,
@@ -280,6 +286,15 @@ static inline
 void qemu_plugin_vcpu_syscall_ret(CPUState *cpu, int64_t num, int64_t ret)
 { }
 
+static inline bool
+qemu_plugin_vcpu_syscall_filter(CPUState *cpu, 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 *ret)
+{
+    return false;
+}
+
 static inline void qemu_plugin_vcpu_mem_cb(CPUState *cpu, uint64_t vaddr,
                                            uint64_t value_low,
                                            uint64_t value_high,
diff --git a/include/qemu/qemu-plugin.h b/include/qemu/qemu-plugin.h
index 60de4fdd3fa..84976fd67dd 100644
--- a/include/qemu/qemu-plugin.h
+++ b/include/qemu/qemu-plugin.h
@@ -798,6 +798,33 @@ typedef void
                                  uint64_t a3, uint64_t a4, uint64_t a5,
                                  uint64_t a6, uint64_t a7, uint64_t a8);
 
+/**
+ * typedef qemu_plugin_vcpu_syscall_filter_cb_t - vCPU syscall filter callback
+ * function type
+ * @id: plugin id
+ * @vcpu_index: the executing vCPU
+ * @num: the syscall number
+ * @a1: the 1st syscall argument
+ * @a2: the 2nd syscall argument
+ * @a3: the 3rd syscall argument
+ * @a4: the 4th syscall argument
+ * @a5: the 5th syscall argument
+ * @a6: the 6th syscall argument
+ * @a7: the 7th syscall argument
+ * @a8: the 8th syscall argument
+ * @sysret: reference of the syscall return value, must set this if filtered
+ *
+ * Returns true if you want to filter this syscall (i.e. stop it being
+ * handled further), otherwise returns false.
+ */
+typedef bool
+(*qemu_plugin_vcpu_syscall_filter_cb_t)(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);
+
 QEMU_PLUGIN_API
 void qemu_plugin_register_vcpu_syscall_cb(qemu_plugin_id_t id,
                                           qemu_plugin_vcpu_syscall_cb_t cb);
@@ -811,6 +838,11 @@ void
 qemu_plugin_register_vcpu_syscall_ret_cb(qemu_plugin_id_t id,
                                          qemu_plugin_vcpu_syscall_ret_cb_t cb);
 
+QEMU_PLUGIN_API
+void
+qemu_plugin_register_vcpu_syscall_filter_cb(qemu_plugin_id_t id,
+                                            qemu_plugin_vcpu_syscall_filter_cb_t cb);
+
 
 /**
  * qemu_plugin_insn_disas() - return disassembly string for instruction
diff --git a/include/user/syscall-trace.h b/include/user/syscall-trace.h
index 9bd7ca19c84..4ee6c4e5134 100644
--- a/include/user/syscall-trace.h
+++ b/include/user/syscall-trace.h
@@ -39,5 +39,22 @@ static inline void record_syscall_return(CPUState *cpu, int num, abi_long ret)
     gdb_syscall_return(cpu, num);
 }
 
+static bool send_through_syscall_filters(CPUState *cpu, int num,
+                                         abi_long arg1, abi_long arg2,
+                                         abi_long arg3, abi_long arg4,
+                                         abi_long arg5, abi_long arg6,
+                                         abi_long arg7, abi_long arg8,
+                                         abi_long *sysret)
+{
+    uint64_t sysret64 = 0;
+    bool filtered = qemu_plugin_vcpu_syscall_filter(cpu, num, arg1, arg2,
+                                                    arg3, arg4, arg5, arg6,
+                                                    arg7, arg8, &sysret64);
+    if (filtered) {
+        *sysret = sysret64;
+    }
+    return filtered;
+}
+
 
 #endif /* SYSCALL_TRACE_H */
diff --git a/linux-user/syscall.c b/linux-user/syscall.c
index 3944004568f..c904febd9bb 100644
--- a/linux-user/syscall.c
+++ b/linux-user/syscall.c
@@ -14368,8 +14368,11 @@ abi_long do_syscall(CPUArchState *cpu_env, int num, abi_long arg1,
         print_syscall(cpu_env, num, arg1, arg2, arg3, arg4, arg5, arg6);
     }
 
-    ret = do_syscall1(cpu_env, num, arg1, arg2, arg3, arg4,
-                      arg5, arg6, arg7, arg8);
+    if (!send_through_syscall_filters(cpu, num, arg1, arg2, arg3, arg4, arg5,
+                                      arg6, arg7, arg8, &ret)) {
+        ret = do_syscall1(cpu_env, num, arg1, arg2, arg3, arg4,
+                          arg5, arg6, arg7, arg8);
+    }
 
     if (unlikely(qemu_loglevel_mask(LOG_STRACE))) {
         print_syscall_ret(cpu_env, num, ret, arg1, arg2,
diff --git a/plugins/api.c b/plugins/api.c
index eac04cc1f6b..478d0c88890 100644
--- a/plugins/api.c
+++ b/plugins/api.c
@@ -208,6 +208,13 @@ qemu_plugin_register_vcpu_syscall_ret_cb(qemu_plugin_id_t id,
     plugin_register_cb(id, QEMU_PLUGIN_EV_VCPU_SYSCALL_RET, cb);
 }
 
+void
+qemu_plugin_register_vcpu_syscall_filter_cb(qemu_plugin_id_t id,
+                                            qemu_plugin_vcpu_syscall_filter_cb_t cb)
+{
+    plugin_register_cb(id, QEMU_PLUGIN_EV_VCPU_SYSCALL_FILTER, cb);
+}
+
 /*
  * Plugin Queries
  *
diff --git a/plugins/core.c b/plugins/core.c
index b4b783008f7..85fabf9ec81 100644
--- a/plugins/core.c
+++ b/plugins/core.c
@@ -564,6 +564,43 @@ void qemu_plugin_vcpu_syscall_ret(CPUState *cpu, int64_t num, int64_t ret)
     }
 }
 
+/*
+ * Disable CFI checks.
+ * The callback function has been loaded from an external library so we do not
+ * have type information
+ */
+QEMU_DISABLE_CFI
+bool
+qemu_plugin_vcpu_syscall_filter(CPUState *cpu, 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)
+{
+    struct qemu_plugin_cb *cb, *next;
+    enum qemu_plugin_event ev = QEMU_PLUGIN_EV_VCPU_SYSCALL_FILTER;
+    bool filtered = false;
+
+    if (!test_bit(ev, cpu->plugin_state->event_mask)) {
+        return false;
+    }
+
+    qemu_plugin_set_cb_flags(cpu, QEMU_PLUGIN_CB_RW_REGS);
+
+    QLIST_FOREACH_SAFE_RCU(cb, &plugin.cb_lists[ev], entry, next) {
+        qemu_plugin_vcpu_syscall_filter_cb_t func = cb->f.vcpu_syscall_filter;
+
+        if (func(cb->ctx->id, cpu->cpu_index, num, a1, a2, a3, a4,
+                 a5, a6, a7, a8, sysret)) {
+            filtered = true;
+            break;
+        }
+    }
+
+    qemu_plugin_set_cb_flags(cpu, QEMU_PLUGIN_CB_NO_REGS);
+
+    return filtered;
+}
+
 void qemu_plugin_vcpu_idle_cb(CPUState *cpu)
 {
     /* idle and resume cb may be called before init, ignore in this case */
-- 
2.47.3
Re: [PULL 06/20] linux-user: add plugin API to filter syscalls
Posted by Richard Henderson 1 week, 5 days ago
On 1/29/26 12:31, Pierrick Bouvier wrote:
> --- a/include/user/syscall-trace.h
> +++ b/include/user/syscall-trace.h
> @@ -39,5 +39,22 @@ static inline void record_syscall_return(CPUState *cpu, int num, abi_long ret)
>       gdb_syscall_return(cpu, num);
>   }
>   
> +static bool send_through_syscall_filters(CPUState *cpu, int num,
> +                                         abi_long arg1, abi_long arg2,
> +                                         abi_long arg3, abi_long arg4,
> +                                         abi_long arg5, abi_long arg6,
> +                                         abi_long arg7, abi_long arg8,
> +                                         abi_long *sysret)
> +{
> +    uint64_t sysret64 = 0;
> +    bool filtered = qemu_plugin_vcpu_syscall_filter(cpu, num, arg1, arg2,
> +                                                    arg3, arg4, arg5, arg6,
> +                                                    arg7, arg8, &sysret64);
> +    if (filtered) {
> +        *sysret = sysret64;
> +    }
> +    return filtered;
> +}

FreeBSD build failure:

https://gitlab.com/qemu-project/qemu/-/jobs/12906481914

In file included from ../bsd-user/freebsd/os-syscall.c:34:
/tmp/cirrus-ci-build/include/user/syscall-trace.h:42:13: error: unused function 
'send_through_syscall_filters' [-Werror,-Wunused-function]
    42 | static bool send_through_syscall_filters(CPUState *cpu, int num,
       |             ^~~~~~~~~~~~~~~~~~~~~~~~~~~~
1 error generated.

Why is this in the header anyway?  If it really *should* be shared with FreeBSD, then it 
could just as well live somewhere in plugins/.  If it really is linux-specific, then it 
could just as well live in linux-user/syscall.c.


r~
Re: [PULL 06/20] linux-user: add plugin API to filter syscalls
Posted by Pierrick Bouvier 1 week, 5 days ago
On 1/28/26 6:28 PM, Richard Henderson wrote:
> On 1/29/26 12:31, Pierrick Bouvier wrote:
>> --- a/include/user/syscall-trace.h
>> +++ b/include/user/syscall-trace.h
>> @@ -39,5 +39,22 @@ static inline void record_syscall_return(CPUState *cpu, int num, abi_long ret)
>>        gdb_syscall_return(cpu, num);
>>    }
>>    
>> +static bool send_through_syscall_filters(CPUState *cpu, int num,
>> +                                         abi_long arg1, abi_long arg2,
>> +                                         abi_long arg3, abi_long arg4,
>> +                                         abi_long arg5, abi_long arg6,
>> +                                         abi_long arg7, abi_long arg8,
>> +                                         abi_long *sysret)
>> +{
>> +    uint64_t sysret64 = 0;
>> +    bool filtered = qemu_plugin_vcpu_syscall_filter(cpu, num, arg1, arg2,
>> +                                                    arg3, arg4, arg5, arg6,
>> +                                                    arg7, arg8, &sysret64);
>> +    if (filtered) {
>> +        *sysret = sysret64;
>> +    }
>> +    return filtered;
>> +}
> 
> FreeBSD build failure:
> 
> https://gitlab.com/qemu-project/qemu/-/jobs/12906481914
> 
> In file included from ../bsd-user/freebsd/os-syscall.c:34:
> /tmp/cirrus-ci-build/include/user/syscall-trace.h:42:13: error: unused function
> 'send_through_syscall_filters' [-Werror,-Wunused-function]
>      42 | static bool send_through_syscall_filters(CPUState *cpu, int num,
>         |             ^~~~~~~~~~~~~~~~~~~~~~~~~~~~
> 1 error generated.
> 
> Why is this in the header anyway?  If it really *should* be shared with FreeBSD, then it
> could just as well live somewhere in plugins/.  If it really is linux-specific, then it
> could just as well live in linux-user/syscall.c.
> 
> 
> r~

Sorry, it fell through the cracks.

FreeBSD has no werror by default, and the pipeline I ran doesn't add 
explicitly add --enable-werror. Warning is there though:
https://github.com/pbo-linaro/qemu/actions/runs/21461090659/job/61813517774
I'll make sure to build with werror enabled for next runs.

This function was added in this header, next to 
record_syscall_start/end, which is not used by BSD neither. It doesn't 
raise a warning because they are static inline. The original dev added 
it where it made sense.

Current series only has BSD implementation, but original 
qemu_plugin_vcpu_syscall supports only Linux too.
It could be implemented though, but not sure someone will spend time on 
that for BSD.

If that's ok for you, I can move the function to linux-user/syscall.c 
and remove the header. How about that?

Regards,
Pierrick
Re: [PULL 06/20] linux-user: add plugin API to filter syscalls
Posted by Richard Henderson 1 week, 5 days ago
On 1/29/26 13:56, Pierrick Bouvier wrote:
> If that's ok for you, I can move the function to linux-user/syscall.c and remove the 
> header. How about that?

Sounds good.

r~
Re: [PULL 06/20] linux-user: add plugin API to filter syscalls
Posted by Pierrick Bouvier 1 week, 5 days ago
On 1/28/26 6:56 PM, Pierrick Bouvier wrote:
> On 1/28/26 6:28 PM, Richard Henderson wrote:
>> On 1/29/26 12:31, Pierrick Bouvier wrote:
>>> --- a/include/user/syscall-trace.h
>>> +++ b/include/user/syscall-trace.h
>>> @@ -39,5 +39,22 @@ static inline void record_syscall_return(CPUState *cpu, int num, abi_long ret)
>>>         gdb_syscall_return(cpu, num);
>>>     }
>>>     
>>> +static bool send_through_syscall_filters(CPUState *cpu, int num,
>>> +                                         abi_long arg1, abi_long arg2,
>>> +                                         abi_long arg3, abi_long arg4,
>>> +                                         abi_long arg5, abi_long arg6,
>>> +                                         abi_long arg7, abi_long arg8,
>>> +                                         abi_long *sysret)
>>> +{
>>> +    uint64_t sysret64 = 0;
>>> +    bool filtered = qemu_plugin_vcpu_syscall_filter(cpu, num, arg1, arg2,
>>> +                                                    arg3, arg4, arg5, arg6,
>>> +                                                    arg7, arg8, &sysret64);
>>> +    if (filtered) {
>>> +        *sysret = sysret64;
>>> +    }
>>> +    return filtered;
>>> +}
>>
>> FreeBSD build failure:
>>
>> https://gitlab.com/qemu-project/qemu/-/jobs/12906481914
>>
>> In file included from ../bsd-user/freebsd/os-syscall.c:34:
>> /tmp/cirrus-ci-build/include/user/syscall-trace.h:42:13: error: unused function
>> 'send_through_syscall_filters' [-Werror,-Wunused-function]
>>       42 | static bool send_through_syscall_filters(CPUState *cpu, int num,
>>          |             ^~~~~~~~~~~~~~~~~~~~~~~~~~~~
>> 1 error generated.
>>
>> Why is this in the header anyway?  If it really *should* be shared with FreeBSD, then it
>> could just as well live somewhere in plugins/.  If it really is linux-specific, then it
>> could just as well live in linux-user/syscall.c.
>>
>>
>> r~
> 
> Sorry, it fell through the cracks.
> 
> FreeBSD has no werror by default, and the pipeline I ran doesn't add
> explicitly add --enable-werror. Warning is there though:
> https://github.com/pbo-linaro/qemu/actions/runs/21461090659/job/61813517774
> I'll make sure to build with werror enabled for next runs.
> 
> This function was added in this header, next to
> record_syscall_start/end, which is not used by BSD neither. It doesn't
> raise a warning because they are static inline. The original dev added
> it where it made sense.
> 
> Current series only has BSD implementation, but original

only has *Linux* implementation

> qemu_plugin_vcpu_syscall supports only Linux too.
> It could be implemented though, but not sure someone will spend time on
> that for BSD.
> 
> If that's ok for you, I can move the function to linux-user/syscall.c
> and remove the header. How about that?
> 
> Regards,
> Pierrick