master
c 262 lines 9.67 KB
Raw
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), and how it handles argument marshalling, callbacks and variadic
14 * functions can serve as a reference:
15 * https://github.com/rover2024/lorelei
16 *
17 * See docs/about/emulation.rst|Dynamic Linking Call for details and examples.
18 *
19 * WARNING: trusted guests only. The guest can load arbitrary host libraries
20 * and execute arbitrary host code with arbitrary arguments, i.e. full code
21 * execution in the QEMU host process. It is NOT a sandbox and provides no
22 * isolation; only load it for guests you fully trust.
23 *
24 * WARNING: requires guest_base == 0, which is qemu-user's default, and a
25 * guest whose pointer width and endianness match the host's. Pointer operands
26 * are dereferenced as host addresses directly, and the invoked host functions
27 * dereference guest pointers with no address translation, so guest and host
28 * must share a single address space and agree on how a pointer is stored. A
29 * non-zero guest_base (e.g. set via -B/-R) would make every pointer off by
30 * guest_base and hit unrelated host memory.
31 *
32 * SPDX-License-Identifier: GPL-2.0-or-later
33 */
34
35 #include <assert.h>
36 #include <errno.h>
37 #include <string.h>
38 #include <stdio.h>
39 #include <glib.h>
40 #include <dlfcn.h>
41
42 #include <qemu-plugin.h>
43
44 QEMU_PLUGIN_EXPORT int qemu_plugin_version = QEMU_PLUGIN_VERSION;
45
46 /*
47 * The magic system call number for dlcall.
48 *
49 * It defaults to DLCALL_SYSCALL_DEFAULT and can be overridden at load time
50 * with the "syscall_num=N" argument. To avoid hijacking a real syscall the
51 * guest might issue, N must be at least DLCALL_SYSCALL_MIN, which most Linux
52 * ABIs keep their syscall numbers well below.
53 *
54 * N also has to reach the filter at all, which bounds it from above in a
55 * target specific way: arm32 answers anything past ARM_NR_BASE (0xf0000) with
56 * ENOSYS or SIGILL before do_syscall() runs, while aarch64 has no such bound.
57 *
58 * MIPS O32 bases its numbering at 4000, so the default is a real syscall there
59 * (getpriority). Raising N does not help either, because O32 rejects numbers
60 * its table does not define, again before the filter runs, which leaves no
61 * number that is both free and reachable on that ABI. Its N32 and N64 ABIs
62 * base at 6000 and 5000 and have no such gate, so they are unaffected.
63 */
64 enum {
65 DLCALL_SYSCALL_DEFAULT = 4096,
66 DLCALL_SYSCALL_MIN = 4096,
67 };
68
69 static int64_t dlcall_syscall_num = DLCALL_SYSCALL_DEFAULT;
70
71 /*
72 * dlcall calling convention.
73 *
74 * The guest issues the magic system call (dlcall_syscall_num). The first
75 * argument (a1) is one of the call IDs below; the remaining arguments (a2, a3,
76 * a4, ...) are that ID's operands. All pointer operands are guest virtual
77 * addresses that the plugin dereferences as host addresses directly (see the
78 * guest_base requirement above). Results are written back through
79 * caller-provided "out" pointers rather than returned in the syscall value.
80 *
81 * The syscall return value (*sysret) only reports dispatch status: 0 on a
82 * recognised ID, -EINVAL for an unknown one. The actual success/failure of an
83 * operation (e.g. a NULL handle from dlopen) is delivered through its out
84 * pointer, exactly like the underlying libdl call.
85 *
86 * Operands per ID:
87 *
88 * DLCALL_ID_GET_HOST_ATTRIBUTE
89 * a2 const char *key in: attribute name to query
90 * a3 const char **attr_ptr out: matching value, or NULL if unknown
91 *
92 * DLCALL_ID_LOAD_LIBRARY (wraps dlopen)
93 * a2 const char *path in: library path
94 * a3 int flags in: dlopen() flags (e.g. RTLD_NOW)
95 * a4 void **handle_ptr out: library handle, or NULL on failure
96 *
97 * DLCALL_ID_GET_PROC_ADDRESS (wraps dlsym)
98 * a2 void *handle in: library handle
99 * a3 const char *name in: symbol name
100 * a4 void **entry_ptr out: symbol address, or NULL if not found
101 *
102 * DLCALL_ID_FREE_LIBRARY (wraps dlclose)
103 * a2 void *handle in: library handle
104 * a3 int *ret_ptr out: dlclose() return value (0 on success)
105 *
106 * DLCALL_ID_GET_LIBRARY_ERROR (wraps dlerror)
107 * a2 const char **error_ptr out: last libdl error string, or NULL
108 *
109 * DLCALL_ID_INVOKE_PROC (calls the symbol)
110 * a2 void *proc in: function pointer, signature
111 * void (*)(void *arg1, void *arg2)
112 * a3 void *arg1 in: first argument forwarded to proc
113 * a4 void *arg2 in: second argument forwarded to proc
114 */
115 enum DlcallID {
116 DLCALL_ID_GET_HOST_ATTRIBUTE,
117 DLCALL_ID_LOAD_LIBRARY,
118 DLCALL_ID_GET_PROC_ADDRESS,
119 DLCALL_ID_FREE_LIBRARY,
120 DLCALL_ID_GET_LIBRARY_ERROR,
121 DLCALL_ID_INVOKE_PROC,
122 };
123
124 static inline const char *query_host_attribute(const char *key)
125 {
126 if (strcmp(key, "emu") == 0) {
127 return "qemu";
128 }
129 return NULL;
130 }
131
132 static inline void invoke_proc(void *proc, void *arg1, void *arg2)
133 {
134 typedef void (*Func)(void * /*arg1*/, void * /*arg2*/);
135 Func func = (Func) proc;
136 func(arg1, arg2);
137 }
138
139 static bool vcpu_syscall_filter(unsigned int vcpu_index,
140 int64_t num, uint64_t a1, uint64_t a2,
141 uint64_t a3, uint64_t a4, uint64_t a5,
142 uint64_t a6, uint64_t a7, uint64_t a8,
143 int64_t *sysret, void *userdata)
144 {
145 if (num == dlcall_syscall_num) {
146 switch (a1) {
147 /* Query host attribute by a reserved key. */
148 case DLCALL_ID_GET_HOST_ATTRIBUTE: {
149 const char *key = (const char *) a2;
150 const char **attr_ptr = (const char **) a3;
151 assert(attr_ptr);
152 *attr_ptr = query_host_attribute(key);
153 *sysret = 0;
154 break;
155 }
156
157 /* Load a shared library. */
158 case DLCALL_ID_LOAD_LIBRARY: {
159 const char *path = (const char *) a2;
160 int flags = (int) a3;
161 void **handle_ptr = (void **) a4;
162 assert(handle_ptr);
163 *handle_ptr = dlopen(path, flags);
164 *sysret = 0;
165 break;
166 }
167
168 /* Get the address of a function in a shared library. */
169 case DLCALL_ID_GET_PROC_ADDRESS: {
170 void *handle = (void *) a2;
171 const char *name = (const char *) a3;
172 void **entry_ptr = (void **) a4;
173 assert(entry_ptr);
174 *entry_ptr = dlsym(handle, name);
175 *sysret = 0;
176 break;
177 }
178
179 /* Free a shared library. */
180 case DLCALL_ID_FREE_LIBRARY: {
181 void *handle = (void *) a2;
182 int *ret_ptr = (int *) a3;
183 assert(ret_ptr);
184 *ret_ptr = dlclose(handle);
185 *sysret = 0;
186 break;
187 }
188
189 /* Get the last error message for a library event. */
190 case DLCALL_ID_GET_LIBRARY_ERROR: {
191 const char **error_ptr = (const char **) a2;
192 assert(error_ptr);
193 *error_ptr = dlerror();
194 *sysret = 0;
195 break;
196 }
197
198 /* Invoke a function of a common interface. */
199 case DLCALL_ID_INVOKE_PROC: {
200 void *proc = (void *) a2;
201 void *arg1 = (void *) a3;
202 void *arg2 = (void *) a4;
203 assert(proc);
204 invoke_proc(proc, arg1, arg2);
205 *sysret = 0;
206 break;
207 }
208
209 default:
210 *sysret = -EINVAL;
211 break;
212 }
213 return true;
214 }
215 return false;
216 }
217
218 QEMU_PLUGIN_EXPORT int qemu_plugin_install(qemu_plugin_id_t id,
219 const qemu_info_t *info,
220 int argc, char **argv)
221 {
222 if (info->system_emulation) {
223 fprintf(stderr, "plugin dlcall: only useful for user emulation\n");
224 return -1;
225 }
226
227 for (int i = 0; i < argc; i++) {
228 char *opt = argv[i];
229 g_auto(GStrv) tokens = g_strsplit(opt, "=", 2);
230 if (g_strcmp0(tokens[0], "syscall_num") == 0) {
231 const char *val = tokens[1];
232 char *endptr = NULL;
233 guint64 num;
234 if (!val || *val == '\0') {
235 fprintf(stderr,
236 "plugin dlcall: missing value for syscall_num\n");
237 return -1;
238 }
239 num = g_ascii_strtoull(val, &endptr, 0);
240 if (*endptr != '\0' || g_strrstr(val, "-") != NULL) {
241 fprintf(stderr,
242 "plugin dlcall: invalid syscall_num '%s'\n", val);
243 return -1;
244 }
245 if (num < DLCALL_SYSCALL_MIN || num > G_MAXINT64) {
246 fprintf(stderr,
247 "plugin dlcall: syscall_num %s is out of range; "
248 "it must be >= %d to avoid clashing with a real "
249 "syscall\n", val, DLCALL_SYSCALL_MIN);
250 return -1;
251 }
252 dlcall_syscall_num = (int64_t) num;
253 } else {
254 fprintf(stderr, "plugin dlcall: unknown option '%s'\n", opt);
255 return -1;
256 }
257 }
258
259 qemu_plugin_register_vcpu_syscall_filter_cb(id, vcpu_syscall_filter, NULL);
260
261 return 0;
262 }