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
+}