-
Notifications
You must be signed in to change notification settings - Fork 36
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
Creating wasm-uncatchable exceptions in Javascript #101
Comments
Your interpretation is correct. Currently the only reliable way to throw an exception that is uncatchable by wasm is to call into a wasm function that does nothing but trap. It is possible to extend our current JS API so that we can generate trap in JS. This should be a different API from |
I worry that this is just the first of many issues that will arise from having a catch-all in a low-trust setting... |
@RossTate Could you elaborate? Catch-all was basically adopted to make wasm catch foreign exceptions, having the multi-language environments in mind. |
I don't think @Macil's concern is specific to multi-language environments. He wants to be able to call into untrusted code and know that it can't catch his exceptions. His concern is a security application, but there is also a concern about composability. This paper talks about the issues that arise from accidental exception handling, i.e. accidentally handling exceptions that weren't meant for you, and it proposes ideas for addressing it. (To be clear, I'm not proposing those ideas for wasm—I do not even know yet if they make sense for a low-level language—but the discussion of accidental handling seems pertinent regardless.) Having looked through the discussion (thanks for posting and linking to those minutes!), I got the impression that the reason for having catch-all was that it enabled code reuse and finally-clauses/using-clauses/destructors. I didn't see any discussion of this potential downside, though, which is why I noted it here. Is that an accurate impression? Or did I miss an application or a discussion about the tradeoff with this concern? Sorry to bug you; I'm trying to catch up as quickly as I can. |
@RossTate The difference between the first and the current versions is, in the first version, we had both And the reason we wanted |
Without uncatchable exceptions, C++ cannot guarantee that it will be able to handle the exceptions it throws through untrusted frames, so it cannot guarantee that exception objects and their payloads will be cleaned up. But with uncatchable exceptions, no one can throw them through a C++ frame without forcing the C++ module to leak resources because the block that runs the destructors will have been popped off the control flow stack. The solution is for the uncatchable exception to be resumable, so the thrower can return to the throw point and throw a new exception that C++ can catch to run its destructors. Without resumable exceptions, using uncatchable exceptions would force resource leakage, even if all modules were benevolent. Without uncatchable exceptions, malicious modules could force resource leakage, but benevolent modules would be able to clean up all resources correctly. Since we cannot have a well-encapsulated, non-leaky world without both resumable and uncatchable exceptions, and because resumable exceptions are out of scope for this proposal, I suggest that we defer uncatchable exceptions to be included in the future resumable exceptions proposal as well. Until both are implemented, WebAssembly in untrusted contexts will not be able to safely throw exceptions across untrusted frames. |
I'm interested in uncatchable exceptions specifically for the ability to halt everything immediately without the module running cleanup or any other code. I fully expect the module's memory to be left in a bad/leaky state after that, but I don't mind because I'm trying to do the closest thing possible to an in-process SIGKILL on it and then get back to my original JS calling code. I'm not sure my use-case alone would warrant uncatchable exceptions to exist, but this is already the behavior of WASM trap exceptions, which I assume also had the goal of halting things immediately without any cleanup happening. If this concept of uncatchable exceptions is going to exist, then it might be nice to make this more explicit and make some JS APIs around that, maybe even so the trap error can have a custom message. But if it's likely that WASM trap exceptions will become catchable in this proposal or a future one, and WASM-uncatchable exceptions will stop being a thing, then I could instead do binary rewriting to solve my use-case. (If WASM moves in the direction of being able to catch all exceptions/traps, then it seems like I would have to make wasm-metering rewrite WASM binaries so that after every |
I'm a little confused because I mentioned that y'all had raised finally/using/destructors as motivators for catch-all, but y'all seem to have countered with resource releasing as the motivator, i.e. the primary application of finally/using/destructors. So I suspect this is just a case of describing the same thing with different, but if that's not the case then please help me understand the distinction you are making. What's important to note is that none of finally/using/destructors catch all exceptions; in fact all of them catch no exceptions—they clean up resources when an exception is thrown but do not catch the exception itself. So what I'm pointing out is that catch-all is much stronger than necessary to achieve the desired use case and is even perhaps harmfully powerful. Really what it seems you want is two separate features: exception handling (i.e. catching specific exceptions), and some sort of "on exit" feature (the most general form of which I believe is Scheme/Racket's |
@Macil We decided that traps would not be catchable. Does that cover your use case? @RossTate we’re not countering, we’re agreeing with you and filling in some details! We currently implement landing pads by catching exceptions, running destructors, then rethrowing. I don’t know that we’ve considered a construct that runs when an exception passes through without catching it. Wouldn’t such a construct still break encapsulation by observing an exception passing through? Or does it not matter because the exceptions can’t be inspected or tampered with? |
As @tlively pointed out above, traps are exactly for this use case. Sorry that we don't currently have a convenient way of trapping in JS; this can be solved by adding a corresponding JS API later. But in the meantime, you can call into a wasm function that does nothing but trap, as you first suggested.
What you described is composed of multiple steps: catching (or at least detecting?) an exception, running destructors, and rethrowing the exception. And the current proposal provides these low-level constructs. I guess what you meant was in some way similar to the first proposal, with an implicit
This has the same set of problems with our first proposal that we cannot rethrow outside |
Ah, thanks for the clarification (and the additional details)!
So you're right that information is still being leaked, i.e. that some exception was thrown, and that's important for people to be aware of (though I doubt that they'd be surprised), but it's overall fine because as you say the exception itself would not be tampered with.
This seems to be a
I want to table this scenario for now, not because I don't think it needs to be addressed, but because I want to first get everyone on the same page for the "easy" case before adding more complications. My suspicion is that this is actually an orthogonal issue, but we should revisit that suspicion later.
Ah, so this is the second motivation that I had noted. I'm hoping you can give me a better understanding of the specifics here. In particular, suppose we had a (familiar)
|
@RossTate We've been discussing this at length here in the office, and one of the missing pieces of this discussion is a shared understanding of what security properties are under consideration. The higher level question here is "Do we want to design WebAssembly features so that they can be used across trust boundaries and still preserve X properties?" What is X and why do we care about those properties in particular? I assume X is something like full abstraction or contextual equivalence that has been rigorously defined and justified in the literature. I hope having an answer to those questions will imply an answer to the following questions: Why should we prefer having the ability to guarantee that thrown exceptions will not be caught by modules in a separate trust domain over having the ability to catch all exceptions that propagate through a particular trust domain? Why privilege the thrower over the catcher? Yes, a catch-all construct can be created by importing all exception tags from all trust domains in an application, but I don't think that can be represented at the source level in today's languages, so practically there is a real tradeoff of functionality here for applications with a privileged trust domain. |
I don't think we have a definition of trust domain in the first place. @RossTate, in your scenario, a module may not be able to drop an exception that does not know the tag for, but it can still drop exceptions they can match with known tags. And tags don't imply anything about trust domains; they were invented primarily for differentiating languages. So I'm not sure if making a module incapable of dropping some exceptions but not others is particularly safer than the current situation. And I don't think we have a good threat model or definition of what's safe and not safe in the first place. One can argue it is safer if a module is be able to catch all exceptions as it wants and run a handler for that and continue execution, and it should not be disrupted by an untrusted module's exception unwinding through itself. (I'm not necessarily endorsing this definition; what I'm saying is we don't have a definition of what's safe in the first place.) |
I continue to think Interface Types solves everything 🤷♂️ In particular, consider that IT has to do something in the share-nothing linking case. It would be very strange to say "I don't trust this module one bit and don't want it to interfere with me whatsoever, but also if it ever traps I should abort instantly." So IT needs to be able to reason about exceptions across module boundaries. (I consider this a good thing: it should be possible to turn a C++ exception into a Rust So to my (obviously biased) view, any non-IT inter-module linking is going to be shared-something linking. And I think it makes sense to have less trust at that layer of wasm. If two modules share memory, one can corrupt the other if they aren't coordinating. If two modules share an exception stack, ditto. |
These are great questions that admittedly I don't have the answer to. I do know the paper I linked to above has abstraction theorems for exceptions (and algebraic effects) akin to relational parametricity, which has known security benefits, but I do not yet know whether its design is appropriate for or translates well to WebAssembly. That said, just because we do not know the goal precisely doesn't mean we should throw it away entirely. All this uncertainty makes me all the more compelled to take a conservative approach if we can find one that sufficiently addresses the current pressing considerations in increasing order of importance (as I understand it).
This is addressed fairly obviously by So altogether, while
Y'all never answered whether my pattern above addresses code sharing, but I'm going to operate under the assumption that it does. It occurs to me that there might be code to share across exception handlers (besides releasing of resources). If so, a slightly different design would have a(/the?) catch clause take a list of tags. If not though, then note that we no longer need Now suppose the handler itself throws an exception (whether explicitly or by some called function). Then the resource releasing still needs to be done. But with So altogether, it seems like
Now for the most important consideration. Unfortunately Since Andreas just spoke about it, a less familiar situation is continuations. The basic idea of continuations is to make stacks be just another construct on the heap. In Andreas's lightweight-threads example, he had a stack/continuation for each thread. Now suppose a thread gets discarded because the lightweight system discovered that its work had already been completed. That discarded (i.e. to-be-garbage-collected) thread might have acquired resources that need to be released, but the code for doing so is buried in the stack. Before garbage-collecting that stack, we need to free those resources. In other words, stacks-as-heap-objects, i.e. continuations, have finalizers. The So altogether, it seems like That was a lot, so let me summarize. |
I feel arguing about what's more conservative without defining security goals does not have much meaning. As I said above, one could very much argue that the ability to catch and handle all exceptions is more conservative and helpful to guard a module from any malicious module out there, especially in case one module does not have full knowledge about all other participants, which can be a probable scenario in the world where wasm becomes popular and people start to use some kind of package managers to download modules. After all, as @jgravelle-google pointed out, I think each module should resort to Interface Types to guard itself from malicious modules out there.
The scenario you described about a callback seems rather contrived. Why a module has to rely on the host callback for such a basic capability? And I'll repeat my question above; if catch-and-drop is such a threat, why is making a module incapable of dropping some exceptions but not others is any safer than the current situation? A module can still catch and drop exceptions with a tag it understands.
Was this principle agreed on at some point? Isn't Interface Types all about making a module not trust anyone else and giving it capability to provide its own adapter to transform, or coerce, other modules' activity upon it?
I'm not very sure if we should change the proposal in a significant way over a use case we don't really understand.
As I mentioned above, we need explicit exnrefs, which was the whole point of switching to the second proposal. That we only should rethrow within a catch clause is a very severe restriction on code generation, and as I said, there are cases we rethrow not within the current function but within a callee. (
Clang, and I think other compiler frontends too, generates cleanup (destructors) code for normal path and exception path separately, for a reason.
So making cleanup code shared between the two paths is not very feasible in the first place. Of course code generation will be a nightmare. If we really should do that, we can't even use clang's EH CodeGen. (For some details, wasm is currently using a modified variant of LLVM's Windows exception handling IR, which is different from Itanium ABI, but the all EH generation schemes separately generate cleanup code for the normal and the exception path.) And I don't see what we achieve from deviating from the normal EH codegen in such a significant way.
Clang compiles away |
Okay, so I had a chance to talk with someone who is both a security expert and an exceptions expert about this topic, relaying this useful conversation to catch them up on the specific issues we're considering. They were not particularly concerned about So the only remaining concerns that came up in this discussion are
(1) now seems pretty orthogonal to this discussion, though this was very useful in understanding it better. The study above seems to suggest that (2) shouldn't be a big cause of code bloat. And (3) seems out of scope, though I do get the impression y'all are expecting magic to happen there :) Regardless, all of these seem to be separate from this issue. |
Oh, I forgot, they did raise a new concern. For languages that want to have fast exceptions, you don’t want to collect the stack trace by default so that a single-level catch/throw can be just a few times slower than a return. |
Just to give my 2c here: I also don't see the security use case of trying to unwind through untrusted code while still calling into that untrusted code. It seems like, if you reach a point where you want to kill the guest, you don't want to go and then run a bit more guest code. Rather, you should just trap. Now you may say "but that will kill the host", but since the guest can also trap, you already have to worry about the host being taken down by a guest trap. If anything, this is a use case for some separate feature providing a unit of isolation and trap recovery (maybe not in core wasm, but perhaps WASI or Interface Types...). |
Also fix some typos from previous CL (see macros.def)
If a WebAssembly function calls a Javascript function, is it possible for the Javascript function to throw an error that's uncatchable to the WASM function (and may be catchable to the Javascript function that called the WASM function)?
I'm interested in being able to abort sandboxed/untrusted WebAssembly code when it calls into a Javascript function. One case where this is useful is when using https://github.com/ewasm/wasm-metering, which rewrites a WASM binary to keep track of instruction counts and to call an imported function to abort once a limit is hit. It's important that the exception thrown by the called abort function isn't catchable by the WASM. This can be used to deterministically limit the execution time of a WASM function.
(Since wasm-metering already relies on rewriting WASM binaries, I guess it would be possible to make it rewrite them to not catch the "out of gas, stop execution" error, but it would be convenient if there was a simpler option.)
I tried to figure out whether this proposal already had a way to do this, but I had trouble understanding this part:
I think it's saying that traps create special
WebAssembly.RuntimeError
instances that are uncatchable, and that normally-createdWebAssembly.RuntimeError
instances are catchable like regular errors. If it were the case that JS-created instances ofWebAssembly.RuntimeError
(or another class) were uncatchable to WASM, then that would solve my use-case.It seems like one workaround to accomplish this with the spec as-is would be to have the Javascript function call another WASM function which does nothing but trap, and then let that error bubble up uncatchably through the WASM stack frames. It seems a little weird that only WASM could directly make an uncatchable error like this though, which makes me think I might be missing something.
The text was updated successfully, but these errors were encountered: