Stream: general

Topic: Signal Handling


view this post on Zulip Nicholas Renner (Dec 03 2024 at 01:29):

I was reading this article: https://withbighair.com/webassembly/2024/08/26/WebAssembly-and-signals.html by @Chris Woods (I believe?) and see that though you can compile programs with signals/signal handlers to run in Wasmtime, they aren't actually handled. There is an addendum about "a story for another day".

Was there any progress or documentation on how to go about this?

Thanks for any help!

You can think of signal’s like user space interrupts. Essentially the running application is interrupted, and a signal handler function is invoked. This function is passed some data to describe why it is being interrupted. Once the function is invoked it can process a response to this “signal”. You’ve probably seen this on Linux. Geeks for Geeks has a great description of Signals. They are effectively used to communicate some information to the running process, a common use case is sending a signal to notify a process that is about to be terminated, thus allowing it to clean up resources, closing file handles, etc.

view this post on Zulip Soni L. (Dec 03 2024 at 01:38):

in theory you should be able to run an isolated wasm module per signal handler too, but there's no spec for how to declare such wasm modules in a wasm component yet

view this post on Zulip Chris Woods (Dec 03 2024 at 03:18):

Ohh, yeah, that's me! :)

And yes, you hand handle them, within the limitations of WebAssembly (without stack switching) today. But it requires a change to the host to do it. I was planning on an article showing how it can be done in WAMR.

Long story short; Most applications that use a signal handler, typically use them to detect a request to terminate the running application, and then they exit gracefully. Most of the applications I've come across do this by having a global boolean flag, called say quit. Then they have a busy / working loop, which is basicallyl while (!quit) { /* work here */ }. These native applications have a single signal handler that sets the quit value to true when a terminate signal is caught. (This is also the example from my blog post). - I've seen examples of this within the mongoose http server demos for example.

To implement this same graceful termination, you would write the signal handler in the host environment. When the WASM application starts it would call the host and register the global variable quit as the flag to be set. As the host can see this global variable, it can write to it and change it. So now when the host receives the signal to quit, it sets the quit global to true, and the Wasm application's loop will terminate gracefully.

This delivers the same functionality, but by necessity moves the signal handler out to the host.

The 3rd part of this would be to use the setjmp / longjmp port to implement co-operative multi-tasking and an event loop. But that is most definetly a story for another day ;-)

I hope this helps.

view this post on Zulip Nicholas Renner (Dec 03 2024 at 03:38):

Thanks for the quick reply!

I unfortunately need a solution that doesn't require rewriting application code or moving things to host, while handling more than just exit scenarios.

By having the host handle it do you mean something like is shown here?

This commit fixes a mistake where these tests were not being run on macOS after signal handling was moved behind a `Config` flag instead of being a crate feature. Additionally these tests are moved...

view this post on Zulip Chris Woods (Dec 03 2024 at 03:50):

Yup, after a quick skim read, yeah, I think that's about the height of it. You'd need your main (host) application to catch the signal.

Unless you implement an event loop in your WASM code, and async call backs from the host, where the requests to invoke the async functions are queued in some way. That's the only way I saw to do it.... hence the (for another day).

There is only one stack (at the moment) and there is only one thread (at the moment). So if your application is currently executing, then your kinda stuck. Unless your runtime would allow you to pause execution, preserve it's current stack state, and then invoke another function for the signal handler. WAMR (which is the RT I've been focusing on) doesn't do this. So...

Do you have any ideas?

Re-writing code is something I'm trying to avoid, for the simple applications which catch just, say termination, it's easy enough to do with the concepts I outlined, and at least in c, the code changes can be hidden mostly by some macros. It's not a big deal. But for more complex situations where signals are used as a form of IPC, well, yeah, that's going to be more involved for sure.

view this post on Zulip Soni L. (Dec 03 2024 at 08:38):

basically you could implement the old module-per-thread proposal but adapt it for signal handlers. also, you should omit signal-unsafe functions (malloc, etc) from the signal handler modules while at it, for obvious reasons. (if it breaks stuff, stuff was already broken anyway)

view this post on Zulip Chris Woods (Dec 03 2024 at 13:45):

@Soni L. Ohh, yeah.... you could, at least in WAMR which has thread support today, kick off a seperate thread from the host side, and use it to call functions in the same module. So you could capture the signal in the host, have it queue the request to call the signal handler via the thread, which would give you a largely WASM / guest code compatible solution.

view this post on Zulip Soni L. (Dec 03 2024 at 13:55):

we would just build a wasm engine that can be used as an OS signal handler

view this post on Zulip Soni L. (Dec 03 2024 at 13:56):

like, make the relevant call to signal() instantiate an entire wasm module, then when the signal hits, use the wasm module to run the relevant wasm code within the signal context (and apply the necessary restrictions like forbidding memory.grow and whatnot)

view this post on Zulip Soni L. (Dec 03 2024 at 13:58):

afterwards it's just a matter of getting llvm/emscripten/etc to add support

view this post on Zulip Soni L. (Dec 03 2024 at 14:00):

(particularly, the signal.wasm module should be different from the main.wasm module in that it should not have libc implementations of malloc/free/etc, nor should it have wasi imports)

view this post on Zulip Chris Woods (Dec 03 2024 at 14:32):

Well, yeah, we'd need to break the signal handling functions out of the compilation unit and place them, and any of their invoked fuctions into that other module. I see what your suggestion, basically keeping the same signal handling limitations in place. Umm....

Or for code compatibility, could we do all of the signal handling in the host, and basically just register at start time for every possible signal we can, then when signal is called, we start and pause a thread. When a signal hits we schedule the function call back.

Umm... some more thinking required, and I think inspriation for the next blog post ! ;-)

view this post on Zulip Soni L. (Dec 03 2024 at 15:11):

nah, we don't think that's a good idea. besides, ideally you want it such that whatever you come up with for signal handling, is also useful for embedded wasm, where interrupts exist.

having separate modules and embracing the restrictions is a good way to do it.

view this post on Zulip Chris Woods (Dec 03 2024 at 15:33):

Ummm, interrupts exist everywhere, but the handlers to the bare min to shunt the work to another process. It's the same as a signal handler. It would be nice if we could interrupt the process and switch to a different stack for execution. But without stack switching and more drastic changes to the runtime this is going to be impossible.

Changing the entire tool chain is going to be a lot of work... way beyond a blog post, that's for sure.... and for the most part, if like @Nicholas Renner 's requirements you just want source compatibility, then I think the threading behind the curtiain would be ok.

view this post on Zulip Nicholas Renner (Dec 03 2024 at 17:18):

We implemented signal handling on an earlier prototype of what were working on in NaCl. It involved saving the register state on the stack and then using a sigreturn function that we added. I'm not sure we'll have as much flexibility here but we're going to play around with it.

view this post on Zulip Soni L. (Dec 03 2024 at 17:53):

all we can say is "works in wasm2c"

view this post on Zulip Soni L. (Dec 03 2024 at 18:03):

all we* need is for Instance to be SignalSend(*), it need not be SignalSync

(*) or rather, &mut Instance needs to be SignalSend, since you can't call the deallocator in a signal (&mut suppresses Drop while still carrying ownership).

view this post on Zulip Chris Woods (Dec 03 2024 at 18:04):

Nicholas Renner said:

We implemented signal handling on an earlier prototype of what were working on in NaCl. It involved saving the register state on the stack and then using a sigreturn function that we added. I'm not sure we'll have as much flexibility here but we're going to play around with it.

Yeah, the Runtime itself maintains state info, similar to CPU register states. The specification doesn't provide access to this. So it's impossible to record / preserve the state, at least from inside the WebAssembly byte code. Instead you will need support from the host environment and the runtime.

It may be possible to do this via stack switching, which would effectively force the runtime to record similar information in order to allow execution to resume when the stack is switched. But you'd still need some way of triggering a stack switch from the host environment. This is likely to be runtime dependent.

view this post on Zulip Chris Woods (Dec 03 2024 at 18:18):

Soni L. said:

all we can say is "works in wasm2c"

I'm sure it does! - Love wasm2c.

For singal handling specifically, if the goal is to enable existing code to be ported to Wasm. Then we'd need something that has limited impact on the host code, ideally zero, but hey, beggers can't be choosers.

Existing code doesn't have a seperate compilation module for signal handling, it's bundled with the existing .c and .o files. If you want to put the signal handling in a specific restricted module and run it in another thread, then you'd need to modify the code, or modify the compiler to do something like this for you. - I think you were suggesting this.

That's a lot of effort.... .but if you've done it, I'd love to see it ! - It would be cool.

Even if we've got this far, and we've Wasm code that's safe, we still need some way to trigger that function. In Wasm2C I'm sure you can register the wasm function directly, but that's not possible for WAMR, or I'm sure WasmTime. Instead we'd need to register a host function, which invokes the runtime to execute the wasm function. This level of indirection causes issues.

We don't know what the runtime implementation does. Wasm2C is nice, since you can see exactly what it does - it's just a c function. But WAMR, WasmTime and others may be different. Before the wasm function starts to execute there could be memory allocations / frees etc. Certainly this is true in the Component Model interfaces. Therefore it will not be possible to directly call a wasm function from inside a signal handler.... at least not across runtimes.

That's why I suggested just recording the signal, and allowing the signal handler to exit, and a second thread to actually invoke the function.

Every runtime has it's pros and cons, and Wasm2c is nice. Directly invoking a wasm function from a singal handler is likely to be something that's specific to Wasm2c.

view this post on Zulip Chris Woods (Dec 03 2024 at 18:20):

likewise the threading idea is going to be, at least for now, specific to WAMR.

view this post on Zulip Soni L. (Dec 04 2024 at 11:08):

defining a wasi-signal would be a good first step towards seeing what works for different runtimes


Last updated: Dec 23 2024 at 12:05 UTC