You signed in with another tab or window. Reload to refresh your session.You signed out in another tab or window. Reload to refresh your session.You switched accounts on another tab or window. Reload to refresh your session.Dismiss alert
{{ message }}
Repository navigation
Map Ctrl+C to cancellation of the main function #858
Currently, WASI does not describe any way for a CLI guest to receive the Ctrl+C signal (SIGINT on Unix). This signal is commonly used to request graceful shutdown, giving the application some time to e.g.:
clean up resources,
flush buffers to disk,
stop listening for new network requests, while letting the existing ones run to completion,
etc..
The component model has built-in support for async cancellation, and I think that in the CLI world Ctrl+C maps neatly to cancellation of the main (run) function.
I propose we update the documentation of the run function to say something along the lines of:
If a host wishes to propagate Ctrl+C into the guest, then cancellation of the run task is the canonical way to do so.
Conceptually I like this a fair bit! Ctrl+C is the most commonly used Unix signal, and it seems like it should unambiguously map to cancellation of the main function.
There are additional questions about how/whether we could handle other signals in wasi:cli. But I don't think we need to answer those now to support this particular behavior.
There are additional questions about how/whether we could handle other signals in wasi:cli.
The referenced issue above already goes into some detail on that topic with conclusions like:
(DG): Full POSIX-style signal handling would likely be a massive undertaking, involving complex security, portability, and VM implementation considerations, and probably core wasm semantics changes (outside the scope of WASI).
(NF): POSIX signals, in their full generality, are infamously tricky, footgun-y, and generally not a paragon of API design.
So, I wanted to stay as far away from this as possible. 😃 Ctrl+C / SIGINT is by far the most commonly used one, and is also one of the few signals (if not the only) that has a 1:1 counterpart on Windows. So I'd say: lets try just this one first.
I'm broadly in favor of adding some kind of documentation about this. Some questions/thoughts:
"Graceful shutdown" won't be meaningful for all guests; what (if any) default behavior should be prescribed for cancellation? (Transitively cancel subtasks? Call exit_with_code(130)?)
Async cancellation is cooperative and could take an indeterminate amount of time. Should we say anything about this situation? It's somewhat common for systems to treat a second Ctrl+C as a less-graceful shutdown signal, for example.
We should be careful to distinguish this as not being simply a "propagation" of SIGINT/CTRL_C_EVENT here. Async cancellation is fundamentally less powerful than signals but close enough in simple/happy-path cases to cause confusion.
Both of those are indeed examples of how the guest could implement cancellation. Ultimately, neither the host nor the spec need to care about how the guest decides to wrap up.
Async cancellation is cooperative and could take an indeterminate amount of time. Should we say anything about this situation?
SIGINT is cooperative too, so I don't think we'll be introducing any new unexpected behavior on that front. But I agree that we should document that hosts are free to kill the process on a second Ctrl+C.
Async cancellation is fundamentally less powerful than signals
In what sense exactly? Assuming we're talking only about SIGINT and not signals in its full generality, I think they're equivalent.
close enough in simple/happy-path cases to cause confusion.
Ultimately, neither the host nor the spec need to care about how the guest decides to wrap up.
This is probably true when the host is the caller but may not be true for inter-component calls.
The quickest way for a callee to wrap up would be to trap which is probably not what a component caller would want from cancellation / graceful shutdown.
close enough in simple/happy-path cases to cause confusion.
What confusion do you foresee?
I suppose this will depend on how async cancellation is expected to behave more generally, but where SIGINT's default behavior is preemptive termination, it seems like async cancellation's default behavior may be less reliable. My expectation for SIGINT is that it will either terminate immediately or it will trigger some documented graceful shutdown / cleanup process, but with component composition the cancellation behavior may not be entirely under the run implementer's control.
While modeling ctrl-c as an async cancel of the run function, I'm not sure that'll work out in practice w.r.t. how toolchains are going to interact with a main function.
For @lann's concerns our historical thinking is that async cancellation is generally a cooperative system where each component is responsible for propagating the signal to callees. In Rust for example when an async export is cancelled that'll drop the future representing the export which has the effect of cancelling anything that was in-progress there as well. This sort of maps to signals and sort of not. The default behavior of signals to kill everything doesn't map well, but the behavior of "whoever caught the signal has to forward that to others" is modeled here as well. For example if I install a SIGINT handler there's no guarantee that I actually respect it and properly shut down the libraries I'm using. Regardless, though, I wouldn't personally see this cooperative nature as a blocker for modeling ctrl-c as such.
My main concern is that a lot of programs are likely going to be using wasi-libc or some other sync-looking main function signature in guest languages (e.g. performing a sync-lift into an async type of run: async func() -> ...). In doing so ctrl-c is only delivered to blocking calls flagged as cancellable, and all wasi-libc could really do is surface that as a return value of poll for example. That's generally not how SIGINT works in native platforms at least.
Integrating ctrl-c into a sync-looking main would probably require something along the lines of:
A wasi-libc "sigaction" which is a which is a glorified callback registration for ctrl-c
All calls to blocking functions anywhere in wasi-libc would be flagged as cancellable
Upon seeing anything cancelled wasi-libc would invoke the ctrl-c callback and hope it does something reasonable. Probably EINTR then returned and pray for the best.
Well, ok, maybe this isn't as bad as I originally thought? Supporting ctrl-c in wasi-libc, regardless though, is I think going to be a quite involved piece of implementation
Once we've added a proper callback type to WIT, then agreed that adding some new sigaction-like import that allows guest code to register a callback would make sense and would avoid the tricky plumbing questions w.r.t run cancellation. In fact, once we have shared functions in core wasm, if the callback is lifted from a shared function, then semantically it'd be fine to call it immediately when the host receives the signal as-if the callback were called from some "other" thread which would make it a pretty effective approximation of externally-originating signals (like SIGINT and SIGIO).
Activity
yoshuawuyts commentedon Dec 28, 2025
Conceptually I like this a fair bit! Ctrl+C is the most commonly used Unix signal, and it seems like it should unambiguously map to cancellation of the main function.
There are additional questions about how/whether we could handle other signals in
wasi:cli. But I don't think we need to answer those now to support this particular behavior.badeend commentedon Dec 28, 2025
The referenced issue above already goes into some detail on that topic with conclusions like:
So, I wanted to stay as far away from this as possible. 😃 Ctrl+C / SIGINT is by far the most commonly used one, and is also one of the few signals (if not the only) that has a 1:1 counterpart on Windows. So I'd say: lets try just this one first.
yoshuawuyts commentedon Dec 28, 2025
Oh I hadn't clicked through on the other link; that's good to know! But it sounds like we're on the same page about this!
lann commentedon Dec 29, 2025
I'm broadly in favor of adding some kind of documentation about this. Some questions/thoughts:
exit_with_code(130)?)SIGINT/CTRL_C_EVENThere. Async cancellation is fundamentally less powerful than signals but close enough in simple/happy-path cases to cause confusion.badeend commentedon Dec 29, 2025
For the guest to wrap up ASAP.
Both of those are indeed examples of how the guest could implement cancellation. Ultimately, neither the host nor the spec need to care about how the guest decides to wrap up.
SIGINTis cooperative too, so I don't think we'll be introducing any new unexpected behavior on that front. But I agree that we should document that hosts are free to kill the process on a second Ctrl+C.In what sense exactly? Assuming we're talking only about
SIGINTand not signals in its full generality, I think they're equivalent.What confusion do you foresee?
lann commentedon Dec 30, 2025
This is probably true when the host is the caller but may not be true for inter-component calls.
The quickest way for a callee to wrap up would be to trap which is probably not what a component caller would want from cancellation / graceful shutdown.
I suppose this will depend on how async cancellation is expected to behave more generally, but where
SIGINT's default behavior is preemptive termination, it seems like async cancellation's default behavior may be less reliable. My expectation forSIGINTis that it will either terminate immediately or it will trigger some documented graceful shutdown / cleanup process, but with component composition the cancellation behavior may not be entirely under therunimplementer's control.alexcrichton commentedon Feb 10, 2026
While modeling ctrl-c as an async cancel of the
runfunction, I'm not sure that'll work out in practice w.r.t. how toolchains are going to interact with a main function.For @lann's concerns our historical thinking is that async cancellation is generally a cooperative system where each component is responsible for propagating the signal to callees. In Rust for example when an async export is cancelled that'll drop the future representing the export which has the effect of cancelling anything that was in-progress there as well. This sort of maps to signals and sort of not. The default behavior of signals to kill everything doesn't map well, but the behavior of "whoever caught the signal has to forward that to others" is modeled here as well. For example if I install a SIGINT handler there's no guarantee that I actually respect it and properly shut down the libraries I'm using. Regardless, though, I wouldn't personally see this cooperative nature as a blocker for modeling ctrl-c as such.
My main concern is that a lot of programs are likely going to be using wasi-libc or some other sync-looking
mainfunction signature in guest languages (e.g. performing a sync-lift into an async type ofrun: async func() -> ...). In doing so ctrl-c is only delivered to blocking calls flagged ascancellable, and all wasi-libc could really do is surface that as a return value ofpollfor example. That's generally not howSIGINTworks in native platforms at least.Integrating ctrl-c into a sync-looking
mainwould probably require something along the lines of:Well, ok, maybe this isn't as bad as I originally thought? Supporting ctrl-c in wasi-libc, regardless though, is I think going to be a quite involved piece of implementation
lukewagner commentedon Feb 11, 2026
Once we've added a proper callback type to WIT, then agreed that adding some new
sigaction-like import that allows guest code to register a callback would make sense and would avoid the tricky plumbing questions w.r.truncancellation. In fact, once we havesharedfunctions in core wasm, if the callback is lifted from asharedfunction, then semantically it'd be fine to call it immediately when the host receives the signal as-if the callback were called from some "other" thread which would make it a pretty effective approximation of externally-originating signals (likeSIGINTandSIGIO).