Merge tag 'pbouvier/pr/plugins-20260713' of https://gitlab.com/p-b-o/qemu into staging

Changes:
- [PATCH v1] uftrace: riscv privilege level (Yanfeng Liu <yfliu2008@qq.com>)
  Link: https://lore.kernel.org/qemu-devel/tencent_E17E8F6494EE130F71527C6BCE481AF33E08@qq.com
- [PATCH v12 0/2] contrib/plugins: add dlcall to call host functions (Ziyang Zhang <functioner@sjtu.edu.cn>)
  Link: https://lore.kernel.org/qemu-devel/20260711094523.622997-1-functioner@sjtu.edu.cn

# -----BEGIN PGP SIGNATURE-----
#
# iQGzBAABCgAdFiEEN8FWlNi6l2Sxlz/btEQ30ZwoYt8FAmpVKSgACgkQtEQ30Zwo
# Yt9wYgv8D3Uh6ebumK4SlHAmMm41gF5wjZsfmA5YXstcPlmxt2sZEbVaTnyLyhZd
# Q8F34HVJ5vaClPoFiQKz+arCLFvLZ4MAgqMy/pxedIhJVhW2b/dt0Z10HkJc8kDx
# baJyv36GmUwnXrTWGZAuLZq7CbQ1xSH4BN6qHAY6mpHogdISABhG6hArz9leHZO/
# NlnxFpfkr29Hd1XLu5u7de35SWRtQbAJYCqtELmR/ZxhKOJU6kr63VR4zr5jVVNX
# px6JQxPkeSFypHV1+klbxCQiXkflwBrYpIKRXrAVNZ7HwMXK6iDw4D/L5TzyxdJn
# 7yMVZ05+xI0o+yS46A0btGoinrnNyjeZjKh1VV7A4DuZ7mfbIII+fvE3JdAzvhzO
# xvrTwwYksiztEK86r8yq7DT9FIx5LLF+B93NgdUtUvvOQP1z1BvvCdePN1KwUrpo
# UgO45akAv8QBVD5uNS4Lh/fG3HarYgdrwt87P3vgynEhuGIE+7QZfOmooNwlwu8v
# SSngbr/5
# =9JyL
# -----END PGP SIGNATURE-----
# gpg: Signature made Mon 13 Jul 2026 19:06:32 BST
# gpg:                using RSA key 37C15694D8BA9764B1973FDBB44437D19C2862DF
# gpg: Good signature from "Pierrick Bouvier <pierrick.bouvier@oss.qualcomm.com>" [unknown]
# gpg: WARNING: This key is not certified with a trusted signature!
# gpg:          There is no indication that the signature belongs to the owner.
# Primary key fingerprint: 37C1 5694 D8BA 9764 B197  3FDB B444 37D1 9C28 62DF

* tag 'pbouvier/pr/plugins-20260713' of https://gitlab.com/p-b-o/qemu:
  docs/about/emulation: document the dlcall plugin
  contrib/plugins: add a minimal dlcall plugin
  uftrace: riscv privilege level

Signed-off-by: Stefan Hajnoczi <stefanha@redhat.com>
This commit is contained in:
Stefan Hajnoczi
2026-07-14 07:25:53 +01:00
4 changed files with 421 additions and 3 deletions

248
contrib/plugins/dlcall.c Normal file
View File

@@ -0,0 +1,248 @@
/*
* Copyright (C) 2026, Ziyang Zhang <functioner@sjtu.edu.cn>
*
* dlcall (Dynamic Linking Call) 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.
*
* The plugin intentionally keeps the QEMU side lightweight and prescribes
* nothing about how a library is thunked. Any toolchain can implement the
* userspace side. Lorelei is one end-to-end implementation (guest/host
* runtimes plus a thunk compiler that generates thunks from a library's
* headers):
* https://github.com/rover2024/lorelei
*
* See docs/about/emulation.rst|Dynamic Linking Call for details and examples.
*
* 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;
}

View File

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

View File

@@ -109,6 +109,8 @@ typedef enum {
RISCV64_SUPERVISOR,
RISCV64_RESERVED,
RISCV64_MACHINE,
RISCV64_VUSER,
RISCV64_VSUPERVISOR,
RISCV64_PRIVILEGE_LEVEL_MAX,
} Riscv64PrivilegeLevel;
@@ -153,8 +155,10 @@ static void uftrace_write_map(bool system_emulation)
const char *path = "./uftrace.data/sid-0.map";
if (system_emulation && access(path, F_OK) == 0) {
/* do not erase existing map in system emulation, as a custom one might
* already have been generated by uftrace_symbols.py */
/*
* do not erase existing map in system emulation, as a custom one might
* already have been generated by uftrace_symbols.py
*/
return;
}
@@ -706,6 +710,8 @@ static const char *riscv64_get_privilege_level_name(uint8_t pl)
case RISCV64_SUPERVISOR: return "Supervisor";
case RISCV64_RESERVED: return "Unknown";
case RISCV64_MACHINE: return "Machine";
case RISCV64_VUSER: return "VUser";
case RISCV64_VSUPERVISOR: return "VSupervisor";
default:
g_assert_not_reached();
}
@@ -986,7 +992,8 @@ QEMU_PLUGIN_EXPORT int qemu_plugin_install(qemu_plugin_id_t id,
score = qemu_plugin_scoreboard_new(sizeof(Cpu));
qemu_plugin_register_vcpu_init_cb(id, vcpu_init, NULL);
qemu_plugin_register_atexit_cb(id, at_exit, (void *) info->system_emulation);
qemu_plugin_register_atexit_cb(id, at_exit,
(void *) info->system_emulation);
qemu_plugin_register_vcpu_tb_trans_cb(id, vcpu_tb_trans, NULL);
return 0;

View File

@@ -1046,6 +1046,164 @@ Count traps
This plugin counts the number of interrupts (asynchronous events), exceptions
(synchronous events) and host calls (e.g. semihosting) per cpu.
Dynamic Linking Call
....................
``contrib/plugins/dlcall.c``
This plugin provides a dynamic linking function call interception mechanism
for linux-user guests: the guest hands a call off to the host, where the plugin
runs native code in its place instead of the guest emulating it. Interception
alone enables several uses, for instance tracing or auditing guest calls.
One use is acceleration by leveraging the host's native shared libraries. For
example, a thunk layer can run the stock zlib ``minizip`` utility under
emulation while forwarding its ``deflate`` calls to the host's native zlib
library (libz). This avoids emulating those selected library calls instruction
by instruction.
The guest issues a reserved "magic" system call (4096 by default, configurable
with ``syscall_num=N``) whose first argument selects a pass-through operation:
dlopen/dlclose a host library, dlsym a symbol, and invoke a resolved host
function. The plugin performs the operation on the host and consumes the
syscall, so the real kernel never sees it.
.. warning::
Trusted guests only. The guest can load arbitrary host libraries and run
arbitrary code in the QEMU host process. The plugin is not a sandbox and
provides no isolation. It also requires ``guest_base == 0`` (qemu-user's
default), as guest pointers are dereferenced as host addresses with no
translation.
The plugin intentionally keeps the QEMU side lightweight and knows nothing
about any particular library or its calling convention. Turning a real library
into working thunks, including argument marshalling, callbacks and variadic
functions, is done entirely in userspace, and any toolchain can implement the
interface.
Loading the plugin is all that is required from QEMU's side:
.. code-block:: shell
qemu-x86_64 -plugin contrib/plugins/libdlcall.so <guest-program> ...
`Lorelei <https://github.com/rover2024/lorelei>`_ is one end-to-end userspace
implementation of this: it provides the guest and host runtimes and an
automated toolchain that generates the thunks from a library's headers, so guest
library calls run on the host's native libraries. It supports an x86_64 guest
running on an x86_64, aarch64 or riscv64 host.
A minimal end-to-end example uses a one-function library, ``libhello.so``, built
two ways: the guest build tags its output ``(from the guest)`` and the host
build ``(from the host)``. An unmodified guest program ``main`` calls
``hello("World", 7)``, and the thunk makes that same binary reach the host build
in place of its own. The sources live under ``src/``:
.. code-block:: c
/* src/hello.h */
void hello(const char *name, int lucky);
.. code-block:: c
/* src/hello_guest.c */
#include "hello.h"
#include <stdio.h>
void hello(const char *name, int lucky)
{
printf("Hello, %s! Your lucky number is %d. (from the guest)\n", name, lucky);
}
.. code-block:: c
/* src/hello_host.c */
#include "hello.h"
#include <stdio.h>
void hello(const char *name, int lucky)
{
printf("Hello, %s! Your lucky number is %d. (from the host)\n", name, lucky);
}
.. code-block:: c
/* src/main.c */
#include "hello.h"
int main(void)
{
hello("World", 7);
return 0;
}
Lorelei ships a prebuilt toolchain (a "devkit") in its releases. Download the
one for your host and unpack it:
.. code-block:: shell
# ARCH is your host's architecture: x86_64, aarch64 or riscv64. This example uses aarch64.
# See https://github.com/rover2024/lorelei/releases
ARCH=aarch64
VERSION=$(curl -fsSL -o /dev/null -w '%{url_effective}' \
https://github.com/rover2024/lorelei/releases/latest | sed 's|.*/tag/v||')
wget "https://github.com/rover2024/lorelei/releases/download/v$VERSION/lorelei-devkit-$ARCH-$VERSION.tar.xz"
tar -xf lorelei-devkit-$ARCH-$VERSION.tar.xz
DEVKIT=lorelei-devkit-$ARCH
Build the guest ``libhello.so`` (x86_64) and the host ``libhello.so`` (this
host's architecture), then the guest program:
.. code-block:: shell
mkdir -p build/guest build/host
$DEVKIT/bin/x86_64-linux-gnu-clang -shared -fPIC src/hello_guest.c -o build/guest/libhello.so
cc -shared -fPIC src/hello_host.c -o build/host/libhello.so
$DEVKIT/bin/x86_64-linux-gnu-clang src/main.c -Isrc -Lbuild/guest -lhello -o build/guest/main
Run it under qemu:
.. code-block:: shell
qemu-x86_64 -L /usr/x86_64-linux-gnu/ -E LD_LIBRARY_PATH=build/guest build/guest/main
which prints::
Hello, World! Your lucky number is 7. (from the guest)
Now generate the thunk from the host ``libhello.so``. This produces a guest-side
``libhello.so`` that stands in for the guest build, and a host-side thunk library
that dispatches to the host build:
.. code-block:: shell
$DEVKIT/bin/LoreMakeThunk.py --name hello --lib build/host/libhello.so \
--header hello.h -o thunks -- -Isrc
Run the same ``main`` under the plugin. The call reaches the host build now:
.. code-block:: shell
LD_LIBRARY_PATH=$DEVKIT/lib:build/host \
qemu-x86_64 -plugin contrib/plugins/libdlcall.so \
-E LD_LIBRARY_PATH=$DEVKIT/x86_64/lib:thunks/x86_64 \
-L /usr/x86_64-linux-gnu/ \
build/guest/main
which prints::
Hello, World! Your lucky number is 7. (from the host)
.. list-table:: Dynamic Linking Call arguments
:widths: 20 80
:header-rows: 1
* - Option
- Description
* - syscall_num=N
- The magic syscall number the guest issues (default 4096). Must be high
enough not to clash with a real syscall.
Other emulation features
------------------------