1 /* 2 * Copyright (C) 2026 Jo-Philipp Wich <jo@mein.io> 3 * 4 * Permission to use, copy, modify, and/or distribute this software for any 5 * purpose with or without fee is hereby granted, provided that the above 6 * copyright notice and this permission notice appear in all copies. 7 * 8 * THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES 9 * WITH REGARD TO THIS SOFTWARE INCLUDING ANY IMPLIED WARRANTIES OF 10 * MERCHANTABILITY AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR 11 * ANY SPECIAL, DIRECT, INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES 12 * WHATSOEVER RESULTING FROM LOSS OF USE, DATA OR PROFITS, WHETHER IN AN 13 * ACTION OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT OF 14 * OR IN CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE. 15 */ 16 17 /* 18 * Remote debugger socket transport. 19 * 20 * This file only deals with the socket transport: creating Unix domain 21 * sockets (both the PID-derived SIGUSR1 attach socket and arbitrary 22 * caller-supplied paths for debug.listen()), accepting a `udbg` client 23 * connection, and pushing asynchronous "EVENT " notifications to an 24 * attached client. The script-facing debug.listen() API and the actual 25 * interactive command session - which reuses the exact same command set, 26 * tab completion and readline-style editing as the local terminal debugger 27 * - live in debug.c (see uc_debug_listen() / debug_cli_run_remote_session()), 28 * once a client fd has been accepted here. 29 */ 30 31 #include <stdio.h> 32 #include <stdlib.h> 33 #include <string.h> 34 #include <unistd.h> 35 #include <errno.h> 36 #include <fcntl.h> 37 #include <signal.h> 38 #include <sys/socket.h> 39 #include <sys/un.h> 40 #include <sys/types.h> 41 #include <sys/stat.h> 42 43 #include "ucode/lib.h" 44 #include "ucode/util.h" 45 #include "ucode/vm.h" 46 #include "debug_remote.h" 47 #include "debug_proto.h" 48 49 static int remote_debug_fd = -1; 50 51 52 void 53 debug_remote_set_active_fd(int fd) 54 { 55 remote_debug_fd = fd; 56 } 57 58 bool 59 debug_remote_has_active_connection(void) 60 { 61 return remote_debug_fd >= 0; 62 } 63 64 int 65 debug_remote_get_active_fd(void) 66 { 67 return remote_debug_fd; 68 } 69 70 71 /* Wait for a udbg client to connect to the SIGUSR1 attach socket, with a 72 * 30s timeout. Returns the accepted client fd on success, -1 on timeout 73 * (caller should resume execution unattended), or -2 on a fatal error. */ 74 int 75 debug_remote_handle_break(uc_vm_t *vm) 76 { 77 int listen_fd = debug_remote_create_attach_socket(); 78 fd_set readfds; 79 struct timeval tv; 80 int ret, client_fd; 81 82 if (listen_fd < 0) { 83 fprintf(stderr, "Failed to create attach socket: %s\n", strerror(errno)); 84 return -2; 85 } 86 87 fprintf(stderr, "Debugger socket ready, waiting for connection...\n"); 88 89 for (;;) { 90 FD_ZERO(&readfds); 91 FD_SET(listen_fd, &readfds); 92 tv.tv_sec = 30; 93 tv.tv_usec = 0; 94 95 ret = select(listen_fd + 1, &readfds, NULL, NULL, &tv); 96 97 if (ret < 0 && errno == EINTR) 98 continue; 99 100 break; 101 } 102 103 if (ret <= 0) { 104 close(listen_fd); 105 debug_remote_cleanup_attach_socket(); 106 107 if (ret == 0) 108 fprintf(stderr, "Timeout waiting for debugger connection - continuing execution\n"); 109 else 110 fprintf(stderr, "Error waiting for debugger connection: %s\n", strerror(errno)); 111 112 return -1; 113 } 114 115 client_fd = accept(listen_fd, NULL, NULL); 116 close(listen_fd); 117 118 if (client_fd < 0) { 119 debug_remote_cleanup_attach_socket(); 120 return -1; 121 } 122 123 return client_fd; 124 } 125 126 127 /* Create, bind (mode 0600) and listen on a Unix domain socket at the given 128 * path, removing any stale socket file first. Shared by both the 129 * SIGUSR1-triggered attach socket (fixed, PID-derived path) and 130 * debug.listen() (arbitrary caller-supplied path). Returns the listening 131 * fd, or -1 on error. */ 132 static int 133 debug_remote_bind_and_listen(const char *path) 134 { 135 struct sockaddr_un addr = { 0 }; 136 int listen_fd; 137 socklen_t addrlen; 138 mode_t old_umask; 139 140 listen_fd = socket(AF_UNIX, SOCK_STREAM, 0); 141 if (listen_fd < 0) 142 return -1; 143 144 addr.sun_family = AF_UNIX; 145 strncpy(addr.sun_path, path, sizeof(addr.sun_path) - 1); 146 addr.sun_path[sizeof(addr.sun_path) - 1] = '\0'; 147 addrlen = sizeof(sa_family_t) + strlen(path) + 1; 148 149 unlink(path); 150 151 old_umask = umask(077); 152 153 if (bind(listen_fd, (struct sockaddr *)&addr, addrlen) < 0) { 154 umask(old_umask); 155 close(listen_fd); 156 return -1; 157 } 158 159 umask(old_umask); 160 161 if (listen(listen_fd, 1) < 0) { 162 close(listen_fd); 163 return -1; 164 } 165 166 return listen_fd; 167 } 168 169 170 /* Global socket path for SIGUSR1-triggered attach */ 171 static char attach_socket_path[1024] = { 0 }; 172 173 int 174 debug_remote_create_attach_socket(void) 175 { 176 pid_t pid = getpid(); 177 178 snprintf(attach_socket_path, sizeof(attach_socket_path), 179 "/tmp/ucode-debug-%d.sock", pid); 180 181 return debug_remote_bind_and_listen(attach_socket_path); 182 } 183 184 const char * 185 debug_remote_get_socket_path(void) 186 { 187 return attach_socket_path[0] ? attach_socket_path : NULL; 188 } 189 190 void 191 debug_remote_cleanup_attach_socket(void) 192 { 193 if (attach_socket_path[0] != '\0') { 194 unlink(attach_socket_path); 195 attach_socket_path[0] = '\0'; 196 } 197 } 198 199 /* Bind, listen on and accept a single connection on an arbitrary, 200 * caller-supplied Unix domain socket path, blocking indefinitely. Returns 201 * the accepted client fd, or -1 on error. Used by debug.listen(path) (see 202 * debug.c) for the explicit-path case, as opposed to the PID-derived attach 203 * socket used for the SIGUSR1/-X flow above. */ 204 int 205 debug_remote_accept_on_path(const char *path) 206 { 207 int listen_fd, client_fd; 208 209 listen_fd = debug_remote_bind_and_listen(path); 210 if (listen_fd < 0) 211 return -1; 212 213 client_fd = accept(listen_fd, NULL, NULL); 214 close(listen_fd); 215 216 /* The socket file is no longer needed once accepted (or on error) - 217 * the connection itself doesn't depend on the path persisting. */ 218 unlink(path); 219 220 return client_fd; 221 } 222 223 224 /* Shallow-copy a plain object's own keys into a fresh object with no 225 * prototype - values are shared (ucv_get()'d, not deep-cloned). 226 * 227 * Used to defuse uc_vm_exception_object()'s tostring() prototype method 228 * (attached for script-facing try/catch ergonomics, so `catch (e) { 229 * print(e) }` prints the message) before JSON-serializing it: ucv_to_json 230 * string() invokes tostring() if present instead of serializing the 231 * object's own fields, which would collapse the whole thing down to just 232 * the message string. Worse, invoking it runs through the VM's own call 233 * machinery, which calls uc_vm_clear_exception() as a side effect - wiping 234 * out vm->exception (including freeing ->message) out from under whatever 235 * runs next. Copying rather than mutating the prototype in place on the 236 * original object avoids surprising a caller who still holds a reference 237 * to it for other purposes. */ 238 static uc_value_t * 239 object_shallow_copy_no_proto(uc_vm_t *vm, uc_value_t *obj) 240 { 241 uc_value_t *copy = ucv_object_new(vm); 242 243 ucv_object_foreach(obj, k, v) 244 ucv_object_add(copy, k, ucv_get(v)); 245 246 return copy; 247 } 248 249 /* Push an unsolicited exception notification to the connected debugger 250 * client, if any, as the same JSON exception object shape script code sees 251 * via try/catch ({type, message, stacktrace} - see uc_vm_exception_object() 252 * in vm.c) - safe to call unconditionally from the VM's exception handler 253 * chain; a no-op when nobody is attached. `ex` is expected to still be 254 * `&vm->exception` at this point (true for the exception handler chain, 255 * which runs synchronously before anything gets cleared), since the actual 256 * object is built from vm->exception directly. */ 257 void 258 debug_remote_notify_exception(uc_vm_t *vm, uc_exception_t *ex) 259 { 260 uc_value_t *exo, *plain, *evo; 261 262 (void)ex; 263 264 if (remote_debug_fd < 0) 265 return; 266 267 exo = uc_vm_exception_object(vm); 268 plain = object_shallow_copy_no_proto(vm, exo); 269 ucv_put(exo); 270 271 evo = ucv_object_new(vm); 272 ucv_object_add(evo, "event", ucv_string_new("exception")); 273 ucv_object_add(evo, "exception", plain); 274 275 debug_proto_write(remote_debug_fd, vm, "EVENT", evo); 276 ucv_put(evo); 277 } 278 279 static const char * 280 vm_status_name(uc_vm_status_t status) 281 { 282 switch (status) { 283 case STATUS_OK: return "OK"; 284 case STATUS_EXIT: return "EXIT"; 285 case STATUS_BREAK: return "BREAK"; 286 case ERROR_COMPILE: return "ERROR_COMPILE"; 287 case ERROR_RUNTIME: return "ERROR_RUNTIME"; 288 default: return "UNKNOWN"; 289 } 290 } 291 292 /* Push a final "the target is going away" notification to the connected 293 * debugger client, if any, as a JSON object describing the full final VM 294 * state - {status}, plus {code} for STATUS_EXIT or the same {type, message, 295 * stacktrace} exception object shape used above for ERROR_COMPILE/ 296 * ERROR_RUNTIME. Called from main.c right after uc_vm_execute() returns, 297 * before the process actually exits and the connection drops - without 298 * this, a client only finds out the target is gone once the socket EOFs, 299 * with no indication of why. 300 * 301 * Takes the raw uc_vm_status_t rather than main.c's own CLI exit-code 302 * translation (which flattens both ERROR_COMPILE and ERROR_RUNTIME to the 303 * same -2 and loses the actual exception), plus exit_code and a 304 * pre-built exception object (or NULL). Both must be supplied by the 305 * caller rather than read off the vm here: by the time this runs 306 * (dispatched through a ucode-level call), uc_vm_call() has already 307 * cleared vm->exception as its own first action, so main.c has to 308 * snapshot vm->arg.s32 / call uc_vm_exception_object() *before* making 309 * this call. */ 310 void 311 debug_remote_notify_exit(uc_vm_t *vm, uc_vm_status_t status, int32_t exit_code, 312 uc_value_t *exception_obj) 313 { 314 uc_value_t *evo; 315 316 if (remote_debug_fd < 0) 317 return; 318 319 evo = ucv_object_new(vm); 320 321 ucv_object_add(evo, "event", ucv_string_new("exit")); 322 ucv_object_add(evo, "status", ucv_string_new(vm_status_name(status))); 323 324 if (status == STATUS_EXIT) 325 ucv_object_add(evo, "code", ucv_int64_new(exit_code)); 326 else if (exception_obj) { 327 /* Copy without the tostring() prototype uc_vm_exception_object() 328 * attaches (for script-facing try/catch ergonomics) before nesting 329 * it - see the comment on object_shallow_copy_no_proto() above: 330 * otherwise ucv_to_jsonstring() below would invoke it and collapse 331 * this down to just the message string instead of serializing 332 * {type, message, stacktrace}. */ 333 uc_value_t *plain = object_shallow_copy_no_proto(vm, exception_obj); 334 335 ucv_object_add(evo, "exception", plain); 336 } 337 338 debug_proto_write(remote_debug_fd, vm, "EVENT", evo); 339 ucv_put(evo); 340 } 341
This page was automatically generated by LXR 0.3.1. • OpenWrt