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