@samitouri / QOSamiQemu / commits / 09b7a409a7

contrib/plugins: add a minimal dlcall plugin

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> Reviewed-by: Pierrick Bouvier <pierrick.bouvier@oss.qualcomm.com> Link: https://lore.kernel.org/qemu-devel/20260711094523.622997-2-functioner@sjtu.edu.cn Signed-off-by: Pierrick Bouvier <pierrick.bouvier@oss.qualcomm.com>

Ziyang Zhang committed Jul 11, 2026 at 17:45 UTC 09b7a409a7c9bbf1e7f22cccb96422f1827b0105
2 files changed +253
contrib/plugins/dlcall.c new
+248
@@ -0,0 +1,248 @@
1 +/*
2 + * Copyright (C) 2026, Ziyang Zhang <functioner@sjtu.edu.cn>
3 + *
4 + * dlcall (Dynamic Linking Call) plugin: lets a linux-user guest invoke host
5 + * functions by issuing a magic system call. The guest can ask QEMU to dlopen()
6 + * a host shared library, dlsym() a symbol, and call it with guest-supplied
7 + * arguments.
8 + *
9 + * The plugin intentionally keeps the QEMU side lightweight and prescribes
10 + * nothing about how a library is thunked. Any toolchain can implement the
11 + * userspace side. Lorelei is one end-to-end implementation (guest/host
12 + * runtimes plus a thunk compiler that generates thunks from a library's
13 + * headers):
14 + * https://github.com/rover2024/lorelei
15 + *
16 + * See docs/about/emulation.rst|Dynamic Linking Call for details and examples.
17 + *
18 + * WARNING: trusted guests only. The guest can load arbitrary host libraries
19 + * and execute arbitrary host code with arbitrary arguments, i.e. full code
20 + * execution in the QEMU host process. It is NOT a sandbox and provides no
21 + * isolation; only load it for guests you fully trust.
22 + *
23 + * WARNING: requires guest_base == 0, which is qemu-user's default. Pointer
24 + * operands are dereferenced as host addresses directly, and the invoked host
25 + * functions dereference guest pointers with no address translation, so guest
26 + * and host must share a single address space. A non-zero guest_base (e.g. set
27 + * via -B/-R) would make every pointer off by guest_base and hit unrelated
28 + * host memory.
29 + *
30 + * SPDX-License-Identifier: GPL-2.0-or-later
31 + */
32 +
33 +#include <assert.h>
34 +#include <errno.h>
35 +#include <string.h>
36 +#include <stdio.h>
37 +#include <glib.h>
38 +#include <dlfcn.h>
39 +
40 +#include <qemu-plugin.h>
41 +
42 +QEMU_PLUGIN_EXPORT int qemu_plugin_version = QEMU_PLUGIN_VERSION;
43 +
44 +/*
45 + * The magic system call number for dlcall.
46 + *
47 + * It defaults to DLCALL_SYSCALL_DEFAULT and can be overridden at load time
48 + * with the "syscall_num=N" argument. To avoid hijacking a real syscall the
49 + * guest might issue, N must be at least DLCALL_SYSCALL_MIN: every Linux ABI
50 + * keeps its syscall numbers well below this; numbers from here up are free.
51 + */
52 +enum {
53 + DLCALL_SYSCALL_DEFAULT = 4096,
54 + DLCALL_SYSCALL_MIN = 4096,
55 +};
56 +
57 +static int64_t dlcall_syscall_num = DLCALL_SYSCALL_DEFAULT;
58 +
59 +/*
60 + * dlcall calling convention.
61 + *
62 + * The guest issues the magic system call (dlcall_syscall_num). The first
63 + * argument (a1) is one of the call IDs below; the remaining arguments (a2, a3,
64 + * a4, ...) are that ID's operands. All pointer operands are guest virtual
65 + * addresses that the plugin dereferences as host addresses directly (see the
66 + * guest_base requirement above). Results are written back through
67 + * caller-provided "out" pointers rather than returned in the syscall value.
68 + *
69 + * The syscall return value (*sysret) only reports dispatch status: 0 on a
70 + * recognised ID, -EINVAL for an unknown one. The actual success/failure of an
71 + * operation (e.g. a NULL handle from dlopen) is delivered through its out
72 + * pointer, exactly like the underlying libdl call.
73 + *
74 + * Operands per ID:
75 + *
76 + * DLCALL_ID_GET_HOST_ATTRIBUTE
77 + * a2 const char *key in: attribute name to query
78 + * a3 const char **attr_ptr out: matching value, or NULL if unknown
79 + *
80 + * DLCALL_ID_LOAD_LIBRARY (wraps dlopen)
81 + * a2 const char *path in: library path
82 + * a3 int flags in: dlopen() flags (e.g. RTLD_NOW)
83 + * a4 void **handle_ptr out: library handle, or NULL on failure
84 + *
85 + * DLCALL_ID_GET_PROC_ADDRESS (wraps dlsym)
86 + * a2 void *handle in: library handle
87 + * a3 const char *name in: symbol name
88 + * a4 void **entry_ptr out: symbol address, or NULL if not found
89 + *
90 + * DLCALL_ID_FREE_LIBRARY (wraps dlclose)
91 + * a2 void *handle in: library handle
92 + * a3 int *ret_ptr out: dlclose() return value (0 on success)
93 + *
94 + * DLCALL_ID_GET_LIBRARY_ERROR (wraps dlerror)
95 + * a2 const char **error_ptr out: last libdl error string, or NULL
96 + *
97 + * DLCALL_ID_INVOKE_PROC (calls the symbol)
98 + * a2 void *proc in: function pointer, signature
99 + * void (*)(void *arg1, void *arg2)
100 + * a3 void *arg1 in: first argument forwarded to proc
101 + * a4 void *arg2 in: second argument forwarded to proc
102 + */
103 +enum DlcallID {
104 + DLCALL_ID_GET_HOST_ATTRIBUTE,
105 + DLCALL_ID_LOAD_LIBRARY,
106 + DLCALL_ID_GET_PROC_ADDRESS,
107 + DLCALL_ID_FREE_LIBRARY,
108 + DLCALL_ID_GET_LIBRARY_ERROR,
109 + DLCALL_ID_INVOKE_PROC,
110 +};
111 +
112 +static inline const char *query_host_attribute(const char *key)
113 +{
114 + if (strcmp(key, "emu") == 0) {
115 + return "qemu";
116 + }
117 + return NULL;
118 +}
119 +
120 +static inline void invoke_proc(void *proc, void *arg1, void *arg2)
121 +{
122 + typedef void (*Func)(void * /*arg1*/, void * /*arg2*/);
123 + Func func = (Func) proc;
124 + func(arg1, arg2);
125 +}
126 +
127 +static bool vcpu_syscall_filter(unsigned int vcpu_index,
128 + int64_t num, uint64_t a1, uint64_t a2,
129 + uint64_t a3, uint64_t a4, uint64_t a5,
130 + uint64_t a6, uint64_t a7, uint64_t a8,
131 + int64_t *sysret, void *userdata)
132 +{
133 + if (num == dlcall_syscall_num) {
134 + switch (a1) {
135 + /* Query host attribute by a reserved key. */
136 + case DLCALL_ID_GET_HOST_ATTRIBUTE: {
137 + const char *key = (const char *) a2;
138 + const char **attr_ptr = (const char **) a3;
139 + assert(attr_ptr);
140 + *attr_ptr = query_host_attribute(key);
141 + *sysret = 0;
142 + break;
143 + }
144 +
145 + /* Load a shared library. */
146 + case DLCALL_ID_LOAD_LIBRARY: {
147 + const char *path = (const char *) a2;
148 + int flags = (int) a3;
149 + void **handle_ptr = (void **) a4;
150 + assert(handle_ptr);
151 + *handle_ptr = dlopen(path, flags);
152 + *sysret = 0;
153 + break;
154 + }
155 +
156 + /* Get the address of a function in a shared library. */
157 + case DLCALL_ID_GET_PROC_ADDRESS: {
158 + void *handle = (void *) a2;
159 + const char *name = (const char *) a3;
160 + void **entry_ptr = (void **) a4;
161 + assert(entry_ptr);
162 + *entry_ptr = dlsym(handle, name);
163 + *sysret = 0;
164 + break;
165 + }
166 +
167 + /* Free a shared library. */
168 + case DLCALL_ID_FREE_LIBRARY: {
169 + void *handle = (void *) a2;
170 + int *ret_ptr = (int *) a3;
171 + *ret_ptr = dlclose(handle);
172 + *sysret = 0;
173 + break;
174 + }
175 +
176 + /* Get the last error message for a library event. */
177 + case DLCALL_ID_GET_LIBRARY_ERROR: {
178 + const char **error_ptr = (const char **) a2;
179 + *error_ptr = dlerror();
180 + *sysret = 0;
181 + break;
182 + }
183 +
184 + /* Invoke a function of a common interface. */
185 + case DLCALL_ID_INVOKE_PROC: {
186 + void *proc = (void *) a2;
187 + void *arg1 = (void *) a3;
188 + void *arg2 = (void *) a4;
189 + assert(proc);
190 + invoke_proc(proc, arg1, arg2);
191 + *sysret = 0;
192 + break;
193 + }
194 +
195 + default:
196 + *sysret = -EINVAL;
197 + break;
198 + }
199 + return true;
200 + }
201 + return false;
202 +}
203 +
204 +QEMU_PLUGIN_EXPORT int qemu_plugin_install(qemu_plugin_id_t id,
205 + const qemu_info_t *info,
206 + int argc, char **argv)
207 +{
208 + if (info->system_emulation) {
209 + fprintf(stderr, "plugin dlcall: only useful for user emulation\n");
210 + return -1;
211 + }
212 +
213 + for (int i = 0; i < argc; i++) {
214 + char *opt = argv[i];
215 + g_auto(GStrv) tokens = g_strsplit(opt, "=", 2);
216 + if (g_strcmp0(tokens[0], "syscall_num") == 0) {
217 + const char *val = tokens[1];
218 + char *endptr = NULL;
219 + guint64 num;
220 + if (!val || *val == '\0') {
221 + fprintf(stderr,
222 + "plugin dlcall: missing value for syscall_num\n");
223 + return -1;
224 + }
225 + num = g_ascii_strtoull(val, &endptr, 0);
226 + if (*endptr != '\0' || g_strrstr(val, "-") != NULL) {
227 + fprintf(stderr,
228 + "plugin dlcall: invalid syscall_num '%s'\n", val);
229 + return -1;
230 + }
231 + if (num < DLCALL_SYSCALL_MIN || num > G_MAXINT64) {
232 + fprintf(stderr,
233 + "plugin dlcall: syscall_num %s is out of range; "
234 + "it must be >= %d to avoid clashing with a real "
235 + "syscall\n", val, DLCALL_SYSCALL_MIN);
236 + return -1;
237 + }
238 + dlcall_syscall_num = (int64_t) num;
239 + } else {
240 + fprintf(stderr, "plugin dlcall: unknown option '%s'\n", opt);
241 + return -1;
242 + }
243 + }
244 +
245 + qemu_plugin_register_vcpu_syscall_filter_cb(id, vcpu_syscall_filter, NULL);
246 +
247 + return 0;
248 +}
contrib/plugins/meson.build
+5
@@ -19,6 +19,11 @@ if host_os != 'windows'
19 contrib_plugins += 'lockstep.c'
20 endif
21
22 +if host_os == 'linux'
23 + # dlcall passes guest calls through to host libraries; linux-user only
24 + contrib_plugins += 'dlcall.c'
25 +endif
26 +
27 if 'cpp' in all_languages
28 contrib_plugins += 'cpp.cpp'
29 endif