All of lore.kernel.org
 help / color / mirror / Atom feed
* [PATCH v3 0/1] contrib/plugins: add dlcall to call host functions from a guest
@ 2026-06-22 16:34 Ziyang Zhang
  2026-06-22 16:34 ` [PATCH v3 1/1] contrib/plugins: add a minimal dlcall plugin Ziyang Zhang
                   ` (2 more replies)
  0 siblings, 3 replies; 15+ messages in thread
From: Ziyang Zhang @ 2026-06-22 16:34 UTC (permalink / raw)
  To: qemu-devel
  Cc: Riku Voipio, Laurent Vivier, Alex Bennee, Alexandre Iooss,
	Mahmoud Mandour, Pierrick Bouvier, Richard Henderson, Zhengwei Qi,
	Yun Wang, Mingyuan Xia, Kailiang Xu, Ziyang Zhang

Hi,

This patch adds a single plugin, contrib/plugins/dlcall.c (~240 lines,
no changes to QEMU core), that lets a linux-user guest call functions in the
host's native shared libraries instead of emulating them.

It is the natural next step on top of the vCPU syscall-filter callback that I
contributed and that was merged earlier:

  https://lore.kernel.org/qemu-devel/20251214144620.179282-1-functioner@sjtu.edu.cn/

Why bother? Because it turns slow, instruction-by-instruction emulation of a
library into a native host call. Some results, all on completely unmodified
guest binaries:

  * minizip (the stock zlib utility) compresses several times faster, because
    the actual deflate runs natively on the host instead of being translated.
  * Real OpenGL/Vulkan games run under qemu-user: SuperTuxKart and Hollow
    Knight are playable, with their graphics calls going straight to the host
    GPU.

You can watch the demos build, run, and report timings in CI, without checking
anything out:

  https://github.com/rover2024/qemu-passthrough-test/actions/runs/27671747420

How it works
============

The guest makes a system call with a reserved number (4096 by default) that no
real Linux ABI uses. Its first argument selects a pass-through operation; the
rest carry operands:

  syscall(4096, op, arg1, arg2, ...)
          |     |    \............ operands (pointers / values)
          |     \................. which pass-through operation
          \....................... the reserved "magic" number

The plugin registers a vCPU syscall filter: before QEMU forwards a syscall to
the host kernel, the filter runs, sees 4096, performs the operation on the
host, writes the result back, and tells QEMU the syscall is consumed, so the
real kernel never sees it.

The whole interface is just a handful of primitives:

  * query a host attribute
  * dlopen / dlclose a host shared library
  * dlsym a symbol, and read the last dlerror
  * invoke a resolved host function with a void(void *, void *) signature

That is all the plugin does. It knows nothing about zlib, X11 or OpenGL, or
about any library's calling convention.

The same machinery also runs in reverse: when a host function needs to call
back into the guest (a qsort comparator, an allocator, a GUI or game callback),
control re-enters the guest to run the callback and then resumes the suspended
host call. This reentry is what lets stateful, callback-driven APIs work, not
just leaf functions.

Why the plugin belongs in QEMU, and the rest does not
=====================================================

Only the plugin lives in the tree. Everything else is ordinary userspace:

  --- userspace (out of tree, not tied to any DBT) -------------
      guest: unmodified program  ->  guest runtime + thunk libs
  --------------------------------------------------------------
                 |  syscall(4096, op, args)   (only crossing point)
                 v
  === inside QEMU: THIS PATCH, ~240 lines ======================
      dlcall plugin:  dlopen / dlsym / invoke a host fn
  ==============================================================
                 |
                 v
  --- userspace (out of tree) ----------------------------------
      host: host runtime + thunk libs  ->  real libz / libGL ...
  --------------------------------------------------------------

A complete reference implementation, with the minizip and OpenGL/X11 examples
above, is here:

  https://github.com/rover2024/qemu-passthrough-test

The split is deliberate, and it is why only this one file is proposed for the
tree:

  * This plugin defines the most general interaction interface for native
    pass-through: the magic-syscall ABI between an emulated guest and its
    emulator. That contract is what every pass-through implementation builds
    on, so it belongs in a stable, shared place.
  * It is also the only piece that is inherently QEMU-specific: it plugs into
    QEMU's syscall-filter hook and runs inside the QEMU process. The argument
    marshalling, calling conventions, callbacks/reentry and per-library
    coverage are not tied to any particular DBT and behave as ordinary
    userspace, so they should stay out of tree rather than couple QEMU to them.

Background: we presented this approach at KVM Forum 2025, "Lorelei: Enable QEMU
to Leverage Native Shared Libraries":

  https://www.youtube.com/watch?v=_jioQFm7wyU

A note on automation
====================

The userspace thunks in that reference implementation are currently
hand-written rather than generated by the LLVM-based toolchain from the talk.
That is a deliberate choice for a demo: the automated toolchain pulls in a full
LLVM installation, which adds substantial setup time, and the example projects
are slow to build and cannot be reduced to a single Makefile. Hand-writing the
thunks was the cheaper path to a self-contained, reproducible demo, and it was
already enough to get minizip working end to end.

For real, large-scale use I will rely on the automated toolchain. The key point
is that hand-written vs. generated thunks is entirely decoupled from this plugin
and from the magic-syscall interface it defines: the toolchain only emits
out-of-tree userspace code and never touches the in-tree plugin. Automation
becomes mandatory for complex, callback-heavy targets such as the OpenGL/Vulkan
games, which is the direction this work is heading next.

It is fully opt-in (loaded with -plugin) and targets linux-user, where the
guest and host already share a trust domain. The test cases use x86_64 guests
and run on x86_64, arm64 and riscv64 Linux hosts.

The changes to the documentation (docs/about/emulation.rst) will be sent as a
separate patch.

Feedback on the plugin and on the pass-through approach is welcome.

Changes since v2:

  * Dropped the RFC tag; the approach was positively received on v2.
  * Rebased on master and adjusted the syscall-filter callback to its updated
    signature (int64_t sysret, added userdata, dropped the plugin id
    argument).

Changes since v1:

  * Renamed the plugin from "passthrough" to "dlcall" (Pierrick Bouvier).
    The old name was too generic; "dlcall" reflects what the plugin actually
    does (dlopen/dlsym a host symbol and call it) and avoids confusion with
    QEMU's existing plugin hostcall concept (QEMU_PLUGIN_*_HOSTCALL).
  * Made the magic syscall number configurable at load time via the
    "syscall_num=N" argument, defaulting to 4096 and rejecting values low
    enough to clash with a real syscall (Pierrick Bouvier).

v1: https://lore.kernel.org/qemu-devel/20260617130742.769234-1-functioner@sjtu.edu.cn/
v2: https://lore.kernel.org/qemu-devel/20260619045404.820960-1-functioner@sjtu.edu.cn/

Thanks,
Ziyang Zhang

Ziyang Zhang (1):
  contrib/plugins: add a minimal dlcall plugin

 contrib/plugins/dlcall.c    | 238 ++++++++++++++++++++++++++++++++++++
 contrib/plugins/meson.build |   5 +
 2 files changed, 243 insertions(+)
 create mode 100644 contrib/plugins/dlcall.c

-- 
2.34.1



^ permalink raw reply	[flat|nested] 15+ messages in thread

* [PATCH v3 1/1] contrib/plugins: add a minimal dlcall plugin
  2026-06-22 16:34 [PATCH v3 0/1] contrib/plugins: add dlcall to call host functions from a guest Ziyang Zhang
@ 2026-06-22 16:34 ` Ziyang Zhang
  2026-06-22 17:16 ` [PATCH v3 0/1] contrib/plugins: add dlcall to call host functions from a guest Pierrick Bouvier
  2026-06-29 16:02 ` [PATCH v4 " Ziyang Zhang
  2 siblings, 0 replies; 15+ messages in thread
From: Ziyang Zhang @ 2026-06-22 16:34 UTC (permalink / raw)
  To: qemu-devel
  Cc: Riku Voipio, Laurent Vivier, Alex Bennee, Alexandre Iooss,
	Mahmoud Mandour, Pierrick Bouvier, Richard Henderson, Zhengwei Qi,
	Yun Wang, Mingyuan Xia, Kailiang Xu, Ziyang Zhang

Add a minimal dlcall plugin that lets the guest invoke host functions
through magic system calls. The plugin registers a vCPU syscall filter
callback that intercepts a reserved syscall number and dispatches a set
of pass-through operations: querying host attributes, loading and freeing
shared libraries, resolving symbols, retrieving the last library error,
and invoking a host function through a common interface.

The magic syscall number defaults to 4096 and can be overridden at load
time with the "syscall_num=N" argument; values low enough to clash with a
real syscall are rejected.

Co-authored-by: Kailiang Xu <xukl2019@sjtu.edu.cn>
Co-authored-by: Mingyuan Xia <xiamy@ultrarisc.com>
Signed-off-by: Ziyang Zhang <functioner@sjtu.edu.cn>
---
 contrib/plugins/dlcall.c    | 238 ++++++++++++++++++++++++++++++++++++
 contrib/plugins/meson.build |   5 +
 2 files changed, 243 insertions(+)
 create mode 100644 contrib/plugins/dlcall.c

diff --git a/contrib/plugins/dlcall.c b/contrib/plugins/dlcall.c
new file mode 100644
index 0000000000..3a8f47cdf8
--- /dev/null
+++ b/contrib/plugins/dlcall.c
@@ -0,0 +1,238 @@
+/*
+ * Copyright (C) 2026, Ziyang Zhang <functioner@sjtu.edu.cn>
+ *
+ * dlcall plugin: lets a linux-user guest invoke host functions by issuing a
+ * magic system call. The guest can ask QEMU to dlopen() a host shared
+ * library, dlsym() a symbol, and call it with guest-supplied arguments.
+ *
+ * WARNING: trusted guests only. The guest can load arbitrary host libraries
+ * and execute arbitrary host code with arbitrary arguments, i.e. full code
+ * execution in the QEMU host process. It is NOT a sandbox and provides no
+ * isolation; only load it for guests you fully trust.
+ *
+ * WARNING: requires guest_base == 0, which is qemu-user's default. Pointer
+ * operands are dereferenced as host addresses directly, and the invoked host
+ * functions dereference guest pointers with no address translation, so guest
+ * and host must share a single address space. A non-zero guest_base (e.g. set
+ * via -B/-R) would make every pointer off by guest_base and hit unrelated
+ * host memory.
+ *
+ * SPDX-License-Identifier: GPL-2.0-or-later
+ */
+
+#include <assert.h>
+#include <errno.h>
+#include <string.h>
+#include <stdio.h>
+#include <glib.h>
+#include <dlfcn.h>
+
+#include <qemu-plugin.h>
+
+QEMU_PLUGIN_EXPORT int qemu_plugin_version = QEMU_PLUGIN_VERSION;
+
+/*
+ * The magic system call number for dlcall.
+ *
+ * It defaults to DLCALL_SYSCALL_DEFAULT and can be overridden at load time
+ * with the "syscall_num=N" argument. To avoid hijacking a real syscall the
+ * guest might issue, N must be at least DLCALL_SYSCALL_MIN: every Linux ABI
+ * keeps its syscall numbers well below this; numbers from here up are free.
+ */
+enum {
+    DLCALL_SYSCALL_DEFAULT = 4096,
+    DLCALL_SYSCALL_MIN = 4096,
+};
+
+static int64_t dlcall_syscall_num = DLCALL_SYSCALL_DEFAULT;
+
+/*
+ * dlcall calling convention.
+ *
+ * The guest issues the magic system call (dlcall_syscall_num). The first
+ * argument (a1) is one of the call IDs below; the remaining arguments (a2, a3,
+ * a4, ...) are that ID's operands. All pointer operands are guest virtual
+ * addresses that the plugin dereferences as host addresses directly (see the
+ * guest_base requirement above). Results are written back through
+ * caller-provided "out" pointers rather than returned in the syscall value.
+ *
+ * The syscall return value (*sysret) only reports dispatch status: 0 on a
+ * recognised ID, -EINVAL for an unknown one. The actual success/failure of an
+ * operation (e.g. a NULL handle from dlopen) is delivered through its out
+ * pointer, exactly like the underlying libdl call.
+ *
+ * Operands per ID:
+ *
+ *   DLCALL_ID_GET_HOST_ATTRIBUTE
+ *     a2  const char *key        in:  attribute name to query
+ *     a3  const char **attr_ptr  out: matching value, or NULL if unknown
+ *
+ *   DLCALL_ID_LOAD_LIBRARY                            (wraps dlopen)
+ *     a2  const char *path       in:  library path
+ *     a3  int flags              in:  dlopen() flags (e.g. RTLD_NOW)
+ *     a4  void **handle_ptr      out: library handle, or NULL on failure
+ *
+ *   DLCALL_ID_GET_PROC_ADDRESS                        (wraps dlsym)
+ *     a2  void *handle           in:  library handle
+ *     a3  const char *name       in:  symbol name
+ *     a4  void **entry_ptr       out: symbol address, or NULL if not found
+ *
+ *   DLCALL_ID_FREE_LIBRARY                            (wraps dlclose)
+ *     a2  void *handle           in:  library handle
+ *     a3  int *ret_ptr           out: dlclose() return value (0 on success)
+ *
+ *   DLCALL_ID_GET_LIBRARY_ERROR                       (wraps dlerror)
+ *     a2  const char **error_ptr out: last libdl error string, or NULL
+ *
+ *   DLCALL_ID_INVOKE_PROC                             (calls the symbol)
+ *     a2  void *proc             in:  function pointer, signature
+ *                                     void (*)(void *arg1, void *arg2)
+ *     a3  void *arg1             in:  first argument forwarded to proc
+ *     a4  void *arg2             in:  second argument forwarded to proc
+ */
+enum DlcallID {
+    DLCALL_ID_GET_HOST_ATTRIBUTE,
+    DLCALL_ID_LOAD_LIBRARY,
+    DLCALL_ID_GET_PROC_ADDRESS,
+    DLCALL_ID_FREE_LIBRARY,
+    DLCALL_ID_GET_LIBRARY_ERROR,
+    DLCALL_ID_INVOKE_PROC,
+};
+
+static inline const char *query_host_attribute(const char *key)
+{
+    if (strcmp(key, "emu") == 0) {
+        return "qemu";
+    }
+    return NULL;
+}
+
+static inline void invoke_proc(void *proc, void *arg1, void *arg2)
+{
+    typedef void (*Func)(void * /*arg1*/, void * /*arg2*/);
+    Func func = (Func) proc;
+    func(arg1, arg2);
+}
+
+static bool vcpu_syscall_filter(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,
+                                int64_t *sysret, void *userdata)
+{
+    if (num == dlcall_syscall_num) {
+        switch (a1) {
+        /* Query host attribute by a reserved key. */
+        case DLCALL_ID_GET_HOST_ATTRIBUTE: {
+            const char *key = (const char *) a2;
+            const char **attr_ptr = (const char **) a3;
+            assert(attr_ptr);
+            *attr_ptr = query_host_attribute(key);
+            *sysret = 0;
+            break;
+        }
+
+        /* Load a shared library. */
+        case DLCALL_ID_LOAD_LIBRARY: {
+            const char *path = (const char *) a2;
+            int flags = (int) a3;
+            void **handle_ptr = (void **) a4;
+            assert(handle_ptr);
+            *handle_ptr = dlopen(path, flags);
+            *sysret = 0;
+            break;
+        }
+
+        /* Get the address of a function in a shared library. */
+        case DLCALL_ID_GET_PROC_ADDRESS: {
+            void *handle = (void *) a2;
+            const char *name = (const char *) a3;
+            void **entry_ptr = (void **) a4;
+            assert(entry_ptr);
+            *entry_ptr = dlsym(handle, name);
+            *sysret = 0;
+            break;
+        }
+
+        /* Free a shared library. */
+        case DLCALL_ID_FREE_LIBRARY: {
+            void *handle = (void *) a2;
+            int *ret_ptr = (int *) a3;
+            *ret_ptr = dlclose(handle);
+            *sysret = 0;
+            break;
+        }
+
+        /* Get the last error message for a library event. */
+        case DLCALL_ID_GET_LIBRARY_ERROR: {
+            const char **error_ptr = (const char **) a2;
+            *error_ptr = dlerror();
+            *sysret = 0;
+            break;
+        }
+
+        /* Invoke a function of a common interface. */
+        case DLCALL_ID_INVOKE_PROC: {
+            void *proc = (void *) a2;
+            void *arg1 = (void *) a3;
+            void *arg2 = (void *) a4;
+            assert(proc);
+            invoke_proc(proc, arg1, arg2);
+            *sysret = 0;
+            break;
+        }
+
+        default:
+            *sysret = -EINVAL;
+            break;
+        }
+        return true;
+    }
+    return false;
+}
+
+QEMU_PLUGIN_EXPORT int qemu_plugin_install(qemu_plugin_id_t id,
+                                           const qemu_info_t *info,
+                                           int argc, char **argv)
+{
+    if (info->system_emulation) {
+        fprintf(stderr, "plugin dlcall: only useful for user emulation\n");
+        return -1;
+    }
+
+    for (int i = 0; i < argc; i++) {
+        char *opt = argv[i];
+        g_auto(GStrv) tokens = g_strsplit(opt, "=", 2);
+        if (g_strcmp0(tokens[0], "syscall_num") == 0) {
+            const char *val = tokens[1];
+            char *endptr = NULL;
+            guint64 num;
+            if (!val || *val == '\0') {
+                fprintf(stderr,
+                        "plugin dlcall: missing value for syscall_num\n");
+                return -1;
+            }
+            num = g_ascii_strtoull(val, &endptr, 0);
+            if (*endptr != '\0' || g_strrstr(val, "-") != NULL) {
+                fprintf(stderr,
+                        "plugin dlcall: invalid syscall_num '%s'\n", val);
+                return -1;
+            }
+            if (num < DLCALL_SYSCALL_MIN || num > G_MAXINT64) {
+                fprintf(stderr,
+                        "plugin dlcall: syscall_num %s is out of range; "
+                        "it must be >= %d to avoid clashing with a real "
+                        "syscall\n", val, DLCALL_SYSCALL_MIN);
+                return -1;
+            }
+            dlcall_syscall_num = (int64_t) num;
+        } else {
+            fprintf(stderr, "plugin dlcall: unknown option '%s'\n", opt);
+            return -1;
+        }
+    }
+
+    qemu_plugin_register_vcpu_syscall_filter_cb(id, vcpu_syscall_filter, NULL);
+
+    return 0;
+}
diff --git a/contrib/plugins/meson.build b/contrib/plugins/meson.build
index 099319e7a1..e7fc4d6d8f 100644
--- a/contrib/plugins/meson.build
+++ b/contrib/plugins/meson.build
@@ -19,6 +19,11 @@ if host_os != 'windows'
   contrib_plugins += 'lockstep.c'
 endif
 
+if host_os == 'linux'
+  # dlcall passes guest calls through to host libraries; linux-user only
+  contrib_plugins += 'dlcall.c'
+endif
+
 if 'cpp' in all_languages
   contrib_plugins += 'cpp.cpp'
 endif
-- 
2.34.1



^ permalink raw reply related	[flat|nested] 15+ messages in thread

* Re: [PATCH v3 0/1] contrib/plugins: add dlcall to call host functions from a guest
  2026-06-22 16:34 [PATCH v3 0/1] contrib/plugins: add dlcall to call host functions from a guest Ziyang Zhang
  2026-06-22 16:34 ` [PATCH v3 1/1] contrib/plugins: add a minimal dlcall plugin Ziyang Zhang
@ 2026-06-22 17:16 ` Pierrick Bouvier
  2026-06-22 17:50   ` Ziyang Zhang
  2026-06-29 16:02 ` [PATCH v4 " Ziyang Zhang
  2 siblings, 1 reply; 15+ messages in thread
From: Pierrick Bouvier @ 2026-06-22 17:16 UTC (permalink / raw)
  To: Ziyang Zhang, qemu-devel
  Cc: Riku Voipio, Laurent Vivier, Alex Bennee, Alexandre Iooss,
	Mahmoud Mandour, Pierrick Bouvier, Richard Henderson, Zhengwei Qi,
	Yun Wang, Mingyuan Xia, Kailiang Xu

On 6/22/2026 9:34 AM, Ziyang Zhang wrote:
> Hi,
> 
> This patch adds a single plugin, contrib/plugins/dlcall.c (~240 lines,
> no changes to QEMU core), that lets a linux-user guest call functions in the
> host's native shared libraries instead of emulating them.
> 
> It is the natural next step on top of the vCPU syscall-filter callback that I
> contributed and that was merged earlier:
> 
>   https://lore.kernel.org/qemu-devel/20251214144620.179282-1-functioner@sjtu.edu.cn/
> 
> Why bother? Because it turns slow, instruction-by-instruction emulation of a
> library into a native host call. Some results, all on completely unmodified
> guest binaries:
> 
>   * minizip (the stock zlib utility) compresses several times faster, because
>     the actual deflate runs natively on the host instead of being translated.
>   * Real OpenGL/Vulkan games run under qemu-user: SuperTuxKart and Hollow
>     Knight are playable, with their graphics calls going straight to the host
>     GPU.
> 
> You can watch the demos build, run, and report timings in CI, without checking
> anything out:
> 
>   https://github.com/rover2024/qemu-passthrough-test/actions/runs/27671747420
> 
> How it works
> ============
> 
> The guest makes a system call with a reserved number (4096 by default) that no
> real Linux ABI uses. Its first argument selects a pass-through operation; the
> rest carry operands:
> 
>   syscall(4096, op, arg1, arg2, ...)
>           |     |    \............ operands (pointers / values)
>           |     \................. which pass-through operation
>           \....................... the reserved "magic" number
> 
> The plugin registers a vCPU syscall filter: before QEMU forwards a syscall to
> the host kernel, the filter runs, sees 4096, performs the operation on the
> host, writes the result back, and tells QEMU the syscall is consumed, so the
> real kernel never sees it.
> 
> The whole interface is just a handful of primitives:
> 
>   * query a host attribute
>   * dlopen / dlclose a host shared library
>   * dlsym a symbol, and read the last dlerror
>   * invoke a resolved host function with a void(void *, void *) signature
> 
> That is all the plugin does. It knows nothing about zlib, X11 or OpenGL, or
> about any library's calling convention.
> 
> The same machinery also runs in reverse: when a host function needs to call
> back into the guest (a qsort comparator, an allocator, a GUI or game callback),
> control re-enters the guest to run the callback and then resumes the suspended
> host call. This reentry is what lets stateful, callback-driven APIs work, not
> just leaf functions.
> 
> Why the plugin belongs in QEMU, and the rest does not
> =====================================================
> 
> Only the plugin lives in the tree. Everything else is ordinary userspace:
> 
>   --- userspace (out of tree, not tied to any DBT) -------------
>       guest: unmodified program  ->  guest runtime + thunk libs
>   --------------------------------------------------------------
>                  |  syscall(4096, op, args)   (only crossing point)
>                  v
>   === inside QEMU: THIS PATCH, ~240 lines ======================
>       dlcall plugin:  dlopen / dlsym / invoke a host fn
>   ==============================================================
>                  |
>                  v
>   --- userspace (out of tree) ----------------------------------
>       host: host runtime + thunk libs  ->  real libz / libGL ...
>   --------------------------------------------------------------
> 
> A complete reference implementation, with the minizip and OpenGL/X11 examples
> above, is here:
> 
>   https://github.com/rover2024/qemu-passthrough-test
> 
> The split is deliberate, and it is why only this one file is proposed for the
> tree:
> 
>   * This plugin defines the most general interaction interface for native
>     pass-through: the magic-syscall ABI between an emulated guest and its
>     emulator. That contract is what every pass-through implementation builds
>     on, so it belongs in a stable, shared place.
>   * It is also the only piece that is inherently QEMU-specific: it plugs into
>     QEMU's syscall-filter hook and runs inside the QEMU process. The argument
>     marshalling, calling conventions, callbacks/reentry and per-library
>     coverage are not tied to any particular DBT and behave as ordinary
>     userspace, so they should stay out of tree rather than couple QEMU to them.
> 
> Background: we presented this approach at KVM Forum 2025, "Lorelei: Enable QEMU
> to Leverage Native Shared Libraries":
> 
>   https://www.youtube.com/watch?v=_jioQFm7wyU
> 
> A note on automation
> ====================
> 
> The userspace thunks in that reference implementation are currently
> hand-written rather than generated by the LLVM-based toolchain from the talk.
> That is a deliberate choice for a demo: the automated toolchain pulls in a full
> LLVM installation, which adds substantial setup time, and the example projects
> are slow to build and cannot be reduced to a single Makefile. Hand-writing the
> thunks was the cheaper path to a self-contained, reproducible demo, and it was
> already enough to get minizip working end to end.
> 
> For real, large-scale use I will rely on the automated toolchain. The key point
> is that hand-written vs. generated thunks is entirely decoupled from this plugin
> and from the magic-syscall interface it defines: the toolchain only emits
> out-of-tree userspace code and never touches the in-tree plugin. Automation
> becomes mandatory for complex, callback-heavy targets such as the OpenGL/Vulkan
> games, which is the direction this work is heading next.
> 
> It is fully opt-in (loaded with -plugin) and targets linux-user, where the
> guest and host already share a trust domain. The test cases use x86_64 guests
> and run on x86_64, arm64 and riscv64 Linux hosts.
> 
> The changes to the documentation (docs/about/emulation.rst) will be sent as a
> separate patch.
> 
> Feedback on the plugin and on the pass-through approach is welcome.
> 
> Changes since v2:
> 
>   * Dropped the RFC tag; the approach was positively received on v2.
>   * Rebased on master and adjusted the syscall-filter callback to its updated
>     signature (int64_t sysret, added userdata, dropped the plugin id
>     argument).
> 
> Changes since v1:
> 
>   * Renamed the plugin from "passthrough" to "dlcall" (Pierrick Bouvier).
>     The old name was too generic; "dlcall" reflects what the plugin actually
>     does (dlopen/dlsym a host symbol and call it) and avoids confusion with
>     QEMU's existing plugin hostcall concept (QEMU_PLUGIN_*_HOSTCALL).
>   * Made the magic syscall number configurable at load time via the
>     "syscall_num=N" argument, defaulting to 4096 and rejecting values low
>     enough to clash with a real syscall (Pierrick Bouvier).
> 
> v1: https://lore.kernel.org/qemu-devel/20260617130742.769234-1-functioner@sjtu.edu.cn/
> v2: https://lore.kernel.org/qemu-devel/20260619045404.820960-1-functioner@sjtu.edu.cn/
> 
> Thanks,
> Ziyang Zhang
> 
> Ziyang Zhang (1):
>   contrib/plugins: add a minimal dlcall plugin
> 
>  contrib/plugins/dlcall.c    | 238 ++++++++++++++++++++++++++++++++++++
>  contrib/plugins/meson.build |   5 +
>  2 files changed, 243 insertions(+)
>  create mode 100644 contrib/plugins/dlcall.c
> 

Thanks for the update.

At this stage, even though the plugin in itself looks good, it's not
something we can really merge in, as it's relying on external behavior
that is not specified or being a standalone tool.

For instance, if we have a public Lorelei toolchain that implements this
for libraries, it would definitely have it's place upstream IMHO.

Also, the same kind of contribution can be done for other
instrumentation frameworks, so we can end up having a single interface
(and set of libraries) shared among all of them.

What do you think?

Regards,
Pierrick


^ permalink raw reply	[flat|nested] 15+ messages in thread

* Re: [PATCH v3 0/1] contrib/plugins: add dlcall to call host functions from a guest
  2026-06-22 17:16 ` [PATCH v3 0/1] contrib/plugins: add dlcall to call host functions from a guest Pierrick Bouvier
@ 2026-06-22 17:50   ` Ziyang Zhang
  2026-06-22 18:04     ` Pierrick Bouvier
  0 siblings, 1 reply; 15+ messages in thread
From: Ziyang Zhang @ 2026-06-22 17:50 UTC (permalink / raw)
  To: Pierrick Bouvier, qemu-devel
  Cc: Riku Voipio, Laurent Vivier, Alex Bennee, Alexandre Iooss,
	Mahmoud Mandour, Pierrick Bouvier, Richard Henderson, Zhengwei Qi,
	Yun Wang, Mingyuan Xia, Kailiang Xu

On Mon, 22 Jun 2026 10:16:29 -0700, Pierrick Bouvier wrote:
> Thanks for the update.
> 
> At this stage, even though the plugin in itself looks good, it's not
> something we can really merge in, as it's relying on external behavior
> that is not specified or being a standalone tool.
> 
> For instance, if we have a public Lorelei toolchain that implements this
> for libraries, it would definitely have it's place upstream IMHO.
> 
> Also, the same kind of contribution can be done for other
> instrumentation frameworks, so we can end up having a single interface
> (and set of libraries) shared among all of them.
> 
> What do you think?
> 

Hi Pierrick,

That makes sense.

A few thoughts in response:

The Lorelei toolchain already exists, but it will take me a little time 
to migrate it onto this minimal plugin interface, since the ABI changed 
somewhat over the course of the RFC.

I think Lorelei is better kept as a standalone repository: the thunk 
libraries and the guest/host runtimes clearly do not belong in the QEMU 
tree.

Since the plugin only defines the dlcall interface and says nothing 
about how thunking is implemented, the Lorelei repo is just one 
reference implementation of that side; a user is free to build their 
own, with any toolchain or for any framework.

So, to make sure I take the right next step: would you like me to make 
the Lorelei repository public, with CI that demonstrates the automatic 
thunk generation? And should Lorelei be referenced from the plugin's 
comments as a reference implementation?

Regards,
Ziyang



^ permalink raw reply	[flat|nested] 15+ messages in thread

* Re: [PATCH v3 0/1] contrib/plugins: add dlcall to call host functions from a guest
  2026-06-22 17:50   ` Ziyang Zhang
@ 2026-06-22 18:04     ` Pierrick Bouvier
  2026-06-23  2:51       ` Ziyang Zhang
  0 siblings, 1 reply; 15+ messages in thread
From: Pierrick Bouvier @ 2026-06-22 18:04 UTC (permalink / raw)
  To: Ziyang Zhang, qemu-devel
  Cc: Riku Voipio, Laurent Vivier, Alex Bennee, Alexandre Iooss,
	Mahmoud Mandour, Pierrick Bouvier, Richard Henderson, Zhengwei Qi,
	Yun Wang, Mingyuan Xia, Kailiang Xu

On 6/22/2026 10:50 AM, Ziyang Zhang wrote:
> On Mon, 22 Jun 2026 10:16:29 -0700, Pierrick Bouvier wrote:
>> Thanks for the update.
>>
>> At this stage, even though the plugin in itself looks good, it's not
>> something we can really merge in, as it's relying on external behavior
>> that is not specified or being a standalone tool.
>>
>> For instance, if we have a public Lorelei toolchain that implements this
>> for libraries, it would definitely have it's place upstream IMHO.
>>
>> Also, the same kind of contribution can be done for other
>> instrumentation frameworks, so we can end up having a single interface
>> (and set of libraries) shared among all of them.
>>
>> What do you think?
>>
> 
> Hi Pierrick,
> 
> That makes sense.
> 
> A few thoughts in response:
> 
> The Lorelei toolchain already exists, but it will take me a little time
> to migrate it onto this minimal plugin interface, since the ABI changed
> somewhat over the course of the RFC.
>

That's my point. As long as Lorelei is not a stable tool, we can't
really integrate the current plugin upstream. However, it's great that
you posted this as a RFC, as it shows the approach work.

> I think Lorelei is better kept as a standalone repository: the thunk
> libraries and the guest/host runtimes clearly do not belong in the QEMU
> tree.
>

Agree on that part.

> Since the plugin only defines the dlcall interface and says nothing
> about how thunking is implemented, the Lorelei repo is just one
> reference implementation of that side; a user is free to build their
> own, with any toolchain or for any framework.
> 
> So, to make sure I take the right next step: would you like me to make
> the Lorelei repository public, with CI that demonstrates the automatic
> thunk generation? And should Lorelei be referenced from the plugin's
> comments as a reference implementation?
>

For now, I think the current plugin should stay downstream, until
Lorelei is officially published and people can take any library and
compile it with it. Once we have this, it's easy to motivate integrating
this plugin upstream, as people can do everything themselves.

> Regards,
> Ziyang
> 



^ permalink raw reply	[flat|nested] 15+ messages in thread

* Re: [PATCH v3 0/1] contrib/plugins: add dlcall to call host functions from a guest
  2026-06-22 18:04     ` Pierrick Bouvier
@ 2026-06-23  2:51       ` Ziyang Zhang
  2026-06-23 17:16         ` Pierrick Bouvier
  0 siblings, 1 reply; 15+ messages in thread
From: Ziyang Zhang @ 2026-06-23  2:51 UTC (permalink / raw)
  To: Pierrick Bouvier, qemu-devel
  Cc: Riku Voipio, Laurent Vivier, Alex Bennee, Alexandre Iooss,
	Mahmoud Mandour, Pierrick Bouvier, Richard Henderson, Zhengwei Qi,
	Yun Wang, Mingyuan Xia, Kailiang Xu

On Mon, 22 Jun 2026 11:04:43 -0700, Pierrick Bouvier wrote:
> That's my point. As long as Lorelei is not a stable tool, we can't
> really integrate the current plugin upstream. However, it's great that
> you posted this as a RFC, as it shows the approach work.

The main reason for Lorelei's instability is the repeated modifications
to the plugin API. However, this current PATCH indicates that the plugin
is at least stable now. Therefore, Lorelei can become stable very soon.
I will release Lorelei in a few days.

> For now, I think the current plugin should stay downstream, until
> Lorelei is officially published and people can take any library and
> compile it with it. Once we have this, it's easy to motivate integrating
> this plugin upstream, as people can do everything themselves.

Since the current QEMU does not force the guest program to load at a low
address as it did in version 9.0, the callback functionality in the
earlier versions of Lorelei was limited. However, I don't think this is
a major issue. I must first publish Lorelei and integrate the plugin
into the upstream before we can discuss the modifications to the mmap
handler.

What do you think?

> 
>> Regards,
>> Ziyang
>>
> 



^ permalink raw reply	[flat|nested] 15+ messages in thread

* Re: [PATCH v3 0/1] contrib/plugins: add dlcall to call host functions from a guest
  2026-06-23  2:51       ` Ziyang Zhang
@ 2026-06-23 17:16         ` Pierrick Bouvier
  2026-06-23 17:34           ` Ziyang Zhang
  0 siblings, 1 reply; 15+ messages in thread
From: Pierrick Bouvier @ 2026-06-23 17:16 UTC (permalink / raw)
  To: Ziyang Zhang, qemu-devel
  Cc: Riku Voipio, Laurent Vivier, Alex Bennee, Alexandre Iooss,
	Mahmoud Mandour, Pierrick Bouvier, Richard Henderson, Zhengwei Qi,
	Yun Wang, Mingyuan Xia, Kailiang Xu

On 6/22/2026 7:51 PM, Ziyang Zhang wrote:
> On Mon, 22 Jun 2026 11:04:43 -0700, Pierrick Bouvier wrote:
>> That's my point. As long as Lorelei is not a stable tool, we can't
>> really integrate the current plugin upstream. However, it's great that
>> you posted this as a RFC, as it shows the approach work.
> 
> The main reason for Lorelei's instability is the repeated modifications
> to the plugin API. However, this current PATCH indicates that the plugin
> is at least stable now. Therefore, Lorelei can become stable very soon.
> I will release Lorelei in a few days.
>

Sounds good!

>> For now, I think the current plugin should stay downstream, until
>> Lorelei is officially published and people can take any library and
>> compile it with it. Once we have this, it's easy to motivate integrating
>> this plugin upstream, as people can do everything themselves.
> 
> Since the current QEMU does not force the guest program to load at a low
> address as it did in version 9.0, the callback functionality in the
> earlier versions of Lorelei was limited. However, I don't think this is
> a major issue. I must first publish Lorelei and integrate the plugin
> into the upstream before we can discuss the modifications to the mmap
> handler.
> 
> What do you think?
>

Sure, as soon as we have this publicly available, I would be happy to
see this plugin merged also. Corner cases can be be solved later, it
doesn't have to support 100% of the libraries that exist in real world.

>>
>>> Regards,
>>> Ziyang
>>>
>>
> 



^ permalink raw reply	[flat|nested] 15+ messages in thread

* Re: [PATCH v3 0/1] contrib/plugins: add dlcall to call host functions from a guest
  2026-06-23 17:16         ` Pierrick Bouvier
@ 2026-06-23 17:34           ` Ziyang Zhang
  0 siblings, 0 replies; 15+ messages in thread
From: Ziyang Zhang @ 2026-06-23 17:34 UTC (permalink / raw)
  To: Pierrick Bouvier, qemu-devel
  Cc: Riku Voipio, Laurent Vivier, Alex Bennee, Alexandre Iooss,
	Mahmoud Mandour, Pierrick Bouvier, Richard Henderson, Zhengwei Qi,
	Yun Wang, Mingyuan Xia, Kailiang Xu

On Tue, 23 Jun 2026 10:16:18 -0700, Pierrick Bouvier wrote:
> Sure, as soon as we have this publicly available, I would be happy to
> see this plugin merged also. Corner cases can be be solved later, it
> doesn't have to support 100% of the libraries that exist in real world.
Thank you! Lorelei currently only needs to add some documentation and
integrate the host runtime with the current plugin. This won't take up
too much time. Of course, we will also provide CI for x86_64, arm64 and
riscv64 for everyone to review.

Regards,
Ziyang Zhang


^ permalink raw reply	[flat|nested] 15+ messages in thread

* [PATCH v4 0/1] contrib/plugins: add dlcall to call host functions from a guest
  2026-06-22 16:34 [PATCH v3 0/1] contrib/plugins: add dlcall to call host functions from a guest Ziyang Zhang
  2026-06-22 16:34 ` [PATCH v3 1/1] contrib/plugins: add a minimal dlcall plugin Ziyang Zhang
  2026-06-22 17:16 ` [PATCH v3 0/1] contrib/plugins: add dlcall to call host functions from a guest Pierrick Bouvier
@ 2026-06-29 16:02 ` Ziyang Zhang
  2026-06-29 16:02   ` [PATCH v4 1/1] contrib/plugins: add a minimal dlcall plugin Ziyang Zhang
  2026-06-30 19:34   ` [PATCH v4 0/1] contrib/plugins: add dlcall to call host functions from a guest Pierrick Bouvier
  2 siblings, 2 replies; 15+ messages in thread
From: Ziyang Zhang @ 2026-06-29 16:02 UTC (permalink / raw)
  To: qemu-devel
  Cc: Riku Voipio, Laurent Vivier, Alex Bennee, Alexandre Iooss,
	Mahmoud Mandour, Pierrick Bouvier, Richard Henderson, Zhengwei Qi,
	Yun Wang, Mingyuan Xia, Kailiang Xu, Ziyang Zhang

Hi,

This patch adds a single plugin, contrib/plugins/dlcall.c (~240 lines,
no changes to QEMU core), that lets a linux-user guest call functions in the
host's native shared libraries instead of emulating them.

It is the natural next step on top of the vCPU syscall-filter callback that I
contributed and that was merged earlier:

  https://lore.kernel.org/qemu-devel/20251214144620.179282-1-functioner@sjtu.edu.cn/

Why bother? Because it turns slow, instruction-by-instruction emulation of a
library into a native host call. Some results, all on completely unmodified
guest binaries:

  * minizip (the stock zlib utility) compresses several times faster, because
    the actual deflate runs natively on the host instead of being translated.
  * Real OpenGL/Vulkan games run under qemu-user: SuperTuxKart and Hollow
    Knight are playable, with their graphics calls going straight to the host
    GPU.

How it works
============

The guest makes a system call with a reserved number (4096 by default) that no
real Linux ABI uses. Its first argument selects a pass-through operation, and the
rest carry operands:

  syscall(4096, op, arg1, arg2, ...)
          |     |    \............ operands (pointers / values)
          |     \................. which pass-through operation
          \....................... the reserved "magic" number

The plugin registers a vCPU syscall filter: before QEMU forwards a syscall to
the host kernel, the filter runs, sees 4096, performs the operation on the
host, writes the result back, and tells QEMU the syscall is consumed, so the
real kernel never sees it.

The whole interface is just a handful of primitives:

  * query a host attribute
  * dlopen / dlclose a host shared library
  * dlsym a symbol, and read the last dlerror
  * invoke a resolved host function with a void(void *, void *) signature

That is all the plugin does. It knows nothing about zlib, X11 or OpenGL, or
about any library's calling convention.

The same machinery also runs in reverse: when a host function needs to call
back into the guest (a qsort comparator, an allocator, a GUI or game callback),
control re-enters the guest to run the callback and then resumes the suspended
host call. This reentry is what lets stateful, callback-driven APIs work, not
just leaf functions.

Why the plugin belongs in QEMU, and the rest does not
=====================================================

Only the plugin lives in the tree. Everything else is ordinary userspace:

  --- userspace (out of tree, not tied to any DBT) -------------
      guest: unmodified program  ->  guest runtime + thunk libs
  --------------------------------------------------------------
                 |  syscall(4096, op, args)   (only crossing point)
                 v
  === inside QEMU: THIS PATCH, ~240 lines ======================
      dlcall plugin:  dlopen / dlsym / invoke a host fn
  ==============================================================
                 |
                 v
  --- userspace (out of tree) ----------------------------------
      host: host runtime + thunk libs  ->  real libz / libGL ...
  --------------------------------------------------------------

The split is deliberate, and it is why only this one file is proposed for the
tree:

  * This plugin defines the most general interaction interface for native
    pass-through: the magic-syscall ABI between an emulated guest and its
    emulator. That contract is what every pass-through implementation builds
    on, so it belongs in a stable, shared place.
  * It is also the only piece that is inherently QEMU-specific: it plugs into
    QEMU's syscall-filter hook and runs inside the QEMU process. The argument
    marshalling, calling conventions, callbacks/reentry and per-library
    coverage are not tied to any particular DBT and behave as ordinary
    userspace, so they should stay out of tree rather than couple QEMU to them.

Background: we presented this approach at KVM Forum 2025, "Lorelei: Enable QEMU
to Leverage Native Shared Libraries":

  https://www.youtube.com/watch?v=_jioQFm7wyU

The userspace side
==================

A fair point on v3 was that, on its own, the plugin is only half of an
interface: useful only if the other half (the guest/host runtimes and the
per-library thunks) is public and specified, rather than a private demo. That
half is now available as a standalone, documented, CI-tested project, Lorelei:

  https://github.com/rover2024/lorelei

Lorelei provides the guest and host runtimes and a Thunk Library Compiler
(TLC, built on Clang LibTooling) that reads a library's headers and generates
the guest and host thunks automatically, including the awkward cases of
function-pointer callbacks and variadic functions. It has CI for x86_64, arm64
and riscv64 hosts. Lorelei and its thunk libraries are MIT-licensed.

Because the plugin is not upstream yet, Lorelei currently builds and tests
against the QEMU fork that carries it.

The plugin stays deliberately minimal and prescribes nothing about how
thunking is done. Lorelei is one reference implementation of the userspace
side. Any toolchain, or another instrumentation framework, can implement the
same dlcall interface.

A from-scratch walkthrough of the bare mechanism, with the minizip and
OpenGL/X11 examples above, is also available here:

  https://github.com/rover2024/qemu-passthrough-test

It is fully opt-in (loaded with -plugin) and targets linux-user, where the
guest and host already share a trust domain. The test cases use x86_64 guests
and run on x86_64, arm64 and riscv64 Linux hosts.

I have deliberately kept Lorelei out of the patch itself: neither the code
nor its comments mention it, since I am not sure whether referencing an
external project from the tree is welcome. I would appreciate guidance on
where a pointer to the toolchain belongs, if anywhere. For example, would it
be appropriate to mention it from the documentation patch?

Feedback on the plugin and on the pass-through approach is welcome.

Changes since v3:

  * Lorelei, the userspace toolchain that implements this interface, is now a
    public, documented, CI-tested project, with a Thunk Library Compiler that
    generates the guest/host thunks automatically. This addresses the v3
    feedback that the interface needs a public implementation behind it. The
    plugin code itself is unchanged.
  * The documentation patch is held back for now.

Changes since v2:

  * Dropped the RFC tag. The approach was positively received on v2.
  * Rebased on master and adjusted the syscall-filter callback to its updated
    signature (int64_t sysret, added userdata, dropped the plugin id
    argument).

Changes since v1:

  * Renamed the plugin from "passthrough" to "dlcall" (Pierrick Bouvier).
    The old name was too generic. The name "dlcall" reflects what the plugin actually
    does (dlopen/dlsym a host symbol and call it) and avoids confusion with
    QEMU's existing plugin hostcall concept (QEMU_PLUGIN_*_HOSTCALL).
  * Made the magic syscall number configurable at load time via the
    "syscall_num=N" argument, defaulting to 4096 and rejecting values low
    enough to clash with a real syscall (Pierrick Bouvier).

v1: https://lore.kernel.org/qemu-devel/20260617130742.769234-1-functioner@sjtu.edu.cn/
v2: https://lore.kernel.org/qemu-devel/20260619045404.820960-1-functioner@sjtu.edu.cn/
v3: https://lore.kernel.org/qemu-devel/20260622163438.130746-1-functioner@sjtu.edu.cn/

Thanks,
Ziyang Zhang

Ziyang Zhang (1):
  contrib/plugins: add a minimal dlcall plugin

 contrib/plugins/dlcall.c    | 238 ++++++++++++++++++++++++++++++++++++
 contrib/plugins/meson.build |   5 +
 2 files changed, 243 insertions(+)
 create mode 100644 contrib/plugins/dlcall.c

-- 
2.34.1



^ permalink raw reply	[flat|nested] 15+ messages in thread

* [PATCH v4 1/1] contrib/plugins: add a minimal dlcall plugin
  2026-06-29 16:02 ` [PATCH v4 " Ziyang Zhang
@ 2026-06-29 16:02   ` Ziyang Zhang
  2026-06-30 19:18     ` Pierrick Bouvier
  2026-06-30 19:34   ` [PATCH v4 0/1] contrib/plugins: add dlcall to call host functions from a guest Pierrick Bouvier
  1 sibling, 1 reply; 15+ messages in thread
From: Ziyang Zhang @ 2026-06-29 16:02 UTC (permalink / raw)
  To: qemu-devel
  Cc: Riku Voipio, Laurent Vivier, Alex Bennee, Alexandre Iooss,
	Mahmoud Mandour, Pierrick Bouvier, Richard Henderson, Zhengwei Qi,
	Yun Wang, Mingyuan Xia, Kailiang Xu, Ziyang Zhang

Add a minimal dlcall plugin that lets the guest invoke host functions
through magic system calls. The plugin registers a vCPU syscall filter
callback that intercepts a reserved syscall number and dispatches a set
of pass-through operations: querying host attributes, loading and freeing
shared libraries, resolving symbols, retrieving the last library error,
and invoking a host function through a common interface.

The magic syscall number defaults to 4096 and can be overridden at load
time with the "syscall_num=N" argument; values low enough to clash with a
real syscall are rejected.

Co-authored-by: Kailiang Xu <xukl2019@sjtu.edu.cn>
Co-authored-by: Mingyuan Xia <xiamy@ultrarisc.com>
Signed-off-by: Ziyang Zhang <functioner@sjtu.edu.cn>
---
 contrib/plugins/dlcall.c    | 238 ++++++++++++++++++++++++++++++++++++
 contrib/plugins/meson.build |   5 +
 2 files changed, 243 insertions(+)
 create mode 100644 contrib/plugins/dlcall.c

diff --git a/contrib/plugins/dlcall.c b/contrib/plugins/dlcall.c
new file mode 100644
index 0000000000..3a8f47cdf8
--- /dev/null
+++ b/contrib/plugins/dlcall.c
@@ -0,0 +1,238 @@
+/*
+ * Copyright (C) 2026, Ziyang Zhang <functioner@sjtu.edu.cn>
+ *
+ * dlcall plugin: lets a linux-user guest invoke host functions by issuing a
+ * magic system call. The guest can ask QEMU to dlopen() a host shared
+ * library, dlsym() a symbol, and call it with guest-supplied arguments.
+ *
+ * WARNING: trusted guests only. The guest can load arbitrary host libraries
+ * and execute arbitrary host code with arbitrary arguments, i.e. full code
+ * execution in the QEMU host process. It is NOT a sandbox and provides no
+ * isolation; only load it for guests you fully trust.
+ *
+ * WARNING: requires guest_base == 0, which is qemu-user's default. Pointer
+ * operands are dereferenced as host addresses directly, and the invoked host
+ * functions dereference guest pointers with no address translation, so guest
+ * and host must share a single address space. A non-zero guest_base (e.g. set
+ * via -B/-R) would make every pointer off by guest_base and hit unrelated
+ * host memory.
+ *
+ * SPDX-License-Identifier: GPL-2.0-or-later
+ */
+
+#include <assert.h>
+#include <errno.h>
+#include <string.h>
+#include <stdio.h>
+#include <glib.h>
+#include <dlfcn.h>
+
+#include <qemu-plugin.h>
+
+QEMU_PLUGIN_EXPORT int qemu_plugin_version = QEMU_PLUGIN_VERSION;
+
+/*
+ * The magic system call number for dlcall.
+ *
+ * It defaults to DLCALL_SYSCALL_DEFAULT and can be overridden at load time
+ * with the "syscall_num=N" argument. To avoid hijacking a real syscall the
+ * guest might issue, N must be at least DLCALL_SYSCALL_MIN: every Linux ABI
+ * keeps its syscall numbers well below this; numbers from here up are free.
+ */
+enum {
+    DLCALL_SYSCALL_DEFAULT = 4096,
+    DLCALL_SYSCALL_MIN = 4096,
+};
+
+static int64_t dlcall_syscall_num = DLCALL_SYSCALL_DEFAULT;
+
+/*
+ * dlcall calling convention.
+ *
+ * The guest issues the magic system call (dlcall_syscall_num). The first
+ * argument (a1) is one of the call IDs below; the remaining arguments (a2, a3,
+ * a4, ...) are that ID's operands. All pointer operands are guest virtual
+ * addresses that the plugin dereferences as host addresses directly (see the
+ * guest_base requirement above). Results are written back through
+ * caller-provided "out" pointers rather than returned in the syscall value.
+ *
+ * The syscall return value (*sysret) only reports dispatch status: 0 on a
+ * recognised ID, -EINVAL for an unknown one. The actual success/failure of an
+ * operation (e.g. a NULL handle from dlopen) is delivered through its out
+ * pointer, exactly like the underlying libdl call.
+ *
+ * Operands per ID:
+ *
+ *   DLCALL_ID_GET_HOST_ATTRIBUTE
+ *     a2  const char *key        in:  attribute name to query
+ *     a3  const char **attr_ptr  out: matching value, or NULL if unknown
+ *
+ *   DLCALL_ID_LOAD_LIBRARY                            (wraps dlopen)
+ *     a2  const char *path       in:  library path
+ *     a3  int flags              in:  dlopen() flags (e.g. RTLD_NOW)
+ *     a4  void **handle_ptr      out: library handle, or NULL on failure
+ *
+ *   DLCALL_ID_GET_PROC_ADDRESS                        (wraps dlsym)
+ *     a2  void *handle           in:  library handle
+ *     a3  const char *name       in:  symbol name
+ *     a4  void **entry_ptr       out: symbol address, or NULL if not found
+ *
+ *   DLCALL_ID_FREE_LIBRARY                            (wraps dlclose)
+ *     a2  void *handle           in:  library handle
+ *     a3  int *ret_ptr           out: dlclose() return value (0 on success)
+ *
+ *   DLCALL_ID_GET_LIBRARY_ERROR                       (wraps dlerror)
+ *     a2  const char **error_ptr out: last libdl error string, or NULL
+ *
+ *   DLCALL_ID_INVOKE_PROC                             (calls the symbol)
+ *     a2  void *proc             in:  function pointer, signature
+ *                                     void (*)(void *arg1, void *arg2)
+ *     a3  void *arg1             in:  first argument forwarded to proc
+ *     a4  void *arg2             in:  second argument forwarded to proc
+ */
+enum DlcallID {
+    DLCALL_ID_GET_HOST_ATTRIBUTE,
+    DLCALL_ID_LOAD_LIBRARY,
+    DLCALL_ID_GET_PROC_ADDRESS,
+    DLCALL_ID_FREE_LIBRARY,
+    DLCALL_ID_GET_LIBRARY_ERROR,
+    DLCALL_ID_INVOKE_PROC,
+};
+
+static inline const char *query_host_attribute(const char *key)
+{
+    if (strcmp(key, "emu") == 0) {
+        return "qemu";
+    }
+    return NULL;
+}
+
+static inline void invoke_proc(void *proc, void *arg1, void *arg2)
+{
+    typedef void (*Func)(void * /*arg1*/, void * /*arg2*/);
+    Func func = (Func) proc;
+    func(arg1, arg2);
+}
+
+static bool vcpu_syscall_filter(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,
+                                int64_t *sysret, void *userdata)
+{
+    if (num == dlcall_syscall_num) {
+        switch (a1) {
+        /* Query host attribute by a reserved key. */
+        case DLCALL_ID_GET_HOST_ATTRIBUTE: {
+            const char *key = (const char *) a2;
+            const char **attr_ptr = (const char **) a3;
+            assert(attr_ptr);
+            *attr_ptr = query_host_attribute(key);
+            *sysret = 0;
+            break;
+        }
+
+        /* Load a shared library. */
+        case DLCALL_ID_LOAD_LIBRARY: {
+            const char *path = (const char *) a2;
+            int flags = (int) a3;
+            void **handle_ptr = (void **) a4;
+            assert(handle_ptr);
+            *handle_ptr = dlopen(path, flags);
+            *sysret = 0;
+            break;
+        }
+
+        /* Get the address of a function in a shared library. */
+        case DLCALL_ID_GET_PROC_ADDRESS: {
+            void *handle = (void *) a2;
+            const char *name = (const char *) a3;
+            void **entry_ptr = (void **) a4;
+            assert(entry_ptr);
+            *entry_ptr = dlsym(handle, name);
+            *sysret = 0;
+            break;
+        }
+
+        /* Free a shared library. */
+        case DLCALL_ID_FREE_LIBRARY: {
+            void *handle = (void *) a2;
+            int *ret_ptr = (int *) a3;
+            *ret_ptr = dlclose(handle);
+            *sysret = 0;
+            break;
+        }
+
+        /* Get the last error message for a library event. */
+        case DLCALL_ID_GET_LIBRARY_ERROR: {
+            const char **error_ptr = (const char **) a2;
+            *error_ptr = dlerror();
+            *sysret = 0;
+            break;
+        }
+
+        /* Invoke a function of a common interface. */
+        case DLCALL_ID_INVOKE_PROC: {
+            void *proc = (void *) a2;
+            void *arg1 = (void *) a3;
+            void *arg2 = (void *) a4;
+            assert(proc);
+            invoke_proc(proc, arg1, arg2);
+            *sysret = 0;
+            break;
+        }
+
+        default:
+            *sysret = -EINVAL;
+            break;
+        }
+        return true;
+    }
+    return false;
+}
+
+QEMU_PLUGIN_EXPORT int qemu_plugin_install(qemu_plugin_id_t id,
+                                           const qemu_info_t *info,
+                                           int argc, char **argv)
+{
+    if (info->system_emulation) {
+        fprintf(stderr, "plugin dlcall: only useful for user emulation\n");
+        return -1;
+    }
+
+    for (int i = 0; i < argc; i++) {
+        char *opt = argv[i];
+        g_auto(GStrv) tokens = g_strsplit(opt, "=", 2);
+        if (g_strcmp0(tokens[0], "syscall_num") == 0) {
+            const char *val = tokens[1];
+            char *endptr = NULL;
+            guint64 num;
+            if (!val || *val == '\0') {
+                fprintf(stderr,
+                        "plugin dlcall: missing value for syscall_num\n");
+                return -1;
+            }
+            num = g_ascii_strtoull(val, &endptr, 0);
+            if (*endptr != '\0' || g_strrstr(val, "-") != NULL) {
+                fprintf(stderr,
+                        "plugin dlcall: invalid syscall_num '%s'\n", val);
+                return -1;
+            }
+            if (num < DLCALL_SYSCALL_MIN || num > G_MAXINT64) {
+                fprintf(stderr,
+                        "plugin dlcall: syscall_num %s is out of range; "
+                        "it must be >= %d to avoid clashing with a real "
+                        "syscall\n", val, DLCALL_SYSCALL_MIN);
+                return -1;
+            }
+            dlcall_syscall_num = (int64_t) num;
+        } else {
+            fprintf(stderr, "plugin dlcall: unknown option '%s'\n", opt);
+            return -1;
+        }
+    }
+
+    qemu_plugin_register_vcpu_syscall_filter_cb(id, vcpu_syscall_filter, NULL);
+
+    return 0;
+}
diff --git a/contrib/plugins/meson.build b/contrib/plugins/meson.build
index 099319e7a1..e7fc4d6d8f 100644
--- a/contrib/plugins/meson.build
+++ b/contrib/plugins/meson.build
@@ -19,6 +19,11 @@ if host_os != 'windows'
   contrib_plugins += 'lockstep.c'
 endif
 
+if host_os == 'linux'
+  # dlcall passes guest calls through to host libraries; linux-user only
+  contrib_plugins += 'dlcall.c'
+endif
+
 if 'cpp' in all_languages
   contrib_plugins += 'cpp.cpp'
 endif
-- 
2.34.1



^ permalink raw reply related	[flat|nested] 15+ messages in thread

* Re: [PATCH v4 1/1] contrib/plugins: add a minimal dlcall plugin
  2026-06-29 16:02   ` [PATCH v4 1/1] contrib/plugins: add a minimal dlcall plugin Ziyang Zhang
@ 2026-06-30 19:18     ` Pierrick Bouvier
  0 siblings, 0 replies; 15+ messages in thread
From: Pierrick Bouvier @ 2026-06-30 19:18 UTC (permalink / raw)
  To: Ziyang Zhang, qemu-devel
  Cc: Riku Voipio, Laurent Vivier, Alex Bennee, Alexandre Iooss,
	Mahmoud Mandour, Pierrick Bouvier, Richard Henderson, Zhengwei Qi,
	Yun Wang, Mingyuan Xia, Kailiang Xu

On 6/29/2026 9:02 AM, Ziyang Zhang wrote:
> Add a minimal dlcall plugin that lets the guest invoke host functions
> through magic system calls. The plugin registers a vCPU syscall filter
> callback that intercepts a reserved syscall number and dispatches a set
> of pass-through operations: querying host attributes, loading and freeing
> shared libraries, resolving symbols, retrieving the last library error,
> and invoking a host function through a common interface.
> 
> The magic syscall number defaults to 4096 and can be overridden at load
> time with the "syscall_num=N" argument; values low enough to clash with a
> real syscall are rejected.
> 
> Co-authored-by: Kailiang Xu <xukl2019@sjtu.edu.cn>
> Co-authored-by: Mingyuan Xia <xiamy@ultrarisc.com>
> Signed-off-by: Ziyang Zhang <functioner@sjtu.edu.cn>
> ---
>  contrib/plugins/dlcall.c    | 238 ++++++++++++++++++++++++++++++++++++
>  contrib/plugins/meson.build |   5 +
>  2 files changed, 243 insertions(+)
>  create mode 100644 contrib/plugins/dlcall.c
> 
> diff --git a/contrib/plugins/dlcall.c b/contrib/plugins/dlcall.c
> new file mode 100644
> index 0000000000..3a8f47cdf8
> --- /dev/null
> +++ b/contrib/plugins/dlcall.c
> @@ -0,0 +1,238 @@
> +/*
> + * Copyright (C) 2026, Ziyang Zhang <functioner@sjtu.edu.cn>
> + *
> + * dlcall plugin: lets a linux-user guest invoke host functions by issuing a
> + * magic system call. The guest can ask QEMU to dlopen() a host shared
> + * library, dlsym() a symbol, and call it with guest-supplied arguments.
> + *
> + * WARNING: trusted guests only. The guest can load arbitrary host libraries
> + * and execute arbitrary host code with arbitrary arguments, i.e. full code
> + * execution in the QEMU host process. It is NOT a sandbox and provides no
> + * isolation; only load it for guests you fully trust.
> + *
> + * WARNING: requires guest_base == 0, which is qemu-user's default. Pointer
> + * operands are dereferenced as host addresses directly, and the invoked host
> + * functions dereference guest pointers with no address translation, so guest
> + * and host must share a single address space. A non-zero guest_base (e.g. set
> + * via -B/-R) would make every pointer off by guest_base and hit unrelated
> + * host memory.
> + *
> + * SPDX-License-Identifier: GPL-2.0-or-later
> + */
> +
> +#include <assert.h>
> +#include <errno.h>
> +#include <string.h>
> +#include <stdio.h>
> +#include <glib.h>
> +#include <dlfcn.h>
> +
> +#include <qemu-plugin.h>
> +
> +QEMU_PLUGIN_EXPORT int qemu_plugin_version = QEMU_PLUGIN_VERSION;
> +
> +/*
> + * The magic system call number for dlcall.
> + *
> + * It defaults to DLCALL_SYSCALL_DEFAULT and can be overridden at load time
> + * with the "syscall_num=N" argument. To avoid hijacking a real syscall the
> + * guest might issue, N must be at least DLCALL_SYSCALL_MIN: every Linux ABI
> + * keeps its syscall numbers well below this; numbers from here up are free.
> + */
> +enum {
> +    DLCALL_SYSCALL_DEFAULT = 4096,
> +    DLCALL_SYSCALL_MIN = 4096,
> +};
> +
> +static int64_t dlcall_syscall_num = DLCALL_SYSCALL_DEFAULT;
> +
> +/*
> + * dlcall calling convention.
> + *
> + * The guest issues the magic system call (dlcall_syscall_num). The first
> + * argument (a1) is one of the call IDs below; the remaining arguments (a2, a3,
> + * a4, ...) are that ID's operands. All pointer operands are guest virtual
> + * addresses that the plugin dereferences as host addresses directly (see the
> + * guest_base requirement above). Results are written back through
> + * caller-provided "out" pointers rather than returned in the syscall value.
> + *
> + * The syscall return value (*sysret) only reports dispatch status: 0 on a
> + * recognised ID, -EINVAL for an unknown one. The actual success/failure of an
> + * operation (e.g. a NULL handle from dlopen) is delivered through its out
> + * pointer, exactly like the underlying libdl call.
> + *
> + * Operands per ID:
> + *
> + *   DLCALL_ID_GET_HOST_ATTRIBUTE
> + *     a2  const char *key        in:  attribute name to query
> + *     a3  const char **attr_ptr  out: matching value, or NULL if unknown
> + *
> + *   DLCALL_ID_LOAD_LIBRARY                            (wraps dlopen)
> + *     a2  const char *path       in:  library path
> + *     a3  int flags              in:  dlopen() flags (e.g. RTLD_NOW)
> + *     a4  void **handle_ptr      out: library handle, or NULL on failure
> + *
> + *   DLCALL_ID_GET_PROC_ADDRESS                        (wraps dlsym)
> + *     a2  void *handle           in:  library handle
> + *     a3  const char *name       in:  symbol name
> + *     a4  void **entry_ptr       out: symbol address, or NULL if not found
> + *
> + *   DLCALL_ID_FREE_LIBRARY                            (wraps dlclose)
> + *     a2  void *handle           in:  library handle
> + *     a3  int *ret_ptr           out: dlclose() return value (0 on success)
> + *
> + *   DLCALL_ID_GET_LIBRARY_ERROR                       (wraps dlerror)
> + *     a2  const char **error_ptr out: last libdl error string, or NULL
> + *
> + *   DLCALL_ID_INVOKE_PROC                             (calls the symbol)
> + *     a2  void *proc             in:  function pointer, signature
> + *                                     void (*)(void *arg1, void *arg2)
> + *     a3  void *arg1             in:  first argument forwarded to proc
> + *     a4  void *arg2             in:  second argument forwarded to proc
> + */
> +enum DlcallID {
> +    DLCALL_ID_GET_HOST_ATTRIBUTE,
> +    DLCALL_ID_LOAD_LIBRARY,
> +    DLCALL_ID_GET_PROC_ADDRESS,
> +    DLCALL_ID_FREE_LIBRARY,
> +    DLCALL_ID_GET_LIBRARY_ERROR,
> +    DLCALL_ID_INVOKE_PROC,
> +};
> +
> +static inline const char *query_host_attribute(const char *key)
> +{
> +    if (strcmp(key, "emu") == 0) {
> +        return "qemu";
> +    }
> +    return NULL;
> +}
> +
> +static inline void invoke_proc(void *proc, void *arg1, void *arg2)
> +{
> +    typedef void (*Func)(void * /*arg1*/, void * /*arg2*/);
> +    Func func = (Func) proc;
> +    func(arg1, arg2);
> +}
> +
> +static bool vcpu_syscall_filter(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,
> +                                int64_t *sysret, void *userdata)
> +{
> +    if (num == dlcall_syscall_num) {
> +        switch (a1) {
> +        /* Query host attribute by a reserved key. */
> +        case DLCALL_ID_GET_HOST_ATTRIBUTE: {
> +            const char *key = (const char *) a2;
> +            const char **attr_ptr = (const char **) a3;
> +            assert(attr_ptr);
> +            *attr_ptr = query_host_attribute(key);
> +            *sysret = 0;
> +            break;
> +        }
> +
> +        /* Load a shared library. */
> +        case DLCALL_ID_LOAD_LIBRARY: {
> +            const char *path = (const char *) a2;
> +            int flags = (int) a3;
> +            void **handle_ptr = (void **) a4;
> +            assert(handle_ptr);
> +            *handle_ptr = dlopen(path, flags);
> +            *sysret = 0;
> +            break;
> +        }
> +
> +        /* Get the address of a function in a shared library. */
> +        case DLCALL_ID_GET_PROC_ADDRESS: {
> +            void *handle = (void *) a2;
> +            const char *name = (const char *) a3;
> +            void **entry_ptr = (void **) a4;
> +            assert(entry_ptr);
> +            *entry_ptr = dlsym(handle, name);
> +            *sysret = 0;
> +            break;
> +        }
> +
> +        /* Free a shared library. */
> +        case DLCALL_ID_FREE_LIBRARY: {
> +            void *handle = (void *) a2;
> +            int *ret_ptr = (int *) a3;
> +            *ret_ptr = dlclose(handle);
> +            *sysret = 0;
> +            break;
> +        }
> +
> +        /* Get the last error message for a library event. */
> +        case DLCALL_ID_GET_LIBRARY_ERROR: {
> +            const char **error_ptr = (const char **) a2;
> +            *error_ptr = dlerror();
> +            *sysret = 0;
> +            break;
> +        }
> +
> +        /* Invoke a function of a common interface. */
> +        case DLCALL_ID_INVOKE_PROC: {
> +            void *proc = (void *) a2;
> +            void *arg1 = (void *) a3;
> +            void *arg2 = (void *) a4;
> +            assert(proc);
> +            invoke_proc(proc, arg1, arg2);
> +            *sysret = 0;
> +            break;
> +        }
> +
> +        default:
> +            *sysret = -EINVAL;
> +            break;
> +        }
> +        return true;
> +    }
> +    return false;
> +}
> +
> +QEMU_PLUGIN_EXPORT int qemu_plugin_install(qemu_plugin_id_t id,
> +                                           const qemu_info_t *info,
> +                                           int argc, char **argv)
> +{
> +    if (info->system_emulation) {
> +        fprintf(stderr, "plugin dlcall: only useful for user emulation\n");
> +        return -1;
> +    }
> +
> +    for (int i = 0; i < argc; i++) {
> +        char *opt = argv[i];
> +        g_auto(GStrv) tokens = g_strsplit(opt, "=", 2);
> +        if (g_strcmp0(tokens[0], "syscall_num") == 0) {
> +            const char *val = tokens[1];
> +            char *endptr = NULL;
> +            guint64 num;
> +            if (!val || *val == '\0') {
> +                fprintf(stderr,
> +                        "plugin dlcall: missing value for syscall_num\n");
> +                return -1;
> +            }
> +            num = g_ascii_strtoull(val, &endptr, 0);
> +            if (*endptr != '\0' || g_strrstr(val, "-") != NULL) {
> +                fprintf(stderr,
> +                        "plugin dlcall: invalid syscall_num '%s'\n", val);
> +                return -1;
> +            }
> +            if (num < DLCALL_SYSCALL_MIN || num > G_MAXINT64) {
> +                fprintf(stderr,
> +                        "plugin dlcall: syscall_num %s is out of range; "
> +                        "it must be >= %d to avoid clashing with a real "
> +                        "syscall\n", val, DLCALL_SYSCALL_MIN);
> +                return -1;
> +            }
> +            dlcall_syscall_num = (int64_t) num;
> +        } else {
> +            fprintf(stderr, "plugin dlcall: unknown option '%s'\n", opt);
> +            return -1;
> +        }
> +    }
> +
> +    qemu_plugin_register_vcpu_syscall_filter_cb(id, vcpu_syscall_filter, NULL);
> +
> +    return 0;
> +}
> diff --git a/contrib/plugins/meson.build b/contrib/plugins/meson.build
> index 099319e7a1..e7fc4d6d8f 100644
> --- a/contrib/plugins/meson.build
> +++ b/contrib/plugins/meson.build
> @@ -19,6 +19,11 @@ if host_os != 'windows'
>    contrib_plugins += 'lockstep.c'
>  endif
>  
> +if host_os == 'linux'
> +  # dlcall passes guest calls through to host libraries; linux-user only
> +  contrib_plugins += 'dlcall.c'
> +endif
> +
>  if 'cpp' in all_languages
>    contrib_plugins += 'cpp.cpp'
>  endif

Plugin can be renamed to Lorelei.
Also, some (QEMU) documentation should be added about how to use it.
(see docs/about/emulation.rst)
Ideally, with a minimal example of how to use Lorelei, without all the
internal details.

Overall, it's all good, there are just some details about Lorelei itself
that we need to take care of. See my answer on the cover letter.

Regards,
Pierrick


^ permalink raw reply	[flat|nested] 15+ messages in thread

* Re: [PATCH v4 0/1] contrib/plugins: add dlcall to call host functions from a guest
  2026-06-29 16:02 ` [PATCH v4 " Ziyang Zhang
  2026-06-29 16:02   ` [PATCH v4 1/1] contrib/plugins: add a minimal dlcall plugin Ziyang Zhang
@ 2026-06-30 19:34   ` Pierrick Bouvier
  2026-07-01  1:48     ` Ziyang Zhang
  1 sibling, 1 reply; 15+ messages in thread
From: Pierrick Bouvier @ 2026-06-30 19:34 UTC (permalink / raw)
  To: Ziyang Zhang, qemu-devel
  Cc: Riku Voipio, Laurent Vivier, Alex Bennee, Alexandre Iooss,
	Mahmoud Mandour, Pierrick Bouvier, Richard Henderson, Zhengwei Qi,
	Yun Wang, Mingyuan Xia, Kailiang Xu

On 6/29/2026 9:02 AM, Ziyang Zhang wrote:
> Hi,
> 
> This patch adds a single plugin, contrib/plugins/dlcall.c (~240 lines,
> no changes to QEMU core), that lets a linux-user guest call functions in the
> host's native shared libraries instead of emulating them.
> 
> It is the natural next step on top of the vCPU syscall-filter callback that I
> contributed and that was merged earlier:
> 
>   https://lore.kernel.org/qemu-devel/20251214144620.179282-1-functioner@sjtu.edu.cn/
> 
> Why bother? Because it turns slow, instruction-by-instruction emulation of a
> library into a native host call. Some results, all on completely unmodified
> guest binaries:
> 
>   * minizip (the stock zlib utility) compresses several times faster, because
>     the actual deflate runs natively on the host instead of being translated.
>   * Real OpenGL/Vulkan games run under qemu-user: SuperTuxKart and Hollow
>     Knight are playable, with their graphics calls going straight to the host
>     GPU.
> 
> How it works
> ============
> 
> The guest makes a system call with a reserved number (4096 by default) that no
> real Linux ABI uses. Its first argument selects a pass-through operation, and the
> rest carry operands:
> 
>   syscall(4096, op, arg1, arg2, ...)
>           |     |    \............ operands (pointers / values)
>           |     \................. which pass-through operation
>           \....................... the reserved "magic" number
> 
> The plugin registers a vCPU syscall filter: before QEMU forwards a syscall to
> the host kernel, the filter runs, sees 4096, performs the operation on the
> host, writes the result back, and tells QEMU the syscall is consumed, so the
> real kernel never sees it.
> 
> The whole interface is just a handful of primitives:
> 
>   * query a host attribute
>   * dlopen / dlclose a host shared library
>   * dlsym a symbol, and read the last dlerror
>   * invoke a resolved host function with a void(void *, void *) signature
> 
> That is all the plugin does. It knows nothing about zlib, X11 or OpenGL, or
> about any library's calling convention.
> 
> The same machinery also runs in reverse: when a host function needs to call
> back into the guest (a qsort comparator, an allocator, a GUI or game callback),
> control re-enters the guest to run the callback and then resumes the suspended
> host call. This reentry is what lets stateful, callback-driven APIs work, not
> just leaf functions.
> 
> Why the plugin belongs in QEMU, and the rest does not
> =====================================================
> 
> Only the plugin lives in the tree. Everything else is ordinary userspace:
> 
>   --- userspace (out of tree, not tied to any DBT) -------------
>       guest: unmodified program  ->  guest runtime + thunk libs
>   --------------------------------------------------------------
>                  |  syscall(4096, op, args)   (only crossing point)
>                  v
>   === inside QEMU: THIS PATCH, ~240 lines ======================
>       dlcall plugin:  dlopen / dlsym / invoke a host fn
>   ==============================================================
>                  |
>                  v
>   --- userspace (out of tree) ----------------------------------
>       host: host runtime + thunk libs  ->  real libz / libGL ...
>   --------------------------------------------------------------
> 
> The split is deliberate, and it is why only this one file is proposed for the
> tree:
> 
>   * This plugin defines the most general interaction interface for native
>     pass-through: the magic-syscall ABI between an emulated guest and its
>     emulator. That contract is what every pass-through implementation builds
>     on, so it belongs in a stable, shared place.
>   * It is also the only piece that is inherently QEMU-specific: it plugs into
>     QEMU's syscall-filter hook and runs inside the QEMU process. The argument
>     marshalling, calling conventions, callbacks/reentry and per-library
>     coverage are not tied to any particular DBT and behave as ordinary
>     userspace, so they should stay out of tree rather than couple QEMU to them.
> 
> Background: we presented this approach at KVM Forum 2025, "Lorelei: Enable QEMU
> to Leverage Native Shared Libraries":
> 
>   https://www.youtube.com/watch?v=_jioQFm7wyU
> 
> The userspace side
> ==================
> 
> A fair point on v3 was that, on its own, the plugin is only half of an
> interface: useful only if the other half (the guest/host runtimes and the
> per-library thunks) is public and specified, rather than a private demo. That
> half is now available as a standalone, documented, CI-tested project, Lorelei:
> 
>   https://github.com/rover2024/lorelei
> 
> Lorelei provides the guest and host runtimes and a Thunk Library Compiler
> (TLC, built on Clang LibTooling) that reads a library's headers and generates
> the guest and host thunks automatically, including the awkward cases of
> function-pointer callbacks and variadic functions. It has CI for x86_64, arm64
> and riscv64 hosts. Lorelei and its thunk libraries are MIT-licensed.
>

Thanks for publishing it.

A few ideas to make Lorelei even more easy to use:

Would that be possible to add binary artifacts to the existing 1.0.0
Lorelei release?
Ideally, it should provide a toolchain (for x64 hosts) with support for
x64, arm64 and riscv64 guest thunks.

Also, would that possible to provide a script to generate boilerplate
for thunks, and extract list of symbols from a given library?

My ultimate goal would be to have something as simple as:
$ wget toolchain.hostx64.tar.gz

# automatically generate boilerplate + compile guest/host thunks
$ toolchain.hostx64/bin/generate-thunks \
  --arch arm64 --library /usr/lib/libfoo.so \
  --header /usr/include/foo.h --out thunks_arm64

$ aarch64-linux-gnu-gcc main.c -lfoo
$ env LD_LIBRARY_PATH=./thunks_arm64 \
  qemu-arm64 ./a.out -plugin contrib/plugins/liblorelei.so

I understand it asks for more, but we really need to make lorelei as
simple to use as possible, without pushing the internal details to users.

What do you think about this?

> Because the plugin is not upstream yet, Lorelei currently builds and tests
> against the QEMU fork that carries it.
> 
> The plugin stays deliberately minimal and prescribes nothing about how
> thunking is done. Lorelei is one reference implementation of the userspace
> side. Any toolchain, or another instrumentation framework, can implement the
> same dlcall interface.
> 
> A from-scratch walkthrough of the bare mechanism, with the minizip and
> OpenGL/X11 examples above, is also available here:
> 
>   https://github.com/rover2024/qemu-passthrough-test
> 
> It is fully opt-in (loaded with -plugin) and targets linux-user, where the
> guest and host already share a trust domain. The test cases use x86_64 guests
> and run on x86_64, arm64 and riscv64 Linux hosts.
> 
> I have deliberately kept Lorelei out of the patch itself: neither the code
> nor its comments mention it, since I am not sure whether referencing an
> external project from the tree is welcome. I would appreciate guidance on
> where a pointer to the toolchain belongs, if anywhere. For example, would it
> be appropriate to mention it from the documentation patch?
> 
> Feedback on the plugin and on the pass-through approach is welcome.
> 
> Changes since v3:
> 
>   * Lorelei, the userspace toolchain that implements this interface, is now a
>     public, documented, CI-tested project, with a Thunk Library Compiler that
>     generates the guest/host thunks automatically. This addresses the v3
>     feedback that the interface needs a public implementation behind it. The
>     plugin code itself is unchanged.
>   * The documentation patch is held back for now.
> 
> Changes since v2:
> 
>   * Dropped the RFC tag. The approach was positively received on v2.
>   * Rebased on master and adjusted the syscall-filter callback to its updated
>     signature (int64_t sysret, added userdata, dropped the plugin id
>     argument).
> 
> Changes since v1:
> 
>   * Renamed the plugin from "passthrough" to "dlcall" (Pierrick Bouvier).
>     The old name was too generic. The name "dlcall" reflects what the plugin actually
>     does (dlopen/dlsym a host symbol and call it) and avoids confusion with
>     QEMU's existing plugin hostcall concept (QEMU_PLUGIN_*_HOSTCALL).
>   * Made the magic syscall number configurable at load time via the
>     "syscall_num=N" argument, defaulting to 4096 and rejecting values low
>     enough to clash with a real syscall (Pierrick Bouvier).
> 
> v1: https://lore.kernel.org/qemu-devel/20260617130742.769234-1-functioner@sjtu.edu.cn/
> v2: https://lore.kernel.org/qemu-devel/20260619045404.820960-1-functioner@sjtu.edu.cn/
> v3: https://lore.kernel.org/qemu-devel/20260622163438.130746-1-functioner@sjtu.edu.cn/
> 
> Thanks,
> Ziyang Zhang
> 
> Ziyang Zhang (1):
>   contrib/plugins: add a minimal dlcall plugin
> 
>  contrib/plugins/dlcall.c    | 238 ++++++++++++++++++++++++++++++++++++
>  contrib/plugins/meson.build |   5 +
>  2 files changed, 243 insertions(+)
>  create mode 100644 contrib/plugins/dlcall.c
> 

Thanks,
Pierrick


^ permalink raw reply	[flat|nested] 15+ messages in thread

* Re: [PATCH v4 0/1] contrib/plugins: add dlcall to call host functions from a guest
  2026-06-30 19:34   ` [PATCH v4 0/1] contrib/plugins: add dlcall to call host functions from a guest Pierrick Bouvier
@ 2026-07-01  1:48     ` Ziyang Zhang
  2026-07-02 18:32       ` Pierrick Bouvier
  0 siblings, 1 reply; 15+ messages in thread
From: Ziyang Zhang @ 2026-07-01  1:48 UTC (permalink / raw)
  To: Pierrick Bouvier, qemu-devel
  Cc: Riku Voipio, Laurent Vivier, Alex Bennee, Alexandre Iooss,
	Mahmoud Mandour, Pierrick Bouvier, Richard Henderson, Zhengwei Qi,
	Yun Wang, Mingyuan Xia, Kailiang Xu

Hi Pierrick,

Thanks for the review, and for thinking about the end-user experience.

On Tue, 30 Jun 2026 12:34:59 -0700, Pierrick Bouvier wrote:
> Thanks for publishing it.
> 
> A few ideas to make Lorelei even more easy to use:
> 
> Would that be possible to add binary artifacts to the existing 1.0.0
> Lorelei release?
> Ideally, it should provide a toolchain (for x64 hosts) with support for
> x64, arm64 and riscv64 guest thunks.
> 
> Also, would that possible to provide a script to generate boilerplate
> for thunks, and extract list of symbols from a given library?
> 
> My ultimate goal would be to have something as simple as:
> $ wget toolchain.hostx64.tar.gz
> 
> # automatically generate boilerplate + compile guest/host thunks
> $ toolchain.hostx64/bin/generate-thunks \
>    --arch arm64 --library /usr/lib/libfoo.so \
>    --header /usr/include/foo.h --out thunks_arm64
> 
> $ aarch64-linux-gnu-gcc main.c -lfoo
> $ env LD_LIBRARY_PATH=./thunks_arm64 \
>    qemu-arm64 ./a.out -plugin contrib/plugins/liblorelei.so
> 
> I understand it asks for more, but we really need to make lorelei as
> simple to use as possible, without pushing the internal details to users.
> 
> What do you think about this?


Prebuilt toolchain / instant acceleration

Agreed: the goal is to install it and feel the speedup right away,
with no build step. I'll do this, but as a point release on top of
1.0.0.0 rather than in the initial tag, so it lands as its own
increment. The model is FEX's FEXFetchRootFS: a prebuilt host
toolchain plus ready-made thunks, so a user goes from download
straight to an accelerated guest. Like FEXFetchRootFS, the tool will
likely have the user pick their distribution and version, because a
thunk is bound to a specific library build: the exported symbols, and
sometimes the ABI, differ across distros and releases, so the prebuilt
thunks must match the host's real libraries.

Generating thunks for a new library

One caveat on the generate-thunks sketch. Lorelei guarantees that
thunk *generation* is automatic and reproducible: once a library is
described, TLC deterministically emits both sides, including the
callback and variadic cases. What it cannot do on its own is
*describe* the library, that is, pick the symbols you need and write
descriptors for the ones that do not marshal trivially (callbacks by
pointer, ownership crossing the boundary). Those stay judgment calls
that need a human, or an AI, in the loop.

So adding a new library is not one fully automatic command. What
Lorelei does is drastically cut the effort versus hand-writing every
thunk the way Box64 does: the generate-thunks helper automates what
is automatable (symbol extraction, boilerplate, building both sides),
so simple libraries are close to one-shot and harder ones start from
a skeleton rather than a blank file.

Plugin name

I would rather keep "dlcall" than rename it to "lorelei". The plugin
defines a general interface (the magic-syscall dlopen/dlsym/invoke
ABI), and Lorelei is only one userspace implementation of it now:

   - "dlcall" says what the plugin does and is understood without
     leaving the tree. "lorelei" names an external project and says
     nothing about the mechanism.
   - An in-tree interface should not be tied to the branding or
     lifetime of an out-of-tree project. If Lorelei is renamed,
     forked, or joined by other toolchains, the name becomes
     misleading.
   - Naming it after one implementation implies it is the blessed one
     and discourages alternatives. A generic name keeps QEMU neutral.
   - It also matches how the other contrib plugins are named, by
     functionality (lockstep, cache, ...).

I do agree Lorelei should be discoverable from the tree, so I'll add
docs to docs/about/emulation.rst that introduce the dlcall interface
and point at Lorelei as a ready-to-use implementation, with a minimal
example and without the internal details.

Does that sound reasonable?

Thanks,
Ziyang Zhang




^ permalink raw reply	[flat|nested] 15+ messages in thread

* Re: [PATCH v4 0/1] contrib/plugins: add dlcall to call host functions from a guest
  2026-07-01  1:48     ` Ziyang Zhang
@ 2026-07-02 18:32       ` Pierrick Bouvier
  2026-07-03  9:17         ` Alex Bennée
  0 siblings, 1 reply; 15+ messages in thread
From: Pierrick Bouvier @ 2026-07-02 18:32 UTC (permalink / raw)
  To: Ziyang Zhang, qemu-devel
  Cc: Riku Voipio, Laurent Vivier, Alex Bennee, Alexandre Iooss,
	Mahmoud Mandour, Pierrick Bouvier, Richard Henderson, Zhengwei Qi,
	Yun Wang, Mingyuan Xia, Kailiang Xu

On 6/30/2026 6:48 PM, Ziyang Zhang wrote:
> Hi Pierrick,
> 
> Thanks for the review, and for thinking about the end-user experience.
> 
> On Tue, 30 Jun 2026 12:34:59 -0700, Pierrick Bouvier wrote:
>> Thanks for publishing it.
>>
>> A few ideas to make Lorelei even more easy to use:
>>
>> Would that be possible to add binary artifacts to the existing 1.0.0
>> Lorelei release?
>> Ideally, it should provide a toolchain (for x64 hosts) with support for
>> x64, arm64 and riscv64 guest thunks.
>>
>> Also, would that possible to provide a script to generate boilerplate
>> for thunks, and extract list of symbols from a given library?
>>
>> My ultimate goal would be to have something as simple as:
>> $ wget toolchain.hostx64.tar.gz
>>
>> # automatically generate boilerplate + compile guest/host thunks
>> $ toolchain.hostx64/bin/generate-thunks \
>>    --arch arm64 --library /usr/lib/libfoo.so \
>>    --header /usr/include/foo.h --out thunks_arm64
>>
>> $ aarch64-linux-gnu-gcc main.c -lfoo
>> $ env LD_LIBRARY_PATH=./thunks_arm64 \
>>    qemu-arm64 ./a.out -plugin contrib/plugins/liblorelei.so
>>
>> I understand it asks for more, but we really need to make lorelei as
>> simple to use as possible, without pushing the internal details to users.
>>
>> What do you think about this?
> 
> 
> Prebuilt toolchain / instant acceleration
> 
> Agreed: the goal is to install it and feel the speedup right away,
> with no build step. I'll do this, but as a point release on top of
> 1.0.0.0 rather than in the initial tag, so it lands as its own
> increment. The model is FEX's FEXFetchRootFS: a prebuilt host
> toolchain plus ready-made thunks, so a user goes from download
> straight to an accelerated guest. Like FEXFetchRootFS, the tool will
> likely have the user pick their distribution and version, because a
> thunk is bound to a specific library build: the exported symbols, and
> sometimes the ABI, differ across distros and releases, so the prebuilt
> thunks must match the host's real libraries.
> 
> Generating thunks for a new library
> 
> One caveat on the generate-thunks sketch. Lorelei guarantees that
> thunk *generation* is automatic and reproducible: once a library is
> described, TLC deterministically emits both sides, including the
> callback and variadic cases. What it cannot do on its own is
> *describe* the library, that is, pick the symbols you need and write
> descriptors for the ones that do not marshal trivially (callbacks by
> pointer, ownership crossing the boundary). Those stay judgment calls
> that need a human, or an AI, in the loop.
> 
> So adding a new library is not one fully automatic command. What
> Lorelei does is drastically cut the effort versus hand-writing every
> thunk the way Box64 does: the generate-thunks helper automates what
> is automatable (symbol extraction, boilerplate, building both sides),
> so simple libraries are close to one-shot and harder ones start from
> a skeleton rather than a blank file.
> 
> Plugin name
> 
> I would rather keep "dlcall" than rename it to "lorelei". The plugin
> defines a general interface (the magic-syscall dlopen/dlsym/invoke
> ABI), and Lorelei is only one userspace implementation of it now:
> 
>   - "dlcall" says what the plugin does and is understood without
>     leaving the tree. "lorelei" names an external project and says
>     nothing about the mechanism.
>   - An in-tree interface should not be tied to the branding or
>     lifetime of an out-of-tree project. If Lorelei is renamed,
>     forked, or joined by other toolchains, the name becomes
>     misleading.
>   - Naming it after one implementation implies it is the blessed one
>     and discourages alternatives. A generic name keeps QEMU neutral.
>   - It also matches how the other contrib plugins are named, by
>     functionality (lockstep, cache, ...).
>

I agree with your arguments, let's keep this name then.

> I do agree Lorelei should be discoverable from the tree, so I'll add
> docs to docs/about/emulation.rst that introduce the dlcall interface
> and point at Lorelei as a ready-to-use implementation, with a minimal
> example and without the internal details.
> 
> Does that sound reasonable?
>

Yes sounds good to me!

> Thanks,
> Ziyang Zhang
> 
> 

Thanks,
Pierrick


^ permalink raw reply	[flat|nested] 15+ messages in thread

* Re: [PATCH v4 0/1] contrib/plugins: add dlcall to call host functions from a guest
  2026-07-02 18:32       ` Pierrick Bouvier
@ 2026-07-03  9:17         ` Alex Bennée
  0 siblings, 0 replies; 15+ messages in thread
From: Alex Bennée @ 2026-07-03  9:17 UTC (permalink / raw)
  To: Pierrick Bouvier
  Cc: Ziyang Zhang, qemu-devel, Riku Voipio, Laurent Vivier,
	Alexandre Iooss, Mahmoud Mandour, Pierrick Bouvier,
	Richard Henderson, Zhengwei Qi, Yun Wang, Mingyuan Xia,
	Kailiang Xu

Pierrick Bouvier <pierrick.bouvier@oss.qualcomm.com> writes:

> On 6/30/2026 6:48 PM, Ziyang Zhang wrote:
>> Hi Pierrick,
>> 
>> Thanks for the review, and for thinking about the end-user experience.
>> 
>> On Tue, 30 Jun 2026 12:34:59 -0700, Pierrick Bouvier wrote:
>>> Thanks for publishing it.
>>>
>>> A few ideas to make Lorelei even more easy to use:
>>>
>>> Would that be possible to add binary artifacts to the existing 1.0.0
>>> Lorelei release?
>>> Ideally, it should provide a toolchain (for x64 hosts) with support for
>>> x64, arm64 and riscv64 guest thunks.
>>>
>>> Also, would that possible to provide a script to generate boilerplate
>>> for thunks, and extract list of symbols from a given library?
>>>
>>> My ultimate goal would be to have something as simple as:
>>> $ wget toolchain.hostx64.tar.gz
>>>
>>> # automatically generate boilerplate + compile guest/host thunks
>>> $ toolchain.hostx64/bin/generate-thunks \
>>>    --arch arm64 --library /usr/lib/libfoo.so \
>>>    --header /usr/include/foo.h --out thunks_arm64
>>>
>>> $ aarch64-linux-gnu-gcc main.c -lfoo
>>> $ env LD_LIBRARY_PATH=./thunks_arm64 \
>>>    qemu-arm64 ./a.out -plugin contrib/plugins/liblorelei.so
>>>
>>> I understand it asks for more, but we really need to make lorelei as
>>> simple to use as possible, without pushing the internal details to users.
>>>
>>> What do you think about this?
>> 
>> 
>> Prebuilt toolchain / instant acceleration
>> 
>> Agreed: the goal is to install it and feel the speedup right away,
>> with no build step. I'll do this, but as a point release on top of
>> 1.0.0.0 rather than in the initial tag, so it lands as its own
>> increment. The model is FEX's FEXFetchRootFS: a prebuilt host
>> toolchain plus ready-made thunks, so a user goes from download
>> straight to an accelerated guest. Like FEXFetchRootFS, the tool will
>> likely have the user pick their distribution and version, because a
>> thunk is bound to a specific library build: the exported symbols, and
>> sometimes the ABI, differ across distros and releases, so the prebuilt
>> thunks must match the host's real libraries.
>> 
>> Generating thunks for a new library
>> 
>> One caveat on the generate-thunks sketch. Lorelei guarantees that
>> thunk *generation* is automatic and reproducible: once a library is
>> described, TLC deterministically emits both sides, including the
>> callback and variadic cases. What it cannot do on its own is
>> *describe* the library, that is, pick the symbols you need and write
>> descriptors for the ones that do not marshal trivially (callbacks by
>> pointer, ownership crossing the boundary). Those stay judgment calls
>> that need a human, or an AI, in the loop.
>> 
>> So adding a new library is not one fully automatic command. What
>> Lorelei does is drastically cut the effort versus hand-writing every
>> thunk the way Box64 does: the generate-thunks helper automates what
>> is automatable (symbol extraction, boilerplate, building both sides),
>> so simple libraries are close to one-shot and harder ones start from
>> a skeleton rather than a blank file.
>> 
>> Plugin name
>> 
>> I would rather keep "dlcall" than rename it to "lorelei". The plugin
>> defines a general interface (the magic-syscall dlopen/dlsym/invoke
>> ABI), and Lorelei is only one userspace implementation of it now:
>> 
>>   - "dlcall" says what the plugin does and is understood without
>>     leaving the tree. "lorelei" names an external project and says
>>     nothing about the mechanism.
>>   - An in-tree interface should not be tied to the branding or
>>     lifetime of an out-of-tree project. If Lorelei is renamed,
>>     forked, or joined by other toolchains, the name becomes
>>     misleading.
>>   - Naming it after one implementation implies it is the blessed one
>>     and discourages alternatives. A generic name keeps QEMU neutral.
>>   - It also matches how the other contrib plugins are named, by
>>     functionality (lockstep, cache, ...).
>>
>
> I agree with your arguments, let's keep this name then.
>
>> I do agree Lorelei should be discoverable from the tree, so I'll add
>> docs to docs/about/emulation.rst that introduce the dlcall interface
>> and point at Lorelei as a ready-to-use implementation, with a minimal
>> example and without the internal details.
>> 
>> Does that sound reasonable?
>>
>
> Yes sounds good to me!

Sounds good to me as well ;-)

-- 
Alex Bennée
Virtualisation Tech Lead @ Linaro


^ permalink raw reply	[flat|nested] 15+ messages in thread

end of thread, other threads:[~2026-07-03  9:17 UTC | newest]

Thread overview: 15+ messages (download: mbox.gz follow: Atom feed
-- links below jump to the message on this page --
2026-06-22 16:34 [PATCH v3 0/1] contrib/plugins: add dlcall to call host functions from a guest Ziyang Zhang
2026-06-22 16:34 ` [PATCH v3 1/1] contrib/plugins: add a minimal dlcall plugin Ziyang Zhang
2026-06-22 17:16 ` [PATCH v3 0/1] contrib/plugins: add dlcall to call host functions from a guest Pierrick Bouvier
2026-06-22 17:50   ` Ziyang Zhang
2026-06-22 18:04     ` Pierrick Bouvier
2026-06-23  2:51       ` Ziyang Zhang
2026-06-23 17:16         ` Pierrick Bouvier
2026-06-23 17:34           ` Ziyang Zhang
2026-06-29 16:02 ` [PATCH v4 " Ziyang Zhang
2026-06-29 16:02   ` [PATCH v4 1/1] contrib/plugins: add a minimal dlcall plugin Ziyang Zhang
2026-06-30 19:18     ` Pierrick Bouvier
2026-06-30 19:34   ` [PATCH v4 0/1] contrib/plugins: add dlcall to call host functions from a guest Pierrick Bouvier
2026-07-01  1:48     ` Ziyang Zhang
2026-07-02 18:32       ` Pierrick Bouvier
2026-07-03  9:17         ` Alex Bennée

This is an external index of several public inboxes,
see mirroring instructions on how to clone and mirror
all data and code used by this external index.