diff --git a/contrib/plugins/dlcall.c b/contrib/plugins/dlcall.c new file mode 100644 index 0000000000..9d2230b2d1 --- /dev/null +++ b/contrib/plugins/dlcall.c @@ -0,0 +1,248 @@ +/* + * Copyright (C) 2026, Ziyang Zhang + * + * 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 +#include +#include +#include +#include +#include + +#include + +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 diff --git a/contrib/plugins/uftrace.c b/contrib/plugins/uftrace.c index 9b0a4963ae..063e32220b 100644 --- a/contrib/plugins/uftrace.c +++ b/contrib/plugins/uftrace.c @@ -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; diff --git a/docs/about/emulation.rst b/docs/about/emulation.rst index 3b4c365933..b861501e85 100644 --- a/docs/about/emulation.rst +++ b/docs/about/emulation.rst @@ -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 ... + +`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 + + 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 + + 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 ------------------------