From 29b1c0deba8487d1d7ad86242a1a5e90aa775218 Mon Sep 17 00:00:00 2001 From: Jo-Philipp Wich Date: Tue, 11 Jul 2023 13:30:51 +0200 Subject: vm: introduce basic signal handling infrastructure Introduce the basic facilities to implement UNIX process signal handling in ucode. The VM structure is extended by a bitmap keeping track of received signal numbers, a sigaction structure which holds a generic signal handler to update the bitmap, a pipe where the generic signal handler will send received signal numbers to and an array of managed signal handler functions, indexed by signal number. Additionally, three new C API functions are added to control signal delivery in the VM instance: - `uc_vm_signal_dispatch()` This function invokes signal handler callbacks for each received signal number, clears the bitmap and empties the pipe. The VM itself will invoke this function after each executed bytecode instruction. In some cases however it is useful for C code driving the VM to force immediate signal delivery, e.g. from within long running C functions that do not return to ucode (to the next bytecode instruction) in a timely manner. - `uc_vm_signal_raise()` This function will deliver the given signal number, so that it is picked up by the next call to `uc_vm_signal_dispatch()`. It is used by the generic C signal handler function to forward received signals to the VM instance. - `uc_vm_signal_notifyfd()` This functions returns the read end of the internal signal pipe. It is mainly useful for integration into select or poll based event loops, in order to be notified when there's pending signals for delivery into the VM instance. Signed-off-by: Jo-Philipp Wich --- include/ucode/types.h | 8 +++ include/ucode/vm.h | 4 ++ vm.c | 138 ++++++++++++++++++++++++++++++++++++++++++++++++++ 3 files changed, 150 insertions(+) diff --git a/include/ucode/types.h b/include/ucode/types.h index 9827db5..641c469 100644 --- a/include/ucode/types.h +++ b/include/ucode/types.h @@ -20,6 +20,7 @@ #include #include #include +#include #include #include "util.h" @@ -230,6 +231,7 @@ typedef struct { bool raw_mode; uc_search_path_t module_search_path; uc_search_path_t force_dynlink_list; + bool setup_signal_handlers; } uc_parse_config_t; extern uc_parse_config_t uc_default_parse_config; @@ -311,6 +313,12 @@ struct uc_vm { uc_stringbuf_t *strbuf; uc_exception_handler_t *exhandler; FILE *output; + struct { + uint64_t raised[((NSIG + 63) & ~63) / 64]; + uc_value_t *handler; + struct sigaction sa; + int sigpipe[2]; + } signal; }; diff --git a/include/ucode/vm.h b/include/ucode/vm.h index 161f1ee..3cb6dc0 100644 --- a/include/ucode/vm.h +++ b/include/ucode/vm.h @@ -156,4 +156,8 @@ uc_vm_raise_exception(uc_vm_t *vm, uc_exception_type_t type, const char *fmt, .. uc_vm_status_t uc_vm_execute(uc_vm_t *vm, uc_program_t *fn, uc_value_t **retval); uc_value_t *uc_vm_invoke(uc_vm_t *vm, const char *fname, size_t nargs, ...); +uc_exception_type_t uc_vm_signal_dispatch(uc_vm_t *vm); +void uc_vm_signal_raise(uc_vm_t *vm, int signo); +int uc_vm_signal_notifyfd(uc_vm_t *vm); + #endif /* UCODE_VM_H */ diff --git a/vm.c b/vm.c index 16bf6c5..d3c26bf 100644 --- a/vm.c +++ b/vm.c @@ -21,6 +21,8 @@ #include #include #include +#include +#include #include "ucode/vm.h" #include "ucode/compiler.h" @@ -142,6 +144,69 @@ uc_vm_alloc_global_scope(uc_vm_t *vm) static void uc_vm_output_exception(uc_vm_t *vm, uc_exception_t *ex); +static uc_vm_t *signal_handler_vm; + +static void +uc_vm_signal_handler(int sig) +{ + assert(signal_handler_vm); + + uc_vm_signal_raise(signal_handler_vm, sig); +} + +#ifdef __APPLE__ +static int pipe2(int pipefd[2], int flags) +{ + if (pipe(pipefd) != 0) + return -1; + + if (flags & O_CLOEXEC) { + if (fcntl(pipefd[0], F_SETFD, FD_CLOEXEC) != 0 || + fcntl(pipefd[1], F_SETFD, FD_CLOEXEC) != 0) { + close(pipefd[0]); + close(pipefd[1]); + + return -1; + } + + flags &= ~O_CLOEXEC; + } + + if (fcntl(pipefd[0], F_SETFL, flags) != 0 || + fcntl(pipefd[1], F_SETFL, flags) != 0) { + close(pipefd[0]); + close(pipefd[1]); + + return -1; + } + + return 0; +} +#endif + +static void +uc_vm_signal_handlers_setup(uc_vm_t *vm) +{ + memset(&vm->signal, 0, sizeof(vm->signal)); + + vm->signal.sigpipe[0] = -1; + vm->signal.sigpipe[1] = -1; + + if (!vm->config->setup_signal_handlers) + return; + + if (pipe2(vm->signal.sigpipe, O_CLOEXEC | O_NONBLOCK) != 0) + return; + + signal_handler_vm = vm; + + vm->signal.handler = ucv_array_new_length(vm, NSIG - 1); + + vm->signal.sa.sa_handler = uc_vm_signal_handler; + vm->signal.sa.sa_flags = SA_RESTART | SA_ONSTACK; + sigemptyset(&vm->signal.sa.sa_mask); +} + void uc_vm_init(uc_vm_t *vm, uc_parse_config_t *config) { vm->exception.type = EXCEPTION_NONE; @@ -165,6 +230,8 @@ void uc_vm_init(uc_vm_t *vm, uc_parse_config_t *config) uc_vm_exception_handler_set(vm, uc_vm_output_exception); uc_vm_trace_set(vm, 0); + + uc_vm_signal_handlers_setup(vm); } void uc_vm_free(uc_vm_t *vm) @@ -2564,6 +2631,53 @@ uc_vm_output_exception(uc_vm_t *vm, uc_exception_t *ex) fprintf(stderr, "\n"); } +uc_exception_type_t +uc_vm_signal_dispatch(uc_vm_t *vm) +{ + uc_exception_type_t ex; + uc_value_t *handler; + uint64_t mask; + size_t i, j; + int sig, rv; + + if (!vm->config->setup_signal_handlers) + return EXCEPTION_NONE; + + for (i = 0; i < ARRAY_SIZE(vm->signal.raised); i++) { + if (!vm->signal.raised[i]) + continue; + + do { + rv = read(vm->signal.sigpipe[0], &sig, sizeof(sig)); + } while (rv > 0 || (rv == -1 && errno == EINTR)); + + for (j = 0; j < 64; j++) { + mask = 1ull << j; + + if (vm->signal.raised[i] & mask) { + vm->signal.raised[i] &= ~mask; + + sig = i * 64 + j; + handler = ucv_array_get(vm->signal.handler, sig); + + if (ucv_is_callable(handler)) { + uc_vm_stack_push(vm, ucv_get(handler)); + uc_vm_stack_push(vm, ucv_int64_new(sig)); + + ex = uc_vm_call(vm, false, 1); + + if (ex != EXCEPTION_NONE) + return ex; + + ucv_put(uc_vm_stack_pop(vm)); + } + } + } + } + + return EXCEPTION_NONE; +} + static uc_vm_status_t uc_vm_execute_chunk(uc_vm_t *vm) { @@ -2812,6 +2926,7 @@ uc_vm_execute_chunk(uc_vm_t *vm) break; } +exception: /* previous instruction raised exception */ if (vm->exception.type != EXCEPTION_NONE) { /* VM termination was requested */ @@ -2840,6 +2955,10 @@ uc_vm_execute_chunk(uc_vm_t *vm) chunk = uc_vm_frame_chunk(frame); } } + + /* run handler for signal(s) delivered during previous instruction */ + if (uc_vm_signal_dispatch(vm) != EXCEPTION_NONE) + goto exception; } return STATUS_OK; @@ -3057,3 +3176,22 @@ uc_vm_gc_stop(uc_vm_t *vm) return true; } + +void +uc_vm_signal_raise(uc_vm_t *vm, int signo) +{ + uint8_t signum = signo; + + if (signo <= 0 || signo >= NSIG) + return; + + vm->signal.raised[signo / 64] |= (1ull << (signo % 64)); + + write(vm->signal.sigpipe[1], &signum, sizeof(signum)); +} + +int +uc_vm_signal_notifyfd(uc_vm_t *vm) +{ + return vm->signal.sigpipe[0]; +} -- cgit v1.2.3 From 1623245c3ea7422d2b311036c4a784ab18895b92 Mon Sep 17 00:00:00 2001 From: Jo-Philipp Wich Date: Tue, 11 Jul 2023 13:41:32 +0200 Subject: types: treat signal handler array as GC root In order to prevent a premature release of the managed ucode signal handler callbacks, ensure to treat the containing array as GC root to mark the function values as reachable. Signed-off-by: Jo-Philipp Wich --- types.c | 1 + 1 file changed, 1 insertion(+) diff --git a/types.c b/types.c index d601479..d3aae0a 100644 --- a/types.c +++ b/types.c @@ -2279,6 +2279,7 @@ ucv_gc_common(uc_vm_t *vm, bool final) /* mark reachable objects */ ucv_gc_mark(vm->globals); ucv_gc_mark(vm->registry); + ucv_gc_mark(vm->signal.handler); ucv_gc_mark(vm->exception.stacktrace); for (i = 0; i < vm->callframes.count; i++) { -- cgit v1.2.3 From 1dbbb6a2561fcc8076f868baaa792ea589dde85b Mon Sep 17 00:00:00 2001 From: Jo-Philipp Wich Date: Tue, 11 Jul 2023 13:44:18 +0200 Subject: main: enable signal dispatching in the standalone cli interpreter Enable signal dispatching by default for standalone ucode programs. Also adjust the `gc()` testcase output as the default number of allocations with enabled signal dispatching changes slightly. Signed-off-by: Jo-Philipp Wich --- main.c | 3 ++- tests/custom/03_stdlib/60_gc | 50 ++++++++++++++++++++++---------------------- 2 files changed, 27 insertions(+), 26 deletions(-) diff --git a/main.c b/main.c index 138447a..a51c370 100644 --- a/main.c +++ b/main.c @@ -502,7 +502,8 @@ main(int argc, char **argv) .strict_declarations = false, .lstrip_blocks = true, .trim_blocks = true, - .raw_mode = true + .raw_mode = true, + .setup_signal_handlers = true }; app = appname(argv[0]); diff --git a/tests/custom/03_stdlib/60_gc b/tests/custom/03_stdlib/60_gc index 44e5d9e..28c0d00 100644 --- a/tests/custom/03_stdlib/60_gc +++ b/tests/custom/03_stdlib/60_gc @@ -45,11 +45,11 @@ Returns an object count if the given operation is `count`. -- End -- -- Expect stdout -- -Count #1: 5 -Count #2: 6 -Count #3: 5 -Count #4: 6 -Count #5: 5 +Count #1: 6 +Count #2: 7 +Count #3: 6 +Count #4: 7 +Count #5: 6 -- End -- @@ -84,24 +84,24 @@ Testing enabling the automatic collector. -- End -- -- Expect stdout -- -Count #1: 6 -Count #2: 12 -Count #3: 12 -Count #4: 12 -Count #5: 12 -Count #6: 12 -Count #7: 12 -Count #8: 12 -Count #9: 12 -Count #10: 12 -Count #11: 12 -Count #12: 22 -Count #13: 32 -Count #14: 42 -Count #15: 52 -Count #16: 62 -Count #17: 72 -Count #18: 82 -Count #19: 92 -Count #20: 102 +Count #1: 7 +Count #2: 14 +Count #3: 14 +Count #4: 14 +Count #5: 14 +Count #6: 14 +Count #7: 14 +Count #8: 14 +Count #9: 14 +Count #10: 14 +Count #11: 14 +Count #12: 24 +Count #13: 34 +Count #14: 44 +Count #15: 54 +Count #16: 64 +Count #17: 74 +Count #18: 84 +Count #19: 94 +Count #20: 104 -- End -- -- cgit v1.2.3 From 97a5292307bebc4c3be45f47743268084b376d09 Mon Sep 17 00:00:00 2001 From: Jo-Philipp Wich Date: Tue, 11 Jul 2023 13:47:03 +0200 Subject: lib: add `signal()` stdlib function The `signal()` standard library function provides functionality for registering signal handler callbacks, restoring default signal behaviour or ignoring specific signals. Signed-off-by: Jo-Philipp Wich --- lib.c | 243 ++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 243 insertions(+) diff --git a/lib.c b/lib.c index 9ca2790..b06c02b 100644 --- a/lib.c +++ b/lib.c @@ -5647,6 +5647,248 @@ uc_callfunc(uc_vm_t *vm, size_t nargs) } +static const char *signal_names[] = { +#if defined(SIGINT) + [SIGINT] = "INT", +#endif +#if defined(SIGILL) + [SIGILL] = "ILL", +#endif +#if defined(SIGABRT) + [SIGABRT] = "ABRT", +#endif +#if defined(SIGFPE) + [SIGFPE] = "FPE", +#endif +#if defined(SIGSEGV) + [SIGSEGV] = "SEGV", +#endif +#if defined(SIGTERM) + [SIGTERM] = "TERM", +#endif +#if defined(SIGHUP) + [SIGHUP] = "HUP", +#endif +#if defined(SIGQUIT) + [SIGQUIT] = "QUIT", +#endif +#if defined(SIGTRAP) + [SIGTRAP] = "TRAP", +#endif +#if defined(SIGKILL) + [SIGKILL] = "KILL", +#endif +#if defined(SIGPIPE) + [SIGPIPE] = "PIPE", +#endif +#if defined(SIGALRM) + [SIGALRM] = "ALRM", +#endif +#if defined(SIGSTKFLT) + [SIGSTKFLT] = "STKFLT", +#endif +#if defined(SIGPWR) + [SIGPWR] = "PWR", +#endif +#if defined(SIGBUS) + [SIGBUS] = "BUS", +#endif +#if defined(SIGSYS) + [SIGSYS] = "SYS", +#endif +#if defined(SIGURG) + [SIGURG] = "URG", +#endif +#if defined(SIGSTOP) + [SIGSTOP] = "STOP", +#endif +#if defined(SIGTSTP) + [SIGTSTP] = "TSTP", +#endif +#if defined(SIGCONT) + [SIGCONT] = "CONT", +#endif +#if defined(SIGCHLD) + [SIGCHLD] = "CHLD", +#endif +#if defined(SIGTTIN) + [SIGTTIN] = "TTIN", +#endif +#if defined(SIGTTOU) + [SIGTTOU] = "TTOU", +#endif +#if defined(SIGPOLL) + [SIGPOLL] = "POLL", +#endif +#if defined(SIGXFSZ) + [SIGXFSZ] = "XFSZ", +#endif +#if defined(SIGXCPU) + [SIGXCPU] = "XCPU", +#endif +#if defined(SIGVTALRM) + [SIGVTALRM] = "VTALRM", +#endif +#if defined(SIGPROF) + [SIGPROF] = "PROF", +#endif +#if defined(SIGUSR1) + [SIGUSR1] = "USR1", +#endif +#if defined(SIGUSR2) + [SIGUSR2] = "USR2", +#endif +}; + +/** + * Set or query process signal handler function. + * + * When invoked with two arguments, a signal specification and a signal handler + * value, this function configures a new process signal handler. + * + * When invoked with one argument, a signal specification, this function returns + * the currently configured handler for the given signal. + * + * The signal specification might either be an integer signal number or a string + * value containing a signal name (with or without "SIG" prefix). Signal names + * are treated case-insensitively. + * + * The signal handler might be either a callable function value or one of the + * two special string values `"ignore"` and `"default"`. Passing `"ignore"` will + * mask the given process signal while `"default"` will restore the operating + * systems default behaviour for the given signal. + * + * In case a callable handler function is provided, it is invoked at the + * earliest opportunity after receiving the corresponding signal from the + * operating system. The invoked function will receive a single argument, the + * number of the signal it is invoked for. + * + * Note that within the ucode VM, process signals are not immediately delivered, + * instead the VM keeps track of received signals and delivers them to the ucode + * script environment at the next opportunity, usually before executing the next + * byte code instruction. This means that if a signal is received while + * performing a computationally expensive operation in C mode, such as a complex + * regexp match, the corresponding ucode signal handler will only be invoked + * after that operation concluded and control flow returns to the VM. + * + * Returns the signal handler function or one of the special values `"ignore"` + * or `"default"` corresponding to the given signal specification. + * + * Returns `null` if an invalid signal spec or signal handler was provided. + * + * Returns `null` if changing the signal action failed, e.g. due to insufficient + * permission, or when attempting to ignore a non-ignorable signal. + * + * @function module:core#signal + * + * @param {number|string} signal + * The signal to query/set handler for. + * + * @param {Function|string} [handler] + * The signal handler to install for the given signal. + * + * @returns {Function|string} + * + * @example + * // Ignore signals + * signal('INT', 'ignore'); // "ignore" + * signal('SIGINT', 'ignore'); // "ignore" (equivalent to 'INT') + * signal('sigterm', 'ignore'); // "ignore" (signal names are case insensitive) + * signal(9, 'ignore'); // null (SIGKILL cannot be ignored) + * + * // Restore signal default behavior + * signal('INT', 'default'); // "default" + * signal('foobar', 'default'); // null (unknown signal name) + * signal(-313, 'default'); // null (invalid signal number) + * + * // Set custom handler function + * function intexit(signo) { + * printf("I received signal number %d\n", signo); + * exit(1); + * } + * + * signal('SIGINT', intexit); // returns intexit + * signal('SIGINT') == intexit; // true + */ +static uc_value_t * +uc_signal(uc_vm_t *vm, size_t nargs) +{ + uc_value_t *signame = uc_fn_arg(0); + uc_value_t *sighandler = uc_fn_arg(1); + struct sigaction sa = { 0 }; + char *sigstr; + int sig; + + if (ucv_type(signame) == UC_INTEGER) { + sig = (int)ucv_int64_get(signame); + + if (errno || sig >= (int)ARRAY_SIZE(signal_names) || !signal_names[sig]) + return NULL; + } + else if (ucv_type(signame) == UC_STRING) { + sigstr = ucv_string_get(signame); + + if (!strncasecmp(sigstr, "SIG", 3)) + sigstr += 3; + + for (sig = 0; sig < (int)ARRAY_SIZE(signal_names); sig++) + if (signal_names[sig] && !strcasecmp(signal_names[sig], sigstr)) + break; + + if (sig == (int)ARRAY_SIZE(signal_names)) + return NULL; + } + else { + return NULL; + } + + /* Query current signal handler state */ + if (nargs < 2) { + if (sigaction(sig, NULL, &sa) != 0) + return NULL; + + if (sa.sa_handler == SIG_IGN) + return ucv_string_new("ignore"); + + if (sa.sa_handler == SIG_DFL) + return ucv_string_new("default"); + + return ucv_get(ucv_array_get(vm->signal.handler, sig)); + } + + /* Install new signal handler */ + if (ucv_type(sighandler) == UC_STRING) { + sigstr = ucv_string_get(sighandler); + + sa.sa_flags = SA_ONSTACK | SA_RESTART; + sigemptyset(&sa.sa_mask); + + if (!strcmp(sigstr, "ignore")) + sa.sa_handler = SIG_IGN; + else if (!strcmp(sigstr, "default")) + sa.sa_handler = SIG_DFL; + else + return NULL; + + if (sigaction(sig, &sa, NULL) != 0) + return NULL; + + ucv_array_set(vm->signal.handler, sig, NULL); + } + else if (ucv_is_callable(sighandler)) { + if (sigaction(sig, &vm->signal.sa, NULL) != 0) + return NULL; + + ucv_array_set(vm->signal.handler, sig, ucv_get(sighandler)); + } + else { + return NULL; + } + + return ucv_get(sighandler); +} + + const uc_function_list_t uc_stdlib_functions[] = { { "chr", uc_chr }, { "die", uc_die }, @@ -5718,6 +5960,7 @@ const uc_function_list_t uc_stdlib_functions[] = { { "loadstring", uc_loadstring }, { "loadfile", uc_loadfile }, { "call", uc_callfunc }, + { "signal", uc_signal }, }; -- cgit v1.2.3 From 2593270498be82de82f770cb3744d9e5f8d0b0bd Mon Sep 17 00:00:00 2001 From: Jo-Philipp Wich Date: Tue, 11 Jul 2023 13:49:55 +0200 Subject: uloop: interrupt on VM signals When the VM instance loading the uloop module has signal dispatching enabled, implicitly register the signal pipe as file descriptor in the global uloop and dispatch received signals to the VM instance. This ensures that the respective managed ucode signal handlers are invoked immediately and not just after `uloop_run()` returns. Also end the uloop in case any invoked signal handler threw an exception, to match the expected behaviour with other kinds of callbacks. Signed-off-by: Jo-Philipp Wich --- lib/uloop.c | 24 ++++++++++++++++++++++++ 1 file changed, 24 insertions(+) diff --git a/lib/uloop.c b/lib/uloop.c index f45f8c3..99cd984 100644 --- a/lib/uloop.c +++ b/lib/uloop.c @@ -1155,8 +1155,22 @@ static void close_pipe(void *ud) } +static struct { + struct uloop_fd ufd; + uc_vm_t *vm; +} signal_handle; + +static void +uc_uloop_signal_cb(struct uloop_fd *ufd, unsigned int events) +{ + if (uc_vm_signal_dispatch(signal_handle.vm) != EXCEPTION_NONE) + uloop_end(); +} + void uc_module_init(uc_vm_t *vm, uc_value_t *scope) { + int signal_fd; + uc_function_list_register(scope, global_fns); #define ADD_CONST(x) ucv_object_add(scope, #x, ucv_int64_new(x)) @@ -1175,4 +1189,14 @@ void uc_module_init(uc_vm_t *vm, uc_value_t *scope) object_registry = ucv_array_new(vm); uc_vm_registry_set(vm, "uloop.registry", object_registry); + + signal_fd = uc_vm_signal_notifyfd(vm); + + if (signal_fd != -1 && uloop_init() == 0) { + signal_handle.vm = vm; + signal_handle.ufd.cb = uc_uloop_signal_cb; + signal_handle.ufd.fd = signal_fd; + + uloop_fd_add(&signal_handle.ufd, ULOOP_READ); + } } -- cgit v1.2.3