• source navigation  • diff markup  • identifier search  • freetext search  • 

Sources/ucode/lib/debug_remote.c

  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