Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Understanding catch_all #128

Closed
RossTate opened this issue Sep 21, 2020 · 1 comment
Closed

Understanding catch_all #128

RossTate opened this issue Sep 21, 2020 · 1 comment

Comments

@RossTate
Copy link
Contributor

My understanding is that the primary motivation for catch_all comes from C++'s catch (...), which is required to catch all C++ exceptions, but which in some ABIs is supposed to catch foreign exceptions. Note that there is flexibility here, and in general C++ has a fair amount of flexibility in its semantics for exceptions. Another example of flexibility is that destructors may or may not execute for uncaught exceptions; that choice is left up to the ABI, and I believe largely in order to permit both single-phase and two-phase exception-handling implementations. Similarly, I believe leaving the handling of foreign exceptions up to the ABI was done in recognition of the fact that there are many semantics for and implementations of exceptions in other systems, and there is no way a single C++ semantics could anticipate all those systems and how they could/would interact with C++.

In other issues, I suggested that C++'s catch (...) could be implemented by an unwind that branches out of the unwind block. @aheejin and @dschuff raised concerns about how this would interact with two-phase exception handling, giving good arguments for why there deserves to be a distinction between that approach and catch_all. But those concerns made me wonder more generally how catch_all is supposed to interact with other exception systems we would like WebAssembly to be able to support. I wanted to devote this issue to understanding those interactions.

Bypassing catch_all

First, I want to point out what catch_all cannot (or at least should not) do. catch_all cannot prevent control from moving from inside the catch_all to outside the catch_all within the current stack through other means. Here are three examples illustrating why.

  1. Suppose we extend WebAssembly with stack inspection so that, say, a program can mark its own linear-memory-managed GC roots on the stack. That likely requires executing code associated with frames on the current stack belong with the current program. Some of these frames might be outside a catch_all, so it would be problematic if catch_all prevented code from executing on those frames. That code could than branch (without unwinding) outside of the inspection code. There's no exception here, so the body of catch_all shouldn't be expected to catch here.

  2. The catch_all could be executed on a "child" stack, and a function call within the catch_all could switch control (or suspend control) to a "parent" stack.

  3. The program could trap within the catch_all and the embedding language could catch the trap outside the catch_all.

Note that in all these cases the code outside of the catch_all has to set up something for the code inside catch_all to connect to, whether it's a stack mark, a stack allocation, or host privilege. So it's not like control is able to go anywhere arbitrarily. In fact this pattern is useful for supporting sandboxing. Also, notice that in each case the portion of the stack with catch_all is not unwound. This again is useful to support, say for sandboxing purposes (and fault-tolerant programming already has to consider unwinders as not guaranteed to execute for a variety of reasons, like trapping).

Supporting throw; within catch (...)

This GitHub issue overlaps with #127 because the expectation is that rethrow would be needed to support C++ throw; within catch (...). But at present the rethrow instruction would only be able to implement the most trivial uses of throw;, i.e. the ones that occur syntactically within the catch (...). Those uses could also be supported by having end of catch_all rethrow the exception, so a rethrow instruction is strictly speaking not necessary for this use case.

But let's take a step back and consider whether we should support more advanced uses of throw; for foreign "exceptions", i.e. uses that occur within different dynamic contexts. This would make it possible (through some layers of cross-program calls) for program A to intercept exceptional control flow of program B (which may or may not be conceptual "exceptions/errors") and then have that exceptional control flow be evaluated within a different evaluation context of program B than its original intent without B exporting its exception event. For first-class stacks, we decided not to provide direct support for stack duplication because we felt it could be used as a way to put programs in situations they were not designed for and cannot easily protect against without them even knowing it, and this ability to rethrow foreign exceptions within different contexts seems to bare similar issues.

Now let's consider how we could support such advanced uses of throw; within catch (...). There are two-phase exception-handling systems that support resumable/restartable exceptions (i.e. no continuations/first-class-stacks required). One way is for the first-phase handler to throw an exception that the code that initiated the first phase can catch. So if WebAssembly eventually gets an expressive two-phase exception-handling system, then C++ throw; could be implemented by initiating a first-phase search for a containing (compiled) C++ catch that then throws the caught exception within the first-phase handler. If a C++ catch (...) were compiled using catch_all, then that could be done by executing rethrow in the first-phase handler. This means that catch_all/rethrow combined with expressive two-phase exception handling would enable the above situation (that at least I believe is concerning). On the other hand, the variant where the end of catch_all rethrows the exception, rather than a rethrow instruction, would not have this problem. (It's still possible to implement another ABI for rethrowing foreign exceptions, though that seems too detailed a discussion for now.)

Interacting with foreign exception systems

I mentioned resumable/restartable exceptions, which are one of many examples of other exception systems foreign to C++. It is unclear how this particular example should interact with catch_all. Suppose some code throws a resumable/restartable exception within a catch_all within a first-phase handler that could resume/restart the exception. What should happen? At the point in time where the search propagates to the catch_all, the search still does not know whether it is actually an exception or not. Consider the case where the code throwing the exception knows there is a handler and just needs to know the handler's response. In this case, it would be problematic for catch_all to interfere, so I would think we should let the search for handlers pass through catch_all. But suppose this search finds nothing, in which case different exception systems do different things. Some trap, some unwind the stack, some prompt the user for the missing value, and some generate a dummy value in attempt to keep the system going. What should the catch_all do in each of these situations? Or suppose the search finds a handler, but that handler catches the exception rather than resuming/restarting it. What should catch_all do here?

Closing thoughts

My sense is that, like C++, rather than putting WebAssembly into a position of having to bake in various policy decisions about an ever-expanding range of potential interactions between exception systems, we should defer those to the ABI. WebAssembly's exception-event system is generative (i.e. instances get distinct events unless they import/export) for the sake of preventing unintended interferences and enabling compositional reasoning. For example, a throw; in one C++ module instance won't rethrow the most recent exception caught by a different C++ module instance unless they explicitly coordinate exceptions, which seems like what one would want. But catch_all seems to have conflicts with both of these desirables, especially if it has a rethrow instruction rather than rethrowing at its end.

For now, an ABI for C++ that likely addresses the major use cases is one where catch (...) explicitly catches host/JS exceptions (i.e. by importing an exception event from the host with payload externref) and C++ exceptions thrown within the same module instance (or ones that were instantiated with the same __cpp_exception event). JS programs already have to deal with their exceptions being thrown in different dynamic contexts than where they originated, so that concern with throw; should be fine. The ABI could also/alternatively use unwind to implement catch (...), which would intercept all unwindings, which is a reasonable approximation of exceptions (especially for a single-phase exception ABI).

In the future, if we add two-phase exception handling, then an ABI for C++ could compile catch (...) to use a filter for C++ (and host/JS?) exceptions that always indicates to handle the exception. This would miss other foreign exceptions, but that might be necessary given the diversity of foreign exception systems (e.g. resumable/restartable exceptions). For those, the ABI could still use unwind to catch at least unwindings.

In the long future, an inter-language ABI might develop that would facilitate interactions between various exception systems. It's hard to say now what this would look like, but my preference is to make WebAssembly flexible/expressive enough for the community to evolve these conventions on their own as they develop an understanding of what sorts of inter-program/language coordination does and does not need to be done.

Or we might find that the first ABI is enough. The point is that the recent change makes the EH proposal composable enough for programs to reasonably interop with other programs compiled with different/future ABIs, and so we can reasonably make this an ABI problem rather than a core-spec problem.

@tlively
Copy link
Member

tlively commented Sep 21, 2020

Bypassing catch_all...

I agree with everything in this section. Is it correct that you're not identifying any problems in this section, but rather just writing down your understanding of the situation to make sure everyone is on the same page?


But at present the rethrow instruction would only be able to implement the most trivial uses of throw;, i.e. the ones that occur syntactically within the catch (...).

This does seem unfortunate, but for native C++ exceptions, throw; at non-trivial locations can still be implemented via a normal throw, so this is only a problem for foreign exceptions. You give an idea for a solution that depends on two-phase exception handling, but is it correct that we have no solution that will work with this single-phase exception proposal? If so, I guess we just won't support this functionality for foreign exceptions. That seems like it will probably be fine in practice.

Those uses could also be supported by having end of catch_all rethrow the exception, so a rethrow instruction is strictly speaking not necessary for this use case.

Meta-comment: Arguing that something is not strictly necessary but predicating that argument on something else having different semantics is quite a rhetorical leap and reads very much like "we shouldn't use your proposed semantics because my proposed semantics work, too." When you make this kind of argument, it gives the impression that you have a vendetta against certain proposed features, in this case rethrow.

But let's take a step back and consider whether we should support more advanced uses of throw; for foreign "exceptions", i.e. uses that occur within different dynamic contexts.

IIUC, this is essentially the same problem you raised in #101, where the general conclusion was that although it is true that modules can use catch_all to interfere with each other's exceptions in undesirable ways, this wasn't an important enough concern to be worth removing catch_all over. I would prefer not to relitigate this.


Interacting with foreign exception systems

I don't fully follow the examples in this section, but it looks like you're identifying a new forward-compatibility problem. Could you break this out into its own issue with a more detailed illustration of the problem? It would also be good to reference specific languages/systems because I'm not familiar with any of the behaviors you describe. (Prompting the user for a value!?)


My sense is that, like C++, rather than putting WebAssembly into a position of having to bake in various policy decisions about an ever-expanding range of potential interactions between exception systems, we should defer those to the ABI.

I fully agree with this statement. It's important to note that ABI compliance cannot (and should not) be validated by engines. So if one ABI wants to disallow interfering with foreign exceptions, that's fine, and if another ABI wants to allow such interference, that should be fine, too.


This opening post was a big mix of explanations, discussions of issues, statements of principles, etc. The issues I identified in it were the composability issue originally discussed in #101 and the issue of interactions with various foreign exceptions systems, which I requested to be split into its own issue. Are there any issues you raised here that I missed? Can you provide a brief summary of the action items you're introducing, i.e. investigations we need to do and issues we need to settle?

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
None yet
Projects
None yet
Development

No branches or pull requests

2 participants